---
title: "SPAs & client-side routing"
description: "CanaryLytics tracks single-page app navigations automatically by patching the history API. Hash-based routers opt in with the data-hash attribute."
last_updated: "2026-07-30"
---

# 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.

## History-based routers

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.

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

## Hash-router SPAs

<Aside type="caution" title="Opt-in required">
  Hash-based routing (`/#/about`, common in older Vue Router / Angular / Reach Router setups) is **not tracked by default**. `pathname` doesn't change on a hash navigation, so without extra configuration every route inside the app looks like the same page.
</Aside>

Add the `data-hash` attribute to opt in:

```html
<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.

## Link clicks

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

## Related

<CardGrid>
  <LinkCard title="Framework integrations" description="Where the tag goes in Vite, Next.js, Astro, and TanStack Start." href="/docs/environments/framework-integrations/" />
  <LinkCard title="Script reference" description="How SPA patching and dedup work under the hood." href="/docs/tracking/script-reference/" />
</CardGrid>

## Sitemap

- [Docs sitemap](https://analytics.canarycoders.es/docs/sitemap.md)
- [Full documentation as a single file](https://analytics.canarycoders.es/llms-full.txt)
