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,306 @@
1
+ import { Suspense } from "react"
2
+ import {
3
+ type DynamicFetchOptions,
4
+ getDynamicFetchOptions,
5
+ toSanityFetchOptions,
6
+ sanityFetch,
7
+ } from "@/lib/integrations/sanity/live"
8
+ import {
9
+ ALL_BLOG_CATEGORIES_QUERY,
10
+ BLOG_ARTICLES_COUNT_QUERY,
11
+ BLOG_ARTICLES_PAGINATED_QUERY,
12
+ COLLECTION_PAGES_QUERY,
13
+ } from "@/lib/integrations/sanity/queries"
14
+ import {
15
+ BLOG_ARTICLES_COUNT_TAG,
16
+ BLOG_ARTICLES_TAG,
17
+ BLOG_CATEGORIES_TAG,
18
+ } from "@/lib/integrations/sanity/tags"
19
+ import { getPagePath } from "@/lib/utils/sitemap"
20
+ import { ContentFilters } from "./content-filters"
21
+ import { ContentGrid } from "./content-grid"
22
+ import { ContentPaginationNav } from "./content-pagination-nav"
23
+ import { CollectionResultsSkeleton } from "./skeleton"
24
+ import type {
25
+ CollectionPage,
26
+ ContentCategory,
27
+ ContentCollectionBlock,
28
+ ContentCollectionSearchParams,
29
+ ContentItemCard,
30
+ PinnedCollectionCategory,
31
+ SearchParams,
32
+ } from "./types"
33
+
34
+ type ContentCollectionProps = {
35
+ block: ContentCollectionBlock
36
+ searchParams: ContentCollectionSearchParams
37
+ /** Category pages pin the collection to one category. */
38
+ pinnedCategory: PinnedCollectionCategory
39
+ }
40
+
41
+ const FEATURED_COUNT = 2
42
+ const PAGE_SIZE = 6
43
+
44
+ const normalizePageValue = (value: string | string[] | undefined) => {
45
+ const page = Number(Array.isArray(value) ? value[0] : value)
46
+ return Number.isFinite(page) && page > 0 ? Math.floor(page) : 1
47
+ }
48
+
49
+ // The pin is the only filter now — the chips are links to category pages, so
50
+ // there's no `?category=` to reconcile against the category list.
51
+ const getPinnedSlug = (pinnedCategory: PinnedCollectionCategory) =>
52
+ pinnedCategory?.slug ?? ""
53
+
54
+ const getTotalPages = ({
55
+ activeCategory,
56
+ totalCount,
57
+ }: {
58
+ activeCategory: string
59
+ totalCount: number
60
+ }) => {
61
+ const regularTotal =
62
+ activeCategory === ""
63
+ ? Math.max(0, totalCount - FEATURED_COUNT)
64
+ : totalCount
65
+
66
+ return Math.max(1, Math.ceil(regularTotal / PAGE_SIZE))
67
+ }
68
+
69
+ const getCollectionWindow = ({
70
+ activeCategory,
71
+ currentPage,
72
+ }: {
73
+ activeCategory: string
74
+ currentPage: number
75
+ }) => {
76
+ const isFirstPageNoFilter = currentPage === 1 && activeCategory === ""
77
+
78
+ return {
79
+ isFirstPageNoFilter,
80
+ fetchLimit: isFirstPageNoFilter ? PAGE_SIZE + FEATURED_COUNT : PAGE_SIZE,
81
+ fetchOffset: isFirstPageNoFilter
82
+ ? 0
83
+ : (currentPage - 1) * PAGE_SIZE +
84
+ (activeCategory === "" ? FEATURED_COUNT : 0),
85
+ }
86
+ }
87
+
88
+ // `'use cache'` helpers: searchParams (resolved to plain values) and the draft
89
+ // The fetch options arrive as props so each window of results caches and
90
+ // revalidates by tag independently.
91
+ //
92
+ // Don't cap the empty results: these render inside a live page's body and a
93
+ // nested lifetime propagates as a min(). BLOG_COLLECTION_TAGS clears them.
94
+ async function getBlogCategories(fetchOptions: DynamicFetchOptions) {
95
+ "use cache"
96
+ const { data } = await sanityFetch({
97
+ query: ALL_BLOG_CATEGORIES_QUERY,
98
+ ...toSanityFetchOptions(fetchOptions),
99
+ tags: [BLOG_CATEGORIES_TAG],
100
+ })
101
+ return (data as ContentCategory[] | null) ?? []
102
+ }
103
+
104
+ async function getCollectionPages(fetchOptions: DynamicFetchOptions) {
105
+ "use cache"
106
+ const { data } = await sanityFetch({
107
+ query: COLLECTION_PAGES_QUERY,
108
+ ...toSanityFetchOptions(fetchOptions),
109
+ tags: [BLOG_CATEGORIES_TAG],
110
+ })
111
+ return (data as CollectionPage[] | null) ?? []
112
+ }
113
+
114
+ async function getBlogArticlesCount({
115
+ category,
116
+ ...fetchOptions
117
+ }: { category: string } & DynamicFetchOptions) {
118
+ "use cache"
119
+ const { data } = await sanityFetch({
120
+ query: BLOG_ARTICLES_COUNT_QUERY,
121
+ params: { category },
122
+ ...toSanityFetchOptions(fetchOptions),
123
+ tags: [BLOG_ARTICLES_COUNT_TAG],
124
+ })
125
+ return (data as number | null) ?? 0
126
+ }
127
+
128
+ async function getBlogArticles({
129
+ category,
130
+ offset,
131
+ end,
132
+ ...fetchOptions
133
+ }: {
134
+ category: string
135
+ offset: number
136
+ end: number
137
+ } & DynamicFetchOptions) {
138
+ "use cache"
139
+ const { data } = await sanityFetch({
140
+ query: BLOG_ARTICLES_PAGINATED_QUERY,
141
+ params: { category, offset, end },
142
+ ...toSanityFetchOptions(fetchOptions),
143
+ tags: [BLOG_ARTICLES_TAG],
144
+ })
145
+ return (data as ContentItemCard[] | null) ?? []
146
+ }
147
+
148
+ async function CollectionToolbar({
149
+ pinnedCategory,
150
+ }: {
151
+ pinnedCategory: PinnedCollectionCategory
152
+ }) {
153
+ const activeCategory = getPinnedSlug(pinnedCategory)
154
+ const fetchOptions = await getDynamicFetchOptions()
155
+
156
+ const [categories, collectionPages, count] = await Promise.all([
157
+ getBlogCategories(fetchOptions),
158
+ getCollectionPages(fetchOptions),
159
+ getBlogArticlesCount({ category: activeCategory, ...fetchOptions }),
160
+ ])
161
+
162
+ // The chips navigate to the page pinned to each category, so a visitor on one
163
+ // can still reach the others. Shortest unpinned path wins as the index.
164
+ const categoryHrefs: Record<string, string> = {}
165
+ let indexHref = "/"
166
+ let indexLength = Number.POSITIVE_INFINITY
167
+ for (const entry of collectionPages) {
168
+ // getPagePath, not a template literal: a "/" slug falls through the resolver
169
+ // raw, and `/${"/"}` is a scheme-relative URL that navigates off-site.
170
+ const href = getPagePath(entry.path)
171
+ if (entry.category) categoryHrefs[entry.category] = href
172
+ else if (href.length < indexLength) {
173
+ indexHref = href
174
+ indexLength = href.length
175
+ }
176
+ }
177
+
178
+ return (
179
+ <ContentFilters
180
+ categories={categories}
181
+ activeCategory={activeCategory}
182
+ count={count}
183
+ categoryHrefs={categoryHrefs}
184
+ indexHref={indexHref}
185
+ />
186
+ )
187
+ }
188
+
189
+ function CollectionResultsSection({
190
+ activeCategory,
191
+ items,
192
+ isFirstPageNoFilter,
193
+ subtitle,
194
+ }: {
195
+ activeCategory: string
196
+ items: ContentItemCard[]
197
+ isFirstPageNoFilter: boolean
198
+ subtitle: string | null | undefined
199
+ }) {
200
+ const featuredItems = isFirstPageNoFilter
201
+ ? items.slice(0, FEATURED_COUNT)
202
+ : []
203
+ const regularItems = isFirstPageNoFilter ? items.slice(FEATURED_COUNT) : items
204
+ const isEmpty = featuredItems.length === 0 && regularItems.length === 0
205
+
206
+ return (
207
+ <>
208
+ {featuredItems.length > 0 ? (
209
+ <ContentGrid
210
+ items={featuredItems}
211
+ className="grid grid-cols-1 gap-8 md:grid-cols-2"
212
+ />
213
+ ) : null}
214
+ {isFirstPageNoFilter && activeCategory === "" && subtitle ? (
215
+ <h2 className="font-medium text-2xl leading-tight">{subtitle}</h2>
216
+ ) : null}
217
+ {isEmpty ? (
218
+ <div className="rounded-[1.5rem] border border-black/15 border-dashed bg-black/[0.02] px-6 py-12 text-center">
219
+ <p className="font-medium text-lg">No content found</p>
220
+ <p className="mt-2 text-current/60 text-sm">
221
+ Try a different category to explore more content.
222
+ </p>
223
+ </div>
224
+ ) : (
225
+ <ContentGrid items={regularItems} />
226
+ )}
227
+ </>
228
+ )
229
+ }
230
+
231
+ function CollectionPaginationSection({
232
+ currentPage,
233
+ totalPages,
234
+ }: {
235
+ currentPage: number
236
+ totalPages: number
237
+ }) {
238
+ if (totalPages <= 1) return null
239
+
240
+ return (
241
+ <ContentPaginationNav currentPage={currentPage} totalPages={totalPages} />
242
+ )
243
+ }
244
+
245
+ async function CollectionResults({
246
+ block,
247
+ searchParams,
248
+ pinnedCategory,
249
+ }: ContentCollectionProps) {
250
+ const [resolvedParams, fetchOptions] = await Promise.all([
251
+ Promise.resolve(searchParams).then(
252
+ (params): SearchParams => params ?? {}
253
+ ),
254
+ getDynamicFetchOptions(),
255
+ ])
256
+ const currentPage = normalizePageValue(resolvedParams.page)
257
+ const activeCategory = getPinnedSlug(pinnedCategory)
258
+
259
+ const totalCount = await getBlogArticlesCount({
260
+ category: activeCategory,
261
+ ...fetchOptions,
262
+ })
263
+ const totalPages = getTotalPages({ activeCategory, totalCount })
264
+ const canonicalPage = Math.min(currentPage, totalPages)
265
+
266
+ const { isFirstPageNoFilter, fetchLimit, fetchOffset } = getCollectionWindow({
267
+ activeCategory,
268
+ currentPage: canonicalPage,
269
+ })
270
+
271
+ const items = await getBlogArticles({
272
+ category: activeCategory,
273
+ offset: fetchOffset,
274
+ end: fetchOffset + fetchLimit,
275
+ ...fetchOptions,
276
+ })
277
+
278
+ return (
279
+ <>
280
+ <CollectionResultsSection
281
+ activeCategory={activeCategory}
282
+ items={items}
283
+ isFirstPageNoFilter={isFirstPageNoFilter}
284
+ subtitle={block.subtitle}
285
+ />
286
+ <CollectionPaginationSection
287
+ currentPage={canonicalPage}
288
+ totalPages={totalPages}
289
+ />
290
+ </>
291
+ )
292
+ }
293
+
294
+ // Only the results wait on `searchParams` (for `?page=`), which is the dynamic
295
+ // read the boundary exists for. The toolbar reads nothing request-scoped and
296
+ // every fetch behind it is cached, so it prerenders into the static shell.
297
+ export function ContentCollection(props: ContentCollectionProps) {
298
+ return (
299
+ <section className="flex flex-col gap-8 py-8">
300
+ <CollectionToolbar pinnedCategory={props.pinnedCategory} />
301
+ <Suspense fallback={<CollectionResultsSkeleton />}>
302
+ <CollectionResults {...props} />
303
+ </Suspense>
304
+ </section>
305
+ )
306
+ }
@@ -0,0 +1,41 @@
1
+ const FEATURED_PLACEHOLDERS = ["featured-1", "featured-2"]
2
+ const CARD_PLACEHOLDERS = [
3
+ "card-1",
4
+ "card-2",
5
+ "card-3",
6
+ "card-4",
7
+ "card-5",
8
+ "card-6",
9
+ ]
10
+
11
+ function CardPlaceholder() {
12
+ return (
13
+ <li className="flex flex-col gap-3">
14
+ <div className="aspect-16/9 w-full rounded-lg bg-black/10" />
15
+ <div className="flex flex-col gap-2">
16
+ <div className="h-3 w-16 rounded bg-black/10" />
17
+ <div className="h-5 w-4/5 rounded bg-black/10" />
18
+ <div className="h-4 w-1/2 rounded bg-black/10" />
19
+ </div>
20
+ </li>
21
+ )
22
+ }
23
+
24
+ // Results only: the filter chips render in the shell above this boundary.
25
+ export function CollectionResultsSkeleton() {
26
+ return (
27
+ <div className="flex animate-pulse flex-col gap-8" aria-hidden="true">
28
+ <ul className="grid grid-cols-1 gap-8 md:grid-cols-2">
29
+ {FEATURED_PLACEHOLDERS.map((key) => (
30
+ <CardPlaceholder key={key} />
31
+ ))}
32
+ </ul>
33
+
34
+ <ul className="grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3">
35
+ {CARD_PLACEHOLDERS.map((key) => (
36
+ <CardPlaceholder key={key} />
37
+ ))}
38
+ </ul>
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,32 @@
1
+ import type { PageBuilderBlock } from "@/components/page-builder/types"
2
+ import type {
3
+ ALL_BLOG_ARTICLES_QUERY_RESULT,
4
+ PAGE_QUERY_RESULT,
5
+ } from "@/lib/integrations/sanity/sanity.types"
6
+
7
+ export type SearchParams = Record<string, string | string[] | undefined>
8
+
9
+ export type ContentCollectionSearchParams = Promise<SearchParams> | undefined
10
+
11
+ export type ContentCollectionBlock = Extract<
12
+ PageBuilderBlock,
13
+ {
14
+ _type: "blogCollection"
15
+ }
16
+ >
17
+
18
+ export type PinnedCollectionCategory = NonNullable<
19
+ PAGE_QUERY_RESULT
20
+ >["collectionCategory"]
21
+
22
+ export type BlogArticleCard = ALL_BLOG_ARTICLES_QUERY_RESULT[number]
23
+ export type ContentCategory = NonNullable<
24
+ NonNullable<BlogArticleCard["blogContent"]>["categories"]
25
+ >[number]
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,17 @@
1
+ import type { PageBuilderBlock } from "../types"
2
+
3
+ type DescriptionProps = {
4
+ block: Extract<PageBuilderBlock, { _type: "description" }>
5
+ }
6
+
7
+ export function Description({ block }: DescriptionProps) {
8
+ if (!block.description) return null
9
+
10
+ return (
11
+ <section className="py-4">
12
+ <p className="max-w-[62ch] text-base/7 text-current/72 md:text-lg/8">
13
+ {block.description}
14
+ </p>
15
+ </section>
16
+ )
17
+ }
@@ -0,0 +1,169 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import {
5
+ type GatedContentPayload,
6
+ unlockGatedContent,
7
+ } from "@/app/actions/unlock-gated-content"
8
+ import type { PageBuilderBlock } from "@/components/page-builder/types"
9
+ import { RichText } from "@/components/sanity/rich-text"
10
+ import { Link } from "@/components/ui/link"
11
+
12
+ type GatedContentBlock = Extract<PageBuilderBlock, { _type: "gatedContent" }>
13
+
14
+ type GatedContentProps = {
15
+ block: GatedContentBlock
16
+ }
17
+
18
+ type GateState = "locked" | "loading" | "unlocked"
19
+
20
+ const STORAGE_PREFIX = "gated-content:"
21
+
22
+ export function GatedContent({ block }: GatedContentProps) {
23
+ const storageKey = `${STORAGE_PREFIX}${block._id}`
24
+ const [state, setState] = useState<GateState>("locked")
25
+ const [payload, setPayload] = useState<GatedContentPayload | null>(null)
26
+ const [error, setError] = useState<string | null>(null)
27
+ const [justUnlocked, setJustUnlocked] = useState(false)
28
+
29
+ // The payload is withheld from the page query, so it never reaches the client
30
+ // until this action runs. We remember the submitted email so a returning
31
+ // visitor re-fetches without seeing the form again.
32
+ const unlock = async (email: string, persist: boolean) => {
33
+ setState("loading")
34
+ setError(null)
35
+
36
+ const result = await unlockGatedContent({ documentId: block._id, email })
37
+
38
+ if (!result.ok) {
39
+ setError(result.error)
40
+ setState("locked")
41
+ if (!persist) window.localStorage.removeItem(storageKey)
42
+ return
43
+ }
44
+
45
+ setPayload(result.payload)
46
+ setState("unlocked")
47
+ if (persist) {
48
+ window.localStorage.setItem(storageKey, email)
49
+ setJustUnlocked(true)
50
+ }
51
+ }
52
+
53
+ // biome-ignore lint/correctness/useExhaustiveDependencies: re-fetch only when the block changes; unlock closes over stable values
54
+ useEffect(() => {
55
+ const savedEmail = window.localStorage.getItem(storageKey)
56
+ if (savedEmail) void unlock(savedEmail, false)
57
+ }, [storageKey])
58
+
59
+ const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
60
+ event.preventDefault()
61
+ const email =
62
+ new FormData(event.currentTarget).get("email")?.toString() ?? ""
63
+ void unlock(email, true)
64
+ }
65
+
66
+ return (
67
+ <section className="py-8 md:py-12">
68
+ {block.teaserHeading ? (
69
+ <h2 className="mb-4 font-semibold text-2xl tracking-tight md:text-4xl">
70
+ {block.teaserHeading}
71
+ </h2>
72
+ ) : null}
73
+
74
+ {block.teaserBody ? (
75
+ <div className="max-w-[62ch] space-y-4 text-base/7 text-current/80 md:text-lg/8">
76
+ <RichText content={block.teaserBody} />
77
+ </div>
78
+ ) : null}
79
+
80
+ {state === "unlocked" && payload ? (
81
+ <div className="mt-8">
82
+ {justUnlocked && block.successMessage ? (
83
+ <p className="mb-6 rounded-lg bg-black/[0.04] px-4 py-3 text-current/72 text-sm">
84
+ {block.successMessage}
85
+ </p>
86
+ ) : null}
87
+ <GatedReveal payload={payload} />
88
+ </div>
89
+ ) : (
90
+ <form
91
+ onSubmit={handleSubmit}
92
+ className="mt-8 flex max-w-md flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-6"
93
+ >
94
+ {block.gateHeadline ? (
95
+ <h3 className="font-semibold text-lg">{block.gateHeadline}</h3>
96
+ ) : null}
97
+ {block.gateDescription ? (
98
+ <p className="text-current/72 text-sm">{block.gateDescription}</p>
99
+ ) : null}
100
+ <input
101
+ type="email"
102
+ name="email"
103
+ required
104
+ placeholder="you@example.com"
105
+ className="rounded-lg border border-black/10 bg-white px-4 py-2.5 text-sm outline-none focus:border-black/40"
106
+ />
107
+ {error ? <p className="text-red-600 text-sm">{error}</p> : null}
108
+ <button
109
+ type="submit"
110
+ disabled={state === "loading"}
111
+ className="rounded-lg bg-black px-4 py-2.5 font-medium text-sm text-white transition-colors hover:bg-black/80 disabled:opacity-60"
112
+ >
113
+ {state === "loading" ? "Unlocking…" : block.submitLabel || "Unlock"}
114
+ </button>
115
+ </form>
116
+ )}
117
+ </section>
118
+ )
119
+ }
120
+
121
+ function GatedReveal({ payload }: { payload: GatedContentPayload }) {
122
+ if (payload.kind === "richText" && payload.content) {
123
+ return (
124
+ <div className="max-w-[62ch] space-y-4 text-base/7 text-current/80 md:text-lg/8">
125
+ <RichText content={payload.content} />
126
+ </div>
127
+ )
128
+ }
129
+
130
+ if (payload.kind === "file" && payload.file?.url) {
131
+ return (
132
+ <Link
133
+ href={payload.file.url}
134
+ download
135
+ className="inline-flex items-center gap-2 rounded-lg bg-black px-4 py-2.5 font-medium text-sm text-white transition-colors hover:bg-black/80"
136
+ >
137
+ Download {payload.file.originalFilename ?? "file"}
138
+ </Link>
139
+ )
140
+ }
141
+
142
+ if (payload.kind === "video") {
143
+ if (payload.video.url) {
144
+ return (
145
+ <div className="aspect-video w-full max-w-3xl overflow-hidden rounded-lg">
146
+ <iframe
147
+ src={payload.video.url}
148
+ title="Gated video"
149
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
150
+ allowFullScreen
151
+ className="h-full w-full border-0"
152
+ />
153
+ </div>
154
+ )
155
+ }
156
+ if (payload.video.file?.url) {
157
+ return (
158
+ // biome-ignore lint/a11y/useMediaCaption: editor-supplied video without a caption track
159
+ <video
160
+ controls
161
+ src={payload.video.file.url}
162
+ className="w-full max-w-3xl rounded-lg"
163
+ />
164
+ )
165
+ }
166
+ }
167
+
168
+ return null
169
+ }
@@ -0,0 +1,17 @@
1
+ import type { PageBuilderBlock } from "../types"
2
+
3
+ type HeroProps = {
4
+ block: Extract<PageBuilderBlock, { _type: "hero" }>
5
+ }
6
+
7
+ export function Hero({ block }: HeroProps) {
8
+ if (!block.headline) return null
9
+
10
+ return (
11
+ <section className="py-8 md:py-12">
12
+ <h1 className="max-w-4xl font-semibold text-4xl tracking-tight md:text-6xl">
13
+ {block.headline}
14
+ </h1>
15
+ </section>
16
+ )
17
+ }
@@ -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
+ }
@@ -0,0 +1,71 @@
1
+ import { ArticleContent } from "./components/article-content"
2
+ import { ContentCollection } from "./components/content-collection"
3
+ import type {
4
+ ContentCollectionSearchParams,
5
+ PinnedCollectionCategory,
6
+ } from "./components/content-collection/types"
7
+ import { Description } from "./components/description"
8
+ import { GatedContent } from "./components/gated-content"
9
+ import { Hero } from "./components/hero"
10
+ import { RichTextBlock } from "./components/rich-text-block"
11
+ import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
12
+
13
+ type RendererProps = {
14
+ blocks: PageBuilderBlocks
15
+ searchParams: ContentCollectionSearchParams
16
+ pinnedCategory: PinnedCollectionCategory
17
+ }
18
+
19
+ export function Renderer({
20
+ blocks,
21
+ searchParams,
22
+ pinnedCategory,
23
+ }: RendererProps) {
24
+ if (!blocks?.length) return null
25
+
26
+ return (
27
+ <div className="space-y-16">
28
+ {blocks.map((block: PageBuilderBlock) => (
29
+ <PageBuilderNode
30
+ key={block._key}
31
+ block={block}
32
+ searchParams={searchParams}
33
+ pinnedCategory={pinnedCategory}
34
+ />
35
+ ))}
36
+ </div>
37
+ )
38
+ }
39
+
40
+ function PageBuilderNode({
41
+ block,
42
+ searchParams,
43
+ pinnedCategory,
44
+ }: {
45
+ block: PageBuilderBlock
46
+ searchParams: ContentCollectionSearchParams
47
+ pinnedCategory: PinnedCollectionCategory
48
+ }) {
49
+ switch (block._type) {
50
+ case "blogCollection":
51
+ return (
52
+ <ContentCollection
53
+ block={block}
54
+ searchParams={searchParams}
55
+ pinnedCategory={pinnedCategory}
56
+ />
57
+ )
58
+ case "blogContent":
59
+ return <ArticleContent block={block} />
60
+ case "hero":
61
+ return <Hero block={block} />
62
+ case "description":
63
+ return <Description block={block} />
64
+ case "richTextBlock":
65
+ return <RichTextBlock block={block} />
66
+ case "gatedContent":
67
+ return <GatedContent block={block} />
68
+ default:
69
+ return null
70
+ }
71
+ }
@@ -0,0 +1,25 @@
1
+ import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
2
+
3
+ export type PageBuilderBlock = NonNullable<
4
+ NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]
5
+ >[number]
6
+
7
+ export type PageBuilderBlocks = NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]
8
+
9
+ export type BlogContentBlock = Extract<
10
+ PageBuilderBlock,
11
+ { _type: "blogContent" }
12
+ >
13
+
14
+ export type ArticleContentProps = {
15
+ block: BlogContentBlock
16
+ }
17
+
18
+ export type ArticleRelatedPost = {
19
+ _id: string
20
+ title: string | null
21
+ resolvedSlug: string | null
22
+ metadata?: {
23
+ image?: unknown
24
+ } | null
25
+ }