Next.js install + SPA pageview validation
Next.js Install + SPA Pageview Validation
Admaxxer is a DTC analytics platform with built-in Meta + Google ad ops. Next.js uses client-side routing, which means route transitions do not trigger a full page load and therefore do not fire the browser's native pageview — attribution and on-page behavior go silent after the first hit. TL;DR: fire admaxxer.track('pageview') on every route change — router.events.on('routeChangeComplete') for the pages router, or a usePathname + useEffect pair in a client component for the app router.
Symptoms
- First landing pageview is captured, but subsequent internal navigations are missing from Admaxxer.
- Time-on-site and session length look artificially short.
- Attribution works on the first page but funnels that span multiple routes look broken.
- Admaxxer realtime shows user activity flat-line after the entry page.
window.admaxxeris defined, but only one event per session appears in the log.
Root cause
Next.js renders the initial document on the server, then hydrates and hands navigation to the client. Internal <Link> clicks trigger a pushState and re-render without reloading the document. Any analytics code that lives inside the <script> tag (which only runs once per hard reload) will only fire once per session unless you explicitly hook into the client router.
Fix
Step 1: Add the Admaxxer loader
Put the loader script in your root layout (app/layout.tsx for app router) or pages/_app.tsx (pages router) using Next's <Script strategy="afterInteractive">. Use your Admaxxer public website id.
Step 2 (pages router): listen to router events
In pages/_app.tsx:
import { useEffect } from 'react';
import { useRouter } from 'next/router';
export default function App({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const handle = (url: string) => window.admaxxer?.('track', 'pageview', { path: url });
router.events.on('routeChangeComplete', handle);
return () => router.events.off('routeChangeComplete', handle);
}, [router.events]);
return <Component {...pageProps} />;
}
Step 3 (app router): use usePathname
Create a client component that tracks path changes:
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
export function AdmaxxerPageviews() {
const pathname = usePathname();
const search = useSearchParams();
useEffect(() => {
window.admaxxer?.('track', 'pageview', { path: pathname + (search?.toString() ? '?' + search.toString() : '') });
}, [pathname, search]);
return null;
}
Mount <AdmaxxerPageviews /> inside app/layout.tsx.
Step 4: De-duplicate the initial pageview
If Admaxxer's autocapture already fires on load, your first route-change effect will double-fire. Skip the first useEffect tick (ref-guard the initial mount) to avoid counting the landing twice.
Step 5: Attribute UTMs
On landing, grab utm_* from window.location.search and pass them once through admaxxer('identify', { utm }) so every downstream event inherits the source/medium — otherwise subsequent pageviews look like direct traffic. Do this in the same effect that fires the first pageview so the attribution metadata arrives before any subsequent event.
Step 6: Handle edge-case routers
If you use next-intl, next/dynamic, or streaming server components, routes can transition without changing pathname (query-only updates). Consider tracking on pathname + searchParams.toString() as a joint key so query-only navigations (filters, facet selection) are captured if they matter to your analytics.
Verify the fix
- Open DevTools -> Network -> filter for the Admaxxer ingest endpoint.
- Click through five internal links. You should see five pageview beacons, one per navigation.
- In Admaxxer -> Realtime, the session shows the correct path sequence.
- Session duration approximates actual time on site.
- Funnel and LTV pipes reflect multi-step journeys, not just the landing page.
- UTM fields appear on every pageview, not just the first, confirming identify propagated.
Prevent it next time
- Centralize the tracker in one component so future developers cannot accidentally remove it.
- Write a Playwright test that navigates across three pages and asserts three beacons.
- Watch for double-fires when you refactor layouts — repeating effects are easy to introduce in app router.
- Document the approach in your repo so swapping pages for app router does not delete your tracking.
Related guides
FAQs
Q: Does Admaxxer autocapture SPA pageviews?
A: The loader attempts to hook history.pushState and fire a pageview on change. It works for most SPAs, but Next.js's internal router and streaming server components can bypass it, so the explicit hook above is more reliable.
Q: Should I fire pageview on routeChangeStart or routeChangeComplete?
A: Complete. On start, the new URL is not finalized and UTM/query strings can be off.
Q: What about middleware.ts redirects?
A: Middleware redirects happen before the client router sees the route, so your hook only runs on the final URL — which is what you want for attribution.
Frequently Asked Questions
Does Admaxxer autocapture SPA pageviews?
The loader tries to hook history.pushState. It works for most SPAs, but Next.js's router and streaming server components can bypass it, so the explicit hook is more reliable.
Should I fire pageview on routeChangeStart or routeChangeComplete?
Complete. On start, the new URL is not finalized and UTM/query strings can be off.
What about middleware.ts redirects?
Middleware redirects happen before the client router sees the route, so the hook only runs on the final URL — which is what you want for attribution.
Put This Knowledge Into Action
Bring Meta and Google ads into one self-hosted workspace.
Get Started Free