@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
@@ -0,0 +1,74 @@
1
+ ---
2
+ title: Data fetching
3
+ description: Choose cached manifests for a Next.js server, backend
4
+ initialization for a simpler setup, or offline mode for local development.
5
+ group: frameworks
6
+ ---
7
+
8
+ ## Cached manifests
9
+
10
+ Recommended for Next.js deployments with a server.
11
+
12
+ A manifest contains public policy configuration, such as the rules used to
13
+ decide which consent permissions apply. Your Next.js server caches this data
14
+ and resolves consent separately for each visitor. Reusing the manifest avoids
15
+ asking the backend to resolve policy on every request.
16
+
17
+ ```text
18
+ Inth policy
19
+ ↓
20
+ Next.js manifest cache
21
+ ↓
22
+ Request → Consent decision
23
+
24
+ Choices → Inth records
25
+ ```
26
+
27
+ Policy data is shared through the cache. Each visitor's decision uses their
28
+ location, privacy signals and saved choices. Consent submissions still go to
29
+ Inth.
30
+
31
+ Follow the [App Router](./app-router.md) or
32
+ [Pages Router](./pages-router.md) setup. Both configure the
33
+ manifest route explicitly. A `backendURL` alone does
34
+ not enable manifests.
35
+
36
+ You can prepare consent during server rendering or initialize it in the browser.
37
+ See [rendering choices](./server-side.md).
38
+
39
+ ## Backend initialization
40
+
41
+ Use regular backend `/init` when you want fewer configuration steps, or when
42
+ [exporting a static site](./static-export.md) that cannot run
43
+ Next.js API routes.
44
+
45
+ c15t asks the backend to resolve consent when initialization is needed. This
46
+ requires only `backendURL` in your consent configuration, with no local manifest
47
+ or init handlers. Inth still manages policy and stores consent records.
48
+
49
+ Start with [client-side initialization](./client-side.md) for
50
+ a complete setup. You can also use backend initialization with server prefetch,
51
+ as shown in the router guides.
52
+
53
+ ## Offline mode
54
+
55
+ Not recommended for production environments.
56
+
57
+ Use offline mode for local development, tests or demos that do not need backend
58
+ records. It resolves bundled policy rules and keeps choices in browser storage.
59
+ It makes no consent backend requests and does not look up the visitor's location.
60
+
61
+ See the [offline configuration example](./api-reference/data-fetching.md#offline-configuration).
62
+
63
+ ## Hosting and further configuration
64
+
65
+ We recommend Inth to manage your backend. A
66
+ [self-hosted backend](https://c15t.com/docs/self-host/quickstart) supports both manifests and
67
+ regular initialization, but you operate its database, policies and availability.
68
+ Hosting is a separate choice from how your application fetches consent data.
69
+
70
+ For optional same-origin rewrites and cache settings, see
71
+ [Optimization](./optimization.md). Use the
72
+ [fetching reference](./api-reference/data-fetching.md) for exact
73
+ URL behavior and location headers, or
74
+ [Troubleshooting](./troubleshooting.md) if consent fails to initialize.
@@ -0,0 +1,251 @@
1
+ ---
2
+ title: Forward geography headers
3
+ description: Use c15tProxy in Next.js proxy.ts or middleware.ts so Server
4
+ Components and Route Handlers receive the visitor's country and region.
5
+ group: frameworks
6
+ ---
7
+
8
+ ## When do I need the proxy?
9
+
10
+ Add `c15tProxy` when your hosting platform exposes the visitor's location to
11
+ Next.js middleware or proxy but strips those headers before Server Components
12
+ and Route Handlers run. Without them, `resolveConsent`, the awaited
13
+ server helpers and the optional local `/api/c15t/init` handler see an unknown
14
+ location and apply your unknown-location policy rule for every visitor.
15
+
16
+ c15t reads location from the platform headers listed in
17
+ [Which headers are read?](#which-headers-are-read): Cloudflare (`cf-ipcountry`,
18
+ `cf-region-code`), Vercel (`x-vercel-ip-country`,
19
+ `x-vercel-ip-country-region`), the `x-amz-cf-ipcountry` header some
20
+ CloudFront setups add, and generic
21
+ proxy headers (`x-country-code`, `x-country`, `x-region-code`). c15t does not
22
+ keep a list of which hosts strip them. Check your deployment: log
23
+ `(await headers()).get('x-vercel-ip-country')` or the equivalent for your host
24
+ inside a Server Component. If the value is present, you do not need the proxy.
25
+ If it is missing while the same header is present in `proxy.ts`, add the proxy.
26
+
27
+ The proxy copies the incoming request headers, resolves country, region and
28
+ Global Privacy Control from them, and forwards the result on the request as
29
+ `x-c15t-country`, `x-c15t-region` and `sec-gpc`. Those application override
30
+ headers have the highest precedence, so Server Components and Route Handlers
31
+ read the same values the proxy saw. When no location header is present, the
32
+ proxy sets nothing and the location stays unknown.
33
+
34
+ The proxy runs on Next.js `^15.0.0 || ^16.0.0`. It does not apply to a
35
+ [static export](./static-export.md), which has no server.
36
+
37
+ ## Add the proxy (Next.js 16)
38
+
39
+ Create `proxy.ts` at the project root, or in `src/` if your app lives there:
40
+
41
+ ```ts title="proxy.ts"
42
+ import { c15tProxy } from 'c15t/next/proxy';
43
+ import type { NextRequest } from 'next/server';
44
+
45
+ export function proxy(request: NextRequest) {
46
+ return c15tProxy(request);
47
+ }
48
+
49
+ export const config = {
50
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
51
+ };
52
+ ```
53
+
54
+ `c15tProxy` returns `NextResponse.next()` with the forwarded request headers.
55
+ If you already have a proxy, call `c15tProxy(request)` where you would
56
+ otherwise return `NextResponse.next()`, and set any response headers or
57
+ cookies on the returned response.
58
+
59
+ `config.matcher` must cover every page that renders `ConsentRoot`,
60
+ because `resolveConsent` and the awaited helpers run during those page
61
+ requests. It must also cover `/api/c15t/:path*` if you serve the optional local
62
+ init route and want it to resolve policy with the visitor's location. The
63
+ manifest route serves public policy data and does not need location.
64
+
65
+ ## Add the middleware (Next.js 15)
66
+
67
+ On Next.js 15 the file is `middleware.ts` and the export is `middleware`:
68
+
69
+ ```ts title="middleware.ts"
70
+ import { c15tMiddleware } from 'c15t/next/middleware';
71
+ import type { NextRequest } from 'next/server';
72
+
73
+ export function middleware(request: NextRequest) {
74
+ return c15tMiddleware(request);
75
+ }
76
+
77
+ export const config = {
78
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
79
+ };
80
+ ```
81
+
82
+ `c15tMiddleware` is the same function as `c15tProxy` under the Next.js 15
83
+ name. Both imports stay supported on both Next.js versions. When you upgrade to
84
+ Next.js 16 and rename `middleware.ts` to `proxy.ts`, switch the import to
85
+ `c15tProxy` from `c15t/next/proxy` at the same time. The options type is
86
+ exported as `C15tMiddlewareOptions` and `C15tProxyOptions` respectively.
87
+
88
+ ## Persist geography in cookies
89
+
90
+ Pass `cookie: true` to also write the resolved location into cookies. Use this
91
+ on runtimes where the forwarded request headers do not reach React Server
92
+ Components, so that your own server code can read the location on later
93
+ requests. The proxy writes the cookie on the response, and `cookies()` reads
94
+ the incoming request, so the first visit still resolves as unknown and the
95
+ first request after a location change still sees the previous value:
96
+
97
+ ```ts title="proxy.ts"
98
+ import { c15tProxy } from 'c15t/next/proxy';
99
+ import type { NextRequest } from 'next/server';
100
+
101
+ export function proxy(request: NextRequest) {
102
+ return c15tProxy(request, { cookie: true });
103
+ }
104
+ ```
105
+
106
+ The default cookie names are `c15t-country` and `c15t-region`. Both are set
107
+ with `httpOnly: true`, `sameSite: 'lax'` and `path: '/'`, so browser scripts
108
+ cannot read them. Pass an object to rename them:
109
+
110
+ ```ts title="proxy.ts (partial)"
111
+ c15tProxy(request, {
112
+ cookie: { countryName: 'geo-country', regionName: 'geo-region' },
113
+ });
114
+ ```
115
+
116
+ A cookie is written only when the matching header resolved a value. The
117
+ proxy never clears a stale cookie, so a visitor whose location header
118
+ disappears keeps the previous cookie until it is cleared or the session cookie
119
+ expires.
120
+
121
+ The c15t server helpers read request headers, not these cookies. To use the
122
+ cookie, read it where you call `resolveConsent` and pass it as the
123
+ `country` override:
124
+
125
+ ```tsx title="app/layout.tsx (partial)"
126
+ import type { ReactNode } from 'react';
127
+ import { cookies } from 'next/headers';
128
+ import { resolveConsent } from 'c15t/next/server';
129
+ import { consentConfig } from '../c15t.config';
130
+ import { Consent } from '../components/consent';
131
+
132
+ async function ResolvedConsent({ children }: { children: ReactNode }) {
133
+ const country = (await cookies()).get('c15t-country')?.value;
134
+ const state = await resolveConsent({
135
+ config: consentConfig,
136
+ country,
137
+ });
138
+ return <Consent state={state}>{children}</Consent>;
139
+ }
140
+ ```
141
+
142
+ This is the `ResolvedConsent` component from the
143
+ [App Router guide](./app-router.md); keep it inside the
144
+ `Suspense` boundary shown there.
145
+
146
+ `resolveConsent` accepts `country` and `language` overrides. It has no
147
+ `region` override in the current API, so the region cookie is available only to
148
+ your own code.
149
+
150
+ The cookie comes back in the client-controlled `Cookie` header. `HttpOnly`
151
+ stops page scripts from reading it; it does not stop a visitor from sending
152
+ `c15t-country=<value>` and, through the `country` override, choosing a less
153
+ restrictive rule for themselves. Deleting the incoming cookie is not an
154
+ option here, because `cookies()` reads that same request and `c15tProxy` only
155
+ sets the cookie on the response. Use this fallback only when the edge that
156
+ terminates all traffic overwrites the incoming `c15t-country` and
157
+ `c15t-region` cookies with its own trusted geography on every request, or
158
+ when your code signs the value and verifies the signature before passing it
159
+ as `country`. Where you can do neither, keep the cookie out of policy
160
+ resolution and use it only for non-policy code such as display defaults.
161
+
162
+ ## Which headers are read?
163
+
164
+ `c15tProxy` and the server helpers use the same extraction from
165
+ `@c15t/schema`. Within each group, the first header with a value wins:
166
+
167
+ | Input | Headers, highest precedence first | Source |
168
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
169
+ | Country | `x-c15t-country`, `cf-ipcountry`, `x-vercel-ip-country`, `x-amz-cf-ipcountry`, `x-country-code`, `x-country` | c15t override, Cloudflare, Vercel, CloudFront, generic |
170
+ | Region | `x-c15t-region`, `cf-region-code`, `x-vercel-ip-country-region`, `x-region-code` | c15t override, Cloudflare, Vercel, generic |
171
+ | Global Privacy Control | `x-c15t-gpc`, `sec-gpc` | c15t override, browser signal |
172
+ | Language | `accept-language` | browser |
173
+
174
+ CloudFront's own geolocation headers, `CloudFront-Viewer-Country` and
175
+ `CloudFront-Viewer-Country-Region`, are not in this list. Forward them to the
176
+ origin with an origin request policy, then map them in `proxy.ts` before
177
+ calling `c15tProxy`. An origin request policy forwards headers but cannot
178
+ rename them. A CloudFront Function can also read geography headers when a
179
+ cache policy or origin request policy exposes them to the function, as shown
180
+ in [AWS's viewer-request example](https://github.com/aws-samples/amazon-cloudfront-functions/tree/main/redirect-based-on-country).
181
+
182
+ Clear every country and region input c15t recognizes before mapping the
183
+ trusted CloudFront values:
184
+
185
+ ```ts title="proxy.ts"
186
+ import { c15tProxy } from 'c15t/next/proxy';
187
+ import { NextRequest } from 'next/server';
188
+
189
+ export function proxy(request: NextRequest) {
190
+ const headers = new Headers(request.headers);
191
+ for (const name of [
192
+ 'x-c15t-country',
193
+ 'cf-ipcountry',
194
+ 'x-vercel-ip-country',
195
+ 'x-amz-cf-ipcountry',
196
+ 'x-country-code',
197
+ 'x-country',
198
+ 'x-c15t-region',
199
+ 'cf-region-code',
200
+ 'x-vercel-ip-country-region',
201
+ 'x-region-code',
202
+ ]) {
203
+ headers.delete(name);
204
+ }
205
+ const country = headers.get('cloudfront-viewer-country');
206
+ const region = headers.get('cloudfront-viewer-country-region');
207
+ if (country) headers.set('x-c15t-country', country);
208
+ if (region) headers.set('x-c15t-region', region);
209
+ return c15tProxy(new NextRequest(request, { headers }));
210
+ }
211
+ ```
212
+
213
+ The `delete` calls drop client-supplied overrides and fallback geography
214
+ headers. When CloudFront omits a country or region, that value stays unknown
215
+ instead of falling through to a header supplied by the visitor.
216
+
217
+ GPC values are meaningful only as `1` or `0`; any other value is treated as
218
+ absent. The proxy writes the normalized result to `sec-gpc`, so an incoming
219
+ `x-c15t-gpc: 1` reaches Server Components as `sec-gpc: 1`. Browsers refuse to
220
+ let scripts set `Sec-*` request headers, which is why the `x-c15t-gpc` override
221
+ exists for the browser's own init request.
222
+
223
+ Only trusted infrastructure may set `x-c15t-country`, `x-c15t-region` or
224
+ `x-c15t-gpc` in production, because they always win. A client that sends them
225
+ chooses its own policy rule, and `c15tProxy` forwards a client-supplied value
226
+ over the platform header rather than stripping it. The edge that terminates all
227
+ traffic must therefore delete incoming `x-c15t-*` headers before anything sets
228
+ them, in every deployment. Blocking direct origin access with a firewall or
229
+ platform origin protection is an additional control that keeps requests on that
230
+ edge; it does not replace the stripping, because a forwarded client header
231
+ still passes through the protected path.
232
+
233
+ ## Verify
234
+
235
+ Deploy with the proxy and load the site from two locations with different
236
+ configured policy rules, for example through a VPN or your host's geo testing
237
+ tools. After server prefetch resolves, the rendered consent policy must match
238
+ each location: an opt-in region shows the banner with optional categories denied, while a region
239
+ configured for notice-only or no notice renders accordingly. Confirm the values
240
+ by logging `(await headers()).get('x-c15t-country')` in a Server Component;
241
+ it should equal the platform header seen in the proxy.
242
+
243
+ Remove the proxy temporarily and reload. If both locations now resolve the
244
+ unknown-location rule, your platform strips location headers before Server
245
+ Components and the proxy is required. If they still resolve correctly, your
246
+ platform already passes the headers through and the proxy is optional.
247
+
248
+ With the local init route in the matcher, request `/api/c15t/init` from each
249
+ location and confirm its `policyResolution` reflects the location. If you use
250
+ `cookie: true`, check the response for `Set-Cookie: c15t-country=...` with
251
+ `HttpOnly` and `SameSite=Lax`.
@@ -1,192 +1,102 @@
1
1
  ---
2
- title: Headless Mode
3
- description: Build fully custom consent UI using only hooks - no pre-built
4
- components required.
2
+ title: Headless
3
+ description: Build a custom consent banner in Next.js with the
4
+ c15t/next/headless hooks inside your existing ConsentRoot.
5
5
  group: frameworks
6
6
  ---
7
- c15t's headless mode means using the hooks (`useConsentManager`, `useTranslations`, etc.) without any pre-built UI components. This gives you complete control over the consent experience.
8
-
9
- Before you go headless, walk the customization ladder in order:
10
-
11
- 1. **Pre-built components** - Use provider options, component props, tokens, slots, and `theme.consentActions`
12
- 2. **Compound components** - Rearrange c15t primitives when the markup order must change
13
- 3. **`noStyle`** - Keep c15t structure but replace its styling
14
- 4. **Headless** - Use only hooks and build the entire UI yourself
15
-
16
- ## When to Go Headless
17
-
18
- Go headless when:
19
-
20
- * Your design system requires complete control over markup
21
- * You need a consent flow that doesn't fit the banner/dialog pattern
22
- * You want to embed consent choices inline rather than as overlays
23
-
24
- Use a lower-power tool instead when:
25
-
26
- * The component structure works but the styling doesn't -> use tokens, slots, or `noStyle`
27
- * You only need to rearrange existing c15t parts -> use compound components
28
- * You want to change copy -> use `ConsentManagerProvider.options.i18n`
29
- * You only need to restyle stock actions -> use `theme.consentActions`
30
-
31
- > ⚠️ **Warning:**
32
- > Headless mode is not the first answer for pure theming. If you are still trying to debug why a banner footer color did not change, stay in the styling system and verify the token-to-component mapping before you rebuild the UI.
33
-
34
- > ℹ️ **Info:**
35
- > Need a policy-aware implementation guide? See Building Headless Components.
36
-
37
- ## Full Example: Custom Consent Banner
38
-
39
- ```tsx
40
- import { useConsentManager, useTranslations } from '@c15t/nextjs';
41
-
42
- function CustomConsentBanner() {
43
- const {
44
- activeUI,
45
- consents,
46
- consentCategories,
47
- consentTypes,
48
- saveConsents,
49
- setSelectedConsent,
50
- selectedConsents,
51
- } = useConsentManager();
52
- const translations = useTranslations();
53
-
54
- if (activeUI !== 'banner') return null;
55
-
56
- const displayedTypes = consentTypes.filter(
57
- (t) => consentCategories.includes(t.name) && t.display
58
- );
59
-
60
- return (
61
- <div className="fixed bottom-0 inset-x-0 bg-white border-t p-6 shadow-lg z-50">
62
- <h2 className="text-lg font-semibold">
63
- {translations.cookieBanner.title}
64
- </h2>
65
- <p className="text-sm text-gray-600 mt-1">
66
- {translations.cookieBanner.description}
67
- </p>
68
-
69
- <div className="mt-4 space-y-3">
70
- {displayedTypes.map((type) => (
71
- <label key={type.name} className="flex items-center gap-3">
72
- <input
73
- type="checkbox"
74
- checked={selectedConsents[type.name] ?? consents[type.name] ?? false}
75
- disabled={type.disabled}
76
- onChange={(e) => setSelectedConsent(type.name, e.target.checked)}
77
- />
78
- <div>
79
- <span className="font-medium">
80
- {translations.consentTypes[type.name]?.title ?? type.name}
81
- </span>
82
- <p className="text-xs text-gray-500">{type.description}</p>
83
- </div>
84
- </label>
85
- ))}
86
- </div>
87
-
88
- <div className="mt-4 flex gap-3">
89
- <button
90
- onClick={() => saveConsents('necessary')}
91
- className="px-4 py-2 border rounded"
92
- >
93
- {translations.common.rejectAll}
94
- </button>
95
- <button
96
- onClick={() => saveConsents('custom')}
97
- className="px-4 py-2 border rounded"
98
- >
99
- {translations.common.save}
100
- </button>
101
- <button
102
- onClick={() => saveConsents('all')}
103
- className="px-4 py-2 bg-blue-600 text-white rounded"
104
- >
105
- {translations.common.acceptAll}
106
- </button>
107
- </div>
108
- </div>
109
- );
110
- }
111
- ```
112
7
 
113
- ## Usage with Provider
114
-
115
- The headless UI still needs a `ConsentManagerProvider`:
116
-
117
- ```tsx
118
- import { type ReactNode } from 'react';
119
- import { ConsentManagerProvider } from '@c15t/nextjs';
120
-
121
- export function ConsentManager({ children }: { children: ReactNode }) {
122
- return (
123
- <ConsentManagerProvider
124
- options={{
125
- mode: 'hosted',
126
- backendURL: '/api/c15t',
127
- consentCategories: ['necessary', 'measurement', 'marketing'],
128
- }}
129
- >
130
- <CustomConsentBanner />
131
- {children}
132
- </ConsentManagerProvider>
133
- );
8
+ ## When to go headless
9
+
10
+ The pre-built banner and dialog cover most designs through props, slots, and
11
+ the stylesheet. Go headless when your markup has to be something else
12
+ entirely: a design system component, a native sheet, or a layout the compound
13
+ parts cannot express.
14
+
15
+ Headless code owns the rendered controls, so it also owns the compliance
16
+ outcome. The hooks hand you the actions a policy requires, the rights it must
17
+ keep reachable, and diagnostics when your presentation drops one. Render from
18
+ those lists rather than from a fixed set of buttons, and a site that later adds
19
+ a notice region keeps working without a code change.
20
+
21
+ Check for a policy before rendering your own surfaces. `useModel()` from
22
+ `c15t/next` returns `null` while no rule has resolved and `'none'` under a rule
23
+ that owes no consent UI, and the pre-built surfaces render nothing in that
24
+ state.
25
+
26
+ ## Minimal example
27
+
28
+ The headless hooks read the runtime from the `ConsentRoot` set up in your
29
+ [App Router](./app-router.md) or
30
+ [Pages Router](./pages-router.md) guide, so they only run in a
31
+ Client Component. Render this component inside that existing root in place
32
+ of the stock banner. Keep the dialog and persistent preferences control.
33
+
34
+ ```tsx title="components/consent-banner.tsx"
35
+ 'use client';
36
+
37
+ import { useHeadlessConsentUI, useTranslations } from 'c15t/next/headless';
38
+
39
+ const ACTION_LABELS = {
40
+ accept: 'acceptAll',
41
+ reject: 'rejectAll',
42
+ customize: 'customize',
43
+ dismiss: 'acknowledge',
44
+ save: 'save',
45
+ } as const;
46
+
47
+ export function Banner() {
48
+ const { banner, performAction, openDialog } = useHeadlessConsentUI();
49
+ const { common, rights } = useTranslations();
50
+
51
+ if (!banner.isVisible) {
52
+ return null;
53
+ }
54
+
55
+ return (
56
+ <section role="region" aria-label="Privacy">
57
+ {banner.preferenceControls.map((right) => (
58
+ <button key={right} type="button" onClick={openDialog}>
59
+ {right === 'opt-out' ? rights?.optOut : rights?.preferences}
60
+ </button>
61
+ ))}
62
+ {banner.actionGroups.map((group) => (
63
+ <div key={group.join('-')}>
64
+ {group.map((action) => (
65
+ <button
66
+ key={action}
67
+ type="button"
68
+ data-primary={banner.primaryActions.includes(action) || undefined}
69
+ onClick={() => performAction(action)}
70
+ >
71
+ {common[ACTION_LABELS[action]]}
72
+ </button>
73
+ ))}
74
+ </div>
75
+ ))}
76
+ </section>
77
+ );
134
78
  }
135
79
  ```
136
80
 
137
- ## Custom Dialog
138
-
139
- Build a custom consent dialog for detailed category management:
140
-
141
- ```tsx
142
- import { useConsentManager, useTranslations } from '@c15t/nextjs';
143
-
144
- function CustomConsentDialog() {
145
- const {
146
- activeUI,
147
- setActiveUI,
148
- consentTypes,
149
- consentCategories,
150
- selectedConsents,
151
- consents,
152
- setSelectedConsent,
153
- saveConsents,
154
- has,
155
- } = useConsentManager();
156
- const translations = useTranslations();
157
-
158
- if (activeUI !== 'dialog') return null;
159
-
160
- return (
161
- <div className="fixed inset-0 z-50 flex items-center justify-center">
162
- <div className="absolute inset-0 bg-black/50" onClick={() => setActiveUI('none')} />
163
- <div className="relative bg-white rounded-xl p-6 max-w-md w-full">
164
- <h2 className="text-lg font-semibold">{translations.consentManagerDialog.title}</h2>
165
-
166
- {consentTypes
167
- .filter((t) => consentCategories.includes(t.name))
168
- .map((type) => (
169
- <div key={type.name} className="flex items-center justify-between py-3 border-b">
170
- <div>
171
- <p className="font-medium">{translations.consentTypes[type.name]?.title}</p>
172
- <p className="text-sm text-gray-500">{type.description}</p>
173
- </div>
174
- <input
175
- type="checkbox"
176
- checked={selectedConsents[type.name] ?? consents[type.name] ?? false}
177
- disabled={type.disabled}
178
- onChange={(e) => setSelectedConsent(type.name, e.target.checked)}
179
- />
180
- </div>
181
- ))}
182
-
183
- <div className="mt-4 flex justify-end gap-2">
184
- <button onClick={() => saveConsents('necessary')}>Reject</button>
185
- <button onClick={() => saveConsents('custom')}>Save</button>
186
- <button onClick={() => saveConsents('all')}>Accept All</button>
187
- </div>
188
- </div>
189
- </div>
190
- );
191
- }
192
- ```
81
+ `banner.actionGroups` is the resolved layout: reject and accept share a group
82
+ at equal prominence, and the rest follow. `banner.orderedActions` is the same
83
+ list flattened. Under a notice the only action is `dismiss`, and
84
+ `banner.preferenceControls` recommends additional buttons for opening
85
+ preferences. Under a notice it contains `opt-out`, which selects the
86
+ "Do not sell or share my data" label. The example renders that button and
87
+ an "OK" button. Both controls keep their own command: opening
88
+ preferences and dismissing the notice.
89
+
90
+ The list is a rendering helper. It does not establish that your UI implements
91
+ all policy rights. Provide disclosure and persistent preferences access.
92
+
93
+ `performAction` saves all categories for `accept`, none for `reject`, the
94
+ current draft for `save`, records a dismissal for `dismiss`, and opens the
95
+ preference center for `customize`. `banner.diagnostics` reports when a host
96
+ layout drops a required action or gives equivalent actions different
97
+ prominence. Review each diagnostic when configuring custom presentation.
98
+
99
+ `banner.variant`, `banner.position`, and `banner.blocking` carry the resolved
100
+ shape from `presentation.prompt`, so a headless surface can follow the same
101
+ bar, widget, or wall choice the pre-built banner would make, and can trap
102
+ focus and lock scroll exactly when `blocking` is true.