@c15t/react 3.0.0-alpha.8 → 3.0.0

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 (164) hide show
  1. package/AGENTS.md +10 -12
  2. package/README.md +41 -70
  3. package/SKILL.md +1 -1
  4. package/dist/aggregate-components.js +1 -1
  5. package/dist/components/consent-gate/atoms.js +1 -1
  6. package/dist/components/consent-gate/frame.js +2 -0
  7. package/dist/components/consent-gate/index.js +1 -1
  8. package/dist/components/iab-panel/atoms/root.js +1 -1
  9. package/dist/components/iab-panel/iab-panel.js +1 -1
  10. package/dist/components/iab-prompt/atoms/root.js +1 -1
  11. package/dist/components/iab-prompt/iab-prompt.js +1 -1
  12. package/dist/components/panel/atoms/root.js +1 -1
  13. package/dist/components/panel-trigger/atoms/root.js +1 -1
  14. package/dist/components/panel-trigger/panel-trigger.js +1 -1
  15. package/dist/components/panel-trigger/toolbar-loader.js +2 -0
  16. package/dist/components/preferences/atoms/root.js +1 -1
  17. package/dist/components/prompt/atoms/early-tap-script.js +2 -0
  18. package/dist/components/prompt/atoms/root.js +1 -1
  19. package/dist/components/prompt/prompt.js +1 -1
  20. package/dist/components/shared/dialog-sheets.js +1 -0
  21. package/dist/components/shared/first-paint-sheets.js +1 -0
  22. package/dist/components/shared/iab-dialog-sheets.js +1 -0
  23. package/dist/components/shared/iab-first-paint-sheets.js +1 -0
  24. package/dist/components/shared/primitives/button.js +1 -1
  25. package/dist/components/shared/surface-styles.js +2 -0
  26. package/dist/consent-gate.js +1 -1
  27. package/dist/context/index.js +1 -1
  28. package/dist/devtools.js +1 -1
  29. package/dist/hooks/use-iab-policy-guard.js +1 -0
  30. package/dist/iab/styles.tw3.css +2 -2
  31. package/dist/iab-context.js +1 -1
  32. package/dist/iab-dialog-deferred.js +2 -0
  33. package/dist/iab.js +1 -1
  34. package/dist/index-parts/consent-gate.js +1 -1
  35. package/dist/index-parts/core.js +1 -1
  36. package/dist/index-parts/frame.js +1 -0
  37. package/dist/index-parts/hosted.js +1 -0
  38. package/dist/index-parts/offline.js +1 -0
  39. package/dist/index-parts/presets.js +1 -0
  40. package/dist/index.js +1 -1
  41. package/dist/modes.js +1 -0
  42. package/dist/panel-link.js +1 -1
  43. package/dist/panel-trigger.js +1 -1
  44. package/dist/provider.js +1 -1
  45. package/dist/styles.tw3.css +2 -2
  46. package/dist/transports/hosted.js +1 -0
  47. package/dist/transports/manifest.js +1 -0
  48. package/dist/utils/deferred-module.js +1 -1
  49. package/dist/version.js +1 -1
  50. package/dist-types/chunk-warming.d.ts +7 -6
  51. package/dist-types/components/consent-gate/frame.d.ts +19 -0
  52. package/dist-types/components/consent-gate/index.d.ts +3 -19
  53. package/dist-types/components/iab-prompt/iab-prompt.d.ts +1 -1
  54. package/dist-types/components/panel/index.d.ts +1 -1
  55. package/dist-types/components/panel-trigger/toolbar-loader.d.ts +34 -0
  56. package/dist-types/components/preferences/index.d.ts +1 -1
  57. package/dist-types/components/prompt/atoms/early-tap-script.d.ts +17 -0
  58. package/dist-types/components/prompt/index.d.ts +1 -1
  59. package/dist-types/components/shared/dialog-sheets.d.ts +9 -0
  60. package/dist-types/components/shared/first-paint-sheets.d.ts +8 -0
  61. package/dist-types/components/shared/iab-dialog-sheets.d.ts +3 -0
  62. package/dist-types/components/shared/iab-first-paint-sheets.d.ts +3 -0
  63. package/dist-types/components/shared/surface-styles.d.ts +60 -0
  64. package/dist-types/consent-gate.d.ts +1 -1
  65. package/dist-types/context/index.d.ts +3 -3
  66. package/dist-types/context.d.ts +2 -0
  67. package/dist-types/gpp.d.ts +1 -1
  68. package/dist-types/hooks/use-iab-policy-guard.d.ts +35 -0
  69. package/dist-types/iab-dialog-deferred.d.ts +36 -0
  70. package/dist-types/iab.d.ts +3 -2
  71. package/dist-types/index-parts/consent-gate.d.ts +1 -1
  72. package/dist-types/index-parts/core.d.ts +1 -2
  73. package/dist-types/index-parts/frame.d.ts +7 -0
  74. package/dist-types/index-parts/hosted.d.ts +7 -0
  75. package/dist-types/index-parts/offline.d.ts +7 -0
  76. package/dist-types/index-parts/presets.d.ts +7 -0
  77. package/dist-types/index.d.ts +5 -1
  78. package/dist-types/modes.d.ts +26 -0
  79. package/dist-types/panel-link.d.ts +1 -1
  80. package/dist-types/panel-trigger.d.ts +1 -1
  81. package/dist-types/provider.d.ts +22 -4
  82. package/dist-types/transports/hosted.d.ts +36 -0
  83. package/dist-types/transports/manifest.d.ts +48 -0
  84. package/dist-types/ui-config-context.d.ts +4 -0
  85. package/dist-types/version.d.ts +1 -1
  86. package/docs/README.md +10 -12
  87. package/docs/concepts/choose-your-setup.md +45 -8
  88. package/docs/concepts/consent-categories.md +1 -1
  89. package/docs/concepts/consent-state.md +5 -2
  90. package/docs/concepts/data-fetching.md +100 -40
  91. package/docs/concepts/modes.md +281 -0
  92. package/docs/customization/dark-mode.md +1 -1
  93. package/docs/customization/motion.md +2 -2
  94. package/docs/customization/overview.md +3 -2
  95. package/docs/customization/recipes.md +4 -4
  96. package/docs/customization/slots.md +25 -25
  97. package/docs/customization/stylesheets.md +161 -50
  98. package/docs/customization/tailwind.md +71 -50
  99. package/docs/customization/tokens.md +42 -13
  100. package/docs/frameworks/react/components/consent-banner.md +14 -12
  101. package/docs/frameworks/react/components/consent-dialog.md +15 -13
  102. package/docs/frameworks/react/components/consent-provider.md +51 -46
  103. package/docs/frameworks/react/components.md +16 -13
  104. package/docs/frameworks/react/compose.md +3 -3
  105. package/docs/frameworks/react/content-security-policy.md +35 -12
  106. package/docs/frameworks/react/customize.md +21 -13
  107. package/docs/frameworks/react/hooks.md +2 -2
  108. package/docs/frameworks/react/iab.md +56 -6
  109. package/docs/frameworks/react/quickstart.md +65 -99
  110. package/docs/frameworks/react/rendering.md +170 -94
  111. package/docs/frameworks/react/scripts.md +19 -30
  112. package/docs/frameworks/react/troubleshooting.md +100 -32
  113. package/docs/frameworks/react/upgrade-v3.md +133 -79
  114. package/docs/frameworks/react/vendor-consent.md +19 -7
  115. package/docs/guides/banner-experiments.md +21 -8
  116. package/docs/guides/troubleshooting.md +90 -26
  117. package/docs/guides/verify-consent.md +23 -8
  118. package/docs/integrations/adobe-analytics.md +60 -64
  119. package/docs/integrations/ahrefs-analytics.md +58 -63
  120. package/docs/integrations/amplitude.md +60 -64
  121. package/docs/integrations/clearbit.md +59 -63
  122. package/docs/integrations/cloudflare-web-analytics.md +56 -60
  123. package/docs/integrations/cloudflare-zaraz.md +55 -57
  124. package/docs/integrations/crisp.md +64 -69
  125. package/docs/integrations/databuddy.md +62 -64
  126. package/docs/integrations/fathom-analytics.md +56 -60
  127. package/docs/integrations/front-chat.md +56 -60
  128. package/docs/integrations/google-maps.md +25 -13
  129. package/docs/integrations/google-tag-manager.md +153 -77
  130. package/docs/integrations/google-tag.md +134 -78
  131. package/docs/integrations/heap.md +56 -60
  132. package/docs/integrations/hightouch.md +55 -60
  133. package/docs/integrations/hotjar.md +60 -64
  134. package/docs/integrations/intercom.md +55 -60
  135. package/docs/integrations/klaviyo.md +65 -66
  136. package/docs/integrations/linkedin-insights.md +58 -63
  137. package/docs/integrations/logrocket.md +55 -60
  138. package/docs/integrations/matomo-analytics.md +58 -60
  139. package/docs/integrations/meta-pixel.md +56 -60
  140. package/docs/integrations/microsoft-clarity.md +57 -60
  141. package/docs/integrations/microsoft-uet.md +58 -61
  142. package/docs/integrations/mixpanel-analytics.md +73 -75
  143. package/docs/integrations/one-dollar-stats.md +53 -59
  144. package/docs/integrations/openai-pixel.md +58 -60
  145. package/docs/integrations/overview.md +59 -42
  146. package/docs/integrations/pinterest-tag.md +56 -60
  147. package/docs/integrations/pirsch.md +54 -60
  148. package/docs/integrations/plausible-analytics.md +55 -60
  149. package/docs/integrations/posthog.md +56 -60
  150. package/docs/integrations/promptwatch.md +57 -63
  151. package/docs/integrations/reddit-pixel.md +57 -60
  152. package/docs/integrations/rudderstack.md +65 -67
  153. package/docs/integrations/rybbit-analytics.md +70 -75
  154. package/docs/integrations/segment.md +56 -60
  155. package/docs/integrations/sentry.md +65 -60
  156. package/docs/integrations/snapchat-pixel.md +54 -60
  157. package/docs/integrations/tiktok-pixel.md +59 -63
  158. package/docs/integrations/umami-analytics.md +54 -60
  159. package/docs/integrations/vercel-analytics.md +53 -59
  160. package/docs/integrations/vercel-speed-insights.md +317 -0
  161. package/docs/integrations/x-pixel.md +57 -63
  162. package/docs/integrations/youtube.md +25 -13
  163. package/package.json +18 -8
  164. package/docs/upgrade-v3.md +0 -429
