Add analytics to a Next.js App Router site without cookies
To add analytics to a Next.js App Router app, put one script tag in the root layout using the next/script component. For Simplytics that is a <Script> in app/layout.tsx with strategy="afterInteractive". It sets no cookies, needs no consent banner, and — the part most Next.js guides make you write extra code for — counts client-side navigations automatically, without a usePathname() effect.
The App Router changes how a tracker has to work. Your first paint is a normal server-rendered HTML load, so any tag counts it the ordinary way. But every <Link> click after that is a soft navigation: Next.js swaps the route in place using the browser History API instead of reloading the document. A tracker that only fires on page load would count the landing page and miss every navigation after it. The tag below handles both, because it hooks the same History API that the App Router drives.
Where the tag goes in the App Router
Put it once, in the root layout every route shares — app/layout.tsx. That component renders on the first request and then persists across soft navigations (the App Router does not re-mount the root layout when you move between routes), so the tag loads exactly once and stays live for the whole session.
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script
async
src="https://simplytics.dev/track.js"
data-key="YOUR_KEY"
strategy="afterInteractive"
/>
</body>
</html>
)
}
Use next/script, not a bare <script> tag. Next.js recommends the <Script> component for third-party scripts because it controls when the tag loads through the strategy prop, where a plain <script> in your JSX gives you no say over load timing. Next.js lists analytics under afterInteractive, which loads the tag as soon as the page is interactive — early enough to catch the first view, late enough not to compete with your app's own hydration. Replace YOUR_KEY with the site key from your dashboard; it is the same public key that ships in the snippet on every customer page, safe to commit.
The script is ~1.8 KB gzipped. For how that compares to the alternatives, see how much JavaScript each analytics tool ships, where GA4's tag weighs about 140 KB.
On the Pages Router (an older pages/ directory), the equivalent home for the tag is pages/_document.tsx, inside <Head> — that file, like the App Router's root layout, renders on every route. The rest of this post assumes the App Router.
Do client-side navigations get counted?
Yes — with Simplytics, App Router soft navigations are counted automatically, and you should not add a usePathname() effect to send page views. The App Router navigates with the browser History API: a <Link> click or a router.push() calls history.pushState, and the back button fires popstate. The Simplytics script wraps exactly pushState and replaceState and listens for popstate, so when the App Router moves to a new path, the tracker sees it, records a view, and sets the page you just left as the referrer (which the server folds into Direct, like any in-site click). Only the path is compared, so a change to the query string or hash alone — a filter, a tab, an anchor — is ignored and does not inflate your numbers.
This is worth being explicit about because the App Router does not expose a route-change event the way the old Pages Router did (router.events is gone). The documented workaround is to read usePathname() and useSearchParams() in a Client Component and fire a page view whenever they change. With a tag that already patches the History API, you do not need that code — and if you add it anyway, every navigation is counted twice. So:
- Don't add a
usePathname()/useSearchParams()effect that calls a "track page view" function. The tracker already caught thepushState; firing again double-counts every navigation. - Don't wire up a second analytics init on route change. The tag initializes once in the persistent root layout and stays live; re-initializing it re-patches the History API and double-counts.
Here is what each analytics tool needs to count App Router soft navigations:
| Tool | App Router soft navigation | What you add |
|---|---|---|
| Simplytics | Automatic | Nothing beyond the tag. It patches the History API the App Router uses. |
| Plausible | Automatic | Nothing for History-API routers; its standard script handles pushState navigation. |
| Fathom | Manual (their Next.js doc) | Fathom's Next.js guide fires views from a usePathname() effect via fathom-client; its generic SPA mode is the data-spa="auto" attribute. |
| Google Analytics 4 | Config or manual | Turn on the "Page changes based on browser history events" Enhanced-Measurement option, or send page_view from a usePathname() effect (Next.js ships a GoogleAnalytics component in @next/third-parties) — never both, or you double-count. |
Simplytics and Plausible auto-track History-API navigation with nothing extra; Fathom's own Next.js doc has you fire views manually; GA4 makes you choose a mechanism and avoid the double-count trap. (For the framework-agnostic version of this, see how to track single-page app page views without cookies; for the Astro flavour, add analytics to an Astro site.)
How to verify the first hit
Next.js dev and production differ, so verify against a production build, not next dev:
- Build and serve the production output:
next build && next start(or deploy). The tag runs from the served HTML. - Open the site and open your browser's DevTools Network tab. Filter for
track. On the first page you should see aPOSTtohttps://simplytics.dev/trackthat returns{"status":"success"}. - Click an internal
<Link>. The URL changes without a full document reload — and a newPOST /trackshould fire for the new path. That is the soft-navigation count working. If it doesn't fire, the tag isn't in the shared root layout, or a bare<script>was used instead ofnext/script. - Open your dashboard. The first real page view starts your 30-day free trial (no card), and the site flips to verified once a hit arrives.
No hit on step 2 usually means the tag landed in a single route segment's layout instead of the root app/layout.tsx, or a browser extension is blocking the request.
How to exclude your own visits
Simplytics stores no cookie and no IP address — country comes from an edge header and the identifier is a same-day hash that is wiped nightly — so there is nothing to key a per-visitor "exclude me" toggle on, and the dashboard has no such setting. Two honest ways to keep your own traffic out:
Keep the tag out of development. Render it only in a production build, so it never loads under
next dev:{process.env.NODE_ENV === 'production' && ( <Script async src="https://simplytics.dev/track.js" data-key="YOUR_KEY" strategy="afterInteractive" /> )}process.env.NODE_ENVis'production'innext buildoutput and'development'undernext dev, so your local work never sends hits.Block the request in your own browser for the live site — uBlock Origin, a Pi-hole, or a
hostsentry forsimplytics.dev. Raw visit rows are deleted nightly, so none of your own visits leave a per-visit trail; a handful of extra views is a negligible share of the aggregate totals once real traffic arrives.
Where Simplytics is not the right choice
Honest limits, because the tag isn't magic:
- Hash-based routing is not auto-tracked. If some part of your app changes only the URL hash (
#/path) instead of the path, the History-API hook never fires. The App Router uses real path-based URLs, so this is rare on Next.js, but a stray hash-router widget is worth knowing about. - Country-level geography only. Simplytics reports countries, not cities — it never stores an IP to resolve one. If you need city-level location or long-lived per-visitor journeys, GA4 or Matomo fit better.
- No Vercel-native panel. If you deploy on Vercel and want analytics inside the Vercel dashboard next to your deploys,
@vercel/analyticsis the frictionless in-house option; Simplytics is a separate dashboard on its own domain.
On the Next.js question itself, Simplytics isn't uniquely magic — Plausible auto-tracks the same History-API navigation. What differs is everything around it: no cookies (so no consent banner), EU data storage in Warsaw, raw rows deleted nightly with aggregates kept forever, and the price. That's the recurring theme — a lot of functionality for a lot less money: Simplytics is $1/month, billed yearly ($12/year, with 50,000 page views a month across up to 12 sites, and a 30-day trial with no card) against Plausible's $9/month. The full trade-offs are in Simplytics vs Plausible and vs Google Analytics, and you can see a live dashboard on the demo or start from pricing.
Next.js behaviour reflects the App Router docs (next/script, root layout, usePathname) and each vendor's published Next.js or SPA guidance. Last verified: 2026-09-13.