create-nextblock 0.16.1 → 0.16.3

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 (264) 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 -221
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
  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/route.ts +4 -4
  30. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +4542 -2904
  31. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  32. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  33. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  34. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  35. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  37. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  38. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  39. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  40. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  41. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
  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 +406 -406
  46. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  47. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  48. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  49. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  50. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  51. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  52. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +40 -0
  53. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
  54. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  55. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  56. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  58. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  59. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  60. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  61. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  63. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  64. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  65. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  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/components/PageForm.tsx +649 -649
  74. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +624 -618
  77. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
  78. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  79. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  80. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  81. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  82. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  83. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  84. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  85. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  86. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  90. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  91. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  92. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  93. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  96. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  98. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  99. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  100. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  101. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  102. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  103. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  104. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  105. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  106. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  107. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  108. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  109. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  110. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  111. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  112. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  113. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  114. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  115. package/templates/nextblock-template/app/layout.tsx +671 -671
  116. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  117. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  118. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  119. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  120. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  121. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  122. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  123. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  124. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  125. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  126. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  127. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  128. package/templates/nextblock-template/app/providers.tsx +96 -96
  129. package/templates/nextblock-template/app/robots.ts +123 -123
  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 -137
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +251 -244
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  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 -86
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  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 -154
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +157 -140
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +384 -350
  231. package/templates/nextblock-template/lib/seo/page-document.ts +463 -412
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -22
  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 +1 -2
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/scripts/backup.js +115 -115
  256. package/templates/nextblock-template/scripts/restore.js +385 -385
  257. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  258. package/templates/nextblock-template/tailwind.config.js +25 -25
  259. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  260. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  261. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  262. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  263. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  264. package/tsconfig.base.json +3 -3
