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

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 (111) hide show
  1. package/AGENTS.md +8 -5
  2. package/dist/component-hooks/use-headless-consent-ui.js +1 -1
  3. package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
  4. package/dist/components/iab-panel/atoms/vendor-list.js +1 -1
  5. package/dist/components/iab-panel/hooks/use-gvl-data.js +1 -1
  6. package/dist/components/iab-panel/index.js +1 -1
  7. package/dist/components/panel/index.js +1 -1
  8. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  9. package/dist/components/panel-trigger/atoms/trigger-icons.js +1 -1
  10. package/dist/components/panel-trigger/panel-trigger.js +1 -1
  11. package/dist/components/preferences/atoms/accordion.js +1 -1
  12. package/dist/components/preferences/index.js +1 -1
  13. package/dist/components/shared/primitives/button.js +1 -1
  14. package/dist/components/shared/ui/accordion/accordion.js +1 -1
  15. package/dist/components/shared/ui/collapsible/collapsible.js +1 -1
  16. package/dist/components/shared/ui/preference-item/preference-item.js +1 -1
  17. package/dist/components/shared/ui/switch/switch.js +1 -1
  18. package/dist/components/shared/ui/tabs/tabs.js +1 -1
  19. package/dist/devtools-launcher.js +2 -0
  20. package/dist/devtools.js +1 -1
  21. package/dist/draft-context.js +2 -0
  22. package/dist/draft.js +1 -1
  23. package/dist/external-iab-context.js +1 -1
  24. package/dist/gpp.js +2 -0
  25. package/dist/hooks/use-color-scheme.js +1 -1
  26. package/dist/hooks/use-headless-iab-consent-ui.js +1 -1
  27. package/dist/hooks.js +1 -1
  28. package/dist/index-parts/consent-banner.js +1 -0
  29. package/dist/index-parts/consent-dialog-link.js +1 -0
  30. package/dist/index-parts/consent-dialog-trigger.js +1 -0
  31. package/dist/index-parts/consent-dialog.js +1 -0
  32. package/dist/index-parts/consent-gate.js +1 -0
  33. package/dist/index-parts/core.js +1 -0
  34. package/dist/index-parts/draft.js +1 -0
  35. package/dist/index-parts/hooks.js +1 -0
  36. package/dist/index-parts/module-hooks.js +1 -0
  37. package/dist/index-parts/provider.js +1 -0
  38. package/dist/index-parts/theme.js +1 -0
  39. package/dist/index-parts/translations.js +1 -0
  40. package/dist/index.js +1 -1
  41. package/dist/kernel-selector.js +1 -1
  42. package/dist/module-hooks/iframe-blocker.js +1 -1
  43. package/dist/provider.js +1 -1
  44. package/dist/styles.tw3.css +27 -3
  45. package/dist/version.js +1 -1
  46. package/dist-types/component-hooks/use-headless-consent-ui.d.ts +4 -4
  47. package/dist-types/components/iab-panel/index.d.ts +0 -1
  48. package/dist-types/components/panel/index.d.ts +0 -1
  49. package/dist-types/components/panel-trigger/atoms/trigger-icons.d.ts +5 -0
  50. package/dist-types/components/panel-trigger/panel-trigger.d.ts +0 -8
  51. package/dist-types/components/preferences/index.d.ts +0 -1
  52. package/dist-types/components/shared/ui/accordion/accordion.d.ts +0 -1
  53. package/dist-types/components/shared/ui/collapsible/collapsible.d.ts +0 -1
  54. package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +0 -1
  55. package/dist-types/components/shared/ui/switch/switch.d.ts +0 -1
  56. package/dist-types/components/shared/ui/tabs/tabs.d.ts +0 -1
  57. package/dist-types/devtools-launcher.d.ts +29 -0
  58. package/dist-types/draft-context.d.ts +31 -0
  59. package/dist-types/draft.d.ts +13 -36
  60. package/dist-types/gpp.d.ts +27 -0
  61. package/dist-types/hooks/use-iab-manager.d.ts +4 -4
  62. package/dist-types/hooks.d.ts +30 -5
  63. package/dist-types/index-parts/consent-banner.d.ts +7 -0
  64. package/dist-types/index-parts/consent-dialog-link.d.ts +7 -0
  65. package/dist-types/index-parts/consent-dialog-trigger.d.ts +7 -0
  66. package/dist-types/index-parts/consent-dialog.d.ts +7 -0
  67. package/dist-types/index-parts/consent-gate.d.ts +7 -0
  68. package/dist-types/index-parts/core.d.ts +8 -0
  69. package/dist-types/index-parts/draft.d.ts +7 -0
  70. package/dist-types/index-parts/hooks.d.ts +7 -0
  71. package/dist-types/index-parts/module-hooks.d.ts +7 -0
  72. package/dist-types/index-parts/provider.d.ts +7 -0
  73. package/dist-types/index-parts/theme.d.ts +8 -0
  74. package/dist-types/index-parts/translations.d.ts +7 -0
  75. package/dist-types/index.d.ts +15 -16
  76. package/dist-types/kernel-selector.d.ts +0 -8
  77. package/dist-types/module-hooks/iframe-blocker.d.ts +12 -7
  78. package/dist-types/provider.d.ts +47 -13
  79. package/dist-types/version.d.ts +1 -1
  80. package/docs/README.md +8 -5
  81. package/docs/concepts/choose-your-setup.md +22 -5
  82. package/docs/concepts/consent-state.md +60 -5
  83. package/docs/concepts/data-fetching.md +34 -11
  84. package/docs/customization/recipes.md +4 -5
  85. package/docs/customization/slots.md +3 -2
  86. package/docs/customization/stylesheets.md +29 -39
  87. package/docs/customization/tailwind.md +5 -5
  88. package/docs/customization/tokens.md +1 -1
  89. package/docs/customization/translations.md +33 -7
  90. package/docs/frameworks/react/components/consent-dialog-trigger.md +5 -0
  91. package/docs/frameworks/react/components/consent-dialog.md +2 -1
  92. package/docs/frameworks/react/components/consent-provider.md +6 -2
  93. package/docs/frameworks/react/components/dev-tools.md +24 -1
  94. package/docs/frameworks/react/gpp.md +163 -0
  95. package/docs/frameworks/react/headless.md +4 -4
  96. package/docs/frameworks/react/hooks.md +10 -5
  97. package/docs/frameworks/react/iab.md +24 -18
  98. package/docs/frameworks/react/quickstart.md +77 -16
  99. package/docs/frameworks/react/rendering.md +82 -11
  100. package/docs/frameworks/react/scripts.md +8 -3
  101. package/docs/frameworks/react/upgrade-v3.md +596 -0
  102. package/docs/frameworks/react/vendor-consent.md +6 -0
  103. package/docs/guides/banner-experiments.md +26 -5
  104. package/docs/guides/troubleshooting.md +3 -2
  105. package/docs/integrations/overview.md +28 -27
  106. package/docs/integrations/posthog.md +99 -11
  107. package/docs/integrations/sentry.md +416 -0
  108. package/docs/upgrade-v3.md +222 -314
  109. package/package.json +13 -8
  110. package/dist/ui-save.js +0 -1
  111. package/dist-types/ui-save.d.ts +0 -25
