Skip to content

A practical checklist for single-page app pageviews

Clicktag tracks SPA pathname changes through pushState and popstate. Check query-only changes, replaceState and hash routes separately; use manual mode when your router needs explicit pageview calls.

Understanding default SPA tracking mechanics

Client-side frameworks update the URL in the browser address bar without requesting a fresh HTML document from the server. The Clicktag script, loaded via latest.js, accommodates this workflow out of the box. When included in your root document without special flags, the script hooks window.history.pushState and attaches a listener to the popstate event.

When a visitor clicks an internal client-side link or uses browser back and forward buttons, the script intercepts the location change. The script compares the new pathname against the previously recorded pathname (or hash when running in hash mode). If the pathname differs from the current active route, the script sends a new pageview beacon. Query-only modifications do not trigger an automatic pageview. To prevent duplicate hits caused by component re-renders or internal state churn, consecutive identical pathnames are suppressed automatically.

Browser history APIs include more than pushState. Frameworks frequently rely on replaceState for updating query parameters, modal states, drawer visibility, or client-side redirects. In Clicktag, calling replaceState does not trigger an automatic pageview beacon. If your router uses replaceState to finalize page navigations, those transitions will not generate a pageview under default automatic observation.

Furthermore, the helper function window.tt_pageview is not exposed on window during automatic tracking mode. In automatic mode, the script manages route observation internally, so guarded calls to window.tt_pageview simply do not run. It is impossible to inadvertently double count by attempting to call window.tt_pageview alongside automatic mode.

The single-page app verification checklist

