@c15t/nextjs 2.2.1 → 3.0.0-alpha.0

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 (220) hide show
  1. package/AGENTS.md +102 -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 +133 -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 +173 -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 +86 -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 +102 -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 +60 -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 +134 -467
  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 +166 -250
  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/clearbit.md +247 -86
  82. package/docs/integrations/cloudflare-web-analytics.md +250 -84
  83. package/docs/integrations/crisp.md +251 -97
  84. package/docs/integrations/databuddy.md +259 -153
  85. package/docs/integrations/fathom-analytics.md +239 -96
  86. package/docs/integrations/google-maps.md +328 -207
  87. package/docs/integrations/google-tag-manager.md +248 -96
  88. package/docs/integrations/google-tag.md +261 -90
  89. package/docs/integrations/heap.md +222 -149
  90. package/docs/integrations/hightouch.md +225 -131
  91. package/docs/integrations/hotjar.md +239 -90
  92. package/docs/integrations/intercom.md +239 -98
  93. package/docs/integrations/linkedin-insights.md +243 -113
  94. package/docs/integrations/logrocket.md +241 -123
  95. package/docs/integrations/matomo-analytics.md +256 -111
  96. package/docs/integrations/meta-pixel.md +197 -324
  97. package/docs/integrations/microsoft-clarity.md +233 -114
  98. package/docs/integrations/microsoft-uet.md +245 -110
  99. package/docs/integrations/mixpanel-analytics.md +252 -87
  100. package/docs/integrations/openai-pixel.md +441 -0
  101. package/docs/integrations/overview.md +95 -133
  102. package/docs/integrations/pirsch.md +249 -96
  103. package/docs/integrations/plausible-analytics.md +241 -100
  104. package/docs/integrations/posthog.md +353 -214
  105. package/docs/integrations/promptwatch.md +251 -81
  106. package/docs/integrations/reddit-pixel.md +226 -173
  107. package/docs/integrations/rudderstack.md +244 -187
  108. package/docs/integrations/rybbit-analytics.md +244 -91
  109. package/docs/integrations/segment.md +238 -92
  110. package/docs/integrations/snapchat-pixel.md +240 -110
  111. package/docs/integrations/tiktok-pixel.md +249 -81
  112. package/docs/integrations/umami-analytics.md +242 -95
  113. package/docs/integrations/vercel-analytics.md +242 -90
  114. package/docs/integrations/x-pixel.md +238 -104
  115. package/docs/integrations/youtube.md +354 -142
  116. package/docs/upgrade-v3.md +334 -0
  117. package/iab/styles.css +1 -1
  118. package/iab/styles.tw3.css +1 -1
  119. package/package.json +106 -65
  120. package/readme.json +3 -3
  121. package/src/iab/styles.css +1 -1
  122. package/src/iab/styles.tw3.css +1 -1
  123. package/src/styles.css +1 -1
  124. package/src/styles.tw3.css +1 -1
  125. package/styles.css +1 -1
  126. package/styles.tw3.css +1 -1
  127. package/client/components/consent-dialog-link.js +0 -3
  128. package/client/components/integrations.js +0 -3
  129. package/dist/components/integrations/index.cjs +0 -1
  130. package/dist/components/integrations/index.js +0 -1
  131. package/dist/headless.cjs +0 -1
  132. package/dist/index.cjs +0 -1
  133. package/dist/libs/browser-initial-data.cjs +0 -1
  134. package/dist/libs/browser-initial-data.js +0 -1
  135. package/dist/libs/initial-data.cjs +0 -1
  136. package/dist/libs/initial-data.js +0 -1
  137. package/dist/types.cjs +0 -1
  138. package/dist/version.cjs +0 -1
  139. package/dist-types/components/integrations/index.d.ts +0 -1
  140. package/dist-types/libs/browser-initial-data.d.ts +0 -9
  141. package/dist-types/libs/initial-data.d.ts +0 -33
  142. package/docs/frameworks/next/building-headless-components.md +0 -379
  143. package/docs/frameworks/next/callbacks.md +0 -186
  144. package/docs/frameworks/next/concepts/client-modes.md +0 -177
  145. package/docs/frameworks/next/concepts/consent-models.md +0 -117
  146. package/docs/frameworks/next/concepts/cookie-management.md +0 -122
  147. package/docs/frameworks/next/concepts/glossary.md +0 -24
  148. package/docs/frameworks/next/concepts/initialization-flow.md +0 -149
  149. package/docs/frameworks/next/concepts/policy-packs.md +0 -230
  150. package/docs/frameworks/next/hooks/use-color-scheme.md +0 -41
  151. package/docs/frameworks/next/hooks/use-consent-manager/checking-consent.md +0 -96
  152. package/docs/frameworks/next/hooks/use-consent-manager/location-info.md +0 -97
  153. package/docs/frameworks/next/hooks/use-consent-manager/setting-consent.md +0 -94
  154. package/docs/frameworks/next/hooks/use-draggable.md +0 -59
  155. package/docs/frameworks/next/hooks/use-focus-trap.md +0 -42
  156. package/docs/frameworks/next/hooks/use-reduced-motion.md +0 -37
  157. package/docs/frameworks/next/hooks/use-ssr-status.md +0 -32
  158. package/docs/frameworks/next/hooks/use-text-direction.md +0 -50
  159. package/docs/frameworks/next/hooks/use-translations.md +0 -55
  160. package/docs/frameworks/next/iab/consent-banner.md +0 -91
  161. package/docs/frameworks/next/iab/consent-dialog.md +0 -129
  162. package/docs/frameworks/next/iab/use-gvl-data.md +0 -21
  163. package/docs/frameworks/next/iframe-blocking.md +0 -106
  164. package/docs/frameworks/next/internationalization.md +0 -207
  165. package/docs/frameworks/next/network-blocker.md +0 -140
  166. package/docs/frameworks/next/policy-packs.md +0 -248
  167. package/docs/frameworks/next/styling/classnames.md +0 -94
  168. package/docs/frameworks/next/styling/color-scheme.md +0 -84
  169. package/docs/frameworks/next/styling/css-variables.md +0 -53
  170. package/docs/frameworks/next/styling/slots.md +0 -94
  171. package/docs/frameworks/next/styling/tailwind.md +0 -137
  172. package/docs/frameworks/next/styling/tokens.md +0 -156
  173. package/docs/shared/concepts/client-modes.md +0 -103
  174. package/docs/shared/concepts/consent-categories.md +0 -41
  175. package/docs/shared/concepts/consent-models.md +0 -72
  176. package/docs/shared/concepts/cookie-management.md +0 -88
  177. package/docs/shared/concepts/glossary.md +0 -24
  178. package/docs/shared/concepts/initialization-flow.md +0 -105
  179. package/docs/shared/concepts/policy-packs.md +0 -225
  180. package/docs/shared/react/components/consent-banner.md +0 -242
  181. package/docs/shared/react/components/consent-dialog-link.md +0 -45
  182. package/docs/shared/react/components/consent-dialog-trigger.md +0 -185
  183. package/docs/shared/react/components/consent-dialog.md +0 -119
  184. package/docs/shared/react/components/consent-manager-provider.md +0 -225
  185. package/docs/shared/react/components/consent-widget.md +0 -121
  186. package/docs/shared/react/components/dev-tools.md +0 -81
  187. package/docs/shared/react/components/frame.md +0 -52
  188. package/docs/shared/react/guides/building-headless-components.md +0 -110
  189. package/docs/shared/react/guides/callbacks.md +0 -89
  190. package/docs/shared/react/guides/headless.md +0 -31
  191. package/docs/shared/react/guides/iframe-blocking.md +0 -65
  192. package/docs/shared/react/guides/internationalization.md +0 -123
  193. package/docs/shared/react/guides/network-blocker.md +0 -72
  194. package/docs/shared/react/guides/optimization.md +0 -44
  195. package/docs/shared/react/guides/policy-packs.md +0 -173
  196. package/docs/shared/react/guides/script-loader.md +0 -311
  197. package/docs/shared/react/hooks/use-color-scheme.md +0 -31
  198. package/docs/shared/react/hooks/use-consent-manager/checking-consent.md +0 -95
  199. package/docs/shared/react/hooks/use-consent-manager/location-info.md +0 -96
  200. package/docs/shared/react/hooks/use-consent-manager/overview.md +0 -74
  201. package/docs/shared/react/hooks/use-consent-manager/setting-consent.md +0 -93
  202. package/docs/shared/react/hooks/use-draggable.md +0 -30
  203. package/docs/shared/react/hooks/use-focus-trap.md +0 -20
  204. package/docs/shared/react/hooks/use-reduced-motion.md +0 -33
  205. package/docs/shared/react/hooks/use-ssr-status.md +0 -16
  206. package/docs/shared/react/hooks/use-text-direction.md +0 -38
  207. package/docs/shared/react/hooks/use-translations.md +0 -15
  208. package/docs/shared/react/iab/consent-banner.md +0 -60
  209. package/docs/shared/react/iab/consent-dialog.md +0 -76
  210. package/docs/shared/react/iab/overview.md +0 -80
  211. package/docs/shared/react/iab/use-gvl-data.md +0 -21
  212. package/docs/shared/react/styling/classnames.md +0 -93
  213. package/docs/shared/react/styling/color-scheme.md +0 -35
  214. package/docs/shared/react/styling/css-variables.md +0 -53
  215. package/docs/shared/react/styling/overview.md +0 -261
  216. package/docs/shared/react/styling/slots.md +0 -93
  217. package/docs/shared/react/styling/stylesheet-entrypoint.md +0 -8
  218. package/docs/shared/react/styling/tailwind.md +0 -88
  219. package/docs/shared/react/styling/tokens.md +0 -155
  220. package/docs/shared/troubleshooting.md +0 -82
