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
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import Image from "next/image";
import Link from "next/link";
import { Nav } from "@/components/nav";
import { PostHogPageView } from "@/components/posthog-pageview";
import { ThemeProvider } from "@/components/theme-provider";
import { ThemeToggle } from "@/components/theme-toggle";
import "./globals.css";
Expand Down Expand Up @@ -165,6 +166,7 @@ export default function RootLayout({
suppressHydrationWarning
>
<body className="min-h-full flex flex-col bg-background text-foreground">
<PostHogPageView />
<ThemeProvider>
<Nav />
<main className="flex-1 dot-grid">{children}</main>
Expand Down
32 changes: 32 additions & 0 deletions src/components/posthog-pageview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { usePathname } from "next/navigation";
import { useEffect } from "react";
import posthog from "posthog-js";

// Captures a PostHog $pageview on the initial load and on every App Router
// client navigation.
//
// We do NOT rely on posthog-js's built-in capture_pageview here. With the
// project's `defaults: '2026-01-30'`, capture_pageview resolves to
// 'history_change', which only fires on History API changes (SPA navigations)
// and misses the first page load — so visitors who land and leave without
// navigating produced zero $pageview events, leaving Web analytics empty.
// instrumentation-client.ts therefore sets capture_pageview:false and this
// effect is the single source of $pageview (fires on mount = initial load,
// and whenever the pathname changes). posthog.capture fills $current_url,
// $host, $pathname, $session_id, etc. automatically.
export function PostHogPageView() {
const pathname = usePathname();

useEffect(() => {
if (!pathname) return;
try {
posthog.capture("$pageview");
} catch {
/* analytics must never break the UX */
}
}, [pathname]);

return null;
}
11 changes: 9 additions & 2 deletions src/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ if (key && typeof window !== "undefined") {
posthog.init(key, {
api_host: "/_ph",
ui_host: "https://us.posthog.com",
// Modern defaults: history-change $pageview capture (works with the
// App Router), autocapture, and web vitals — no manual page tracking.
// Modern defaults give us autocapture, web vitals, and session replay.
defaults: "2026-01-30",
// No login on this site, so only spend person profiles on identified
// users (we have none today). Keeps us comfortably on the free tier.
person_profiles: "identified_only",
// defaults:'2026-01-30' resolves capture_pageview to 'history_change',
// which misses the initial page load — so landing-and-leaving visitors
// produced zero $pageview events and Web analytics stayed empty. Capture
// pageviews manually instead (see components/posthog-pageview.tsx), which
// fires on initial load + every route change. Disable the built-in one
// to avoid double-counting on client navigations.
capture_pageview: false,
capture_pageleave: true,
});
} catch {
// Instrumentation must never break the app (Next.js 16 guidance).
Expand Down
Loading