bsmnt 0.11.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 (217) 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/files/components/ui/sanity-image/index.tsx +36 -4
  88. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +7 -1
  89. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +7 -1
  90. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
  91. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  92. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  93. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  94. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  95. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  96. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  97. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
  98. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
  99. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +72 -34
  100. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  106. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  109. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +11 -0
  110. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  111. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  112. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +9 -0
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
  117. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +7 -1
  118. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +36 -4
  119. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  120. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  121. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  122. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  123. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  124. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
  126. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
  127. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +72 -34
  128. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
  129. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  130. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  131. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  132. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  133. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  134. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +11 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +9 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +5 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
  146. package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
  147. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
  148. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
  149. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
  150. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
  151. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
  152. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
  153. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
  154. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
  155. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
  156. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
  157. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
  158. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
  159. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
  160. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
  161. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
  162. package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
  163. package/src/templates/next-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
  164. package/src/templates/next-default/biome.json +2 -1
  165. package/src/templates/next-default/components/ui/image/index.tsx +20 -12
  166. package/src/templates/next-default/next.config.ts +10 -4
  167. package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
  168. package/src/templates/next-experiments/biome.json +2 -1
  169. package/src/templates/next-experiments/components/ui/image/index.tsx +20 -12
  170. package/src/templates/next-experiments/next.config.ts +10 -4
  171. package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -0
  172. package/src/templates/next-pagebuilder/AGENTS.md +129 -0
  173. package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
  174. package/src/templates/next-pagebuilder/README.md +70 -0
  175. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
  176. package/src/templates/next-pagebuilder/biome.json +2 -1
  177. package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
  178. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +6 -1
  179. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  180. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
  181. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
  182. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
  183. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
  184. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
  185. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
  186. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
  187. package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
  188. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
  189. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +20 -12
  190. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
  191. package/src/templates/next-pagebuilder/next.config.ts +4 -3
  192. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
  193. package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
  194. package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
  195. package/src/templates/next-pagebuilder-cache/README.md +75 -0
  196. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
  197. package/src/templates/next-pagebuilder-cache/biome.json +2 -1
  198. package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
  199. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +6 -1
  200. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  201. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
  202. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
  203. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
  204. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
  205. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
  206. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
  207. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
  208. package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
  209. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
  210. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +21 -12
  211. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
  212. package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
  213. package/src/templates/next-pagebuilder-cache/next.config.ts +4 -3
  214. package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
  215. package/src/templates/next-webgl/biome.json +2 -1
  216. package/src/templates/next-webgl/components/ui/image/index.tsx +20 -12
  217. package/src/templates/next-webgl/next.config.ts +10 -4
@@ -77,7 +77,6 @@ const nextConfig: NextConfig = {
77
77
  ],
78
78
  },
