bsmnt 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/dist/application/add-integration/index.js.map +1 -1
  2. package/dist/application/shared/node-version.js.map +1 -1
  3. package/dist/core/create/internal-ids.d.ts +3 -0
  4. package/dist/core/create/internal-ids.d.ts.map +1 -1
  5. package/dist/core/create/internal-ids.js +4 -0
  6. package/dist/core/create/internal-ids.js.map +1 -1
  7. package/dist/domain/agents.d.ts.map +1 -1
  8. package/dist/domain/animations.d.ts.map +1 -1
  9. package/dist/domain/cms.d.ts.map +1 -1
  10. package/dist/domain/forms.d.ts.map +1 -1
  11. package/dist/domain/hooks.d.ts.map +1 -1
  12. package/dist/domain/skills.d.ts.map +1 -1
  13. package/dist/domain/templates.d.ts +10 -0
  14. package/dist/domain/templates.d.ts.map +1 -1
  15. package/dist/domain/templates.js +5 -0
  16. package/dist/domain/templates.js.map +1 -1
  17. package/dist/index.js +18 -5
  18. package/dist/index.js.map +1 -1
  19. package/dist/infrastructure/agents/register-sanity-mcp.js +3 -8
  20. package/dist/infrastructure/agents/register-sanity-mcp.js.map +1 -1
  21. package/dist/infrastructure/agents/register-storybook-mcp.d.ts +18 -0
  22. package/dist/infrastructure/agents/register-storybook-mcp.d.ts.map +1 -0
  23. package/dist/infrastructure/agents/register-storybook-mcp.js +157 -0
  24. package/dist/infrastructure/agents/register-storybook-mcp.js.map +1 -0
  25. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  26. package/dist/infrastructure/create/lifecycle-registry.js +39 -1
  27. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  28. package/dist/infrastructure/create/merge-registry.d.ts.map +1 -1
  29. package/dist/infrastructure/create/merge-registry.js +4 -0
  30. package/dist/infrastructure/create/merge-registry.js.map +1 -1
  31. package/dist/infrastructure/create/sanity-auth.js.map +1 -1
  32. package/dist/infrastructure/create/setup-agent.d.ts +6 -1
  33. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  34. package/dist/infrastructure/create/setup-agent.js +23 -21
  35. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  36. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
  37. package/dist/infrastructure/create/setup-remote.js.map +1 -1
  38. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  39. package/dist/infrastructure/create/setup-sanity.js +12 -1
  40. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  41. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  42. package/dist/infrastructure/integrations/inject-integration.js +2 -0
  43. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  44. package/dist/infrastructure/update/notifier.js.map +1 -1
  45. package/dist/modules/features/cms/index.d.ts.map +1 -1
  46. package/dist/modules/features/cms/index.js +2 -0
  47. package/dist/modules/features/cms/index.js.map +1 -1
  48. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  49. package/dist/modules/features/cms/sanity/config.js +1 -0
  50. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  51. package/dist/modules/features/cms/sanity/index.d.ts.map +1 -1
  52. package/dist/modules/features/cms/sanity/index.js +10 -1
  53. package/dist/modules/features/cms/sanity/index.js.map +1 -1
  54. package/dist/modules/features/cms/sanity/mergers/check-integration.d.ts.map +1 -1
  55. package/dist/modules/features/cms/sanity/mergers/check-integration.js +15 -6
  56. package/dist/modules/features/cms/sanity/mergers/check-integration.js.map +1 -1
  57. package/dist/modules/features/cms/sanity/mergers/knip.d.ts.map +1 -1
  58. package/dist/modules/features/cms/sanity/mergers/knip.js +1 -0
  59. package/dist/modules/features/cms/sanity/mergers/knip.js.map +1 -1
  60. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  61. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts +12 -0
  62. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts.map +1 -0
  63. package/dist/modules/features/cms/sanity/mergers/package-json.js +19 -0
  64. package/dist/modules/features/cms/sanity/mergers/package-json.js.map +1 -0
  65. package/dist/modules/features/cms/sanity-pagebuilder/index.d.ts.map +1 -1
  66. package/dist/modules/features/cms/sanity-pagebuilder/index.js +7 -1
  67. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  68. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts +14 -0
  69. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -0
  70. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +50 -0
  71. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -0
  72. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts +3 -0
  73. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts.map +1 -0
  74. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js +55 -0
  75. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js.map +1 -0
  76. package/dist/modules/presets/index.d.ts.map +1 -1
  77. package/dist/modules/presets/index.js +2 -0
  78. package/dist/modules/presets/index.js.map +1 -1
  79. package/dist/modules/presets/next-pagebuilder-cache.d.ts +3 -0
  80. package/dist/modules/presets/next-pagebuilder-cache.d.ts.map +1 -0
  81. package/dist/modules/presets/next-pagebuilder-cache.js +10 -0
  82. package/dist/modules/presets/next-pagebuilder-cache.js.map +1 -0
  83. package/dist/paths.d.ts +6 -0
  84. package/dist/paths.d.ts.map +1 -1
  85. package/dist/paths.js +6 -0
  86. package/dist/paths.js.map +1 -1
  87. package/package.json +5 -2
  88. package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +36 -4
  89. package/src/modules/features/cms/sanity/files/lib/integrations/check-integration.ts +7 -6
  90. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/README.md +1 -1
  91. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +8 -1
  92. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/env.ts +2 -2
  93. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +4 -0
  94. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +172 -24
  95. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +14 -7
  96. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +2 -2
  97. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +16 -1
  98. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +8 -1
  99. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +2 -2
  100. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +6 -5
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +8 -0
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-notices.tsx +6 -16
  106. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +2 -2
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +22 -52
  109. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
  110. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +3 -1
  111. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +78 -33
  112. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +70 -57
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +119 -29
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +2 -0
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  117. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  118. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +5 -0
  119. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +12 -0
  120. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +2 -0
  121. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
  122. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +56 -0
  123. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +12 -0
  124. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +11 -8
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/.knip.json +8 -0
  126. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/[[...slug]]/route.ts +112 -0
  127. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/disable/route.ts +7 -0
  128. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/enable/route.ts +20 -0
  129. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +292 -0
  130. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/feed.xml/route.ts +66 -0
  131. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/sitemap.md/route.ts +132 -0
  132. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/layout.tsx +26 -0
  133. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/page.tsx +23 -0
  134. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/draft-mode-toggle.tsx +27 -0
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +94 -0
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/visual-editing.tsx +11 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +83 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/check-integration.ts +6 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/client.ts +37 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +58 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  146. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-field.tsx +24 -0
  147. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  148. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-notices.tsx +125 -0
  149. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  150. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  151. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/env.ts +34 -0
  152. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +39 -0
  153. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/redirects.ts +22 -0
  154. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +81 -0
  155. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/live/index.tsx +140 -0
  156. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  157. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +182 -0
  158. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-category.ts +28 -0
  159. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-tree-pane.tsx +431 -0
  160. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  161. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +119 -0
  162. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +548 -0
  163. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  164. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  165. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +116 -0
  166. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +7 -0
  167. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  168. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  169. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
  170. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  171. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  172. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  173. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  174. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  175. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  176. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +137 -0
  177. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/company-data.ts +70 -0
  178. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  179. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  180. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +152 -0
  181. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  182. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  183. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  184. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +132 -0
  185. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  186. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +43 -0
  187. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  188. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +265 -0
  189. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  190. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  191. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +53 -0
  192. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +339 -0
  193. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +89 -0
  194. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/image.ts +8 -0
  195. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/link.ts +140 -0
  196. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-builder-markdown.ts +113 -0
  197. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  198. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +36 -0
  199. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/check-redirect.ts +30 -0
  200. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/json-ld.tsx +522 -0
  201. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +189 -0
  202. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/rss.ts +96 -0
  203. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/sitemap.ts +58 -0
  204. package/src/modules/features/cms/sanity-pagebuilder-cache/files/proxy.ts +90 -0
  205. package/src/modules/features/env/dotenvx/files/vercelignore +5 -0
  206. package/src/templates/next-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
  207. package/src/templates/next-default/.env.example +2 -2
  208. package/src/templates/next-default/.github/workflows/ci.yml +1 -1
  209. package/src/templates/next-default/.vscode/settings.json +0 -2
  210. package/src/templates/next-default/biome.json +2 -1
  211. package/src/templates/next-default/components/ui/image/index.tsx +21 -15
  212. package/src/templates/next-default/knip.json +1 -1
  213. package/src/templates/next-default/lefthook.yml +1 -1
  214. package/src/templates/next-default/lib/utils/README.md +18 -5
  215. package/src/templates/next-default/lib/utils/image-sizes.ts +72 -0
  216. package/src/templates/next-default/lib/utils/merge-refs.ts +56 -0
  217. package/src/templates/next-default/lib/utils/strings.ts +1 -51
  218. package/src/templates/next-default/next.config.ts +12 -7
  219. package/src/templates/next-default/package.json +2 -5
  220. package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
  221. package/src/templates/next-experiments/.env.example +2 -2
  222. package/src/templates/next-experiments/.github/workflows/ci.yml +1 -1
  223. package/src/templates/next-experiments/.vscode/settings.json +0 -2
  224. package/src/templates/next-experiments/app/page.tsx +1 -1
  225. package/src/templates/next-experiments/biome.json +2 -1
  226. package/src/templates/next-experiments/components/ui/image/index.tsx +21 -15
  227. package/src/templates/next-experiments/knip.json +1 -1
  228. package/src/templates/next-experiments/lefthook.yml +1 -1
  229. package/src/templates/next-experiments/lib/utils/README.md +18 -5
  230. package/src/templates/next-experiments/lib/utils/image-sizes.ts +72 -0
  231. package/src/templates/next-experiments/lib/utils/merge-refs.ts +56 -0
  232. package/src/templates/next-experiments/lib/utils/strings.ts +1 -51
  233. package/src/templates/next-experiments/next.config.ts +12 -7
  234. package/src/templates/next-experiments/package.json +2 -5
  235. package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -0
  236. package/src/templates/next-pagebuilder/.env.example +1 -0
  237. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +1 -1
  238. package/src/templates/next-pagebuilder/.vscode/settings.json +0 -2
  239. package/src/templates/next-pagebuilder/README.md +64 -0
  240. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +3 -1
  241. package/src/templates/next-pagebuilder/biome.json +4 -2
  242. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +3 -6
  243. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -2
  244. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +3 -8
  245. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +5 -0
  246. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  247. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +23 -40
  248. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +88 -58
  249. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/skeleton.tsx +4 -16
  250. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/types.ts +14 -1
  251. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +16 -0
  252. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +21 -7
  253. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -0
  254. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +21 -12
  255. package/src/templates/next-pagebuilder/knip.json +3 -2
  256. package/src/templates/next-pagebuilder/lefthook.yml +1 -1
  257. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +1221 -977
  258. package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +72 -0
  259. package/src/templates/next-pagebuilder/lib/utils/merge-refs.ts +56 -0
  260. package/src/templates/next-pagebuilder/next.config.ts +6 -6
  261. package/src/templates/next-pagebuilder/package.json +4 -6
  262. package/src/templates/next-pagebuilder-cache/.biome/plugins/README.md +21 -0
  263. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-anchor-element.grit +12 -0
  264. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
  265. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-relative-parent-imports.grit +10 -0
  266. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  267. package/src/templates/next-pagebuilder-cache/.env.example +40 -0
  268. package/src/templates/next-pagebuilder-cache/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  269. package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +105 -0
  270. package/src/templates/next-pagebuilder-cache/.vscode/extensions.json +19 -0
  271. package/src/templates/next-pagebuilder-cache/.vscode/settings.json +103 -0
  272. package/src/templates/next-pagebuilder-cache/.zed/settings.json +47 -0
  273. package/src/templates/next-pagebuilder-cache/README.md +212 -0
  274. package/src/templates/next-pagebuilder-cache/_gitignore +65 -0
  275. package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +124 -0
  276. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +28 -0
  277. package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +53 -0
  278. package/src/templates/next-pagebuilder-cache/app/favicon.ico +0 -0
  279. package/src/templates/next-pagebuilder-cache/app/layout.tsx +83 -0
  280. package/src/templates/next-pagebuilder-cache/app/robots.ts +13 -0
  281. package/src/templates/next-pagebuilder-cache/app/sitemap.xml/route.ts +84 -0
  282. package/src/templates/next-pagebuilder-cache/biome.json +258 -0
  283. package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +127 -0
  284. package/src/templates/next-pagebuilder-cache/components/layout/header/components/cta-button.tsx +28 -0
  285. package/src/templates/next-pagebuilder-cache/components/layout/header/components/mega-menu-panel.tsx +90 -0
  286. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-item-renderer.tsx +98 -0
  287. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-leaf-item.tsx +33 -0
  288. package/src/templates/next-pagebuilder-cache/components/layout/header/components/types.ts +7 -0
  289. package/src/templates/next-pagebuilder-cache/components/layout/header/header-client.tsx +110 -0
  290. package/src/templates/next-pagebuilder-cache/components/layout/header/index.tsx +32 -0
  291. package/src/templates/next-pagebuilder-cache/components/layout/json-ld/index.tsx +79 -0
  292. package/src/templates/next-pagebuilder-cache/components/layout/wrapper/index.tsx +33 -0
  293. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +83 -0
  294. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +28 -0
  295. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-card.tsx +66 -0
  296. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-filters.tsx +76 -0
  297. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-grid.tsx +40 -0
  298. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  299. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +306 -0
  300. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/skeleton.tsx +41 -0
  301. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/types.ts +32 -0
  302. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +17 -0
  303. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +169 -0
  304. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +17 -0
  305. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +16 -0
  306. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +71 -0
  307. package/src/templates/next-pagebuilder-cache/components/page-builder/types.ts +25 -0
  308. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +147 -0
  309. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +225 -0
  310. package/src/templates/next-pagebuilder-cache/components/ui/link/index.tsx +152 -0
  311. package/src/templates/next-pagebuilder-cache/components.json +24 -0
  312. package/src/templates/next-pagebuilder-cache/knip.json +35 -0
  313. package/src/templates/next-pagebuilder-cache/lefthook.yml +25 -0
  314. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +1588 -0
  315. package/src/templates/next-pagebuilder-cache/lib/scripts/setup-ci-secrets.ts +117 -0
  316. package/src/templates/next-pagebuilder-cache/lib/styles/cn.ts +5 -0
  317. package/src/templates/next-pagebuilder-cache/lib/styles/global.css +69 -0
  318. package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +17 -0
  319. package/src/templates/next-pagebuilder-cache/lib/utils/format-date.ts +8 -0
  320. package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
  321. package/src/templates/next-pagebuilder-cache/lib/utils/slug-tag.ts +6 -0
  322. package/src/templates/next-pagebuilder-cache/next.config.ts +177 -0
  323. package/src/templates/next-pagebuilder-cache/package.json +62 -0
  324. package/src/templates/next-pagebuilder-cache/postcss.config.mjs +65 -0
  325. package/src/templates/next-pagebuilder-cache/svg.d.ts +6 -0
  326. package/src/templates/next-pagebuilder-cache/tsconfig.json +38 -0
  327. package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
  328. package/src/templates/next-webgl/.env.example +2 -2
  329. package/src/templates/next-webgl/.github/workflows/ci.yml +1 -1
  330. package/src/templates/next-webgl/.vscode/settings.json +0 -2
  331. package/src/templates/next-webgl/biome.json +2 -1
  332. package/src/templates/next-webgl/components/ui/image/index.tsx +21 -15
  333. package/src/templates/next-webgl/knip.json +1 -1
  334. package/src/templates/next-webgl/lefthook.yml +1 -1
  335. package/src/templates/next-webgl/lib/utils/README.md +18 -5
  336. package/src/templates/next-webgl/lib/utils/image-sizes.ts +72 -0
  337. package/src/templates/next-webgl/lib/utils/merge-refs.ts +56 -0
  338. package/src/templates/next-webgl/lib/utils/strings.ts +1 -51
  339. package/src/templates/next-webgl/next.config.ts +12 -7
  340. package/src/templates/next-webgl/package.json +4 -7
  341. package/src/templates/next-experiments/tsconfig.tsbuildinfo +0 -1
  342. package/src/templates/next-pagebuilder/app/actions/refresh.ts +0 -5
  343. package/src/templates/next-webgl/tsconfig.tsbuildinfo +0 -1
