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
@@ -0,0 +1,74 @@
1
+ # Accessibility
2
+
3
+ Contrast, motion and target size are design decisions. Report failures; don't
4
+ change brand colors or layout unasked.
5
+
6
+ ## A11Y-01 · Every page has an `h1`
7
+
8
+ **Fix before launch**
9
+
10
+ One `h1` that names the page. Screen reader users jump to it first.
11
+
12
+ **Check:** `curl -s localhost:3000/<route> | grep -c '<h1'` prints at least `1`.
13
+
14
+ ## A11Y-02 · Everything works by keyboard
15
+
16
+ **Fix before launch**
17
+
18
+ Interactive elements are real controls (`<button>`, `<Link>`, inputs), reachable
19
+ with Tab, with a visible focus style. A `<div onClick>` can't be reached
20
+ without a mouse. Menus and modals close on Escape and return focus.
21
+
22
+ **Check (browser):** Tab through the page. Every control is reachable, and
23
+ you can always see where focus is.
24
+
25
+ ## A11Y-03 · Icon-only controls have a name
26
+
27
+ **Fix before launch**
28
+
29
+ An icon-only button or link carries an `aria-label` or visually hidden text.
30
+ Otherwise a screen reader announces only "button".
31
+
32
+ ## A11Y-04 · Inputs have labels
33
+
34
+ **Fix before launch**
35
+
36
+ Each input has a `<label>`. A placeholder isn't one; it disappears as soon as
37
+ someone types. Fields for personal data carry the matching `autocomplete`
38
+ value (`email`, `name`, `tel`).
39
+
40
+ ## A11Y-05 · Text meets contrast
41
+
42
+ **Fix before launch**
43
+
44
+ Body text is at least 4.5:1 against its background, and large text (24px, or
45
+ 19px bold) at least 3:1. Logos, disabled controls and decorative text are
46
+ exempt.
47
+
48
+ **Check (browser):** Lighthouse's accessibility audit, or the axe DevTools
49
+ extension.
50
+
51
+ ## A11Y-06 · Motion can be paused or reduced
52
+
53
+ **Fix before launch**
54
+
55
+ Anything that moves on its own for more than 5 seconds (autoplay video,
56
+ marquees, carousels) has a pause control. Non-essential animation, smooth
57
+ scrolling included, respects `prefers-reduced-motion`.
58
+
59
+ ## A11Y-07 · Targets are big enough; zoom stays on
60
+
61
+ **Fix before launch**
62
+
63
+ Tap targets are at least 24×24px, or have 24px of clearance. Links inside a
64
+ sentence are exempt.
65
+
66
+ Never set `maximumScale: 1` or `userScalable: false` in the viewport. If iOS
67
+ zooms into a focused input, set the input's font size to at least 16px instead.
68
+
69
+ ## A11Y-08 · No horizontal scroll at 320px
70
+
71
+ **Fix before launch**
72
+
73
+ The page fits a 320px-wide viewport without horizontal scrolling. Maps, data
74
+ tables and diagrams are exempt.
@@ -0,0 +1,65 @@
1
+ # Content
2
+
3
+ ## CONTENT-01 · Indexed copy is in the server HTML
4
+
5
+ **Must not ship** · only if indexed
6
+
7
+ Text meant for search, link previews and AI is in the server HTML. Don't draw
8
+ it only in a canvas, and don't load it only through `dynamic(..., { ssr: false })`.
9
+ A WebGL scene can stay client-only; its headline and copy can't.
10
+
11
+ **Check:** a sentence from the page's main copy appears in
12
+ `curl -s localhost:3000/<route>`.
13
+
14
+ ## CONTENT-02 · No placeholder output
15
+
16
+ **Must not ship**
17
+
18
+ No `undefined`, `null`, `NaN`, `[object Object]` or lorem ipsum in rendered
19
+ text. These usually appear after launch, when an editor leaves a CMS field
20
+ empty, so render nothing for a missing field instead.
21
+
22
+ **Check:**
23
+
24
+ ```bash
25
+ curl -s localhost:3000/<route> | grep -noiE 'undefined|NaN|\[object Object\]|lorem ipsum'
26
+ ```
27
+
28
+ A match is a lead, not proof: documentation can say "returns `undefined`" on
29
+ purpose.
30
+
31
+ ## CONTENT-03 · Each fact has one source
32
+
33
+ **Must not ship**
34
+
35
+ A price, spec or limit shown in more than one place reads from one module or
36
+ one CMS field. Two copies drift, and the site contradicts itself.
37
+
38
+ **Check:** search the repo for the value. It appears once, where it's defined.
39
+
40
+ ## CONTENT-04 · No test pages, drafts or staging links
41
+
42
+ **Must not ship**
43
+
44
+ Test pages, unpublished drafts and links to staging or preview hosts aren't
45
+ reachable in production or listed in the sitemap.
46
+
47
+ **Check:** search the production HTML and `sitemap.xml` for `test`, `staging`,
48
+ `preview` and `vercel.app`.
49
+
50
+ ## CONTENT-05 · No starter leftovers
51
+
52
+ **Fix before launch**
53
+
54
+ Replace basement's placeholders with the site's own, wherever they remain:
55
+
56
+ - the `app/favicon.ico` every template ships
57
+ - the `"Basement Starter"` title in `app/layout.tsx`
58
+ - `siteName = "Basement"` in `lib/utils/metadata.ts`
59
+ - the `"Basement Next Starter"` homepage heading
60
+
61
+ **Check:**
62
+
63
+ ```bash
64
+ grep -rn 'Basement Starter\|Basement Next Starter\|siteName = "Basement"' app lib components
65
+ ```
@@ -0,0 +1,89 @@
1
+ # Crawlers
2
+
3
+ ## CRAWL-01 · Indexing matches the decision
4
+
5
+ **Must not ship** when production and the decision disagree
6
+
7
+ `app/robots.ts` and each page's `noIndex` follow *Indexed by search engines*.
8
+ Previews are never indexable.
9
+
10
+ The starter indexes only the host of `NEXT_PUBLIC_BASE_URL`, and on Vercel
11
+ only on a production deployment. Every other host, including `*.vercel.app`,
12
+ gets `X-Robots-Tag: noindex` and a disallow-all `robots.txt`. Without
13
+ `NEXT_PUBLIC_BASE_URL`, nothing is indexed.
14
+
15
+ **Check:**
16
+
17
+ ```bash
18
+ curl -s https://<production>/robots.txt
19
+ curl -sI https://<production>/ | grep -i x-robots-tag
20
+ curl -sI https://<preview>/ | grep -i x-robots-tag
21
+ ```
22
+
23
+ Production matches the decision. The preview prints `noindex`.
24
+
25
+ ## CRAWL-02 · Canonicals point at the right page
26
+
27
+ **Must not ship**
28
+
29
+ A canonical is absolute and points at a page that returns 200 and is indexable.
30
+
31
+ If `app/layout.tsx` sets `alternates.canonical: "/"`, every page that doesn't
32
+ set its own canonical inherits it, which points the whole site at the
33
+ homepage. Set it per page with `generatePageMetadata({ url })`, or remove it
34
+ from the layout.
35
+
36
+ If the layout declares an `en-US` alternate at `/en-US`, remove it unless a
37
+ route serves that locale.
38
+
39
+ **Check:**
40
+
41
+ ```bash
42
+ curl -s localhost:3000/<route> | grep -o '<link rel="canonical"[^>]*>'
43
+ ```
44
+
45
+ It names that route, not `/`.
46
+
47
+ ## CRAWL-03 · The sitemap lists every indexable page
48
+
49
+ **Fix before launch** · only if indexed
50
+
51
+ Build the sitemap from the same data as the routes: every static route, plus
52
+ each CMS document with a public URL. The starter's `app/sitemap.xml/route.ts`
53
+ lists only `/`; add each route to `getStaticEntries()`. When the sitemap reads
54
+ a CMS, a new content type needs adding to its query.
55
+
56
+ **Check:**
57
+
58
+ ```bash
59
+ curl -s localhost:3000/sitemap.xml | grep -c '<loc>'
60
+ ```
61
+
62
+ The count matches the number of public pages.
63
+
64
+ ## CRAWL-04 · The sitemap lists only live pages
65
+
66
+ **Fix before launch** · only if indexed
67
+
68
+ No sitemap URL redirects, 404s or is `noindex`, and each one uses the
69
+ production host.
70
+
71
+ **Check:**
72
+
73
+ ```bash
74
+ curl -s localhost:3000/sitemap.xml | grep -o '<loc>[^<]*' | cut -c6- |
75
+ while read -r u; do printf '%s %s\n' "$(curl -so /dev/null -w '%{http_code}' "$u")" "$u"; done
76
+ ```
77
+
78
+ Every line starts with `200`.
79
+
80
+ ## CRAWL-05 · AI crawler policy matches the decision
81
+
82
+ **Fix before launch**
83
+
84
+ - *AI crawlers: allowed* needs nothing.
85
+ - *AI crawlers: blocked* adds a `disallow: "/"` rule in `robots.ts` for each
86
+ agent: `GPTBot`, `ClaudeBot`, `PerplexityBot`, `Google-Extended`, `CCBot`.
87
+ - *llms.txt: yes* serves `/llms.txt`.
88
+
89
+ **Check:** `curl -s https://<production>/robots.txt` matches the decision.
@@ -0,0 +1,44 @@
1
+ # On the real domain
2
+
3
+ These only fail in production, so check them against the deployed URL.
4
+
5
+ ## LAUNCH-01 · Forms deliver on production
6
+
7
+ **Must not ship** · only if the site has forms
8
+
9
+ Submit each form on production and confirm the submission arrives. The usual
10
+ failure: the form's API key is set for Preview but not for Production, so the
11
+ form works on every preview and silently drops submissions after launch.
12
+
13
+ ## LAUNCH-02 · CMS publishing updates production
14
+
15
+ **Must not ship** · only if the site uses a CMS
16
+
17
+ Publish a small change in the CMS and confirm production shows it. The
18
+ revalidation webhook points at the production URL and carries its secret.
19
+
20
+ ## LAUNCH-03 · One host
21
+
22
+ **Fix before launch**
23
+
24
+ Pick the apex domain or `www`. In Vercel's domain settings, the other
25
+ redirects to it permanently.
26
+
27
+ **Check:**
28
+
29
+ ```bash
30
+ curl -sI https://<other-host>/ | grep -i '^location'
31
+ ```
32
+
33
+ It points at the chosen host.
34
+
35
+ ## LAUNCH-04 · Old URLs redirect on a relaunch
36
+
37
+ **Fix before launch** · only if *Replaces an existing site: yes*
38
+
39
+ Every old URL with traffic or backlinks redirects to its new equivalent
40
+ through `redirects()` in `next.config.ts`. Ask the user for the old site's URL
41
+ list: its sitemap, or its top pages in analytics.
42
+
43
+ **Check:** request a sample of old URLs. Each returns `308` to a page that
44
+ returns `200`.
@@ -0,0 +1,83 @@
1
+ # Metadata
2
+
3
+ ## META-01 · Every page has a real title
4
+
5
+ **Must not ship**
6
+
7
+ Each route exports `metadata` or `generateMetadata` with its own title. If
8
+ `app/layout.tsx` still has the `"Basement Starter"` default and its
9
+ `"%s - Basement Starter"` template, replace both with the site's name.
10
+
11
+ **Check:**
12
+
13
+ ```bash
14
+ curl -s localhost:3000/<route> | grep -o '<title>[^<]*'
15
+ ```
16
+
17
+ ## META-02 · Metadata is in the server HTML
18
+
19
+ **Must not ship**
20
+
21
+ Set titles, descriptions and share tags through the Metadata API, never with
22
+ `document.title` or a client effect. Crawlers and link previews don't run
23
+ JavaScript.
24
+
25
+ **Check:** the tags appear in `curl -s localhost:3000/<route>`, not only in
26
+ the browser's DOM.
27
+
28
+ ## META-03 · The share image loads
29
+
30
+ **Must not ship**
31
+
32
+ A declared `og:image` is absolute and returns an image.
33
+
34
+ If the layout or `generatePageMetadata` defaults to `/opengraph-image.jpg`,
35
+ add that file: no template ships it. Until `public/opengraph-image.jpg`
36
+ exists, every share card is broken, and so is the Organization JSON-LD logo,
37
+ which uses the same path.
38
+
39
+ **Check:**
40
+
41
+ ```bash
42
+ img=$(curl -s localhost:3000/ | grep -o 'property="og:image" content="[^"]*' | cut -d'"' -f4)
43
+ curl -so /dev/null -w '%{http_code} %{content_type}\n' "$img"
44
+ ```
45
+
46
+ Expect `200 image/...`.
47
+
48
+ ## META-04 · No `localhost` URLs in production
49
+
50
+ **Must not ship**
51
+
52
+ `metadataBase`, canonicals and share URLs read `NEXT_PUBLIC_BASE_URL`, then
53
+ the Vercel deployment URL, and fall back to `http://localhost:3000` off Vercel.
54
+ Set it for the Production environment.
55
+
56
+ **Check:**
57
+
58
+ ```bash
59
+ curl -s https://<production>/ | grep -c 'localhost:3000'
60
+ ```
61
+
62
+ Expect `0`.
63
+
64
+ ## META-05 · Titles are unique
65
+
66
+ **Fix before launch**
67
+
68
+ Distinct pages have distinct titles. Variants that canonicalize to one page
69
+ are exempt.
70
+
71
+ ## META-06 · Public pages have a description
72
+
73
+ **Fix before launch**
74
+
75
+ Slack, LinkedIn and iMessage show the description as the preview text. If the
76
+ layout's default reads `package.json`'s `description`, write a real one there.
77
+
78
+ ## META-07 · `<html lang>` matches the content
79
+
80
+ **Fix before launch**
81
+
82
+ The starter sets `lang="en"`. Change it when the site isn't English, and set
83
+ it per locale on a multilingual site.
@@ -0,0 +1,36 @@
1
+ # Performance
2
+
3
+ ## PERF-01 · The LCP image isn't lazy
4
+
5
+ **Fix before launch**
6
+
7
+ The largest image in the first screen (usually the hero) passes `priority` to
8
+ `@/components/ui/image`. Everything below the fold stays lazy, which is the
9
+ default.
10
+
11
+ ## PERF-02 · Image `sizes` match the layout
12
+
13
+ **Fix before launch**
14
+
15
+ `sizes` describes the width the image renders at. Without it, the image
16
+ component falls back to `100vw` and the browser downloads the largest
17
+ variant, which is only right for full-width images. Build `sizes` with
18
+ `buildImageSizes` or `buildGridImageSizes` from `lib/utils/image-sizes.ts`.
19
+
20
+ The `no-fill-without-sizes` lint rule catches `fill` with no `sizes`. It can't
21
+ tell whether the value is correct.
22
+
23
+ ## PERF-03 · Responses are compressed
24
+
25
+ **Fix before launch** · only when hosted outside Vercel
26
+
27
+ Vercel compresses responses by default. Another host has to serve HTML, JS
28
+ and CSS with gzip or brotli.
29
+
30
+ **Check:**
31
+
32
+ ```bash
33
+ curl -sI -H 'Accept-Encoding: gzip, br' https://<production>/ | grep -i content-encoding
34
+ ```
35
+
36
+ Expect `gzip` or `br`.
@@ -0,0 +1,47 @@
1
+ # Routing
2
+
3
+ ## ROUTE-01 · Missing pages return 404
4
+
5
+ **Must not ship**
6
+
7
+ When a dynamic or CMS route has no data, call `notFound()` before any
8
+ `<Suspense>`. Once a response starts streaming, its status is already 200.
9
+ Next can't change it then, and injects a `noindex` tag instead.
10
+
11
+ ```tsx
12
+ export default async function Page({ params }: PageProps<"/blog/[slug]">) {
13
+ const { slug } = await params
14
+ const post = await getPost(slug)
15
+ if (!post) notFound()
16
+ return <Post post={post} />
17
+ }
18
+ ```
19
+
20
+ **Check:**
21
+
22
+ ```bash
23
+ curl -so /dev/null -w '%{http_code}\n' localhost:3000/does-not-exist
24
+ ```
25
+
26
+ Expect `404`. Repeat under each dynamic segment, such as `/blog/does-not-exist`.
27
+
28
+ **Not this:** redirecting unknown paths to `/`. That's still a missing page
29
+ answering 200.
30
+
31
+ ## ROUTE-02 · No internal link is broken
32
+
33
+ **Must not ship**
34
+
35
+ Every internal link resolves to a page that returns 200. A control that only
36
+ runs JavaScript is a `<button>`, not `<a href="#">`. An `#id` that exists on the
37
+ page, or a "back to top" link, is fine.
38
+
39
+ **Check:** list each route's links and request them.
40
+
41
+ ```bash
42
+ curl -s localhost:3000/ | grep -o 'href="/[^"#]*"' | cut -d'"' -f2 | sort -u |
43
+ while read -r p; do printf '%s %s\n' "$(curl -so /dev/null -w '%{http_code}' "localhost:3000$p")" "$p"; done
44
+ ```
45
+
46
+ Every line starts with `200`. A broken link in the nav or footer breaks on every
47
+ page, so check those first.
@@ -0,0 +1,39 @@
1
+ # Security and hygiene
2
+
3
+ ## SEC-01 · `NEXT_PUBLIC_` holds only public keys
4
+
5
+ **Must not ship**
6
+
7
+ Anything prefixed `NEXT_PUBLIC_` ships in the browser bundle, readable by
8
+ anyone. Only keys designed to be public belong there: a Sanity project ID, a
9
+ Stripe `pk_` key, an analytics site ID. Read every other secret on the server.
10
+
11
+ **Check:**
12
+
13
+ ```bash
14
+ grep -rhoE 'NEXT_PUBLIC_[A-Z0-9_]+' app components lib | sort -u
15
+ ```
16
+
17
+ Confirm each name on the list is meant to be public.
18
+
19
+ ## SEC-02 · Tracking matches the consent decision
20
+
21
+ **Fix before launch** · only if consent is required
22
+
23
+ No analytics or ad script runs, and no tracking cookie is set, before the
24
+ visitor answers the consent banner. The `ketch-consent-check` skill covers
25
+ gating a script behind Ketch, when the project has it.
26
+
27
+ **Check (browser):** in a fresh private window, before you answer the banner,
28
+ DevTools → Application → Cookies lists no tracking cookies.
29
+
30
+ ## SEC-03 · No first-party console errors
31
+
32
+ **Fix before launch**
33
+
34
+ The console shows no errors from the site's own code on load, and no
35
+ hydration mismatches. A mismatch means the server and the client rendered
36
+ different content. Errors from third-party scripts are out of scope.
37
+
38
+ **Check (browser):** load each route with DevTools open, then read the
39
+ console.
@@ -13,21 +13,23 @@ description: >-
13
13
 
