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
@@ -65,7 +65,7 @@ export const redirect = defineType({
65
65
  description:
66
66
  "Where to send visitors. An internal route path without a leading slash (e.g. new-page or blog/new-post), or an absolute URL (e.g. https://example.com).",
67
67
  validation: (Rule) =>
68
- Rule.required().custom((value, context) => {
68
+ Rule.required().custom(async (value, context) => {
69
69
  if (typeof value !== "string") return true;
70
70
 
71
71
  const isAbsolute = /^https?:\/\//.test(value);
@@ -79,13 +79,44 @@ export const redirect = defineType({
79
79
  return "Internal destination must not start or end with a forward slash";
80
80
  }
81
81
 
82
- const source = (
83
- context.document as RedirectValidationDocument | undefined
84
- )?.source?.current;
82
+ const document = context.document as
83
+ | RedirectValidationDocument
84
+ | undefined;
85
+ const source = document?.source?.current;
86
+
85
87
  if (source && source === value) {
86
88
  return "Destination must differ from source";
87
89
  }
88
90
 
91
+ // Rejects a direct loop (a -> b, b -> a); longer chains aren't checked.
92
+ // Normalizes slashes since docs written outside Studio can skip that validation.
93
+ if (source) {
94
+ const id = document?._id?.replace(/^drafts\./, "") ?? "";
95
+ const client = context.getClient({ apiVersion });
96
+ const normalize = (path: string) => path.replace(/^\/+|\/+$/g, "");
97
+ const normalizedSource = normalize(source);
98
+ const normalizedDestination = normalize(value);
99
+ const others = await client.fetch<
100
+ Array<{ source?: string; destination?: string }>
101
+ >(
102
+ `*[
103
+ _type == "redirect" &&
104
+ !(_id in [$id, "drafts." + $id])
105
+ ]{ "source": source.current, destination }`,
106
+ { id },
107
+ );
108
+ const hasInverse = others.some(
109
+ (redirect) =>
110
+ redirect.source &&
111
+ normalize(redirect.source) === normalizedDestination &&
112
+ redirect.destination &&
113
+ normalize(redirect.destination) === normalizedSource,
114
+ );
115
+ if (hasInverse) {
116
+ return "This creates a redirect loop — another redirect sends the destination back to this source";
117
+ }
118
+ }
119
+
89
120
  return true;
90
121
  }),
91
122
  }),
@@ -204,6 +204,18 @@ export const page = defineType({
204
204
  return `These page builder components do not belong to the "${pageType ?? "generic"}" category: ${invalidMemberTitles.join(", ")}`;
205
205
  }),
206
206
  }),
207
+ defineField({
208
+ name: "collectionCategory",
209
+ title: "Pinned Collection Category",
210
+ type: "reference",
211
+ to: [{ type: "blogCategory" }],
212
+ // Lives on the page because the collection block is deliberately a
213
+ // singleton (see `singletonComponents`) — editors reuse one block rather
214
+ // than creating them, so a category set there would apply site-wide. Move
215
+ // the block to the reusable list if you'd rather scope it per instance.
216
+ description:
217
+ "Turns this page into a category page: the content collection shows only this category. Leave empty to show everything.",
218
+ }),
207
219
  defineField({
208
220
  name: "hiddenPageBuilderKeys",
209
221
  title: "Hidden Blocks",
@@ -0,0 +1,56 @@
1
+ // Dependency maps for the publish webhook. Dependency-free on purpose: the
2
+ // modules owning these documents pull in the Studio UI.
3
+ //
4
+ // The strings mirror the `tags: [...]` arrays at each fetch site. Nothing
5
+ // enforces that pairing, so a tag renamed there has to be renamed here.
6
+
7
+ const SANITY_NAVBAR_TAG = "navbar";
8
+ const SANITY_STRUCTURED_DATA_TAG = "structuredData";
9
+
10
+ // Crawler routes: broader than any per-slug tag.
11
+ export const SITEMAP_TAG = "sitemap";
12
+ export const FEED_TAG = "feed";
13
+
14
+ export const BLOG_COLLECTION_TAGS = [
15
+ "blog-categories",
16
+ "blog-articles",
17
+ "blog-articles-count",
18
+ ] as const;
19
+
20
+ // Publishing a `page` revalidates its own path; nothing else reaches the cached
21
+ // collection windows.
22
+ export const BLOG_COLLECTION_TYPES = new Set<string>([
23
+ "page",
24
+ "blogContent",
25
+ "blogCategory",
26
+ "author",
27
+ ]);
28
+
29
+ // Publishing the left-hand type has to bust the right-hand tags; nothing else
30
+ // reaches them.
31
+ export const CROSS_DOC_TAG_DEPENDENCIES: Record<string, readonly string[]> = {
32
+ // Redundant while LAYOUT_QUERY is merged; kept so splitting it can't lose this.
33
+ companyData: [SANITY_STRUCTURED_DATA_TAG],
34
+ // Nav links deref pages; sitemap and feed move whenever any page does.
35
+ page: [SANITY_NAVBAR_TAG, SITEMAP_TAG, FEED_TAG],
36
+ // ...plus the collections, which decide membership on the folder slug, so a
37
+ // rename changes which pages are articles at all.
38
+ pageFolder: [
39
+ SANITY_NAVBAR_TAG,
40
+ ...BLOG_COLLECTION_TAGS,
41
+ SITEMAP_TAG,
42
+ FEED_TAG,
43
+ ],
44
+ // The feed derefs the article's block, then its author and categories.
45
+ blogContent: [FEED_TAG],
46
+ author: [FEED_TAG],
47
+ blogCategory: [FEED_TAG],
48
+ };
49
+
50
+ // Types a page inlines by reference, so publishing one goes stale on every page
51
+ // pointing at it. `page` is here for `relatedPosts[]->` and rich-text links.
52
+ export const REFERENCEABLE_TYPES = new Set<string>([
53
+ "page",
54
+ "author",
55
+ "blogCategory",
56
+ ]);
@@ -29,6 +29,18 @@ export const renderPageBuilderMarkdown = (
29
29
  return block.headline ? [`## ${block.headline}`, ""] : [];
30
30
  case "description":
31
31
  return block.description ? [block.description, ""] : [];
32
+ case "richTextBlock":
33
+ return block.content
34
+ ? [
35
+ portableTextToMarkdown(
36
+ block.content as Parameters<
37
+ typeof portableTextToMarkdown
38
+ >[0],
39
+ markdownOptions,
40
+ ),
41
+ "",
42
+ ]
43
+ : [];
32
44
  case "gatedContent": {
33
45
  // The gated payload is withheld from the page query, so it is not
34
46
  // available here either — only the teaser is emitted, matching what
@@ -1,6 +1,7 @@
1
1
  import { PINNED_PAGE_ENTRIES } from "@/lib/integrations/sanity/pinned-pages";
2
2
  import { PAGE_SITEMAP_QUERY } from "@/lib/integrations/sanity/queries";
3
3
  import type { PAGE_SITEMAP_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
4
+ import { SITEMAP_TAG } from "@/lib/integrations/sanity/tags";
4
5
  import { getBaseUrl } from "./base-url";
5
6
 
6
7
  export const getSitemapBaseUrl = getBaseUrl;
@@ -26,15 +27,17 @@ export const getMarkdownPath = (baseUrl: string, slug: string | null) => {
26
27
  : `${baseUrl}/index.md`;
27
28
  };
28
29
 
30
+ // Tagged, not a bare `client.fetch`: an untagged read can't be invalidated.
29
31
  export const getSitemap = async () => {
30
- const sanityModule = await import("@/lib/integrations/sanity/client");
31
- const client = sanityModule?.client;
32
-
33
- if (!client) return null;
34
-
35
- const p = await client.fetch<PAGE_SITEMAP_QUERY_RESULT>(PAGE_SITEMAP_QUERY, {
36
- pinnedIds: PINNED_PAGE_ENTRIES.map((entry) => entry.id),
32
+ // Lazy: the webhook imports `getPagePath` from here, and a static import would
33
+ // make it evaluate `defineLive()` for a string helper.
34
+ const { fetchSanity } = await import("@/lib/integrations/sanity/live");
35
+
36
+ const { data } = await fetchSanity<PAGE_SITEMAP_QUERY_RESULT>({
37
+ query: PAGE_SITEMAP_QUERY,
38
+ params: { pinnedIds: PINNED_PAGE_ENTRIES.map((entry) => entry.id) },
39
+ tags: [SITEMAP_TAG],
37
40
  });
38
41
 
39
- return p;
42
+ return data;
40
43
  };
@@ -14,6 +14,15 @@ export function proxy(request: NextRequest) {
14
14
  return NextResponse.next();
15
15
  }
16
16
 
17
+ // Collapse repeated slashes before any rewrite, or the rewrite below would
18
+ // mask the alias (e.g. /foo//bar.md silently resolving to /foo/bar.md).
19
+ const collapsedPathname = pathname.replace(/\/{2,}/g, "/");
20
+ if (collapsedPathname !== pathname) {
21
+ const url = request.nextUrl.clone();
22
+ url.pathname = collapsedPathname;
23
+ return NextResponse.redirect(url, 308);
24
+ }
25
+
17
26
  if (pathname === "/" && acceptHeader.includes("text/markdown")) {
18
27
  const url = request.nextUrl.clone();
19
28
  url.pathname = "/api/index.md";
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/knip@6/schema.json",
3
+ "entry": [
4
+ "lib/integrations/sanity/sanity.cli.ts",
5
+ "lib/integrations/sanity/sanity.config.ts"
6
+ ],
7
+ "ignore": ["lib/integrations/sanity/sanity.types.ts"]
8
+ }
@@ -0,0 +1,112 @@
1
+ import { NextResponse } from "next/server";
2
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
3
+ import {
4
+ getDynamicFetchOptions,
5
+ sanityFetchCached,
6
+ } from "@/lib/integrations/sanity/live";
7
+ import { PAGE_QUERY } from "@/lib/integrations/sanity/queries";
8
+ import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
9
+ import { renderPageBuilderMarkdown } from "@/lib/integrations/sanity/utils/page-builder-markdown";
10
+ import { getBaseUrl } from "@/lib/utils/base-url";
11
+ import { getSlugTag } from "@/lib/utils/slug-tag";
12
+
13
+ type PageResult = NonNullable<PAGE_QUERY_RESULT>;
14
+
15
+ const isValidSegment = (segment: string) =>
16
+ segment.length > 0 && segment !== "." && segment !== "..";
17
+
18
+ const getSlugPath = (slugParts?: string[]) => {
19
+ if (!slugParts?.length) return null;
20
+
21
+ const lastSegment = slugParts.at(-1);
22
+
23
+ if (!lastSegment?.endsWith(".md")) return null;
24
+
25
+ const pathSegments = [...slugParts];
26
+ pathSegments[pathSegments.length - 1] = lastSegment.slice(0, -3);
27
+
28
+ if (!pathSegments.every(isValidSegment)) return null;
29
+
30
+ const slug = pathSegments.join("/");
31
+
32
+ return slug === "index" ? null : slug;
33
+ };
34
+
35
+ export async function GET(
36
+ _request: Request,
37
+ props: { params: Promise<{ slug?: string[] }> },
38
+ ) {
39
+ const { slug: rawSlugParts } = await props.params;
40
+ const slug = getSlugPath(rawSlugParts);
41
+
42
+ if (
43
+ rawSlugParts?.length &&
44
+ slug === null &&
45
+ rawSlugParts.join("/") !== "index.md"
46
+ ) {
47
+ return new NextResponse(null, { status: 404 });
48
+ }
49
+
50
+ if (!rawSlugParts?.length) {
51
+ return new NextResponse(null, { status: 404 });
52
+ }
53
+
54
+ if (!isSanityConfigured()) {
55
+ return new NextResponse("# 404 Not Found\n\nCMS not configured.", {
56
+ status: 404,
57
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
58
+ });
59
+ }
60
+
61
+ const fetchOptions = await getDynamicFetchOptions();
62
+ const { data: page } = await sanityFetchCached<PageResult>({
63
+ query: PAGE_QUERY,
64
+ params: { slug },
65
+ ...fetchOptions,
66
+ // This route answers at /api/<slug>.md, which no `revalidatePath` on the
67
+ // page path reaches. The tag is the only thing that clears it.
68
+ tags: [getSlugTag(slug)],
69
+ });
70
+
71
+ const baseUrl = getBaseUrl().replace(/\/$/, "");
72
+
73
+ if (!page) {
74
+ return new NextResponse("# 404 Not Found\n\nPage not found.", {
75
+ status: 404,
76
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
77
+ });
78
+ }
79
+
80
+ const body = renderPageBuilderMarkdown(page.pageBuilder);
81
+ const canonicalPath = slug ? `/${slug}` : "/";
82
+
83
+ return new NextResponse(
84
+ [
85
+ `# ${page.title}`,
86
+ "",
87
+ page._updatedAt
88
+ ? `**Updated:** ${new Date(page._updatedAt).toLocaleDateString()}`
89
+ : null,
90
+ "",
91
+ `Canonical URL: ${baseUrl}${canonicalPath}`,
92
+ "",
93
+ "---",
94
+ "",
95
+ body || "No page builder content yet.",
96
+ "",
97
+ "---",
98
+ "",
99
+ `[View all content](${baseUrl}/sitemap.md)`,
100
+ ]
101
+ .filter((part): part is string => part !== null)
102
+ .join("\n"),
103
+ {
104
+ headers: {
105
+ "Content-Type": "text/markdown; charset=utf-8",
106
+ Vary: "Accept",
107
+ "X-Content-Type-Options": "nosniff",
108
+ Link: `<${baseUrl}${canonicalPath}>; rel="canonical"`,
109
+ },
110
+ },
111
+ );
112
+ }
@@ -0,0 +1,7 @@
1
+ import { draftMode } from "next/headers";
2
+ import { type NextRequest, NextResponse } from "next/server";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ (await draftMode()).disable();
6
+ return NextResponse.redirect(new URL("/", request.url));
7
+ }
@@ -0,0 +1,20 @@
1
+ import { NextResponse } from "next/server";
2
+ import { defineEnableDraftMode } from "next-sanity/draft-mode";
3
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
4
+ import { client } from "@/lib/integrations/sanity/client";
5
+ import { sanityToken } from "@/lib/integrations/sanity/env";
6
+
7
+ const draftModeHandler =
8
+ isSanityConfigured() && client
9
+ ? defineEnableDraftMode({
10
+ client: client.withConfig({ token: sanityToken }),
11
+ })
12
+ : {
13
+ GET: () =>
14
+ NextResponse.json(
15
+ { error: "Sanity is not configured" },
16
+ { status: 503 },
17
+ ),
18
+ };
19
+
20
+ export const { GET } = draftModeHandler;
@@ -0,0 +1,292 @@
1
+ import { revalidatePath, revalidateTag } from "next/cache";
2
+ import { type NextRequest, NextResponse } from "next/server";
3
+ import { parseBody } from "next-sanity/webhook";
4
+ // `directClient`, never `client`: a CDN hit returns the pre-mutation slug, which
5
+ // purges the old path twice and never the new one.
6
+ import { directClient } from "@/lib/integrations/sanity/client";
7
+ import { pageBuilderReferenceMembers as pbrm } from "@/lib/integrations/sanity/page-builder-config";
8
+ import {
9
+ ALL_PAGE_SLUGS_QUERY,
10
+ FOLDER_PATH_QUERY,
11
+ P_REF_DOC_PAGES_QUERY,
12
+ P_REV_FOLDER_DESCENDANTS_QUERY,
13
+ P_REV_SLUGS_QUERY,
14
+ } from "@/lib/integrations/sanity/queries";
15
+ import {
16
+ composeDescendantPaths,
17
+ composeResolvedSlug,
18
+ } from "@/lib/integrations/sanity/revalidate-paths";
19
+ import type {
20
+ ALL_PAGE_SLUGS_QUERY_RESULT,
21
+ FOLDER_PATH_QUERY_RESULT,
22
+ P_REF_DOC_PAGES_QUERY_RESULT,
23
+ P_REV_FOLDER_DESCENDANTS_QUERY_RESULT,
24
+ } from "@/lib/integrations/sanity/sanity.types";
25
+ import {
26
+ BLOG_COLLECTION_TAGS,
27
+ BLOG_COLLECTION_TYPES,
28
+ CROSS_DOC_TAG_DEPENDENCIES,
29
+ PURGEABLE_TAGS,
30
+ REFERENCEABLE_TYPES,
31
+ SELF_TAGGED_TYPES,
32
+ } from "@/lib/integrations/sanity/tags";
33
+ import { getPagePath } from "@/lib/utils/sitemap";
34
+ import { getSlugTag } from "@/lib/utils/slug-tag";
35
+
36
+ const ERRORS = {
37
+ BAD_REQ: ["Bad Request", { status: 400 }],
38
+ INVALID_SIG: ["Invalid signature", { status: 401 }],
39
+ } as const;
40
+
41
+ type WebhookSlug = {
42
+ current?: string | null;
43
+ };
44
+
45
+ type WebhookReference = {
46
+ _ref?: string | null;
47
+ };
48
+
49
+ type WebhookDocument = {
50
+ _id: string;
51
+ _type: string;
52
+ slug?: WebhookSlug | null;
53
+ pageFolder?: WebhookReference | null;
54
+ parentFolder?: WebhookReference | null;
55
+ };
56
+
57
+ type MutationOperation = "create" | "update" | "delete";
58
+
59
+ type RevalidateWebhookBody = WebhookDocument & {
60
+ operation?: MutationOperation;
61
+ purgeAll?: boolean;
62
+ before?: Partial<WebhookDocument> | null;
63
+ after?: Partial<WebhookDocument> | null;
64
+ };
65
+
66
+ const referenceableTypes = new Set<string>([
67
+ ...pbrm.map((m) => m.documentType),
68
+ ...REFERENCEABLE_TYPES,
69
+ ]);
70
+
71
+ // Not `"max"`: a named profile expires a year out, so Next serves the stale entry
72
+ // once while refreshing. A publish should land on the next request.
73
+ const PURGE = { expire: 0 } as const;
74
+
75
+ const bustTags = (tags: readonly string[]) => {
76
+ for (const tag of tags) {
77
+ revalidateTag(tag, PURGE);
78
+ }
79
+ };
80
+
81
+ // Both: the path clears the page's route, the tag clears what no page render
82
+ // touches — above all the markdown route at /api/<slug>.md.
83
+ const bustPage = (slug: string | null) => {
84
+ revalidatePath(getPagePath(slug));
85
+ revalidateTag(getSlugTag(slug), PURGE);
86
+ };
87
+
88
+ const normalizeRevalidationSlug = (slug: string | null | undefined) => {
89
+ if (slug === undefined) return null;
90
+ if (slug === null) return "/";
91
+ return `/${slug}`;
92
+ };
93
+
94
+ const getCanonicalPageSlug = async (documentId: string) => {
95
+ if (!directClient) return null;
96
+
97
+ const pageSlugs = await directClient.fetch<Array<{ slug: string | null }>>(
98
+ P_REV_SLUGS_QUERY,
99
+ { documentIds: [documentId] },
100
+ );
101
+
102
+ return normalizeRevalidationSlug(
103
+ pageSlugs.find((page) => page.slug !== undefined)?.slug,
104
+ );
105
+ };
106
+
107
+ // Homepage comes back as `null`, as `bustPage` expects.
108
+ const getAllPageSlugs = async () => {
109
+ if (!directClient) return [];
110
+
111
+ const pages =
112
+ await directClient.fetch<ALL_PAGE_SLUGS_QUERY_RESULT>(ALL_PAGE_SLUGS_QUERY);
113
+
114
+ return [
115
+ null,
116
+ ...(pages ?? []).flatMap((page) =>
117
+ page.slug && page.slug !== "/" ? [`/${page.slug}`] : [],
118
+ ),
119
+ ];
120
+ };
121
+
122
+ const getFolder = async (folderId: string | null | undefined) => {
123
+ if (!(directClient && folderId)) return null;
124
+
125
+ const folder = await directClient.fetch<FOLDER_PATH_QUERY_RESULT>(
126
+ FOLDER_PATH_QUERY,
127
+ { folderId },
128
+ );
129
+
130
+ return folder ?? null;
131
+ };
132
+
133
+ const getPagesReferencing = async (documentIds: readonly string[]) => {
134
+ if (!(directClient && documentIds.length)) return [];
135
+
136
+ const pages = await directClient.fetch<P_REF_DOC_PAGES_QUERY_RESULT>(
137
+ P_REF_DOC_PAGES_QUERY,
138
+ { documentIds },
139
+ );
140
+
141
+ return (pages ?? []).map((page) => normalizeRevalidationSlug(page.slug));
142
+ };
143
+
144
+ // `slug.current` is only the page's own segment, so the URL is rebuilt from the
145
+ // folder. `undefined` means "no such revision", distinct from the `null` a root
146
+ // page resolves to — without that split, a missing `before()` purges the homepage.
147
+ const resolvePageSlug = async (
148
+ revision: Partial<WebhookDocument> | null | undefined,
149
+ ) => {
150
+ if (!revision) return undefined;
151
+
152
+ const folder = await getFolder(revision.pageFolder?._ref);
153
+
154
+ return composeResolvedSlug(folder?.path ?? null, revision.slug?.current);
155
+ };
156
+
157
+ // A folder move rewrites every URL beneath it, and none of those are `page`
158
+ // mutations, so nothing else here would touch them.
159
+ const revalidateFolderSubtree = async (body: RevalidateWebhookBody) => {
160
+ const { before, after } = body;
161
+
162
+ const previousFolderSlug = before?.slug?.current;
163
+ const moved =
164
+ previousFolderSlug !== after?.slug?.current ||
165
+ before?.parentFolder?._ref !== after?.parentFolder?._ref;
166
+
167
+ // A title-only edit changes no URL. Deletion is skipped outright: these are hard
168
+ // references, so Sanity refuses to delete a folder that still has descendants.
169
+ if (body.operation === "delete") return [];
170
+ if (!(directClient && moved && previousFolderSlug)) return [];
171
+
172
+ const previousParent = await getFolder(before?.parentFolder?._ref);
173
+
174
+ const descendants =
175
+ await directClient.fetch<P_REV_FOLDER_DESCENDANTS_QUERY_RESULT>(
176
+ P_REV_FOLDER_DESCENDANTS_QUERY,
177
+ { folderId: body._id },
178
+ );
179
+
180
+ const movedIds: string[] = [];
181
+
182
+ for (const descendant of descendants ?? []) {
183
+ const paths = composeDescendantPaths({
184
+ folderChain: descendant.folderChain ?? [],
185
+ ownSlug: descendant.ownSlug,
186
+ folderId: body._id,
187
+ previousFolderSlug,
188
+ previousParentPath: previousParent?.path ?? null,
189
+ });
190
+
191
+ if (!paths) {
192
+ console.error("Revalidation: unresolvable folder chain", descendant._id);
193
+ continue;
194
+ }
195
+
196
+ movedIds.push(descendant._id);
197
+ bustPage(paths.current);
198
+ bustPage(paths.previous);
199
+ }
200
+
201
+ // Pages linking into the subtree inline the old slugs too.
202
+ return getPagesReferencing(movedIds);
203
+ };
204
+
205
+ const getAffectedPageSlugsForWebhook = async (body: RevalidateWebhookBody) => {
206
+ const pages: Array<string | null> = [];
207
+
208
+ if (body._type === "page" && body.before) {
209
+ const previousSlug = await resolvePageSlug(body.before);
210
+
211
+ // Resolved, not raw: a moved page keeps its own slug but vacates its URL.
212
+ const vacated =
213
+ body.operation === "delete" ||
214
+ (body.operation === "update" &&
215
+ previousSlug !== (await resolvePageSlug(body.after)));
216
+
217
+ if (vacated && previousSlug !== undefined) bustPage(previousSlug);
218
+ }
219
+
220
+ // If the document is a page, get its slug
221
+ if (body._type === "page") {
222
+ const data = await getCanonicalPageSlug(body._id);
223
+ if (data !== null) pages.push(data);
224
+ }
225
+
226
+ // Page-builder blocks, `author`, `blogCategory` and other pages are all
227
+ // inlined into pages that no other branch here revalidates.
228
+ if (referenceableTypes.has(body._type)) {
229
+ pages.push(...(await getPagesReferencing([body._id])));
230
+ }
231
+
232
+ if (body._type === "pageFolder") {
233
+ pages.push(...(await revalidateFolderSubtree(body)));
234
+ }
235
+
236
+ return [...new Set(pages.filter((page): page is string => page !== null))];
237
+ };
238
+
239
+ export async function POST(request: NextRequest) {
240
+ try {
241
+ const { body, isValidSignature } = await parseBody<RevalidateWebhookBody>(
242
+ request,
243
+ process.env.SANITY_REVALIDATE_SECRET,
244
+ );
245
+
246
+ if (!isValidSignature) return new Response(...ERRORS.INVALID_SIG);
247
+
248
+ // Ahead of the `_type`/`_id` guard: a purge payload carries neither. The hatch
249
+ // for a missed `cacheTag`, which the ~1y cacheLife would otherwise freeze.
250
+ if (body?.purgeAll) {
251
+ // Enumerated here so PURGEABLE_TAGS stays query-free. Without this the hatch
252
+ // never reached page entries or /api/<slug>.md.
253
+ const slugs = await getAllPageSlugs();
254
+
255
+ bustTags(PURGEABLE_TAGS);
256
+ for (const slug of slugs) bustPage(slug);
257
+
258
+ return NextResponse.json({
259
+ status: 200,
260
+ now: Date.now(),
261
+ purged: PURGEABLE_TAGS.length + slugs.length,
262
+ pages: slugs.length,
263
+ });
264
+ }
265
+
266
+ if (!(body?._type && body._id)) return new Response(...ERRORS.BAD_REQ);
267
+ if (body._id.startsWith("drafts."))
268
+ return NextResponse.json({ status: 200, now: Date.now(), skipped: true });
269
+
270
+ const affectedPageSlugs = await getAffectedPageSlugsForWebhook(body);
271
+
272
+ for (const slug of affectedPageSlugs) {
273
+ bustPage(slug);
274
+ }
275
+
276
+ // Layout singletons and redirects are tagged with their own `_type`.
277
+ if (SELF_TAGGED_TYPES.has(body._type)) revalidateTag(body._type, PURGE);
278
+
279
+ // Queries that read across document types — a `structuredData` entry that
280
+ // folds in `companyData`, a navbar that derefs pages.
281
+ bustTags(CROSS_DOC_TAG_DEPENDENCIES[body._type] ?? []);
282
+
283
+ // The content-collection block caches list/count/categories under their own
284
+ // tags, which no per-page revalidation above touches.
285
+ if (BLOG_COLLECTION_TYPES.has(body._type)) bustTags(BLOG_COLLECTION_TAGS);
286
+
287
+ return NextResponse.json({ status: 200, now: Date.now() });
288
+ } catch (error) {
289
+ console.error("Revalidation error:", error);
290
+ return new Response("Internal Server Error", { status: 500 });
291
+ }
292
+ }