@c15t/scripts 2.2.0 → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/AGENTS.md +77 -48
  2. package/README.md +4 -3
  3. package/dist/e2e-test-utils.js +60 -24
  4. package/dist/engine/compile.js +45 -45
  5. package/dist/engine/runtime.js +130 -119
  6. package/dist/registry.js +196 -176
  7. package/dist/resolve.js +12 -12
  8. package/dist/vendors/_shared/attributes.js +5 -5
  9. package/dist/vendors/_shared/google-consent.js +10 -10
  10. package/dist/vendors/_shared/install-builders.js +9 -9
  11. package/dist/vendors/_shared/script-url.js +12 -12
  12. package/dist/vendors/ads-and-pixels/linkedin-insights.js +16 -16
  13. package/dist/vendors/ads-and-pixels/meta-pixel.js +82 -82
  14. package/dist/vendors/ads-and-pixels/microsoft-uet.js +57 -57
  15. package/dist/vendors/ads-and-pixels/openai-pixel.js +88 -0
  16. package/dist/vendors/ads-and-pixels/reddit-pixel.js +39 -39
  17. package/dist/vendors/ads-and-pixels/snapchat-pixel.js +25 -25
  18. package/dist/vendors/ads-and-pixels/tiktok-pixel.js +31 -31
  19. package/dist/vendors/ads-and-pixels/x-pixel.js +17 -17
  20. package/dist/vendors/analytics/adobe-analytics.js +17 -17
  21. package/dist/vendors/analytics/ahrefs-analytics.js +8 -8
  22. package/dist/vendors/analytics/amplitude.js +39 -39
  23. package/dist/vendors/analytics/clearbit.js +11 -11
  24. package/dist/vendors/analytics/cloudflare-web-analytics.js +13 -13
  25. package/dist/vendors/analytics/databuddy.js +45 -45
  26. package/dist/vendors/analytics/fathom-analytics.js +15 -15
  27. package/dist/vendors/analytics/google-tag.js +23 -23
  28. package/dist/vendors/analytics/heap.js +37 -37
  29. package/dist/vendors/analytics/hightouch.js +30 -30
  30. package/dist/vendors/analytics/hotjar.js +14 -14
  31. package/dist/vendors/analytics/logrocket.js +24 -24
  32. package/dist/vendors/analytics/matomo-analytics.js +51 -51
  33. package/dist/vendors/analytics/microsoft-clarity.js +34 -31
  34. package/dist/vendors/analytics/mixpanel-analytics.js +31 -31
  35. package/dist/vendors/analytics/pirsch.js +27 -27
  36. package/dist/vendors/analytics/plausible-analytics.js +24 -24
  37. package/dist/vendors/analytics/posthog.js +84 -79
  38. package/dist/vendors/analytics/promptwatch.js +8 -8
  39. package/dist/vendors/analytics/rudderstack.js +50 -50
  40. package/dist/vendors/analytics/rybbit-analytics.js +30 -30
  41. package/dist/vendors/analytics/segment.js +16 -16
  42. package/dist/vendors/analytics/umami-analytics.js +16 -16
  43. package/dist/vendors/analytics/vercel-analytics.js +22 -22
  44. package/dist/vendors/functional/crisp.js +49 -51
  45. package/dist/vendors/functional/intercom.js +18 -18
  46. package/dist/vendors/tag-managers/cloudflare-zaraz.js +98 -0
  47. package/dist/vendors/tag-managers/google-tag-manager.js +20 -20
  48. package/dist-types/__tests__/helpers.d.ts +11 -11
  49. package/dist-types/engine/compile.d.ts +3 -3
  50. package/dist-types/engine/runtime.d.ts +3 -3
  51. package/dist-types/registry.d.ts +191 -173
  52. package/dist-types/resolve.d.ts +3 -3
  53. package/dist-types/types.d.ts +2 -2
  54. package/dist-types/vendors/_shared/attributes.d.ts +2 -2
  55. package/dist-types/vendors/_shared/google-consent.d.ts +2 -2
  56. package/dist-types/vendors/_shared/install-builders.d.ts +2 -2
  57. package/dist-types/vendors/_shared/script-url.d.ts +6 -6
  58. package/dist-types/vendors/ads-and-pixels/linkedin-insights.d.ts +14 -14
  59. package/dist-types/vendors/ads-and-pixels/meta-pixel.d.ts +27 -27
  60. package/dist-types/vendors/ads-and-pixels/microsoft-uet.d.ts +40 -40
  61. package/dist-types/vendors/ads-and-pixels/openai-pixel.d.ts +211 -0
  62. package/dist-types/vendors/ads-and-pixels/reddit-pixel.d.ts +28 -29
  63. package/dist-types/vendors/ads-and-pixels/snapchat-pixel.d.ts +23 -23
  64. package/dist-types/vendors/ads-and-pixels/tiktok-pixel.d.ts +22 -22
  65. package/dist-types/vendors/ads-and-pixels/x-pixel.d.ts +15 -15
  66. package/dist-types/vendors/analytics/adobe-analytics.d.ts +3 -3
  67. package/dist-types/vendors/analytics/ahrefs-analytics.d.ts +5 -5
  68. package/dist-types/vendors/analytics/amplitude.d.ts +24 -24
  69. package/dist-types/vendors/analytics/clearbit.d.ts +5 -5
  70. package/dist-types/vendors/analytics/cloudflare-web-analytics.d.ts +6 -6
  71. package/dist-types/vendors/analytics/databuddy.d.ts +34 -31
  72. package/dist-types/vendors/analytics/fathom-analytics.d.ts +8 -8
  73. package/dist-types/vendors/analytics/google-tag.d.ts +17 -17
  74. package/dist-types/vendors/analytics/heap.d.ts +17 -17
  75. package/dist-types/vendors/analytics/hightouch.d.ts +15 -15
  76. package/dist-types/vendors/analytics/hotjar.d.ts +9 -9
  77. package/dist-types/vendors/analytics/logrocket.d.ts +11 -11
  78. package/dist-types/vendors/analytics/matomo-analytics.d.ts +3 -3
  79. package/dist-types/vendors/analytics/microsoft-clarity.d.ts +12 -13
  80. package/dist-types/vendors/analytics/mixpanel-analytics.d.ts +20 -20
  81. package/dist-types/vendors/analytics/pirsch.d.ts +10 -10
  82. package/dist-types/vendors/analytics/plausible-analytics.d.ts +13 -13
  83. package/dist-types/vendors/analytics/posthog.d.ts +35 -32
  84. package/dist-types/vendors/analytics/promptwatch.d.ts +5 -5
  85. package/dist-types/vendors/analytics/rudderstack.d.ts +16 -16
  86. package/dist-types/vendors/analytics/rybbit-analytics.d.ts +15 -15
  87. package/dist-types/vendors/analytics/segment.d.ts +11 -11
  88. package/dist-types/vendors/analytics/umami-analytics.d.ts +9 -9
  89. package/dist-types/vendors/analytics/vercel-analytics.d.ts +13 -13
  90. package/dist-types/vendors/functional/crisp.d.ts +9 -9
  91. package/dist-types/vendors/functional/intercom.d.ts +12 -12
  92. package/dist-types/vendors/tag-managers/cloudflare-zaraz.d.ts +39 -0
  93. package/dist-types/vendors/tag-managers/google-tag-manager.d.ts +16 -16
  94. package/docs/README.md +77 -48
  95. package/docs/assets/v3/brand-bar.png +0 -0
  96. package/docs/assets/v3/brand-card.png +0 -0
  97. package/docs/assets/v3/choice-wall.png +0 -0
  98. package/docs/assets/v3/mobile-card.png +0 -0
  99. package/docs/assets/v3/preferences.png +0 -0
  100. package/docs/customization/overview.md +45 -0
  101. package/docs/customization/recipes.md +79 -0
  102. package/docs/customization/slots.md +55 -0
  103. package/docs/customization/tokens.md +76 -0
  104. package/docs/customization/translations.md +49 -0
  105. package/docs/frameworks/javascript/script-loader.md +81 -343
  106. package/docs/frameworks/next/script-loader.md +164 -455
  107. package/docs/frameworks/react/script-loader.md +41 -533
  108. package/docs/guides/consent-state.md +60 -0
  109. package/docs/guides/data-fetching.md +163 -0
  110. package/docs/guides/deployment-modes.md +75 -0
  111. package/docs/guides/troubleshooting.md +68 -0
  112. package/docs/guides/verify-consent.md +62 -0
  113. package/docs/integrations/adobe-analytics.md +239 -105
  114. package/docs/integrations/ahrefs-analytics.md +238 -104
  115. package/docs/integrations/amplitude.md +219 -157
  116. package/docs/integrations/building-integrations.md +36 -223
  117. package/docs/integrations/clear-on-revocation.md +167 -0
  118. package/docs/integrations/clearbit.md +247 -86
  119. package/docs/integrations/cloudflare-web-analytics.md +250 -84
  120. package/docs/integrations/cloudflare-zaraz.md +399 -0
  121. package/docs/integrations/crisp.md +251 -97
  122. package/docs/integrations/databuddy.md +259 -153
  123. package/docs/integrations/fathom-analytics.md +239 -96
  124. package/docs/integrations/google-maps.md +328 -207
  125. package/docs/integrations/google-tag-manager.md +248 -96
  126. package/docs/integrations/google-tag.md +261 -90
  127. package/docs/integrations/granular-consent.md +208 -0
  128. package/docs/integrations/heap.md +222 -149
  129. package/docs/integrations/hightouch.md +225 -131
  130. package/docs/integrations/hotjar.md +239 -90
  131. package/docs/integrations/intercom.md +239 -98
  132. package/docs/integrations/linkedin-insights.md +243 -113
  133. package/docs/integrations/logrocket.md +241 -123
  134. package/docs/integrations/matomo-analytics.md +256 -111
  135. package/docs/integrations/meta-pixel.md +197 -324
  136. package/docs/integrations/microsoft-clarity.md +233 -114
  137. package/docs/integrations/microsoft-uet.md +245 -110
  138. package/docs/integrations/mixpanel-analytics.md +252 -87
  139. package/docs/integrations/openai-pixel.md +441 -0
  140. package/docs/integrations/overview.md +96 -133
  141. package/docs/integrations/pirsch.md +249 -96
  142. package/docs/integrations/plausible-analytics.md +241 -100
  143. package/docs/integrations/posthog.md +353 -214
  144. package/docs/integrations/promptwatch.md +251 -81
  145. package/docs/integrations/reddit-pixel.md +226 -173
  146. package/docs/integrations/rudderstack.md +244 -187
  147. package/docs/integrations/rybbit-analytics.md +244 -91
  148. package/docs/integrations/segment.md +238 -92
  149. package/docs/integrations/snapchat-pixel.md +240 -110
  150. package/docs/integrations/tiktok-pixel.md +249 -81
  151. package/docs/integrations/umami-analytics.md +242 -95
  152. package/docs/integrations/vercel-analytics.md +242 -90
  153. package/docs/integrations/x-pixel.md +238 -104
  154. package/docs/integrations/youtube.md +359 -142
  155. package/docs/upgrade-v3.md +381 -0
  156. package/package.json +90 -78
  157. package/dist/e2e-test-utils.cjs +0 -166
  158. package/dist/engine/compile.cjs +0 -130
  159. package/dist/engine/runtime.cjs +0 -475
  160. package/dist/registry.cjs +0 -423
  161. package/dist/resolve.cjs +0 -71
  162. package/dist/types.cjs +0 -69
  163. package/dist/vendors/_shared/attributes.cjs +0 -55
  164. package/dist/vendors/_shared/google-consent.cjs +0 -69
  165. package/dist/vendors/_shared/install-builders.cjs +0 -59
  166. package/dist/vendors/_shared/script-url.cjs +0 -78
  167. package/dist/vendors/ads-and-pixels/linkedin-insights.cjs +0 -89
  168. package/dist/vendors/ads-and-pixels/meta-pixel.cjs +0 -206
  169. package/dist/vendors/ads-and-pixels/microsoft-uet.cjs +0 -151
  170. package/dist/vendors/ads-and-pixels/reddit-pixel.cjs +0 -151
  171. package/dist/vendors/ads-and-pixels/snapchat-pixel.cjs +0 -131
  172. package/dist/vendors/ads-and-pixels/tiktok-pixel.cjs +0 -130
  173. package/dist/vendors/ads-and-pixels/x-pixel.cjs +0 -92
  174. package/dist/vendors/analytics/adobe-analytics.cjs +0 -90
  175. package/dist/vendors/analytics/ahrefs-analytics.cjs +0 -68
  176. package/dist/vendors/analytics/amplitude.cjs +0 -193
  177. package/dist/vendors/analytics/clearbit.cjs +0 -69
  178. package/dist/vendors/analytics/cloudflare-web-analytics.cjs +0 -73
  179. package/dist/vendors/analytics/databuddy.cjs +0 -144
  180. package/dist/vendors/analytics/fathom-analytics.cjs +0 -76
  181. package/dist/vendors/analytics/google-tag.cjs +0 -107
  182. package/dist/vendors/analytics/heap.cjs +0 -181
  183. package/dist/vendors/analytics/hightouch.cjs +0 -153
  184. package/dist/vendors/analytics/hotjar.cjs +0 -85
  185. package/dist/vendors/analytics/logrocket.cjs +0 -99
  186. package/dist/vendors/analytics/matomo-analytics.cjs +0 -232
  187. package/dist/vendors/analytics/microsoft-clarity.cjs +0 -138
  188. package/dist/vendors/analytics/mixpanel-analytics.cjs +0 -134
  189. package/dist/vendors/analytics/pirsch.cjs +0 -108
  190. package/dist/vendors/analytics/plausible-analytics.cjs +0 -122
  191. package/dist/vendors/analytics/posthog.cjs +0 -236
  192. package/dist/vendors/analytics/promptwatch.cjs +0 -70
  193. package/dist/vendors/analytics/rudderstack.cjs +0 -227
  194. package/dist/vendors/analytics/rybbit-analytics.cjs +0 -104
  195. package/dist/vendors/analytics/segment.cjs +0 -97
  196. package/dist/vendors/analytics/umami-analytics.cjs +0 -80
  197. package/dist/vendors/analytics/vercel-analytics.cjs +0 -94
  198. package/dist/vendors/functional/crisp.cjs +0 -143
  199. package/dist/vendors/functional/intercom.cjs +0 -89
  200. package/dist/vendors/tag-managers/google-tag-manager.cjs +0 -100
  201. package/docs/shared/react/guides/script-loader.md +0 -311
  202. package/readme.json +0 -19
