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.
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:
history.pushState: Called by frameworks when navigating to a new route (e.g., clicking a link in React Router or Vue Router).history.replaceState: Called when the current history entry is replaced (e.g., redirects, query parameter updates).popstateevent: Fired when the user clicks the browser's back or forward buttons.
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.
history.pushState() to update the URLsessionStorage) 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.
Next.js
In Next.js 13+ with the App Router, add the script to your root layout. For the Pages Router, use _document.js.
Vue / Nuxt
For a standard Vue app (Vite), add the script to index.html. For Nuxt 3, use nuxt.config.ts.
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>
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.
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:
- Scroll depth: The tracker records the maximum scroll position reached on the page. When the visitor navigates to a new SPA route, the page typically scrolls back to the top, and the tracker continues recording the new maximum from there.
- Active time: The visibility-aware timer runs continuously. It pauses when the tab is hidden or blurred, and resumes when the tab regains focus. Time accumulates across the entire session.
- Engagement updates: The tracker sends engagement data when the tab is hidden (via
visibilitychange), when the visitor leaves the page entirely (viapagehideandbeforeunload), or incrementally when duration changes by at least 3 seconds or the scroll maximum increases. - Page visit linking: Each engagement update includes the current page visit ID, so the server can associate engagement metrics with the correct page.
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:
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.
# 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', {});
}
});
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:
- Verify your router uses the History API, not hash-based routing. Check your browser's address bar: if routes look like
/#/page, you have hash routing. - Open the browser's Network tab and filter for
/api/eventrequests. Each SPA navigation should produce a new POST request witht: "v"in the body. - Make sure the tracker script loaded successfully. Check the Console tab for any 404 or loading errors.
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.
localhost. To test SPA tracking during development, set window.__CP_TEST__ = true before the tracker loads. See Script Configuration for details.