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 install page with the "Next.js" platform tab selected, showing the App Router variant with the real site key. Light theme.
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 Connected within seconds. Then click a link to a second route and confirm the view count goes up by one, not two.