Skip to content

SPAs & client-side routing

Single-page apps work out of the box: the snippet patches history.pushState/replaceState and listens for popstate, so every client-side navigation fires a page_view with no extra configuration. Only hash-based routers need to opt in.

React Router, TanStack Router, Vue Router (history mode), Next.js, and anything else that uses the browser’s real history API work out of the box. Every navigation fires a page_view for the new pathname + search. Consecutive calls to the same path are deduped, so a replaceState that doesn’t actually change the path won’t double-count.

<script src="https://cdn.analytics.canarycoders.es/lytics.js" data-domain="myapp.com" defer></script>

Add the data-hash attribute to opt in:

<script src="https://cdn.analytics.canarycoders.es/lytics.js" data-domain="myapp.com" data-hash defer></script>

With data-hash present, two things change:

  1. The tracked path becomes pathname + search + hash, so /#/about is reported distinctly from /#/pricing.
  2. A hashchange listener fires a page_view on every hash navigation, in addition to the popstate/patched-history listeners.

Without data-hash, hash changes are invisible to CanaryLytics, and the app looks like a single-page site with no route breakdown.

Plain left-clicks on same-origin <a> elements are tracked as link_click events (target path only). Modified clicks (Cmd/Ctrl/Shift/Alt-click, opening a new tab), #/mailto:/tel:/javascript: links, and external links are all deliberately left untracked.