create-nextblock 0.15.10 → 0.16.2

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 (266) hide show
  1. package/docker-template/.dockerignore +1 -1
  2. package/libs/db/tsconfig.lib.json +3 -3
  3. package/libs/editor/tsconfig.lib.json +3 -3
  4. package/libs/ui/tsconfig.lib.json +3 -3
  5. package/libs/utils/tsconfig.json +3 -3
  6. package/package.json +50 -50
  7. package/project.json +19 -19
  8. package/templates/nextblock-template/.dockerignore +1 -1
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/AGENTS.md +9 -9
  11. package/templates/nextblock-template/CLAUDE.md +1 -1
  12. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  13. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  14. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  15. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
  16. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  17. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
  18. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
  19. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
  20. package/templates/nextblock-template/app/actions/email.ts +241 -241
  21. package/templates/nextblock-template/app/actions/formActions.ts +245 -245
  22. package/templates/nextblock-template/app/actions/interactions.ts +489 -489
  23. package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
  24. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
  25. package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
  27. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  28. package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
  29. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
  30. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  31. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  32. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  33. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  34. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  35. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  37. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  38. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  39. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  40. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  43. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  44. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  45. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
  46. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
  47. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  48. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  49. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  50. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
  51. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  52. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  53. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  54. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  55. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  56. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  58. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  59. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  60. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  63. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  64. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  65. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
  66. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  67. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  68. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  69. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  70. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  71. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  72. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  73. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
  74. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
  77. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  78. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  79. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  80. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  81. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  82. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  83. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  84. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  85. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  86. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  90. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  91. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  92. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  93. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  96. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  98. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  99. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  100. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  101. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  102. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  103. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
  104. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
  105. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
  106. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
  107. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
  108. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
  109. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
  110. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  111. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  112. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  113. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  114. package/templates/nextblock-template/app/layout.tsx +671 -671
  115. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  116. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  117. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  118. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  119. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  120. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  121. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  122. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  123. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  124. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  125. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  126. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  127. package/templates/nextblock-template/app/providers.tsx +96 -96
  128. package/templates/nextblock-template/app/robots.ts +123 -0
  129. package/templates/nextblock-template/app/sitemap.ts +1 -1
  130. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  131. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  132. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  133. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  134. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  135. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  136. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  137. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  138. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  139. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  140. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  141. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  142. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  143. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  144. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  145. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  146. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  147. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  148. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  149. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  150. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  151. package/templates/nextblock-template/components/Header.tsx +38 -38
  152. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  153. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  154. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  155. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  156. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  157. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  158. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  159. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  160. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  161. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  162. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  163. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  164. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  165. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  166. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  167. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  168. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  169. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  170. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  171. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  172. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  173. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  174. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  175. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  176. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  177. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  178. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  179. package/templates/nextblock-template/components/form-message.tsx +32 -32
  180. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  189. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  192. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  193. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  194. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  195. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  196. package/templates/nextblock-template/docs/README.md +42 -42
  197. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  198. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  199. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  200. package/templates/nextblock-template/index.d.ts +7 -7
  201. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  202. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  203. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  204. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  205. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  206. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  207. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  208. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  209. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  210. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  211. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  212. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  213. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
  215. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  216. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  217. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  218. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  219. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  220. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  221. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  222. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  223. package/templates/nextblock-template/lib/search/types.ts +27 -27
  224. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
  231. package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
  239. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  240. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  241. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  242. package/templates/nextblock-template/lib/site-url.ts +102 -102
  243. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  244. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  245. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  246. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  247. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  248. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  249. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  250. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  251. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  252. package/templates/nextblock-template/next-env.d.ts +0 -1
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/proxy.ts +240 -20
  256. package/templates/nextblock-template/scripts/backup.js +115 -115
  257. package/templates/nextblock-template/scripts/restore.js +385 -385
  258. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  259. package/templates/nextblock-template/tailwind.config.js +25 -25
  260. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  261. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  262. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  263. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  264. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  265. package/tsconfig.base.json +3 -3
  266. package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
