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.
Files changed (174) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +23 -0
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/domain/consent.d.ts +17 -0
  5. package/dist/domain/consent.d.ts.map +1 -0
  6. package/dist/domain/consent.js +13 -0
  7. package/dist/domain/consent.js.map +1 -0
  8. package/dist/domain/integration.d.ts +19 -2
  9. package/dist/domain/integration.d.ts.map +1 -1
  10. package/dist/domain/integration.js +10 -1
  11. package/dist/domain/integration.js.map +1 -1
  12. package/dist/domain/jobs.d.ts +17 -0
  13. package/dist/domain/jobs.d.ts.map +1 -0
  14. package/dist/domain/jobs.js +13 -0
  15. package/dist/domain/jobs.js.map +1 -0
  16. package/dist/domain/skills.d.ts +23 -2
  17. package/dist/domain/skills.d.ts.map +1 -1
  18. package/dist/domain/skills.js +23 -2
  19. package/dist/domain/skills.js.map +1 -1
  20. package/dist/domain/templates.d.ts +7 -1
  21. package/dist/domain/templates.d.ts.map +1 -1
  22. package/dist/domain/templates.js +2 -0
  23. package/dist/domain/templates.js.map +1 -1
  24. package/dist/index.js +34 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  27. package/dist/infrastructure/create/setup-agent.js +11 -4
  28. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  29. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  30. package/dist/infrastructure/integrations/inject-integration.js +36 -2
  31. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  32. package/dist/modules/features/consent/ketch/config.d.ts +18 -0
  33. package/dist/modules/features/consent/ketch/config.d.ts.map +1 -0
  34. package/dist/modules/features/consent/ketch/config.js +24 -0
  35. package/dist/modules/features/consent/ketch/config.js.map +1 -0
  36. package/dist/modules/features/consent/ketch/mergers/biome.d.ts +16 -0
  37. package/dist/modules/features/consent/ketch/mergers/biome.d.ts.map +1 -0
  38. package/dist/modules/features/consent/ketch/mergers/biome.js +31 -0
  39. package/dist/modules/features/consent/ketch/mergers/biome.js.map +1 -0
  40. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts +13 -0
  41. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts.map +1 -0
  42. package/dist/modules/features/consent/ketch/mergers/check-integration.js +64 -0
  43. package/dist/modules/features/consent/ketch/mergers/check-integration.js.map +1 -0
  44. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts +12 -0
  45. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts.map +1 -0
  46. package/dist/modules/features/consent/ketch/mergers/env-example.js +28 -0
  47. package/dist/modules/features/consent/ketch/mergers/env-example.js.map +1 -0
  48. package/dist/modules/features/consent/ketch/mergers/footer.d.ts +17 -0
  49. package/dist/modules/features/consent/ketch/mergers/footer.d.ts.map +1 -0
  50. package/dist/modules/features/consent/ketch/mergers/footer.js +40 -0
  51. package/dist/modules/features/consent/ketch/mergers/footer.js.map +1 -0
  52. package/dist/modules/features/consent/ketch/mergers/knip.d.ts +18 -0
  53. package/dist/modules/features/consent/ketch/mergers/knip.d.ts.map +1 -0
  54. package/dist/modules/features/consent/ketch/mergers/knip.js +37 -0
  55. package/dist/modules/features/consent/ketch/mergers/knip.js.map +1 -0
  56. package/dist/modules/features/consent/ketch/mergers/layout.d.ts +25 -0
  57. package/dist/modules/features/consent/ketch/mergers/layout.d.ts.map +1 -0
  58. package/dist/modules/features/consent/ketch/mergers/layout.js +55 -0
  59. package/dist/modules/features/consent/ketch/mergers/layout.js.map +1 -0
  60. package/dist/modules/features/jobs/ashby/config.d.ts +19 -0
  61. package/dist/modules/features/jobs/ashby/config.d.ts.map +1 -0
  62. package/dist/modules/features/jobs/ashby/config.js +29 -0
  63. package/dist/modules/features/jobs/ashby/config.js.map +1 -0
  64. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts +14 -0
  65. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts.map +1 -0
  66. package/dist/modules/features/jobs/ashby/mergers/check-integration.js +78 -0
  67. package/dist/modules/features/jobs/ashby/mergers/check-integration.js.map +1 -0
  68. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts +12 -0
  69. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts.map +1 -0
  70. package/dist/modules/features/jobs/ashby/mergers/env-example.js +27 -0
  71. package/dist/modules/features/jobs/ashby/mergers/env-example.js.map +1 -0
  72. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts +18 -0
  73. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts.map +1 -0
  74. package/dist/modules/features/jobs/ashby/mergers/knip.js +41 -0
  75. package/dist/modules/features/jobs/ashby/mergers/knip.js.map +1 -0
  76. package/dist/paths.d.ts +12 -0
  77. package/dist/paths.d.ts.map +1 -1
  78. package/dist/paths.js +12 -0
  79. package/dist/paths.js.map +1 -1
  80. package/package.json +3 -1
  81. package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +277 -0
  82. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +58 -25
  83. package/src/agent-skills/ketch-consent-check/SKILL.md +197 -0
  84. package/src/agent-skills/ketch-consent-setup/SKILL.md +326 -0
  85. package/src/agent-skills/ketch-consent-setup/references/ui-customization.md +329 -0
  86. package/src/agent-skills/page-builder-block/SKILL.md +150 -0
  87. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  88. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  89. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  90. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
  91. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
  92. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
  93. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  94. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  95. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  96. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  97. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  98. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  99. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +8 -0
  100. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +7 -0
  106. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  107. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  108. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  109. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
  110. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
  111. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
  112. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  113. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  114. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  115. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  116. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  117. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  118. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +8 -0
  119. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  120. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  121. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  122. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  123. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  124. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +7 -0
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
  126. package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
  127. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
  128. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
  129. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
  130. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
  131. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
  132. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
  133. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
  134. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
  135. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
  136. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
  137. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
  138. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
  139. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
  140. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
  141. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
  142. package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
  143. package/src/templates/next-pagebuilder/AGENTS.md +129 -0
  144. package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
  145. package/src/templates/next-pagebuilder/README.md +70 -0
  146. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
  147. package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
  148. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +1 -1
  149. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
  150. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
  151. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
  152. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
  153. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
  154. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
  155. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
  156. package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
  157. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
  158. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
  159. package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
  160. package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
  161. package/src/templates/next-pagebuilder-cache/README.md +75 -0
  162. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
  163. package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
  164. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +1 -1
  165. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
  166. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
  167. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
  168. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
  169. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
  170. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
  171. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
  172. package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
  173. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
  174. 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. Register the schema — `lib/integrations/sanity/schemas/index.ts`
34
+ ## 1. Re-wrap it with `defineBlock`, then register the schema
32
35
 
33
- Add the import and include the block in the `schema.types` array (alongside the
34
- other component documents, e.g. near `hero` / `description`):
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 "@/lib/integrations/hubspot/sanity/form-block"
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. (Optional) Envelope icon — `lib/integrations/sanity/icons.ts`
107
+ ## 4. Envelope icon — `lib/integrations/sanity/icons.ts`
77
108
 
78
- The block already defaults to `@sanity/icons`' `EnvelopeIcon`. To match this
79
- project's phosphor duotone icons, add and apply one:
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
- Then override the icon where the block is registered in `schemas/index.ts`:
117
+ ## 5. Render it — a component plus a registry entry
87
118
 
88
- ```ts
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
- Create `components/page-builder/components/hubspot-form.tsx`:
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
- <section className="px-4 py-8 md:py-12">
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
- </section>
148
+ </div>
125
149
  )
126
150
  }
127
151
  ```
128
152
 
129
- Then add the case to the renderer's `switch (block._type)`:
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
- case "hubspotFormBlock":
135
- return <HubSpotFormBlock block={block} />
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.