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,16 +1,37 @@
1
1
  import { revalidatePath, revalidateTag } from "next/cache";
2
2
  import { type NextRequest, NextResponse } from "next/server";
3
3
  import { parseBody } from "next-sanity/webhook";
4
- import { client } from "@/lib/integrations/sanity/client";
4
+ // `directClient`, never `client`: the shared client runs `useCdn: true`, and every
5
+ // lookup here reads state the mutation that triggered this request just produced.
6
+ // A CDN hit returns the pre-mutation slug or folder chain, which purges the old
7
+ // path twice and never touches the new one.
8
+ import { directClient } from "@/lib/integrations/sanity/client";
5
9
  import { SANITY_LAYOUT_TAGS } from "@/lib/integrations/sanity/fetchers/layout";
6
10
  import { SANITY_REDIRECTS_TAG } from "@/lib/integrations/sanity/fetchers/redirects";
7
11
  import { pageBuilderReferenceMembers as pbrm } from "@/lib/integrations/sanity/page-builder-config";
8
12
  import {
9
- P_REF_PB_COMP_QUERY,
13
+ FOLDER_PATH_QUERY,
14
+ P_REF_DOC_PAGES_QUERY,
15
+ P_REV_FOLDER_DESCENDANTS_QUERY,
10
16
  P_REV_SLUGS_QUERY,
11
17
  } from "@/lib/integrations/sanity/queries";
