@c15t/react 3.0.0-alpha.10 → 3.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/aggregate-components.js +1 -1
  3. package/dist/chunk-warming.js +1 -1
  4. package/dist/components/consent-gate/atoms.js +1 -1
  5. package/dist/components/iab-panel/atoms/root.js +1 -1
  6. package/dist/components/iab-panel/iab-panel.js +1 -1
  7. package/dist/components/iab-prompt/atoms/root.js +1 -1
  8. package/dist/components/iab-prompt/iab-prompt.js +1 -1
  9. package/dist/components/panel/atoms/root.js +1 -1
  10. package/dist/components/panel-trigger/atoms/root.js +1 -1
  11. package/dist/components/panel-trigger/panel-trigger.js +1 -1
  12. package/dist/components/panel-trigger/toolbar-loader.js +2 -0
  13. package/dist/components/preferences/atoms/root.js +1 -1
  14. package/dist/components/prompt/atoms/root.js +1 -1
  15. package/dist/components/shared/surface-styles.js +1 -1
  16. package/dist/devtools.js +1 -1
  17. package/dist/iab/styles.tw3.css +2 -2
  18. package/dist/iab-dialog-deferred.js +2 -0
  19. package/dist/iab.js +1 -1
  20. package/dist/styles.tw3.css +2 -2
  21. package/dist/utils/deferred-module.js +1 -1
  22. package/dist/version.js +1 -1
  23. package/dist-types/chunk-warming.d.ts +8 -11
  24. package/dist-types/components/panel-trigger/toolbar-loader.d.ts +34 -0
  25. package/dist-types/components/shared/surface-styles.d.ts +26 -2
  26. package/dist-types/iab-dialog-deferred.d.ts +36 -0
  27. package/dist-types/iab.d.ts +2 -1
  28. package/dist-types/version.d.ts +1 -1
  29. package/docs/README.md +1 -0
  30. package/docs/concepts/choose-your-setup.md +5 -2
  31. package/docs/concepts/data-fetching.md +24 -10
  32. package/docs/concepts/modes.md +9 -6
  33. package/docs/customization/slots.md +16 -16
  34. package/docs/customization/tokens.md +29 -0
  35. package/docs/frameworks/react/components/consent-banner.md +3 -3
  36. package/docs/frameworks/react/components/consent-dialog.md +3 -3
  37. package/docs/frameworks/react/components/consent-provider.md +12 -9
  38. package/docs/frameworks/react/components.md +3 -3
  39. package/docs/frameworks/react/content-security-policy.md +8 -2
  40. package/docs/frameworks/react/iab.md +19 -0
  41. package/docs/frameworks/react/quickstart.md +25 -79
  42. package/docs/frameworks/react/rendering.md +161 -139
  43. package/docs/frameworks/react/scripts.md +3 -3
  44. package/docs/frameworks/react/troubleshooting.md +17 -16
  45. package/docs/frameworks/react/upgrade-v3.md +5 -4
  46. package/docs/guides/banner-experiments.md +16 -0
  47. package/docs/integrations/overview.md +1 -0
  48. package/docs/integrations/vercel-speed-insights.md +306 -0
  49. package/package.json +7 -7
@@ -42,13 +42,13 @@ import {
42
42
  ConsentDialog,
43
43
  ConsentDialogLink,
44
44
  ConsentProvider,
45
- manifest,
45
+ hosted,
46
46
  } from 'c15t/react';
47
47
  import type { ReactNode } from 'react';
48
48
 
