create-nextblock 0.13.9 → 0.13.11

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 (260) hide show
  1. package/docker-template/.dockerignore +23 -23
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/libs/db/tsconfig.lib.json +3 -3
  8. package/libs/editor/tsconfig.lib.json +3 -3
  9. package/libs/ui/tsconfig.lib.json +3 -3
  10. package/libs/utils/tsconfig.json +3 -3
  11. package/package.json +1 -1
  12. package/project.json +19 -19
  13. package/templates/nextblock-template/.browserslistrc +11 -11
  14. package/templates/nextblock-template/.dockerignore +23 -23
  15. package/templates/nextblock-template/README.md +34 -34
  16. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  17. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  18. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  19. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  20. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
  21. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  22. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  23. package/templates/nextblock-template/app/actions/postActions.ts +146 -146
  24. package/templates/nextblock-template/app/actions/productGridActions.ts +40 -40
  25. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  26. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  27. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  28. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  29. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  30. package/templates/nextblock-template/app/api/cms/ecommerce/product-picker/route.ts +151 -151
  31. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  32. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  33. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5609
  35. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  36. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  37. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  38. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  39. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  40. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  41. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  42. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  43. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  44. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  45. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +10 -1
  46. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +5 -1
  47. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  48. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +55 -24
  49. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  50. package/templates/nextblock-template/app/cms/blocks/components/MultiEntityPicker.tsx +263 -251
  51. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  52. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  53. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  54. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  55. package/templates/nextblock-template/app/cms/components/EcommerceActiveContext.tsx +27 -27
  56. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  57. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  58. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  59. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  60. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  61. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  62. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  63. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  64. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  65. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  66. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  67. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  68. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  69. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  70. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  71. package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -189
  72. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  73. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  74. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  75. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  76. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  77. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  78. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  79. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  80. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  81. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  82. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  83. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  84. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  85. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  86. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  87. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  88. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  89. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  90. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  91. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  92. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
  93. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  94. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  95. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  96. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  97. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  98. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  99. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageDetectionPanel.tsx +188 -188
  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/components/DeleteLogoButton.tsx +21 -21
  103. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
  104. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  105. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  106. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  107. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  108. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  109. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  110. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  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/CreateUserForm.tsx +217 -217
  114. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  115. package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -44
  116. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  117. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  118. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  119. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  120. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  121. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  122. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  123. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  124. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  125. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  126. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  127. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  128. package/templates/nextblock-template/app/sitemap.ts +130 -130
  129. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  130. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  131. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  132. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  133. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  134. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  135. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  136. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  137. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  138. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  139. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  140. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  141. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  142. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  143. package/templates/nextblock-template/components/blocks/ProductGridClient.tsx +114 -114
  144. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  145. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +8 -1
  146. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  147. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +29 -23
  148. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  149. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  150. package/templates/nextblock-template/components/form-message.tsx +32 -32
  151. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +69 -0
  152. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -0
  153. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  154. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  155. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  156. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  157. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -464
  158. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  159. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  160. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  161. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  162. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  163. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  164. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  165. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  166. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  167. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  168. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  169. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  170. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  171. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  172. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  173. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  174. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  175. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  176. package/templates/nextblock-template/docs/README.md +39 -39
  177. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  178. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  179. package/templates/nextblock-template/index.d.ts +7 -7
  180. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  181. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  182. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  183. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  184. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  185. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +78 -78
  186. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  187. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +21 -1
  188. package/templates/nextblock-template/lib/blocks/product-grid-data.ts +210 -210
  189. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  190. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  191. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  192. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  193. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  194. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  195. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  196. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  197. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  198. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  199. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  200. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  201. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  202. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  203. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  204. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  205. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  206. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  207. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  208. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  209. package/templates/nextblock-template/lib/i18n/country-languages.ts +247 -247
  210. package/templates/nextblock-template/lib/i18n/detection.test.ts +197 -197
  211. package/templates/nextblock-template/lib/i18n/detection.ts +192 -192
  212. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  213. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  214. package/templates/nextblock-template/lib/media/youtube.ts +81 -0
  215. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  216. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  217. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  218. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  219. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  220. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  221. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  222. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  223. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  224. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  225. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  226. package/templates/nextblock-template/lib/search/types.ts +27 -27
  227. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  228. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  229. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  230. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -82
  231. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  232. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  233. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  234. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  235. package/templates/nextblock-template/lib/site-url.ts +48 -48
  236. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  237. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  238. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  239. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  240. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  241. package/templates/nextblock-template/next-env.d.ts +1 -1
  242. package/templates/nextblock-template/package.json +1 -1
  243. package/templates/nextblock-template/postcss.config.js +6 -6
  244. package/templates/nextblock-template/scripts/backup.js +115 -115
  245. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  246. package/templates/nextblock-template/scripts/restore.js +385 -385
  247. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  248. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  249. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  250. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  251. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  252. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  253. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  254. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  255. package/templates/nextblock-template/tailwind.config.js +25 -25
  256. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  257. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  258. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  259. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  260. package/tsconfig.base.json +3 -3