79
79
  images: {
80
- dangerouslyAllowSVG: true,
81
80
  remotePatterns: [
82
81
  {
83
82
  protocol: "https",
@@ -85,9 +84,11 @@ const nextConfig: NextConfig = {
85
84
  },
86
85
  ],
87
86
  minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
88
- qualities: [90],
87
+ qualities: [75, 90, 100],
89
88
  formats: ["image/avif", "image/webp"],
90
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
89
+ deviceSizes: [
90
+ 640, 750, 828, 1080, 1200, 1320, 1620, 1920, 2048, 2560, 3024, 3456, 3840,
91
+ ],
91
92
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
92
93
  },
93
94
  headers: async () => [
@@ -0,0 +1,11 @@
1
+ language js
2
+
3
+ or {
4
+ `<Image $attrs />`,
5
+ `<SanityImage $attrs />`
6
+ } as $image where {
7
+ $attrs <: contains `fill`,
8
+ ! $attrs <: contains `fill={false}`,
9
+ ! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
10
+ register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
11
+ }
@@ -0,0 +1,136 @@
1
+ # AGENTS.md
2
+
3
+ Instructions for AI agents working in this repository. Human-facing setup,
4
+ environment variables and the publish webhook live in the README — read it too.
5
+
6
+ This is a **Next.js + Sanity page builder**: all URL structure is managed in
7
+ Sanity, not in code, and a single catch-all route `[[...slug]]` renders every
8
+ page.
9
+
10
+ **Never add a page route under `app/`.** A new page is a `page` document in the
11
+ Studio with a slug, not a directory. `app/(content)/[[...slug]]/page.tsx`
12
+ resolves every one of them; a hand-written route shadows that resolver and
13
+ becomes a URL no editor can see, move or delete. The only things that belong in
14
+ `app/` are non-page endpoints — the Studio, API handlers, sitemap, feed.
15
+
16
+ It runs with [Cache Components](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents)
17
+ (`cacheComponents: true`), so every Sanity read sits behind a `'use cache'`
18
+ boundary and lands in the static shell. Nothing is time-based — the publish
19
+ webhook is what invalidates. A new read that isn't cached or wrapped in
20
+ `<Suspense>` takes its whole route out of the static shell, so keep the
21
+ boundaries where they are unless you mean to move them.
22
+
23
+ ## Page-builder blocks
24
+
25
+ **A block is `defineBlock` + a registry entry.** Define the schema with
26
+ `defineBlock` (`lib/integrations/sanity/schemas/shared/block.ts`), never
27
+ `defineType` — it implies `type: "document"` and appends **Anchor ID** and
28
+ **Presentation Mode** for you. Register the component in
29
+ `components/page-builder/registry.tsx`; the renderer is driven by that map, so
30
+ there is no switch to extend.
31
+
32
+ Both halves are enforced, not documented: the registry's key set is typed as
33
+ exactly `PageBuilderDocumentType`, so a block registered in
34
+ `page-builder-config.ts` without an entry there — or an entry there for a block
35
+ that was never registered — is a typecheck error.
36
+
37
+ **Every block declares two orthogonal axes** in `page-builder-config.ts`:
38
+ `pageTypes` (which page documents may host it — drives validation) and
39
+ `category` (what the block structurally is, one of `componentCategories` —
40
+ drives the insert menu; `misc` only when nothing else fits). Never collapse them
41
+ into one field: that is what made the menu a single flat list.
42
+
43
+ **And a preview image.** The insert menu renders one tile per block from a
44
+ `studioPreview` document (Components → Component Previews) keyed
45
+ `block.<memberName>`. A block without one falls back to its schema icon, which
46
+ is what made the old menu unreadable.
47
+
48
+ **Never hardcode vertical padding in a block component.** The renderer wraps
49
+ each block in `<Section>` (`components/page-builder/section.tsx`), which owns
50
+ the `<section>` element, the anchor `id`, `scroll-mt` and the responsive padding
51
+ top/bottom. A block component returns its content only — no outer `<section>`,
52
+ no `py-*` / `pt-*` / `pb-*` on its root. Internal gaps between a block's own
53
+ parts are fine.
54
+
55
+ **Never hardcode px for section spacing either.** The scale lives in the Studio
56
+ at **Layout → Spacing Scale**, where editors add or rename steps and mark one as
57
+ the default, and it reaches the page as CSS custom properties. `Section` only
58
+ references `var(--section-space-<key>, var(--section-space-default, 2.5rem))`.
59
+
60
+ **An empty Anchor ID is generated from the block's title** (`headline` /
61
+ `heading` / `teaserHeading` / `title` / `label` — never `internalTitle`, which is
62
+ Studio-only) and de-duplicated per page. Name your block's headline field one of
63
+ those and anchor links work for free.
64
+
65
+ Declare `isEmpty` in the registry when a block can be left unfilled — the
66
+ renderer then skips the block *and* its `<Section>`, so it leaves no padded gap
67
+ and claims no anchor id. Don't early-return `null` from the component instead;
68
+ the renderer has to know before it wraps anything.
69
+
70
+ Full recipe and a checklist for a new block: the `page-builder-block` skill, or
71
+ the "Page-builder blocks" section of the README.
72
+
73
+ ## Sanity
74
+
75
+ - Everything Sanity lives in `lib/integrations/sanity/`.
76
+ - Schemas use `defineType` / `defineField` / `defineArrayMember` (blocks use
77
+ `defineBlock`). GROQ queries are `SCREAMING_SNAKE_CASE` and wrapped in
78
+ `defineQuery`.
79
+ - **Regenerate types after any schema or query change:** `bun sanity:typegen`,
80
+ and commit the result in the same PR. Never hand-edit `sanity.types.ts`.
81
+ - The page query spreads `...` for non-gated blocks, so new block fields flow
82
+ through on their own — except `gatedContent`, whose GROQ branch lists fields
83
+ explicitly in `lib/integrations/sanity/queries.ts`. Add fields there by hand.
84
+ - Read through `sanityFetch` / `fetchSanity`, not a bare client, so the
85
+ draft-mode perspective and cache tags are applied.
86
+ - Publishing a document must invalidate what renders it. Layout singletons are
87
+ listed in `SANITY_LAYOUT_TAGS`; a new one that isn't there serves stale.
88
+ - Add `data-sanity` attributes where visual editing should reach.
89
+ - Nested layouts (e.g. `app/studio/layout.tsx`) must NOT render `<html>` or
90
+ `<body>` — the root layout already provides them.
91
+
92
+ ## React and Next.js
93
+
94
+ - **React Compiler is on.** Do not write `useMemo`, `useCallback` or
95
+ `React.memo`. The one exception is `useRef` to instantiate an object once:
96
+
97
+ ```tsx
98
+ const instanceRef = useRef<SomeClass | null>(null)
99
+ if (!instanceRef.current) instanceRef.current = new SomeClass()
100
+ ```
101
+
102
+ - React 19: refs are regular props, so no `forwardRef` (a lint rule enforces it).
103
+ - **Never call `headers()`, `cookies()`, `draftMode()` or read `searchParams` in
104
+ a root layout** — it takes the whole page out of the prerender. Detect the
105
+ route with `useSelectedLayoutSegment()` in a `"use client"` wrapper, and put
106
+ draft-mode / visual-editing behaviour in a child component with its own
107
+ `<Suspense>` boundary.
108
+ - Use `@/components/ui/image` rather than `next/image` directly, and
109
+ `@/components/ui/sanity-image` for Sanity assets.
110
+ - Compose classNames with `cn` from `@/lib/styles/cn`.
111
+ - Import with the `@/` alias; relative parent imports are a lint error.
112
+ - Gate debug UI behind `process.env.NODE_ENV === "development"` — it is not
113
+ stripped in production otherwise.
114
+
115
+ ## House rules
116
+
117
+ - **Bun only.** `bun install`, `bun run <script>`, `bunx`. Never npm, pnpm or
118
+ yarn — a second lockfile next to `bun.lock` is how the versions drift.
119
+ - **Don't add a dependency without asking.** Check `bun info <package>` and the
120
+ current docs first.
121
+ <!-- bsmnt:if dotenvx -->
122
+ - **Secrets are encrypted at rest with dotenvx.** Never put a real secret in
123
+ plaintext in a committed env file — gitleaks blocks the commit. Use
124
+ `bun env:set KEY value` (`--plain` for trivial config and `NEXT_PUBLIC_*`).
125
+ The `manage-env` skill has the full procedure.
126
+ <!-- bsmnt:endif -->
127
+ - Don't leave commented-out code or `TODO`s behind; write the code so it reads
128
+ without them.
129
+ - **Verify before claiming done:**
130
+
131
+ ```bash
132
+ bun sanity:typegen && bun typecheck && bun lint && bun run build && bun knip
133
+ ```
134
+
135
+ (`bun run build`, not `bun build` — the bare form is Bun's bundler.) Say what
136
+ you ran and what it printed; don't report success you haven't seen.
@@ -0,0 +1,3 @@
1
+ # CLAUDE.md
2
+
3
+ @AGENTS.md
@@ -19,9 +19,84 @@ bun dev
19
19
 
20
20
  - Single catch-all route `[[...slug]]` handles all pages
21
21
  - Sanity page builder with composable content blocks
22
+ - Editor-owned vertical spacing scale and per-block anchor links
22
23
  - Blog with categories, filtering, and pagination
23
24
  - Visual editing and live preview support
24
25
 
26
+ ## Page-builder blocks
27
+
28
+ A block is two halves — a schema document and a React component — and each half
29
+ has one place that makes it a block rather than an ordinary document or a loose
30
+ component. Use them and Anchor ID, Presentation Mode and the spacing scale come
31
+ for free.
32
+
33
+ **Define the schema with `defineBlock`, never `defineType`**
34
+ (`lib/integrations/sanity/schemas/shared/block.ts`). It implies
35
+ `type: "document"` and appends two fields to every block:
36
+
37
+ - **Anchor ID** — optional. Left empty, the anchor is generated from the block's
38
+ own title at render time (`headline` / `heading` / `teaserHeading` / `title` /
39
+ `label` — never `internalTitle`, which is Studio-only) and de-duplicated per
40
+ page, so two blocks with the same heading get `-2`. A value typed by hand
41
+ always wins, and the Studio shows the generated one as the placeholder. Link
42
+ to it as `/<page>#<anchor>`.
43
+ - **Presentation Mode** — padding top and bottom, each picked from the shared
44
+ spacing scale. No free-form px. A block that never touches it follows whichever
45
+ step is marked as the default.
46
+
47
+ Blocks that declare `groups` get a **Presentation** tab for both fields
48
+ automatically — don't list it in `groups` yourself.
49
+
50
+ **Register the component in `components/page-builder/registry.tsx`.** One entry
51
+ per block, keyed by schema type; the renderer is driven by that map, so there is
52
+ no switch to extend. `isEmpty` declares when a block has nothing to show — the
53
+ renderer then skips the block *and* its `<Section>`, so an unfilled block leaves
54
+ no padded gap behind and claims no anchor id. The registry's key set is typed as
55
+ exactly `PageBuilderDocumentType`, so a block registered in one place and not the
56
+ other is a typecheck error.
57
+
58
+ **Never hardcode vertical padding in a block component.** The renderer wraps each
59
+ block in `<Section>` (`components/page-builder/section.tsx`), which owns the
60
+ `<section>` element, the anchor `id`, `scroll-mt` and the padding. A block
61
+ component returns its content only — no outer `<section>`, no `py-*` / `pt-*` /
62
+ `pb-*` on its root. Internal gaps between a block's own parts are fine.
63
+
64
+ ### The spacing scale is content, not code
65
+
66
+ Editors own it at **Layout → Spacing Scale**: a list of steps, each with a name
67
+ and px per breakpoint (mobile / tablet / desktop, empty reuses Desktop), one
68
+ marked as the default. Ticking a second step as default unticks the previous, so
69
+ there is always exactly one. Add a "Hero 120px" step and it shows up in every
70
+ block's dropdown, no deploy.
71
+
72
+ - Blocks store the step's `_key`, so renaming a step breaks nothing and a deleted
73
+ step degrades to the default instead of breaking the page.
74
+ - The list arrives seeded with the built-in five (XS 16 · SM 24 · **MD 32,
75
+ default** · LG 48 · XL 80, from `schemas/shared/spacing.ts`), which also apply
76
+ as the fallback if you empty it. Hit **"Add the built-in steps"** in the field
77
+ to get them back as editable rows.
78
+ - Values reach the page as CSS custom properties injected server-side by
79
+ `components/layout/spacing-scale/`, one `--section-space-<key>` per step plus
80
+ `--section-space-default`, re-declared inside the `768px` and `1440px` media
81
+ queries. `Section` only ever references
82
+ `var(--section-space-<key>, var(--section-space-default, 2.5rem))`, so an
83
+ unknown key degrades instead of breaking.
84
+
85
+ Under Cache Components the scale is read inside a `'use cache'` boundary and
86
+ prerenders into the static shell alongside the rest of the chrome, so the first
87
+ paint already carries the variables; draft mode streams the editor's copy behind
88
+ a `<Suspense>` that falls back to the built-in steps.
89
+ - `spacingSettings` is a layout singleton, so publishing it busts the layout
90
+ cache the same way navbar and footer do.
91
+
92
+ After a schema change run `bun sanity:typegen`. The page query spreads `...` for
93
+ non-gated blocks, so new fields flow through on their own — except
94
+ `gatedContent`, whose GROQ branch lists fields explicitly in
95
+ `lib/integrations/sanity/queries.ts`.
96
+
97
+ The bundled `page-builder-block` agent skill has the full recipe and a checklist
98
+ for adding a new block.
99
+
25
100
  ## Commands
26
101
 
27
102
  - `bun dev` - Start dev server
@@ -1,5 +1,6 @@
1
1
  import { draftMode } from "next/headers"
2
2
  import { JsonLd } from "@/components/layout/json-ld"
3
+ import { SpacingScale } from "@/components/layout/spacing-scale"
3
4
  import { Wrapper } from "@/components/layout/wrapper"
4
5
  import { SanityVisualEditing } from "@/components/sanity/visual-editing"
5
6
  import { SanityLive } from "@/lib/integrations/sanity/live"
@@ -11,6 +12,7 @@ const Layout = async ({ children }: { children: React.ReactNode }) => {
11
12
 
12
13
  return (
13
14
  <>
15
+ <SpacingScale isDraftMode={isDraftMode} />
14
16
  <JsonLd isDraftMode={isDraftMode} />
15
17
  <Wrapper isDraftMode={isDraftMode}>{children}</Wrapper>
16
18
  {/* Draft mode only: for the public this costs a live connection and a
@@ -247,7 +247,8 @@
247
247
  "plugins": [
248
248
  "./.biome/plugins/no-anchor-element.grit",
249
249
  "./.biome/plugins/no-unnecessary-forwardref.grit",
250
- "./.biome/plugins/no-relative-parent-imports.grit"
250
+ "./.biome/plugins/no-relative-parent-imports.grit",
251
+ "./.biome/plugins/no-fill-without-sizes.grit"
251
252
  ],
252
253
  "vcs": {
253
254
  "clientKind": "git",
@@ -0,0 +1,46 @@
1
+ import { Suspense } from "react"
2
+ import { getLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
3
+ import {
4
+ type DynamicFetchOptions,
5
+ getDynamicFetchOptions,
6
+ PUBLISHED_FETCH_OPTIONS,
7
+ } from "@/lib/integrations/sanity/live"
8
+ import {
9
+ defaultSpacingSteps,
10
+ resolveSpacingSteps,
11
+ type SpacingStep,
12
+ spacingScaleCss,
13
+ } from "@/lib/integrations/sanity/schemas/shared/spacing"
14
+
15
+ const SpacingScaleStyle = ({ steps }: { steps: SpacingStep[] }) => (
16
+ <style
17
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: css custom properties from the spacing singleton
18
+ dangerouslySetInnerHTML={{ __html: spacingScaleCss(steps) }}
19
+ />
20
+ )
21
+
22
+ // Cached into the static shell so the first paint already has the scale; the
23
+ // built-in steps stand in while draft mode streams the editor's own.
24
+ export const SpacingScale = ({ isDraftMode }: { isDraftMode: boolean }) =>
25
+ isDraftMode ? (
26
+ <Suspense fallback={<SpacingScaleStyle steps={defaultSpacingSteps} />}>
27
+ <DynamicSpacingScale />
28
+ </Suspense>
29
+ ) : (
30
+ <CachedSpacingScale {...PUBLISHED_FETCH_OPTIONS} />
31
+ )
32
+
33
+ async function DynamicSpacingScale() {
34
+ const fetchOptions = await getDynamicFetchOptions()
35
+ return <CachedSpacingScale {...fetchOptions} />
36
+ }
37
+
38
+ async function CachedSpacingScale(fetchOptions: DynamicFetchOptions) {
39
+ "use cache"
40
+ // One entry, shared with the header, footer and json-ld.
41
+ const { spacingSettings } = await getLayoutData(fetchOptions)
42
+
43
+ // Don't cap this branch: it renders in every route's shell and a nested
44
+ // lifetime propagates as a min(). SELF_TAGGED_TYPES clears it on publish.
45
+ return <SpacingScaleStyle steps={resolveSpacingSteps(spacingSettings?.steps)} />
46
+ }
@@ -7,7 +7,7 @@ import { RelatedPostItem } from "./related-post-item"
7
7
  export function ArticleContent({ block }: ArticleContentProps) {
8
8
  const { author, body, categories, date, relatedPosts, thumbnail } = block
9
9
  return (
10
- <div className="py-8 md:py-12">
10
+ <div>
11
11
  <header className="mx-auto max-w-3xl space-y-6">
12
12
  {categories && categories.length > 0 ? (
13
13
  <div className="flex flex-wrap gap-2">
@@ -28,6 +28,9 @@ export function ArticleContent({ block }: ArticleContentProps) {
28
28
  <SanityImage
29
29
  image={author.avatar}
30
30
  alt={author.name || ""}
31
+ width={40}
32
+ height={40}
33
+ maxWidth={80}
31
34
  className="size-10 rounded-full object-cover"
32
35
  />
33
36
  ) : null}
@@ -50,6 +53,8 @@ export function ArticleContent({ block }: ArticleContentProps) {
50
53
  image={thumbnail}
51
54
  alt={thumbnail.alt || ""}
52
55
  priority
56
+ quality={90}
57
+ sizes="(min-width: 896px) 896px, 100vw"
53
58
  className="aspect-video w-full object-cover"
54
59
  />
55
60
  </div>
@@ -14,6 +14,7 @@ export const RelatedPostItem = ({ post }: { post: ArticleRelatedPost }) => {
14
14
  <SanityImage
15
15
  image={post.metadata.image}
16
16
  alt={post.title || ""}
17
+ sizes="(min-width: 768px) 768px, 100vw"
17
18
  className="aspect-[16/9] w-full object-cover transition-transform duration-300 group-hover:scale-105"
18
19
  />
19
20
  </div>
@@ -296,11 +296,11 @@ async function CollectionResults({
296
296
  // every fetch behind it is cached, so it prerenders into the static shell.
297
297
  export function ContentCollection(props: ContentCollectionProps) {
298
298
  return (
299
- <section className="flex flex-col gap-8 py-8">
299
+ <div className="flex flex-col gap-8">
300
300
  <CollectionToolbar pinnedCategory={props.pinnedCategory} />
301
301
  <Suspense fallback={<CollectionResultsSkeleton />}>
302
302
  <CollectionResults {...props} />
303
303
  </Suspense>
304
- </section>
304
+ </div>
305
305
  )
306
306
  }
@@ -5,13 +5,9 @@ type DescriptionProps = {
5
5
  }
6
6
 
7
7
  export function Description({ block }: DescriptionProps) {
8
- if (!block.description) return null
9
-
10
8
  return (
11
- <section className="py-4">
12
- <p className="max-w-[62ch] text-base/7 text-current/72 md:text-lg/8">
13
- {block.description}
14
- </p>
15
- </section>
9
+ <p className="max-w-[62ch] text-base/7 text-current/72 md:text-lg/8">
10
+ {block.description}
11
+ </p>
16
12
  )
17
13
  }
@@ -64,7 +64,7 @@ export function GatedContent({ block }: GatedContentProps) {
64
64
  }
65
65
 
66
66
  return (
67
- <section className="py-8 md:py-12">
67
+ <div>
68
68
  {block.teaserHeading ? (
69
69
  <h2 className="mb-4 font-semibold text-2xl tracking-tight md:text-4xl">
70
70
  {block.teaserHeading}
@@ -114,7 +114,7 @@ export function GatedContent({ block }: GatedContentProps) {
114
114
  </button>
115
115
  </form>
116
116
  )}
117
- </section>
117
+ </div>
118
118
  )
119
119
  }
120
120
 
@@ -5,13 +5,9 @@ type HeroProps = {
5
5
  }
6
6
 
7
7
  export function Hero({ block }: HeroProps) {
8
- if (!block.headline) return null
9
-
10
8
  return (
11
- <section className="py-8 md:py-12">
12
- <h1 className="max-w-4xl font-semibold text-4xl tracking-tight md:text-6xl">
13
- {block.headline}
14
- </h1>
15
- </section>
9
+ <h1 className="max-w-4xl font-semibold text-4xl tracking-tight md:text-6xl">
10
+ {block.headline}
11
+ </h1>
16
12
  )
17
13
  }
@@ -1,16 +1,14 @@
1
- import { RichText } from "@/components/sanity/rich-text"
2
1
  import type { PageBuilderBlock } from "@/components/page-builder/types"
2
+ import { RichText } from "@/components/sanity/rich-text"
3
3
 
4
4
  type RichTextBlockProps = {
5
5
  block: Extract<PageBuilderBlock, { _type: "richTextBlock" }>
6
6
  }
7
7
 
8
8
  export function RichTextBlock({ block }: RichTextBlockProps) {
9
- if (!block.content) return null
10
-
11
9
  return (
12
- <section className="prose max-w-none">
10
+ <div className="prose max-w-none">
13
11
  <RichText content={block.content} />
14
- </section>
12
+ </div>
15
13
  )
16
14
  }
@@ -0,0 +1,70 @@
1
+ import type { ReactNode } from "react"
2
+ import type { PageBuilderDocumentType } from "@/lib/integrations/sanity/page-builder-config"
3
+ import { ArticleContent } from "./components/article-content"
4
+ import { ContentCollection } from "./components/content-collection"
5
+ import type {
6
+ ContentCollectionSearchParams,
7
+ PinnedCollectionCategory,
8
+ } from "./components/content-collection/types"
9
+ import { Description } from "./components/description"
10
+ import { GatedContent } from "./components/gated-content"
11
+ import { Hero } from "./components/hero"
12
+ import { RichTextBlock } from "./components/rich-text-block"
13
+ import type { PageBuilderBlock } from "./types"
14
+
15
+ export type BlockContext = {
16
+ searchParams: ContentCollectionSearchParams
17
+ pinnedCategory: PinnedCollectionCategory
18
+ }
19
+
20
+ type BlockOf<T extends PageBuilderDocumentType> = Extract<
21
+ PageBuilderBlock,
22
+ { _type: T }
23
+ >
24
+
25
+ type BlockEntry<T extends PageBuilderDocumentType> = {
26
+ render: (block: BlockOf<T>, context: BlockContext) => ReactNode
27
+ /** Skips the block *and* its `<Section>`, so no padded gap is left behind. */
28
+ isEmpty?: (block: BlockOf<T>) => boolean
29
+ }
30
+
31
+ const blockRegistry: {
32
+ [T in PageBuilderDocumentType]: BlockEntry<T>
33
+ } = {
34
+ blogCollection: {
35
+ render: (block, { searchParams, pinnedCategory }) => (
36
+ <ContentCollection
37
+ block={block}
38
+ pinnedCategory={pinnedCategory}
39
+ searchParams={searchParams}
40
+ />
41
+ ),
42
+ },
43
+ blogContent: {
44
+ render: (block) => <ArticleContent block={block} />,
45
+ },
46
+ description: {
47
+ render: (block) => <Description block={block} />,
48
+ isEmpty: (block) => !block.description,
49
+ },
50
+ gatedContent: {
51
+ render: (block) => <GatedContent block={block} />,
52
+ },
53
+ hero: {
54
+ render: (block) => <Hero block={block} />,
55
+ isEmpty: (block) => !block.headline,
56
+ },
57
+ richTextBlock: {
58
+ render: (block) => <RichTextBlock block={block} />,
59
+ isEmpty: (block) => !block.content?.length,
60
+ },
61
+ }
62
+
63
+ /**
64
+ * Indexing the registry with the union collapses its per-key signatures into an
65
+ * intersection nothing satisfies, so the widening happens once, here.
66
+ */
67
+ export const getBlockEntry = (
68
+ type: PageBuilderDocumentType
69
+ ): BlockEntry<PageBuilderDocumentType> | undefined =>
70
+ blockRegistry[type] as BlockEntry<PageBuilderDocumentType> | undefined
@@ -1,13 +1,13 @@
1
- import { ArticleContent } from "./components/article-content"
2
- import { ContentCollection } from "./components/content-collection"
1
+ import {
2
+ reserveAnchorIds,
3
+ resolveAnchorId,
4
+ } from "@/lib/integrations/sanity/schemas/shared/anchor"
3
5
  import type {
4
6
  ContentCollectionSearchParams,
5
7
  PinnedCollectionCategory,
6
8
  } from "./components/content-collection/types"
7
- import { Description } from "./components/description"
8
- import { GatedContent } from "./components/gated-content"
9
- import { Hero } from "./components/hero"
10
- import { RichTextBlock } from "./components/rich-text-block"
9
+ import { type BlockContext, getBlockEntry } from "./registry"
10
+ import { Section } from "./section"
11
11
  import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
12
12
 
13
13
  type RendererProps = {
@@ -23,49 +23,33 @@ export function Renderer({
23
23
  }: RendererProps) {
24
24
  if (!blocks?.length) return null
25
25
 
26
+ const context: BlockContext = { searchParams, pinnedCategory }
27
+
28
+ const reservedAnchorIds = reserveAnchorIds(blocks)
29
+ const takenAnchorIds = new Set<string>()
30
+
26
31
  return (
27
- <div className="space-y-16">
28
- {blocks.map((block: PageBuilderBlock) => (
29
- <PageBuilderNode
30
- key={block._key}
31
- block={block}
32
- searchParams={searchParams}
33
- pinnedCategory={pinnedCategory}
34
- />
35
- ))}
36
- </div>
37
- )
38
- }
32
+ <>
33
+ {blocks.map((block: PageBuilderBlock) => {
34
+ const entry = getBlockEntry(block._type)
39
35
 
40
- function PageBuilderNode({
41
- block,
42
- searchParams,
43
- pinnedCategory,
44
- }: {
45
- block: PageBuilderBlock
46
- searchParams: ContentCollectionSearchParams
47
- pinnedCategory: PinnedCollectionCategory
48
- }) {
49
- switch (block._type) {
50
- case "blogCollection":
51
- return (
52
- <ContentCollection
53
- block={block}
54
- searchParams={searchParams}
55
- pinnedCategory={pinnedCategory}
56
- />
57
- )
58
- case "blogContent":
59
- return <ArticleContent block={block} />
60
- case "hero":
61
- return <Hero block={block} />
62
- case "description":
63
- return <Description block={block} />
64
- case "richTextBlock":
65
- return <RichTextBlock block={block} />
66
- case "gatedContent":
67
- return <GatedContent block={block} />
68
- default:
69
- return null
70
- }
36
+ // No entry means the content is ahead of the generated types.
37
+ if (!entry || entry.isEmpty?.(block)) return null
38
+
39
+ return (
40
+ <Section
41
+ anchorId={resolveAnchorId(
42
+ block,
43
+ takenAnchorIds,
44
+ reservedAnchorIds
45
+ )}
46
+ key={block._key}
47
+ presentation={block.presentation}
48
+ >
49
+ {entry.render(block, context)}
50
+ </Section>
51
+ )
52
+ })}
53
+ </>
54
+ )
71
55
  }
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from "react"
2
+ import { spacingPaddingValue } from "@/lib/integrations/sanity/schemas/shared/spacing"
3
+ import { cn } from "@/lib/styles/cn"
4
+
5
+ export type SectionPresentation = {
6
+ paddingTop?: string | null
7
+ paddingBottom?: string | null
8
+ } | null
9
+
10
+ type SectionProps = {
11
+ anchorId?: string | null | undefined
12
+ presentation?: SectionPresentation | undefined
13
+ className?: string | undefined
14
+ children: ReactNode
15
+ }
16
+
17
+ export function Section({
18
+ anchorId,
19
+ presentation,
20
+ className,
21
+ children,
22
+ }: SectionProps) {
23
+ return (
24
+ <section
25
+ id={anchorId || undefined}
26
+ style={{
27
+ paddingTop: spacingPaddingValue(presentation?.paddingTop),
28
+ paddingBottom: spacingPaddingValue(presentation?.paddingBottom),
29
+ }}
30
+ className={cn(anchorId && "scroll-mt-24", className)}
31
+ >
32
+ {children}
33
+ </section>
34
+ )
35
+ }