create-nextblock 0.2.77 → 0.8.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 (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  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 +191 -151
  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 -116
  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
@@ -1,253 +1,435 @@
1
- import { createServerClient, type CookieOptions } from '@supabase/ssr';
2
- import { NextResponse, type NextRequest } from 'next/server';
3
- import type { Database } from '@nextblock-cms/db';
4
-
5
- type Profile = Database['public']['Tables']['profiles']['Row'];
6
- type UserRole = Database['public']['Enums']['user_role'];
7
-
8
- const LANGUAGE_COOKIE_KEY = 'NEXT_USER_LOCALE';
9
- const DEFAULT_LOCALE = 'en';
10
- const SUPPORTED_LOCALES = ['en', 'fr'];
11
-
12
- const cmsRoutePermissions: Record<string, UserRole[]> = {
13
- '/cms': ['WRITER', 'ADMIN'],
14
- '/cms/admin': ['ADMIN'],
15
- '/cms/users': ['ADMIN'],
16
- '/cms/settings': ['ADMIN'],
17
- };
18
-
19
- function getRequiredRolesForPath(pathname: string): UserRole[] | null {
20
- const sortedPaths = Object.keys(cmsRoutePermissions).sort(
21
- (a, b) => b.length - a.length,
22
- );
23
- for (const specificPath of sortedPaths) {
24
- if (
25
- pathname === specificPath ||
26
- pathname.startsWith(specificPath + (specificPath === '/' ? '' : '/'))
27
- ) {
28
- return cmsRoutePermissions[specificPath];
29
- }
30
- }
31
- return null;
32
- }
33
-
34
- export default async function proxy(request: NextRequest) {
35
- const requestHeaders = new Headers(request.headers);
36
- const nonce = crypto.randomUUID();
37
- requestHeaders.set('x-nonce', nonce);
38
-
39
- let response = NextResponse.next({
40
- request: {
41
- headers: requestHeaders,
42
- },
43
- });
44
-
45
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
46
- const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
47
-
48
- if (!supabaseUrl || !supabaseAnonKey) {
49
- throw new Error('Missing required Supabase environment variables');
50
- }
51
-
52
- const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
53
- cookies: {
54
- get(name: string) {
55
- return request.cookies.get(name)?.value;
56
- },
57
- set(name: string, value: string, options: CookieOptions) {
58
- request.cookies.set({ name, value, ...options });
59
- response = NextResponse.next({ request: { headers: requestHeaders } });
60
- response.cookies.set({ name, value, ...options });
61
- },
62
- remove(name: string, options: CookieOptions) {
63
- request.cookies.set({ name, value: '', ...options });
64
- response = NextResponse.next({ request: { headers: requestHeaders } });
65
- response.cookies.set({ name, value: '', ...options });
66
- },
67
- },
68
- });
69
-
70
- await supabase.auth.getSession();
71
-
72
- const cookieLocale = request.cookies.get(LANGUAGE_COOKIE_KEY)?.value;
73
- let currentLocale = cookieLocale;
74
-
75
- if (!currentLocale || !SUPPORTED_LOCALES.includes(currentLocale)) {
76
- currentLocale = DEFAULT_LOCALE;
77
- }
78
-
79
- requestHeaders.set('X-User-Locale', currentLocale);
80
-
81
- const {
82
- data: { user },
83
- error: userError,
84
- } = await supabase.auth.getUser();
85
- const { pathname } = request.nextUrl;
86
-
87
- if (pathname.startsWith('/cms')) {
88
- if (userError || !user) {
89
- return NextResponse.redirect(
90
- new URL(`/sign-in?redirect=${pathname}`, request.url),
91
- );
92
- }
93
-
94
- const requiredRoles = getRequiredRolesForPath(pathname);
95
-
96
- if (requiredRoles && requiredRoles.length > 0) {
97
- const {
98
- data: profile,
99
- error: profileError,
100
- } = await supabase
101
- .from('profiles')
102
- .select('role')
103
- .eq('id', user.id)
104
- .single<Pick<Profile, 'role'>>();
105
-
106
- if (profileError || !profile) {
107
- console.error(
108
- `Proxy: Profile error for user ${user.id} accessing ${pathname}. Error: ${profileError?.message}. Redirecting to unauthorized.`,
109
- );
110
- return NextResponse.redirect(
111
- new URL('/unauthorized?error=profile_issue', request.url),
112
- );
113
- }
114
-
115
- const userRole = profile.role as UserRole;
116
- if (!requiredRoles.includes(userRole)) {
117
- console.warn(
118
- `Proxy: User ${user.id} (Role: ${userRole}) denied access to ${pathname}. Required: ${requiredRoles.join(' OR ')}. Redirecting to unauthorized.`,
119
- );
120
- return NextResponse.redirect(
121
- new URL(
122
- `/unauthorized?path=${pathname}&required=${requiredRoles.join(',')}`,
123
- request.url,
124
- ),
125
- );
126
- }
127
- }
128
- }
129
-
130
- if (response.headers.get('location')) {
131
- return response;
132
- }
133
-
134
- const finalResponse = NextResponse.next({
135
- request: {
136
- headers: requestHeaders,
137
- },
138
- });
139
-
140
- response.cookies.getAll().forEach((cookie) => {
141
- finalResponse.cookies.set(cookie.name, cookie.value, cookie);
142
- });
143
-
144
- if (request.cookies.get(LANGUAGE_COOKIE_KEY)?.value !== currentLocale) {
145
- finalResponse.cookies.set(LANGUAGE_COOKIE_KEY, currentLocale, {
146
- path: '/',
147
- maxAge: 31_536_000,
148
- sameSite: 'lax',
149
- });
150
- }
151
-
152
- if (
153
- pathname === '/sign-in' ||
154
- pathname === '/sign-up' ||
155
- pathname === '/forgot-password'
156
- ) {
157
- finalResponse.headers.set('X-Page-Type', 'auth');
158
- finalResponse.headers.set('X-Prefetch-Priority', 'critical');
159
- } else if (pathname === '/') {
160
- finalResponse.headers.set('X-Page-Type', 'home');
161
- finalResponse.headers.set('X-Prefetch-Priority', 'high');
162
- } else if (pathname === '/articles') {
163
- finalResponse.headers.set('X-Page-Type', 'articles-index');
164
- finalResponse.headers.set('X-Prefetch-Priority', 'high');
165
- } else if (pathname.startsWith('/article/')) {
166
- finalResponse.headers.set('X-Page-Type', 'article');
167
- finalResponse.headers.set('X-Prefetch-Priority', 'medium');
168
- } else {
169
- const segments = pathname.split('/').filter(Boolean);
170
- if (segments.length === 1 && !pathname.startsWith('/cms')) {
171
- finalResponse.headers.set('X-Page-Type', 'dynamic-page');
172
- finalResponse.headers.set('X-Prefetch-Priority', 'medium');
173
- }
174
- }
175
-
176
- const acceptHeader = request.headers.get('accept');
177
- if (acceptHeader && acceptHeader.includes('text/html') && !pathname.startsWith('/api/')) {
178
- finalResponse.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
179
- finalResponse.headers.set('X-BFCache-Applied', 'true');
180
- }
181
-
182
- finalResponse.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
183
- finalResponse.headers.set('X-Frame-Options', 'SAMEORIGIN');
184
- finalResponse.headers.set('X-Content-Type-Options', 'nosniff');
185
- finalResponse.headers.set('Referrer-Policy', 'origin-when-cross-origin');
186
- finalResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
187
- finalResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
188
-
189
- if (process.env.NODE_ENV === 'production') {
190
- const nonceValue = requestHeaders.get('x-nonce');
191
- if (nonceValue) {
192
- const supabaseHostname = new URL(supabaseUrl).hostname;
193
-
194
- const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL;
195
- const r2PublicUrl = process.env.NEXT_PUBLIC_R2_PUBLIC_URL;
196
- const r2BucketName = process.env.R2_BUCKET_NAME;
197
-
198
- let r2Hostnames = '';
199
- if (r2BaseUrl) {
200
- try {
201
- r2Hostnames += ` https://${new URL(r2BaseUrl).hostname}`;
202
- } catch (e) {
203
- console.error('Invalid NEXT_PUBLIC_R2_BASE_URL', e);
204
- }
205
- }
206
- if (r2PublicUrl && r2BucketName) {
207
- try {
208
- const publicHostname = new URL(r2PublicUrl).hostname;
209
- r2Hostnames += ` https://${r2BucketName}.${publicHostname}`;
210
- } catch (e) {
211
- console.error('Invalid NEXT_PUBLIC_R2_PUBLIC_URL', e);
212
- }
213
- }
214
-
215
- const csp = [
216
- "default-src 'self'",
217
- `script-src 'self' blob: data: 'nonce-${nonceValue}' https://vercel.live https://vercel.com`,
218
- "style-src 'self' 'unsafe-inline' https://vercel.live https://vercel.com",
219
- `img-src 'self' data: blob:${r2Hostnames} https://vercel.live https://vercel.com`,
220
- "font-src 'self' https://vercel.live https://assets.vercel.com",
221
- "object-src 'none'",
222
- `connect-src 'self' https://${supabaseHostname} wss://${supabaseHostname}${r2Hostnames} https://vercel.live https://vercel.com`,
223
- "frame-src 'self' blob: data: https://www.youtube.com https://vercel.live https://vercel.com",
224
- "form-action 'self'",
225
- "base-uri 'self'",
226
- ].join('; ');
227
-
228
- finalResponse.headers.set('Content-Security-Policy', csp);
229
- }
230
- }
231
-
232
- const responseForLogging = finalResponse.clone();
233
- const cacheStatus = responseForLogging.headers.get('x-vercel-cache') || 'none';
234
-
235
- if (!pathname.startsWith('/api/')) {
236
- console.log(
237
- JSON.stringify({
238
- type: 'cache',
239
- status: cacheStatus,
240
- path: pathname,
241
- }),
242
- );
243
- }
244
-
245
- return finalResponse;
246
- }
247
-
248
- export const config = {
249
- matcher: [
250
- '/((?!_next/static|_next/image|favicon.ico|auth/.*|sign-in|sign-up|forgot-password|unauthorized|api/auth/.*|api/revalidate|api/revalidate-log).*)',
251
- '/cms/:path*',
252
- ],
253
- };
1
+ import { createServerClient, type CookieOptions } from '@supabase/ssr';
2
+ import { NextResponse, type NextRequest } from 'next/server';
3
+ import type { Database } from '@nextblock-cms/db';
4
+
5
+ type Profile = Database['public']['Tables']['profiles']['Row'];
6
+ type UserRole = Database['public']['Enums']['user_role'];
7
+
8
+ const LANGUAGE_COOKIE_KEY = 'NEXT_USER_LOCALE';
9
+ const DEFAULT_LOCALE = 'en';
10
+ const SUPPORTED_LOCALES = ['en', 'fr'];
11
+ const cacheLoggingEnabled = process.env.NEXTBLOCK_CACHE_LOGGING_ENABLED === 'true';
12
+
13
+ const cmsRoutePermissions: Record<string, UserRole[]> = {
14
+ '/cms': ['WRITER', 'ADMIN'],
15
+ '/cms/admin': ['ADMIN'],
16
+ '/cms/users': ['ADMIN'],
17
+ '/cms/settings': ['ADMIN'],
18
+ };
19
+
20
+ const securityHeaders = [
21
+ ['X-DNS-Prefetch-Control', 'on'],
22
+ ['Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload'],
23
+ ['X-Frame-Options', 'SAMEORIGIN'],
24
+ ['X-Content-Type-Options', 'nosniff'],
25
+ ['Referrer-Policy', 'strict-origin-when-cross-origin'],
26
+ ['Permissions-Policy', 'camera=(), microphone=(), geolocation=()'],
27
+ ['Cross-Origin-Opener-Policy', 'same-origin'],
28
+ ] as const;
29
+
30
+ function getRequiredRolesForPath(pathname: string): UserRole[] | null {
31
+ const sortedPaths = Object.keys(cmsRoutePermissions).sort(
32
+ (a, b) => b.length - a.length,
33
+ );
34
+ for (const specificPath of sortedPaths) {
35
+ if (
36
+ pathname === specificPath ||
37
+ pathname.startsWith(specificPath + (specificPath === '/' ? '' : '/'))
38
+ ) {
39
+ return cmsRoutePermissions[specificPath];
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function getHttpOrigin(value: string | undefined): string | null {
46
+ if (!value) {
47
+ return null;
48
+ }
49
+
50
+ try {
51
+ const parsed = new URL(value);
52
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
53
+ return parsed.origin;
54
+ }
55
+ } catch (error) {
56
+ console.error('Invalid URL used while building CSP sources', error);
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ function uniqueSources(sources: Array<string | null | undefined>): string[] {
63
+ return Array.from(new Set(sources.filter(Boolean) as string[]));
64
+ }
65
+
66
+ function createDirective(name: string, sources: Array<string | null | undefined>): string {
67
+ return `${name} ${uniqueSources(sources).join(' ')}`;
68
+ }
69
+
70
+ function getAssetSources(): string[] {
71
+ const sources: string[] = [];
72
+ const r2BaseOrigin = getHttpOrigin(process.env.NEXT_PUBLIC_R2_BASE_URL);
73
+ const r2PublicOrigin = getHttpOrigin(process.env.NEXT_PUBLIC_R2_PUBLIC_URL);
74
+
75
+ if (r2BaseOrigin) {
76
+ sources.push(r2BaseOrigin);
77
+ }
78
+
79
+ if (r2PublicOrigin) {
80
+ sources.push(r2PublicOrigin);
81
+ }
82
+
83
+ if (r2PublicOrigin && process.env.R2_BUCKET_NAME) {
84
+ const parsed = new URL(r2PublicOrigin);
85
+ sources.push(`${parsed.protocol}//${process.env.R2_BUCKET_NAME}.${parsed.hostname}`);
86
+ }
87
+
88
+ return uniqueSources(sources);
89
+ }
90
+
91
+ function applySecurityHeaders(
92
+ response: NextResponse,
93
+ contentSecurityPolicy?: string | null,
94
+ ): NextResponse {
95
+ for (const [key, value] of securityHeaders) {
96
+ response.headers.set(key, value);
97
+ }
98
+
99
+ if (contentSecurityPolicy) {
100
+ response.headers.set('Content-Security-Policy', contentSecurityPolicy);
101
+ }
102
+
103
+ return response;
104
+ }
105
+
106
+ function createRedirectResponse(
107
+ url: URL,
108
+ contentSecurityPolicy?: string | null,
109
+ ): NextResponse {
110
+ return applySecurityHeaders(NextResponse.redirect(url), contentSecurityPolicy);
111
+ }
112
+
113
+ function createContentSecurityPolicy(nonceValue: string, supabaseUrl: string): string {
114
+ const isDev = process.env.NODE_ENV !== 'production';
115
+ const parsedSupabaseUrl = new URL(supabaseUrl);
116
+ const supabaseOrigin = parsedSupabaseUrl.origin;
117
+ const supabaseRealtimeOrigin = `${parsedSupabaseUrl.protocol === 'https:' ? 'wss:' : 'ws:'}//${parsedSupabaseUrl.host}`;
118
+ const assetSources = getAssetSources();
119
+
120
+ const googleSources = [
121
+ 'https://www.googletagmanager.com',
122
+ 'https://*.googletagmanager.com',
123
+ 'https://www.google-analytics.com',
124
+ 'https://*.google-analytics.com',
125
+ 'https://analytics.google.com',
126
+ 'https://stats.g.doubleclick.net',
127
+ ];
128
+
129
+ const vercelSources = [
130
+ 'https://vercel.live',
131
+ 'https://vercel.com',
132
+ 'https://assets.vercel.com',
133
+ 'https://vitals.vercel-insights.com',
134
+ 'https://*.vercel-insights.com',
135
+ ];
136
+ const vercelToolbarConnectSources = ['wss://ws-us3.pusher.com'];
137
+ const turnstileSources = ['https://challenges.cloudflare.com'];
138
+
139
+ const developmentHttpSources = isDev
140
+ ? [
141
+ 'http://localhost:*',
142
+ 'https://localhost:*',
143
+ 'http://127.0.0.1:*',
144
+ ]
145
+ : [];
146
+ const developmentConnectSources = isDev
147
+ ? [
148
+ ...developmentHttpSources,
149
+ 'ws://localhost:*',
150
+ 'wss://localhost:*',
151
+ 'ws://127.0.0.1:*',
152
+ ]
153
+ : [];
154
+
155
+ const directives = [
156
+ createDirective('default-src', ["'self'"]),
157
+ createDirective(
158
+ 'script-src',
159
+ isDev
160
+ ? [
161
+ "'self'",
162
+ `'nonce-${nonceValue}'`,
163
+ "'unsafe-inline'",
164
+ "'unsafe-eval'",
165
+ 'blob:',
166
+ 'data:',
167
+ ...vercelSources,
168
+ ...turnstileSources,
169
+ ...developmentHttpSources,
170
+ ]
171
+ : [
172
+ "'self'",
173
+ `'nonce-${nonceValue}'`,
174
+ 'blob:',
175
+ ...googleSources,
176
+ ...vercelSources,
177
+ ...turnstileSources,
178
+ ],
179
+ ),
180
+ createDirective('script-src-attr', ["'none'"]),
181
+ createDirective('style-src', [
182
+ "'self'",
183
+ "'unsafe-inline'",
184
+ 'https://vercel.live',
185
+ 'https://vercel.com',
186
+ ]),
187
+ createDirective('img-src', [
188
+ "'self'",
189
+ 'data:',
190
+ 'blob:',
191
+ supabaseOrigin,
192
+ ...assetSources,
193
+ 'https://checkout.freemius.com',
194
+ ...googleSources,
195
+ ...vercelSources,
196
+ ...developmentHttpSources,
197
+ ]),
198
+ createDirective('font-src', [
199
+ "'self'",
200
+ 'data:',
201
+ 'https://vercel.live',
202
+ 'https://assets.vercel.com',
203
+ ]),
204
+ createDirective('connect-src', [
205
+ "'self'",
206
+ supabaseOrigin,
207
+ supabaseRealtimeOrigin,
208
+ ...assetSources,
209
+ ...googleSources,
210
+ ...vercelSources,
211
+ ...vercelToolbarConnectSources,
212
+ ...turnstileSources,
213
+ ...developmentConnectSources,
214
+ ]),
215
+ createDirective('frame-src', [
216
+ "'self'",
217
+ 'blob:',
218
+ 'data:',
219
+ 'https://checkout.freemius.com',
220
+ 'https://www.youtube.com',
221
+ 'https://www.youtube-nocookie.com',
222
+ 'https://player.vimeo.com',
223
+ 'https://vercel.live',
224
+ 'https://vercel.com',
225
+ ...turnstileSources,
226
+ ]),
227
+ createDirective('media-src', ["'self'", 'data:', 'blob:', supabaseOrigin, ...assetSources]),
228
+ createDirective('worker-src', ["'self'", 'blob:']),
229
+ createDirective('manifest-src', ["'self'"]),
230
+ createDirective('object-src', ["'none'"]),
231
+ createDirective('base-uri', ["'self'"]),
232
+ createDirective('form-action', ["'self'"]),
233
+ createDirective('frame-ancestors', ["'self'"]),
234
+ isDev ? null : 'upgrade-insecure-requests',
235
+ ];
236
+
237
+ return directives.filter(Boolean).join('; ');
238
+ }
239
+
240
+ export async function proxy(request: NextRequest) {
241
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
242
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
243
+
244
+ if (!supabaseUrl || !supabaseAnonKey) {
245
+ throw new Error('Missing required Supabase environment variables');
246
+ }
247
+
248
+ const requestHeaders = new Headers(request.headers);
249
+ const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
250
+ const contentSecurityPolicy = createContentSecurityPolicy(nonce, supabaseUrl);
251
+
252
+ requestHeaders.set('x-nonce', nonce);
253
+ if (contentSecurityPolicy) {
254
+ requestHeaders.set('Content-Security-Policy', contentSecurityPolicy);
255
+ }
256
+
257
+ let response = NextResponse.next({
258
+ request: {
259
+ headers: requestHeaders,
260
+ },
261
+ });
262
+
263
+ const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
264
+ cookies: {
265
+ get(name: string) {
266
+ return request.cookies.get(name)?.value;
267
+ },
268
+ set(name: string, value: string, options: CookieOptions) {
269
+ request.cookies.set({ name, value, ...options });
270
+ response = NextResponse.next({ request: { headers: requestHeaders } });
271
+ response.cookies.set({ name, value, ...options });
272
+ },
273
+ remove(name: string, options: CookieOptions) {
274
+ request.cookies.set({ name, value: '', ...options });
275
+ response = NextResponse.next({ request: { headers: requestHeaders } });
276
+ response.cookies.set({ name, value: '', ...options });
277
+ },
278
+ },
279
+ });
280
+
281
+ await supabase.auth.getSession();
282
+
283
+ const cookieLocale = request.cookies.get(LANGUAGE_COOKIE_KEY)?.value;
284
+ let currentLocale = cookieLocale;
285
+
286
+ if (!currentLocale || !SUPPORTED_LOCALES.includes(currentLocale)) {
287
+ currentLocale = DEFAULT_LOCALE;
288
+ }
289
+
290
+ requestHeaders.set('X-User-Locale', currentLocale);
291
+
292
+ const {
293
+ data: { user },
294
+ error: userError,
295
+ } = await supabase.auth.getUser();
296
+ const { pathname } = request.nextUrl;
297
+
298
+ if (pathname.startsWith('/cms')) {
299
+ if (userError || !user) {
300
+ return createRedirectResponse(
301
+ new URL(`/sign-in?redirect=${pathname}`, request.url),
302
+ contentSecurityPolicy,
303
+ );
304
+ }
305
+
306
+ const requiredRoles = getRequiredRolesForPath(pathname);
307
+
308
+ if (requiredRoles && requiredRoles.length > 0) {
309
+ const {
310
+ data: profile,
311
+ error: profileError,
312
+ } = await supabase
313
+ .from('profiles')
314
+ .select('role')
315
+ .eq('id', user.id)
316
+ .single<Pick<Profile, 'role'>>();
317
+
318
+ if (profileError || !profile) {
319
+ console.error(
320
+ `Proxy: Profile error for user ${user.id} accessing ${pathname}. Error: ${profileError?.message}. Redirecting to unauthorized.`,
321
+ );
322
+ return createRedirectResponse(
323
+ new URL('/unauthorized?error=profile_issue', request.url),
324
+ contentSecurityPolicy,
325
+ );
326
+ }
327
+
328
+ const userRole = profile.role as UserRole;
329
+ if (!requiredRoles.includes(userRole)) {
330
+ console.warn(
331
+ `Proxy: User ${user.id} (Role: ${userRole}) denied access to ${pathname}. Required: ${requiredRoles.join(' OR ')}. Redirecting to unauthorized.`,
332
+ );
333
+ return createRedirectResponse(
334
+ new URL(
335
+ `/unauthorized?path=${pathname}&required=${requiredRoles.join(',')}`,
336
+ request.url,
337
+ ),
338
+ contentSecurityPolicy,
339
+ );
340
+ }
341
+ }
342
+ }
343
+
344
+ if (
345
+ user &&
346
+ !pathname.startsWith('/cms') &&
347
+ pathname !== '/profile' &&
348
+ pathname !== '/profile/password' &&
349
+ pathname !== '/checkout/success'
350
+ ) {
351
+ const { data: profile } = await supabase
352
+ .from('profiles')
353
+ .select('role, full_name')
354
+ .eq('id', user.id)
355
+ .single<Pick<Profile, 'role' | 'full_name'>>();
356
+
357
+ if (profile?.role === 'USER' && !profile.full_name?.trim()) {
358
+ return createRedirectResponse(new URL('/profile', request.url), contentSecurityPolicy);
359
+ }
360
+ }
361
+
362
+ if (response.headers.get('location')) {
363
+ return applySecurityHeaders(response, contentSecurityPolicy);
364
+ }
365
+
366
+ const finalResponse = NextResponse.next({
367
+ request: {
368
+ headers: requestHeaders,
369
+ },
370
+ });
371
+
372
+ response.cookies.getAll().forEach((cookie) => {
373
+ finalResponse.cookies.set(cookie.name, cookie.value, cookie);
374
+ });
375
+
376
+ if (request.cookies.get(LANGUAGE_COOKIE_KEY)?.value !== currentLocale) {
377
+ finalResponse.cookies.set(LANGUAGE_COOKIE_KEY, currentLocale, {
378
+ path: '/',
379
+ maxAge: 31_536_000,
380
+ sameSite: 'lax',
381
+ });
382
+ }
383
+
384
+ if (
385
+ pathname === '/sign-in' ||
386
+ pathname === '/sign-up' ||
387
+ pathname === '/forgot-password'
388
+ ) {
389
+ finalResponse.headers.set('X-Page-Type', 'auth');
390
+ finalResponse.headers.set('X-Prefetch-Priority', 'critical');
391
+ } else if (pathname === '/') {
392
+ finalResponse.headers.set('X-Page-Type', 'home');
393
+ finalResponse.headers.set('X-Prefetch-Priority', 'high');
394
+ } else if (pathname === '/articles') {
395
+ finalResponse.headers.set('X-Page-Type', 'articles-index');
396
+ finalResponse.headers.set('X-Prefetch-Priority', 'high');
397
+ } else if (pathname.startsWith('/article/')) {
398
+ finalResponse.headers.set('X-Page-Type', 'article');
399
+ finalResponse.headers.set('X-Prefetch-Priority', 'medium');
400
+ } else {
401
+ const segments = pathname.split('/').filter(Boolean);
402
+ if (segments.length === 1 && !pathname.startsWith('/cms')) {
403
+ finalResponse.headers.set('X-Page-Type', 'dynamic-page');
404
+ finalResponse.headers.set('X-Prefetch-Priority', 'medium');
405
+ }
406
+ }
407
+
408
+ const acceptHeader = request.headers.get('accept');
409
+ if (acceptHeader && acceptHeader.includes('text/html') && !pathname.startsWith('/api/')) {
410
+ finalResponse.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
411
+ finalResponse.headers.set('X-BFCache-Applied', 'true');
412
+ }
413
+
414
+ applySecurityHeaders(finalResponse, contentSecurityPolicy);
415
+
416
+ if (cacheLoggingEnabled && !pathname.startsWith('/api/')) {
417
+ const cacheStatus = finalResponse.headers.get('x-vercel-cache') || 'none';
418
+ console.log(
419
+ JSON.stringify({
420
+ type: 'cache',
421
+ status: cacheStatus,
422
+ path: pathname,
423
+ }),
424
+ );
425
+ }
426
+
427
+ return finalResponse;
428
+ }
429
+
430
+ export const config = {
431
+ matcher: [
432
+ '/((?!_next/static|_next/image|favicon.ico|auth/.*|api/auth/.*|api/revalidate|api/revalidate-log).*)',
433
+ '/cms/:path*',
434
+ ],
435
+ };