Skip to content

Analytics

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

Founder2 min read

Summarize with

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.

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

AnalyticsNext.jsGuide

Keep reading

All articles

See your own numbers, live.

One script tag, no cookies, real-time data in about three seconds. The Free plan stays free forever.

Or continue with