Skip to content
All posts
AnalyticsNext.jsGuide

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 free

Keep reading