# Add CLItrail to Next.js (Pages Router)

Where the CLItrail tag goes in Next.js (Pages Router), in its own idiom, with the consent, CSP and client-side routing details and how to check it works.

Written for Next.js 16 (the same code works from 12.2)

## Where the tag goes

Edit `pages/_app.tsx`. Put the tag on every page that shows your install command; site-wide is best, after your existing analytics tags.

```js
import type { AppProps } from 'next/app';
import Script from 'next/script';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Script src="https://YOUR_SERVICE/browser.js" data-website="YOUR_WEBSITE_ID" strategy="afterInteractive" />
    </>
  );
}
```

## Only after consent

Use the loader in place of the tag: nothing from CLItrail loads until `startClitrail()` runs. Load the loader before your consent manager's script and call `startClitrail()` from the manager's accept callback (most managers also run it on later visits once a choice is stored; check yours). Use the marketing category when installs go to Google Ads, Meta, TikTok or X Ads, and the analytics category when they only reach GA4 or your webhooks. In the Pages Router, `beforeInteractive` scripts go in `pages/_document.tsx`; drop the Script from `_app.tsx` when you gate on consent.

Consent loader

```js
// Loads CLItrail only after the visitor agrees. Call startClitrail() from your
// consent manager's "accepted" callback; nothing is fetched or stored before that.
window.startClitrail = () => {
  if (document.getElementById('clitrail')) return;
  const tag = document.createElement('script');
  tag.id = 'clitrail';
  tag.src = 'https://YOUR_SERVICE/browser.js';
  tag.dataset.website = 'YOUR_WEBSITE_ID';
  document.head.append(tag);
};
```

```js
import { Html, Head, Main, NextScript } from 'next/document';
import Script from 'next/script';

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <Main />
        <NextScript />
        <Script src="/clitrail-consent.js" strategy="beforeInteractive" />
      </body>
    </Html>
  );
}
```

## Client-side routing

Links and `router.push` change pages without a reload. Call `useClitrailRoutes()` once in `pages/_app.tsx`: it records a visit after each `routeChangeComplete`. The page the browser loaded is recorded by the tag, not again.

Route helper

```js
// Records a visit when client-side navigation opens a new page path. The tag records
// the page the browser loaded, so that page is never recorded twice; before the SDK has
// started (for example, before consent) it records nothing.
declare global {
  interface Window {
    InstallAttribution?: { init(options: { service: string; project: string }): Promise<unknown>; ready?: Promise<unknown> };
  }
}

let recorded: string | undefined;

export function recordVisit(): void {
  if (typeof window === 'undefined') return;
  recorded ??= new URL(performance.getEntriesByType('navigation')[0]?.name ?? location.href).pathname;
  const sdk = window.InstallAttribution;
  if (!sdk?.ready || location.pathname === recorded) return;
  recorded = location.pathname;
  sdk.ready = sdk.init({ service: 'https://YOUR_SERVICE', project: 'YOUR_WEBSITE_ID' });
}
```

```js
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { recordVisit } from './clitrail';

// Call once in pages/_app.tsx: records a visit after each client-side route change.
export function useClitrailRoutes() {
  const router = useRouter();
  useEffect(() => {
    router.events.on('routeChangeComplete', recordVisit);
    return () => router.events.off('routeChangeComplete', recordVisit);
  }, [router.events]);
}
```

Call it in App

```js
import { useClitrailRoutes } from '../lib/use-clitrail-routes';

// First line inside App():
useClitrailRoutes();
```

## Content Security Policy

A fixed policy goes in `next.config.js` `headers()`; a nonce-based one in `proxy.ts` (`middleware.ts` before Next.js 16). In either, add `https://YOUR_SERVICE` to `script-src` and `connect-src`, and `blob:` to `worker-src` (only Safari before 26 needs that one). With a nonce and `'strict-dynamic'`, also pass the nonce to the Script (`nonce={nonce}`, read from `headers()` in a Server Component) as Next.js's CSP guide shows.

```
script-src 'self' https://YOUR_SERVICE;
connect-src 'self' https://YOUR_SERVICE;
worker-src 'self' blob:;
```

## Verify it works

1. Run `npm run dev` (http://localhost:3000) or deploy.
2. Open a page with the tag in Chrome or Firefox (accept analytics first if you gate CLItrail on consent), open the developer console and run `await InstallAttribution.ready`. It resolves to `{ ok: true, urlId, hasAnalyticsContext, expiresAt }`.
3. In the CLItrail dashboard, open Visits & identities: the visit is listed.
4. Anything else names the cause: `disabled` (no consent yet, or `data-enabled="false"`), `opfs_unavailable` (the page is not served over https or from localhost), `Error` (the service refused the visit: add the page's origin, including a development origin such as `http://localhost:3000`, under Settings → Additional domains). If `InstallAttribution` is undefined, the tag did not load: check the Network tab and your Content-Security-Policy.

Then run your installer on the same computer and check **Install events**, or run the hook with [`--doctor`](https://clitrail.com/docs/doctor).

## Notes

`next/script` forwards `data-*` props to the `<script>` element, so the tag starts itself.

Next.js (Pages Router) documentation: [nextjs.org/docs/pages/guides/scripts](https://nextjs.org/docs/pages/guides/scripts)

Using other install paths or destinations? The [setup generator](https://clitrail.com/docs/setup-generator) puts this snippet together with your hooks and destination checklist.