@@ -1,193 +1,410 @@
1
1
  ---
2
2
  title: YouTube
3
- description: Keep YouTube iframes unmounted until consent with a
4
- privacy-enhanced, lazy-loaded React or Next.js embed.
5
- icon: youtube
3
+ description: Gate YouTube embeds with c15t v3 in Next.js, TanStack Start, React,
4
+ Nuxt, Vue, Astro, Svelte, SvelteKit or JavaScript.
6
5
  group: integrations
7
6
  ---
8
- `YouTubeEmbed` is a renderable integration for React and Next.js. It uses c15t's
9
- `Frame` consent boundary, so the YouTube iframe is not mounted and no YouTube
10
- request is made until the configured consent category is allowed.
11
7
 
12
- The component does not load the YouTube IFrame Player API. It is intended for
13
- standard iframe embeds and keeps script readiness separate from iframe gating.
8
+ ## Configure the video
14
9
 
15
- ## Integrate with c15t
10
+ Use the ID of an embeddable video and the category matching its purpose in your
11
+ policy. The current v3 adapters use consent-gated iframes rather than the v2
12
+ `YouTubeEmbed` convenience component.
16
13
 
17
- `YouTubeEmbed` must render inside a `ConsentManagerProvider`. Complete the
18
- [React quickstart](/docs/frameworks/react/quickstart) or
19
- [Next.js quickstart](/docs/frameworks/next/quickstart) first.
14
+ ```ts title="src/embed-config.ts"
15
+ export const embedCategory = 'marketing' as const;
16
+ export const embedURL = 'https://www.youtube-nocookie.com/embed/VIDEO_ID?playsinline=1';
17
+ export const embedTitle = 'Product walkthrough';
18
+ export const embedAspectRatio = '16 / 9';
19
+ ```
20
20
 
