Frameworks · 2 min setup

How to add analytics to Next.js

To add VisitTrack to Next.js, render one next/script tag with your data-site id in the root layout (app/layout.tsx, or pages/_app.tsx on the Pages Router). It loads once, after hydration, and counts every client-side route change as a pageview with no router code.

Updated

There is no npm package to install: the whole integration is a ~5 KB gzipped script that you load from visitrack.app. Because Next.js navigates between pages with the History API, the script's built-in History API listener sees every <Link> navigation and router.push() — you don't need usePathname() effects or a custom _app listener like older analytics setups did.

Next.js at a glance

Where the tag goes
app/layout.tsx (App Router) or pages/_app.tsx (Pages Router)
Component
next/script with strategy="afterInteractive" (the default)
Route changes
Tracked automatically (pushState / popstate)
Works with
Next.js 13–16, static export, Vercel, Netlify, self-hosted Node
Cookie banner
Not needed in cookieless mode

How to install VisitTrack on Next.js

  1. Step 1: Copy your site id

    Create a site in VisitTrack (or open an existing one). The install step — and later Settings → General — shows your snippet with the real data-site id filled in. Every snippet below uses SITE_ID as the placeholder.

  2. Step 2: App Router: add next/script to the root layout

    Put the <Script> inside <body> in app/layout.tsx, once. Root layouts wrap every route, so this is the only file you touch. Next.js forwards data-* attributes to the final <script> element, which is how the tracker reads your site id.

    app/layout.tsx
    import Script from "next/script";
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <body>
            {children}
            <Script
              defer
              data-site="SITE_ID"
              src="https://visitrack.app/tracker.js"
              strategy="afterInteractive"
            />
          </body>
        </html>
      );
    }
  3. Step 3: Pages Router: add it to _app.tsx instead

    On the Pages Router, _app.tsx wraps every page. Render the same <Script> next to <Component>. Don't put it in an individual page — it would only load when that page is the first one opened.

    pages/_app.tsx
    import type { AppProps } from "next/app";
    import Script from "next/script";
    
    export default function App({ Component, pageProps }: AppProps) {
      return (
        <>
          <Component {...pageProps} />
          <Script defer data-site="SITE_ID" src="https://visitrack.app/tracker.js" strategy="afterInteractive" />
        </>
      );
    }
  4. Step 4: Allow the script in your Content-Security-Policy

    If you set a CSP (in next.config.js headers or in proxy.ts, which replaced middleware.ts in Next.js 16), add visitrack.app to both script-src and connect-src. If your policy uses a nonce with 'strict-dynamic', host allowlists are ignored — pass the nonce to the <Script> with its nonce prop instead.

    Content-Security-Policy (additions)
    script-src 'self' https://visitrack.app; connect-src 'self' https://visitrack.app;
  5. Step 5: Deploy and open your site

    The tracker ignores localhost on purpose, so next dev traffic never pollutes your numbers. Deploy (a Vercel preview works), open the deployed URL and browse a couple of pages.

How to check VisitTrack is working on Next.js

  1. Open the deployed site in a normal browser tab and click between two or three pages using your nav links.
  2. In the VisitTrack dashboard, the live view shows you as an active visitor within a few seconds, and the Pages tab lists each route you opened — including ones reached by client-side navigation.
  3. In DevTools → Network, filter for collect: every route change sends one POST to visitrack.app/api/collect.
  4. Testing on localhost? Temporarily add data-allow-local and data-debug to the <Script>; the console then logs each event and why any was skipped.

The tracker patches history.pushState and history.replaceState and listens for popstate, so App Router <Link> clicks, router.push(), router.replace() and the back button each produce a pageview. It records the pathname only, and it skips a pageview when the pathname didn't change — so updating search params with router.replace("?tab=billing") doesn't double-count.

Parallel and intercepted routes (a modal at /photo/123 over a feed) count as a pageview for the URL the browser shows, because that's the pathname that changed.

To keep a logged-in area out of your marketing analytics, add data-exclude="/dashboard/*,/settings" to the <Script>. It's checked on every navigation, so moving from the marketing site into the app stops tracking without a reload.

How to track custom events and goals in Next.js

Call window.visitrack(name, props) from a client component after something meaningful succeeds — not on button click, so a failed request never counts. Declare the global once for TypeScript and use optional chaining so nothing throws if an ad blocker stopped the script.

For a plain click with no logic (a CTA, an OAuth button), add data-vt-goal="signup_click" to the element instead — no JavaScript needed. Then turn any event name into a goal in Settings → Goals.

global.d.ts + SignupForm.tsx
// global.d.ts
declare global {
  interface Window {
    visitrack?: ((name: string, props?: Record<string, unknown>) => void) & {
      identify?: (userId: string, traits?: Record<string, unknown>) => void;
      visitorId?: () => string;
    };
  }
}
export {};

