PilotFoxDocs

Single-page apps and routing

How the script counts pageviews in single-page apps: History API navigation, hash routing, debouncing and manual pageviews.

In a single-page app the page loads once and the router changes the address. With Single-page app navigation on (the default, Site settings → Tracking code), the script counts those route changes as pageviews: you add the tag once, in the root layout or document template, and write no tracking code for navigation.

History API navigation

The script counts a pageview when the address changes through:

  • history.pushState and history.replaceState (the script wraps both);
  • the back and forward buttons (popstate);
  • the Navigation API's navigatesuccess, in browsers that have it, for routers that intercept navigations.

It then waits 300 ms before sending the pageview, so a burst of changes counts once, for the address the burst ends on (a redirect from /account to /account/profile, for example). It never counts the same address twice in a row: a replaceState that keeps the address, or a change that comes back to it within the delay, sends nothing. Going back to an earlier page is a new pageview.

Change, with the default 300 msPageviews sent
pushState("/a"), then pushState("/b") 10 ms laterOne, for /b
replaceState to the address already recordedNone
pushState("/c?tab=2#section")One, for /c?tab=2 (the fragment is never sent)
Back button from /c to /bOne, for /b

Each pageview after the first carries the previous page as its referrer, and the page title of the moment it is sent: set document.title within the delay for the right title to be recorded.

Set data-debounce on the tag to change the delay, in milliseconds (0 removes it; changes made in the same task, such as two pushState calls in a row, still count once):

<script defer src="https://pilotfox.app/api/s.js" data-site="SITE_ID" data-debounce="500"></script>

Frameworks

Routers that change the address with the History API need nothing more than the tag. For example, the Next.js App Router: with the tag in app/layout.tsx (see Install the script), every <Link> navigation is a pageview.

To check any other router, open the browser's Network tab, navigate within your app and look for a POST to /api/e about 300 ms after each route change. If there is none, your router does not use the History API: see Hash routing or Manual pageviews.

Hash routing

Apps that route with the fragment (https://example.com/#/settings) need Hash routing turned on in Site settings → Tracking code. The script then reads #/path and #!/path as the page's path, with the route's own query string:

AddressRecorded as
https://example.com/#/starthttps://example.com/start
https://example.com/#!/settings/profile?tab=2https://example.com/settings/profile?tab=2
https://example.com/app#sectionhttps://example.com/app (not a route: the real path)

With hash routing on, the script also listens for hashchange, so a route change through location.hash counts. A change to a plain anchor (#section) counts as a pageview of the real path if the last pageview was of another route.

Keep Single-page app navigation on as well: it is what counts route changes. With it off, hash routing only shapes the first pageview, manual pageviews and the page address of events.

Manual pageviews

To send pageviews yourself, turn Single-page app navigation off and call pilotfox.pageview() after each route change:

// After your router has changed the address and the title:
window.pilotfox?.pageview();

// Or record another address than the current one (relative to the current page, or absolute):
window.pilotfox?.pageview("/checkout/step-2");
  • A manual call is sent at once, without the delay, and is sent even when the address has not changed.
  • data-skip, data-mask, hash routing and the query string setting apply as for automatic pageviews.
  • Turn Initial pageview off too if your code also sends the first pageview of each load.

Other details

  • Prerendering: a page the browser prerenders (Speculation Rules) sends its first pageview only when it is shown, and not at all if it never is.
  • Web vitals (when on): one vital event per page view. Where the browser reports soft navigations (route changes), each route's measurements are their own event. The script keeps the measurements of the latest four navigations at most: when a fifth one starts, it sends what it has for the oldest. Each navigation is sent once; later reports for it, after a restore from the back-forward cache for example, are dropped.
  • Session replay follows route changes too: a route that data-skip or the site's exclusions cover stops the recording, and it starts again on the next allowed route. See Session replay.

On this page