Next.js install + SPA pageview validation

Next.js Install + SPA Pageview Validation

5 min read • install

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

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

Prevent it next time

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