<- Back to blog
How-to8 min readUpdated May 1, 2026

How to Track Pageviews on a Single Page App (SPA)

A 2026 guide to tracking pageviews on React, Vue, Astro, and other single-page applications. Why SPAs break analytics by default and how to fix it.

spa pageview trackingsingle page app analyticsreact analytics pageviewnext js spa pageview

TL;DR

  • 1.Single-page apps don't reload on internal navigation — your analytics tool only sees the initial load by default.
  • 2.Result: typical SPA setup misses 80%+ of pageviews. Most teams don't notice until traffic looks suspiciously low.
  • 3.Fix: hook into your router's navigation event and fire a pageview on each route change.
  • 4.Sleek's tracker handles this automatically — listens for History API changes and fires pageviews on `pushState`/`replaceState`.
  • 5.Verify in browser devtools Network tab — you should see one analytics request per route change.

Why SPAs break default analytics

Traditional websites reload a new HTML document on every navigation. The analytics tool fires once per page load — easy.

Single-page apps (React, Vue, Angular, Svelte, Astro with view transitions) don't reload. The browser stays on the same HTML document and JavaScript swaps the visible content. From the analytics tool's perspective, no new page loaded — so no new pageview gets tracked.

On a SPA, the user might navigate through 10 different "pages" but your analytics shows only 1 pageview.

warning:This is the single most common analytics installation bug. Most teams discover it months later when someone notices traffic is way lower than expected. Verify SPA pageview tracking explicitly on day one.

How Sleek handles SPAs automatically

Sleek's tracker listens for History API events (pushState, replaceState, popstate) and fires a pageview on each. Most modern frameworks use the History API for client-side routing — React Router, Next.js, Vue Router, SvelteKit — so Sleek automatically tracks route changes without integration.

Sleek install — works for SPAs out of the box
<script async src="https://getsleek.io/v1.js" data-site="YOUR_SITE_KEY"></script>

Manual SPA tracking with other tools

Next.js (app router): app/layout.tsx
'use client'
import { usePathname } from 'next/navigation'
import { useEffect } from 'react'

export function PageviewTracker() {
  const pathname = usePathname()
  useEffect(() => {
    if (typeof window.sleek === 'function') {
      window.sleek('pageview')
    }
  }, [pathname])
  return null
}

React Router pattern

React Router v6+
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'

export function PageviewTracker() {
  const location = useLocation()
  useEffect(() => {
    if (typeof window.sleek === 'function') {
      window.sleek('pageview')
    }
  }, [location.pathname])
  return null
}

Vue Router pattern

Vue 3 / Vue Router 4
import { createRouter } from 'vue-router'

const router = createRouter({ /* ... */ })

router.afterEach(() => {
  if (typeof window.sleek === 'function') {
    window.sleek('pageview')
  }
})

Generic History API hook

This is essentially what Sleek's tracker does internally. If you're using Sleek, you don't need this.

Generic SPA pageview tracker
;(function() {
  const fire = () => {
    if (typeof window.sleek === 'function') {
      window.sleek('pageview')
    }
  }
  const origPushState = history.pushState
  history.pushState = function(...args) {
    origPushState.apply(this, args)
    fire()
  }
  const origReplaceState = history.replaceState
  history.replaceState = function(...args) {
    origReplaceState.apply(this, args)
    fire()
  }
  window.addEventListener('popstate', fire)
})()

Verifying it works

  1. Open your site in a browser, open developer tools, and switch to the Network tab.
  2. Filter the requests to just analytics calls (filter by "collect" for Sleek).
  3. Click through 3–4 internal routes.
  4. You should see ONE analytics request fire per route change. If you only see one request total (initial load), your SPA tracking isn't working.

Common SPA tracking mistakes

  • Forgetting it entirely. Default GA4 / Plausible installs don't track SPA routes.
  • Hooking into the wrong event. `useEffect` without a dependency array fires on every render, not just route changes.
  • Tracking before the route renders. The URL might still be the old one.
  • Not handling popstate. Browser back/forward buttons trigger popstate, not pushState.
  • Double-counting. If your tool already detects SPAs, manually firing adds duplicates.

Frequently asked questions

Why doesn't my analytics track SPA route changes by default?

Single-page apps don't reload on internal navigation — they swap content via JavaScript. Most analytics tools fire on page load, not on JavaScript-driven navigation. You need a tool that auto-detects SPAs (like Sleek) or manually fire pageviews on route change.

Does Sleek work with Next.js out of the box?

Yes. Sleek's tracker listens for History API events, which Next.js uses for client-side routing. Just install the snippet in your root layout.

How do I verify SPA pageview tracking is working?

Open browser devtools Network tab, filter for your analytics endpoint, and click through 3–4 routes. You should see one analytics request per route change.

Should I track SPA route changes with GA4?

Yes — GA4's Enhanced Measurement includes "page changes from browser history events" which catches some SPAs. But it doesn't cover all routing approaches. Verify it's working, fall back to manual firing if not.

Does Astro need special pageview tracking?

Default Astro (multi-page mode) reloads on navigation, so pageview tracking works without configuration. Astro with View Transitions (since 4.0) is SPA-like and needs the same SPA pageview pattern as React.

Can I track SPA pageviews server-side?

For SPAs, no — the server doesn't see internal navigation. Each route change happens client-side only. You must fire pageviews from the browser.

Track your own growth loop

Sleek Analytics gives you visitors, sources, pages, devices, and real-time behavior with one lightweight script. No cookies, no GDPR banners.

Related reading