Track single-page apps (SPAs)
How the tracker follows single-page app navigation with the History API or hash routing, what counts as a new page, and how to avoid counting pageviews twice.
Most single-page apps (React Router, Next.js, Vue Router, SvelteKit,
Angular, Astro view transitions…) change pages with history.pushState.
The tracker handles them with no configuration.
Modes (data-spa)#
| Value | Behavior |
|---|---|
auto (default) |
Wraps history.pushState and listens to popstate (back/forward) and hashchange. Each navigation to a new path sends a pageview. Route-like fragments (starting with #/ or #!/) are part of the path, so hash routers work too. Plain anchors (#pricing) are dropped and aren't pageviews |
history |
Wraps history.pushState and listens to popstate only. The fragment is always dropped |
hash |
Listens to hashchange only (history.pushState isn't followed) and keeps every #fragment in the path (/#/settings), plain anchors included |
off |
Only the initial page load is tracked. Use the JavaScript API for the rest |
history.replaceState is never tracked (routers use it for things like
updating query strings). data-hash="true"
keeps every fragment and follows hashchange on top of auto or
history.
What counts as a new page#
A pageview is sent when the path changes: the pathname, plus the
fragment when the mode keeps it. Changing only the query string (?tab=2) doesn't
send one, and neither does navigating to the same path again. Call
privatus.pageview() to force one.
For each SPA navigation:
- The engagement time and scroll depth of the page you're leaving are
sent (with the
engagemodule). - The referrer of the new pageview is a URL on your own site, so internal navigation never shows up as a new traffic source. The visit keeps the source it started with.
Double counting#
If you also call privatus.pageview() in your router's navigation hook,
or fire the tracker again from a tag manager on history changes, each
navigation is counted twice. Pick one: the built-in SPA tracking, or
manual mode with data-spa="off". See
SPA double counting.