21
- **React**
21
+ Replace `VIDEO_ID` before running the example.
22
+
23
+ ## Add the embed to your framework
24
+
25
+ Create `src/embed-config.ts` using the configuration on this page, then select
26
+ your framework. Keep the existing [consent setup](https://c15t.com/docs/frameworks), its policy
27
+ and preferences UI. These examples do not create a second consent provider.
28
+
29
+ **Next.js**
30
+
31
+ Render this component inside your existing consent boundary or provider.
32
+
33
+ ```tsx title="src/consent-embed.tsx"
34
+ 'use client';
22
35
 
23
- ```tsx
24
- import { YouTubeEmbed } from '@c15t/react';
36
+ import { ConsentGate } from 'c15t/next';
37
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
25
38
 
26
- export function ProductVideo() {
39
+ export function ConsentEmbed() {
27
40
  return (
28
- <YouTubeEmbed
29
- consentCategory="marketing"
30
- params={{ controls: true, playsinline: true }}
31
- title="Product overview"
32
- videoId="dQw4w9WgXcQ"
33
- />
41
+ <ConsentGate category={embedCategory}>
42
+ <iframe
43
+ src={embedURL}
44
+ title={embedTitle}
45
+ loading="lazy"
46
+ allowFullScreen
47
+ style={{ width: '100%', aspectRatio: embedAspectRatio, minHeight: 200, border: 0 }}
48
+ />
49
+ </ConsentGate>
34
50
  );
35
51
  }
36
52
  ```
37
53
 
38
- **Next.js**
54
+ `ConsentGate` keeps the iframe absent while permission is denied and removes it
55
+ on revocation. Keep your existing consent styles and preferences dialog.
39
56
 
40
- ```tsx
41
- 'use client';
57
+ **TanStack Start**
42
58
 
43
- import { YouTubeEmbed } from '@c15t/nextjs';
59
+ Render this component inside your existing consent boundary or provider.
44
60
 
45
- export function ProductVideo() {
61
+ ```tsx title="src/consent-embed.tsx"
62
+ import { ConsentGate } from 'c15t/tanstack-start';
63
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
64
+
65
+ export function ConsentEmbed() {
46
66
  return (
47
- <YouTubeEmbed
48
- consentCategory="marketing"
49
- params={{ controls: true, playsinline: true }}
50
- title="Product overview"
51
- videoId="dQw4w9WgXcQ"
52
- />
67
+ <ConsentGate category={embedCategory}>
68
+ <iframe
69
+ src={embedURL}
70
+ title={embedTitle}
71
+ loading="lazy"
72
+ allowFullScreen
73
+ style={{ width: '100%', aspectRatio: embedAspectRatio, minHeight: 200, border: 0 }}
74
+ />
75
+ </ConsentGate>
53
76
  );
54
77
  }
55
78
  ```
56
79
 
57
- `marketing` is the default category. Choose a different category only when it
58
- matches the purpose of the embed and the policy presented to your users.
59
-
60
- ## How c15t loads it
61
-
62
- * **Before consent:** c15t renders a placeholder and does not mount the iframe.
63
- * **After consent:** the iframe mounts with its final embed URL.
64
- * **On revocation:** the `Frame` boundary unmounts the iframe, stopping the
65
- embedded player and future requests from that document.
66
- * **Loading behavior:** c15t reserves the final player size, shows a localized
67
- loading state, and uses native iframe lazy loading by default.
68
- * **Default layout:** the placeholder and player share a responsive 16:9,
69
- borderless frame with a `200px` minimum height.
70
- * **Privacy-enhanced mode:** URLs built from `videoId` use
71
- `youtube-nocookie.com` by default.
72
-
73
- Privacy-enhanced mode changes the YouTube host but does not replace consent
74
- gating. Keep the iframe behind the category required by your privacy policy.
75
-
76
- ## Build the embed URL
77
-
78
- Prefer `videoId` when you control the video:
79
-
80
- ```tsx
81
- <YouTubeEmbed
82
- consentCategory="marketing"
83
- params={{
84
- autoplay: false,
85
- controls: true,
86
- playsinline: true,
87
- rel: false,
88
- }}
89
- start={36}
90
- title="Quarterly product update"
91
- videoId="dQw4w9WgXcQ"
92
- />
80
+ `ConsentGate` keeps the iframe absent while permission is denied and removes it
81
+ on revocation. Keep your existing consent styles and preferences dialog.
82
+
83
+ **React**
84
+
85
+ Render this component inside your existing consent boundary or provider.
86
+
87
+ ```tsx title="src/consent-embed.tsx"
88
+ import { ConsentGate } from 'c15t/react';
89
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
90
+
91
+ export function ConsentEmbed() {
92
+ return (
93
+ <ConsentGate category={embedCategory}>
94
+ <iframe
95
+ src={embedURL}
96
+ title={embedTitle}
97
+ loading="lazy"
98
+ allowFullScreen
99
+ style={{ width: '100%', aspectRatio: embedAspectRatio, minHeight: 200, border: 0 }}
100
+ />
101
+ </ConsentGate>
102
+ );
103
+ }
93
104
  ```
94
105
 
95
- Boolean `params` are serialized as YouTube's `1` and `0` values. `start` is
96
- serialized as the player's start time in seconds. If `params.start` is also
97
- present, the top-level `start` prop takes precedence.
106
+ `ConsentGate` keeps the iframe absent while permission is denied and removes it
107
+ on revocation. Keep your existing consent styles and preferences dialog.
108
+
109
+ **Nuxt**
110
+
111
+ Use the reactive snapshot from the existing consent runtime. The Nuxt module auto-imports the consent composables.
112
+
113
+ ```vue title="app/components/ConsentEmbed.vue"
114
+ <script setup lang="ts">
115
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from '../../src/embed-config';
116
+
117
+ const snapshot = useConsentSnapshot();
118
+ const activeUI = useConsentActiveUI();
119
+ </script>
120
+
121
+ <template>
122
+ <iframe
123
+ v-if="snapshot.effectivePermissions[embedCategory]"
124
+ :src="embedURL"
125
+ :title="embedTitle"
126
+ loading="lazy"
127
+ allowfullscreen
128
+ :style="{ width: '100%', aspectRatio: embedAspectRatio, minHeight: '200px', border: 0 }"
129
+ />
130
+ <button v-else type="button" @click="activeUI = 'manager'">
131
+ Open privacy settings to view this content
132
+ </button>
133
+ </template>
134
+ ```
98
135
 
99
- Set `privacyEnhanced={false}` only when you intentionally need the regular
100
- `youtube.com` host.
136
+ Use `v-if` so a denied iframe is removed from the DOM. Hiding an existing
137
+ iframe with `v-show` or CSS does not prevent its requests.
138
+
139
+ **Vue**
140
+
141
+ Use the reactive snapshot from the existing consent runtime. The c15t Vue plugin must already be installed on this app.
142
+
143
+ ```vue title="src/ConsentEmbed.vue"
144
+ <script setup lang="ts">
145
+ import { useConsentSnapshot, useConsentActiveUI } from 'c15t/vue/vue-plugin';
146
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
147
+
148
+ const snapshot = useConsentSnapshot();
149
+ const activeUI = useConsentActiveUI();
150
+ </script>
151
+
152
+ <template>
153
+ <iframe
154
+ v-if="snapshot.effectivePermissions[embedCategory]"
155
+ :src="embedURL"
156
+ :title="embedTitle"
157
+ loading="lazy"
158
+ allowfullscreen
159
+ :style="{ width: '100%', aspectRatio: embedAspectRatio, minHeight: '200px', border: 0 }"
160
+ />
161
+ <button v-else type="button" @click="activeUI = 'manager'">
162
+ Open privacy settings to view this content
163
+ </button>
164
+ </template>
165
+ ```
101
166
 
102
- ### Migrate an existing iframe URL
167
+ Use `v-if` so a denied iframe is removed from the DOM. Hiding an existing
168
+ iframe with `v-show` or CSS does not prevent its requests.
169
+
170
+ **Astro**
171
+
172
+ Use the shared browser helper below with Astro's existing page runtime.
173
+ Add this component to pages using your consent-enabled base layout:
174
+
175
+ ```astro title="src/components/ConsentEmbed.astro"
176
+ <c15t-consent-embed style="display: block"></c15t-consent-embed>
177
+
178
+ <script>
179
+ import { getConsentClient } from '@c15t/astro/client';
180
+ import { mountConsentEmbed } from '../consent-embed';
181
+
182
+ class ConsentEmbed extends HTMLElement {
183
+ dispose?: () => void;
184
+
185
+ connect = () => {
186
+ if (this.dispose) return;
187
+ const client = getConsentClient();
188
+ if (!client) return;
189
+ this.dispose = mountConsentEmbed(
190
+ this,
191
+ client.runtime.kernel,
192
+ () => { void client.openDialog(); },
193
+ );
194
+ };
195
+
196
+ connectedCallback() {
197
+ document.addEventListener('DOMContentLoaded', this.connect, { once: true });
198
+ this.connect();
199
+ }
200
+
201
+ disconnectedCallback() {
202
+ document.removeEventListener('DOMContentLoaded', this.connect);
203
+ this.dispose?.();
204
+ this.dispose = undefined;
205
+ }
206
+ }
103
207
 
104
- Use `src` when you already have a complete embed URL:
208
+ if (!customElements.get('c15t-consent-embed')) {
209
+ customElements.define('c15t-consent-embed', ConsentEmbed);
210
+ }
211
+ </script>
212
+ ```
105
213
 
106
- ```tsx
107
- <YouTubeEmbed
108
- consentCategory="marketing"
109
- src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?start=36"
110
- title="Quarterly product update"
111
- />
214
+ The first mount waits for the page's module scripts, including c15t's boot
215
+ script. Later `ClientRouter` mounts reuse the existing runtime. Removing the
216
+ component unsubscribes and removes the iframe. Do not create another consent
217
+ runtime for this embed.
218
+
219
+ **Svelte**
220
+
221
+ Render this component inside the existing `ConsentManagerProvider`.
222
+ The provider from your quickstart supplies its consent state.
223
+
224
+ ```svelte title="src/ConsentEmbed.svelte"
225
+ <script lang="ts">
226
+ import { ConsentGate } from '@c15t/svelte';
227
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
228
+ </script>
229
+
230
+ <ConsentGate category={embedCategory}>
231
+ <iframe
232
+ src={embedURL}
233
+ title={embedTitle}
234
+ loading="lazy"
235
+ allowfullscreen
236
+ style:width="100%"
237
+ style:aspect-ratio={embedAspectRatio}
238
+ style:min-height="200px"
239
+ style:border="0"
240
+ ></iframe>
241
+ </ConsentGate>
112
242
  ```
113
243
 
114
- When `src` is provided, c15t uses it unchanged. TypeScript treats `src` and
115
- `videoId` as mutually exclusive source modes: `start`, `params`, and
116
- `privacyEnhanced` are available only with `videoId`.
244
+ The Svelte `ConsentGate` waits until the browser is mounted and the category is
245
+ allowed. Its default placeholder opens preferences. Revocation removes the
246
+ iframe.
247
+
248
+ **SvelteKit**
249
+
250
+ Render this component inside the existing `ConsentManagerProvider`.
251
+ Keep the SvelteKit root provider and its server prefetch unchanged.
252
+
253
+ ```svelte title="src/lib/ConsentEmbed.svelte"
254
+ <script lang="ts">
255
+ import { ConsentGate } from '@c15t/svelte';
256
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from '../embed-config';
257
+ </script>
258
+
259
+ <ConsentGate category={embedCategory}>
260
+ <iframe
261
+ src={embedURL}
262
+ title={embedTitle}
263
+ loading="lazy"
264
+ allowfullscreen
265
+ style:width="100%"
266
+ style:aspect-ratio={embedAspectRatio}
267
+ style:min-height="200px"
268
+ style:border="0"
269
+ ></iframe>
270
+ </ConsentGate>
271
+ ```
117
272
 
118
- ## Style the wrapper and iframe
273
+ The Svelte `ConsentGate` waits until the browser is mounted and the category is
274
+ allowed. Its default placeholder opens preferences. Revocation removes the
275
+ iframe.
119
276
 
120
- `wrapperClassName` targets the consent-gated `Frame`. `className` and the
121
- forwarded ref target the iframe itself.
277
+ **JavaScript**
122
278
 
123
- The defaults are responsive and stable without utility classes. Override them
124
- only when your layout needs a different aspect ratio or height:
279
+ Use the shared browser helper below with your existing kernel. Put an
280
+ empty container where the embed should appear:
125
281
 
126
- ```tsx
127
- <YouTubeEmbed
128
- consentCategory="marketing"
129
- frameProps={{ style: { aspectRatio: '4 / 3', minHeight: 280 } }}
130
- style={{ borderRadius: 4 }}
131
- title="Product overview"
132
- videoId="dQw4w9WgXcQ"
133
- />
282
+ ```html
283
+ <div id="consent-embed"></div>
134
284
  ```
135
285
 
136
- `frameProps.style` is merged after the wrapper defaults. The iframe fills that
137
- wrapper, has no border, and inherits the wrapper radius by default.
286
+ In your browser entry point, after creating the kernel:
138
287
 
139
- ## Customize the placeholder
288
+ ```ts
289
+ import { mountConsentEmbed } from './consent-embed';
140
290
 
141
- ```tsx
142
- import { Frame, YouTubeEmbed } from '@c15t/react';
291
+ const container = document.querySelector<HTMLElement>('#consent-embed');
292
+ if (!container) throw new Error('Missing consent embed container');
143
293
 
144
- <YouTubeEmbed
145
- consentCategory="marketing"
146
- placeholder={
147
- <Frame.Root>
148
- <Frame.Title>Allow marketing consent to watch this video.</Frame.Title>
149
- <Frame.Button category="marketing" />
150
- </Frame.Root>
151
- }
152
- title="Product overview"
153
- videoId="dQw4w9WgXcQ"
154
- />
294
+ const disposeEmbed = mountConsentEmbed(container, kernel, openPreferences);
295
+ ```
296
+
297
+ `kernel` is the instance from your quickstart. `openPreferences` is your
298
+ application's function for showing its consent preferences UI. Call
299
+ `disposeEmbed()` when the page or component is destroyed. The helper observes
300
+ both the current snapshot and later changes.
301
+
302
+ ## Browser helper for Astro and JavaScript
303
+
304
+ Only the Astro and JavaScript examples need this helper. It creates the iframe
305
+ when permission allows it, keeps an existing player mounted across unrelated
306
+ snapshot updates, and removes it on revocation.
307
+
308
+ ```ts title="src/consent-embed.ts"
309
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
310
+
311
+ type EmbedKernel = {
312
+ getSnapshot: () => {
313
+ effectivePermissions: Record<typeof embedCategory, boolean>;
314
+ };
315
+ subscribe: (listener: () => void) => () => void;
316
+ };
317
+
318
+ export function mountConsentEmbed(
319
+ container: HTMLElement,
320
+ kernel: EmbedKernel,
321
+ openPreferences: () => void,
322
+ ) {
323
+ const render = () => {
324
+ if (kernel.getSnapshot().effectivePermissions[embedCategory]) {
325
+ if (container.querySelector('iframe')) return;
326
+ const frame = document.createElement('iframe');
327
+ frame.src = embedURL;
328
+ frame.title = embedTitle;
329
+ frame.loading = 'lazy';
330
+ frame.allowFullscreen = true;
331
+ Object.assign(frame.style, {
332
+ width: '100%',
333
+ aspectRatio: embedAspectRatio,
334
+ minHeight: '200px',
335
+ border: '0',
336
+ });
337
+ container.replaceChildren(frame);
338
+ } else {
339
+ if (container.querySelector('button')) return;
340
+ const button = document.createElement('button');
341
+ button.type = 'button';
342
+ button.textContent = 'Open privacy settings to view this content';
343
+ button.onclick = openPreferences;
344
+ container.replaceChildren(button);
345
+ }
346
+ };
347
+
348
+ render();
349
+ const unsubscribe = kernel.subscribe(render);
350
+ return () => {
351
+ unsubscribe();
352
+ container.replaceChildren();
353
+ };
354
+ }
155
355
  ```
156
356
 
157
- Custom placeholders should explain why the content is blocked and provide a
158
- clear way to change consent. Always give the iframe a meaningful `title`.
357
+ Keep a transcript, address or other useful alternative outside the embed.
358
+ `loading="lazy"` is a performance hint; the consent condition controls whether
359
+ the iframe exists at all. A denied category may be fixed by policy, so opening
360
+ preferences does not guarantee that the visitor can grant it.
361
+
362
+ ## Gate existing iframe markup
363
+
364
+ For markup that uses `data-category` and `data-src`, the
365
+ `createIframeBlocker` module from `c15t/modules/iframe-blocker` promotes
366
+ `data-src` only when the category has consent and the URL resolves to HTTP or
367
+ HTTPS. Relative URLs resolve against the iframe document's base URL.
368
+ Malformed URLs and other schemes, including `javascript:`, `data:` and `blob:`,
369
+ remain in `data-src` without loading. Correct the URL and call
370
+ `processAllIframes()` to retry.
371
+
372
+ Keep the initial URL in `data-src`, without `src`, so the browser cannot load
373
+ it before the blocker runs. The module does not sanitize arbitrary HTML or
374
+ modify iframes without `data-category`.
159
375
 
160
- Use `loadingFallback` to replace the post-consent loading message and
161
- `errorFallback` to replace the configuration error state. If neither `videoId`
162
- nor `src` is supplied at runtime, the component renders that error state instead
163
- of mounting an iframe.
376
+ ## Customize the player and placeholder
164
377
 
165
- Standard cross-origin iframes do not provide a reliable player-error signal.
166
- `YouTubeEmbed` forwards the iframe's native `onError` when a browser emits it,
167
- but player-level errors require the YouTube IFrame Player API and are outside
168
- this iframe-only component.
378
+ Set playback options in the iframe URL. For example, append `start=36` to begin
379
+ 36 seconds into the video. Use supported
380
+ [YouTube player parameters](https://developers.google.com/youtube/player_parameters)
381
+ and reserve at least a 200-by-200-pixel player area.
169
382
 
170
- ## Verify setup
383
+ React and Svelte `ConsentGate` components provide a blocked-state placeholder and
384
+ consent action. Their `placeholder` APIs differ: React takes a node, Svelte
385
+ takes a snippet. Keep a clear way to reopen preferences when customizing them.
386
+ The Vue and browser examples show that action explicitly. `loading="lazy"` delays an
387
+ allowed iframe for performance; it does not implement consent gating.
171
388
 
172
- 1. Clear saved consent and reload the page.
173
- 2. Confirm there is no YouTube iframe or YouTube network request before consent.
174
- 3. Grant the configured category and confirm exactly one iframe mounts.
175
- 4. Confirm a `videoId` embed uses `youtube-nocookie.com` unless
176
- `privacyEnhanced={false}`.
177
- 5. Revoke consent and confirm the iframe is removed.
178
- 6. Confirm the default 16:9 frame is borderless and reserves the same space at
179
- mobile and desktop widths.
389
+ ## Consent behavior
180
390
 
181
- See YouTube's [embedded player parameters](https://developers.google.com/youtube/player_parameters)
182
- for the supported query parameters.
391
+ Add `data-vendor="youtube"` next to `data-category` and declare a `youtube`
392
+ vendor in the runtime's `vendors` option or the backend manifest to let visitors
393
+ turn YouTube off while keeping the rest of the category on. See
394
+ [granular consent](./granular-consent.md).
183
395
 
184
- ## Types
396
+ Each example keeps the iframe absent until the category is allowed. After
397
+ permission, the iframe can load. Revoking permission removes the iframe and its
398
+ embedded document. Requests already sent cannot be recalled.
185
399
 
186
- ### YouTubeEmbedProps
400
+ The `youtube-nocookie.com` hostname does not replace the consent boundary. Avoid
401
+ loading a remote thumbnail, preconnecting to YouTube, or installing the IFrame
402
+ Player API separately if you require no YouTube request before permission.
403
+ This recipe embeds a video; it does not load the JavaScript Player API.
187
404
 
188
- |Property|Value|
189
- |:--|:--|
190
- |Type Name|\`YouTubeEmbedProps\`|
191
- |Source Path|\`./packages/react/src/components/integrations/youtube-embed.tsx\`|
405
+ ## Verify the integration
192
406
 
193
- \*ExtractedTypeTable: Could not extract "YouTubeEmbedProps" from "./packages/react/src/components/integrations/youtube-embed.tsx" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
407
+ In a fresh opt-in session, confirm no iframe or YouTube request exists. Grant
408
+ marketing permission and play the video, then revoke it and confirm the iframe
409
+ is removed. Test a narrow viewport and keyboard access to the placeholder and
410
+ player. See [consent verification](../guides/verify-consent.md).