12
- import type { P_REF_PB_COMP_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
18
+ import {
19
+ composeDescendantPaths,
20
+ composeResolvedSlug,
21
+ } from "@/lib/integrations/sanity/revalidate-paths";
22
+ import type {
23
+ FOLDER_PATH_QUERY_RESULT,
24
+ P_REF_DOC_PAGES_QUERY_RESULT,
25
+ P_REV_FOLDER_DESCENDANTS_QUERY_RESULT,
26
+ } from "@/lib/integrations/sanity/sanity.types";
27
+ import {
28
+ BLOG_COLLECTION_TAGS,
29
+ BLOG_COLLECTION_TYPES,
30
+ CROSS_DOC_TAG_DEPENDENCIES,
31
+ REFERENCEABLE_TYPES,
32
+ } from "@/lib/integrations/sanity/tags";
13
33
  import { getPagePath } from "@/lib/utils/sitemap";
34
+ import { getSlugTag } from "@/lib/utils/slug-tag";
14
35
 
15
36
  const ERRORS = {
16
37
  BAD_REQ: ["Bad Request", { status: 400 }],
@@ -21,11 +42,16 @@ type WebhookSlug = {
21
42
  current?: string | null;
22
43
  };
23
44
 
45
+ type WebhookReference = {
46
+ _ref?: string | null;
47
+ };
48
+
24
49
  type WebhookDocument = {
25
50
  _id: string;
26
51
  _type: string;
27
- resolvedSlug?: string | null;
28
52
  slug?: WebhookSlug | null;
53
+ pageFolder?: WebhookReference | null;
54
+ parentFolder?: WebhookReference | null;
29
55
  };
30
56
 
31
57
  type MutationOperation = "create" | "update" | "delete";
@@ -36,7 +62,27 @@ type RevalidateWebhookBody = WebhookDocument & {
36
62
  after?: Partial<WebhookDocument> | null;
37
63
  };
38
64
 
39
- const pageBuilderComponent = new Set<string>(pbrm.map((m) => m.documentType));
65
+ const referenceableTypes = new Set<string>([
66
+ ...pbrm.map((m) => m.documentType),
67
+ ...REFERENCEABLE_TYPES,
68
+ ]);
69
+
70
+ // Not `"max"`: a named profile expires a year out, so Next serves the stale entry
71
+ // once while refreshing. A publish should land on the next request, not the one after.
72
+ const PURGE = { expire: 0 } as const;
73
+
74
+ const bustTags = (tags: readonly string[]) => {
75
+ for (const tag of tags) {
76
+ revalidateTag(tag, PURGE);
77
+ }
78
+ };
79
+
80
+ // Both: the path clears the page's route, the tag clears what no page render
81
+ // touches — above all the markdown route, which answers at /api/<slug>.md.
82
+ const bustPage = (slug: string | null) => {
83
+ revalidatePath(getPagePath(slug));
84
+ revalidateTag(getSlugTag(slug), PURGE);
85
+ };
40
86
 
41
87
  const normalizeRevalidationSlug = (slug: string | null | undefined) => {
42
88
  if (slug === undefined) return null;
@@ -45,9 +91,9 @@ const normalizeRevalidationSlug = (slug: string | null | undefined) => {
45
91
  };
46
92
 
47
93
  const getCanonicalPageSlug = async (documentId: string) => {
48
- if (!client) return null;
94
+ if (!directClient) return null;
49
95
 
50
- const pageSlugs = await client.fetch<Array<{ slug: string | null }>>(
96
+ const pageSlugs = await directClient.fetch<Array<{ slug: string | null }>>(
51
97
  P_REV_SLUGS_QUERY,
52
98
  { documentIds: [documentId] },
53
99
  );
@@ -57,24 +103,114 @@ const getCanonicalPageSlug = async (documentId: string) => {
57
103
  );
58
104
  };
59
105
 
60
- const getPagesThatReferenceComp = async (documentId: string) => {
61
- if (!client) return [];
106
+ const getFolder = async (folderId: string | null | undefined) => {
107
+ if (!(directClient && folderId)) return null;
62
108
 
63
- const pages = await client.fetch<P_REF_PB_COMP_QUERY_RESULT>(
64
- P_REF_PB_COMP_QUERY,
65
- {
66
- documentId,
67
- },
109
+ const folder = await directClient.fetch<FOLDER_PATH_QUERY_RESULT>(
110
+ FOLDER_PATH_QUERY,
111
+ { folderId },
112
+ );
113
+
114
+ return folder ?? null;
115
+ };
116
+
117
+ const getPagesReferencing = async (documentIds: readonly string[]) => {
118
+ if (!(directClient && documentIds.length)) return [];
119
+
120
+ const pages = await directClient.fetch<P_REF_DOC_PAGES_QUERY_RESULT>(
121
+ P_REF_DOC_PAGES_QUERY,
122
+ { documentIds },
68
123
  );
69
124
 
70
125
  return (pages ?? []).map((page) => normalizeRevalidationSlug(page.slug));
71
126
  };
72
127
 
128
+ // The webhook carries the raw document, so a page's URL has to be rebuilt from
129
+ // its folder — `slug.current` alone is the page's own segment, which is empty
130
+ // for a folder-index page and missing the prefix for every nested one.
131
+ //
132
+ // `undefined` means "no such revision", distinct from the `null` that a page at
133
+ // the site root legitimately resolves to. Without that split, a payload missing
134
+ // its `before()` projection would purge the homepage on every delete.
135
+ const resolvePageSlug = async (
136
+ revision: Partial<WebhookDocument> | null | undefined,
137
+ ) => {
138
+ if (!revision) return undefined;
139
+
140
+ const folder = await getFolder(revision.pageFolder?._ref);
141
+
142
+ return composeResolvedSlug(folder?.path ?? null, revision.slug?.current);
143
+ };
144
+
145
+ // A folder rename or reparent rewrites the URL of everything beneath it. Neither
146
+ // the folder nor its descendants are `page` mutations, so nothing else in this
147
+ // route would touch them.
148
+ const revalidateFolderSubtree = async (body: RevalidateWebhookBody) => {
149
+ const { before, after } = body;
150
+
151
+ const previousFolderSlug = before?.slug?.current;
152
+ const moved =
153
+ previousFolderSlug !== after?.slug?.current ||
154
+ before?.parentFolder?._ref !== after?.parentFolder?._ref;
155
+
156
+ // A folder edit that touches neither the slug nor the parent — a title, say —
157
+ // changes no URL. `pageFolder` still busts the navbar and collection tags
158
+ // through CROSS_DOC_TAG_DEPENDENCIES below.
159
+ //
160
+ // Deletion is skipped outright: `pageFolder` and `parentFolder` are plain hard
161
+ // references, so Sanity refuses to delete a folder that still has descendants.
162
+ // There is no orphaned subtree to unwind, and querying for one would just cost
163
+ // two round trips to find nothing.
164
+ if (body.operation === "delete") return [];
165
+ if (!(directClient && moved && previousFolderSlug)) return [];
166
+
167
+ const previousParent = await getFolder(before?.parentFolder?._ref);
168
+
169
+ const descendants =
170
+ await directClient.fetch<P_REV_FOLDER_DESCENDANTS_QUERY_RESULT>(
171
+ P_REV_FOLDER_DESCENDANTS_QUERY,
172
+ { folderId: body._id },
173
+ );
174
+
175
+ const movedIds: string[] = [];
176
+
177
+ for (const descendant of descendants ?? []) {
178
+ const paths = composeDescendantPaths({
179
+ folderChain: descendant.folderChain ?? [],
180
+ ownSlug: descendant.ownSlug,
181
+ folderId: body._id,
182
+ previousFolderSlug,
183
+ previousParentPath: previousParent?.path ?? null,
184
+ });
185
+
186
+ if (!paths) {
187
+ console.error("Revalidation: unresolvable folder chain", descendant._id);
188
+ continue;
189
+ }
190
+
191
+ movedIds.push(descendant._id);
192
+ bustPage(paths.current);
193
+ bustPage(paths.previous);
194
+ }
195
+
196
+ // Pages linking *into* the subtree inline the old resolved slugs too.
197
+ return getPagesReferencing(movedIds);
198
+ };
199
+
73
200
  const getAffectedPageSlugsForWebhook = async (body: RevalidateWebhookBody) => {
74
201
  const pages: Array<string | null> = [];
75
202
 
76
- if (body.operation === "delete" && body?.before?.slug?.current) {
77
- revalidateTag(body.before.slug.current, "max");
203
+ if (body._type === "page" && body.before) {
204
+ const previousSlug = await resolvePageSlug(body.before);
205
+
206
+ // Resolved, not raw: a page moved to another folder keeps its own slug and
207
+ // still vacates its old URL, so comparing own slugs would miss it.
208
+ const vacated =
209
+ body.operation === "delete" ||
210
+ (body.operation === "update" &&
211
+ previousSlug !== (await resolvePageSlug(body.after)));
212
+
213
+ if (vacated && previousSlug !== undefined) bustPage(previousSlug);
78
214
  }
79
215
 
80
216
  // If the document is a page, get its slug
@@ -83,13 +219,17 @@ const getAffectedPageSlugsForWebhook = async (body: RevalidateWebhookBody) => {
83
219
  if (data !== null) pages.push(data);
84
220
  }
85
221
 
86
- // If the document is a page builder component, get the page slugs that reference it
87
- if (pageBuilderComponent.has(body._type)) {
88
- const data = await getPagesThatReferenceComp(body._id);
89
- pages.push(...data);
222
+ // Page-builder blocks, `author`, `blogCategory` and other pages are all
223
+ // inlined into pages that no other branch here revalidates.
224
+ if (referenceableTypes.has(body._type)) {
225
+ pages.push(...(await getPagesReferencing([body._id])));
90
226
  }
91
227
 
92
- return pages.filter((page): page is string => page !== null);
228
+ if (body._type === "pageFolder") {
229
+ pages.push(...(await revalidateFolderSubtree(body)));
230
+ }
231
+
232
+ return [...new Set(pages.filter((page): page is string => page !== null))];
93
233
  };
94
234
 
95
235
  export async function POST(request: NextRequest) {
@@ -107,15 +247,23 @@ export async function POST(request: NextRequest) {
107
247
  const affectedPageSlugs = await getAffectedPageSlugsForWebhook(body);
108
248
 
109
249
  for (const slug of affectedPageSlugs) {
110
- revalidatePath(getPagePath(slug));
250
+ bustPage(slug);
111
251
  }
112
252
 
113
253
  if (SANITY_LAYOUT_TAGS.includes(body?._type)) {
114
- revalidateTag(body._type, "max");
254
+ revalidateTag(body._type, PURGE);
115
255
  }
116
256
 
257
+ // Queries that read across document types — a `structuredData` entry that
258
+ // folds in `companyData`, a navbar that derefs pages.
259
+ bustTags(CROSS_DOC_TAG_DEPENDENCIES[body._type] ?? []);
260
+
261
+ // The content-collection block caches list/count/categories under their own
262
+ // tags, which no per-page revalidation above touches.
263
+ if (BLOG_COLLECTION_TYPES.has(body._type)) bustTags(BLOG_COLLECTION_TAGS);
264
+
117
265
  if (body._type === SANITY_REDIRECTS_TAG) {
118
- revalidateTag(SANITY_REDIRECTS_TAG, "max");
266
+ revalidateTag(SANITY_REDIRECTS_TAG, PURGE);
119
267
  }
120
268
 
121
269
  return NextResponse.json({ status: 200, now: Date.now() });
@@ -1,20 +1,27 @@
1
+ import { fetchSanity } from "@/lib/integrations/sanity/live";
1
2
  import { ALL_BLOG_ARTICLES_QUERY } from "@/lib/integrations/sanity/queries";
3
+ import type { ALL_BLOG_ARTICLES_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
4
+ import { FEED_TAG } from "@/lib/integrations/sanity/tags";
2
5
  import { generateRssFeed, rssErrorResponse } from "@/lib/utils/rss";
3
6
  import { getPageUrl, getSitemapBaseUrl } from "@/lib/utils/sitemap";
4
7
 
5
- export const dynamic = "force-dynamic";
6
-
8
+ // Not `force-dynamic`: feed readers poll, so per-request rendering charged a
9
+ // Sanity round trip per poll. The webhook busts FEED_TAG for `page`, `pageFolder`,
10
+ // `author` and `blogCategory` — this query derefs the byline and resolves slugs
11
+ // through folder chains.
7
12
  export async function GET() {
8
13
  const baseUrl = getSitemapBaseUrl();
9
14
 
10
15
  try {
11
- const sanityModule = await import("@/lib/integrations/sanity/client");
12
- const client = sanityModule?.client;
13
- if (!client) return rssErrorResponse();
16
+ const { data: articles } =
17
+ await fetchSanity<ALL_BLOG_ARTICLES_QUERY_RESULT>({
18
+ query: ALL_BLOG_ARTICLES_QUERY,
19
+ tags: [FEED_TAG],
20
+ });
14
21
 
15
- const articles = await client.fetch(ALL_BLOG_ARTICLES_QUERY);
22
+ if (!articles) return rssErrorResponse();
16
23
 
17
- const items = (articles ?? [])
24
+ const items = articles
18
25
  .filter((article) => article.resolvedSlug && !article.noIndex)
19
26
  .map((article) => ({
20
27
  title: article.title ?? "Untitled",
@@ -7,7 +7,8 @@ import {
7
7
  getSitemapBaseUrl,
8
8
  } from "@/lib/utils/sitemap";
9
9
 
10
- export const dynamic = "force-dynamic";
10
+ // Cached, not per-request: this bills itself as being for AI agents, so its
11
+ // traffic is crawler-driven. `getSitemap` tags the read; the webhook busts it.
11
12
 
12
13
  type PageDoc = PAGE_SITEMAP_QUERY_RESULT[number];
13
14
 
@@ -113,7 +114,12 @@ export const GET = async () => {
113
114
 
114
115
  if (pages.length === 0) parts.push("No content published yet.", "");
115
116
 
116
- return new NextResponse(parts.join("\n"));
117
+ return new NextResponse(parts.join("\n"), {
118
+ headers: {
119
+ "Content-Type": "text/markdown; charset=utf-8",
120
+ "X-Content-Type-Options": "nosniff",
121
+ },
122
+ });
117
123
  } catch (error) {
118
124
  console.error("Error generating sitemap.md:", error);
119
125
  return new NextResponse("# Sitemap\n\nError fetching content.", {
@@ -1,8 +1,23 @@
1
1
  "use client";
2
2
 
3
3
  import { NextStudio } from "next-sanity/studio";
4
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
4
5
  import config from "@/lib/integrations/sanity/sanity.config";
5
6
 
6
- const StudioPage = () => <NextStudio config={config} />;
7
+ // The Studio throws on an empty `projectId`, which `env.ts` falls back to when
8
+ // Sanity isn't configured — so this route would fail `next build`.
9
+ const StudioPage = () =>
10
+ isSanityConfigured() ? (
11
+ <NextStudio config={config} />
12
+ ) : (
13
+ <main style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
14
+ <h1>Studio unavailable</h1>
15
+ <p>
16
+ Set <code>NEXT_PUBLIC_SANITY_PROJECT_ID</code> and{" "}
17
+ <code>NEXT_PUBLIC_SANITY_DATASET</code>, then restart, to load the
18
+ embedded Studio.
19
+ </p>
20
+ </main>
21
+ );
7
22
 
8
23
  export default StudioPage;
@@ -26,6 +26,7 @@ const LinkMark = ({ children, value }: LinkMarkProps) => {
26
26
  href={href}
27
27
  target={attrs.target}
28
28
  rel={attrs.rel}
29
+ className="text-blue underline underline-offset-2 transition-opacity hover:opacity-70"
29
30
  data-sanity-edit-target
30
31
  >
31
32
  {children}
@@ -1,7 +1,6 @@
1
1
  import { draftMode } from "next/headers";
2
2
  import { VisualEditing } from "next-sanity/visual-editing";
3
3
  import { Suspense } from "react";
4
- import { refreshAction } from "@/app/actions/refresh";
5
4
  import { DraftModeToggle } from "@/components/sanity/draft-mode-toggle";
6
5
  import { isSanityConfigured } from "@/lib/integrations/check-integration";
7
6
  import { SanityLive } from "@/lib/integrations/sanity/live";
@@ -16,7 +15,8 @@ async function VisualEditingInner() {
16
15
  <>
17
16
  <DraftModeToggle />
18
17
  <VisualEditing />
19
- <SanityLive action={refreshAction} />
18
+ {/* No `action`: in draft mode the default is already `"refresh"`. */}
19
+ <SanityLive />
20
20
  </>
21
21
  );
22
22
  }
@@ -10,7 +10,7 @@ interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
10
10
  };
11
11
  alt?: string;
12
12
  hotspot?: object;
13
- crop?: object;
13
+ crop?: { left?: number; right?: number; top?: number; bottom?: number };
14
14
  };
15
15
  maxWidth?: number;
16
16
  }
@@ -24,16 +24,26 @@ export function SanityImage({
24
24
  }: SanityImageProps) {
25
25
  if (!image?.asset) return null;
26
26
 
27
+ // getImageDimensions is uncropped; the URL builder applies the crop.
28
+ // Rounding order must match @sanity/image-url: origin first, then subtract.
27
29
  const { width, height } = getImageDimensions(image.asset);
28
- const aspectRatio = width / height;
30
+ const { left = 0, right = 0, top = 0, bottom = 0 } = image.crop ?? {};
31
+ const cropLeft = Math.round(left * width);
32
+ const cropTop = Math.round(top * height);
33
+ const sourceWidth = Math.round(width - right * width - cropLeft);
34
+ const sourceHeight = Math.round(height - bottom * height - cropTop);
29
35
 
30
- const imageProps = fill ? { fill: true } : { width, height };
36
+ const imageProps = fill
37
+ ? { fill: true }
38
+ : { width: sourceWidth, height: sourceHeight };
31
39
 
32
40
  return (
33
41
  <Image
34
- src={urlForImage(image).width(maxWidth).url()}
42
+ src={urlForImage(image)
43
+ .width(Math.min(maxWidth, sourceWidth))
44
+ .url()}
35
45
  alt={alt || image.alt || ""}
36
- aspectRatio={aspectRatio}
46
+ aspectRatio={sourceWidth / sourceHeight}
37
47
  {...imageProps}
38
48
  {...props}
39
49
  />
@@ -1,5 +1,6 @@
1
- export const isSanityConfigured = () =>
2
- Boolean(
3
- process.env.NEXT_PUBLIC_SANITY_PROJECT_ID &&
4
- process.env.NEXT_PUBLIC_SANITY_DATASET,
5
- );
1
+ import { dataset, projectId } from "@/lib/integrations/sanity/env";
2
+
3
+ // Derived from env.ts, not a second read of `process.env`. Read directly these
4
+ // disagreed — env.ts also accepts `SANITY_STUDIO_PROJECT_ID` and defaults the
5
+ // dataset — so a resolvable project reported itself unconfigured.
6
+ export const isSanityConfigured = () => Boolean(projectId && dataset);
@@ -25,3 +25,11 @@ export const client: SanityClient | null = isSanityConfigured()
25
25
  },
26
26
  })
27
27
  : null;
28
+
29
+ /**
30
+ * Non-CDN clone for the publish webhook, which has to see the mutation it was
31
+ * notified about rather than whatever the CDN still holds. Reading a stale slug
32
+ * there invalidates the old path and misses the new one.
33
+ */
34
+ export const directClient: SanityClient | null =
35
+ client?.withConfig({ useCdn: false }) ?? null;
@@ -1,18 +1,24 @@
1
+ import { Stack } from "@sanity/ui";
1
2
  import { type FieldProps, useFormValue } from "sanity";
3
+ import { SlugRedirectNotice } from "@/lib/integrations/sanity/components/slug-notices";
2
4
  import { getPinnedPageById } from "@/lib/integrations/sanity/pinned-pages";
3
5
 
4
6
  const DEFAULT_DESCRIPTION = "URL path without leading slash.";
5
7
  const PINNED_DESCRIPTION =
6
8
  "URL path without leading slash. Disabled on 📌 pinned pages.";
7
9
 
8
- // Swaps the slug description to a locked-state note on pinned pages.
9
- // (Sanity descriptions are plain strings, hence the 📌 character.)
10
+ // Swaps the slug description on pinned pages (plain-string field, hence 📌).
10
11
  export function SlugField(props: FieldProps) {
11
12
  const documentId = useFormValue(["_id"]) as string | undefined;
12
- const isPinned = getPinnedPageById(documentId) !== null;
13
+ const pinned = getPinnedPageById(documentId);
13
14
 
14
- return props.renderDefault({
15
- ...props,
16
- description: isPinned ? PINNED_DESCRIPTION : DEFAULT_DESCRIPTION,
17
- });
15
+ return (
16
+ <Stack space={3}>
17
+ {props.renderDefault({
18
+ ...props,
19
+ description: pinned ? PINNED_DESCRIPTION : DEFAULT_DESCRIPTION,
20
+ })}
21
+ <SlugRedirectNotice pinnedPath={pinned?.path} />
22
+ </Stack>
23
+ );
18
24
  }
@@ -0,0 +1,125 @@
1
+ import { Box, Button, Card, Stack, Text } from "@sanity/ui";
2
+ import { useEffect, useState } from "react";
3
+ import { useClient, useFormValue } from "sanity";
4
+ import { useRouter } from "sanity/router";
5
+ import { apiVersion } from "@/lib/integrations/sanity/env";
6
+ import { isEmptyPageSlug } from "@/lib/integrations/sanity/page-category";
7
+ import { FOLDER_PATH_QUERY } from "@/lib/integrations/sanity/queries";
8
+
9
+ const REDIRECT_QUERY = `*[_type == "redirect" && source.current == $source][0]._id`;
10
+
11
+ // The slug field writes on every keystroke.
12
+ const LOOKUP_DEBOUNCE_MS = 400;
13
+
14
+ interface SlugRedirectNoticeProps {
15
+ /** Public path of a pinned page, whose URL is fixed rather than slug-derived. */
16
+ pinnedPath?: string | undefined;
17
+ }
18
+
19
+ /**
20
+ * Warns when a published redirect claims this page's URL — the catch-all
21
+ * route only runs `checkRedirect` after a page lookup misses, so the page
22
+ * wins and the redirect silently never fires.
23
+ *
24
+ * Reads the `page` form shape; don't reuse for other slug types.
25
+ */
26
+ export function SlugRedirectNotice({ pinnedPath }: SlugRedirectNoticeProps) {
27
+ const client = useClient({ apiVersion });
28
+ const router = useRouter();
29
+ const slug = useFormValue(["slug", "current"]) as string | undefined;
30
+ const folderId = useFormValue(["pageFolder", "_ref"]) as string | undefined;
31
+ const [folderPath, setFolderPath] = useState("");
32
+ const [redirectId, setRedirectId] = useState<string | null>(null);
33
+
34
+ // Separate effect so the folder chain isn't re-resolved on every keystroke.
35
+ useEffect(() => {
36
+ if (!folderId) {
37
+ setFolderPath("");
38
+ return;
39
+ }
40
+
41
+ let active = true;
42
+ const published = client.withConfig({ perspective: "published" });
43
+
44
+ published
45
+ // Leaf joins in JS below, so this tracks the live slug rather than the
46
+ // last saved draft.
47
+ .fetch<{ path: string | null } | null>(FOLDER_PATH_QUERY, { folderId })
48
+ .then((folder) => {
49
+ if (active) setFolderPath(folder?.path ?? "");
50
+ })
51
+ .catch(() => {
52
+ if (active) setFolderPath("");
53
+ });
54
+
55
+ return () => {
56
+ active = false;
57
+ };
58
+ }, [client, folderId]);
59
+
60
+ useEffect(() => {
61
+ const leaf = isEmptyPageSlug(slug) ? "" : slug;
62
+ const source = pinnedPath
63
+ ? pinnedPath.replace(/^\/+/, "")
64
+ : [folderPath, leaf].filter(Boolean).join("/");
65
+
66
+ if (!source) {
67
+ setRedirectId(null);
68
+ return;
69
+ }
70
+
71
+ let active = true;
72
+ // Drafts never reach visitors, so an unpublished redirect isn't a conflict.
73
+ const published = client.withConfig({ perspective: "published" });
74
+
75
+ const timer = setTimeout(async () => {
76
+ try {
77
+ const id = await published.fetch<string | null>(REDIRECT_QUERY, {
78
+ source,
79
+ });
80
+ if (active) setRedirectId(id);
81
+ } catch {
82
+ if (active) setRedirectId(null);
83
+ }
84
+ }, LOOKUP_DEBOUNCE_MS);
85
+
86
+ return () => {
87
+ active = false;
88
+ clearTimeout(timer);
89
+ };
90
+ }, [client, folderPath, pinnedPath, slug]);
91
+
92
+ if (!redirectId) return null;
93
+
94
+ return (
95
+ <Card border padding={3} radius={2} tone="caution">
96
+ <Stack space={3}>
97
+ <Stack space={2}>
98
+ <Text size={1} weight="semibold">
99
+ A redirect already claims this URL
100
+ </Text>
101
+ <Text muted size={1}>
102
+ Visitors reach this page instead — a redirect only runs when no page
103
+ matches its path. Remove the redirect, or move this page to another
104
+ slug.
105
+ </Text>
106
+ </Stack>
107
+ <Box>
108
+ <Button
109
+ fontSize={1}
110
+ mode="ghost"
111
+ onClick={() =>
112
+ router.navigateIntent("edit", {
113
+ id: redirectId,
114
+ type: "redirect",
115
+ })
116
+ }
117
+ padding={3}
118
+ text="Go to redirect"
119
+ tone="caution"
120
+ />
121
+ </Box>
122
+ </Stack>
123
+ </Card>
124
+ );
125
+ }
@@ -6,9 +6,9 @@
6
6
  * if Sanity is properly set up before making API calls.
7
7
  */
8
8
 
9
- /** Sanity API version - defaults to latest stable */
9
+ /** Floor, not a preference: Content Releases need 2025-02-19 or later. */
10
10
  export const apiVersion =
11
- process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-03-15";
11
+ process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2025-02-19";
12
12
 
13
13
  /** Sanity dataset name - defaults to 'production' */
14
14
  export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET || "production";