One script tag in your root layout, route changes counted automatically, custom events from client components, and real-user vitals. No cookies, no consent banner, no gtag.js in your bundle graph.
Next.js is a client-side-routed React app, and that is the only thing that trips up analytics. A tool that counts one pageview per full page load will miss most navigations after the first. A tool that is SPA-aware hooks the History API and counts each route change. That is the whole integration problem.
<script defer> tag into the root layout.tsx head (App Router) or _document.tsx (Pages Router). No package, no provider component, no build step.pushState and popstate as new pageviews, which is exactly how the Next.js router navigates.data-gm-event="signup-click" in JSX, or call window.ghostmetrics.track() from a client component.If you are hosted on Vercel and weighing the built-in option, read the Vercel Web Analytics alternatives comparison alongside this one.
A classic multi-page site loads a fresh HTML document on every click. An analytics script initializes, fires one pageview, and is discarded with the page. Next.js does not work that way. After the first document load, the router fetches the next route's payload and swaps the rendered tree in place, updating the URL with the History API. The browser never performs a second full navigation, so the script that loaded once stays alive for the whole visit.
That has three practical consequences for analytics.
The tag must live in the root layout, not in a page. In the App Router, the root layout.tsx persists across navigations and is not remounted. A tag placed there loads once per visit. A tag placed in a page component or a nested layout can be added and removed as the user moves around, depending on where the boundary sits.
Pageview counting must follow the URL, not the load event. If a tool only reports on load, a visitor who lands on your homepage and clicks through four docs pages shows up as one pageview. Tools that patch history.pushState and listen for popstate see all five. GhostMetrics does exactly this, so the Back button and Link clicks both register, with no router hook for you to maintain.
Elements appear after load. Modals, tabs and client-rendered widgets mount long after the initial document. An event tracker that scans the DOM once at startup misses them. GhostMetrics uses one delegated document listener for data-gm-event, so a button rendered by a client component ten navigations later is tracked the same as one in the initial HTML.
The other reason to think about analytics in a Next.js project is weight. A typical Next.js site already ships a framework runtime; adding a Google tag on top is a real cost. We measured gtag.js at about 150 KB compressed, against about 3 KB compressed (10 KB uncompressed) for the GhostMetrics script, which is served from Cloudflare's edge. Neither number includes what your own components add, but one of them is fifty times the other.
Every tool below can be used on a Next.js site. The table describes how each one installs, what it does about cookies, and how it is sold. Where a detail changes with plans or over time, check the vendor's current pricing page rather than trusting a blog post, including this one.
| Tool | How it installs in Next.js | Cookies / consent | Custom events | Pricing model | Best fit |
|---|---|---|---|---|---|
| GA4 | gtag snippet, or the @next/third-parties Google Analytics component | Sets _ga cookies by default; consent generally required in the EU/UK | Yes, plus audiences | Free tier; BigQuery export is free | Google Ads attribution and ecommerce funnels |
| Vercel Web Analytics | @vercel/analytics package, read in the Vercel dashboard | Cookieless; visitors identified by a daily-resetting hash | Not on Hobby; custom events on Pro | Per collected event; Hobby includes 50,000 events a month | Vercel-only projects that want zero setup |
| Plausible | Script tag | Cookieless, EU-only processing | Yes (goals) | No free plan; 30-day trial; Starter from $9/month for 10k pageviews | Open source or EU-residency requirements |
| Fathom | Script tag | Cookieless | Yes | Pageview-tiered; from $15/month for 100,000 pageviews; 7-day trial | Many sites on one plan |
| Umami | Script tag | Cookieless | Yes | Free to self-host (you pay for infra); hosted Umami Cloud also offered, check current pricing | Teams that want to run their own stack |
| GhostMetrics | Script tag | Cookieless, no localStorage, no stored IPs | Yes (data-gm-event or track()) | Free for one site; Pro $9.99/mo flat | One Next.js site with events and vitals, no server |
Three notes on the rows. GA4 is genuinely the right answer if you buy Google Ads and need conversion attribution or ecommerce funnels; nothing on this list replaces that. If GA4 is where you are today, our Google Analytics alternatives page covers the exit criteria. Vercel Web Analytics is convenient, but it is priced per event and only exists on Vercel; the comparison here walks through where it stops. Plausible and Umami are open source, which GhostMetrics is not. If reading the source is a hard requirement, pick one of those.
This is the whole integration for a Next.js 13+ project using the app/ directory. The root layout is a server component by default, and a plain <script> element inside <head> is valid there.
app/layout.tsx. This is the file that exports RootLayout and renders <html> and <body>.<head>. Use your own site ID in data-site.// app/layout.tsx
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<script
defer
src="https://ghostmetrics.nullagency.io/gm.js"
data-site="YOUR-SITE-ID"
/>
</head>
<body>{children}</body>
</html>
);
}
A plain script element is what we show because it is the closest thing to the tag GhostMetrics documents, and it keeps data-site exactly as written. next/script also works if you prefer its loading strategies; either way the tag lives in the root layout and loads once. There is no useEffect, no provider, and no usePathname hook to write, because the script observes the History API itself.
One environment note. The tag runs in every environment that serves the layout, including local development and preview deployments, so those visits land in your dashboard too. If you would rather keep them out, render the element conditionally.
{process.env.NODE_ENV === 'production' && (
<script
defer
src="https://ghostmetrics.nullagency.io/gm.js"
data-site="YOUR-SITE-ID"
/>
)}
That guard only excludes next dev. Preview deployments are production builds, so if you want them out too, gate on your host's environment variable for previews, or use a separate site ID for previews.
If your project still uses pages/, the equivalent of the root layout head is pages/_document.tsx. It is rendered only on the server (per request, or at build time for static pages) and wraps every page.
// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head>
<script
defer
src="https://ghostmetrics.nullagency.io/gm.js"
data-site="YOUR-SITE-ID"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
Do not put the tag in _app.tsx via next/head. That works for many things, but _document is the right home for a tag that should be present on every document and never re-evaluated. As with the App Router, route changes made by next/link and router.push() use the History API and are counted as pageviews automatically.
If your site has a Content-Security-Policy header, allow the ghostmetrics.nullagency.io origin for scripts and for the beacon requests the tracker sends. A blocked tracker fails silently: pages load fine and the dashboard stays empty, which is easy to mistake for a slow start.
A mixed project, with some routes in app/ and some in pages/, needs the tag in both places or, more simply, only in whichever entry point renders the document for a given route. Check both in the real-time view after deploying. The same tag pattern applies to any framework; our guide to adding analytics to a static site has the Astro, Hugo and 11ty variants.
Pageviews tell you what people read. Events tell you what they did: clicked the signup button, opened the pricing modal, copied the install command. GhostMetrics gives you two ways to record them, and both fit how Next.js renders.
Add data-gm-event to any element and clicks on it are recorded under that event name. It works in server components too, because the attribute is just HTML in the output. Nothing needs 'use client'.
// app/pricing/page.tsx (a server component)
export default function Pricing() {
return (
<a href="/signup" data-gm-event="pricing-signup-click">
Start free
</a>
);
}
Because there is one delegated listener on the document, this also covers elements that a client component renders after hydration, after a route change, or after a state update. You do not re-bind anything when the tree changes.
For events that are not clicks, such as a form that validated and submitted, call track() from a handler in a client component. Only the section property is stored as metadata. Each event carries the page path and any UTM values from the session.
'use client';
export function NewsletterForm() {
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
// ...submit to your API...
window.ghostmetrics?.track('newsletter-subscribe', { section: 'footer' });
}
return (
<form onSubmit={onSubmit}>
<input type="email" name="email" required />
<button type="submit">Subscribe</button>
</form>
);
}
If you use TypeScript, the window.ghostmetrics property does not exist on the default Window type, so add one declaration file.
// types/ghostmetrics.d.ts
export {};
declare global {
interface Window {
ghostmetrics?: {
track: (name: string, meta?: { section?: string }) => void;
};
}
}
The optional chaining in the form handler is deliberate. The script is loaded with defer, and a visitor with Global Privacy Control or Do Not Track enabled gets nothing collected, so window.ghostmetrics may be absent in either case. Your form should submit whether or not analytics ran.
Be aware of the limits. Events have a name, a count, a last-fired time, the page path and the session UTM. There is no numeric value or revenue field, and no per-visitor funnel across sessions. For revenue, keep using your payment provider's dashboard. For a fuller treatment of event naming, see how to track custom events without Google Analytics. And if you send traffic from campaigns, the UTM parameters guide covers how to tag links so the source shows up in the UTM Breakdown panel.
Next.js developers tend to care about performance, and lab scores from a laptop on fast Wi-Fi only tell part of the story. GhostMetrics captures four vitals from real visitors' browsers using PerformanceObserver and Navigation Timing: FCP, LCP, CLS and TTFB. The dashboard has a Core Web Vitals panel with a per-page table, so you can see which routes are slow for actual users rather than which one scored badly in a single test run.
INP is not captured. If interaction latency is a concern for your app, and for a client-heavy Next.js UI it may well be, you will need Chrome's own tooling or your own instrumentation for that metric.
One definition worth knowing, since Next.js sites usually sit behind a CDN and often behind redirects. GhostMetrics now measures TTFB the way Google does: responseStart measured from navigation start. That includes redirects, DNS, TCP and TLS, and server time, and it is rated against the 800 ms "good" threshold. So a trailing-slash redirect or a www to apex redirect counts against TTFB, as it does in Google's numbers. Rows recorded by the older tracker, which measured server time only, are excluded from the dashboard average and stay in the raw export, labelled in the manifest.
Practical use: check the per-page table after a release. If the LCP for a route moves, look at what changed in that route, such as a hero image that lost its priority prop or a data fetch that moved from a server to a client component. For the broader method, see Core Web Vitals real user monitoring.
The cookie banner is usually the visible reason teams leave GA4. GA4 sets _ga cookies by default, and consent is generally required for cookie-based analytics in the EU and UK. That means a banner component, a consent state store, and code in your layout that delays loading the Google tag until the visitor answers, which in turn means the visitors who decline are missing from your numbers.
GhostMetrics sets no cookies and uses no localStorage. It stores no IP addresses (they are hashed), holds no personal data, and creates no identifier that follows a visitor across sessions. The only browser storage it touches is per-tab sessionStorage, which holds a scroll-depth counter and the first UTM set seen in that tab, and is cleared when the tab closes. It honors Global Privacy Control and Do Not Track by collecting nothing. For a Next.js team, the practical outcome is that you can delete the consent provider that wraps your tree, or at least stop making analytics depend on it.
Whether you still need a banner for other reasons, such as ad pixels or embedded video, depends on everything else your site loads. See do I need a cookie banner and web analytics without a consent banner for the details, and get legal advice for your own situation.
If your app already uses @next/third-parties or a hand-rolled gtag component, the switch is small in code and mostly a matter of order.
data-gm-event attributes or track() calls for the handful of events you actually use.What you give up is real: Google Ads attribution, audiences, ecommerce funnel reports, and multi-session user paths. GhostMetrics attributes UTMs by first touch within a tab session and has no returning-visitor concept by design. If those capabilities drive revenue for you, keep GA4 and consider our cookieless analytics comparison for a second tool.
GhostMetrics is cookieless web analytics by Null Agency, and we run it on this site. Paste one script tag into your root layout or _document.tsx and you are live; there is no package, no tag manager, no build step. Client-side route changes are counted as pageviews, and events work from JSX with data-gm-event or from code with window.ghostmetrics.track(). The free plan is free forever: one site, unlimited pageviews, real-time dashboard, every view, full history.
Running several Next.js projects or want a public dashboard URL? Pro is $9.99/mo flat with unlimited pageviews and unlimited websites (fair-use ceiling of 250), with a 30-day free trial: a card starts the trial, nothing is charged for 30 days, and you can cancel anytime. Raw event export as CSV or JSON is on every plan, including free.
Start free, no card See the live demoDisclosure: GhostMetrics is built by Null Agency and we use it on this site, so we are not a neutral party about our own product. We have kept the claims to what the free and paid plans actually do, and pointed to GA4, Plausible, Umami and others where they fit better. Pricing for third-party products can change; always check the vendor's current pricing page before deciding.