bsmnt 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +19 -2
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/application/shared/node-version.js.map +1 -1
  5. package/dist/core/create/internal-ids.d.ts +3 -0
  6. package/dist/core/create/internal-ids.d.ts.map +1 -1
  7. package/dist/core/create/internal-ids.js +4 -0
  8. package/dist/core/create/internal-ids.js.map +1 -1
  9. package/dist/domain/agents.d.ts.map +1 -1
  10. package/dist/domain/animations.d.ts.map +1 -1
  11. package/dist/domain/cms.d.ts.map +1 -1
  12. package/dist/domain/forms.d.ts.map +1 -1
  13. package/dist/domain/hooks.d.ts.map +1 -1
  14. package/dist/domain/skills.d.ts.map +1 -1
  15. package/dist/domain/templates.d.ts +10 -0
  16. package/dist/domain/templates.d.ts.map +1 -1
  17. package/dist/domain/templates.js +5 -0
  18. package/dist/domain/templates.js.map +1 -1
  19. package/dist/index.js +18 -5
  20. package/dist/index.js.map +1 -1
  21. package/dist/infrastructure/agents/register-sanity-mcp.js +3 -8
  22. package/dist/infrastructure/agents/register-sanity-mcp.js.map +1 -1
  23. package/dist/infrastructure/agents/register-storybook-mcp.d.ts +18 -0
  24. package/dist/infrastructure/agents/register-storybook-mcp.d.ts.map +1 -0
  25. package/dist/infrastructure/agents/register-storybook-mcp.js +157 -0
  26. package/dist/infrastructure/agents/register-storybook-mcp.js.map +1 -0
  27. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  28. package/dist/infrastructure/create/lifecycle-registry.js +39 -1
  29. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  30. package/dist/infrastructure/create/merge-registry.d.ts.map +1 -1
  31. package/dist/infrastructure/create/merge-registry.js +4 -0
  32. package/dist/infrastructure/create/merge-registry.js.map +1 -1
  33. package/dist/infrastructure/create/sanity-auth.js.map +1 -1
  34. package/dist/infrastructure/create/setup-agent.d.ts +6 -1
  35. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  36. package/dist/infrastructure/create/setup-agent.js +23 -21
  37. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  38. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
  39. package/dist/infrastructure/create/setup-remote.js.map +1 -1
  40. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  41. package/dist/infrastructure/create/setup-sanity.js +12 -1
  42. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  43. package/dist/infrastructure/integrations/ensure-studio-headers.d.ts +6 -0
  44. package/dist/infrastructure/integrations/ensure-studio-headers.d.ts.map +1 -0
  45. package/dist/infrastructure/integrations/ensure-studio-headers.js +153 -0
  46. package/dist/infrastructure/integrations/ensure-studio-headers.js.map +1 -0
  47. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  48. package/dist/infrastructure/integrations/inject-integration.js +2 -0
  49. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  50. package/dist/infrastructure/update/notifier.js.map +1 -1
  51. package/dist/modules/features/cms/index.d.ts.map +1 -1
  52. package/dist/modules/features/cms/index.js +2 -0
  53. package/dist/modules/features/cms/index.js.map +1 -1
  54. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  55. package/dist/modules/features/cms/sanity/config.js +3 -1
  56. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  57. package/dist/modules/features/cms/sanity/index.d.ts.map +1 -1
  58. package/dist/modules/features/cms/sanity/index.js +10 -1
  59. package/dist/modules/features/cms/sanity/index.js.map +1 -1
  60. package/dist/modules/features/cms/sanity/mergers/check-integration.d.ts.map +1 -1
  61. package/dist/modules/features/cms/sanity/mergers/check-integration.js +15 -6
  62. package/dist/modules/features/cms/sanity/mergers/check-integration.js.map +1 -1
  63. package/dist/modules/features/cms/sanity/mergers/knip.d.ts.map +1 -1
  64. package/dist/modules/features/cms/sanity/mergers/knip.js +1 -0
  65. package/dist/modules/features/cms/sanity/mergers/knip.js.map +1 -1
  66. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  67. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts +12 -0
  68. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts.map +1 -0
  69. package/dist/modules/features/cms/sanity/mergers/package-json.js +19 -0
  70. package/dist/modules/features/cms/sanity/mergers/package-json.js.map +1 -0
  71. package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
  72. package/dist/modules/features/cms/sanity-pagebuilder/config.js +2 -1
  73. package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
  74. package/dist/modules/features/cms/sanity-pagebuilder/index.d.ts.map +1 -1
  75. package/dist/modules/features/cms/sanity-pagebuilder/index.js +7 -1
  76. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  77. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts +14 -0
  78. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -0
  79. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +50 -0
  80. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -0
  81. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts +3 -0
  82. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts.map +1 -0
  83. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js +55 -0
  84. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js.map +1 -0
  85. package/dist/modules/presets/index.d.ts.map +1 -1
  86. package/dist/modules/presets/index.js +2 -0
  87. package/dist/modules/presets/index.js.map +1 -1
  88. package/dist/modules/presets/next-pagebuilder-cache.d.ts +3 -0
  89. package/dist/modules/presets/next-pagebuilder-cache.d.ts.map +1 -0
  90. package/dist/modules/presets/next-pagebuilder-cache.js +10 -0
  91. package/dist/modules/presets/next-pagebuilder-cache.js.map +1 -0
  92. package/dist/paths.d.ts +6 -0
  93. package/dist/paths.d.ts.map +1 -1
  94. package/dist/paths.js +6 -0
  95. package/dist/paths.js.map +1 -1
  96. package/package.json +6 -3
  97. package/src/modules/features/cms/sanity/files/app/api/blog/[slug]/route.ts +4 -1
  98. package/src/modules/features/cms/sanity/files/app/sitemap.md/route.ts +2 -1
  99. package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +19 -5
  100. package/src/modules/features/cms/sanity/files/lib/integrations/check-integration.ts +7 -6
  101. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/README.md +32 -1
  102. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +1 -0
  103. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/env.ts +2 -2
  104. package/src/modules/features/cms/sanity/files/proxy.ts +9 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +12 -3
  106. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +172 -24
  107. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +14 -7
  108. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +8 -2
  109. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +16 -1
  110. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +1 -0
  111. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +2 -2
  112. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +15 -5
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +6 -5
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +8 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-field.tsx +13 -7
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-notices.tsx +125 -0
  117. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +2 -2
  118. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +22 -52
  119. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
  120. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +4 -2
  121. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/markdown-proxy.config.ts +1 -1
  122. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +7 -0
  123. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +70 -57
  124. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +119 -29
  125. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  126. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  127. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  128. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +2 -0
  129. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +35 -4
  130. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +12 -0
  131. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +56 -0
  132. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +12 -0
  133. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +11 -8
  134. package/src/modules/features/cms/sanity-pagebuilder/files/proxy.ts +9 -0
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/.knip.json +8 -0
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/[[...slug]]/route.ts +112 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/disable/route.ts +7 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/enable/route.ts +20 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +292 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/feed.xml/route.ts +66 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/sitemap.md/route.ts +132 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/layout.tsx +26 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/page.tsx +23 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/draft-mode-toggle.tsx +27 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +88 -0
  146. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/visual-editing.tsx +11 -0
  147. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +51 -0
  148. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/check-integration.ts +6 -0
  149. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/client.ts +37 -0
  150. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
  151. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  152. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +39 -0
  153. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  154. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  155. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-field.tsx +24 -0
  156. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  157. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-notices.tsx +125 -0
  158. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  159. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/env.ts +34 -0
  160. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +39 -0
  161. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/redirects.ts +22 -0
  162. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +81 -0
  163. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/live/index.tsx +140 -0
  164. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  165. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +144 -0
  166. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-category.ts +28 -0
  167. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-tree-pane.tsx +431 -0
  168. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  169. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +119 -0
  170. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +548 -0
  171. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  172. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  173. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +116 -0
  174. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +6 -0
  175. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  176. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  177. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
  178. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  179. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  180. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  181. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  182. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  183. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +134 -0
  184. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/company-data.ts +70 -0
  185. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  186. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  187. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +152 -0
  188. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  189. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  190. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  191. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +132 -0
  192. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  193. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +43 -0
  194. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  195. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +265 -0
  196. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  197. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  198. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +51 -0
  199. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +334 -0
  200. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +89 -0
  201. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/image.ts +8 -0
  202. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/link.ts +140 -0
  203. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-builder-markdown.ts +113 -0
  204. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  205. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +36 -0
  206. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/check-redirect.ts +30 -0
  207. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/json-ld.tsx +522 -0
  208. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +189 -0
  209. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/rss.ts +96 -0
  210. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/sitemap.ts +58 -0
  211. package/src/modules/features/cms/sanity-pagebuilder-cache/files/proxy.ts +90 -0
  212. package/src/modules/features/env/dotenvx/files/vercelignore +5 -0
  213. package/src/templates/next-default/.env.example +4 -2
  214. package/src/templates/next-default/.github/workflows/ci.yml +1 -1
  215. package/src/templates/next-default/.vscode/settings.json +0 -2
  216. package/src/templates/next-default/app/robots.ts +2 -3
  217. package/src/templates/next-default/components/ui/image/index.tsx +2 -4
  218. package/src/templates/next-default/knip.json +1 -1
  219. package/src/templates/next-default/lefthook.yml +1 -1
  220. package/src/templates/next-default/lib/utils/README.md +18 -5
  221. package/src/templates/next-default/lib/utils/image-sizes.ts +72 -0
  222. package/src/templates/next-default/lib/utils/merge-refs.ts +56 -0
  223. package/src/templates/next-default/lib/utils/strings.ts +1 -51
  224. package/src/templates/next-default/next.config.ts +38 -5
  225. package/src/templates/next-default/package.json +9 -12
  226. package/src/templates/next-experiments/.env.example +4 -2
  227. package/src/templates/next-experiments/.github/workflows/ci.yml +1 -1
  228. package/src/templates/next-experiments/.vscode/settings.json +0 -2
  229. package/src/templates/next-experiments/app/page.tsx +1 -1
  230. package/src/templates/next-experiments/app/robots.ts +2 -3
  231. package/src/templates/next-experiments/components/ui/image/index.tsx +2 -4
  232. package/src/templates/next-experiments/knip.json +1 -1
  233. package/src/templates/next-experiments/lefthook.yml +1 -1
  234. package/src/templates/next-experiments/lib/utils/README.md +18 -5
  235. package/src/templates/next-experiments/lib/utils/image-sizes.ts +72 -0
  236. package/src/templates/next-experiments/lib/utils/merge-refs.ts +56 -0
  237. package/src/templates/next-experiments/lib/utils/strings.ts +1 -51
  238. package/src/templates/next-experiments/next.config.ts +38 -5
  239. package/src/templates/next-experiments/package.json +9 -12
  240. package/src/templates/next-pagebuilder/.env.example +3 -0
  241. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +1 -1
  242. package/src/templates/next-pagebuilder/.vscode/settings.json +0 -2
  243. package/src/templates/next-pagebuilder/README.md +95 -0
  244. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +24 -9
  245. package/src/templates/next-pagebuilder/app/robots.ts +2 -3
  246. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +9 -4
  247. package/src/templates/next-pagebuilder/biome.json +2 -1
  248. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +3 -6
  249. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -2
  250. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +3 -8
  251. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +23 -40
  252. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +88 -58
  253. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/skeleton.tsx +41 -0
  254. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/types.ts +14 -1
  255. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +16 -0
  256. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +23 -3
  257. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -0
  258. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +2 -1
  259. package/src/templates/next-pagebuilder/knip.json +3 -2
  260. package/src/templates/next-pagebuilder/lefthook.yml +1 -1
  261. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +1221 -977
  262. package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +72 -0
  263. package/src/templates/next-pagebuilder/lib/utils/merge-refs.ts +56 -0
  264. package/src/templates/next-pagebuilder/next.config.ts +35 -5
  265. package/src/templates/next-pagebuilder/package.json +8 -10
  266. package/src/templates/next-pagebuilder-cache/.biome/plugins/README.md +21 -0
  267. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-anchor-element.grit +12 -0
  268. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-relative-parent-imports.grit +10 -0
  269. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  270. package/src/templates/next-pagebuilder-cache/.env.example +40 -0
  271. package/src/templates/next-pagebuilder-cache/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  272. package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +105 -0
  273. package/src/templates/next-pagebuilder-cache/.vscode/extensions.json +19 -0
  274. package/src/templates/next-pagebuilder-cache/.vscode/settings.json +103 -0
  275. package/src/templates/next-pagebuilder-cache/.zed/settings.json +47 -0
  276. package/src/templates/next-pagebuilder-cache/README.md +212 -0
  277. package/src/templates/next-pagebuilder-cache/_gitignore +65 -0
  278. package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +124 -0
  279. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +28 -0
  280. package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +53 -0
  281. package/src/templates/next-pagebuilder-cache/app/favicon.ico +0 -0
  282. package/src/templates/next-pagebuilder-cache/app/layout.tsx +83 -0
  283. package/src/templates/next-pagebuilder-cache/app/robots.ts +13 -0
  284. package/src/templates/next-pagebuilder-cache/app/sitemap.xml/route.ts +84 -0
  285. package/src/templates/next-pagebuilder-cache/biome.json +257 -0
  286. package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +127 -0
  287. package/src/templates/next-pagebuilder-cache/components/layout/header/components/cta-button.tsx +28 -0
  288. package/src/templates/next-pagebuilder-cache/components/layout/header/components/mega-menu-panel.tsx +90 -0
  289. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-item-renderer.tsx +98 -0
  290. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-leaf-item.tsx +33 -0
  291. package/src/templates/next-pagebuilder-cache/components/layout/header/components/types.ts +7 -0
  292. package/src/templates/next-pagebuilder-cache/components/layout/header/header-client.tsx +110 -0
  293. package/src/templates/next-pagebuilder-cache/components/layout/header/index.tsx +32 -0
  294. package/src/templates/next-pagebuilder-cache/components/layout/json-ld/index.tsx +79 -0
  295. package/src/templates/next-pagebuilder-cache/components/layout/wrapper/index.tsx +33 -0
  296. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +78 -0
  297. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +27 -0
  298. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-card.tsx +66 -0
  299. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-filters.tsx +76 -0
  300. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-grid.tsx +40 -0
  301. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  302. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +306 -0
  303. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/skeleton.tsx +41 -0
  304. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/types.ts +32 -0
  305. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +17 -0
  306. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +169 -0
  307. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +17 -0
  308. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +16 -0
  309. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +71 -0
  310. package/src/templates/next-pagebuilder-cache/components/page-builder/types.ts +25 -0
  311. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +147 -0
  312. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +216 -0
  313. package/src/templates/next-pagebuilder-cache/components/ui/link/index.tsx +152 -0
  314. package/src/templates/next-pagebuilder-cache/components.json +24 -0
  315. package/src/templates/next-pagebuilder-cache/knip.json +35 -0
  316. package/src/templates/next-pagebuilder-cache/lefthook.yml +25 -0
  317. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +1588 -0
  318. package/src/templates/next-pagebuilder-cache/lib/scripts/setup-ci-secrets.ts +117 -0
  319. package/src/templates/next-pagebuilder-cache/lib/styles/cn.ts +5 -0
  320. package/src/templates/next-pagebuilder-cache/lib/styles/global.css +69 -0
  321. package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +17 -0
  322. package/src/templates/next-pagebuilder-cache/lib/utils/format-date.ts +8 -0
  323. package/src/templates/next-pagebuilder-cache/lib/utils/slug-tag.ts +6 -0
  324. package/src/templates/next-pagebuilder-cache/next.config.ts +176 -0
  325. package/src/templates/next-pagebuilder-cache/package.json +62 -0
  326. package/src/templates/next-pagebuilder-cache/postcss.config.mjs +65 -0
  327. package/src/templates/next-pagebuilder-cache/svg.d.ts +6 -0
  328. package/src/templates/next-pagebuilder-cache/tsconfig.json +38 -0
  329. package/src/templates/next-webgl/.env.example +4 -2
  330. package/src/templates/next-webgl/.github/workflows/ci.yml +1 -1
  331. package/src/templates/next-webgl/.vscode/settings.json +0 -2
  332. package/src/templates/next-webgl/app/robots.ts +2 -3
  333. package/src/templates/next-webgl/components/ui/image/index.tsx +2 -4
  334. package/src/templates/next-webgl/knip.json +1 -1
  335. package/src/templates/next-webgl/lefthook.yml +1 -1
  336. package/src/templates/next-webgl/lib/utils/README.md +18 -5
  337. package/src/templates/next-webgl/lib/utils/image-sizes.ts +72 -0
  338. package/src/templates/next-webgl/lib/utils/merge-refs.ts +56 -0
  339. package/src/templates/next-webgl/lib/utils/strings.ts +1 -51
  340. package/src/templates/next-webgl/next.config.ts +38 -5
  341. package/src/templates/next-webgl/package.json +11 -14
  342. package/src/templates/next-experiments/tsconfig.tsbuildinfo +0 -1
  343. package/src/templates/next-pagebuilder/app/actions/refresh.ts +0 -5
  344. package/src/templates/next-webgl/tsconfig.tsbuildinfo +0 -1
