How do you install a visitor ID tag in a JavaScript app?
Put the tag once, in the file every page shares: the root layout, index.html or the framework's page template (the table below names the file). That part takes minutes. The part that breaks is what happens next. These frameworks change pages in the browser without loading a new document, so the tag loads once and a tag that only records on page load sees the landing page and nothing after it.
That matters more for visitor identification than for most analytics. The page that makes an account worth a sales look is usually the second or third one: pricing, a comparison page, the demo form. If the tag misses it, the account shows up as a blog reader. So after installing, check whether your vendor's tag records client-side page changes. If it does not, call its page-view function from the router hook in the last column.
Where the tag goes and which hook catches later pages
Hooks are the framework's own documented way to run code after each navigation. Your vendor's docs tell you which function to call inside it (often a page or track call); do not invent one.
| Framework | Where the tag goes | Changes pages without a reload? | Hook for later page views |
|---|---|---|---|
| Next.js App Router | <Script> from next/script in app/layout.tsx | Yes, with <Link> | A client component that watches usePathname() and useSearchParams() |
| Next.js Pages Router | <Script> in pages/_app.js (beforeInteractive only in pages/_document.js) | Yes, with <Link> | router.events routeChangeComplete |
| React (Vite) | index.html at the project root | Yes, if you use a client router such as React Router | An effect on the router's location |
| Vue | index.html (create-vue projects use Vite) | Yes, with Vue Router | router.afterEach() |
| Angular | The scripts array in angular.json, or index.html | Yes, with the Angular Router | Router events, filtered to NavigationEnd |
| SvelteKit | src/app.html | Yes, client-side routing is the default | afterNavigate from $app/navigation |
| Nuxt | app.head.script in nuxt.config.ts | Yes, with <NuxtLink> | useRouter().afterEach() |
| React Router v7 (formerly Remix) | A plain <script> in the Layout in app/root.tsx | Yes | An effect on useLocation() in the root |
| Gatsby | <Script> from gatsby in your layout | Yes, with Gatsby <Link> | onRouteUpdate in gatsby-browser.js |
| Astro | <script is:inline> in your base layout | Only if you turned on view transitions (<ClientRouter />) | The astro:page-load event, or data-astro-rerun |
How do you test whether later page views are recorded?
Use a production build, not the dev server: dev servers add hot-reload code and some frameworks render differently in development. Then run this five-minute test.
- Open the site in a private window with the browser's developer tools on the Network panel, and filter by your vendor's domain.
- Load the home page. You should see the tag's script load and at least one request that records the visit.
- Click an internal link to your pricing page. Do not type the URL, because typing it forces a full page load and hides the problem.
- Look for a new recording request that carries the pricing page URL. If none appears, the tag is only seeing landing pages. Add the hook from the table, rebuild and repeat.
- Click the browser's back button and check once more: back and forward navigation goes through the same router.
What if the app sends a Content Security Policy?
A Content Security Policy (CSP) that does not name the vendor's hosts blocks the tag without any visible error on the page; the only trace is a violation message in the browser console. Two directives matter. script-src must allow the host the script loads from. connect-src must allow the host it sends data to, because it covers fetch(), XMLHttpRequest and navigator.sendBeacon(), which is how tags usually report. Ask the vendor for both host lists; they are often different.
Built-in support differs. Next.js generates a nonce in its proxy (formerly middleware) and applies it to <Script>, but nonce-based CSP needs dynamic rendering, so static optimization is off for those pages. SvelteKit has kit.csp in svelte.config.js and a %sveltekit.nonce% placeholder for scripts you add to app.html. Angular's ngCspNonce covers Angular's own inline styles, not third-party scripts. Astro's CSP support is still behind an experimental flag and does not support <ClientRouter /> view transitions. We found no built-in CSP feature in Nuxt's core docs or for a plain Vite app; there you set the header on your host or server.
How do you add the tag on Next.js App Router?
Where it goes: Import Script from next/script and render it in the root layout, app/layout.tsx. The default strategy is afterInteractive, which loads the script after some hydration; that is the right choice for a visitor identification tag.
What breaks it on Next.js App Router
- Next.js loads a
<Script>only once, even when the visitor moves between pages. That is by design, and it is why the tag needs the navigation hook: a client component that runs an effect whenusePathname()oruseSearchParams()changes. Render it inside a<Suspense>boundary in the layout, as the Next.js docs show, becauseuseSearchParams()opts out of prerendering up to the nearest boundary. - The
workerstrategy (Partytown) does not work with the App Router. Do not use it for this tag. afterInteractivescripts are injected on the client, so they do not appear in view-source. Look in the Elements panel instead.
How to check it on Next.js App Router
Build and start the app (next build && next start), then run the navigation test above: load /, click through to pricing with a <Link>, and check that the vendor records the second page.
Sources: Next.js: Scripts (App Router) (read 2026-09-25); Next.js: Script component (read 2026-09-25); Next.js: useRouter, router events (read 2026-09-25); Next.js: Content Security Policy (read 2026-09-25).
How do you add the tag on Next.js Pages Router?
Where it goes: Render <Script> from next/script in pages/_app.js so it is on every page. Only a beforeInteractive script has to go in pages/_document.js, and a visitor ID tag does not need that strategy.
What breaks it on Next.js Pages Router
- As in the App Router, the script loads once across navigations. Record later pages from
router.eventsonrouteChangeCompletein_app.js, and remove the listener on unmount. - The experimental
workerstrategy only works in thepages/directory. Keep this tag on the main thread anyway, so it runs in the same page context as the router events.
How to check it on Next.js Pages Router
In a production build, click between two pages and confirm the vendor records each one. The script itself should not be requested again.
Sources: Next.js: Script component (Pages Router) (read 2026-09-25); Next.js: useRouter (Pages Router) (read 2026-09-25).
How do you add the tag on React with Vite?
Where it goes: Paste the tag into index.html at the project root. Vite treats that file as the app's entry point, and the tag ships in every build. Create React App was deprecated for new apps in February 2025; if you still run it, the file is public/index.html and the rest of this section applies.
What breaks it on React with Vite
- Vite has no router, so page changes depend on the router you added. With React Router, a click on a
<Link>updates the URL with the History API and loads no new document. Record later pages from an effect that depends onuseLocation(). - There is no framework CSP. If the app is served with a CSP header, set
script-srcandconnect-srcwherever the app is hosted.
How to check it on React with Vite
Run npm run build and serve the dist folder with npm run preview, then run the navigation test above.
Sources: Vite: Getting started (index.html as entry) (read 2026-09-25); React blog: Sunsetting Create React App (read 2026-09-25).
How do you add the tag on Vue?
Where it goes: Projects made with npm create vue@latest are Vite projects, so the tag goes in index.html at the project root.
What breaks it on Vue
- Vue Router changes pages without a reload. Its global after hook,
router.afterEach(), runs after each navigation, and Vue Router's docs name analytics as a use for it. Call the vendor's page-view function there. - If the site loads Vue itself from a CDN with no build step, there is no
index.htmlpipeline to rely on: put the tag in each HTML page that loads Vue.
How to check it on Vue
Build and preview, click through with <router-link>, and confirm that one recording request is sent for each route change.
Sources: Vue: Quick start (read 2026-09-25); Vue Router: Navigation guards (global after hooks) (read 2026-09-25).
How do you add the tag on Angular?
Where it goes: Add the tag directly to src/index.html. The alternative, listing a local copy in the scripts array in angular.json, loads it "exactly as if you had added them in a <script> tag inside index.html", but it only takes files in your project, not a vendor URL.
What breaks it on Angular
- Angular Router links move between routes "without triggering a full page reload". Subscribe to router events, filter for
NavigationEnd, and record the page there. Angular's routing guide uses page-view tracking as its own example of this pattern. - Do not load the same library through both the
scriptsarray and animportstatement: Angular's workspace docs warn this leaves you with two copies. ngCspNonceand theCSP_NONCEtoken cover Angular's own inline styles. A CSP still has to list the vendor's hosts inscript-srcandconnect-src.
How to check it on Angular
Run ng build, serve the output, and click between routes with routerLink while you watch the Network panel.
Sources: Angular: Workspace configuration (read 2026-09-25); Angular: Router lifecycle and events (read 2026-09-25); Angular: CSP_NONCE (read 2026-09-25).
How do you add the tag on SvelteKit?
Where it goes: Add the tag to src/app.html, the page template SvelteKit renders around every page. If you use kit.csp, give it nonce="%sveltekit.nonce%" so SvelteKit fills in the nonce.
What breaks it on SvelteKit
- SvelteKit navigates between pages without reloading.
afterNavigatefrom$app/navigationruns "when the current component mounts, and also whenever we navigate to a URL". Put it in the root+layout.svelteto record every page. - Put the tag in
app.html, not in<svelte:head>in a layout. The SvelteKit issue tracker has several reports of<svelte:head>content being duplicated after client-side navigation. - With
kit.cspinautomode, SvelteKit uses nonces for dynamically rendered pages and hashes for prerendered ones. List the vendor's hosts inscript-srcandconnect-srceither way. An inline vendor block on a prerendered page cannot use a nonce, so it needs a hash.
How to check it on SvelteKit
Run npm run build && npm run preview, then run the navigation test. Check the page source once to confirm the tag appears one time.
Sources: SvelteKit: Configuration (app.html, csp) (read 2026-09-25); SvelteKit: $app/navigation (read 2026-09-25).
How do you add the tag on Nuxt?
Where it goes: Add the tag to the app.head.script array in nuxt.config.ts. Nuxt renders it into the head of every page. The useScript() composable is an alternative, but it comes from the separate Nuxt Scripts module, whose current major version needs Nuxt 4.5.1 or newer and Node.js 24 or newer.
What breaks it on Nuxt
- Nuxt routes with Vue Router, so
<NuxtLink>navigation loads no new document. Record later pages fromuseRouter().afterEach()in a client-only plugin. - Nuxt's core docs do not describe a built-in CSP. If you need one, set the header on your server or host and include the vendor's hosts.
How to check it on Nuxt
Build and preview, confirm the tag appears once in the rendered <head>, then click between pages and check that each is recorded.
Sources: Nuxt: Configuration (app.head) (read 2026-09-25); Nuxt Scripts: Installation (read 2026-09-25).
How do you add the tag on React Router?
Where it goes: Remix v2 continues as React Router v7 in framework mode. Add the vendor's tag as an ordinary <script src=...> inside the Layout in app/root.tsx. You do not need dangerouslySetInnerHTML for a script with a src attribute. The <Scripts /> component in the same file renders your app's own runtime, and it takes a nonce prop if you use a CSP.
What breaks it on React Router
- Client-side transitions do not reload the document. Record later pages from an effect in the root component that depends on
useLocation().
How to check it on React Router
Build and start the production server, then run the navigation test above.
Sources: Remix blog: Merging Remix and React Router (read 2026-09-25); React Router: root.tsx (read 2026-09-25).
How do you add the tag on Gatsby?
Where it goes: Use the <Script> component from gatsby in a component every page renders, such as your layout. Its default strategy is post-hydrate; idle is also safe for this tag.
What breaks it on Gatsby
- Gatsby has no
beforeInteractivestrategy. That name belongs to Next.js. - Avoid
off-main-threadfor this tag. Gatsby's docs say those scripts load only on regular<a>navigation, "not on client-side rendering (CSR) navigation (e.g. Gatsby<Link>navigation)". - With the default strategies the script loads once. Record later pages from
onRouteUpdateingatsby-browser.js, which Gatsby calls when the user changes routes, including on the first load. Do not also send a page view from the tag's own load, or the first page is counted twice.
How to check it on Gatsby
Run gatsby build && gatsby serve, then click between pages with <Link> and count the recording requests: one per page.
Sources: Gatsby: Gatsby Script API (read 2026-09-25); Gatsby: Browser APIs (onRouteUpdate) (read 2026-09-25).
How do you add the tag on Astro?
Where it goes: Add the tag to your base layout with <script is:inline src="...">. Without is:inline, Astro processes and bundles <script> tags, and it cannot bundle a remote vendor URL. With is:inline the tag is rendered exactly as written.
What breaks it on Astro
- By default every Astro page is a full browser navigation, so the tag runs on every page with no extra work.
- If you turned on view transitions with
<ClientRouter />, pages swap without a reload. Either adddata-astro-rerunto the tag so it runs again after each transition, or record later pages in a listener for theastro:page-loadevent, which fires after the first load and after every transition. - Astro's CSP support is experimental, does not run in
astro dev, and does not support<ClientRouter />.
How to check it on Astro
Run npm run build && npm run preview. If you use view transitions, run the navigation test; if not, check that each page you open records one visit.
Sources: Astro: Scripts and event handling (read 2026-09-25); Astro: View transitions (read 2026-09-25); Astro: Experimental CSP (read 2026-09-25).
Where to go next
- Post-launch QA test script: the full test once page views are recorded, including CRM and alert checks
- Client-side vs server-side visitor identification: when a browser tag is the wrong design for a single-page app
- Install through Google Tag Manager: if the app already loads a GTM container, use its History Change trigger instead of code