Skip to main content

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

  1. Load the SDK snippet once — in your root HTML file or root component, not in every page/route component
  2. Call scanova('init', ...) once — in your app’s entry point
  3. Disable autoPageview — manage page view events yourself on route changes
  4. 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

MistakeResultFix
Calling scanova('init', ...) in every page/route componentMultiple init calls → duplicate auto-tracking listeners → doubled eventsCall init once in the app root only
Using autoPageview: true in a SPAOnly fires on first load, misses all navigationsDisable it, track manually on route change
Adding the SDK script to every page componentSDK loads multiple timesLoad from root HTML or root layout once
Not waiting for SDK to load before calling trackSilent failuresUse window.scanova?.() optional chaining, or the queue pattern (calls before SDK loads are queued automatically)
Using autoScroll: true and expecting per-route scroll milestonesMilestones are not reset on route change — a user who scrolled 75% on /home will not re-trigger those milestones on /pricingDisable autoScroll in SPAs and send scroll events manually if per-route scroll depth matters