bsmnt 0.10.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 (321) 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/lib/integrations/check-integration.ts +7 -6
  89. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/README.md +1 -1
  90. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +1 -0
  91. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/env.ts +2 -2
  92. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +4 -0
  93. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +172 -24
  94. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +14 -7
  95. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +2 -2
  96. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +16 -1
  97. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +1 -0
  98. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +2 -2
  99. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +6 -5
  100. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +8 -0
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-notices.tsx +6 -16
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +2 -2
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +22 -52
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +3 -1
  106. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +7 -0
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +70 -57
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +119 -29
  109. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  110. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  111. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  112. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +2 -0
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +12 -0
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +56 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +12 -0
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +11 -8
  117. package/src/modules/features/cms/sanity-pagebuilder-cache/files/.knip.json +8 -0
  118. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/[[...slug]]/route.ts +112 -0
  119. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/disable/route.ts +7 -0
  120. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/enable/route.ts +20 -0
  121. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +292 -0
  122. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/feed.xml/route.ts +66 -0
  123. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/sitemap.md/route.ts +132 -0
  124. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/layout.tsx +26 -0
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/page.tsx +23 -0
  126. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/draft-mode-toggle.tsx +27 -0
  127. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +88 -0
  128. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/visual-editing.tsx +11 -0
  129. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +51 -0
  130. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/check-integration.ts +6 -0
  131. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/client.ts +37 -0
  132. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
  133. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  134. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +39 -0
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-field.tsx +24 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-notices.tsx +125 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/env.ts +34 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +39 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/redirects.ts +22 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +81 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/live/index.tsx +140 -0
  146. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  147. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +144 -0
  148. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-category.ts +28 -0
  149. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-tree-pane.tsx +431 -0
  150. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  151. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +119 -0
  152. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +548 -0
  153. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  154. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  155. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +116 -0
  156. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +6 -0
  157. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  158. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  159. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
  160. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  161. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  162. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  163. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  164. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  165. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +134 -0
  166. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/company-data.ts +70 -0
  167. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  168. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  169. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +152 -0
  170. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  171. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  172. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  173. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +132 -0
  174. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  175. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +43 -0
  176. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  177. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +265 -0
  178. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  179. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  180. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +51 -0
  181. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +334 -0
  182. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +89 -0
  183. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/image.ts +8 -0
  184. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/link.ts +140 -0
  185. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-builder-markdown.ts +113 -0
  186. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  187. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +36 -0
  188. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/check-redirect.ts +30 -0
  189. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/json-ld.tsx +522 -0
  190. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +189 -0
  191. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/rss.ts +96 -0
  192. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/sitemap.ts +58 -0
  193. package/src/modules/features/cms/sanity-pagebuilder-cache/files/proxy.ts +90 -0
  194. package/src/modules/features/env/dotenvx/files/vercelignore +5 -0
  195. package/src/templates/next-default/.env.example +2 -2
  196. package/src/templates/next-default/.github/workflows/ci.yml +1 -1
  197. package/src/templates/next-default/.vscode/settings.json +0 -2
  198. package/src/templates/next-default/components/ui/image/index.tsx +2 -4
  199. package/src/templates/next-default/knip.json +1 -1
  200. package/src/templates/next-default/lefthook.yml +1 -1
  201. package/src/templates/next-default/lib/utils/README.md +18 -5
  202. package/src/templates/next-default/lib/utils/image-sizes.ts +72 -0
  203. package/src/templates/next-default/lib/utils/merge-refs.ts +56 -0
  204. package/src/templates/next-default/lib/utils/strings.ts +1 -51
  205. package/src/templates/next-default/next.config.ts +2 -3
  206. package/src/templates/next-default/package.json +2 -5
  207. package/src/templates/next-experiments/.env.example +2 -2
  208. package/src/templates/next-experiments/.github/workflows/ci.yml +1 -1
  209. package/src/templates/next-experiments/.vscode/settings.json +0 -2
  210. package/src/templates/next-experiments/app/page.tsx +1 -1
  211. package/src/templates/next-experiments/components/ui/image/index.tsx +2 -4
  212. package/src/templates/next-experiments/knip.json +1 -1
  213. package/src/templates/next-experiments/lefthook.yml +1 -1
  214. package/src/templates/next-experiments/lib/utils/README.md +18 -5
  215. package/src/templates/next-experiments/lib/utils/image-sizes.ts +72 -0
  216. package/src/templates/next-experiments/lib/utils/merge-refs.ts +56 -0
  217. package/src/templates/next-experiments/lib/utils/strings.ts +1 -51
  218. package/src/templates/next-experiments/next.config.ts +2 -3
  219. package/src/templates/next-experiments/package.json +2 -5
  220. package/src/templates/next-pagebuilder/.env.example +1 -0
  221. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +1 -1
  222. package/src/templates/next-pagebuilder/.vscode/settings.json +0 -2
  223. package/src/templates/next-pagebuilder/README.md +64 -0
  224. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +3 -1
  225. package/src/templates/next-pagebuilder/biome.json +2 -1
  226. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +3 -6
  227. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -2
  228. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +3 -8
  229. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +23 -40
  230. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +88 -58
  231. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/skeleton.tsx +4 -16
  232. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/types.ts +14 -1
  233. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +16 -0
  234. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +21 -7
  235. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -0
  236. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +2 -1
  237. package/src/templates/next-pagebuilder/knip.json +3 -2
  238. package/src/templates/next-pagebuilder/lefthook.yml +1 -1
  239. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +1221 -977
  240. package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +72 -0
  241. package/src/templates/next-pagebuilder/lib/utils/merge-refs.ts +56 -0
  242. package/src/templates/next-pagebuilder/next.config.ts +2 -3
  243. package/src/templates/next-pagebuilder/package.json +4 -6
  244. package/src/templates/next-pagebuilder-cache/.biome/plugins/README.md +21 -0
  245. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-anchor-element.grit +12 -0
  246. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-relative-parent-imports.grit +10 -0
  247. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  248. package/src/templates/next-pagebuilder-cache/.env.example +40 -0
  249. package/src/templates/next-pagebuilder-cache/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  250. package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +105 -0
  251. package/src/templates/next-pagebuilder-cache/.vscode/extensions.json +19 -0
  252. package/src/templates/next-pagebuilder-cache/.vscode/settings.json +103 -0
  253. package/src/templates/next-pagebuilder-cache/.zed/settings.json +47 -0
  254. package/src/templates/next-pagebuilder-cache/README.md +212 -0
  255. package/src/templates/next-pagebuilder-cache/_gitignore +65 -0
  256. package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +124 -0
  257. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +28 -0
  258. package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +53 -0
  259. package/src/templates/next-pagebuilder-cache/app/favicon.ico +0 -0
  260. package/src/templates/next-pagebuilder-cache/app/layout.tsx +83 -0
  261. package/src/templates/next-pagebuilder-cache/app/robots.ts +13 -0
  262. package/src/templates/next-pagebuilder-cache/app/sitemap.xml/route.ts +84 -0
  263. package/src/templates/next-pagebuilder-cache/biome.json +257 -0
  264. package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +127 -0
  265. package/src/templates/next-pagebuilder-cache/components/layout/header/components/cta-button.tsx +28 -0
  266. package/src/templates/next-pagebuilder-cache/components/layout/header/components/mega-menu-panel.tsx +90 -0
  267. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-item-renderer.tsx +98 -0
  268. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-leaf-item.tsx +33 -0
  269. package/src/templates/next-pagebuilder-cache/components/layout/header/components/types.ts +7 -0
  270. package/src/templates/next-pagebuilder-cache/components/layout/header/header-client.tsx +110 -0
  271. package/src/templates/next-pagebuilder-cache/components/layout/header/index.tsx +32 -0
  272. package/src/templates/next-pagebuilder-cache/components/layout/json-ld/index.tsx +79 -0
  273. package/src/templates/next-pagebuilder-cache/components/layout/wrapper/index.tsx +33 -0
  274. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +78 -0
  275. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +27 -0
  276. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-card.tsx +66 -0
  277. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-filters.tsx +76 -0
  278. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-grid.tsx +40 -0
  279. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  280. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +306 -0
  281. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/skeleton.tsx +41 -0
  282. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/types.ts +32 -0
  283. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +17 -0
  284. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +169 -0
  285. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +17 -0
  286. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +16 -0
  287. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +71 -0
  288. package/src/templates/next-pagebuilder-cache/components/page-builder/types.ts +25 -0
  289. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +147 -0
  290. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +216 -0
  291. package/src/templates/next-pagebuilder-cache/components/ui/link/index.tsx +152 -0
  292. package/src/templates/next-pagebuilder-cache/components.json +24 -0
  293. package/src/templates/next-pagebuilder-cache/knip.json +35 -0
  294. package/src/templates/next-pagebuilder-cache/lefthook.yml +25 -0
  295. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +1588 -0
  296. package/src/templates/next-pagebuilder-cache/lib/scripts/setup-ci-secrets.ts +117 -0
  297. package/src/templates/next-pagebuilder-cache/lib/styles/cn.ts +5 -0
  298. package/src/templates/next-pagebuilder-cache/lib/styles/global.css +69 -0
  299. package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +17 -0
  300. package/src/templates/next-pagebuilder-cache/lib/utils/format-date.ts +8 -0
  301. package/src/templates/next-pagebuilder-cache/lib/utils/slug-tag.ts +6 -0
  302. package/src/templates/next-pagebuilder-cache/next.config.ts +176 -0
  303. package/src/templates/next-pagebuilder-cache/package.json +62 -0
  304. package/src/templates/next-pagebuilder-cache/postcss.config.mjs +65 -0
  305. package/src/templates/next-pagebuilder-cache/svg.d.ts +6 -0
  306. package/src/templates/next-pagebuilder-cache/tsconfig.json +38 -0
  307. package/src/templates/next-webgl/.env.example +2 -2
  308. package/src/templates/next-webgl/.github/workflows/ci.yml +1 -1
  309. package/src/templates/next-webgl/.vscode/settings.json +0 -2
  310. package/src/templates/next-webgl/components/ui/image/index.tsx +2 -4
  311. package/src/templates/next-webgl/knip.json +1 -1
  312. package/src/templates/next-webgl/lefthook.yml +1 -1
  313. package/src/templates/next-webgl/lib/utils/README.md +18 -5
  314. package/src/templates/next-webgl/lib/utils/image-sizes.ts +72 -0
  315. package/src/templates/next-webgl/lib/utils/merge-refs.ts +56 -0
  316. package/src/templates/next-webgl/lib/utils/strings.ts +1 -51
  317. package/src/templates/next-webgl/next.config.ts +2 -3
  318. package/src/templates/next-webgl/package.json +4 -7
  319. package/src/templates/next-experiments/tsconfig.tsbuildinfo +0 -1
  320. package/src/templates/next-pagebuilder/app/actions/refresh.ts +0 -5
  321. package/src/templates/next-webgl/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,334 @@