@@ -1,8 +1,20 @@
1
1
  import { createServerClient, type CookieOptions } from '@supabase/ssr';
2
2
  import { NextResponse, type NextRequest } from 'next/server';
3
- import type { SupabaseClient } from '@supabase/supabase-js';
3
+ import { createClient, type SupabaseClient } from '@supabase/supabase-js';
4
4
  import type { Database } from '@nextblock-cms/db';
5
+ import {
6
+ buildRedirectIndex,
7
+ matchRedirect,
8
+ type RedirectRule,
9
+ } from '@nextblock-cms/utils/seo';
5
10
  import { resolveSupabaseAnonKey, resolveSupabaseUrl } from './lib/setup/env-status';
11
+ import {
12
+ fetchActiveRedirects,
13
+ isSchemaMissingError,
14
+ isSelfRedirect,
15
+ resolveRedirectTarget,
16
+ shouldSkipRedirectLookup,
17
+ } from './lib/seo/redirect-store';
6
18
  import {
7
19
  LANGUAGE_DETECTION_SETTING_KEY,
8
20
  DEFAULT_LANGUAGE_DETECTION_SETTINGS,
@@ -86,24 +98,13 @@ function isSetupAllowlisted(pathname: string): boolean {
86
98
  // modules persist across requests in a worker, so this avoids a per-request DB hit.
87
99
  let provisionedAdminCache: { value: boolean; expires: number } | null = null;
88
100
 
89
- /**
90
- * A Supabase/PostgREST error that means the table itself is absent i.e. the schema
91
- * was never applied. This is NOT a transient hiccup: it's the signature of a fresh,
92
- * unprovisioned deploy (env injected, migrations not yet run), so the caller treats it
93
- * as "no admin" and funnels traffic to /setup instead of failing open.
94
- * 42P01 = undefined_table (Postgres); PGRST205 = PostgREST "table not in schema cache".
95
- */
96
- function isSchemaMissingError(error: { code?: string; message?: string } | null): boolean {
97
- if (!error) return false;
98
- const code = error.code ?? '';
99
- if (code === '42P01' || code === 'PGRST205') return true;
100
- const message = (error.message ?? '').toLowerCase();
101
- return (
102
- message.includes('does not exist') ||
103
- message.includes('schema cache') ||
104
- message.includes('could not find the table')
105
- );
106
- }
101
+ // `isSchemaMissingError` used to be defined right here, as a private copy. It now
102
+ // lives in ./lib/seo/redirect-store and is imported above, because the redirect
103
+ // lookup needs exactly the same test "did this error mean the table is absent,
104
+ // or merely that the database is having a bad minute?" and a predicate that
105
+ // decides whether a whole site funnels its traffic to /setup must have one
106
+ // definition. The reasoning behind the specific codes it looks for (42P01,
107
+ // PGRST205, and the message sniffing underneath) is documented there.
107
108
 
108
109
  /**
109
110
  * Returns true once the system has a first admin (site_settings.is_admin_created).
@@ -231,6 +232,116 @@ function getLocaleRuntimeConfig(
231
232
  return localeConfigInflight;
232
233
  }
233
234
 
235
+ // ---------------------------------------------------------------------------
236
+ // Managed redirects
237
+ // ---------------------------------------------------------------------------
238
+ //
239
+ // Operator-authored 301/302 rules live in public.cms_redirects and are resolved
240
+ // here rather than in next.config.js, because a redirect is content: an editor who
241
+ // renames a slug needs the old URL to keep working immediately, without a redeploy.
242
+ // The trade is that every request now potentially needs a database answer, so the
243
+ // caching below is not an optimisation — it is the thing that makes the feature
244
+ // affordable at all.
245
+ //
246
+ // The cache mirrors localeConfigCache/localeConfigInflight deliberately, right down
247
+ // to the shape of the value and the `.finally()` that clears the in-flight handle.
248
+ // Proxy modules persist across requests inside a worker, so a module-level entry is
249
+ // shared by every request that worker serves; the in-flight promise is what stops a
250
+ // burst of concurrent misses (a cold worker taking a page's document plus its
251
+ // subresources) from turning into a burst of identical queries.
252
+ //
253
+ // Two TTLs, because the two outcomes mean different things. A successful read —
254
+ // including a successful read of an EMPTY table, which is what almost every site
255
+ // will have — is cached for 60s: rules change at human speed and a minute of
256
+ // staleness after an operator saves one is acceptable. A failure is cached for only
257
+ // 10s, because the most likely cause is an install that has pulled this code but
258
+ // not yet run `npm run db:migrate`, and after that migration lands redirects should
259
+ // start working within seconds rather than after a full minute of a poisoned cache.
260
+ const REDIRECT_CACHE_TTL_MS = 60_000;
261
+ const REDIRECT_NEGATIVE_CACHE_TTL_MS = 10_000;
262
+
263
+ // `null` here means "we could not read the table", which is not the same as an
264
+ // empty Map ("we read it and there are no rules"). Only the empty Map earns the
265
+ // long TTL.
266
+ let redirectIndexCache: { value: Map<string, RedirectRule> | null; expires: number } | null = null;
267
+ let redirectIndexInflight: Promise<Map<string, RedirectRule> | null> | null = null;
268
+
269
+ // The cookie-bound client built inside `proxy()` cannot be used for this lookup: it
270
+ // is constructed further down, after the redirect check has already had to decide.
271
+ // So this is a plain anon client, mirroring libs/db/src/lib/supabase/ssg-client.ts —
272
+ // no cookies, no session persistence, no token refresh, because reading a public
273
+ // table needs none of that and every one of those features would add work to a path
274
+ // that runs in front of the whole site. It is memoised for the life of the worker
275
+ // alongside the credentials it was built from, so a cache hit constructs nothing at
276
+ // all and a credential change (a redeploy with new env) still rebuilds it.
277
+ let redirectLookupClient: { client: SupabaseClient; key: string; url: string } | null = null;
278
+
279
+ function getRedirectLookupClient(supabaseUrl: string, supabaseAnonKey: string): SupabaseClient {
280
+ if (
281
+ redirectLookupClient &&
282
+ redirectLookupClient.url === supabaseUrl &&
283
+ redirectLookupClient.key === supabaseAnonKey
284
+ ) {
285
+ return redirectLookupClient.client;
286
+ }
287
+
288
+ const client = createClient(supabaseUrl, supabaseAnonKey, {
289
+ auth: { autoRefreshToken: false, persistSession: false },
290
+ });
291
+ redirectLookupClient = { client, key: supabaseAnonKey, url: supabaseUrl };
292
+ return client;
293
+ }
294
+
295
+ async function loadRedirectIndex(
296
+ supabaseUrl: string,
297
+ supabaseAnonKey: string,
298
+ ): Promise<Map<string, RedirectRule> | null> {
299
+ const now = Date.now();
300
+ try {
301
+ const rules = await fetchActiveRedirects(getRedirectLookupClient(supabaseUrl, supabaseAnonKey));
302
+
303
+ // fetchActiveRedirects never throws and returns null only for "could not read".
304
+ // That includes the table not existing yet, which is why the negative TTL is
305
+ // short: this is the state a site sits in between deploying the code and
306
+ // running the migration.
307
+ if (rules === null) {
308
+ redirectIndexCache = { value: null, expires: now + REDIRECT_NEGATIVE_CACHE_TTL_MS };
309
+ return null;
310
+ }
311
+
312
+ const index = buildRedirectIndex(rules);
313
+ redirectIndexCache = { value: index, expires: now + REDIRECT_CACHE_TTL_MS };
314
+ return index;
315
+ } catch {
316
+ redirectIndexCache = { value: null, expires: now + REDIRECT_NEGATIVE_CACHE_TTL_MS };
317
+ return null;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Returns the redirect lookup index, or null when the table could not be read.
323
+ *
324
+ * Never rejects: `loadRedirectIndex` swallows everything, so a caller may await this
325
+ * without a try/catch and still be sure a database outage cannot escape into the
326
+ * request pipeline. The caller wraps it anyway, on the principle that a throw
327
+ * anywhere inside the proxy does not break a redirect — it returns a 500 for every
328
+ * page, asset and API route on the site simultaneously.
329
+ */
330
+ function getRedirectIndex(
331
+ supabaseUrl: string,
332
+ supabaseAnonKey: string,
333
+ ): Promise<Map<string, RedirectRule> | null> {
334
+ if (redirectIndexCache && redirectIndexCache.expires > Date.now()) {
335
+ return Promise.resolve(redirectIndexCache.value);
336
+ }
337
+ if (!redirectIndexInflight) {
338
+ redirectIndexInflight = loadRedirectIndex(supabaseUrl, supabaseAnonKey).finally(() => {
339
+ redirectIndexInflight = null;
340
+ });
341
+ }
342
+ return redirectIndexInflight;
343
+ }
344
+
234
345
  function getHttpOrigin(value: string | undefined): string | null {
235
346
  if (!value) {
236
347
  return null;
@@ -292,11 +403,30 @@ function applySecurityHeaders(
292
403
  return response;
293
404
  }
294
405
 
406
+ /**
407
+ * Every redirect this proxy issues goes through here, so that none of them can
408
+ * accidentally ship without the seven security headers and the CSP. A bare
409
+ * `NextResponse.redirect()` returns a response with none of them.
410
+ *
411
+ * The default status is 307, which is what `NextResponse.redirect()` produces on its
412
+ * own — so adding this parameter changed the behaviour of exactly zero existing call
413
+ * sites. 307 is also the right default for the gates below it: they are all
414
+ * "you cannot be here *right now*" decisions (unprovisioned, signed out, wrong role),
415
+ * and a browser that cached a permanent redirect for those would keep bouncing the
416
+ * user long after they had signed in.
417
+ *
418
+ * Managed content redirects are the exception, and the reason the parameter exists:
419
+ * a 301 is the whole point of "this page moved", because it is the code search
420
+ * engines act on. Only {301, 302, 303, 307, 308} are permitted — Next throws
421
+ * RangeError E529 for anything else — which is also why `RedirectStatusCode` is
422
+ * narrowed to 301 | 302 upstream rather than being an open `number`.
423
+ */
295
424
  function createRedirectResponse(
296
425
  url: URL,
297
426
  contentSecurityPolicy?: string | null,
427
+ status = 307,
298
428
  ): NextResponse {
299
- return applySecurityHeaders(NextResponse.redirect(url), contentSecurityPolicy);
429
+ return applySecurityHeaders(NextResponse.redirect(url, status), contentSecurityPolicy);
300
430
  }
301
431
 
302
432
  function createContentSecurityPolicy(nonceValue: string, supabaseUrl: string | undefined): string {
@@ -486,6 +616,96 @@ export async function proxy(request: NextRequest) {
486
616
  return createRedirectResponse(new URL('/setup', request.url), contentSecurityPolicy);
487
617
  }
488
618
 
619
+ // Managed redirects, resolved before anything else touches Supabase.
620
+ //
621
+ // The position of this block is the point. It sits after the unconfigured gate
622
+ // (there is no database to ask before that) and before `supabase.auth.getSession()`,
623
+ // so a visitor following a dead link is answered with a 301 without the proxy
624
+ // having spent a session round-trip and a user lookup on a request whose response
625
+ // body is empty. It also sits before the CMS role checks, which is harmless
626
+ // because `shouldSkipRedirectLookup` refuses to consider /cms at all.
627
+ //
628
+ // The whole block is wrapped in try/catch and fails open. There is no such thing
629
+ // as a redirect important enough to justify a throw here: the proxy runs in front
630
+ // of every route in the app, so an exception escaping this block would return a
631
+ // 500 for the entire site rather than for one stale URL.
632
+ if (!shouldSkipRedirectLookup(pathname)) {
633
+ try {
634
+ const redirectIndex = await getRedirectIndex(
635
+ supabaseUrl as string,
636
+ supabaseAnonKey as string,
637
+ );
638
+ const rule = redirectIndex ? matchRedirect(redirectIndex, pathname) : null;
639
+
640
+ if (rule) {
641
+ // A destination is either a site-relative path or an absolute https URL —
642
+ // the database CHECK constraint allows nothing else. Resolving the relative
643
+ // form against `request.url` preserves the incoming scheme and host, so a
644
+ // rule works identically on localhost, on a preview deployment and in
645
+ // production without the operator ever writing an origin. The resolution
646
+ // (including whether the visitor's query string travels with them, and what
647
+ // to do about a destination the URL parser cannot make sense of) lives in
648
+ // `resolveRedirectTarget` because it is ordinary logic that deserves ordinary
649
+ // tests, and this function body cannot be given any.
650
+ const target = resolveRedirectTarget(rule.destinationPath, {
651
+ search: request.nextUrl.search,
652
+ url: request.url,
653
+ });
654
+
655
+ // `null` means the stored destination does not parse as a URL at all, which
656
+ // is a broken rule rather than a broken request: skip it and let the page
657
+ // render, the same outcome the catch below produces.
658
+ if (target) {
659
+ // Runtime loop guard. `validateRedirectRule` already rejects cycles when a
660
+ // rule is saved and the table forbids source = destination, but neither
661
+ // catches a pair that differs only by normalisation (`/about` -> `/about/`),
662
+ // and neither can see a rule that was inserted straight into the database.
663
+ // A same-path redirect is an infinite loop in the visitor's browser, so it
664
+ // is checked here, against the resolved URL, every single time. Only paths
665
+ // on this origin can loop; an off-site destination is somebody else's
666
+ // problem by definition.
667
+ //
668
+ // Both sides of this comparison are pathnames, which is why carrying the
669
+ // query onto `target` cannot change its answer: `URL.pathname` never
670
+ // contains a query string. Comparing anything wider here — the href, or a
671
+ // path with its search appended — would let a rule that only adds
672
+ // parameters slip past the guard and loop.
673
+ const isSameOrigin = target.origin === request.nextUrl.origin;
674
+ if (!isSameOrigin || !isSelfRedirect(pathname, target.pathname)) {
675
+ const redirectResponse = createRedirectResponse(
676
+ target,
677
+ contentSecurityPolicy,
678
+ rule.statusCode,
679
+ );
680
+
681
+ // The proxy sets no Cache-Control on any of its other redirects, which is
682
+ // fine for the auth gates (they must be re-evaluated per request) but wrong
683
+ // here, so this one is explicit. A 301 is permanent by definition and every
684
+ // hop it saves is a request that never reaches the origin, so an hour of
685
+ // shared caching is free value. A 302 gets `no-cache` because a temporary
686
+ // redirect that a browser has cached is an operational trap: the operator
687
+ // deletes the rule, the site behaves correctly for everyone new, and the
688
+ // people who already hit it keep being redirected with no way to tell them
689
+ // apart or fix it.
690
+ //
691
+ // Caching a Location that now varies with the incoming query is safe
692
+ // because caches key on the full request URL: `/old?utm=a` and `/old?utm=b`
693
+ // are separate entries, so neither can be served the other's destination.
694
+ redirectResponse.headers.set(
695
+ 'Cache-Control',
696
+ rule.statusCode === 301 ? 'public, max-age=3600' : 'no-cache',
697
+ );
698
+
699
+ return redirectResponse;
700
+ }
701
+ }
702
+ }
703
+ } catch (error) {
704
+ // Fail open: log it and let the request render normally.
705
+ console.error(`Proxy: redirect lookup failed for ${pathname}; serving the page.`, error);
706
+ }
707
+ }
708
+
489
709
  let response = NextResponse.next({
490
710
  request: {
491
711
  headers: requestHeaders,
@@ -1,8 +1,8 @@
1
1
  // apps/nextblock/scripts/backup.js
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { spawn } = require('child_process');
5
- const readline = require('readline');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { spawn } = require('child_process');
5
+ const readline = require('readline');
6
6
 
7
7
  // Load environment (database connection string should be in .env.local or .env)
8
8
  require('dotenv').config({ path: '.env.local' }); // adjust path if needed
@@ -35,114 +35,114 @@ const port = connectionUrl.port || '5432';
35
35
  const dbName = connectionUrl.pathname.replace(/^\//, ''); // strip leading '/'
36
36
  const user = connectionUrl.username;
37
37
  const password = connectionUrl.password;
38
- const sslMode = connectionUrl.searchParams.get('sslmode') || 'require'; // default to require SSL
39
-
40
- // Prepare backup directory with timestamp name
41
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // replace colon and dot with hyphen for filename safety
42
-
43
- function sanitizeName(rawName) {
44
- if (!rawName) {
45
- return '';
46
- }
47
- return rawName
48
- .trim()
49
- .replace(/[^\w.-]+/g, '-') // keep letters/numbers/_/./-
50
- .replace(/-+/g, '-')
51
- .replace(/^-|-$/g, '');
52
- }
53
-
54
- function parseNameFromArgs() {
55
- const args = process.argv.slice(2);
56
- for (let i = 0; i < args.length; i += 1) {
57
- const arg = args[i];
58
- if (arg === '--name' || arg === '-n') {
59
- return args[i + 1] ?? '';
60
- }
61
- const match = arg.match(/^--name=(.*)$/);
62
- if (match) {
63
- return match[1];
64
- }
65
- }
66
- if (typeof process.env.BACKUP_NAME === 'string') {
67
- return process.env.BACKUP_NAME;
68
- }
69
- return null;
70
- }
71
-
72
- function promptForName(defaultLabel) {
73
- return new Promise((resolve) => {
74
- const rl = readline.createInterface({
75
- input: process.stdin,
76
- output: process.stdout,
77
- });
78
- rl.question(
79
- `Enter a friendly name for this backup (press Enter to use ${defaultLabel}): `,
80
- (answer) => {
81
- rl.close();
82
- resolve(answer);
83
- }
84
- );
85
- });
86
- }
87
-
88
- function buildFolderName(baseName, rawName) {
89
- const sanitized = sanitizeName(rawName);
90
- if (!sanitized) {
91
- return baseName;
92
- }
93
- return `${baseName}__${sanitized}`;
94
- }
95
-
96
- async function resolveBackupFolderName(baseName) {
97
- const providedName = parseNameFromArgs();
98
- if (providedName !== null) {
99
- return buildFolderName(baseName, providedName);
100
- }
101
- if (process.stdin.isTTY && process.stdout.isTTY) {
102
- const answer = await promptForName(baseName);
103
- return buildFolderName(baseName, answer);
104
- }
105
- return baseName;
106
- }
107
-
108
- (async () => {
109
- let backupFolderName;
110
- try {
111
- backupFolderName = await resolveBackupFolderName(timestamp);
112
- } catch (err) {
113
- console.error(`❌ Failed to determine backup name: ${err.message}`);
114
- process.exit(1);
115
- }
116
-
117
- const backupDir = path.join(__dirname, '../backups', backupFolderName);
118
- fs.mkdirSync(backupDir, { recursive: true });
119
- const dumpFile = path.join(backupDir, 'dump.sql');
120
-
121
- console.log(`🔄 Backing up database to ${dumpFile} ...`);
122
-
123
- // Spawn the pg_dump process
124
- const dumpArgs = [
125
- '--clean', '--if-exists', '--quote-all-identifiers', // include DROP statements:contentReference[oaicite:8]{index=8}
126
- '-h', host,
127
- '-U', user,
128
- '-p', port,
129
- '-d', dbName,
130
- '-f', dumpFile
131
- ];
132
- const envVars = { ...process.env, PGPASSWORD: password, PGSSLMODE: sslMode };
133
- const pgDump = spawn('pg_dump', dumpArgs, { env: envVars });
134
-
135
- // Forward pg_dump errors to console
136
- pgDump.stderr.on('data', (data) => {
137
- process.stderr.write(data);
138
- });
139
-
140
- // Handle process exit
141
- pgDump.on('close', (code) => {
142
- if (code === 0) {
143
- console.log("✅ Backup completed successfully.");
144
- } else {
145
- console.error(`❌ pg_dump exited with code ${code}. Check error output above for details.`);
146
- }
147
- });
148
- })();
38
+ const sslMode = connectionUrl.searchParams.get('sslmode') || 'require'; // default to require SSL
39
+
40
+ // Prepare backup directory with timestamp name
41
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // replace colon and dot with hyphen for filename safety
42
+
43
+ function sanitizeName(rawName) {
44
+ if (!rawName) {
45
+ return '';
46
+ }
47
+ return rawName
48
+ .trim()
49
+ .replace(/[^\w.-]+/g, '-') // keep letters/numbers/_/./-
50
+ .replace(/-+/g, '-')
51
+ .replace(/^-|-$/g, '');
52
+ }
53
+
54
+ function parseNameFromArgs() {
55
+ const args = process.argv.slice(2);
56
+ for (let i = 0; i < args.length; i += 1) {
57
+ const arg = args[i];
58
+ if (arg === '--name' || arg === '-n') {
59
+ return args[i + 1] ?? '';
60
+ }
61
+ const match = arg.match(/^--name=(.*)$/);
62
+ if (match) {
63
+ return match[1];
64
+ }
65
+ }
66
+ if (typeof process.env.BACKUP_NAME === 'string') {
67
+ return process.env.BACKUP_NAME;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function promptForName(defaultLabel) {
73
+ return new Promise((resolve) => {
74
+ const rl = readline.createInterface({
75
+ input: process.stdin,
76
+ output: process.stdout,
77
+ });
78
+ rl.question(
79
+ `Enter a friendly name for this backup (press Enter to use ${defaultLabel}): `,
80
+ (answer) => {
81
+ rl.close();
82
+ resolve(answer);
83
+ }
84
+ );
85
+ });
86
+ }
87
+
88
+ function buildFolderName(baseName, rawName) {
89
+ const sanitized = sanitizeName(rawName);
90
+ if (!sanitized) {
91
+ return baseName;
92
+ }
93
+ return `${baseName}__${sanitized}`;
94
+ }
95
+
96
+ async function resolveBackupFolderName(baseName) {
97
+ const providedName = parseNameFromArgs();
98
+ if (providedName !== null) {
99
+ return buildFolderName(baseName, providedName);
100
+ }
101
+ if (process.stdin.isTTY && process.stdout.isTTY) {
102
+ const answer = await promptForName(baseName);
103
+ return buildFolderName(baseName, answer);
104
+ }
105
+ return baseName;
106
+ }
107
+
108
+ (async () => {
109
+ let backupFolderName;
110
+ try {
111
+ backupFolderName = await resolveBackupFolderName(timestamp);
112
+ } catch (err) {
113
+ console.error(`❌ Failed to determine backup name: ${err.message}`);
114
+ process.exit(1);
115
+ }
116
+
117
+ const backupDir = path.join(__dirname, '../backups', backupFolderName);
118
+ fs.mkdirSync(backupDir, { recursive: true });
119
+ const dumpFile = path.join(backupDir, 'dump.sql');
120
+
121
+ console.log(`🔄 Backing up database to ${dumpFile} ...`);
122
+
123
+ // Spawn the pg_dump process
124
+ const dumpArgs = [
125
+ '--clean', '--if-exists', '--quote-all-identifiers', // include DROP statements:contentReference[oaicite:8]{index=8}
126
+ '-h', host,
127
+ '-U', user,
128
+ '-p', port,
129
+ '-d', dbName,
130
+ '-f', dumpFile
131
+ ];
132
+ const envVars = { ...process.env, PGPASSWORD: password, PGSSLMODE: sslMode };
133
+ const pgDump = spawn('pg_dump', dumpArgs, { env: envVars });
134
+
135
+ // Forward pg_dump errors to console
136
+ pgDump.stderr.on('data', (data) => {
137
+ process.stderr.write(data);
138
+ });
139
+
140
+ // Handle process exit
141
+ pgDump.on('close', (code) => {
142
+ if (code === 0) {
143
+ console.log("✅ Backup completed successfully.");
144
+ } else {
145
+ console.error(`❌ pg_dump exited with code ${code}. Check error output above for details.`);
146
+ }
147
+ });
148
+ })();