@@ -21,10 +21,11 @@ Install `@c15t/iab` alongside your framework adapter, then mount the IAB
21
21
  components inside an existing consent provider connected to Inth.
22
22
 
23
23
  Import the components from `c15t/react/iab` and render them inside your
24
- existing `ConsentProvider` from `c15t/react`. Import `c15t/react/iab/styles.css`
25
- at your global stylesheet entry, after `c15t/react/styles.css`: the IAB
26
- stylesheet holds only the IAB rules and uses the tokens from the base
27
- stylesheet.
24
+ existing `ConsentProvider` from `c15t/react`.
25
+
26
+ The IAB banner and dialog render their own styles by default. Leave the
27
+ provider's `styles` option enabled and render the components without a CSS
28
+ import:
28
29
 
29
30
  ```tsx title="src/iab-consent.tsx"
30
31
  import {
@@ -52,8 +53,38 @@ Global Vendor List response before changing rendering conditions.
52
53
 
53
54
  IAB presentation follows `presentation.prompt.blocking` and
54
55
  `presentation.preferences.blocking`. A blocking dialog needs focus management,
55
- scroll locking and a backdrop together. See [migration](../../upgrade-v3.md) for
56
- legacy option behavior.
56
+ scroll locking and a backdrop together.
57
+
58
+ ## Mount IABProvider for every IAB policy
59
+
60
+ IAB is opt-in. The standard `ConsentBanner` and `ConsentDialog` do not
61
+ handle the IAB model, so when a visitor's policy uses it, the backend
62
+ sends its vendor list and the app has no `IABProvider`, they throw an
63
+ `IABUnavailableError` (code `C15T_IAB_UNAVAILABLE`):
64
+ `c15t: this visitor's policy uses IAB TCF, but no <IABProvider> is mounted`.
65
+ The error happens during render, on the server and in the browser, so
66
+ your error boundary or the framework's error overlay shows it. Render `IABProvider` from
67
+ `c15t/react/iab` for those visitors, or remove the `iab` model from the
68
+ policy.
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.
57
88
 
