@c15t/react 3.0.0-alpha.3 → 3.0.0-alpha.5

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 (219) hide show
  1. package/AGENTS.md +92 -72
  2. package/README.md +21 -17
  3. package/SKILL.md +34 -0
  4. package/dist/component-hooks/use-headless-consent-ui.js +1 -1
  5. package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
  6. package/dist/components/consent-gate/atoms.js +1 -1
  7. package/dist/components/consent-gate/consent-gate.js +1 -1
  8. package/dist/components/iab-panel/atoms/card.js +1 -1
  9. package/dist/components/iab-panel/atoms/feature-item.js +2 -0
  10. package/dist/components/iab-panel/atoms/header.js +1 -1
  11. package/dist/components/iab-panel/iab-panel.js +1 -1
  12. package/dist/components/iab-panel/index.js +1 -1
  13. package/dist/components/iab-prompt/atoms/overlay.js +1 -1
  14. package/dist/components/iab-prompt/atoms/root.js +1 -1
  15. package/dist/components/panel/atoms/card.js +1 -1
  16. package/dist/components/panel/atoms/root.js +1 -1
  17. package/dist/components/panel-trigger/atoms/button.js +1 -1
  18. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  19. package/dist/components/preferences/atoms/accordion.js +1 -1
  20. package/dist/components/prompt/atoms/overlay.js +1 -1
  21. package/dist/components/prompt/atoms/root.js +1 -1
  22. package/dist/components/prompt/components.js +1 -1
  23. package/dist/components/shared/libs/compose-refs.js +1 -0
  24. package/dist/components/shared/libs/slot.js +1 -1
  25. package/dist/components/shared/primitives/button.js +1 -1
  26. package/dist/components/shared/ui/dialog/dialog.js +1 -1
  27. package/dist/components/shared/ui/icon.js +1 -1
  28. package/dist/context/consent-tracking-context.js +1 -1
  29. package/dist/devtools.js +1 -1
  30. package/dist/draft-context.js +2 -0
  31. package/dist/draft.js +1 -1
  32. package/dist/external-iab-context.js +1 -1
  33. package/dist/gpp.js +2 -0
  34. package/dist/hooks/use-color-scheme.js +1 -1
  35. package/dist/hooks/use-focus-trap.js +1 -1
  36. package/dist/hooks.js +1 -1
  37. package/dist/iab/styles.tw3.css +4 -4
  38. package/dist/index-parts/consent-banner.js +1 -0
  39. package/dist/index-parts/consent-dialog-link.js +1 -0
  40. package/dist/index-parts/consent-dialog-trigger.js +1 -0
  41. package/dist/index-parts/consent-dialog.js +1 -0
  42. package/dist/index-parts/consent-gate.js +1 -0
  43. package/dist/index-parts/core.js +1 -0
  44. package/dist/index-parts/draft.js +1 -0
  45. package/dist/index-parts/hooks.js +1 -0
  46. package/dist/index-parts/module-hooks.js +1 -0
  47. package/dist/index-parts/provider.js +1 -0
  48. package/dist/index-parts/theme.js +1 -0
  49. package/dist/index-parts/translations.js +1 -0
  50. package/dist/index.js +1 -1
  51. package/dist/kernel-selector.js +1 -1
  52. package/dist/module-hooks/iframe-blocker.js +1 -1
  53. package/dist/postcss-tailwind3.js +1 -0
  54. package/dist/provider.js +1 -1
  55. package/dist/server/fetch-ssr-data.js +1 -1
  56. package/dist/server/headers.js +1 -1
  57. package/dist/server/normalize-url.js +1 -1
  58. package/dist/styles.tw3.css +16 -16
  59. package/dist/transports/offline.js +1 -1
  60. package/dist/utils/merge-slot-props.js +1 -1
  61. package/dist/utils/theme-utils.js +1 -1
  62. package/dist/version.js +1 -1
  63. package/dist-types/component-hooks/use-headless-consent-ui.d.ts +1 -1
  64. package/dist-types/components/iab-panel/atoms/feature-item.d.ts +28 -0
  65. package/dist-types/components/iab-panel/iab-panel.d.ts +2 -2
  66. package/dist-types/components/iab-panel/index.d.ts +5 -2
  67. package/dist-types/components/iab-panel/use-iab-translations.d.ts +4 -0
  68. package/dist-types/components/iab-prompt/iab-prompt.d.ts +2 -2
  69. package/dist-types/components/iab-prompt/index.d.ts +2 -2
  70. package/dist-types/components/panel/atoms/card.d.ts +1 -0
  71. package/dist-types/components/prompt/components.d.ts +6 -0
  72. package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
  73. package/dist-types/components/shared/ui/icon.d.ts +2 -1
  74. package/dist-types/context/consent-tracking-context.d.ts +10 -0
  75. package/dist-types/context/theme-context.d.ts +2 -2
  76. package/dist-types/context.d.ts +6 -0
  77. package/dist-types/devtools.d.ts +13 -2
  78. package/dist-types/draft-context.d.ts +31 -0
  79. package/dist-types/draft.d.ts +13 -30
  80. package/dist-types/gpp.d.ts +27 -0
  81. package/dist-types/hooks/use-focus-trap.d.ts +3 -1
  82. package/dist-types/hooks.d.ts +57 -13
  83. package/dist-types/iab.d.ts +1 -1
  84. package/dist-types/index-parts/consent-banner.d.ts +7 -0
  85. package/dist-types/index-parts/consent-dialog-link.d.ts +7 -0
  86. package/dist-types/index-parts/consent-dialog-trigger.d.ts +7 -0
  87. package/dist-types/index-parts/consent-dialog.d.ts +7 -0
  88. package/dist-types/index-parts/consent-gate.d.ts +7 -0
  89. package/dist-types/index-parts/core.d.ts +8 -0
  90. package/dist-types/index-parts/draft.d.ts +7 -0
  91. package/dist-types/index-parts/hooks.d.ts +7 -0
  92. package/dist-types/index-parts/module-hooks.d.ts +7 -0
  93. package/dist-types/index-parts/provider.d.ts +7 -0
  94. package/dist-types/index-parts/theme.d.ts +8 -0
  95. package/dist-types/index-parts/translations.d.ts +7 -0
  96. package/dist-types/index.d.ts +14 -15
  97. package/dist-types/kernel-selector.d.ts +0 -8
  98. package/dist-types/module-hooks/iframe-blocker.d.ts +12 -7
  99. package/dist-types/postcss-tailwind3.d.ts +21 -0
  100. package/dist-types/provider.d.ts +57 -17
  101. package/dist-types/server/headers.d.ts +14 -4
  102. package/dist-types/server/normalize-url.d.ts +14 -7
  103. package/dist-types/server/types.d.ts +12 -1
  104. package/dist-types/transports/offline.d.ts +7 -16
  105. package/dist-types/ui-config-context.d.ts +7 -1
  106. package/dist-types/utils/merge-slot-props.d.ts +15 -1
  107. package/dist-types/utils/theme-utils.d.ts +17 -2
  108. package/dist-types/version.d.ts +1 -1
  109. package/docs/README.md +92 -72
  110. package/docs/assets/v3/bottom-bar.png +0 -0
  111. package/docs/assets/v3/brand-card.png +0 -0
  112. package/docs/assets/v3/brand-preferences.png +0 -0
  113. package/docs/assets/v3/choice-wall.png +0 -0
  114. package/docs/assets/v3/headless-bar-html.png +0 -0
  115. package/docs/assets/v3/headless-bar-mobile.png +0 -0
  116. package/docs/assets/v3/headless-bar.png +0 -0
  117. package/docs/assets/v3/slim-bar.png +0 -0
  118. package/docs/concepts/choose-your-setup.md +87 -0
  119. package/docs/{frameworks/react/concepts → concepts}/consent-categories.md +22 -7
  120. package/docs/{guides → concepts}/consent-state.md +89 -105
  121. package/docs/{guides → concepts}/data-fetching.md +31 -27
  122. package/docs/concepts/how-consent-works.md +123 -0
  123. package/docs/{frameworks/react/concepts/policy-presets.md → concepts/policies.md} +7 -59
  124. package/docs/customization/class-names.md +202 -0
  125. package/docs/customization/dark-mode.md +157 -0
  126. package/docs/customization/motion.md +119 -0
  127. package/docs/customization/overview.md +67 -34
  128. package/docs/customization/recipes.md +839 -49
  129. package/docs/customization/slots.md +216 -35
  130. package/docs/customization/stylesheets.md +147 -0
  131. package/docs/customization/tailwind.md +842 -0
  132. package/docs/customization/tokens.md +163 -96
  133. package/docs/customization/translations.md +60 -3
  134. package/docs/frameworks/react/callbacks.md +118 -0
  135. package/docs/frameworks/react/clear-on-revocation.md +106 -0
  136. package/docs/frameworks/react/components/consent-banner.md +42 -28
  137. package/docs/frameworks/react/components/consent-dialog-link.md +13 -11
  138. package/docs/frameworks/react/components/consent-dialog-trigger.md +81 -76
  139. package/docs/frameworks/react/components/consent-dialog.md +56 -50
  140. package/docs/frameworks/react/components/consent-gate.md +37 -20
  141. package/docs/frameworks/react/components/consent-provider.md +130 -0
  142. package/docs/frameworks/react/components/consent-widget.md +23 -20
  143. package/docs/frameworks/react/components/dev-tools.md +53 -85
  144. package/docs/frameworks/react/components.md +105 -0
  145. package/docs/frameworks/react/compose.md +354 -0
  146. package/docs/frameworks/react/content-security-policy.md +126 -0
  147. package/docs/frameworks/react/customize.md +250 -0
  148. package/docs/frameworks/react/embeds.md +89 -0
  149. package/docs/frameworks/react/gpp.md +163 -0
  150. package/docs/frameworks/react/{hooks/overview.md → hooks.md} +21 -16
  151. package/docs/frameworks/react/{iab/overview.md → iab.md} +80 -31
  152. package/docs/frameworks/react/network-blocker.md +35 -41
  153. package/docs/frameworks/react/quickstart.md +139 -101
  154. package/docs/frameworks/react/rendering.md +128 -0
  155. package/docs/frameworks/react/scripts.md +115 -0
  156. package/docs/frameworks/react/translations.md +155 -0
  157. package/docs/frameworks/react/troubleshooting.md +92 -47
  158. package/docs/frameworks/react/vendor-consent.md +170 -0
  159. package/docs/guides/banner-experiments.md +654 -0
  160. package/docs/guides/troubleshooting.md +120 -47
  161. package/docs/guides/verify-consent.md +81 -49
  162. package/docs/integrations/adobe-analytics.md +167 -159
  163. package/docs/integrations/ahrefs-analytics.md +152 -154
  164. package/docs/integrations/amplitude.md +162 -156
  165. package/docs/integrations/building-integrations.md +136 -37
  166. package/docs/integrations/clearbit.md +154 -154
  167. package/docs/integrations/cloudflare-web-analytics.md +156 -156
  168. package/docs/integrations/cloudflare-zaraz.md +209 -261
  169. package/docs/integrations/crisp.md +164 -158
  170. package/docs/integrations/databuddy.md +157 -173
  171. package/docs/integrations/fathom-analytics.md +158 -156
  172. package/docs/integrations/front-chat.md +167 -167
  173. package/docs/integrations/google-maps.md +118 -83
  174. package/docs/integrations/google-tag-manager.md +178 -163
  175. package/docs/integrations/google-tag.md +163 -160
  176. package/docs/integrations/heap.md +163 -155
  177. package/docs/integrations/hightouch.md +161 -157
  178. package/docs/integrations/hotjar.md +159 -155
  179. package/docs/integrations/intercom.md +183 -153
  180. package/docs/integrations/klaviyo.md +486 -0
  181. package/docs/integrations/linkedin-insights.md +174 -150
  182. package/docs/integrations/logrocket.md +160 -156
  183. package/docs/integrations/matomo-analytics.md +188 -178
  184. package/docs/integrations/meta-pixel.md +188 -150
  185. package/docs/integrations/microsoft-clarity.md +163 -155
  186. package/docs/integrations/microsoft-uet.md +148 -154
  187. package/docs/integrations/mixpanel-analytics.md +155 -160
  188. package/docs/integrations/one-dollar-stats.md +166 -165
  189. package/docs/integrations/openai-pixel.md +204 -301
  190. package/docs/integrations/overview.md +137 -80
  191. package/docs/integrations/pinterest-tag.md +191 -183
  192. package/docs/integrations/pirsch.md +169 -159
  193. package/docs/integrations/plausible-analytics.md +172 -158
  194. package/docs/integrations/posthog.md +313 -240
  195. package/docs/integrations/promptwatch.md +154 -154
  196. package/docs/integrations/reddit-pixel.md +185 -157
  197. package/docs/integrations/rudderstack.md +201 -186
  198. package/docs/integrations/rybbit-analytics.md +171 -160
  199. package/docs/integrations/segment.md +182 -154
  200. package/docs/integrations/snapchat-pixel.md +186 -156
  201. package/docs/integrations/tiktok-pixel.md +171 -150
  202. package/docs/integrations/umami-analytics.md +163 -158
  203. package/docs/integrations/vercel-analytics.md +166 -157
  204. package/docs/integrations/x-pixel.md +176 -150
  205. package/docs/integrations/youtube.md +121 -86
  206. package/docs/upgrade-v3.md +496 -467
  207. package/package.json +20 -9
  208. package/dist/ui-save.js +0 -1
  209. package/dist-types/ui-save.d.ts +0 -25
  210. package/docs/assets/v3/brand-bar.png +0 -0
  211. package/docs/assets/v3/mobile-card.png +0 -0
  212. package/docs/assets/v3/preferences.png +0 -0
  213. package/docs/frameworks/react/components/consent-manager-provider.md +0 -95
  214. package/docs/frameworks/react/script-loader.md +0 -69
  215. package/docs/frameworks/react/styling/overview.md +0 -266
  216. package/docs/guides/deployment-modes.md +0 -75
  217. package/docs/guides/shared-consent-controls.md +0 -158
  218. package/docs/integrations/clear-on-revocation.md +0 -167
  219. package/docs/integrations/granular-consent.md +0 -210
