> ## 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.

# SPA & Framework Setup

> Install the Scanova Browser SDK in React, Next.js, Vue, and other single-page application frameworks. Avoid double-init and capture route-change page views correctly.

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:

```html theme={null}
<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:

```jsx theme={null}
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`:**

```tsx theme={null}
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`:**

```tsx theme={null}
'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`:**

```tsx theme={null}
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`**:

```tsx theme={null}
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:

```html theme={null}
<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`:**

```typescript theme={null}
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                   |
