# Headless and single-page storefronts

> Use ShopAssist on Next.js, Hydrogen, Nuxt and other SPAs: route changes, the JavaScript API, strict CSP, the SRI-pinned loader and identity.

Source: https://shopassist.coec.dk/docs/install/headless · Updated 2026-10-01

Headless storefronts use the [Generic connector](/docs/install/generic) for catalog and orders (or the Shopify app for a Shopify backend with a custom frontend) and embed the same widget.

## Load the widget once [#load]

Add the loader once in your document shell, not inside a route component, so navigation doesn't reload it:

```html
<script src="https://cdn.shopassist.coec.dk/widget/<version>/loader.js" async
        integrity="sha384-<published hash>" crossorigin="anonymous"
        data-shopassist-key="pk_live_YOUR_PUBLISHABLE_KEY" data-platform="generic"></script>
```

The first pinned release is published with the widget's general availability; until then use the channel loader below. Your dashboard shows the current version and hash.

In Next.js, put the tag in the root layout's `<body>` as a plain `<script async>`; avoid wrappers that re-insert it on every route.

## Tell the widget about route changes [#routes]

The widget reads the page type and product from the embed on load. On client-side navigation, call `setPageContext` so recommendations and proactive messages match the current page:

```js
ShopAssist.setPageContext({ type: "product", productId: "SHIRT-42", variantId: "SHIRT-42-BLUE", title: "Linen shirt" })
// on a category page
ShopAssist.setPageContext({ type: "category", categoryId: "summer" })
```

Use your catalog ids (the same `external_id` values as in your feed or Push API). Calls made before the loader finished are queued if you use `ShopAssistQueue`:

```js
;(window.ShopAssistQueue = window.ShopAssistQueue || []).push(["setPageContext", { type: "home" }])
```

## JavaScript API [#api]

Open the chat from your own button, react to events and pass the cart through your own code. See the [JavaScript API](/docs/widget/javascript-api), [events](/docs/widget/events) and the [cart handler](/docs/install/generic/checkout-and-cart#cart-handler).

## Content Security Policy [#csp]

Strict CSPs need four directives. The pinned loader with SRI keeps an end-to-end integrity chain:

```text
script-src https://cdn.shopassist.coec.dk; connect-src https://api.shopassist.coec.dk https://cdn.shopassist.coec.dk; img-src https://cdn.shopassist.coec.dk <store media hosts> blob:; style-src https://cdn.shopassist.coec.dk
```

| Directive | Sources | Why |
| --- | --- | --- |
| `script-src` | `https://cdn.shopassist.coec.dk` | The loader and the chat app. Shopify serves the loader from the theme asset host; the app still needs our CDN. |
| `connect-src` | `https://api.shopassist.coec.dk https://cdn.shopassist.coec.dk` | The widget API, streamed replies (SSE over fetch), the analytics beacon, the signed manifest and language packs. |
| `img-src` | `https://cdn.shopassist.coec.dk <your store media hosts> blob:` | Avatars and logos from our CDN, product and store images from your own media hosts, blob: for photo previews and attachments. |
| `style-src` | `https://cdn.shopassist.coec.dk` | Only used when the browser cannot apply constructable stylesheets (older Safari). 'unsafe-inline' is never needed. |
| `trusted-types` | `shopassist` | Only when you enforce require-trusted-types-for 'script'. |

Details: [Content Security Policy](/docs/widget/content-security-policy).

## Signed-in customers [#identity]

Register an identity provider that asks your server for a token:

```js
ShopAssist.identify(async ({ sessionId }) => {
  const r = await fetch("/my-server/shopassist-token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ session_id: sessionId }),
  })
  return r.ok ? ((await r.json()).token ?? null) : null
})
```

Call `ShopAssist.identify(null)` when the customer signs out. Server side: [identity JWT](/docs/orders-and-identity/generic-identity-jwt).
