Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/website/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# LangGraph Platform URL for the Angular Elements live demo
NEXT_PUBLIC_LANGGRAPH_URL=http://localhost:2024

# PostHog analytics (https://posthog.com)
NEXT_PUBLIC_POSTHOG_TOKEN=
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
NEXT_PUBLIC_POSTHOG_CAPTURE_LOCAL=false

# Anthropic API key for docs generation scripts
ANTHROPIC_API_KEY=your-anthropic-api-key-here

Expand Down
16 changes: 16 additions & 0 deletions apps/website/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import posthog from 'posthog-js';
import {
normalizePostHogHost,
shouldCaptureAnalytics,
} from './src/lib/analytics/properties';

const token = process.env.NEXT_PUBLIC_POSTHOG_TOKEN;
const captureLocal = process.env.NEXT_PUBLIC_POSTHOG_CAPTURE_LOCAL === 'true';
const browserHost = typeof window === 'undefined' ? undefined : window.location.host;

if (shouldCaptureAnalytics({ token, captureLocal, host: browserHost })) {
posthog.init(token!, {
api_host: normalizePostHogHost(process.env.NEXT_PUBLIC_POSTHOG_HOST),
defaults: '2026-01-30',
});
}
2 changes: 2 additions & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"next": "~16.1.6",
"posthog-js": "^1.372.6",
"posthog-node": "^5.20.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"remark-gfm": "^4.0.1",
Expand Down
5 changes: 5 additions & 0 deletions apps/website/src/app/api/leads/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import path from 'path';
import { sendEmail, FROM, NOTIFY_TO, addToAudience } from '../../../../lib/resend';
import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops';
import { leadNotificationHtml } from '../../../../emails/lead-notification';
import { captureLeadConversion } from '../../../lib/analytics/server';
import { getSourcePage } from '../../../lib/analytics/properties';

const LEADS_FILE = path.join(process.cwd(), 'data', 'leads.ndjson');

Expand All @@ -22,6 +24,7 @@ export async function POST(req: NextRequest) {
}

const ts = new Date().toISOString();
const sourcePage = getSourcePage(req.headers.get('referer'));

// NDJSON backup (always writes, even if Resend fails)
try {
Expand Down Expand Up @@ -57,5 +60,7 @@ export async function POST(req: NextRequest) {
console.error('[resend] lead notification failed:', err);
}

await captureLeadConversion({ email, company, sourcePage });

return NextResponse.json({ ok: true });
}
5 changes: 5 additions & 0 deletions apps/website/src/app/api/newsletter/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server';
import { sendEmail, FROM, addToAudience } from '../../../../lib/resend';
import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops';
import { newsletterWelcomeHtml } from '../../../../emails/newsletter-welcome';
import { captureNewsletterConversion } from '../../../lib/analytics/server';
import { getSourcePage } from '../../../lib/analytics/properties';

