Skip to main content

Installing

Install on Next.js

Both routers, plus the proxy setup that makes Next.js the easiest platform to run TrueStat first-party.

Both routers, plus the proxy setup that makes Next.js the easiest platform to run TrueStat first-party.

App Router

Add the tag to your root layout using next/script.

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://truestat.io/js/t.js"
          data-website-id="ts_a1b2c3d4e5f6"
          data-domain="example.com"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Use strategy="afterInteractive". It is the right one for a tracker: the script loads after hydration, does not block anything, and still catches the first page view. beforeInteractive would load it earlier at a cost to your Largest Contentful Paint for no gain, and lazyOnload can miss short visits.

Because the tag is in the root layout, it applies to every route including nested layouts and route groups. You do not need to repeat it.

The Next.js chip renders the App Router version, with your site key already in it.

Pages Router

pages/_document.tsx:

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script
          defer
          data-website-id="ts_a1b2c3d4e5f6"
          data-domain="example.com"
          src="https://truestat.io/js/t.js"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

_document.tsx renders once per page, so this covers every route.

Client-side navigation is handled

You do not need to do anything for route changes. The script watches history.pushState, history.replaceState and popstate, and records a view on each path change.

Navigations are debounced at 100 ms. Next.js's router frequently calls replaceState immediately after pushState, and without the debounce every navigation would count as two page views.

If your app uses hash-based routing (/#/dashboard), path changes in the hash are not tracked by default. Add data-ts-track-hash="true". See Single-page apps.

Keeping your own environment variables out of the tag

Nothing in the tag is secret, so there is no security reason to hide the site key. But if you prefer to keep it in your environment:

<Script
  src="https://truestat.io/js/t.js"
  data-website-id={process.env.NEXT_PUBLIC_TRUESTAT_SITE_ID}
  data-domain={process.env.NEXT_PUBLIC_TRUESTAT_DOMAIN}
  strategy="afterInteractive"
/>

It must be a NEXT_PUBLIC_ variable — the value has to reach the browser.

Serving from your own domain

Next.js is the easiest platform for this, because it is config only. Add rewrites to next.config.ts:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      { source: "/js/t.js", destination: "https://truestat.io/js/t.js" },
      { source: "/api/events", destination: "https://truestat.io/api/events" },
    ];
  },
};

export default nextConfig;

Then change the tag's src to the relative path:

<Script
  src="/js/t.js"
  data-website-id="ts_a1b2c3d4e5f6"
  data-domain="example.com"
  strategy="afterInteractive"
/>

You do not need data-api-url. When the script's own src is not on truestat.io, it sends events to /api/events on the page's own origin automatically.

Next.js rewrites forward the visitor's IP correctly with no extra configuration. That matters more than it sounds — see Serving the script from your own domain, which explains why a proxy that drops the client IP collapses all of your visitors into one.

Content Security Policy

If you set a CSP in next.config.ts under headers(), both directives are needed:

script-src 'self' https://truestat.io; connect-src 'self' https://truestat.io;

If you are using the rewrites above, 'self' covers both and you need no TrueStat entry at all.

Verify

Load a page, then check the install page in the app — it flips to Recording visits within seconds. Then click a link to a second route and confirm the view count goes up by one, not two.

Then add the crawler step

The script above runs in a browser. An AI crawler is not a browser: GPTBot, ClaudeBot and PerplexityBot take the HTML and leave without running any JavaScript, so however correctly the tag is installed, none of their visits appear.

Catching them takes one package in your proxy:

npm install @truestat/ai-crawl
// proxy.ts — middleware.ts on Next 15 and earlier
import { truestat } from "@truestat/ai-crawl/next";

export const proxy = truestat("YOUR_SITE_KEY");
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Your install page has this with your own site key already filled in. Full setup, including how to compose it with a middleware you already have: Server-side tracking.

Was this page helpful?

Last updated September 17, 2026