1
+ import { from } from "rxjs";
2
+ import { map } from "rxjs/operators";
3
+ import type {
4
+ ComponentBuilder,
5
+ StructureBuilder,
6
+ StructureResolver,
7
+ StructureResolverContext,
8
+ } from "sanity/structure";
9
+ import { apiVersion } from "./env";
10
+ import {
11
+ authorIcon,
12
+ blogCategoryIcon,
13
+ componentsFolderIcon,
14
+ contentFolderIcon,
15
+ contentIcon,
16
+ descriptionIcon,
17
+ gatedContentIcon,
18
+ heroIcon,
19
+ layoutFolderIcon,
20
+ pageFolderIcon,
21
+ pageIcon,
22
+ pagesFolderIcon,
23
+ redirectIcon,
24
+ } from "./icons";
25
+ import { PageTreePane } from "./page-tree-pane";
26
+ import { PINNED_PAGE_ENTRIES } from "./pinned-pages";
27
+ import { singletonComponents, singletonLayout } from "./singletons";
28
+
29
+ const PAGE_TREE_ITEM_ID_PREFIX = {
30
+ page: "page-tree-",
31
+ pageFolder: "page-folder-tree-",
32
+ } as const;
33
+
34
+ const normalizeDocumentId = (documentId: string) =>
35
+ documentId.replace(/^drafts\./, "");
36
+
37
+ export const getDocumentIdFromPageTreeItemId = (itemId: string) =>
38
+ normalizeDocumentId(
39
+ itemId
40
+ .replace(PAGE_TREE_ITEM_ID_PREFIX.page, "")
41
+ .replace(PAGE_TREE_ITEM_ID_PREFIX.pageFolder, ""),
42
+ );
43
+
44
+ const getPageTreeTemplateIds = (folderId: string | null) =>
45
+ folderId === null
46
+ ? ["page", "pageFolder"]
47
+ : ["page-with-folder", "page-folder-with-parent"];
48
+
49
+ const buildPageTreeCreateChild = (
50
+ S: StructureBuilder,
51
+ documentId: string,
52
+ folderId: string | null,
53
+ templateId: string,
54
+ ) => {
55
+ if (templateId === "page") {
56
+ return S.documentWithInitialValueTemplate("page").documentId(documentId);
57
+ }
58
+
59
+ if (templateId === "pageFolder") {
60
+ return S.documentWithInitialValueTemplate("pageFolder").documentId(
61
+ documentId,
62
+ );
63
+ }
64
+
65
+ if (templateId === "page-with-folder") {
66
+ return S.documentWithInitialValueTemplate("page-with-folder", {
67
+ pageFolderId: folderId,
68
+ }).documentId(documentId);
69
+ }
70
+
71
+ if (templateId === "page-folder-with-parent") {
72
+ return S.documentWithInitialValueTemplate("page-folder-with-parent", {
73
+ parentFolderId: folderId,
74
+ }).documentId(documentId);
75
+ }
76
+
77
+ return undefined;
78
+ };
79
+
80
+ const getPageTreeClient = (context: StructureResolverContext) =>
81
+ context
82
+ .getClient({ apiVersion })
83
+ .withConfig({ perspective: context.perspectiveStack });
84
+
85
+ function buildPageTreeComponent(
86
+ S: StructureBuilder,
87
+ context: StructureResolverContext,
88
+ folderId: string | null,
89
+ title: string,
90
+ ): ComponentBuilder {
91
+ const allowedTemplateIds = getPageTreeTemplateIds(folderId);
92
+
93
+ const folderMenuItems =
94
+ folderId === null
95
+ ? []
96
+ : [
97
+ S.menuItem()
98
+ .title("Folder Settings")
99
+ .icon(pageFolderIcon)
100
+ .intent({
101
+ type: "edit",
102
+ params: { id: folderId, type: "pageFolder" },
103
+ }),
104
+ ];
105
+
106
+ const createMenuItems =
107
+ folderId === null
108
+ ? [
109
+ S.menuItem()
110
+ .title("Create Page")
111
+ .icon(pageIcon)
112
+ .intent({
113
+ type: "create",
114
+ params: { type: "page", template: "page" },
115
+ }),
116
+ S.menuItem()
117
+ .title("Create Folder")
118
+ .icon(pageFolderIcon)
119
+ .intent({
120
+ type: "create",
121
+ params: { type: "pageFolder", template: "pageFolder" },
122
+ }),
123
+ ]
124
+ : [
125
+ S.menuItem()
126
+ .title("Create Page")
127
+ .icon(pageIcon)
128
+ .intent({
129
+ type: "create",
130
+ params: [
131
+ { type: "page", template: "page-with-folder" },
132
+ { pageFolderId: folderId },
133
+ ],
134
+ }),
135
+ S.menuItem()
136
+ .title("Create Folder")
137
+ .icon(pageFolderIcon)
138
+ .intent({
139
+ type: "create",
140
+ params: [
141
+ { type: "pageFolder", template: "page-folder-with-parent" },
142
+ { parentFolderId: folderId },
143
+ ],
144
+ }),
145
+ ];
146
+
147
+ return S.component()
148
+ .id(`page-tree-${folderId ?? "root"}`)
149
+ .title(title)
150
+ .component(PageTreePane)
151
+ .options({ folderId })
152
+ .canHandleIntent((intentName, params) => {
153
+ if (intentName === "create") {
154
+ return (
155
+ typeof params.template === "string" &&
156
+ allowedTemplateIds.includes(params.template)
157
+ );
158
+ }
159
+
160
+ if (intentName === "edit") {
161
+ const id =
162
+ typeof params.id === "string" ? normalizeDocumentId(params.id) : null;
163
+
164
+ // Pinned pages live at the root pane only.
165
+ if (
166
+ folderId === null &&
167
+ id !== null &&
168
+ params.type === "page" &&
169
+ PINNED_PAGE_ENTRIES.some((entry) => entry.id === id)
170
+ ) {
171
+ return true;
172
+ }
173
+
174
+ return (
175
+ id !== null &&
176
+ folderId !== null &&
177
+ id === normalizeDocumentId(folderId) &&
178
+ params.type === "pageFolder"
179
+ );
180
+ }
181
+
182
+ return false;
183
+ })
184
+ .child((childId, childOptions) => {
185
+ const normalizedId = getDocumentIdFromPageTreeItemId(childId);
186
+
187
+ // Pinned pages open by their raw id; seed via template so a
188
+ // not-yet-created pinned page gets its title/slug.
189
+ const pinnedEntry = PINNED_PAGE_ENTRIES.find(
190
+ (entry) => entry.id === childId,
191
+ );
192
+ if (pinnedEntry) {
193
+ return S.documentWithInitialValueTemplate(pinnedEntry.id).documentId(
194
+ pinnedEntry.id,
195
+ );
196
+ }
197
+
198
+ if (childId.startsWith(PAGE_TREE_ITEM_ID_PREFIX.page)) {
199
+ return S.document().schemaType("page").documentId(normalizedId);
200
+ }
201
+
202
+ if (childId.startsWith(PAGE_TREE_ITEM_ID_PREFIX.pageFolder)) {
203
+ // Fetch the folder title so the recursive pane shows the right header.
204
+ return from(
205
+ getPageTreeClient(context).fetch<{ title: string | null } | null>(
206
+ `*[_id == $id || _id == "drafts." + $id][0]{ title }`,
207
+ { id: normalizedId },
208
+ ),
209
+ ).pipe(
210
+ map((folder) =>
211
+ buildPageTreeComponent(
212
+ S,
213
+ context,
214
+ normalizedId,
215
+ folder?.title ?? "Untitled Folder",
216
+ ),
217
+ ),
218
+ );
219
+ }
220
+
221
+ // Current folder settings (edit intent for the folder itself).
222
+ if (folderId !== null && normalizedId === normalizeDocumentId(folderId)) {
223
+ return S.document().schemaType("pageFolder").documentId(normalizedId);
224
+ }
225
+
226
+ // Template-based document creation.
227
+ const templateId =
228
+ typeof childOptions.params?.template === "string"
229
+ ? childOptions.params.template
230
+ : null;
231
+
232
+ if (templateId) {
233
+ return buildPageTreeCreateChild(S, normalizedId, folderId, templateId);
234
+ }
235
+
236
+ return undefined;
237
+ })
238
+ .menuItems([...folderMenuItems, ...createMenuItems]);
239
+ }
240
+
241
+ // https://www.sanity.io/docs/structure-builder-cheat-sheet
242
+ export const structure: StructureResolver = (S, context) =>
243
+ S.list()
244
+ .title("Content")
245
+ .items([
246
+ // GROUP 1: Pages -- root page tree with inline search/filter
247
+ S.listItem()
248
+ .title("Pages")
249
+ .icon(pagesFolderIcon)
250
+ .child(() => buildPageTreeComponent(S, context, null, "Pages")),
251
+
252
+ S.divider(),
253
+
254
+ // GROUP 2: Content -- authors, blog categories
255
+ S.listItem()
256
+ .title("Content")
257
+ .icon(contentFolderIcon)
258
+ .child(
259
+ S.list()
260
+ .title("Content")
261
+ .items([
262
+ S.documentTypeListItem("author")
263
+ .title("Authors")
264
+ .icon(authorIcon),
265
+ S.documentTypeListItem("blogCategory")
266
+ .title("Blog Categories")
267
+ .icon(blogCategoryIcon),
268
+ ]),
269
+ ),
270
+
271
+ S.divider(),
272
+
273
+ // GROUP 3: Components -- singleton and reusable components
274
+ S.listItem()
275
+ .title("Components")
276
+ .icon(componentsFolderIcon)
277
+ .child(
278
+ S.list()
279
+ .title("Components")
280
+ .items([
281
+ S.divider().title("Singleton Components"),
282
+ ...singletonComponents.map((item) =>
283
+ S.listItem()
284
+ .title(item.title)
285
+ .id(item.documentId)
286
+ .icon(item.icon)
287
+ .child(
288
+ S.document()
289
+ .schemaType(item.schemaType)
290
+ .documentId(item.documentId),
291
+ ),
292
+ ),
293
+ S.divider().title("Reusable Components"),
294
+ S.documentTypeListItem("hero").title("Headings").icon(heroIcon),
295
+ S.documentTypeListItem("description")
296
+ .title("Descriptions")
297
+ .icon(descriptionIcon),
298
+ S.documentTypeListItem("gatedContent")
299
+ .title("Gated Content")
300
+ .icon(gatedContentIcon),
301
+ S.documentTypeListItem("blogContent")
302
+ .title("Blog Content")
303
+ .icon(contentIcon),
304
+ ]),
305
+ ),
306
+
307
+ S.divider(),
308
+
309
+ // GROUP 4: Layout -- global layout singletons + managed collections
310
+ S.listItem()
311
+ .title("Layout")
312
+ .icon(layoutFolderIcon)
313
+ .child(
314
+ S.list()
315
+ .title("Layout")
316
+ .items([
317
+ ...singletonLayout.map((item) =>
318
+ S.listItem()
319
+ .title(item.title)
320
+ .id(item.documentId)
321
+ .icon(item.icon)
322
+ .child(
323
+ S.document()
324
+ .schemaType(item.schemaType)
325
+ .documentId(item.documentId),
326
+ ),
327
+ ),
328
+ S.divider(),
329
+ S.documentTypeListItem("redirect")
330
+ .title("Redirects")
331
+ .icon(redirectIcon),
332
+ ]),
333
+ ),
334
+ ]);
@@ -0,0 +1,89 @@
1
+ // Cache tags shared by the `'use cache'` fetchers and the publish webhook.
2
+ // Dependency-free on purpose: the modules owning these documents pull in the
3
+ // Studio UI, and the webhook can't.
4
+
5
+ // Layout singletons. Each tag is also its document `_type`, which is what lets
6
+ // the webhook bust them without a lookup.
7
+ export const SANITY_NAVBAR_TAG = "navbar";
8
+ export const SANITY_FOOTER_TAG = "footer";
9
+ export const SANITY_COMPANY_DATA_TAG = "companyData";
10
+ export const SANITY_STRUCTURED_DATA_TAG = "structuredData";
11
+
12
+ export const SANITY_LAYOUT_TAGS = [
13
+ SANITY_NAVBAR_TAG,
14
+ SANITY_FOOTER_TAG,
15
+ SANITY_COMPANY_DATA_TAG,
16
+ SANITY_STRUCTURED_DATA_TAG,
17
+ ] as const;
18
+
19
+ export const SANITY_REDIRECTS_TAG = "redirect";
20
+
21
+ // Tags whose name equals the document `_type` that invalidates them.
22
+ export const SELF_TAGGED_TYPES = new Set<string>([
23
+ ...SANITY_LAYOUT_TAGS,
24
+ SANITY_REDIRECTS_TAG,
25
+ ]);
26
+
27
+ // Crawler routes: broader than any per-slug tag.
28
+ export const SITEMAP_TAG = "sitemap";
29
+ export const FEED_TAG = "feed";
30
+
31
+ // The content-collection block caches each window of results separately.
32
+ export const BLOG_CATEGORIES_TAG = "blog-categories";
33
+ export const BLOG_ARTICLES_TAG = "blog-articles";
34
+ export const BLOG_ARTICLES_COUNT_TAG = "blog-articles-count";
35
+
36
+ export const BLOG_COLLECTION_TAGS = [
37
+ BLOG_CATEGORIES_TAG,
38
+ BLOG_ARTICLES_TAG,
39
+ BLOG_ARTICLES_COUNT_TAG,
40
+ ] as const;
41
+
42
+ // Publishing a `page` revalidates its own path; nothing else reaches the cached
43
+ // collection windows.
44
+ export const BLOG_COLLECTION_TYPES = new Set<string>([
45
+ "page",
46
+ "blogContent",
47
+ "blogCategory",
48
+ "author",
49
+ ]);
50
+
51
+ // Tags whose cached query reads a *different* document type. next-sanity's own
52
+ // dependency tags are opaque Live API hashes only <SanityLive /> can expire, and
53
+ // only with a browser connected — so the webhook needs this spelled out.
54
+ export const CROSS_DOC_TAG_DEPENDENCIES: Record<string, readonly string[]> = {
55
+ // Redundant while LAYOUT_QUERY is merged; kept so splitting it can't lose this.
56
+ companyData: [SANITY_STRUCTURED_DATA_TAG],
57
+ // Nav links deref pages; sitemap and feed move whenever any page does.
58
+ page: [SANITY_NAVBAR_TAG, SITEMAP_TAG, FEED_TAG],
59
+ // ...plus the collections, which decide membership on the folder slug, so a
60
+ // rename changes which pages are articles at all.
61
+ pageFolder: [
62
+ SANITY_NAVBAR_TAG,
63
+ ...BLOG_COLLECTION_TAGS,
64
+ SITEMAP_TAG,
65
+ FEED_TAG,
66
+ ],
67
+ // The feed derefs the article's block, then its author and categories.
68
+ blogContent: [FEED_TAG],
69
+ author: [FEED_TAG],
70
+ blogCategory: [FEED_TAG],
71
+ };
72
+
73
+ // Types a page inlines by reference, so publishing one goes stale on every page
74
+ // pointing at it. The route unions this with the page-builder block types.
75
+ export const REFERENCEABLE_TYPES = new Set<string>([
76
+ "page",
77
+ "author",
78
+ "blogCategory",
79
+ ]);
80
+
81
+ // Every fixed tag, for the `purgeAll` hatch. Per-slug tags are absent because
82
+ // enumerating them needs a query; the route does that itself.
83
+ export const PURGEABLE_TAGS = [
84
+ ...SANITY_LAYOUT_TAGS,
85
+ SANITY_REDIRECTS_TAG,
86
+ ...BLOG_COLLECTION_TAGS,
87
+ SITEMAP_TAG,
88
+ FEED_TAG,
89
+ ];
@@ -0,0 +1,8 @@
1
+ import { createImageUrlBuilder } from "@sanity/image-url";
2
+ import { dataset, projectId } from "../env";
3
+
4
+ const builder = createImageUrlBuilder({ projectId, dataset });
5
+
6
+ export const urlForImage = (source: Parameters<typeof builder.image>[0]) => {
7
+ return builder.image(source);
8
+ };
@@ -0,0 +1,140 @@
1
+ import { getPinnedPageById } from "@/lib/integrations/sanity/pinned-pages";
2
+
3
+ type PageReference = {
4
+ _id?: string;
5
+ _ref?: string;
6
+ _type?: string;
7
+ slug?: { current?: string | null } | null;
8
+ resolvedSlug?: string | null;
9
+ title?: string | null;
10
+ };
11
+
12
+ type PageReferenceLink = {
13
+ _type: "pageReference";
14
+ page?: PageReference | null;
15
+ text?: string | null;
16
+ openInNewTab?: boolean | null;
17
+ };
18
+
19
+ type ExternalLinkItem = {
20
+ _type: "externalLink";
21
+ href?: string | null;
22
+ text?: string | null;
23
+ openInNewTab?: boolean | null;
24
+ };
25
+
26
+ type LinkItem = PageReferenceLink | ExternalLinkItem;
27
+
28
+ // Backward-compatible shape (old data without _type)
29
+ type LegacyLink = {
30
+ page?: PageReference | null;
31
+ href?: string | null;
32
+ text?: string | null;
33
+ openInNewTab?: boolean | null;
34
+ };
35
+
36
+ type LinkLike = LinkItem | LegacyLink;
37
+
38
+ type InternalReference = {
39
+ _id?: string;
40
+ _type: string;
41
+ slug?: { current?: string | null } | null;
42
+ resolvedSlug?: string | null;
43
+ title?: string;
44
+ };
45
+
46
+ /**
47
+ * Resolves a link object to a URL string.
48
+ *
49
+ * Supports both the new discriminated union shape (_type: "pageReference" | "externalLink")
50
+ * and the legacy flat shape (page + href without _type).
51
+ */
52
+ export const urlForReference = (link: LinkLike | null): string => {
53
+ if (!link) return "#";
54
+
55
+ // New discriminated union
56
+ if ("_type" in link && link._type === "pageReference") {
57
+ if (
58
+ link.page &&
59
+ typeof link.page === "object" &&
60
+ ("slug" in link.page || "_id" in link.page)
61
+ ) {
62
+ const ref = link.page as InternalReference;
63
+ return resolveDocumentUrl(
64
+ ref._type,
65
+ ref.resolvedSlug ?? undefined,
66
+ ref.slug?.current ?? undefined,
67
+ ref._id,
68
+ );
69
+ }
70
+ return "#";
71
+ }
72
+
73
+ if ("_type" in link && link._type === "externalLink") {
74
+ return link.href || "#";
75
+ }
76
+
77
+ // Legacy fallback (flat shape without _type)
78
+ if (
79
+ "page" in link &&
80
+ link.page &&
81
+ typeof link.page === "object" &&
82
+ ("slug" in link.page || "_id" in link.page)
83
+ ) {
84
+ const ref = link.page as InternalReference;
85
+ return resolveDocumentUrl(
86
+ ref._type,
87
+ ref.resolvedSlug ?? undefined,
88
+ ref.slug?.current ?? undefined,
89
+ ref._id,
90
+ );
91
+ }
92
+
93
+ if ("href" in link && link.href) {
94
+ return link.href;
95
+ }
96
+
97
+ return "#";
98
+ };
99
+
100
+ function resolveDocumentUrl(
101
+ documentType: string,
102
+ resolvedSlug?: string,
103
+ slug?: string,
104
+ documentId?: string,
105
+ ): string {
106
+ if (documentType === "page") {
107
+ const pinned = getPinnedPageById(documentId);
108
+ if (pinned) return pinned.path;
109
+ if (resolvedSlug) return `/${resolvedSlug}`;
110
+ if (slug) return `/${slug}`;
111
+ return "#";
112
+ }
113
+
114
+ if (!slug) return "#";
115
+
116
+ console.warn("Unknown document type for URL resolution:", documentType);
117
+ return "#";
118
+ }
119
+
120
+ /**
121
+ * Returns href, target, and rel attributes for a link object.
122
+ *
123
+ * External detection: externalLink type, or URLs starting with "http".
124
+ */
125
+ export const getLinkAttributes = (link: LinkLike | null) => {
126
+ if (!link) return { href: "#", target: undefined, rel: undefined };
127
+
128
+ const href = urlForReference(link);
129
+ const isExternal =
130
+ ("_type" in link && link._type === "externalLink") ||
131
+ href.startsWith("http");
132
+
133
+ const shouldOpenNewTab = link.openInNewTab || isExternal;
134
+
135
+ return {
136
+ href,
137
+ target: shouldOpenNewTab ? "_blank" : undefined,
138
+ rel: shouldOpenNewTab ? "noopener noreferrer" : undefined,
139
+ };
140
+ };
@@ -0,0 +1,113 @@
1
+ import { portableTextToMarkdown } from "@portabletext/markdown";
2
+ import type { PAGE_QUERY_RESULT } from "../sanity.types";
3
+
4
+ const markdownOptions = {
5
+ types: {
6
+ table: ({ value }: { value: { rows?: Array<{ cells?: string[] }> } }) => {
7
+ const rows = value?.rows;
8
+ if (!rows?.length) return "";
9
+ return rows
10
+ .map((row) => `| ${(row.cells ?? []).join(" | ")} |`)
11
+ .join("\n");
12
+ },
13
+ },
14
+ } satisfies Parameters<typeof portableTextToMarkdown>[1];
15
+
16
+ type PageBuilderBlock = NonNullable<
17
+ NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]
18
+ >[number];
19
+
20
+ export const renderPageBuilderMarkdown = (
21
+ blocks: PageBuilderBlock[] | null | undefined,
22
+ ) => {
23
+ if (!blocks?.length) return "";
24
+
25
+ return blocks
26
+ .flatMap((block) => {
27
+ switch (block._type) {
28
+ case "hero":
29
+ return block.headline ? [`## ${block.headline}`, ""] : [];
30
+ case "description":
31
+ return block.description ? [block.description, ""] : [];
32
+ case "richTextBlock":
33
+ return block.content
34
+ ? [
35
+ portableTextToMarkdown(
36
+ block.content as Parameters<
37
+ typeof portableTextToMarkdown
38
+ >[0],
39
+ markdownOptions,
40
+ ),
41
+ "",
42
+ ]
43
+ : [];
44
+ case "gatedContent": {
45
+ // The gated payload is withheld from the page query, so it is not
46
+ // available here either — only the teaser is emitted, matching what
47
+ // the page ships before the gate is unlocked.
48
+ const lines: string[] = [];
49
+ if (block.teaserHeading) lines.push(`## ${block.teaserHeading}`, "");
50
+ if (block.teaserBody) {
51
+ lines.push(
52
+ portableTextToMarkdown(
53
+ block.teaserBody as Parameters<
54
+ typeof portableTextToMarkdown
55
+ >[0],
56
+ markdownOptions,
57
+ ),
58
+ "",
59
+ );
60
+ }
61
+ lines.push("> Gated content — unlock on the page to view.", "");
62
+ return lines;
63
+ }
64
+ case "blogContent": {
65
+ const lines: (string | null)[] = [];
66
+ const categories =
67
+ "categories" in block ? block.categories : undefined;
68
+ const relatedPosts =
69
+ "relatedPosts" in block ? block.relatedPosts : undefined;
70
+
71
+ if (categories?.length) {
72
+ lines.push(
73
+ categories
74
+ .map((c) => c.title)
75
+ .filter(Boolean)
76
+ .join(", "),
77
+ );
78
+ }
79
+ if (block.author?.name) {
80
+ const authorParts = [block.author.name, block.author.role]
81
+ .filter(Boolean)
82
+ .join(", ");
83
+ lines.push(`**Author:** ${authorParts}`);
84
+ }
85
+ if (block.date) {
86
+ lines.push(`**Date:** ${block.date}`);
87
+ }
88
+ lines.push("");
89
+ if (block.body) {
90
+ lines.push(
91
+ portableTextToMarkdown(
92
+ block.body as Parameters<typeof portableTextToMarkdown>[0],
93
+ markdownOptions,
94
+ ),
95
+ );
96
+ }
97
+ if (relatedPosts?.length) {
98
+ lines.push("", "## Related Articles", "");
99
+ for (const post of relatedPosts) {
100
+ if (post.title && post.resolvedSlug) {
101
+ lines.push(`- [${post.title}](/${post.resolvedSlug}.md)`);
102
+ }
103
+ }
104
+ }
105
+ lines.push("");
106
+ return lines.filter((l) => l !== null);
107
+ }
108
+ default:
109
+ return [];
110
+ }
111
+ })
112
+ .join("\n");
113
+ };