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.
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).popstate event: 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.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.
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.
In Next.js 13+ with the App Router, add the script to your root layout. For the Pages Router, use _document.js.
For a standard Vue app (Vite), add the script to index.html. For Nuxt 3, use nuxt.config.ts.
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>
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>
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>
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.
The engagement module tracks scroll depth and active time continuously throughout the session. Here is how it behaves during SPA navigations:
visibilitychange), when the visitor leaves the page entirely (via pagehide and beforeunload), or incrementally when duration changes by at least 3 seconds or the scroll maximum increases.For the full details on how scroll depth, duration, and engagement scores are calculated, see Engagement.
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.
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.
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.
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.
If some navigations are not being tracked:
/#/page, you have hash routing./api/event requests. Each SPA navigation should produce a new POST request with t: "v" in the body.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.
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.