Single-page applications
SPAs load one HTML page and swap views in JavaScript, so “pageviews” need a little care. BobRay handles the common cases automatically and gives you one function for everything else.
- Key takeaways
- History-API route changes count automatically, React/Vue/Next work with zero code.
- data-spa="off" plus window.bobray('pageview') gives you full manual control.
- Hash routers need data-hash="on" to count #/route changes.
Automatic mode (default)
The snippet listens to the History API, pushState, replaceState, and popstate, and records a pageview on every URL change. For React Router, Vue Router, SvelteKit, and Next.js App Router this simply works with no extra code.
Manual mode
Set data-spa="off" on the snippet and call the tracker yourself wherever your router confirms a navigation:
window.bobray('pageview')Manual mode is the right choice when you have redirects or guards that briefly touch URLs you do not want counted.
Hash routing
Apps routed as example.com/#/settings should add data-hash="on"; hash changes then count as pageviews with the hash path recorded.
Gotchas worth knowing
- Query-string-only changes are ignored by default so filter tweaking does not inflate views; count them manually if you need to.
- Prerendered or SSR first loads count once, hydration does not double-fire.
- In development, calls from
localhostare dropped, see Localhost and staging.
Common pitfalls
The mistakes we see most often on this topic, so you can skip them entirely.
- Counting redirect bounces: guards that touch intermediate URLs fire views in auto mode, go manual there.
- Firing a manual pageview on hydration as well, first loads already count once.
- Expecting query-string-only changes to count, they're ignored by design.
Quick reference
| Auto mode | pushState/replaceState/popstate |
| Manual call | window.bobray('pageview') |
| Hash apps | data-hash="on" |
| Query-only change | Not counted by default |