create-nextblock 0.13.10 → 0.13.12
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.
- package/docker-template/.dockerignore +23 -23
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +1 -1
- package/project.json +19 -19
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.dockerignore +23 -23
- package/templates/nextblock-template/README.md +34 -34
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/postActions.ts +146 -146
- package/templates/nextblock-template/app/actions/productGridActions.ts +40 -40
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/ecommerce/product-picker/route.ts +151 -151
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5609
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/components/MultiEntityPicker.tsx +263 -263
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/EcommerceActiveContext.tsx +27 -27
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -189
- package/templates/nextblock-template/app/cms/layout.tsx +73 -73
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
- package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageDetectionPanel.tsx +188 -188
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/CreateUserForm.tsx +217 -217
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -44
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/sitemap.ts +130 -130
- package/templates/nextblock-template/components/BlockRenderer.tsx +4 -0
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/ProductGridClient.tsx +114 -114
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +8 -1
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +3 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +33 -23
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -0
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -0
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -464
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
- package/templates/nextblock-template/docs/README.md +39 -39
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +78 -78
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/blocks/product-grid-data.ts +210 -210
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
- package/templates/nextblock-template/lib/i18n/country-languages.ts +247 -247
- package/templates/nextblock-template/lib/i18n/detection.test.ts +197 -197
- package/templates/nextblock-template/lib/i18n/detection.ts +192 -192
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -0
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -82
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/site-url.ts +48 -48
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/next.config.js +6 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- 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
|
+
}
|