58
89
  ## Verify vendor and purpose choices
59
90
 
@@ -62,6 +93,25 @@ feature controls used by your integration. Confirm the TC String and `__tcfapi`
62
93
  reflect the saved state, and verify the vendor's actual requests. A category-only
63
94
  banner test does not establish that the IAB flow works.
64
95
 
96
+ ## Load an external stylesheet
97
+
98
+ With Tailwind CSS 3, or when you want to put c15t in a named cascade layer,
99
+ import `c15t/react/styles.css` and then `c15t/react/iab/styles.css` at your
100
+ global stylesheet entry. The IAB stylesheet holds the IAB rules and uses the
101
+ base stylesheet's tokens. Set `styles: false` so the components use those
102
+ stylesheets:
103
+
104
+ ```tsx title="src/consent.tsx (partial)"
105
+ import 'c15t/react/styles.css';
106
+ import 'c15t/react/iab/styles.css';
107
+
108
+ // In the ConsentProvider options:
109
+ styles: false,
110
+ ```
111
+
112
+ Tailwind CSS 3 also needs the
113
+ [PostCSS plugin](../../customization/tailwind.md#set-up-tailwind-css-3).
114
+
65
115
  ## Restrict how vendors use a purpose
66
116
 
67
117
  Publisher restrictions override a vendor's declared use of one purpose. Pass
@@ -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://c15t.com/docs/frameworks). Their adapters resolve consent on the
@@ -28,91 +27,52 @@ 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
 
36
- | Package manager | Command |
37
- | :-------------- | :-------------------------------------------------------------------- |
38
- | npm | `npm install c15t@alpha @c15t/browser@alpha @c15t/integrations@alpha` |
39
- | pnpm | `pnpm add c15t@alpha @c15t/browser@alpha @c15t/integrations@alpha` |
40
- | yarn | `yarn add c15t@alpha @c15t/browser@alpha @c15t/integrations@alpha` |
41
- | bun | `bun add c15t@alpha @c15t/browser@alpha @c15t/integrations@alpha` |
35
+ | Package manager | Command |
36
+ | :-------------- | :------------------------------------ |
37
+ | npm | `npm install c15t @c15t/integrations` |
38
+ | pnpm | `pnpm add c15t @c15t/integrations` |
39
+ | yarn | `yarn add c15t @c15t/integrations` |
40
+ | bun | `bun add c15t @c15t/integrations` |
42
41
 
43
42
  `c15t` contains the React adapter at `c15t/react` and the Vite plugin at
44
- `c15t/build`. `@c15t/browser` resolves the bundled policy in the browser.
45
- `@c15t/integrations` holds the vendor loaders used below.
43
+ `c15t/build`. `@c15t/integrations` holds the vendor loaders used below.
46
44
 
47
- ## Bundle your policy
45
+ ## Add the Vite plugin
48
46
 
49
47
  Add `consentManifest` to `vite.config.ts`:
50
48
 
51
49
  ```ts title="vite.config.ts"
52
50
  import react from '@vitejs/plugin-react';
53
51
  import { consentManifest } from 'c15t/build';
54
- import { defineConfig, loadEnv } from 'vite';
55
-
56
- export default defineConfig(({ mode }) => {
57
- const env = loadEnv(mode, process.cwd(), 'VITE_');
58
- return {
59
- plugins: [
60
- react(),
61
- // Downloads the policy and writes src/c15t-manifest.ts.
62
- consentManifest({
63
- backendURL:
64
- env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
65
- }),
66
- ],
67
- };
68
- });
69
- ```
70
-
71
- `https://benchmarks-inth.inth.app` is a demo project, so the snippet runs as
72
- is. Replace it with your project's backend URL, exactly as Inth shows it,
73
- including any path prefix, or set `VITE_C15T_BACKEND_URL` when you build.
74
-
75
- `consentManifest` downloads your project's public policy from `/manifest` when
76
- `vite build` or `vite dev` starts, and writes it to `src/c15t-manifest.ts`.
77
- Add that file to `.gitignore`.
78
-
79
- The snapshot is fixed at build time:
80
-
81
- * Rebuild after changing policies, translations or vendors. If your CI caches
82
- build output, force a fresh build.
83
- * The build must reach the backend. A failed fetch fails the build.
84
- * Consent choices still go to the backend.
85
-
86
- Run `vite build` or `vite dev` before a standalone `tsc`, `vue-tsc` or
87
- `svelte-check`; the generated file doesn't exist on a fresh checkout. Both
88
- commands fetch the manifest when they start, so both need the backend.
89
- `vite preview` serves the last build without fetching.
52
+ import { defineConfig } from 'vite';
90
53
 
91
- ## Register your vendor scripts
92
-
93
- Create `src/scripts.ts`. PostHog waits for measurement permission, so your
94
- policy must include that category. Remove any other PostHog loader from your
95
- app so PostHog loads once.
54
+ export default defineConfig({ plugins: [react(), consentManifest()] });
55
+ ```
96
56
 
97
- ```ts title="src/scripts.ts"
98
- import { posthog } from '@c15t/integrations/posthog';
57
+ The plugin reads `VITE_C15T_BACKEND_URL`. Set it to your project's backend URL,
58
+ exactly as Inth shows it, including any path prefix, in `.env` or wherever you
59
+ build:
99
60
 
100
- export const scripts = [
101
- posthog({
102
- id: 'phc_your_project_key',
103
- initOptions: { cookieless_mode: 'never' },
104
- loadMode: 'after-consent',
105
- }),
106
- ];
61
+ ```sh title=".env"
62
+ VITE_C15T_BACKEND_URL=https://your-project.inth.app
107
63
  ```
108
64
 
109
- Replace `phc_your_project_key` with your PostHog project key. PostHog uses its
110
- EU host; add `region: 'us'` for a US project.
65
+ If your app already sets `VITE_INTH_PROJECT_URL` for other Inth SDKs, that works
66
+ too. When both are set, `VITE_C15T_BACKEND_URL` wins.
111
67
 
112
- `loadMode: 'after-consent'` keeps the PostHog SDK off the page until the
113
- visitor allows measurement. `cookieless_mode: 'never'` turns off PostHog's
114
- cookieless capture. See [integrations](../../integrations/overview.md) for other
115
- vendors.
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 })`.
71
+
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.
116
76
 
117
77
  ## Mount the provider
118
78
 
@@ -122,28 +82,30 @@ asks for them, and `ConsentDialogLink` keeps preferences reachable after the
122
82
  banner closes.
123
83
 
124
84
  ```tsx title="src/consent.tsx"
