bsmnt 0.13.1 → 0.15.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 (177) hide show
  1. package/dist/core/create/internal-ids.d.ts +1 -0
  2. package/dist/core/create/internal-ids.d.ts.map +1 -1
  3. package/dist/core/create/internal-ids.js +1 -0
  4. package/dist/core/create/internal-ids.js.map +1 -1
  5. package/dist/domain/skills.d.ts +5 -0
  6. package/dist/domain/skills.d.ts.map +1 -1
  7. package/dist/domain/skills.js +9 -1
  8. package/dist/domain/skills.js.map +1 -1
  9. package/dist/infrastructure/create/format-project.d.ts +9 -0
  10. package/dist/infrastructure/create/format-project.d.ts.map +1 -0
  11. package/dist/infrastructure/create/format-project.js +21 -0
  12. package/dist/infrastructure/create/format-project.js.map +1 -0
  13. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  14. package/dist/infrastructure/create/lifecycle-registry.js +5 -0
  15. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  16. package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
  17. package/dist/modules/features/cms/sanity-pagebuilder/config.js +1 -0
  18. package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
  19. package/dist/modules/features/cms/sanity-pagebuilder/index.d.ts.map +1 -1
  20. package/dist/modules/features/cms/sanity-pagebuilder/index.js +1 -0
  21. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  22. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -1
  23. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +1 -0
  24. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -1
  25. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts.map +1 -1
  26. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js +1 -0
  27. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +29 -11
  30. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +31 -12
  31. package/src/agent-skills/launch-ready/SKILL.md +114 -0
  32. package/src/agent-skills/launch-ready/references/a11y.md +74 -0
  33. package/src/agent-skills/launch-ready/references/content.md +65 -0
  34. package/src/agent-skills/launch-ready/references/crawlers.md +89 -0
  35. package/src/agent-skills/launch-ready/references/launch.md +44 -0
  36. package/src/agent-skills/launch-ready/references/metadata.md +83 -0
  37. package/src/agent-skills/launch-ready/references/performance.md +36 -0
  38. package/src/agent-skills/launch-ready/references/routing.md +47 -0
  39. package/src/agent-skills/launch-ready/references/security.md +39 -0
  40. package/src/agent-skills/page-builder-block/SKILL.md +26 -13
  41. package/src/modules/features/cms/sanity/files/app/layout.tsx +2 -2
  42. package/src/modules/features/cms/sanity/files/lib/utils/metadata.ts +2 -2
  43. package/src/modules/features/cms/sanity/files/lib/utils/url.ts +6 -3
  44. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +2 -2
  45. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +4 -2
  46. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/block-actions-item.tsx +2 -2
  47. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/component-preview-frame.tsx +124 -0
  48. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/component-preview-pane.tsx +25 -0
  49. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +3 -26
  50. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +121 -93
  51. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/redirect-import-dialog.tsx +538 -0
  52. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/csv.ts +88 -0
  53. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/breadcrumbs.ts +22 -0
  54. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
  55. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +36 -49
  56. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +1 -50
  57. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +76 -64
  58. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/redirect-import.ts +651 -0
  59. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +6 -8
  60. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +0 -1
  61. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +1 -4
  62. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +9 -6
  63. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/structured-data.ts +8 -0
  64. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +4 -4
  65. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +60 -6
  66. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -18
  67. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +33 -38
  68. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/richText.ts +0 -1
  69. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +0 -16
  70. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +52 -35
  71. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +5 -2
  72. package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +8 -11
  73. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +109 -136
  74. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/studio-embed.ts +22 -0
  75. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +2 -2
  76. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +4 -2
  77. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +2 -2
  78. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/component-preview-frame.tsx +124 -0
  79. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/component-preview-pane.tsx +25 -0
  80. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +3 -26
  81. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +121 -93
  82. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/redirect-import-dialog.tsx +538 -0
  83. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/csv.ts +88 -0
  84. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/breadcrumbs.ts +25 -0
  85. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +2 -2
  86. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +36 -49
  87. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +1 -50
  88. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +76 -64
  89. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/redirect-import.ts +651 -0
  90. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +6 -8
  91. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +0 -1
  92. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +1 -4
  93. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +9 -6
  94. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +8 -0
  95. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +4 -4
  96. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +60 -6
  97. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -18
  98. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +33 -38
  99. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +0 -1
  100. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +0 -16
  101. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +52 -35
  102. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +6 -2
  103. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +8 -11
  104. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +109 -136
  105. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/studio-embed.ts +22 -0
  106. package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-block.ts +4 -10
  107. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/README.md +10 -5
  108. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +0 -7
  109. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +11 -5
  110. package/src/templates/next-default/.env.example +3 -1
  111. package/src/templates/next-default/app/layout.tsx +2 -1
  112. package/src/templates/next-default/app/robots.ts +9 -3
  113. package/src/templates/next-default/lib/utils/metadata.ts +2 -1
  114. package/src/templates/next-default/lib/utils/url.ts +20 -3
  115. package/src/templates/next-default/next.config.ts +16 -0
  116. package/src/templates/next-experiments/.env.example +3 -1
  117. package/src/templates/next-experiments/app/layout.tsx +2 -1
  118. package/src/templates/next-experiments/app/robots.ts +9 -3
  119. package/src/templates/next-experiments/lib/utils/metadata.ts +2 -1
  120. package/src/templates/next-experiments/lib/utils/url.ts +20 -3
  121. package/src/templates/next-experiments/next.config.ts +16 -0
  122. package/src/templates/next-pagebuilder/.env.example +3 -1
  123. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +32 -8
  124. package/src/templates/next-pagebuilder/AGENTS.md +22 -7
  125. package/src/templates/next-pagebuilder/README.md +15 -3
  126. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +45 -7
  127. package/src/templates/next-pagebuilder/app/actions/unlock-gated-content.ts +5 -4
  128. package/src/templates/next-pagebuilder/app/components/page.tsx +81 -0
  129. package/src/templates/next-pagebuilder/app/layout.tsx +3 -41
  130. package/src/templates/next-pagebuilder/app/robots.ts +9 -3
  131. package/src/templates/next-pagebuilder/biome.json +2 -1
  132. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +1 -1
  133. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +8 -3
  134. package/src/templates/next-pagebuilder/components/page-builder/library.ts +183 -0
  135. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +4 -1
  136. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +3 -1
  137. package/src/templates/next-pagebuilder/components/page-builder/stage.tsx +10 -0
  138. package/src/templates/next-pagebuilder/components/page-document/index.tsx +28 -35
  139. package/src/templates/next-pagebuilder/lefthook.yml +1 -1
  140. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +170 -299
  141. package/src/templates/next-pagebuilder/lib/utils/base-url.ts +18 -3
  142. package/src/templates/next-pagebuilder/lib/utils/breadcrumbs.ts +70 -0
  143. package/src/templates/next-pagebuilder/next.config.ts +16 -0
  144. package/src/templates/next-pagebuilder/package.json +1 -1
  145. package/src/templates/next-pagebuilder-cache/.env.example +3 -1
  146. package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +32 -8
  147. package/src/templates/next-pagebuilder-cache/AGENTS.md +22 -7
  148. package/src/templates/next-pagebuilder-cache/README.md +15 -3
  149. package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +46 -7
  150. package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +5 -4
  151. package/src/templates/next-pagebuilder-cache/app/components/page.tsx +91 -0
  152. package/src/templates/next-pagebuilder-cache/app/layout.tsx +5 -46
  153. package/src/templates/next-pagebuilder-cache/app/robots.ts +9 -3
  154. package/src/templates/next-pagebuilder-cache/biome.json +2 -1
  155. package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +1 -1
  156. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +8 -3
  157. package/src/templates/next-pagebuilder-cache/components/page-builder/library.ts +183 -0
  158. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +4 -1
  159. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +3 -1
  160. package/src/templates/next-pagebuilder-cache/components/page-builder/stage.tsx +10 -0
  161. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +28 -35
  162. package/src/templates/next-pagebuilder-cache/lefthook.yml +1 -1
  163. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +170 -299
  164. package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +18 -3
  165. package/src/templates/next-pagebuilder-cache/lib/utils/breadcrumbs.ts +70 -0
  166. package/src/templates/next-pagebuilder-cache/next.config.ts +16 -0
  167. package/src/templates/next-pagebuilder-cache/package.json +1 -1
  168. package/src/templates/next-webgl/.env.example +3 -1
  169. package/src/templates/next-webgl/app/layout.tsx +2 -1
  170. package/src/templates/next-webgl/app/robots.ts +9 -3
  171. package/src/templates/next-webgl/lib/utils/metadata.ts +2 -1
  172. package/src/templates/next-webgl/lib/utils/url.ts +20 -3
  173. package/src/templates/next-webgl/next.config.ts +16 -0
  174. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +0 -62
  175. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +0 -37
  176. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +0 -62
  177. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +0 -37
