@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,263 +1,384 @@
1
1
  ---
2
2
  title: Google Maps
3
- description: Render Google Maps only after consent with one shared Maps
4
- JavaScript API loader and independently managed map instances.
5
- icon: google-maps
3
+ description: Prevent a map iframe from mounting before the required permission.
6
4
  group: integrations
7
5
  ---
8
- `GoogleMap` is a renderable integration for React and Next.js. It keeps the
9
- Google Maps JavaScript API off the page until the configured consent category is
10
- allowed, then loads the SDK once and creates a map for each mounted component.
11
6
 
12
- Unlike the helpers from `@c15t/scripts`, `GoogleMap` owns both the consent-aware
13
- script lifecycle and the visible map container.
7
+ ## Configure the map
14
8
 
15
- > ℹ️ **Info:**
16
- > The browser must receive a Google Maps API key, so the key is visible to site visitors. Keep it out of source control, use a key created for browser use, and restrict it by website and API in Google Cloud.
9
+ Copy the map's iframe embed URL. Choose the category matching its use in your
10
+ application; this example uses functionality permission.
17
11
 
18
- ## Integrate with c15t
12
+ ```ts title="src/embed-config.ts"
13
+ export const embedCategory = 'functionality' as const;
14
+ export const embedURL = 'https://www.google.com/maps/embed?pb=YOUR_EMBED_PARAMETERS';
15
+ export const embedTitle = 'Map showing our office location';
16
+ export const embedAspectRatio = '4 / 3';
17
+ ```
19
18
 
20
- `GoogleMap` must render inside a `ConsentManagerProvider`. Complete the
21
- [React quickstart](/docs/frameworks/react/quickstart) or
22
- [Next.js quickstart](/docs/frameworks/next/quickstart) first.
19
+ Replace the example URL with the complete embed URL for your map. Keep the
20
+ address and a directions link outside the consent-gated embed.
23
21
 
24
- **React**
22
+ ## Add the embed to your framework
25
23
 
26
- ```tsx
27
- import { GoogleMap } from '@c15t/react';
24
+ Create `src/embed-config.ts` using the configuration on this page, then select
25
+ your framework. Keep the existing [consent setup](https://c15t.com/docs/frameworks), its policy
26
+ and preferences UI. These examples do not create a second consent provider.
28
27
 
29
- const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
28
+ **Next.js**
30
29
 
31
- export function StoreMap() {
32
- if (!apiKey) {
33
- return <p>Google Maps is not configured.</p>;
34
- }
30
+ Render this component inside your existing consent boundary or provider.
35
31
 
32
+ ```tsx title="src/consent-embed.tsx"
33
+ 'use client';
34
+
35
+ import { Frame } from 'c15t/next';
36
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
37
+
38
+ export function ConsentEmbed() {
36
39
  return (
37
- <GoogleMap
38
- apiKey={apiKey}
39
- authReferrerPolicy="origin"
40
- center={{ lat: 40.7128, lng: -74.006 }}
41
- consentCategory="measurement"
42
- style={{ height: 400 }}
43
- zoom={12}
44
- />
40
+ <Frame category={embedCategory}>
41
+ <iframe
42
+ src={embedURL}
43
+ title={embedTitle}
44
+ loading="lazy"
45
+ allowFullScreen
46
+ style={{ width: '100%', aspectRatio: embedAspectRatio, minHeight: 200, border: 0 }}
47
+ />
48
+ </Frame>
45
49
  );
46
50
  }
47
51
  ```
48
52
 
49
- **Next.js**
50
-
51
- ```tsx
52
- 'use client';
53
+ `Frame` keeps the iframe absent while permission is denied and removes it
54
+ on revocation. Keep your existing consent styles and preferences dialog.
53
55
 
54
- import { GoogleMap } from '@c15t/nextjs';
56
+ **TanStack Start**
55
57
 
56
- const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
58
+ Render this component inside your existing consent boundary or provider.
57
59
 
58
- export function StoreMap() {
59
- if (!apiKey) {
60
- return <p>Google Maps is not configured.</p>;
61
- }
60
+ ```tsx title="src/consent-embed.tsx"
61
+ import { Frame } from 'c15t/tanstack-start';
62
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
62
63
 
64
+ export function ConsentEmbed() {
63
65
  return (
64
- <GoogleMap
65
- apiKey={apiKey}
66
- authReferrerPolicy="origin"
67
- center={{ lat: 40.7128, lng: -74.006 }}
68
- consentCategory="measurement"
69
- style={{ height: 400 }}
70
- zoom={12}
71
- />
66
+ <Frame category={embedCategory}>
67
+ <iframe
68
+ src={embedURL}
69
+ title={embedTitle}
70
+ loading="lazy"
71
+ allowFullScreen
72
+ style={{ width: '100%', aspectRatio: embedAspectRatio, minHeight: 200, border: 0 }}
73
+ />
74
+ </Frame>
72
75
  );
73
76
  }
74
77
  ```
75
78
 
76
- `measurement` is a common choice for an interactive map, but the correct
77
- category depends on why your application uses Google Maps and the policy shown
78
- to your users.
79
-
80
- ## How c15t loads it
81
-
82
- * **Before consent:** no Maps JavaScript API script is requested and no map
83
- instance is created.
84
- * **After consent:** c15t registers one shared SDK loader and waits for Google's
85
- callback before constructing the map.
86
- * **Multiple maps:** components with the same `scriptId` reuse the SDK while
87
- keeping separate map instances.
88
- * **On revocation or unmount:** the component clears the map instance and its
89
- listeners. The page-level SDK registration is retained after its first load
90
- because Google supports one loader per page.
91
- * **Existing SDK:** if another part of the application already loaded a
92
- compatible Google Maps global, the component adopts it instead of adding a
93
- duplicate script.
94
- * **Visible states:** the blocked, loading, and error states are accessible and
95
- use the active c15t translations by default.
96
-
97
- Keep one `scriptId` and one loader configuration across the page. Override the
98
- default `scriptId` only to coordinate with a known c15t script registration, not
99
- to load a second Maps SDK configuration. If two maps register conflicting
100
- loader options, c15t reports the conflict and tells you to align the key,
101
- language, region, libraries, and other loader options. A different `scriptId`
102
- does not make a second Google loader safe.
103
-
104
- ## Configure the map
105
-
106
- Pass `center`, `zoom`, `mapId`, and `options` as you would when constructing a
107
- Google map:
108
-
109
- ```tsx
110
- <GoogleMap
111
- apiKey={apiKey}
112
- center={{ lat: 51.5072, lng: -0.1276 }}
113
- consentCategory="measurement"
114
- mapId="YOUR_MAP_ID"
115
- options={{
116
- disableDefaultUI: true,
117
- gestureHandling: 'cooperative',
118
- }}
119
- zoom={11}
120
- />
121
- ```
122
-
123
- Changes to `center`, `zoom`, and updateable `options` are applied to the existing
124
- map. Changing `mapId` recreates it because Google treats that value as
125
- construction-time configuration.
126
-
127
- The component has a default height of `320px`. Override `style.height` or apply
128
- a class with an explicit height when your layout needs another size.
129
-
130
- ### Loader options
131
-
132
- The direct Maps JavaScript API loader options are available as component props:
79
+ `Frame` keeps the iframe absent while permission is denied and removes it
80
+ on revocation. Keep your existing consent styles and preferences dialog.
133
81
 
134
- * `libraries`
135
- * `language`
136
- * `region`
137
- * `version`
138
- * `authReferrerPolicy`
139
- * `mapIds`
140
- * `channel`
141
- * `solutionChannel`
142
- * `nonce`
82
+ **React**
143
83
 
144
- `mapIds` preloads map ID configuration; `mapId` selects the map ID for the
145
- component instance.
84
+ Render this component inside your existing consent boundary or provider.
146
85
 
147
- ## Handle loading and errors
86
+ ```tsx title="src/consent-embed.tsx"
87
+ import { Frame } from 'c15t/react';
88
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
148
89
 
149
- The built-in loading and error states use `frame.loading` and `frame.error` from
150
- your c15t messages. The consent placeholder uses the localized consent-type
151
- title in both its message and button—for example, an `Analytics` title produces
152
- “Enable Analytics consent.”
90
+ export function ConsentEmbed() {
91
+ return (
92
+ <Frame category={embedCategory}>
93
+ <iframe
94
+ src={embedURL}
95
+ title={embedTitle}
96
+ loading="lazy"
97
+ allowFullScreen
98
+ style={{ width: '100%', aspectRatio: embedAspectRatio, minHeight: 200, border: 0 }}
99
+ />
100
+ </Frame>
101
+ );
102
+ }
103
+ ```
153
104
 
154
- Use the fallback props when you need integration-specific content and `onError`
155
- for reporting:
105
+ `Frame` keeps the iframe absent while permission is denied and removes it
106
+ on revocation. Keep your existing consent styles and preferences dialog.
107
+
108
+ **Nuxt**
109
+
110
+ Use the reactive snapshot from the existing consent runtime. The Nuxt module auto-imports the consent composables.
111
+
112
+ ```vue title="app/components/ConsentEmbed.vue"
113
+ <script setup lang="ts">
114
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from '../../src/embed-config';
115
+
116
+ const snapshot = useConsentSnapshot();
117
+ const activeUI = useConsentActiveUI();
118
+ </script>
119
+
120
+ <template>
121
+ <iframe
122
+ v-if="snapshot.effectivePermissions[embedCategory]"
123
+ :src="embedURL"
124
+ :title="embedTitle"
125
+ loading="lazy"
126
+ allowfullscreen
127
+ :style="{ width: '100%', aspectRatio: embedAspectRatio, minHeight: '200px', border: 0 }"
128
+ />
129
+ <button v-else type="button" @click="activeUI = 'manager'">
130
+ Open privacy settings to view this content
131
+ </button>
132
+ </template>
133
+ ```
156
134
 
157
- ```tsx
158
- import { Frame, GoogleMap } from '@c15t/react';
135
+ Use `v-if` so a denied iframe is removed from the DOM. Hiding an existing
136
+ iframe with `v-show` or CSS does not prevent its requests.
137
+
138
+ **Vue**
139
+
140
+ Use the reactive snapshot from the existing consent runtime. The c15t Vue plugin must already be installed on this app.
141
+
142
+ ```vue title="src/ConsentEmbed.vue"
143
+ <script setup lang="ts">
144
+ import { useConsentSnapshot, useConsentActiveUI } from 'c15t/vue/vue-plugin';
145
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
146
+
147
+ const snapshot = useConsentSnapshot();
148
+ const activeUI = useConsentActiveUI();
149
+ </script>
150
+
151
+ <template>
152
+ <iframe
153
+ v-if="snapshot.effectivePermissions[embedCategory]"
154
+ :src="embedURL"
155
+ :title="embedTitle"
156
+ loading="lazy"
157
+ allowfullscreen
158
+ :style="{ width: '100%', aspectRatio: embedAspectRatio, minHeight: '200px', border: 0 }"
159
+ />
160
+ <button v-else type="button" @click="activeUI = 'manager'">
161
+ Open privacy settings to view this content
162
+ </button>
163
+ </template>
164
+ ```
159
165
 
160
- function reportMapError(error: Error) {
161
- // Send the error to your observability provider.
162
- }
166
+ Use `v-if` so a denied iframe is removed from the DOM. Hiding an existing
167
+ iframe with `v-show` or CSS does not prevent its requests.
168
+
169
+ **Astro**
170
+
171
+ Use the shared browser helper below with Astro's existing page runtime.
172
+ Add this component to pages using your consent-enabled base layout:
173
+
174
+ ```astro title="src/components/ConsentEmbed.astro"
175
+ <c15t-consent-embed style="display: block"></c15t-consent-embed>
176
+
177
+ <script>
178
+ import { getConsentClient } from '@c15t/astro/client';
179
+ import { mountConsentEmbed } from '../consent-embed';
180
+
181
+ class ConsentEmbed extends HTMLElement {
182
+ dispose?: () => void;
183
+
184
+ connect = () => {
185
+ if (this.dispose) return;
186
+ const client = getConsentClient();
187
+ if (!client) return;
188
+ this.dispose = mountConsentEmbed(
189
+ this,
190
+ client.runtime.kernel,
191
+ () => { void client.openDialog(); },
192
+ );
193
+ };
194
+
195
+ connectedCallback() {
196
+ document.addEventListener('DOMContentLoaded', this.connect, { once: true });
197
+ this.connect();
198
+ }
199
+
200
+ disconnectedCallback() {
201
+ document.removeEventListener('DOMContentLoaded', this.connect);
202
+ this.dispose?.();
203
+ this.dispose = undefined;
204
+ }
205
+ }
163
206
 
164
- <GoogleMap
165
- apiKey={apiKey}
166
- center={{ lat: 40.7128, lng: -74.006 }}
167
- consentCategory="measurement"
168
- loadingFallback={<p>Loading map…</p>}
169
- errorFallback={<p>The map could not be loaded.</p>}
170
- onError={reportMapError}
171
- placeholder={
172
- <Frame.Root>
173
- <Frame.Title>Allow measurement consent to view this map.</Frame.Title>
174
- <Frame.Button category="measurement" />
175
- </Frame.Root>
207
+ if (!customElements.get('c15t-consent-embed')) {
208
+ customElements.define('c15t-consent-embed', ConsentEmbed);
176
209
  }
177
- />
210
+ </script>
178
211
  ```
179
212
 
180
- `onError` covers loader failures, timeouts, map-constructor failures, and
181
- Google's global authentication failure callback. Authentication failures
182
- usually indicate an invalid key, missing billing, a disabled API, or a referrer
183
- that is not allowed.
184
-
185
- If you provide a custom `placeholder`, include `Frame.Button` or another way to
186
- reopen consent preferences or grant the required category.
187
-
188
- ### Retry a failed map
213
+ The first mount waits for the page's module scripts, including c15t's boot
214
+ script. Later `ClientRouter` mounts reuse the existing runtime. Removing the
215
+ component unsubscribes and removes the iframe. Do not create another consent
216
+ runtime for this embed.
217
+
218
+ **Svelte**
219
+
220
+ Render this component inside the existing `ConsentManagerProvider`.
221
+ The provider from your quickstart supplies its consent state.
222
+
223
+ ```svelte title="src/ConsentEmbed.svelte"
224
+ <script lang="ts">
225
+ import { Frame } from '@c15t/svelte';
226
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
227
+ </script>
228
+
229
+ <Frame category={embedCategory}>
230
+ <iframe
231
+ src={embedURL}
232
+ title={embedTitle}
233
+ loading="lazy"
234
+ allowfullscreen
235
+ style:width="100%"
236
+ style:aspect-ratio={embedAspectRatio}
237
+ style:min-height="200px"
238
+ style:border="0"
239
+ ></iframe>
240
+ </Frame>
241
+ ```
189
242
 
190
- Increment `retryKey` to retry the same map after a loader, authentication, or
191
- constructor failure:
243
+ The Svelte `Frame` waits until the browser is mounted and the category is
244
+ allowed. Its default placeholder opens preferences. Revocation removes the
245
+ iframe.
246
+
247
+ **SvelteKit**
248
+
249
+ Render this component inside the existing `ConsentManagerProvider`.
250
+ Keep the SvelteKit root provider and its server prefetch unchanged.
251
+
252
+ ```svelte title="src/lib/ConsentEmbed.svelte"
253
+ <script lang="ts">
254
+ import { Frame } from '@c15t/svelte';
255
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from '../embed-config';
256
+ </script>
257
+
258
+ <Frame category={embedCategory}>
259
+ <iframe
260
+ src={embedURL}
261
+ title={embedTitle}
262
+ loading="lazy"
263
+ allowfullscreen
264
+ style:width="100%"
265
+ style:aspect-ratio={embedAspectRatio}
266
+ style:min-height="200px"
267
+ style:border="0"
268
+ ></iframe>
269
+ </Frame>
270
+ ```
192
271
 
193
- ```tsx
194
- import { GoogleMap } from '@c15t/react';
195
- import { useState } from 'react';
272
+ The Svelte `Frame` waits until the browser is mounted and the category is
273
+ allowed. Its default placeholder opens preferences. Revocation removes the
274
+ iframe.
196
275
 
197
- function RetryableMap({ apiKey }: { apiKey: string }) {
198
- const [retryKey, setRetryKey] = useState(0);
276
+ **JavaScript**
199
277
 
200
- return (
201
- <>
202
- <GoogleMap
203
- apiKey={apiKey}
204
- center={{ lat: 40.7128, lng: -74.006 }}
205
- retryKey={retryKey}
206
- />
278
+ Use the shared browser helper below with your existing kernel. Put an
279
+ empty container where the embed should appear:
207
280
 
208
- <button type="button" onClick={() => setRetryKey((key) => key + 1)}>
209
- Retry map
210
- </button>
211
- </>
212
- );
213
- }
281
+ ```html
282
+ <div id="consent-embed"></div>
214
283
  ```
215
284
 
216
- Changing `retryKey` clears the failed attempt and retries with the same
217
- `scriptId`. A successful page-level SDK registration remains shared and is not
218
- loaded again.
285
+ In your browser entry point, after creating the kernel:
219
286
 
220
- ## Secure the browser key
287
+ ```ts
288
+ import { mountConsentEmbed } from './consent-embed';
221
289
 
222
- For production:
290
+ const container = document.querySelector<HTMLElement>('#consent-embed');
291
+ if (!container) throw new Error('Missing consent embed container');
223
292
 
224
- 1. Create a key specifically for the browser application.
225
- 2. Apply a **Websites** application restriction for every allowed development
226
- and production origin.
227
- 3. Restrict the key to the **Maps JavaScript API** and any additional APIs used
228
- by requested libraries.
229
- 4. Keep the value in an untracked environment file and expose it through the
230
- browser environment variable required by your framework.
231
- 5. Monitor key usage and rotate the key if you see unexpected traffic.
293
+ const disposeEmbed = mountConsentEmbed(container, kernel, openPreferences);
294
+ ```
232
295
 
233
- When `authReferrerPolicy="origin"` is set, configure origin-level website
234
- restrictions without URL paths.
296
+ `kernel` is the instance from your quickstart. `openPreferences` is your
297
+ application's function for showing its consent preferences UI. Call
298
+ `disposeEmbed()` when the page or component is destroyed. The helper observes
299
+ both the current snapshot and later changes.
300
+
301
+ ## Browser helper for Astro and JavaScript
302
+
303
+ Only the Astro and JavaScript examples need this helper. It creates the iframe
304
+ when permission allows it, keeps an existing player mounted across unrelated
305
+ snapshot updates, and removes it on revocation.
306
+
307
+ ```ts title="src/consent-embed.ts"
308
+ import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
309
+
310
+ type EmbedKernel = {
311
+ getSnapshot: () => {
312
+ effectivePermissions: Record<typeof embedCategory, boolean>;
313
+ };
314
+ subscribe: (listener: () => void) => () => void;
315
+ };
316
+
317
+ export function mountConsentEmbed(
318
+ container: HTMLElement,
319
+ kernel: EmbedKernel,
320
+ openPreferences: () => void,
321
+ ) {
322
+ const render = () => {
323
+ if (kernel.getSnapshot().effectivePermissions[embedCategory]) {
324
+ if (container.querySelector('iframe')) return;
325
+ const frame = document.createElement('iframe');
326
+ frame.src = embedURL;
327
+ frame.title = embedTitle;
328
+ frame.loading = 'lazy';
329
+ frame.allowFullscreen = true;
330
+ Object.assign(frame.style, {
331
+ width: '100%',
332
+ aspectRatio: embedAspectRatio,
333
+ minHeight: '200px',
334
+ border: '0',
335
+ });
336
+ container.replaceChildren(frame);
337
+ } else {
338
+ if (container.querySelector('button')) return;
339
+ const button = document.createElement('button');
340
+ button.type = 'button';
341
+ button.textContent = 'Open privacy settings to view this content';
342
+ button.onclick = openPreferences;
343
+ container.replaceChildren(button);
344
+ }
345
+ };
346
+
347
+ render();
348
+ const unsubscribe = kernel.subscribe(render);
349
+ return () => {
350
+ unsubscribe();
351
+ container.replaceChildren();
352
+ };
353
+ }
354
+ ```
235
355
 
236
- * [Google Maps Platform security guidance](https://developers.google.com/maps/api-security-best-practices)
237
- * [Maps JavaScript API loader options](https://developers.google.com/maps/documentation/javascript/load-maps-js-api)
238
- * [Maps Demo Key for local prototypes](https://developers.google.com/maps/documentation/javascript/demo-key)
356
+ Keep a transcript, address or other useful alternative outside the embed.
357
+ `loading="lazy"` is a performance hint; the consent condition controls whether
358
+ the iframe exists at all. A denied category may be fixed by policy, so opening
359
+ preferences does not guarantee that the visitor can grant it.
239
360
 
240
- ## Verify setup
361
+ ## Gate existing iframe markup
241
362
 
242
- 1. Clear saved consent and reload the page.
243
- 2. Confirm no request to `maps.googleapis.com/maps/api/js` occurs before the
244
- configured category is allowed.
245
- 3. Grant consent and confirm the map reaches its ready state.
246
- 4. Render two maps with the same `scriptId` and confirm only one loader script is
247
- added.
248
- 5. Revoke consent and confirm each map instance is removed.
249
- 6. Test a rejected key or referrer and confirm `errorFallback` and `onError`
250
- receive the failure.
251
- 7. Fix the rejected configuration, change `retryKey`, and confirm the map can
252
- recover without changing `scriptId`.
363
+ For markup that uses `data-category` and `data-src`, the
364
+ `createIframeBlocker` module from `c15t/modules/iframe-blocker` promotes
365
+ `data-src` only when the category has consent and the URL resolves to HTTP or
366
+ HTTPS. Relative URLs resolve against the iframe document's base URL.
367
+ Malformed URLs and other schemes, including `javascript:`, `data:` and `blob:`,
368
+ remain in `data-src` without loading. Correct the URL and call
369
+ `processAllIframes()` to retry.
253
370
 
254
- ## Types
371
+ Keep the initial URL in `data-src`, without `src`, so the browser cannot load
372
+ it before the blocker runs. The module does not sanitize arbitrary HTML or
373
+ modify iframes without `data-category`.
255
374
 
256
- ### GoogleMapProps
375
+ ## Verify before and after permission
257
376
 
258
- |Property|Value|
259
- |:--|:--|
260
- |Type Name|\`GoogleMapProps\`|
261
- |Source Path|\`./packages/react/src/components/integrations/google-map.tsx\`|
377
+ On a fresh opt-in session, inspect Network and confirm the iframe has not
378
+ requested the map. Grant the category and confirm the map mounts. Revoke it and
379
+ check the frame returns to its blocked state. Keep useful fallback information,
380
+ such as the address and directions link, available outside the map.
262
381
 
263
- \*ExtractedTypeTable: Could not extract "GoogleMapProps" from "./packages/react/src/components/integrations/google-map.tsx" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
382
+ For a JavaScript Maps SDK instead of an iframe, register a category-gated script
383
+ and manage the widget's cleanup. Do not use this iframe recipe as evidence that
384
+ an independently loaded Maps SDK is blocked.