49
49
  const options = {
50
- // The policy the build downloaded from VITE_C15T_BACKEND_URL.
51
- mode: manifest(),
50
+ // Asks VITE_C15T_BACKEND_URL for each visitor's policy.
51
+ mode: hosted(),
52
52
  scripts: [
53
53
  posthog({
54
54
  id: 'phc_your_project_key',
@@ -16,6 +16,7 @@ A React app with c15t adds these to the page, and your policy must allow them:
16
16
  | `c15t/react/styles.css`, only if you import it with `styles: false` | `style-src 'self'` |
17
17
  | The `<style id="c15t-theme">` element, if you render `ConsentTheme` | `style-src` with a nonce or its hash |
18
18
  | `<script>` elements the script loader creates for `scripts` entries, both `src` and inline | `script-src` with each vendor's host, or a nonce |
19
+ | The inline `<script>` that `consentManifest()` adds to `index.html` to send `/init` early | `script-src` with a nonce or its hash |
19
20
  | With server rendering, the inline `<script>` before the banner's buttons that holds a tap made before hydration | `script-src` with a nonce or its hash |
20
21
  | Browser requests to your backend's `/init` and `/subjects` | `connect-src` with the backend origin |
21
22
  | Iframes inside `ConsentGate`, and the frames, images and requests your vendors load | `frame-src`, `img-src`, `connect-src` for each vendor |
@@ -70,8 +71,9 @@ entry takes precedence for that element. The stock components put it on the
70
71
  With Vite, set `html.cspNonce` to a placeholder in `vite.config.ts`, and have
71
72
  your server replace it with a new nonce on every response. Vite then adds
72
73
  `<meta property="csp-nonce" nonce="...">` to the page, and the browser keeps the
73
- value in the tag's `nonce` property. Read it where you create the provider, in
74
- the quickstart's `src/consent.tsx`:
74
+ value in the tag's `nonce` property. Vite also stamps the nonce on the early
75
+ `/init` script. Read it where you create the provider, in the
76
+ quickstart's `src/consent.tsx`:
75
77
 
76
78
  ```tsx title="src/consent.tsx (partial)"
77
79
  const nonce =
@@ -88,6 +90,10 @@ The provider reads `options.nonce` once, when it mounts. A server-rendered
88
90
  React app, such as one built with React Router framework mode, reads the nonce
89
91
  from its own request and passes it the same way.
90
92
 
93
+ On a static host, allow the early `/init` script by the hash in the browser's
94
+ `Refused to execute inline script` error. If the policy blocks it, the app
95
+ sends `/init` itself.
96
+
91
97
  ## Allow c15t's style elements
92
98
 
93
99
  The banner, dialog and other stock components render c15t's rules as
@@ -67,6 +67,25 @@ your error boundary or the framework's error overlay shows it. Render `IABProvid
67
67
  `c15t/react/iab` for those visitors, or remove the `iab` model from the
68
68
  policy.
69
69
 
70
+ ## When the dialog loads
71
+
72
+ `IABConsentDialog` loads its code on demand, so the first page load
73
+ carries the banner without the preference center. The code downloads at
74
+ the first of:
75
+
76
+ * The browser's first idle period after the page's `load` event, while
77
+ the IAB banner is shown.
78
+ * Hover or focus on the banner's Customize button or partners link.
79
+ * The dialog opening.
80
+
81
+ A visitor who opens the dialog before the download finishes waits for
82
+ it. `open={true}` and `showTrigger` load it on mount, under a model the
83
+ dialog's `models` prop lists. Under another model, `showTrigger` loads
84
+ only the trigger. Set
85
+ `preloadDialog: 'intent'` in the consent provider options to skip the
86
+ idle download. Idle loading is also skipped with Save-Data on, on `2g`
87
+ and `slow-2g` connections, and while offline.
88
+
70
89
  ## Verify vendor and purpose choices
71
90
 
72
91
  Test category choices alongside vendor, purpose, legitimate-interest and special
@@ -7,11 +7,10 @@ group: frameworks
7
7
 
8
8
  ## Before you start
9
9
 
10
- This guide sets up a client-rendered React app built with Vite. The build
11
- bundles your project's public policy. The browser loads the page, resolves the
12
- visitor's policy from that bundle, then shows the banner or runs the scripts
13
- the visitor allowed. Regional policies may still need a backend location
14
- request.
10
+ This guide sets up a client-rendered React app built with Vite. The browser
11
+ loads the page and asks your consent backend for the visitor's policy, then
12
+ shows the banner or runs the scripts the visitor allowed. The backend reads the
13
+ visitor's location from the request, so regional policies need no extra setup.
15
14
 
16
15
  * **Next.js or TanStack Start?** Use that framework's guide from the
17
16
  [framework list](https://v3.c15t.com/docs/frameworks). Their adapters resolve consent on the
@@ -28,8 +27,8 @@ the backend URL.
28
27
  [Choose your setup](../../concepts/choose-your-setup.md#decide-who-runs-the-consent-backend)
29
28
  covers self-hosting and offline mode.
30
29
 
31
- Bundling the policy at build time is the recommended production setup.
32
- Rebuild after changing policies, translations or vendors.
30
+ The guide uses `hosted()` mode. Policy, translation and vendor edits in your
31
+ project reach visitors on their next page load, without a rebuild.
33
32
 
34
33
  ## Install c15t
35
34
 
@@ -43,7 +42,7 @@ Rebuild after changing policies, translations or vendors.
43
42
  `c15t` contains the React adapter at `c15t/react` and the Vite plugin at
44
43
  `c15t/build`. `@c15t/integrations` holds the vendor loaders used below.
45
44
 
46
- ## Bundle your policy
45
+ ## Add the Vite plugin
47
46
 
48
47
  Add `consentManifest` to `vite.config.ts`:
49
48
 
@@ -66,60 +65,14 @@ VITE_C15T_BACKEND_URL=https://your-project.inth.app
66
65
  If your app already sets `VITE_INTH_PROJECT_URL` for other Inth SDKs, that works
67
66
  too. When both are set, `VITE_C15T_BACKEND_URL` wins.
68
67
 
69
- `consentManifest` downloads your project's public policy from `/manifest`
70
- during `vite build`, or in `vite dev` when the app first loads it, and serves
71
- it to your app as `snapshot`
72
- from `c15t/generated`. No file is written into your project.
68
+ `consentManifest` hands the backend URL to `hosted()` through
69
+ `c15t/generated`. No file is written into your project. Without the plugin,
70
+ pass the URL as `hosted({ backendURL })`.
73
71
 
74
- The snapshot is fixed at build time:
75
-
76
- * Rebuild after changing policies, translations or vendors. If your CI caches
77
- build output, force a fresh build.
78
- * Consent choices still go to the backend.
79
-
80
- The build reads the backend URL from your public backend URL variable when
81
- the config doesn't pass one: `NEXT_PUBLIC_C15T_BACKEND_URL` in Next.js,
82
- `NUXT_PUBLIC_C15T_BACKEND_URL` in Nuxt, `PUBLIC_C15T_BACKEND_URL` in Astro,
83
- Svelte and SvelteKit, and `VITE_C15T_BACKEND_URL` in TanStack Start and other
84
- Vite apps. Each also reads the matching Inth variable, such as
85
- `NEXT_PUBLIC_INTH_PROJECT_URL`, when the c15t one is unset. See
86
- [set the backend URL](../../concepts/modes.md#set-the-backend-url).
87
-
88
- The fetch waits at most 10 seconds. When it fails, or no backend URL is set,
89
- every framework does the same thing:
90
-
91
- | Command | Default when the fetch fails |
92
- | ------------------------------------------------------------------------- | -------------------------------------------------------- |
93
- | Production build: `next build`, `vite build`, `nuxt build`, `astro build` | The build stops with an error. |
94
- | Dev: `next dev`, `vite dev`, `nuxt dev`, `astro dev` | A warning, and the server fetches the policy at runtime. |
95
-
96
- Set `onBuildError` to use one behaviour for both. `'fail'` stops dev too.
97
- `'runtime'` lets a production build finish, and the server fetches the policy
98
- at runtime. The `C15T_ON_BUILD_ERROR` environment variable overrides the
99
- option, so you can deploy during a backend outage without a code change:
100
-
101
- ```sh
102
- C15T_ON_BUILD_ERROR=runtime npm run build
103
- ```
104
-
105
- Turborepo's strict environment mode hides undeclared variables from tasks, so
106
- list `C15T_ON_BUILD_ERROR` in the build task's `passThroughEnv` there.
107
-
108
- The build skips the fetch, without an error, when it can't use a snapshot,
109
- for example when the backend URL is relative. With `onBuildError: 'fail'`, a
110
- relative URL stops the build. [Consent modes](../../concepts/modes.md#what-happens-when-the-download-fails)
111
- lists every case.
112
-
113
- `vite build` and `vite dev` fetch the manifest when they start. `vite preview`
114
- serves the last build without fetching. The plugin writes no file into your
115
- app, so there is nothing to keep out of Git. `c15t/generated` ships its own
116
- types, so `tsc`, `vue-tsc` and `svelte-check` pass on a fresh checkout without
117
- a build first.
118
-
119
- The plugin can't see the options your app passes to `manifest()`. When the
120
- app passes `source: 'runtime'` or `manifestURL`, set
121
- `consentManifest({ source: 'runtime' })` too. The build then downloads no
122
- manifest, so it doesn't fail when the backend's `/manifest` is down.
72
+ The plugin also adds a small script to `index.html` that sends `/init` while
73
+ your JavaScript downloads, so the banner shows sooner.
74
+ [Rendering](./rendering.md#send-init-before-the-app-loads)
75
+ covers when to turn it off.
123
76
 
124
77
  ## Mount the provider
125
78
 
@@ -135,13 +88,13 @@ import {
135
88
  ConsentDialog,
136
89
  ConsentDialogLink,
137
90
  ConsentProvider,
138
- manifest,
91
+ hosted,
139
92
  } from 'c15t/react';
140
93
  import type { ReactNode } from 'react';
141
94
 
142
95
  const options = {
143
- // The policy the build downloaded from VITE_C15T_BACKEND_URL.
144
- mode: manifest(),
96
+ // Asks VITE_C15T_BACKEND_URL for each visitor's policy.
97
+ mode: hosted(),
145
98
  scripts: [
146
99
  posthog({
147
100
  id: 'phc_your_project_key',
@@ -163,18 +116,11 @@ export const Consent = ({ children }: { children: ReactNode }) => (
163
116
  );
164
117
  ```
165
118
 
166
- `manifest()` resolves the policy from the bundled snapshot in the browser,
167
- with no `/manifest` request. It reads the snapshot and the backend URL from
168
- what `consentManifest` downloaded. Consent choices go to that backend, and so
169
- does an `/init` request when the policy depends on the visitor's country or
170
- region and the browser does not know it. That request returns the backend's
171
- current policy, which can be newer than the snapshot. Vite reads
172
- `VITE_C15T_BACKEND_URL` at build time, so set it before you build, not when
173
- you serve the files.
174
-
175
- To apply policy edits without rebuilding, use `hosted()` from `c15t/react` as
176
- the mode. The browser then requests the policy from `/init` on every page load.
177
- [Consent modes](../../concepts/modes.md) compares the three modes.
119
+ `hosted()` makes one `/init` request to your backend, started early by the
120
+ script in `index.html`. The backend picks the policy from the visitor's
121
+ location and language and returns its current version. Consent choices go to
122
+ the same backend. Vite reads `VITE_C15T_BACKEND_URL` at build time, so set it
123
+ before you build, not when you serve the files.
178
124
 
179
125
  PostHog waits for measurement permission, so your policy must include that
180
126
  category. Replace `phc_your_project_key` with your PostHog project key.
@@ -234,7 +180,8 @@ Build and preview the production bundle with `vite build` and `vite preview`,
234
180
  then open the site in a private window with DevTools open on the Network tab.
235
181
  Test under a policy that asks for a choice, such as an EU opt-in policy.
236
182
 
237
- 1. **Before a choice.** The banner shows, with no `/manifest` request. There
183
+ 1. **Before a choice.** One `/init` request goes to your backend URL, starting
184
+ before your JavaScript finishes downloading, and the banner shows. There
238
185
  are no requests to `posthog.com`.
239
186
  2. **Reject All.** The banner closes and the vendor requests stay absent.
240
187
  3. **Reload.** The banner stays closed and the vendor requests stay absent.
@@ -243,8 +190,7 @@ Test under a policy that asks for a choice, such as an EU opt-in policy.
243
190
  save. PostHog's `array.js` loads.
244
191
  5. **Turn Analytics off again.** The page reloads and PostHog does not load.
245
192
 
246
- If the banner never appears, check the generated policy and any location
247
- request to `/init` on your backend URL. [Troubleshooting](./troubleshooting.md)
193
+ If the banner never appears, check the `/init` request to your backend URL. [Troubleshooting](./troubleshooting.md)
248
194
  covers the common causes. [Verify consent](../../guides/verify-consent.md) has the
249
195
  full release checklist.
250
196
 
@@ -23,15 +23,168 @@ read the visitor's cookie and location on the server, so the banner can be in
23
23
  the first HTML. [Choose your setup](../../concepts/choose-your-setup.md#server-rendering-or-browser-rendering)
24
24
  explains the difference in general terms.
25
25
 
26
- ## Bundle the manifest during Vite builds
26
+ ## Client-rendered apps
27
27
 
28
28
  The [quickstart](./quickstart.md) and `examples/react` in
29
- the c15t repository use this setup.
29
+ the c15t repository use `hosted()` from `c15t/react`. It reads the backend URL
30
+ `consentManifest` read from `VITE_C15T_BACKEND_URL` or
31
+ `VITE_INTH_PROJECT_URL`; without the plugin, pass `hosted({ backendURL })`.
32
+ Policy edits apply on the next page load, without a rebuild. The page loads
33
+ like this:
34
+
35
+ 1. The page loads with no consent state. Every optional category is denied.
36
+ 2. The browser requests `GET /init` from your backend URL. A
37
+ [script in `index.html`](#send-init-before-the-app-loads) sends it while
38
+ the JavaScript downloads. The backend picks the visitor's policy from their
39
+ location. With an `experiment`, pass it to the plugin as
40
+ `initPrefetch: { experiment }` so the early request carries the visitor's
41
+ arm. Without that, the provider sends its own `/init` when it mounts.
42
+ 3. The provider applies the policy and any stored choice. It shows the banner
43
+ if the policy asks for one, and loads the scripts the visitor allowed.
44
+
45
+ Until step 3 finishes, gated scripts and `ConsentGate` embeds stay blocked. If
46
+ the backend request fails, they stay blocked and no banner shows.
47
+
48
+ ### Send `/init` before the app loads
49
+
50
+ `consentManifest()` adds a small inline script to the `<head>` of
51
+ `index.html`. It sends `/init` while the browser downloads your JavaScript,
52
+ and `hosted()` uses that response instead of sending its own. The banner
53
+ usually shows 50 to 250 ms sooner. There is nothing to configure.
54
+
55
+ The early request can't read your app's options, so in a few setups it won't
56
+ match and the app sends a second `/init`. Pass `initPrefetch: false` when the
57
+ app uses `offline()` or gives `hosted()` its own `backendURL`, `fetch`,
58
+ `initURL` or `headers`. With its own `fetch`, `hosted()` sends `/init` through
59
+ it and ignores the early response. If it sets `credentials`, `overrides`, `storageKey`, an
60
+ `experiment`, or `journey: false`, pass the same values as
61
+ `initPrefetch: { credentials, experiment, journey, overrides, storageKey }`.
62
+ With `journey: 'tab'`, passing it too makes a tab's first `/init` report the
63
+ `'tab'` scope instead of `'page'`. It doesn't save a request.
64
+ Pass `experiment` only when c15t assigns the arm. If a feature flag picks
65
+ it, leave it out, or the script sends an arm the visitor may never see.
66
+ The dev server adds the script even to a `manifest()` app, which then sends
67
+ one `/init` per page load that it doesn't use. A production build leaves it
68
+ out.
69
+
70
+ ## React Router, Remix and other server-rendered apps
71
+
72
+ React Router framework mode, Remix and similar frameworks have no c15t adapter.
73
+ Use the quickstart's `Consent` component and render it in your root component,
74
+ around the outlet. In React Router framework mode that is `app/root.tsx`.
75
+
76
+ `ConsentProvider` renders on the server without touching browser APIs, and the
77
+ browser takes over after hydration:
78
+
79
+ * The server HTML contains your page but no banner and no consent state.
80
+ * After hydration the browser calls `/init`, then shows the banner if the
81
+ policy asks for one.
82
+ * The server render and the first browser render agree, so hydration does not
83
+ warn or change a stored choice.
84
+
85
+ The Vite setup needs no change. The banner renders in the browser and brings
86
+ its own `<style>` element, so there is no stylesheet to import.
87
+
88
+ These frameworks render their own HTML, so the Vite plugin can't add the
89
+ early `/init` script. Put `buildPrefetchScript({ backendURL })` from `c15t` in
90
+ a `<script>` in the root `<head>` instead, unless your loader supplies
91
+ `initialData` as described below. The two are alternatives: with
92
+ `initialData` the provider never reads the script's response.
93
+
94
+ ### Skip the browser's first init request
95
+
96
+ This is optional. `fetchSSRData` from `c15t/react/server` calls the backend's
97
+ `/init` from your server loader, forwarding the visitor's location and
98
+ language headers. Pass the result to the browser, wrap it in a promise and give
99
+ it to `hosted({ backendURL, initialData })`. The provider's first init then uses that
100
+ response instead of a network request.
101
+
102
+ Know its limits before you add it:
103
+
104
+ * It saves the browser's round trip to the backend. It does not put the banner
105
+ in the server HTML; the banner still mounts after hydration.
106
+ * Every page view makes a server-to-server `/init` request. Do not cache the
107
+ loader response, because it contains one visitor's policy.
108
+ * It returns `undefined` when the request has no location or language headers,
109
+ and the browser then requests `/init` as usual.
110
+ * `mode` is read once, when the provider mounts. Create the `hosted()` mode once
111
+ in the browser rather than on every render.
112
+ * A relative `backendURL` resolves against the request's `host` header, over
113
+ `https` for a domain name and over `http` for `localhost`, an IP address or
114
+ a single-label host. `fetchSSRData` ignores `x-forwarded-host`,
115
+ `x-forwarded-proto` and `forwarded`, and does not send them to the backend.
116
+ Behind a proxy that overwrites those headers, pass
117
+ `trustForwardedHeaders: true` to use them.
118
+ * If your server answers requests for any `Host` header, such as a Node server
119
+ exposed directly or a proxy that forwards `Host` from its default virtual
120
+ host, pass `fetchSSRData` an absolute `backendURL`. A relative one resolves
121
+ against whatever `Host` the request sent.
122
+
123
+ ### Render the banner in the server HTML
124
+
125
+ c15t has no framework-neutral helper for this. `ConsentProvider` accepts a
126
+ server-resolved `prefetch` state, but the Next.js and TanStack Start adapters
127
+ build that state from the request's cookie, headers and a cached policy
128
+ manifest. Building it by hand ties your app to internal shapes. If the banner
129
+ must be part of the first paint, use one of those adapters. Otherwise keep the
130
+ browser path.
131
+
132
+ ## Static and prerendered pages
133
+
134
+ A prerendered page is the same HTML for every visitor, so it cannot contain one
135
+ visitor's consent. Use the quickstart's setup. `/init` requests and consent
136
+ saves go to the backend URL directly, so the static host needs no server
137
+ route. Add the site's
138
+ origin to the trusted origins in your Inth project.
139
+
140
+ Do not cache a response that contains a visitor's consent in a CDN or a shared
141
+ page cache. [Data fetching](../../concepts/data-fetching.md) covers what is safe to
142
+ cache.
143
+
144
+ ## Where the browser sends requests
145
+
146
+ With `hosted()`, the browser calls two backend endpoints:
147
+
148
+ | Request | When |
149
+ | --------------------- | ------------------------------------------ |
150
+ | `GET {url}/init` | Once per page load, to resolve the policy |
151
+ | `POST {url}/subjects` | When the visitor accepts, rejects or saves |
152
+
153
+ These requests go to the backend's origin, so it must list your site as a
154
+ trusted origin.
155
+ [Data fetching](../../concepts/data-fetching.md) compares this with proxies and
156
+ custom transports.
157
+
158
+ ## Check the rendering path
159
+
160
+ 1. Open the page in a private window and view the page source. In a
161
+ server-rendered app the HTML contains your content and no element with
162
+ `data-testid="consent-banner-root"`.
163
+ 2. In DevTools Network, confirm one `/init` request runs, unless you supply
164
+ `initialData`.
165
+ 3. The banner appears once the policy resolves. Vendor requests stay absent
166
+ until you allow their category.
167
+
168
+ ## Bundle the policy at build time
169
+
170
+ `manifest()` resolves the policy in the browser from a snapshot the build
171
+ downloaded. It saves the `/init` request only in these cases:
172
+
173
+ * The policy has no location rules, or every location gets the same banner.
174
+ * The page already knows the visitor's location, for example from an edge
175
+ worker, and passes it as `inputs`.
176
+ * You run a same-origin `geoURL` route that returns the location.
177
+
178
+ Otherwise the browser still calls `/init` on a first visit and uses the
179
+ backend's answer, so the bundle adds bytes without saving a request.
180
+ `consentManifest` warns during the build when that happens. The resolver adds
181
+ about 3 KB gzipped to the bundle, and policy edits need a rebuild. The browser
182
+ sends no `/init`, so the backend doesn't count visitors it resolves this way.
30
183
 
31
184
  The build fetches your public policy once and bundles it, so the server never
32
185
  fetches it at runtime.
33
186
 
34
- Add `consentManifest` to your existing Vite plugin list:
187
+ Keep `consentManifest` in your Vite plugin list:
35
188
 
36
189
  ```ts title="vite.config.ts (plugins option)"
37
190
  import { consentManifest } from 'c15t/build';
@@ -68,20 +221,16 @@ When the build has no snapshot, as in `vite dev` after a failed fetch,
68
221
  `${backendURL}/manifest` instead.
69
222
 
70
223
  When the required country or region is missing, the transport calls backend
71
- `/init` and uses its resolved policy and translations. `consentManifest` warns
72
- during the build when it downloads such a policy, and suggests `hosted()`. That response can use
73
- newer backend policy than the build snapshot. This is a full consent resolution,
74
- so these visitors still wait for a backend round trip. `ConsentProvider` sends
75
- that request while it first renders, before the page mounts, unless the visitor
76
- has a stored choice.
224
+ `/init` and uses its resolved policy and translations. That response can use
225
+ newer backend policy than the build snapshot. `ConsentProvider` sends that
226
+ request while it first renders, before the page mounts, unless the visitor has
227
+ a stored choice.
77
228
 
78
229
  The browser cannot guess the banner from the snapshot alone when locations
79
230
  differ. With a European opt-in rule and no banner elsewhere, a banner shown
80
231
  before `/init` answers would vanish for most of the world. The exception is a
81
232
  policy whose country and region rules all show the same banner, with a default
82
- rule that covers everywhere else: the browser resolves it at once. To skip the
83
- round trip for location-specific rules, pass the country your CDN or edge
84
- already knows through `inputs`.
233
+ rule that covers everywhere else: the browser resolves it at once.
85
234
 
86
235
  Policy and resolver code are part of the browser bundle.
87
236
 
@@ -134,130 +283,3 @@ The plugin can't see the options your app passes to `manifest()`. When the
134
283
  app passes `source: 'runtime'` or `manifestURL`, set
135
284
  `consentManifest({ source: 'runtime' })` too. The build then downloads no
136
285
  manifest, so it doesn't fail when the backend's `/manifest` is down.
137
-
138
- For policy updates without a rebuild, use `hosted({ backendURL })` from `c15t/react`
139
- as the mode and omit the build plugin. That setup resolves policy through the
140
- backend `/init` on each page load.
141
-
142
- ## Client-rendered apps
143
-
144
- The recommended [quickstart](./quickstart.md) bundles the
145
- manifest and resolves it in the browser. See [build-time manifests](#bundle-the-manifest-during-vite-builds).
146
-
147
- For policy updates without rebuilding, use `hosted()` from `c15t/react` as
148
- the mode. It reads the backend URL `consentManifest` read from
149
- `VITE_C15T_BACKEND_URL` or `VITE_INTH_PROJECT_URL`; without the plugin, pass
150
- `hosted({ backendURL })`.
151
- That runtime path works as follows:
152
-
153
- 1. The page loads with no consent state. Every optional category is denied.
154
- 2. `ConsentProvider` requests `GET /init` from your backend URL while it first
155
- renders, before the page paints. The backend picks the visitor's policy
156
- from their location. With an `experiment`, the request waits until the
157
- provider mounts, because it carries the visitor's arm.
158
- 3. The provider applies the policy and any stored choice. It shows the banner
159
- if the policy asks for one, and loads the scripts the visitor allowed.
160
-
161
- Until step 3 finishes, gated scripts and `ConsentGate` embeds stay blocked. If
162
- the backend request fails, they stay blocked and no banner shows.
163
-
164
- ## React Router, Remix and other server-rendered apps
165
-
166
- React Router framework mode, Remix and similar frameworks have no c15t adapter.
167
- Use the quickstart's `Consent` component and render it in your root component,
168
- around the outlet. In React Router framework mode that is `app/root.tsx`.
169
-
170
- `ConsentProvider` renders on the server without touching browser APIs, and the
171
- browser takes over after hydration:
172
-
173
- * The server HTML contains your page but no banner and no consent state.
174
- * After hydration the browser resolves the bundled manifest, then shows the
175
- banner if the policy asks for one. When the banner depends on a country or
176
- region the browser does not know, it calls `/init` and uses the backend's
177
- current policy and translations, which can differ from the build snapshot.
178
- With `hosted()`, it always calls `/init`.
179
- * The server render and the first browser render agree, so hydration does not
180
- warn or change a stored choice.
181
-
182
- The Vite setup needs no change. The banner renders in the browser and brings
183
- its own `<style>` element, so there is no stylesheet to import.
184
-
185
- ### Skip the browser's first init request
186
-
187
- This is optional. `fetchSSRData` from `c15t/react/server` calls the backend's
188
- `/init` from your server loader, forwarding the visitor's location and
189
- language headers. Pass the result to the browser, wrap it in a promise and give
190
- it to `hosted({ backendURL, initialData })`. The provider's first init then uses that
191
- response instead of a network request.
192
-
193
- Know its limits before you add it:
194
-
195
- * It saves the browser's round trip to the backend. It does not put the banner
196
- in the server HTML; the banner still mounts after hydration.
197
- * Every page view makes a server-to-server `/init` request. Do not cache the
198
- loader response, because it contains one visitor's policy.
199
- * It returns `undefined` when the request has no location or language headers,
200
- and the browser then requests `/init` as usual.
201
- * `mode` is read once, when the provider mounts. Create the `hosted()` mode once
202
- in the browser rather than on every render.
203
- * A relative `backendURL` resolves against the request's `host` header, over
204
- `https` for a domain name and over `http` for `localhost`, an IP address or
205
- a single-label host. `fetchSSRData` ignores `x-forwarded-host`,
206
- `x-forwarded-proto` and `forwarded`, and does not send them to the backend.
207
- Behind a proxy that overwrites those headers, pass
208
- `trustForwardedHeaders: true` to use them.
209
- * If your server answers requests for any `Host` header, such as a Node server
210
- exposed directly or a proxy that forwards `Host` from its default virtual
211
- host, pass `fetchSSRData` an absolute `backendURL`. A relative one resolves
212
- against whatever `Host` the request sent.
213
-
214
- ### Render the banner in the server HTML
215
-
216
- c15t has no framework-neutral helper for this. `ConsentProvider` accepts a
217
- server-resolved `prefetch` state, but the Next.js and TanStack Start adapters
218
- build that state from the request's cookie, headers and a cached policy
219
- manifest. Building it by hand ties your app to internal shapes. If the banner
220
- must be part of the first paint, use one of those adapters. Otherwise keep the
221
- browser path.
222
-
223
- ## Static and prerendered pages
224
-
225
- A prerendered page is the same HTML for every visitor, so it cannot contain one
226
- visitor's consent. Use the quickstart's build-time snapshot. Consent saves and
227
- any `/init` requests go to the backend URL directly, so the static host
228
- needs no server route. Add the site's
229
- origin to the trusted origins in your Inth project.
230
-
231
- Do not cache a response that contains a visitor's consent in a CDN or a shared
232
- page cache. [Data fetching](../../concepts/data-fetching.md) covers what is safe to
233
- cache.
234
-
235
- ## Where the browser sends requests
236
-
237
- With `hosted()`, the browser calls two backend endpoints:
238
-
239
- | Request | When |
240
- | --------------------- | ------------------------------------------ |
241
- | `GET {url}/init` | Once per page load, to resolve the policy |
242
- | `POST {url}/subjects` | When the visitor accepts, rejects or saves |
243
-
244
- With a bundled manifest, `manifest()` sends the same
245
- `/subjects` request, and requests `/init` only when the policy depends on a
246
- country or region the browser does not know.
247
-
248
- These requests go to the backend's origin, so it must list your site as a
249
- trusted origin.
250
- [Data fetching](../../concepts/data-fetching.md) compares this with proxies and
251
- custom transports.
252
-
253
- ## Check the rendering path
254
-
255
- 1. Open the page in a private window and view the page source. In a
256
- server-rendered app the HTML contains your content and no element with
257
- `data-testid="consent-banner-root"`.
258
- 2. In DevTools Network, confirm there is no `/manifest` request with a
259
- build-time snapshot. A regional policy may still need an `/init` request
260
- to resolve consent when geography is missing. With `hosted()`, one `/init`
261
- request runs unless you supply `initialData`.
262
- 3. The banner appears once the policy resolves. Vendor requests stay absent
263
- until you allow their category.
@@ -20,13 +20,13 @@ import {
20
20
  ConsentDialog,
21
21
  ConsentDialogLink,
22
22
  ConsentProvider,
23
- manifest,
23
+ hosted,
24
24
  } from 'c15t/react';
25
25
  import type { ReactNode } from 'react';
26
26
 
27
27
  const options = {
28
- // The policy the build downloaded from VITE_C15T_BACKEND_URL.
29
- mode: manifest(),
28
+ // Asks VITE_C15T_BACKEND_URL for each visitor's policy.
29
+ mode: hosted(),
30
30
  scripts: [
31
31
  posthog({
32
32
  id: 'phc_your_project_key',
@@ -5,6 +5,23 @@ description: Fix a missing banner, early vendor requests, unstyled components
5
5
  group: frameworks
6
6
  ---
7
7
 
8
+ ## There is no banner
9
+
10
+ **Check.** In DevTools Network, find the `GET` request to `/init` on your
11
+ backend URL. With `hosted()`, the browser sends it while the provider first
12
+ renders. With a bundled `manifest()`, the browser makes no request before the
13
+ banner unless the policy depends on a country or region it does not know.
14
+
15
+ | What you see | Cause | Fix |
16
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | No request and no banner | `ConsentProvider` is not mounted, an error stopped `src/consent.tsx` from rendering, or the bundled policy asks for no banner | Check the console for errors, render one provider at the app root as in the [quickstart](./quickstart.md), and inspect the active policy with the component below |
18
+ | `/init` fails with a CORS error | Your site's origin is not trusted | Add the exact origin, including port in development, to the trusted origins in your Inth project |
19
+ | `/init` returns 200 | The policy for this visitor asks for no banner, or a stored choice is still valid | Inspect the active policy with the component below, or clear site data |
20
+
21
+ A policy with `prompt: 'none'` shows no banner by design, and so does a
22
+ returning visitor with a valid stored choice. [Policies](../../concepts/policies.md)
23
+ explains why a region may not show one.
24
+
8
25
  ## Why does the build fail to download the manifest?
9
26
 
10
27
  **Check.** `vite build` stops with an error from `consentManifest` that
@@ -59,22 +76,6 @@ requests `${backendURL}/manifest` on page load.
59
76
  Fix the cause, then restart `vite dev` or rebuild. The plugin fetches once
60
77
  when Vite starts.
61
78
 
62
- ## There is no banner
63
-
64
- **Check.** In DevTools Network, find the `GET` request to `/init` on your
65
- backend URL. With the bundled manifest, the browser makes no request before
66
- the banner unless the policy depends on a country or region it does not know.
67
-
68
- | What you see | Cause | Fix |
69
- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
- | No request and no banner | `ConsentProvider` is not mounted, an error stopped `src/consent.tsx` from rendering, or the bundled policy asks for no banner | Check the console for errors, render one provider at the app root as in the [quickstart](./quickstart.md), and inspect the active policy with the component below |
71
- | `/init` fails with a CORS error | Your site's origin is not trusted | Add the exact origin, including port in development, to the trusted origins in your Inth project |
72
- | `/init` returns 200 | The policy for this visitor asks for no banner, or a stored choice is still valid | Inspect the active policy with the component below, or clear site data |
73
-
74
- A policy with `prompt: 'none'` shows no banner by design, and so does a
75
- returning visitor with a valid stored choice. [Policies](../../concepts/policies.md)
76
- explains why a region may not show one.
77
-
78
79
  ## The page throws that no IABProvider is mounted
79
80
 
80
81
  The visitor's policy uses the `iab` model and the backend sent its vendor
@@ -255,10 +255,11 @@ projects use URLs such as `https://your-project.inth.app`. The
255
255
  [React quickstart](./quickstart.md) shows the full provider,
256
256
  and [data fetching](../../concepts/data-fetching.md) covers custom transports.
257
257
 
258
- v3 also adds `manifest()`, which the quickstart uses: `consentManifest()` from
259
- `c15t/build` downloads the policy when Vite builds, and the browser resolves it
260
- without asking `/init`. `manifest()` and `hosted()` then read the backend URL
261
- from `VITE_C15T_BACKEND_URL`, so they need no arguments. See
258
+ The quickstart keeps `hosted()` and adds `consentManifest()` from
259
+ `c15t/build` to `vite.config.ts`. `hosted()` then reads the backend URL from
260
+ `VITE_C15T_BACKEND_URL`, so it needs no arguments. v3 also adds `manifest()`,
261
+ which bundles the policy when Vite builds. It skips `/init` only when the
262
+ policy has no location rules or the page knows the visitor's location. See
262
263
  [consent modes](../../concepts/modes.md).
263
264
 
264
265
  Offline mode keeps choices in the browser and records nothing on a server. Not