@c15t/scripts 2.2.0 → 3.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/AGENTS.md +74 -48
  2. package/README.md +3 -3
  3. package/dist/e2e-test-utils.js +60 -24
  4. package/dist/engine/compile.js +45 -45
  5. package/dist/engine/runtime.js +119 -119
  6. package/dist/registry.js +186 -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 +31 -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/google-tag-manager.js +20 -20
  47. package/dist-types/__tests__/helpers.d.ts +10 -10
  48. package/dist-types/engine/compile.d.ts +2 -2
  49. package/dist-types/engine/runtime.d.ts +3 -3
  50. package/dist-types/registry.d.ts +182 -173
  51. package/dist-types/resolve.d.ts +2 -2
  52. package/dist-types/types.d.ts +2 -2
  53. package/dist-types/vendors/_shared/attributes.d.ts +2 -2
  54. package/dist-types/vendors/_shared/google-consent.d.ts +2 -2
  55. package/dist-types/vendors/_shared/install-builders.d.ts +1 -1
  56. package/dist-types/vendors/_shared/script-url.d.ts +6 -6
  57. package/dist-types/vendors/ads-and-pixels/linkedin-insights.d.ts +14 -14
  58. package/dist-types/vendors/ads-and-pixels/meta-pixel.d.ts +27 -27
  59. package/dist-types/vendors/ads-and-pixels/microsoft-uet.d.ts +40 -40
  60. package/dist-types/vendors/ads-and-pixels/openai-pixel.d.ts +211 -0
  61. package/dist-types/vendors/ads-and-pixels/reddit-pixel.d.ts +28 -29
  62. package/dist-types/vendors/ads-and-pixels/snapchat-pixel.d.ts +23 -23
  63. package/dist-types/vendors/ads-and-pixels/tiktok-pixel.d.ts +22 -22
  64. package/dist-types/vendors/ads-and-pixels/x-pixel.d.ts +15 -15
  65. package/dist-types/vendors/analytics/adobe-analytics.d.ts +3 -3
  66. package/dist-types/vendors/analytics/ahrefs-analytics.d.ts +5 -5
  67. package/dist-types/vendors/analytics/amplitude.d.ts +24 -24
  68. package/dist-types/vendors/analytics/clearbit.d.ts +5 -5
  69. package/dist-types/vendors/analytics/cloudflare-web-analytics.d.ts +6 -6
  70. package/dist-types/vendors/analytics/databuddy.d.ts +34 -31
  71. package/dist-types/vendors/analytics/fathom-analytics.d.ts +8 -8
  72. package/dist-types/vendors/analytics/google-tag.d.ts +17 -17
  73. package/dist-types/vendors/analytics/heap.d.ts +17 -17
  74. package/dist-types/vendors/analytics/hightouch.d.ts +15 -15
  75. package/dist-types/vendors/analytics/hotjar.d.ts +9 -9
  76. package/dist-types/vendors/analytics/logrocket.d.ts +11 -11
  77. package/dist-types/vendors/analytics/matomo-analytics.d.ts +3 -3
  78. package/dist-types/vendors/analytics/microsoft-clarity.d.ts +12 -13
  79. package/dist-types/vendors/analytics/mixpanel-analytics.d.ts +20 -20
  80. package/dist-types/vendors/analytics/pirsch.d.ts +10 -10
  81. package/dist-types/vendors/analytics/plausible-analytics.d.ts +13 -13
  82. package/dist-types/vendors/analytics/posthog.d.ts +35 -32
  83. package/dist-types/vendors/analytics/promptwatch.d.ts +5 -5
  84. package/dist-types/vendors/analytics/rudderstack.d.ts +16 -16
  85. package/dist-types/vendors/analytics/rybbit-analytics.d.ts +15 -15
  86. package/dist-types/vendors/analytics/segment.d.ts +11 -11
  87. package/dist-types/vendors/analytics/umami-analytics.d.ts +9 -9
  88. package/dist-types/vendors/analytics/vercel-analytics.d.ts +13 -13
  89. package/dist-types/vendors/functional/crisp.d.ts +9 -9
  90. package/dist-types/vendors/functional/intercom.d.ts +12 -12
  91. package/dist-types/vendors/tag-managers/google-tag-manager.d.ts +16 -16
  92. package/docs/README.md +74 -48
  93. package/docs/assets/v3/brand-bar.png +0 -0
  94. package/docs/assets/v3/brand-card.png +0 -0
  95. package/docs/assets/v3/choice-wall.png +0 -0
  96. package/docs/assets/v3/mobile-card.png +0 -0
  97. package/docs/assets/v3/preferences.png +0 -0
  98. package/docs/customization/overview.md +45 -0
  99. package/docs/customization/recipes.md +79 -0
  100. package/docs/customization/slots.md +55 -0
  101. package/docs/customization/tokens.md +76 -0
  102. package/docs/customization/translations.md +49 -0
  103. package/docs/frameworks/javascript/script-loader.md +30 -339
  104. package/docs/frameworks/next/script-loader.md +134 -467
  105. package/docs/frameworks/react/script-loader.md +35 -535
  106. package/docs/guides/consent-state.md +60 -0
  107. package/docs/guides/data-fetching.md +163 -0
  108. package/docs/guides/deployment-modes.md +63 -0
  109. package/docs/guides/troubleshooting.md +68 -0
  110. package/docs/guides/verify-consent.md +62 -0
  111. package/docs/integrations/adobe-analytics.md +239 -105
  112. package/docs/integrations/ahrefs-analytics.md +238 -104
  113. package/docs/integrations/amplitude.md +219 -157
  114. package/docs/integrations/building-integrations.md +32 -224
  115. package/docs/integrations/clearbit.md +247 -86
  116. package/docs/integrations/cloudflare-web-analytics.md +250 -84
  117. package/docs/integrations/crisp.md +251 -97
  118. package/docs/integrations/databuddy.md +259 -153
  119. package/docs/integrations/fathom-analytics.md +239 -96
  120. package/docs/integrations/google-maps.md +328 -207
  121. package/docs/integrations/google-tag-manager.md +248 -96
  122. package/docs/integrations/google-tag.md +261 -90
  123. package/docs/integrations/heap.md +222 -149
  124. package/docs/integrations/hightouch.md +225 -131
  125. package/docs/integrations/hotjar.md +239 -90
  126. package/docs/integrations/intercom.md +239 -98
  127. package/docs/integrations/linkedin-insights.md +243 -113
  128. package/docs/integrations/logrocket.md +241 -123
  129. package/docs/integrations/matomo-analytics.md +256 -111
  130. package/docs/integrations/meta-pixel.md +197 -324
  131. package/docs/integrations/microsoft-clarity.md +233 -114
  132. package/docs/integrations/microsoft-uet.md +245 -110
  133. package/docs/integrations/mixpanel-analytics.md +252 -87
  134. package/docs/integrations/openai-pixel.md +441 -0
  135. package/docs/integrations/overview.md +95 -133
  136. package/docs/integrations/pirsch.md +249 -96
  137. package/docs/integrations/plausible-analytics.md +241 -100
  138. package/docs/integrations/posthog.md +353 -214
  139. package/docs/integrations/promptwatch.md +251 -81
  140. package/docs/integrations/reddit-pixel.md +226 -173
  141. package/docs/integrations/rudderstack.md +244 -187
  142. package/docs/integrations/rybbit-analytics.md +244 -91
  143. package/docs/integrations/segment.md +238 -92
  144. package/docs/integrations/snapchat-pixel.md +240 -110
  145. package/docs/integrations/tiktok-pixel.md +249 -81
  146. package/docs/integrations/umami-analytics.md +242 -95
  147. package/docs/integrations/vercel-analytics.md +242 -90
  148. package/docs/integrations/x-pixel.md +238 -104
  149. package/docs/integrations/youtube.md +354 -142
  150. package/docs/upgrade-v3.md +334 -0
  151. package/package.json +85 -78
  152. package/readme.json +2 -2
  153. package/dist/e2e-test-utils.cjs +0 -166
  154. package/dist/engine/compile.cjs +0 -130
  155. package/dist/engine/runtime.cjs +0 -475
  156. package/dist/registry.cjs +0 -423
  157. package/dist/resolve.cjs +0 -71
  158. package/dist/types.cjs +0 -69
  159. package/dist/vendors/_shared/attributes.cjs +0 -55
  160. package/dist/vendors/_shared/google-consent.cjs +0 -69
  161. package/dist/vendors/_shared/install-builders.cjs +0 -59
  162. package/dist/vendors/_shared/script-url.cjs +0 -78
  163. package/dist/vendors/ads-and-pixels/linkedin-insights.cjs +0 -89
  164. package/dist/vendors/ads-and-pixels/meta-pixel.cjs +0 -206
  165. package/dist/vendors/ads-and-pixels/microsoft-uet.cjs +0 -151
  166. package/dist/vendors/ads-and-pixels/reddit-pixel.cjs +0 -151
  167. package/dist/vendors/ads-and-pixels/snapchat-pixel.cjs +0 -131
  168. package/dist/vendors/ads-and-pixels/tiktok-pixel.cjs +0 -130
  169. package/dist/vendors/ads-and-pixels/x-pixel.cjs +0 -92
  170. package/dist/vendors/analytics/adobe-analytics.cjs +0 -90
  171. package/dist/vendors/analytics/ahrefs-analytics.cjs +0 -68
  172. package/dist/vendors/analytics/amplitude.cjs +0 -193
  173. package/dist/vendors/analytics/clearbit.cjs +0 -69
  174. package/dist/vendors/analytics/cloudflare-web-analytics.cjs +0 -73
  175. package/dist/vendors/analytics/databuddy.cjs +0 -144
  176. package/dist/vendors/analytics/fathom-analytics.cjs +0 -76
  177. package/dist/vendors/analytics/google-tag.cjs +0 -107
  178. package/dist/vendors/analytics/heap.cjs +0 -181
  179. package/dist/vendors/analytics/hightouch.cjs +0 -153
  180. package/dist/vendors/analytics/hotjar.cjs +0 -85
  181. package/dist/vendors/analytics/logrocket.cjs +0 -99
  182. package/dist/vendors/analytics/matomo-analytics.cjs +0 -232
  183. package/dist/vendors/analytics/microsoft-clarity.cjs +0 -138
  184. package/dist/vendors/analytics/mixpanel-analytics.cjs +0 -134
  185. package/dist/vendors/analytics/pirsch.cjs +0 -108
  186. package/dist/vendors/analytics/plausible-analytics.cjs +0 -122
  187. package/dist/vendors/analytics/posthog.cjs +0 -236
  188. package/dist/vendors/analytics/promptwatch.cjs +0 -70
  189. package/dist/vendors/analytics/rudderstack.cjs +0 -227
  190. package/dist/vendors/analytics/rybbit-analytics.cjs +0 -104
  191. package/dist/vendors/analytics/segment.cjs +0 -97
  192. package/dist/vendors/analytics/umami-analytics.cjs +0 -80
  193. package/dist/vendors/analytics/vercel-analytics.cjs +0 -94
  194. package/dist/vendors/functional/crisp.cjs +0 -143
  195. package/dist/vendors/functional/intercom.cjs +0 -89
  196. package/dist/vendors/tag-managers/google-tag-manager.cjs +0 -100
  197. package/docs/shared/react/guides/script-loader.md +0 -311
