Clickport
Start free trial

SPA Tracking

Clickport automatically tracks page navigations in single page applications. If your site is built with React, Vue, Next.js, Nuxt, Angular, Svelte, or any other SPA framework, pageviews are recorded on every route change without any extra configuration.

Zero configuration required. Just add the standard Clickport script tag to your app. The tracker detects SPA navigations automatically. There are no special SPA modes, plugins, or router integrations to set up.

How it works

Modern SPA frameworks use the browser's History API to change the URL without triggering a full page reload. Clickport intercepts these changes at the browser level by wrapping two History API methods and listening for navigation events:

The tracker wraps pushState and replaceState with thin wrappers that call the original method, then check if the URL has changed. If it has, a new pageview event is sent to Clickport. The popstate listener handles back/forward button navigation the same way.

How SPA tracking works
What happens when a visitor navigates between routes in your SPA
1
Visitor clicks a link
Your framework's router calls history.pushState() to update the URL
2
Clickport intercepts the call
The wrapped pushState runs the original method, then checks if the URL changed
3
Engagement data is linked to the page visit
Scroll depth and active time for the previous page are associated with its page visit ID
4
New pageview is recorded
A pageview event is sent with the new URL. The server assigns a new page visit ID.
5
Session continues
The session ID (stored in sessionStorage) persists across navigations. All pages belong to one session.

URL change detection

The tracker keeps a reference to the last known URL. On every pushState, replaceState, or popstate event, it compares the current location.href to this reference. A new pageview is only sent when the full URL actually changes. This prevents duplicate pageviews when a framework calls replaceState without changing the URL.

Framework installation guides

The installation is the same for every SPA framework: add the Clickport script tag once in your app's HTML entry point. The tracker handles everything else.

React / Vite / Create React App
<!-- public/index.html (CRA) or index.html (Vite) --> <head> <meta charset="UTF-8"> <title>My App</title> <!-- Clickport Analytics --> <script defer data-domain="yoursite.com" src="https://clickport.io/tracker.js"></script> </head>

Next.js

In Next.js 13+ with the App Router, add the script to your root layout. For the Pages Router, use _document.js.

Next.js App Router (app/layout.js)
import Script from 'next/script' export default function RootLayout({ children }) { return ( <html> <head> <Script defer data-domain="yoursite.com" src="https://clickport.io/tracker.js" strategy="afterInteractive" /> </head> <body>{children}</body> </html> ) }

Vue / Nuxt

For a standard Vue app (Vite), add the script to index.html. For Nuxt 3, use nuxt.config.ts.

Nuxt 3 (nuxt.config.ts)
export default defineNuxtConfig({ app: { head: { script: [{ src: 'https://clickport.io/tracker.js', defer: true, 'data-domain': 'yoursite.com' }] } } })

Angular

Add the script tag to src/index.html inside the <head> section. Angular's router uses pushState by default, so SPA tracking works automatically.

<!-- src/index.html -->
<head>
  <!-- Clickport Analytics -->
  <script defer data-domain="yoursite.com"
    src="https://clickport.io/tracker.js"></script>
</head>

Svelte / SvelteKit

For SvelteKit, add the script to src/app.html. For a plain Svelte app, use index.html.

<!-- src/app.html -->
<head>
  <!-- Clickport Analytics -->
  <script defer data-domain="yoursite.com"
    src="https://clickport.io/tracker.js"></script>

  %sveltekit.head%
</head>

Astro

Add the script in your base layout component, typically src/layouts/Layout.astro.

<!-- src/layouts/Layout.astro -->
<head>
  <!-- Clickport Analytics -->
  <script defer data-domain="yoursite.com"
    src="https://clickport.io/tracker.js"></script>
</head>
Astro note. Astro renders pages as static HTML by default (MPA mode). SPA tracking only applies to interactive islands or if you have enabled client-side routing via View Transitions. For standard Astro sites, the tracker's regular pageview tracking handles each page load.

Session continuity

