bsmnt 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/add-integration/index.d.ts.map +1 -1
- package/dist/application/add-integration/index.js +23 -0
- package/dist/application/add-integration/index.js.map +1 -1
- package/dist/domain/consent.d.ts +17 -0
- package/dist/domain/consent.d.ts.map +1 -0
- package/dist/domain/consent.js +13 -0
- package/dist/domain/consent.js.map +1 -0
- package/dist/domain/integration.d.ts +19 -2
- package/dist/domain/integration.d.ts.map +1 -1
- package/dist/domain/integration.js +10 -1
- package/dist/domain/integration.js.map +1 -1
- package/dist/domain/jobs.d.ts +17 -0
- package/dist/domain/jobs.d.ts.map +1 -0
- package/dist/domain/jobs.js +13 -0
- package/dist/domain/jobs.js.map +1 -0
- package/dist/domain/skills.d.ts +23 -2
- package/dist/domain/skills.d.ts.map +1 -1
- package/dist/domain/skills.js +23 -2
- package/dist/domain/skills.js.map +1 -1
- package/dist/domain/templates.d.ts +7 -1
- package/dist/domain/templates.d.ts.map +1 -1
- package/dist/domain/templates.js +2 -0
- package/dist/domain/templates.js.map +1 -1
- package/dist/index.js +34 -9
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-agent.js +11 -4
- package/dist/infrastructure/create/setup-agent.js.map +1 -1
- package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
- package/dist/infrastructure/integrations/inject-integration.js +36 -2
- package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
- package/dist/modules/features/consent/ketch/config.d.ts +18 -0
- package/dist/modules/features/consent/ketch/config.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/config.js +24 -0
- package/dist/modules/features/consent/ketch/config.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/biome.d.ts +16 -0
- package/dist/modules/features/consent/ketch/mergers/biome.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/biome.js +31 -0
- package/dist/modules/features/consent/ketch/mergers/biome.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts +13 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.js +64 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.d.ts +12 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.js +28 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/footer.d.ts +17 -0
- package/dist/modules/features/consent/ketch/mergers/footer.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/footer.js +40 -0
- package/dist/modules/features/consent/ketch/mergers/footer.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/knip.d.ts +18 -0
- package/dist/modules/features/consent/ketch/mergers/knip.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/knip.js +37 -0
- package/dist/modules/features/consent/ketch/mergers/knip.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/layout.d.ts +25 -0
- package/dist/modules/features/consent/ketch/mergers/layout.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/layout.js +55 -0
- package/dist/modules/features/consent/ketch/mergers/layout.js.map +1 -0
- package/dist/modules/features/jobs/ashby/config.d.ts +19 -0
- package/dist/modules/features/jobs/ashby/config.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/config.js +29 -0
- package/dist/modules/features/jobs/ashby/config.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts +14 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.js +78 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts +12 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.js +27 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.d.ts +18 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.js +41 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -1
- package/package.json +3 -1
- package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +277 -0
- package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +58 -25
- package/src/agent-skills/ketch-consent-check/SKILL.md +197 -0
- package/src/agent-skills/ketch-consent-setup/SKILL.md +326 -0
- package/src/agent-skills/ketch-consent-setup/references/ui-customization.md +329 -0
- package/src/agent-skills/page-builder-block/SKILL.md +150 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
- package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
- package/src/templates/next-pagebuilder/AGENTS.md +129 -0
- package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
- package/src/templates/next-pagebuilder/README.md +70 -0
- package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
- package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +1 -1
- package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
- package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
- package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
- package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
- package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
- package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
- package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
- package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
- package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
- package/src/templates/next-pagebuilder-cache/README.md +75 -0
- package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
- package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +1 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
- package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
- package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
- package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
- package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ketch-consent-setup
|
|
3
|
+
description: >-
|
|
4
|
+
Install the Ketch consent gate (CMP) in a basement Next.js project, and control
|
|
5
|
+
its UI — env vars, the Smart Tag, the consent store, where preferences are
|
|
6
|
+
reachable from, and styling. Use when the user asks to "add Ketch", "install
|
|
7
|
+
Ketch", "set up the cookie banner", "set up the consent banner", "add a CMP",
|
|
8
|
+
"add the consent gate", "we need cookie consent". Also use for UI questions
|
|
9
|
+
about an existing Ketch install: "hide the floating cookie button", "move the
|
|
10
|
+
consent widget out of the corner / into the footer", "restyle the consent
|
|
11
|
+
banner", "pass custom styles to Ketch", "make the banner match our design
|
|
12
|
+
tokens", or "why isn't the consent banner showing". Gating an individual
|
|
13
|
+
tracker is the `ketch-consent-check` skill.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Install the Ketch consent gate
|
|
17
|
+
|
|
18
|
+
Ketch is the CMP. It owns the consent record, the jurisdiction resolution, the
|
|
19
|
+
legal copy, the banner and preferences modal, and the persistence. The project
|
|
20
|
+
owns exactly three things, and this skill wires all three:
|
|
21
|
+
|
|
22
|
+
1. Loading Ketch's Smart Tag.
|
|
23
|
+
2. **One** helper that answers "do I have consent for category X?".
|
|
24
|
+
3. An entry point to reopen preferences after the banner is gone.
|
|
25
|
+
|
|
26
|
+
**Already installed?** If `lib/integrations/ketch/` exists, stop — you want the
|
|
27
|
+
`ketch-consent-check` skill instead, which gates an individual tracker.
|
|
28
|
+
|
|
29
|
+
Read each file first and adapt to its actual shape — do not blindly
|
|
30
|
+
pattern-match. If an anchor below is missing, stop and report it rather than
|
|
31
|
+
guessing.
|
|
32
|
+
|
|
33
|
+
## 0. Preconditions
|
|
34
|
+
|
|
35
|
+
- A basement Next.js project (App Router).
|
|
36
|
+
- A Ketch account for the client, with a **property** already created. You need
|
|
37
|
+
its org code and property code. If the property doesn't exist yet, that's a
|
|
38
|
+
dashboard task at https://app.ketch.com and it blocks this skill — report it.
|
|
39
|
+
- If you intend to hide Ketch's floating button, a persistent surface every
|
|
40
|
+
template renders — footer, nav, settings menu (in a basement project usually
|
|
41
|
+
`components/layout/footer` reached via `components/layout/wrapper`).
|
|
42
|
+
|
|
43
|
+
## 1. Env vars
|
|
44
|
+
|
|
45
|
+
Both codes are public — they appear in the boot URL in the page source — so
|
|
46
|
+
they go in as plain values:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bun run env:set NEXT_PUBLIC_KETCH_ORG_CODE <org_code> --plain
|
|
50
|
+
bun run env:set NEXT_PUBLIC_KETCH_PROPERTY_CODE <property_code> --plain
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Get them from 1Password → **Development** vault → the project's integrations
|
|
54
|
+
item. They are the two path segments of the boot URL:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
https://global.ketchcdn.com/web/v3/config/<org>/<property>/boot.js
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If all you have is the raw Ketch `<script>` snippet, read both out of that URL.
|
|
61
|
+
|
|
62
|
+
Because they are plain, **nothing needs to be pushed to Vercel** — the build
|
|
63
|
+
reads the committed `.env` and Next inlines them. Do not run
|
|
64
|
+
`env:setup-remote` for these.
|
|
65
|
+
|
|
66
|
+
Document them in `.env.example` too.
|
|
67
|
+
|
|
68
|
+
## 2. Files — `lib/integrations/ketch/`
|
|
69
|
+
|
|
70
|
+
Follow the shape of the project's other integrations (e.g.
|
|
71
|
+
`lib/integrations/hubspot/`): self-contained folder, relative imports inside it,
|
|
72
|
+
`@/` imports across folders, and a `README.md`.
|
|
73
|
+
|
|
74
|
+
| File | Role |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `env.ts` | Validates both codes against `/^[a-z0-9_-]+$/i`, exports `ketchBootUrl` (`null` when unset) |
|
|
77
|
+
| `types.ts` | Types for the `window.ketch` / `window.semaphore` surface |
|
|
78
|
+
| `categories.ts` | Category ↔ Ketch purpose ↔ tracker mapping |
|
|
79
|
+
| `consent-store.ts` | The only module that talks to Ketch |
|
|
80
|
+
| `use-consent.ts` | The helper everything else imports |
|
|
81
|
+
| `script.tsx` | Renders the Smart Tag |
|
|
82
|
+
| `preferences-button.tsx` | Preferences entry point (`cva`, takes `variant` / `size`) |
|
|
83
|
+
| `ketch-experience.css` | Maps Ketch's theme to the project's tokens |
|
|
84
|
+
|
|
85
|
+
### 2.1 Categories
|
|
86
|
+
|
|
87
|
+
Three stable local names so nothing else hardcodes a Ketch purpose code:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export const CONSENT_CATEGORIES = {
|
|
91
|
+
essential: "essential_services",
|
|
92
|
+
analytics: "analytics",
|
|
93
|
+
marketing: "targeted_advertising",
|
|
94
|
+
} as const
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Confirm the right-hand side against the property's real purpose codes:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
curl -s https://global.ketchcdn.com/web/v3/config/<org>/<property>/production/default/en/config.json \
|
|
101
|
+
| jq -r '.purposes[] | "\(.code) \(.name)"'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2.2 The store
|
|
105
|
+
|
|
106
|
+
Module-level state read through `useSyncExternalStore` — no Provider, no
|
|
107
|
+
Context. It attaches `ketch('on', 'consent')` and `ketch('on', 'jurisdiction')`
|
|
108
|
+
once, on first subscribe, and re-notifies subscribers on every preference
|
|
109
|
+
change. `useConsent()` wraps it and exposes `hasConsent(category)`, `granted`,
|
|
110
|
+
`isResolved`, `jurisdiction` and `openPreferences()`.
|
|
111
|
+
|
|
112
|
+
Three facts about Ketch's runtime that the store has to respect:
|
|
113
|
+
|
|
114
|
+
- Consent arrives as `Record<purposeCode, boolean>`, and a purpose key is
|
|
115
|
+
**absent** until a record exists. **Absent must read as denied** — this is
|
|
116
|
+
what guarantees nothing fires before consent.
|
|
117
|
+
- `window.ketch(cmd, …)` pushes onto `window.semaphore`, and Ketch's `boot.js`
|
|
118
|
+
**unshifts** its own `["init", config]` entry. So queuing commands before the
|
|
119
|
+
SDK loads is safe; after init, `semaphore.push` is rebound to dispatch
|
|
120
|
+
directly and `semaphore.loaded` flips to `true`.
|
|
121
|
+
- Persistence is Ketch's, not ours: `_ketch_consent_v1_` in `localStorage` plus
|
|
122
|
+
the `_swb` cookie, surviving navigation and reloads. **Do not add a second
|
|
123
|
+
source of truth** — they drift.
|
|
124
|
+
|
|
125
|
+
### 2.3 knip
|
|
126
|
+
|
|
127
|
+
Register the folder as an entry or CI fails on "unused exports":
|
|
128
|
+
|
|
129
|
+
```jsonc
|
|
130
|
+
// knip.json
|
|
131
|
+
"entry": ["lib/integrations/ketch/**/*.{ts,tsx}", /* …existing… */]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 3. Load the tag
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
export const KetchConsentScript = () => {
|
|
138
|
+
if (!ketchBootUrl) return null
|
|
139
|
+
return <script async src={ketchBootUrl} />
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Mount it in the layout that renders the site chrome, not the root layout.**
|
|
144
|
+
In a basement project that is the route group holding the header and footer —
|
|
145
|
+
`app/(content)/layout.tsx` — alongside the entry point from step 4b:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const Layout = ({ children }: { children: React.ReactNode }) => (
|
|
149
|
+
<>
|
|
150
|
+
<KetchConsentScript />
|
|
151
|
+
<Wrapper>{children}</Wrapper>
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The root layout also wraps admin routes like `/studio`, and a CMP has no
|
|
157
|
+
business loading there: nothing on those routes tracks anyone, the banner
|
|
158
|
+
covers the tooling, and the floating button lands on top of the Studio UI.
|
|
159
|
+
Mounting one level down keeps Ketch on the pages that actually need it.
|
|
160
|
+
|
|
161
|
+
Two consequences worth knowing, neither of them a problem:
|
|
162
|
+
|
|
163
|
+
- `script.tsx` imports `ketch-experience.css`, so the styles ride along with
|
|
164
|
+
the component and only ship on the routes that render it. That is what you
|
|
165
|
+
want — it also means the floating-button override is scoped the same way.
|
|
166
|
+
- Ketch does not load on the 404, which falls back to the root layout. Fine as
|
|
167
|
+
long as nothing tracks there either. If you later mount a tracker in the root
|
|
168
|
+
layout, move this back up and accept it on `/studio`.
|
|
169
|
+
|
|
170
|
+
Three traps, all verified on Next 16.3 + React 19:
|
|
171
|
+
|
|
172
|
+
- **Do not use `next/script` with `strategy="beforeInteractive"`.** It throws
|
|
173
|
+
*"Encountered a script tag while rendering React component"* and emits no
|
|
174
|
+
server-side `<script>`. A plain `<script async src>` is hoisted into `<head>`
|
|
175
|
+
by React 19 during SSR, which is what a CMP needs.
|
|
176
|
+
- **Do not render it as a sibling of `<body>`.** A `<script>` as a direct child
|
|
177
|
+
of `<html>` is invalid HTML and produces a cascade of hydration errors. If
|
|
178
|
+
you do mount it in the root layout, it goes *inside* `<body>`.
|
|
179
|
+
- **Do not mount it in a layout that some templates skip.** Consent has to be
|
|
180
|
+
askable on every page a visitor can land on. One layout above the footer, not
|
|
181
|
+
per-page.
|
|
182
|
+
|
|
183
|
+
When `ketchBootUrl` is `null` the tag is not rendered and every consent check
|
|
184
|
+
returns `false` — the site degrades to "no tracking", never to "tracking
|
|
185
|
+
without consent".
|
|
186
|
+
|
|
187
|
+
## 4. Decide the UI — do not assume
|
|
188
|
+
|
|
189
|
+
Ketch ships a banner, a preferences modal and a floating button, all in **light
|
|
190
|
+
DOM**, so the project can keep, restyle, reposition or hide any of it. There is
|
|
191
|
+
no single correct answer here — **ask, or follow the design.** Do not copy
|
|
192
|
+
another project's choice.
|
|
193
|
+
|
|
194
|
+
Two decisions:
|
|
195
|
+
|
|
196
|
+
**a. The floating button.** Ketch mounts it into `<div id="lanyard_fab_button">`
|
|
197
|
+
as `body.firstChild` whenever the property config has an `accessibilityButton`.
|
|
198
|
+
|
|
199
|
+
| Option | When |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| Keep it as configured | No design opinion yet; fastest path |
|
|
202
|
+
| Reposition / restyle it | It collides with a chat widget or sticky CTA |
|
|
203
|
+
| Hide it | The design forbids persistent floating UI (common — it covers the layout on every page and is worst on mobile) |
|
|
204
|
+
|
|
205
|
+
**b. Where preferences are reachable from.** Consent must stay reopenable after
|
|
206
|
+
the banner is gone. The floating button, a footer link, a nav item, a
|
|
207
|
+
privacy-page link, or several — all valid. If you hide the floating button you
|
|
208
|
+
**must** provide another entry point, and it has to be on something every
|
|
209
|
+
template renders.
|
|
210
|
+
|
|
211
|
+
Whatever the project picks, route the call through the integration's helper
|
|
212
|
+
(`openPreferences()`), not `window.ketch` directly.
|
|
213
|
+
|
|
214
|
+
Mechanisms for all of the above — the exact element IDs, why hiding the button
|
|
215
|
+
needs `!important`, the available `ketch(…)` commands — are in
|
|
216
|
+
[`references/ui-customization.md`](references/ui-customization.md).
|
|
217
|
+
|
|
218
|
+
## 5. Styling
|
|
219
|
+
|
|
220
|
+
Ketch's UI is styleable three ways, and they compose. Choose by how much design
|
|
221
|
+
control is needed and whether you have dashboard access:
|
|
222
|
+
|
|
223
|
+
| Approach | Trade-off |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| **Dashboard theme** | Survives code changes, no CSS to maintain — but hexes are duplicated from the design system and drift |
|
|
226
|
+
| **`--k-*` variable overrides** | Token-driven, so the design system stays the source of truth and Ketch follows automatically — but the names are Ketch-internal |
|
|
227
|
+
| **Direct CSS on Ketch's element IDs** | Total control, needed for layout and responsive work — but couples to Ketch's DOM |
|
|
228
|
+
|
|
229
|
+
Default recommendation for a basement project: **variables for colour and type,
|
|
230
|
+
direct CSS only for structure.** That keeps the banner following the design
|
|
231
|
+
tokens with no maintenance, e.g.:
|
|
232
|
+
|
|
233
|
+
```css
|
|
234
|
+
html:root {
|
|
235
|
+
--k-banner-container-background-color: var(--color-primary);
|
|
236
|
+
--k-banner-header-title-color: var(--color-secondary);
|
|
237
|
+
--k-banner-description-link-color: var(--color-contrast);
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Scope overrides to `html:root`, **not** `:root` — Ketch injects its own `:root`
|
|
242
|
+
block into `<head>` at runtime, after the project stylesheet.
|
|
243
|
+
|
|
244
|
+
If there is no design yet, wire it to whatever tokens exist rather than
|
|
245
|
+
hardcoding hexes; the UI then updates on its own when real tokens land. Say so
|
|
246
|
+
explicitly instead of inventing a visual direction.
|
|
247
|
+
|
|
248
|
+
Variable naming, how to enumerate what a property exposes, the `!important`
|
|
249
|
+
rules, mobile and safe-area handling, and the case for replacing the UI entirely:
|
|
250
|
+
[`references/ui-customization.md`](references/ui-customization.md).
|
|
251
|
+
|
|
252
|
+
## 6. Verify
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
bun run typecheck && bun run lint && bun run knip && bun run build
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Then in the browser:
|
|
259
|
+
|
|
260
|
+
1. Load a page that renders your chosen entry point. **In a Sanity
|
|
261
|
+
page-builder project `/` is usually a 404** — no page document exists for
|
|
262
|
+
it — and the 404 falls back to the root layout, so it has no chrome and no
|
|
263
|
+
Ketch. Testing there shows no banner and no button and looks broken. Use a
|
|
264
|
+
real slug.
|
|
265
|
+
2. The tag is in `<head>`:
|
|
266
|
+
`curl -s <url> | grep -o '<script async[^>]*ketchcdn[^>]*>'`
|
|
267
|
+
If it only appears escaped inside the RSC payload (`\"src\":\"https://…`)
|
|
268
|
+
and not as a real tag, you are looking at a 404, not a page.
|
|
269
|
+
3. `window.semaphore.loaded === true` in the console.
|
|
270
|
+
4. The banner shows on first visit, and the chosen entry point reopens the
|
|
271
|
+
preferences modal.
|
|
272
|
+
5. The floating button is in the state you intended — kept, moved or hidden —
|
|
273
|
+
both while the banner is open and after it's dismissed.
|
|
274
|
+
6. Check at <640px too; a widget that looks fine on desktop can cover the
|
|
275
|
+
layout on mobile.
|
|
276
|
+
7. Choose something, reload, confirm it persisted.
|
|
277
|
+
|
|
278
|
+
## 7. Region behaviour — confirm and document it
|
|
279
|
+
|
|
280
|
+
Ketch resolves a jurisdiction from the visitor's IP and applies that
|
|
281
|
+
jurisdiction's legal basis per purpose. This is dashboard configuration, not
|
|
282
|
+
code, so **read it rather than assuming**:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
for j in gdpr US_California US_Texas Canada Quebec default; do
|
|
286
|
+
echo "== $j"
|
|
287
|
+
curl -s https://global.ketchcdn.com/web/v3/config/<org>/<property>/production/$j/en/config.json \
|
|
288
|
+
| jq -r '[.regulations[0]] + [.purposes[] | "\(.code)=\(.legalBasisCode)"] | join(" ")'
|
|
289
|
+
done
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Typical result: EEA/UK/CH, California and Quebec are **opt-in**
|
|
293
|
+
(`consent_optin` — off until accepted); Canada and most US state-privacy-law
|
|
294
|
+
states are **opt-out** (`consent_optout` — on until refused); everywhere else
|
|
295
|
+
falls to `default` (`disclosure`, notice only). The full jurisdiction → region
|
|
296
|
+
map is in the property's `boot.js` under `jurisdiction.scopes`.
|
|
297
|
+
|
|
298
|
+
Put the resulting table in the integration's `README.md` — "what an EU visitor
|
|
299
|
+
sees vs a US visitor" is a question that gets asked repeatedly.
|
|
300
|
+
|
|
301
|
+
Note that because the helper defaults to denied until Ketch confirms, the
|
|
302
|
+
project's own trackers stay off in opt-out regions too until the record
|
|
303
|
+
arrives. That is stricter than required and intentional.
|
|
304
|
+
|
|
305
|
+
**Opt-out and notice-only jurisdictions won't show Accept/Reject, and the
|
|
306
|
+
banner won't come back on reload** once Ketch auto-grants that jurisdiction's
|
|
307
|
+
default record — that's the legal basis, not a bug. To actually see and test
|
|
308
|
+
the opt-in banner, force a jurisdiction that has one — append
|
|
309
|
+
`?ketch_jurisdiction=gdpr` (or `US_Texas`, …) to the URL, no code change
|
|
310
|
+
needed. Full mechanics, including a code-level override for previews, are in
|
|
311
|
+
`references/ui-customization.md`.
|
|
312
|
+
|
|
313
|
+
## 8. Document it
|
|
314
|
+
|
|
315
|
+
Document it wherever the project already documents integrations — a section in
|
|
316
|
+
the root `README.md`, or a `lib/integrations/ketch/README.md` if the project uses
|
|
317
|
+
per-integration READMEs. Ask rather than adding a file nobody wanted.
|
|
318
|
+
|
|
319
|
+
Cover: what Ketch is, who owns the account, where the keys live, **how another
|
|
320
|
+
integration checks consent before firing**, the region-by-region behaviour, and
|
|
321
|
+
open dashboard items.
|
|
322
|
+
|
|
323
|
+
## Next
|
|
324
|
+
|
|
325
|
+
Gating an actual tracker (GTM, PostHog, Claydar, …) is the
|
|
326
|
+
`ketch-consent-check` skill. Do not wire trackers here.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Controlling Ketch's UI
|
|
2
|
+
|
|
3
|
+
Ketch ships its own banner, modal, preferences center and floating button. This
|
|
4
|
+
file documents the surface it exposes so a project can keep, restyle, reposition
|
|
5
|
+
or replace any of it. **None of these choices is "the" right one** — pick per
|
|
6
|
+
project and per design.
|
|
7
|
+
|
|
8
|
+
## What Ketch actually renders
|
|
9
|
+
|
|
10
|
+
All of it is **light DOM — there is no shadow root**, so ordinary CSS reaches it.
|
|
11
|
+
|
|
12
|
+
| Element | What it is |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `<div id="lanyard_root">` | Host for every experience: banner, modal, preferences, consent gate |
|
|
15
|
+
| `<div id="lanyard_fab_button">` | The floating button. Inserted as **`body.firstChild`** |
|
|
16
|
+
| `#ketch-banner` | The banner container |
|
|
17
|
+
| `#ketch-banner-content` | Banner copy area |
|
|
18
|
+
| `#ketch-banner-buttons-container-standard` / `-compact` | Banner button rows (which one depends on the configured size) |
|
|
19
|
+
| `#ketch-banner-button-*` | Individual banner buttons |
|
|
20
|
+
| `#ketch-modal` | The "customize settings" modal |
|
|
21
|
+
| `#ketch-preferences` | The preferences center |
|
|
22
|
+
| `#ketch-preferences-purposes-tab`, `-subscriptions-tab`, `-requests-tab`, … | Preference center tabs |
|
|
23
|
+
|
|
24
|
+
At runtime Ketch also appends a `<style>` to `<head>` containing
|
|
25
|
+
`:root { --k-…: …; }` with its whole resolved theme. **That stylesheet lands
|
|
26
|
+
after the project's CSS** — the single most important fact for overriding it.
|
|
27
|
+
|
|
28
|
+
The floating button is only rendered when both are true:
|
|
29
|
+
|
|
30
|
+
- the property config has an `accessibilityButton` object, and
|
|
31
|
+
- experiences aren't disabled.
|
|
32
|
+
|
|
33
|
+
So removing `accessibilityButton` in the Ketch dashboard is the only way to stop
|
|
34
|
+
it being created at all. Everything below is client-side.
|
|
35
|
+
|
|
36
|
+
## Choice 1 — the floating button
|
|
37
|
+
|
|
38
|
+
### Keep it (Ketch's default)
|
|
39
|
+
|
|
40
|
+
Do nothing. Position and colour come from `accessibilityButton` in the
|
|
41
|
+
dashboard (`position: "bottom-left" | "bottom-right" | …`, `color`). Change it
|
|
42
|
+
there, no code.
|
|
43
|
+
|
|
44
|
+
### Reposition or restyle it
|
|
45
|
+
|
|
46
|
+
```css
|
|
47
|
+
#lanyard_fab_button {
|
|
48
|
+
inset: auto 1.5rem 1.5rem auto;
|
|
49
|
+
z-index: 40;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Useful when the dashboard's corner collides with a chat widget or a sticky CTA.
|
|
54
|
+
|
|
55
|
+
### Hide it
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
#lanyard_fab_button {
|
|
59
|
+
display: none !important;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`!important` is **required**: Ketch toggles this element's inline
|
|
64
|
+
`style.display` when experiences open and close, and an inline style beats any
|
|
65
|
+
plain rule. Without it the button reappears after the banner is dismissed.
|
|
66
|
+
|
|
67
|
+
If the project's linter forbids `!important`, scope an override rather than
|
|
68
|
+
dropping it — the rule doesn't work without it:
|
|
69
|
+
|
|
70
|
+
```jsonc
|
|
71
|
+
// biome.json → overrides
|
|
72
|
+
{
|
|
73
|
+
"includes": ["lib/integrations/ketch/ketch-experience.css"],
|
|
74
|
+
"linter": { "rules": { "complexity": { "noImportantStyles": "off" } } }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Hiding it in CSS is a patch: Ketch still mounts the element on every page.
|
|
79
|
+
Removing `accessibilityButton` from the dashboard is the durable fix. Do both
|
|
80
|
+
when you have dashboard access; flag it as a follow-up when you don't.
|
|
81
|
+
|
|
82
|
+
## Choice 2 — where preferences are reachable from
|
|
83
|
+
|
|
84
|
+
Consent has to stay reopenable after the banner is gone. Any of these works:
|
|
85
|
+
|
|
86
|
+
| Option | How |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| Ketch's floating button | Default, nothing to build |
|
|
89
|
+
| A footer link | Render a button that calls `openPreferences()` |
|
|
90
|
+
| A nav / settings menu item | Same call, different mount point |
|
|
91
|
+
| A privacy-policy page link | Same call |
|
|
92
|
+
| Several at once | Fine — the call is idempotent |
|
|
93
|
+
|
|
94
|
+
The integration's `openPreferences()` helper opens the preferences center as a
|
|
95
|
+
**modal** over the current page, not a full-page navigation:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
window.ketch("showConsent", { displayHint: "experiences.consent.modal" })
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Prefer routing it through the integration's helper (`openPreferences()` from
|
|
102
|
+
`useConsent()`) so nothing else touches `window.ketch`.
|
|
103
|
+
|
|
104
|
+
It is safe to call before the SDK has loaded — it queues on `window.semaphore`
|
|
105
|
+
and dispatches after init — so a control doesn't need to wait for readiness or
|
|
106
|
+
be disabled on first paint.
|
|
107
|
+
|
|
108
|
+
Related commands:
|
|
109
|
+
|
|
110
|
+
| Command | Effect |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `ketch("showConsent", { displayHint: "experiences.consent.modal" })` | Open preferences as a modal — what `openPreferences()` calls |
|
|
113
|
+
| `ketch("showPreferences")` | Open the full-page preference experience instead of a modal |
|
|
114
|
+
| `ketch("showConsent")` | Re-show the consent banner |
|
|
115
|
+
| `ketch("showPreferences", { tab: "purposes" })` | Open the full-page experience on a specific tab |
|
|
116
|
+
|
|
117
|
+
Whichever mount point you choose, put it somewhere **every template renders** —
|
|
118
|
+
usually the footer or a persistent nav — and verify it on a route that isn't the
|
|
119
|
+
homepage.
|
|
120
|
+
|
|
121
|
+
## Choice 3 — styling
|
|
122
|
+
|
|
123
|
+
Three approaches. They compose; pick by how much design control the project
|
|
124
|
+
needs and whether you have dashboard access.
|
|
125
|
+
|
|
126
|
+
### A. Theme it in the Ketch dashboard
|
|
127
|
+
|
|
128
|
+
Set colours, radii and fonts in the property's theme editor.
|
|
129
|
+
|
|
130
|
+
- **For:** survives any code change, no CSS to maintain, works for teams
|
|
131
|
+
without frontend access.
|
|
132
|
+
- **Against:** hex values duplicated from the design system, so it drifts when
|
|
133
|
+
tokens change; needs dashboard access; can't reference CSS variables.
|
|
134
|
+
|
|
135
|
+
Best when the site's palette is stable and nobody wants Ketch CSS in the repo.
|
|
136
|
+
|
|
137
|
+
### B. Override the `--k-*` variables (token-driven)
|
|
138
|
+
|
|
139
|
+
Ketch's components consume their theme through CSS custom properties, so
|
|
140
|
+
redefining them restyles the UI without touching structure:
|
|
141
|
+
|
|
142
|
+
```css
|
|
143
|
+
html:root {
|
|
144
|
+
--k-banner-container-background-color: var(--color-primary);
|
|
145
|
+
--k-banner-header-title-color: var(--color-secondary);
|
|
146
|
+
--k-banner-description-link-color: var(--color-contrast);
|
|
147
|
+
--k-banner-buttons-primary-background-color: var(--color-secondary);
|
|
148
|
+
--k-banner-buttons-primary-text-color: var(--color-primary);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **For:** the design system stays the single source of truth — when tokens
|
|
153
|
+
change, Ketch follows automatically, no edits.
|
|
154
|
+
- **Against:** variable names are Ketch-internal and could change across SDK
|
|
155
|
+
versions; only covers what the theme models.
|
|
156
|
+
|
|
157
|
+
**Use `html:root`, not `:root`.** Ketch injects its own `:root` block into
|
|
158
|
+
`<head>` at runtime, so it comes after the project stylesheet and wins on equal
|
|
159
|
+
specificity. `html:root` is one step higher and wins regardless of order.
|
|
160
|
+
`:root { … !important }` also works; `html:root` is cleaner.
|
|
161
|
+
|
|
162
|
+
#### Deriving the variable names
|
|
163
|
+
|
|
164
|
+
The rule is mechanical: **take the theme value's dot path and replace dots with
|
|
165
|
+
dashes, prefixed `--k-`.**
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
banner.container.background.color → --k-banner-container-background-color
|
|
169
|
+
modal.purposeList.switchButtons.on.background.color
|
|
170
|
+
→ --k-modal-purposeList-switchButtons-on-background-color
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Casing is preserved, so `purposeList` stays camelCase. Enumerate what's
|
|
174
|
+
available for the property:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
curl -s https://global.ketchcdn.com/web/v3/config/<org>/<property>/production/default/en/config.json \
|
|
178
|
+
| jq '.theme.banner, .theme.modal, .theme.preference'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Watch out for non-colour values. Radii are sometimes stored unitless (`"8"`),
|
|
182
|
+
which is invalid in `border-radius: var(…)` — set those explicitly with a unit
|
|
183
|
+
(`8px`) or handle them with direct CSS instead.
|
|
184
|
+
|
|
185
|
+
### C. Direct CSS on the element IDs
|
|
186
|
+
|
|
187
|
+
For anything the theme doesn't model — layout, stacking, spacing, responsive
|
|
188
|
+
behaviour, fonts:
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
#lanyard_root,
|
|
192
|
+
#lanyard_root * {
|
|
193
|
+
font-family: var(--font-sans), system-ui, sans-serif !important;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@media (max-width: 640px) {
|
|
197
|
+
#ketch-banner {
|
|
198
|
+
inset-inline: 0 !important;
|
|
199
|
+
width: 100% !important;
|
|
200
|
+
border-radius: 0 !important;
|
|
201
|
+
padding-bottom: max(1rem, var(--safe-area-inset-bottom, 0px)) !important;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#ketch-banner-buttons-container-standard {
|
|
205
|
+
flex-direction: column !important;
|
|
206
|
+
width: 100% !important;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`!important` is usually needed here because Ketch's internals are Tailwind
|
|
212
|
+
classes with an `!` important prefix reading the variables (e.g.
|
|
213
|
+
`!ketch-bg-[--k-banner-container-background-color]`).
|
|
214
|
+
|
|
215
|
+
Ketch exposes safe-area values on its root for notched devices:
|
|
216
|
+
`--safe-area-inset-top` / `-right` / `-bottom` / `-left`.
|
|
217
|
+
|
|
218
|
+
- **For:** total control, reaches everything.
|
|
219
|
+
- **Against:** couples to Ketch's DOM, so it's the first thing to break on an
|
|
220
|
+
SDK upgrade. Prefer B for colours and keep C for structure.
|
|
221
|
+
|
|
222
|
+
### D. Replace the UI entirely
|
|
223
|
+
|
|
224
|
+
Possible — suppress Ketch's experiences and drive consent with `setConsent` from
|
|
225
|
+
a custom component. **Think hard before doing this.** You take on per-jurisdiction
|
|
226
|
+
copy, GPC and ATT signal banners, vendor lists, TCF strings and the audit trail,
|
|
227
|
+
which is the bulk of what the CMP is paid for. Only justified when design
|
|
228
|
+
requirements genuinely cannot be met by A–C, and worth confirming with whoever
|
|
229
|
+
owns compliance.
|
|
230
|
+
|
|
231
|
+
## Verifying UI changes
|
|
232
|
+
|
|
233
|
+
CSS against a third-party widget can't be verified from a build — check in a
|
|
234
|
+
browser:
|
|
235
|
+
|
|
236
|
+
1. Clear site data so the banner shows again (or use `ketch("showConsent")`).
|
|
237
|
+
2. Banner: colours, fonts, button styles, and the link to the privacy policy.
|
|
238
|
+
3. Open "customize settings": purpose toggles, accept/reject buttons, scrolling.
|
|
239
|
+
4. Dismiss the banner and confirm the floating button is in the state you
|
|
240
|
+
intended — this is where a missing `!important` shows up.
|
|
241
|
+
5. Mobile viewport, and a notched device if safe-area padding matters.
|
|
242
|
+
6. Keyboard: tab through the banner and modal, confirm focus is visible and
|
|
243
|
+
trapped in the modal.
|
|
244
|
+
|
|
245
|
+
## Testing a different jurisdiction
|
|
246
|
+
|
|
247
|
+
The banner and preferences center render completely differently per
|
|
248
|
+
jurisdiction, so "the toggles are locked" or "Accept All does nothing" is
|
|
249
|
+
usually **not a bug** — it's the visitor resolving to a jurisdiction where every
|
|
250
|
+
purpose is `disclosure` (notice-only). Ketch then shows each purpose as
|
|
251
|
+
**"Always Active"** with the toggle disabled, and the accept/reject buttons have
|
|
252
|
+
nothing to change. Check the legal basis before debugging the UI:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
curl -s https://global.ketchcdn.com/web/v3/config/<org>/<property>/production/default/en/config.json \
|
|
256
|
+
| jq -r '.purposes[] | "\(.code) \(.legalBasisCode)"'
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Force a jurisdiction instead of relying on geo-IP by setting an attribute on the
|
|
260
|
+
root element:
|
|
261
|
+
|
|
262
|
+
```html
|
|
263
|
+
<html jurisdiction="gdpr">
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
For a quick manual check with no code change, Ketch also accepts a URL query
|
|
267
|
+
parameter that sets the same thing:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
https://example.com/?ketch_jurisdiction=gdpr
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Verified against a real property: LatAm traffic normally resolves to
|
|
274
|
+
`default` (notice-only), where consent is granted automatically and the
|
|
275
|
+
banner won't re-show on reload — see the gotcha below.
|
|
276
|
+
`?ketch_jurisdiction=gdpr` (or `US_Texas`, `US_California`, …) forces the
|
|
277
|
+
banner to render for that jurisdiction instead, with real Accept/Reject
|
|
278
|
+
buttons — no VPN needed. Use this to actually see the opt-in UI from a
|
|
279
|
+
region that defaults to notice-only.
|
|
280
|
+
|
|
281
|
+
In Next.js, drive it from a gitignored env var so it never ships:
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
<html
|
|
285
|
+
lang="en"
|
|
286
|
+
{...(forcedJurisdiction
|
|
287
|
+
? ({ jurisdiction: forcedJurisdiction } as Record<string, string>)
|
|
288
|
+
: {})}
|
|
289
|
+
>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Ketch's resolution order, highest priority first:
|
|
293
|
+
|
|
294
|
+
1. An override in Ketch's own storage
|
|
295
|
+
2. `config.jurisdiction.code`
|
|
296
|
+
3. **`document.documentElement.getAttribute("jurisdiction")`**
|
|
297
|
+
4. A `dataLayer` variable, if the property configures one
|
|
298
|
+
5. The region → jurisdiction map (`jurisdiction.scopes` in `boot.js`)
|
|
299
|
+
6. `defaultJurisdictionCode`
|
|
300
|
+
|
|
301
|
+
Because the attribute is checked before the region map, it works from anywhere
|
|
302
|
+
with no VPN. Try `gdpr` or `US_California` for opt-in UI with live toggles,
|
|
303
|
+
`US_Texas` for opt-out, `default` for notice-only.
|
|
304
|
+
|
|
305
|
+
Countries absent from `jurisdiction.scopes` fall through to `default`. If the
|
|
306
|
+
site has real traffic from a country that isn't mapped, that's a dashboard
|
|
307
|
+
decision for whoever owns compliance — surface it rather than working around it.
|
|
308
|
+
|
|
309
|
+
## Gotchas
|
|
310
|
+
|
|
311
|
+
- **Opt-out and notice-only jurisdictions don't offer Reject, and the banner
|
|
312
|
+
won't re-show.** Once Ketch auto-grants a jurisdiction's default record,
|
|
313
|
+
that resolves `isResolved` the same way an explicit accept would — no
|
|
314
|
+
reload brings the banner back, and there's no Reject All to click if you
|
|
315
|
+
were expecting one. This is the jurisdiction's legal basis, not a bug in
|
|
316
|
+
the integration or a broken build. Force a different jurisdiction (above)
|
|
317
|
+
before assuming something's wrong.
|
|
318
|
+
- **`options.appDivs`.** The property config can list element IDs that Ketch
|
|
319
|
+
gates on consent (commonly a chat widget container). If a third-party widget
|
|
320
|
+
mysteriously doesn't appear, check whether it's listed there.
|
|
321
|
+
- **Ketch normalises margins/padding** inside `#lanyard_root` with its own
|
|
322
|
+
injected stylesheet. Reset-style rules of your own may be redundant or fight
|
|
323
|
+
it.
|
|
324
|
+
- **Ketch's stylesheet is added on first experience render**, not at boot. A
|
|
325
|
+
rule that works on the banner may not have applied yet when you inspect
|
|
326
|
+
before it opens.
|
|
327
|
+
- **Custom CSS in the dashboard.** The property supports a `customCss` field
|
|
328
|
+
injected at runtime. If overrides behave unpredictably, check whether
|
|
329
|
+
something is already set there before adding more CSS in the repo.
|