@@ -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": {
@@ -246,7 +247,8 @@
246
247
  "plugins": [
247
248
  "./.biome/plugins/no-anchor-element.grit",
248
249
  "./.biome/plugins/no-unnecessary-forwardref.grit",
249
- "./.biome/plugins/no-relative-parent-imports.grit"
250
+ "./.biome/plugins/no-relative-parent-imports.grit",
251
+ "./.biome/plugins/no-fill-without-sizes.grit"
250
252
  ],
251
253
  "vcs": {
252
254
  "clientKind": "git",
@@ -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)
@@ -28,6 +28,9 @@ export function ArticleContent({ block }: ArticleContentProps) {
28
28
  <SanityImage
29
29
  image={author.avatar}
30
30
  alt={author.name || ""}
31
+ width={40}
32
+ height={40}
33
+ maxWidth={80}
31
34
  className="size-10 rounded-full object-cover"
32
35
  />
33
36
  ) : null}
@@ -50,6 +53,8 @@ export function ArticleContent({ block }: ArticleContentProps) {
50
53
  image={thumbnail}
51
54
  alt={thumbnail.alt || ""}
52
55
  priority
56
+ quality={90}
57
+ sizes="(min-width: 896px) 896px, 100vw"
53
58
  className="aspect-video w-full object-cover"
54
59
  />
55
60
  </div>
@@ -14,6 +14,7 @@ export const RelatedPostItem = ({ post }: { post: ArticleRelatedPost }) => {
14
14
  <SanityImage
15
15
  image={post.metadata.image}
16
16
  alt={post.title || ""}
17
+ sizes="(min-width: 768px) 768px, 100vw"
17
18
  className="aspect-[16/9] w-full object-cover transition-transform duration-300 group-hover:scale-105"
18
19
  />
19
20
  </div>
@@ -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
  }
@@ -7,7 +7,6 @@ const CARD_PLACEHOLDERS = [
7
7
  "card-5",
8
8
  "card-6",
9
9
  ]
10
- const CHIP_PLACEHOLDERS = ["chip-1", "chip-2", "chip-3", "chip-4"]
11
10
 
12
11
  function CardPlaceholder() {
13
12
  return (
@@ -22,21 +21,10 @@ function CardPlaceholder() {
22
21
  )
23
22
  }
24
23
 
25
- export function ContentCollectionSkeleton() {
24
+ // Results only: the filter chips render in the shell above this boundary.
25
+ export function CollectionResultsSkeleton() {
26
26
  return (
27
- <section
28
- className="flex animate-pulse flex-col gap-8 py-8"
29
- aria-hidden="true"
30
- >
31
- <div className="flex flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-4 sm:p-5">
32
- <div className="flex flex-wrap gap-2">
33
- {CHIP_PLACEHOLDERS.map((key) => (
34
- <div className="h-9 w-28 rounded-full bg-black/10" key={key} />
35
- ))}
36
- </div>
37
- <div className="h-4 w-40 rounded bg-black/10" />
38
- </div>
39
-
27
+ <div className="flex animate-pulse flex-col gap-8" aria-hidden="true">
40
28
  <ul className="grid grid-cols-1 gap-8 md:grid-cols-2">
41
29
  {FEATURED_PLACEHOLDERS.map((key) => (
42
30
  <CardPlaceholder key={key} />
@@ -48,6 +36,6 @@ export function ContentCollectionSkeleton() {
48
36
  <CardPlaceholder key={key} />
49
37
  ))}
50
38
  </ul>
51
- </section>
39
+ </div>
52
40
  )
53
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,19 +1,26 @@
1
- import { Suspense } from "react"
2
1
  import { ArticleContent } from "./components/article-content"
3
2
  import { ContentCollection } from "./components/content-collection"
4
- import { ContentCollectionSkeleton } from "./components/content-collection/skeleton"
5
- import type { ContentCollectionSearchParams } from "./components/content-collection/types"
3
+ import type {
4
+ ContentCollectionSearchParams,
5
+ PinnedCollectionCategory,
6
+ } from "./components/content-collection/types"
6
7
  import { Description } from "./components/description"
7
8
  import { GatedContent } from "./components/gated-content"
8
9
  import { Hero } from "./components/hero"
10
+ import { RichTextBlock } from "./components/rich-text-block"
9
11
  import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
10
12
 
11
13
  type RendererProps = {
12
14
  blocks: PageBuilderBlocks
13
15
  searchParams: ContentCollectionSearchParams
16
+ pinnedCategory: PinnedCollectionCategory
14
17
  }
15
18
 
16
- export function Renderer({ blocks, searchParams }: RendererProps) {
19
+ export function Renderer({
20
+ blocks,
21
+ searchParams,
22
+ pinnedCategory,
23
+ }: RendererProps) {
17
24
  if (!blocks?.length) return null
18
25
 
19
26
  return (
@@ -23,6 +30,7 @@ export function Renderer({ blocks, searchParams }: RendererProps) {
23
30
  key={block._key}
24
31
  block={block}
25
32
  searchParams={searchParams}
33
+ pinnedCategory={pinnedCategory}
26
34
  />
27
35
  ))}
28
36
  </div>
@@ -32,16 +40,20 @@ export function Renderer({ blocks, searchParams }: RendererProps) {
32
40
  function PageBuilderNode({
33
41
  block,
34
42
  searchParams,
43
+ pinnedCategory,
35
44
  }: {
36
45
  block: PageBuilderBlock
37
46
  searchParams: ContentCollectionSearchParams
47
+ pinnedCategory: PinnedCollectionCategory
38
48
  }) {
39
49
  switch (block._type) {
40
50
  case "blogCollection":
41
51
  return (
42
- <Suspense fallback={<ContentCollectionSkeleton />}>
43
- <ContentCollection block={block} searchParams={searchParams} />
44
- </Suspense>
52
+ <ContentCollection
53
+ block={block}
54
+ searchParams={searchParams}
55
+ pinnedCategory={pinnedCategory}
56
+ />
45
57
  )
46
58
  case "blogContent":
47
59
  return <ArticleContent block={block} />
@@ -49,6 +61,8 @@ function PageBuilderNode({
49
61
  return <Hero block={block} />
50
62
  case "description":
51
63
  return <Description block={block} />
64
+ case "richTextBlock":
65
+ return <RichTextBlock block={block} />
52
66
  case "gatedContent":
53
67
  return <GatedContent block={block} />
54
68
  default:
@@ -32,6 +32,7 @@ export const getPageSections = (
32
32
  key="page-builder"
33
33
  blocks={page.pageBuilder}
34
34
  searchParams={searchParams}
35
+ pinnedCategory={stegaClean(page.collectionCategory)}
35
36
  />,
36
37
  ]
37
38
  : []