Skip to content

Commit a028094

Browse files
cdskillclaude
andcommitted
feat(docs): cookieless PostHog analytics (SSR-safe)
- posthog-js service with isPlatformBrowser guards; init skipped without key - events across hero/header/sidebar/playground/theme; install-copy = conversion - key/host from VITE_POSTHOG_* (.env gitignored) + CI build via repo vars - persistence: memory -> cookieless, no consent banner (RGPD); EU host Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f0b2686 commit a028094

12 files changed

Lines changed: 233 additions & 19 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ jobs:
7979

8080
- name: Build ${{ matrix.app }}
8181
run: pnpm nx build ${{ matrix.app }}
82+
# PostHog (docs) reads VITE_* at build time. Public project key → repo variables, not secrets.
83+
env:
84+
VITE_POSTHOG_KEY: ${{ vars.POSTHOG_KEY }}
85+
VITE_POSTHOG_HOST: ${{ vars.POSTHOG_HOST }}
8286

8387
- name: Resolve deploy target
8488
id: cfg

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ Thumbs.db
4444
.claude/settings.local.json
4545

4646
.angular
47+
.env

apps/docs/src/app/app.component.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
1-
import { ChangeDetectionStrategy, Component } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnInit,
5+
inject,
6+
} from '@angular/core';
27
import { RouterOutlet } from '@angular/router';
38

9+
import { PosthogService } from './services/posthog.service';
10+
import { environment } from '../environments/environment';
11+
412
@Component({
513
selector: 'app-root',
614
imports: [RouterOutlet],
7-
template: ` <router-outlet/> `,
15+
template: ` <router-outlet /> `,
816
changeDetection: ChangeDetectionStrategy.OnPush,
917
})
10-
export class AppComponent {}
18+
export class AppComponent implements OnInit {
19+
private readonly posthogService = inject(PosthogService);
20+
21+
ngOnInit(): void {
22+
if (!environment.posthogKey) {
23+
return;
24+
}
25+
26+
this.posthogService.init(environment.posthogKey, {
27+
api_host: environment.posthogHost,
28+
defaults: '2026-01-30',
29+
capture_exceptions: true,
30+
persistence: 'memory', // cookieless: no consent banner needed (RGPD)
31+
});
32+
}
33+
}