Before shipping client routing updates to production, verify your analytics configuration against these operational checkpoints:

  1. Mount the script once in the root document: Load latest.js in your main HTML template or root application shell. If an identical script tag accidentally executes a second time, internal guards halt duplicate initialization, but loading it multiple times remains poor practice.
  2. Understand pathname-only change detection: Automatic tracking evaluates only the pathname (or the hash string in hash mode). Changing query parameters without changing the pathname does not trigger a pageview beacon.
  3. Identify replaceState navigations: Because replaceState calls are deliberately ignored by default history hooks, audit your router configuration. If internal route redirects rely on replaceState, configure those transitions to use pushState or switch the tracking setup to manual mode.
  4. Confirm the routing structure: If your application uses hash routing (such as example.com/#/dashboard), standard path listeners do not capture the changes. Add data-mode="hash" to the script tag so the script compares the hash segment instead of the pathname.
  5. Choose manual mode when programmatic control is needed: If your router manages page lifecycle events via custom callbacks or delays route completions until data loads, disable automatic observation by setting data-auto-collect="false". Only in manual mode does Clicktag expose window.tt_pageview and detach history observation.
Routing scenario Default automated behavior Required configuration or action
Router calls pushState with new pathname Fires pageview on distinct pathname None; automatic tracking handles it
User clicks Back or Forward (popstate) Fires pageview on distinct pathname None; automatic tracking handles it
Router updates only query string No pageview fired Expected behavior; pathname has not changed
Router calls replaceState No pageview fired Switch router to pushState or use manual mode
Hash-based route change (/#/path) Ignored by default Add data-mode="hash" to the script tag
Consecutive identical pathname call Suppressed automatically None; duplicates are rejected
Framework requires manual route triggers History hooks fire unless disabled Add data-auto-collect="false" to disable history hooks and expose window.tt_pageview

Setting up manual pageview tracking

Some frontend architectures require precise programmatic control over when a pageview is recorded. For instance, you may wish to wait until dynamic route titles, permissions, or metadata are fully resolved before dispatching the beacon. In other cases, hybrid routing setups mix custom micro-frontend events with replaceState calls.

To disable automatic history observation and prevent an immediate pageview beacon on script execution, set data-auto-collect="false" on the script tag (or set autoCollect: false in window.tt_settings). Setting data-auto-collect="false" accomplishes two things: it prevents automatic history interception, and it installs the window.tt_pageview helper.

Because the script loads asynchronously, window.tt_pageview is not available immediately on initial HTML evaluation. There is no custom global readiness event; instead, verify function availability in application code or use the standard script element onload attribute for the initial page load.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Application</title>
    <script>
      function trackInitialPage() {
        if (typeof window.tt_pageview === "function") {
          window.tt_pageview(window.location.pathname);
        }
      }
    </script>
    <script
      async
      src="https://clicktag.io/latest.js"
      data-hostname="example.com"
      data-verify="YOUR_SITE_VERIFY_TOKEN"
      data-auto-collect="false"
      onload="trackInitialPage()"
    ></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Replace example.com with your registered domain and YOUR_SITE_VERIFY_TOKEN with your site verification token. For further script configuration details, review the installation documentation.

Triggering manual views in router transitions

After handling the initial pageview, use your router transition hooks to trigger subsequent views. Ensure that route callbacks run after the script has loaded, or guard every invocation by checking whether window.tt_pageview is defined.

Visitors with Do Not Track enabled (navigator.doNotTrack === "1") will not have the tracking helper registered on window. A defensive check prevents unhandled runtime errors in those browsers:

function handleRouteChange(newPath) {
  if (typeof window.tt_pageview === "function") {
    window.tt_pageview(newPath);
  }
}

In manual mode, window.tt_pageview continues to enforce duplicate suppression on consecutive identical path submissions. If a component re-renders or emits redundant transition events for the exact same path string, the collector drops the repeat hit automatically.

Custom events can also be sent alongside manual routing workflows using window.tt_event. Learn how custom event tracking operates alongside pageviews in the event tracking guide.

Managing Content Security Policy and localhost tests

When testing single-page applications locally, note that the script defaults to using location.host when data-hostname is omitted. Local addresses such as localhost:3000 cannot be registered as production sites, so the collector ignores hits sent from them. If you explicitly specify your live production data-hostname during local testing, your local browser sessions will record traffic against your live dashboard. To avoid skewing live numbers, test your integrations on a staging domain matching a registered test site.

If your SPA serves a Content Security Policy (CSP), ensure the tracking host is permitted in your security directives:

script-src 'self' https://clicktag.io;
img-src 'self' https://clicktag.io;
connect-src 'self' https://clicktag.io;

Review the troubleshooting guide if beacons fail to send or if ownership verification checks encounter errors.

Frequently asked questions

Why are my client-side route redirects missing from analytics?

If your application uses history.replaceState to forward visitors to a different route, Clicktag will not record that transition under default automatic tracking. Automatic tracking hooks pushState and popstate, leaving replaceState unobserved to prevent noise from routine state updates. If you want a redirect or route replacement recorded as a new pageview, execute pushState or disable auto collection and trigger window.tt_pageview explicitly.

Can calling window.tt_pageview create duplicate pageviews in automatic mode?

No. In automatic mode (data-auto-collect left at its default true value), the script does not expose window.tt_pageview on the global object. Any guarded calls to window.tt_pageview in your application will safely no-op, preventing double counts. The window.tt_pageview function is only defined when you explicitly set data-auto-collect="false".

What happens if window.tt_pageview is called before latest.js finishes loading?

Because the script loads asynchronously, calling window.tt_pageview before the script evaluates will throw a TypeError unless guarded. Ensure route callbacks check typeof window.tt_pageview === "function" before calling it. If a visitor enables Do Not Track (navigator.doNotTrack === "1"), the script intentionally does not define window.tt_pageview, making this guard necessary in all environments.

Does changing URL query parameters trigger an automatic pageview?

No. The script compares the active pathname (or hash when configured with data-mode="hash"). Updating query parameters alone without altering the pathname does not trigger a pageview beacon in automatic mode. If you need query parameter updates recorded as pageviews, use manual mode with data-auto-collect="false" and invoke window.tt_pageview explicitly.

Last updated