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

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 (248) hide show
  1. package/AGENTS.md +91 -67
  2. package/README.md +21 -17
  3. package/SKILL.md +34 -0
  4. package/dist/aggregate-components.js +1 -1
  5. package/dist/chunk-warming.js +1 -1
  6. package/dist/component-hooks/use-headless-consent-ui.js +1 -1
  7. package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
  8. package/dist/component-hooks/use-panel-trigger.js +1 -1
  9. package/dist/components/consent-gate/atoms.js +1 -1
  10. package/dist/components/consent-gate/consent-gate.js +1 -1
  11. package/dist/components/consent-preferences-link/consent-preferences-link.js +1 -1
  12. package/dist/components/iab-panel/atoms/card.js +1 -1
  13. package/dist/components/iab-panel/atoms/feature-item.js +2 -0
  14. package/dist/components/iab-panel/atoms/header.js +1 -1
  15. package/dist/components/iab-panel/atoms/purpose-item.js +1 -1
  16. package/dist/components/iab-panel/atoms/stack-item.js +1 -1
  17. package/dist/components/iab-panel/iab-panel.js +1 -1
  18. package/dist/components/iab-panel/index.js +1 -1
  19. package/dist/components/iab-prompt/atoms/overlay.js +1 -1
  20. package/dist/components/iab-prompt/atoms/root.js +1 -1
  21. package/dist/components/panel/atoms/card.js +1 -1
  22. package/dist/components/panel/atoms/overlay.js +1 -1
  23. package/dist/components/panel/atoms/root.js +1 -1
  24. package/dist/components/panel/index.js +1 -1
  25. package/dist/components/panel/panel.js +1 -1
  26. package/dist/components/panel-trigger/atoms/button.js +1 -1
  27. package/dist/components/panel-trigger/atoms/icon.js +1 -1
  28. package/dist/components/panel-trigger/atoms/root.js +1 -1
  29. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  30. package/dist/components/panel-trigger/atoms/trigger-icons.js +1 -0
  31. package/dist/components/panel-trigger/panel-trigger-toolbar.js +1 -1
  32. package/dist/components/preferences/atoms/accordion.js +1 -1
  33. package/dist/components/preferences/atoms/root.js +1 -1
  34. package/dist/components/preferences/atoms/vendor-list.js +1 -1
  35. package/dist/components/preferences/index.js +1 -1
  36. package/dist/components/prompt/atoms/overlay.js +1 -1
  37. package/dist/components/prompt/atoms/root.js +1 -1
  38. package/dist/components/prompt/components.js +1 -1
  39. package/dist/components/prompt/policy-actions.js +1 -1
  40. package/dist/components/shared/libs/compose-refs.js +1 -0
  41. package/dist/components/shared/libs/slot.js +1 -1
  42. package/dist/components/shared/primitives/button.js +1 -1
  43. package/dist/components/shared/ui/accordion/accordion.js +1 -1
  44. package/dist/components/shared/ui/collapsible/collapsible.js +1 -1
  45. package/dist/components/shared/ui/dialog/dialog.js +1 -1
  46. package/dist/components/shared/ui/icon.js +1 -1
  47. package/dist/components/shared/ui/logo.js +1 -1
  48. package/dist/components/shared/ui/preference-item/preference-item.js +1 -1
  49. package/dist/components/shared/ui/switch/switch.js +1 -1
  50. package/dist/components/shared/ui/tabs/tabs.js +1 -1
  51. package/dist/consent-theme.js +1 -0
  52. package/dist/context/consent-tracking-context.js +1 -1
  53. package/dist/devtools.js +1 -1
  54. package/dist/draft.js +1 -1
  55. package/dist/external-iab-context.js +1 -1
  56. package/dist/headless.js +1 -1
  57. package/dist/hooks/use-committed-ref.js +2 -0
  58. package/dist/hooks/use-focus-trap.js +1 -1
  59. package/dist/hooks.js +1 -1
  60. package/dist/iab/styles.tw3.css +5 -45
  61. package/dist/iab-context.js +1 -1
  62. package/dist/index.js +1 -1
  63. package/dist/kernel-selector.js +1 -0
  64. package/dist/module-hooks/iframe-blocker.js +1 -1
  65. package/dist/module-hooks/network-blocker.js +1 -1
  66. package/dist/module-hooks/network-hold.js +2 -0
  67. package/dist/module-hooks/persistence.js +1 -1
  68. package/dist/panel.js +1 -1
  69. package/dist/postcss-tailwind3.js +1 -0
  70. package/dist/preferences.js +1 -2
  71. package/dist/provider.js +1 -1
  72. package/dist/server/fetch-ssr-data.js +1 -1
  73. package/dist/server/headers.js +1 -1
  74. package/dist/server/normalize-url.js +1 -1
  75. package/dist/styles.tw3.css +16 -61
  76. package/dist/transports/offline.js +1 -1
  77. package/dist/ui-save.js +1 -1
  78. package/dist/utils/deferred-module.js +1 -1
  79. package/dist/utils/merge-slot-props.js +1 -1
  80. package/dist/utils/theme-utils.js +1 -1
  81. package/dist/version.js +1 -1
  82. package/dist-types/chunk-warming.d.ts +41 -8
  83. package/dist-types/component-hooks/use-headless-consent-ui.d.ts +1 -1
  84. package/dist-types/component-hooks/use-headless-iab-consent-ui.d.ts +1 -1
  85. package/dist-types/components/iab-panel/atoms/feature-item.d.ts +28 -0
  86. package/dist-types/components/iab-panel/atoms/purpose-item.d.ts +4 -1
  87. package/dist-types/components/iab-panel/atoms/stack-item.d.ts +6 -1
  88. package/dist-types/components/iab-panel/iab-panel.d.ts +2 -2
  89. package/dist-types/components/iab-panel/index.d.ts +6 -2
  90. package/dist-types/components/iab-panel/use-iab-translations.d.ts +4 -0
  91. package/dist-types/components/iab-prompt/iab-prompt.d.ts +2 -2
  92. package/dist-types/components/iab-prompt/index.d.ts +2 -2
  93. package/dist-types/components/panel/atoms/card.d.ts +1 -0
  94. package/dist-types/components/panel/index.d.ts +1 -0
  95. package/dist-types/components/panel/panel.d.ts +1 -1
  96. package/dist-types/components/panel-trigger/atoms/trigger-icons.d.ts +20 -0
  97. package/dist-types/components/preferences/index.d.ts +1 -0
  98. package/dist-types/components/prompt/components.d.ts +6 -0
  99. package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
  100. package/dist-types/components/shared/ui/accordion/accordion.d.ts +1 -0
  101. package/dist-types/components/shared/ui/collapsible/collapsible.d.ts +1 -0
  102. package/dist-types/components/shared/ui/icon.d.ts +2 -1
  103. package/dist-types/components/shared/ui/logo.d.ts +0 -10
  104. package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +14 -0
  105. package/dist-types/components/shared/ui/switch/switch.d.ts +1 -0
  106. package/dist-types/components/shared/ui/tabs/tabs.d.ts +1 -0
  107. package/dist-types/consent-theme.d.ts +38 -0
  108. package/dist-types/context/consent-tracking-context.d.ts +10 -0
  109. package/dist-types/context/theme-context.d.ts +2 -2
  110. package/dist-types/context.d.ts +6 -0
  111. package/dist-types/devtools.d.ts +13 -2
  112. package/dist-types/draft.d.ts +50 -7
  113. package/dist-types/headless.d.ts +0 -1
  114. package/dist-types/hooks/use-committed-ref.d.ts +24 -0
  115. package/dist-types/hooks/use-focus-trap.d.ts +3 -1
  116. package/dist-types/hooks.d.ts +56 -13
  117. package/dist-types/iab.d.ts +1 -1
  118. package/dist-types/index.d.ts +4 -3
  119. package/dist-types/kernel-selector.d.ts +66 -0
  120. package/dist-types/module-hooks/iframe-blocker.d.ts +27 -0
  121. package/dist-types/module-hooks/network-blocker.d.ts +23 -0
  122. package/dist-types/module-hooks/network-hold.d.ts +47 -0
  123. package/dist-types/panel.d.ts +13 -1
  124. package/dist-types/postcss-tailwind3.d.ts +21 -0
  125. package/dist-types/preferences.d.ts +12 -1
  126. package/dist-types/provider.d.ts +62 -12
  127. package/dist-types/server/headers.d.ts +14 -4
  128. package/dist-types/server/normalize-url.d.ts +14 -7
  129. package/dist-types/server/types.d.ts +12 -1
  130. package/dist-types/transports/offline.d.ts +7 -16
  131. package/dist-types/ui-config-context.d.ts +9 -1
  132. package/dist-types/ui-save.d.ts +21 -1
  133. package/dist-types/utils/deferred-module.d.ts +3 -1
  134. package/dist-types/utils/merge-slot-props.d.ts +15 -1
  135. package/dist-types/utils/theme-utils.d.ts +17 -2
  136. package/dist-types/version.d.ts +1 -1
  137. package/docs/README.md +91 -67
  138. package/docs/assets/v3/bottom-bar.png +0 -0
  139. package/docs/assets/v3/brand-card.png +0 -0
  140. package/docs/assets/v3/brand-preferences.png +0 -0
  141. package/docs/assets/v3/choice-wall.png +0 -0
  142. package/docs/assets/v3/headless-bar-html.png +0 -0
  143. package/docs/assets/v3/headless-bar-mobile.png +0 -0
  144. package/docs/assets/v3/headless-bar.png +0 -0
  145. package/docs/assets/v3/slim-bar.png +0 -0
  146. package/docs/concepts/choose-your-setup.md +87 -0
  147. package/docs/{frameworks/react/concepts → concepts}/consent-categories.md +30 -8
  148. package/docs/concepts/consent-state.md +357 -0
  149. package/docs/{guides → concepts}/data-fetching.md +31 -27
  150. package/docs/concepts/how-consent-works.md +123 -0
  151. package/docs/{frameworks/react/concepts/policy-presets.md → concepts/policies.md} +7 -59
  152. package/docs/customization/class-names.md +202 -0
  153. package/docs/customization/dark-mode.md +157 -0
  154. package/docs/customization/motion.md +119 -0
  155. package/docs/customization/overview.md +67 -33
  156. package/docs/customization/recipes.md +839 -47
  157. package/docs/customization/slots.md +216 -35
  158. package/docs/customization/stylesheets.md +147 -0
  159. package/docs/customization/tailwind.md +842 -0
  160. package/docs/customization/tokens.md +166 -36
  161. package/docs/customization/translations.md +61 -3
  162. package/docs/frameworks/react/callbacks.md +118 -0
  163. package/docs/frameworks/react/clear-on-revocation.md +106 -0
  164. package/docs/frameworks/react/components/consent-banner.md +42 -28
  165. package/docs/frameworks/react/components/consent-dialog-link.md +13 -11
  166. package/docs/frameworks/react/components/consent-dialog-trigger.md +81 -76
  167. package/docs/frameworks/react/components/consent-dialog.md +100 -55
  168. package/docs/frameworks/react/components/consent-gate.md +45 -24
  169. package/docs/frameworks/react/components/consent-provider.md +126 -0
  170. package/docs/frameworks/react/components/consent-widget.md +32 -20
  171. package/docs/frameworks/react/components/dev-tools.md +52 -84
  172. package/docs/frameworks/react/components.md +105 -0
  173. package/docs/frameworks/react/compose.md +354 -0
  174. package/docs/frameworks/react/content-security-policy.md +126 -0
  175. package/docs/frameworks/react/customize.md +250 -0
  176. package/docs/frameworks/react/embeds.md +89 -0
  177. package/docs/frameworks/react/hooks.md +65 -0
  178. package/docs/frameworks/react/iab.md +222 -0
  179. package/docs/frameworks/react/network-blocker.md +140 -0
  180. package/docs/frameworks/react/quickstart.md +139 -101
  181. package/docs/frameworks/react/rendering.md +128 -0
  182. package/docs/frameworks/react/scripts.md +115 -0
  183. package/docs/frameworks/react/translations.md +155 -0
  184. package/docs/frameworks/react/troubleshooting.md +92 -47
  185. package/docs/frameworks/react/vendor-consent.md +170 -0
  186. package/docs/guides/banner-experiments.md +654 -0
  187. package/docs/guides/troubleshooting.md +120 -47
  188. package/docs/guides/verify-consent.md +81 -49
  189. package/docs/integrations/adobe-analytics.md +168 -160
  190. package/docs/integrations/ahrefs-analytics.md +153 -155
  191. package/docs/integrations/amplitude.md +163 -157
  192. package/docs/integrations/building-integrations.md +136 -37
  193. package/docs/integrations/clearbit.md +155 -155
  194. package/docs/integrations/cloudflare-web-analytics.md +157 -157
  195. package/docs/integrations/cloudflare-zaraz.md +210 -262
  196. package/docs/integrations/crisp.md +165 -159
  197. package/docs/integrations/databuddy.md +158 -174
  198. package/docs/integrations/fathom-analytics.md +159 -157
  199. package/docs/integrations/front-chat.md +322 -0
  200. package/docs/integrations/google-maps.md +119 -84
  201. package/docs/integrations/google-tag-manager.md +179 -164
  202. package/docs/integrations/google-tag.md +164 -161
  203. package/docs/integrations/heap.md +164 -156
  204. package/docs/integrations/hightouch.md +162 -158
  205. package/docs/integrations/hotjar.md +160 -156
  206. package/docs/integrations/intercom.md +184 -154
  207. package/docs/integrations/klaviyo.md +486 -0
  208. package/docs/integrations/linkedin-insights.md +175 -151
  209. package/docs/integrations/logrocket.md +161 -157
  210. package/docs/integrations/matomo-analytics.md +189 -179
  211. package/docs/integrations/meta-pixel.md +189 -151
  212. package/docs/integrations/microsoft-clarity.md +164 -156
  213. package/docs/integrations/microsoft-uet.md +149 -155
  214. package/docs/integrations/mixpanel-analytics.md +156 -161
  215. package/docs/integrations/one-dollar-stats.md +306 -0
  216. package/docs/integrations/openai-pixel.md +205 -302
  217. package/docs/integrations/overview.md +143 -83
  218. package/docs/integrations/pinterest-tag.md +329 -0
  219. package/docs/integrations/pirsch.md +170 -160
  220. package/docs/integrations/plausible-analytics.md +173 -159
  221. package/docs/integrations/posthog.md +227 -242
  222. package/docs/integrations/promptwatch.md +155 -155
  223. package/docs/integrations/reddit-pixel.md +186 -158
  224. package/docs/integrations/rudderstack.md +202 -187
  225. package/docs/integrations/rybbit-analytics.md +172 -161
  226. package/docs/integrations/segment.md +183 -155
  227. package/docs/integrations/snapchat-pixel.md +187 -157
  228. package/docs/integrations/tiktok-pixel.md +172 -151
  229. package/docs/integrations/umami-analytics.md +164 -159
  230. package/docs/integrations/vercel-analytics.md +167 -158
  231. package/docs/integrations/x-pixel.md +177 -151
  232. package/docs/integrations/youtube.md +122 -87
  233. package/docs/upgrade-v3.md +490 -354
  234. package/package.json +15 -9
  235. package/dist/component-hooks/use-manager.js +0 -2
  236. package/dist-types/component-hooks/use-manager.d.ts +0 -45
  237. package/docs/assets/v3/brand-bar.png +0 -0
  238. package/docs/assets/v3/mobile-card.png +0 -0
  239. package/docs/assets/v3/preferences.png +0 -0
  240. package/docs/frameworks/react/components/consent-manager-provider.md +0 -75
  241. package/docs/frameworks/react/hooks/use-consent-manager/overview.md +0 -58
  242. package/docs/frameworks/react/iab/overview.md +0 -53
  243. package/docs/frameworks/react/script-loader.md +0 -63
  244. package/docs/frameworks/react/styling/overview.md +0 -217
  245. package/docs/guides/consent-state.md +0 -60
  246. package/docs/guides/deployment-modes.md +0 -75
  247. package/docs/integrations/clear-on-revocation.md +0 -167
  248. package/docs/integrations/granular-consent.md +0 -208
@@ -0,0 +1,140 @@
1
+ ---
2
+ title: Network blocker
3
+ description: Hold fetch and XMLHttpRequest calls in a React app until their
4
+ consent category is allowed, with networkBlocker rules in the ConsentProvider
5
+ options.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Add rules
10
+
11
+ Some SDKs are already on the page and send beacons or API calls of their own.
12
+ Add `networkBlocker` rules to the provider options to stop `fetch` and
13
+ `XMLHttpRequest` calls to those domains until the category is allowed. Load
14
+ vendor SDKs through [`scripts`](./scripts.md) first, so they
15
+ do not run at all before consent.
16
+
17
+ This partial example adds the option next to the existing `mode` and
18
+ `scripts`:
19
+
20
+ ```tsx title="src/consent.tsx"
21
+ const networkBlocker = {
22
+ rules: [
23
+ {
24
+ id: 'google-analytics',
25
+ domain: 'google-analytics.com',
26
+ category: 'measurement',
27
+ },
28
+ {
29
+ id: 'meta-pixel',
30
+ domain: 'facebook.com',
31
+ pathIncludes: '/tr',
32
+ category: 'marketing',
33
+ },
34
+ ],
35
+ };
36
+
37
+ <ConsentProvider options={{ mode, scripts, networkBlocker }}>
38
+ ```
39
+
40
+ ## Match requests with rules
41
+
42
+ Each rule names a `domain` and the consent `category` a request needs. The
43
+ domain also matches its subdomains: `google-analytics.com` covers
44
+ `www.google-analytics.com`. `pathIncludes` narrows the rule to paths that
45
+ contain a substring, and `methods` narrows it to HTTP methods. A request is
46
+ blocked when a matching rule's condition is not met by the visitor's
47
+ effective permissions.
48
+
49
+ `category` takes the same conditions as scripts:
50
+
51
+ ```ts
52
+ { category: 'measurement' }
53
+ { category: { and: ['measurement', 'marketing'] } }
54
+ { category: { or: ['measurement', 'marketing'] } }
55
+ ```
56
+
57
+ Add `vendor` to also block the request while the visitor has turned that
58
+ vendor off. IAB TCF rules use `vendorId` and the `iab*` purpose fields
59
+ instead.
60
+
61
+ | Option | Default | Purpose |
62
+ | -------------------- | -------- | ------------------------------------------------------------ |
63
+ | `rules` | required | Rules described above |
64
+ | `enabled` | `true` | Set `false` to keep the rules but stop blocking |
65
+ | `logBlockedRequests` | `true` | Log each blocked request with `console.warn` |
66
+ | `onRequestBlocked` | none | Called with `{ method, url, rule }` for each blocked request |
67
+
68
+ ## What a blocked request looks like
69
+
70
+ The blocker wraps `window.fetch` and `XMLHttpRequest`. A blocked `fetch`
71
+ resolves to a `451` response with the status text
72
+ `Request blocked by consent`, and nothing is sent. A blocked XHR is aborted
73
+ and fires an `error` event. Requests that match no rule are not delayed.
74
+
75
+ ## When blocking starts
76
+
77
+ The provider holds matching requests from its first render in the browser,
78
+ before any of its children render or run effects. That covers requests from
79
+ child components, including their mount effects, and from effects in
80
+ components rendered next to the provider. The blocker module itself loads
81
+ after mount and decides each held request. Apps without `networkBlocker` do
82
+ not download it.
83
+
84
+ The standalone `useNetworkBlocker` hook works the same way from the first
85
+ render of the component that calls it. That render patches `fetch` and
86
+ `XMLHttpRequest`. If React throws the render away and never commits it, the
87
+ hold ends after 10 seconds. Nothing checked consent for the requests it held,
88
+ so they fail the way the blocker fails a blocked request: a 451 response for
89
+ `fetch`, a failed XHR. The same happens when the component unmounts before
90
+ the blocker loads.
91
+
92
+ While consent is unknown, a matching request that would be blocked waits
93
+ instead of failing. Consent is unknown until the policy has loaded, which is
94
+ also when a returning visitor's stored choice takes effect. The request is
95
+ then sent if the choice allows it and blocked otherwise. If the policy fails
96
+ to load, optional categories stay denied and waiting requests are blocked.
97
+ If the policy request never finishes, they keep waiting and are never sent.
98
+
99
+ A synchronous XHR cannot wait. Before the blocker module has loaded, a
100
+ matching one throws a `NetworkError` from `send()`. After that, one that
101
+ consent does not allow yet is blocked.
102
+
103
+ ## What the network blocker cannot stop
104
+
105
+ The blocker only sees requests made after the provider starts rendering in
106
+ the browser, through `fetch` or `XMLHttpRequest`. It cannot stop:
107
+
108
+ * Code that runs before the provider renders: inline scripts in the HTML,
109
+ third-party tags in `<head>`, scripts loaded before hydration (such as
110
+ `next/script` with `beforeInteractive`), and client modules that evaluate
111
+ earlier. Webpack builds evaluate a route's client component modules when
112
+ its chunk loads, so their top-level code runs first. Turbopack evaluates a
113
+ client component module when its first element renders, which inside the
114
+ provider is after blocking starts.
115
+ * Code that saved its own reference to `fetch` or `XMLHttpRequest` before the
116
+ provider rendered.
117
+ * `navigator.sendBeacon`, `WebSocket`, `EventSource`, and requests made by
118
+ `<img>`, `<script>` or `<iframe>` elements, web workers and service
119
+ workers.
120
+ * With the standalone `useNetworkBlocker` hook instead of the provider
121
+ option, requests sent before the component that calls it renders.
122
+ Blocking starts in that component's first render, not the provider's.
123
+
124
+ Keep tracking calls out of that window:
125
+
126
+ * Send them from an effect or an event handler, never at module top level.
127
+ * Load vendor SDKs through `scripts` instead of a `<script>` tag or
128
+ `next/script`, so they wait for consent before they run at all.
129
+ * Check `useConsent('measurement')` (or the category you need) before you
130
+ call a vendor from your own code, and treat the blocker as a backstop.
131
+
132
+ ## Verify the blocked requests
133
+
134
+ Open DevTools Network in a private window, under a policy that asks for
135
+ consent.
136
+
137
+ 1. Before a choice, requests matching a rule are absent, and the console logs
138
+ each blocked request.
139
+ 2. Allow the rule's category and save. Matching requests go out.
140
+ 3. Reject, reload, and check that they stay absent.
@@ -1,143 +1,181 @@
1
1
  ---
2
2
  title: Quickstart
3
- description: Connect a React application to Inth and add consent UI, persistence
4
- and a preferences link.
3
+ description: Add a consent banner, preferences dialog and consent-gated scripts
4
+ to a React single-page app built with Vite, using Inth as the consent backend.
5
5
  group: frameworks
6
6
  ---
7
7
 
8
- ## Connect Inth
8
+ ## Before you start
9
9
 
10
- [Create an Inth project](https://inth.com), configure policy rules and the app's
11
- allowed origin, and copy its backend endpoint. This guide uses a client-rendered
12
- React app with Vite. Applications with a framework server should use that
13
- framework's adapter for request prefetch.
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.
14
13
 
15
- | Package manager | Command |
16
- | :-------------- | :------------------------------- |
17
- | npm | `npm install c15t @c15t/scripts` |
18
- | pnpm | `pnpm add c15t @c15t/scripts` |
19
- | yarn | `yarn add c15t @c15t/scripts` |
20
- | bun | `bun add c15t @c15t/scripts` |
14
+ * **Next.js or TanStack Start?** Use that framework's guide from the
15
+ [framework list](https://c15t.com/docs/frameworks). Their adapters resolve consent on the
16
+ server.
17
+ * **React Router framework mode, Remix or another server-rendered React app?**
18
+ This guide still works. The server renders the page without consent state and
19
+ the browser resolves it after hydration. Read
20
+ [rendering](./rendering.md#react-router-remix-and-other-server-rendered-apps)
21
+ for what that means for the first paint.
21
22
 
22
- Set `VITE_C15T_BACKEND_URL` to the endpoint from Inth. Vite embeds public values
23
- at build time, so rebuild when changing it. Keep management credentials out of
24
- public variables.
23
+ You need an [Inth](https://inth.com) project. In the project, set the policy
24
+ rules for your regions, add your site's origin to the trusted origins, and copy
25
+ the backend URL.
26
+ [Choose your setup](../../concepts/choose-your-setup.md#decide-who-runs-the-consent-backend)
27
+ covers self-hosting and offline mode.
25
28
 
26
- ## Register consent-gated scripts
29
+ ## Install c15t
27
30
 
28
- Include measurement and marketing categories in your policy for these vendors.
29
- Remove any existing loaders for them so each script loads once.
31
+ | Package manager | Command |
32
+ | :-------------- | :------------------------------------------------ |
33
+ | npm | `npm install c15t@alpha @c15t/integrations@alpha` |
34
+ | pnpm | `pnpm add c15t@alpha @c15t/integrations@alpha` |
35
+ | yarn | `yarn add c15t@alpha @c15t/integrations@alpha` |
36
+ | bun | `bun add c15t@alpha @c15t/integrations@alpha` |
30
37
 
31
- This setup uses Vite's public environment variables. Set the Inth endpoint and
32
- add your PostHog project key, PostHog API host and X Pixel ID in `.env.local`:
38
+ `c15t` contains the React adapter at `c15t/react`. `@c15t/integrations` holds the
39
+ vendor loaders used below.
33
40
 
34
- ```dotenv title=".env.local"
35
- VITE_C15T_BACKEND_URL=
36
- VITE_POSTHOG_KEY=
37
- VITE_POSTHOG_HOST=
38
- VITE_X_PIXEL_ID=
39
- ```
40
-
41
- Copy the exact backend endpoint from Inth. The vendor IDs are optional: leaving
42
- an ID empty disables that integration. When enabling PostHog, fill in both its project key and the API host
43
- shown in your PostHog project. Restart Vite after editing these values and
44
- rebuild for deployment. Other bundlers need their own configuration mechanism.
41
+ ## Register your vendor scripts
45
42
 
46
- Create `src/scripts.ts`. PostHog waits for measurement permission with
47
- `loadMode: 'after-consent'` and disables cookieless capture with
48
- `cookieless_mode: 'never'`. X Pixel waits for marketing permission.
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.
49
47
 
50
48
  ```ts title="src/scripts.ts"
51
- import { posthog } from '@c15t/scripts/posthog';
52
- import { xPixel } from '@c15t/scripts/x-pixel';
53
-
54
- const posthogKey = import.meta.env.VITE_POSTHOG_KEY;
55
- const xPixelId = import.meta.env.VITE_X_PIXEL_ID;
49
+ import { posthog } from '@c15t/integrations/posthog';
50
+ import { xPixel } from '@c15t/integrations/x-pixel';
56
51
 
57
52
  export const scripts = [
58
- ...(posthogKey
59
- ? [
60
- posthog({
61
- apiHost: import.meta.env.VITE_POSTHOG_HOST,
62
- id: posthogKey,
63
- initOptions: { cookieless_mode: 'never' },
64
- loadMode: 'after-consent',
65
- }),
66
- ]
67
- : []),
68
- ...(xPixelId ? [xPixel({ pixelId: xPixelId })] : []),
53
+ posthog({
54
+ id: 'phc_your_project_key',
55
+ initOptions: { cookieless_mode: 'never' },
56
+ loadMode: 'after-consent',
57
+ }),
58
+ xPixel({ pixelId: 'your-pixel-id' }),
69
59
  ];
70
60
  ```
71
61
 
72
- ## Mount the provider and UI
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.
65
+
66
+ `loadMode: 'after-consent'` keeps the PostHog SDK off the page until the
67
+ visitor allows measurement. `cookieless_mode: 'never'` turns off PostHog's
68
+ cookieless capture. See [integrations](../../integrations/overview.md) for other
69
+ vendors.
70
+
71
+ ## Mount the provider
72
+
73
+ Create `src/consent.tsx`. `ConsentProvider` creates the consent runtime and
74
+ loads the scripts. `ConsentBanner` and `ConsentDialog` render when the policy
75
+ asks for them, and `ConsentDialogLink` keeps preferences reachable after the
76
+ banner closes.
73
77
 
74
78
  ```tsx title="src/consent.tsx"
75
- import type { ReactNode } from 'react';
76
79
  import {
77
- ConsentBanner,
78
- ConsentDialog,
79
- ConsentDialogLink,
80
- ConsentProvider,
81
- hosted,
80
+ ConsentBanner,
81
+ ConsentDialog,
82
+ ConsentDialogLink,
83
+ ConsentProvider,
84
+ hosted,
82
85
  } from 'c15t/react';
83
- import 'c15t/react/styles.css';
86
+ import type { ReactNode } from 'react';
87
+
84
88
  import { scripts } from './scripts';
85
89
 
86
- const backendURL = import.meta.env.VITE_C15T_BACKEND_URL;
87
- if (!backendURL) throw new Error('Set VITE_C15T_BACKEND_URL');
88
- const mode = hosted({ url: backendURL });
89
-
90
- export function Consent({ children }: { children: ReactNode }) {
91
- return (
92
- <ConsentProvider options={{ mode, scripts }}>
93
- {children}
94
- <ConsentBanner />
95
- <ConsentDialog />
96
- <footer><ConsentDialogLink>Privacy settings</ConsentDialogLink></footer>
97
- </ConsentProvider>
98
- );
99
- }
90
+ import 'c15t/react/styles.css';
91
+
92
+ const mode = hosted({ url: 'https://your-project.inth.app' });
93
+
94
+ export const Consent = ({ children }: { children: ReactNode }) => (
95
+ <ConsentProvider options={{ mode, scripts }}>
96
+ {children}
97
+ <ConsentBanner />
98
+ <ConsentDialog />
99
+ <footer>
100
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
101
+ </footer>
102
+ </ConsentProvider>
103
+ );
100
104
  ```
101
105
 
106
+ Replace `https://your-project.inth.app` with your project's backend URL,
107
+ exactly as Inth shows it, including any path prefix.
108
+
109
+ Wrap your app with it in `src/main.tsx`:
110
+
102
111
  ```tsx title="src/main.tsx"
103
112
  import { createRoot } from 'react-dom/client';
104
- import App from './App';
113
+
114
+ import { App } from './app';
105
115
  import { Consent } from './consent';
106
116
 
107
117
  const root = document.getElementById('root');
108
- if (!root) throw new Error('Missing #root element');
109
- createRoot(root).render(<Consent><App /></Consent>);
118
+ if (!root) {
119
+ throw new Error('Missing #root element');
120
+ }
121
+ createRoot(root).render(
122
+ <Consent>
123
+ <App />
124
+ </Consent>
125
+ );
110
126
  ```
111
127
 
112
- Keep `Consent` mounted across navigation. The banner appears when the resolved
113
- policy requires a prompt; not every region or returning visitor needs one.
114
- The dialog link keeps preferences reachable after the prompt closes.
128
+ Keep `Consent` mounted for the life of the app. If you use a client-side
129
+ router, render the router inside `Consent` so navigation does not remount the
130
+ provider. The stylesheet import in `src/consent.tsx` styles the banner and
131
+ dialog; import it once.
132
+
133
+ ## Gate your own features
115
134
 
116
- ## Gate optional functionality
135
+ Read a category's permission with `useConsent` inside the provider:
117
136
 
118
- ```tsx
137
+ ```tsx title="src/marketing-banner.tsx"
119
138
  import { useConsent } from 'c15t/react';
120
139
 
121
- export function MarketingContent() {
122
- return useConsent('marketing') ? <div>Optional marketing content</div> : null;
140
+ export function MarketingBanner() {
141
+ return useConsent('marketing') ? <aside>Spring sale</aside> : null;
123
142
  }
124
143
  ```
125
144
 
126
- This controls the rendered component. Move vendor SDKs into the
127
- [script loader](./script-loader.md) too. An independently
128
- loaded analytics script does not wait merely because a consent provider exists.
129
-
130
- `useConsent` returns effective permission, not proof of an explicit grant.
131
- Read [consent state](../../guides/consent-state.md) when recording choice events.
132
-
133
- ## Verify and customize
134
-
135
- Reject under an opt-in policy, reload and confirm optional requests stay blocked.
136
- Reopen preferences, save a different choice and test navigation. Follow
137
- [verification](../../guides/verify-consent.md), then
138
- [customization](../../customization/overview.md) to apply your design.
139
-
140
- ## Run the complete example
141
-
142
- Try the [runnable example](https://c15t.com/docs/examples) with PostHog, X Pixel, a consent-gated
143
- YouTube video and a persistent preferences control. Use it to test rejection, permission changes and reloads.
145
+ `useConsent` returns the permission right now. Under an opt-out policy it can be
146
+ `true` before the visitor has chosen anything. Read
147
+ [how consent works](../../concepts/how-consent-works.md#a-permission-is-not-a-recorded-choice)
148
+ before you record or report choices. For iframes, use
149
+ [`ConsentGate`](./components/consent-gate.md).
150
+
151
+ ## Check that it works
152
+
153
+ Build and preview the production bundle with `vite build` and `vite preview`,
154
+ then open the site in a private window with DevTools open on the Network tab.
155
+ Test under a policy that asks for a choice, such as an EU opt-in policy.
156
+
157
+ 1. **Before a choice.** The banner shows. There are no requests to
158
+ `posthog.com` or `static.ads-twitter.com`.
159
+ 2. **Reject All.** The banner closes and the vendor requests stay absent.
160
+ 3. **Reload.** The banner stays closed and the vendor requests stay absent.
161
+ 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.
164
+ 5. **Turn Analytics off again.** The page reloads and PostHog does not load.
165
+
166
+ If the banner never appears, check the Network tab for the request to
167
+ `/init` on your backend URL. [Troubleshooting](./troubleshooting.md)
168
+ covers the common causes. [Verify consent](../../guides/verify-consent.md) has the
169
+ full release checklist.
170
+
171
+ ## Next steps
172
+
173
+ * [Rendering](./rendering.md) for server-rendered React apps and
174
+ what the first paint contains.
175
+ * [Scripts and embeds](./scripts.md) for more vendors, network
176
+ blocking and data cleanup.
177
+ * [Customize](./customize.md) for colors, fonts, layout and copy.
178
+ * [Components](./components/consent-provider.md) for
179
+ every provider option and component prop.
180
+ * The runnable app in `examples/react` of the
181
+ [c15t repository](https://github.com/c15t/c15t) contains this setup.
@@ -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.