apps/docs/src/app/components/docs-header.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
inject,
5+
signal,
6+
} from '@angular/core';
27
import { RouterLink, RouterLinkActive } from '@angular/router';
38
import { NgIcon, provideIcons } from '@ng-icons/core';
49
import {
@@ -11,6 +16,7 @@ import {
1116

1217
import { HlmButton } from '../ui/button';
1318
import { ThemeToggle } from './theme-toggle';
19+
import { PosthogService } from '../services/posthog.service';
1420

1521
/**
1622
* Minimal top bar in the shadcn / plate spirit: short height, no dividing
@@ -21,11 +27,19 @@ import { ThemeToggle } from './theme-toggle';
2127
selector: 'app-docs-header',
2228
imports: [RouterLink, RouterLinkActive, NgIcon, HlmButton, ThemeToggle],
2329
providers: [
24-
provideIcons({ lucideFeather, lucideGithub, lucideMenu, lucideSearch, lucideX }),
30+
provideIcons({
31+
lucideFeather,
32+
lucideGithub,
33+
lucideMenu,
34+
lucideSearch,
35+
lucideX,
36+
}),
2537
],
2638
template: `
2739
<header class="sticky top-0 z-40 w-full bg-background/70 backdrop-blur-md">
28-
<div class="relative mx-auto flex h-14 max-w-6xl items-center gap-6 px-4 sm:px-6">
40+
<div
41+
class="relative mx-auto flex h-14 max-w-6xl items-center gap-6 px-4 sm:px-6"
42+
>
2943
<a href="/" class="flex items-center gap-2" aria-label="Qalma home">
3044
<ng-icon
3145
name="lucideFeather"
@@ -40,7 +54,10 @@ import { ThemeToggle } from './theme-toggle';
4054
<nav
4155
class="hidden items-center gap-5 text-sm text-muted-foreground md:flex"
4256
>
43-
<a class="transition-colors hover:text-foreground" href="/#playground">
57+
<a
58+
class="transition-colors hover:text-foreground"
59+
href="/#playground"
60+
>
4461
Playground
4562
</a>
4663
<a
@@ -75,6 +92,7 @@ import { ThemeToggle } from './theme-toggle';
7592
target="_blank"
7693
rel="noreferrer"
7794
aria-label="GitHub repository"
95+
(click)="trackGithubClick()"
7896
>
7997
<ng-icon name="lucideGithub" aria-hidden="true" />
8098
</a>
@@ -89,7 +107,7 @@ import { ThemeToggle } from './theme-toggle';
89107
class="md:hidden"
90108
[attr.aria-label]="mobileNavOpen() ? 'Close menu' : 'Open menu'"
91109
[attr.aria-expanded]="mobileNavOpen()"
92-
(click)="mobileNavOpen.set(!mobileNavOpen())"
110+
(click)="toggleMobileNav()"
93111
>
94112
<ng-icon
95113
[name]="mobileNavOpen() ? 'lucideX' : 'lucideMenu'"
@@ -124,5 +142,19 @@ import { ThemeToggle } from './theme-toggle';
124142
`,
125143
})
126144
export class DocsHeader {
145+
private readonly posthogService = inject(PosthogService);
146+
127147
protected readonly mobileNavOpen = signal(false);
148+
149+
protected toggleMobileNav(): void {
150+
const opening = !this.mobileNavOpen();
151+
this.mobileNavOpen.set(opening);
152+
if (opening) {
153+
this.posthogService.posthog.capture('mobile_nav_opened');
154+
}
155+
}
156+
157+
protected trackGithubClick(): void {
158+
this.posthogService.posthog.capture('github_link_clicked');
159+
}
128160
}

apps/docs/src/app/components/hero.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
ChangeDetectionStrategy,
33
Component,
4+
inject,
45
signal,
56
} from '@angular/core';
67
import { NgIcon, provideIcons } from '@ng-icons/core';
@@ -12,6 +13,7 @@ import {
1213
} from '@ng-icons/lucide';
1314

1415
import { HlmButton } from '../ui/button';
16+
import { PosthogService } from '../services/posthog.service';
1517

1618
const INSTALL_COMMAND = 'pnpm add @qalma/editor';
1719

@@ -28,7 +30,9 @@ const INSTALL_COMMAND = 'pnpm add @qalma/editor';
2830
provideIcons({ lucideArrowRight, lucideCheck, lucideCopy, lucideSparkles }),
2931
],
3032
template: `
31-
<section class="mx-auto max-w-3xl px-4 pb-10 pt-12 text-center sm:pb-12 sm:pt-16">
33+
<section
34+
class="mx-auto max-w-3xl px-4 pb-10 pt-12 text-center sm:pb-12 sm:pt-16"
35+
>
3236
<span
3337
class="mb-5 inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1 text-xs font-medium text-muted-foreground"
3438
>
@@ -60,13 +64,15 @@ const INSTALL_COMMAND = 'pnpm add @qalma/editor';
6064
crafted for Angular.
6165
</h1>
6266
63-
<p class="mx-auto mt-5 max-w-xl text-base leading-relaxed text-muted-foreground">
67+
<p
68+
class="mx-auto mt-5 max-w-xl text-base leading-relaxed text-muted-foreground"
69+
>
6470
A headless, plugin-based editor built on ProseMirror. Composable
6571
primitives, Tailwind-first styling, and zero opinions about your design.
6672
</p>
6773
6874
<div class="mt-7 flex flex-wrap items-center justify-center gap-3">
69-
<a appBtn size="lg" href="#playground">
75+
<a appBtn size="lg" href="#playground" (click)="trackGetStarted()">
7076
Get started
7177
<ng-icon name="lucideArrowRight" aria-hidden="true" />
7278
</a>
@@ -80,7 +86,11 @@ const INSTALL_COMMAND = 'pnpm add @qalma/editor';
8086
>
8187
{{ installCommand }}
8288
@if (copied()) {
83-
<ng-icon name="lucideCheck" class="text-accent" aria-hidden="true" />
89+
<ng-icon
90+
name="lucideCheck"
91+
class="text-accent"
92+
aria-hidden="true"
93+
/>
8494
} @else {
8595
<ng-icon name="lucideCopy" aria-hidden="true" />
8696
}
@@ -90,6 +100,8 @@ const INSTALL_COMMAND = 'pnpm add @qalma/editor';
90100
`,
91101
})
92102
export class Hero {
103+
private readonly posthogService = inject(PosthogService);
104+
93105
protected readonly installCommand = INSTALL_COMMAND;
94106
protected readonly copied = signal(false);
95107

@@ -98,5 +110,13 @@ export class Hero {
98110
this.copied.set(true);
99111
setTimeout(() => this.copied.set(false), 1400);
100112
});
113+
114+
this.posthogService.posthog.capture('install_command_copied', {
115+
command: INSTALL_COMMAND,
116+
});
117+
}
118+
119+
protected trackGetStarted(): void {
120+
this.posthogService.posthog.capture('get_started_clicked');
101121
}
102122
}

apps/docs/src/app/components/theme-toggle.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import {
22
ChangeDetectionStrategy,
33
Component,
44
afterNextRender,
5+
inject,
56
signal,
67
} from '@angular/core';
78
import { NgIcon, provideIcons } from '@ng-icons/core';
89
import { lucideMoon, lucideSun } from '@ng-icons/lucide';
910

1011
import { HlmButton } from '../ui/button';
12+
import { PosthogService } from '../services/posthog.service';
1113

1214
@Component({
1315
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -21,7 +23,9 @@ import { HlmButton } from '../ui/button';
2123
size="icon"
2224
type="button"
2325
(click)="toggle()"
24-
[attr.aria-label]="isDark() ? 'Switch to light theme' : 'Switch to dark theme'"
26+
[attr.aria-label]="
27+
isDark() ? 'Switch to light theme' : 'Switch to dark theme'
28+
"
2529
title="Toggle theme"
2630
>
2731
@if (isDark()) {
@@ -33,6 +37,8 @@ import { HlmButton } from '../ui/button';
3337
`,
3438
})
3539
export class ThemeToggle {
40+
private readonly posthogService = inject(PosthogService);
41+
3642
protected readonly isDark = signal(false);
3743

3844
constructor() {
@@ -54,5 +60,9 @@ export class ThemeToggle {
5460
}
5561

5662
this.isDark.set(next);
63+
64+
this.posthogService.posthog.capture('theme_toggled', {
65+
theme: next ? 'dark' : 'light',
66+
});
5767
}
5868
}

apps/docs/src/app/docs/docs-sidebar.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Component,
55
ElementRef,
66
HostListener,
7+
inject,
78
output,
89
signal,
910
viewChild,
@@ -13,6 +14,7 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
1314
import { lucideMouse } from '@ng-icons/lucide';
1415

1516
import { DOCS_NAV } from './docs-nav';
17+
import { PosthogService } from '../services/posthog.service';
1618

1719
/**
1820
* Left-nav for the docs site. Plain grouped link list (no collapsible
@@ -47,7 +49,7 @@ import { DOCS_NAV } from './docs-nav';
4749
[routerLink]="item.href"
4850
routerLinkActive="bg-accent-subtle !text-accent font-medium"
4951
[routerLinkActiveOptions]="{ exact: true }"
50-
(click)="linkClick.emit()"
52+
(click)="onLinkClick(item.title, item.href)"
5153
class="block rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
5254
>
5355
{{ item.title }}
@@ -78,9 +80,12 @@ import { DOCS_NAV } from './docs-nav';
7880
`,
7981
})
8082
export class DocsSidebar implements AfterViewInit {
83+
private readonly posthogService = inject(PosthogService);
84+
8185
protected readonly groups = DOCS_NAV;
8286

83-
private readonly scrollEl = viewChild.required<ElementRef<HTMLElement>>('scrollEl');
87+
private readonly scrollEl =
88+
viewChild.required<ElementRef<HTMLElement>>('scrollEl');
8489

8590
protected readonly showTopFade = signal(false);
8691
protected readonly showBottomFade = signal(false);
@@ -97,6 +102,16 @@ export class DocsSidebar implements AfterViewInit {
97102
const el = this.scrollEl().nativeElement;
98103

99104
this.showTopFade.set(el.scrollTop > 4);
100-
this.showBottomFade.set(el.scrollTop + el.clientHeight < el.scrollHeight - 4);
105+
this.showBottomFade.set(
106+
el.scrollTop + el.clientHeight < el.scrollHeight - 4,
107+
);
108+
}
109+
110+
protected onLinkClick(title: string, href: string): void {
111+
this.linkClick.emit();
112+
this.posthogService.posthog.capture('docs_sidebar_clicked', {
113+
title,
114+
href,
115+
});
101116
}
102117
}

apps/docs/src/app/playground/playground.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@ import {
4545
} from './image';
4646
import { LinkPopoverController } from './link-popover-controller';
4747
import { PlaygroundLinkPopover } from './link-popover';
48+
import { LinkPopover } from './link-popover.model';
4849
import {
4950
PlaygroundMentionController,
51+
PlaygroundMentionOption,
5052
createPlaygroundMentionSource,
5153
} from './mention';
5254
import { PlaygroundMentionMenu } from './mention-menu';
5355
import { PlaygroundToolbar } from './toolbar';
56+
import { PosthogService } from '../services/posthog.service';
5457

5558
@Component({
5659
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -103,7 +106,7 @@ import { PlaygroundToolbar } from './toolbar';
103106
[loading]="mentionController.loading()"
104107
[activeIndex]="mentionController.activeIndex()"
105108
(activate)="mentionController.setActiveIndex($event)"
106-
(pick)="mentionController.insert($event)"
109+
(pick)="onMentionPick($event)"
107110
(dismiss)="mentionController.hide()"
108111
/>
109112
}
@@ -113,7 +116,7 @@ import { PlaygroundToolbar } from './toolbar';
113116
[href]="linkPopover.href()"
114117
(hrefChange)="linkPopover.href.set($event)"
115118
(edit)="linkPopover.edit($event)"
116-
(save)="linkPopover.save($event)"
119+
(save)="onLinkSave($event)"
117120
(remove)="linkPopover.remove($event)"
118121
(dismiss)="linkPopover.hide()"
119122
(keepOpen)="linkPopover.keepOpen()"
@@ -135,6 +138,7 @@ import { PlaygroundToolbar } from './toolbar';
135138
})
136139
export class Playground {
137140
private readonly destroyRef = inject(DestroyRef);
141+
private readonly posthogService = inject(PosthogService);
138142
// `#mentionSurface` sits on the <qalma-content> component, so without an
139143
// explicit `read` the query resolves to the component instance (whose
140144
// `.nativeElement` is undefined). Read the host ElementRef instead.
@@ -238,6 +242,10 @@ export class Playground {
238242
currentImage ? 'updateImage' : 'insertImage',
239243
commandValue,
240244
);
245+
246+
this.posthogService.posthog.capture('playground_image_inserted', {
247+
type: 'url',
248+
});
241249
}
242250

243251
protected chooseImageFile(): void {
@@ -269,6 +277,20 @@ export class Playground {
269277
title: file.name,
270278
previewSrc,
271279
});
280+
281+
this.posthogService.posthog.capture('playground_image_uploaded', {
282+
mime_type: file.type,
283+
});
284+
}
285+
286+
protected onLinkSave(popover: LinkPopover): void {
287+
this.linkPopover.save(popover);
288+
this.posthogService.posthog.capture('playground_link_saved');
289+
}
290+
291+
protected onMentionPick(option: PlaygroundMentionOption): void {
292+
this.mentionController.insert(option);
293+
this.posthogService.posthog.capture('playground_mention_inserted');
272294
}
273295

274296
private revokeImagePreviewUrls(): void {

0 commit comments

Comments
 (0)