How the Plausible Tracker Handles SPA Routing: Hash-Based and pushState Detection

Plausible Analytics automatically tracks Single Page Application (SPA) navigation by instrumenting browser history methods and monitoring hash changes, emitting pageview events whenever the URL updates without triggering a full page reload.

Single Page Applications present unique challenges for privacy-focused analytics because they dynamically update content without traditional browser refreshes. The Plausible tracker in the plausible/analytics repository solves this through a dual-path detection system that captures both hash-based routing and HTML5 History API navigation as distinct pageviews.

How Plausible Detects SPA Navigation

The tracker distinguishes between routing paradigms using compile-time flags and runtime configuration options defined in tracker/src/config.js. Depending on your application architecture, Plausible activates one of two detection mechanisms during initialization.

Hash-Based Routing Detection

When the COMPILE_HASH compilation flag is true and the runtime option hashBasedRouting is enabled, Plausible attaches a hashchange event listener inside tracker/src/autocapture.js. Every modification to the URL hash fragment (e.g., example.com/#/about changing to example.com/#/contact) triggers the internal page(true) helper function, which subsequently invokes track('pageview') to record the navigation event.

History API Navigation (pushState and popstate)

For applications utilizing the History API, Plausible patches window.history.pushState directly. The original native method is stored in a closure, then wrapped with custom logic that calls onSPANavigation() after the original pushState execution. This wrapper ensures that programmatic navigation (such as React Router's push() calls) immediately triggers page(true) and emits a tracking event. Additionally, Plausible attaches a popstate listener to capture back and forward button navigation through the browser history stack.

Core Tracking Flow

The SPA detection system operates through a coordinated initialization sequence:

  1. Initialization — When plausible.init() executes (or automatically for the default web script), it calls initAutocapture(track) from tracker/src/plausible.js to configure the appropriate listeners based on your routing mode.
  2. Page View Logic — The local page() helper checks COMPILE_HASH && config.hashBasedRouting to filter duplicate hash-only navigations. Valid SPA transitions invoke track('pageview'), which sends the event to Plausible's API via tracker/src/track.js.
  3. Visibility Handling — Beyond navigation events, Plausible listens for visibilitychange and pageshow events (checking event.persisted for bfcache restores) to ensure pageviews fire when users return to previously loaded tabs.

Configuration Examples

Enabling Hash-Based Routing

For applications using Vue Router in "hash" mode or similar hash-based frameworks, explicitly enable hash tracking during initialization:

// Configure before loading the Plausible script
window.plausible = window.plausible || [];
window.plausible.q = window.plausible.q || [];

// Initialize with hash-based routing enabled
plausible('init', { hashBasedRouting: true });

History API Usage (Automatic)

Standard SPAs using React Router, Vue Router in "history" mode, or Next.js require no special configuration:

// Standard initialization
plausible('init');

// Navigation like router.push('/dashboard') automatically triggers
// pushState patching → onSPANavigation() → pageview tracking

Manual Pageview Triggering

For edge cases or custom routing implementations, trigger pageviews programmatically:

// Force a pageview event outside the automatic detection
plausible('pageview');

Summary

  • Dual Detection: Plausible supports both hash-based routing (via hashchange listeners) and History API navigation (via pushState patching) in tracker/src/autocapture.js.
  • Automatic Instrumentation: The initAutocapture(track) function in tracker/src/plausible.js configures the appropriate listeners based on compile flags and runtime configuration.
  • Deduplication: The page() helper filters duplicate hash navigations when hashBasedRouting is active.
  • Visibility Resilience: Additional listeners for visibilitychange and pageshow ensure accurate tracking across browser back-forward cache scenarios.

Frequently Asked Questions

Do I need to configure anything for React Router or Vue Router?

No. Plausible automatically detects History API navigation used by React Router, Vue Router (history mode), and Next.js by patching window.history.pushState in tracker/src/autocapture.js. No manual configuration is required unless you use hash-based routing, in which case you must set hashBasedRouting: true during initialization.

How does Plausible handle the back button in SPAs?

The tracker attaches a popstate event listener that invokes onSPANavigation() whenever users navigate backward or forward through their history. This ensures that page(true) runs and a pageview is recorded even when the browser history state changes rather than the URL hash.

Can I disable automatic SPA tracking?

While there is no explicit "disable SPA" flag, you can prevent automatic hash-based tracking by ensuring hashBasedRouting remains false (the default). For History API applications, the pushState patching occurs automatically in the compiled script, but you can avoid triggering events by not calling the initialization function if you require completely manual control.

Why would I need to manually trigger a pageview?

Manual plausible('pageview') calls are useful for custom routing implementations that bypass standard History API methods, soft navigation within tabbed interfaces, or when tracking specific user actions as distinct page events that don't correspond to URL changes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →