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 visitor cookies and counts client-side navigations automatically, without a usePathname() effect.
After the first load, every <Link> click is a soft navigation: Next.js swaps the route with the browser History API instead of reloading, so a tracker that only fires on page load misses it. This tag hooks the same API.
Where the tag goes in the App Router
Put it once in the root layout, app/layout.tsx. The App Router doesn't re-mount the root layout between routes, so the tag loads once and stays live.
// 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>: Next.js recommends it for third-party scripts, and its strategy prop controls when the tag loads, and Next.js lists analytics under afterInteractive — early enough to catch the first view, late enough not to slow hydration. Replace YOUR_KEY with your site's public key from the dashboard; it's safe to commit.
The script is ~1.2 KB gzipped; GA4's tag weighs about 140 KB (see how much JavaScript each analytics tool ships).
On the Pages Router (a pages/ directory), put the tag in pages/_document.tsx, inside <Head>.
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. A <Link> click or router.push() calls history.pushState, and the back button fires popstate.
The Simplytics script wraps pushState and replaceState and listens for popstate, so each new path records a view; the page you left counts as the referrer and folds into Direct. Query-only and hash-only changes (a filter, a tab, an anchor) are ignored.
The App Router has no route-change event (router.events is gone). The documented workaround is to fire a page view from usePathname() and useSearchParams() in a Client Component. With this tag, that code double-counts every navigation:
- Don't add a
usePathname()/useSearchParams()effect that tracks page views. The tracker already caught thepushState. - Don't re-initialize analytics on route change. That re-patches the History API and double-counts.
What each tool needs:
| 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. |
For the general version, see how to track single-page app page views without cookies; for Astro, add analytics to an Astro site.
How to verify the first hit
Test a production build, not next dev:
- Run
next build && next start(or deploy). - Open DevTools Network, filter for
track, and load a page. You should see aPOSTtohttps://simplytics.dev/trackthat returns{"status":"success"}. - Click an internal
<Link>. A newPOST /trackshould fire for the new path. If it doesn't, the tag isn't in the rootapp/layout.tsx, a bare<script>was used instead ofnext/script, or an extension is blocking it. - Open your dashboard. For a new account, its first accepted page view starts the 30-day free trial (no card). Adding another site does not restart that account trial. The site shows as verified once a valid hit arrives.
How to exclude your own visits
Three ways to keep your own traffic out:
Keep the tag out of development. Render it only in a production build:
{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.Switch off your own browser. On your live site, run
localStorage.simplytics_ignore = '1'in the DevTools console. The tag then sends nothing from that browser on that site.Block the request in your own browser on the live site — uBlock Origin, a Pi-hole, or a
hostsentry forsimplytics.dev.
When another tool fits better
- Hash-based routing is not auto-tracked. The App Router uses real paths, so only a stray
#/pathwidget is affected. - Country-level geography only. Simplytics reports countries, not cities, and stores no raw IP address in its analytics database. For city-level location or long-lived per-visitor journeys, GA4 or Matomo fit better.
- No Vercel-native panel. If you want analytics inside the Vercel dashboard,
@vercel/analyticsis the in-house option; Simplytics is a separate dashboard.
Plausible auto-tracks App Router navigation the same way. What differs is no visitor cookies, a primary analytics database in Warsaw, raw rows deleted nightly with no scheduled expiry for held daily totals, and the price: $1/month, billed yearly ($12/year, with unlimited sites and pageviews, and a 30-day trial with no card) against Plausible's $9/month. Compare Simplytics vs Plausible and vs Google Analytics, try the demo or see pricing.
See also: add analytics to WordPress without a plugin, the no-framework version of the same one-line tag.
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.