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
@@ -0,0 +1,110 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef, useState } from "react"
4
+
5
+ import { Image } from "@/components/ui/image"
6
+ import { Link } from "@/components/ui/link"
7
+ import type { NavbarData } from "@/lib/integrations/sanity/fetchers/layout"
8
+
9
+ import { CtaButtonRenderer } from "./components/cta-button"
10
+ import { NavItemRenderer } from "./components/nav-item-renderer"
11
+
12
+ export const HeaderClient = ({ data: navbar }: { data: NavbarData | null }) => {
13
+ const [openIndex, setOpenIndex] = useState<number | null>(null)
14
+ const navRef = useRef<HTMLElement>(null)
15
+ const triggerRefs = useRef<Map<number, HTMLButtonElement>>(new Map())
16
+
17
+ const logoUrl = navbar?.logo?.asset?.url
18
+ const navigationItems = navbar?.navigationItems ?? []
19
+ const ctaButtons = navbar?.ctaButtons ?? []
20
+
21
+ // Close on click outside & Escape
22
+ useEffect(() => {
23
+ if (openIndex === null) return
24
+
25
+ const onClick = (e: MouseEvent) => {
26
+ if (
27
+ navRef.current &&
28
+ e.target instanceof Node &&
29
+ !navRef.current.contains(e.target)
30
+ ) {
31
+ setOpenIndex(null)
32
+ }
33
+ }
34
+
35
+ const onKeyDown = (e: KeyboardEvent) => {
36
+ if (e.key === "Escape") {
37
+ const prevIndex = openIndex
38
+ setOpenIndex(null)
39
+ triggerRefs.current.get(prevIndex)?.focus()
40
+ }
41
+ }
42
+
43
+ document.addEventListener("click", onClick)
44
+ document.addEventListener("keydown", onKeyDown)
45
+ return () => {
46
+ document.removeEventListener("click", onClick)
47
+ document.removeEventListener("keydown", onKeyDown)
48
+ }
49
+ }, [openIndex])
50
+
51
+ const handleToggle = (index: number) => {
52
+ setOpenIndex((prev) => (prev === index ? null : index))
53
+ }
54
+
55
+ return (
56
+ <nav
57
+ ref={navRef}
58
+ className="sticky top-0 z-50 border-black/10 border-b bg-white"
59
+ aria-label="Main navigation"
60
+ >
61
+ <div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
62
+ {/* Logo */}
63
+ <Link href="/" className="shrink-0" aria-label="Home">
64
+ {logoUrl ? (
65
+ <Image
66
+ src={logoUrl}
67
+ alt="Basement"
68
+ width={131}
69
+ height={23}
70
+ className="h-8 w-auto"
71
+ />
72
+ ) : (
73
+ <span className="font-semibold text-black text-lg">Basement</span>
74
+ )}
75
+ </Link>
76
+
77
+ {/* Navigation items */}
78
+ {navigationItems.length > 0 ? (
79
+ <ul className="hidden items-center gap-8 lg:flex">
80
+ {navigationItems.map((item, i) => (
81
+ <NavItemRenderer
82
+ key={item._key}
83
+ item={item}
84
+ isOpen={openIndex === i}
85
+ onToggle={() => handleToggle(i)}
86
+ triggerRef={(el) => {
87
+ if (el) {
88
+ triggerRefs.current.set(i, el)
89
+ return
90
+ }
91
+
92
+ triggerRefs.current.delete(i)
93
+ }}
94
+ />
95
+ ))}
96
+ </ul>
97
+ ) : null}
98
+
99
+ {/* CTA buttons */}
100
+ {ctaButtons.length > 0 ? (
101
+ <div className="hidden items-center gap-3 lg:flex">
102
+ {ctaButtons.map((cta) => (
103
+ <CtaButtonRenderer key={cta._key} cta={cta} />
104
+ ))}
105
+ </div>
106
+ ) : null}
107
+ </div>
108
+ </nav>
109
+ )
110
+ }
@@ -0,0 +1,32 @@
1
+ import { Suspense } from "react"
2
+ import { getLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
3
+ import {
4
+ type DynamicFetchOptions,
5
+ getDynamicFetchOptions,
6
+ PUBLISHED_FETCH_OPTIONS,
7
+ } from "@/lib/integrations/sanity/live"
8
+ import { HeaderClient } from "./header-client"
9
+
10
+ // Published navbar caches into the static shell; draft mode streams.
11
+ export const Header = ({ isDraftMode }: { isDraftMode: boolean }) =>
12
+ isDraftMode ? (
13
+ <Suspense fallback={<HeaderClient data={null} />}>
14
+ <DynamicHeader />
15
+ </Suspense>
16
+ ) : (
17
+ <CachedHeader {...PUBLISHED_FETCH_OPTIONS} />
18
+ )
19
+
20
+ async function DynamicHeader() {
21
+ const fetchOptions = await getDynamicFetchOptions()
22
+ return <CachedHeader {...fetchOptions} />
23
+ }
24
+
25
+ // Spread whole rather than destructured field by field, so the cache key keeps
26
+ // tracking every option -- perspective, stega and the editing variant.
27
+ async function CachedHeader(fetchOptions: DynamicFetchOptions) {
28
+ "use cache"
29
+ // Shares one entry with the footer and json-ld — same query, same options.
30
+ const { navbar } = await getLayoutData(fetchOptions)
31
+ return <HeaderClient data={navbar} />
32
+ }
@@ -0,0 +1,79 @@
1
+ import { stegaClean } from "next-sanity"
2
+ import { Suspense } from "react"
3
+ import { getLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
4
+ import {
5
+ type DynamicFetchOptions,
6
+ getDynamicFetchOptions,
7
+ PUBLISHED_FETCH_OPTIONS,
8
+ } from "@/lib/integrations/sanity/live"
9
+ import {
10
+ generateOrganizationJsonLd,
11
+ generateWebSiteJsonLd,
12
+ JsonLd as JsonLdBase,
13
+ } from "@/lib/utils/json-ld"
14
+
15
+ const nonEmpty = (value: unknown): string | undefined =>
16
+ typeof value === "string" && value.trim() !== "" ? value : undefined
17
+
18
+ // Cached into the static shell so crawlers see it; draft mode streams, since
19
+ // drafts don't need SEO markup.
20
+ export const JsonLd = ({ isDraftMode }: { isDraftMode: boolean }) =>
21
+ isDraftMode ? (
22
+ <Suspense fallback={null}>
23
+ <DynamicJsonLd />
24
+ </Suspense>
25
+ ) : (
26
+ <CachedJsonLd {...PUBLISHED_FETCH_OPTIONS} />
27
+ )
28
+
29
+ async function DynamicJsonLd() {
30
+ const fetchOptions = await getDynamicFetchOptions()
31
+ return <CachedJsonLd {...fetchOptions} />
32
+ }
33
+
34
+ async function CachedJsonLd(fetchOptions: DynamicFetchOptions) {
35
+ "use cache"
36
+ // One entry, shared with the header and footer.
37
+ const { structuredData, navbar } = await getLayoutData(fetchOptions)
38
+
39
+ // Deep-clean stega artifacts before any value reaches the emitted markup.
40
+ const data = stegaClean(structuredData)
41
+
42
+ const name = nonEmpty(data?.name)
43
+
44
+ // Without an organization name there's nothing meaningful to emit. Editors
45
+ // fill in Layout -> Structured Data to enable site-wide JSON-LD.
46
+ //
47
+ // Don't cap this branch: it renders in every route's shell and a nested
48
+ // lifetime propagates as a min(). SELF_TAGGED_TYPES clears it on publish.
49
+ if (!name) return null
50
+
51
+ const description = nonEmpty(data?.description)
52
+ const url = nonEmpty(data?.url)
53
+
54
+ // Prefer the structured-data logo, fall back to the navbar logo.
55
+ const logo = nonEmpty(data?.logo) ?? nonEmpty(navbar?.logo?.asset?.url)
56
+
57
+ // Merged and deduped in GROQ (flagged Company Data links + manual profiles).
58
+ const sameAs = (data?.sameAs ?? [])
59
+ .map(nonEmpty)
60
+ .filter((value): value is string => Boolean(value))
61
+
62
+ return (
63
+ <>
64
+ <JsonLdBase data={generateWebSiteJsonLd({ name, url, description })} />
65
+ <JsonLdBase
66
+ data={generateOrganizationJsonLd({
67
+ name,
68
+ url,
69
+ logo,
70
+ description,
71
+ legalName: nonEmpty(data?.legalName),
72
+ sameAs: sameAs.length ? sameAs : undefined,
73
+ contactPoint: data?.contactPoint,
74
+ address: data?.address,
75
+ })}
76
+ />
77
+ </>
78
+ )
79
+ }
@@ -0,0 +1,33 @@
1
+ import Link from "next/link"
2
+ import { Footer } from "@/components/layout/footer"
3
+ import { Header } from "@/components/layout/header"
4
+ import { cn } from "@/lib/styles/cn"
5
+
6
+ const SkipToMainContent = () => (
7
+ <Link
8
+ href="#main-content"
9
+ className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-9999 focus:bg-black focus:p-2 focus:text-white focus:outline-none focus:ring-2 focus:ring-white"
10
+ >
11
+ Skip to main content
12
+ </Link>
13
+ )
14
+
15
+ export const Wrapper = ({
16
+ children,
17
+ className,
18
+ isDraftMode,
19
+ ...props
20
+ }: React.HTMLAttributes<HTMLDivElement> & { isDraftMode: boolean }) => (
21
+ <>
22
+ <SkipToMainContent />
23
+ <Header isDraftMode={isDraftMode} />
24
+ <main
25
+ id="main-content"
26
+ className={cn("relative flex grow flex-col", className)}
27
+ {...props}
28
+ >
29
+ {children}
30
+ </main>
31
+ <Footer isDraftMode={isDraftMode} />
32
+ </>
33
+ )
@@ -0,0 +1,78 @@
1
+ import type { ArticleContentProps } from "@/components/page-builder/types"
2
+ import { RichText } from "@/components/sanity/rich-text"
3
+ import { SanityImage } from "@/components/ui/sanity-image"
4
+ import { formatDate } from "@/lib/utils/format-date"
5
+ import { RelatedPostItem } from "./related-post-item"
6
+
7
+ export function ArticleContent({ block }: ArticleContentProps) {
8
+ const { author, body, categories, date, relatedPosts, thumbnail } = block
9
+ return (
10
+ <div className="py-8 md:py-12">
11
+ <header className="mx-auto max-w-3xl space-y-6">
12
+ {categories && categories.length > 0 ? (
13
+ <div className="flex flex-wrap gap-2">
14
+ {/* @ts-ignore */}
15
+ {categories.map((category) => (
16
+ <span
17
+ key={category._id}
18
+ className="rounded-full border border-current/20 px-3 py-1 text-xs"
19
+ >
20
+ {category.title}
21
+ </span>
22
+ ))}
23
+ </div>
24
+ ) : null}
25
+
26
+ <div className="flex items-center gap-3">
27
+ {author?.avatar ? (
28
+ <SanityImage
29
+ image={author.avatar}
30
+ alt={author.name || ""}
31
+ className="size-10 rounded-full object-cover"
32
+ />
33
+ ) : null}
34
+ <div className="flex flex-col">
35
+ {author?.name ? (
36
+ <span className="font-medium text-sm">{author.name}</span>
37
+ ) : null}
38
+ <div className="flex items-center gap-2 text-current/60 text-sm">
39
+ {author?.role ? <span>{author.role}</span> : null}
40
+ {author?.role && date ? <span aria-hidden>·</span> : null}
41
+ {date ? <time dateTime={date}>{formatDate(date)}</time> : null}
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </header>
46
+
47
+ {thumbnail ? (
48
+ <div className="mx-auto mt-8 max-w-4xl overflow-hidden rounded-lg">
49
+ <SanityImage
50
+ image={thumbnail}
51
+ alt={thumbnail.alt || ""}
52
+ priority
53
+ className="aspect-video w-full object-cover"
54
+ />
55
+ </div>
56
+ ) : null}
57
+
58
+ {body ? (
59
+ <div className="mx-auto mt-10 max-w-3xl space-y-4 text-base/7 text-current/80 md:text-lg/8">
60
+ <RichText content={body} />
61
+ </div>
62
+ ) : null}
63
+
64
+ {relatedPosts && relatedPosts.length > 0 ? (
65
+ <aside className="mx-auto mt-16 max-w-3xl">
66
+ <h2 className="font-semibold text-2xl">Related Articles</h2>
67
+ <ul className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
68
+ {relatedPosts.map((post) => {
69
+ if (!post.resolvedSlug) return null
70
+
71
+ return <RelatedPostItem key={post._id} post={post} />
72
+ })}
73
+ </ul>
74
+ </aside>
75
+ ) : null}
76
+ </div>
77
+ )
78
+ }
@@ -0,0 +1,27 @@
1
+ import type { ArticleRelatedPost } from "@/components/page-builder/types"
2
+ import { Link } from "@/components/ui/link"
3
+ import { SanityImage } from "@/components/ui/sanity-image"
4
+
5
+ export const RelatedPostItem = ({ post }: { post: ArticleRelatedPost }) => {
6
+ return (
7
+ <li key={post._id}>
8
+ <Link
9
+ href={`/${post.resolvedSlug}`}
10
+ className="group flex flex-col gap-3"
11
+ >
12
+ {post.metadata?.image ? (
13
+ <div className="overflow-hidden rounded-lg">
14
+ <SanityImage
15
+ image={post.metadata.image}
16
+ alt={post.title || ""}
17
+ className="aspect-[16/9] w-full object-cover transition-transform duration-300 group-hover:scale-105"
18
+ />
19
+ </div>
20
+ ) : null}
21
+ <h3 className="text-balance font-medium text-base leading-snug">
22
+ {post.title}
23
+ </h3>
24
+ </Link>
25
+ </li>
26
+ )
27
+ }
@@ -0,0 +1,66 @@
1
+ import { Link } from "@/components/ui/link"
2
+ import { SanityImage } from "@/components/ui/sanity-image"
3
+ import { formatDate } from "@/lib/utils/format-date"
4
+ import type { ContentItemCard } from "./types"
5
+
6
+ type ContentCardProps = {
7
+ article: ContentItemCard
8
+ className?: string | undefined
9
+ }
10
+
11
+ export const ContentCard = ({ article, className }: ContentCardProps) => {
12
+ const href = article.resolvedSlug ? `/${article.resolvedSlug}` : null
13
+ if (!href) return null
14
+
15
+ const blog = article.blogContent
16
+ const title = article.title ?? ""
17
+ const categories = blog?.categories ?? []
18
+ const authorName = blog?.author?.name
19
+ const publishedAt = blog?.date ?? null
20
+ const formattedDate = publishedAt ? formatDate(publishedAt) : null
21
+ const thumbnail = blog?.thumbnail ?? article.metadata?.image
22
+
23
+ return (
24
+ <li className={className}>
25
+ <Link href={href} className="group flex flex-col gap-3">
26
+ <div className="relative aspect-16/9 w-full overflow-hidden rounded-lg bg-gray-100">
27
+ {thumbnail ? (
28
+ <SanityImage
29
+ image={thumbnail}
30
+ alt=""
31
+ fill
32
+ sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
33
+ className="w-full object-cover transition-transform duration-300 group-hover:scale-105"
34
+ />
35
+ ) : null}
36
+ </div>
37
+
38
+ <div className="flex flex-col gap-2">
39
+ {categories.length > 0 ? (
40
+ <div className="flex flex-wrap gap-1.5">
41
+ {categories.map((category) => (
42
+ <span key={category._id} className="text-current/60 text-xs">
43
+ {category.title}
44
+ </span>
45
+ ))}
46
+ </div>
47
+ ) : null}
48
+
49
+ <h3 className="text-balance font-medium text-lg leading-snug">
50
+ {title}
51
+ </h3>
52
+
53
+ {authorName || formattedDate ? (
54
+ <div className="flex items-center gap-2 text-current/60 text-sm">
55
+ {authorName ? <span>{authorName}</span> : null}
56
+ {authorName && formattedDate ? <span aria-hidden>·</span> : null}
57
+ {publishedAt && formattedDate ? (
58
+ <time dateTime={publishedAt}>{formattedDate}</time>
59
+ ) : null}
60
+ </div>
61
+ ) : null}
62
+ </div>
63
+ </Link>
64
+ </li>
65
+ )
66
+ }
@@ -0,0 +1,76 @@
1
+ import { Link } from "@/components/ui/link"
2
+ import { cn } from "@/lib/styles/cn"
3
+ import type { ContentCategory } from "./types"
4
+
5
+ type ContentFiltersProps = {
6
+ categories: ContentCategory[]
7
+ activeCategory: string
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
13
+ }
14
+
15
+ function FilterChip({
16
+ active,
17
+ label,
18
+ href,
19
+ }: {
20
+ active: boolean
21
+ label: string
22
+ href: string
23
+ }) {
24
+ return (
25
+ <Link
26
+ href={href}
27
+ aria-current={active ? "page" : undefined}
28
+ className={cn(
29
+ "rounded-full border px-4 py-2 text-sm transition-colors",
30
+ active
31
+ ? "border-black bg-black text-white"
32
+ : "border-black/10 bg-white hover:border-black/30"
33
+ )}
34
+ >
35
+ {label}
36
+ </Link>
37
+ )
38
+ }
39
+
40
+ export function ContentFilters({
41
+ categories,
42
+ activeCategory,
43
+ count,
44
+ categoryHrefs,
45
+ indexHref,
46
+ }: ContentFiltersProps) {
47
+ return (
48
+ <div className="flex flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-4 sm:p-5">
49
+ <nav aria-label="Filter by category" className="flex flex-wrap gap-2">
50
+ <FilterChip
51
+ active={activeCategory === ""}
52
+ label="All content"
53
+ href={indexHref}
54
+ />
55
+ {categories.map((category) => {
56
+ const slug = category.slug?.current
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
60
+
61
+ return (
62
+ <FilterChip
63
+ key={category._id}
64
+ active={activeCategory === slug}
65
+ label={category.title || "Untitled"}
66
+ href={href}
67
+ />
68
+ )
69
+ })}
70
+ </nav>
71
+ <p className="text-current/60 text-sm">
72
+ Showing {count} {count === 1 ? "item" : "items"}
73
+ </p>
74
+ </div>
75
+ )
76
+ }
@@ -0,0 +1,40 @@
1
+ import { ContentCard } from "./content-card"
2
+ import type { ContentItemCard } from "./types"
3
+
4
+ type ContentGridProps = {
5
+ items: ContentItemCard[]
6
+ start?: number
7
+ end?: number
8
+ className?: string
9
+ }
10
+
11
+ export function ContentGrid({
12
+ items,
13
+ start = 0,
14
+ end,
15
+ className,
16
+ }: ContentGridProps) {
17
+ const slice = items.slice(start, end)
18
+
19
+ if (slice.length === 0) {
20
+ if (start === 0) {
21
+ return <p className="text-current/70 text-sm">No articles found.</p>
22
+ }
23
+ return null
24
+ }
25
+
26
+ return (
27
+ <ul
28
+ className={
29
+ className ?? "grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3"
30
+ }
31
+ >
32
+ {slice.map((item) => {
33
+ const slug = item.resolvedSlug
34
+ if (!slug) return null
35
+
36
+ return <ContentCard key={item._id} article={item} />
37
+ })}
38
+ </ul>
39
+ )
40
+ }
@@ -0,0 +1,71 @@
1
+ "use client"
2
+
3
+ import { usePathname, useSearchParams } from "next/navigation"
4
+ import { Link } from "@/components/ui/link"
5
+
6
+ type ContentPaginationNavProps = {
7
+ currentPage: number
8
+ totalPages: number
9
+ }
10
+
11
+ function buildPaginationHref({
12
+ pathname,
13
+ page,
14
+ searchParams,
15
+ }: {
16
+ pathname: string
17
+ page: number
18
+ searchParams: ReturnType<typeof useSearchParams>
19
+ }) {
20
+ const nextParams = new URLSearchParams(searchParams.toString())
21
+
22
+ if (page > 1) nextParams.set("page", String(page))
23
+ else nextParams.delete("page")
24
+
25
+ const query = nextParams.toString()
26
+ return query ? `${pathname}?${query}` : pathname
27
+ }
28
+
29
+ export function ContentPaginationNav({
30
+ currentPage,
31
+ totalPages,
32
+ }: ContentPaginationNavProps) {
33
+ const pathname = usePathname()
34
+ const searchParams = useSearchParams()
35
+
36
+ if (!pathname || totalPages <= 1) return null
37
+
38
+ return (
39
+ <nav className="flex flex-wrap items-center gap-2" aria-label="Pagination">
40
+ {currentPage > 1 ? (
41
+ <Link
42
+ href={buildPaginationHref({
43
+ pathname,
44
+ page: currentPage - 1,
45
+ searchParams,
46
+ })}
47
+ className="rounded-full border border-black/10 px-4 py-2 text-sm transition-colors hover:border-black/30"
48
+ >
49
+ Previous
50
+ </Link>
51
+ ) : null}
52
+
53
+ <span className="text-current/60 text-sm">
54
+ Page {currentPage} of {totalPages}
55
+ </span>
56
+
57
+ {currentPage < totalPages ? (
58
+ <Link
59
+ href={buildPaginationHref({
60
+ pathname,
61
+ page: currentPage + 1,
62
+ searchParams,
63
+ })}
64
+ className="rounded-full border border-black/10 px-4 py-2 text-sm transition-colors hover:border-black/30"
65
+ >
66
+ Next
67
+ </Link>
68
+ ) : null}
69
+ </nav>
70
+ )
71
+ }