New: Free plan is live 7 August 2026 · View changelog →
Antlytics logoAntlytics
← Blog
5

SPA Analytics: Tracking Page Views in Single-Page Apps

SPAs don't trigger full page loads. Here's how analytics handles that and how to set it up correctly.

SPA Analytics: Tracking Page Views in Single-Page Apps

Traditional web analytics is built around one simple assumption: each page load is a pageview. This breaks in single-page applications.

The SPA problem

In a single-page application (React, Vue, Angular, Svelte), the initial page load fetches the app shell. All subsequent "page" changes update the DOM without triggering a new HTTP request. From the browser's perspective, the user is still on the same page.

A naive analytics snippet that only fires once on load will undercount. Navigate from /home to /pricing to /signup — you get one pageview and miss everything after.

This is not a theoretical concern. If you install a script that only listens for full reloads (or only popstate) on a React or Next.js SPA, your analytics will undercount pageviews and misrepresent which pages users actually visit.

How History API navigation works

Browsers expose a popstate event that fires when the URL changes due to browser history navigation (back/forward buttons). The History API also provides pushState and replaceState for programmatic URL changes.

Most SPA routers use pushState to navigate. The problem: pushState does not fire a popstate event.

Older snippets that only did this:

window.addEventListener("popstate", send);

caught browser back/forward navigation, but not pushState navigation — the router-driven transitions that make up most SPA navigation.

The solution: hosted tracker (or official SDK)

Antlytics's hosted tracker.js patches history.pushState / replaceState and listens for popstate, so soft navigations are counted automatically. Prefer that over a hand-rolled IIFE that posts to /api/ingest/pageview.

<script
  defer
  src="https://www.antlytics.com/tracker.js"
  data-tracking-id="YOUR_TRACKING_ID"
  data-api-host="https://www.antlytics.com"
></script>

Paste that into your app shell (index.html, root layout, or framework head config). Replace YOUR_TRACKING_ID with the UUID from Settings → Tracking Snippet. See Quick start.

You do not need to wrap History methods yourself. The hosted tracker also covers engagement beacons and sticky UTMs that a minimal custom script usually skips.

Framework-specific solutions

Next.js (recommended approach)

For Next.js, use @antlytics/analytics/next. The <Analytics> component loads the hosted tracker and handles App Router and Pages Router navigation automatically.

See the Next.js analytics setup guide and Next.js App Router docs.

React Router (without Next.js)

Add the hosted tracker to index.html or public/index.html in the <head>. History API patches cover React Router navigations without a useLocation hook.

SvelteKit

Prefer @antlytics/svelte in +layout.svelte, or the hosted script in app.html. See analytics for SvelteKit.

Vue Router / Nuxt

Prefer @antlytics/nuxt — see analytics for Nuxt and the Nuxt docs. For plain Vue without Nuxt, add the hosted script to your HTML entry; History patches cover Vue Router.

Testing SPA analytics

Verify your setup captures all navigations:

  1. Open your site in the browser with dev tools → Network tab open.
  2. Navigate through multiple pages using internal links.
  3. Confirm tracker.js is loaded and a POST request to api/ingest/pageview fires for each navigation.
  4. Check that the pathname in each request matches the URL shown in the address bar.

Common failure modes:

FAQ

Can I use a minimal script and just accept missing navigations? For content sites where most sessions are single-page (someone arrives, reads, leaves), the initial pageview covers most of your meaningful data. For apps where users navigate between many views, missing navigations significantly distorts your data — use the hosted tracker.

Does the SDK handle all of this automatically? Yes for Next.js (@antlytics/analytics/next), Nuxt (@antlytics/nuxt), SvelteKit (@antlytics/svelte), and any site using the hosted tracker.js. All of those use History patches plus popstate.

Why not write my own pushState wrapper? You can, but you will recreate (and then maintain) behaviour that already ships in tracker.js. Prefer the official install.

What about hash-based routing? If your SPA uses hash routing (/#/page instead of /page), listen for the hashchange event in addition to History-based tracking, or prefer path-based routing when you can.


Related: Next.js analytics setup guide · SvelteKit analytics · Nuxt analytics · Quick start · Antlytics implementation guides