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,277 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ashby-pagebuilder-block
|
|
3
|
+
description: >-
|
|
4
|
+
Register an Ashby job board as a selectable `ashbyJobBoardBlock` in a basement
|
|
5
|
+
Sanity page builder. Use when the user asks to "integrate/add Ashby to the page
|
|
6
|
+
builder", "add a job board / open positions block", "make the job list
|
|
7
|
+
selectable in the page builder / Studio", "register the Ashby block in Sanity",
|
|
8
|
+
or "wire up open positions so editors can drop them on a page". Also covers
|
|
9
|
+
rendering the Ashby job list directly (no CMS) when asked how to show open
|
|
10
|
+
positions.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Wire the Ashby job-board block into the page builder
|
|
14
|
+
|
|
15
|
+
The Ashby integration ships an agnostic core plus an optional Sanity adapter
|
|
16
|
+
(`lib/integrations/ashby/sanity/`). The adapter is **not** auto-registered into
|
|
17
|
+
the page builder (that wiring couples to each project's schema), so do it here by
|
|
18
|
+
editing the project's real files. Read each file first and adapt to its actual
|
|
19
|
+
shape — do not blindly pattern-match. If an anchor below is missing, stop and
|
|
20
|
+
report it rather than guessing.
|
|
21
|
+
|
|
22
|
+
## 0. Preconditions
|
|
23
|
+
|
|
24
|
+
- `lib/integrations/ashby/` exists (the integration was added).
|
|
25
|
+
- `lib/integrations/ashby/sanity/job-board-block.ts` exists (Sanity adapter —
|
|
26
|
+
only copied when Sanity is present). If it's missing, the project isn't a
|
|
27
|
+
Sanity project; skip to **Direct rendering** below.
|
|
28
|
+
- The project has a page builder: `lib/integrations/sanity/page-builder-config.ts`
|
|
29
|
+
exists. If not, it's the standard Sanity integration (no page builder) — skip to
|
|
30
|
+
**Direct rendering**.
|
|
31
|
+
- Read the `page-builder-block` skill first. Blocks in this template are defined
|
|
32
|
+
with `defineBlock` and drawn through a registry, and both halves are enforced
|
|
33
|
+
by a typecheck; the steps below are the Ashby-specific version of that recipe.
|
|
34
|
+
- `ASHBY_JOB_BOARD_NAME` is set (the slug in the board's `jobs.ashbyhq.com` URL),
|
|
35
|
+
or the editor will set a per-block `boardName`. No token is needed — Ashby's
|
|
36
|
+
job-board endpoint is public. `isAshbyConfigured()` from
|
|
37
|
+
`@/lib/integrations/check-integration` reports whether the env var resolves.
|
|
38
|
+
|
|
39
|
+
## 1. Re-wrap it with `defineBlock`, then register the schema
|
|
40
|
+
|
|
41
|
+
The shipped adapter uses plain `defineType` so it also works in the standard
|
|
42
|
+
Sanity integration, which has no page builder. In this template every block must
|
|
43
|
+
go through `defineBlock`
|
|
44
|
+
(`lib/integrations/sanity/schemas/shared/block.ts`) — it implies
|
|
45
|
+
`type: "document"` and appends **Anchor ID** and **Presentation Mode**, the two
|
|
46
|
+
fields the renderer reads for the anchor and the spacing. A block without them
|
|
47
|
+
fails `page-builder-config`'s conventions test.
|
|
48
|
+
|
|
49
|
+
Create `lib/integrations/sanity/schemas/components/reusable/ashby-job-board.ts`
|
|
50
|
+
that re-declares the adapter through the factory. Keep the adapter as the source
|
|
51
|
+
of the fields, so an integration update still flows through:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block"
|
|
55
|
+
import { ashbyJobBoardBlock as base } from "@/lib/integrations/ashby/sanity/job-board-block"
|
|
56
|
+
import { jobBoardIcon } from "@/lib/integrations/sanity/icons"
|
|
57
|
+
|
|
58
|
+
export const ashbyJobBoardBlock = defineBlock({
|
|
59
|
+
name: base.name,
|
|
60
|
+
title: base.title,
|
|
61
|
+
icon: jobBoardIcon,
|
|
62
|
+
description: base.description,
|
|
63
|
+
fields: base.fields,
|
|
64
|
+
preview: base.preview,
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`heading` is one of the fields `resolveAnchorId` walks, so the block gets a
|
|
69
|
+
generated anchor from the editor's heading with no extra work.
|
|
70
|
+
|
|
71
|
+
Export it from `lib/integrations/sanity/schemas/components/index.ts` and add it
|
|
72
|
+
to the `schema.types` array in `lib/integrations/sanity/schemas/index.ts`,
|
|
73
|
+
alongside the other component documents (near `hero` / `description`):
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { ashbyJobBoardBlock } from "./components"
|
|
77
|
+
// ...
|
|
78
|
+
export const schema: { types: SchemaTypeDefinition[] } = {
|
|
79
|
+
types: [
|
|
80
|
+
// ...existing component types...
|
|
81
|
+
ashbyJobBoardBlock,
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 2. Add it to the page-builder config — `lib/integrations/sanity/page-builder-config.ts`
|
|
87
|
+
|
|
88
|
+
Extend the `PageBuilderDocumentType` union and add a reusable reference member.
|
|
89
|
+
Match the member shape the file actually uses — in the current basement page
|
|
90
|
+
builder that's `pageTypes` (which page documents may host it) plus `category`
|
|
91
|
+
(which insert-menu group it lands in):
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
export type PageBuilderDocumentType =
|
|
95
|
+
| "hero"
|
|
96
|
+
| "description"
|
|
97
|
+
| "ashbyJobBoardBlock"
|
|
98
|
+
// ...rest...
|
|
99
|
+
|
|
100
|
+
// inside reusablePageBuilderReferenceMembers:
|
|
101
|
+
{
|
|
102
|
+
documentType: "ashbyJobBoardBlock",
|
|
103
|
+
name: "ashbyJobBoardBlockReference",
|
|
104
|
+
title: "Job Board",
|
|
105
|
+
pageTypes: ["generic"],
|
|
106
|
+
category: "list",
|
|
107
|
+
},
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 3. List it in the Studio — `lib/integrations/sanity/structure.ts`
|
|
111
|
+
|
|
112
|
+
Under the "Reusable Components" list, add (the schema's own case icon is used
|
|
113
|
+
automatically, so `.icon()` is optional):
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
S.documentTypeListItem("ashbyJobBoardBlock").title("Job Boards"),
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 4. Icon — `lib/integrations/sanity/icons.ts`
|
|
120
|
+
|
|
121
|
+
The adapter defaults to `@sanity/icons`' `CaseIcon`. This project uses phosphor
|
|
122
|
+
duotone throughout, so add one and pass it in step 1:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { BriefcaseIcon } from "@phosphor-icons/react/dist/ssr"
|
|
126
|
+
export const jobBoardIcon = asDuotoneIcon(BriefcaseIcon)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 5. Render it — a component plus a registry entry
|
|
130
|
+
|
|
131
|
+
Create `components/page-builder/components/ashby-job-board.tsx`. It must be an
|
|
132
|
+
**async Server Component** — `getCareerJobs` is server-only. Style it with the
|
|
133
|
+
project's own conventions (CSS modules vs Tailwind, `Link` primitive); the markup
|
|
134
|
+
below is a starting point, not a spec.
|
|
135
|
+
|
|
136
|
+
**No outer `<section>` and no vertical padding.** The renderer wraps every block
|
|
137
|
+
in `<Section>`, which owns the `<section>` element, the anchor `id` and the
|
|
138
|
+
padding the editor picked in Presentation Mode. A `py-*` here would fight it:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import type { ReactNode } from "react"
|
|
142
|
+
import { stegaClean } from "next-sanity"
|
|
143
|
+
import { Link } from "@/components/ui/link"
|
|
144
|
+
import type { CareerJob } from "@/lib/integrations/ashby"
|
|
145
|
+
import { getCareerJobs } from "@/lib/integrations/ashby"
|
|
146
|
+
import { isAshbyConfigured } from "@/lib/integrations/check-integration"
|
|
147
|
+
import type { PageBuilderBlock } from "../types"
|
|
148
|
+
|
|
149
|
+
type AshbyJobBoardProps = {
|
|
150
|
+
block: Extract<PageBuilderBlock, { _type: "ashbyJobBoardBlock" }>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function AshbyJobBoard({ block }: AshbyJobBoardProps) {
|
|
154
|
+
// stegaClean strips visual-editing metadata so the board name resolves.
|
|
155
|
+
const boardName = stegaClean(block.boardName)
|
|
156
|
+
if (!(boardName || isAshbyConfigured())) return null
|
|
157
|
+
|
|
158
|
+
let jobs: CareerJob[] = []
|
|
159
|
+
let hasError = false
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
jobs = await getCareerJobs(boardName)
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error("Failed to load Ashby jobs", error)
|
|
165
|
+
hasError = true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let body: ReactNode = null
|
|
169
|
+
|
|
170
|
+
if (hasError) {
|
|
171
|
+
body = (
|
|
172
|
+
<p className="text-base text-current/60">
|
|
173
|
+
Our job board is temporarily unavailable. Please try again shortly.
|
|
174
|
+
</p>
|
|
175
|
+
)
|
|
176
|
+
} else if (jobs.length === 0) {
|
|
177
|
+
body = (
|
|
178
|
+
<p className="text-base text-current/60">No open positions right now.</p>
|
|
179
|
+
)
|
|
180
|
+
} else {
|
|
181
|
+
body = (
|
|
182
|
+
<ul className="divide-y divide-current/10 border-current/10 border-t">
|
|
183
|
+
{jobs.map((job) => (
|
|
184
|
+
<li key={job.id}>
|
|
185
|
+
<Link
|
|
186
|
+
href={job.href}
|
|
187
|
+
className="flex flex-col gap-1 py-4 transition-opacity hover:opacity-70 md:flex-row md:items-center md:justify-between"
|
|
188
|
+
>
|
|
189
|
+
<span className="font-medium text-base">{job.title}</span>
|
|
190
|
+
<span className="text-current/60 text-sm">
|
|
191
|
+
{[job.department, job.location].filter(Boolean).join(" · ")}
|
|
192
|
+
</span>
|
|
193
|
+
</Link>
|
|
194
|
+
</li>
|
|
195
|
+
))}
|
|
196
|
+
</ul>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
{block.heading ? (
|
|
203
|
+
<h2 className="mb-6 font-semibold text-2xl tracking-tight md:text-3xl">
|
|
204
|
+
{block.heading}
|
|
205
|
+
</h2>
|
|
206
|
+
) : null}
|
|
207
|
+
{body}
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Then add one entry to `components/page-builder/registry.tsx`, keyed by schema
|
|
214
|
+
type. There is no switch to extend, and the map's key set is typed as exactly
|
|
215
|
+
`PageBuilderDocumentType` — miss this and `bun typecheck` fails:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { AshbyJobBoard } from "./components/ashby-job-board"
|
|
219
|
+
// ...inside blockRegistry:
|
|
220
|
+
ashbyJobBoardBlock: {
|
|
221
|
+
render: (block) => <AshbyJobBoard block={block} />,
|
|
222
|
+
},
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Don't add an `isEmpty` here: whether the board has open roles is only known after
|
|
226
|
+
the fetch, and `isEmpty` runs before the block renders. The component's own
|
|
227
|
+
"No open positions right now" copy covers the empty case.
|
|
228
|
+
|
|
229
|
+
The renderer is synchronous and returns this async component as a child, which
|
|
230
|
+
React resolves — no change to `Renderer` itself is needed. If the project runs
|
|
231
|
+
with **Cache Components** (`cacheComponents: true`), the live Ashby read is
|
|
232
|
+
uncached, so wrap the block in a `<Suspense>` boundary (or wrap `getCareerJobs`
|
|
233
|
+
in your own `"use cache"` function with `cacheLife`) to keep the route's static
|
|
234
|
+
shell prerenderable.
|
|
235
|
+
|
|
236
|
+
## 6. Regenerate types + verify
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
bun run sanity:typegen # regenerates sanity.types.ts so PageBuilderBlock includes the block
|
|
240
|
+
bun typecheck
|
|
241
|
+
bun lint
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`bun typecheck` must pass. The page query already spreads dereferenced block
|
|
245
|
+
fields (`...@->{ ... }`), so `heading`, `boardName`, `anchorId` and
|
|
246
|
+
`presentation` flow through with no query change.
|
|
247
|
+
|
|
248
|
+
Then check it in the Studio: the block's form ends with **Anchor ID** and
|
|
249
|
+
**Presentation Mode**, and dropping it on a page with the heading "Open roles"
|
|
250
|
+
renders `<section id="open-roles">` with the padding from the scale.
|
|
251
|
+
|
|
252
|
+
## Direct rendering (no page builder)
|
|
253
|
+
|
|
254
|
+
For any project, render the board without a CMS from a Server Component:
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
import { getCareerJobs } from "@/lib/integrations/ashby"
|
|
258
|
+
|
|
259
|
+
export default async function Careers() {
|
|
260
|
+
const jobs = await getCareerJobs()
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<ul>
|
|
264
|
+
{jobs.map((job) => (
|
|
265
|
+
<li key={job.id}>
|
|
266
|
+
<a href={job.href}>{job.title}</a>
|
|
267
|
+
</li>
|
|
268
|
+
))}
|
|
269
|
+
</ul>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`getCareerJobs()` reads `ASHBY_JOB_BOARD_NAME`; pass a board name to override it
|
|
275
|
+
(`getCareerJobs("other-board")`). It throws when the board name is missing or
|
|
276
|
+
Ashby returns a non-2xx status, so catch it if a job-board outage shouldn't fail
|
|
277
|
+
the page. See `lib/integrations/ashby/README.md` for the full `CareerJob` shape.
|
|
@@ -27,14 +27,45 @@ report it rather than guessing.
|
|
|
27
27
|
- The project has a page builder: `lib/integrations/sanity/page-builder-config.ts`
|
|
28
28
|
exists. If not, it's the standard Sanity integration (no page builder) — skip to
|
|
29
29
|
**Direct rendering**.
|
|
30
|
+
- Read the `page-builder-block` skill first. Blocks in this template are defined
|
|
31
|
+
with `defineBlock` and drawn through a registry, and both halves are enforced
|
|
32
|
+
by a typecheck; the steps below are the HubSpot-specific version of that recipe.
|
|
30
33
|
|
|
31
|
-
## 1.
|
|
34
|
+
## 1. Re-wrap it with `defineBlock`, then register the schema
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
The shipped adapter uses plain `defineType` so it also works in the standard
|
|
37
|
+
Sanity integration, which has no page builder. In this template every block must
|
|
38
|
+
go through `defineBlock`
|
|
39
|
+
(`lib/integrations/sanity/schemas/shared/block.ts`) — it implies
|
|
40
|
+
`type: "document"` and appends **Anchor ID** and **Presentation Mode**, the two
|
|
41
|
+
fields the renderer reads for the anchor and the spacing. A block without them
|
|
42
|
+
fails `page-builder-config`'s conventions test.
|
|
43
|
+
|
|
44
|
+
Create `lib/integrations/sanity/schemas/components/reusable/hubspot-form.ts`
|
|
45
|
+
that re-declares the adapter through the factory. Keep the adapter as the source
|
|
46
|
+
of the fields, so an integration update still flows through:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block"
|
|
50
|
+
import { hubspotFormBlock as base } from "@/lib/integrations/hubspot/sanity/form-block"
|
|
51
|
+
import { hubspotFormIcon } from "@/lib/integrations/sanity/icons"
|
|
52
|
+
|
|
53
|
+
export const hubspotFormBlock = defineBlock({
|
|
54
|
+
name: base.name,
|
|
55
|
+
title: base.title,
|
|
56
|
+
icon: hubspotFormIcon,
|
|
57
|
+
description: base.description,
|
|
58
|
+
fields: base.fields,
|
|
59
|
+
preview: base.preview,
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Export it from `lib/integrations/sanity/schemas/components/index.ts` and add it
|
|
64
|
+
to the `schema.types` array in `lib/integrations/sanity/schemas/index.ts`,
|
|
65
|
+
alongside the other component documents (near `hero` / `description`):
|
|
35
66
|
|
|
36
67
|
```ts
|
|
37
|
-
import { hubspotFormBlock } from "
|
|
68
|
+
import { hubspotFormBlock } from "./components"
|
|
38
69
|
// ...
|
|
39
70
|
export const schema: { types: SchemaTypeDefinition[] } = {
|
|
40
71
|
types: [
|
|
@@ -73,30 +104,23 @@ automatically, so `.icon()` is optional):
|
|
|
73
104
|
S.documentTypeListItem("hubspotFormBlock").title("HubSpot Forms"),
|
|
74
105
|
```
|
|
75
106
|
|
|
76
|
-
## 4.
|
|
107
|
+
## 4. Envelope icon — `lib/integrations/sanity/icons.ts`
|
|
77
108
|
|
|
78
|
-
The
|
|
79
|
-
|
|
109
|
+
The adapter defaults to `@sanity/icons`' `EnvelopeIcon`. This project uses
|
|
110
|
+
phosphor duotone throughout, so add one and pass it in step 1:
|
|
80
111
|
|
|
81
112
|
```ts
|
|
82
113
|
import { EnvelopeIcon } from "@phosphor-icons/react/dist/ssr"
|
|
83
114
|
export const hubspotFormIcon = asDuotoneIcon(EnvelopeIcon)
|
|
84
115
|
```
|
|
85
116
|
|
|
86
|
-
|
|
117
|
+
## 5. Render it — a component plus a registry entry
|
|
87
118
|
|
|
88
|
-
|
|
89
|
-
import { hubspotFormBlock as hubspotFormBlockBase } from "@/lib/integrations/hubspot/sanity/form-block"
|
|
90
|
-
import { hubspotFormIcon } from "../icons"
|
|
91
|
-
const hubspotFormBlock: SchemaTypeDefinition = {
|
|
92
|
-
...hubspotFormBlockBase,
|
|
93
|
-
icon: hubspotFormIcon,
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## 5. Render it — `components/page-builder/renderer.tsx` + a new component
|
|
119
|
+
Create `components/page-builder/components/hubspot-form.tsx`.
|
|
98
120
|
|
|
99
|
-
|
|
121
|
+
**No outer `<section>` and no vertical padding.** The renderer wraps every block
|
|
122
|
+
in `<Section>`, which owns the `<section>` element, the anchor `id` and the
|
|
123
|
+
padding the editor picked in Presentation Mode. A `py-*` here would fight it:
|
|
100
124
|
|
|
101
125
|
```tsx
|
|
102
126
|
import { stegaClean } from "next-sanity"
|
|
@@ -114,32 +138,41 @@ export function HubSpotFormBlock({ block }: HubSpotFormBlockProps) {
|
|
|
114
138
|
if (!selection) return null
|
|
115
139
|
|
|
116
140
|
return (
|
|
117
|
-
<
|
|
141
|
+
<div className="px-4">
|
|
118
142
|
<HubSpotForm
|
|
119
143
|
className="mx-auto max-w-[590px]"
|
|
120
144
|
formId={selection.formId}
|
|
121
145
|
portalId={selection.portalId}
|
|
122
146
|
region={selection.region}
|
|
123
147
|
/>
|
|
124
|
-
</
|
|
148
|
+
</div>
|
|
125
149
|
)
|
|
126
150
|
}
|
|
127
151
|
```
|
|
128
152
|
|
|
129
|
-
Then add
|
|
153
|
+
Then add one entry to `components/page-builder/registry.tsx`, keyed by schema
|
|
154
|
+
type. There is no switch to extend, and the map's key set is typed as exactly
|
|
155
|
+
`PageBuilderDocumentType` — miss this and `bun typecheck` fails:
|
|
130
156
|
|
|
131
157
|
```tsx
|
|
132
158
|
import { HubSpotFormBlock } from "./components/hubspot-form"
|
|
133
|
-
// ...
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
// ...inside blockRegistry:
|
|
160
|
+
hubspotFormBlock: {
|
|
161
|
+
render: (block) => <HubSpotFormBlock block={block} />,
|
|
162
|
+
isEmpty: (block) => !block.form,
|
|
163
|
+
},
|
|
136
164
|
```
|
|
137
165
|
|
|
166
|
+
`isEmpty` is what makes an unconfigured block leave no padded gap behind, and
|
|
167
|
+
keeps it from claiming an anchor id — prefer it over the early `return null`
|
|
168
|
+
above, which the renderer cannot see before it wraps the block.
|
|
169
|
+
|
|
138
170
|
## 6. Regenerate types + verify
|
|
139
171
|
|
|
140
172
|
```bash
|
|
141
173
|
bun run sanity:typegen # regenerates sanity.types.ts so PageBuilderBlock includes the block
|
|
142
174
|
bun typecheck
|
|
175
|
+
bun lint
|
|
143
176
|
```
|
|
144
177
|
|
|
145
178
|
`bun typecheck` must pass. The page query already spreads dereferenced block
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ketch-consent-check
|
|
3
|
+
description: >-
|
|
4
|
+
Gate a tracking or third-party script behind Ketch consent, using the
|
|
5
|
+
project's single consent helper. Use when the user asks to "add
|
|
6
|
+
GTM/PostHog/Claydar/Segment/Hotjar/Clarity", "wire up analytics", "add the
|
|
7
|
+
tracking script", "fire this only with consent", "check consent before
|
|
8
|
+
loading", "make tracking respect consent", "gate this behind cookie consent",
|
|
9
|
+
"stop tracking when someone opts out", or "revoking consent doesn't stop
|
|
10
|
+
tracking". Installing Ketch itself is the `ketch-consent-setup` skill.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Check consent before firing anything
|
|
14
|
+
|
|
15
|
+
There is exactly one way to ask whether tracking is allowed. Never read
|
|
16
|
+
`window.ketch`, `window.semaphore`, cookies or `localStorage` anywhere else —
|
|
17
|
+
that is how a codebase ends up with five slightly different, silently wrong
|
|
18
|
+
checks and no way to audit them.
|
|
19
|
+
|
|
20
|
+
Read each file first and adapt to its actual shape — do not blindly
|
|
21
|
+
pattern-match. If an anchor below is missing, stop and report it rather than
|
|
22
|
+
guessing.
|
|
23
|
+
|
|
24
|
+
## 0. Preconditions
|
|
25
|
+
|
|
26
|
+
- `lib/integrations/ketch/` exists. If it doesn't, the consent gate isn't
|
|
27
|
+
installed — stop and run the `ketch-consent-setup` skill first.
|
|
28
|
+
- `lib/integrations/ketch/use-consent.ts` exports `useConsent`. If the helper
|
|
29
|
+
has a different shape than below, follow the real file, not this doc.
|
|
30
|
+
|
|
31
|
+
## 1. Map the tracker to a category
|
|
32
|
+
|
|
33
|
+
Categories are `essential`, `analytics` and `marketing`. Add the tracker to
|
|
34
|
+
`TRACKER_CONSENT_CATEGORY` in `lib/integrations/ketch/categories.ts` so the
|
|
35
|
+
mapping lives in one place and is reviewable:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
export const TRACKER_CONSENT_CATEGORY = {
|
|
39
|
+
gtm: "analytics",
|
|
40
|
+
posthog: "analytics",
|
|
41
|
+
claydar: "marketing",
|
|
42
|
+
} as const satisfies Record<string, ConsentCategory>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
How to choose:
|
|
46
|
+
|
|
47
|
+
| The tracker… | Category |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| measures traffic or behaviour (analytics, session replay, heatmaps) | `analytics` |
|
|
50
|
+
| builds advertising profiles, retargets, or links identity across sites | `marketing` |
|
|
51
|
+
| is required for the site to function (session, security, CSRF) | `essential` |
|
|
52
|
+
|
|
53
|
+
`essential` is exempt from consent. Do not use it to sneak a tracker through —
|
|
54
|
+
if it sends data to a vendor for measurement or advertising, it is not
|
|
55
|
+
essential.
|
|
56
|
+
|
|
57
|
+
When a tool does both (GTM firing ad tags, PostHog with ads integrations
|
|
58
|
+
enabled), gate on the **stricter** category, or split it into two loaders.
|
|
59
|
+
|
|
60
|
+
## 2. Write the loader
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
"use client"
|
|
64
|
+
|
|
65
|
+
import { useEffect } from "react"
|
|
66
|
+
import { useConsent } from "@/lib/integrations/ketch/use-consent"
|
|
67
|
+
|
|
68
|
+
export const PostHogLoader = () => {
|
|
69
|
+
const { hasConsent } = useConsent()
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!hasConsent("analytics")) return
|
|
73
|
+
|
|
74
|
+
const posthog = startPostHog()
|
|
75
|
+
return () => posthog.shutdown()
|
|
76
|
+
}, [hasConsent])
|
|
77
|
+
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Mount it in the same layout as `KetchConsentScript` — the one that renders the
|
|
83
|
+
site chrome, typically `app/(content)/layout.tsx`, not the root layout. A
|
|
84
|
+
tracker mounted above the CMP would run on admin routes where Ketch never
|
|
85
|
+
loads, and `hasConsent` would sit at `false` forever.
|
|
86
|
+
|
|
87
|
+
Three things make this correct, and all three are load-bearing:
|
|
88
|
+
|
|
89
|
+
- **`hasConsent` returns `false` until Ketch resolves.** A tracker written this
|
|
90
|
+
way cannot fire before consent. Do not add an "optimistic" path, a timeout
|
|
91
|
+
fallback, or a "load it disabled and enable later" variant.
|
|
92
|
+
- **The cleanup function is what makes revocation work.** When the visitor turns
|
|
93
|
+
a category off, `hasConsent` changes, the effect re-runs, and the cleanup
|
|
94
|
+
tears the tracker down — no reload. Without a real cleanup, revoking consent
|
|
95
|
+
silently keeps collecting, which is the actual compliance failure.
|
|
96
|
+
- **The dependency is `hasConsent`, not `granted`.** `hasConsent` is stable
|
|
97
|
+
between consent changes, so the effect doesn't thrash.
|
|
98
|
+
|
|
99
|
+
`useConsent()` also returns `granted`, `isResolved`, `jurisdiction` and
|
|
100
|
+
`openPreferences()`.
|
|
101
|
+
|
|
102
|
+
### If the SDK has no teardown
|
|
103
|
+
|
|
104
|
+
Some tags can't be unloaded. Then do not load it inside the effect body at all:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!hasConsent("analytics")) return
|
|
109
|
+
|
|
110
|
+
let cancelled = false
|
|
111
|
+
const load = async () => {
|
|
112
|
+
const mod = await import("some-sdk")
|
|
113
|
+
if (cancelled) return
|
|
114
|
+
mod.init()
|
|
115
|
+
return mod
|
|
116
|
+
}
|
|
117
|
+
const pending = load()
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true
|
|
121
|
+
pending.then((mod) => mod?.optOut())
|
|
122
|
+
}
|
|
123
|
+
}, [hasConsent])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If there is no `optOut`, no `shutdown`, and no way to stop it, say so
|
|
127
|
+
explicitly rather than shipping a gate that only works one way — the vendor may
|
|
128
|
+
need to be swapped or fronted by a server-side proxy.
|
|
129
|
+
|
|
130
|
+
## 3. Scripts outside React
|
|
131
|
+
|
|
132
|
+
Subscribe to the same store — same default-deny semantics, same live updates:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { consentStore } from "@/lib/integrations/ketch/consent-store"
|
|
136
|
+
|
|
137
|
+
const unsubscribe = consentStore.subscribe(() => {
|
|
138
|
+
if (consentStore.hasConsent("marketing")) startClaydar()
|
|
139
|
+
else stopClaydar()
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`consentStore.hasConsent(category)` reads the current snapshot;
|
|
144
|
+
`consentStore.subscribe(fn)` fires on every change and returns an unsubscribe.
|
|
145
|
+
|
|
146
|
+
## 4. Do not
|
|
147
|
+
|
|
148
|
+
- **Do not inject the tag unconditionally and "configure consent mode" inside
|
|
149
|
+
it.** The tag must not load at all without consent. A tag that loads and
|
|
150
|
+
self-restricts still opens a connection and often still sets cookies.
|
|
151
|
+
- **Do not gate on `isResolved` alone.** That only says Ketch answered, not that
|
|
152
|
+
the visitor said yes.
|
|
153
|
+
- **Do not add a second source of truth** (own cookie, a context seeded on the
|
|
154
|
+
server, a `useState` copy). Ketch already persists across navigation and
|
|
155
|
+
reloads; two sources drift and the bug is invisible.
|
|
156
|
+
- **Do not read the Ketch purpose code directly** (`"targeted_advertising"`).
|
|
157
|
+
Go through the category names so a purpose rename is a one-line change.
|
|
158
|
+
- **Do not gate with `dynamic()` / `next/script` alone.** Deferring a script is
|
|
159
|
+
not consent — it still loads.
|
|
160
|
+
|
|
161
|
+
## 5. Verify
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
bun run typecheck && bun run lint && bun run build
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Then in the browser, on a page that renders the footer (in a Sanity
|
|
168
|
+
page-builder project `/` often 404s and the 404 has no footer — use a real
|
|
169
|
+
slug):
|
|
170
|
+
|
|
171
|
+
1. **Nothing fires before consent.** Fresh profile or cleared storage, open the
|
|
172
|
+
Network tab, filter by the vendor's domain, load the page → zero requests.
|
|
173
|
+
This is the acceptance criterion; check it per tracker, not once.
|
|
174
|
+
2. **Accepting starts it.** Accept in the banner → the vendor's request appears
|
|
175
|
+
without a reload.
|
|
176
|
+
3. **Revoking stops it.** Open preferences from the footer, turn the category
|
|
177
|
+
off → no further requests, still without a reload. This is what proves the
|
|
178
|
+
cleanup is wired; it is the step most likely to be silently broken.
|
|
179
|
+
4. **It survives a reload.** Reload and confirm the choice held, and that the
|
|
180
|
+
tracker's state matches it.
|
|
181
|
+
|
|
182
|
+
If step 3 passes only after a reload, the effect cleanup is missing or the
|
|
183
|
+
tracker was loaded outside the effect. Fix that, don't document it as a
|
|
184
|
+
limitation.
|
|
185
|
+
|
|
186
|
+
## Region note
|
|
187
|
+
|
|
188
|
+
The helper defaults to denied until Ketch confirms, so trackers stay off even
|
|
189
|
+
in opt-out jurisdictions until the consent record arrives. That is stricter
|
|
190
|
+
than the law requires and intentional — do not "optimise" it away. The
|
|
191
|
+
region-by-region table lives with the project's Ketch documentation.
|
|
192
|
+
|
|
193
|
+
Steps 2 and 3 of Verify need an explicit Accept/Reject to test, which only
|
|
194
|
+
opt-in jurisdictions (EEA/UK, California, Quebec) show. From a default
|
|
195
|
+
(notice-only) or opt-out region, consent is auto-granted with no banner
|
|
196
|
+
interaction — append `?ketch_jurisdiction=gdpr` to the URL to test the real
|
|
197
|
+
gated behaviour instead of assuming it's broken.
|