@@ -22,8 +22,14 @@ const getPageDocument = async (slug: string | null, stega?: boolean) =>
22
22
  stega,
23
23
  })
24
24
 
25
- const toPath = (segments?: string[]) =>
26
- segments?.filter(Boolean).join("/") || null
25
+ const isValidSegment = (segment: string) =>
26
+ segment.length > 0 && segment !== "." && segment !== ".."
27
+
28
+ const toPath = (segments?: string[]) => {
29
+ if (!segments?.length) return null
30
+ if (!segments.every(isValidSegment)) return null
31
+ return segments.join("/")
32
+ }
27
33
 
28
34
  const getResolvedPage = async (
29
35
  path: string | null,
@@ -35,18 +41,21 @@ const getResolvedPage = async (
35
41
 
36
42
  type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
37
43
 
44
+ const HOMEPAGE_PARAM: { slug: string[] } = { slug: [] }
45
+
38
46
  export const generateStaticParams = async () => {
39
- if (!client) return [{ slug: [] }]
47
+ if (!client) return [HOMEPAGE_PARAM]
40
48
 
41
49
  const slugs =
42
50
  await client.fetch<ALL_PAGE_SLUGS_QUERY_RESULT>(ALL_PAGE_SLUGS_QUERY)
43
51
 
44
- if (!slugs?.length) return [{ slug: [] }]
45
-
46
- return (slugs ?? []).flatMap((page) => {
47
- if (!page.slug) return []
48
- return page.slug === "/" ? { slug: [] } : { slug: page.slug.split("/") }
49
- })
52
+ return [
53
+ HOMEPAGE_PARAM,
54
+ ...(slugs ?? []).flatMap((page) => {
55
+ if (!page.slug || page.slug === "/") return []
56
+ return { slug: page.slug.split("/") }
57
+ }),
58
+ ]
50
59
  }
51
60
 
52
61
  type Props = { params: Promise<{ slug?: string[] }> }
@@ -54,6 +63,9 @@ type Props = { params: Promise<{ slug?: string[] }> }
54
63
  export const generateMetadata = async ({ params }: Props) => {
55
64
  const { slug } = await params
56
65
  const path = toPath(slug)
66
+
67
+ if (slug?.length && path === null) return {}
68
+
57
69
  const page = await getResolvedPage(path, false)
58
70
 
59
71
  return page
@@ -70,6 +82,9 @@ export default async function CatchAllPage({
70
82
  const { slug } = await params
71
83
 
72
84
  const path = toPath(slug)
85
+
86
+ if (slug?.length && path === null) notFound()
87
+
73
88
  const page = await getResolvedPage(path)
74
89
 
75
90
  if (!page) {
@@ -1,6 +1,5 @@
1
1
  import type { MetadataRoute } from "next"
2
-
3
- const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"
2
+ import { getBaseUrl } from "@/lib/utils/base-url"
4
3
 
5
4
  export default function robots(): MetadataRoute.Robots {
6
5
  return {
@@ -9,6 +8,6 @@ export default function robots(): MetadataRoute.Robots {
9
8
  allow: "/",
10
9
  disallow: [],
11
10
  },
12
- sitemap: `${APP_BASE_URL}/sitemap.xml`,
11
+ sitemap: `${getBaseUrl()}/sitemap.xml`,
13
12
  }
14
13
  }
@@ -3,7 +3,9 @@ import { isSanityConfigured } from "@/lib/integrations/check-integration"
3
3
  import type { PAGE_SITEMAP_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
4
4
  import { getPageUrl, getSitemap, getSitemapBaseUrl } from "@/lib/utils/sitemap"
5
5
 
6
- export const dynamic = "force-dynamic"
6
+ // Not `force-dynamic`: per-request rendering meant a Sanity round trip per crawler
7
+ // hit. No `Cache-Control` either — a CDN copy is a second cache the webhook
8
+ // can't purge.
7
9
 
8
10
  type PageDoc = PAGE_SITEMAP_QUERY_RESULT[number]
9
11
 
@@ -51,8 +53,11 @@ const renderSitemap = (entries: SitemapEntry[]) =>
51
53
  "</urlset>",
52
54
  ].join("\n")
53
55
 
54
- const getSitemapResponse = (entries: SitemapEntry[]) =>
55
- new NextResponse(renderSitemap(entries))
56
+ const getSitemapResponse = (entries: SitemapEntry[], status = 200) =>
57
+ new NextResponse(renderSitemap(entries), {
58
+ status,
59
+ headers: { "Content-Type": "application/xml; charset=utf-8" },
60
+ })
56
61
 
57
62
  export const GET = async () => {
58
63
  const baseUrl = getSitemapBaseUrl()
@@ -75,6 +80,6 @@ export const GET = async () => {
75
80
  )
76
81
  } catch (error) {
77
82
  console.error("Error generating sitemap.xml:", error)
78
- return getSitemapResponse([homepageEntry])
83
+ return getSitemapResponse([homepageEntry], 503)
79
84
  }
80
85
  }
@@ -35,7 +35,8 @@
35
35
  "!**/*.md",
36
36
  "!**/*.mdx",
37
37
  "!**/tailwind.css",
38
- "!**/*.grit"
38
+ "!**/*.grit",
39
+ "!**/sanity.types.ts"
39
40
  ]
40
41
  },
41
42
  "formatter": {
@@ -3,8 +3,7 @@ import { SanityImage } from "@/components/ui/sanity-image"
3
3
  import {
4
4
  type CompanyData,
5
5
  type FooterData,
6
- getCompanyData,
7
- getFooterData,
6
+ getLayoutData,
8
7
  } from "@/lib/integrations/sanity/fetchers/layout"
9
8
 
10
9
  type FooterLinkGroup = NonNullable<FooterData["links"]>[number]
@@ -12,10 +11,8 @@ type FooterLinkItem = NonNullable<FooterLinkGroup["items"]>[number]
12
11
  type SocialLink = NonNullable<CompanyData["socialLinks"]>[number]
13
12
 
14
13
  export const Footer = async () => {
15
- const [footer, companyData] = await Promise.all([
16
- getFooterData(),
17
- getCompanyData(),
18
- ])
14
+ // Shares one request-scoped fetch with the header and json-ld.
15
+ const { footer, companyData } = await getLayoutData()
19
16
 
20
17
  const links = footer?.links ?? []
21
18
 
@@ -1,8 +1,9 @@
1
- import { getNavbarData } from "@/lib/integrations/sanity/fetchers/layout"
1
+ import { getLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
2
2
  import { HeaderClient } from "./header-client"
3
3
 
4
4
  export const Header = async () => {
5
- const navbar = await getNavbarData()
5
+ // Shares one request-scoped fetch with the footer and json-ld.
6
+ const { navbar } = await getLayoutData()
6
7
 
7
8
  return <HeaderClient data={navbar} />
8
9
  }
@@ -1,8 +1,5 @@
1
1
  import { stegaClean } from "next-sanity"
2
- import {
3
- getNavbarData,
4
- getStructuredData,
5
- } from "@/lib/integrations/sanity/fetchers/layout"
2
+ import { getLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
6
3
  import {
7
4
  generateOrganizationJsonLd,
8
5
  generateWebSiteJsonLd,
@@ -13,10 +10,8 @@ const nonEmpty = (value: unknown): string | undefined =>
13
10
  typeof value === "string" && value.trim() !== "" ? value : undefined
14
11
 
15
12
  export const JsonLd = async () => {
16
- const [structuredData, navbar] = await Promise.all([
17
- getStructuredData(),
18
- getNavbarData(),
19
- ])
13
+ // Shares one request-scoped fetch with the header and footer.
14
+ const { structuredData, navbar } = await getLayoutData()
20
15
 
21
16
  // Deep-clean stega artifacts before any value reaches the emitted markup.
22
17
  const data = stegaClean(structuredData)
@@ -1,29 +1,30 @@
1
- "use client"
2
-
3
- import { usePathname, useRouter, useSearchParams } from "next/navigation"
1
+ import { Link } from "@/components/ui/link"
4
2
  import { cn } from "@/lib/styles/cn"
5
3
  import type { ContentCategory } from "./types"
6
4
 
7
5
  type ContentFiltersProps = {
8
6
  categories: ContentCategory[]
9
7
  activeCategory: string
10
- totalCount: number
11
- filteredCount: number
8
+ count: number
9
+ /** Category slug → the page pinned to it. Categories without one are omitted. */
10
+ categoryHrefs: Record<string, string>
11
+ /** Where "All content" goes. */
12
+ indexHref: string
12
13
  }
13
14
 
14
15
  function FilterChip({
15
16
  active,
16
17
  label,
17
- onClick,
18
+ href,
18
19
  }: {
19
20
  active: boolean
20
21
  label: string
21
- onClick: () => void
22
+ href: string
22
23
  }) {
23
24
  return (
24
- <button
25
- type="button"
26
- onClick={onClick}
25
+ <Link
26
+ href={href}
27
+ aria-current={active ? "page" : undefined}
27
28
  className={cn(
28
29
  "rounded-full border px-4 py-2 text-sm transition-colors",
29
30
  active
@@ -32,61 +33,43 @@ function FilterChip({
32
33
  )}
33
34
  >
34
35
  {label}
35
- </button>
36
+ </Link>
36
37
  )
37
38
  }
38
39
 
39
40
  export function ContentFilters({
40
41
  categories,
41
42
  activeCategory,
42
- totalCount,
43
- filteredCount,
43
+ count,
44
+ categoryHrefs,
45
+ indexHref,
44
46
  }: ContentFiltersProps) {
45
- const pathname = usePathname()
46
- const router = useRouter()
47
- const searchParams = useSearchParams()
48
-
49
- const navigate = (category: string) => {
50
- const nextParams = new URLSearchParams(searchParams.toString())
51
-
52
- if (category) nextParams.set("category", category)
53
- else nextParams.delete("category")
54
-
55
- nextParams.delete("page")
56
-
57
- const href = nextParams.toString()
58
- ? `${pathname}?${nextParams.toString()}`
59
- : pathname
60
-
61
- // biome-ignore lint/suspicious/noExplicitAny: typed routes do not accept dynamic query string helpers cleanly
62
- router.push(href as any, { scroll: false })
63
- }
64
-
65
47
  return (
66
48
  <div className="flex flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-4 sm:p-5">
67
- <div className="flex flex-wrap gap-2">
49
+ <nav aria-label="Filter by category" className="flex flex-wrap gap-2">
68
50
  <FilterChip
69
51
  active={activeCategory === ""}
70
52
  label="All content"
71
- onClick={() => navigate("")}
53
+ href={indexHref}
72
54
  />
73
55
  {categories.map((category) => {
74
56
  const slug = category.slug?.current
75
- if (!slug) return null
57
+ const href = slug ? categoryHrefs[slug] : undefined
58
+ // A category with no page of its own has nowhere to link to.
59
+ if (!slug || !href) return null
76
60
 
77
61
  return (
78
62
  <FilterChip
79
63
  key={category._id}
80
64
  active={activeCategory === slug}
81
65
  label={category.title || "Untitled"}
82
- onClick={() => navigate(slug)}
66
+ href={href}
83
67
  />
84
68
  )
85
69
  })}
86
- </div>
70
+ </nav>
87
71
  <p className="text-current/60 text-sm">
88
- Showing {filteredCount} {filteredCount === 1 ? "item" : "items"}
89
- {filteredCount !== totalCount ? ` out of ${totalCount}` : ""}
72
+ Showing {count} {count === 1 ? "item" : "items"}
90
73
  </p>
91
74
  </div>
92
75
  )
@@ -1,23 +1,31 @@
1
+ import { Suspense } from "react"
1
2
  import { fetchSanity } from "@/lib/integrations/sanity/live"
2
3
  import {
3
4
  ALL_BLOG_CATEGORIES_QUERY,
4
5
  BLOG_ARTICLES_COUNT_QUERY,
5
6
  BLOG_ARTICLES_PAGINATED_QUERY,
7
+ COLLECTION_PAGES_QUERY,
6
8
  } from "@/lib/integrations/sanity/queries"
9
+ import { getPagePath } from "@/lib/utils/sitemap"
7
10
  import { ContentFilters } from "./content-filters"
8
11
  import { ContentGrid } from "./content-grid"
9
12
  import { ContentPaginationNav } from "./content-pagination-nav"
13
+ import { CollectionResultsSkeleton } from "./skeleton"
10
14
  import type {
15
+ CollectionPage,
11
16
  ContentCategory,
12
17
  ContentCollectionBlock,
13
18
  ContentCollectionSearchParams,
14
19
  ContentItemCard,
20
+ PinnedCollectionCategory,
15
21
  SearchParams,
16
22
  } from "./types"
17
23
 
18
24
  type ContentCollectionProps = {
19
25
  block: ContentCollectionBlock
20
26
  searchParams: ContentCollectionSearchParams
27
+ /** Category pages pin the collection to one category. */
28
+ pinnedCategory: PinnedCollectionCategory
21
29
  }
22
30
 
23
31
  const FEATURED_COUNT = 2
@@ -28,11 +36,10 @@ const normalizePageValue = (value: string | string[] | undefined) => {
28
36
  return Number.isFinite(page) && page > 0 ? Math.floor(page) : 1
29
37
  }
30
38
 
31
- const getCollectionParams = (searchParams: SearchParams) => ({
32
- currentPage: normalizePageValue(searchParams.page),
33
- activeCategory:
34
- typeof searchParams.category === "string" ? searchParams.category : "",
35
- })
39
+ // The pin is the only filter now — the chips are links to category pages, so
40
+ // there's no `?category=` to reconcile against the category list.
41
+ const getPinnedSlug = (pinnedCategory: PinnedCollectionCategory) =>
42
+ pinnedCategory?.slug ?? ""
36
43
 
37
44
  const getTotalPages = ({
38
45
  activeCategory,
@@ -68,21 +75,61 @@ const getCollectionWindow = ({
68
75
  }
69
76
  }
70
77
 
71
- function CollectionToolbarSection({
72
- activeCategory,
73
- categories,
74
- totalCount,
78
+ // Same query on both sides of the boundary, so the second call is a fetch-cache
79
+ // hit rather than a second round trip.
80
+ const getArticlesCount = async (category: string) => {
81
+ const { data } = await fetchSanity<number>({
82
+ query: BLOG_ARTICLES_COUNT_QUERY,
83
+ params: { category },
84
+ tags: ["blog-articles-count"],
85
+ })
86
+
87
+ return data ?? 0
88
+ }
89
+
90
+ async function CollectionToolbar({
91
+ pinnedCategory,
75
92
  }: {
76
- activeCategory: string
77
- categories: ContentCategory[]
78
- totalCount: number
93
+ pinnedCategory: PinnedCollectionCategory
79
94
  }) {
95
+ const activeCategory = getPinnedSlug(pinnedCategory)
96
+
97
+ const [{ data: categoriesData }, { data: collectionPagesData }, count] =
98
+ await Promise.all([
99
+ fetchSanity<ContentCategory[]>({
100
+ query: ALL_BLOG_CATEGORIES_QUERY,
101
+ tags: ["blog-categories"],
102
+ }),
103
+ fetchSanity<CollectionPage[]>({
104
+ query: COLLECTION_PAGES_QUERY,
105
+ tags: ["blog-categories"],
106
+ }),
107
+ getArticlesCount(activeCategory),
108
+ ])
109
+
110
+ // The chips navigate to the page pinned to each category, so a visitor on one
111
+ // can still reach the others. Shortest unpinned path wins as the index.
112
+ const categoryHrefs: Record<string, string> = {}
113
+ let indexHref = "/"
114
+ let indexLength = Number.POSITIVE_INFINITY
115
+ for (const entry of collectionPagesData ?? []) {
116
+ // getPagePath, not a template literal: a "/" slug falls through the resolver
117
+ // raw, and `/${"/"}` is a scheme-relative URL that navigates off-site.
118
+ const href = getPagePath(entry.path)
119
+ if (entry.category) categoryHrefs[entry.category] = href
120
+ else if (href.length < indexLength) {
121
+ indexHref = href
122
+ indexLength = href.length
123
+ }
124
+ }
125
+
80
126
  return (
81
127
  <ContentFilters
82
- categories={categories}
128
+ categories={categoriesData ?? []}
83
129
  activeCategory={activeCategory}
84
- totalCount={totalCount}
85
- filteredCount={totalCount}
130
+ count={count}
131
+ categoryHrefs={categoryHrefs}
132
+ indexHref={indexHref}
86
133
  />
87
134
  )
88
135
  }
@@ -143,70 +190,39 @@ function CollectionPaginationSection({
143
190
  )
144
191
  }
145
192
 
146
- export async function ContentCollection({
193
+ async function CollectionResults({
147
194
  block,
148
195
  searchParams,
196
+ pinnedCategory,
149
197
  }: ContentCollectionProps) {
150
- const resolvedParams = (await searchParams) ?? {}
151
- const { currentPage, activeCategory } = getCollectionParams(resolvedParams)
152
-
153
- const { data: categoriesData } = await fetchSanity<ContentCategory[]>({
154
- query: ALL_BLOG_CATEGORIES_QUERY,
155
- tags: ["blog-categories"],
156
- })
157
-
158
- const categories = categoriesData ?? []
159
- const validCategorySlugs = new Set(
160
- categories
161
- .map((category) => category.slug?.current)
162
- .filter((slug): slug is string => Boolean(slug))
163
- )
198
+ const resolvedParams: SearchParams = (await searchParams) ?? {}
199
+ const currentPage = normalizePageValue(resolvedParams.page)
200
+ const activeCategory = getPinnedSlug(pinnedCategory)
164
201
 
165
- const canonicalCategory =
166
- activeCategory && validCategorySlugs.has(activeCategory)
167
- ? activeCategory
168
- : ""
169
-
170
- const { data: totalCountData } = await fetchSanity<number>({
171
- query: BLOG_ARTICLES_COUNT_QUERY,
172
- params: { category: canonicalCategory },
173
- tags: ["blog-articles-count"],
174
- })
175
-
176
- const totalCount = totalCountData ?? 0
177
- const totalPages = getTotalPages({
178
- activeCategory: canonicalCategory,
179
- totalCount,
180
- })
202
+ const totalCount = await getArticlesCount(activeCategory)
203
+ const totalPages = getTotalPages({ activeCategory, totalCount })
181
204
  const canonicalPage = Math.min(currentPage, totalPages)
182
205
 
183
206
  const { isFirstPageNoFilter, fetchLimit, fetchOffset } = getCollectionWindow({
184
- activeCategory: canonicalCategory,
207
+ activeCategory,
185
208
  currentPage: canonicalPage,
186
209
  })
187
210
 
188
211
  const { data: itemsData } = await fetchSanity<ContentItemCard[]>({
189
212
  query: BLOG_ARTICLES_PAGINATED_QUERY,
190
213
  params: {
191
- category: canonicalCategory,
214
+ category: activeCategory,
192
215
  offset: fetchOffset,
193
216
  end: fetchOffset + fetchLimit,
194
217
  },
195
218
  tags: ["blog-articles"],
196
219
  })
197
220
 
198
- const items = itemsData ?? []
199
-
200
221
  return (
201
- <section className="flex flex-col gap-8 py-8">
202
- <CollectionToolbarSection
203
- activeCategory={canonicalCategory}
204
- categories={categories}
205
- totalCount={totalCount}
206
- />
222
+ <>
207
223
  <CollectionResultsSection
208
- activeCategory={canonicalCategory}
209
- items={items}
224
+ activeCategory={activeCategory}
225
+ items={itemsData ?? []}
210
226
  isFirstPageNoFilter={isFirstPageNoFilter}
211
227
  subtitle={block.subtitle}
212
228
  />
@@ -214,6 +230,20 @@ export async function ContentCollection({
214
230
  currentPage={canonicalPage}
215
231
  totalPages={totalPages}
216
232
  />
233
+ </>
234
+ )
235
+ }
236
+
237
+ // Only the results wait on `searchParams` (for `?page=`). The toolbar reads
238
+ // nothing request-scoped, so it renders in the shell and the chips are
239
+ // clickable before the grid resolves.
240
+ export function ContentCollection(props: ContentCollectionProps) {
241
+ return (
242
+ <section className="flex flex-col gap-8 py-8">
243
+ <CollectionToolbar pinnedCategory={props.pinnedCategory} />
244
+ <Suspense fallback={<CollectionResultsSkeleton />}>
245
+ <CollectionResults {...props} />
246
+ </Suspense>
217
247
  </section>
218
248
  )
219
249
  }
@@ -0,0 +1,41 @@
1
+ const FEATURED_PLACEHOLDERS = ["featured-1", "featured-2"]
2
+ const CARD_PLACEHOLDERS = [
3
+ "card-1",
4
+ "card-2",
5
+ "card-3",
6
+ "card-4",
7
+ "card-5",
8
+ "card-6",
9
+ ]
10
+
11
+ function CardPlaceholder() {
12
+ return (
13
+ <li className="flex flex-col gap-3">
14
+ <div className="aspect-16/9 w-full rounded-lg bg-black/10" />
15
+ <div className="flex flex-col gap-2">
16
+ <div className="h-3 w-16 rounded bg-black/10" />
17
+ <div className="h-5 w-4/5 rounded bg-black/10" />
18
+ <div className="h-4 w-1/2 rounded bg-black/10" />
19
+ </div>
20
+ </li>
21
+ )
22
+ }
23
+
24
+ // Results only: the filter chips render in the shell above this boundary.
25
+ export function CollectionResultsSkeleton() {
26
+ return (
27
+ <div className="flex animate-pulse flex-col gap-8" aria-hidden="true">
28
+ <ul className="grid grid-cols-1 gap-8 md:grid-cols-2">
29
+ {FEATURED_PLACEHOLDERS.map((key) => (
30
+ <CardPlaceholder key={key} />
31
+ ))}
32
+ </ul>
33
+
34
+ <ul className="grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3">
35
+ {CARD_PLACEHOLDERS.map((key) => (
36
+ <CardPlaceholder key={key} />
37
+ ))}
38
+ </ul>
39
+ </div>
40
+ )
41
+ }
@@ -1,5 +1,8 @@
1
1
  import type { PageBuilderBlock } from "@/components/page-builder/types"
2
- import type { ALL_BLOG_ARTICLES_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
2
+ import type {
3
+ ALL_BLOG_ARTICLES_QUERY_RESULT,
4
+ PAGE_QUERY_RESULT,
5
+ } from "@/lib/integrations/sanity/sanity.types"
3
6
 
4
7
  export type SearchParams = Record<string, string | string[] | undefined>
5
8
 
@@ -12,8 +15,18 @@ export type ContentCollectionBlock = Extract<
12
15
  }
13
16
  >
14
17
 
18
+ export type PinnedCollectionCategory = NonNullable<
19
+ PAGE_QUERY_RESULT
20
+ >["collectionCategory"]
21
+
15
22
  export type BlogArticleCard = ALL_BLOG_ARTICLES_QUERY_RESULT[number]
16
23
  export type ContentCategory = NonNullable<
17
24
  NonNullable<BlogArticleCard["blogContent"]>["categories"]
18
25
  >[number]
19
26
  export type ContentItemCard = BlogArticleCard
27
+
28
+ /** A page hosting a content collection; `category` set means it's pinned to one. */
29
+ export type CollectionPage = {
30
+ path: string | null
31
+ category: string | null
32
+ }
@@ -0,0 +1,16 @@
1
+ import { RichText } from "@/components/sanity/rich-text"
2
+ import type { PageBuilderBlock } from "@/components/page-builder/types"
3
+
4
+ type RichTextBlockProps = {
5
+ block: Extract<PageBuilderBlock, { _type: "richTextBlock" }>
6
+ }
7
+
8
+ export function RichTextBlock({ block }: RichTextBlockProps) {
9
+ if (!block.content) return null
10
+
11
+ return (
12
+ <section className="prose max-w-none">
13
+ <RichText content={block.content} />
14
+ </section>
15
+ )
16
+ }
@@ -1,17 +1,26 @@
1
1
  import { ArticleContent } from "./components/article-content"
2
2
  import { ContentCollection } from "./components/content-collection"
3
- import type { ContentCollectionSearchParams } from "./components/content-collection/types"
3
+ import type {
4
+ ContentCollectionSearchParams,
5
+ PinnedCollectionCategory,
6
+ } from "./components/content-collection/types"
4
7
  import { Description } from "./components/description"
5
8
  import { GatedContent } from "./components/gated-content"
6
9
  import { Hero } from "./components/hero"
10
+ import { RichTextBlock } from "./components/rich-text-block"
7
11
  import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
8
12
 
9
13
  type RendererProps = {
10
14
  blocks: PageBuilderBlocks
11
15
  searchParams: ContentCollectionSearchParams
16
+ pinnedCategory: PinnedCollectionCategory
12
17
  }
13
18
 
14
- export function Renderer({ blocks, searchParams }: RendererProps) {
19
+ export function Renderer({
20
+ blocks,
21
+ searchParams,
22
+ pinnedCategory,
23
+ }: RendererProps) {
15
24
  if (!blocks?.length) return null
16
25
 
17
26
  return (
@@ -21,6 +30,7 @@ export function Renderer({ blocks, searchParams }: RendererProps) {
21
30
  key={block._key}
22
31
  block={block}
23
32
  searchParams={searchParams}
33
+ pinnedCategory={pinnedCategory}
24
34
  />
25
35
  ))}
26
36
  </div>
@@ -30,19 +40,29 @@ export function Renderer({ blocks, searchParams }: RendererProps) {
30
40
  function PageBuilderNode({
31
41
  block,
32
42
  searchParams,
43
+ pinnedCategory,
33
44
  }: {
34
45
  block: PageBuilderBlock
35
46
  searchParams: ContentCollectionSearchParams
47
+ pinnedCategory: PinnedCollectionCategory
36
48
  }) {
37
49
  switch (block._type) {
38
50
  case "blogCollection":
39
- return <ContentCollection block={block} searchParams={searchParams} />
51
+ return (
52
+ <ContentCollection
53
+ block={block}
54
+ searchParams={searchParams}
55
+ pinnedCategory={pinnedCategory}
56
+ />
57
+ )
40
58
  case "blogContent":
41
59
  return <ArticleContent block={block} />
42
60
  case "hero":
43
61
  return <Hero block={block} />
44
62
  case "description":
45
63
  return <Description block={block} />
64
+ case "richTextBlock":
65
+ return <RichTextBlock block={block} />
46
66
  case "gatedContent":
47
67
  return <GatedContent block={block} />
48
68
  default: