@c15t/scripts 3.0.0-alpha.1 → 3.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +129 -59
- package/README.md +8 -29
- package/SKILL.md +33 -0
- package/dist/adobe-analytics.js +2 -0
- package/dist/ahrefs-analytics.js +2 -0
- package/dist/amplitude.js +2 -0
- package/dist/clearbit.js +2 -0
- package/dist/cloudflare-web-analytics.js +2 -0
- package/dist/cloudflare-zaraz.js +2 -0
- package/dist/crisp.js +2 -0
- package/dist/databuddy.js +2 -0
- package/dist/e2e-test-utils.js +2 -137
- package/dist/engine/compile.js +2 -89
- package/dist/engine/runtime.js +2 -448
- package/dist/events.js +2 -0
- package/dist/fathom-analytics.js +2 -0
- package/dist/front-chat.js +2 -0
- package/dist/google-tag-manager.js +2 -0
- package/dist/google-tag.js +2 -0
- package/dist/heap.js +2 -0
- package/dist/hightouch.js +2 -0
- package/dist/hotjar.js +2 -0
- package/dist/intercom.js +2 -0
- package/dist/klaviyo.js +2 -0
- package/dist/linkedin-insights.js +2 -0
- package/dist/logrocket.js +2 -0
- package/dist/matomo-analytics.js +2 -0
- package/dist/meta-pixel.js +2 -0
- package/dist/microsoft-clarity.js +2 -0
- package/dist/microsoft-uet.js +2 -0
- package/dist/mixpanel-analytics.js +2 -0
- package/dist/one-dollar-stats.js +2 -0
- package/dist/openai-pixel.js +2 -0
- package/dist/pinterest-tag.js +2 -0
- package/dist/pirsch.js +2 -0
- package/dist/plausible-analytics.js +2 -0
- package/dist/posthog.js +2 -0
- package/dist/promptwatch.js +2 -0
- package/dist/reddit-pixel.js +2 -0
- package/dist/registry.js +2 -392
- package/dist/resolve.js +2 -33
- package/dist/rudderstack.js +2 -0
- package/dist/rybbit-analytics.js +2 -0
- package/dist/segment.js +2 -0
- package/dist/snapchat-pixel.js +2 -0
- package/dist/tiktok-pixel.js +2 -0
- package/dist/types.js +2 -16
- package/dist/umami-analytics.js +2 -0
- package/dist/vendors/_shared/attributes.js +2 -14
- package/dist/vendors/_shared/google-consent.js +2 -27
- package/dist/vendors/_shared/install-builders.js +2 -21
- package/dist/vendors/_shared/required-id.js +2 -0
- package/dist/vendors/_shared/script-url.js +2 -28
- package/dist/vendors/ads-and-pixels/linkedin-insights.js +2 -48
- package/dist/vendors/ads-and-pixels/meta-pixel.js +2 -153
- package/dist/vendors/ads-and-pixels/microsoft-uet.js +2 -110
- package/dist/vendors/ads-and-pixels/openai-pixel.js +2 -88
- package/dist/vendors/ads-and-pixels/pinterest-tag.js +2 -0
- package/dist/vendors/ads-and-pixels/reddit-pixel.js +2 -107
- package/dist/vendors/ads-and-pixels/snapchat-pixel.js +2 -87
- package/dist/vendors/ads-and-pixels/tiktok-pixel.js +2 -89
- package/dist/vendors/ads-and-pixels/x-pixel.js +2 -48
- package/dist/vendors/analytics/adobe-analytics.js +2 -49
- package/dist/vendors/analytics/ahrefs-analytics.js +2 -27
- package/dist/vendors/analytics/amplitude.js +2 -134
- package/dist/vendors/analytics/clearbit.js +2 -28
- package/dist/vendors/analytics/cloudflare-web-analytics.js +2 -32
- package/dist/vendors/analytics/databuddy.js +2 -103
- package/dist/vendors/analytics/fathom-analytics.js +2 -35
- package/dist/vendors/analytics/google-tag.js +2 -66
- package/dist/vendors/analytics/heap.js +2 -134
- package/dist/vendors/analytics/hightouch.js +2 -109
- package/dist/vendors/analytics/hotjar.js +2 -44
- package/dist/vendors/analytics/logrocket.js +2 -58
- package/dist/vendors/analytics/matomo-analytics.js +2 -191
- package/dist/vendors/analytics/microsoft-clarity.js +2 -100
- package/dist/vendors/analytics/mixpanel-analytics.js +2 -93
- package/dist/vendors/analytics/one-dollar-stats.js +2 -0
- package/dist/vendors/analytics/pirsch.js +2 -67
- package/dist/vendors/analytics/plausible-analytics.js +2 -81
- package/dist/vendors/analytics/posthog.js +2 -200
- package/dist/vendors/analytics/promptwatch.js +2 -29
- package/dist/vendors/analytics/rudderstack.js +2 -183
- package/dist/vendors/analytics/rybbit-analytics.js +2 -63
- package/dist/vendors/analytics/segment.js +2 -56
- package/dist/vendors/analytics/umami-analytics.js +2 -39
- package/dist/vendors/analytics/vercel-analytics.js +2 -53
- package/dist/vendors/email-and-sms/klaviyo.js +2 -0
- package/dist/vendors/functional/crisp.js +2 -100
- package/dist/vendors/functional/front-chat.js +2 -0
- package/dist/vendors/functional/intercom.js +2 -45
- package/dist/vendors/tag-managers/cloudflare-zaraz.js +2 -98
- package/dist/vendors/tag-managers/google-tag-manager.js +2 -59
- package/dist/vercel-analytics.js +2 -0
- package/dist/x-pixel.js +2 -0
- package/dist-types/adobe-analytics.d.ts +2 -0
- package/dist-types/ahrefs-analytics.d.ts +2 -0
- package/dist-types/amplitude.d.ts +2 -0
- package/dist-types/clearbit.d.ts +2 -0
- package/dist-types/cloudflare-web-analytics.d.ts +2 -0
- package/dist-types/cloudflare-zaraz.d.ts +2 -0
- package/dist-types/crisp.d.ts +2 -0
- package/dist-types/databuddy.d.ts +2 -0
- package/dist-types/e2e-test-utils.d.ts +2 -0
- package/dist-types/engine/compile.d.ts +2 -3
- package/dist-types/engine/runtime.d.ts +2 -3
- package/dist-types/events.d.ts +2 -0
- package/dist-types/fathom-analytics.d.ts +2 -0
- package/dist-types/front-chat.d.ts +2 -0
- package/dist-types/google-tag-manager.d.ts +2 -0
- package/dist-types/google-tag.d.ts +2 -0
- package/dist-types/heap.d.ts +2 -0
- package/dist-types/hightouch.d.ts +2 -0
- package/dist-types/hotjar.d.ts +2 -0
- package/dist-types/intercom.d.ts +2 -0
- package/dist-types/klaviyo.d.ts +2 -0
- package/dist-types/linkedin-insights.d.ts +2 -0
- package/dist-types/logrocket.d.ts +2 -0
- package/dist-types/matomo-analytics.d.ts +2 -0
- package/dist-types/meta-pixel.d.ts +2 -0
- package/dist-types/microsoft-clarity.d.ts +2 -0
- package/dist-types/microsoft-uet.d.ts +2 -0
- package/dist-types/mixpanel-analytics.d.ts +2 -0
- package/dist-types/one-dollar-stats.d.ts +2 -0
- package/dist-types/openai-pixel.d.ts +2 -0
- package/dist-types/pinterest-tag.d.ts +2 -0
- package/dist-types/pirsch.d.ts +2 -0
- package/dist-types/plausible-analytics.d.ts +2 -0
- package/dist-types/posthog.d.ts +2 -0
- package/dist-types/promptwatch.d.ts +2 -0
- package/dist-types/reddit-pixel.d.ts +2 -0
- package/dist-types/registry.d.ts +2 -458
- package/dist-types/resolve.d.ts +2 -9
- package/dist-types/rudderstack.d.ts +2 -0
- package/dist-types/rybbit-analytics.d.ts +2 -0
- package/dist-types/segment.d.ts +2 -0
- package/dist-types/snapchat-pixel.d.ts +2 -0
- package/dist-types/tiktok-pixel.d.ts +2 -0
- package/dist-types/types.d.ts +2 -314
- package/dist-types/umami-analytics.d.ts +2 -0
- package/dist-types/vendors/_shared/attributes.d.ts +2 -35
- package/dist-types/vendors/_shared/google-consent.d.ts +2 -47
- package/dist-types/vendors/_shared/install-builders.d.ts +2 -30
- package/dist-types/vendors/_shared/required-id.d.ts +2 -0
- package/dist-types/vendors/_shared/script-url.d.ts +2 -75
- package/dist-types/vendors/ads-and-pixels/linkedin-insights.d.ts +2 -92
- package/dist-types/vendors/ads-and-pixels/meta-pixel.d.ts +2 -289
- package/dist-types/vendors/ads-and-pixels/microsoft-uet.d.ts +2 -105
- package/dist-types/vendors/ads-and-pixels/openai-pixel.d.ts +2 -211
- package/dist-types/vendors/ads-and-pixels/pinterest-tag.d.ts +2 -0
- package/dist-types/vendors/ads-and-pixels/reddit-pixel.d.ts +2 -210
- package/dist-types/vendors/ads-and-pixels/snapchat-pixel.d.ts +2 -171
- package/dist-types/vendors/ads-and-pixels/tiktok-pixel.d.ts +2 -106
- package/dist-types/vendors/ads-and-pixels/x-pixel.d.ts +2 -183
- package/dist-types/vendors/analytics/adobe-analytics.d.ts +2 -75
- package/dist-types/vendors/analytics/ahrefs-analytics.d.ts +2 -62
- package/dist-types/vendors/analytics/amplitude.d.ts +2 -234
- package/dist-types/vendors/analytics/clearbit.d.ts +2 -60
- package/dist-types/vendors/analytics/cloudflare-web-analytics.d.ts +2 -67
- package/dist-types/vendors/analytics/databuddy.d.ts +2 -147
- package/dist-types/vendors/analytics/fathom-analytics.d.ts +2 -90
- package/dist-types/vendors/analytics/google-tag.d.ts +2 -93
- package/dist-types/vendors/analytics/heap.d.ts +2 -316
- package/dist-types/vendors/analytics/hightouch.d.ts +2 -285
- package/dist-types/vendors/analytics/hotjar.d.ts +2 -73
- package/dist-types/vendors/analytics/logrocket.d.ts +2 -101
- package/dist-types/vendors/analytics/matomo-analytics.d.ts +2 -41
- package/dist-types/vendors/analytics/microsoft-clarity.d.ts +2 -97
- package/dist-types/vendors/analytics/mixpanel-analytics.d.ts +2 -113
- package/dist-types/vendors/analytics/one-dollar-stats.d.ts +2 -0
- package/dist-types/vendors/analytics/pirsch.d.ts +2 -96
- package/dist-types/vendors/analytics/plausible-analytics.d.ts +2 -122
- package/dist-types/vendors/analytics/posthog.d.ts +2 -175
- package/dist-types/vendors/analytics/promptwatch.d.ts +2 -36
- package/dist-types/vendors/analytics/rudderstack.d.ts +2 -330
- package/dist-types/vendors/analytics/rybbit-analytics.d.ts +2 -82
- package/dist-types/vendors/analytics/segment.d.ts +2 -158
- package/dist-types/vendors/analytics/umami-analytics.d.ts +2 -93
- package/dist-types/vendors/analytics/vercel-analytics.d.ts +2 -66
- package/dist-types/vendors/email-and-sms/klaviyo.d.ts +2 -0
- package/dist-types/vendors/functional/crisp.d.ts +2 -78
- package/dist-types/vendors/functional/front-chat.d.ts +2 -0
- package/dist-types/vendors/functional/intercom.d.ts +2 -135
- package/dist-types/vendors/tag-managers/cloudflare-zaraz.d.ts +2 -39
- package/dist-types/vendors/tag-managers/google-tag-manager.d.ts +2 -94
- package/dist-types/vercel-analytics.d.ts +2 -0
- package/dist-types/x-pixel.d.ts +2 -0
- package/docs/README.md +129 -59
- package/docs/assets/v3/bottom-bar.png +0 -0
- package/docs/assets/v3/brand-card.png +0 -0
- package/docs/assets/v3/brand-preferences.png +0 -0
- package/docs/assets/v3/choice-wall.png +0 -0
- package/docs/assets/v3/headless-bar-html.png +0 -0
- package/docs/assets/v3/headless-bar-mobile.png +0 -0
- package/docs/assets/v3/headless-bar.png +0 -0
- package/docs/assets/v3/slim-bar.png +0 -0
- package/docs/concepts/choose-your-setup.md +87 -0
- package/docs/concepts/consent-categories.md +84 -0
- package/docs/concepts/consent-state.md +357 -0
- package/docs/{guides → concepts}/data-fetching.md +31 -27
- package/docs/concepts/how-consent-works.md +123 -0
- package/docs/concepts/policies.md +71 -0
- package/docs/customization/class-names.md +202 -0
- package/docs/customization/dark-mode.md +157 -0
- package/docs/customization/motion.md +119 -0
- package/docs/customization/overview.md +67 -33
- package/docs/customization/recipes.md +839 -47
- package/docs/customization/slots.md +216 -35
- package/docs/customization/stylesheets.md +147 -0
- package/docs/customization/tailwind.md +842 -0
- package/docs/customization/tokens.md +166 -36
- package/docs/customization/translations.md +61 -3
- package/docs/frameworks/astro/embeds.md +160 -0
- package/docs/frameworks/astro/network-blocker.md +86 -0
- package/docs/frameworks/astro/scripts.md +146 -0
- package/docs/frameworks/html/embeds.md +142 -0
- package/docs/frameworks/html/network-blocker.md +105 -0
- package/docs/frameworks/html/scripts.md +164 -0
- package/docs/frameworks/javascript/scripts.md +119 -0
- package/docs/frameworks/next/embeds.md +90 -0
- package/docs/frameworks/next/network-blocker.md +153 -0
- package/docs/frameworks/next/scripts.md +196 -0
- package/docs/frameworks/nuxt/embeds.md +81 -0
- package/docs/frameworks/nuxt/network-blocker.md +97 -0
- package/docs/frameworks/nuxt/scripts.md +89 -0
- package/docs/frameworks/react/embeds.md +89 -0
- package/docs/frameworks/react/network-blocker.md +140 -0
- package/docs/frameworks/react/scripts.md +115 -0
- package/docs/frameworks/svelte/embeds.md +96 -0
- package/docs/frameworks/svelte/network-blocker.md +141 -0
- package/docs/frameworks/svelte/scripts.md +137 -0
- package/docs/frameworks/sveltekit/embeds.md +103 -0
- package/docs/frameworks/sveltekit/network-blocker.md +149 -0
- package/docs/frameworks/sveltekit/scripts.md +141 -0
- package/docs/frameworks/tanstack-start/embeds.md +96 -0
- package/docs/frameworks/tanstack-start/network-blocker.md +145 -0
- package/docs/frameworks/tanstack-start/scripts.md +103 -0
- package/docs/frameworks/vue/embeds.md +84 -0
- package/docs/frameworks/vue/network-blocker.md +99 -0
- package/docs/frameworks/vue/scripts.md +93 -0
- package/docs/guides/banner-experiments.md +654 -0
- package/docs/guides/troubleshooting.md +120 -47
- package/docs/guides/verify-consent.md +81 -49
- package/docs/integrations/adobe-analytics.md +168 -160
- package/docs/integrations/ahrefs-analytics.md +153 -155
- package/docs/integrations/amplitude.md +163 -157
- package/docs/integrations/building-integrations.md +136 -37
- package/docs/integrations/clearbit.md +155 -155
- package/docs/integrations/cloudflare-web-analytics.md +157 -157
- package/docs/integrations/cloudflare-zaraz.md +210 -262
- package/docs/integrations/crisp.md +165 -159
- package/docs/integrations/databuddy.md +158 -174
- package/docs/integrations/fathom-analytics.md +159 -157
- package/docs/integrations/front-chat.md +322 -0
- package/docs/integrations/google-maps.md +119 -84
- package/docs/integrations/google-tag-manager.md +179 -164
- package/docs/integrations/google-tag.md +164 -161
- package/docs/integrations/heap.md +164 -156
- package/docs/integrations/hightouch.md +162 -158
- package/docs/integrations/hotjar.md +160 -156
- package/docs/integrations/intercom.md +184 -154
- package/docs/integrations/klaviyo.md +486 -0
- package/docs/integrations/linkedin-insights.md +175 -151
- package/docs/integrations/logrocket.md +161 -157
- package/docs/integrations/matomo-analytics.md +189 -179
- package/docs/integrations/meta-pixel.md +189 -151
- package/docs/integrations/microsoft-clarity.md +164 -156
- package/docs/integrations/microsoft-uet.md +149 -155
- package/docs/integrations/mixpanel-analytics.md +156 -161
- package/docs/integrations/one-dollar-stats.md +306 -0
- package/docs/integrations/openai-pixel.md +205 -302
- package/docs/integrations/overview.md +143 -83
- package/docs/integrations/pinterest-tag.md +329 -0
- package/docs/integrations/pirsch.md +170 -160
- package/docs/integrations/plausible-analytics.md +173 -159
- package/docs/integrations/posthog.md +227 -242
- package/docs/integrations/promptwatch.md +155 -155
- package/docs/integrations/reddit-pixel.md +186 -158
- package/docs/integrations/rudderstack.md +202 -187
- package/docs/integrations/rybbit-analytics.md +172 -161
- package/docs/integrations/segment.md +183 -155
- package/docs/integrations/snapchat-pixel.md +187 -157
- package/docs/integrations/tiktok-pixel.md +172 -151
- package/docs/integrations/umami-analytics.md +164 -159
- package/docs/integrations/vercel-analytics.md +167 -158
- package/docs/integrations/x-pixel.md +177 -151
- package/docs/integrations/youtube.md +122 -87
- package/docs/upgrade-v3.md +490 -354
- package/package.json +12 -236
- package/dist-types/__tests__/helpers.d.ts +0 -141
- package/docs/assets/v3/brand-bar.png +0 -0
- package/docs/assets/v3/mobile-card.png +0 -0
- package/docs/assets/v3/preferences.png +0 -0
- package/docs/frameworks/javascript/script-loader.md +0 -94
- package/docs/frameworks/next/script-loader.md +0 -210
- package/docs/frameworks/react/script-loader.md +0 -63
- package/docs/guides/consent-state.md +0 -60
- package/docs/guides/deployment-modes.md +0 -75
- package/docs/integrations/clear-on-revocation.md +0 -167
- package/docs/integrations/granular-consent.md +0 -208
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Network blocker
|
|
3
|
+
description: Hold fetch and XMLHttpRequest calls in a TanStack Start app until
|
|
4
|
+
their consent category is allowed, with networkBlocker rules on ConsentRoot.
|
|
5
|
+
group: frameworks
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Add rules
|
|
9
|
+
|
|
10
|
+
Pass `networkBlocker` rules to `ConsentRoot` to stop `fetch` and
|
|
11
|
+
`XMLHttpRequest` calls to a domain until its category is allowed. Use it as a
|
|
12
|
+
backstop for beacons an SDK sends itself. Load vendor SDKs through
|
|
13
|
+
[`scripts`](./scripts.md) first, so they do not run
|
|
14
|
+
at all before consent.
|
|
15
|
+
|
|
16
|
+
```tsx title="src/routes/__root.tsx"
|
|
17
|
+
const networkBlocker = {
|
|
18
|
+
rules: [
|
|
19
|
+
{
|
|
20
|
+
id: 'google-analytics',
|
|
21
|
+
domain: 'google-analytics.com',
|
|
22
|
+
category: 'measurement',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'meta-pixel',
|
|
26
|
+
domain: 'facebook.com',
|
|
27
|
+
pathIncludes: '/tr',
|
|
28
|
+
category: 'marketing',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
<ConsentRoot
|
|
34
|
+
state={consent}
|
|
35
|
+
backendURL={backendURL}
|
|
36
|
+
initRoute={false}
|
|
37
|
+
scripts={scripts}
|
|
38
|
+
networkBlocker={networkBlocker}
|
|
39
|
+
>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The blocker runs in the browser only. It does not see requests your server
|
|
43
|
+
functions or server routes make.
|
|
44
|
+
|
|
45
|
+
## Match requests with rules
|
|
46
|
+
|
|
47
|
+
Each rule names a `domain` and the consent `category` a request needs. The
|
|
48
|
+
domain also matches its subdomains: `google-analytics.com` covers
|
|
49
|
+
`www.google-analytics.com`. `pathIncludes` narrows the rule to paths that
|
|
50
|
+
contain a substring, and `methods` narrows it to HTTP methods. A request is
|
|
51
|
+
blocked when a matching rule's condition is not met by the visitor's
|
|
52
|
+
effective permissions.
|
|
53
|
+
|
|
54
|
+
`category` takes the same conditions as scripts:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
{ category: 'measurement' }
|
|
58
|
+
{ category: { and: ['measurement', 'marketing'] } }
|
|
59
|
+
{ category: { or: ['measurement', 'marketing'] } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Add `vendor` to also block the request while the visitor has turned that
|
|
63
|
+
vendor off. IAB TCF rules use `vendorId` and the `iab*` purpose fields
|
|
64
|
+
instead.
|
|
65
|
+
|
|
66
|
+
| Option | Default | Purpose |
|
|
67
|
+
| -------------------- | -------- | ------------------------------------------------------------ |
|
|
68
|
+
| `rules` | required | Rules described above |
|
|
69
|
+
| `enabled` | `true` | Set `false` to keep the rules but stop blocking |
|
|
70
|
+
| `logBlockedRequests` | `true` | Log each blocked request with `console.warn` |
|
|
71
|
+
| `onRequestBlocked` | none | Called with `{ method, url, rule }` for each blocked request |
|
|
72
|
+
|
|
73
|
+
## What a blocked request looks like
|
|
74
|
+
|
|
75
|
+
The blocker wraps `window.fetch` and `XMLHttpRequest`. A blocked `fetch`
|
|
76
|
+
resolves to a `451` response with the status text
|
|
77
|
+
`Request blocked by consent`, and nothing is sent. A blocked XHR is aborted
|
|
78
|
+
and fires an `error` event. Requests that match no rule are not delayed.
|
|
79
|
+
|
|
80
|
+
## When blocking starts
|
|
81
|
+
|
|
82
|
+
The provider holds matching requests from its first render in the browser,
|
|
83
|
+
before any of its children render or run effects. That covers requests from
|
|
84
|
+
child components, including their mount effects, and from effects in
|
|
85
|
+
components rendered next to the provider. The blocker module itself loads
|
|
86
|
+
after mount and decides each held request. Apps without `networkBlocker` do
|
|
87
|
+
not download it.
|
|
88
|
+
|
|
89
|
+
The standalone `useNetworkBlocker` hook works the same way from the first
|
|
90
|
+
render of the component that calls it. That render patches `fetch` and
|
|
91
|
+
`XMLHttpRequest`. If React throws the render away and never commits it, the
|
|
92
|
+
hold ends after 10 seconds. Nothing checked consent for the requests it held,
|
|
93
|
+
so they fail the way the blocker fails a blocked request: a 451 response for
|
|
94
|
+
`fetch`, a failed XHR. The same happens when the component unmounts before
|
|
95
|
+
the blocker loads.
|
|
96
|
+
|
|
97
|
+
While consent is unknown, a matching request that would be blocked waits
|
|
98
|
+
instead of failing. Consent is unknown until the policy has loaded, which is
|
|
99
|
+
also when a returning visitor's stored choice takes effect. The request is
|
|
100
|
+
then sent if the choice allows it and blocked otherwise. If the policy fails
|
|
101
|
+
to load, optional categories stay denied and waiting requests are blocked.
|
|
102
|
+
If the policy request never finishes, they keep waiting and are never sent.
|
|
103
|
+
|
|
104
|
+
A synchronous XHR cannot wait. Before the blocker module has loaded, a
|
|
105
|
+
matching one throws a `NetworkError` from `send()`. After that, one that
|
|
106
|
+
consent does not allow yet is blocked.
|
|
107
|
+
|
|
108
|
+
## What the network blocker cannot stop
|
|
109
|
+
|
|
110
|
+
The blocker only sees requests made after the provider starts rendering in
|
|
111
|
+
the browser, through `fetch` or `XMLHttpRequest`. It cannot stop:
|
|
112
|
+
|
|
113
|
+
* Code that runs before the provider renders: inline scripts in the HTML,
|
|
114
|
+
third-party tags in `<head>`, scripts loaded before hydration (such as
|
|
115
|
+
`next/script` with `beforeInteractive`), and client modules that evaluate
|
|
116
|
+
earlier. Webpack builds evaluate a route's client component modules when
|
|
117
|
+
its chunk loads, so their top-level code runs first. Turbopack evaluates a
|
|
118
|
+
client component module when its first element renders, which inside the
|
|
119
|
+
provider is after blocking starts.
|
|
120
|
+
* Code that saved its own reference to `fetch` or `XMLHttpRequest` before the
|
|
121
|
+
provider rendered.
|
|
122
|
+
* `navigator.sendBeacon`, `WebSocket`, `EventSource`, and requests made by
|
|
123
|
+
`<img>`, `<script>` or `<iframe>` elements, web workers and service
|
|
124
|
+
workers.
|
|
125
|
+
* With the standalone `useNetworkBlocker` hook instead of the provider
|
|
126
|
+
option, requests sent before the component that calls it renders.
|
|
127
|
+
Blocking starts in that component's first render, not the provider's.
|
|
128
|
+
|
|
129
|
+
Keep tracking calls out of that window:
|
|
130
|
+
|
|
131
|
+
* Send them from an effect or an event handler, never at module top level.
|
|
132
|
+
* Load vendor SDKs through `scripts` instead of a `<script>` tag or
|
|
133
|
+
`next/script`, so they wait for consent before they run at all.
|
|
134
|
+
* Check `useConsent('measurement')` (or the category you need) before you
|
|
135
|
+
call a vendor from your own code, and treat the blocker as a backstop.
|
|
136
|
+
|
|
137
|
+
## Verify the blocked requests
|
|
138
|
+
|
|
139
|
+
Open DevTools Network in a private window, under a policy that asks for
|
|
140
|
+
consent.
|
|
141
|
+
|
|
142
|
+
1. Before a choice, requests matching a rule are absent, and the console logs
|
|
143
|
+
each blocked request.
|
|
144
|
+
2. Allow the rule's category and save. Matching requests go out.
|
|
145
|
+
3. Reject, reload, and check that they stay absent.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Scripts
|
|
3
|
+
description: Load vendor scripts by consent category in a TanStack Start app
|
|
4
|
+
with ConsentRoot, and clear stored data or reload the page when a visitor
|
|
5
|
+
withdraws consent.
|
|
6
|
+
group: frameworks
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Register vendor scripts
|
|
10
|
+
|
|
11
|
+
Pass vendor loaders to `ConsentRoot` through its `scripts` prop. `ConsentRoot`
|
|
12
|
+
loads each script in the browser when its category is allowed and removes it
|
|
13
|
+
when the category is denied. The [quickstart](https://c15t.com/docs/frameworks/tanstack-start/quickstart)
|
|
14
|
+
registers PostHog and X Pixel in `src/scripts.ts`:
|
|
15
|
+
|
|
16
|
+
```ts title="src/scripts.ts"
|
|
17
|
+
import { posthog } from '@c15t/integrations/posthog';
|
|
18
|
+
import { xPixel } from '@c15t/integrations/x-pixel';
|
|
19
|
+
|
|
20
|
+
export const scripts = [
|
|
21
|
+
posthog({
|
|
22
|
+
id: 'phc_your_project_key',
|
|
23
|
+
initOptions: { cookieless_mode: 'never' },
|
|
24
|
+
loadMode: 'after-consent',
|
|
25
|
+
region: 'eu',
|
|
26
|
+
}),
|
|
27
|
+
xPixel({ pixelId: 'your-pixel-id' }),
|
|
28
|
+
];
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The root route imports that module and passes it on:
|
|
32
|
+
|
|
33
|
+
```tsx title="src/routes/__root.tsx"
|
|
34
|
+
import { scripts } from '../scripts';
|
|
35
|
+
|
|
36
|
+
<ConsentRoot
|
|
37
|
+
state={consent}
|
|
38
|
+
backendURL={backendURL}
|
|
39
|
+
initRoute={false}
|
|
40
|
+
scripts={scripts}
|
|
41
|
+
>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Import vendor helpers in route modules, not in a server function. A server
|
|
45
|
+
function's return value must be serializable, and vendor loaders contain
|
|
46
|
+
functions. Scripts only load in the browser, so importing them in the root
|
|
47
|
+
route adds nothing to the server HTML.
|
|
48
|
+
|
|
49
|
+
Each helper from `@c15t/integrations` sets its own category and a stable `id`.
|
|
50
|
+
Remove every other loader for the same vendor, such as a `scripts` entry in a
|
|
51
|
+
route's `head()` or an SDK you initialize at module level, so the vendor loads
|
|
52
|
+
once and only through c15t. [Integrations](../../integrations/overview.md) lists
|
|
53
|
+
every helper, and [building integrations](../../integrations/building-integrations.md)
|
|
54
|
+
covers a vendor without one.
|
|
55
|
+
|
|
56
|
+
## What happens when consent changes
|
|
57
|
+
|
|
58
|
+
* **Allowed.** The script loads, or its SDK starts, the first time its category
|
|
59
|
+
is allowed. With an awaited root loader, a returning visitor's allowed
|
|
60
|
+
scripts start right after hydration.
|
|
61
|
+
* **Denied later.** The script element is removed. Code that already ran keeps
|
|
62
|
+
running, so after the visitor turns off a category they had allowed, the page
|
|
63
|
+
reloads once the save finishes. Set `reloadOnConsentRevoked: false` in
|
|
64
|
+
`ConsentRoot`'s `options` only if every gated vendor stops itself.
|
|
65
|
+
* **`alwaysLoad` helpers.** Some integrations, such as Google Consent Mode,
|
|
66
|
+
load before consent and pass the visitor's choice to the vendor. Read the
|
|
67
|
+
vendor's guide; a category on a script does not always mean zero requests.
|
|
68
|
+
|
|
69
|
+
## Embeds and other requests
|
|
70
|
+
|
|
71
|
+
Scripts cover vendor code c15t loads for you. For the rest:
|
|
72
|
+
|
|
73
|
+
* [Embeds](./embeds.md) keeps iframes out of the
|
|
74
|
+
page with `ConsentGate` or the iframe blocker.
|
|
75
|
+
* [Network blocker](./network-blocker.md) holds
|
|
76
|
+
`fetch` and XHR calls that match a rule until their category is allowed.
|
|
77
|
+
|
|
78
|
+
## Clear stored data after revocation
|
|
79
|
+
|
|
80
|
+
Removing a script does not delete the cookies or storage entries it wrote. Pass
|
|
81
|
+
`clearOnRevocation` to `ConsentRoot` to delete the entries you list for each
|
|
82
|
+
category when it is denied. `ConsentRoot` reads it once, when it mounts.
|
|
83
|
+
[Clear on revocation](https://c15t.com/docs/frameworks/tanstack-start/clear-on-revocation) covers
|
|
84
|
+
configuration and browser limits.
|
|
85
|
+
|
|
86
|
+
## Let visitors turn off one vendor
|
|
87
|
+
|
|
88
|
+
A visitor can allow marketing and still switch off one vendor in it. Pass
|
|
89
|
+
`vendors` to `ConsentRoot`. Helpers from `@c15t/integrations` already carry
|
|
90
|
+
their vendor slug. See [vendor consent](https://c15t.com/docs/frameworks/tanstack-start/vendor-consent).
|
|
91
|
+
|
|
92
|
+
## Check the scripts
|
|
93
|
+
|
|
94
|
+
Open DevTools Network in a private window, filter by each vendor's domain and
|
|
95
|
+
reload.
|
|
96
|
+
|
|
97
|
+
1. Before a choice under an opt-in policy, no vendor script loads. The page
|
|
98
|
+
source has no vendor `<script>` tags.
|
|
99
|
+
2. Allow one category. Only that category's vendors load.
|
|
100
|
+
3. Turn the category off. The page reloads and the vendor stays absent.
|
|
101
|
+
4. Reload again. The rejection holds, and the server HTML shows no banner.
|
|
102
|
+
|
|
103
|
+
[Verify consent](../../guides/verify-consent.md) has the full checklist.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Embeds
|
|
3
|
+
description: Keep YouTube videos, maps and other iframes out of a Vue page until
|
|
4
|
+
their consent category is allowed, with ConsentGate or the iframe blocker.
|
|
5
|
+
group: frameworks
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Pick a method
|
|
9
|
+
|
|
10
|
+
| Method | Use it when |
|
|
11
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| `ConsentGate` | You render the iframe in a Vue component and want a placeholder with a way to open preferences. |
|
|
13
|
+
| The iframe blocker | The iframe comes from markup you do not render with a component, such as CMS content, or you want no wrapper. |
|
|
14
|
+
|
|
15
|
+
Both remove the iframe's `src` until the category is allowed, so the embed's
|
|
16
|
+
host receives no request before consent.
|
|
17
|
+
|
|
18
|
+
## Gate an embed with ConsentGate
|
|
19
|
+
|
|
20
|
+
Wrap the iframe in [`ConsentGate`](https://c15t.com/docs/frameworks/vue/components/consent-gate)
|
|
21
|
+
from `c15t/vue/runtime/components/consent-gate.vue`:
|
|
22
|
+
|
|
23
|
+
```vue title="src/VideoEmbed.vue"
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import ConsentGate from 'c15t/vue/runtime/components/consent-gate.vue';
|
|
26
|
+
import ConsentPreferencesLink from 'c15t/vue/runtime/components/consent-preferences-link.vue';
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<ConsentGate category="measurement">
|
|
31
|
+
<iframe
|
|
32
|
+
src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y"
|
|
33
|
+
title="YouTube video"
|
|
34
|
+
loading="lazy"
|
|
35
|
+
allowfullscreen
|
|
36
|
+
/>
|
|
37
|
+
<template #placeholder>
|
|
38
|
+
<p>Allow measurement to load this YouTube video.</p>
|
|
39
|
+
<ConsentPreferencesLink>Choose video permissions</ConsentPreferencesLink>
|
|
40
|
+
</template>
|
|
41
|
+
</ConsentGate>
|
|
42
|
+
</template>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The iframe does not exist in the page until the visitor allows measurement.
|
|
46
|
+
The `placeholder` slot shows a message and a
|
|
47
|
+
[`ConsentPreferencesLink`](https://c15t.com/docs/frameworks/vue/components/consent-preferences-link)
|
|
48
|
+
until then. When the visitor withdraws measurement, Vue removes the iframe.
|
|
49
|
+
|
|
50
|
+
Pick the category that matches what the embed does. The example uses
|
|
51
|
+
measurement for YouTube because its player measures views. A map or chat
|
|
52
|
+
widget usually belongs under functionality or experience.
|
|
53
|
+
|
|
54
|
+
## Gate an iframe with the iframe blocker
|
|
55
|
+
|
|
56
|
+
The Vue plugin runs the iframe blocker by default. Give an iframe `data-src`
|
|
57
|
+
instead of `src`, and a `data-category`:
|
|
58
|
+
|
|
59
|
+
```vue title="src/MapEmbed.vue"
|
|
60
|
+
<template>
|
|
61
|
+
<iframe
|
|
62
|
+
data-src="https://www.google.com/maps/embed?pb=YOUR_EMBED_ID"
|
|
63
|
+
data-category="functionality"
|
|
64
|
+
title="Store map"
|
|
65
|
+
/>
|
|
66
|
+
</template>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
c15t sets `src` from `data-src` when the category is allowed and removes it
|
|
70
|
+
when the category is withdrawn. It watches the page, so iframes added later,
|
|
71
|
+
for example by a route change, are handled too. The iframe element stays in
|
|
72
|
+
the page while blocked, empty. Add `data-vendor` with a vendor ID to also
|
|
73
|
+
block the iframe while the visitor has turned that vendor off.
|
|
74
|
+
|
|
75
|
+
Pass `iframeBlocker: false` to the plugin to turn the blocker off.
|
|
76
|
+
|
|
77
|
+
## Verify
|
|
78
|
+
|
|
79
|
+
Open the page in a private window with the Network tab open, under a policy
|
|
80
|
+
that asks for consent. No request goes to `youtube-nocookie.com` or
|
|
81
|
+
`google.com/maps`. Allow the category and save: the iframe loads. Withdraw it
|
|
82
|
+
and save: the page reloads without the embed. The
|
|
83
|
+
[YouTube](../../integrations/youtube.md) and
|
|
84
|
+
[Google Maps](../../integrations/google-maps.md) guides cover sizing and titles.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Network blocker
|
|
3
|
+
description: Hold fetch and XMLHttpRequest calls in a Vue app until their
|
|
4
|
+
consent category is allowed, with rules passed to the c15t Vue plugin.
|
|
5
|
+
group: frameworks
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Add rules
|
|
9
|
+
|
|
10
|
+
The network blocker stops `fetch` and `XMLHttpRequest` calls that match a
|
|
11
|
+
rule until the rule's category is allowed. Use it as a backstop for tracking
|
|
12
|
+
calls your own code or a vendor SDK makes. Load vendor SDKs through
|
|
13
|
+
[`scripts`](./scripts.md) first, so they do not run at all
|
|
14
|
+
before consent.
|
|
15
|
+
|
|
16
|
+
Define the rules in their own file:
|
|
17
|
+
|
|
18
|
+
```ts title="src/network-blocker.ts"
|
|
19
|
+
import type { UseNetworkBlockerOptions } from 'c15t/vue/vue-plugin';
|
|
20
|
+
|
|
21
|
+
export const networkBlocker = {
|
|
22
|
+
onRequestBlocked: (info) => {
|
|
23
|
+
console.info('Blocked until consent', info.method, info.url);
|
|
24
|
+
},
|
|
25
|
+
rules: [
|
|
26
|
+
{ category: 'measurement', domain: 'google-analytics.com' },
|
|
27
|
+
{
|
|
28
|
+
category: 'marketing',
|
|
29
|
+
domain: 'facebook.com',
|
|
30
|
+
methods: ['POST'],
|
|
31
|
+
pathIncludes: '/tr',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
} satisfies UseNetworkBlockerOptions;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Pass them to the plugin in `src/main.ts`:
|
|
38
|
+
|
|
39
|
+
```ts title="src/main.ts"
|
|
40
|
+
app.use(c15tVue, {
|
|
41
|
+
backendURL: 'https://your-project.inth.app',
|
|
42
|
+
networkBlocker,
|
|
43
|
+
scripts,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Match requests with rules
|
|
48
|
+
|
|
49
|
+
Each rule names a `domain` and the consent `category` a request needs. The
|
|
50
|
+
domain also matches its subdomains, so `google-analytics.com` covers
|
|
51
|
+
`www.google-analytics.com`. `pathIncludes` narrows a rule to paths that
|
|
52
|
+
contain a substring, and `methods` narrows it to HTTP methods. `category`
|
|
53
|
+
also takes conditions such as `{ and: ['measurement', 'marketing'] }`.
|
|
54
|
+
|
|
55
|
+
Add `vendor` with a vendor ID to also block the request while the visitor has
|
|
56
|
+
turned that vendor off. Rules for IAB TCF vendors use `vendorId` and the
|
|
57
|
+
`iabPurposes` fields instead.
|
|
58
|
+
|
|
59
|
+
| Option | Default | Purpose |
|
|
60
|
+
| -------------------- | -------- | ------------------------------------------------------------- |
|
|
61
|
+
| `rules` | Required | The rules above. |
|
|
62
|
+
| `enabled` | `true` | `false` keeps the rules but stops blocking. |
|
|
63
|
+
| `logBlockedRequests` | `true` | Logs each blocked request with `console.warn`. |
|
|
64
|
+
| `onRequestBlocked` | Unset | Called with `{ method, url, rule }` for each blocked request. |
|
|
65
|
+
|
|
66
|
+
## What a blocked request looks like
|
|
67
|
+
|
|
68
|
+
A blocked `fetch` resolves to a `451` response with the status text
|
|
69
|
+
`Request blocked by consent`, and nothing is sent. A blocked XHR is aborted
|
|
70
|
+
and fires an `error` event. Requests that match no rule are not delayed.
|
|
71
|
+
|
|
72
|
+
## When blocking starts
|
|
73
|
+
|
|
74
|
+
The plugin starts holding matching requests when `app.use(c15tVue, ...)`
|
|
75
|
+
runs, before your app mounts. While consent is unknown, a matching request
|
|
76
|
+
waits instead of failing. Once the policy has loaded and the stored choice
|
|
77
|
+
applies, a waiting request is sent if consent allows it and blocked
|
|
78
|
+
otherwise. If the policy fails to load, optional categories stay denied and
|
|
79
|
+
waiting requests are blocked.
|
|
80
|
+
|
|
81
|
+
## What it cannot stop
|
|
82
|
+
|
|
83
|
+
The blocker sees only `fetch` and `XMLHttpRequest` calls made after
|
|
84
|
+
`app.use()` runs. It cannot stop:
|
|
85
|
+
|
|
86
|
+
* Scripts in `index.html` and modules that run before `src/main.ts`.
|
|
87
|
+
* Code that saved its own reference to `fetch` before the plugin installed.
|
|
88
|
+
* `navigator.sendBeacon`, `WebSocket`, `EventSource`, and requests from
|
|
89
|
+
`<img>`, `<script>` and `<iframe>` elements, web workers and service
|
|
90
|
+
workers.
|
|
91
|
+
|
|
92
|
+
Check `useConsent()` before you call a vendor from your own code, and treat
|
|
93
|
+
the blocker as a backstop.
|
|
94
|
+
|
|
95
|
+
## Verify
|
|
96
|
+
|
|
97
|
+
Open the Network tab, clear site data and reload. Requests that match a rule
|
|
98
|
+
do not appear until you allow their category, and the console lists each
|
|
99
|
+
blocked request. Reject, reload, and check that they still do not appear.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Scripts
|
|
3
|
+
description: Load vendor scripts by consent category in a Vue app with the c15t
|
|
4
|
+
Vue plugin, and what happens when a visitor withdraws consent.
|
|
5
|
+
group: frameworks
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Register vendor scripts
|
|
9
|
+
|
|
10
|
+
A banner does not stop a script you load with a `<script>` tag in
|
|
11
|
+
`index.html` or a vendor SDK you initialize yourself. Remove those loaders and
|
|
12
|
+
register the vendor with c15t instead, so c15t loads it only while its
|
|
13
|
+
category is allowed.
|
|
14
|
+
|
|
15
|
+
Create the script configuration with the helpers from `@c15t/integrations`:
|
|
16
|
+
|
|
17
|
+
```ts title="src/scripts.ts"
|
|
18
|
+
import { posthog } from '@c15t/integrations/posthog';
|
|
19
|
+
import { xPixel } from '@c15t/integrations/x-pixel';
|
|
20
|
+
|
|
21
|
+
export const scripts = [
|
|
22
|
+
posthog({
|
|
23
|
+
id: 'phc_your_project_key',
|
|
24
|
+
initOptions: { cookieless_mode: 'never' },
|
|
25
|
+
loadMode: 'after-consent',
|
|
26
|
+
}),
|
|
27
|
+
xPixel({ pixelId: 'your-pixel-id' }),
|
|
28
|
+
];
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Pass it to the plugin as `scripts`:
|
|
32
|
+
|
|
33
|
+
```ts title="src/main.ts"
|
|
34
|
+
import { c15tVue } from 'c15t/vue/vue-plugin';
|
|
35
|
+
import { createApp } from 'vue';
|
|
36
|
+
|
|
37
|
+
import App from './App.vue';
|
|
38
|
+
import { scripts } from './scripts';
|
|
39
|
+
|
|
40
|
+
createApp(App)
|
|
41
|
+
.use(c15tVue, { backendURL: 'https://your-project.inth.app', scripts })
|
|
42
|
+
.mount('#app');
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The plugin starts one script loader when the app mounts, after it has applied
|
|
46
|
+
the visitor's stored choice and privacy signals. Do not also call
|
|
47
|
+
`createScriptLoader` from a component. A component's `onMounted` runs before
|
|
48
|
+
the plugin has read the stored choice, and each script would load twice.
|
|
49
|
+
|
|
50
|
+
Every vendor guide under [integrations](../../integrations/overview.md) gives the
|
|
51
|
+
helper and options for that vendor.
|
|
52
|
+
|
|
53
|
+
## Embeds and other requests
|
|
54
|
+
|
|
55
|
+
Scripts cover vendor code c15t loads for you. For the rest:
|
|
56
|
+
|
|
57
|
+
* [Embeds](./embeds.md) gates iframes with `ConsentGate` or
|
|
58
|
+
the iframe blocker.
|
|
59
|
+
* [Network blocker](./network-blocker.md) holds `fetch` and
|
|
60
|
+
XHR calls that match a rule until their category is allowed.
|
|
61
|
+
|
|
62
|
+
## When a visitor withdraws consent
|
|
63
|
+
|
|
64
|
+
Removing a script tag cannot stop code that already ran. When a save turns off
|
|
65
|
+
a category or vendor that was allowed, c15t reloads the page so the new
|
|
66
|
+
document starts with only permitted code. Set `reloadOnConsentRevoked: false`
|
|
67
|
+
to handle revocation yourself, or use the
|
|
68
|
+
[`onBeforeConsentRevocationReload` callback](https://c15t.com/docs/frameworks/vue/callbacks#before-a-revocation-reload)
|
|
69
|
+
to run code before the reload.
|
|
70
|
+
|
|
71
|
+
`clearOnRevocation` deletes first-party cookies and storage keys that belong to
|
|
72
|
+
a category when it is withdrawn. See
|
|
73
|
+
[clear on revocation](https://c15t.com/docs/frameworks/vue/clear-on-revocation).
|
|
74
|
+
|
|
75
|
+
## Let visitors turn off one vendor
|
|
76
|
+
|
|
77
|
+
A visitor can allow marketing and still switch off one vendor in it. Declare
|
|
78
|
+
the vendors in the `vendors` option; helpers from `@c15t/integrations` already carry
|
|
79
|
+
their vendor slug. See [vendor consent](https://c15t.com/docs/frameworks/vue/vendor-consent).
|
|
80
|
+
|
|
81
|
+
## Content Security Policy
|
|
82
|
+
|
|
83
|
+
Allow each vendor's script host in `script-src`. The plugin's `nonce` option
|
|
84
|
+
adds a nonce to every script the loader creates. See
|
|
85
|
+
[Content Security Policy](https://c15t.com/docs/frameworks/vue/content-security-policy).
|
|
86
|
+
|
|
87
|
+
## Verify gating
|
|
88
|
+
|
|
89
|
+
In a private window, open the Network tab and load the app under a policy that
|
|
90
|
+
asks for consent. Requests to your vendors are absent.
|
|
91
|
+
Allow one category and save, and only that category's vendors load. Withdraw
|
|
92
|
+
it, and the page reloads without loading the vendor again.
|
|
93
|
+
[Verify consent](../../guides/verify-consent.md) has the full checklist.
|