@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.
- package/AGENTS.md +103 -142
- package/README.md +4 -4
- package/dist/api.js +1 -0
- package/dist/config.js +1 -0
- package/dist/devtools.js +2 -0
- package/dist/headers.js +1 -0
- package/dist/iab/styles.css +1 -1
- package/dist/iab/styles.tw3.css +27 -19
- package/dist/index.js +1 -1
- package/dist/middleware.js +1 -0
- package/dist/node-bridge.js +1 -0
- package/dist/pages.js +1 -0
- package/dist/proxy.js +1 -0
- package/dist/root.js +2 -0
- package/dist/server.js +1 -0
- package/dist/static.js +1 -0
- package/dist/styles.css +1 -1
- package/dist/styles.tw3.css +67 -29
- package/dist/version.js +1 -1
- package/dist-types/api.d.ts +88 -0
- package/dist-types/config.d.ts +110 -0
- package/dist-types/devtools.d.ts +1 -0
- package/dist-types/headers.d.ts +4 -0
- package/dist-types/index.d.ts +28 -10
- package/dist-types/middleware.d.ts +15 -0
- package/dist-types/node-bridge.d.ts +57 -0
- package/dist-types/pages.d.ts +88 -0
- package/dist-types/proxy.d.ts +34 -0
- package/dist-types/root.d.ts +92 -0
- package/dist-types/server.d.ts +164 -0
- package/dist-types/static.d.ts +42 -0
- package/dist-types/types.d.ts +5 -36
- package/dist-types/version.d.ts +1 -1
- package/docs/README.md +103 -142
- package/docs/assets/v3/brand-bar.png +0 -0
- package/docs/assets/v3/brand-card.png +0 -0
- package/docs/assets/v3/choice-wall.png +0 -0
- package/docs/assets/v3/mobile-card.png +0 -0
- package/docs/assets/v3/preferences.png +0 -0
- package/docs/customization/overview.md +45 -0
- package/docs/customization/recipes.md +79 -0
- package/docs/customization/slots.md +55 -0
- package/docs/customization/tokens.md +76 -0
- package/docs/customization/translations.md +49 -0
- package/docs/frameworks/next/api-reference/data-fetching.md +416 -0
- package/docs/frameworks/next/app-router.md +403 -0
- package/docs/frameworks/next/client-side.md +118 -0
- package/docs/frameworks/next/components/consent-banner.md +251 -211
- package/docs/frameworks/next/components/consent-dialog-link.md +96 -35
- package/docs/frameworks/next/components/consent-dialog-trigger.md +74 -149
- package/docs/frameworks/next/components/consent-dialog.md +189 -134
- package/docs/frameworks/next/components/consent-manager-provider.md +68 -318
- package/docs/frameworks/next/components/consent-widget.md +172 -114
- package/docs/frameworks/next/components/dev-tools.md +199 -40
- package/docs/frameworks/next/components/frame.md +137 -42
- package/docs/frameworks/next/concepts/consent-categories.md +24 -89
- package/docs/frameworks/next/concepts/policy-presets.md +142 -0
- package/docs/frameworks/next/content-security-policy.md +189 -0
- package/docs/frameworks/next/data-fetching.md +74 -0
- package/docs/frameworks/next/geography-headers.md +251 -0
- package/docs/frameworks/next/headless.md +95 -185
- package/docs/frameworks/next/hooks/use-consent-manager/overview.md +42 -163
- package/docs/frameworks/next/iab/overview.md +37 -107
- package/docs/frameworks/next/optimization.md +158 -194
- package/docs/frameworks/next/pages-router.md +296 -0
- package/docs/frameworks/next/quickstart.md +31 -132
- package/docs/frameworks/next/script-loader.md +140 -465
- package/docs/frameworks/next/server-side.md +97 -130
- package/docs/frameworks/next/static-export.md +164 -0
- package/docs/frameworks/next/styling/overview.md +174 -248
- package/docs/frameworks/next/troubleshooting.md +134 -144
- package/docs/guides/consent-state.md +60 -0
- package/docs/guides/data-fetching.md +163 -0
- package/docs/guides/deployment-modes.md +63 -0
- package/docs/guides/troubleshooting.md +68 -0
- package/docs/guides/verify-consent.md +62 -0
- package/docs/integrations/adobe-analytics.md +239 -105
- package/docs/integrations/ahrefs-analytics.md +238 -104
- package/docs/integrations/amplitude.md +219 -157
- package/docs/integrations/building-integrations.md +32 -224
- package/docs/integrations/clear-on-revocation.md +167 -0
- package/docs/integrations/clearbit.md +247 -86
- package/docs/integrations/cloudflare-web-analytics.md +250 -84
- package/docs/integrations/crisp.md +251 -97
- package/docs/integrations/databuddy.md +259 -153
- package/docs/integrations/fathom-analytics.md +239 -96
- package/docs/integrations/google-maps.md +328 -207
- package/docs/integrations/google-tag-manager.md +248 -96
- package/docs/integrations/google-tag.md +261 -90
- package/docs/integrations/heap.md +222 -149
- package/docs/integrations/hightouch.md +225 -131
- package/docs/integrations/hotjar.md +239 -90
- package/docs/integrations/intercom.md +239 -98
- package/docs/integrations/linkedin-insights.md +243 -113
- package/docs/integrations/logrocket.md +241 -123
- package/docs/integrations/matomo-analytics.md +256 -111
- package/docs/integrations/meta-pixel.md +197 -324
- package/docs/integrations/microsoft-clarity.md +233 -114
- package/docs/integrations/microsoft-uet.md +245 -110
- package/docs/integrations/mixpanel-analytics.md +252 -87
- package/docs/integrations/openai-pixel.md +441 -0
- package/docs/integrations/overview.md +95 -133
- package/docs/integrations/pirsch.md +249 -96
- package/docs/integrations/plausible-analytics.md +241 -100
- package/docs/integrations/posthog.md +353 -214
- package/docs/integrations/promptwatch.md +251 -81
- package/docs/integrations/reddit-pixel.md +226 -173
- package/docs/integrations/rudderstack.md +244 -187
- package/docs/integrations/rybbit-analytics.md +244 -91
- package/docs/integrations/segment.md +238 -92
- package/docs/integrations/snapchat-pixel.md +240 -110
- package/docs/integrations/tiktok-pixel.md +249 -81
- package/docs/integrations/umami-analytics.md +242 -95
- package/docs/integrations/vercel-analytics.md +242 -90
- package/docs/integrations/x-pixel.md +238 -104
- package/docs/integrations/youtube.md +354 -142
- package/docs/upgrade-v3.md +334 -0
- package/iab/styles.css +1 -1
- package/iab/styles.tw3.css +1 -1
- package/package.json +106 -65
- package/readme.json +3 -3
- package/src/iab/styles.css +1 -1
- package/src/iab/styles.tw3.css +1 -1
- package/src/styles.css +1 -1
- package/src/styles.tw3.css +1 -1
- package/styles.css +1 -1
- package/styles.tw3.css +1 -1
- package/client/components/consent-dialog-link.js +0 -3
- package/client/components/integrations.js +0 -3
- package/dist/components/integrations/index.cjs +0 -1
- package/dist/components/integrations/index.js +0 -1
- package/dist/headless.cjs +0 -1
- package/dist/index.cjs +0 -1
- package/dist/libs/browser-initial-data.cjs +0 -1
- package/dist/libs/browser-initial-data.js +0 -1
- package/dist/libs/initial-data.cjs +0 -1
- package/dist/libs/initial-data.js +0 -1
- package/dist/types.cjs +0 -1
- package/dist/version.cjs +0 -1
- package/dist-types/components/integrations/index.d.ts +0 -1
- package/dist-types/libs/browser-initial-data.d.ts +0 -9
- package/dist-types/libs/initial-data.d.ts +0 -33
- package/docs/frameworks/next/building-headless-components.md +0 -379
- package/docs/frameworks/next/callbacks.md +0 -186
- package/docs/frameworks/next/concepts/client-modes.md +0 -177
- package/docs/frameworks/next/concepts/consent-models.md +0 -117
- package/docs/frameworks/next/concepts/cookie-management.md +0 -122
- package/docs/frameworks/next/concepts/glossary.md +0 -24
- package/docs/frameworks/next/concepts/initialization-flow.md +0 -149
- package/docs/frameworks/next/concepts/policy-packs.md +0 -230
- package/docs/frameworks/next/hooks/use-color-scheme.md +0 -41
- package/docs/frameworks/next/hooks/use-consent-manager/checking-consent.md +0 -96
- package/docs/frameworks/next/hooks/use-consent-manager/location-info.md +0 -97
- package/docs/frameworks/next/hooks/use-consent-manager/setting-consent.md +0 -94
- package/docs/frameworks/next/hooks/use-draggable.md +0 -59
- package/docs/frameworks/next/hooks/use-focus-trap.md +0 -42
- package/docs/frameworks/next/hooks/use-reduced-motion.md +0 -37
- package/docs/frameworks/next/hooks/use-ssr-status.md +0 -32
- package/docs/frameworks/next/hooks/use-text-direction.md +0 -50
- package/docs/frameworks/next/hooks/use-translations.md +0 -55
- package/docs/frameworks/next/iab/consent-banner.md +0 -91
- package/docs/frameworks/next/iab/consent-dialog.md +0 -129
- package/docs/frameworks/next/iab/use-gvl-data.md +0 -21
- package/docs/frameworks/next/iframe-blocking.md +0 -106
- package/docs/frameworks/next/internationalization.md +0 -207
- package/docs/frameworks/next/network-blocker.md +0 -140
- package/docs/frameworks/next/policy-packs.md +0 -248
- package/docs/frameworks/next/styling/classnames.md +0 -94
- package/docs/frameworks/next/styling/color-scheme.md +0 -84
- package/docs/frameworks/next/styling/css-variables.md +0 -53
- package/docs/frameworks/next/styling/slots.md +0 -94
- package/docs/frameworks/next/styling/tailwind.md +0 -137
- package/docs/frameworks/next/styling/tokens.md +0 -156
- package/docs/shared/concepts/client-modes.md +0 -103
- package/docs/shared/concepts/consent-categories.md +0 -41
- package/docs/shared/concepts/consent-models.md +0 -72
- package/docs/shared/concepts/cookie-management.md +0 -88
- package/docs/shared/concepts/glossary.md +0 -24
- package/docs/shared/concepts/initialization-flow.md +0 -105
- package/docs/shared/concepts/policy-packs.md +0 -225
- package/docs/shared/react/components/consent-banner.md +0 -242
- package/docs/shared/react/components/consent-dialog-link.md +0 -45
- package/docs/shared/react/components/consent-dialog-trigger.md +0 -185
- package/docs/shared/react/components/consent-dialog.md +0 -119
- package/docs/shared/react/components/consent-manager-provider.md +0 -225
- package/docs/shared/react/components/consent-widget.md +0 -121
- package/docs/shared/react/components/dev-tools.md +0 -81
- package/docs/shared/react/components/frame.md +0 -52
- package/docs/shared/react/guides/building-headless-components.md +0 -110
- package/docs/shared/react/guides/callbacks.md +0 -89
- package/docs/shared/react/guides/headless.md +0 -31
- package/docs/shared/react/guides/iframe-blocking.md +0 -65
- package/docs/shared/react/guides/internationalization.md +0 -123
- package/docs/shared/react/guides/network-blocker.md +0 -72
- package/docs/shared/react/guides/optimization.md +0 -44
- package/docs/shared/react/guides/policy-packs.md +0 -173
- package/docs/shared/react/guides/script-loader.md +0 -311
- package/docs/shared/react/hooks/use-color-scheme.md +0 -31
- package/docs/shared/react/hooks/use-consent-manager/checking-consent.md +0 -95
- package/docs/shared/react/hooks/use-consent-manager/location-info.md +0 -96
- package/docs/shared/react/hooks/use-consent-manager/overview.md +0 -74
- package/docs/shared/react/hooks/use-consent-manager/setting-consent.md +0 -93
- package/docs/shared/react/hooks/use-draggable.md +0 -30
- package/docs/shared/react/hooks/use-focus-trap.md +0 -20
- package/docs/shared/react/hooks/use-reduced-motion.md +0 -33
- package/docs/shared/react/hooks/use-ssr-status.md +0 -16
- package/docs/shared/react/hooks/use-text-direction.md +0 -38
- package/docs/shared/react/hooks/use-translations.md +0 -15
- package/docs/shared/react/iab/consent-banner.md +0 -60
- package/docs/shared/react/iab/consent-dialog.md +0 -76
- package/docs/shared/react/iab/overview.md +0 -80
- package/docs/shared/react/iab/use-gvl-data.md +0 -21
- package/docs/shared/react/styling/classnames.md +0 -93
- package/docs/shared/react/styling/color-scheme.md +0 -35
- package/docs/shared/react/styling/css-variables.md +0 -53
- package/docs/shared/react/styling/overview.md +0 -261
- package/docs/shared/react/styling/slots.md +0 -93
- package/docs/shared/react/styling/stylesheet-entrypoint.md +0 -8
- package/docs/shared/react/styling/tailwind.md +0 -88
- package/docs/shared/react/styling/tokens.md +0 -155
- 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.
|