@@ -1,193 +1,405 @@
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 { Frame } 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
+ <Frame 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
+ </Frame>
34
50
  );
35
51
  }
36
52
  ```
37
53
 
38
- **Next.js**
54
+ `Frame` 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**
58
+
59
+ Render this component inside your existing consent boundary or provider.
42
60
 
43
- import { YouTubeEmbed } from '@c15t/nextjs';
61
+ ```tsx title="src/consent-embed.tsx"
62
+ import { Frame } from 'c15t/tanstack-start';
63
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
44
64
 
45
- export function ProductVideo() {
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
+ <Frame 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
+ </Frame>
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
+ `Frame` 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 { Frame } from 'c15t/react';
89
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
90
+
91
+ export function ConsentEmbed() {
92
+ return (
93
+ <Frame 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
+ </Frame>
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
+ `Frame` 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 { Frame } from '@c15t/svelte';
227
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
228
+ </script>
229
+
230
+ <Frame 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
+ </Frame>
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 `Frame` 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 { Frame } from '@c15t/svelte';
256
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from '../embed-config';
257
+ </script>
258
+
259
+ <Frame 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
+ </Frame>
271
+ ```
117
272
 
118
- ## Style the wrapper and iframe
273
+ The Svelte `Frame` 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.
159
371
 
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.
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`.
164
375
 
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.
376
+ ## Customize the player and placeholder
169
377
 
170
- ## Verify setup
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.
171
382
 
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.
383
+ React and Svelte `Frame` 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.
180
388
 
181
- See YouTube's [embedded player parameters](https://developers.google.com/youtube/player_parameters)
182
- for the supported query parameters.
389
+ ## Consent behavior
183
390
 
184
- ## Types
391
+ Each example keeps the iframe absent until the category is allowed. After
392
+ permission, the iframe can load. Revoking permission removes the iframe and its
393
+ embedded document. Requests already sent cannot be recalled.
185
394
 
186
- ### YouTubeEmbedProps
395
+ The `youtube-nocookie.com` hostname does not replace the consent boundary. Avoid
396
+ loading a remote thumbnail, preconnecting to YouTube, or installing the IFrame
397
+ Player API separately if you require no YouTube request before permission.
398
+ This recipe embeds a video; it does not load the JavaScript Player API.
187
399
 
188
- |Property|Value|
189
- |:--|:--|
190
- |Type Name|\`YouTubeEmbedProps\`|
191
- |Source Path|\`./packages/react/src/components/integrations/youtube-embed.tsx\`|
400
+ ## Verify the integration
192
401
 
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.\*
402
+ In a fresh opt-in session, confirm no iframe or YouTube request exists. Grant
403
+ marketing permission and play the video, then revoke it and confirm the iframe
404
+ is removed. Test a narrow viewport and keyboard access to the placeholder and
405
+ player. See [consent verification](../guides/verify-consent.md).