Frameworks · 2 min setup
How to add analytics to Next.js
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) orpages/_app.tsx(Pages Router)- Component
next/scriptwithstrategy="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
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-siteid filled in. Every snippet below usesSITE_IDas the placeholder.Step 2: App Router: add next/script to the root layout
Put the
<Script>inside<body>inapp/layout.tsx, once. Root layouts wrap every route, so this is the only file you touch. Next.js forwardsdata-*attributes to the final<script>element, which is how the tracker reads your site id.app/layout.tsximport 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> ); }Step 3: Pages Router: add it to _app.tsx instead
On the Pages Router,
_app.tsxwraps 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.tsximport 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" /> </> ); }Step 4: Allow the script in your Content-Security-Policy
If you set a CSP (in
next.config.jsheaders or inproxy.ts, which replacedmiddleware.tsin Next.js 16), addvisitrack.appto bothscript-srcandconnect-src. If your policy uses a nonce with'strict-dynamic', host allowlists are ignored — pass the nonce to the<Script>with itsnonceprop instead.Content-Security-Policy (additions)script-src 'self' https://visitrack.app; connect-src 'self' https://visitrack.app;Step 5: Deploy and open your site
The tracker ignores
localhoston purpose, sonext devtraffic 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
- Open the deployed site in a normal browser tab and click between two or three pages using your nav links.
- 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.
- In DevTools → Network, filter for
collect: every route change sends onePOSTtovisitrack.app/api/collect. - Testing on
localhost? Temporarily adddata-allow-localanddata-debugto the<Script>; the console then logs each event and why any was skipped.
Does VisitTrack track Next.js page navigation?
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
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
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.
| Attribute | What it does |
|---|---|
data-cookieless | Store 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-local | Track on localhost and *.local — for testing an install only. |
data-debug | Log 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
- React analyticsAdd VisitTrack to a React single-page app (Vite, Create React App) through index.html — React Router navigations are tracked automatically.
- Remix analyticsAdd VisitTrack to Remix or React Router v7 (framework mode) in app/root.tsx — server-rendered once, with client navigations tracked automatically.
- Vercel analyticsUse VisitTrack on Vercel: add the tag in your framework, keep preview deployments out with VERCEL_ENV or allowed hostnames, and set CSP headers.
- SvelteKit analyticsAdd VisitTrack to SvelteKit by adding one tag to src/app.html — client-side navigations are tracked, and kit.csp gets two extra sources.
- Install the tracking scriptThe reference tag, CSP notes and how to check it's working.
- Custom events and signupsvisitrack(), data-vt-goal, scroll events and server-side signups.
- Use casesHow SaaS teams, indie hackers, stores and agencies use VisitTrack.
- VisitTrack vs Google AnalyticsCookie-free analytics with revenue attribution, compared honestly.
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.