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,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.txt/route.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
+ }