Add cookieless analytics to a Next.js app
One script tag for the App Router and the Pages Router, why route changes are the only catch, and how to fire custom events — no npm package, no consent banner.
DA Orbit
Adding analytics to a Next.js app is one script tag and one decision: where it goes so that client-side route changes are counted. This covers both the App Router and the Pages Router, cookieless, with no consent banner to add.
You do not need an npm package for this. A package pulls the same script in with extra weight and a React wrapper you do not need — the tag below is the whole integration.
App Router
Put the tag in app/layout.tsx, after {children}, using next/script with strategy="afterInteractive" so it loads once hydration is done and does not block the first paint.
app/layout.tsx
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Script
src="${site.api}/tracker.js"
data-site="YOUR_SITE_KEY"
strategy="afterInteractive"
/>
</body>
</html>
);
}
That is it. The tracker patches history.pushState, so every client-side navigation the App Router does is reported as a pageview without any extra wiring.
Pages Router
Same tag, placed in pages/_app.tsx. Do not put it in _document.tsx — scripts there run before hydration and miss the route-change hook.
pages/_app.tsx
// pages/_app.tsx
import Script from "next/script";
export default function App({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<Script
src="${site.api}/tracker.js"
data-site="YOUR_SITE_KEY"
strategy="afterInteractive"
/>
</>
);
}
The Pages Router fires routeChangeComplete on navigation and also calls pushState, so the same automatic pageview counting applies.
Why route changes are the catch
A naive analytics tag counts the first server-rendered page and nothing after it, because a Next.js navigation never reloads the document. Any tracker that hooks pushState — this one does — reports SPA navigations on its own. If you were using a tool that does not, you had to call its page function manually in a router event; here you do not.
Custom events and goals
Call the global from any client component — a form submit handler, a button onClick, a useEffect.
signup-form.tsx
// Anywhere in a client component
window.quantalog?.("signup_completed");
The optional chaining matters: during SSR and before the script loads the global is undefined, and window.quantalog?.() is a no-op in that window rather than a crash.
No consent banner needed
The tracker sets no cookies and stores nothing in the browser, so there is no analytics cookie to disclose. If your Next.js app also runs ad pixels or a session recorder, those still need a banner — it covers every cookie on the site, not just one. See do you need a cookie banner for analytics .
docs
the analytics page
Try Quantalog on your own site
One script tag, no cookies, live numbers in about three seconds. Free forever on the Hobby plan.
Start freeKeep reading
Add cookieless analytics to an Astro site
One inline tag in your base layout, what to do about view transitions, and how to fire events from an island — keeping Astro's near-zero JavaScript promise intact.
Add cookieless analytics to a WordPress site
The three places to put the tag ranked by how well they survive updates, why page caching is not a problem, and how to track form submissions — no consent banner for the analytics itself.