@c15t/react 3.0.0-alpha.6 → 3.0.0-alpha.8

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 (40) hide show
  1. package/SKILL.md +1 -1
  2. package/dist/components/iab-prompt/atoms/overlay.js +1 -1
  3. package/dist/components/iab-prompt/atoms/root.js +1 -1
  4. package/dist/components/prompt/atoms/overlay.js +1 -1
  5. package/dist/components/prompt/atoms/root.js +1 -1
  6. package/dist/components/prompt/prompt.js +1 -1
  7. package/dist/components/shared/primitives/legal-links/index.js +1 -1
  8. package/dist/components/shared/streamed-surface.js +2 -0
  9. package/dist/context.js +1 -1
  10. package/dist/hooks/use-late-entry.js +2 -0
  11. package/dist/hooks.js +1 -1
  12. package/dist/iab/styles.tw3.css +2 -2
  13. package/dist/kernel-selector.js +1 -1
  14. package/dist/provider.js +1 -1
  15. package/dist/styles.tw3.css +4 -4
  16. package/dist/version.js +1 -1
  17. package/dist-types/components/prompt/atoms/root.d.ts +4 -0
  18. package/dist-types/components/prompt/prompt.d.ts +4 -0
  19. package/dist-types/components/shared/streamed-surface.d.ts +23 -0
  20. package/dist-types/context.d.ts +17 -1
  21. package/dist-types/hooks/use-late-entry.d.ts +14 -0
  22. package/dist-types/kernel-selector.d.ts +10 -0
  23. package/dist-types/provider.d.ts +28 -6
  24. package/dist-types/version.d.ts +1 -1
  25. package/docs/concepts/choose-your-setup.md +2 -2
  26. package/docs/concepts/consent-state.md +6 -0
  27. package/docs/concepts/data-fetching.md +34 -0
  28. package/docs/customization/motion.md +22 -13
  29. package/docs/customization/tokens.md +1 -0
  30. package/docs/frameworks/react/components/consent-banner.md +7 -2
  31. package/docs/frameworks/react/components/consent-dialog.md +7 -2
  32. package/docs/frameworks/react/components/consent-provider.md +41 -33
  33. package/docs/frameworks/react/components.md +7 -2
  34. package/docs/frameworks/react/quickstart.md +80 -81
  35. package/docs/frameworks/react/rendering.md +19 -9
  36. package/docs/frameworks/react/scripts.md +7 -2
  37. package/docs/frameworks/react/troubleshooting.md +28 -13
  38. package/docs/guides/banner-experiments.md +2 -2
  39. package/docs/upgrade-v3.md +5 -1
  40. package/package.json +7 -7
@@ -1,4 +1,4 @@
1
- import type { AllConsentNames, Callbacks, ClearOnRevocationConfig, ConsentExperiment, ConsentPresentation, ExperimentState, I18nConfig, KernelConfig, KernelOverrides, KernelUser, LegalLinks, ProviderTransportFactory, StorageConfig, User, Vendor } from '@c15t/core';
1
+ import type { AllConsentNames, Callbacks, ClearOnRevocationConfig, ConsentExperiment, ConsentJourneyOption, ConsentPresentation, ExperimentState, JourneyState, I18nConfig, KernelConfig, KernelOverrides, KernelUser, LegalLinks, ProviderTransportFactory, StorageConfig, User, Vendor } from '@c15t/core';
2
2
  import type { Script } from '@c15t/core/modules/script-loader';
3
3
  import type { ConsentControlOptions } from '@c15t/core/runtime';
4
4
  import type { ConsentRuntime, ConsentRuntimeModules, ResolveStreamedInit } from '@c15t/core/runtime/provider';
@@ -13,9 +13,10 @@ export type ConsentProviderCallbacks = Pick<Callbacks, 'onChoiceRecorded' | 'onP
13
13
  /**
14
14
  * Prepared policy and records; legacy consent projections are not provider
15
15
  * inputs. An `experiment` a server helper resolved runs instead of
16
- * `options.experiment`.
16
+ * `options.experiment`, and a `journey` it started is the one the provider
17
+ * continues.
17
18
  */