@@ -0,0 +1,403 @@
1
+ ---
2
+ title: App Router
3
+ description: Set up App Router with Inth, cached manifests and consent-gated scripts.
4
+ group: frameworks
5
+ ---
6
+
7
+ ## Before you start
8
+
9
+ This guide uses [Inth](https://inth.com) with cached manifests. Your Next.js
10
+ server reuses public policy data and resolves consent for each visitor. The
11
+ browser sends consent submissions to Inth.
12
+
13
+ You can use a [self-hosted backend](https://c15t.com/docs/self-host/quickstart) with the same
14
+ configuration and your own endpoint. [Offline mode](./api-reference/data-fetching.md#offline-configuration)
15
+ uses bundled policy and browser storage without backend records. Not recommended
16
+ for production environments.
17
+
18
+ This setup needs a Next.js server. For `output: 'export'`, follow
19
+ [static export](./static-export.md). For a smaller setup with no
20
+ local API routes, use [client-side initialization](./client-side.md).
21
+ Same-origin [rewrites](./optimization.md) are optional.
22
+
23
+ Location-based policies need trusted location headers from your host. Without
24
+ them, c15t uses your unknown-location rule. Check the supported
25
+ [geography headers](./api-reference/data-fetching.md#geography-and-privacy-signals)
26
+ before deploying.
27
+
28
+ The default layout awaits consent on the server inside a `Suspense` boundary,
29
+ so the server renders the resolved banner into the response instead of
30
+ deciding in the browser. You can also
31
+ [stream the page while consent resolves](#stream-the-page-while-consent-resolves)
32
+ or initialize in the browser. Awaiting has a cost; see
33
+ [what server prefetch costs](#what-server-prefetch-costs) before deploying.
34
+
35
+ ## Install c15t and configure your backend
36
+
37
+ [Create an Inth project](https://inth.com), configure its policy rules and allow
38
+ your app's origin. Copy its backend endpoint for the upstream configuration.
39
+
40
+ If you chose self-hosting, complete the
41
+ [backend quickstart](https://c15t.com/docs/self-host/quickstart) and use its public endpoint in
42
+ the steps below. Offline setups do not need an endpoint; use the
43
+ [offline example](./api-reference/data-fetching.md#offline-configuration).
44
+
45
+ | Package manager | Command |
46
+ | :-------------- | :----------------- |
47
+ | npm | `npm install c15t` |
48
+ | pnpm | `pnpm add c15t` |
49
+ | yarn | `yarn add c15t` |
50
+ | bun | `bun add c15t` |
51
+
52
+ | Package manager | Command |
53
+ | :-------------- | :-------------------------- |
54
+ | npm | `npm install @c15t/scripts` |
55
+ | pnpm | `pnpm add @c15t/scripts` |
56
+ | yarn | `yarn add @c15t/scripts` |
57
+ | bun | `bun add @c15t/scripts` |
58
+
59
+ With Tailwind CSS 4, add the prebuilt stylesheet to your global CSS after
60
+ Tailwind so its component layer follows Tailwind's:
61
+
62
+ ```css title="app/globals.css"
63
+ @import 'tailwindcss';
64
+ @import 'c15t/next/styles.css';
65
+ ```
66
+
67
+ For Tailwind CSS 3, use the [Tailwind 3 stylesheet recipe](./styling/overview.md#stylesheet)
68
+ instead. It loads `c15t/next/styles.tw3.css` between the `components` and
69
+ `utilities` directives to prevent Tailwind from purging c15t selectors.
70
+
71
+ The layouts on this page import `./globals.css`; without Tailwind the same
72
+ `@import` line goes at the top of whatever global stylesheet the layout loads.
73
+
74
+ Set `NEXT_PUBLIC_C15T_BACKEND_URL` to the absolute endpoint supplied by Inth or
75
+ your self-hosted backend, including any path prefix. This is a public endpoint,
76
+ not a secret. Configure the backend to allow your app's origin.
77
+
78
+ ## Share the consent URLs
79
+
80
+ Create this file at the project root. Set `manifestURL` explicitly to select
81
+ manifest resolution.
82
+
83
+ ```ts title="c15t.config.ts"
84
+ import { defineConsentConfig } from 'c15t/next';
85
+
86
+ const backendURL = process.env.NEXT_PUBLIC_C15T_BACKEND_URL;
87
+ if (!backendURL) throw new Error('Set NEXT_PUBLIC_C15T_BACKEND_URL');
88
+
89
+ export const consentConfig = defineConsentConfig({
90
+ backendURL,
91
+ manifestURL: '/api/c15t/manifest',
92
+ });
93
+ ```
94
+
95
+ ## Serve the manifest route
96
+
97
+ The built-in handler reads the absolute upstream URL from `C15T_BACKEND_URL`,
98
+ falling back to `NEXT_PUBLIC_C15T_BACKEND_URL` from the setup above. If you set
99
+ both, point them at the same backend.
100
+
101
+ ```ts title="app/api/c15t/manifest/route.ts"
102
+ export { manifestGET as GET } from 'c15t/next/api';
103
+ ```
104
+
105
+ The manifest handler caches public policy data. Server prefetch resolves it
106
+ using the visitor's request. Consent submissions go directly to `backendURL`.
107
+
108
+ The paths below assume `app/` is at the project root. Adjust imports if you use
109
+ `src/app/`.
110
+
111
+ ## Register scripts in a client wrapper
112
+
113
+ The [runnable Next.js example](https://c15t.com/docs/examples) uses PostHog for measurement and
114
+ X Pixel for marketing. Set `NEXT_PUBLIC_POSTHOG_KEY` and
115
+ `NEXT_PUBLIC_X_PIXEL_ID` to your own project identifiers before building.
116
+ `NEXT_PUBLIC_POSTHOG_HOST` optionally selects your PostHog region's API host.
117
+ Omit a vendor's ID to leave that integration disabled, or replace its helper
118
+ with the [integration](../../integrations/overview.md) your application uses. Include the
119
+ measurement and marketing categories in your policy for these two vendors.
120
+
121
+ Create `lib/scripts.ts` with the example's script configuration:
122
+
123
+ ```ts title="lib/scripts.ts"
124
+ import { posthog } from '@c15t/scripts/posthog';
125
+ import { xPixel } from '@c15t/scripts/x-pixel';
126
+ import type { Script } from 'c15t';
127
+
128
+ export const posthogConfigured = Boolean(process.env.NEXT_PUBLIC_POSTHOG_KEY);
129
+ export const xPixelConfigured = Boolean(process.env.NEXT_PUBLIC_X_PIXEL_ID);
130
+
131
+ export const scripts: Script[] = [];
132
+
133
+ if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
134
+ scripts.push(
135
+ posthog({
136
+ apiHost: process.env.NEXT_PUBLIC_POSTHOG_HOST,
137
+ id: process.env.NEXT_PUBLIC_POSTHOG_KEY,
138
+ initOptions: { cookieless_mode: 'never' },
139
+ loadMode: 'after-consent',
140
+ })
141
+ );
142
+ }
143
+
144
+ if (process.env.NEXT_PUBLIC_X_PIXEL_ID) {
145
+ scripts.push(xPixel({ pixelId: process.env.NEXT_PUBLIC_X_PIXEL_ID }));
146
+ }
147
+ ```
148
+
149
+ PostHog waits for measurement consent here. `cookieless_mode: 'never'` disables
150
+ cookieless capture after rejection. X Pixel waits for marketing consent. Remove
151
+ any existing loader for these vendors, including `next/script` and tag-manager
152
+ entries, so each integration loads once.
153
+
154
+ Create this client wrapper. It keeps scripts and browser callbacks in the client
155
+ while the router supplies the visitor's resolved state through `state`:
156
+
157
+ ```tsx title="components/consent.tsx"
158
+ 'use client';
159
+
160
+ import type { ReactNode } from 'react';
161
+ import {
162
+ ConsentBanner,
163
+ ConsentDialog,
164
+ ConsentDialogLink,
165
+ ConsentRoot,
166
+ } from 'c15t/next';
167
+ import type { ConsentRootProps } from 'c15t/next';
168
+ import { consentConfig } from '../c15t.config';
169
+ import { scripts } from '../lib/scripts';
170
+
171
+ export function Consent({
172
+ children,
173
+ state,
174
+ }: {
175
+ children: ReactNode;
176
+ state: ConsentRootProps['state'];
177
+ }) {
178
+ return (
179
+ <ConsentRoot state={state} config={consentConfig} scripts={scripts}>
180
+ {children}
181
+ <ConsentBanner />
182
+ <ConsentDialog />
183
+ <footer>
184
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
185
+ </footer>
186
+ </ConsentRoot>
187
+ );
188
+ }
189
+ ```
190
+
191
+ `ConsentRoot` already provides the consent runtime. Mount this wrapper once;
192
+ do not add a second provider. Keep your site's content and footer inside it.
193
+
194
+ ## Start consent resolution in the root layout
195
+
196
+ Await `resolveConsent` in an async Server Component and mount that
197
+ component inside `Suspense`. The helper reads request cookies and geographic
198
+ headers, resolves policy from the manifest and returns the visitor's resolved
199
+ state, which the `Consent` wrapper passes to `ConsentRoot` as `state`.
200
+
201
+ ```tsx title="app/layout.tsx"
202
+ import { Suspense } from 'react';
203
+ import type { ReactNode } from 'react';
204
+ import { resolveConsent } from 'c15t/next/server';
205
+ import { consentConfig } from '../c15t.config';
206
+ import { Consent } from '../components/consent';
207
+ import './globals.css';
208
+
209
+ async function ResolvedConsent({ children }: { children: ReactNode }) {
210
+ const state = await resolveConsent({ config: consentConfig });
211
+
212
+ return <Consent state={state}>{children}</Consent>;
213
+ }
214
+
215
+ export default function RootLayout({ children }: { children: ReactNode }) {
216
+ return (
217
+ <html lang="en">
218
+ <body>
219
+ <Suspense fallback={null}>
220
+ <ResolvedConsent>{children}</ResolvedConsent>
221
+ </Suspense>
222
+ </body>
223
+ </html>
224
+ );
225
+ }
226
+ ```
227
+
228
+ With this layout, the server renders the consent subtree only after policy has
229
+ resolved, so the banner arrives already decided and does not change after
230
+ hydration. Because the boundary uses `fallback={null}`, Next.js may stream the
231
+ shell first and the consent subtree a moment later in the same response. A
232
+ returning visitor whose request cookie carries a valid stored choice sees no
233
+ banner; an expired choice, a choice recorded under a different policy
234
+ fingerprint, or one that no longer covers every required category prompts
235
+ again. Location rules apply when your host supplies trusted geography
236
+ headers. The [runnable Next.js example](https://c15t.com/docs/examples) uses this awaited form.
237
+
238
+ If the manifest request fails, `resolveConsent` returns a baseline
239
+ state without a resolved policy and logs the failure outside production. The
240
+ page still renders, and the browser resolves policy after hydration, so on that
241
+ request a new visitor sees the prompt appear late. Pass `onError` to report
242
+ these failures; see [troubleshooting](./troubleshooting.md).
243
+
244
+ The page content waits with the consent subtree. Because `{children}` sits
245
+ inside `ResolvedConsent` and the fallback is `null`, a slow manifest response
246
+ leaves the whole page blank until the helper returns or its 10 second fetch
247
+ timeout fires and the baseline renders. A warm manifest cache makes this a few
248
+ milliseconds; a cold cache pays the backend round trip. If the site cannot
249
+ accept that, use the [streaming form](#stream-the-page-while-consent-resolves),
250
+ which renders the shell immediately.
251
+
252
+ Keep the c15t stylesheet in `globals.css` rather than importing
253
+ `c15t/next/styles.css` from a layout or component. Tailwind CSS 4 declares its
254
+ cascade layers in that file; when the c15t stylesheet is bundled separately
255
+ and lands first, Tailwind's preflight sits above the c15t component layer and
256
+ strips the banner's padding, borders and button styles.
257
+
258
+ The `Suspense` boundary is required with `cacheComponents: true`: awaiting
259
+ request data directly in the root layout without it fails the build. Without
260
+ `cacheComponents`, the boundary is still safe to keep. If `next dev` reports an
261
+ unstable `Date.now()` during prerendering, see
262
+ [troubleshooting](./troubleshooting.md#why-does-nextjs-report-an-unstable-datenow-during-prerendering).
263
+
264
+ ## What server prefetch costs
265
+
266
+ `resolveConsent` reads request cookies and headers through
267
+ `next/headers`. Without `cacheComponents`, this makes routes under the layout
268
+ dynamically rendered. With `cacheComponents` enabled, Next.js can prerender
269
+ and cache the static shell outside `Suspense`, while the visitor-specific
270
+ consent subtree resolves and streams per request.
271
+
272
+ The first request on a new server instance has an empty manifest cache and
273
+ waits for the upstream `/manifest` fetch. In the local benchmark with 150 ms of
274
+ simulated backend latency, the single cold sample measured a 252 ms time to
275
+ first byte. A build-time manifest seed to remove this cold start is planned in
276
+ [c15t/c15t#1096](https://github.com/c15t/c15t/issues/1096).
277
+
278
+ With a warm manifest cache in the same local benchmark (awaited SSR, new
279
+ visitors, 150 ms simulated backend latency), time to first byte was 7.4 ms
280
+ median and 15.2 ms at p95 across 10 samples, and the banner markup was present
281
+ in the first HTML. The benchmark layout awaits the helper without a `Suspense`
282
+ boundary, so it does not measure the streaming behavior of the recipe on this
283
+ page, and its 28 ms banner metric is a post-hydration visibility probe, not
284
+ first paint. See the
285
+ [benchmark results](https://github.com/c15t/c15t/tree/ccfe906682ba06cef7b56a16cc8fd10728df77d3/benchmarks/reports/next-manifest-2026-09-10)
286
+ for conditions.
287
+
288
+ The resolved consent subtree contains one visitor's state. Do not put that
289
+ subtree, or the complete response containing it, in a shared cache. Public
290
+ manifests and the prerendered static shell can be cached separately. See
291
+ [cache policy data without sharing visitor state](./server-side.md#cache-policy-data-without-sharing-visitor-state).
292
+
293
+ When the page shell must not wait for consent, use the
294
+ [streaming form](#stream-the-page-while-consent-resolves); it still reads the
295
+ request, so the route stays dynamic. When the site must stay fully static, use
296
+ [client-side initialization](./client-side.md).
297
+
298
+ ## Stream the page while consent resolves
299
+
300
+ To render the page shell before consent resolves, keep the root layout
301
+ synchronous and pass the pending promise from `resolveConsent` to
302
+ `state`. `ConsentRoot` accepts either the resolved result or its promise.
303
+
304
+ ```tsx title="app/layout.tsx"
305
+ import type { ReactNode } from 'react';
306
+ import { resolveConsent } from 'c15t/next/server';
307
+ import { consentConfig } from '../c15t.config';
308
+ import { Consent } from '../components/consent';
309
+ import './globals.css';
310
+
311
+ export default function RootLayout({ children }: { children: ReactNode }) {
312
+ const state = resolveConsent({ config: consentConfig });
313
+
314
+ return (
315
+ <html lang="en">
316
+ <body><Consent state={state}>{children}</Consent></body>
317
+ </html>
318
+ );
319
+ }
320
+ ```
321
+
322
+ This promise form does not render the banner in the initial HTML. The provider
323
+ mounts with a provisional policy, consent surfaces stay hidden until the
324
+ promise resolves, and the browser then applies the server result. Keep this
325
+ layout synchronous, including when Next.js `cacheComponents` is enabled; the
326
+ form works with `cacheComponents: true`. Request helpers still need a request
327
+ context even though the layout passes a promise. See
328
+ [server rendering](./server-side.md) for the comparison of
329
+ rendering paths.
330
+
331
+ ## Verify the manifest path
332
+
333
+ For an opt-in policy, reject and confirm neither configured vendor loads. Allow
334
+ measurement only: PostHog can load, while X Pixel stays blocked. Revoke
335
+ measurement from Privacy settings and check PostHog's capture is disabled.
336
+ See [script loading](./script-loader.md) for vendor-specific
337
+ loading and revocation behavior.
338
+
339
+ Inspect server requests and confirm that consent resolution reads the manifest
340
+ instead of calling Inth `/init`. Warm requests should reuse policy data according
341
+ to its cache headers. Consent choices should post to `${backendURL}/subjects`.
342
+
343
+ Test a new visitor, a returning rejection and two locations. Missing geographic
344
+ headers must follow your unknown-location rule. See
345
+ [fetching reference](./api-reference/data-fetching.md) for
346
+ location headers and [Optimization](./optimization.md) for cache
347
+ settings. If resolution fails, use
348
+ [Troubleshooting](./troubleshooting.md), then run
349
+ [the consent checks](../../guides/verify-consent.md).
350
+
351
+ ## Initialize in the browser
352
+
353
+ Keep the shared config and manifest route. Remove the `resolveConsent`
354
+ and `Suspense` imports and the `ResolvedConsent` component, and render the
355
+ wrapper directly with an empty state:
356
+
357
+ ```tsx title="app/layout.tsx"
358
+ import type { ReactNode } from 'react';
359
+ import { Consent } from '../components/consent';
360
+ import './globals.css';
361
+
362
+ export default function RootLayout({ children }: { children: ReactNode }) {
363
+ return (
364
+ <html lang="en">
365
+ <body>
366
+ <Consent state={{}}>{children}</Consent>
367
+ </body>
368
+ </html>
369
+ );
370
+ }
371
+ ```
372
+
373
+ The browser loads the manifest and resolves policy locally, so the initial
374
+ HTML has no resolved prompt. Without supplied location, geography is unknown.
375
+ For browser initialization with request geography, see the
376
+ [optional local init route](./api-reference/data-fetching.md#do-i-need-the-local-init-route).
377
+
378
+ ## Use regular backend initialization instead
379
+
380
+ For a complete browser-only setup with hosted records, follow
381
+ [client-side initialization](./client-side.md). It needs no
382
+ local route handlers or server prefetch.
383
+
384
+ For the smallest hosted configuration, keep only `backendURL` in
385
+ `defineConsentConfig`:
386
+
387
+ ```ts title="c15t.config.ts"
388
+ import { defineConsentConfig } from 'c15t/next';
389
+
390
+ const backendURL = process.env.NEXT_PUBLIC_C15T_BACKEND_URL;
391
+ if (!backendURL) throw new Error('Set NEXT_PUBLIC_C15T_BACKEND_URL');
392
+
393
+ export const consentConfig = defineConsentConfig({ backendURL });
394
+ ```
395
+
396
+ This is an alternative to the manifest configuration. Remove the unused local
397
+ manifest route. The existing prefetch and boundary now initialize
398
+ through `${backendURL}/init`; consent saves still use `${backendURL}/subjects`.
399
+ A backend-only config does not enable manifest mode automatically.
400
+
401
+ Compare the choices in [data fetching](./data-fetching.md), or
402
+ configure cache settings and optional rewrites in
403
+ [Optimization](./optimization.md).
@@ -0,0 +1,118 @@
1
+ ---
2
+ title: Client-side initialization
3
+ description: Initialize Next.js consent in the browser with one backend URL,
4
+ without server prefetch or local API handlers.
5
+ group: frameworks
6
+ ---
7
+
8
+ ## Choose when consent resolves
9
+
10
+ Use browser initialization when consent does not need to be resolved in the
11
+ initial HTML. This works with prerendered pages and client navigation. No
12
+ `resolveConsent`, manifest route or rewrite is required for this setup.
13
+
14
+ Use [server rendering](./server-side.md) when consent UI should
15
+ render on the server after policy resolution, or when resolution should start
16
+ on the server while the page streams.
17
+
18
+ "Client-side initialization" describes where consent resolves. Next.js can
19
+ still prerender a Client Component's initial markup. Optional permissions stay
20
+ denied and consent UI stays hidden until policy resolution succeeds.
21
+
22
+ ## Configure one backend URL
23
+
24
+ [Inth](https://inth.com) is recommended for production. A self-hosted c15t
25
+ backend uses the same configuration with its own public endpoint.
26
+
27
+ | Package manager | Command |
28
+ | :-------------- | :----------------- |
29
+ | npm | `npm install c15t` |
30
+ | pnpm | `pnpm add c15t` |
31
+ | yarn | `yarn add c15t` |
32
+ | bun | `bun add c15t` |
33
+
34
+ Set `NEXT_PUBLIC_C15T_BACKEND_URL` to the absolute public backend endpoint and
35
+ configure your app's allowed origin there. Create this shared file at the
36
+ project root:
37
+
38
+ ```ts title="c15t.config.ts"
39
+ import { defineConsentConfig } from 'c15t/next';
40
+
41
+ const backendURL = process.env.NEXT_PUBLIC_C15T_BACKEND_URL;
42
+ if (!backendURL) throw new Error('Set NEXT_PUBLIC_C15T_BACKEND_URL');
43
+
44
+ export const consentConfig = defineConsentConfig({ backendURL });
45
+ ```
46
+
47
+ With the current API, a config containing only `backendURL` initializes through
48
+ `${backendURL}/init` and submits choices to `${backendURL}/subjects`. It does
49
+ not enable manifest resolution or generate API routes. Use
50
+ [data fetching](./data-fetching.md) to choose manifest mode.
51
+
52
+ ## Mount one browser ConsentRoot
53
+
54
+ ```tsx title="components/consent.tsx"
55
+ 'use client';
56
+
57
+ import type { ReactNode } from 'react';
58
+ import {
59
+ ConsentBanner,
60
+ ConsentDialog,
61
+ ConsentDialogLink,
62
+ ConsentRoot,
63
+ } from 'c15t/next';
64
+ import { consentConfig } from '../c15t.config';
65
+
66
+ export function Consent({ children }: { children: ReactNode }) {
67
+ return (
68
+ <ConsentRoot state={{}} config={consentConfig}>
69
+ {children}
70
+ <ConsentBanner />
71
+ <ConsentDialog />
72
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
73
+ </ConsentRoot>
74
+ );
75
+ }
76
+ ```
77
+
78
+ `state={{}}` supplies no server-resolved policy. `ConsentRoot` starts hosted
79
+ initialization in the browser. Keep it mounted across client navigation.
80
+
81
+ For App Router, load the [stylesheet for your Tailwind version](./styling/overview.md#stylesheet)
82
+ from `app/globals.css`, then mount the wrapper in the root layout:
83
+
84
+ ```tsx title="app/layout.tsx"
85
+ import type { ReactNode } from 'react';
86
+ import { Consent } from '../components/consent';
87
+ import './globals.css';
88
+
89
+ export default function RootLayout({ children }: { children: ReactNode }) {
90
+ return (
91
+ <html lang="en">
92
+ <body><Consent>{children}</Consent></body>
93
+ </html>
94
+ );
95
+ }
96
+ ```
97
+
98
+ For Pages Router, put the same `Consent` wrapper around
99
+ `<Component {...pageProps} />` in `pages/_app.tsx` and import the stylesheet
100
+ there. No `getServerSideProps` is needed for consent in this variant.
101
+
102
+ ## Optional optimizations and local development
103
+
104
+ A [same-origin rewrite](./optimization.md) can avoid an
105
+ additional browser connection to the backend domain. It is optional and needs
106
+ a Next.js server or equivalent hosting proxy. A full static export can keep
107
+ the direct public URL shown here.
108
+
109
+ Offline mode is not recommended for production environments. For local
110
+ development and tests without a backend, use the
111
+ [offline example](./api-reference/data-fetching.md#offline-configuration).
112
+
113
+ ## Verify browser initialization
114
+
115
+ In a fresh session, confirm the browser calls `/init`, displays the resolved
116
+ prompt, and sends no denied vendor requests. Save a rejection, reload, and
117
+ reopen preferences. The choice should persist. Test a failed backend request
118
+ too; hidden consent UI must not be interpreted as permission.