14
14
  # Page-builder block conventions
15
15
 
16
- Blocks are **documents** referenced from the `pageBuilder` array, drawn by
16
+ Blocks are **inline objects** in the `pageBuilder` array — never documents, and
17
+ never referenced. A placement belongs to exactly one page, so the block lives on
18
+ that page and nothing is dereferenced at read time. They are drawn by
17
19
  `components/page-builder/renderer.tsx` through the registry in
18
20
  `components/page-builder/registry.tsx`. Read one existing block end to end
19
21
  before writing a new one — `hero` is the smallest complete example:
20
22
 
21
23
  - schema: `lib/integrations/sanity/schemas/components/reusable/hero.ts`
22
24
  - component: `components/page-builder/components/hero.tsx`
25
+ - fixture: `components/page-builder/library.ts`
23
26
  - registration: `lib/integrations/sanity/schemas/components/index.ts`,
24
27
  `lib/integrations/sanity/schemas/index.ts`,
25
28
  `lib/integrations/sanity/page-builder-config.ts`,
26
- `lib/integrations/sanity/structure.ts`,
27
29
  `components/page-builder/registry.tsx`
28
30
 
29
31
  A block exists in two halves — a schema and a component — and each half has one
30
- place that makes it a block rather than an ordinary document or a loose React
32
+ place that makes it a block rather than an ordinary object type or a loose React
31
33
  component. Use them and the conventions below come for free; skip them and a