@@ -1,671 +1,671 @@
1
- import '@nextblock-cms/ui/styles/globals.css';
2
- // app/layout.tsx
3
-
4
- import type { Metadata } from 'next';
5
- import Script from 'next/script';
6
- import { Providers } from './providers';
7
- import { DeferredCartDrawer } from '../components/DeferredCartDrawer';
8
- import { CURRENCY_COOKIE_NAME } from '@nextblock-cms/ecommerce/currency-constants';
9
- import { ToasterProvider } from './ToasterProvider';
10
- import { AppShell } from '../components/AppShell';
11
- import { PublicEnvBootstrap } from '../components/PublicEnvBootstrap';
12
- import { ConsentGatedAnalytics } from '../components/privacy/ConsentGatedAnalytics';
13
- import { ConsentBanner } from '../components/privacy/ConsentBanner';
14
- import { getPrivacySettings } from '../lib/privacy/settings';
15
- import { DEFAULT_PRIVACY_SETTINGS } from '../lib/privacy/types';
16
- import {
17
- activeThemeSlugs,
18
- buildThemeCss,
19
- defaultThemeSlug,
20
- type SiteTheme,
21
- } from '../lib/themes/buildThemeCss';
22
- import { SITE_SCRIPT_COLUMNS, type SiteScript } from '../lib/site-scripts/types';
23
- import SiteScripts from '../components/SiteScripts';
24
- import { DeferredSpeedInsights } from '../components/DeferredSpeedInsights';
25
- import { DeferredVisualEditing } from '../components/visual-editing/DeferredVisualEditing';
26
- import {
27
- createClient as createSupabaseServerClient,
28
- getProfileWithRoleServerSide,
29
- } from '@nextblock-cms/db/server';
30
- import { getActiveLanguagesServerSide } from '@nextblock-cms/db/server';
31
- import type { Database } from '@nextblock-cms/db';
32
- import { headers, cookies, draftMode } from 'next/headers';
33
- import { verifyPackageOnline } from '@nextblock-cms/db/server';
34
- import { unstable_cache } from 'next/cache';
35
- import { createStaticSupabaseClient, getSiteSettings } from './lib/site-settings';
36
- import {
37
- DEFAULT_OG_IMAGE,
38
- DEFAULT_OG_IMAGE_WIDTH,
39
- DEFAULT_OG_IMAGE_HEIGHT,
40
- } from './lib/seo';
41
- import { resolveActiveLogo } from '../lib/logos/active-logo';
42
- import {
43
- isSupabaseConfigured,
44
- resolveSupabaseAnonKey,
45
- resolveSupabaseUrl,
46
- } from '../lib/setup/env-status';
47
- import { resolveMediaBaseUrl } from '../lib/storage/provider';
48
- import {
49
- LANGUAGE_DETECTION_SETTING_KEY,
50
- LANGUAGE_DETECTION_CACHE_TAG,
51
- DEFAULT_LANGUAGE_DETECTION_SETTINGS,
52
- normalizeLanguageDetectionSettings,
53
- type LanguageDetectionSettings,
54
- } from '../lib/i18n/detection';
55
-
56
- const defaultUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000';
57
-
58
- const DEFAULT_LOCALE_FOR_LAYOUT = 'en';
59
- const PUBLIC_LAYOUT_REVALIDATE_SECONDS = 60;
60
- const PUBLIC_LAYOUT_LOGO_CACHE_TAG = 'public-layout-logo';
61
- const TRUSTED_TYPES_SCRIPT_STRATEGY =
62
- process.env.NODE_ENV === 'production' ? 'beforeInteractive' : 'afterInteractive';
63
- const TRUSTED_TYPES_BOOTSTRAP = `
64
- (function () {
65
- if (!window.trustedTypes || window.__nextblockTrustedTypesPolicy) return;
66
- try {
67
- window.__nextblockTrustedTypesPolicy = window.trustedTypes.createPolicy('default', {
68
- createHTML: function (value) { return value; },
69
- createScript: function (value) { return value; },
70
- createScriptURL: function (value) { return value; }
71
- });
72
- } catch (error) {
73
- window.__nextblockTrustedTypesPolicy = true;
74
- }
75
- })();
76
- `;
77
-
78
- type Language = Database['public']['Tables']['languages']['Row'];
79
- type StoreCurrency = Database['public']['Tables']['currencies']['Row'];
80
- type NavigationItem = Database['public']['Tables']['navigation_items']['Row'];
81
- type MenuLocation = Database['public']['Enums']['menu_location'];
82
- type HeaderLogo = Database['public']['Tables']['logos']['Row'] & {
83
- media: (Database['public']['Tables']['media']['Row'] & { alt_text: string | null }) | null;
84
- };
85
-
86
- const getCachedLanguages = unstable_cache(
87
- async (): Promise<Language[]> => {
88
- const supabase = createStaticSupabaseClient();
89
- const { data, error } = await supabase
90
- .from('languages')
91
- .select('id, code, name, is_default, is_active, created_at, updated_at')
92
- .order('name', { ascending: true });
93
-
94
- if (error) {
95
- console.error('Error fetching cached languages:', error.message);
96
- return [];
97
- }
98
-
99
- return data || [];
100
- },
101
- ['public-layout-languages'],
102
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
103
- );
104
-
105
- const getCachedLanguageDetectionSettings = unstable_cache(
106
- async (): Promise<LanguageDetectionSettings> => {
107
- const supabase = createStaticSupabaseClient();
108
- const { data, error } = await supabase
109
- .from('site_settings')
110
- .select('value')
111
- .eq('key', LANGUAGE_DETECTION_SETTING_KEY)
112
- .maybeSingle();
113
-
114
- // Absent row or read error = defaults (browser detection, remembered choice).
115
- if (error) {
116
- return { ...DEFAULT_LANGUAGE_DETECTION_SETTINGS };
117
- }
118
- return normalizeLanguageDetectionSettings(data?.value);
119
- },
120
- ['public-language-detection'],
121
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS, tags: [LANGUAGE_DETECTION_CACHE_TAG] }
122
- );
123
-
124
- const getCachedCopyrightSettings = unstable_cache(
125
- async (): Promise<Record<string, string>> => {
126
- const supabase = createStaticSupabaseClient();
127
- const { data, error } = await supabase
128
- .from('site_settings')
129
- .select('value')
130
- .eq('key', 'footer_copyright')
131
- .single();
132
-
133
- if (error || !data) {
134
- console.error('Error fetching cached copyright settings:', error);
135
- return { en: '(c) {year} Nextblock CMS. All rights reserved.' };
136
- }
137
-
138
- return data.value as Record<string, string>;
139
- },
140
- ['public-layout-copyright'],
141
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
142
- );
143
-
144
- const getCachedFooterAttribution = unstable_cache(
145
- async (): Promise<boolean> => {
146
- const supabase = createStaticSupabaseClient();
147
- const { data } = await supabase
148
- .from('site_settings')
149
- .select('value')
150
- .eq('key', 'footer_show_attribution')
151
- .maybeSingle();
152
-
153
- // Absent row = enabled (default); only an explicit `false` disables it.
154
- return data ? data.value !== false : true;
155
- },
156
- ['public-layout-footer-attribution'],
157
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
158
- );
159
-
160
- const getCachedGlobalCss = unstable_cache(
161
- async (): Promise<string> => {
162
- const supabase = createStaticSupabaseClient();
163
- const { data, error } = await supabase
164
- .from('site_settings')
165
- .select('value')
166
- .eq('key', 'global_css')
167
- .single();
168
-
169
- if (error || !data || !data.value) {
170
- return '';
171
- }
172
- if (typeof data.value === 'string') {
173
- if (data.value.startsWith('"') && data.value.endsWith('"')) {
174
- try { return JSON.parse(data.value); } catch { return data.value; }
175
- }
176
- return data.value;
177
- }
178
- return String(data.value);
179
- },
180
- ['public-layout-global-css'],
181
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
182
- );
183
-
184
- const getCachedSiteThemes = unstable_cache(
185
- async (): Promise<SiteTheme[]> => {
186
- const supabase = createStaticSupabaseClient();
187
- const { data, error } = await supabase
188
- .from('site_themes')
189
- .select('id, slug, name, description, icon, color_scheme, tokens, extra_css, is_system, is_default, is_active, sort_order')
190
- .order('sort_order');
191
-
192
- if (error || !data) {
193
- // A missing table (pre-migration install) must not take the site down —
194
- // libs/ui/src/styles/theme.css still ships a working fallback palette.
195
- return [];
196
- }
197
- return data as unknown as SiteTheme[];
198
- },
199
- ['public-layout-site-themes'],
200
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS, tags: ['public-layout-site-themes'] }
201
- );
202
-
203
- const getCachedSiteScripts = unstable_cache(
204
- async (): Promise<SiteScript[]> => {
205
- const supabase = createStaticSupabaseClient();
206
- const { data, error } = await supabase
207
- .from('site_scripts')
208
- .select(SITE_SCRIPT_COLUMNS)
209
- .eq('is_active', true)
210
- .order('sort_order');
211
-
212
- if (error || !data) {
213
- // A missing table (pre-migration install) must not take the site down — the
214
- // site simply renders with no author scripts, exactly as before the feature.
215
- return [];
216
- }
217
-
218
- return data as SiteScript[];
219
- },
220
- ['public-layout-site-scripts'],
221
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS, tags: ['public-layout-site-scripts'] }
222
- );
223
-
224
- const getCachedTranslations = unstable_cache(
225
- async () => {
226
- const supabase = createStaticSupabaseClient();
227
- const { data, error } = await supabase
228
- .from('translations')
229
- .select('key, translations, created_at, updated_at')
230
- .order('key');
231
-
232
- if (error) {
233
- console.error('Error fetching cached translations:', error.message);
234
- return [];
235
- }
236
-
237
- return data || [];
238
- },
239
- ['public-layout-translations'],
240
- {
241
- revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS,
242
- tags: ['public-layout-translations'],
243
- }
244
- );
245
-
246
- const getCachedCurrencies = unstable_cache(
247
- async (): Promise<StoreCurrency[]> => {
248
- const supabase = createStaticSupabaseClient();
249
- const { data, error } = await supabase
250
- .from('currencies')
251
- .select(
252
- 'id, code, symbol, exchange_rate, is_default, is_active, auto_sync_product_prices, auto_update_exchange_rate, exchange_rate_source, exchange_rate_updated_at, rounding_mode, rounding_increment, rounding_charm_amount, created_at, updated_at'
253
- )
254
- .eq('is_active', true)
255
- .order('code', { ascending: true });
256
-
257
- if (error) {
258
- console.error('Error fetching cached currencies:', error.message);
259
- return [];
260
- }
261
-
262
- return data || [];
263
- },
264
- ['public-layout-currencies'],
265
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
266
- );
267
-
268
- const getCachedNavigationMenu = unstable_cache(
269
- async (menuKey: MenuLocation, languageCode: string): Promise<NavigationItem[]> => {
270
- const supabase = createStaticSupabaseClient();
271
-
272
- const { data: language, error: langError } = await supabase
273
- .from('languages')
274
- .select('id')
275
- .eq('code', languageCode)
276
- .single();
277
-
278
- if (langError || !language) {
279
- console.error(
280
- `Error fetching cached navigation language ${languageCode} for ${menuKey}:`,
281
- langError
282
- );
283
- return [];
284
- }
285
-
286
- const { data: items, error: itemsError } = await supabase
287
- .from('navigation_items')
288
- .select('*, pages(slug)')
289
- .eq('menu_key', menuKey)
290
- .eq('language_id', language.id)
291
- .order('parent_id', { nullsFirst: true })
292
- .order('order');
293
-
294
- if (itemsError) {
295
- console.error(
296
- `Error fetching cached navigation items for ${menuKey} (${languageCode}):`,
297
- itemsError
298
- );
299
- return [];
300
- }
301
-
302
- return (items || []).map((item) => ({ ...item, id: Number(item.id) }));
303
- },
304
- ['public-layout-navigation'],
305
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
306
- );
307
-
308
- const getCachedActiveLogo = unstable_cache(
309
- async (): Promise<HeaderLogo | null> => {
310
- try {
311
- const supabase = createStaticSupabaseClient();
312
- // Honor the admin-pinned active logo (site_settings.active_logo_id), else newest.
313
- const logo = await resolveActiveLogo(supabase);
314
- return (logo as HeaderLogo | null) ?? null;
315
- } catch (error) {
316
- console.error('Error fetching cached active logo:', error);
317
- return null;
318
- }
319
- },
320
- ['public-layout-logo'],
321
- { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS, tags: [PUBLIC_LAYOUT_LOGO_CACHE_TAG] }
322
- );
323
-
324
- async function loadLayoutData() {
325
- const headerList = await headers();
326
- const nonce = headerList.get('x-nonce') || '';
327
- const requestPath = headerList.get('x-nextblock-path') || '';
328
-
329
- // Skip the public-chrome data loading when there's nothing to render it on: an
330
- // unconfigured instance, OR the standalone /setup wizard. On /setup, AppShell shows no
331
- // header/footer anyway, and the DB schema may not exist yet (configured-but-pre-migrate)
332
- // — querying it just produces noisy "table not found" errors for data nobody displays.
333
- if (!isSupabaseConfigured() || requestPath.startsWith('/setup')) {
334
- return {
335
- user: null,
336
- profile: null,
337
- serverDeterminedLocale: DEFAULT_LOCALE_FOR_LAYOUT,
338
- availableCurrencies: [] as StoreCurrency[],
339
- serverCurrencyCode: null,
340
- availableLanguages: [] as Language[],
341
- defaultLanguage: null,
342
- translations: [] as Awaited<ReturnType<typeof getCachedTranslations>>,
343
- copyrightText: '',
344
- nonce,
345
- hasSupabaseEnv: false,
346
- headerNavItems: [] as NavigationItem[],
347
- footerNavItems: [] as NavigationItem[],
348
- logo: null as HeaderLogo | null,
349
- canAccessCms: false,
350
- siteTitle: 'NextBlock',
351
- isEcommerceActive: false,
352
- globalCss: '',
353
- siteThemes: [] as SiteTheme[],
354
- siteScripts: [] as SiteScript[],
355
- privacySettings: DEFAULT_PRIVACY_SETTINGS,
356
- footerAttributionEnabled: true,
357
- rememberVisitorChoice: DEFAULT_LANGUAGE_DETECTION_SETTINGS.rememberVisitorChoice,
358
- };
359
- }
360
-
361
- const supabase = createSupabaseServerClient();
362
- const cookieStore = await cookies();
363
-
364
- const xUserLocaleHeader = headerList.get('x-user-locale');
365
- const nextUserLocaleCookie = cookieStore.get('NEXT_USER_LOCALE')?.value;
366
- const serverCurrencyCode = cookieStore.get(CURRENCY_COOKIE_NAME)?.value ?? null;
367
-
368
- let serverDeterminedLocale =
369
- xUserLocaleHeader ??
370
- nextUserLocaleCookie ??
371
- DEFAULT_LOCALE_FOR_LAYOUT;
372
-
373
- const [
374
- {
375
- data: { user },
376
- },
377
- availableLanguagesResult,
378
- currenciesResult,
379
- copyrightSettingsResult,
380
- globalCssResult,
381
- siteThemesResult,
382
- siteScriptsResult,
383
- translationsResult,
384
- isEcommerceActive,
385
- privacySettings,
386
- languageDetectionSettings,
387
- ] = await Promise.all([
388
- supabase.auth.getUser(),
389
- getCachedLanguages().catch(() => getActiveLanguagesServerSide().catch(() => [])),
390
- getCachedCurrencies().catch(() => []),
391
- getCachedCopyrightSettings().catch(() => ({
392
- en: '(c) {year} Nextblock CMS. All rights reserved.',
393
- })),
394
- getCachedGlobalCss().catch(() => ''),
395
- getCachedSiteThemes().catch(() => [] as SiteTheme[]),
396
- getCachedSiteScripts().catch(() => [] as SiteScript[]),
397
- getCachedTranslations().catch(() => []),
398
- verifyPackageOnline('ecommerce').catch(() => false),
399
- getPrivacySettings().catch(() => DEFAULT_PRIVACY_SETTINGS),
400
- getCachedLanguageDetectionSettings().catch(() => ({
401
- ...DEFAULT_LANGUAGE_DETECTION_SETTINGS,
402
- })),
403
- ]);
404
-
405
- // Serve only active languages, matching the proxy's detection set (is_active
406
- // null counts as active). getCachedLanguages / getActiveLanguagesServerSide
407
- // both return every row, so filtering here keeps the public switcher and
408
- // LanguageProvider in step with the locales the proxy will actually honor —
409
- // otherwise picking a deactivated language would ping-pong against the proxy.
410
- const availableLanguages: Language[] = availableLanguagesResult.filter(
411
- (lang) => lang.is_active !== false,
412
- );
413
- const availableCurrencies: StoreCurrency[] = currenciesResult;
414
- const defaultLanguage: Language | null =
415
- availableLanguages.find((lang) => lang.is_default) ?? availableLanguages[0] ?? null;
416
-
417
- if (!availableLanguages.some((lang) => lang.code === serverDeterminedLocale) && defaultLanguage) {
418
- serverDeterminedLocale = defaultLanguage.code;
419
- } else if (!availableLanguages.some((lang) => lang.code === serverDeterminedLocale)) {
420
- serverDeterminedLocale = DEFAULT_LOCALE_FOR_LAYOUT;
421
- }
422
-
423
- const copyrightSettings = copyrightSettingsResult as Record<string, string>;
424
- const fallbackTemplate =
425
- copyrightSettings.en ?? '(c) {year} Nextblock CMS. All rights reserved.';
426
- const templateForLocale = copyrightSettings[serverDeterminedLocale] ?? fallbackTemplate;
427
- const copyrightText = templateForLocale.replace('{year}', new Date().getFullYear().toString());
428
-
429
- const globalCss = typeof globalCssResult === 'string' ? globalCssResult : '';
430
- const siteThemes = Array.isArray(siteThemesResult) ? siteThemesResult : [];
431
- const siteScripts = Array.isArray(siteScriptsResult) ? siteScriptsResult : [];
432
- const translations = Array.isArray(translationsResult) ? translationsResult : [];
433
-
434
- const hasSupabaseEnv = isSupabaseConfigured();
435
-
436
- const [profile, headerNavItems, footerNavItems, logo] = await Promise.all([
437
- user ? getProfileWithRoleServerSide(user.id) : Promise.resolve(null),
438
- getCachedNavigationMenu('HEADER', serverDeterminedLocale).catch(() => []),
439
- getCachedNavigationMenu('FOOTER', serverDeterminedLocale).catch(() => []),
440
- getCachedActiveLogo().catch(() => null),
441
- ]);
442
-
443
- const role = profile?.role ?? null;
444
- const canAccessCms = role === 'ADMIN' || role === 'WRITER';
445
- const { siteTitle } = await getSiteSettings();
446
- const footerAttributionEnabled = await getCachedFooterAttribution().catch(() => true);
447
-
448
- return {
449
- user,
450
- profile,
451
- serverDeterminedLocale,
452
- availableCurrencies,
453
- serverCurrencyCode,
454
- availableLanguages,
455
- defaultLanguage,
456
- translations,
457
- copyrightText,
458
- nonce,
459
- hasSupabaseEnv,
460
- headerNavItems,
461
- footerNavItems,
462
- logo,
463
- canAccessCms,
464
- siteTitle,
465
- isEcommerceActive,
466
- globalCss,
467
- siteThemes,
468
- siteScripts,
469
- privacySettings,
470
- footerAttributionEnabled,
471
- rememberVisitorChoice: languageDetectionSettings.rememberVisitorChoice,
472
- };
473
- }
474
-
475
- export async function generateMetadata(): Promise<Metadata> {
476
- const { siteTitle, siteDescription, siteKeywords } = await getSiteSettings();
477
- const isSandbox = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true';
478
-
479
- return {
480
- metadataBase: new URL(defaultUrl),
481
- title: {
482
- default: siteTitle,
483
- template: `%s | ${siteTitle}`,
484
- },
485
- description: siteDescription,
486
- keywords: siteKeywords,
487
- applicationName: siteTitle,
488
- openGraph: {
489
- title: siteTitle,
490
- description: siteDescription,
491
- url: defaultUrl,
492
- siteName: siteTitle,
493
- images: [
494
- {
495
- url: DEFAULT_OG_IMAGE,
496
- width: DEFAULT_OG_IMAGE_WIDTH,
497
- height: DEFAULT_OG_IMAGE_HEIGHT,
498
- alt: siteTitle,
499
- },
500
- ],
501
- locale: 'en_US',
502
- type: 'website',
503
- },
504
- twitter: {
505
- card: 'summary_large_image',
506
- title: siteTitle,
507
- description: siteDescription,
508
- images: [DEFAULT_OG_IMAGE],
509
- },
510
- icons: {
511
- icon: [
512
- { url: '/favicon/favicon.ico' },
513
- { url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
514
- { url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
515
- ],
516
- apple: [{ url: '/favicon/apple-touch-icon.png' }],
517
- },
518
- manifest: '/favicon/site.webmanifest',
519
- // Sandbox is a copy of production, so keep it out of the index. Use
520
- // `noindex, follow` (not nofollow) so Googlebot still follows internal links,
521
- // recrawls every page, and drops them all — paired with an allow-crawl
522
- // robots.txt (see app/robots.ts) so the noindex is actually seen.
523
- robots: isSandbox ? { index: false, follow: true } : { index: true, follow: true },
524
- };
525
- }
526
-
527
- export default async function RootLayout({
528
- children,
529
- }: Readonly<{
530
- children: React.ReactNode;
531
- }>) {
532
- const {
533
- user,
534
- profile,
535
- serverDeterminedLocale,
536
- availableCurrencies,
537
- serverCurrencyCode,
538
- availableLanguages,
539
- defaultLanguage,
540
- translations,
541
- copyrightText,
542
- nonce,
543
- hasSupabaseEnv,
544
- headerNavItems,
545
- logo,
546
- footerNavItems,
547
- canAccessCms,
548
- siteTitle,
549
- isEcommerceActive,
550
- globalCss,
551
- siteThemes,
552
- siteScripts,
553
- privacySettings,
554
- footerAttributionEnabled,
555
- rememberVisitorChoice,
556
- } = await loadLayoutData();
557
- // Themes are rendered server-side into <head> so the palette is correct on the
558
- // very first paint, before next-themes' blocking script adds the html class.
559
- const themeCss = buildThemeCss(siteThemes);
560
- const themeSlugs = activeThemeSlugs(siteThemes);
561
- const initialTheme = defaultThemeSlug(siteThemes);
562
- const themeCatalog = siteThemes
563
- .filter((theme) => theme.is_active)
564
- .sort((a, b) => a.sort_order - b.sort_order)
565
- .map((theme) => ({
566
- slug: theme.slug,
567
- name: theme.name,
568
- icon: theme.icon,
569
- colorScheme: theme.color_scheme,
570
- }));
571
- const draft = await draftMode();
572
- // GTM container id comes solely from the privacy settings row (site_settings).
573
- // There is intentionally no NEXT_PUBLIC_GTM_ID env fallback — analytics is
574
- // configured in the CMS (Settings -> Privacy), not via build-time env.
575
- const resolvedGtmId = privacySettings.gtm_id || '';
576
- const visualEditingEnabled =
577
- draft.isEnabled || process.env.NEXTBLOCK_VISUAL_EDITING_ENABLED === 'true';
578
- const isVercelDeployment = process.env.VERCEL === '1';
579
- const toolbarEnabled =
580
- process.env.NEXTBLOCK_VERCEL_TOOLBAR_ENABLED === 'true' ||
581
- (isVercelDeployment && visualEditingEnabled);
582
- const Toolbar = toolbarEnabled
583
- ? (await import('@vercel/toolbar/next')).VercelToolbar
584
- : null;
585
-
586
- // Expose the PUBLIC Supabase values (url + anon key — both safe to ship to the browser)
587
- // to the client at runtime via <PublicEnvBootstrap>. In production the client uses the
588
- // build-time-inlined NEXT_PUBLIC_* and these just match; it only matters in local dev,
589
- // where the wizard writes those vars at runtime and the loaded bundle would otherwise
590
- // hold stale empties until a dev-server restart. Read from server process.env (fresh).
591
- const publicSupabaseUrl = resolveSupabaseUrl() || '';
592
- const publicSupabaseAnonKey = resolveSupabaseAnonKey() || '';
593
-
594
- return (
595
- <html lang={serverDeterminedLocale} suppressHydrationWarning>
596
- <head>
597
- <meta name="viewport" content="width=device-width, initial-scale=1" />
598
- {themeCss && <style id="nb-theme-tokens" dangerouslySetInnerHTML={{ __html: themeCss }} />}
599
- {globalCss && <style dangerouslySetInnerHTML={{ __html: globalCss }} />}
600
- <SiteScripts nonce={nonce} placement="head" scripts={siteScripts} />
601
- </head>
602
- <body className="min-h-screen">
603
- <SiteScripts nonce={nonce} placement="body_start" scripts={siteScripts} />
604
- {/* Sets window.__NEXTBLOCK_PUBLIC_ENV__ synchronously during render, before any
605
- descendant calls the browser Supabase client — the local-dev runtime fallback. */}
606
- <PublicEnvBootstrap
607
- url={publicSupabaseUrl}
608
- anonKey={publicSupabaseAnonKey}
609
- r2Base={resolveMediaBaseUrl()}
610
- />
611
- {/* In development this loads after hydration to avoid browser-hidden nonce comparisons. */}
612
- <Script
613
- id="trusted-types-bootstrap"
614
- strategy={TRUSTED_TYPES_SCRIPT_STRATEGY}
615
- nonce={nonce}
616
- dangerouslySetInnerHTML={{ __html: TRUSTED_TYPES_BOOTSTRAP }}
617
- />
618
- <Providers
619
- serverUser={user}
620
- serverProfile={profile}
621
- serverLocale={serverDeterminedLocale}
622
- initialCurrencies={availableCurrencies}
623
- initialCurrencyCode={serverCurrencyCode}
624
- initialAvailableLanguages={availableLanguages}
625
- initialDefaultLanguage={defaultLanguage}
626
- rememberVisitorChoice={rememberVisitorChoice}
627
- translations={translations}
628
- nonce={nonce}
629
- themeSlugs={themeSlugs}
630
- initialTheme={initialTheme}
631
- themeCatalog={themeCatalog}
632
- >
633
- <ToasterProvider />
634
- <AppShell
635
- canAccessCms={canAccessCms}
636
- copyrightText={copyrightText}
637
- corporateFooter={{
638
- legalName: privacySettings.corporate.legal_name,
639
- address: privacySettings.corporate.address,
640
- supportEmail: privacySettings.corporate.support_email,
641
- }}
642
- footerNavItems={footerNavItems}
643
- hasSupabaseEnv={hasSupabaseEnv}
644
- headerNavItems={headerNavItems}
645
- isDraftModeEnabled={draft.isEnabled}
646
- isEcommerceActive={isEcommerceActive}
647
- logo={logo}
648
- showFooterAttribution={footerAttributionEnabled}
649
- siteTitle={siteTitle}
650
- >
651
- {children}
652
- </AppShell>
653
-
654
- {isEcommerceActive && <DeferredCartDrawer />}
655
- {visualEditingEnabled && <DeferredVisualEditing />}
656
- {Toolbar && <Toolbar nonce={nonce} />}
657
- {privacySettings.banner_enabled && <ConsentBanner />}
658
- </Providers>
659
- <DeferredSpeedInsights />
660
- <ConsentGatedAnalytics
661
- gtmId={resolvedGtmId}
662
- gaMeasurementId={privacySettings.ga_measurement_id || ''}
663
- customScripts={privacySettings.custom_scripts}
664
- nonce={nonce}
665
- />
666
- {/* Last in <body> so the DOM these snippets query is already present. */}
667
- <SiteScripts nonce={nonce} placement="body_end" scripts={siteScripts} />
668
- </body>
669
- </html>
670
- );
671
- }
1
+ import '@nextblock-cms/ui/styles/globals.css';
2
+ // app/layout.tsx
3
+
4
+ import type { Metadata } from 'next';
5
+ import Script from 'next/script';
6
+ import { Providers } from './providers';
7
+ import { DeferredCartDrawer } from '../components/DeferredCartDrawer';
8
+ import { CURRENCY_COOKIE_NAME } from '@nextblock-cms/ecommerce/currency-constants';
9
+ import { ToasterProvider } from './ToasterProvider';
10
+ import { AppShell } from '../components/AppShell';
11
+ import { PublicEnvBootstrap } from '../components/PublicEnvBootstrap';
12
+ import { ConsentGatedAnalytics } from '../components/privacy/ConsentGatedAnalytics';
13
+ import { ConsentBanner } from '../components/privacy/ConsentBanner';
14
+ import { getPrivacySettings } from '../lib/privacy/settings';
15
+ import { DEFAULT_PRIVACY_SETTINGS } from '../lib/privacy/types';
16
+ import {
17
+ activeThemeSlugs,
18
+ buildThemeCss,
19
+ defaultThemeSlug,
20
+ type SiteTheme,
21
+ } from '../lib/themes/buildThemeCss';
22
+ import { SITE_SCRIPT_COLUMNS, type SiteScript } from '../lib/site-scripts/types';
23
+ import SiteScripts from '../components/SiteScripts';
24
+ import { DeferredSpeedInsights } from '../components/DeferredSpeedInsights';
25
+ import { DeferredVisualEditing } from '../components/visual-editing/DeferredVisualEditing';
26
+ import {
27
+ createClient as createSupabaseServerClient,
28
+ getProfileWithRoleServerSide,
29
+ } from '@nextblock-cms/db/server';
30
+ import { getActiveLanguagesServerSide } from '@nextblock-cms/db/server';
31
+ import type { Database } from '@nextblock-cms/db';
32
+ import { headers, cookies, draftMode } from 'next/headers';
33
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
34
+ import { unstable_cache } from 'next/cache';
35
+ import { createStaticSupabaseClient, getSiteSettings } from './lib/site-settings';
36
+ import {
37
+ DEFAULT_OG_IMAGE,
38
+ DEFAULT_OG_IMAGE_WIDTH,
39
+ DEFAULT_OG_IMAGE_HEIGHT,
40
+ } from './lib/seo';
41
+ import { resolveActiveLogo } from '../lib/logos/active-logo';
42
+ import {
43
+ isSupabaseConfigured,
44
+ resolveSupabaseAnonKey,
45
+ resolveSupabaseUrl,
46
+ } from '../lib/setup/env-status';
47
+ import { resolveMediaBaseUrl } from '../lib/storage/provider';
48
+ import {
49
+ LANGUAGE_DETECTION_SETTING_KEY,
50
+ LANGUAGE_DETECTION_CACHE_TAG,
51
+ DEFAULT_LANGUAGE_DETECTION_SETTINGS,
52
+ normalizeLanguageDetectionSettings,
53
+ type LanguageDetectionSettings,
54
+ } from '../lib/i18n/detection';
55
+
56
+ const defaultUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000';
57
+
58
+ const DEFAULT_LOCALE_FOR_LAYOUT = 'en';
59
+ const PUBLIC_LAYOUT_REVALIDATE_SECONDS = 60;
60
+ const PUBLIC_LAYOUT_LOGO_CACHE_TAG = 'public-layout-logo';
61
+ const TRUSTED_TYPES_SCRIPT_STRATEGY =
62
+ process.env.NODE_ENV === 'production' ? 'beforeInteractive' : 'afterInteractive';
63
+ const TRUSTED_TYPES_BOOTSTRAP = `
64
+ (function () {
65
+ if (!window.trustedTypes || window.__nextblockTrustedTypesPolicy) return;
66
+ try {
67
+ window.__nextblockTrustedTypesPolicy = window.trustedTypes.createPolicy('default', {
68
+ createHTML: function (value) { return value; },
69
+ createScript: function (value) { return value; },
70
+ createScriptURL: function (value) { return value; }
71
+ });
72
+ } catch (error) {
73
+ window.__nextblockTrustedTypesPolicy = true;
74
+ }
75
+ })();
76
+ `;
77
+
78
+ type Language = Database['public']['Tables']['languages']['Row'];
79
+ type StoreCurrency = Database['public']['Tables']['currencies']['Row'];
80
+ type NavigationItem = Database['public']['Tables']['navigation_items']['Row'];
81
+ type MenuLocation = Database['public']['Enums']['menu_location'];
82
+ type HeaderLogo = Database['public']['Tables']['logos']['Row'] & {
83
+ media: (Database['public']['Tables']['media']['Row'] & { alt_text: string | null }) | null;
84
+ };
85
+
86
+ const getCachedLanguages = unstable_cache(
87
+ async (): Promise<Language[]> => {
88
+ const supabase = createStaticSupabaseClient();
89
+ const { data, error } = await supabase
90
+ .from('languages')
91
+ .select('id, code, name, is_default, is_active, created_at, updated_at')
92
+ .order('name', { ascending: true });
93
+
94
+ if (error) {
95
+ console.error('Error fetching cached languages:', error.message);
96
+ return [];
97
+ }
98
+
99
+ return data || [];
100
+ },
101
+ ['public-layout-languages'],
102
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
103
+ );
104
+
105
+ const getCachedLanguageDetectionSettings = unstable_cache(
106
+ async (): Promise<LanguageDetectionSettings> => {
107
+ const supabase = createStaticSupabaseClient();
108
+ const { data, error } = await supabase
109
+ .from('site_settings')
110
+ .select('value')
111
+ .eq('key', LANGUAGE_DETECTION_SETTING_KEY)
112
+ .maybeSingle();
113
+
114
+ // Absent row or read error = defaults (browser detection, remembered choice).
115
+ if (error) {
116
+ return { ...DEFAULT_LANGUAGE_DETECTION_SETTINGS };
117
+ }
118
+ return normalizeLanguageDetectionSettings(data?.value);
119
+ },
120
+ ['public-language-detection'],
121
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS, tags: [LANGUAGE_DETECTION_CACHE_TAG] }
122
+ );
123
+
124
+ const getCachedCopyrightSettings = unstable_cache(
125
+ async (): Promise<Record<string, string>> => {
126
+ const supabase = createStaticSupabaseClient();
127
+ const { data, error } = await supabase
128
+ .from('site_settings')
129
+ .select('value')
130
+ .eq('key', 'footer_copyright')
131
+ .single();
132
+
133
+ if (error || !data) {
134
+ console.error('Error fetching cached copyright settings:', error);
135
+ return { en: '(c) {year} Nextblock CMS. All rights reserved.' };
136
+ }
137
+
138
+ return data.value as Record<string, string>;
139
+ },
140
+ ['public-layout-copyright'],
141
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
142
+ );
143
+
144
+ const getCachedFooterAttribution = unstable_cache(
145
+ async (): Promise<boolean> => {
146
+ const supabase = createStaticSupabaseClient();
147
+ const { data } = await supabase
148
+ .from('site_settings')
149
+ .select('value')
150
+ .eq('key', 'footer_show_attribution')
151
+ .maybeSingle();
152
+
153
+ // Absent row = enabled (default); only an explicit `false` disables it.
154
+ return data ? data.value !== false : true;
155
+ },
156
+ ['public-layout-footer-attribution'],
157
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
158
+ );
159
+
160
+ const getCachedGlobalCss = unstable_cache(
161
+ async (): Promise<string> => {
162
+ const supabase = createStaticSupabaseClient();
163
+ const { data, error } = await supabase
164
+ .from('site_settings')
165
+ .select('value')
166
+ .eq('key', 'global_css')
167
+ .single();
168
+
169
+ if (error || !data || !data.value) {
170
+ return '';
171
+ }
172
+ if (typeof data.value === 'string') {
173
+ if (data.value.startsWith('"') && data.value.endsWith('"')) {
174
+ try { return JSON.parse(data.value); } catch { return data.value; }
175
+ }
176
+ return data.value;
177
+ }
178
+ return String(data.value);
179
+ },
180
+ ['public-layout-global-css'],
181
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
182
+ );
183
+
184
+ const getCachedSiteThemes = unstable_cache(
185
+ async (): Promise<SiteTheme[]> => {
186
+ const supabase = createStaticSupabaseClient();
187
+ const { data, error } = await supabase
188
+ .from('site_themes')
189
+ .select('id, slug, name, description, icon, color_scheme, tokens, extra_css, is_system, is_default, is_active, sort_order')
190
+ .order('sort_order');
191
+
192
+ if (error || !data) {
193
+ // A missing table (pre-migration install) must not take the site down —
194
+ // libs/ui/src/styles/theme.css still ships a working fallback palette.
195
+ return [];
196
+ }
197
+ return data as unknown as SiteTheme[];
198
+ },
199
+ ['public-layout-site-themes'],
200
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS, tags: ['public-layout-site-themes'] }
201
+ );
202
+
203
+ const getCachedSiteScripts = unstable_cache(
204
+ async (): Promise<SiteScript[]> => {
205
+ const supabase = createStaticSupabaseClient();
206
+ const { data, error } = await supabase
207
+ .from('site_scripts')
208
+ .select(SITE_SCRIPT_COLUMNS)
209
+ .eq('is_active', true)
210
+ .order('sort_order');
211
+
212
+ if (error || !data) {
213
+ // A missing table (pre-migration install) must not take the site down — the
214
+ // site simply renders with no author scripts, exactly as before the feature.
215
+ return [];
216
+ }
217
+
218
+ return data as SiteScript[];
219
+ },
220
+ ['public-layout-site-scripts'],
221
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS, tags: ['public-layout-site-scripts'] }
222
+ );
223
+
224
+ const getCachedTranslations = unstable_cache(
225
+ async () => {
226
+ const supabase = createStaticSupabaseClient();
227
+ const { data, error } = await supabase
228
+ .from('translations')
229
+ .select('key, translations, created_at, updated_at')
230
+ .order('key');
231
+
232
+ if (error) {
233
+ console.error('Error fetching cached translations:', error.message);
234
+ return [];
235
+ }
236
+
237
+ return data || [];
238
+ },
239
+ ['public-layout-translations'],
240
+ {
241
+ revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS,
242
+ tags: ['public-layout-translations'],
243
+ }
244
+ );
245
+
246
+ const getCachedCurrencies = unstable_cache(
247
+ async (): Promise<StoreCurrency[]> => {
248
+ const supabase = createStaticSupabaseClient();
249
+ const { data, error } = await supabase
250
+ .from('currencies')
251
+ .select(
252
+ 'id, code, symbol, exchange_rate, is_default, is_active, auto_sync_product_prices, auto_update_exchange_rate, exchange_rate_source, exchange_rate_updated_at, rounding_mode, rounding_increment, rounding_charm_amount, created_at, updated_at'
253
+ )
254
+ .eq('is_active', true)
255
+ .order('code', { ascending: true });
256
+
257
+ if (error) {
258
+ console.error('Error fetching cached currencies:', error.message);
259
+ return [];
260
+ }
261
+
262
+ return data || [];
263
+ },
264
+ ['public-layout-currencies'],
265
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
266
+ );
267
+
268
+ const getCachedNavigationMenu = unstable_cache(
269
+ async (menuKey: MenuLocation, languageCode: string): Promise<NavigationItem[]> => {
270
+ const supabase = createStaticSupabaseClient();
271
+
272
+ const { data: language, error: langError } = await supabase
273
+ .from('languages')
274
+ .select('id')
275
+ .eq('code', languageCode)
276
+ .single();
277
+
278
+ if (langError || !language) {
279
+ console.error(
280
+ `Error fetching cached navigation language ${languageCode} for ${menuKey}:`,
281
+ langError
282
+ );
283
+ return [];
284
+ }
285
+
286
+ const { data: items, error: itemsError } = await supabase
287
+ .from('navigation_items')
288
+ .select('*, pages(slug)')
289
+ .eq('menu_key', menuKey)
290
+ .eq('language_id', language.id)
291
+ .order('parent_id', { nullsFirst: true })
292
+ .order('order');
293
+
294
+ if (itemsError) {
295
+ console.error(
296
+ `Error fetching cached navigation items for ${menuKey} (${languageCode}):`,
297
+ itemsError
298
+ );
299
+ return [];
300
+ }
301
+
302
+ return (items || []).map((item) => ({ ...item, id: Number(item.id) }));
303
+ },
304
+ ['public-layout-navigation'],
305
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS }
306
+ );
307
+
308
+ const getCachedActiveLogo = unstable_cache(
309
+ async (): Promise<HeaderLogo | null> => {
310
+ try {
311
+ const supabase = createStaticSupabaseClient();
312
+ // Honor the admin-pinned active logo (site_settings.active_logo_id), else newest.
313
+ const logo = await resolveActiveLogo(supabase);
314
+ return (logo as HeaderLogo | null) ?? null;
315
+ } catch (error) {
316
+ console.error('Error fetching cached active logo:', error);
317
+ return null;
318
+ }
319
+ },
320
+ ['public-layout-logo'],
321
+ { revalidate: PUBLIC_LAYOUT_REVALIDATE_SECONDS, tags: [PUBLIC_LAYOUT_LOGO_CACHE_TAG] }
322
+ );
323
+
324
+ async function loadLayoutData() {
325
+ const headerList = await headers();
326
+ const nonce = headerList.get('x-nonce') || '';
327
+ const requestPath = headerList.get('x-nextblock-path') || '';
328
+
329
+ // Skip the public-chrome data loading when there's nothing to render it on: an
330
+ // unconfigured instance, OR the standalone /setup wizard. On /setup, AppShell shows no
331
+ // header/footer anyway, and the DB schema may not exist yet (configured-but-pre-migrate)
332
+ // — querying it just produces noisy "table not found" errors for data nobody displays.
333
+ if (!isSupabaseConfigured() || requestPath.startsWith('/setup')) {
334
+ return {
335
+ user: null,
336
+ profile: null,
337
+ serverDeterminedLocale: DEFAULT_LOCALE_FOR_LAYOUT,
338
+ availableCurrencies: [] as StoreCurrency[],
339
+ serverCurrencyCode: null,
340
+ availableLanguages: [] as Language[],
341
+ defaultLanguage: null,
342
+ translations: [] as Awaited<ReturnType<typeof getCachedTranslations>>,
343
+ copyrightText: '',
344
+ nonce,
345
+ hasSupabaseEnv: false,
346
+ headerNavItems: [] as NavigationItem[],
347
+ footerNavItems: [] as NavigationItem[],
348
+ logo: null as HeaderLogo | null,
349
+ canAccessCms: false,
350
+ siteTitle: 'NextBlock',
351
+ isEcommerceActive: false,
352
+ globalCss: '',
353
+ siteThemes: [] as SiteTheme[],
354
+ siteScripts: [] as SiteScript[],
355
+ privacySettings: DEFAULT_PRIVACY_SETTINGS,
356
+ footerAttributionEnabled: true,
357
+ rememberVisitorChoice: DEFAULT_LANGUAGE_DETECTION_SETTINGS.rememberVisitorChoice,
358
+ };
359
+ }
360
+
361
+ const supabase = createSupabaseServerClient();
362
+ const cookieStore = await cookies();
363
+
364
+ const xUserLocaleHeader = headerList.get('x-user-locale');
365
+ const nextUserLocaleCookie = cookieStore.get('NEXT_USER_LOCALE')?.value;
366
+ const serverCurrencyCode = cookieStore.get(CURRENCY_COOKIE_NAME)?.value ?? null;
367
+
368
+ let serverDeterminedLocale =
369
+ xUserLocaleHeader ??
370
+ nextUserLocaleCookie ??
371
+ DEFAULT_LOCALE_FOR_LAYOUT;
372
+
373
+ const [
374
+ {
375
+ data: { user },
376
+ },
377
+ availableLanguagesResult,
378
+ currenciesResult,
379
+ copyrightSettingsResult,
380
+ globalCssResult,
381
+ siteThemesResult,
382
+ siteScriptsResult,
383
+ translationsResult,
384
+ isEcommerceActive,
385
+ privacySettings,
386
+ languageDetectionSettings,
387
+ ] = await Promise.all([
388
+ supabase.auth.getUser(),
389
+ getCachedLanguages().catch(() => getActiveLanguagesServerSide().catch(() => [])),
390
+ getCachedCurrencies().catch(() => []),
391
+ getCachedCopyrightSettings().catch(() => ({
392
+ en: '(c) {year} Nextblock CMS. All rights reserved.',
393
+ })),
394
+ getCachedGlobalCss().catch(() => ''),
395
+ getCachedSiteThemes().catch(() => [] as SiteTheme[]),
396
+ getCachedSiteScripts().catch(() => [] as SiteScript[]),
397
+ getCachedTranslations().catch(() => []),
398
+ verifyPackageOnline('ecommerce').catch(() => false),
399
+ getPrivacySettings().catch(() => DEFAULT_PRIVACY_SETTINGS),
400
+ getCachedLanguageDetectionSettings().catch(() => ({
401
+ ...DEFAULT_LANGUAGE_DETECTION_SETTINGS,
402
+ })),
403
+ ]);
404
+
405
+ // Serve only active languages, matching the proxy's detection set (is_active
406
+ // null counts as active). getCachedLanguages / getActiveLanguagesServerSide
407
+ // both return every row, so filtering here keeps the public switcher and
408
+ // LanguageProvider in step with the locales the proxy will actually honor —
409
+ // otherwise picking a deactivated language would ping-pong against the proxy.
410
+ const availableLanguages: Language[] = availableLanguagesResult.filter(
411
+ (lang) => lang.is_active !== false,
412
+ );
413
+ const availableCurrencies: StoreCurrency[] = currenciesResult;
414
+ const defaultLanguage: Language | null =
415
+ availableLanguages.find((lang) => lang.is_default) ?? availableLanguages[0] ?? null;
416
+
417
+ if (!availableLanguages.some((lang) => lang.code === serverDeterminedLocale) && defaultLanguage) {
418
+ serverDeterminedLocale = defaultLanguage.code;
419
+ } else if (!availableLanguages.some((lang) => lang.code === serverDeterminedLocale)) {
420
+ serverDeterminedLocale = DEFAULT_LOCALE_FOR_LAYOUT;
421
+ }
422
+
423
+ const copyrightSettings = copyrightSettingsResult as Record<string, string>;
424
+ const fallbackTemplate =
425
+ copyrightSettings.en ?? '(c) {year} Nextblock CMS. All rights reserved.';
426
+ const templateForLocale = copyrightSettings[serverDeterminedLocale] ?? fallbackTemplate;
427
+ const copyrightText = templateForLocale.replace('{year}', new Date().getFullYear().toString());
428
+
429
+ const globalCss = typeof globalCssResult === 'string' ? globalCssResult : '';
430
+ const siteThemes = Array.isArray(siteThemesResult) ? siteThemesResult : [];
431
+ const siteScripts = Array.isArray(siteScriptsResult) ? siteScriptsResult : [];
432
+ const translations = Array.isArray(translationsResult) ? translationsResult : [];
433
+
434
+ const hasSupabaseEnv = isSupabaseConfigured();
435
+
436
+ const [profile, headerNavItems, footerNavItems, logo] = await Promise.all([
437
+ user ? getProfileWithRoleServerSide(user.id) : Promise.resolve(null),
438
+ getCachedNavigationMenu('HEADER', serverDeterminedLocale).catch(() => []),
439
+ getCachedNavigationMenu('FOOTER', serverDeterminedLocale).catch(() => []),
440
+ getCachedActiveLogo().catch(() => null),
441
+ ]);
442
+
443
+ const role = profile?.role ?? null;
444
+ const canAccessCms = role === 'ADMIN' || role === 'WRITER';
445
+ const { siteTitle } = await getSiteSettings();
446
+ const footerAttributionEnabled = await getCachedFooterAttribution().catch(() => true);
447
+
448
+ return {
449
+ user,
450
+ profile,
451
+ serverDeterminedLocale,
452
+ availableCurrencies,
453
+ serverCurrencyCode,
454
+ availableLanguages,
455
+ defaultLanguage,
456
+ translations,
457
+ copyrightText,
458
+ nonce,
459
+ hasSupabaseEnv,
460
+ headerNavItems,
461
+ footerNavItems,
462
+ logo,
463
+ canAccessCms,
464
+ siteTitle,
465
+ isEcommerceActive,
466
+ globalCss,
467
+ siteThemes,
468
+ siteScripts,
469
+ privacySettings,
470
+ footerAttributionEnabled,
471
+ rememberVisitorChoice: languageDetectionSettings.rememberVisitorChoice,
472
+ };
473
+ }
474
+
475
+ export async function generateMetadata(): Promise<Metadata> {
476
+ const { siteTitle, siteDescription, siteKeywords } = await getSiteSettings();
477
+ const isSandbox = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true';
478
+
479
+ return {
480
+ metadataBase: new URL(defaultUrl),
481
+ title: {
482
+ default: siteTitle,
483
+ template: `%s | ${siteTitle}`,
484
+ },
485
+ description: siteDescription,
486
+ keywords: siteKeywords,
487
+ applicationName: siteTitle,
488
+ openGraph: {
489
+ title: siteTitle,
490
+ description: siteDescription,
491
+ url: defaultUrl,
492
+ siteName: siteTitle,
493
+ images: [
494
+ {
495
+ url: DEFAULT_OG_IMAGE,
496
+ width: DEFAULT_OG_IMAGE_WIDTH,
497
+ height: DEFAULT_OG_IMAGE_HEIGHT,
498
+ alt: siteTitle,
499
+ },
500
+ ],
501
+ locale: 'en_US',
502
+ type: 'website',
503
+ },
504
+ twitter: {
505
+ card: 'summary_large_image',
506
+ title: siteTitle,
507
+ description: siteDescription,
508
+ images: [DEFAULT_OG_IMAGE],
509
+ },
510
+ icons: {
511
+ icon: [
512
+ { url: '/favicon/favicon.ico' },
513
+ { url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
514
+ { url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
515
+ ],
516
+ apple: [{ url: '/favicon/apple-touch-icon.png' }],
517
+ },
518
+ manifest: '/favicon/site.webmanifest',
519
+ // Sandbox is a copy of production, so keep it out of the index. Use
520
+ // `noindex, follow` (not nofollow) so Googlebot still follows internal links,
521
+ // recrawls every page, and drops them all — paired with an allow-crawl
522
+ // robots.txt (see app/robots.ts) so the noindex is actually seen.
523
+ robots: isSandbox ? { index: false, follow: true } : { index: true, follow: true },
524
+ };
525
+ }
526
+
527
+ export default async function RootLayout({
528
+ children,
529
+ }: Readonly<{
530
+ children: React.ReactNode;
531
+ }>) {
532
+ const {
533
+ user,
534
+ profile,
535
+ serverDeterminedLocale,
536
+ availableCurrencies,
537
+ serverCurrencyCode,
538
+ availableLanguages,
539
+ defaultLanguage,
540
+ translations,
541
+ copyrightText,
542
+ nonce,
543
+ hasSupabaseEnv,
544
+ headerNavItems,
545
+ logo,
546
+ footerNavItems,
547
+ canAccessCms,
548
+ siteTitle,
549
+ isEcommerceActive,
550
+ globalCss,
551
+ siteThemes,
552
+ siteScripts,
553
+ privacySettings,
554
+ footerAttributionEnabled,
555
+ rememberVisitorChoice,
556
+ } = await loadLayoutData();
557
+ // Themes are rendered server-side into <head> so the palette is correct on the
558
+ // very first paint, before next-themes' blocking script adds the html class.
559
+ const themeCss = buildThemeCss(siteThemes);
560
+ const themeSlugs = activeThemeSlugs(siteThemes);
561
+ const initialTheme = defaultThemeSlug(siteThemes);
562
+ const themeCatalog = siteThemes
563
+ .filter((theme) => theme.is_active)
564
+ .sort((a, b) => a.sort_order - b.sort_order)
565
+ .map((theme) => ({
566
+ slug: theme.slug,
567
+ name: theme.name,
568
+ icon: theme.icon,
569
+ colorScheme: theme.color_scheme,
570
+ }));
571
+ const draft = await draftMode();
572
+ // GTM container id comes solely from the privacy settings row (site_settings).
573
+ // There is intentionally no NEXT_PUBLIC_GTM_ID env fallback — analytics is
574
+ // configured in the CMS (Settings -> Privacy), not via build-time env.
575
+ const resolvedGtmId = privacySettings.gtm_id || '';
576
+ const visualEditingEnabled =
577
+ draft.isEnabled || process.env.NEXTBLOCK_VISUAL_EDITING_ENABLED === 'true';
578
+ const isVercelDeployment = process.env.VERCEL === '1';
579
+ const toolbarEnabled =
580
+ process.env.NEXTBLOCK_VERCEL_TOOLBAR_ENABLED === 'true' ||
581
+ (isVercelDeployment && visualEditingEnabled);
582
+ const Toolbar = toolbarEnabled
583
+ ? (await import('@vercel/toolbar/next')).VercelToolbar
584
+ : null;
585
+
586
+ // Expose the PUBLIC Supabase values (url + anon key — both safe to ship to the browser)
587
+ // to the client at runtime via <PublicEnvBootstrap>. In production the client uses the
588
+ // build-time-inlined NEXT_PUBLIC_* and these just match; it only matters in local dev,
589
+ // where the wizard writes those vars at runtime and the loaded bundle would otherwise
590
+ // hold stale empties until a dev-server restart. Read from server process.env (fresh).
591
+ const publicSupabaseUrl = resolveSupabaseUrl() || '';
592
+ const publicSupabaseAnonKey = resolveSupabaseAnonKey() || '';
593
+
594
+ return (
595
+ <html lang={serverDeterminedLocale} suppressHydrationWarning>
596
+ <head>
597
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
598
+ {themeCss && <style id="nb-theme-tokens" dangerouslySetInnerHTML={{ __html: themeCss }} />}
599
+ {globalCss && <style dangerouslySetInnerHTML={{ __html: globalCss }} />}
600
+ <SiteScripts nonce={nonce} placement="head" scripts={siteScripts} />
601
+ </head>
602
+ <body className="min-h-screen">
603
+ <SiteScripts nonce={nonce} placement="body_start" scripts={siteScripts} />
604
+ {/* Sets window.__NEXTBLOCK_PUBLIC_ENV__ synchronously during render, before any
605
+ descendant calls the browser Supabase client — the local-dev runtime fallback. */}
606
+ <PublicEnvBootstrap
607
+ url={publicSupabaseUrl}
608
+ anonKey={publicSupabaseAnonKey}
609
+ r2Base={resolveMediaBaseUrl()}
610
+ />
611
+ {/* In development this loads after hydration to avoid browser-hidden nonce comparisons. */}
612
+ <Script
613
+ id="trusted-types-bootstrap"
614
+ strategy={TRUSTED_TYPES_SCRIPT_STRATEGY}
615
+ nonce={nonce}
616
+ dangerouslySetInnerHTML={{ __html: TRUSTED_TYPES_BOOTSTRAP }}
617
+ />
618
+ <Providers
619
+ serverUser={user}
620
+ serverProfile={profile}
621
+ serverLocale={serverDeterminedLocale}
622
+ initialCurrencies={availableCurrencies}
623
+ initialCurrencyCode={serverCurrencyCode}
624
+ initialAvailableLanguages={availableLanguages}
625
+ initialDefaultLanguage={defaultLanguage}
626
+ rememberVisitorChoice={rememberVisitorChoice}
627
+ translations={translations}
628
+ nonce={nonce}
629
+ themeSlugs={themeSlugs}
630
+ initialTheme={initialTheme}
631
+ themeCatalog={themeCatalog}
632
+ >
633
+ <ToasterProvider />
634
+ <AppShell
635
+ canAccessCms={canAccessCms}
636
+ copyrightText={copyrightText}
637
+ corporateFooter={{
638
+ legalName: privacySettings.corporate.legal_name,
639
+ address: privacySettings.corporate.address,
640
+ supportEmail: privacySettings.corporate.support_email,
641
+ }}
642
+ footerNavItems={footerNavItems}
643
+ hasSupabaseEnv={hasSupabaseEnv}
644
+ headerNavItems={headerNavItems}
645
+ isDraftModeEnabled={draft.isEnabled}
646
+ isEcommerceActive={isEcommerceActive}
647
+ logo={logo}
648
+ showFooterAttribution={footerAttributionEnabled}
649
+ siteTitle={siteTitle}
650
+ >
651
+ {children}
652
+ </AppShell>
653
+
654
+ {isEcommerceActive && <DeferredCartDrawer />}
655
+ {visualEditingEnabled && <DeferredVisualEditing />}
656
+ {Toolbar && <Toolbar nonce={nonce} />}
657
+ {privacySettings.banner_enabled && <ConsentBanner />}
658
+ </Providers>
659
+ <DeferredSpeedInsights />
660
+ <ConsentGatedAnalytics
661
+ gtmId={resolvedGtmId}
662
+ gaMeasurementId={privacySettings.ga_measurement_id || ''}
663
+ customScripts={privacySettings.custom_scripts}
664
+ nonce={nonce}
665
+ />
666
+ {/* Last in <body> so the DOM these snippets query is already present. */}
667
+ <SiteScripts nonce={nonce} placement="body_end" scripts={siteScripts} />
668
+ </body>
669
+ </html>
670
+ );
671
+ }