Documentation Index
Fetch the complete documentation index at: https://docs.scanova.io/llms.txt
Use this file to discover all available pages before exploring further.
Single-page applications (SPAs) have a different page lifecycle than traditional multi-page sites. The browser does not fully reload on navigation, so the standard autoPageview will only fire once — on the initial load. This guide shows how to handle tracking correctly in React, Next.js, and Vue.
The key difference with SPAs
In a traditional website, every page navigation triggers a full browser reload, which re-runs the SDK and fires a pageview. In a SPA:
- The page loads once
- Subsequent navigations are JavaScript-driven (the URL changes, but the page does not reload)
autoPageview fires only on that first load
- You must manually fire a
pageview event on each route change
General rules for all SPAs
- Load the SDK snippet once — in your root HTML file or root component, not in every page/route component
- Call
scanova('init', ...) once — in your app’s entry point
- Disable
autoPageview — manage page view events yourself on route changes
- Fire
scanova('track', 'pageview') on each route change — using your router’s navigation event or hook
React (Create React App / Vite)
public/index.html — load the SDK:
<head>
<script>
(function(w,d,s,o,f,js,fjs){
w['ScanovaTrackingObject']=o;w[o]=w[o]||function(){(w[o].q=w[o].q||[]).push(arguments)};
js=d.createElement(s),fjs=d.getElementsByTagName(s)[0];
js.id=o;js.src=f;js.async=1;fjs.parentNode.insertBefore(js,fjs);
})(window,document,'script','scanova','https://cdn.scanova.io/ct/js/qcg.min.js');
</script>
</head>
src/main.jsx or src/App.jsx — init once and track route changes:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
// Init once at app boot (outside any component)
window.scanova?.('init', 'YOUR_SITE_ID', {
autoPageview: false, // manage manually
autoClicks: true,
autoForms: true,
autoScroll: true,
});
// Hook to track route changes
function usePageTracking() {
const location = useLocation();
useEffect(() => {
window.scanova?.('track', 'pageview', {
path: location.pathname,
});
}, [location.pathname]);
}
export default function App() {
usePageTracking();
return <RouterOutlet />;
}
Next.js (App Router)
In Next.js App Router, use a client component in your root layout.
app/layout.tsx:
import ScanovaTracker from '@/components/ScanovaTracker';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,o,f,js,fjs){
w['ScanovaTrackingObject']=o;w[o]=w[o]||function(){(w[o].q=w[o].q||[]).push(arguments)};
js=d.createElement(s),fjs=d.getElementsByTagName(s)[0];
js.id=o;js.src=f;js.async=1;fjs.parentNode.insertBefore(js,fjs);
})(window,document,'script','scanova','https://cdn.scanova.io/ct/js/qcg.min.js');
scanova('init', 'YOUR_SITE_ID', { autoPageview: false, autoClicks: true, autoForms: true, autoScroll: true });
`,
}}
/>
</head>
<body>
<ScanovaTracker />
{children}
</body>
</html>
);
}
components/ScanovaTracker.tsx:
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
export default function ScanovaTracker() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
window.scanova?.('track', 'pageview', {
path: pathname,
});
}, [pathname, searchParams]);
return null;
}
Next.js (Pages Router)
pages/_app.tsx:
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
useEffect(() => {
// Init once
(window as any).scanova?.('init', 'YOUR_SITE_ID', {
autoPageview: false,
autoClicks: true,
autoForms: true,
autoScroll: true,
});
// Track initial page
(window as any).scanova?.('track', 'pageview', { path: router.pathname });
// Track subsequent navigations
const handleRouteChange = (url: string) => {
(window as any).scanova?.('track', 'pageview', { path: url });
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => router.events.off('routeChangeComplete', handleRouteChange);
}, []);
return <Component {...pageProps} />;
}
Add the SDK loader to pages/_document.tsx:
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
<script dangerouslySetInnerHTML={{ __html: `
(function(w,d,s,o,f,js,fjs){
w['ScanovaTrackingObject']=o;w[o]=w[o]||function(){(w[o].q=w[o].q||[]).push(arguments)};
js=d.createElement(s),fjs=d.getElementsByTagName(s)[0];
js.id=o;js.src=f;js.async=1;fjs.parentNode.insertBefore(js,fjs);
})(window,document,'script','scanova','https://cdn.scanova.io/ct/js/qcg.min.js');
`}} />
</Head>
<body><Main /><NextScript /></body>
</Html>
);
}
Vue 3 (Vue Router)
index.html — load the SDK:
<head>
<script>
(function(w,d,s,o,f,js,fjs){
w['ScanovaTrackingObject']=o;w[o]=w[o]||function(){(w[o].q=w[o].q||[]).push(arguments)};
js=d.createElement(s),fjs=d.getElementsByTagName(s)[0];
js.id=o;js.src=f;js.async=1;fjs.parentNode.insertBefore(js,fjs);
})(window,document,'script','scanova','https://cdn.scanova.io/ct/js/qcg.min.js');
</script>
</head>
src/main.ts:
import { createApp } from 'vue';
import { createRouter } from 'vue-router';
import App from './App.vue';
const router = createRouter({ ... });
// Init once
window.scanova?.('init', 'YOUR_SITE_ID', {
autoPageview: false,
autoClicks: true,
autoForms: true,
autoScroll: true,
});
// Track route changes
router.afterEach((to) => {
window.scanova?.('track', 'pageview', { path: to.path });
});
createApp(App).use(router).mount('#app');
Common mistakes
| Mistake | Result | Fix |
|---|
Calling scanova('init', ...) in every page/route component | Multiple init calls → duplicate auto-tracking listeners → doubled events | Call init once in the app root only |
Using autoPageview: true in a SPA | Only fires on first load, misses all navigations | Disable it, track manually on route change |
| Adding the SDK script to every page component | SDK loads multiple times | Load from root HTML or root layout once |
Not waiting for SDK to load before calling track | Silent failures | Use window.scanova?.() optional chaining, or the queue pattern (calls before SDK loads are queued automatically) |
Using autoScroll: true and expecting per-route scroll milestones | Milestones are not reset on route change — a user who scrolled 75% on /home will not re-trigger those milestones on /pricing | Disable autoScroll in SPAs and send scroll events manually if per-route scroll depth matters |