create-nextblock 0.16.1 → 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 (258) 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/sandboxResetSql.ts +3092 -3092
  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 -558
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  42. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  43. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  44. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
  45. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  46. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  47. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  48. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  49. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  50. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  51. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  52. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  53. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  54. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  55. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  57. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  58. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  59. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  60. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  61. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  62. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  63. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  64. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  65. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  66. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  67. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  68. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  69. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  70. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
  71. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -618
  72. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  73. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  74. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  75. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  76. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  77. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  78. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  79. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  80. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  81. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  82. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  83. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  84. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  85. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  86. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  87. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  88. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  89. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  90. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  91. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  92. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  93. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  94. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  95. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  96. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  97. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  98. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  99. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  100. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  101. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  102. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  103. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  104. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  105. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  106. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  107. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  108. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  109. package/templates/nextblock-template/app/layout.tsx +671 -671
  110. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  111. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  112. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  113. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  114. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  115. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  116. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  117. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  118. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  119. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  120. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  121. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  122. package/templates/nextblock-template/app/providers.tsx +96 -96
  123. package/templates/nextblock-template/app/robots.ts +123 -123
  124. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  125. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  126. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  127. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  128. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  129. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  130. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  131. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  132. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  133. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  134. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  135. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  136. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  137. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  138. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  139. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  140. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  141. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  142. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  143. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  144. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  145. package/templates/nextblock-template/components/Header.tsx +38 -38
  146. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  147. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  148. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  149. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  150. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  151. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  152. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  153. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  154. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  155. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  156. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  157. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  158. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  159. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  160. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  161. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  162. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  163. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  164. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  165. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  166. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  167. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  168. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  169. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  170. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  171. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  172. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  173. package/templates/nextblock-template/components/form-message.tsx +32 -32
  174. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
  175. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -244
  176. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  177. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  178. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  179. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  180. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  181. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  182. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  183. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  184. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  185. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  186. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  187. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  188. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  189. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  190. package/templates/nextblock-template/docs/README.md +42 -42
  191. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  192. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  193. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  194. package/templates/nextblock-template/index.d.ts +7 -7
  195. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  196. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  197. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  198. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  199. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  200. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  201. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  202. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  203. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  204. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  205. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  206. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  207. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
  208. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  209. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  210. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  211. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  212. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  213. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  214. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  215. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  216. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  217. package/templates/nextblock-template/lib/search/types.ts +27 -27
  218. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
  219. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  220. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  221. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  222. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  223. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -140
  224. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
  225. package/templates/nextblock-template/lib/seo/page-document.ts +412 -412
  226. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  227. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  228. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  229. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  230. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  231. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  232. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -177
  233. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  234. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  235. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  236. package/templates/nextblock-template/lib/site-url.ts +102 -102
  237. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  238. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  239. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  240. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  241. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  242. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  243. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  244. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  245. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  246. package/templates/nextblock-template/next-env.d.ts +1 -2
  247. package/templates/nextblock-template/package.json +1 -1
  248. package/templates/nextblock-template/postcss.config.js +6 -6
  249. package/templates/nextblock-template/scripts/backup.js +115 -115
  250. package/templates/nextblock-template/scripts/restore.js +385 -385
  251. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  252. package/templates/nextblock-template/tailwind.config.js +25 -25
  253. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  254. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  255. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  256. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  257. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  258. 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
+ }