bsmnt 0.9.0 → 0.11.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 (344) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +19 -2
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/application/shared/node-version.js.map +1 -1
  5. package/dist/core/create/internal-ids.d.ts +3 -0
  6. package/dist/core/create/internal-ids.d.ts.map +1 -1
  7. package/dist/core/create/internal-ids.js +4 -0
  8. package/dist/core/create/internal-ids.js.map +1 -1
  9. package/dist/domain/agents.d.ts.map +1 -1
  10. package/dist/domain/animations.d.ts.map +1 -1
  11. package/dist/domain/cms.d.ts.map +1 -1
  12. package/dist/domain/forms.d.ts.map +1 -1
  13. package/dist/domain/hooks.d.ts.map +1 -1
  14. package/dist/domain/skills.d.ts.map +1 -1
  15. package/dist/domain/templates.d.ts +10 -0
  16. package/dist/domain/templates.d.ts.map +1 -1
  17. package/dist/domain/templates.js +5 -0
  18. package/dist/domain/templates.js.map +1 -1
  19. package/dist/index.js +18 -5
  20. package/dist/index.js.map +1 -1
  21. package/dist/infrastructure/agents/register-sanity-mcp.js +3 -8
  22. package/dist/infrastructure/agents/register-sanity-mcp.js.map +1 -1
  23. package/dist/infrastructure/agents/register-storybook-mcp.d.ts +18 -0
  24. package/dist/infrastructure/agents/register-storybook-mcp.d.ts.map +1 -0
  25. package/dist/infrastructure/agents/register-storybook-mcp.js +157 -0
  26. package/dist/infrastructure/agents/register-storybook-mcp.js.map +1 -0
  27. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  28. package/dist/infrastructure/create/lifecycle-registry.js +39 -1
  29. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  30. package/dist/infrastructure/create/merge-registry.d.ts.map +1 -1
  31. package/dist/infrastructure/create/merge-registry.js +4 -0
  32. package/dist/infrastructure/create/merge-registry.js.map +1 -1
  33. package/dist/infrastructure/create/sanity-auth.js.map +1 -1
  34. package/dist/infrastructure/create/setup-agent.d.ts +6 -1
  35. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  36. package/dist/infrastructure/create/setup-agent.js +23 -21
  37. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  38. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
  39. package/dist/infrastructure/create/setup-remote.js.map +1 -1
  40. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  41. package/dist/infrastructure/create/setup-sanity.js +12 -1
  42. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  43. package/dist/infrastructure/integrations/ensure-studio-headers.d.ts +6 -0
  44. package/dist/infrastructure/integrations/ensure-studio-headers.d.ts.map +1 -0
  45. package/dist/infrastructure/integrations/ensure-studio-headers.js +153 -0
  46. package/dist/infrastructure/integrations/ensure-studio-headers.js.map +1 -0
  47. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  48. package/dist/infrastructure/integrations/inject-integration.js +2 -0
  49. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  50. package/dist/infrastructure/update/notifier.js.map +1 -1
  51. package/dist/modules/features/cms/index.d.ts.map +1 -1
  52. package/dist/modules/features/cms/index.js +2 -0
  53. package/dist/modules/features/cms/index.js.map +1 -1
  54. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  55. package/dist/modules/features/cms/sanity/config.js +3 -1
  56. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  57. package/dist/modules/features/cms/sanity/index.d.ts.map +1 -1
  58. package/dist/modules/features/cms/sanity/index.js +10 -1
  59. package/dist/modules/features/cms/sanity/index.js.map +1 -1
  60. package/dist/modules/features/cms/sanity/mergers/check-integration.d.ts.map +1 -1
  61. package/dist/modules/features/cms/sanity/mergers/check-integration.js +15 -6
  62. package/dist/modules/features/cms/sanity/mergers/check-integration.js.map +1 -1
  63. package/dist/modules/features/cms/sanity/mergers/knip.d.ts.map +1 -1
  64. package/dist/modules/features/cms/sanity/mergers/knip.js +1 -0
  65. package/dist/modules/features/cms/sanity/mergers/knip.js.map +1 -1
  66. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  67. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts +12 -0
  68. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts.map +1 -0
  69. package/dist/modules/features/cms/sanity/mergers/package-json.js +19 -0
  70. package/dist/modules/features/cms/sanity/mergers/package-json.js.map +1 -0
  71. package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
  72. package/dist/modules/features/cms/sanity-pagebuilder/config.js +2 -1
  73. package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
  74. package/dist/modules/features/cms/sanity-pagebuilder/index.d.ts.map +1 -1
  75. package/dist/modules/features/cms/sanity-pagebuilder/index.js +7 -1
  76. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  77. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts +14 -0
  78. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -0
  79. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +50 -0
  80. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -0
  81. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts +3 -0
  82. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts.map +1 -0
  83. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js +55 -0
  84. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js.map +1 -0
  85. package/dist/modules/presets/index.d.ts.map +1 -1
  86. package/dist/modules/presets/index.js +2 -0
  87. package/dist/modules/presets/index.js.map +1 -1
  88. package/dist/modules/presets/next-pagebuilder-cache.d.ts +3 -0
  89. package/dist/modules/presets/next-pagebuilder-cache.d.ts.map +1 -0
  90. package/dist/modules/presets/next-pagebuilder-cache.js +10 -0
  91. package/dist/modules/presets/next-pagebuilder-cache.js.map +1 -0
  92. package/dist/paths.d.ts +6 -0
  93. package/dist/paths.d.ts.map +1 -1
  94. package/dist/paths.js +6 -0
  95. package/dist/paths.js.map +1 -1
  96. package/package.json +6 -3
  97. package/src/modules/features/cms/sanity/files/app/api/blog/[slug]/route.ts +4 -1
  98. package/src/modules/features/cms/sanity/files/app/sitemap.md/route.ts +2 -1
  99. package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +19 -5
  100. package/src/modules/features/cms/sanity/files/lib/integrations/check-integration.ts +7 -6
  101. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/README.md +32 -1
  102. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +1 -0
  103. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/env.ts +2 -2
  104. package/src/modules/features/cms/sanity/files/proxy.ts +9 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +12 -3
  106. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +172 -24
  107. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +14 -7
  108. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +8 -2
  109. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +16 -1
  110. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +1 -0
  111. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +2 -2
  112. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +15 -5
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +6 -5
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +8 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-field.tsx +13 -7
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-notices.tsx +125 -0
  117. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +2 -2
  118. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +22 -52
  119. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
  120. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +4 -2
  121. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/markdown-proxy.config.ts +1 -1
  122. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +7 -0
  123. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +70 -57
  124. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +119 -29
  125. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  126. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  127. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  128. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +2 -0
  129. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +35 -4
  130. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +12 -0
  131. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +56 -0
  132. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +12 -0
  133. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +11 -8
  134. package/src/modules/features/cms/sanity-pagebuilder/files/proxy.ts +9 -0
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/.knip.json +8 -0
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/[[...slug]]/route.ts +112 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/disable/route.ts +7 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/enable/route.ts +20 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +292 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/feed.xml/route.ts +66 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/sitemap.md/route.ts +132 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/layout.tsx +26 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/page.tsx +23 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/draft-mode-toggle.tsx +27 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +88 -0
  146. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/visual-editing.tsx +11 -0
  147. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +51 -0
  148. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/check-integration.ts +6 -0
  149. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/client.ts +37 -0
  150. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
  151. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  152. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +39 -0
  153. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  154. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  155. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-field.tsx +24 -0
  156. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  157. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-notices.tsx +125 -0
  158. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  159. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/env.ts +34 -0
  160. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +39 -0
  161. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/redirects.ts +22 -0
  162. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +81 -0
  163. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/live/index.tsx +140 -0
  164. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  165. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +144 -0
  166. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-category.ts +28 -0
  167. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-tree-pane.tsx +431 -0
  168. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  169. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +119 -0
  170. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +548 -0
  171. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  172. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  173. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +116 -0
  174. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +6 -0
  175. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  176. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  177. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
  178. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  179. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  180. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  181. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  182. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  183. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +134 -0
  184. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/company-data.ts +70 -0
  185. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  186. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  187. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +152 -0
  188. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  189. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  190. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  191. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +132 -0
  192. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  193. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +43 -0
  194. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  195. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +265 -0
  196. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  197. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  198. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +51 -0
  199. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +334 -0
  200. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +89 -0
  201. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/image.ts +8 -0
  202. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/link.ts +140 -0
  203. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-builder-markdown.ts +113 -0
  204. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  205. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +36 -0
  206. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/check-redirect.ts +30 -0
  207. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/json-ld.tsx +522 -0
  208. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +189 -0
  209. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/rss.ts +96 -0
  210. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/sitemap.ts +58 -0
  211. package/src/modules/features/cms/sanity-pagebuilder-cache/files/proxy.ts +90 -0
  212. package/src/modules/features/env/dotenvx/files/vercelignore +5 -0
  213. package/src/templates/next-default/.env.example +4 -2
  214. package/src/templates/next-default/.github/workflows/ci.yml +1 -1
  215. package/src/templates/next-default/.vscode/settings.json +0 -2
  216. package/src/templates/next-default/app/robots.ts +2 -3
  217. package/src/templates/next-default/components/ui/image/index.tsx +2 -4
  218. package/src/templates/next-default/knip.json +1 -1
  219. package/src/templates/next-default/lefthook.yml +1 -1
  220. package/src/templates/next-default/lib/utils/README.md +18 -5
  221. package/src/templates/next-default/lib/utils/image-sizes.ts +72 -0
  222. package/src/templates/next-default/lib/utils/merge-refs.ts +56 -0
  223. package/src/templates/next-default/lib/utils/strings.ts +1 -51
  224. package/src/templates/next-default/next.config.ts +38 -5
  225. package/src/templates/next-default/package.json +9 -12
  226. package/src/templates/next-experiments/.env.example +4 -2
  227. package/src/templates/next-experiments/.github/workflows/ci.yml +1 -1
  228. package/src/templates/next-experiments/.vscode/settings.json +0 -2
  229. package/src/templates/next-experiments/app/page.tsx +1 -1
  230. package/src/templates/next-experiments/app/robots.ts +2 -3
  231. package/src/templates/next-experiments/components/ui/image/index.tsx +2 -4
  232. package/src/templates/next-experiments/knip.json +1 -1
  233. package/src/templates/next-experiments/lefthook.yml +1 -1
  234. package/src/templates/next-experiments/lib/utils/README.md +18 -5
  235. package/src/templates/next-experiments/lib/utils/image-sizes.ts +72 -0
  236. package/src/templates/next-experiments/lib/utils/merge-refs.ts +56 -0
  237. package/src/templates/next-experiments/lib/utils/strings.ts +1 -51
  238. package/src/templates/next-experiments/next.config.ts +38 -5
  239. package/src/templates/next-experiments/package.json +9 -12
  240. package/src/templates/next-pagebuilder/.env.example +3 -0
  241. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +1 -1
  242. package/src/templates/next-pagebuilder/.vscode/settings.json +0 -2
  243. package/src/templates/next-pagebuilder/README.md +95 -0
  244. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +24 -9
  245. package/src/templates/next-pagebuilder/app/robots.ts +2 -3
  246. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +9 -4
  247. package/src/templates/next-pagebuilder/biome.json +2 -1
  248. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +3 -6
  249. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -2
  250. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +3 -8
  251. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +23 -40
  252. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +88 -58
  253. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/skeleton.tsx +41 -0
  254. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/types.ts +14 -1
  255. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +16 -0
  256. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +23 -3
  257. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -0
  258. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +2 -1
  259. package/src/templates/next-pagebuilder/knip.json +3 -2
  260. package/src/templates/next-pagebuilder/lefthook.yml +1 -1
  261. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +1221 -977
  262. package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +72 -0
  263. package/src/templates/next-pagebuilder/lib/utils/merge-refs.ts +56 -0
  264. package/src/templates/next-pagebuilder/next.config.ts +35 -5
  265. package/src/templates/next-pagebuilder/package.json +8 -10
  266. package/src/templates/next-pagebuilder-cache/.biome/plugins/README.md +21 -0
  267. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-anchor-element.grit +12 -0
  268. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-relative-parent-imports.grit +10 -0
  269. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  270. package/src/templates/next-pagebuilder-cache/.env.example +40 -0
  271. package/src/templates/next-pagebuilder-cache/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  272. package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +105 -0
  273. package/src/templates/next-pagebuilder-cache/.vscode/extensions.json +19 -0
  274. package/src/templates/next-pagebuilder-cache/.vscode/settings.json +103 -0
  275. package/src/templates/next-pagebuilder-cache/.zed/settings.json +47 -0
  276. package/src/templates/next-pagebuilder-cache/README.md +212 -0
  277. package/src/templates/next-pagebuilder-cache/_gitignore +65 -0
  278. package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +124 -0
  279. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +28 -0
  280. package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +53 -0
  281. package/src/templates/next-pagebuilder-cache/app/favicon.ico +0 -0
  282. package/src/templates/next-pagebuilder-cache/app/layout.tsx +83 -0
  283. package/src/templates/next-pagebuilder-cache/app/robots.ts +13 -0
  284. package/src/templates/next-pagebuilder-cache/app/sitemap.xml/route.ts +84 -0
  285. package/src/templates/next-pagebuilder-cache/biome.json +257 -0
  286. package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +127 -0
  287. package/src/templates/next-pagebuilder-cache/components/layout/header/components/cta-button.tsx +28 -0
  288. package/src/templates/next-pagebuilder-cache/components/layout/header/components/mega-menu-panel.tsx +90 -0
  289. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-item-renderer.tsx +98 -0
  290. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-leaf-item.tsx +33 -0
  291. package/src/templates/next-pagebuilder-cache/components/layout/header/components/types.ts +7 -0
  292. package/src/templates/next-pagebuilder-cache/components/layout/header/header-client.tsx +110 -0
  293. package/src/templates/next-pagebuilder-cache/components/layout/header/index.tsx +32 -0
  294. package/src/templates/next-pagebuilder-cache/components/layout/json-ld/index.tsx +79 -0
  295. package/src/templates/next-pagebuilder-cache/components/layout/wrapper/index.tsx +33 -0
  296. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +78 -0
  297. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +27 -0
  298. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-card.tsx +66 -0
  299. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-filters.tsx +76 -0
  300. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-grid.tsx +40 -0
  301. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  302. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +306 -0
  303. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/skeleton.tsx +41 -0
  304. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/types.ts +32 -0
  305. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +17 -0
  306. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +169 -0
  307. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +17 -0
  308. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +16 -0
  309. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +71 -0
  310. package/src/templates/next-pagebuilder-cache/components/page-builder/types.ts +25 -0
  311. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +147 -0
  312. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +216 -0
  313. package/src/templates/next-pagebuilder-cache/components/ui/link/index.tsx +152 -0
  314. package/src/templates/next-pagebuilder-cache/components.json +24 -0
  315. package/src/templates/next-pagebuilder-cache/knip.json +35 -0
  316. package/src/templates/next-pagebuilder-cache/lefthook.yml +25 -0
  317. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +1588 -0
  318. package/src/templates/next-pagebuilder-cache/lib/scripts/setup-ci-secrets.ts +117 -0
  319. package/src/templates/next-pagebuilder-cache/lib/styles/cn.ts +5 -0
  320. package/src/templates/next-pagebuilder-cache/lib/styles/global.css +69 -0
  321. package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +17 -0
  322. package/src/templates/next-pagebuilder-cache/lib/utils/format-date.ts +8 -0
  323. package/src/templates/next-pagebuilder-cache/lib/utils/slug-tag.ts +6 -0
  324. package/src/templates/next-pagebuilder-cache/next.config.ts +176 -0
  325. package/src/templates/next-pagebuilder-cache/package.json +62 -0
  326. package/src/templates/next-pagebuilder-cache/postcss.config.mjs +65 -0
  327. package/src/templates/next-pagebuilder-cache/svg.d.ts +6 -0
  328. package/src/templates/next-pagebuilder-cache/tsconfig.json +38 -0
  329. package/src/templates/next-webgl/.env.example +4 -2
  330. package/src/templates/next-webgl/.github/workflows/ci.yml +1 -1
  331. package/src/templates/next-webgl/.vscode/settings.json +0 -2
  332. package/src/templates/next-webgl/app/robots.ts +2 -3
  333. package/src/templates/next-webgl/components/ui/image/index.tsx +2 -4
  334. package/src/templates/next-webgl/knip.json +1 -1
  335. package/src/templates/next-webgl/lefthook.yml +1 -1
  336. package/src/templates/next-webgl/lib/utils/README.md +18 -5
  337. package/src/templates/next-webgl/lib/utils/image-sizes.ts +72 -0
  338. package/src/templates/next-webgl/lib/utils/merge-refs.ts +56 -0
  339. package/src/templates/next-webgl/lib/utils/strings.ts +1 -51
  340. package/src/templates/next-webgl/next.config.ts +38 -5
  341. package/src/templates/next-webgl/package.json +11 -14
  342. package/src/templates/next-experiments/tsconfig.tsbuildinfo +0 -1
  343. package/src/templates/next-pagebuilder/app/actions/refresh.ts +0 -5
  344. package/src/templates/next-webgl/tsconfig.tsbuildinfo +0 -1
