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