18
- export type ConsentProviderPrefetch = Omit<KernelConfig, 'initialDraft' | 'transport'> & ExperimentState;
19
+ export type ConsentProviderPrefetch = Omit<KernelConfig, 'initialDraft' | 'transport'> & ExperimentState & JourneyState;
19
20
  export interface ConsentProviderOptions extends ConsentControlOptions, Pick<ReactUIOptions, 'colorScheme' | 'disableAnimation' | 'noStyle' | 'scrollLock' | 'theme' | 'trapFocus'> {
20
21
  enabled?: boolean;
21
22
  presentation?: ConsentPresentation;
@@ -26,6 +27,13 @@ export interface ConsentProviderOptions extends ConsentControlOptions, Pick<Reac
26
27
  * provider to change the experiment.
27
28
  */
28
29
  experiment?: ConsentExperiment;
30
+ /**
31
+ * Random journey id that links each `/init` to the save that follows:
32
+ * `'page'`, `'tab'` or `false`. Initial-only.
33
+ *
34
+ * @default 'page'
35
+ */
36
+ journey?: ConsentJourneyOption;
29
37
  /**
30
38
  * Content Security Policy nonce applied to DOM nodes c15t injects.
31
39
  *
@@ -77,9 +85,12 @@ export interface ConsentProviderOptions extends ConsentControlOptions, Pick<Reac
77
85
  * Pass the pending `Promise<KernelConfig>` instead and the provider
78
86
  * mounts at once with a provisional policy, so `children` render (and
79
87
  * the static shell can prerender) while the consent data streams in.
80
- * Consent surfaces stay hidden until the promise resolves; its first
81
- * `init()` then applies the resolved config in place of the transport's
82
- * network init. A config that resolves without a policy (persisted
88
+ * Consent surfaces stay hidden until the promise resolves. On the
89
+ * server, `ConsentBanner` waits for it in its own Suspense boundary and
90
+ * follows the page in a later chunk of the same response, before
91
+ * hydration, when the resolved policy shows a banner; see
92
+ * `streamBanner`. The resolved config then answers the first `init()`
93
+ * in place of the transport's network init. A config that resolves without a policy (persisted
83
94
  * consents, geo, language only) is applied to the kernel and the
84
95
  * transport init runs as usual; a rejected promise is logged outside
85
96
  * production and falls through to the transport init. Initial-only:
@@ -97,6 +108,17 @@ export interface ConsentProviderOptions extends ConsentControlOptions, Pick<Reac
97
108
  * ```
98
109
  */
99
110
  prefetch?: ConsentProviderPrefetch | Promise<ConsentProviderPrefetch>;
111
+ /**
112
+ * Whether `ConsentBanner` renders on the server once a `prefetch`
113
+ * promise resolves, so it arrives in the streamed response before the
114
+ * page hydrates. It shows only when the resolved policy shows a banner
115
+ * for this visitor. Set `false` to mount the banner after hydration
116
+ * instead, as with a client-only provider. No effect when `prefetch` is
117
+ * not a promise. Use the same value on the server and in the browser.
118
+ *
119
+ * @default true
120
+ */
121
+ streamBanner?: boolean;
100
122
  callbacks?: ConsentProviderCallbacks;
101
123
  /**
102
124
  * Remove configured browser data when its consent permission is revoked.
@@ -1 +1 @@
1
- export declare const version = "3.0.0-alpha.6";
1
+ export declare const version = "3.0.0-alpha.8";
@@ -13,8 +13,8 @@ to the alternatives it rules out.
13
13
  | Your app | Setup | Guide |
14
14
  | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
15
15
  | Next.js App Router with a Next.js server | Server resolves consent from a build-time manifest and streams it to the page | [App Router](https://c15t.com/docs/frameworks/next/quickstart) |
16
- | Next.js App Router, banner must be in the first HTML | Await consent inside `Suspense` | [Stream or await](https://c15t.com/docs/frameworks/next/rendering#stream-or-await-consent-in-the-app-router) |
17
- | Next.js with static, ISR or `'use cache'` pages | Browser resolves consent on those pages | [Static, ISR and cached pages](https://c15t.com/docs/frameworks/next/rendering#render-static-isr-and-cached-pages) |
16
+ | Next.js App Router, page must arrive with its banner | Await consent inside `Suspense` | [Stream or await](https://c15t.com/docs/frameworks/next/rendering#stream-or-await-consent-in-the-app-router) |
17
+ | Next.js with static, ISR, `'use cache'` or `ensureStatic = 'navigation'` pages | Browser resolves consent on those pages | [Static, ISR and cached pages](https://c15t.com/docs/frameworks/next/rendering#render-static-isr-and-cached-pages) |
18
18
  | Next.js Pages Router | Resolve consent in `getServerSideProps` | [Pages Router](https://c15t.com/docs/frameworks/next/rendering#pick-your-rendering-path) |
19
19
  | Next.js with `output: 'export'` | Browser resolves consent | [Static export](https://c15t.com/docs/frameworks/next/static-export) |
20
20
  | TanStack Start with a server | Root loader resolves consent on the server | [TanStack Start](https://c15t.com/docs/frameworks/tanstack-start/quickstart) |
@@ -99,6 +99,12 @@ backend that signs policy snapshot tokens rejects a replay made after the
99
99
  token expires, which is 30 minutes by default for the self-hosted backend. A
100
100
  save still queued by then is recorded only in the browser.
101
101
 
102
+ A replay that fails and stays queued emits `save:replayed` with `ok: false` but
103
+ not `command:error`, since the save already reported its first failure. A save
104
+ that leaves the queue without the backend recording it, because it ran out of
105
+ attempts, waited more than 7 days or was refused, emits `command:error` once.
106
+ Development builds also log a `[c15t]` console warning for each of these.
107
+
102
108
  IAB surfaces close in the click task too, but an IAB choice is recorded only
103
109
  after its TC string is encoded, which can wait for the TCF library to load.
104
110
  If that local step records nothing, for example because the vendor list
@@ -68,6 +68,40 @@ no identity; the visitor's IP address and user agent are forwarded under the
68
68
  backend's usual IP handling. Static output resolves in the browser and sends
69
69
  no report. Set `reportSessions: false` on an adapter to turn it off.
70
70
 
71
+ ## Link an init to the save that follows
72
+
73
+ Each page load gets a journey id, a random UUID sent on requests c15t already
74
+ makes:
75
+
76
+ | Request | Query parameters |
77
+ | ---------------- | ------------------------------------------------------------------------------------------------ |
78
+ | `GET /init` | `c15tJourney`, `c15tJourneyScope`, `c15tStored` (`1` if a choice or notice dismissal was stored) |
79
+ | `POST /subjects` | `c15tJourney`, `c15tJourneyScope` |
80
+
81
+ Session reports carry it as `journey: { id, scope, storedChoice, prompt, domain }`.
82
+ A report and a save with the same `id` belong to one page load. `prompt` is
83
+ `due`, `stored` or `not-required`; a page that falls back to the default opt-in
84
+ banner counts as `due` or `stored`. A stored answer counts as `stored` even if a
85
+ policy change makes c15t ask again.
86
+
87
+ The id is random, is never the subject id and is never written to a cookie.
88
+ Every save that carries one follows an `/init` or report with the same id.
89
+ Saves replayed after going offline carry none.
90
+
91
+ | `journey` | Behavior |
92
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `'page'` (default) | One id per page load, in memory. |
94
+ | `'tab'` | Kept in `sessionStorage` while a prompt is due, and removed once the visitor chooses or no prompt is due. Only when the browser resolves init; server-rendered pages use `'page'`. |
95
+ | `false` | No journey. |
96
+
97
+ Set it on the runtime or provider. In Next.js use `defineConsentConfig({ journey })`;
98
+ in TanStack Start pass the same value to `resolveConsent` and `ConsentRoot`; with
99
+ the inline prefetch script, pass it to `buildPrefetchScript`. Vue, Svelte, Astro
100
+ and the script tag use `'page'`. A manifest that resolves locally in the
101
+ browser reports no init, so it sends no journey.
102
+
103
+ If you pass `hosted()` a `fetch`, match on the path: URLs now carry a query string.
104
+
71
105
  ## What does regular `/init` do?
72
106
 
73
107
  `hosted({ url })` uses `${url}/init` for initialization and `${url}/subjects` for
@@ -8,20 +8,21 @@ group: customization
8
8
 
9
9
  ## Change the speed with motion tokens
10
10
 
11
- c15t animates its surfaces with three durations and four easing curves. Set
11
+ c15t animates its surfaces with three durations and five easing curves. Set
12
12
  them in the theme's `motion` object, or as CSS variables:
13
13
 
14
- | CSS variable | Theme key | Default | Used by |
15
- | ------------------------ | ------------------------ | -------------------------------------- | ----------------------------------------------- |
16
- | `--c15t-duration-fast` | `motion.duration.fast` | `80ms` | Banner, buttons, tabs |
17
- | `--c15t-duration-normal` | `motion.duration.normal` | `150ms` | Preference dialog, switches, accordions |
18
- | `--c15t-duration-slow` | `motion.duration.slow` | `200ms` | Legal links, floating trigger |
19
- | `--c15t-easing` | `motion.easing` | `cubic-bezier(0.4, 0, 0.2, 1)` | Banner fade, switches, accordions |
20
- | `--c15t-easing-out` | `motion.easingOut` | `cubic-bezier(0.215, 0.61, 0.355, 1)` | Preference dialog, floating trigger hover |
21
- | `--c15t-easing-in-out` | `motion.easingInOut` | `cubic-bezier(0.645, 0.045, 0.355, 1)` | Floating trigger snapping to a corner |
22
- | `--c15t-easing-spring` | `motion.easingSpring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | The banner's slide in and the dialog's scale in |
14
+ | CSS variable | Theme key | Default | Used by |
15
+ | ------------------------ | ------------------------ | ---------------------------------------- | -------------------------------------------------------------- |
16
+ | `--c15t-duration-fast` | `motion.duration.fast` | `80ms` | Banner fade out, buttons, tabs |
17
+ | `--c15t-duration-normal` | `motion.duration.normal` | `150ms` | Preference dialog, late banner fade in, switches, accordions |
18
+ | `--c15t-duration-slow` | `motion.duration.slow` | `200ms` | Legal links, floating trigger |
19
+ | `--c15t-easing` | `motion.easing` | `cubic-bezier(0.4, 0, 0.2, 1)` | Switches, accordions |
20
+ | `--c15t-easing-out` | `motion.easingOut` | `cubic-bezier(0.215, 0.61, 0.355, 1)` | Preference dialog, late banner fade in, floating trigger hover |
21
+ | `--c15t-easing-in` | `motion.easingIn` | `cubic-bezier(0.55, 0.055, 0.675, 0.19)` | Banner fade out |
22
+ | `--c15t-easing-in-out` | `motion.easingInOut` | `cubic-bezier(0.645, 0.045, 0.355, 1)` | Floating trigger snapping to a corner |
23
+ | `--c15t-easing-spring` | `motion.easingSpring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | The dialog's scale in |
23
24
 
24
- This theme slows the dialog down and removes the banner's overshoot:
25
+ This theme slows the dialog down and removes its overshoot:
25
26
 
26
27
  ```ts title="consent-theme.ts"
27
28
  export const theme = {
@@ -32,6 +33,14 @@ export const theme = {
32
33
  };
33
34
  ```
34
35
 
36
+ The banner never slides in. When it shows with the page, it is part of the
37
+ first paint and appears on its first frame. When it arrives more than 100ms
38
+ after the page first painted, such as after a slow script or a client-side
39
+ init, it fades in over `--c15t-duration-normal` instead, and its root and
40
+ backdrop carry `data-entry="late"`. A banner rendered on the server always
41
+ shows at once. When it hides, Vue, Svelte and the script tag fade it out over
42
+ `--c15t-duration-fast`.
43
+
35
44
  The theme goes where your framework's other tokens go. See
36
45
  [theme tokens](./tokens.md) and your framework's customize page.
37
46
  In Vue and Nuxt, set `tokens` with the variable name without the leading
@@ -39,8 +48,8 @@ In Vue and Nuxt, set `tokens` with the variable name without the leading
39
48
 
40
49
  ## Turn animations off
41
50
 
42
- `disableAnimation` removes the enter and exit transitions of the banner and
43
- dialogs, and the hover and snap transitions of the floating
51
+ `disableAnimation` removes the banner's fades, the enter and exit
52
+ transitions of the dialogs, and the hover and snap transitions of the floating
44
53
  `ConsentDialogTrigger`. Set it once for every surface, then override it for one
45
54
  surface where your framework allows:
46
55
 
@@ -130,6 +130,7 @@ easing tokens.
130
130
  | `--c15t-duration-slow` | `motion.duration.slow` | `200ms` |
131
131
  | `--c15t-easing` | `motion.easing` | `cubic-bezier(0.4, 0, 0.2, 1)` |
132
132
  | `--c15t-easing-out` | `motion.easingOut` | `cubic-bezier(0.215, 0.61, 0.355, 1)` |
133
+ | `--c15t-easing-in` | `motion.easingIn` | `cubic-bezier(0.55, 0.055, 0.675, 0.19)` |
133
134
  | `--c15t-easing-in-out` | `motion.easingInOut` | `cubic-bezier(0.645, 0.045, 0.355, 1)` |
134
135
  | `--c15t-easing-spring` | `motion.easingSpring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` |
135
136
 
@@ -12,20 +12,25 @@ The [quickstart](../quickstart.md) mounts both in
12
12
  `src/consent.tsx`:
13
13
 
14
14
  ```tsx title="src/consent.tsx"
15
+ import { manifest } from '@c15t/browser/headless';
15
16
  import {
16
17
  ConsentBanner,
17
18
  ConsentDialog,
18
19
  ConsentDialogLink,
19
20
  ConsentProvider,
20
- hosted,
21
21
  } from 'c15t/react';
22
22
  import type { ReactNode } from 'react';
23
23
 
24
+ import { consentManifest } from './c15t-manifest';
24
25
  import { scripts } from './scripts';
25
26
 
26
27
  import 'c15t/react/styles.css';
27
28
 
28
- const mode = hosted({ url: 'https://your-project.inth.app' });
29
+ const mode = manifest({
30
+ backendURL:
31
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
32
+ manifest: consentManifest,
33
+ });
29
34
 
30
35
  export const Consent = ({ children }: { children: ReactNode }) => (
31
36
  <ConsentProvider options={{ mode, scripts }}>
@@ -14,20 +14,25 @@ group: frameworks
14
14
  code.
15
15
 
16
16
  ```tsx title="src/consent.tsx"
17
+ import { manifest } from '@c15t/browser/headless';
17
18
  import {
18
19
  ConsentBanner,
19
20
  ConsentDialog,
20
21
  ConsentDialogLink,
21
22
  ConsentProvider,
22
- hosted,
23
23
  } from 'c15t/react';
24
24
  import type { ReactNode } from 'react';
25
25
 
26
+ import { consentManifest } from './c15t-manifest';
26
27
  import { scripts } from './scripts';
27
28
 
28
29
  import 'c15t/react/styles.css';
29
30
 
30
- const mode = hosted({ url: 'https://your-project.inth.app' });
31
+ const mode = manifest({
32
+ backendURL:
33
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
34
+ manifest: consentManifest,
35
+ });
31
36
 
32
37
  export const Consent = ({ children }: { children: ReactNode }) => (
33
38
  <ConsentProvider options={{ mode, scripts }}>
@@ -14,20 +14,25 @@ The [quickstart](../quickstart.md) does this in
14
14
  `src/consent.tsx`:
15
15
 
16
16
  ```tsx title="src/consent.tsx"
17
+ import { manifest } from '@c15t/browser/headless';
17
18
  import {
18
19
  ConsentBanner,
19
20
  ConsentDialog,
20
21
  ConsentDialogLink,
21
22
  ConsentProvider,
22
- hosted,
23
23
  } from 'c15t/react';
24
24
  import type { ReactNode } from 'react';
25
25
 
26
+ import { consentManifest } from './c15t-manifest';
26
27
  import { scripts } from './scripts';
27
28
 
28
29
  import 'c15t/react/styles.css';
29
30
 
30
- const mode = hosted({ url: 'https://your-project.inth.app' });
31
+ const mode = manifest({
32
+ backendURL:
33
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
34
+ manifest: consentManifest,
35
+ });
31
36
 
32
37
  export const Consent = ({ children }: { children: ReactNode }) => (
33
38
  <ConsentProvider options={{ mode, scripts }}>
@@ -41,9 +46,11 @@ export const Consent = ({ children }: { children: ReactNode }) => (
41
46
  );
42
47
  ```
43
48
 
44
- `hosted({ url })` points the provider at your Inth backend. Replace
45
- `https://your-project.inth.app` with the URL from your Inth project. The
46
- Next.js and TanStack Start adapters export `ConsentRoot` instead, which wraps
49
+ `manifest()` from `@c15t/browser/headless` resolves the policy from the
50
+ snapshot the build bundled, and sends consent choices to `backendURL`. Replace
51
+ the demo project URL with the URL from your Inth project, or set
52
+ `VITE_C15T_BACKEND_URL`. To fetch the policy from the backend on every page
53
+ load instead, pass `hosted({ url })` from `c15t/react`. The Next.js and TanStack Start adapters export `ConsentRoot` instead, which wraps
47
54
  this provider and passes it server-resolved state.
48
55
 
49
56
  ## Provider options
@@ -56,34 +63,35 @@ the provider to change them. `scripts`, `vendors`, `networkBlocker`,
56
63
  `iframeBlocker`, `consentCategories`, `overrides`, `user`, `callbacks` and
57
64
  `enabled` apply when they change.
58
65
 
59
- | Option | Purpose |
60
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
61
- | `mode` | Required. `hosted({ url })` for a backend, `offline()` for local policies, or `custom()` for your own transport |
62
- | `prefetch` | Server-resolved state, or a promise of it, from a framework adapter's server helper |
63
- | `scripts` | Consent-gated vendor scripts. See [scripts and embeds](../scripts.md) |
64
- | `vendors` | Vendors listed under their category with their own switch. See [vendor consent](../vendor-consent.md) |
65
- | `scriptLoader` | Options for the script loader module |
66
- | `networkBlocker` | Rules that hold `fetch` and `XMLHttpRequest` calls until their category is allowed, or `false` |
67
- | `iframeBlocker` | Gates DOM iframes that carry `data-category`. On by default; `false` turns it off |
68
- | `persistence` | Stores the visitor's choice in a cookie and localStorage. On by default |
69
- | `storageConfig` | Storage key and cookie settings for the stored choice |
70
- | `clearOnRevocation` | Cookies and storage keys to delete when a category is denied |
71
- | `reloadOnConsentRevoked` | Reload the page after the visitor turns off a granted category or vendor. Defaults to `true` |
72
- | `consentCategories` | Categories the preference dialog offers alongside those your scripts use, within the policy's scope |
73
- | `overrides` | Force the country, region or language the policy is resolved for, for testing |
74
- | `user` | Link consent records to a signed-in user |
75
- | `callbacks` | Choice, permission and error events |
76
- | `i18n` | Languages and message overrides |
77
- | `legalLinks` | Privacy policy and other links shown in the banner and dialog |
78
- | `theme` | `consentActions` and slot styles. Tokens go through `ConsentTheme`; see [customize](../customize.md) |
79
- | `components` | Slot attributes per component part |
80
- | `presentation` | Banner and dialog shape, position and blocking |
81
- | `experiment` | A/B test of `presentation`, read once at mount. See [banner experiments](../../../guides/banner-experiments.md) |
82
- | `colorScheme` | `'light'`, `'dark'`, `'system'`, or `null` to leave the `c15t-dark` class alone |
83
- | `noStyle`, `disableAnimation` | Drop the built-in styles, or turn off animations |
84
- | `preloadDialog` | When the deferred `ConsentDialog` starts loading: `'idle'` (default) or `'intent'`; see [ConsentDialog](./consent-dialog.md#behavior) |
85
- | `nonce` | Content Security Policy nonce for script elements c15t inserts |
86
- | `enabled` | Set `false` only when you deliberately bypass consent enforcement |
66
+ | Option | Purpose |
67
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `mode` | Required. `hosted({ url })` for a backend, `manifest()` from `@c15t/browser/headless` for a bundled policy, `offline()` for local policies, or `custom()` for your own transport |
69
+ | `prefetch` | Server-resolved state, or a promise of it, from a framework adapter's server helper |
70
+ | `scripts` | Consent-gated vendor scripts. See [scripts and embeds](../scripts.md) |
71
+ | `vendors` | Vendors listed under their category with their own switch. See [vendor consent](../vendor-consent.md) |
72
+ | `scriptLoader` | Options for the script loader module |
73
+ | `networkBlocker` | Rules that hold `fetch` and `XMLHttpRequest` calls until their category is allowed, or `false` |
74
+ | `iframeBlocker` | Gates DOM iframes that carry `data-category`. On by default; `false` turns it off |
75
+ | `persistence` | Stores the visitor's choice in a cookie and localStorage. On by default |
76
+ | `storageConfig` | Storage key and cookie settings for the stored choice |
77
+ | `clearOnRevocation` | Cookies and storage keys to delete when a category is denied |
78
+ | `reloadOnConsentRevoked` | Reload the page after the visitor turns off a granted category or vendor. Defaults to `true` |
79
+ | `consentCategories` | Categories the preference dialog offers alongside those your scripts use, within the policy's scope |
80
+ | `overrides` | Force the country, region or language the policy is resolved for, for testing |
81
+ | `user` | Link consent records to a signed-in user |
82
+ | `callbacks` | Choice, permission and error events |
83
+ | `i18n` | Languages and message overrides |
84
+ | `legalLinks` | Privacy policy and other links shown in the banner and dialog |
85
+ | `theme` | `consentActions` and slot styles. Tokens go through `ConsentTheme`; see [customize](../customize.md) |
86
+ | `components` | Slot attributes per component part |
87
+ | `presentation` | Banner and dialog shape, position and blocking |
88
+ | `experiment` | A/B test of `presentation`, read once at mount. See [banner experiments](../../../guides/banner-experiments.md) |
89
+ | `journey` | Random id that links each `/init` to the save that follows. See [session reports](../../../concepts/data-fetching.md#link-an-init-to-the-save-that-follows) |
90
+ | `colorScheme` | `'light'`, `'dark'`, `'system'`, or `null` to leave the `c15t-dark` class alone |
91
+ | `noStyle`, `disableAnimation` | Drop the built-in styles, or turn off animations |
92
+ | `preloadDialog` | When the deferred `ConsentDialog` starts loading: `'idle'` (default) or `'intent'`; see [ConsentDialog](./consent-dialog.md#behavior) |
93
+ | `nonce` | Content Security Policy nonce for script elements c15t inserts |
94
+ | `enabled` | Set `false` only when you deliberately bypass consent enforcement |
87
95
 
88
96
  `enabled: false` grants categories and allows gated loading while suppressing
89
97
  consent UI. It is not a way to fix failed initialization or hide a banner in
@@ -36,20 +36,25 @@ Most apps start with four components. `ConsentProvider` holds the runtime,
36
36
  one file:
37
37
 
38
38
  ```tsx title="src/consent.tsx"
39
+ import { manifest } from '@c15t/browser/headless';
39
40
  import {
40
41
  ConsentBanner,
41
42
  ConsentDialog,
42
43
  ConsentDialogLink,
43
44
  ConsentProvider,
44
- hosted,
45
45
  } from 'c15t/react';
46
46
  import type { ReactNode } from 'react';
47
47
 
48
+ import { consentManifest } from './c15t-manifest';
48
49
  import { scripts } from './scripts';
49
50
 
50
51
  import 'c15t/react/styles.css';
51
52
 
52
- const mode = hosted({ url: 'https://your-project.inth.app' });
53
+ const mode = manifest({
54
+ backendURL:
55
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
56
+ manifest: consentManifest,
57
+ });
53
58
 
54
59
  export const Consent = ({ children }: { children: ReactNode }) => (
55
60
  <ConsentProvider options={{ mode, scripts }}>
@@ -7,10 +7,11 @@ 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 browser
11
- loads the page, resolves the visitor's policy, then shows the banner or runs
12
- the scripts the visitor allowed. The build-time step bundles the public
13
- policy; regional policies may still need a backend location request.
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.
14
15
 
15
16
  * **Next.js or TanStack Start?** Use that framework's guide from the
16
17
  [framework list](https://c15t.com/docs/frameworks). Their adapters resolve consent on the
@@ -27,21 +28,65 @@ the backend URL.
27
28
  [Choose your setup](../../concepts/choose-your-setup.md#decide-who-runs-the-consent-backend)
28
29
  covers self-hosting and offline mode.
29
30
 
30
- Build-time manifests are the recommended production setup. Complete the
31
- [build-time manifest step](#bundle-the-manifest-during-builds) before
32
- deploying, and rebuild after changing policies, translations or vendors.
31
+ Bundling the policy at build time is the recommended production setup.
32
+ Rebuild after changing policies, translations or vendors.
33
33
 
34
34
  ## Install c15t
35
35
 
36
- | Package manager | Command |
37
- | :-------------- | :------------------------------------------------ |
38
- | npm | `npm install c15t@alpha @c15t/integrations@alpha` |
39
- | pnpm | `pnpm add c15t@alpha @c15t/integrations@alpha` |
40
- | yarn | `yarn add c15t@alpha @c15t/integrations@alpha` |
41
- | bun | `bun add c15t@alpha @c15t/integrations@alpha` |
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` |
42
42
 
43
- `c15t` contains the React adapter at `c15t/react`. `@c15t/integrations` holds the
44
- vendor loaders used below.
43
+ `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.
46
+
47
+ ## Bundle your policy
48
+
49
+ Add `consentManifest` to `vite.config.ts`:
50
+
51
+ ```ts title="vite.config.ts"
52
+ import react from '@vitejs/plugin-react';
53
+ 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.
45
90
 
46
91
  ## Register your vendor scripts
47
92
 
@@ -77,20 +122,25 @@ asks for them, and `ConsentDialogLink` keeps preferences reachable after the
77
122
  banner closes.
78
123
 
79
124
  ```tsx title="src/consent.tsx"
125
+ import { manifest } from '@c15t/browser/headless';
80
126
  import {
81
127
  ConsentBanner,
82
128
  ConsentDialog,
83
129
  ConsentDialogLink,
84
130
  ConsentProvider,
85
- hosted,
86
131
  } from 'c15t/react';
87
132
  import type { ReactNode } from 'react';
88
133
 
134
+ import { consentManifest } from './c15t-manifest';
89
135
  import { scripts } from './scripts';
90
136
 
91
137
  import 'c15t/react/styles.css';
92
138
 
93
- const mode = hosted({ url: 'https://your-project.inth.app' });
139
+ const mode = manifest({
140
+ backendURL:
141
+ import.meta.env.VITE_C15T_BACKEND_URL ?? 'https://benchmarks-inth.inth.app',
142
+ manifest: consentManifest,
143
+ });
94
144
 
95
145
  export const Consent = ({ children }: { children: ReactNode }) => (
96
146
  <ConsentProvider options={{ mode, scripts }}>
@@ -104,8 +154,17 @@ export const Consent = ({ children }: { children: ReactNode }) => (
104
154
  );
105
155
  ```
106
156
 
107
- Replace `https://your-project.inth.app` with your project's backend URL,
108
- exactly as Inth shows it, including any path prefix.
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.
164
+
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.
109
168
 
110
169
  Wrap your app with it in `src/main.tsx`:
111
170
 
@@ -131,66 +190,6 @@ router, render the router inside `Consent` so navigation does not remount the
131
190
  provider. The stylesheet import in `src/consent.tsx` styles the banner and
132
191
  dialog; import it once.
133
192
 
134
- ## Bundle the manifest during builds
135
-
136
- The build fetches your public policy once and bundles it, so the server never
137
- fetches it at runtime.
138
-
139
- Add `consentManifest` to your existing Vite plugin list:
140
-
141
- ```ts title="vite.config.ts (plugins option)"
142
- import { consentManifest } from 'c15t/build';
143
-
144
- export default {
145
- plugins: [
146
- // Keep your existing framework plugins here.
147
- consentManifest({ backendURL: 'https://your-project.inth.app' }),
148
- ],
149
- };
150
- ```
151
-
152
- Vite writes `src/c15t-manifest.ts` before compiling, including at development
153
- startup. Keep the generated file out of Git. Install `@c15t/browser@alpha` for
154
- its browser manifest transport. In the quickstart's `src/consent.tsx`, replace
155
- the `hosted()` mode with this factory:
156
-
157
- ```tsx title="src/consent.tsx"
158
- import { manifest } from '@c15t/browser/headless';
159
-
160
- import { consentManifest } from './c15t-manifest';
161
-
162
- const mode = manifest({
163
- backendURL: 'https://your-project.inth.app',
164
- manifest: consentManifest,
165
- });
166
- ```
167
-
168
- Keep passing `mode` to `ConsentProvider`. The browser resolves the snapshot
169
- without `/manifest` when it has the geography the policy needs, or the policy
170
- does not depend on geography. Supply known country and region through `inputs`
171
- or overrides to keep regional resolution on the snapshot.
172
-
173
- When the required country or region is missing, the transport calls backend
174
- `/init` and uses its resolved policy and translations. That response can use
175
- newer backend policy than the build snapshot. This is a full consent resolution,
176
- so these visitors still wait for a backend round trip.
177
-
178
- Policy and resolver code are part of the browser bundle.
179
-
180
- The snapshot is fixed at build time:
181
-
182
- * Rebuild after changing policies, translations or vendors. If your CI caches
183
- build output, force a fresh build.
184
- * The build must reach the backend. A failed fetch fails the build.
185
- * Consent choices still go to the backend.
186
-
187
- Run `vite build` or `vite dev` before a standalone `tsc`, `vue-tsc` or
188
- `svelte-check`; the generated file doesn't exist on a fresh checkout.
189
-
190
- For policy updates without a rebuild, keep the original `hosted()` factory
191
- and omit the build plugin. That setup resolves policy through the backend
192
- `/init` on each page load.
193
-
194
193
  ## Gate your own features
195
194
 
196
195
  Read a category's permission with `useConsent` inside the provider:
@@ -215,8 +214,8 @@ Build and preview the production bundle with `vite build` and `vite preview`,
215
214
  then open the site in a private window with DevTools open on the Network tab.
216
215
  Test under a policy that asks for a choice, such as an EU opt-in policy.
217
216
 
218
- 1. **Before a choice.** The banner shows. There are no requests to
219
- `posthog.com`.
217
+ 1. **Before a choice.** The banner shows, with no `/manifest` request. There
218
+ are no requests to `posthog.com`.
220
219
  2. **Reject All.** The banner closes and the vendor requests stay absent.
221
220
  3. **Reload.** The banner stays closed and the vendor requests stay absent.
222
221
  4. **Privacy settings.** The footer link opens the dialog with **Analytics** (the