Single-page apps
A single-page app uses the same tag as any other site, with nothing more to set up.
The tag counts each page your app moves to as a pageview of its own, whichever router it uses, so its pages rank and filter like any other site’s. If your paths hold account names or record numbers, one attribute keeps them out of your stats.
What counts as a pageview
- The page the browser loads.
- Each move to a new address with
history.pushState, which is how the routers of React, Vue, Svelte, Angular and the rest change pages. - Each press of the back or forward button that changes the address.
- A page the back or forward button restores from the browser’s back/forward cache without loading it.
A move to the address already counted isn’t counted again, and nor is a change after the #
alone. history.replaceState doesn’t count either, since it only renames the page the visitor is
already on.
Pages are counted by path, so a move that changes only the query string, such as to ?page=2,
is another view of the same page.
A page the browser prerenders in case the visitor goes to it next is counted when they do, and not at all if they don’t.
Private paths
If your paths hold an account’s name or a record’s number, such as /acme/invoices/4815, count
them without it using data-path. One ending in
/* fits every route that starts the same way, and follows the app as it moves:
data-path="/:account/*" counts /acme/invoices as /:account/invoices.
For routes of other shapes, update the attribute before the app changes the address, with
tag.setAttribute('data-path', '/:account/invoices/:id'): the script reads it for every page. The
back and forward buttons are counted as the address changes, before your router runs, so a single
/* pattern that fits every route is the most reliable.
Apps that route after #
An app whose addresses differ only after the #, such as /#/about, is counted as one
page, /. To count its pages separately, switch the router to real paths, which most routers call
history mode.
Sources
A page the app moves to is reached from the site itself, like the second page of any other site, so it has no source of its own, not even Direct. The visit keeps the source it arrived with, so for traffic from a source, read visits rather than pageviews.
Before 24 September 2026, each page an app moved to took the referrer of the page it landed on, so pageviews from a source include an app’s later pages before then.
Speed and engagement
Each page the app moves to gets its own time on page and scroll depth, sent when the app moves on. Page speed is measured once, for the page the browser loaded, since a move within the app loads nothing to time.
Site search
A search counts with the pageview it was made on, so a search box that rewrites the address with
history.replaceState as the visitor types counts none, and one that pushes a new address as they type
counts a search for each. Where the address doesn’t hold the search, set data-search on the tag before
the app moves to the results. See Site search.
Error pages
Only a page the browser loaded has an HTTP status, so a not found page the app draws itself is counted as an ordinary page. See Error pages.
Custom properties
The script reads its tag once, as it loads, so every page the app moves to carries the custom properties of the page it loaded on.