32
34
  typecheck fails.
33
35
 
@@ -35,8 +37,10 @@ typecheck fails.
35
37
 
36
38
  1. **Define the schema with `defineBlock`, never `defineType`.**
37
39
  `lib/integrations/sanity/schemas/shared/block.ts` implies
38
- `type: "document"` and appends Anchor ID + Presentation Mode as the last two
39
- fields:
40
+ `type: "object"` and appends Anchor ID + Presentation Mode as the last two
41
+ fields. The array member's type name is the block's `_type`, so the schema
42
+ name is also what the registry, the insert menu and the fixture library key
43
+ off:
40
44
 
41
45
  ```ts
42
46
  import { defineField } from "sanity"
@@ -123,7 +127,15 @@ typecheck fails.
123
127
  `SANITY_LAYOUT_TAGS` (`fetchers/layout.ts`, or `tags.ts` on the Cache
124
128
  Components variant), same as navbar and footer.
125
129
 
126
- 5. **Regenerate types after a schema change:** `bun run sanity:typegen`. The
130
+ 5. **Ship a fixture in `components/page-builder/library.ts`.** One entry per
131
+ block: a stand-in value typed as the block's *query result* (so a schema or
132
+ projection change breaks the fixture at typecheck), a one-line note and a
133
+ stage background. That entry is the block's only preview — the live pane in
134
+ the insert menu, its row in Studio → Components and the `/components` route
135
+ all render it through the production renderer. Nothing needs adding to
136
+ `structure.ts`.
137
+
138
+ 6. **Regenerate types after a schema change:** `bun run sanity:typegen`. The
127
139
  page query spreads `...` for non-gated blocks, so new fields flow through
128
140
  automatically — except `gatedContent`, whose GROQ branch lists fields
129
141
  explicitly (`lib/integrations/sanity/queries.ts`). Add new fields there by
@@ -131,20 +143,21 @@ typecheck fails.
131
143
 
132
144
  ## Checklist for a new block
133
145
 
134
- - [ ] Schema in `schemas/components/reusable/<block>.ts` (or `singleton/` for a
135
- one-instance block), built with `defineBlock` and carrying a `preview`.
146
+ - [ ] Schema in `schemas/components/reusable/<block>.ts`, built with
147
+ `defineBlock` and carrying a `preview`.
136
148
  - [ ] Icon in `lib/integrations/sanity/icons.ts` (phosphor duotone).
137
149
  - [ ] Exported from `schemas/components/index.ts` and registered in
138
150
  `schemas/index.ts` (`schema.types`).
139
151
  - [ ] Member added to `page-builder-config.ts`: the `PageBuilderDocumentType`
140
- union plus an entry in `reusablePageBuilderReferenceMembers` carrying
141
- **both** axes — `pageTypes` (which page documents may host it) and
142
- `category` (what the block is, one of `componentCategories`; `misc` only
143
- when nothing fits).
144
- - [ ] Listed in `structure.ts` under the right folder.
152
+ union plus an entry in `pageBuilderBlockMembers` carrying **both** axes —
153
+ `pageTypes` (which page documents may host it) and `category` (what the
154
+ block is, one of `componentCategories`; `misc` only when nothing fits).
145
155
  - [ ] Component in `components/page-builder/components/<block>.tsx`, typed with
146
156
  `Extract<PageBuilderBlock, { _type: "<block>" }>`, no outer section, no
147
157
  vertical padding. Variants via `cva`/`cn` when the block has variants.
148
158
  - [ ] Entry in `components/page-builder/registry.tsx`, with `isEmpty` when the
149
159
  block can be left unfilled.
160
+ - [ ] Fixture in `components/page-builder/library.ts`, typed as the block's
161
+ query result. It is the block's preview everywhere, so nothing needs
162
+ adding to `structure.ts`.
150
163
  - [ ] `bun run sanity:typegen && bun run typecheck && bun run lint && bun run build && bun run knip`.
@@ -9,13 +9,13 @@ import { fontsVariable } from "@/lib/styles/fonts";
9
9
  import AppData from "@/package.json";
10
10
  import "@/lib/styles/css/index.css";
11
11
  import { cn } from "@/lib/styles/cn";
12
+ import { getBaseUrl } from "@/lib/utils/url";
12
13
 
13
14
  const APP_NAME = AppData.name;
14
15
  const APP_DEFAULT_TITLE = "Basement Starter";
15
16
  const APP_TITLE_TEMPLATE = "%s - Basement Starter";
16
17
  const APP_DESCRIPTION = AppData.description;
17
- const APP_BASE_URL =
18
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
18
+ const APP_BASE_URL = getBaseUrl();
19
19
 
20
20
  const geist = Geist({
21
21
  subsets: ["latin"],
@@ -1,4 +1,5 @@
1
1
  import type { Metadata } from "next";
2
+ import { getBaseUrl } from "@/lib/utils/url";
2
3
 
3
4
  /**
4
5
  * Metadata Generation Utilities
@@ -26,8 +27,7 @@ interface GenerateMetadataOptions {
26
27
  authors?: string[];
27
28
  }
28
29
 
29
- const APP_BASE_URL =
30
- process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
30
+ const APP_BASE_URL = getBaseUrl();
31
31
 
32
32
  /**
33
33
  * Generate complete metadata object for pages
@@ -3,15 +3,18 @@
3
3
  *
4
4
  * Priority:
5
5
  * 1. NEXT_PUBLIC_BASE_URL - explicit override (custom domains)
6
- * 2. VERCEL_PROJECT_PRODUCTION_URL - auto-set by Vercel (production domain)
7
- * 3. VERCEL_URL - auto-set by Vercel (preview/branch deploys)
6
+ * 2. VERCEL_PROJECT_PRODUCTION_URL - auto-set by Vercel, on production deployments only
7
+ * 3. VERCEL_URL - the deployment's own URL, so previews never emit production URLs
8
8
  * 4. localhost fallback
9
9
  */
10
10
  export function getBaseUrl(): string {
11
11
  if (process.env.NEXT_PUBLIC_BASE_URL) {
12
12
  return process.env.NEXT_PUBLIC_BASE_URL;
13
13
  }
14
- if (process.env.VERCEL_PROJECT_PRODUCTION_URL) {
14
+ if (
15
+ process.env.VERCEL_ENV === "production" &&
16
+ process.env.VERCEL_PROJECT_PRODUCTION_URL
17
+ ) {
15
18
  return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
16
19
  }
17
20
  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
@@ -8,7 +8,7 @@ import { parseBody } from "next-sanity/webhook";
8
8
  import { directClient } from "@/lib/integrations/sanity/client";
9
9
  import { SANITY_LAYOUT_TAGS } from "@/lib/integrations/sanity/fetchers/layout";
10
10
  import { SANITY_REDIRECTS_TAG } from "@/lib/integrations/sanity/fetchers/redirects";
11
- import { pageBuilderReferenceMembers as pbrm } from "@/lib/integrations/sanity/page-builder-config";
11
+ import { pageBuilderBlockMembers } from "@/lib/integrations/sanity/page-builder-config";
12
12
  import {
13
13
  FOLDER_PATH_QUERY,
14
14
  P_REF_DOC_PAGES_QUERY,
@@ -63,7 +63,7 @@ type RevalidateWebhookBody = WebhookDocument & {
63
63
  };
64
64
 
65
65
  const referenceableTypes = new Set<string>([
66
- ...pbrm.map((m) => m.documentType),
66
+ ...pageBuilderBlockMembers.map((member) => member.documentType),
67
67
  ...REFERENCEABLE_TYPES,
68
68
  ]);
69
69
 
@@ -4,7 +4,7 @@ import { SanityImage } from "@/components/ui/sanity-image";
4
4
  import {
5
5
  getLinkAttributes,
6
6
  urlForReference,
7
- } from "../../lib/integrations/sanity/utils/link";
7
+ } from "@/lib/integrations/sanity/utils/link";
8
8
 
9
9
  interface RichTextProps {
10
10
  content: PortableTextProps["value"];
@@ -80,7 +80,9 @@ export const RichText = ({ content }: RichTextProps) => {
80
80
  externalLink: LinkMark,
81
81
  },
82
82
  block: {
83
- h1: ({ children }) => <h1 className="h1">{children}</h1>,
83
+ // The h1 style is retired: the page owns its H1, so legacy content
84
+ // drops a level.
85
+ h1: ({ children }) => <h2 className="h2">{children}</h2>,
84
86
  h2: ({ children }) => <h2 className="h2">{children}</h2>,
85
87
  h3: ({ children }) => <h3 className="h3">{children}</h3>,
86
88
  h4: ({ children }) => <h4 className="h4">{children}</h4>,