@@ -0,0 +1,128 @@
1
+ ---
2
+ title: Rendering and deployment
3
+ description: How c15t resolves consent in client-rendered React apps, React
4
+ Router framework mode, Remix and other server-rendered React apps without a
5
+ c15t adapter.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Pick your rendering path
10
+
11
+ `c15t/react` resolves consent in the browser. That works for every React app,
12
+ including server-rendered ones. What changes is what the first HTML contains.
13
+
14
+ | Your app | Where consent resolves | Setup |
15
+ | ------------------------------------------------------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------- |
16
+ | Vite, or another client-rendered single-page app | Browser, after the bundle loads | [Quickstart](./quickstart.md) |
17
+ | Prerendered or static HTML | Browser, after the page loads | [Quickstart](./quickstart.md), see [static pages](#static-and-prerendered-pages) |
18
+ | React Router framework mode, Remix, or another server-rendered React app | Browser, after hydration | [Quickstart](./quickstart.md), see [server-rendered apps](#react-router-remix-and-other-server-rendered-apps) |
19
+ | Next.js or TanStack Start | Server, per request | That framework's guide in the [framework list](https://c15t.com/docs/frameworks) |
20
+
21
+ Pick the Next.js or TanStack Start adapter if you use those frameworks. They
22
+ read the visitor's cookie and location on the server, so the banner can be in
23
+ the first HTML. [Choose your setup](../../concepts/choose-your-setup.md#server-rendering-or-browser-rendering)
24
+ explains the difference in general terms.
25
+
26
+ ## Client-rendered apps
27
+
28
+ The [quickstart](./quickstart.md) setup runs entirely in the
29
+ browser:
30
+
31
+ 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.
34
+ 3. The provider applies the policy and any stored choice. It shows the banner
35
+ if the policy asks for one, and loads the scripts the visitor allowed.
36
+
37
+ Until step 3 finishes, gated scripts and `ConsentGate` embeds stay blocked. If
38
+ the backend request fails, they stay blocked and no banner shows.
39
+
40
+ ## React Router, Remix and other server-rendered apps
41
+
42
+ React Router framework mode, Remix and similar frameworks have no c15t adapter.
43
+ Use the quickstart's `Consent` component and render it in your root component,
44
+ around the outlet. In React Router framework mode that is `app/root.tsx`.
45
+
46
+ `ConsentProvider` renders on the server without touching browser APIs, and the
47
+ browser takes over after hydration:
48
+
49
+ * 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.
52
+ * The server render and the first browser render agree, so hydration does not
53
+ warn or change a stored choice.
54
+
55
+ One detail differs from the Vite setup: import `c15t/react/styles.css` where
56
+ your framework loads global CSS, so the server HTML links it.
57
+
58
+ ### Skip the browser's first init request
59
+
60
+ This is optional. `fetchSSRData` from `c15t/react/server` calls the backend's
61
+ `/init` from your server loader, forwarding the visitor's location and
62
+ language headers. Pass the result to the browser, wrap it in a promise and give
63
+ it to `hosted({ url, initialData })`. The provider's first init then uses that
64
+ response instead of a network request.
65
+
66
+ Know its limits before you add it:
67
+
68
+ * It saves the browser's round trip to the backend. It does not put the banner
69
+ in the server HTML; the banner still mounts after hydration.
70
+ * Every page view makes a server-to-server `/init` request. Do not cache the
71
+ loader response, because it contains one visitor's policy.
72
+ * It returns `undefined` when the request has no location or language headers,
73
+ and the browser then requests `/init` as usual.
74
+ * `mode` is read once, when the provider mounts. Create the `hosted()` mode once
75
+ in the browser rather than on every render.
76
+ * A relative `backendURL` resolves against the request's `host` header, over
77
+ `https` for a domain name and over `http` for `localhost`, an IP address or
78
+ a single-label host. `fetchSSRData` ignores `x-forwarded-host`,
79
+ `x-forwarded-proto` and `forwarded`, and does not send them to the backend.
80
+ Behind a proxy that overwrites those headers, pass
81
+ `trustForwardedHeaders: true` to use them.
82
+ * If your server answers requests for any `Host` header, such as a Node server
83
+ exposed directly or a proxy that forwards `Host` from its default virtual
84
+ host, pass `fetchSSRData` an absolute `backendURL`. A relative one resolves
85
+ against whatever `Host` the request sent.
86
+
87
+ ### Render the banner in the server HTML
88
+
89
+ c15t has no framework-neutral helper for this. `ConsentProvider` accepts a
90
+ server-resolved `prefetch` state, but the Next.js and TanStack Start adapters
91
+ build that state from the request's cookie, headers and a cached policy
92
+ manifest. Building it by hand ties your app to internal shapes. If the banner
93
+ must be part of the first paint, use one of those adapters. Otherwise keep the
94
+ browser path.
95
+
96
+ ## Static and prerendered pages
97
+
98
+ 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
101
+ origin to the trusted origins in your Inth project.
102
+
103
+ Do not cache a response that contains a visitor's consent in a CDN or a shared
104
+ page cache. [Data fetching](../../concepts/data-fetching.md) covers what is safe to
105
+ cache.
106
+
107
+ ## Where the browser sends requests
108
+
109
+ With `hosted({ url })`, the browser calls two backend endpoints:
110
+
111
+ | Request | When |
112
+ | --------------------- | ------------------------------------------ |
113
+ | `GET {url}/init` | Once per page load, to resolve the policy |
114
+ | `POST {url}/subjects` | When the visitor accepts, rejects or saves |
115
+
116
+ Both go to the backend's origin, so it must list your site as a trusted origin.
117
+ [Data fetching](../../concepts/data-fetching.md) compares this with proxies and
118
+ custom transports.
119
+
120
+ ## Check the rendering path
121
+
122
+ 1. Open the page in a private window and view the page source. In a
123
+ server-rendered app the HTML contains your content and no element with
124
+ `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
128
+ until you allow their category.
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Scripts
3
+ description: Load vendor scripts by consent category in a React app with
4
+ ConsentProvider, and clear stored data or reload the page when a visitor
5
+ withdraws consent.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Register vendor scripts
10
+
11
+ Pass vendor loaders to `ConsentProvider` through `options.scripts`. The
12
+ provider loads each script when its category is allowed and removes it when
13
+ the category is denied. The [quickstart](./quickstart.md)
14
+ registers PostHog and X Pixel this way:
15
+
16
+ ```ts title="src/scripts.ts"
17
+ import { posthog } from '@c15t/integrations/posthog';
18
+ import { xPixel } from '@c15t/integrations/x-pixel';
19
+
20
+ export const scripts = [
21
+ posthog({
22
+ id: 'phc_your_project_key',
23
+ initOptions: { cookieless_mode: 'never' },
24
+ loadMode: 'after-consent',
25
+ }),
26
+ xPixel({ pixelId: 'your-pixel-id' }),
27
+ ];
28
+ ```
29
+
30
+ The provider in `src/consent.tsx` receives them:
31
+
32
+ ```tsx title="src/consent.tsx"
33
+ import {
34
+ ConsentBanner,
35
+ ConsentDialog,
36
+ ConsentDialogLink,
37
+ ConsentProvider,
38
+ hosted,
39
+ } from 'c15t/react';
40
+ import type { ReactNode } from 'react';
41
+
42
+ import { scripts } from './scripts';
43
+
44
+ import 'c15t/react/styles.css';
45
+
46
+ const mode = hosted({ url: 'https://your-project.inth.app' });
47
+
48
+ export const Consent = ({ children }: { children: ReactNode }) => (
49
+ <ConsentProvider options={{ mode, scripts }}>
50
+ {children}
51
+ <ConsentBanner />
52
+ <ConsentDialog />
53
+ <footer>
54
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
55
+ </footer>
56
+ </ConsentProvider>
57
+ );
58
+ ```
59
+
60
+ Each helper from `@c15t/integrations` sets its own category and a stable `id`.
61
+ Remove every other loader for the same vendor, such as a `<script>` tag in
62
+ `index.html` or an SDK you initialize at module level, so the vendor loads
63
+ once and only through c15t. [Integrations](../../integrations/overview.md) lists
64
+ every helper, and [building integrations](../../integrations/building-integrations.md)
65
+ covers a vendor without one.
66
+
67
+ ## What happens when consent changes
68
+
69
+ * **Allowed.** The script loads, or its SDK is started, the first time its
70
+ category is allowed.
71
+ * **Denied later.** The provider removes the script element. Code that already
72
+ ran keeps running, so after the visitor turns off a category they had
73
+ allowed, the provider reloads the page once the save finishes. Set
74
+ `reloadOnConsentRevoked: false` only if every gated vendor stops itself.
75
+ * **`alwaysLoad` helpers.** Some integrations, such as Google Consent Mode,
76
+ load before consent and pass the visitor's choice to the vendor. Read the
77
+ vendor's guide; a category on a script does not always mean zero requests.
78
+
79
+ ## Embeds and other requests
80
+
81
+ Scripts cover vendor code c15t loads for you. For the rest:
82
+
83
+ * [Embeds](./embeds.md) keeps iframes out of the page with
84
+ `ConsentGate` or the iframe blocker.
85
+ * [Network blocker](./network-blocker.md) holds `fetch` and
86
+ XHR calls that match a rule until their category is allowed.
87
+
88
+ ## Clear stored data after revocation
89
+
90
+ Removing a script does not delete the cookies or storage entries it wrote. Add
91
+ `clearOnRevocation` to the provider options to delete the entries you list for
92
+ each category when it is denied. The option is read once, when the provider
93
+ mounts. [Clear on revocation](./clear-on-revocation.md) covers
94
+ configuration and browser limits.
95
+
96
+ ## Let visitors turn off one vendor
97
+
98
+ A visitor can allow marketing and still switch off one vendor in it. Pass
99
+ `vendors` to the provider options. Helpers from `@c15t/integrations` already carry
100
+ their vendor slug. See [vendor consent](./vendor-consent.md).
101
+
102
+ To send your own events only to allowed integrations, see
103
+ [send events only to allowed integrations](../../integrations/overview.md#send-events-only-to-allowed-integrations).
104
+
105
+ ## Check the scripts
106
+
107
+ Open DevTools Network in a private window, filter by each vendor's domain and
108
+ reload.
109
+
110
+ 1. Before a choice under an opt-in policy, no vendor script loads.
111
+ 2. Allow one category. Only that category's vendors load.
112
+ 3. Turn the category off. The page reloads and the vendor stays absent.
113
+ 4. Reload again. The rejection holds.
114
+
115
+ [Verify consent](../../guides/verify-consent.md) has the full checklist.
@@ -0,0 +1,155 @@
1
+ ---
2
+ title: Translations
3
+ description: Where c15t banner and dialog copy comes from in a React app, how
4
+ the browser picks the visitor's language, and how to override copy and switch
5
+ languages with the provider's i18n option and hooks.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Where the copy comes from
10
+
11
+ The banner, dialog, widget and gate placeholder read their text from one set
12
+ of translations for the visitor's language. Which set depends on the
13
+ provider's `mode`:
14
+
15
+ | Mode | Copy comes from |
16
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `hosted()` | The backend's `/init` response for the visitor's language, from the translations in your Inth project or your self-hosted backend's configuration, with your `i18n` messages for that language on top |
18
+ | `offline()` | Your `i18n` messages, merged over bundled English |
19
+
20
+ With a backend, edit wording in your Inth project first, so it changes without
21
+ a deploy. Code can override that copy in two places:
22
+
23
+ * `i18n` in the `ConsentProvider` options changes a message everywhere it
24
+ appears.
25
+ * Text props on `ConsentBanner` change one banner.
26
+
27
+ [Copy and translations](../../customization/translations.md) lists the message
28
+ keys and explains how code messages combine with the project's copy.
29
+
30
+ ## How c15t picks the language
31
+
32
+ In `hosted()` mode the browser's `/init` request carries its own
33
+ `Accept-Language` header, and the backend answers with copy in that language.
34
+ When your project has no copy for it, the backend returns its fallback
35
+ language. Set `overrides.language` in the provider options to request a
36
+ specific language instead.
37
+
38
+ In `offline()` mode, `i18n.locale` picks the language, `en` by default. The
39
+ browser's language is not read.
40
+
41
+ The banner, dialog and widget set `dir` from the active language. Right-to-left
42
+ languages such as Arabic and Hebrew render with `dir="rtl"`, and a floating
43
+ banner you did not position yourself moves to the opposite side.
44
+
45
+ ## Change the copy in code
46
+
47
+ Keep the messages in their own module:
48
+
49
+ ```ts title="src/consent-i18n.ts"
50
+ import type { ConsentProviderOptions } from 'c15t/react';
51
+
52
+ // Keys you leave out keep the copy from your Inth project.
53
+ export const i18n = {
54
+ messages: {
55
+ de: {
56
+ common: { rejectAll: 'Optionale ablehnen' },
57
+ cookieBanner: { title: 'Cookies auf dieser Website' },
58
+ },
59
+ en: {
60
+ common: { rejectAll: 'Reject optional' },
61
+ cookieBanner: { title: 'Cookies on this site' },
62
+ },
63
+ },
64
+ } satisfies ConsentProviderOptions['i18n'];
65
+ ```
66
+
67
+ Pass them to the provider in the file that renders it, such as
68
+ `src/consent.tsx` from the [quickstart](./quickstart.md):
69
+
70
+ ```tsx title="src/consent.tsx"
71
+ <ConsentProvider options={{ mode, scripts, i18n }}>
72
+ ```
73
+
74
+ For the visitor's language, your messages replace the backend's copy key by
75
+ key. Keys you leave out keep the project's wording, and messages for other
76
+ languages are not applied. In `offline()` mode c15t bundles only English, so
77
+ keys you leave out for another language show in English.
78
+
79
+ The provider reads `i18n` once, when it mounts. In development it logs a
80
+ warning if `i18n` changes later. Define the object at module level, as above,
81
+ rather than inside a component.
82
+
83
+ ## Change one banner's copy
84
+
85
+ `ConsentBanner` takes `title`, `description`, `acceptButtonText`,
86
+ `rejectButtonText`, `customizeButtonText` and `dismissButtonText`. The props
87
+ replace the text in every language, so pass values in the page's language:
88
+
89
+ ```tsx
90
+ <ConsentBanner
91
+ title="Cookies on this site"
92
+ rejectButtonText="Reject optional"
93
+ />;
94
+ ```
95
+
96
+ ## Switch the language at runtime
97
+
98
+ `useSetLanguage()` stores the new language, and `useInit()` resolves the policy
99
+ again, which fetches the copy for that language. Call both from your language
100
+ switcher:
101
+
102
+ ```tsx title="src/language-switcher.tsx"
103
+ import { useInit, useSetLanguage, useSnapshot } from 'c15t/react';
104
+
105
+ const languages = [
106
+ { code: 'en', label: 'English' },
107
+ { code: 'de', label: 'Deutsch' },
108
+ ];
109
+
110
+ export const LanguageSwitcher = () => {
111
+ const setLanguage = useSetLanguage();
112
+ const init = useInit();
113
+ const language = useSnapshot().translations?.language ?? 'en';
114
+
115
+ return (
116
+ <select
117
+ aria-label="Consent language"
118
+ value={language}
119
+ onChange={(event) => {
120
+ setLanguage(event.target.value);
121
+ // Setting the language alone keeps the current copy. Init fetches
122
+ // the policy and copy for the new language.
123
+ void init();
124
+ }}
125
+ >
126
+ {languages.map(({ code, label }) => (
127
+ <option key={code} value={code}>
128
+ {label}
129
+ </option>
130
+ ))}
131
+ </select>
132
+ );
133
+ };
134
+ ```
135
+
136
+ Setting the language without calling `init()` changes nothing on screen. Your
137
+ `i18n` messages for the new language apply on top of the copy that arrives.
138
+ Render the switcher anywhere inside `ConsentProvider`.
139
+
140
+ Switching needs a backend. In `offline()` mode the copy stays in the
141
+ `i18n.locale` language.
142
+
143
+ If your site changes language through its own i18n library, you can instead
144
+ pass its locale as `overrides.language` in the provider options. When that
145
+ value changes, the provider resolves the policy again in the new language.
146
+
147
+ ## Verify
148
+
149
+ 1. Set your browser language to one your project translates, clear site data
150
+ and reload under a policy that shows a banner. DevTools Network shows the
151
+ `/init` request with that `Accept-Language` header, and the banner uses
152
+ that language.
153
+ 2. Check that a message you set in `i18n` shows in the banner.
154
+ 3. Switch the language with your switcher. A new `/init` request runs, and the
155
+ banner and dialog text change without a reload.
@@ -1,67 +1,112 @@
1
1
  ---
2
- title: Troubleshoot React consent
3
- description: Diagnose policy resolution, early scripts, storage and rendering problems.
2
+ title: Troubleshooting
3
+ description: Fix a missing banner, early vendor requests, unstyled components
4
+ and theme warnings in a React app that uses c15t/react.
4
5
  group: frameworks
5
6
  ---
6
7
 
7
- ## Why is there no banner?
8
+ ## The /init request goes to your-project.inth.app
8
9
 
9
- Inspect `resolution` and `promptRequirement` before changing styles. Pending or
10
- failed initialization, no matching rule, a valid stored choice and a rule that
11
- requires no prompt can all produce no banner for different reasons. A missing
12
- stylesheet can also make rendered controls appear incorrectly.
10
+ **Check.** In DevTools Network, the `GET` request to `/init` goes to
11
+ `your-project.inth.app` and fails.
13
12
 
14
- Check the backend URL and Network response, then confirm the active policy for
15
- the visitor's location. Do not solve missing UI by granting every category or
16
- setting `enabled: false`: disabling the runtime permits optional loading.
13
+ **Fix.** Replace the placeholder URL in `hosted({ url })` in `src/consent.tsx`
14
+ with the backend URL from your Inth project, exactly as Inth shows it, then
15
+ rebuild.
17
16
 
18
- ## Why does the UI disappear with a content blocker?
17
+ ## There is no banner
19
18
 
20
- Check the browser Network panel for `ERR_BLOCKED_BY_CLIENT` or a failed dynamic
21
- import. Older c15t builds used component filenames such as
22
- `consent-dialog-*.js`, which some cookie-annoyance lists block. Update the c15t
23
- packages and rebuild the app. Current component modules use neutral filenames;
24
- public component imports stay the same. This also covers Vite development
25
- requests used by TanStack Start and other Vite integrations.
19
+ **Check.** In DevTools Network, find the `GET` request to `/init` on your
20
+ backend URL.
26
21
 
27
- Extensions can separately hide elements with cosmetic filters or block a
28
- configured backend URL. Check the failed request or hidden element to distinguish
29
- those cases from a missing component module.
22
+ | What you see | Cause | Fix |
23
+ | ------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
24
+ | No `/init` request | `ConsentProvider` is not mounted, or an error stopped `src/consent.tsx` from rendering | Check the console for errors and render one provider at the app root, as in the [quickstart](./quickstart.md) |
25
+ | `/init` fails with a CORS error | Your site's origin is not trusted | Add the exact origin, including port in development, to the trusted origins in your Inth project |
26
+ | `/init` returns 200 | The policy for this visitor asks for no banner, or a stored choice is still valid | Inspect the active policy with the component below, or clear site data |
30
27
 
31
- ## Why does analytics run before a choice?
28
+ A policy with `prompt: 'none'` shows no banner by design, and so does a
29
+ returning visitor with a valid stored choice. [Policies](../../concepts/policies.md)
30
+ explains why a region may not show one.
32
31
 
33
- Check effective permission under the selected policy, then find every loader
34
- for that vendor. Remove unconditional script tags, framework analytics plugins
35
- and duplicate tag-manager entries. c15t's script registration only controls the
36
- scripts registered with it.
32
+ ## Inspect the active policy
37
33
 
38
- Google helpers intentionally load with Consent Mode defaults. A Google request
39
- is not by itself proof that its storage consent was granted. If your requirement
40
- is no request at all, do not use an always-loaded helper unchanged.
34
+ Render this component inside `ConsentProvider`. It reads state and records no
35
+ choice:
41
36
 
42
- ## Why does a choice disappear on reload?
37
+ ```tsx title="src/consent-policy-debug.tsx"
38
+ import { useSnapshot } from 'c15t/react';
43
39
 
44
- Check whether persistence is disabled, browser storage is blocked, the origin
45
- changed, or the receipt expired or no longer matches the current policy.
46
- A development example with `persistence: false` deliberately resets on reload.
47
- Do not "repair" persistence by saving permissions automatically on mount.
40
+ export function ConsentPolicyDebug() {
41
+ const snapshot = useSnapshot();
42
+ const resolution = snapshot.resolution;
48
43
 
49
- ## Why does hydration differ from server HTML?
44
+ return (
45
+ <pre>
46
+ {JSON.stringify(
47
+ {
48
+ status: resolution.status,
49
+ policy: resolution.status === 'matched' ? resolution.policy.id : null,
50
+ prompt: snapshot.promptRequirement,
51
+ permissions: snapshot.effectivePermissions,
52
+ location: snapshot.location,
53
+ privacySignals: snapshot.privacySignals,
54
+ },
55
+ null,
56
+ 2
57
+ )}
58
+ </pre>
59
+ );
60
+ }
61
+ ```
50
62
 
51
- Use the adapter's request helper and pass the returned configuration unchanged
52
- to its boundary. Check that server and browser use the same backend and policy
53
- inputs. A module-level mutable runtime on a server can share one visitor's state
54
- with another request; create request-owned state instead.
63
+ The component re-renders on every consent change. Remove it after you finish.
64
+ For a feature gate, use `useConsent('marketing')` or the category you need.
55
65
 
56
- ## Why does static hosting fail when development works?
66
+ ## A vendor loads before the visitor chooses
57
67
 
58
- A static host has no app server for init routes, proxies or server functions.
59
- Use absolute external consent URLs or an explicitly local policy. Test the
60
- production output with a static file server, not the framework dev server.
68
+ **Check.** Search `index.html`, your entry module and any tag manager for the
69
+ vendor's script or SDK initializer.
61
70
 
62
- ## Why does customization do nothing?
71
+ **Fix.** Remove every loader except the one in `options.scripts`. c15t only
72
+ gates scripts registered with it, and the network blocker cannot stop code that
73
+ runs before the provider renders. [Scripts and embeds](./scripts.md)
74
+ covers the options.
63
75
 
64
- Check the imported stylesheet, the correct token or slot, and which element
65
- carries the state attribute. `data-variant` on a banner root is not a matching
66
- attribute on its child card. Check cascade layers and the Tailwind version
67
- before adding specificity. See [customization](../../customization/overview.md).
76
+ ## The banner has no styles
77
+
78
+ **Check.** In DevTools Elements, the banner has classes but no rules apply.
79
+
80
+ **Fix.** Import `c15t/react/styles.css` once in the module that renders the
81
+ provider. With Tailwind 3, keep that import above the `@tailwind` directives
82
+ and add `c15t/postcss-tailwind3` before `tailwindcss` in your PostCSS
83
+ config. Without the plugin, Tailwind 3 drops c15t's rules or fails the build
84
+ with `` `@layer components` is used but no matching `@tailwind components` directive is present ``.
85
+ See [Tailwind CSS](../../customization/tailwind.md#set-up-tailwind-css-3).
86
+
87
+ ## The console warns that theme tokens are no longer turned into CSS
88
+
89
+ **Check.** `options.theme` holds colors, radius or other tokens, and the page
90
+ has no `c15t-theme` style element. The provider only reads `consentActions`
91
+ and slot styles from `theme`.
92
+
93
+ **Fix.** Move the tokens to your CSS or render `ConsentTheme`, as shown in
94
+ [customize](./customize.md#change-colors-fonts-and-radius).
95
+
96
+ ## The banner is missing from server-rendered HTML
97
+
98
+ **Check.** Your app uses React Router framework mode, Remix or another
99
+ server-rendered setup without a c15t adapter. There this is expected: the
100
+ browser resolves consent after hydration.
101
+
102
+ **Fix.** None is needed for consent to work.
103
+ [Rendering](./rendering.md#react-router-remix-and-other-server-rendered-apps)
104
+ explains the options.
105
+
106
+ ## More help
107
+
108
+ [Troubleshoot consent](../../guides/troubleshooting.md) covers problems shared by
109
+ every framework, such as a missing banner, analytics that load before a choice,
110
+ imports that fail because npm installed c15t v2, choices that disappear on
111
+ reload, server HTML that differs from the browser, static builds that fail and
112
+ content blockers that hide the consent UI.