@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.
- package/AGENTS.md +102 -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 +133 -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 +173 -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 +86 -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 +102 -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 +60 -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 +134 -467
- 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 +166 -250
- 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/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,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
|
|
3
|
-
description: Build
|
|
4
|
-
|
|
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
|
-
##
|
|
114
|
-
|
|
115
|
-
The
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|