@@ -1,22 +1,13 @@
1
1
  import { cache } from "react";
2
2
  import { fetchSanity } from "@/lib/integrations/sanity/live";
3
- import {
4
- COMPANY_DATA_QUERY,
5
- FOOTER_QUERY,
6
- NAVBAR_QUERY,
7
- STRUCTURED_DATA_QUERY,
8
- } from "@/lib/integrations/sanity/queries";
9
- import type {
10
- COMPANY_DATA_QUERY_RESULT,
11
- FOOTER_QUERY_RESULT,
12
- NAVBAR_QUERY_RESULT,
13
- STRUCTURED_DATA_QUERY_RESULT,
14
- } from "@/lib/integrations/sanity/sanity.types";
3
+ import { LAYOUT_QUERY } from "@/lib/integrations/sanity/queries";
4
+ import type { LAYOUT_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
15
5
 
16
- export type NavbarData = NonNullable<NAVBAR_QUERY_RESULT>;
17
- export type FooterData = NonNullable<FOOTER_QUERY_RESULT>;
18
- export type CompanyData = NonNullable<COMPANY_DATA_QUERY_RESULT>;
19
- export type StructuredData = NonNullable<STRUCTURED_DATA_QUERY_RESULT>;
6
+ export type LayoutData = LAYOUT_QUERY_RESULT;
7
+ export type NavbarData = NonNullable<LayoutData["navbar"]>;
8
+ export type FooterData = NonNullable<LayoutData["footer"]>;
9
+ export type CompanyData = NonNullable<LayoutData["companyData"]>;
10
+ export type StructuredData = NonNullable<LayoutData["structuredData"]>;
20
11
 
21
12
  // Singleton document types that, when published, should bust the layout cache.
22
13
  export const SANITY_LAYOUT_TAGS = [
@@ -26,42 +17,21 @@ export const SANITY_LAYOUT_TAGS = [
26
17
  "structuredData",
27
18
  ];
28
19
 
29
- // Wrapped in React cache so components sharing a singleton (e.g. Footer + JsonLd
30
- // both reading companyData) only hit Sanity once per request.
31
- export const getNavbarData = cache(async (): Promise<NavbarData | null> => {
32
- const { data } = await fetchSanity<NavbarData>({
33
- query: NAVBAR_QUERY,
34
- tags: ["navbar"],
20
+ const EMPTY_LAYOUT: LayoutData = {
21
+ navbar: null,
22
+ footer: null,
23
+ companyData: null,
24
+ structuredData: null,
25
+ };
26
+
27
+ // One query for all four layout singletons, wrapped in React cache so the header,
28
+ // footer and json-ld share a single round trip. Tagged with all four `_type` tags
29
+ // because it reads all four documents.
30
+ export const getLayoutData = cache(async (): Promise<LayoutData> => {
31
+ const { data } = await fetchSanity<LayoutData>({
32
+ query: LAYOUT_QUERY,
33
+ tags: SANITY_LAYOUT_TAGS,
35
34
  });
36
35
 
37
- return data ?? null;
36
+ return data ?? EMPTY_LAYOUT;
38
37
  });
39
-
40
- export const getFooterData = cache(async (): Promise<FooterData | null> => {
41
- const { data } = await fetchSanity<FooterData>({
42
- query: FOOTER_QUERY,
43
- tags: ["footer"],
44
- });
45
-
46
- return data ?? null;
47
- });
48
-
49
- export const getCompanyData = cache(async (): Promise<CompanyData | null> => {
50
- const { data } = await fetchSanity<CompanyData>({
51
- query: COMPANY_DATA_QUERY,
52
- tags: ["companyData"],
53
- });
54
-
55
- return data ?? null;
56
- });
57
-
58
- export const getStructuredData = cache(
59
- async (): Promise<StructuredData | null> => {
60
- const { data } = await fetchSanity<StructuredData>({
61
- query: STRUCTURED_DATA_QUERY,
62
- tags: ["structuredData"],
63
- });
64
-
65
- return data ?? null;
66
- },
67
- );
@@ -17,13 +17,13 @@ import {
17
17
  ListIcon,
18
18
  LockKeyIcon,
19
19
  PackageIcon,
20
- PushPinIcon,
21
20
  QuestionIcon,
22
21
  RocketIcon,
23
22
  ShapesIcon,
24
23
  ShareNetworkIcon,
25
24
  TagIcon,
26
25
  TextAlignLeftIcon,
26
+ TextTIcon,
27
27
  TreeStructureIcon,
28
28
  UserCircleIcon,
29
29
  VideoCameraIcon,
@@ -71,11 +71,11 @@ export const customJsonLdIcon = asDuotoneIcon(BracketsCurlyIcon);
71
71
 
72
72
  export const heroIcon = asDuotoneIcon(RocketIcon);
73
73
  export const descriptionIcon = asDuotoneIcon(TextAlignLeftIcon);
74
+ export const richTextIcon = asDuotoneIcon(TextTIcon);
74
75
  export const contentIcon = asDuotoneIcon(ArticleNyTimesIcon);
75
76
  export const gatedContentIcon = asDuotoneIcon(LockKeyIcon);
76
77
 
77
78
  export const pageIcon = asDuotoneIcon(FileTextIcon);
78
- export const pinIcon = asDuotoneIcon(PushPinIcon);
79
79
  export const pagesFolderIcon = asDuotoneIcon(BrowsersIcon);
80
80
  export const pageFolderIcon = asDuotoneIcon(FolderOpenIcon);
81
81
  export const contentFolderIcon = asDuotoneIcon(FolderOpenIcon);
@@ -25,8 +25,10 @@ type SanityFetchOptions = {
25
25
  query: string;
26
26
  params?: QueryParams;
27
27
  tags?: string[];
28
- stega?: boolean;
29
- perspective?: "published" | "drafts" | "previewDrafts";
28
+ stega?: boolean | undefined;
29
+ // `previewDrafts` was the old name for `drafts` and is gone from the API
30
+ // version this template pins.
31
+ perspective?: "published" | "drafts";
30
32
  };
31
33
 
32
34
  export async function fetchSanity<T = unknown>({
@@ -22,7 +22,7 @@ export interface MarkdownRoute {
22
22
  /** .md extension routes -- rewrite to API endpoints */
23
23
  export const mdExtensionRoutes: MarkdownRoute[] = [
24
24
  {
25
- regex: /^\/(.+)\.md$/,
25
+ regex: /^\/((?:[^/]+\/)*[^/]+)\.md$/,
26
26
  apiPath: "/api/[slug].md",
27
27
  publicPath: "/[slug].md",
28
28
  },
@@ -4,6 +4,7 @@ import { singletonComponents } from "./singletons";
4
4
  export type PageBuilderDocumentType =
5
5
  | "hero"
6
6
  | "description"
7
+ | "richTextBlock"
7
8
  | "gatedContent"
8
9
  | "blogContent"
9
10
  | (typeof singletonComponents)[number]["schemaType"];
@@ -48,6 +49,12 @@ const reusablePageBuilderReferenceMembers: PageBuilderReferenceMember[] = [
48
49
  title: "Description",
49
50
  groups: ["generic"],
50
51
  },
52
+ {
53
+ documentType: "richTextBlock",
54
+ name: "richTextBlockReference",
55
+ title: "Rich Text",
56
+ groups: ["generic"],
57
+ },
51
58
  {
52
59
  documentType: "gatedContent",
53
60
  name: "gatedContentReference",
@@ -1,4 +1,7 @@
1
- import { MagnifyingGlassIcon } from "@phosphor-icons/react/dist/ssr";
1
+ import {
2
+ MagnifyingGlassIcon,
3
+ PushPinIcon,
4
+ } from "@phosphor-icons/react/dist/ssr";
2
5
  import {
3
6
  Badge,
4
7
  Box,
@@ -9,13 +12,13 @@ import {
9
12
  Text,
10
13
  TextInput,
11
14
  } from "@sanity/ui";
12
- import { useEffect, useRef, useState } from "react";
15
+ import { type ReactNode, useEffect, useRef, useState } from "react";
13
16
  import { asyncScheduler, from, merge, of } from "rxjs";
14
17
  import { switchMap, throttleTime } from "rxjs/operators";
15
18
  import { type SanityClient, useClient } from "sanity";
16
19
  import { usePaneRouter } from "sanity/structure";
17
20
  import { apiVersion } from "./env";
18
- import { pageFolderIcon, pinIcon } from "./icons";
21
+ import { pageFolderIcon } from "./icons";
19
22
  import {
20
23
  getPageCategoryIcon,
21
24
  type PageBuilderGroupName,
@@ -317,13 +320,30 @@ const ItemIcon = ({ item }: { item: PageTreeItem }) => {
317
320
  return <Icon />;
318
321
  };
319
322
 
320
- const ItemRow = ({ item, ChildLink, activeState }: ItemRowProps) => {
321
- const fallback = item._type === "page" ? "Untitled Page" : "Untitled Folder";
323
+ // One presentation for every row in the tree, so pinned entries and documents
324
+ // can't drift apart on spacing or which lines they show.
325
+ const TreeRow = ({
326
+ childId,
327
+ icon,
328
+ title,
329
+ path,
330
+ badge,
331
+ ChildLink,
332
+ activeState,
333
+ }: {
334
+ childId: string;
335
+ icon: ReactNode;
336
+ title: string;
337
+ path: string | null;
338
+ badge?: ReactNode;
339
+ ChildLink: ReturnType<typeof usePaneRouter>["ChildLink"];
340
+ activeState: ActiveState;
341
+ }) => {
322
342
  const isLeaf = activeState === "leaf";
323
343
  const isAncestor = activeState === "ancestor";
324
344
 
325
345
  return (
326
- <ChildLink childId={getItemId(item)}>
346
+ <ChildLink childId={childId}>
327
347
  <Card
328
348
  as="div"
329
349
  padding={3}
@@ -344,23 +364,22 @@ const ItemRow = ({ item, ChildLink, activeState }: ItemRowProps) => {
344
364
  }}
345
365
  >
346
366
  <Flex align="center" gap={3}>
367
+ {/* Sized here rather than by the caller: Phosphor icons are 1em, so
368
+ one left to inherit the ambient font size renders wider than one
369
+ wrapped in <Text> and pushes its whole row out of line. */}
347
370
  <Box style={{ flexShrink: 0 }}>
348
- <ItemIcon item={item} />
371
+ <Text size={1}>{icon}</Text>
349
372
  </Box>
350
373
  <Stack space={2} flex={1}>
351
374
  <Flex align="center" gap={2}>
352
375
  <Text size={1} weight="medium">
353
- {item.title ?? fallback}
376
+ {title}
354
377
  </Text>
355
- {isDraftItem(item) && (
356
- <Badge fontSize={0} mode="outline" tone="caution">
357
- Draft
358
- </Badge>
359
- )}
378
+ {badge}
360
379
  </Flex>
361
- {item.slug && (
380
+ {path && (
362
381
  <Text size={0} muted weight="regular">
363
- /{item.slug}
382
+ {path}
364
383
  </Text>
365
384
  )}
366
385
  </Stack>
@@ -370,49 +389,43 @@ const ItemRow = ({ item, ChildLink, activeState }: ItemRowProps) => {
370
389
  );
371
390
  };
372
391
 
373
- interface PinnedRowProps {
374
- entry: PinnedPageEntry;
375
- ChildLink: ReturnType<typeof usePaneRouter>["ChildLink"];
376
- activeState: ActiveState;
377
- }
378
-
379
- const PinnedRow = ({ entry, ChildLink, activeState }: PinnedRowProps) => {
380
- const isLeaf = activeState === "leaf";
381
- const isAncestor = activeState === "ancestor";
382
- const Icon = pinIcon;
392
+ const ItemRow = ({ item, ChildLink, activeState }: ItemRowProps) => {
393
+ const fallback = item._type === "page" ? "Untitled Page" : "Untitled Folder";
383
394
 
384
395
  return (
385
- <ChildLink childId={entry.id}>
386
- <Card
387
- as="div"
388
- padding={3}
389
- radius={0}
390
- pressed={isLeaf}
391
- selected={isAncestor}
392
- tone={isLeaf ? "primary" : "default"}
393
- style={{
394
- cursor: "pointer",
395
- display: "block",
396
- ...(isLeaf && {
397
- backgroundColor: "var(--card-badge-primary-bg-color)",
398
- color: "var(--card-badge-primary-fg-color)",
399
- }),
400
- ...(isAncestor && {
401
- backgroundColor: "var(--card-muted-bg-color)",
402
- }),
403
- }}
404
- >
405
- <Flex align="center" gap={3}>
406
- <Box style={{ flexShrink: 0 }}>
407
- <Icon />
408
- </Box>
409
- <Stack space={2} flex={1}>
410
- <Text size={1} weight="medium">
411
- {entry.title}
412
- </Text>
413
- </Stack>
414
- </Flex>
415
- </Card>
416
- </ChildLink>
396
+ <TreeRow
397
+ childId={getItemId(item)}
398
+ icon={<ItemIcon item={item} />}
399
+ title={item.title ?? fallback}
400
+ path={item.slug ? `/${item.slug}` : null}
401
+ badge={
402
+ isDraftItem(item) ? (
403
+ <Badge fontSize={0} mode="outline" tone="caution">
404
+ Draft
405
+ </Badge>
406
+ ) : undefined
407
+ }
408
+ ChildLink={ChildLink}
409
+ activeState={activeState}
410
+ />
417
411
  );
418
412
  };
413
+
414
+ const PinnedRow = ({
415
+ entry,
416
+ ChildLink,
417
+ activeState,
418
+ }: {
419
+ entry: PinnedPageEntry;
420
+ ChildLink: ReturnType<typeof usePaneRouter>["ChildLink"];
421
+ activeState: ActiveState;
422
+ }) => (
423
+ <TreeRow
424
+ childId={entry.id}
425
+ icon={<PushPinIcon />}
426
+ title={entry.title}
427
+ path={entry.path}
428
+ ChildLink={ChildLink}
429
+ activeState={activeState}
430
+ />
431
+ );
@@ -18,6 +18,32 @@ const pageResolvedSlugExpression = `
18
18
  )
19
19
  `;
20
20
 
21
+ // Hand-expanded, not generated: typegen walks the AST for `defineQuery` args and
22
+ // silently drops the result type for anything built by `.map().join()`.
23
+
24
+ // Root-to-leaf so joining yields the prefix in order. Paired id/slug, not two
25
+ // arrays: `array::compact` can drop from one and not the other.
26
+ const folderChainProjection = `pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->{ "id": _id, "slug": slug.current },
27
+ pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->{ "id": _id, "slug": slug.current },
28
+ pageFolder->parentFolder->parentFolder->parentFolder->{ "id": _id, "slug": slug.current },
29
+ pageFolder->parentFolder->parentFolder->{ "id": _id, "slug": slug.current },
30
+ pageFolder->parentFolder->{ "id": _id, "slug": slug.current },
31
+ pageFolder->{ "id": _id, "slug": slug.current }`;
32
+
33
+ const folderDescendantConditions = `pageFolder._ref == $folderId ||
34
+ pageFolder->parentFolder._ref == $folderId ||
35
+ pageFolder->parentFolder->parentFolder._ref == $folderId ||
36
+ pageFolder->parentFolder->parentFolder->parentFolder._ref == $folderId ||
37
+ pageFolder->parentFolder->parentFolder->parentFolder->parentFolder._ref == $folderId ||
38
+ pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder._ref == $folderId`;
39
+
40
+ // Rooted at a `pageFolder`, so the walk starts at `parentFolder`.
41
+ const folderParentChain = `parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current,
42
+ parentFolder->parentFolder->parentFolder->parentFolder->slug.current,
43
+ parentFolder->parentFolder->parentFolder->slug.current,
44
+ parentFolder->parentFolder->slug.current,
45
+ parentFolder->slug.current`;
46
+
21
47
  const linkItemProjection = `
22
48
  ...,
23
49
  _type == "pageReference" => {
@@ -84,6 +110,12 @@ const pageBuilderProjection = `
84
110
  },
85
111
  _type != "gatedContent" => {
86
112
  ...,
113
+ _type == "richTextBlock" => {
114
+ ...,
115
+ content[]{
116
+ ${richTextLinkProjection}
117
+ }
118
+ },
87
119
  _type == "blogContent" => {
88
120
  ...,
89
121
  categories[]->{
@@ -143,6 +175,7 @@ const pageProjection = `
143
175
  title,
144
176
  slug,
145
177
  "resolvedSlug": ${pageResolvedSlugExpression},
178
+ "collectionCategory": collectionCategory->{title, "slug": slug.current},
146
179
  ${pageBuilderProjection},
147
180
  metadata,
148
181
  _updatedAt
@@ -196,10 +229,16 @@ export const P_REV_SLUGS_QUERY = defineQuery(`
196
229
  }
197
230
  `);
198
231
 
199
- export const P_REF_PB_COMP_QUERY = defineQuery(`
232
+ // Pages depending on `$documentIds`, one or two hops out: one covers
233
+ // `collectionCategory->`, two covers anything reached through a page-builder
234
+ // block. All inline the referenced title or slug, so a change there goes stale.
235
+ export const P_REF_DOC_PAGES_QUERY = defineQuery(`
200
236
  *[
201
237
  _type == "page" &&
202
- count(pageBuilder[_ref == $documentId]) > 0
238
+ (
239
+ references($documentIds) ||
240
+ count(pageBuilder[_ref in *[references($documentIds)]._id]) > 0
241
+ )
203
242
  ] | order(title asc) {
204
243
  _id,
205
244
  title,
@@ -207,6 +246,43 @@ export const P_REF_PB_COMP_QUERY = defineQuery(`
207
246
  }
208
247
  `);
209
248
 
249
+ // A folder's resolved path, plus its own segment and parent path separately —
250
+ // what the webhook needs to rebuild where it used to be.
251
+ export const FOLDER_PATH_QUERY = defineQuery(`
252
+ *[_type == "pageFolder" && _id == $folderId][0]{
253
+ "slug": slug.current,
254
+ "parentPath": array::join(array::compact([
255
+ ${folderParentChain}
256
+ ]), "/"),
257
+ "path": array::join(array::compact([
258
+ ${folderParentChain},
259
+ slug.current
260
+ ]), "/")
261
+ }
262
+ `);
263
+
264
+ // Every page under `$folderId`, at any depth. The folder chain comes back as
265
+ // id/slug pairs rather than two parallel arrays -- `array::compact` on two
266
+ // independent lists can drop entries from one and not the other, and the webhook
267
+ // indexes into the chain by id. The page's own slug stays *out* of the array:
268
+ // `array::compact` removes null but not "" or "/", so folding a folder-index
269
+ // page's empty slug in here would yield "parent/" where the resolved path is
270
+ // "parent". `composeResolvedSlug` joins the leaf instead.
271
+ export const P_REV_FOLDER_DESCENDANTS_QUERY = defineQuery(`
272
+ *[
273
+ _type == "page" &&
274
+ (
275
+ ${folderDescendantConditions}
276
+ )
277
+ ]{
278
+ _id,
279
+ "folderChain": array::compact([
280
+ ${folderChainProjection}
281
+ ]),
282
+ "ownSlug": slug.current
283
+ }
284
+ `);
285
+
210
286
  export const PAGE_SITEMAP_QUERY = defineQuery(`
211
287
  *[
212
288
  _type == "page" &&
@@ -330,19 +406,31 @@ export const ALL_BLOG_CATEGORIES_QUERY = defineQuery(`
330
406
  }
331
407
  `);
332
408
 
333
- // Company Data
334
- export const COMPANY_DATA_QUERY = defineQuery(`
335
- *[_type == "companyData"][0]{
336
- socialLinks[]{
337
- _key,
338
- name,
339
- icon,
340
- url,
341
- label
342
- }
409
+ // Every page hosting a content collection, so the filter chips can link to the
410
+ // page pinned to each category instead of appending a `?category=` param.
411
+ // Hidden keys are filtered like `pageBuilderProjection` does — a page whose
412
+ // collection block is hidden renders no collection, so it can't be a chip target.
413
+ export const COLLECTION_PAGES_QUERY = defineQuery(`
414
+ *[
415
+ _type == "page" &&
416
+ count(pageBuilder[!(_key in coalesce(^.hiddenPageBuilderKeys, [])) && _ref in *[_type == "blogCollection"]._id]) > 0
417
+ ] {
418
+ "path": ${pageResolvedSlugExpression},
419
+ "category": collectionCategory->slug.current
343
420
  }
344
421
  `);
345
422
 
423
+ // Company Data -- folded into LAYOUT_QUERY below.
424
+ const companyDataProjection = `
425
+ socialLinks[]{
426
+ _key,
427
+ name,
428
+ icon,
429
+ url,
430
+ label
431
+ }
432
+ `;
433
+
346
434
  // Navbar
347
435
  // Shared link projection for navbar -- flattens array-of-one to single object
348
436
  const navLinkProjection = `
@@ -427,23 +515,25 @@ const structuredDataProjection = `
427
515
  ))
428
516
  `;
429
517
 
430
- export const NAVBAR_QUERY = defineQuery(`
431
- *[_type == "navbar"][0]{
432
- ${navbarProjection}
433
- }
434
- `);
435
-
436
- // Footer
437
- export const FOOTER_QUERY = defineQuery(`
438
- *[_type == "footer"][0]{
439
- ${footerProjection}
440
- }
441
- `);
442
-
443
- // Structured Data (site-wide JSON-LD identity)
444
- export const STRUCTURED_DATA_QUERY = defineQuery(`
445
- *[_type == "structuredData"][0]{
446
- ${structuredDataProjection}
518
+ // All four layout singletons in one round trip: every route's shell reads three of
519
+ // them, and as separate parameterless queries that was four Data Cache entries and
520
+ // four fetches. The single-document NAVBAR_QUERY / FOOTER_QUERY /
521
+ // COMPANY_DATA_QUERY / STRUCTURED_DATA_QUERY exports are gone on purpose — nothing
522
+ // consumed them, and they were a second untagged path to the same documents.
523
+ export const LAYOUT_QUERY = defineQuery(`
524
+ {
525
+ "navbar": *[_type == "navbar"][0]{
526
+ ${navbarProjection}
527
+ },
528
+ "footer": *[_type == "footer"][0]{
529
+ ${footerProjection}
530
+ },
531
+ "companyData": *[_type == "companyData"][0]{
532
+ ${companyDataProjection}
533
+ },
534
+ "structuredData": *[_type == "structuredData"][0]{
535
+ ${structuredDataProjection}
536
+ }
447
537
  }
448
538
  `);
449
539
 
@@ -0,0 +1,70 @@
1
+ // Sibling-relative, not `@/`: the repo's test suite imports this file directly.
2
+ import { isEmptyPageSlug } from "./page-category";
3
+
4
+ // Path arithmetic for the publish webhook. Free of `next/*` so it can be unit
5
+ // tested, because the payload carries the raw document, never the resolved URL.
6
+
7
+ export interface FolderChainEntry {
8
+ id: string | null;
9
+ slug: string | null;
10
+ }
11
+
12
+ export interface DescendantPaths {
13
+ current: string | null;
14
+ previous: string | null;
15
+ }
16
+
17
+ interface DescendantPathsInput {
18
+ /** Root-to-leaf folder chain of the descendant, as projected by GROQ. */
19
+ folderChain: readonly FolderChainEntry[];
20
+ ownSlug: string | null | undefined;
21
+ /** The mutated folder. */
22
+ folderId: string;
23
+ previousFolderSlug: string;
24
+ /** Resolved path of the folder's *previous* parent; null at the root. */
25
+ previousParentPath: string | null;
26
+ }
27
+
28
+ // The JS half of `pageResolvedSlugExpression`. An empty own slug means the page
29
+ // *is* its folder's index, so the folder path is the whole URL.
30
+ export const composeResolvedSlug = (
31
+ folderPath: string | null,
32
+ ownSlug: string | null | undefined,
33
+ ): string | null => {
34
+ if (isEmptyPageSlug(ownSlug)) return folderPath || null;
35
+
36
+ return folderPath ? `${folderPath}/${ownSlug}` : (ownSlug ?? null);
37
+ };
38
+
39
+ // Where a descendant of a moved folder lives now and where it lived before.
40
+ // Null when the chain can't be trusted: a fabricated old path would purge an
41
+ // unrelated URL and leave the real one stale.
42
+ export const composeDescendantPaths = ({
43
+ folderChain,
44
+ ownSlug,
45
+ folderId,
46
+ previousFolderSlug,
47
+ previousParentPath,
48
+ }: DescendantPathsInput): DescendantPaths | null => {
49
+ const index = folderChain.findIndex((folder) => folder.id === folderId);
50
+ // Selected *by* this id, so an absent one means chain and filter disagree.
51
+ if (index === -1) return null;
52
+
53
+ const segments: string[] = [];
54
+ for (const folder of folderChain) {
55
+ // Required on `pageFolder`; a gap would silently shift every segment.
56
+ if (!folder.slug) return null;
57
+ segments.push(folder.slug);
58
+ }
59
+
60
+ const previousFolderPath = [
61
+ ...(previousParentPath ? [previousParentPath] : []),
62
+ previousFolderSlug,
63
+ ...segments.slice(index + 1),
64
+ ].join("/");
65
+
66
+ return {
67
+ current: composeResolvedSlug(segments.join("/"), ownSlug),
68
+ previous: composeResolvedSlug(previousFolderPath, ownSlug),
69
+ };
70
+ };
@@ -2,4 +2,5 @@ export { blogContent } from "./reusable/blog-content";
2
2
  export { description } from "./reusable/description";
3
3
  export { gatedContent } from "./reusable/gated-content";
4
4
  export { hero } from "./reusable/hero";
5
+ export { richTextBlock } from "./reusable/rich-text-block";
5
6
  export { blogCollection } from "./singleton/content-collection";
@@ -0,0 +1,39 @@
1
+ import { defineField, defineType } from "sanity";
2
+ import { richTextIcon } from "@/lib/integrations/sanity/icons";
3
+
4
+ export const richTextBlock = defineType({
5
+ name: "richTextBlock",
6
+ title: "Rich Text",
7
+ type: "document",
8
+ icon: richTextIcon,
9
+ fields: [
10
+ defineField({
11
+ name: "content",
12
+ title: "Content",
13
+ type: "richText",
14
+ validation: (Rule) => Rule.required(),
15
+ }),
16
+ ],
17
+ preview: {
18
+ select: {
19
+ content: "content",
20
+ },
21
+ prepare({ content }) {
22
+ // Portable Text has no plain-text field, so derive a subtitle from the
23
+ // first block's spans.
24
+ const firstBlock = Array.isArray(content) ? content[0] : null;
25
+ const spans = Array.isArray(firstBlock?.children)
26
+ ? firstBlock.children
27
+ : [];
28
+ const text = spans
29
+ .map((span: { text?: string }) => span?.text ?? "")
30
+ .join("")
31
+ .trim();
32
+
33
+ return {
34
+ title: "Rich Text",
35
+ subtitle: text || "Empty",
36
+ };
37
+ },
38
+ },
39
+ });
@@ -14,6 +14,7 @@ import {
14
14
  description,
15
15
  gatedContent,
16
16
  hero,
17
+ richTextBlock,
17
18
  } from "./components";
18
19
  import { author } from "./content/author";
19
20
  import { blogCategory } from "./content/blog-category";
@@ -110,6 +111,7 @@ export const schema: { types: SchemaTypeDefinition[] } = {
110
111
  blogContent,
111
112
  hero,
112
113
  description,
114
+ richTextBlock,
113
115
  gatedContent,
114
116
 
115
117
  // Layout types (singletons for site-wide layout)