Skip to content

Next.js

Terminal window
npx @plaintrace/cli init --site <site id> --endpoint https://collect.plaintrace.dev

The CLI edits app/layout.* or src/app/layout.* (App Router), or pages/_document.* (Pages Router; created if missing). It shows the diff and asks before writing (--yes skips the question).

app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script src="https://collect.plaintrace.dev/t.js" data-site="<site id>" strategy="afterInteractive" />
</body>
</html>
)
}

Route changes are picked up automatically: no router hook is needed.

Add <script async src="https://collect.plaintrace.dev/t.js" data-site="<site id>" /> inside <Head> in pages/_document.tsx.

With the script tag, call the global px the script defines:

'use client'
declare global {
interface Window { px?: (...args: unknown[]) => unknown }
}
<button onClick={() => window.px?.('track', 'signup_clicked', { plan: 'pro' })}>Start</button>

Prefer the npm package? Use it instead of the script tag: call init() once in a client component, then track() anywhere. track() calls made before init() are queued until it runs.

'use client'
import { init } from '@plaintrace/browser'
import { useEffect } from 'react'
export function Analytics() {
useEffect(() => void init({ site: '<site id>', endpoint: 'https://collect.plaintrace.dev' }), [])
return null
}

See events.

The sample uses middleware.ts (Next.js 15). In Next.js 16 the file is called proxy.ts and exports proxy; the body is the same.

Serve the tracker from your own domain, record crawler reads and measure loss:

middleware.ts
import { createClient } from '@plaintrace/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
const analytics = createClient({
key: process.env.ANALYTICS_KEY!,
endpoint: 'https://collect.plaintrace.dev',
ipHeader: 'x-vercel-forwarded-for', // on Vercel; see the proxy page for other hosts
})
export async function middleware(request: NextRequest, event: NextFetchEvent) {
const res = await analytics.handle(request, { waitUntil: (p) => event.waitUntil(p) })
if (res) return res
}

Then load the script from your domain: src="/_i/t.js" with data-endpoint="/_i". See first-party proxy.