bsmnt 0.10.0 → 0.12.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 (343) hide show
  1. package/dist/application/add-integration/index.js.map +1 -1
  2. package/dist/application/shared/node-version.js.map +1 -1
  3. package/dist/core/create/internal-ids.d.ts +3 -0
  4. package/dist/core/create/internal-ids.d.ts.map +1 -1
  5. package/dist/core/create/internal-ids.js +4 -0
  6. package/dist/core/create/internal-ids.js.map +1 -1
  7. package/dist/domain/agents.d.ts.map +1 -1
  8. package/dist/domain/animations.d.ts.map +1 -1
  9. package/dist/domain/cms.d.ts.map +1 -1
  10. package/dist/domain/forms.d.ts.map +1 -1
  11. package/dist/domain/hooks.d.ts.map +1 -1
  12. package/dist/domain/skills.d.ts.map +1 -1
  13. package/dist/domain/templates.d.ts +10 -0
  14. package/dist/domain/templates.d.ts.map +1 -1
  15. package/dist/domain/templates.js +5 -0
  16. package/dist/domain/templates.js.map +1 -1
  17. package/dist/index.js +18 -5
  18. package/dist/index.js.map +1 -1
  19. package/dist/infrastructure/agents/register-sanity-mcp.js +3 -8
  20. package/dist/infrastructure/agents/register-sanity-mcp.js.map +1 -1
  21. package/dist/infrastructure/agents/register-storybook-mcp.d.ts +18 -0
  22. package/dist/infrastructure/agents/register-storybook-mcp.d.ts.map +1 -0
  23. package/dist/infrastructure/agents/register-storybook-mcp.js +157 -0
  24. package/dist/infrastructure/agents/register-storybook-mcp.js.map +1 -0
  25. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  26. package/dist/infrastructure/create/lifecycle-registry.js +39 -1
  27. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  28. package/dist/infrastructure/create/merge-registry.d.ts.map +1 -1
  29. package/dist/infrastructure/create/merge-registry.js +4 -0
  30. package/dist/infrastructure/create/merge-registry.js.map +1 -1
  31. package/dist/infrastructure/create/sanity-auth.js.map +1 -1
  32. package/dist/infrastructure/create/setup-agent.d.ts +6 -1
  33. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  34. package/dist/infrastructure/create/setup-agent.js +23 -21
  35. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  36. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
  37. package/dist/infrastructure/create/setup-remote.js.map +1 -1
  38. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  39. package/dist/infrastructure/create/setup-sanity.js +12 -1
  40. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  41. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  42. package/dist/infrastructure/integrations/inject-integration.js +2 -0
  43. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  44. package/dist/infrastructure/update/notifier.js.map +1 -1
  45. package/dist/modules/features/cms/index.d.ts.map +1 -1
  46. package/dist/modules/features/cms/index.js +2 -0
  47. package/dist/modules/features/cms/index.js.map +1 -1
  48. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  49. package/dist/modules/features/cms/sanity/config.js +1 -0
  50. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  51. package/dist/modules/features/cms/sanity/index.d.ts.map +1 -1
  52. package/dist/modules/features/cms/sanity/index.js +10 -1
  53. package/dist/modules/features/cms/sanity/index.js.map +1 -1
  54. package/dist/modules/features/cms/sanity/mergers/check-integration.d.ts.map +1 -1
  55. package/dist/modules/features/cms/sanity/mergers/check-integration.js +15 -6
  56. package/dist/modules/features/cms/sanity/mergers/check-integration.js.map +1 -1
  57. package/dist/modules/features/cms/sanity/mergers/knip.d.ts.map +1 -1
  58. package/dist/modules/features/cms/sanity/mergers/knip.js +1 -0
  59. package/dist/modules/features/cms/sanity/mergers/knip.js.map +1 -1
  60. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  61. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts +12 -0
  62. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts.map +1 -0
  63. package/dist/modules/features/cms/sanity/mergers/package-json.js +19 -0
  64. package/dist/modules/features/cms/sanity/mergers/package-json.js.map +1 -0
  65. package/dist/modules/features/cms/sanity-pagebuilder/index.d.ts.map +1 -1
  66. package/dist/modules/features/cms/sanity-pagebuilder/index.js +7 -1
  67. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  68. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts +14 -0
  69. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -0
  70. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +50 -0
  71. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -0
  72. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts +3 -0
  73. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts.map +1 -0
  74. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js +55 -0
  75. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js.map +1 -0
  76. package/dist/modules/presets/index.d.ts.map +1 -1
  77. package/dist/modules/presets/index.js +2 -0
  78. package/dist/modules/presets/index.js.map +1 -1
  79. package/dist/modules/presets/next-pagebuilder-cache.d.ts +3 -0
  80. package/dist/modules/presets/next-pagebuilder-cache.d.ts.map +1 -0
  81. package/dist/modules/presets/next-pagebuilder-cache.js +10 -0
  82. package/dist/modules/presets/next-pagebuilder-cache.js.map +1 -0
  83. package/dist/paths.d.ts +6 -0
  84. package/dist/paths.d.ts.map +1 -1
  85. package/dist/paths.js +6 -0
  86. package/dist/paths.js.map +1 -1
  87. package/package.json +5 -2
  88. package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +36 -4
  89. package/src/modules/features/cms/sanity/files/lib/integrations/check-integration.ts +7 -6
  90. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/README.md +1 -1
  91. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +8 -1
  92. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/env.ts +2 -2
  93. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +4 -0
  94. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +172 -24
  95. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +14 -7
  96. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +2 -2
  97. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +16 -1
  98. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +8 -1
  99. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +2 -2
  100. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +6 -5
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +8 -0
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-notices.tsx +6 -16
  106. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +2 -2
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +22 -52
  109. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
  110. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +3 -1
  111. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +78 -33
  112. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +70 -57
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +119 -29
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +2 -0
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  117. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  118. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +5 -0
  119. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +12 -0
  120. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +2 -0
  121. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
  122. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +56 -0
  123. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +12 -0
  124. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +11 -8
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/.knip.json +8 -0
  126. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/[[...slug]]/route.ts +112 -0
  127. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/disable/route.ts +7 -0
  128. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/enable/route.ts +20 -0
  129. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +292 -0
  130. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/feed.xml/route.ts +66 -0
  131. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/sitemap.md/route.ts +132 -0
  132. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/layout.tsx +26 -0
  133. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/page.tsx +23 -0
  134. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/draft-mode-toggle.tsx +27 -0
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +94 -0
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/visual-editing.tsx +11 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +83 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/check-integration.ts +6 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/client.ts +37 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +58 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  146. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-field.tsx +24 -0
  147. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  148. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-notices.tsx +125 -0
  149. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  150. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  151. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/env.ts +34 -0
  152. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +39 -0
  153. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/redirects.ts +22 -0
  154. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +81 -0
  155. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/live/index.tsx +140 -0
  156. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  157. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +182 -0
  158. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-category.ts +28 -0
  159. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-tree-pane.tsx +431 -0
  160. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  161. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +119 -0
  162. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +548 -0
  163. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  164. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  165. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +116 -0
  166. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +7 -0
  167. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  168. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  169. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
  170. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  171. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  172. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  173. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  174. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  175. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  176. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +137 -0
  177. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/company-data.ts +70 -0
  178. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  179. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  180. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +152 -0
  181. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  182. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  183. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  184. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +132 -0
  185. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  186. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +43 -0
  187. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  188. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +265 -0
  189. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  190. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  191. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +53 -0
  192. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +339 -0
  193. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +89 -0
  194. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/image.ts +8 -0
  195. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/link.ts +140 -0
  196. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-builder-markdown.ts +113 -0
  197. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  198. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +36 -0
  199. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/check-redirect.ts +30 -0
  200. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/json-ld.tsx +522 -0
  201. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +189 -0
  202. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/rss.ts +96 -0
  203. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/sitemap.ts +58 -0
  204. package/src/modules/features/cms/sanity-pagebuilder-cache/files/proxy.ts +90 -0
  205. package/src/modules/features/env/dotenvx/files/vercelignore +5 -0
  206. package/src/templates/next-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
  207. package/src/templates/next-default/.env.example +2 -2
  208. package/src/templates/next-default/.github/workflows/ci.yml +1 -1
  209. package/src/templates/next-default/.vscode/settings.json +0 -2
  210. package/src/templates/next-default/biome.json +2 -1
  211. package/src/templates/next-default/components/ui/image/index.tsx +21 -15
  212. package/src/templates/next-default/knip.json +1 -1
  213. package/src/templates/next-default/lefthook.yml +1 -1
  214. package/src/templates/next-default/lib/utils/README.md +18 -5
  215. package/src/templates/next-default/lib/utils/image-sizes.ts +72 -0
  216. package/src/templates/next-default/lib/utils/merge-refs.ts +56 -0
  217. package/src/templates/next-default/lib/utils/strings.ts +1 -51
  218. package/src/templates/next-default/next.config.ts +12 -7
  219. package/src/templates/next-default/package.json +2 -5
  220. package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
  221. package/src/templates/next-experiments/.env.example +2 -2
  222. package/src/templates/next-experiments/.github/workflows/ci.yml +1 -1
  223. package/src/templates/next-experiments/.vscode/settings.json +0 -2
  224. package/src/templates/next-experiments/app/page.tsx +1 -1
  225. package/src/templates/next-experiments/biome.json +2 -1
  226. package/src/templates/next-experiments/components/ui/image/index.tsx +21 -15
  227. package/src/templates/next-experiments/knip.json +1 -1
  228. package/src/templates/next-experiments/lefthook.yml +1 -1
  229. package/src/templates/next-experiments/lib/utils/README.md +18 -5
  230. package/src/templates/next-experiments/lib/utils/image-sizes.ts +72 -0
  231. package/src/templates/next-experiments/lib/utils/merge-refs.ts +56 -0
  232. package/src/templates/next-experiments/lib/utils/strings.ts +1 -51
  233. package/src/templates/next-experiments/next.config.ts +12 -7
  234. package/src/templates/next-experiments/package.json +2 -5
  235. package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -0
  236. package/src/templates/next-pagebuilder/.env.example +1 -0
  237. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +1 -1
  238. package/src/templates/next-pagebuilder/.vscode/settings.json +0 -2
  239. package/src/templates/next-pagebuilder/README.md +64 -0
  240. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +3 -1
  241. package/src/templates/next-pagebuilder/biome.json +4 -2
  242. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +3 -6
  243. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -2
  244. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +3 -8
  245. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +5 -0
  246. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  247. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +23 -40
  248. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +88 -58
  249. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/skeleton.tsx +4 -16
  250. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/types.ts +14 -1
  251. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +16 -0
  252. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +21 -7
  253. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -0
  254. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +21 -12
  255. package/src/templates/next-pagebuilder/knip.json +3 -2
  256. package/src/templates/next-pagebuilder/lefthook.yml +1 -1
  257. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +1221 -977
  258. package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +72 -0
  259. package/src/templates/next-pagebuilder/lib/utils/merge-refs.ts +56 -0
  260. package/src/templates/next-pagebuilder/next.config.ts +6 -6
  261. package/src/templates/next-pagebuilder/package.json +4 -6
  262. package/src/templates/next-pagebuilder-cache/.biome/plugins/README.md +21 -0
  263. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-anchor-element.grit +12 -0
  264. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
  265. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-relative-parent-imports.grit +10 -0
  266. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  267. package/src/templates/next-pagebuilder-cache/.env.example +40 -0
  268. package/src/templates/next-pagebuilder-cache/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  269. package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +105 -0
  270. package/src/templates/next-pagebuilder-cache/.vscode/extensions.json +19 -0
  271. package/src/templates/next-pagebuilder-cache/.vscode/settings.json +103 -0
  272. package/src/templates/next-pagebuilder-cache/.zed/settings.json +47 -0
  273. package/src/templates/next-pagebuilder-cache/README.md +212 -0
  274. package/src/templates/next-pagebuilder-cache/_gitignore +65 -0
  275. package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +124 -0
  276. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +28 -0
  277. package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +53 -0
  278. package/src/templates/next-pagebuilder-cache/app/favicon.ico +0 -0
  279. package/src/templates/next-pagebuilder-cache/app/layout.tsx +83 -0
  280. package/src/templates/next-pagebuilder-cache/app/robots.ts +13 -0
  281. package/src/templates/next-pagebuilder-cache/app/sitemap.xml/route.ts +84 -0
  282. package/src/templates/next-pagebuilder-cache/biome.json +258 -0
  283. package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +127 -0
  284. package/src/templates/next-pagebuilder-cache/components/layout/header/components/cta-button.tsx +28 -0
  285. package/src/templates/next-pagebuilder-cache/components/layout/header/components/mega-menu-panel.tsx +90 -0
  286. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-item-renderer.tsx +98 -0
  287. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-leaf-item.tsx +33 -0
  288. package/src/templates/next-pagebuilder-cache/components/layout/header/components/types.ts +7 -0
  289. package/src/templates/next-pagebuilder-cache/components/layout/header/header-client.tsx +110 -0
  290. package/src/templates/next-pagebuilder-cache/components/layout/header/index.tsx +32 -0
  291. package/src/templates/next-pagebuilder-cache/components/layout/json-ld/index.tsx +79 -0
  292. package/src/templates/next-pagebuilder-cache/components/layout/wrapper/index.tsx +33 -0
  293. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +83 -0
  294. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +28 -0
  295. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-card.tsx +66 -0
  296. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-filters.tsx +76 -0
  297. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-grid.tsx +40 -0
  298. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  299. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +306 -0
  300. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/skeleton.tsx +41 -0
  301. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/types.ts +32 -0
  302. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +17 -0
  303. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +169 -0
  304. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +17 -0
  305. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +16 -0
  306. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +71 -0
  307. package/src/templates/next-pagebuilder-cache/components/page-builder/types.ts +25 -0
  308. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +147 -0
  309. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +225 -0
  310. package/src/templates/next-pagebuilder-cache/components/ui/link/index.tsx +152 -0
  311. package/src/templates/next-pagebuilder-cache/components.json +24 -0
  312. package/src/templates/next-pagebuilder-cache/knip.json +35 -0
  313. package/src/templates/next-pagebuilder-cache/lefthook.yml +25 -0
  314. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +1588 -0
  315. package/src/templates/next-pagebuilder-cache/lib/scripts/setup-ci-secrets.ts +117 -0
  316. package/src/templates/next-pagebuilder-cache/lib/styles/cn.ts +5 -0
  317. package/src/templates/next-pagebuilder-cache/lib/styles/global.css +69 -0
  318. package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +17 -0
  319. package/src/templates/next-pagebuilder-cache/lib/utils/format-date.ts +8 -0
  320. package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
  321. package/src/templates/next-pagebuilder-cache/lib/utils/slug-tag.ts +6 -0
  322. package/src/templates/next-pagebuilder-cache/next.config.ts +177 -0
  323. package/src/templates/next-pagebuilder-cache/package.json +62 -0
  324. package/src/templates/next-pagebuilder-cache/postcss.config.mjs +65 -0
  325. package/src/templates/next-pagebuilder-cache/svg.d.ts +6 -0
  326. package/src/templates/next-pagebuilder-cache/tsconfig.json +38 -0
  327. package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
  328. package/src/templates/next-webgl/.env.example +2 -2
  329. package/src/templates/next-webgl/.github/workflows/ci.yml +1 -1
  330. package/src/templates/next-webgl/.vscode/settings.json +0 -2
  331. package/src/templates/next-webgl/biome.json +2 -1
  332. package/src/templates/next-webgl/components/ui/image/index.tsx +21 -15
  333. package/src/templates/next-webgl/knip.json +1 -1
  334. package/src/templates/next-webgl/lefthook.yml +1 -1
  335. package/src/templates/next-webgl/lib/utils/README.md +18 -5
  336. package/src/templates/next-webgl/lib/utils/image-sizes.ts +72 -0
  337. package/src/templates/next-webgl/lib/utils/merge-refs.ts +56 -0
  338. package/src/templates/next-webgl/lib/utils/strings.ts +1 -51
  339. package/src/templates/next-webgl/next.config.ts +12 -7
  340. package/src/templates/next-webgl/package.json +4 -7
  341. package/src/templates/next-experiments/tsconfig.tsbuildinfo +0 -1
  342. package/src/templates/next-pagebuilder/app/actions/refresh.ts +0 -5
  343. package/src/templates/next-webgl/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,242 @@