export async function POST(req: NextRequest) {
let body: { email?: string };
Expand All @@ -12,6 +14,7 @@ export async function POST(req: NextRequest) {
}

const email = (body.email || '').trim().slice(0, 320);
const sourcePage = getSourcePage(req.headers.get('referer'));

if (!email || !email.includes('@')) {
return NextResponse.json({ error: 'Valid email required' }, { status: 400 });
Expand Down Expand Up @@ -40,5 +43,7 @@ export async function POST(req: NextRequest) {
console.error('[resend] newsletter signup failed:', err);
}

await captureNewsletterConversion({ email, sourcePage });

return NextResponse.json({ ok: true });
}
5 changes: 5 additions & 0 deletions apps/website/src/app/api/whitepaper-signup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { whitepaperDownloadHtml } from '../../../../emails/whitepaper-download';
import { angularDownloadHtml } from '../../../../emails/angular-download';
import { renderDownloadHtml } from '../../../../emails/render-download';
import { chatDownloadHtml } from '../../../../emails/chat-download';
import { captureWhitepaperConversion } from '../../../lib/analytics/server';
import { getSourcePage } from '../../../lib/analytics/properties';

const SIGNUPS_FILE = path.join(process.cwd(), 'data', 'whitepaper-signups.ndjson');

Expand Down Expand Up @@ -38,6 +40,7 @@ export async function POST(req: NextRequest) {
const name = (body.name || '').trim().slice(0, 200);
const email = (body.email || '').trim().slice(0, 320);
const paper = (VALID_PAPERS.includes(body.paper as PaperId) ? body.paper : 'overview') as PaperId;
const sourcePage = getSourcePage(req.headers.get('referer'));

if (!email || !email.includes('@')) {
return NextResponse.json({ error: 'Valid email required' }, { status: 400 });
Expand Down Expand Up @@ -71,5 +74,7 @@ export async function POST(req: NextRequest) {
console.error('[whitepaper-signup] email pipeline failed:', err);
}

await captureWhitepaperConversion({ email, paper, sourcePage });

return NextResponse.json({ ok: true });
}
6 changes: 6 additions & 0 deletions apps/website/src/components/docs/CopyPromptButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';
import { useState } from 'react';
import { tokens } from '@ngaf/design-tokens';
import { analyticsEvents } from '../../lib/analytics/events';
import { track } from '../../lib/analytics/client';

interface Props {
prompt: string;
Expand All @@ -14,6 +16,10 @@ export function CopyPromptButton({ prompt, variant = 'docs', label }: Props) {
const handleClick = async () => {
try {
await navigator.clipboard.writeText(prompt);
track(analyticsEvents.docsCopyPromptClick, {
surface: variant === 'hero' ? 'home' : 'docs',
cta_id: label ?? 'copy_prompt',
});
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
Expand Down
21 changes: 20 additions & 1 deletion apps/website/src/components/docs/DocsSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { docsConfig, type LibraryId } from '../../lib/docs-config';
import { tokens } from '@ngaf/design-tokens';
import { analyticsEvents } from '../../lib/analytics/events';
import { track } from '../../lib/analytics/client';

interface SearchablePage {
title: string;
Expand Down Expand Up @@ -60,6 +62,13 @@ export function DocsSearch({ library }: { library?: LibraryId }) {
}, [open]);

const navigate = (page: SearchablePage) => {
track(analyticsEvents.docsSearchResultClick, {
surface: 'docs',
destination_url: `/docs/${page.library}/${page.section}/${page.slug}`,
library: page.library === 'agent' || page.library === 'render' || page.library === 'chat' ? page.library : 'unknown',
query_length: query.length,
result_count: results.length,
});
router.push(`/docs/${page.library}/${page.section}/${page.slug}`);
setOpen(false);
};
Expand Down Expand Up @@ -95,7 +104,17 @@ export function DocsSearch({ library }: { library?: LibraryId }) {
<input
ref={inputRef}
value={query}
onChange={(e) => { setQuery(e.target.value); setSelected(0); }}
onChange={(e) => {
const nextQuery = e.target.value;
setQuery(nextQuery);
setSelected(0);
if (nextQuery.length === 1) {
track(analyticsEvents.docsSearchSubmit, {
surface: 'docs',
library: library === 'agent' || library === 'render' || library === 'chat' ? library : 'unknown',
});
}
}}
onKeyDown={handleInputKeyDown}
placeholder="Search documentation..."
style={{
Expand Down
6 changes: 6 additions & 0 deletions apps/website/src/components/docs/mdx/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';
import { useRef, useState } from 'react';
import { analyticsEvents } from '../../../lib/analytics/events';
import { track } from '../../../lib/analytics/client';

function CopyIcon() {
return (
Expand All @@ -25,6 +27,10 @@ export function Pre({ children, ...props }: React.HTMLAttributes<HTMLPreElement>
const copy = async () => {
const text = ref.current?.textContent ?? '';
await navigator.clipboard.writeText(text);
track(analyticsEvents.docsCopyCodeClick, {
surface: 'docs',
cta_id: 'copy_code',
});
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
Expand Down
16 changes: 16 additions & 0 deletions apps/website/src/components/landing/WhitePaperGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { tokens } from '../../../lib/design-tokens';
import { analyticsEvents } from '../../lib/analytics/events';
import { track } from '../../lib/analytics/client';

type FormState = 'idle' | 'submitting' | 'done' | 'error';

Expand Down Expand Up @@ -71,6 +73,11 @@ export function WhitePaperGate() {
? `Role: ${role}${message ? '\n\n' + message : ''}`
: message;

track(analyticsEvents.marketingLeadFormSubmit, {
surface: 'home',
source_section: 'whitepaper-gate',
});

try {
const res = await fetch('/api/leads', {
method: 'POST',
Expand All @@ -89,8 +96,17 @@ export function WhitePaperGate() {
throw new Error(data.error ?? 'Server error');
}

track(analyticsEvents.marketingLeadFormSuccess, {
surface: 'home',
source_section: 'whitepaper-gate',
});
setFormState('done');
} catch {
track(analyticsEvents.marketingLeadFormFail, {
surface: 'home',
source_section: 'whitepaper-gate',
error_reason: 'api_error',
});
setFormState('error');
}
};
Expand Down
28 changes: 28 additions & 0 deletions apps/website/src/components/landing/WhitePaperSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { tokens } from '../../../lib/design-tokens';
import { analyticsEvents } from '../../lib/analytics/events';
import { track, trackWhitepaperDownloadClick } from '../../lib/analytics/client';

type FormState = 'idle' | 'submitting' | 'done' | 'error';

Expand All @@ -14,15 +16,31 @@ export function WhitePaperSection() {
e.preventDefault();
if (!email) return;
setFormState('submitting');
track(analyticsEvents.marketingWhitepaperSignupSubmit, {
surface: 'home',
source_section: 'whitepaper-section',
paper: 'overview',
});
try {
const res = await fetch('/api/whitepaper-signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email }),
});
if (!res.ok) throw new Error('Server error');
track(analyticsEvents.marketingWhitepaperSignupSuccess, {
surface: 'home',
source_section: 'whitepaper-section',
paper: 'overview',
});
setFormState('done');
} catch {
track(analyticsEvents.marketingWhitepaperSignupFail, {
surface: 'home',
source_section: 'whitepaper-section',
paper: 'overview',
error_reason: 'api_error',
});
setFormState('error');
}
};
Expand Down Expand Up @@ -99,6 +117,11 @@ export function WhitePaperSection() {
<a
href="/whitepaper.pdf"
download="streamresource-angular-agent-readiness-guide.pdf"
onClick={() => trackWhitepaperDownloadClick('overview', {
surface: 'home',
source_section: 'whitepaper-section',
cta_id: 'whitepaper_section_direct_download',
})}
style={{
display: 'inline-block',
marginTop: 12,
Expand Down Expand Up @@ -162,6 +185,11 @@ export function WhitePaperSection() {
<a
href="/whitepaper.pdf"
download="streamresource-angular-agent-readiness-guide.pdf"
onClick={() => trackWhitepaperDownloadClick('overview', {
surface: 'home',
source_section: 'whitepaper-section',
cta_id: 'whitepaper_section_direct_download',
})}
style={{
display: 'inline-block',
marginTop: 12,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { tokens } from '@ngaf/design-tokens';
import { analyticsEvents } from '../../../lib/analytics/events';
import { track, trackWhitepaperDownloadClick } from '../../../lib/analytics/client';

type FormState = 'idle' | 'submitting' | 'done' | 'error';

Expand All @@ -15,15 +17,34 @@ export function AngularWhitePaperGate() {
e.preventDefault();
if (!email) return;
setFormState('submitting');
track(analyticsEvents.marketingWhitepaperSignupSubmit, {
surface: 'library_landing',
source_section: 'angular-whitepaper-gate',
library: 'agent',
paper: 'angular',
});
try {
const res = await fetch('/api/whitepaper-signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, paper: 'angular' }),
});
if (!res.ok) throw new Error('Server error');
track(analyticsEvents.marketingWhitepaperSignupSuccess, {
surface: 'library_landing',
source_section: 'angular-whitepaper-gate',
library: 'agent',
paper: 'angular',
});
setFormState('done');
} catch {
track(analyticsEvents.marketingWhitepaperSignupFail, {
surface: 'library_landing',
source_section: 'angular-whitepaper-gate',
library: 'agent',
paper: 'angular',
error_reason: 'api_error',
});
setFormState('error');
}
};
Expand Down Expand Up @@ -85,6 +106,12 @@ export function AngularWhitePaperGate() {
Part of the Cacheplane Angular Agent Framework.
</p>
<a href="/whitepapers/angular.pdf" download="cacheplane-angular-enterprise-guide.pdf"
onClick={() => trackWhitepaperDownloadClick('angular', {
surface: 'library_landing',
source_section: 'angular-whitepaper-gate',
library: 'agent',
cta_id: 'angular_whitepaper_download',
})}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
background: tokens.colors.accent, color: '#fff',
Expand Down
Loading
Loading