125
- import { manifest } from '@c15t/browser/headless';
85
+ import { posthog } from '@c15t/integrations/posthog';
126
86
  import {
127
87
  ConsentBanner,
128
88
  ConsentDialog,
129
89
  ConsentDialogLink,
130
90
  ConsentProvider,
91
+ hosted,
131
92
  } from 'c15t/react';
132
93
  import type { ReactNode } from 'react';
133
94
 
134
- import { consentManifest } from './c15t-manifest';
135
- import { scripts } from './scripts';
136
-
137
- import 'c15t/react/styles.css';
138
-
139
- const mode = manifest({
140
- backendURL:
141
- import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
142
- manifest: consentManifest,
143
- });
95
+ const options = {
96
+ // Asks VITE_C15T_BACKEND_URL for each visitor's policy.
97
+ mode: hosted(),
98
+ scripts: [
99
+ posthog({
100
+ id: 'phc_your_project_key',
101
+ initOptions: { cookieless_mode: 'never' },
102
+ loadMode: 'after-consent',
103
+ }),
104
+ ],
105
+ };
144
106
 
145
107
  export const Consent = ({ children }: { children: ReactNode }) => (
146
- <ConsentProvider options={{ mode, scripts }}>
108
+ <ConsentProvider options={options}>
147
109
  {children}
148
110
  <ConsentBanner />
149
111
  <ConsentDialog />
@@ -154,17 +116,21 @@ export const Consent = ({ children }: { children: ReactNode }) => (
154
116
  );
155
117
  ```
156
118
 
157
- `manifest()` resolves the policy from the bundled snapshot in the browser,
158
- with no `/manifest` request. Use the same backend URL as in
159
- `vite.config.ts`: consent choices go to it, and so does an `/init` request
160
- when the policy depends on the visitor's country or region and the browser
161
- does not know it. That request returns the backend's current policy, which can
162
- be newer than the snapshot. Vite reads `VITE_C15T_BACKEND_URL` at build time
163
- for both files, so set it before you build, not when you serve the files.
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.
164
124
 
165
- To apply policy edits without rebuilding, remove `consentManifest` and use
166
- `hosted({ url })` from `c15t/react` as the mode. The browser then requests the
167
- policy from `/init` on every page load.
125
+ PostHog waits for measurement permission, so your policy must include that
126
+ category. Replace `phc_your_project_key` with your PostHog project key.
127
+ PostHog uses its EU host; add `region: 'us'` for a US project. Remove any
128
+ other PostHog loader from your app so PostHog loads once.
129
+
130
+ `loadMode: 'after-consent'` keeps the PostHog SDK off the page until the
131
+ visitor allows measurement. `cookieless_mode: 'never'` turns off PostHog's
132
+ cookieless capture. See [integrations](../../integrations/overview.md) for other
133
+ vendors.
168
134
 
169
135
  Wrap your app with it in `src/main.tsx`:
170
136
 
@@ -187,8 +153,8 @@ createRoot(root).render(
187
153
 
188
154
  Keep `Consent` mounted for the life of the app. If you use a client-side
189
155
  router, render the router inside `Consent` so navigation does not remount the
190
- provider. The stylesheet import in `src/consent.tsx` styles the banner and
191
- dialog; import it once.
156
+ provider. There is no stylesheet to import: the banner and dialog render their
157
+ own rules as `<style>` elements.
192
158
 
193
159
  ## Gate your own features
194
160
 
@@ -214,7 +180,8 @@ Build and preview the production bundle with `vite build` and `vite preview`,
214
180
  then open the site in a private window with DevTools open on the Network tab.
215
181
  Test under a policy that asks for a choice, such as an EU opt-in policy.
216
182
 
217
- 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
218
185
  are no requests to `posthog.com`.
219
186
  2. **Reject All.** The banner closes and the vendor requests stay absent.
220
187
  3. **Reload.** The banner stays closed and the vendor requests stay absent.
@@ -223,8 +190,7 @@ Test under a policy that asks for a choice, such as an EU opt-in policy.
223
190
  save. PostHog's `array.js` loads.
224
191
  5. **Turn Analytics off again.** The page reloads and PostHog does not load.
225
192
 
226
- If the banner never appears, check the generated policy and any location
227
- 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)
228
194
  covers the common causes. [Verify consent](../../guides/verify-consent.md) has the
229
195
  full release checklist.
230
196
 
@@ -23,90 +23,50 @@ 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
27
-
28
- The [quickstart](./quickstart.md) and `examples/react` in
29
- the c15t repository use this setup.
30
-
31
- The build fetches your public policy once and bundles it, so the server never
32
- fetches it at runtime.
33
-
34
- Add `consentManifest` to your existing Vite plugin list:
35
-
36
- ```ts title="vite.config.ts (plugins option)"
37
- import { consentManifest } from 'c15t/build';
38
-
39
- export default {
40
- plugins: [
41
- // Keep your existing framework plugins here.
42
- consentManifest({ backendURL: 'https://your-project.inth.app' }),
43
- ],
44
- };
45
- ```
46
-
47
- Vite writes `src/c15t-manifest.ts` before compiling, including at development
48
- startup. Keep the generated file out of Git. Install `@c15t/browser@alpha` for
49
- its browser manifest transport. In `src/consent.tsx`, create the provider's
50
- mode with `manifest()` instead of `hosted()`:
51
-
52
- ```tsx title="src/consent.tsx"
53
- import { manifest } from '@c15t/browser/headless';
54
-
55
- import { consentManifest } from './c15t-manifest';
56
-
57
- const mode = manifest({
58
- backendURL: 'https://your-project.inth.app',
59
- manifest: consentManifest,
60
- });
61
- ```
62
-
63
- Keep passing `mode` to `ConsentProvider`. The browser resolves the snapshot
64
- without `/manifest` when it has the geography the policy needs, or the policy
65
- does not depend on geography. Supply known country and region through `inputs`
66
- or overrides to keep regional resolution on the snapshot.
67
-
68
- When the required country or region is missing, the transport calls backend
69
- `/init` and uses its resolved policy and translations. That response can use
70
- newer backend policy than the build snapshot. This is a full consent resolution,
71
- so these visitors still wait for a backend round trip.
72
-
73
- Policy and resolver code are part of the browser bundle.
74
-
75
- The snapshot is fixed at build time:
76
-
77
- * Rebuild after changing policies, translations or vendors. If your CI caches
78
- build output, force a fresh build.
79
- * The build must reach the backend. A failed fetch fails the build.
80
- * Consent choices still go to the backend.
81
-
82
- Run `vite build` or `vite dev` before a standalone `tsc`, `vue-tsc` or
83
- `svelte-check`; the generated file doesn't exist on a fresh checkout. Both
84
- commands fetch the manifest when they start, so both need the backend.
85
- `vite preview` serves the last build without fetching.
86
-
87
- For policy updates without a rebuild, use `hosted({ url })` from `c15t/react`
88
- as the mode and omit the build plugin. That setup resolves policy through the
89
- backend `/init` on each page load.
90
-
91
26
  ## Client-rendered apps
92
27
 
93
- The recommended [quickstart](./quickstart.md) bundles the
94
- manifest and resolves it in the browser. See [build-time manifests](#bundle-the-manifest-during-vite-builds).
95
-
96
- For policy updates without rebuilding, use `hosted({ url })` from
97
- `c15t/react` as the mode. That runtime path works as follows:
28
+ The [quickstart](./quickstart.md) and `examples/react` in
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:
98
34
 
99
35
  1. The page loads with no consent state. Every optional category is denied.
100
- 2. `ConsentProvider` requests `GET /init` from your backend URL while it first
101
- renders, before the page paints. The backend picks the visitor's policy
102
- from their location. With an `experiment`, the request waits until the
103
- provider mounts, because it carries the visitor's arm.
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.
104
42
  3. The provider applies the policy and any stored choice. It shows the banner
105
43
  if the policy asks for one, and loads the scripts the visitor allowed.
106
44
 
107
45
  Until step 3 finishes, gated scripts and `ConsentGate` embeds stay blocked. If
108
46
  the backend request fails, they stay blocked and no banner shows.
109
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
+
110
70
  ## React Router, Remix and other server-rendered apps
111
71
 
112
72
  React Router framework mode, Remix and similar frameworks have no c15t adapter.
@@ -117,23 +77,26 @@ around the outlet. In React Router framework mode that is `app/root.tsx`.
117
77
  browser takes over after hydration:
118
78
 
119
79
  * The server HTML contains your page but no banner and no consent state.
120
- * After hydration the browser resolves the bundled manifest, then shows the
121
- banner if the policy asks for one. When required country or region is
122
- missing, it calls `/init` and uses the backend's current policy and
123
- translations, which can differ from the build snapshot. With `hosted()`,
124
- it always calls `/init`.
80
+ * After hydration the browser calls `/init`, then shows the banner if the
81
+ policy asks for one.
125
82
  * The server render and the first browser render agree, so hydration does not
126
83
  warn or change a stored choice.
127
84
 
128
- One detail differs from the Vite setup: import `c15t/react/styles.css` where
129
- your framework loads global CSS, so the server HTML links it.
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.
130
93
 
131
94
  ### Skip the browser's first init request
132
95
 
133
96
  This is optional. `fetchSSRData` from `c15t/react/server` calls the backend's
134
97
  `/init` from your server loader, forwarding the visitor's location and
135
98
  language headers. Pass the result to the browser, wrap it in a promise and give
136
- it to `hosted({ url, initialData })`. The provider's first init then uses that
99
+ it to `hosted({ backendURL, initialData })`. The provider's first init then uses that
137
100
  response instead of a network request.
138
101
 
139
102
  Know its limits before you add it:
@@ -169,9 +132,9 @@ browser path.
169
132
  ## Static and prerendered pages
170
133
 
171
134
  A prerendered page is the same HTML for every visitor, so it cannot contain one
172
- visitor's consent. Use the quickstart's build-time snapshot. Consent saves and
173
- any `/init` requests go to the backend URL directly, so the static host
174
- needs no server route. Add the site's
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
175
138
  origin to the trusted origins in your Inth project.
176
139
 
177
140
  Do not cache a response that contains a visitor's consent in a CDN or a shared
@@ -180,17 +143,13 @@ cache.
180
143
 
181
144
  ## Where the browser sends requests
182
145
 
183
- With `hosted({ url })`, the browser calls two backend endpoints:
146
+ With `hosted()`, the browser calls two backend endpoints:
184
147
 
185
148
  | Request | When |
186
149
  | --------------------- | ------------------------------------------ |
187
150
  | `GET {url}/init` | Once per page load, to resolve the policy |
188
151
  | `POST {url}/subjects` | When the visitor accepts, rejects or saves |
189
152
 
190
- With a bundled manifest, `manifest({ backendURL })` sends the same
191
- `/subjects` request, and requests `/init` only when the policy depends on a
192
- country or region the browser does not know.
193
-
194
153
  These requests go to the backend's origin, so it must list your site as a
195
154
  trusted origin.
196
155
  [Data fetching](../../concepts/data-fetching.md) compares this with proxies and
@@ -201,9 +160,126 @@ custom transports.
201
160
  1. Open the page in a private window and view the page source. In a
202
161
  server-rendered app the HTML contains your content and no element with
203
162
  `data-testid="consent-banner-root"`.
204
- 2. In DevTools Network, confirm there is no `/manifest` request with a
205
- build-time snapshot. A regional policy may still need an `/init` request
206
- to resolve consent when geography is missing. With `hosted()`, one `/init`
207
- request runs unless you supply `initialData`.
163
+ 2. In DevTools Network, confirm one `/init` request runs, unless you supply
164
+ `initialData`.
208
165
  3. The banner appears once the policy resolves. Vendor requests stay absent
209
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.
183
+
184
+ The build fetches your public policy once and bundles it, so the server never
185
+ fetches it at runtime.
186
+
187
+ Keep `consentManifest` in your Vite plugin list:
188
+
189
+ ```ts title="vite.config.ts (plugins option)"
190
+ import { consentManifest } from 'c15t/build';
191
+
192
+ export default {
193
+ plugins: [
194
+ // Keep your existing framework plugins here.
195
+ consentManifest({ backendURL: 'https://your-project.inth.app' }),
196
+ ],
197
+ };
198
+ ```
199
+
200
+ Without `backendURL`, the plugin reads `VITE_C15T_BACKEND_URL`, then
201
+ `VITE_INTH_PROJECT_URL`. The plugin
202
+ fetches the policy during `vite build`, or in `vite dev` when the app first
203
+ loads it, and serves it as `snapshot` from `c15t/generated`. A build fetches
204
+ only when the app uses `manifest()`. In `src/consent.tsx`, create the
205
+ provider's mode with `manifest()` from `c15t/react` instead of `hosted()`. It
206
+ reads the snapshot and the backend URL from `c15t/generated`:
207
+
208
+ ```tsx title="src/consent.tsx"
209
+ import { manifest } from 'c15t/react';
210
+
211
+ const mode = manifest();
212
+ ```
213
+
214
+ Keep passing `mode` to `ConsentProvider`. The browser resolves the snapshot
215
+ without `/manifest` when it has the geography the policy needs, or the policy
216
+ does not depend on geography. Supply known country and region through `inputs`
217
+ or overrides to keep regional resolution on the snapshot.
218
+
219
+ When the build has no snapshot, as in `vite dev` after a failed fetch,
220
+ `snapshot` is `undefined`, and the browser fetches
221
+ `${backendURL}/manifest` instead.
222
+
223
+ When the required country or region is missing, the transport calls backend
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.
228
+
229
+ The browser cannot guess the banner from the snapshot alone when locations
230
+ differ. With a European opt-in rule and no banner elsewhere, a banner shown
231
+ before `/init` answers would vanish for most of the world. The exception is a
232
+ policy whose country and region rules all show the same banner, with a default
233
+ rule that covers everywhere else: the browser resolves it at once.
234
+
235
+ Policy and resolver code are part of the browser bundle.
236
+
237
+ The snapshot is fixed at build time:
238
+
239
+ * Rebuild after changing policies, translations or vendors. If your CI caches
240
+ build output, force a fresh build.
241
+ * Consent choices still go to the backend.
242
+
243
+ The build reads the backend URL from your public backend URL variable when
244
+ the config doesn't pass one: `NEXT_PUBLIC_C15T_BACKEND_URL` in Next.js,
245
+ `NUXT_PUBLIC_C15T_BACKEND_URL` in Nuxt, `PUBLIC_C15T_BACKEND_URL` in Astro,
246
+ Svelte and SvelteKit, and `VITE_C15T_BACKEND_URL` in TanStack Start and other
247
+ Vite apps. Each also reads the matching Inth variable, such as
248
+ `NEXT_PUBLIC_INTH_PROJECT_URL`, when the c15t one is unset. See
249
+ [set the backend URL](../../concepts/modes.md#set-the-backend-url).
250
+
251
+ The fetch waits at most 10 seconds. When it fails, or no backend URL is set,
252
+ every framework does the same thing:
253
+
254
+ | Command | Default when the fetch fails |
255
+ | ------------------------------------------------------------------------- | -------------------------------------------------------- |
256
+ | Production build: `next build`, `vite build`, `nuxt build`, `astro build` | The build stops with an error. |
257
+ | Dev: `next dev`, `vite dev`, `nuxt dev`, `astro dev` | A warning, and the server fetches the policy at runtime. |
258
+
259
+ Set `onBuildError` to use one behaviour for both. `'fail'` stops dev too.
260
+ `'runtime'` lets a production build finish, and the server fetches the policy
261
+ at runtime. The `C15T_ON_BUILD_ERROR` environment variable overrides the
262
+ option, so you can deploy during a backend outage without a code change:
263
+
264
+ ```sh
265
+ C15T_ON_BUILD_ERROR=runtime npm run build
266
+ ```
267
+
268
+ Turborepo's strict environment mode hides undeclared variables from tasks, so
269
+ list `C15T_ON_BUILD_ERROR` in the build task's `passThroughEnv` there.
270
+
271
+ The build skips the fetch, without an error, when it can't use a snapshot,
272
+ for example when the backend URL is relative. With `onBuildError: 'fail'`, a
273
+ relative URL stops the build. [Consent modes](../../concepts/modes.md#what-happens-when-the-download-fails)
274
+ lists every case.
275
+
276
+ `vite build` and `vite dev` fetch the manifest when they start. `vite preview`
277
+ serves the last build without fetching. The plugin writes no file into your
278
+ app, so there is nothing to keep out of Git. `c15t/generated` ships its own
279
+ types, so `tsc`, `vue-tsc` and `svelte-check` pass on a fresh checkout without
280
+ a build first.
281
+
282
+ The plugin can't see the options your app passes to `manifest()`. When the
283
+ app passes `source: 'runtime'` or `manifestURL`, set
284
+ `consentManifest({ source: 'runtime' })` too. The build then downloads no
285
+ manifest, so it doesn't fail when the backend's `/manifest` is down.