---
title: "Single-page apps, frameworks and tag managers"
description: "Load the Noxtica collector once in a single-page app, handle route changes and framework forms, and install it through a tag manager or a CMS theme."
canonical_url: "https://noxtica.com/docs/sdk/single-page-apps/"
markdown_url: "https://noxtica.com/docs/sdk/single-page-apps/index.md"
last_updated: 2026-10-27
published: 2026-10-27
section: "browser-sdk"
section_title: "Browser SDK"
site: "Noxtica"
language: "en"
source: markdown
---

# Single-page apps, frameworks and tag managers

A single-page app changes the URL without loading a new document, a framework re-renders its own markup, and a tag manager injects scripts on its own schedule. This page shows what the collector does in those setups and what you do to keep one clean install.

## What it is

Availability: Evaluation access. Availability depends on your plan, role and the modules enabled for your organisation.

The collector is a plain script. It needs one core tag per document, a Site Key that matches the page origin, and a `window` and `document` to run in. It does not depend on your framework. The core script watches client-side navigation through the History API when a protected action matches a page path, and it does nothing else on a route change. The page check belongs to the document, not to the route.

The typed `@noxtica/sdk` wrapper has React, Next.js and Vue helpers in its source. That package is not installable from the public registry today (the registry answered `E404` on 2026-10-27), so this page gives no install command for it. Ask your account team for access; see [SDK family and availability](https://noxtica.com/docs/sdk/family/). Every recipe below works with the script tag alone.

## Prerequisites

- A Site Key for the origin your app is served from. Staging and production origins have separate keys. A key used on another origin gets `403` with `Origin mismatch`.
- The core tag from Backoffice → Integrations & API → Script & Consent → Snippets. See [Install and Browser Runtime](https://noxtica.com/docs/browser-runtime/).
- Your consent mode decided. See [Consent modes](https://noxtica.com/docs/sdk/consent/).
- For tag managers: the right to publish tags that run on every page of the site.

## Steps

### 1. Load the core once, in the app shell

Put the tag in the document that the app mounts into: `index.html`, the root layout, or the theme's `<head>`. Do not add it per route or per component.

```html
<head>
	<script>
		document.addEventListener('noxtica:collected', (event) => {
			if (event.detail.skipped === 'consent_required') return; // not an error
			// Use event.detail.fingerprintId as a hint for your backend.
		});
	</script>
	<script
		src="https://collect.noxtica.com/collector/noxtica-collector-core.js"
		data-site-key="pk_prod_your_site_key_here"
		data-auto-init
		data-auto-check-once
		async
	></script>
</head>
```

Register your `noxtica:*` listeners in a script that runs before the tag. An event that already fired is not replayed. If your framework cannot run code before the tag, queue calls with the `window.noxtica` stub instead, and read `window.noxticaClient` and `window.noxticaResult` after load. See [Client API, events and the command queue](https://noxtica.com/docs/sdk/client-api/).

If the core competes with the initial render or hydration, inject it after the `load` event and the next idle period. A ready-made snippet is in [Load timing and weight](https://noxtica.com/docs/browser-runtime/#load-timing-and-weight).

### 2. Pick a framework recipe

| Setup | What you do |
| --- | --- |
| Vite, Create React App or any bundler with an `index.html` | Add the tag from step 1 to `index.html`. |
| Vue | Add the tag to `index.html`. |
| Next.js, App Router | Render the tag with `next/script` in the root layout. |
| Angular | Add the tag to `index.html`. To keep the collector's timers out of change detection, load it outside the Angular zone; see [Single-page apps and frameworks](https://noxtica.com/docs/browser-runtime/#single-page-apps-and-frameworks). |
| Server-side rendering | Emit the tag in the HTML. The loader does nothing without a `document`, so a server render is not affected. Do not import or call the collector in server code. |

For Next.js, the markup below is what the wrapper's `NoxticaScript` component renders, taken from the wrapper source. `afterInteractive` is its default strategy. Test it in your own app:

```tsx
// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en">
			<body>
				{children}
				<Script
					src="https://collect.noxtica.com/collector/noxtica-collector-core.js"
					strategy="afterInteractive"
					data-site-key="pk_prod_your_site_key_here"
					data-auto-init=""
					data-auto-check-once=""
				/>
			</body>
		</html>
	);
}
```

The wrapper's helpers, for accounts that have the package:

| Helper | What it does, from the 0.3.0 source |
| --- | --- |
| `NoxticaProvider`, `useNoxtica` (React) | The provider creates one client in an effect, so it runs in the browser and not on the server. `useNoxtica()` returns `{ client, loading, error }`. The provider destroys its client when it unmounts. |
| `useVerdict` (React) | Requests a verdict when the client is ready, unless you pass `auto: false`. Returns `{ verdict, loading, error, refresh }`. |
| `NoxticaScript` (Next.js) | Renders the tag through `next/script` with `data-site-key`, `data-auto-init` and `data-auto-check-once`. Options: `apiUrl`, `ingestUrl`, `scriptUrl`, `script`, `extensions`, `auto`, `checkIntervalDays`, `debug`, `logLevel`, `strategy`. It has no consent-mode option. |
| `createNoxtica`, `useNoxtica` (Vue 3) | A plugin and a composable. They return `client`, `loading`, `error`, `verdict`, `getVerdict` and `identify`. The plugin stays inert during server rendering. |

The wrapper's `init()` rejects outside a browser. Guard shared code with `isBrowser()`.

### 3. Handle route changes

A route change does not run a new page check. `data-auto-check-once` runs `checkOnce()` once for the document, and the cache answers later calls. What you can rely on:

- Protected actions that match a **Page path** run on the initial load and again when the History API changes the URL (`pushState`, `replaceState`, `popstate`). The core shares one wrapper per page and restores the original methods when no subscriber is left. It leaves your own wrappers in place.
- Calling `checkOnce()` on every route is allowed. With the default `recordVisit: true`, a cache hit records a visit with the collector, so each call sends a request. Pass `{ recordVisit: false }` when you do not want that.
- Call `checkOnce({ forceRefresh: true })` or `collectAndSubmit()` when a flow needs a fresh check, and not otherwise. Both collect again and submit.
- For application events and session tags, use `track`, `event` and `setTag` from [Custom events, session tags and request tracing](https://noxtica.com/docs/observability/events-and-tracing/).

Do not decide whether Noxtica is on a page from an attribute that your framework renders. A locale switch or a remount can clear attributes on `<html>`. Decide from the host name or your build configuration.

### 4. Protect forms that your script submits

A script-submitted form (React, Vue, any `fetch` flow) cannot receive the hidden `nox_request_id` field. Read `form.dataset.noxRequestId` at the top of your submit handler, and call `protect` synchronously when the id is not there yet. The full handler is in [Protected actions](https://noxtica.com/docs/sdk/protected-actions/). The id is withdrawn when your handler returns, so a retry runs a fresh check.

### 5. Install through a tag manager or a CMS theme

A tag manager or a theme has to put the same script element on the page, and nothing more. The collector sees the element, not the tool. Check these points in your tool:

1. **Raw HTML.** Use the tag type that accepts a `<script>` with attributes, or a script that creates the element. The attributes `data-site-key` and `data-auto-init` must be present when the element is added. The loader reads them once.
2. **File name.** Keep `noxtica-collector` in the `src` file name. Do not rename the file.
3. **One tag.** Remove a hand-pasted tag when the tag manager adds one. A second core tag is ignored and logs `NX001`.
4. **Order.** Define the `window.noxtica` stub and any listeners before the core runs. If the tool cannot order them, read `window.noxticaClient` after load.
5. **Trigger.** Fire the tag on every page of the site. For an `api` consent mode, also fire a call to `window.noxticaClient.setConsent(...)` from the trigger that carries your consent decision, after `window.noxticaClient` exists.
6. **Dynamic injection.** A tag added after the page has loaded works. Set every attribute before you append the element, because the loader reads them when it starts.
7. **Google Consent Mode.** When Google tags run on the page, the bridge can follow `analytics_storage`. See [Consent modes](https://noxtica.com/docs/sdk/consent/).

For a CMS theme, add the tag to the template that outputs `<head>`, or to the theme's header-scripts setting. Tag managers and themes differ in how they order and defer scripts; Noxtica does not publish tool-specific steps.

```javascript
// Custom HTML or script-injection example: set the attributes, then append the element.
if (!window.noxtica || !window.noxtica.v) {
	const s = document.createElement('script');
	s.src = 'https://collect.noxtica.com/collector/noxtica-collector-core.js';
	s.async = true;
	s.setAttribute('data-site-key', 'pk_prod_your_site_key_here');
	s.setAttribute('data-auto-init', '');
	s.setAttribute('data-auto-check-once', '');
	document.head.appendChild(s);
}
```

The `window.noxtica.v` check skips the injection when a core script has already claimed the page.

### 6. Check script optimizers and duplicate tags

A script optimizer that rewrites or defers script tags can delay the collector. The Backoffice notes that Cloudflare Rocket Loader defers every script tag it sees unless the tag carries `data-cfasync="false"`. The session-replay early loader requires that attribute; see [Replay recorder head](https://noxtica.com/docs/browser-runtime/#replay-recorder-head). Noxtica publishes no such requirement for the core tag. If an optimizer delays your core tag, test the tag with `data-cfasync="false"`.

A framework that mounts an effect twice, such as React strict mode in development, can add the tag twice. Guard the injection as in step 5. A duplicate is harmless: the second core script logs `NX001` and stops without creating a client.

## Verify it works

1. Reload a page, then navigate to another route without a reload. The route change itself adds no `POST /client/submit` request, unless a Page path protected action matches the new route.
2. `typeof window.noxticaClient` is `'object'`, and `window.noxtica.v` holds the loader version.
3. `document.querySelectorAll('script[src*="noxtica-collector"]').length` is `1`.
4. If a Page path protected action matches a route, navigating to it starts a protected-action check. Calling `await window.noxticaClient.protect('<name>')` resolves a `requestId`, and a bound form receives a `noxtica:protected` event.
5. Your listener received `noxtica:collected` once for the document.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `NX001: already loaded` in the console | Two core tags: for example a hand-pasted one and a tag-manager one | Remove one. The initial tag keeps running. |
| `window.noxticaClient` is undefined with a tag manager | The injected element lacks `data-auto-init` or `data-site-key`, or the tag manager changed the file name | Set both attributes before appending, and keep `noxtica-collector` in the file name. |
| `403` with `Origin mismatch` on staging | The staging origin uses the production Site Key | Create a domain for the staging origin and use its key. |
| A `noxtica:collected` listener never fires | The listener ran after the event, or the tag was injected late | Register listeners before the tag, or read `window.noxticaResult`. |
| Route changes send a request each time | `checkOnce()` is called per route with `recordVisit` left at `true` | Call it once, or pass `{ recordVisit: false }`. |
| The id is missing in a framework form handler | The handler read it after an `await` | Read `form.dataset.noxRequestId` and call `protect` before any `await`. |
| `window.noxticaClient` appears late | An optimizer defers the script | Test `data-cfasync="false"`, or inject the tag after load on purpose. |
| `@noxtica/sdk` import fails | The package is not on the public registry | Use the script tag, or ask your account team for access. |

## Related

- [Install and Browser Runtime](https://noxtica.com/docs/browser-runtime/): install order, pinned tags and CSP.
- [Script attributes and client options](https://noxtica.com/docs/sdk/script-attributes/): every attribute you can set on the tag.
- [Client API, events and the command queue](https://noxtica.com/docs/sdk/client-api/): the queue stub and events.
- [Protected actions](https://noxtica.com/docs/sdk/protected-actions/): forms, route triggers and the server check.
- [Content Security Policy, hosts and first-party relay](https://noxtica.com/docs/sdk/csp-and-relay/): policies for apps and tag managers.
- [SDK family and availability](https://noxtica.com/docs/sdk/family/): the typed wrapper and its status.
- [Browser SDK troubleshooting](https://noxtica.com/docs/sdk/troubleshooting/): symptoms across the SDK.