@@ -1,192 +1,192 @@
1
- // Pure language-detection helpers shared by proxy.ts (edge), app/layout.tsx and
2
- // the CMS languages settings. Keep this module free of server-only / next/*
3
- // imports so the proxy can use it.
4
- import { COUNTRY_PRIMARY_LANGUAGES } from './country-languages';
5
-
6
- /** site_settings key holding the detection config (non-sensitive, anon-readable). */
7
- export const LANGUAGE_DETECTION_SETTING_KEY = 'language_detection_settings';
8
-
9
- /** next/cache tag for the layout's cached read of the detection settings. */
10
- export const LANGUAGE_DETECTION_CACHE_TAG = 'public-language-detection';
11
-
12
- export const LANGUAGE_DETECTION_MODES = [
13
- 'browser',
14
- 'country',
15
- 'browser_then_country',
16
- 'default',
17
- ] as const;
18
-
19
- export type LanguageDetectionMode = (typeof LANGUAGE_DETECTION_MODES)[number];
20
-
21
- export interface LanguageDetectionSettings {
22
- /** How the first language served to a new visitor is chosen. */
23
- mode: LanguageDetectionMode;
24
- /** true = persist the visitor's language for a year; false = session-only cookie. */
25
- rememberVisitorChoice: boolean;
26
- }
27
-
28
- // "browser" matches the long-standing page-level Accept-Language fallback, so an
29
- // absent row keeps today's intended behavior.
30
- export const DEFAULT_LANGUAGE_DETECTION_SETTINGS: LanguageDetectionSettings = {
31
- mode: 'browser',
32
- rememberVisitorChoice: true,
33
- };
34
-
35
- /** Coerce an arbitrary site_settings jsonb value into valid settings. */
36
- export function normalizeLanguageDetectionSettings(value: unknown): LanguageDetectionSettings {
37
- const fallback = DEFAULT_LANGUAGE_DETECTION_SETTINGS;
38
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
39
- return { ...fallback };
40
- }
41
- const record = value as Record<string, unknown>;
42
- const mode = LANGUAGE_DETECTION_MODES.includes(record.mode as LanguageDetectionMode)
43
- ? (record.mode as LanguageDetectionMode)
44
- : fallback.mode;
45
- const rememberVisitorChoice =
46
- typeof record.rememberVisitorChoice === 'boolean'
47
- ? record.rememberVisitorChoice
48
- : fallback.rememberVisitorChoice;
49
- return { mode, rememberVisitorChoice };
50
- }
51
-
52
- export interface AcceptLanguageEntry {
53
- /** Full lowercased tag, e.g. "fr-ca". */
54
- tag: string;
55
- /** Base language, e.g. "fr". */
56
- base: string;
57
- quality: number;
58
- }
59
-
60
- /** Parse an Accept-Language header into tags sorted by q-value (wildcards and q=0 dropped). */
61
- export function parseAcceptLanguage(header: string | null | undefined): AcceptLanguageEntry[] {
62
- if (!header) return [];
63
- const entries: AcceptLanguageEntry[] = [];
64
- for (const part of header.split(',')) {
65
- const [rawTag, ...params] = part.trim().split(';');
66
- const tag = rawTag?.trim().toLowerCase();
67
- if (!tag || tag === '*' || !/^[a-z]{1,8}(-[a-z0-9]{1,8})*$/.test(tag)) continue;
68
- let quality = 1;
69
- for (const param of params) {
70
- const eq = param.indexOf('=');
71
- if (eq === -1) continue;
72
- if (param.slice(0, eq).trim() !== 'q') continue;
73
- const parsed = Number(param.slice(eq + 1).trim());
74
- if (!Number.isNaN(parsed)) quality = Math.min(Math.max(parsed, 0), 1);
75
- }
76
- if (quality <= 0) continue;
77
- entries.push({ tag, base: tag.split('-')[0] as string, quality });
78
- }
79
- // Stable sort: q descending, original order preserved among equals.
80
- return entries
81
- .map((entry, index) => ({ entry, index }))
82
- .sort((a, b) => b.entry.quality - a.entry.quality || a.index - b.index)
83
- .map(({ entry }) => entry);
84
- }
85
-
86
- /**
87
- * Match ordered candidate tags against the site's language codes.
88
- * Candidate priority wins: for each candidate (in order) try, exact tag
89
- * ("fr-ca" = "fr-CA"), then candidate base to site code ("fr-CA" -> "fr"),
90
- * then candidate base to site code base ("fr" -> "fr-CA") — so a visitor's
91
- * top language matched loosely beats a lower preference matched exactly.
92
- * Returns the site code as configured (original casing) or null.
93
- */
94
- export function matchLocale(candidates: string[], availableCodes: string[]): string | null {
95
- if (candidates.length === 0 || availableCodes.length === 0) return null;
96
- const available = availableCodes.map((code) => ({
97
- code,
98
- lower: code.toLowerCase(),
99
- base: code.toLowerCase().split('-')[0] as string,
100
- }));
101
-
102
- for (const candidate of candidates) {
103
- const tag = candidate.toLowerCase();
104
- const base = tag.split('-')[0] as string;
105
- const match =
106
- available.find((lang) => lang.lower === tag) ??
107
- available.find((lang) => lang.lower === base) ??
108
- available.find((lang) => lang.base === base);
109
- if (match) return match.code;
110
- }
111
- return null;
112
- }
113
-
114
- // Geo country headers injected by the popular hosts/CDNs, in precedence order.
115
- const COUNTRY_HEADER_NAMES = [
116
- 'x-vercel-ip-country', // Vercel
117
- 'cf-ipcountry', // Cloudflare
118
- 'cloudfront-viewer-country', // AWS CloudFront
119
- 'x-country-code', // generic reverse proxies
120
- ] as const;
121
-
122
- /** Read the visitor's ISO 3166-1 alpha-2 country from host geo headers, if any. */
123
- export function getCountryFromRequestHeaders(headers: {
124
- get(name: string): string | null;
125
- }): string | null {
126
- for (const name of COUNTRY_HEADER_NAMES) {
127
- const value = headers.get(name)?.trim().toUpperCase();
128
- // Cloudflare uses XX/T1 for unknown/Tor; only accept real alpha-2 codes.
129
- if (value && /^[A-Z]{2}$/.test(value) && value !== 'XX' && value !== 'T1') {
130
- return value;
131
- }
132
- }
133
- return null;
134
- }
135
-
136
- export function detectLocaleFromBrowser(
137
- acceptLanguage: string | null | undefined,
138
- availableCodes: string[],
139
- ): string | null {
140
- return matchLocale(
141
- parseAcceptLanguage(acceptLanguage).map((entry) => entry.tag),
142
- availableCodes,
143
- );
144
- }
145
-
146
- export function detectLocaleFromCountry(
147
- countryCode: string | null | undefined,
148
- availableCodes: string[],
149
- ): string | null {
150
- if (!countryCode) return null;
151
- const candidates = COUNTRY_PRIMARY_LANGUAGES[countryCode.toUpperCase()];
152
- if (!candidates || candidates.length === 0) return null;
153
- return matchLocale(candidates, availableCodes);
154
- }
155
-
156
- export interface ResolveDetectedLocaleInput {
157
- mode: LanguageDetectionMode;
158
- acceptLanguage: string | null | undefined;
159
- countryCode: string | null | undefined;
160
- availableCodes: string[];
161
- defaultCode: string;
162
- }
163
-
164
- /**
165
- * Pick the first language for a visitor with no (valid) language cookie.
166
- * Always falls back to the site's default language when detection finds no match.
167
- */
168
- export function resolveDetectedLocale({
169
- mode,
170
- acceptLanguage,
171
- countryCode,
172
- availableCodes,
173
- defaultCode,
174
- }: ResolveDetectedLocaleInput): string {
175
- let detected: string | null = null;
176
- switch (mode) {
177
- case 'browser':
178
- detected = detectLocaleFromBrowser(acceptLanguage, availableCodes);
179
- break;
180
- case 'country':
181
- detected = detectLocaleFromCountry(countryCode, availableCodes);
182
- break;
183
- case 'browser_then_country':
184
- detected =
185
- detectLocaleFromBrowser(acceptLanguage, availableCodes) ??
186
- detectLocaleFromCountry(countryCode, availableCodes);
187
- break;
188
- case 'default':
189
- break;
190
- }
191
- return detected ?? defaultCode;
192
- }
1
+ // Pure language-detection helpers shared by proxy.ts (edge), app/layout.tsx and
2
+ // the CMS languages settings. Keep this module free of server-only / next/*
3
+ // imports so the proxy can use it.
4
+ import { COUNTRY_PRIMARY_LANGUAGES } from './country-languages';
5
+
6
+ /** site_settings key holding the detection config (non-sensitive, anon-readable). */
7
+ export const LANGUAGE_DETECTION_SETTING_KEY = 'language_detection_settings';
8
+
9
+ /** next/cache tag for the layout's cached read of the detection settings. */
10
+ export const LANGUAGE_DETECTION_CACHE_TAG = 'public-language-detection';
11
+
12
+ export const LANGUAGE_DETECTION_MODES = [
13
+ 'browser',
14
+ 'country',
15
+ 'browser_then_country',
16
+ 'default',
17
+ ] as const;
18
+
19
+ export type LanguageDetectionMode = (typeof LANGUAGE_DETECTION_MODES)[number];
20
+
21
+ export interface LanguageDetectionSettings {
22
+ /** How the first language served to a new visitor is chosen. */
23
+ mode: LanguageDetectionMode;
24
+ /** true = persist the visitor's language for a year; false = session-only cookie. */
25
+ rememberVisitorChoice: boolean;
26
+ }
27
+
28
+ // "browser" matches the long-standing page-level Accept-Language fallback, so an
29
+ // absent row keeps today's intended behavior.
30
+ export const DEFAULT_LANGUAGE_DETECTION_SETTINGS: LanguageDetectionSettings = {
31
+ mode: 'browser',
32
+ rememberVisitorChoice: true,
33
+ };
34
+
35
+ /** Coerce an arbitrary site_settings jsonb value into valid settings. */
36
+ export function normalizeLanguageDetectionSettings(value: unknown): LanguageDetectionSettings {
37
+ const fallback = DEFAULT_LANGUAGE_DETECTION_SETTINGS;
38
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
39
+ return { ...fallback };
40
+ }
41
+ const record = value as Record<string, unknown>;
42
+ const mode = LANGUAGE_DETECTION_MODES.includes(record.mode as LanguageDetectionMode)
43
+ ? (record.mode as LanguageDetectionMode)
44
+ : fallback.mode;
45
+ const rememberVisitorChoice =
46
+ typeof record.rememberVisitorChoice === 'boolean'
47
+ ? record.rememberVisitorChoice
48
+ : fallback.rememberVisitorChoice;
49
+ return { mode, rememberVisitorChoice };
50
+ }
51
+
52
+ export interface AcceptLanguageEntry {
53
+ /** Full lowercased tag, e.g. "fr-ca". */
54
+ tag: string;
55
+ /** Base language, e.g. "fr". */
56
+ base: string;
57
+ quality: number;
58
+ }
59
+
60
+ /** Parse an Accept-Language header into tags sorted by q-value (wildcards and q=0 dropped). */
61
+ export function parseAcceptLanguage(header: string | null | undefined): AcceptLanguageEntry[] {
62
+ if (!header) return [];
63
+ const entries: AcceptLanguageEntry[] = [];
64
+ for (const part of header.split(',')) {
65
+ const [rawTag, ...params] = part.trim().split(';');
66
+ const tag = rawTag?.trim().toLowerCase();
67
+ if (!tag || tag === '*' || !/^[a-z]{1,8}(-[a-z0-9]{1,8})*$/.test(tag)) continue;
68
+ let quality = 1;
69
+ for (const param of params) {
70
+ const eq = param.indexOf('=');
71
+ if (eq === -1) continue;
72
+ if (param.slice(0, eq).trim() !== 'q') continue;
73
+ const parsed = Number(param.slice(eq + 1).trim());
74
+ if (!Number.isNaN(parsed)) quality = Math.min(Math.max(parsed, 0), 1);
75
+ }
76
+ if (quality <= 0) continue;
77
+ entries.push({ tag, base: tag.split('-')[0] as string, quality });
78
+ }
79
+ // Stable sort: q descending, original order preserved among equals.
80
+ return entries
81
+ .map((entry, index) => ({ entry, index }))
82
+ .sort((a, b) => b.entry.quality - a.entry.quality || a.index - b.index)
83
+ .map(({ entry }) => entry);
84
+ }
85
+
86
+ /**
87
+ * Match ordered candidate tags against the site's language codes.
88
+ * Candidate priority wins: for each candidate (in order) try, exact tag
89
+ * ("fr-ca" = "fr-CA"), then candidate base to site code ("fr-CA" -> "fr"),
90
+ * then candidate base to site code base ("fr" -> "fr-CA") — so a visitor's
91
+ * top language matched loosely beats a lower preference matched exactly.
92
+ * Returns the site code as configured (original casing) or null.
93
+ */
94
+ export function matchLocale(candidates: string[], availableCodes: string[]): string | null {
95
+ if (candidates.length === 0 || availableCodes.length === 0) return null;
96
+ const available = availableCodes.map((code) => ({
97
+ code,
98
+ lower: code.toLowerCase(),
99
+ base: code.toLowerCase().split('-')[0] as string,
100
+ }));
101
+
102
+ for (const candidate of candidates) {
103
+ const tag = candidate.toLowerCase();
104
+ const base = tag.split('-')[0] as string;
105
+ const match =
106
+ available.find((lang) => lang.lower === tag) ??
107
+ available.find((lang) => lang.lower === base) ??
108
+ available.find((lang) => lang.base === base);
109
+ if (match) return match.code;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ // Geo country headers injected by the popular hosts/CDNs, in precedence order.
115
+ const COUNTRY_HEADER_NAMES = [
116
+ 'x-vercel-ip-country', // Vercel
117
+ 'cf-ipcountry', // Cloudflare
118
+ 'cloudfront-viewer-country', // AWS CloudFront
119
+ 'x-country-code', // generic reverse proxies
120
+ ] as const;
121
+
122
+ /** Read the visitor's ISO 3166-1 alpha-2 country from host geo headers, if any. */
123
+ export function getCountryFromRequestHeaders(headers: {
124
+ get(name: string): string | null;
125
+ }): string | null {
126
+ for (const name of COUNTRY_HEADER_NAMES) {
127
+ const value = headers.get(name)?.trim().toUpperCase();
128
+ // Cloudflare uses XX/T1 for unknown/Tor; only accept real alpha-2 codes.
129
+ if (value && /^[A-Z]{2}$/.test(value) && value !== 'XX' && value !== 'T1') {
130
+ return value;
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
136
+ export function detectLocaleFromBrowser(
137
+ acceptLanguage: string | null | undefined,
138
+ availableCodes: string[],
139
+ ): string | null {
140
+ return matchLocale(
141
+ parseAcceptLanguage(acceptLanguage).map((entry) => entry.tag),
142
+ availableCodes,
143
+ );
144
+ }
145
+
146
+ export function detectLocaleFromCountry(
147
+ countryCode: string | null | undefined,
148
+ availableCodes: string[],
149
+ ): string | null {
150
+ if (!countryCode) return null;
151
+ const candidates = COUNTRY_PRIMARY_LANGUAGES[countryCode.toUpperCase()];
152
+ if (!candidates || candidates.length === 0) return null;
153
+ return matchLocale(candidates, availableCodes);
154
+ }
155
+
156
+ export interface ResolveDetectedLocaleInput {
157
+ mode: LanguageDetectionMode;
158
+ acceptLanguage: string | null | undefined;
159
+ countryCode: string | null | undefined;
160
+ availableCodes: string[];
161
+ defaultCode: string;
162
+ }
163
+
164
+ /**
165
+ * Pick the first language for a visitor with no (valid) language cookie.
166
+ * Always falls back to the site's default language when detection finds no match.
167
+ */
168
+ export function resolveDetectedLocale({
169
+ mode,
170
+ acceptLanguage,
171
+ countryCode,
172
+ availableCodes,
173
+ defaultCode,
174
+ }: ResolveDetectedLocaleInput): string {
175
+ let detected: string | null = null;
176
+ switch (mode) {
177
+ case 'browser':
178
+ detected = detectLocaleFromBrowser(acceptLanguage, availableCodes);
179
+ break;
180
+ case 'country':
181
+ detected = detectLocaleFromCountry(countryCode, availableCodes);
182
+ break;
183
+ case 'browser_then_country':
184
+ detected =
185
+ detectLocaleFromBrowser(acceptLanguage, availableCodes) ??
186
+ detectLocaleFromCountry(countryCode, availableCodes);
187
+ break;
188
+ case 'default':
189
+ break;
190
+ }
191
+ return detected ?? defaultCode;
192
+ }
@@ -1,53 +1,53 @@
1
- import type { SupabaseClient } from '@supabase/supabase-js';
2
- import type { Database } from '@nextblock-cms/db';
3
-
4
- // The single, shared definition of "the active logo", used by the public header, the CMS
5
- // branding page, transactional-email branding, and invoices. The operator can pin a
6
- // specific logo (stored as `site_settings.active_logo_id`); when none is pinned we fall
7
- // back to the most recently created logo (the historical default). The setting key is a
8
- // plain, non-secret `site_settings` row, so it is publicly readable under RLS (the public
9
- // header can read it with the anon client) and writable by ADMIN/WRITER.
10
-
11
- /** site_settings key holding the admin-selected active logo id (a `logos.id` uuid string). */
12
- export const ACTIVE_LOGO_SETTING_KEY = 'active_logo_id';
13
-
14
- // Accept any of the app's Supabase clients (request-scoped, static/anon, or service-role).
15
- type AnyClient = SupabaseClient<Database>;
16
-
17
- /** The admin-pinned active logo id, or null when none has been chosen. */
18
- export async function resolveActiveLogoId(client: AnyClient): Promise<string | null> {
19
- const { data } = await client
20
- .from('site_settings')
21
- .select('value')
22
- .eq('key', ACTIVE_LOGO_SETTING_KEY)
23
- .maybeSingle();
24
- const value = data?.value;
25
- return typeof value === 'string' && value.length > 0 ? value : null;
26
- }
27
-
28
- /**
29
- * Resolve the active logo row (joined with its media) for the given client: the pinned
30
- * logo when set and still present, otherwise the most recently created logo. Returns the
31
- * raw row; callers cast to their local logo type. Returns null when there are no logos.
32
- */
33
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
- export async function resolveActiveLogo(client: AnyClient): Promise<any> {
35
- const activeId = await resolveActiveLogoId(client);
36
-
37
- if (activeId) {
38
- const { data } = await client
39
- .from('logos')
40
- .select('*, media:media_id (*)')
41
- .eq('id', activeId)
42
- .maybeSingle();
43
- if (data) return data;
44
- }
45
-
46
- const { data } = await client
47
- .from('logos')
48
- .select('*, media:media_id (*)')
49
- .order('created_at', { ascending: false })
50
- .limit(1)
51
- .maybeSingle();
52
- return data ?? null;
53
- }
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { Database } from '@nextblock-cms/db';
3
+
4
+ // The single, shared definition of "the active logo", used by the public header, the CMS
5
+ // branding page, transactional-email branding, and invoices. The operator can pin a
6
+ // specific logo (stored as `site_settings.active_logo_id`); when none is pinned we fall
7
+ // back to the most recently created logo (the historical default). The setting key is a
8
+ // plain, non-secret `site_settings` row, so it is publicly readable under RLS (the public
9
+ // header can read it with the anon client) and writable by ADMIN/WRITER.
10
+
11
+ /** site_settings key holding the admin-selected active logo id (a `logos.id` uuid string). */
12
+ export const ACTIVE_LOGO_SETTING_KEY = 'active_logo_id';
13
+
14
+ // Accept any of the app's Supabase clients (request-scoped, static/anon, or service-role).
15
+ type AnyClient = SupabaseClient<Database>;
16
+
17
+ /** The admin-pinned active logo id, or null when none has been chosen. */
18
+ export async function resolveActiveLogoId(client: AnyClient): Promise<string | null> {
19
+ const { data } = await client
20
+ .from('site_settings')
21
+ .select('value')
22
+ .eq('key', ACTIVE_LOGO_SETTING_KEY)
23
+ .maybeSingle();
24
+ const value = data?.value;
25
+ return typeof value === 'string' && value.length > 0 ? value : null;
26
+ }
27
+
28
+ /**
29
+ * Resolve the active logo row (joined with its media) for the given client: the pinned
30
+ * logo when set and still present, otherwise the most recently created logo. Returns the
31
+ * raw row; callers cast to their local logo type. Returns null when there are no logos.
32
+ */
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ export async function resolveActiveLogo(client: AnyClient): Promise<any> {
35
+ const activeId = await resolveActiveLogoId(client);
36
+
37
+ if (activeId) {
38
+ const { data } = await client
39
+ .from('logos')
40
+ .select('*, media:media_id (*)')
41
+ .eq('id', activeId)
42
+ .maybeSingle();
43
+ if (data) return data;
44
+ }
45
+
46
+ const { data } = await client
47
+ .from('logos')
48
+ .select('*, media:media_id (*)')
49
+ .order('created_at', { ascending: false })
50
+ .limit(1)
51
+ .maybeSingle();
52
+ return data ?? null;
53
+ }
@@ -1,54 +1,54 @@
1
- const BUNDLED_PUBLIC_MEDIA_KEYS = new Set([
2
- 'images/NBcover.webp',
3
- 'images/cap.webp',
4
- 'images/commerce-plan.webp',
5
- 'images/commerce-square.webp',
6
- 'images/commerce-wide.webp',
7
- 'images/cortex-ai-square.webp',
8
- 'images/cortex-ai.webp',
9
- 'images/developer.webp',
10
- 'images/extensibility.webp',
11
- 'images/goals.webp',
12
- 'images/included.webp',
13
- 'images/metadata_image.webp',
14
- 'images/nextblock-logo-small.webp',
15
- 'images/nextblock-logo-button-tiny.png',
16
- 'images/nx-graph.webp',
17
- 'images/pants.webp',
18
- 'images/programmer-upscaled.webp',
19
- 'images/t-shirt.webp',
20
- ]);
21
-
22
- export function resolveMediaUrl(
23
- objectKey?: string | null,
24
- // On the client, NEXT_PUBLIC_R2_BASE_URL is inlined at build time, so a fresh-clone
25
- // dev bundle built with no env holds an empty string; fall back to the runtime value
26
- // injected by <PublicEnvBootstrap> (window.__NEXTBLOCK_PUBLIC_ENV__.r2Base). On the
27
- // server, process.env is always current and the window branch is skipped.
28
- baseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL ||
29
- (typeof window !== 'undefined'
30
- ? (window as { __NEXTBLOCK_PUBLIC_ENV__?: { r2Base?: string } })
31
- .__NEXTBLOCK_PUBLIC_ENV__?.r2Base || ''
32
- : '')
33
- ) {
34
- if (!objectKey) return null;
35
-
36
- if (objectKey.startsWith('http://') || objectKey.startsWith('https://')) {
37
- return objectKey;
38
- }
39
-
40
- if (objectKey.startsWith('/')) {
41
- return objectKey;
42
- }
43
-
44
- if (BUNDLED_PUBLIC_MEDIA_KEYS.has(objectKey)) {
45
- return `/${objectKey}`;
46
- }
47
-
48
- const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
49
- const normalizedObjectKey = objectKey.replace(/^\/+/, '');
50
-
51
- return normalizedBaseUrl
52
- ? `${normalizedBaseUrl}/${normalizedObjectKey}`
53
- : `/${normalizedObjectKey}`;
54
- }
1
+ const BUNDLED_PUBLIC_MEDIA_KEYS = new Set([
2
+ 'images/NBcover.webp',
3
+ 'images/cap.webp',
4
+ 'images/commerce-plan.webp',
5
+ 'images/commerce-square.webp',
6
+ 'images/commerce-wide.webp',
7
+ 'images/cortex-ai-square.webp',
8
+ 'images/cortex-ai.webp',
9
+ 'images/developer.webp',
10
+ 'images/extensibility.webp',
11
+ 'images/goals.webp',
12
+ 'images/included.webp',
13
+ 'images/metadata_image.webp',
14
+ 'images/nextblock-logo-small.webp',
15
+ 'images/nextblock-logo-button-tiny.png',
16
+ 'images/nx-graph.webp',
17
+ 'images/pants.webp',
18
+ 'images/programmer-upscaled.webp',
19
+ 'images/t-shirt.webp',
20
+ ]);
21
+
22
+ export function resolveMediaUrl(
23
+ objectKey?: string | null,
24
+ // On the client, NEXT_PUBLIC_R2_BASE_URL is inlined at build time, so a fresh-clone
25
+ // dev bundle built with no env holds an empty string; fall back to the runtime value
26
+ // injected by <PublicEnvBootstrap> (window.__NEXTBLOCK_PUBLIC_ENV__.r2Base). On the
27
+ // server, process.env is always current and the window branch is skipped.
28
+ baseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL ||
29
+ (typeof window !== 'undefined'
30
+ ? (window as { __NEXTBLOCK_PUBLIC_ENV__?: { r2Base?: string } })
31
+ .__NEXTBLOCK_PUBLIC_ENV__?.r2Base || ''
32
+ : '')
33
+ ) {
34
+ if (!objectKey) return null;
35
+
36
+ if (objectKey.startsWith('http://') || objectKey.startsWith('https://')) {
37
+ return objectKey;
38
+ }
39
+
40
+ if (objectKey.startsWith('/')) {
41
+ return objectKey;
42
+ }
43
+
44
+ if (BUNDLED_PUBLIC_MEDIA_KEYS.has(objectKey)) {
45
+ return `/${objectKey}`;
46
+ }
47
+
48
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
49
+ const normalizedObjectKey = objectKey.replace(/^\/+/, '');
50
+
51
+ return normalizedBaseUrl
52
+ ? `${normalizedBaseUrl}/${normalizedObjectKey}`
53
+ : `/${normalizedObjectKey}`;
54
+ }