@@ -0,0 +1,163 @@
1
+ ---
2
+ title: IAB GPP
3
+ description: Add the IAB Global Privacy Platform API (__gpp) to a React app with
4
+ ConsentGPP from @c15t/react/gpp, so ad tech can read US state opt-outs and the
5
+ TCF EU consent string.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## What GPP does
10
+
11
+ `ConsentGPP` from `@c15t/react/gpp` installs `window.__gpp`, the GPP 1.1 CMP
12
+ API, and keeps its GPP string in step with the visitor's choices. Prebid.js,
13
+ Google Ad Manager and other ad tech read US privacy signals from it.
14
+
15
+ ## Turn on GPP
16
+
17
+ Render `ConsentGPP` inside `ConsentProvider`:
18
+
19
+ ```tsx title="src/consent.tsx"
20
+ import { ConsentGPP } from '@c15t/react/gpp';
21
+
22
+ <ConsentProvider options={{ mode, scripts }}>
23
+ <ConsentGPP />
24
+ {children}
25
+ </ConsentProvider>;
26
+ ```
27
+
28
+ `ConsentGPP` renders nothing. It installs `__gpp` after it mounts and
29
+ removes it when it unmounts. It lives in its own entry, like
30
+ `@c15t/react/iab`, so apps that do not import it bundle none of the GPP
31
+ code. `@c15t/react` already depends on `@c15t/iab`; with the `c15t`
32
+ package, import from `c15t/react/gpp`.
33
+
34
+ The props are the GPP options listed below, for example
35
+ `<ConsentGPP usFallback="none" />`. It works the same with a runtime you
36
+ pass to `ConsentProvider` as `runtime`.
37
+
38
+ The GPP string follows the policy and location your backend resolves for the
39
+ visitor, so a US visitor's state comes from the backend. With `offline()`
40
+ mode, pass the state yourself, for example
41
+ `overrides: { country: 'US', region: 'CA' }`.
42
+
43
+ To add the TCF EU section for visitors under an `iab` policy, set up
44
+ [IAB TCF](./iab.md) as well.
45
+
46
+ ## How the policy decides the section
47
+
48
+ The policy rule c15t matched for the visitor decides whether a section
49
+ applies. The visitor's location only picks which US section carries it.
50
+
51
+ | Matched rule and visitor | Section in the GPP string |
52
+ | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
53
+ | `iab` rule, with IAB TCF set up | `tcfeuv2` (ID 2): the TC String the CMP confirmed, unchanged |
54
+ | `iab` rule, any visitor, with `tcf: false` | None; `applicableSections` is `[-1]` |
55
+ | Any other rule with the `preferences` or `opt-out` right, US visitor in a state with a section | That state's section, such as `usca` (ID 8) |
56
+ | The same rule, US visitor whose region is unknown or whose state has no section | `usnat` (ID 7), MSPA US National version 2; none with `usFallback: 'none'` |
57
+ | The same rule, `usApproach: 'national'` | `usnat` for every US visitor |
58
+ | A rule without those rights, such as `none` | None |
59
+ | A visitor outside the US under any rule other than `iab` | None |
60
+
61
+ Every `opt-in` and `opt-out` rule has the `preferences` right, and every
62
+ `opt-out` rule also has `opt-out`. A `none` rule has them only if you add
63
+ them. A rule that gives the visitor no way to opt out produces no US
64
+ section, even when the visitor sends a GPC signal.
65
+
66
+ State sections exist for California, Colorado, Connecticut, Delaware, Florida,
67
+ Iowa, Minnesota, Montana, Nebraska, New Hampshire, New Jersey, Oregon,
68
+ Tennessee, Texas, Utah and Virginia. Indiana, Kentucky, Maryland and Rhode
69
+ Island are not encoded yet: their published specifications start the section
70
+ with a header that the IAB reference implementation does not encode. Visitors
71
+ there get the fallback, `usnat` by default. So does a US visitor whose region
72
+ the geo headers did not supply.
73
+
74
+ The MSPA US National specification describes `usnat` for MSPA signatories
75
+ that chose the national approach. Without `mspaMode`, c15t reports the
76
+ fallback as a transaction the MSPA does not cover (`MspaCoveredTransaction:
77
+ 2`), which still carries the opt-outs to vendors such as Prebid.js. If you
78
+ reserve `usnat` for the national approach, set `usFallback: 'none'`; those
79
+ visitors then get no section until their state resolves.
80
+
81
+ ## What the US sections report
82
+
83
+ * `SaleOptOut`, `SharingOptOut` and `TargetedAdvertisingOptOut`: `1` (opted
84
+ out) when the `marketing` category is not permitted, otherwise `2`. A
85
+ refusal, a GPC signal the rule maps to `marketing` through
86
+ `privacySignals.gpc`, and an opt-in rule without a grant all count.
87
+ * The opt-out notices: `1` (given), since the rule offers an opt-out.
88
+ * The sharing or processing notice: `1` when the rule has the `disclosure`
89
+ right, otherwise `2`.
90
+ * `Gpc`: the browser's Global Privacy Control signal, in sections that have a
91
+ GPC subsection. It is reported even when the rule does not map GPC to a
92
+ category, so give US rules a `privacySignals.gpc` mapping if the opt-outs
93
+ should follow it too.
94
+ * Sensitive data and known-child consents: `0` (not applicable). c15t has no
95
+ category for them, so the string states that you do not process sensitive
96
+ data or knowingly process children's data. If you do, GPP from c15t does
97
+ not describe your processing; collect and signal that consent separately.
98
+ * `MspaCoveredTransaction`: `2` (not covered) unless you set `mspaMode`.
99
+
100
+ The notice fields are your attestation, made through the rule's rights:
101
+ c15t cannot see whether your privacy notice is on the page. Show the notices
102
+ the rule promises, including a persistent opt-out control.
103
+
104
+ ## GPP options
105
+
106
+ Every field is optional. Pass them in the `gpp` option, where `gpp: true`
107
+ uses the defaults, as props on `ConsentGPP` in React, or to `mountGPP()` in
108
+ `@c15t/browser`.
109
+
110
+ | Option | Default | Effect |
111
+ | ------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
112
+ | `cmpId` | the IAB CMP ID c15t holds, else `1` | CMP ID reported by `ping`. The GPP specification has string creators without a registered ID, including MSPA US National creators, use `1`. A TCF EU section needs the registered ID its TC String names. |
113
+ | `usApproach` | `'state'` | `'state'` uses the visitor's state section. `'national'` reports `usnat` for every US visitor; the MSPA reserves it for signatories that chose the national approach. |
114
+ | `usFallback` | `'usnat'` | Under the state approach, the section for a US visitor whose state is unknown or has no section: `'usnat'` or `'none'`. |
115
+ | `mspaMode` | unset | `'opt-out-option'` or `'service-provider'`. Set it only if you signed the IAB Multi-State Privacy Agreement; the transaction is then reported as covered. In service provider mode the opt-outs are reported as not applicable. |
116
+ | `optOutCategories` | `['marketing']` | Categories whose refusal reports an opt-out of sale, sharing and targeted advertising. |
117
+ | `tcf` | `true` | Include the `tcfeuv2` section under an `iab` policy. It needs IAB TCF set up as well. |
118
+
119
+ ## Check the signal
120
+
121
+ Open the browser console on a page with GPP on and call:
122
+
123
+ ```js
124
+ __gpp('ping', (data) => console.log(data));
125
+ ```
126
+
127
+ For a California visitor who has not opted out, the result includes
128
+ `applicableSections: [8]`, `signalStatus: 'ready'` and
129
+ `parsedSections.usca[0].SaleOptOut === 2`. After the visitor refuses
130
+ `marketing`, `SaleOptOut` is `1`. `__gpp('getField', cb, 'usca.SaleOptOut')`
131
+ returns the same value. Scripts in iframes reach the API through the
132
+ `__gppLocator` frame and `postMessage`.
133
+
134
+ Under an `iab` rule, `parsedSections.tcfeuv2` uses the field names of the
135
+ IAB Europe TCF section, such as `PurposeConsent`, `VendorConsent` and
136
+ `PubRestrictions`. `getSection` and `getField` answer `null` for `tcfeuv2`,
137
+ as the IAB reference implementation does: TCF vendors read consent from
138
+ events, not on demand.
139
+
140
+ `signalStatus` stays `'not ready'` while the policy is pending, while the
141
+ banner or dialog is open, while an `iab` rule has no confirmed TC String,
142
+ and while a new TC String is decoded. Listeners added with
143
+ `addEventListener` get `signalStatus: 'not ready'`, then `cmpDisplayStatus`
144
+ and `sectionChange` events, then `signalStatus: 'ready'`.
145
+
146
+ ## Ad tags that load before c15t
147
+
148
+ c15t installs `__gpp` in the browser once consent starts, after it loads the
149
+ GPP code. Vendor scripts that call `__gpp` earlier need a stub. c15t takes over
150
+ the calls and listeners a stub queued, whether the stub keeps listeners on
151
+ `__gpp.events`, as the GPP specification's sample does, or returns them from
152
+ `__gpp('events')`, as `@iabgpp/stub` does. Call `initializeGPPStub()` from
153
+ `@c15t/iab/gpp` in your first script, or use the IAB's own stub snippet.
154
+ When another stub already answers iframes, c15t leaves that to it, so each
155
+ iframe call gets one reply.
156
+
157
+ If a CMP that has already loaded owns `__gpp`, c15t leaves it in place.
158
+ The `gpp` option reports the conflict to the `onError` callback,
159
+ `ConsentGPP` logs it, and `mountGPP()` and `createGPP()` throw. Run one GPP
160
+ CMP per page.
161
+
162
+ GPP signals what the visitor chose; it does not block scripts. Gate vendor
163
+ scripts with c15t's script loading as well.
@@ -34,13 +34,13 @@ import { useHeadlessConsentUI, useTranslations } from 'c15t/react/headless';
34
34
 