Clickport maintains a single session across all SPA navigations. The session ID is stored in sessionStorage, which persists as long as the browser tab stays open. When a visitor navigates from /home to /pricing to /signup within your SPA, all three pageviews belong to the same session.

Each pageview within the session receives its own unique page visit ID. This ID is used to link engagement data (scroll depth, active time) to the correct page.

Pages Sessions Goals
Session #48291 3 pages 4:32 duration Eng 74%
Chrome / macOS / Berlin, Germany
14:02
/home
Entry page. Referrer: google.com
14:03
/home engagement
Scroll: 68% / Active time: 48s
14:03
/pricing
SPA navigation (pushState)
14:05
CTA Click
Custom event: { button: 'start-trial' }
14:05
/signup
SPA navigation (pushState)
14:06
/signup engagement
Scroll: 92% / Active time: 4:32

In this example, the visitor navigated through three pages in one session. Each SPA route change was automatically detected and recorded as a separate pageview. Custom events and engagement data are all part of the same session.

Engagement in SPAs

The engagement module tracks scroll depth and active time continuously throughout the session. Here is how it behaves during SPA navigations:

For the full details on how scroll depth, duration, and engagement scores are calculated, see Engagement.

Custom events in SPAs

The window.clickport.track() API works exactly the same in SPAs as on traditional websites. Call it from any component, event handler, or lifecycle hook:

Custom events in React
function PricingPage() { function handleUpgrade(plan) { // Track the event window.clickport.track('Plan Selected', { plan: plan.name, interval: 'monthly' }, { amount: plan.price, currency: 'USD' }); } return <button onClick={() => handleUpgrade(selectedPlan)}>Upgrade</button> }

Use window.clickport.track() (with the window. prefix) since the tracker attaches to the global window object, not as a module import. For full details on event names, properties, and revenue tracking, see Custom Events.

Hash-based routing

Some older SPA frameworks use hash-based routing (e.g., /#/about, /#/contact) instead of the History API. Hash changes do not trigger pushState or replaceState, so Clickport does not automatically track them as separate pageviews.

Hash routing is not tracked automatically. If your app uses hash-based URLs (with # in the path), route changes will not be detected. Most modern frameworks use the History API by default. Check your router configuration if you are unsure.

If you are using hash routing, you can send pageviews manually by listening for hash changes and calling the tracker's send function:

window.addEventListener('hashchange', function() {
  if (window.clickport) {
    window.clickport.send('v', {});
  }
});
Consider switching to History routing. All major frameworks support History API routing out of the box. React Router, Vue Router, Angular Router, and SvelteKit all use pushState by default. Hash routing is generally only needed for static file hosting without server-side URL rewriting.

UTM parameters

The tracker reads UTM parameters (utm_source, utm_medium, utm_campaign, utm_content, utm_term) from the URL query string on every pageview. In an SPA, UTM parameters are typically only present on the landing URL. The initial pageview captures them and associates them with the session.

If your SPA preserves UTM parameters across route changes (some do, some strip them), the tracker will include them on subsequent pageview events as well. In most cases, this is handled correctly without any intervention.

Troubleshooting

Duplicate pageviews

If you see double pageviews for a single navigation, check that the Clickport script is only included once. In frameworks like Next.js, avoid placing the script in both _document.js and _app.js. Also verify that you are not calling a manual pageview alongside the automatic tracking.

Missing pageviews on route change

If some navigations are not being tracked:

Pageview on initial load plus route change

This is expected behavior. The tracker sends one pageview when the script first loads (the initial page), and then one for each subsequent SPA navigation. If you see an initial pageview for / followed by a navigation to /dashboard, that means the visitor landed on the root URL and your app redirected them client-side.

Server-side rendering (SSR)

Frameworks like Next.js and Nuxt can render pages on the server before hydrating on the client. The Clickport tracker only runs in the browser, so there is no conflict with SSR. The initial pageview is sent when the script loads on the client side after hydration. Subsequent navigations are handled by the client-side router as normal SPA transitions.

Testing locally. The tracker auto-disables on localhost. To test SPA tracking during development, set window.__CP_TEST__ = true before the tracker loads. See Script Configuration for details.