@@ -13,14 +13,15 @@ type GatedContentBlock = Extract<PageBuilderBlock, { _type: "gatedContent" }>
13
13
 
14
14
  type GatedContentProps = {
15
15
  block: GatedContentBlock
16
+ pageId: string
16
17
  }
17
18
 
18
19
  type GateState = "locked" | "loading" | "unlocked"
19
20
 
20
21
  const STORAGE_PREFIX = "gated-content:"
21
22
 
22
- export function GatedContent({ block }: GatedContentProps) {
23
- const storageKey = `${STORAGE_PREFIX}${block._id}`
23
+ export function GatedContent({ block, pageId }: GatedContentProps) {
24
+ const storageKey = `${STORAGE_PREFIX}${pageId}:${block._key}`
24
25
  const [state, setState] = useState<GateState>("locked")
25
26
  const [payload, setPayload] = useState<GatedContentPayload | null>(null)
26
27
  const [error, setError] = useState<string | null>(null)
@@ -33,7 +34,11 @@ export function GatedContent({ block }: GatedContentProps) {
33
34
  setState("loading")
34
35
  setError(null)
35
36
 
36
- const result = await unlockGatedContent({ documentId: block._id, email })
37
+ const result = await unlockGatedContent({
38
+ pageId,
39
+ blockKey: block._key,
40
+ email,
41
+ })
37
42
 
38
43
  if (!result.ok) {
39
44
  setError(result.error)
@@ -0,0 +1,183 @@
1
+ import type { PageBuilderDocumentType } from "@/lib/integrations/sanity/page-builder-config"
2
+ import type { PageBuilderBlock } from "./types"
3
+
4
+ /** One block's stand-in value, rendered by `/components` through `Renderer`. */
5
+ export type LibraryEntry = {
6
+ documentType: PageBuilderDocumentType
7
+ title: string
8
+ notes: string
9
+ background: string
10
+ block: PageBuilderBlock
11
+ }
12
+
13
+ type BlockOf<T extends PageBuilderDocumentType> = Extract<
14
+ PageBuilderBlock,
15
+ { _type: T }
16
+ >
17
+
18
+ const span = (key: string, text: string) => ({
19
+ _type: "span" as const,
20
+ _key: key,
21
+ text,
22
+ marks: [],
23
+ })
24
+
25
+ const paragraph = (key: string, text: string) => ({
26
+ _type: "block" as const,
27
+ _key: key,
28
+ style: "normal" as const,
29
+ markDefs: [],
30
+ children: [span(`${key}-span`, text)],
31
+ })
32
+
33
+ const heading = (key: string, text: string) => ({
34
+ ...paragraph(key, text),
35
+ style: "h2" as const,
36
+ })
37
+
38
+ const PLACEHOLDER_KEY = "placeholder"
39
+
40
+ const hero: BlockOf<"hero"> = {
41
+ _key: PLACEHOLDER_KEY,
42
+ _type: "hero",
43
+ headline: "A heading that says what this page is for.",
44
+ }
45
+
46
+ const description: BlockOf<"description"> = {
47
+ _key: PLACEHOLDER_KEY,
48
+ _type: "description",
49
+ description:
50
+ "One paragraph of standfirst copy under a heading — long enough to set up the page, short enough that nobody skips it.",
51
+ }
52
+
53
+ const richTextBlock: BlockOf<"richTextBlock"> = {
54
+ _key: PLACEHOLDER_KEY,
55
+ _type: "richTextBlock",
56
+ content: [
57
+ heading("rt-head", "What long-form copy looks like here"),
58
+ paragraph(
59
+ "rt-one",
60
+ "Rich Text is the block for body copy: headings, paragraphs, lists, links and quotes, all authored in the Studio and rendered with the site's own typography."
61
+ ),
62
+ paragraph(
63
+ "rt-two",
64
+ "Because it is Portable Text rather than HTML, the same content renders in the page, in the markdown route and in the feed without a second template."
65
+ ),
66
+ ],
67
+ }
68
+
69
+ const gatedContent: BlockOf<"gatedContent"> = {
70
+ _key: PLACEHOLDER_KEY,
71
+ _type: "gatedContent",
72
+ internalTitle: "Gated guide",
73
+ teaserHeading: "A guide worth an email address",
74
+ teaserBody: [
75
+ paragraph(
76
+ "gc-teaser",
77
+ "The teaser is public. The payload — rich text, a file or a video — is withheld from the page query until a visitor submits the form."
78
+ ),
79
+ ],
80
+ gateType: "form",
81
+ gateHeadline: "Read the full guide",
82
+ gateDescription: "We'll email you a copy as well.",
83
+ submitLabel: "Get the guide",
84
+ successMessage: "Sent — it's on its way.",
85
+ kind: "richText",
86
+ anchorId: null,
87
+ presentation: null,
88
+ }
89
+
90
+ const blogCollection: BlockOf<"blogCollection"> = {
91
+ _key: PLACEHOLDER_KEY,
92
+ _type: "blogCollection",
93
+ subtitle: "Everything else we've written",
94
+ }
95
+
96
+ const blogContent: BlockOf<"blogContent"> = {
97
+ _key: PLACEHOLDER_KEY,
98
+ _type: "blogContent",
99
+ title: "How this starter renders a blog post",
100
+ categories: [],
101
+ date: "2026-02-11",
102
+ author: {
103
+ _id: "placeholder-author",
104
+ name: "Jane Doe",
105
+ avatar: null,
106
+ role: "Engineer",
107
+ },
108
+ body: [
109
+ paragraph(
110
+ "bc-one",
111
+ "A blog post is an ordinary page whose page builder holds a Blog Posts block: byline, date, categories and body all live on the block."
112
+ ),
113
+ heading("bc-head", "One block, one article"),
114
+ paragraph(
115
+ "bc-two",
116
+ "The collection block reads the same fields, so an article added in the Studio shows up in the index without touching code."
117
+ ),
118
+ ],
119
+ relatedPosts: [],
120
+ }
121
+
122
+ export const COMPONENT_LIBRARY: LibraryEntry[] = [
123
+ {
124
+ documentType: "hero",
125
+ title: "Heading",
126
+ notes: "The page's opening statement. Headline only, no media.",
127
+ background: "bg-white",
128
+ block: hero,
129
+ },
130
+ {
131
+ documentType: "description",
132
+ title: "Description",
133
+ notes: "A single paragraph of standfirst copy under a heading.",
134
+ background: "bg-white",
135
+ block: description,
136
+ },
137
+ {
138
+ documentType: "richTextBlock",
139
+ title: "Rich Text",
140
+ notes: "Long-form body copy — headings, lists, links and quotes.",
141
+ background: "bg-white",
142
+ block: richTextBlock,
143
+ },
144
+ {
145
+ documentType: "gatedContent",
146
+ title: "Gated Content",
147
+ notes:
148
+ "A teaser with an email gate. The payload is withheld until a visitor submits, so the preview shows the locked state.",
149
+ background: "bg-white",
150
+ block: gatedContent,
151
+ },
152
+ {
153
+ documentType: "blogCollection",
154
+ title: "Blog Collection",
155
+ notes:
156
+ "The full article index with search and category filters. Reads real posts.",
157
+ background: "bg-white",
158
+ block: blogCollection,
159
+ },
160
+ {
161
+ documentType: "blogContent",
162
+ title: "Blog Posts",
163
+ notes:
164
+ "The body of an article — byline, date, categories and rich text. Only valid on a blog entry page.",
165
+ background: "bg-white",
166
+ block: blogContent,
167
+ },
168
+ ]
169
+
170
+ export const getLibraryEntry = (documentType: string | undefined) =>
171
+ documentType
172
+ ? (COMPONENT_LIBRARY.find((entry) => entry.documentType === documentType) ??
173
+ null)
174
+ : null
175
+
176
+ export const COMPONENT_PREVIEW_PATH = "/components"
177
+
178
+ export const COMPONENT_PREVIEW_PARAM = "component"
179
+
180
+ export const getComponentPreviewPath = (documentType?: string) =>
181
+ documentType
182
+ ? `${COMPONENT_PREVIEW_PATH}?${COMPONENT_PREVIEW_PARAM}=${encodeURIComponent(documentType)}`
183
+ : COMPONENT_PREVIEW_PATH
@@ -13,6 +13,7 @@ import { RichTextBlock } from "./components/rich-text-block"
13
13
  import type { PageBuilderBlock } from "./types"
14
14
 
15
15
  export type BlockContext = {
16
+ pageId: string
16
17
  searchParams: ContentCollectionSearchParams
17
18
  pinnedCategory: PinnedCollectionCategory
18
19
  }
@@ -48,7 +49,9 @@ const blockRegistry: {
48
49
  isEmpty: (block) => !block.description,
49
50
  },
50
51
  gatedContent: {
51
- render: (block) => <GatedContent block={block} />,
52
+ render: (block, { pageId }) => (
53
+ <GatedContent block={block} pageId={pageId} />
54
+ ),
52
55
  },
53
56
  hero: {
54
57
  render: (block) => <Hero block={block} />,
@@ -12,18 +12,20 @@ import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
12
12
 
13
13
  type RendererProps = {
14
14
  blocks: PageBuilderBlocks
15
+ pageId: string
15
16
  searchParams: ContentCollectionSearchParams
16
17
  pinnedCategory: PinnedCollectionCategory
17
18
  }
18
19
 
19
20
  export function Renderer({
20
21
  blocks,
22
+ pageId,
21
23
  searchParams,
22
24
  pinnedCategory,
23
25
  }: RendererProps) {
24
26
  if (!blocks?.length) return null
25
27
 
26
- const context: BlockContext = { searchParams, pinnedCategory }
28
+ const context: BlockContext = { pageId, searchParams, pinnedCategory }
27
29
 
28
30
  const reservedAnchorIds = reserveAnchorIds(blocks)
29
31
  const takenAnchorIds = new Set<string>()
@@ -0,0 +1,10 @@
1
+ import type { PropsWithChildren } from "react"
2
+ import { cn } from "@/lib/styles/cn"
3
+
4
+ type StageProps = PropsWithChildren<{ background?: string }>
5
+
6
+ export const Stage = ({ background, children }: StageProps) => (
7
+ <div className={cn("grid min-h-svh content-center", background)}>
8
+ {children}
9
+ </div>
10
+ )
@@ -1,9 +1,13 @@
1
1
  import { stegaClean } from "@sanity/client/stega"
2
2
  import type { ContentCollectionSearchParams } from "@/components/page-builder/components/content-collection/types"
3
3
  import { Renderer } from "@/components/page-builder/renderer"
4
- import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
4
+ import type {
5
+ BREADCRUMB_PAGES_QUERY_RESULT,
6
+ PAGE_QUERY_RESULT,
7
+ } from "@/lib/integrations/sanity/sanity.types"
5
8
  import { urlForImage } from "@/lib/integrations/sanity/utils/image"
6
9
  import { buildPageJsonLd } from "@/lib/integrations/sanity/utils/page-json-ld"
10
+ import { buildBreadcrumbs } from "@/lib/utils/breadcrumbs"
7
11
  import {
8
12
  generateBreadcrumbJsonLd,
9
13
  generateSanityArticleJsonLd,
@@ -15,6 +19,7 @@ type PageDocumentProps = {
15
19
  page: NonNullable<PAGE_QUERY_RESULT>
16
20
  path: string | null
17
21
  searchParams: ContentCollectionSearchParams
22
+ breadcrumbPages: BREADCRUMB_PAGES_QUERY_RESULT
18
23
  }
19
24
 
20
25
  type PageBuilderBlock = NonNullable<
@@ -22,6 +27,13 @@ type PageBuilderBlock = NonNullable<
22
27
  >[number]
23
28
  type BlogContentBlock = Extract<PageBuilderBlock, { _type: "blogContent" }>
24
29
 
30
+ // Blog posts are pages with a Blog Posts block; it carries the post's date,
31
+ // author and thumbnail.
32
+ export const findBlogBlock = (page: NonNullable<PAGE_QUERY_RESULT>) =>
33
+ page.pageBuilder?.find(
34
+ (block): block is BlogContentBlock => block?._type === "blogContent"
35
+ )
36
+
25
37
  export const getPageSections = (
26
38
  page: NonNullable<PAGE_QUERY_RESULT>,
27
39
  searchParams: ContentCollectionSearchParams
@@ -31,6 +43,7 @@ export const getPageSections = (
31
43
  <Renderer
32
44
  key="page-builder"
33
45
  blocks={page.pageBuilder}
46
+ pageId={page._id}
34
47
  searchParams={searchParams}
35
48
  // Cleaned because the slug becomes a GROQ parameter: in draft mode the
36
49
  // projection carries stega characters that would break the match.
@@ -39,42 +52,20 @@ export const getPageSections = (
39
52
  ]
40
53
  : []
41
54
 
42
- const buildBreadcrumbs = (
43
- slugPath: string | null,
44
- pageTitle: string | null
45
- ): Array<{ name: string; url: string }> => {
46
- const items: Array<{ name: string; url: string }> = [
47
- { name: "Home", url: "/" },
48
- ]
49
-
50
- if (!slugPath) return items
51
-
52
- const segments = slugPath.split("/").filter(Boolean)
53
- for (let i = 0; i < segments.length; i++) {
54
- const isLast = i === segments.length - 1
55
- const url = `/${segments.slice(0, i + 1).join("/")}`
56
-
57
- //TODO: we might change this in the future if slug title and folder diverge too much :)
58
-
59
- const name =
60
- isLast && pageTitle
61
- ? pageTitle
62
- : (segments[i]
63
- ?.replace(/-/g, " ")
64
- .replace(/\b\w/g, (c) => c.toUpperCase()) ?? "")
65
- items.push({ name, url })
66
- }
67
-
68
- return items
69
- }
70
-
71
55
  export const PageDocument = ({
72
56
  page,
73
57
  path,
74
58
  searchParams,
59
+ breadcrumbPages,
75
60
  }: PageDocumentProps) => {
76
61
  const title = stegaClean(page.title) || page.title
77
62
  const sections = getPageSections(page, searchParams)
63
+ // A Heading block renders the page's H1, so the title only stands in without
64
+ // one. Hidden blocks are already filtered out of the query, and an empty one
65
+ // renders nothing (see the registry's `isEmpty`).
66
+ const hasHeadingBlock = page.pageBuilder?.some(
67
+ (block) => block?._type === "hero" && Boolean(block.headline)
68
+ )
78
69
 
79
70
  const cleanTitle = stegaClean(page.title) || "Basement"
80
71
  const cleanDescription = stegaClean(
@@ -86,7 +77,11 @@ export const PageDocument = ({
86
77
  ? urlForImage(page.metadata.image).width(1200).height(630).url()
87
78
  : undefined
88
79
 
89
- const breadcrumbs = buildBreadcrumbs(path, cleanTitle)
80
+ const breadcrumbs = buildBreadcrumbs({
81
+ path,
82
+ name: cleanTitle,
83
+ pages: breadcrumbPages,
84
+ })
90
85
  const hasBreadcrumbs = breadcrumbs.length > 1
91
86
 
92
87
  // Editor-selected page-level structured data (FAQ, Event, custom, etc.).
@@ -95,9 +90,7 @@ export const PageDocument = ({
95
90
  // Blog posts (pages with a Blog Posts block) emit Article JSON-LD with
96
91
  // fields derived from the post — no Custom block needed. Skipped when the
97
92
  // editor already supplied an Article (via a Custom block) to avoid duplicates.
98
- const blogBlock = page.pageBuilder?.find(
99
- (block): block is BlogContentBlock => block?._type === "blogContent"
100
- )
93
+ const blogBlock = findBlogBlock(page)
101
94
  const hasManualArticle = pageJsonLd.some(
102
95
  ({ data }) => (data as { "@type"?: unknown })["@type"] === "Article"
103
96
  )
@@ -135,7 +128,7 @@ export const PageDocument = ({
135
128
  <JsonLd key={key} data={data} />
136
129
  ))}
137
130
  <article className="mx-auto flex w-full max-w-6xl flex-1 flex-col px-4 py-14 md:px-6 md:py-20">
138
- <h1>{title}</h1>
131
+ {hasHeadingBlock ? null : <h1>{title}</h1>}
139
132
  {sections.length ? (
140
133
  sections
141
134
  ) : (
@@ -7,7 +7,7 @@ pre-commit:
7
7
  commands:
8
8
  biome:
9
9
  glob: '*.{js,mjs,ts,jsx,tsx,css,scss}'
10
- run: bun biome check --max-diagnostics=200 --write --unsafe {staged_files}
10
+ run: bun biome check --max-diagnostics=200 --write {staged_files}
11
11
  stage_fixed: true
12
12
  typecheck:
13
13
  glob: '*.{ts,tsx}'