1
+ import { AddIcon, SearchIcon } from "@sanity/icons";
2
+ import {
3
+ Box,
4
+ Button,
5
+ Card,
6
+ Dialog,
7
+ Flex,
8
+ Grid,
9
+ Stack,
10
+ Text,
11
+ TextInput,
12
+ } from "@sanity/ui";
13
+ import { useMemo, useState } from "react";
14
+ import type { ArrayInputFunctionsProps, ArraySchemaType } from "sanity";
15
+
16
+ type ArrayItem = { _key: string };
17
+ type MemberSchemaType = ArraySchemaType["of"][number];
18
+
19
+ type CreateItem = (memberType: MemberSchemaType) => ArrayItem;
20
+
21
+ type InsertMenuSection = {
22
+ name: string;
23
+ title: string;
24
+ members: MemberSchemaType[];
25
+ };
26
+
27
+ const SCROLL_HEIGHT = "min(60vh, 34rem)";
28
+ const TILE_ASPECT_RATIO = "3 / 2";
29
+
30
+ const memberTitle = (member: MemberSchemaType) => member.title ?? member.name;
31
+
32
+ function BlockTile({
33
+ member,
34
+ previewUrl,
35
+ onSelect,
36
+ }: {
37
+ member: MemberSchemaType;
38
+ previewUrl: string | undefined;
39
+ onSelect: () => void;
40
+ }) {
41
+ const Icon = member.icon;
42
+
43
+ return (
44
+ <Card
45
+ as="button"
46
+ border
47
+ onClick={onSelect}
48
+ overflow="hidden"
49
+ padding={0}
50
+ radius={2}
51
+ style={{ cursor: "pointer", textAlign: "left", width: "100%" }}
52
+ type="button"
53
+ >
54
+ <Stack space={0}>
55
+ <Box
56
+ style={{
57
+ aspectRatio: TILE_ASPECT_RATIO,
58
+ background: "var(--card-muted-bg-color)",
59
+ position: "relative",
60
+ }}
61
+ >
62
+ {previewUrl ? (
63
+ // biome-ignore lint/performance/noImgElement: Studio-only CMS asset
64
+ <img
65
+ alt=""
66
+ src={`${previewUrl}?w=640&fit=max&auto=format`}
67
+ style={{
68
+ height: "100%",
69
+ inset: 0,
70
+ objectFit: "contain",
71
+ position: "absolute",
72
+ width: "100%",
73
+ }}
74
+ />
75
+ ) : (
76
+ <Flex
77
+ align="center"
78
+ justify="center"
79
+ style={{ inset: 0, position: "absolute" }}
80
+ >
81
+ {Icon ? <Icon /> : null}
82
+ </Flex>
83
+ )}
84
+ </Box>
85
+ <Box padding={2}>
86
+ <Text size={1} textOverflow="ellipsis" weight="medium">
87
+ {memberTitle(member)}
88
+ </Text>
89
+ </Box>
90
+ </Stack>
91
+ </Card>
92
+ );
93
+ }
94
+
95
+ function InsertMenuDialog({
96
+ sections,
97
+ previewImageUrl,
98
+ onSelect,
99
+ onClose,
100
+ }: {
101
+ sections: InsertMenuSection[];
102
+ previewImageUrl: ((schemaTypeName: string) => string | undefined) | undefined;
103
+ onSelect: (member: MemberSchemaType) => void;
104
+ onClose: () => void;
105
+ }) {
106
+ const [search, setSearch] = useState("");
107
+
108
+ const visibleSections = useMemo(() => {
109
+ const query = search.trim().toLowerCase();
110
+ if (!query) return sections;
111
+ return sections
112
+ .map((section) => ({
113
+ ...section,
114
+ members: section.members.filter((member) =>
115
+ memberTitle(member).toLowerCase().includes(query),
116
+ ),
117
+ }))
118
+ .filter((section) => section.members.length > 0);
119
+ }, [search, sections]);
120
+
121
+ return (
122
+ <Dialog
123
+ header="Components"
124
+ id="page-builder-insert"
125
+ onClose={onClose}
126
+ open
127
+ width={3}
128
+ >
129
+ <Stack padding={4} space={4}>
130
+ <TextInput
131
+ autoFocus
132
+ icon={SearchIcon}
133
+ onChange={(event) => setSearch(event.currentTarget.value)}
134
+ placeholder="Search components…"
135
+ value={search}
136
+ />
137
+ <Box style={{ height: SCROLL_HEIGHT, overflowY: "auto" }}>
138
+ {visibleSections.length === 0 ? (
139
+ <Text muted size={1}>
140
+ No components match “{search}”.
141
+ </Text>
142
+ ) : (
143
+ <Stack space={5}>
144
+ {visibleSections.map((section) => (
145
+ <Stack key={section.name} space={3}>
146
+ <Text muted size={1} weight="semibold">
147
+ {section.title}
148
+ </Text>
149
+ <Grid columns={[2, 3, 4]} gap={3}>
150
+ {section.members.map((member) => (
151
+ <BlockTile
152
+ key={member.name}
153
+ member={member}
154
+ onSelect={() => onSelect(member)}
155
+ previewUrl={previewImageUrl?.(member.name)}
156
+ />
157
+ ))}
158
+ </Grid>
159
+ </Stack>
160
+ ))}
161
+ </Stack>
162
+ )}
163
+ </Box>
164
+ </Stack>
165
+ </Dialog>
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Pass as `arrayFunctions`. Reads `insertMenu.groups` and `insertMenu.views`, so
171
+ * a schema already configuring the built-in menu needs no extra wiring.
172
+ *
173
+ * Replaces the built-in popover, which is anchored to the array grid element and
174
+ * so clips on page scroll and resizes when switching tabs.
175
+ */
176
+ export function PageBuilderInsertMenu(
177
+ props: ArrayInputFunctionsProps<ArrayItem, ArraySchemaType>,
178
+ ) {
179
+ const { onItemAppend, onValueCreate, readOnly, schemaType } = props;
180
+ const [open, setOpen] = useState(false);
181
+
182
+ const insertMenu = schemaType.options?.insertMenu;
183
+ const members = schemaType.of;
184
+
185
+ const sections = useMemo<InsertMenuSection[]>(() => {
186
+ const byName = new Map(members.map((member) => [member.name, member]));
187
+ const groups = insertMenu?.groups;
188
+ if (!groups) {
189
+ return [{ name: "all", title: "Components", members: [...members] }];
190
+ }
191
+ return groups
192
+ .map((group) => ({
193
+ name: group.name,
194
+ title: group.title ?? group.name,
195
+ members: (group.of ?? [])
196
+ .map((name) => byName.get(name))
197
+ .filter((member) => member !== undefined),
198
+ }))
199
+ .filter((section) => section.members.length > 0);
200
+ }, [members, insertMenu]);
201
+
202
+ const previewImageUrl = useMemo(() => {
203
+ const gridView = insertMenu?.views?.find((view) => view.name === "grid");
204
+ return gridView && "previewImageUrl" in gridView
205
+ ? gridView.previewImageUrl
206
+ : undefined;
207
+ }, [insertMenu]);
208
+
209
+ const insert = (member: MemberSchemaType) => {
210
+ onItemAppend((onValueCreate as unknown as CreateItem)(member));
211
+ setOpen(false);
212
+ };
213
+
214
+ const onlyMember = members.length === 1 ? members[0] : undefined;
215
+
216
+ if (schemaType.options?.disableActions?.includes("add")) return null;
217
+
218
+ return (
219
+ <>
220
+ <Grid>
221
+ <Button
222
+ disabled={readOnly === true}
223
+ fontSize={1}
224
+ icon={AddIcon}
225
+ mode="ghost"
226
+ onClick={() => (onlyMember ? insert(onlyMember) : setOpen(true))}
227
+ padding={3}
228
+ text={onlyMember ? "Add item" : "Add item…"}
229
+ />
230
+ </Grid>
231
+
232
+ {open && (
233
+ <InsertMenuDialog
234
+ onClose={() => setOpen(false)}
235
+ onSelect={insert}
236
+ previewImageUrl={previewImageUrl}
237
+ sections={sections}
238
+ />
239
+ )}
240
+ </>
241
+ );
242
+ }
@@ -0,0 +1,50 @@
1
+ import { Select } from "@sanity/ui";
2
+ import { useEffect } from "react";
3
+ import { type StringInputProps, set, setIfMissing } from "sanity";
4
+
5
+ type PageCategoryOption = {
6
+ title: string;
7
+ value: string;
8
+ };
9
+
10
+ const isPageCategoryOption = (option: unknown): option is PageCategoryOption =>
11
+ typeof option === "object" &&
12
+ option !== null &&
13
+ "title" in option &&
14
+ "value" in option &&
15
+ typeof option.title === "string" &&
16
+ typeof option.value === "string";
17
+
18
+ export function PageCategoryInput(props: StringInputProps) {
19
+ useEffect(() => {
20
+ if (props.value === undefined) {
21
+ props.onChange(setIfMissing("generic"));
22
+ }
23
+ }, [props.onChange, props.value]);
24
+
25
+ const options = Array.isArray(props.schemaType.options?.list)
26
+ ? props.schemaType.options.list.filter(isPageCategoryOption)
27
+ : [];
28
+
29
+ if (options.length === 0) {
30
+ return props.renderDefault({ ...props, value: props.value ?? "generic" });
31
+ }
32
+
33
+ return (
34
+ <Select
35
+ {...props.elementProps}
36
+ disabled={props.readOnly}
37
+ onChange={(event) => props.onChange(set(event.currentTarget.value))}
38
+ value={props.value ?? "generic"}
39
+ {...(props.validationError
40
+ ? { customValidity: props.validationError }
41
+ : {})}
42
+ >
43
+ {options.map((option) => (
44
+ <option key={option.value} value={option.value}>
45
+ {option.title}
46
+ </option>
47
+ ))}
48
+ </Select>
49
+ );
50
+ }
@@ -0,0 +1,66 @@
1
+ import { Badge, Card, Flex, Stack, Text } from "@sanity/ui";
2
+ import { useEffect, useState } from "react";
3
+ import { type ArrayOfPrimitivesInputProps, useClient } from "sanity";
4
+ import { apiVersion } from "../env";
5
+
6
+ type CompanyProfile = { name?: string | null; url?: string | null };
7
+
8
+ // Draft-first so editors see unpublished toggle changes.
9
+ export const INCLUDED_COMPANY_PROFILES_QUERY = `coalesce(
10
+ *[_id == "drafts.companyData"][0],
11
+ *[_id == "companyData"][0]
12
+ ).socialLinks[includeInStructuredData == true]{ name, url }`;
13
+
14
+ // Shows which Company Data social links are merged into sameAs automatically.
15
+ export function SameAsInput(props: ArrayOfPrimitivesInputProps) {
16
+ const { renderDefault } = props;
17
+ const client = useClient({ apiVersion });
18
+ const [included, setIncluded] = useState<CompanyProfile[]>([]);
19
+
20
+ useEffect(() => {
21
+ let active = true;
22
+ client
23
+ .fetch<CompanyProfile[] | null>(INCLUDED_COMPANY_PROFILES_QUERY)
24
+ .then((rows) => {
25
+ if (active) setIncluded(rows ?? []);
26
+ })
27
+ .catch(() => {
28
+ // Best-effort preview — never block editing.
29
+ });
30
+ return () => {
31
+ active = false;
32
+ };
33
+ }, [client]);
34
+
35
+ return (
36
+ <Stack space={3}>
37
+ <Card padding={3} radius={2} border tone="transparent">
38
+ <Stack space={3}>
39
+ {included.length > 0 ? (
40
+ <>
41
+ <Text size={1} muted>
42
+ Included from Company Data (toggle off there to remove):
43
+ </Text>
44
+ {included.map((profile) => (
45
+ <Flex key={profile.url} gap={2} align="center">
46
+ <Badge fontSize={0} mode="outline">
47
+ {profile.name ?? "Profile"}
48
+ </Badge>
49
+ <Text size={1} muted textOverflow="ellipsis">
50
+ {profile.url}
51
+ </Text>
52
+ </Flex>
53
+ ))}
54
+ </>
55
+ ) : (
56
+ <Text size={1} muted>
57
+ No Company Data social links are included yet — enable "Include in
58
+ Structured Data" on a social link to add it here.
59
+ </Text>
60
+ )}
61
+ </Stack>
62
+ </Card>
63
+ {renderDefault(props)}
64
+ </Stack>
65
+ );
66
+ }
@@ -0,0 +1,24 @@
1
+ import { Stack } from "@sanity/ui";
2
+ import { type FieldProps, useFormValue } from "sanity";
3
+ import { SlugRedirectNotice } from "@/lib/integrations/sanity/components/slug-notices";
4
+ import { getPinnedPageById } from "@/lib/integrations/sanity/pinned-pages";
5
+
6
+ const DEFAULT_DESCRIPTION = "URL path without leading slash.";
7
+ const PINNED_DESCRIPTION =
8
+ "URL path without leading slash. Disabled on 📌 pinned pages.";
9
+
10
+ // Swaps the slug description on pinned pages (plain-string field, hence 📌).
11
+ export function SlugField(props: FieldProps) {
12
+ const documentId = useFormValue(["_id"]) as string | undefined;
13
+ const pinned = getPinnedPageById(documentId);
14
+
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
+ );
24
+ }
@@ -0,0 +1,14 @@
1
+ import type { SlugInputProps } from "sanity";
2
+
3
+ // Adds a not-allowed cursor to the slug field when it's read-only (pinned pages).
4
+ export function SlugInput(props: SlugInputProps) {
5
+ if (!props.readOnly) {
6
+ return props.renderDefault(props);
7
+ }
8
+
9
+ return (
10
+ <div style={{ cursor: "not-allowed" }}>
11
+ <div style={{ pointerEvents: "none" }}>{props.renderDefault(props)}</div>
12
+ </div>
13
+ );
14
+ }
@@ -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
+ }
@@ -0,0 +1,62 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useClient } from "sanity";
3
+ import { apiVersion } from "@/lib/integrations/sanity/env";
4
+
5
+ const PREVIEW_ASSETS_QUERY = `*[
6
+ _type == "studioPreview" &&
7
+ key in $keys &&
8
+ defined(preview.asset._ref)
9
+ ] | order(_updatedAt desc) {
10
+ key,
11
+ "url": preview.asset->url
12
+ }`;
13
+
14
+ type PreviewDocument = {
15
+ key: string | null;
16
+ url: string | null;
17
+ };
18
+
19
+ export type StudioPreviewUrls = Record<string, string>;
20
+
21
+ const SEPARATOR = " ";
22
+
23
+ /** `undefined` while in flight, so callers can tell loading from "no preview". */
24
+ export function useStudioPreviewUrls(keys: readonly string[]) {
25
+ const client = useClient({ apiVersion });
26
+ const [urls, setUrls] = useState<StudioPreviewUrls>();
27
+ // Keys off contents, not array identity — callers pass a freshly mapped array.
28
+ const keyList = keys.join(SEPARATOR);
29
+
30
+ useEffect(() => {
31
+ if (!keyList) {
32
+ setUrls({});
33
+ return;
34
+ }
35
+
36
+ let cancelled = false;
37
+
38
+ client
39
+ .fetch<PreviewDocument[]>(PREVIEW_ASSETS_QUERY, {
40
+ keys: keyList.split(SEPARATOR),
41
+ })
42
+ .then((documents) => {
43
+ if (cancelled) return;
44
+ const next: StudioPreviewUrls = {};
45
+ for (const document of documents) {
46
+ if (document.key && document.url && !next[document.key]) {
47
+ next[document.key] = document.url;
48
+ }
49
+ }
50
+ setUrls(next);
51
+ })
52
+ .catch(() => {
53
+ if (!cancelled) setUrls({});
54
+ });
55
+
56
+ return () => {
57
+ cancelled = true;
58
+ };
59
+ }, [client, keyList]);
60
+
61
+ return urls;
62
+ }
@@ -0,0 +1,40 @@
1
+ import { useState } from "react";
2
+ import type {
3
+ DocumentActionComponent,
4
+ DocumentActionDialogProps,
5
+ DocumentActionProps,
6
+ } from "sanity";
7
+
8
+ export function createConfirmPublishAction(
9
+ originalPublishAction: DocumentActionComponent,
10
+ ): DocumentActionComponent {
11
+ const ConfirmPublishAction = (props: DocumentActionProps) => {
12
+ const original = originalPublishAction(props);
13
+ const [dialogOpen, setDialogOpen] = useState(false);
14
+
15
+ if (!original) return null;
16
+
17
+ const { dialog: _originalDialog, ...originalWithoutDialog } = original;
18
+ const dialog: DocumentActionDialogProps | false = dialogOpen
19
+ ? {
20
+ type: "confirm",
21
+ tone: "critical",
22
+ message:
23
+ "This will make these changes live on the production site immediately. Continue?",
24
+ onCancel: () => setDialogOpen(false),
25
+ onConfirm: () => {
26
+ setDialogOpen(false);
27
+ original.onHandle?.();
28
+ },
29
+ }
30
+ : false;
31
+
32
+ return {
33
+ ...originalWithoutDialog,
34
+ onHandle: () => setDialogOpen(true),
35
+ dialog,
36
+ };
37
+ };
38
+
39
+ return ConfirmPublishAction;
40
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Sanity Environment Configuration
3
+ *
4
+ * All values have safe defaults to allow the app to build and run
5
+ * without Sanity configured. Use isSanityConfigured() to check
6
+ * if Sanity is properly set up before making API calls.
7
+ */
8
+
9
+ /** Floor, not a preference: Content Releases need 2025-02-19 or later. */
10
+ export const apiVersion =
11
+ process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2025-02-19";
12
+
13
+ /** Sanity dataset name - defaults to 'production' */
14
+ export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET || "production";
15
+
16
+ /** Sanity project ID - empty string if not configured */
17
+ export const projectId =
18
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID ||
19
+ process.env.SANITY_STUDIO_PROJECT_ID ||
20
+ "";
21
+
22
+ /** Sanity Studio URL for visual editing */
23
+ export const studioUrl =
24
+ process.env.NODE_ENV === "development"
25
+ ? "http://localhost:3000/studio"
26
+ : `${process.env.NEXT_PUBLIC_BASE_URL || ""}/studio`;
27
+
28
+ export const sanityToken = process.env.SANITY_API_READ_TOKEN || "";
29
+
30
+ /** Preview URL for draft mode */
31
+ export const previewURL =
32
+ process.env.NODE_ENV === "development"
33
+ ? "http://localhost:3000"
34
+ : process.env.NEXT_PUBLIC_BASE_URL || "";
@@ -0,0 +1,39 @@
1
+ import {
2
+ type DynamicFetchOptions,
3
+ sanityFetch,
4
+ toSanityFetchOptions,
5
+ } from "@/lib/integrations/sanity/live"
6
+ import { LAYOUT_QUERY } from "@/lib/integrations/sanity/queries"
7
+ import type { LAYOUT_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
8
+ import { SANITY_LAYOUT_TAGS } from "@/lib/integrations/sanity/tags"
9
+
10
+ export type LayoutData = LAYOUT_QUERY_RESULT
11
+ export type NavbarData = NonNullable<LayoutData["navbar"]>
12
+ export type FooterData = NonNullable<LayoutData["footer"]>
13
+ export type CompanyData = NonNullable<LayoutData["companyData"]>
14
+ export type StructuredData = NonNullable<LayoutData["structuredData"]>
15
+
16
+ const EMPTY_LAYOUT: LayoutData = {
17
+ navbar: null,
18
+ footer: null,
19
+ companyData: null,
20
+ structuredData: null,
21
+ }
22
+
23
+ // One entry for all four layout singletons: header, footer and json-ld call this
24
+ // with the same options, so they share one fetch instead of four.
25
+ //
26
+ // Don't cap the empty branch — this renders in every route's shell, and a nested
27
+ // lifetime propagates upwards as a min().
28
+ export async function getLayoutData(
29
+ fetchOptions: DynamicFetchOptions
30
+ ): Promise<LayoutData> {
31
+ "use cache"
32
+ const { data } = await sanityFetch({
33
+ query: LAYOUT_QUERY,
34
+ ...toSanityFetchOptions(fetchOptions),
35
+ tags: [...SANITY_LAYOUT_TAGS],
36
+ })
37
+
38
+ return (data as LayoutData | null) ?? EMPTY_LAYOUT
39
+ }