create-nextblock 0.2.78 → 0.8.1

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 (413) hide show
  1. package/bin/create-nextblock.js +793 -472
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -120
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -0,0 +1,279 @@
1
+ import type { Metadata } from 'next';
2
+
3
+ export const DEFAULT_SITE_TITLE = 'NextBlock™ CMS';
4
+
5
+ export const DEFAULT_SITE_DESCRIPTION =
6
+ 'NextBlock is an open-source CMS on Next.js + Supabase — a visual block editor, blazing-fast multilingual pages, and built-in e-commerce.';
7
+
8
+ export const DEFAULT_SITE_KEYWORDS =
9
+ 'NextBlock, CMS, Next.js, Supabase, headless CMS, block editor, visual page builder, multilingual, e-commerce, open source';
10
+
11
+ /** Bundled fallback Open Graph image (resolved to absolute via metadataBase). */
12
+ export const DEFAULT_OG_IMAGE = '/images/metadata_image.webp';
13
+
14
+ const DEFAULT_META_DESCRIPTION_LENGTH = 160;
15
+
16
+ function normalizeWhitespace(value: string) {
17
+ return value.replace(/\s+/g, ' ').trim();
18
+ }
19
+
20
+ function decodeHtmlEntities(value: string) {
21
+ const entities: Record<string, string> = {
22
+ amp: '&',
23
+ apos: "'",
24
+ gt: '>',
25
+ lt: '<',
26
+ nbsp: ' ',
27
+ quot: '"',
28
+ };
29
+
30
+ return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity) => {
31
+ const key = String(entity);
32
+ if (key[0] === '#') {
33
+ const isHex = key[1]?.toLowerCase() === 'x';
34
+ const codePoint = Number.parseInt(key.slice(isHex ? 2 : 1), isHex ? 16 : 10);
35
+ return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
36
+ }
37
+
38
+ return entities[key] ?? match;
39
+ });
40
+ }
41
+
42
+ export function stripHtmlToText(value: string) {
43
+ return normalizeWhitespace(
44
+ decodeHtmlEntities(
45
+ value
46
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
47
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
48
+ .replace(/<[^>]+>/g, ' ')
49
+ )
50
+ );
51
+ }
52
+
53
+ function extractParagraphTextFromHtml(value: string) {
54
+ const paragraphs = Array.from(value.matchAll(/<p\b[^>]*>([\s\S]*?)<\/p>/gi))
55
+ .map((match) => stripHtmlToText(match[1] ?? ''))
56
+ .filter(Boolean);
57
+
58
+ if (paragraphs.length > 0) {
59
+ return paragraphs[0];
60
+ }
61
+
62
+ return stripHtmlToText(value.replace(/<h[1-6]\b[\s\S]*?<\/h[1-6]>/gi, ' '));
63
+ }
64
+
65
+ function truncateMetaDescription(value: string, maxLength = DEFAULT_META_DESCRIPTION_LENGTH) {
66
+ const normalized = normalizeWhitespace(value);
67
+ if (normalized.length <= maxLength) {
68
+ return normalized;
69
+ }
70
+
71
+ const truncated = normalized.slice(0, maxLength + 1);
72
+ const lastSpace = truncated.lastIndexOf(' ');
73
+ const candidate = lastSpace > 80 ? truncated.slice(0, lastSpace) : normalized.slice(0, maxLength);
74
+
75
+ return candidate.replace(/[.,;:!?-]+$/, '').trim();
76
+ }
77
+
78
+ function normalizeMetaCandidate(value: string | null | undefined) {
79
+ if (!value) {
80
+ return null;
81
+ }
82
+
83
+ const normalized = stripHtmlToText(value);
84
+ return normalized || null;
85
+ }
86
+
87
+ function collectIntroTextCandidates(value: unknown, candidates: string[]) {
88
+ if (!value) {
89
+ return;
90
+ }
91
+
92
+ if (Array.isArray(value)) {
93
+ value.forEach((item) => collectIntroTextCandidates(item, candidates));
94
+ return;
95
+ }
96
+
97
+ if (typeof value !== 'object') {
98
+ return;
99
+ }
100
+
101
+ const block = value as {
102
+ block_type?: string;
103
+ content?: Record<string, unknown>;
104
+ };
105
+
106
+ if (block.block_type === 'section' || block.block_type === 'hero') {
107
+ collectIntroTextCandidates(block.content?.column_blocks, candidates);
108
+ collectIntroTextCandidates(block.content?.slides, candidates);
109
+ return;
110
+ }
111
+
112
+ if (block.block_type === 'text') {
113
+ const htmlContent = block.content?.html_content;
114
+ const textContent = block.content?.text_content;
115
+ const candidate =
116
+ typeof htmlContent === 'string'
117
+ ? extractParagraphTextFromHtml(htmlContent)
118
+ : typeof textContent === 'string'
119
+ ? normalizeWhitespace(textContent)
120
+ : '';
121
+
122
+ if (candidate) {
123
+ candidates.push(candidate);
124
+ }
125
+ }
126
+ }
127
+
128
+ export function extractIntroExcerptFromBlocks(blocks: unknown) {
129
+ const candidates: string[] = [];
130
+ collectIntroTextCandidates(blocks, candidates);
131
+
132
+ return (
133
+ candidates.find((candidate) => candidate.length >= 80) ??
134
+ candidates[0] ??
135
+ null
136
+ );
137
+ }
138
+
139
+ export function resolveMetaTitle(
140
+ manualTitle: string | null | undefined,
141
+ fallbackTitle: string | null | undefined
142
+ ) {
143
+ return (
144
+ normalizeMetaCandidate(manualTitle) ??
145
+ normalizeMetaCandidate(fallbackTitle) ??
146
+ DEFAULT_SITE_TITLE
147
+ );
148
+ }
149
+
150
+ export function resolveMetaDescription(...candidates: Array<string | null | undefined>) {
151
+ for (const candidate of candidates) {
152
+ const description = normalizeMetaCandidate(candidate);
153
+ if (description) {
154
+ return truncateMetaDescription(description);
155
+ }
156
+ }
157
+
158
+ return DEFAULT_SITE_DESCRIPTION;
159
+ }
160
+
161
+ export function resolvePageMetaDescription(
162
+ manualDescription: string | null | undefined,
163
+ blocks: unknown
164
+ ) {
165
+ return resolveMetaDescription(manualDescription, extractIntroExcerptFromBlocks(blocks));
166
+ }
167
+
168
+ export function resolvePostMetaDescription(
169
+ manualDescription: string | null | undefined,
170
+ subtitle: string | null | undefined
171
+ ) {
172
+ return resolveMetaDescription(manualDescription, subtitle);
173
+ }
174
+
175
+ export function resolveProductMetaDescription(
176
+ manualDescription: string | null | undefined,
177
+ shortDescription: string | null | undefined
178
+ ) {
179
+ return resolveMetaDescription(manualDescription, shortDescription);
180
+ }
181
+
182
+ export function stringifyJsonLd(value: unknown) {
183
+ return JSON.stringify(value).replace(/</g, '\\u003c');
184
+ }
185
+
186
+ /**
187
+ * Appends the site title to a page title for social cards, e.g.
188
+ * `composeTitleWithSite('Home', 'NextBlock™ CMS') === 'Home | NextBlock™ CMS'`.
189
+ * Unlike Next.js' `title.template` (which only affects the `<title>` tag), this
190
+ * lets us produce a complete `og:title` / `twitter:title`.
191
+ */
192
+ export function composeTitleWithSite(
193
+ pageTitle: string | null | undefined,
194
+ siteTitle: string | null | undefined
195
+ ): string {
196
+ const cleanTitle = (pageTitle ?? '').trim();
197
+ const cleanSite = (siteTitle ?? '').trim();
198
+
199
+ if (!cleanSite) return cleanTitle;
200
+ if (!cleanTitle) return cleanSite;
201
+
202
+ const suffix = ` | ${cleanSite}`;
203
+ return cleanTitle === cleanSite || cleanTitle.endsWith(suffix)
204
+ ? cleanTitle
205
+ : `${cleanTitle}${suffix}`;
206
+ }
207
+
208
+ /** Maps a language code (e.g. `fr`, `en-US`) to an Open Graph locale (`fr_FR`). */
209
+ export function toOpenGraphLocale(languageCode?: string | null): string {
210
+ const code = (languageCode ?? '').toLowerCase().split('-')[0];
211
+ const map: Record<string, string> = {
212
+ en: 'en_US',
213
+ fr: 'fr_FR',
214
+ es: 'es_ES',
215
+ de: 'de_DE',
216
+ pt: 'pt_PT',
217
+ it: 'it_IT',
218
+ nl: 'nl_NL',
219
+ };
220
+ return map[code] ?? 'en_US';
221
+ }
222
+
223
+ export interface SocialMetadataInput {
224
+ /** Bare page title (without the site-title suffix). */
225
+ title: string;
226
+ description: string;
227
+ /** Canonical URL of the page (absolute, or path resolved via metadataBase). */
228
+ url: string;
229
+ siteTitle: string;
230
+ /** Resolved feature-image URL; falls back to the default OG image when empty. */
231
+ imageUrl?: string | null;
232
+ type?: 'website' | 'article';
233
+ publishedTime?: string | null;
234
+ locale?: string | null;
235
+ }
236
+
237
+ /**
238
+ * Builds the `openGraph` + `twitter` metadata for a public page so that every
239
+ * page emits a complete, suffixed social title and always has an OG image
240
+ * (the feature image when present, otherwise the bundled default).
241
+ */
242
+ export function buildSocialMetadata(
243
+ input: SocialMetadataInput
244
+ ): Pick<Metadata, 'openGraph' | 'twitter'> {
245
+ const usingDefaultImage = !input.imageUrl;
246
+ const imageUrl = input.imageUrl || DEFAULT_OG_IMAGE;
247
+ const socialTitle = composeTitleWithSite(input.title, input.siteTitle);
248
+ const image = usingDefaultImage
249
+ ? { url: imageUrl, width: 1200, height: 630, alt: socialTitle }
250
+ : { url: imageUrl, alt: socialTitle };
251
+
252
+ const openGraphBase = {
253
+ title: socialTitle,
254
+ description: input.description,
255
+ url: input.url,
256
+ siteName: input.siteTitle,
257
+ images: [image],
258
+ ...(input.locale ? { locale: input.locale } : {}),
259
+ };
260
+
261
+ const openGraph =
262
+ input.type === 'article'
263
+ ? {
264
+ ...openGraphBase,
265
+ type: 'article' as const,
266
+ ...(input.publishedTime ? { publishedTime: input.publishedTime } : {}),
267
+ }
268
+ : { ...openGraphBase, type: 'website' as const };
269
+
270
+ return {
271
+ openGraph,
272
+ twitter: {
273
+ card: 'summary_large_image',
274
+ title: socialTitle,
275
+ description: input.description,
276
+ images: [imageUrl],
277
+ },
278
+ };
279
+ }
@@ -0,0 +1,87 @@
1
+ import 'server-only';
2
+ import { unstable_cache } from 'next/cache';
3
+ import { createClient as createSupabaseJsClient } from '@supabase/supabase-js';
4
+ import type { Database } from '@nextblock-cms/db';
5
+ import {
6
+ DEFAULT_SITE_TITLE,
7
+ DEFAULT_SITE_DESCRIPTION,
8
+ DEFAULT_SITE_KEYWORDS,
9
+ } from './seo';
10
+
11
+ export const SITE_SETTINGS_CACHE_TAG = 'public-site-settings';
12
+ const SITE_SETTINGS_REVALIDATE_SECONDS = 60;
13
+
14
+ export interface SiteSettings {
15
+ siteTitle: string;
16
+ siteDescription: string;
17
+ siteKeywords: string;
18
+ }
19
+
20
+ /**
21
+ * Service-role (or anon) Supabase client used for cached, request-agnostic
22
+ * reads of public layout/SEO data. Shared by the root layout and the public
23
+ * route `generateMetadata` functions.
24
+ */
25
+ export function createStaticSupabaseClient() {
26
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
27
+ const supabaseKey =
28
+ process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
29
+
30
+ if (!supabaseUrl || !supabaseKey) {
31
+ throw new Error('Missing Supabase environment variables for public layout data');
32
+ }
33
+
34
+ return createSupabaseJsClient<Database>(supabaseUrl, supabaseKey, {
35
+ auth: {
36
+ persistSession: false,
37
+ autoRefreshToken: false,
38
+ detectSessionInUrl: false,
39
+ },
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Cached read of the site-wide SEO identity (title / description / keywords)
45
+ * from the `site_settings` key-value store. This is the single source of truth
46
+ * used for both `<title>`/OpenGraph metadata and the header brand.
47
+ */
48
+ export const getSiteSettings = unstable_cache(
49
+ async (): Promise<SiteSettings> => {
50
+ const fallback: SiteSettings = {
51
+ siteTitle: DEFAULT_SITE_TITLE,
52
+ siteDescription: DEFAULT_SITE_DESCRIPTION,
53
+ siteKeywords: DEFAULT_SITE_KEYWORDS,
54
+ };
55
+
56
+ try {
57
+ const supabase = createStaticSupabaseClient();
58
+ const { data, error } = await supabase
59
+ .from('site_settings')
60
+ .select('key, value')
61
+ .in('key', ['site_title', 'site_description', 'site_keywords']);
62
+
63
+ if (error || !data) {
64
+ console.error('Error fetching cached site settings:', error);
65
+ return fallback;
66
+ }
67
+
68
+ const settings: Record<string, string> = {};
69
+ data.forEach((item) => {
70
+ if (typeof item.value === 'string') {
71
+ settings[item.key] = item.value;
72
+ }
73
+ });
74
+
75
+ return {
76
+ siteTitle: settings.site_title?.trim() || fallback.siteTitle,
77
+ siteDescription: settings.site_description?.trim() || fallback.siteDescription,
78
+ siteKeywords: settings.site_keywords?.trim() || fallback.siteKeywords,
79
+ };
80
+ } catch (caught) {
81
+ console.error('Unexpected error fetching site settings:', caught);
82
+ return fallback;
83
+ }
84
+ },
85
+ ['public-site-settings'],
86
+ { revalidate: SITE_SETTINGS_REVALIDATE_SECONDS, tags: [SITE_SETTINGS_CACHE_TAG] }
87
+ );
@@ -1,68 +1,253 @@
1
- import { createClient } from '@nextblock-cms/db/server';
2
-
3
- interface SitemapEntry {
4
- path: string;
5
- lastModified: string;
6
- }
7
-
8
- /**
9
- * Fetches all published pages from Supabase and formats them for the sitemap.
10
- * @returns {Promise<Array<SitemapEntry>>} A promise that resolves to an array of sitemap entries for pages.
11
- */
1
+ import { getSsgSupabaseClient } from '@nextblock-cms/db/server';
2
+
3
+ /**
4
+ * A single, language-aware entry destined for the XML sitemap.
5
+ *
6
+ * Paths are kept site-root-relative (e.g. "/about", "/article/my-post"); the
7
+ * `app/sitemap.ts` route prefixes the absolute origin so the base URL lives in
8
+ * exactly one place.
9
+ */
10
+ export interface SitemapEntry {
11
+ /** Root-relative path for this row, e.g. "/about" or "/article/hello". */
12
+ path: string;
13
+ /** ISO-8601 timestamp mapped from the row's `updated_at`. */
14
+ lastModified: string;
15
+ /**
16
+ * hreflang map of `languageCode -> root-relative path` covering every
17
+ * published translation that shares this row's `translation_group_id`, plus
18
+ * an `x-default` key pointing at the default-language version. Left
19
+ * `undefined` for content that only exists in a single language.
20
+ *
21
+ * NextBlock's i18n model gives each language its own slug (there is no
22
+ * `/en` `/fr` path prefix), so alternates are resolved by grouping sibling
23
+ * rows on `translation_group_id` rather than by swapping a locale segment.
24
+ */
25
+ alternates?: Record<string, string>;
26
+ }
27
+
28
+ /**
29
+ * Page slugs that are rendered by their own canonical route and must not be
30
+ * re-advertised under the generic `/{slug}` catch-all:
31
+ * - `product-template` backs the product layout (app/product/[slug]); it is
32
+ * not a public page.
33
+ * - `home` / `accueil` back the locale-resolved homepage served at "/"
34
+ * (see getHomepageSlugForLocale in app/page.tsx); listing them as `/home`
35
+ * and `/accueil` would duplicate the canonical "/" entry.
36
+ */
37
+ const EXCLUDED_PAGE_SLUGS = new Set(['product-template', 'home', 'accueil']);
38
+
39
+ type SupabaseLikeClient = ReturnType<typeof getSsgSupabaseClient>;
40
+
41
+ /** Minimal shape every content table exposes for sitemap purposes. */
42
+ interface ContentRow {
43
+ slug: string | null;
44
+ updated_at: string | null;
45
+ created_at?: string | null;
46
+ language_id: number | null;
47
+ translation_group_id: string | null;
48
+ }
49
+
50
+ function toIsoDate(value: string | null | undefined): string {
51
+ if (value) {
52
+ const parsed = new Date(value);
53
+ if (!Number.isNaN(parsed.getTime())) {
54
+ return parsed.toISOString();
55
+ }
56
+ }
57
+ return new Date().toISOString();
58
+ }
59
+
60
+ /**
61
+ * Loads the language set once so callers can resolve a row's `language_id` to
62
+ * its BCP-47 `code` and identify the default language used for `x-default`.
63
+ */
64
+ async function fetchLanguageMap(
65
+ supabase: SupabaseLikeClient,
66
+ ): Promise<{ codeById: Map<number, string>; defaultCode: string | null }> {
67
+ const codeById = new Map<number, string>();
68
+ let defaultCode: string | null = null;
69
+
70
+ const { data, error } = await supabase
71
+ .from('languages')
72
+ .select('id, code, is_default');
73
+
74
+ if (error) {
75
+ console.error('Error fetching languages for sitemap alternates:', error);
76
+ return { codeById, defaultCode };
77
+ }
78
+
79
+ for (const lang of data ?? []) {
80
+ if (lang.code) {
81
+ codeById.set(lang.id, lang.code);
82
+ if (lang.is_default) {
83
+ defaultCode = lang.code;
84
+ }
85
+ }
86
+ }
87
+
88
+ return { codeById, defaultCode };
89
+ }
90
+
91
+ /**
92
+ * Turns raw content rows into language-aware sitemap entries. Rows that share a
93
+ * `translation_group_id` are emitted as sibling URLs that cross-link to one
94
+ * another via `alternates`.
95
+ */
96
+ function rowsToEntries(
97
+ rows: ContentRow[],
98
+ pathForSlug: (slug: string) => string,
99
+ codeById: Map<number, string>,
100
+ defaultCode: string | null,
101
+ ): SitemapEntry[] {
102
+ // Bucket rows by their translation group so each URL can advertise its siblings.
103
+ const groups = new Map<string, ContentRow[]>();
104
+ for (const row of rows) {
105
+ if (!row.slug) continue;
106
+ const key = row.translation_group_id ?? `solo:${row.slug}`;
107
+ const bucket = groups.get(key);
108
+ if (bucket) {
109
+ bucket.push(row);
110
+ } else {
111
+ groups.set(key, [row]);
112
+ }
113
+ }
114
+
115
+ const entries: SitemapEntry[] = [];
116
+ for (const group of groups.values()) {
117
+ // languageCode -> path for every translated sibling in this group.
118
+ const byCode: Record<string, string> = {};
119
+ let defaultPath: string | undefined;
120
+ for (const sibling of group) {
121
+ if (!sibling.slug || sibling.language_id == null) continue;
122
+ const code = codeById.get(sibling.language_id);
123
+ if (!code) continue;
124
+ const path = pathForSlug(sibling.slug);
125
+ byCode[code] = path;
126
+ if (code === defaultCode) {
127
+ defaultPath = path;
128
+ }
129
+ }
130
+
131
+ // hreflang is only meaningful when 2+ *distinct* URLs exist. NextBlock has no
132
+ // locale path prefix, so two translations can share a slug (e.g. `/articles`
133
+ // is the slug in both en and fr); that collapses to a single URL with nothing
134
+ // to cross-link, so no alternates are emitted.
135
+ const hasAlternates = new Set(Object.values(byCode)).size > 1;
136
+ const alternates = hasAlternates
137
+ ? { ...byCode, 'x-default': defaultPath ?? Object.values(byCode)[0] }
138
+ : undefined;
139
+
140
+ for (const row of group) {
141
+ if (!row.slug) continue;
142
+ entries.push({
143
+ path: pathForSlug(row.slug),
144
+ lastModified: toIsoDate(row.updated_at ?? row.created_at),
145
+ alternates,
146
+ });
147
+ }
148
+ }
149
+
150
+ return entries;
151
+ }
152
+
153
+ /**
154
+ * Fetches all published pages from Supabase and formats them, with language
155
+ * alternates, for the sitemap.
156
+ */
12
157
  export async function fetchAllPublishedPages(): Promise<SitemapEntry[]> {
13
- const supabase = createClient();
158
+ const supabase = getSsgSupabaseClient();
14
159
  try {
15
- const { data: pages, error } = await supabase
16
- .from('pages')
17
- .select('slug, updated_at')
18
- .eq('status', 'published');
160
+ const [{ data: pages, error }, languageMap] = await Promise.all([
161
+ supabase
162
+ .from('pages')
163
+ .select('slug, updated_at, language_id, translation_group_id')
164
+ .eq('status', 'published'),
165
+ fetchLanguageMap(supabase),
166
+ ]);
19
167
 
20
168
  if (error) {
21
169
  console.error('Error fetching published pages:', error);
22
170
  return [];
23
171
  }
24
172
 
25
- if (!pages) {
26
- return [];
27
- }
173
+ const rows = (pages ?? []).filter(
174
+ (page) => page.slug && !EXCLUDED_PAGE_SLUGS.has(page.slug),
175
+ );
28
176
 
29
- return pages.map((page) => ({
30
- path: `/${page.slug}`,
31
- lastModified: new Date(page.updated_at).toISOString(),
32
- }));
177
+ return rowsToEntries(
178
+ rows,
179
+ (slug) => `/${slug}`,
180
+ languageMap.codeById,
181
+ languageMap.defaultCode,
182
+ );
33
183
  } catch (err) {
34
184
  console.error('An unexpected error occurred while fetching pages:', err);
35
185
  return [];
36
186
  }
37
187
  }
38
-
39
- /**
40
- * Fetches all published posts from Supabase and formats them for the sitemap.
41
- * @returns {Promise<Array<SitemapEntry>>} A promise that resolves to an array of sitemap entries for posts.
42
- */
188
+
189
+ /**
190
+ * Fetches all published posts (respecting scheduled `published_at`) from
191
+ * Supabase and formats them, with language alternates, for the sitemap.
192
+ */
43
193
  export async function fetchAllPublishedPosts(): Promise<SitemapEntry[]> {
44
- const supabase = createClient();
194
+ const supabase = getSsgSupabaseClient();
45
195
  try {
46
- const { data: posts, error } = await supabase
47
- .from('posts')
48
- .select('slug, updated_at')
49
- .eq('status', 'published');
196
+ const nowIso = new Date().toISOString();
197
+ const [{ data: posts, error }, languageMap] = await Promise.all([
198
+ supabase
199
+ .from('posts')
200
+ .select('slug, updated_at, language_id, translation_group_id')
201
+ .eq('status', 'published')
202
+ .or(`published_at.is.null,published_at.lte.${nowIso}`),
203
+ fetchLanguageMap(supabase),
204
+ ]);
50
205
 
51
206
  if (error) {
52
207
  console.error('Error fetching published posts:', error);
53
208
  return [];
54
209
  }
55
210
 
56
- if (!posts) {
211
+ return rowsToEntries(
212
+ posts ?? [],
213
+ (slug) => `/article/${slug}`,
214
+ languageMap.codeById,
215
+ languageMap.defaultCode,
216
+ );
217
+ } catch (err) {
218
+ console.error('An unexpected error occurred while fetching posts:', err);
219
+ return [];
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Fetches all active storefront products from Supabase and formats them, with
225
+ * language alternates, for the sitemap.
226
+ */
227
+ export async function fetchAllActiveProducts(): Promise<SitemapEntry[]> {
228
+ const supabase = getSsgSupabaseClient();
229
+ try {
230
+ const [{ data: products, error }, languageMap] = await Promise.all([
231
+ supabase
232
+ .from('products')
233
+ .select('slug, updated_at, created_at, language_id, translation_group_id')
234
+ .eq('status', 'active'),
235
+ fetchLanguageMap(supabase),
236
+ ]);
237
+
238
+ if (error) {
239
+ console.error('Error fetching active products:', error);
57
240
  return [];
58
241
  }
59
242
 
60
- return posts.map((post) => ({
61
- path: `/article/${post.slug}`,
62
- lastModified: new Date(post.updated_at).toISOString(),
63
- }));
243
+ return rowsToEntries(
244
+ products ?? [],
245
+ (slug) => `/product/${slug}`,
246
+ languageMap.codeById,
247
+ languageMap.defaultCode,
248
+ );
64
249
  } catch (err) {
65
- console.error('An unexpected error occurred while fetching posts:', err);
250
+ console.error('An unexpected error occurred while fetching products:', err);
66
251
  return [];
67
252
  }
68
253
  }