feat(analytics): connect Umami analytics across www, docs, and static services (#49)

## Summary of Changes

- **`@ermnvldmr/ssg`**: Added extensible `AnalyticsProvider` interface,
`umamiAnalytics` integration in `@ermnvldmr/ssg/dev`, and runtime client
analytics dispatchers (`trackEvent`, `identifySession`).
- **`services/docs`**: Configured native Hextra Umami parameters in
`hugo.yaml` (`docs.ermnvldmr.com` with UUID
`fbdadf3f-3484-4186-a2c2-cedcf76cf44f`).
- **`services/www`**: Injected Umami tracking provider into
`rsbuild.config.ts` (`ermnvldmr.com`, `www.ermnvldmr.com` with UUID
`4fca5ac2-4fe4-4626-b708-933c37bfe918`).
- **`services/static`**: Injected Umami tracking provider into
`rsbuild.config.ts` (`static.ermnvldmr.com` with UUID
`0831532a-2cec-4955-8f5b-452d8bba96d8`).
- **Environment & Privacy**: Injections are automatically disabled in
development builds. Configured `data-domains` filtering and
`data-do-not-track="true"`.

## Verification
- `pnpm typecheck`: Passed (0 errors)
- `pnpm lint`: Passed (0 warnings)
- `pnpm test`: Passed (all unit test suites)
- `pnpm build`: Passed clean workspace build and pre-rendered SSG
injection

---
Disclosure: I'm an AI assistant helping Vladimir Eremin; I'm not
Vladimir Eremin.
This commit is contained in:
deytenit authored and GitHub committed 2026-08-23 20:28:22 +03:00
1 parent c8beec4d47
commit ea805c724b
31 files changed
+622 -60

No files matched your search

+11 -1
View File
@@ -1,7 +1,17 @@
import { defineSSGServiceConfig, discoverEntries } from '@ermnvldmr/ssg/dev';
import { defineSSGServiceConfig, discoverEntries, umamiAnalytics } from '@ermnvldmr/ssg/dev';
import { THEME_INIT_SCRIPT, THEME_INIT_STYLES } from '@ermnvldmr/ui/dev';
const UMAMI_STATIC_WEBSITE_ID = '0831532a-2cec-4955-8f5b-452d8bba96d8';
export default defineSSGServiceConfig({
analytics: [
umamiAnalytics({
serverUrl: 'https://umami.ermnvldmr.com',
websiteId: UMAMI_STATIC_WEBSITE_ID,
domains: 'static.ermnvldmr.com',
doNotTrack: true,
}),
],
source: {
entry: discoverEntries(import.meta.dirname, './src/app/**/*.tsx'),
preEntry: ['./src/static/global.css'],
@@ -1,5 +1,6 @@
import { PageRoot, ThemeSwitch } from '@ermnvldmr/kits';
import { Container, HStack, Icon, Separator, Text, VStack } from '@ermnvldmr/ui';
import { trackEvent } from '@ermnvldmr/ssg';
import { Container, HStack, Icon, LogProvider, Separator, Text, VStack } from '@ermnvldmr/ui';
import { Monitor, Moon, Sun } from 'lucide-react';
import React from 'react';
@@ -25,25 +26,27 @@ export function IndexLayout({ children }: IndexLayoutProps): React.JSX.Element {
const currentYear = new Date().getFullYear();
return (
<PageRoot className="flex flex-col justify-between">
<main className="flex-1 w-full">{children}</main>
<VStack as="footer" className="w-full" gap={0}>
<Separator thinned="none" />
<Container className="w-full" padding={12}>
<HStack align="center" className="w-full" justify="between">
<VStack align="start" gap={1}>
<Text color="muted" size="s">
© {currentYear} Vladimir Eremin
</Text>
</VStack>
<ThemeSwitch
darkIcon={<Icon icon={Moon} />}
lightIcon={<Icon icon={Sun} />}
systemIcon={<Icon icon={Monitor} />}
/>
</HStack>
</Container>
</VStack>
</PageRoot>
<LogProvider logger={trackEvent}>
<PageRoot className="flex flex-col justify-between">
<main className="flex-1 w-full">{children}</main>
<VStack as="footer" className="w-full" gap={0}>
<Separator thinned="none" />
<Container className="w-full" padding={12}>
<HStack align="center" className="w-full" justify="between">
<VStack align="start" gap={1}>
<Text color="muted" size="s">
© {currentYear} Vladimir Eremin
</Text>
</VStack>
<ThemeSwitch
darkIcon={<Icon icon={Moon} />}
lightIcon={<Icon icon={Sun} />}
systemIcon={<Icon icon={Monitor} />}
/>
</HStack>
</Container>
</VStack>
</PageRoot>
</LogProvider>
);
}