35
35
  const ACTION_LABELS = {
36
36
  accept: 'acceptAll',
37
- reject: 'rejectAll',
38
37
  customize: 'customize',
39
38
  dismiss: 'acknowledge',
39
+ reject: 'rejectAll',
40
40
  save: 'save',
41
41
  } as const;
42
42
 
43
- export function Banner() {
43
+ export const Banner = () => {
44
44
  const { banner, performAction, openDialog } = useHeadlessConsentUI();
45
45
  const { common, rights } = useTranslations();
46
46
 
@@ -49,7 +49,7 @@ export function Banner() {
49
49
  }
50
50
 
51
51
  return (
52
- <section role="region" aria-label="Privacy">
52
+ <section aria-label="Privacy">
53
53
  {banner.preferenceControls.map((right) => (
54
54
  <button key={right} type="button" onClick={openDialog}>
55
55
  {right === 'opt-out' ? rights?.optOut : rights?.preferences}
@@ -71,7 +71,7 @@ export function Banner() {
71
71
  ))}
72
72
  </section>
73
73
  );
74
- }
74
+ };
75
75
  ```
76
76
 
77
77
  `banner.actionGroups` is the resolved layout: reject and accept share a group
@@ -14,10 +14,10 @@ from components rendered inside that provider and import them from
14
14
  ```tsx title="src/optional-feature.tsx"
15
15
  import { useConsent } from 'c15t/react';
16
16
 
17
- export function OptionalFeature() {
17
+ export const OptionalFeature = () => {
18
18
  const allowed = useConsent('marketing');
19
19
  return allowed ? <div>Optional marketing content</div> : null;
20
- }
20
+ };
21
21
  ```
22
22
 
23
23
  Call hooks inside the consent provider. `useConsent` reads effective permission,
@@ -42,14 +42,19 @@ Render this component inside your existing `ConsentProvider`.
42
42
  ```tsx title="src/reject-button.tsx"
43
43
  import { useSaveConsents } from 'c15t/react';
44
44
 
45
- export function RejectButton() {
45
+ export const RejectButton = () => {
46
46
  const save = useSaveConsents();
47
47
  return (
48
- <button type="button" onClick={() => void save('none')}>
48
+ <button
49
+ type="button"
50
+ onClick={() => {
51
+ void save('none');
52
+ }}
53
+ >
49
54
  Reject optional
50
55
  </button>
51
56
  );
52
- }
57
+ };
53
58
  ```
54
59
 
55
60
  This is an isolated action example, not a complete policy-aware banner. Use the
@@ -33,14 +33,12 @@ import {
33
33
  IABConsentDialog,
34
34
  } from 'c15t/react/iab';
35
35
 
36
- export function IABConsent({ cmpId }: { cmpId: number }) {
37
- return (
38
- <IABProvider cmpId={cmpId}>
39
- <IABConsentBanner />
40
- <IABConsentDialog />
41
- </IABProvider>
42
- );
43
- }
36
+ export const IABConsent = ({ cmpId }: { cmpId: number }) => (
37
+ <IABProvider cmpId={cmpId}>
38
+ <IABConsentBanner />
39
+ <IABConsentDialog />
40
+ </IABProvider>
41
+ );
44
42
  ```
45
43
 
46
44
  Supply the CMP ID from your deployment's IAB configuration. Keep a persistent
@@ -85,14 +83,12 @@ const publisherRestrictions = [
85
83
  { purposeId: 7, restrictionType: 2 as const, vendorIds: [755] },
86
84
  ];
87
85
 
88
- export function IABConsent({ cmpId }: { cmpId: number }) {
89
- return (
90
- <IABProvider cmpId={cmpId} publisherRestrictions={publisherRestrictions}>
91
- <IABConsentBanner />
92
- <IABConsentDialog />
93
- </IABProvider>
94
- );
95
- }
86
+ export const IABConsent = ({ cmpId }: { cmpId: number }) => (
87
+ <IABProvider cmpId={cmpId} publisherRestrictions={publisherRestrictions}>
88
+ <IABConsentBanner />
89
+ <IABConsentDialog />
90
+ </IABProvider>
91
+ );
96
92
  ```
97
93
 
98
94
  Each restriction type has a requirement that depends on the vendor list:
@@ -154,8 +150,10 @@ because turning one off would change nothing the vendors rely on; its
154
150
  objection control is the opt-out. Such a purpose does not decide its c15t
155
151
  category: the category follows the purposes in it that the visitor can
156
152
  consent to. Legitimate interest never grants a category on its own. If none of
157
- a category's purposes has a consent basis, the category needs all of them
158
- consented, which only Accept All does. Stack switches
153
+ a category's purposes has a consent basis, nothing can consent to them, Accept
154
+ All included, so the category stays denied. See
155
+ [categories under an IAB policy](../../concepts/consent-state.md#categories-under-an-iab-policy).
156
+ Stack switches
159
157
  cover only the purposes some vendor processes on consent. Display-model rows
160
158
  expose this as `hasConsentBasis`. Custom preference UIs can call
161
159
  `applyPublisherRestrictionsToGVL` from `@c15t/iab/headless`, or pass
@@ -209,6 +207,14 @@ the special purposes only. `featureRows` holds the features, with
209
207
  `featuresStandardText` is the standard text, or `null` when you should use the
210
208
  translation.
211
209
 
210
+ Each vendor's privacy policy and legitimate interest links come from its
211
+ `urls[]` entry in the Global Vendor List. The stock preference centres pick
212
+ the entry for the language the banner shows, then English. A custom UI gets
213
+ the same links from `resolveIABVendorUrls(vendor, language)` in
214
+ `@c15t/iab/headless`, or from `policyUrl` and `legitimateInterestUrl` on the
215
+ vendors `processGVLForDialog` returns when you pass it `language`. Vendor lists
216
+ older than GVL v3 have a single `policyUrl` field, which c15t still reads.
217
+
212
218
  ## Consent scope
213
219
 
214
220
  c15t stores TCF choices per site and browser, which the TCF calls
@@ -8,8 +8,9 @@ group: frameworks
8
8
  ## Before you start
9
9
 
10
10
  This guide sets up a client-rendered React app built with Vite. The browser
11
- loads the page, asks the consent backend for the visitor's policy, then shows
12
- the banner or runs the scripts the visitor allowed.
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.
13
14
 
14
15
  * **Next.js or TanStack Start?** Use that framework's guide from the
15
16
  [framework list](https://c15t.com/docs/frameworks). Their adapters resolve consent on the
@@ -26,6 +27,10 @@ the backend URL.
26
27
  [Choose your setup](../../concepts/choose-your-setup.md#decide-who-runs-the-consent-backend)
27
28
  covers self-hosting and offline mode.
28
29
 
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.
33
+
29
34
  ## Install c15t
30
35
 
31
36
  | Package manager | Command |
@@ -40,14 +45,12 @@ vendor loaders used below.
40
45
 
41
46
  ## Register your vendor scripts
42
47
 
43
- Create `src/scripts.ts`. PostHog waits for measurement permission and X Pixel
44
- waits for marketing permission, so your policy must include both categories.
45
- Remove any other PostHog or X Pixel loader from your app so each vendor loads
46
- once.
48
+ Create `src/scripts.ts`. PostHog waits for measurement permission, so your
49
+ policy must include that category. Remove any other PostHog loader from your
50
+ app so PostHog loads once.
47
51
 
48
52
  ```ts title="src/scripts.ts"
49
53
  import { posthog } from '@c15t/integrations/posthog';
50
- import { xPixel } from '@c15t/integrations/x-pixel';
51
54
 
52
55
  export const scripts = [
53
56
  posthog({
@@ -55,13 +58,11 @@ export const scripts = [
55
58
  initOptions: { cookieless_mode: 'never' },
56
59
  loadMode: 'after-consent',
57
60
  }),
58
- xPixel({ pixelId: 'your-pixel-id' }),
59
61
  ];
60
62
  ```
61
63
 
62
- Replace `phc_your_project_key` with your PostHog project key and
63
- `your-pixel-id` with your X Pixel ID. PostHog uses its EU host; add
64
- `region: 'us'` for a US project. Remove the loader for a vendor you don't use.
64
+ Replace `phc_your_project_key` with your PostHog project key. PostHog uses its
65
+ EU host; add `region: 'us'` for a US project.
65
66
 
66
67
  `loadMode: 'after-consent'` keeps the PostHog SDK off the page until the
67
68
  visitor allows measurement. `cookieless_mode: 'never'` turns off PostHog's
@@ -130,6 +131,66 @@ router, render the router inside `Consent` so navigation does not remount the
130
131
  provider. The stylesheet import in `src/consent.tsx` styles the banner and
131
132
  dialog; import it once.
132
133
 
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
+
133
194
  ## Gate your own features
134
195
 
135
196
  Read a category's permission with `useConsent` inside the provider:
@@ -155,16 +216,16 @@ then open the site in a private window with DevTools open on the Network tab.
155
216
  Test under a policy that asks for a choice, such as an EU opt-in policy.
156
217
 
157
218
  1. **Before a choice.** The banner shows. There are no requests to
158
- `posthog.com` or `static.ads-twitter.com`.
219
+ `posthog.com`.
159
220
  2. **Reject All.** The banner closes and the vendor requests stay absent.
160
221
  3. **Reload.** The banner stays closed and the vendor requests stay absent.
161
222
  4. **Privacy settings.** The footer link opens the dialog with **Analytics** (the
162
- `measurement` category) and **Marketing** off. Turn on **Analytics** and save. PostHog's `array.js` loads and
163
- X Pixel stays absent.
223
+ `measurement` category) and **Marketing** off. Turn on **Analytics** and
224
+ save. PostHog's `array.js` loads.
164
225
  5. **Turn Analytics off again.** The page reloads and PostHog does not load.
165
226
 
166
- If the banner never appears, check the Network tab for the request to
167
- `/init` on your backend URL. [Troubleshooting](./troubleshooting.md)
227
+ If the banner never appears, check the generated policy and any location
228
+ request to `/init` on your backend URL. [Troubleshooting](./troubleshooting.md)
168
229
  covers the common causes. [Verify consent](../../guides/verify-consent.md) has the
169
230
  full release checklist.
170
231
 
@@ -23,14 +23,79 @@ 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 build fetches your public policy once and bundles it, so the server never
29
+ fetches it at runtime.
30
+
31
+ Add `consentManifest` to your existing Vite plugin list:
32
+
33
+ ```ts title="vite.config.ts (plugins option)"
34
+ import { consentManifest } from 'c15t/build';
35
+
36
+ export default {
37
+ plugins: [
38
+ // Keep your existing framework plugins here.
39
+ consentManifest({ backendURL: 'https://your-project.inth.app' }),
40
+ ],
41
+ };
42
+ ```
43
+
44
+ Vite writes `src/c15t-manifest.ts` before compiling, including at development
45
+ startup. Keep the generated file out of Git. Install `@c15t/browser@alpha` for
46
+ its browser manifest transport. In the quickstart's `src/consent.tsx`, replace
47
+ the `hosted()` mode with this factory:
48
+
49
+ ```tsx title="src/consent.tsx"
50
+ import { manifest } from '@c15t/browser/headless';
51
+
52
+ import { consentManifest } from './c15t-manifest';
53
+
54
+ const mode = manifest({
55
+ backendURL: 'https://your-project.inth.app',
56
+ manifest: consentManifest,
57
+ });
58
+ ```
59
+
60
+ Keep passing `mode` to `ConsentProvider`. The browser resolves the snapshot
61
+ without `/manifest` when it has the geography the policy needs, or the policy
62
+ does not depend on geography. Supply known country and region through `inputs`
63
+ or overrides to keep regional resolution on the snapshot.
64
+
65
+ When the required country or region is missing, the transport calls backend
66
+ `/init` and uses its resolved policy and translations. That response can use
67
+ newer backend policy than the build snapshot. This is a full consent resolution,
68
+ so these visitors still wait for a backend round trip.
69
+
70
+ Policy and resolver code are part of the browser bundle.
71
+
72
+ The snapshot is fixed at build time:
73
+
74
+ * Rebuild after changing policies, translations or vendors. If your CI caches
75
+ build output, force a fresh build.
76
+ * The build must reach the backend. A failed fetch fails the build.
77
+ * Consent choices still go to the backend.
78
+
79
+ Run `vite build` or `vite dev` before a standalone `tsc`, `vue-tsc` or
80
+ `svelte-check`; the generated file doesn't exist on a fresh checkout.
81
+
82
+ For policy updates without a rebuild, keep the original `hosted()` factory
83
+ and omit the build plugin. That setup resolves policy through the backend
84
+ `/init` on each page load.
85
+
26
86
  ## Client-rendered apps
27
87
 
28
- The [quickstart](./quickstart.md) setup runs entirely in the
29
- browser:
88
+ The recommended [quickstart](./quickstart.md) bundles the
89
+ manifest and resolves it in the browser. See [build-time manifests](#bundle-the-manifest-during-vite-builds).
90
+
91
+ For policy updates without rebuilding, keep `hosted({ url })`. That runtime
92
+ path works as follows:
30
93
 
31
94
  1. The page loads with no consent state. Every optional category is denied.
32
- 2. `ConsentProvider` requests `GET /init` from your backend URL. The backend
33
- picks the visitor's policy from their location.
95
+ 2. `ConsentProvider` requests `GET /init` from your backend URL while it first
96
+ renders, before the page paints. The backend picks the visitor's policy
97
+ from their location. With an `experiment`, the request waits until the
98
+ provider mounts, because it carries the visitor's arm.
34
99
  3. The provider applies the policy and any stored choice. It shows the banner
35
100
  if the policy asks for one, and loads the scripts the visitor allowed.
36
101
 
@@ -47,8 +112,11 @@ around the outlet. In React Router framework mode that is `app/root.tsx`.
47
112
  browser takes over after hydration:
48
113
 
49
114
  * The server HTML contains your page but no banner and no consent state.
50
- * After hydration the browser requests `/init`, then shows the banner if the
51
- policy asks for one.
115
+ * After hydration the browser resolves the bundled manifest, then shows the
116
+ banner if the policy asks for one. When required country or region is
117
+ missing, it calls `/init` and uses the backend's current policy and
118
+ translations, which can differ from the build snapshot. With `hosted()`,
119
+ it always calls `/init`.
52
120
  * The server render and the first browser render agree, so hydration does not
53
121
  warn or change a stored choice.
54
122
 
@@ -96,8 +164,9 @@ browser path.
96
164
  ## Static and prerendered pages
97
165
 
98
166
  A prerendered page is the same HTML for every visitor, so it cannot contain one
99
- visitor's consent. Use the quickstart setup unchanged. The browser talks to the
100
- backend URL directly, so the static host needs no server route. Add the site's
167
+ visitor's consent. Use the quickstart's build-time snapshot. Consent saves and
168
+ any `/init` requests go to the backend URL directly, so the static host
169
+ needs no server route. Add the site's
101
170
  origin to the trusted origins in your Inth project.
102
171
 
103
172
  Do not cache a response that contains a visitor's consent in a CDN or a shared
@@ -122,7 +191,9 @@ custom transports.
122
191
  1. Open the page in a private window and view the page source. In a
123
192
  server-rendered app the HTML contains your content and no element with
124
193
  `data-testid="consent-banner-root"`.
125
- 2. In DevTools Network, filter by your backend URL. One `/init` request runs
126
- after the JavaScript loads, or none if you pass `initialData`.
127
- 3. The banner appears once that request finishes. Vendor requests stay absent
194
+ 2. In DevTools Network, confirm there is no `/manifest` request with a
195
+ build-time snapshot. A regional policy may still need an `/init` request
196
+ to resolve consent when geography is missing. With `hosted()`, one `/init`
197
+ request runs unless you supply `initialData`.
198
+ 3. The banner appears once the policy resolves. Vendor requests stay absent
128
199
  until you allow their category.
@@ -11,11 +11,10 @@ group: frameworks
11
11
  Pass vendor loaders to `ConsentProvider` through `options.scripts`. The
12
12
  provider loads each script when its category is allowed and removes it when
13
13
  the category is denied. The [quickstart](./quickstart.md)
14
- registers PostHog and X Pixel this way:
14
+ registers PostHog this way:
15
15
 
16
16
  ```ts title="src/scripts.ts"
17
17
  import { posthog } from '@c15t/integrations/posthog';
18
- import { xPixel } from '@c15t/integrations/x-pixel';
19
18
 
20
19
  export const scripts = [
21
20
  posthog({
@@ -23,7 +22,6 @@ export const scripts = [
23
22
  initOptions: { cookieless_mode: 'never' },
24
23
  loadMode: 'after-consent',
25
24
  }),
26
- xPixel({ pixelId: 'your-pixel-id' }),
27
25
  ];
28
26
  ```
29
27
 
@@ -76,6 +74,13 @@ covers a vendor without one.
76
74
  load before consent and pass the visitor's choice to the vendor. Read the
77
75
  vendor's guide; a category on a script does not always mean zero requests.
78
76
 
77
+ ## When the script loader downloads
78
+
79
+ The script loader is a separate chunk, downloaded only when `scripts` is not
80
+ empty. `ConsentProvider` loads it when it mounts. The Next.js and TanStack
81
+ Start `ConsentRoot` start the download earlier, during their first render,
82
+ when the visitor's consent already lets a script run.
83
+
79
84
  ## Embeds and other requests
80
85
 
81
86
  Scripts cover vendor code c15t loads for you. For the rest: