@c15t/nextjs 2.2.1 → 3.0.0-alpha.1

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 (221) hide show
  1. package/AGENTS.md +103 -142
  2. package/README.md +4 -4
  3. package/dist/api.js +1 -0
  4. package/dist/config.js +1 -0
  5. package/dist/devtools.js +2 -0
  6. package/dist/headers.js +1 -0
  7. package/dist/iab/styles.css +1 -1
  8. package/dist/iab/styles.tw3.css +27 -19
  9. package/dist/index.js +1 -1
  10. package/dist/middleware.js +1 -0
  11. package/dist/node-bridge.js +1 -0
  12. package/dist/pages.js +1 -0
  13. package/dist/proxy.js +1 -0
  14. package/dist/root.js +2 -0
  15. package/dist/server.js +1 -0
  16. package/dist/static.js +1 -0
  17. package/dist/styles.css +1 -1
  18. package/dist/styles.tw3.css +67 -29
  19. package/dist/version.js +1 -1
  20. package/dist-types/api.d.ts +88 -0
  21. package/dist-types/config.d.ts +110 -0
  22. package/dist-types/devtools.d.ts +1 -0
  23. package/dist-types/headers.d.ts +4 -0
  24. package/dist-types/index.d.ts +28 -10
  25. package/dist-types/middleware.d.ts +15 -0
  26. package/dist-types/node-bridge.d.ts +57 -0
  27. package/dist-types/pages.d.ts +88 -0
  28. package/dist-types/proxy.d.ts +34 -0
  29. package/dist-types/root.d.ts +92 -0
  30. package/dist-types/server.d.ts +164 -0
  31. package/dist-types/static.d.ts +42 -0
  32. package/dist-types/types.d.ts +5 -36
  33. package/dist-types/version.d.ts +1 -1
  34. package/docs/README.md +103 -142
  35. package/docs/assets/v3/brand-bar.png +0 -0
  36. package/docs/assets/v3/brand-card.png +0 -0
  37. package/docs/assets/v3/choice-wall.png +0 -0
  38. package/docs/assets/v3/mobile-card.png +0 -0
  39. package/docs/assets/v3/preferences.png +0 -0
  40. package/docs/customization/overview.md +45 -0
  41. package/docs/customization/recipes.md +79 -0
  42. package/docs/customization/slots.md +55 -0
  43. package/docs/customization/tokens.md +76 -0
  44. package/docs/customization/translations.md +49 -0
  45. package/docs/frameworks/next/api-reference/data-fetching.md +416 -0
  46. package/docs/frameworks/next/app-router.md +403 -0
  47. package/docs/frameworks/next/client-side.md +118 -0
  48. package/docs/frameworks/next/components/consent-banner.md +251 -211
  49. package/docs/frameworks/next/components/consent-dialog-link.md +96 -35
  50. package/docs/frameworks/next/components/consent-dialog-trigger.md +74 -149
  51. package/docs/frameworks/next/components/consent-dialog.md +189 -134
  52. package/docs/frameworks/next/components/consent-manager-provider.md +68 -318
  53. package/docs/frameworks/next/components/consent-widget.md +172 -114
  54. package/docs/frameworks/next/components/dev-tools.md +199 -40
  55. package/docs/frameworks/next/components/frame.md +137 -42
  56. package/docs/frameworks/next/concepts/consent-categories.md +24 -89
  57. package/docs/frameworks/next/concepts/policy-presets.md +142 -0
  58. package/docs/frameworks/next/content-security-policy.md +189 -0
  59. package/docs/frameworks/next/data-fetching.md +74 -0
  60. package/docs/frameworks/next/geography-headers.md +251 -0
  61. package/docs/frameworks/next/headless.md +95 -185
  62. package/docs/frameworks/next/hooks/use-consent-manager/overview.md +42 -163
  63. package/docs/frameworks/next/iab/overview.md +37 -107
  64. package/docs/frameworks/next/optimization.md +158 -194
  65. package/docs/frameworks/next/pages-router.md +296 -0
  66. package/docs/frameworks/next/quickstart.md +31 -132
  67. package/docs/frameworks/next/script-loader.md +140 -465
  68. package/docs/frameworks/next/server-side.md +97 -130
  69. package/docs/frameworks/next/static-export.md +164 -0
  70. package/docs/frameworks/next/styling/overview.md +174 -248
  71. package/docs/frameworks/next/troubleshooting.md +134 -144
  72. package/docs/guides/consent-state.md +60 -0
  73. package/docs/guides/data-fetching.md +163 -0
  74. package/docs/guides/deployment-modes.md +63 -0
  75. package/docs/guides/troubleshooting.md +68 -0
  76. package/docs/guides/verify-consent.md +62 -0
  77. package/docs/integrations/adobe-analytics.md +239 -105
  78. package/docs/integrations/ahrefs-analytics.md +238 -104
  79. package/docs/integrations/amplitude.md +219 -157
  80. package/docs/integrations/building-integrations.md +32 -224
  81. package/docs/integrations/clear-on-revocation.md +167 -0
  82. package/docs/integrations/clearbit.md +247 -86
  83. package/docs/integrations/cloudflare-web-analytics.md +250 -84
  84. package/docs/integrations/crisp.md +251 -97
  85. package/docs/integrations/databuddy.md +259 -153
  86. package/docs/integrations/fathom-analytics.md +239 -96
  87. package/docs/integrations/google-maps.md +328 -207
  88. package/docs/integrations/google-tag-manager.md +248 -96
  89. package/docs/integrations/google-tag.md +261 -90
  90. package/docs/integrations/heap.md +222 -149
  91. package/docs/integrations/hightouch.md +225 -131
  92. package/docs/integrations/hotjar.md +239 -90
  93. package/docs/integrations/intercom.md +239 -98
  94. package/docs/integrations/linkedin-insights.md +243 -113
  95. package/docs/integrations/logrocket.md +241 -123
  96. package/docs/integrations/matomo-analytics.md +256 -111
  97. package/docs/integrations/meta-pixel.md +197 -324
  98. package/docs/integrations/microsoft-clarity.md +233 -114
  99. package/docs/integrations/microsoft-uet.md +245 -110
  100. package/docs/integrations/mixpanel-analytics.md +252 -87
  101. package/docs/integrations/openai-pixel.md +441 -0
  102. package/docs/integrations/overview.md +95 -133
  103. package/docs/integrations/pirsch.md +249 -96
  104. package/docs/integrations/plausible-analytics.md +241 -100
  105. package/docs/integrations/posthog.md +353 -214
  106. package/docs/integrations/promptwatch.md +251 -81
  107. package/docs/integrations/reddit-pixel.md +226 -173
  108. package/docs/integrations/rudderstack.md +244 -187
  109. package/docs/integrations/rybbit-analytics.md +244 -91
  110. package/docs/integrations/segment.md +238 -92
  111. package/docs/integrations/snapchat-pixel.md +240 -110
  112. package/docs/integrations/tiktok-pixel.md +249 -81
  113. package/docs/integrations/umami-analytics.md +242 -95
  114. package/docs/integrations/vercel-analytics.md +242 -90
  115. package/docs/integrations/x-pixel.md +238 -104
  116. package/docs/integrations/youtube.md +354 -142
  117. package/docs/upgrade-v3.md +334 -0
  118. package/iab/styles.css +1 -1
  119. package/iab/styles.tw3.css +1 -1
  120. package/package.json +106 -65
  121. package/readme.json +3 -3
  122. package/src/iab/styles.css +1 -1
  123. package/src/iab/styles.tw3.css +1 -1
  124. package/src/styles.css +1 -1
  125. package/src/styles.tw3.css +1 -1
  126. package/styles.css +1 -1
  127. package/styles.tw3.css +1 -1
  128. package/client/components/consent-dialog-link.js +0 -3
  129. package/client/components/integrations.js +0 -3
  130. package/dist/components/integrations/index.cjs +0 -1
  131. package/dist/components/integrations/index.js +0 -1
  132. package/dist/headless.cjs +0 -1
  133. package/dist/index.cjs +0 -1
  134. package/dist/libs/browser-initial-data.cjs +0 -1
  135. package/dist/libs/browser-initial-data.js +0 -1
  136. package/dist/libs/initial-data.cjs +0 -1
  137. package/dist/libs/initial-data.js +0 -1
  138. package/dist/types.cjs +0 -1
  139. package/dist/version.cjs +0 -1
  140. package/dist-types/components/integrations/index.d.ts +0 -1
  141. package/dist-types/libs/browser-initial-data.d.ts +0 -9
  142. package/dist-types/libs/initial-data.d.ts +0 -33
  143. package/docs/frameworks/next/building-headless-components.md +0 -379
  144. package/docs/frameworks/next/callbacks.md +0 -186
  145. package/docs/frameworks/next/concepts/client-modes.md +0 -177
  146. package/docs/frameworks/next/concepts/consent-models.md +0 -117
  147. package/docs/frameworks/next/concepts/cookie-management.md +0 -122
  148. package/docs/frameworks/next/concepts/glossary.md +0 -24
  149. package/docs/frameworks/next/concepts/initialization-flow.md +0 -149
  150. package/docs/frameworks/next/concepts/policy-packs.md +0 -230
  151. package/docs/frameworks/next/hooks/use-color-scheme.md +0 -41
  152. package/docs/frameworks/next/hooks/use-consent-manager/checking-consent.md +0 -96
  153. package/docs/frameworks/next/hooks/use-consent-manager/location-info.md +0 -97
  154. package/docs/frameworks/next/hooks/use-consent-manager/setting-consent.md +0 -94
  155. package/docs/frameworks/next/hooks/use-draggable.md +0 -59
  156. package/docs/frameworks/next/hooks/use-focus-trap.md +0 -42
  157. package/docs/frameworks/next/hooks/use-reduced-motion.md +0 -37
  158. package/docs/frameworks/next/hooks/use-ssr-status.md +0 -32
  159. package/docs/frameworks/next/hooks/use-text-direction.md +0 -50
  160. package/docs/frameworks/next/hooks/use-translations.md +0 -55
  161. package/docs/frameworks/next/iab/consent-banner.md +0 -91
  162. package/docs/frameworks/next/iab/consent-dialog.md +0 -129
  163. package/docs/frameworks/next/iab/use-gvl-data.md +0 -21
  164. package/docs/frameworks/next/iframe-blocking.md +0 -106
  165. package/docs/frameworks/next/internationalization.md +0 -207
  166. package/docs/frameworks/next/network-blocker.md +0 -140
  167. package/docs/frameworks/next/policy-packs.md +0 -248
  168. package/docs/frameworks/next/styling/classnames.md +0 -94
  169. package/docs/frameworks/next/styling/color-scheme.md +0 -84
  170. package/docs/frameworks/next/styling/css-variables.md +0 -53
  171. package/docs/frameworks/next/styling/slots.md +0 -94
  172. package/docs/frameworks/next/styling/tailwind.md +0 -137
  173. package/docs/frameworks/next/styling/tokens.md +0 -156
  174. package/docs/shared/concepts/client-modes.md +0 -103
  175. package/docs/shared/concepts/consent-categories.md +0 -41
  176. package/docs/shared/concepts/consent-models.md +0 -72
  177. package/docs/shared/concepts/cookie-management.md +0 -88
  178. package/docs/shared/concepts/glossary.md +0 -24
  179. package/docs/shared/concepts/initialization-flow.md +0 -105
  180. package/docs/shared/concepts/policy-packs.md +0 -225
  181. package/docs/shared/react/components/consent-banner.md +0 -242
  182. package/docs/shared/react/components/consent-dialog-link.md +0 -45
  183. package/docs/shared/react/components/consent-dialog-trigger.md +0 -185
  184. package/docs/shared/react/components/consent-dialog.md +0 -119
  185. package/docs/shared/react/components/consent-manager-provider.md +0 -225
  186. package/docs/shared/react/components/consent-widget.md +0 -121
  187. package/docs/shared/react/components/dev-tools.md +0 -81
  188. package/docs/shared/react/components/frame.md +0 -52
  189. package/docs/shared/react/guides/building-headless-components.md +0 -110
  190. package/docs/shared/react/guides/callbacks.md +0 -89
  191. package/docs/shared/react/guides/headless.md +0 -31
  192. package/docs/shared/react/guides/iframe-blocking.md +0 -65
  193. package/docs/shared/react/guides/internationalization.md +0 -123
  194. package/docs/shared/react/guides/network-blocker.md +0 -72
  195. package/docs/shared/react/guides/optimization.md +0 -44
  196. package/docs/shared/react/guides/policy-packs.md +0 -173
  197. package/docs/shared/react/guides/script-loader.md +0 -311
  198. package/docs/shared/react/hooks/use-color-scheme.md +0 -31
  199. package/docs/shared/react/hooks/use-consent-manager/checking-consent.md +0 -95
  200. package/docs/shared/react/hooks/use-consent-manager/location-info.md +0 -96
  201. package/docs/shared/react/hooks/use-consent-manager/overview.md +0 -74
  202. package/docs/shared/react/hooks/use-consent-manager/setting-consent.md +0 -93
  203. package/docs/shared/react/hooks/use-draggable.md +0 -30
  204. package/docs/shared/react/hooks/use-focus-trap.md +0 -20
  205. package/docs/shared/react/hooks/use-reduced-motion.md +0 -33
  206. package/docs/shared/react/hooks/use-ssr-status.md +0 -16
  207. package/docs/shared/react/hooks/use-text-direction.md +0 -38
  208. package/docs/shared/react/hooks/use-translations.md +0 -15
  209. package/docs/shared/react/iab/consent-banner.md +0 -60
  210. package/docs/shared/react/iab/consent-dialog.md +0 -76
  211. package/docs/shared/react/iab/overview.md +0 -80
  212. package/docs/shared/react/iab/use-gvl-data.md +0 -21
  213. package/docs/shared/react/styling/classnames.md +0 -93
  214. package/docs/shared/react/styling/color-scheme.md +0 -35
  215. package/docs/shared/react/styling/css-variables.md +0 -53
  216. package/docs/shared/react/styling/overview.md +0 -261
  217. package/docs/shared/react/styling/slots.md +0 -93
  218. package/docs/shared/react/styling/stylesheet-entrypoint.md +0 -8
  219. package/docs/shared/react/styling/tailwind.md +0 -88
  220. package/docs/shared/react/styling/tokens.md +0 -155
  221. package/docs/shared/troubleshooting.md +0 -82
@@ -1,72 +1,167 @@
1
1
  ---
2
2
  title: Frame
3
- description: A consent-gated content wrapper - children only mount when the
4
- required consent category is granted.
3
+ description: Consent-gate an iframe in Next.js with Frame inside ConsentRoot;
4
+ with server prefetch the placeholder or embed is decided in the server HTML.
5
5
  group: frameworks
6
6
  ---
7
- `Frame` conditionally renders its children based on consent state. When consent for the specified category is not granted, a placeholder is shown instead. Children are not mounted at all until consent is given, preventing any network requests or script execution.
8
7
 
9
- ## Basic Usage
8
+ ## Gate an embed behind consent
10
9
 
11
- ```tsx
12
- import { Frame } from '@c15t/nextjs';
10
+ `Frame` mounts its children only while the effective permission for one
11
+ consent category is granted, and shows a placeholder otherwise. Wrap any
12
+ iframe or third-party widget that would set cookies or contact a vendor on
13
+ load. It is a Client Component; render it from any route under the layout
14
+ that mounts the `ConsentRoot` from your
15
+ [App Router](../app-router.md) or
16
+ [Pages Router](../pages-router.md) setup, which also mounts
17
+ the `ConsentDialog` the placeholder button opens.
18
+
19
+ ```tsx title="components/product-video.tsx"
20
+ 'use client';
13
21
 
14
- function YouTubeEmbed() {
22
+ import { Frame } from 'c15t/next';
23
+
24
+ export function ProductVideo() {
15
25
  return (
16
- <Frame category="marketing">
26
+ <Frame category="marketing" className="video-frame">
17
27
  <iframe
18
- src="https://www.youtube.com/embed/dQw4w9WgXcQ"
19
- width="560"
20
- height="315"
28
+ src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
29
+ title="Product tour"
30
+ loading="lazy"
21
31
  allowFullScreen
32
+ style={{ width: '100%', aspectRatio: '16 / 9', border: 0 }}
22
33
  />
23
34
  </Frame>
24
35
  );
25
36
  }
26
37
  ```
27
38
 
28
- ## Custom Placeholder
39
+ Import `ProductVideo` into any page or Server Component; only this file needs
40
+ `'use client'`. `marketing` must be in your policy scope. If you set a
41
+ non-empty `options.consentCategories` list on `ConsentRoot`, include
42
+ `marketing` there too so the preference center can display and save it.
43
+ `Frame` does not add categories to that list.
44
+
45
+ The placeholder names the category using its title from your translations. Set
46
+ `className` or `style` on `Frame` to reserve the embed's space, so the page
47
+ does not shift when the placeholder is replaced.
48
+
49
+ With the awaited `resolveConsent` from the App Router guide, the
50
+ server decides between placeholder and embed from the resolved rule and the
51
+ request cookie. Pages Router works the same way when `getServerSideProps`
52
+ awaits `resolveConsent` from `c15t/next/pages` and passes the result
53
+ through `state` to `ConsentRoot`, as in the
54
+ [Pages Router guide](../pages-router.md). Hydration keeps that choice as long as
55
+ the browser's privacy signals agree with the request: a `navigator.globalPrivacyControl`
56
+ that the request did not carry as `Sec-GPC` is re-detected on hydration and
57
+ can withdraw a category. Under an opt-in rule a new
58
+ visitor gets the placeholder and a returning visitor who allowed `marketing`
59
+ gets the iframe; under an opt-out rule a new visitor already has effective
60
+ permission, so the iframe renders until they opt out.
61
+
62
+ ## Props
63
+
64
+ | Prop | Type | Default | Description |
65
+ | ------------- | ----------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
+ | `category` | `AllConsentNames` | required | The consent category whose effective permission gates the children, for example `'marketing'` or `'functionality'`. |
67
+ | `children` | `ReactNode` | required | The embed. It is not mounted until the category is allowed, so it makes no requests before permission. |
68
+ | `placeholder` | `ReactNode` | built-in placeholder | Replaces the built-in title and button while the category is not allowed. Falsy values such as `null`, `false`, `0` and an empty string use the built-in placeholder. Pass an empty fragment, `<></>`, to render no placeholder content. |
69
+
70
+ Other `div` attributes such as `className`, `style` and `ref` apply to the
71
+ wrapper element that stays in the page in both states. `FrameProps` also
72
+ declares `noStyle` and `theme`, but the component does not apply them yet;
73
+ style the wrapper with `className` and theme the placeholder through the
74
+ provider `theme` option.
75
+
76
+ ## Behavior
77
+
78
+ `Frame` renders a `div` wrapper. Inside it, when the effective permission for
79
+ `category` is granted, it renders the children; otherwise it renders the
80
+ placeholder. Permission is the same value `useConsent(category)` returns,
81
+ so it can be granted under an opt-out rule before the visitor records a
82
+ choice, and a recorded grant can be overridden by a privacy signal.
83
+
84
+ While effective permission is denied, the children are absent from the DOM,
85
+ so an iframe or a third-party widget inside `Frame` sends no requests. When the visitor later revokes it, the
86
+ children unmount and the embed disappears. `Frame` does not depend on the
87
+ network blocker or the iframe blocker modules; use those for markup you
88
+ cannot wrap in a component.
89
+
90
+ The built-in placeholder shows the title `frame.title`, "Accept {category}
91
+ consent to view this content.", with `{category}` replaced by
92
+ `consentTypes.<category>.title`, and a button labelled `frame.actionButton`,
93
+ "Enable {category} consent". The button opens the preference center; it
94
+ does not grant the category by itself, because the visitor still has to
95
+ save. Mount a `ConsentDialog` in the same provider so the button has
96
+ something to open.
29
97
 
30
- Replace the default placeholder:
98
+ Under a policy with `scopeMode: 'strict'`, a category outside the rule's
99
+ scope cannot be granted. The built-in placeholder then shows `frame.policyBlocked`,
100
+ "This content is unavailable under your region's consent policy.", without
101
+ the button, and a stored grant for that category stays blocked.
102
+
103
+ When the provider starts from a snapshot prefetched on the server, the
104
+ server renders the children or placeholder according to effective permission.
105
+ A grant in the request cookie allows the embed only when policy and privacy
106
+ signals permit it. Browser privacy signals detected during hydration can
107
+ withdraw that permission. In a browser-only setup the
108
+ provider has no permission until it resolves policy on the client, so
109
+ `Frame` shows the placeholder first and swaps in the embed after resolution
110
+ when the category is already granted.
111
+
112
+ `Frame` is one component on this page; the per-vendor embed guides for
113
+ [YouTube](../../../integrations/youtube.md) and
114
+ [Google Maps](../../../integrations/google-maps.md) show a complete embed
115
+ configuration with sizing, titles and the same `Frame` usage across
116
+ frameworks.
117
+
118
+ To read the same permission in your own Client Components, use `useConsent`
119
+ from `c15t/next`; the `useIframeBlocker` and `useNetworkBlocker` module
120
+ hooks also import from there.
121
+
122
+ ## Composition
123
+
124
+ The placeholder parts are available as `Frame.Root`, `Frame.Title` and
125
+ `Frame.Button` for a custom placeholder that keeps the built-in copy and
126
+ behavior. `Frame.Title` and `Frame.Button` accept `category` and fill in the
127
+ translated text; pass children to either to replace it.
31
128
 
32
129
  ```tsx
33
130
  <Frame
34
- category="experience"
131
+ category="marketing"
35
132
  placeholder={
36
- <div className="rounded-lg border p-8 text-center">
37
- <p>This content requires experience cookies.</p>
38
- <p>Please enable them in your privacy settings.</p>
39
- </div>
133
+ <Frame.Root>
134
+ <Frame.Title category="marketing" />
135
+ <p>The video is also available on our channel.</p>
136
+ <Frame.Button category="marketing">Choose cookies</Frame.Button>
137
+ </Frame.Root>
40
138
  }
41
139
  >
42
- <InteractiveWidget />
140
+ <iframe src="https://www.youtube-nocookie.com/embed/..." title="Product tour" />
43
141
  </Frame>
44
142
  ```
45
143
 
46
- ## Compound Components
47
-
48
- Build fully custom placeholder layouts:
49
-
50
- ```tsx
51
- <Frame.Root category="marketing">
52
- <Frame.Title category="marketing" />
53
- <Frame.Button category="marketing" />
54
- </Frame.Root>
55
- ```
56
-
57
- * `Frame.Root` - Container with default placeholder styling
58
- * `Frame.Title` - Displays a consent-request message with the category name
59
- * `Frame.Button` - Button that opens the consent dialog for the specified category
60
-
61
- ## Automatic Category Registration
144
+ The built-in placeholder carries `data-testid="frame-placeholder"` and its
145
+ button `data-testid="frame-open-dialog"`.
62
146
 
63
- When `Frame` mounts, it automatically adds its `category` to the active `consentCategories` list. This means you don't need to explicitly list the category in your provider's `consentCategories` option - if a `Frame` component uses it, it will be registered.
147
+ ## Accessibility
64
148
 
65
- ## Props
149
+ The placeholder is plain text and a `button`, so it is readable and
150
+ operable without the embed. Give the iframe a descriptive `title`, and keep a
151
+ transcript, address or link outside the `Frame` for visitors who decline
152
+ the category. A denied category may be fixed by policy, so opening the
153
+ preference center does not guarantee that the visitor can grant it.
66
154
 
67
- |Property|Value|
68
- |:--|:--|
69
- |Type Name|\`FrameProps\`|
70
- |Source Path|\`./packages/react/src/components/frame/types.ts\`|
155
+ ## Verify
71
156
 
72
- \*ExtractedTypeTable: Could not extract "FrameProps" from "./packages/react/src/components/frame/types.ts" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
157
+ Use an optional in-scope category that is available in the preference
158
+ center and is not restricted by policy or privacy signals such as GPC.
159
+ Load the page with the category denied. The placeholder text names the
160
+ category and the network panel shows no request to the embed's host. With a
161
+ prefetched snapshot, the server HTML contains the placeholder for a denied
162
+ category or the embed for a granted category. Browser-only initialization
163
+ shows the placeholder before policy resolves, then replaces it with the
164
+ embed if the category is granted. After policy resolves, activate the
165
+ placeholder button: the preference center opens. Turn the category on and
166
+ Save: the placeholder is replaced by the embed and its requests start. Reject the
167
+ category again from the preference center: the embed disappears.
@@ -1,98 +1,33 @@
1
1
  ---
2
- title: Consent Categories
3
- description: How c15t organizes tracking technologies into five consent categories.
2
+ title: Consent categories
3
+ description: Assign optional features to categories and understand how policy
4
+ scope affects permission.
4
5
  group: frameworks
5
6
  ---
6
- c15t organizes tracking technologies into five consent categories that align with GDPR and ePrivacy Directive requirements. Rather than asking users to approve or deny individual cookies or scripts, each category groups related tracking purposes together so users can make meaningful, informed choices about how their data is used.
7
7
 
8
- > ℹ️ **Info:**
9
- > Why categories, not cookie lists? Many consent banners list individual cookie names like \_ga, \_gid, or \_fbp. This is counterproductive:Technical names are meaningless to users — nobody knows what \_gid does by reading its name.Information overload drives "accept all" — a wall of cookie names pushes users toward dismissing the banner as fast as possible, which is the opposite of informed consent.Purpose is what matters — privacy regulations (GDPR, ePrivacy) require clear information about the purposes of data processing, not a cookie-by-cookie inventory.Cookie lists go stale — third-party scripts change their cookie names across versions, creating a maintenance burden that provides no real transparency.c15t's category-based approach — "measurement", "marketing", "experience" — communicates purpose directly. Users understand why data is collected, not how it is stored.
8
+ ## Choose categories by purpose
10
9
 
11
- Configure which categories your app supports in the provider:
10
+ | Category | Purpose |
11
+ | --------------- | ---------------------------------------------- |
12
+ | `necessary` | Functionality required for the site to operate |
13
+ | `functionality` | Optional features such as support widgets |
14
+ | `measurement` | Analytics and usage measurement |
15
+ | `experience` | Optional personalization |
16
+ | `marketing` | Advertising and marketing |
12
17
 
13
- ```tsx
14
- import { type ReactNode } from 'react';
15
- import { ConsentManagerProvider } from '@c15t/nextjs';
18
+ `necessary` is always permitted. Assign categories based on what an integration
19
+ does in your application; renaming analytics to necessary does not change its
20
+ purpose. The provider's displayed categories and the resolved policy scope must
21
+ agree.
16
22
 
17
- export function ConsentManager({ children }: { children: ReactNode }) {
18
- return (
19
- <ConsentManagerProvider
20
- options={{
21
- mode: 'hosted',
22
- backendURL: '/api/c15t',
23
- consentCategories: ['necessary', 'measurement', 'marketing'],
24
- }}
25
- >
26
- {children}
27
- </ConsentManagerProvider>
28
- );
29
- }
30
- ```
23
+ ## Respect policy scope
31
24
 
32
- ## The Five Categories
25
+ A strict scope denies categories outside the rule. A permissive scope can allow
26
+ out-of-scope categories unless another restriction applies. When a rule selects
27
+ only some optional categories, set `scopeMode` explicitly. An omitted scope,
28
+ `['*']`, or a list containing only `necessary` expands to the default optional
29
+ categories; a necessary-only list is not a shortcut for disabling all tracking.
33
30
 
34
- |Category|Default|Toggleable|Description|
35
- |--|--|--|--|
36
- |`necessary`|`true`|No|Strictly necessary to operate or deliver the service|
37
- |`functionality`|`false`|Yes|Basic interactions and functionalities|
38
- |`experience`|`false`|Yes|Improve quality of user experience|
39
- |`measurement`|`false`|Yes|Measure traffic and analyze behavior|
40
- |`marketing`|`false`|Yes|Deliver personalized ads or marketing content|
41
-
42
- The `necessary` category has `disabled: true` set internally, which prevents the user from toggling it off in the consent UI. All other categories can be freely toggled by the user.
43
-
44
- Note that all categories except `necessary` have `display: false` by default. A category appears in the consent UI only if you include it in `consentCategories`. Categories not listed are hidden from the UI, but they still exist in consent state and may be affected by model-level behavior (for example, auto-grant in `opt-out` or `null` model flows).
45
-
46
- Check if a specific category has consent using the `has()` method:
47
-
48
- ```tsx
49
- import { useConsentManager } from '@c15t/nextjs';
50
-
51
- function AnalyticsLoader() {
52
- const { has } = useConsentManager();
53
-
54
- if (has('measurement')) {
55
- // Safe to load analytics scripts
56
- }
57
-
58
- return null;
59
- }
60
- ```
61
-
62
- ## Configuring Categories
63
-
64
- The `consentCategories` array controls which consent categories are presented to the user in the consent UI. Only categories you list in this array will appear as toggleable options in the consent banner or modal.
65
-
66
- The `necessary` category is always implicitly included even if you do not add it to the array. You never need to worry about accidentally omitting it -- c15t ensures it is always present and always enabled.
67
-
68
- For example, if you set:
69
-
70
- ```
71
- consentCategories: ['necessary', 'measurement', 'marketing']
72
- ```
73
-
74
- then only those three categories will show toggles in the consent UI. The `functionality` and `experience` categories will not appear as user-configurable toggles.
75
-
76
- In `opt-in`/`iab` flows, hidden categories usually remain `false` unless you explicitly set them. In `opt-out`/`null` flows, categories may be auto-granted even when hidden.
77
-
78
- This gives you precise control over which consent choices to present to your users. A simple blog that only runs an analytics script might only need `measurement`. A media site with ad integrations would include `marketing`. A SaaS application with personalization features might add `experience` and `functionality` as well. You choose what is relevant to your site and c15t handles the rest - storing consent state, exposing it through hooks, and ensuring the right categories are active based on the user's choices.
79
-
80
- You can dynamically update which categories are active:
81
-
82
- ```tsx
83
- import { useConsentManager } from '@c15t/nextjs';
84
-
85
- function CategoryManager() {
86
- const { consentCategories, setConsentCategories } = useConsentManager();
87
-
88
- const addExperienceCategory = () => {
89
- setConsentCategories([...consentCategories, 'experience']);
90
- };
91
-
92
- return (
93
- <button onClick={addExperienceCategory}>
94
- Enable experience features
95
- </button>
96
- );
97
- }
98
- ```
31
+ Use effective permissions to gate work and explicit choices to inspect what the
32
+ visitor confirmed. Read [consent state](../../../guides/consent-state.md) for that
33
+ distinction and [migration](../../../upgrade-v3.md) for v3 policy configuration.
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: Policy presets
3
+ description: Understand how policy rules affect Next.js prompts, permissions and
4
+ persistent privacy controls.
5
+ group: frameworks
6
+ ---
7
+
8
+ ## What the policy controls
9
+
10
+ A policy rule selects the permission model, categories, prompt and persistent
11
+ privacy controls for a visitor. Your app reads the resolved rule and effective
12
+ permissions. It should not choose a different rule just to hide a banner.
13
+
14
+ | Policy setting | What your app should expect |
15
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
16
+ | `model: 'opt-in'` | Optional categories in scope wait for consent. |
17
+ | `model: 'opt-out'` | Categories in scope can be allowed before a choice. Saved refusals and privacy signals can restrict them. |
18
+ | `model: 'none'` | Categories in scope are allowed without an initial prompt. Stock consent controls stay hidden unless the rule grants privacy rights. |
19
+ | `prompt: 'choice'` | The banner asks for a choice when the current state requires one. |
20
+ | `prompt: 'notice'` | Dismissing the notice records an acknowledgement, not a consent grant. |
21
+ | `prompt: 'none'` | No automatic prompt. The rule can still require a way to open preferences. |
22
+ | `scopeMode: 'strict'` | Optional categories outside the rule's scope stay denied. |
23
+
24
+ Keep a persistent preferences entry point wherever the policy provides that
25
+ right. A visitor who dismissed a notice or rejected tracking still needs a way
26
+ to revisit their choice. Stock components read the rule's rights; custom UI
27
+ must do the same.
28
+
29
+ Use `effectivePermissions` to gate a script or embed. A `true` value under an
30
+ opt-out rule does not mean the visitor clicked Accept. Read `explicitChoice`
31
+ when you need the visitor's recorded decision. See
32
+ [consent state](../../../guides/consent-state.md) for these distinctions.
33
+
34
+ ## Where to change the rules
35
+
36
+ For Inth, configure policy rules on your hosted project. The app receives those
37
+ rules through the configured [data-fetching path](../../../guides/data-fetching.md). Importing
38
+ `recommendedPolicyRules()` or changing browser styling does not override the
39
+ hosted policy.
40
+
41
+ For a backend you operate, use the
42
+ [policy packs guide](https://c15t.com/docs/self-host/guides/policy-packs). It covers
43
+ `manifest.policyRules`, preset selection and custom matchers. Browser-only
44
+ setups own their rules locally; see
45
+ [deployment modes](../../../guides/deployment-modes.md).
46
+
47
+ Presets are starting configurations. Select them for your actual processing,
48
+ and review their assumptions before allowing optional categories by default.
49
+ A country match does not establish a legal basis for a vendor's data use.
50
+
51
+ ## Why the banner may be absent
52
+
53
+ A missing banner can be an expected policy outcome or an initialization problem.
54
+ Check `resolution.status` before interpreting the active model.
55
+
56
+ | State | What to check |
57
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | A policy matched and `promptRequirement.kind` is `none` | The rule does not ask for an initial prompt, or the stored records already satisfy it. Check persistent preferences separately. |
59
+ | A policy matched but a category is denied | Inspect `effectivePermissions`, `explicitChoice`, `privacySignals` and `restrictions`. A saved refusal or GPC can keep it denied. |
60
+ | No policy has matched | Check backend connectivity, location inputs and policy configuration. Optional categories stay denied while resolution is unresolved. |
61
+
62
+ Do not treat `policyRule` alone as proof that a policy resolved. The snapshot
63
+ also carries a safe rule for evaluation while resolution is pending or failed.
64
+ Use `resolution.policy` only after checking for `status: 'matched'`.
65
+
66
+ The local `recommendedPolicyRules()` pack uses opt-in rules for Europe and
67
+ Québec, opt-out for its supported US privacy states, and a no-prompt `none`
68
+ default for other known locations. An unknown country uses its strict opt-in
69
+ fallback. A US country with a missing state gets the US opt-out fallback.
70
+ These are the local pack's defaults, not a promise about your Inth project.
71
+ Browser-only mode does not discover a visitor's country from their IP address.
72
+
73
+ ## Keep Next.js server and browser state aligned
74
+
75
+ Keep the prepared configuration from your
76
+ [App Router](../app-router.md) or
77
+ [Pages Router](../pages-router.md) setup intact when passing it
78
+ to the consent boundary. That configuration includes the resolved policy,
79
+ stored records and evaluation time. Replacing it with a separately selected
80
+ browser preset can change the first render's permissions or prompt.
81
+
82
+ Use the request's location and privacy signals when resolving policy. Do not
83
+ cache a visitor's resolved configuration as shared policy data. A public
84
+ manifest can be shared; the decision made from it belongs to the request.
85
+ Use [data fetching](../data-fetching.md) to choose between a
86
+ shared manifest, request-time `/init` and local policy resolution.
87
+ For a deployment without a Next.js server, follow
88
+ [static export](../static-export.md).
89
+
90
+ ## Inspect the active policy
91
+
92
+ Place this diagnostic component inside your existing consent
93
+ boundary. It reads state without recording a choice.
94
+
95
+ ```tsx title="app/consent-policy-debug.tsx"
96
+ 'use client';
97
+
98
+ import { useSnapshot } from 'c15t/next';
99
+
100
+ export function ConsentPolicyDebug() {
101
+ const snapshot = useSnapshot();
102
+ const resolution = snapshot.resolution;
103
+
104
+ return (
105
+ <pre>
106
+ {JSON.stringify(
107
+ {
108
+ status: resolution.status,
109
+ policy: resolution.status === 'matched' ? resolution.policy.id : null,
110
+ prompt: snapshot.promptRequirement,
111
+ permissions: snapshot.effectivePermissions,
112
+ location: snapshot.location,
113
+ privacySignals: snapshot.privacySignals,
114
+ },
115
+ null,
116
+ 2,
117
+ )}
118
+ </pre>
119
+ );
120
+ }
121
+ ```
122
+
123
+ The diagnostic subscribes to consent changes. Remove it after verification.
124
+ For a production feature gate, use `useConsent('marketing')` or the category
125
+ your feature needs instead of subscribing to the whole snapshot.
126
+
127
+ ## Change appearance without changing policy
128
+
129
+ Keep one banner and one dialog in your existing
130
+ boundary. Use
131
+ [styling](../styling/overview.md) to change their appearance,
132
+ or [headless UI](../headless.md) for custom markup.
133
+ Presentation settings change layout and emphasis; policy rules decide which
134
+ actions and privacy rights the app must expose.
135
+
136
+ ## Verify policy behavior
137
+
138
+ Use a fresh browser profile to check the intended location, then repeat with a
139
+ saved rejection and GPC enabled. Confirm that the expected prompt and
140
+ preferences controls appear, and that denied scripts and embeds make no
141
+ requests. Test missing location inputs too. Follow
142
+ [verify consent](../../../guides/verify-consent.md) for the full flow.
@@ -0,0 +1,189 @@
1
+ ---
2
+ title: Content Security Policy
3
+ description: Pass a per-request CSP nonce to ConsentRoot in Next.js and allow
4
+ the consent backend in connect-src.
5
+ group: frameworks
6
+ ---
7
+
8
+ ## Pass the request nonce to ConsentRoot
9
+
10
+ Read the nonce from the `x-nonce` request header in the root layout and pass it
11
+ as `options={{ nonce }}` on `ConsentRoot`. This is the standard Next.js
12
+ nonce pattern: your `proxy.ts` (Next.js 16) or `middleware.ts` (Next.js 15)
13
+ generates a nonce per request, sets the `Content-Security-Policy` response
14
+ header with `'nonce-<value>'` in `script-src` and `style-src`, and forwards the
15
+ same value on the request as `x-nonce`. See the
16
+ [Next.js CSP guide](https://nextjs.org/docs/app/guides/content-security-policy)
17
+ for the header-generating proxy.
18
+
19
+ If you also run [`c15tProxy`](./geography-headers.md), set
20
+ both `x-nonce` and the generated `Content-Security-Policy` header on
21
+ `request.headers` before calling it, and set the policy on the returned
22
+ response as well. Next.js reads the nonce for its own bootstrap and page
23
+ scripts from the request-side policy header, so forwarding only `x-nonce`
24
+ leaves the framework scripts without a nonce and the browser blocks hydration.
25
+ `c15tProxy` copies the incoming request headers into the forwarded request, so
26
+ both headers travel with the geography headers.
27
+
28
+ Add a `nonce` prop to the `Consent` wrapper from the
29
+ [App Router guide](./app-router.md):
30
+
31
+ ```tsx title="components/consent.tsx (partial)"
32
+ 'use client';
33
+
34
+ import type { ReactNode } from 'react';
35
+ import { ConsentRoot } from 'c15t/next';
36
+ import type { ConsentRootProps } from 'c15t/next';
37
+ import { consentConfig } from '../c15t.config';
38
+ import { scripts } from '../lib/scripts';
39
+
40
+ export function Consent({
41
+ children,
42
+ state,
43
+ nonce,
44
+ }: {
45
+ children: ReactNode;
46
+ state: ConsentRootProps['state'];
47
+ nonce?: string;
48
+ }) {
49
+ return (
50
+ <ConsentRoot
51
+ state={state}
52
+ config={consentConfig}
53
+ options={{ nonce }}
54
+ scripts={scripts}
55
+ >
56
+ {children}
57
+ {/* banner, dialog and footer as in the App Router guide */}
58
+ </ConsentRoot>
59
+ );
60
+ }
61
+ ```
62
+
63
+ Then read the header in the async consent component from the App Router guide
64
+ and pass it through:
65
+
66
+ ```tsx title="app/layout.tsx"
67
+ import { Suspense } from 'react';
68
+ import type { ReactNode } from 'react';
69
+ import { headers } from 'next/headers';
70
+ import { resolveConsent } from 'c15t/next/server';
71
+ import { consentConfig } from '../c15t.config';
72
+ import { Consent } from '../components/consent';
73
+ import './globals.css';
74
+
75
+ async function ResolvedConsent({ children }: { children: ReactNode }) {
76
+ const nonce = (await headers()).get('x-nonce') ?? undefined;
77
+ const state = await resolveConsent({ config: consentConfig });
78
+
79
+ return (
80
+ <Consent state={state} nonce={nonce}>
81
+ {children}
82
+ </Consent>
83
+ );
84
+ }
85
+
86
+ export default function RootLayout({ children }: { children: ReactNode }) {
87
+ return (
88
+ <html lang="en">
89
+ <body>
90
+ <Suspense fallback={null}>
91
+ <ResolvedConsent>{children}</ResolvedConsent>
92
+ </Suspense>
93
+ </body>
94
+ </html>
95
+ );
96
+ }
97
+ ```
98
+
99
+ Reading `headers()` keeps the route dynamic, which a per-request nonce requires
100
+ anyway. Do not combine a nonce-based policy with `cacheComponents: true`
101
+ (Partial Prerendering): the static shell is rendered once at build time, so its
102
+ scripts cannot carry a per-request nonce and the browser blocks them. Next.js
103
+ documents this limitation in its CSP guide. For a prerendered shell use the
104
+ hash-based policy that guide describes; a host-only `script-src` such as
105
+ `'self'` does not authorize Next.js's inline bootstrap scripts and would need
106
+ `'unsafe-inline'`, which defeats the policy. Otherwise keep the route fully
107
+ dynamic and use the nonce.
108
+
109
+ `options.nonce` is read when `ConsentRoot` mounts. The provider keeps the latest
110
+ value in a ref, but the script loader is created once per runtime, so a nonce
111
+ that changes during client-side navigation is not applied to scripts already
112
+ created. Each full page load gets the fresh nonce from its own request.
113
+
114
+ ## What the nonce applies to
115
+
116
+ c15t stamps `options.nonce` on two kinds of DOM nodes:
117
+
118
+ * Every `<script>` element the script loader creates for a `scripts` entry,
119
+ both `src` and inline `textContent` scripts. A `nonce` set on an individual
120
+ `scripts` entry takes precedence over `options.nonce` for that element.
121
+ * The `<style id="c15t-theme">` element the provider injects with the theme's
122
+ `--c15t-*` custom properties.
123
+
124
+ It does not apply to anything else:
125
+
126
+ * Scripts that a vendor script loads itself, such as a tag manager injecting
127
+ its tags, do not receive the nonce. Add `'strict-dynamic'` to `script-src` so
128
+ scripts loaded by a nonced script are allowed, or list those vendor hosts
129
+ explicitly.
130
+ * The prebuilt stylesheet `c15t/next/styles.css` is a regular stylesheet from
131
+ your own origin. It is covered by `style-src 'self'`, not by the nonce.
132
+ * Inline `style` attributes on rendered components. A nonce cannot authorize
133
+ style attributes, and browsers ignore `'unsafe-inline'` in a `style-src` list
134
+ that also contains a nonce or hash. Allow them with a separate
135
+ `style-src-attr 'unsafe-inline'` directive, or with `'unsafe-hashes'` plus
136
+ the matching `'sha256-...'` values; hashes only work for static values, not
137
+ for the animated collapse heights. The IAB TCF dialog, the consent dialog
138
+ trigger toolbar and the animated collapse used inside the dialogs render
139
+ inline `style` attributes. Check the
140
+ browser console with your policy enforced to see whether your setup triggers
141
+ a `style-src` violation.
142
+ * Iframes, images or requests made by vendor scripts. Allow those hosts in
143
+ `frame-src`, `img-src` and `connect-src` according to each vendor.
144
+
145
+ ## Allow the consent backend in connect-src
146
+
147
+ The browser sends consent submissions to `${backendURL}/subjects`, and, when
148
+ the browser initializes or retries after a failed server prefetch, requests to
149
+ `${backendURL}/init` or the manifest URL. Add the backend origin to
150
+ `connect-src`, and the manifest origin too when `manifestURL` is an absolute
151
+ URL on a different host such as a CDN:
152
+
153
+ ```txt title="Content-Security-Policy (partial)"
154
+ connect-src 'self' https://<your-backend-host> https://<your-manifest-host>;
155
+ ```
156
+
157
+ Use the endpoint host supplied by your Inth project or your self-hosted backend.
158
+ When you use the same-origin rewrite from
159
+ [Optimization](./optimization.md), `backendURL` is `/api/c15t`
160
+ and `'self'` already covers the browser requests; the Next.js server connects to
161
+ the backend outside the browser's CSP. A local manifest route at
162
+ `/api/c15t/manifest` is also same-origin.
163
+
164
+ IAB TCF policies can fetch the Global Vendor List from the URL the policy
165
+ provides; allow that host in `connect-src` if you use the
166
+ [IAB TCF add-on](./iab/overview.md). Server-side prefetch runs
167
+ in Next.js and is not subject to the browser's CSP.
168
+
169
+ ## Verify
170
+
171
+ Load a page with the policy enforced, not in report-only mode, and open the
172
+ browser console. The banner should render with its theme colors and no CSP
173
+ violation should mention `c15t-theme`. Grant a category that gates one of your
174
+ `scripts` entries, then check:
175
+
176
+ * The injected `<script>` element carries the request's nonce. Browsers hide
177
+ the value from `getAttribute('nonce')`; read the element's `nonce` property
178
+ in the console or inspect it in the Elements panel.
179
+ * The `<style id="c15t-theme">` element carries the same nonce and the
180
+ `--c15t-*` variables are applied to the banner.
181
+ * No `Refused to load the script` or `Refused to apply inline style` errors
182
+ mention a c15t element. A refused vendor dependency points to a missing
183
+ `'strict-dynamic'` or vendor host.
184
+ * The consent submission to `/subjects` succeeds. A `connect-src` violation
185
+ here means the backend origin is missing.
186
+
187
+ Reload with a different nonce and confirm the elements update. A stale nonce
188
+ after a full page load usually means a cached HTML response; nonce-based pages
189
+ must not be served from a shared cache.