// app/signup/SignupForm.tsx ("use client")
async function onSubmit(values: FormValues) {
  const res = await fetch("/api/signup", { method: "POST", body: JSON.stringify(values) });
  if (!res.ok) return;
  const user = await res.json();
  window.visitrack?.("signup", { method: "email" });
  window.visitrack?.identify?.(user.id, { plan: user.plan });
}

Accounts created in a server action or an OAuth callback can be reported from the server with POST /api/track and a secret API key — see server-side events. Report each signup from one place only.

Revenue attribution on Next.js

Next.js apps usually start Stripe Checkout from a route handler. Read the visitor id in the browser (window.visitrack?.visitorId?.()), send it with the checkout request and set it as client_reference_id on the Checkout Session. VisitTrack then credits each payment to the referrer, campaign and landing page that first brought that visitor in. Full walkthrough: Stripe revenue attribution; other providers are in Revenue attribution.

app/api/checkout/route.ts
// app/api/checkout/route.ts
const { priceId, visitorId } = await req.json();
const session = await stripe.checkout.sessions.create({
  mode: "subscription",
  line_items: [{ price: priceId, quantity: 1 }],
  success_url: "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
  cancel_url: "https://example.com/pricing",
  client_reference_id: visitorId ?? undefined,
});
return Response.json({ url: session.url });

Script options you might need on Next.js

All optional — add them to the same tag. Full reference: script configuration.

AttributeWhat it does
data-cookielessStore nothing in the browser; the server derives a daily-rotating id. See cookieless mode.
data-exclude="/app/*"Never track these paths (comma-separated, * wildcard). Checked on every navigation.
data-allow-localTrack on localhost and *.local — for testing an install only.
data-debugLog every event the script sends, and why it skipped one, to the console.
data-auto="false"Turn off automatic events (downloads, contact clicks, form submits, rage clicks, JS errors, 404s).

Next.js troubleshooting

No data from next dev

Expected — localhost is ignored. Test on a deployment, or add data-allow-local while testing and remove it afterwards.

Pageviews counted twice per navigation

The tag is rendered in two places (for example in both app/layout.tsx and a nested layout, or both _document.tsx and _app.tsx). Keep exactly one.

Script blocked with a CSP error in the console

Add https://visitrack.app to script-src and connect-src. With a nonce-based policy and 'strict-dynamic', give the <Script> the nonce — host allowlists don't apply there.

Preview deployments show up in your stats

Open Settings → General → Allowed hostnames and save your production domain (plus *.yourdomain.com). Hits from *.vercel.app previews are then dropped before they're stored.

window.visitrack is not a function in a component

The deferred script hadn't loaded yet. Use window.visitrack?.(…), or add the one-line queue stub from Script configuration so early calls are buffered.

Next.js analytics FAQ

Do I need an npm package to add VisitTrack to Next.js?

No. VisitTrack is a single script tag loaded with next/script; there's no package, no provider component and no build step. That also means nothing to upgrade — the script is updated on our side.

Does VisitTrack track client-side navigation in the Next.js App Router?

Yes. The script listens to pushState, replaceState and popstate, so every <Link> navigation and router.push() is recorded as a pageview automatically. Search-param-only changes aren't counted again, because pageviews are keyed on the pathname.

Should I use strategy="afterInteractive" or "lazyOnload"?

Use afterInteractive (the default). It loads right after hydration, like defer on a plain tag. lazyOnload waits for browser idle time, so visitors who bounce quickly may never be counted; beforeInteractive is for scripts that must run before hydration, which analytics doesn't need.

Will VisitTrack slow down my Next.js site or hurt Core Web Vitals?

It shouldn't measurably. The script is about 5 KB gzipped, loads after hydration and never blocks rendering. It also reports your real-user LCP, INP, CLS, FCP and TTFB, so you can check the effect yourself in the dashboard.

Does it work with static export (output: "export")?

Yes. The tag is plain HTML in the exported pages, and route changes are still tracked client-side. Only server-side events (signups from server actions, /api/track) need a server.

Do I need a cookie banner for Next.js analytics with VisitTrack?

Not in cookieless mode, which stores nothing in the browser — that's what the ePrivacy consent rule is about. Standard mode keeps a first-party visitor id, which in the EU/UK generally needs consent. This is general information, not legal advice.

Can I compare VisitTrack with Vercel Web Analytics?

Yes — see VisitTrack vs Vercel Analytics. The short version: Vercel's analytics is tied to Vercel hosting, while VisitTrack works on any host and adds revenue attribution, funnels and session replays.

Keep going

Add VisitTrack to your Next.js site

Cookie-free analytics with revenue attribution, live visitors, funnels and session replays. One script tag, every feature on every plan. 14 days free, no card required.

14-day free trial · No card required · Cookie-free · Cancel anytime

Or explore the live demo first →

Not on Next.js? See all 29 integration guides.