create-nextblock 0.2.78 → 0.8.1
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/bin/create-nextblock.js +793 -472
- package/package.json +1 -2
- package/scripts/sync-template.js +18 -1
- package/templates/nextblock-template/.browserslistrc +11 -0
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/README.md +23 -114
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
- package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
- package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
- package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
- package/templates/nextblock-template/app/actions/consent.ts +57 -0
- package/templates/nextblock-template/app/actions/formActions.ts +130 -11
- package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
- package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
- package/templates/nextblock-template/app/actions/postActions.ts +146 -48
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
- package/templates/nextblock-template/app/actions.ts +67 -12
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
- package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
- package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
- package/templates/nextblock-template/app/api/draft/route.ts +93 -0
- package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
- package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
- package/templates/nextblock-template/app/api/search/route.ts +43 -0
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
- package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
- package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
- package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
- package/templates/nextblock-template/app/cart/page.tsx +7 -0
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
- package/templates/nextblock-template/app/checkout/page.tsx +52 -0
- package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
- package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
- package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
- package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
- package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
- package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
- package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
- package/templates/nextblock-template/app/cms/layout.tsx +29 -10
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
- package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
- package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
- package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
- package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
- package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
- package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
- package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
- package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
- package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
- package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
- package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
- package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
- package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
- package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
- package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
- package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
- package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
- package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
- package/templates/nextblock-template/app/globals.css +9 -0
- package/templates/nextblock-template/app/layout.tsx +372 -120
- package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
- package/templates/nextblock-template/app/lib/seo.ts +279 -0
- package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
- package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
- package/templates/nextblock-template/app/page.tsx +165 -73
- package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
- package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
- package/templates/nextblock-template/app/profile/account-data.ts +37 -0
- package/templates/nextblock-template/app/profile/account-links.ts +22 -0
- package/templates/nextblock-template/app/profile/account-types.ts +11 -0
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
- package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
- package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
- package/templates/nextblock-template/app/profile/page.tsx +51 -0
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
- package/templates/nextblock-template/app/providers.tsx +55 -17
- package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
- package/templates/nextblock-template/app/sitemap.ts +128 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
- package/templates/nextblock-template/components/AppShell.tsx +154 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
- package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
- package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
- package/templates/nextblock-template/components/Header.tsx +49 -41
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
- package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
- package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
- package/templates/nextblock-template/components/blocks/types.ts +7 -6
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -26
- package/templates/nextblock-template/components/header-auth.tsx +69 -17
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
- package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
- package/templates/nextblock-template/context/AuthContext.tsx +23 -90
- package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
- package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
- package/templates/nextblock-template/context/language-rest-client.ts +31 -0
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
- package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
- package/templates/nextblock-template/docs/README.md +34 -0
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
- package/templates/nextblock-template/index.d.ts +2 -0
- package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
- package/templates/nextblock-template/lib/ai-client.ts +247 -0
- package/templates/nextblock-template/lib/ai-config.ts +81 -0
- package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
- package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
- package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
- package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
- package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
- package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
- package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
- package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
- package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
- package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
- package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
- package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
- package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
- package/templates/nextblock-template/lib/blocks/README.md +13 -670
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
- package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
- package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
- package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
- package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
- package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
- package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
- package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
- package/templates/nextblock-template/lib/privacy/types.ts +67 -0
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
- package/templates/nextblock-template/lib/promotions/server.ts +741 -0
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
- package/templates/nextblock-template/lib/search/server.ts +585 -0
- package/templates/nextblock-template/lib/search/types.ts +27 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
- package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
- package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
- package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
- package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
- package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
- package/templates/nextblock-template/lib/zod-config.ts +5 -0
- package/templates/nextblock-template/next.config.js +190 -66
- package/templates/nextblock-template/package.json +34 -30
- package/templates/nextblock-template/proxy.ts +435 -253
- package/templates/nextblock-template/public/images/NBcover.webp +0 -0
- package/templates/nextblock-template/public/images/cap.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
- package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
- package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
- package/templates/nextblock-template/public/images/extensibility.webp +0 -0
- package/templates/nextblock-template/public/images/goals.webp +0 -0
- package/templates/nextblock-template/public/images/included.webp +0 -0
- package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
- package/templates/nextblock-template/public/images/pants.webp +0 -0
- package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
- package/templates/nextblock-template/tailwind.config.js +1 -0
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
- package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
- package/templates/nextblock-template/tsconfig.json +52 -66
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -0
- package/templates/nextblock-template/app/force-styles.tsx +0 -31
- package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
- package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
- package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
- package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
- package/templates/nextblock-template/docs/files-structure.md +0 -426
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_SITE_TITLE = 'NextBlock™ CMS';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_SITE_DESCRIPTION =
|
|
6
|
+
'NextBlock is an open-source CMS on Next.js + Supabase — a visual block editor, blazing-fast multilingual pages, and built-in e-commerce.';
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_SITE_KEYWORDS =
|
|
9
|
+
'NextBlock, CMS, Next.js, Supabase, headless CMS, block editor, visual page builder, multilingual, e-commerce, open source';
|
|
10
|
+
|
|
11
|
+
/** Bundled fallback Open Graph image (resolved to absolute via metadataBase). */
|
|
12
|
+
export const DEFAULT_OG_IMAGE = '/images/metadata_image.webp';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_META_DESCRIPTION_LENGTH = 160;
|
|
15
|
+
|
|
16
|
+
function normalizeWhitespace(value: string) {
|
|
17
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function decodeHtmlEntities(value: string) {
|
|
21
|
+
const entities: Record<string, string> = {
|
|
22
|
+
amp: '&',
|
|
23
|
+
apos: "'",
|
|
24
|
+
gt: '>',
|
|
25
|
+
lt: '<',
|
|
26
|
+
nbsp: ' ',
|
|
27
|
+
quot: '"',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity) => {
|
|
31
|
+
const key = String(entity);
|
|
32
|
+
if (key[0] === '#') {
|
|
33
|
+
const isHex = key[1]?.toLowerCase() === 'x';
|
|
34
|
+
const codePoint = Number.parseInt(key.slice(isHex ? 2 : 1), isHex ? 16 : 10);
|
|
35
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return entities[key] ?? match;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function stripHtmlToText(value: string) {
|
|
43
|
+
return normalizeWhitespace(
|
|
44
|
+
decodeHtmlEntities(
|
|
45
|
+
value
|
|
46
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
47
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
48
|
+
.replace(/<[^>]+>/g, ' ')
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractParagraphTextFromHtml(value: string) {
|
|
54
|
+
const paragraphs = Array.from(value.matchAll(/<p\b[^>]*>([\s\S]*?)<\/p>/gi))
|
|
55
|
+
.map((match) => stripHtmlToText(match[1] ?? ''))
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
|
|
58
|
+
if (paragraphs.length > 0) {
|
|
59
|
+
return paragraphs[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return stripHtmlToText(value.replace(/<h[1-6]\b[\s\S]*?<\/h[1-6]>/gi, ' '));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function truncateMetaDescription(value: string, maxLength = DEFAULT_META_DESCRIPTION_LENGTH) {
|
|
66
|
+
const normalized = normalizeWhitespace(value);
|
|
67
|
+
if (normalized.length <= maxLength) {
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const truncated = normalized.slice(0, maxLength + 1);
|
|
72
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
73
|
+
const candidate = lastSpace > 80 ? truncated.slice(0, lastSpace) : normalized.slice(0, maxLength);
|
|
74
|
+
|
|
75
|
+
return candidate.replace(/[.,;:!?-]+$/, '').trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeMetaCandidate(value: string | null | undefined) {
|
|
79
|
+
if (!value) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const normalized = stripHtmlToText(value);
|
|
84
|
+
return normalized || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function collectIntroTextCandidates(value: unknown, candidates: string[]) {
|
|
88
|
+
if (!value) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
value.forEach((item) => collectIntroTextCandidates(item, candidates));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof value !== 'object') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const block = value as {
|
|
102
|
+
block_type?: string;
|
|
103
|
+
content?: Record<string, unknown>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (block.block_type === 'section' || block.block_type === 'hero') {
|
|
107
|
+
collectIntroTextCandidates(block.content?.column_blocks, candidates);
|
|
108
|
+
collectIntroTextCandidates(block.content?.slides, candidates);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (block.block_type === 'text') {
|
|
113
|
+
const htmlContent = block.content?.html_content;
|
|
114
|
+
const textContent = block.content?.text_content;
|
|
115
|
+
const candidate =
|
|
116
|
+
typeof htmlContent === 'string'
|
|
117
|
+
? extractParagraphTextFromHtml(htmlContent)
|
|
118
|
+
: typeof textContent === 'string'
|
|
119
|
+
? normalizeWhitespace(textContent)
|
|
120
|
+
: '';
|
|
121
|
+
|
|
122
|
+
if (candidate) {
|
|
123
|
+
candidates.push(candidate);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function extractIntroExcerptFromBlocks(blocks: unknown) {
|
|
129
|
+
const candidates: string[] = [];
|
|
130
|
+
collectIntroTextCandidates(blocks, candidates);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
candidates.find((candidate) => candidate.length >= 80) ??
|
|
134
|
+
candidates[0] ??
|
|
135
|
+
null
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function resolveMetaTitle(
|
|
140
|
+
manualTitle: string | null | undefined,
|
|
141
|
+
fallbackTitle: string | null | undefined
|
|
142
|
+
) {
|
|
143
|
+
return (
|
|
144
|
+
normalizeMetaCandidate(manualTitle) ??
|
|
145
|
+
normalizeMetaCandidate(fallbackTitle) ??
|
|
146
|
+
DEFAULT_SITE_TITLE
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resolveMetaDescription(...candidates: Array<string | null | undefined>) {
|
|
151
|
+
for (const candidate of candidates) {
|
|
152
|
+
const description = normalizeMetaCandidate(candidate);
|
|
153
|
+
if (description) {
|
|
154
|
+
return truncateMetaDescription(description);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return DEFAULT_SITE_DESCRIPTION;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resolvePageMetaDescription(
|
|
162
|
+
manualDescription: string | null | undefined,
|
|
163
|
+
blocks: unknown
|
|
164
|
+
) {
|
|
165
|
+
return resolveMetaDescription(manualDescription, extractIntroExcerptFromBlocks(blocks));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function resolvePostMetaDescription(
|
|
169
|
+
manualDescription: string | null | undefined,
|
|
170
|
+
subtitle: string | null | undefined
|
|
171
|
+
) {
|
|
172
|
+
return resolveMetaDescription(manualDescription, subtitle);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function resolveProductMetaDescription(
|
|
176
|
+
manualDescription: string | null | undefined,
|
|
177
|
+
shortDescription: string | null | undefined
|
|
178
|
+
) {
|
|
179
|
+
return resolveMetaDescription(manualDescription, shortDescription);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function stringifyJsonLd(value: unknown) {
|
|
183
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Appends the site title to a page title for social cards, e.g.
|
|
188
|
+
* `composeTitleWithSite('Home', 'NextBlock™ CMS') === 'Home | NextBlock™ CMS'`.
|
|
189
|
+
* Unlike Next.js' `title.template` (which only affects the `<title>` tag), this
|
|
190
|
+
* lets us produce a complete `og:title` / `twitter:title`.
|
|
191
|
+
*/
|
|
192
|
+
export function composeTitleWithSite(
|
|
193
|
+
pageTitle: string | null | undefined,
|
|
194
|
+
siteTitle: string | null | undefined
|
|
195
|
+
): string {
|
|
196
|
+
const cleanTitle = (pageTitle ?? '').trim();
|
|
197
|
+
const cleanSite = (siteTitle ?? '').trim();
|
|
198
|
+
|
|
199
|
+
if (!cleanSite) return cleanTitle;
|
|
200
|
+
if (!cleanTitle) return cleanSite;
|
|
201
|
+
|
|
202
|
+
const suffix = ` | ${cleanSite}`;
|
|
203
|
+
return cleanTitle === cleanSite || cleanTitle.endsWith(suffix)
|
|
204
|
+
? cleanTitle
|
|
205
|
+
: `${cleanTitle}${suffix}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Maps a language code (e.g. `fr`, `en-US`) to an Open Graph locale (`fr_FR`). */
|
|
209
|
+
export function toOpenGraphLocale(languageCode?: string | null): string {
|
|
210
|
+
const code = (languageCode ?? '').toLowerCase().split('-')[0];
|
|
211
|
+
const map: Record<string, string> = {
|
|
212
|
+
en: 'en_US',
|
|
213
|
+
fr: 'fr_FR',
|
|
214
|
+
es: 'es_ES',
|
|
215
|
+
de: 'de_DE',
|
|
216
|
+
pt: 'pt_PT',
|
|
217
|
+
it: 'it_IT',
|
|
218
|
+
nl: 'nl_NL',
|
|
219
|
+
};
|
|
220
|
+
return map[code] ?? 'en_US';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface SocialMetadataInput {
|
|
224
|
+
/** Bare page title (without the site-title suffix). */
|
|
225
|
+
title: string;
|
|
226
|
+
description: string;
|
|
227
|
+
/** Canonical URL of the page (absolute, or path resolved via metadataBase). */
|
|
228
|
+
url: string;
|
|
229
|
+
siteTitle: string;
|
|
230
|
+
/** Resolved feature-image URL; falls back to the default OG image when empty. */
|
|
231
|
+
imageUrl?: string | null;
|
|
232
|
+
type?: 'website' | 'article';
|
|
233
|
+
publishedTime?: string | null;
|
|
234
|
+
locale?: string | null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Builds the `openGraph` + `twitter` metadata for a public page so that every
|
|
239
|
+
* page emits a complete, suffixed social title and always has an OG image
|
|
240
|
+
* (the feature image when present, otherwise the bundled default).
|
|
241
|
+
*/
|
|
242
|
+
export function buildSocialMetadata(
|
|
243
|
+
input: SocialMetadataInput
|
|
244
|
+
): Pick<Metadata, 'openGraph' | 'twitter'> {
|
|
245
|
+
const usingDefaultImage = !input.imageUrl;
|
|
246
|
+
const imageUrl = input.imageUrl || DEFAULT_OG_IMAGE;
|
|
247
|
+
const socialTitle = composeTitleWithSite(input.title, input.siteTitle);
|
|
248
|
+
const image = usingDefaultImage
|
|
249
|
+
? { url: imageUrl, width: 1200, height: 630, alt: socialTitle }
|
|
250
|
+
: { url: imageUrl, alt: socialTitle };
|
|
251
|
+
|
|
252
|
+
const openGraphBase = {
|
|
253
|
+
title: socialTitle,
|
|
254
|
+
description: input.description,
|
|
255
|
+
url: input.url,
|
|
256
|
+
siteName: input.siteTitle,
|
|
257
|
+
images: [image],
|
|
258
|
+
...(input.locale ? { locale: input.locale } : {}),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const openGraph =
|
|
262
|
+
input.type === 'article'
|
|
263
|
+
? {
|
|
264
|
+
...openGraphBase,
|
|
265
|
+
type: 'article' as const,
|
|
266
|
+
...(input.publishedTime ? { publishedTime: input.publishedTime } : {}),
|
|
267
|
+
}
|
|
268
|
+
: { ...openGraphBase, type: 'website' as const };
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
openGraph,
|
|
272
|
+
twitter: {
|
|
273
|
+
card: 'summary_large_image',
|
|
274
|
+
title: socialTitle,
|
|
275
|
+
description: input.description,
|
|
276
|
+
images: [imageUrl],
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { unstable_cache } from 'next/cache';
|
|
3
|
+
import { createClient as createSupabaseJsClient } from '@supabase/supabase-js';
|
|
4
|
+
import type { Database } from '@nextblock-cms/db';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SITE_TITLE,
|
|
7
|
+
DEFAULT_SITE_DESCRIPTION,
|
|
8
|
+
DEFAULT_SITE_KEYWORDS,
|
|
9
|
+
} from './seo';
|
|
10
|
+
|
|
11
|
+
export const SITE_SETTINGS_CACHE_TAG = 'public-site-settings';
|
|
12
|
+
const SITE_SETTINGS_REVALIDATE_SECONDS = 60;
|
|
13
|
+
|
|
14
|
+
export interface SiteSettings {
|
|
15
|
+
siteTitle: string;
|
|
16
|
+
siteDescription: string;
|
|
17
|
+
siteKeywords: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Service-role (or anon) Supabase client used for cached, request-agnostic
|
|
22
|
+
* reads of public layout/SEO data. Shared by the root layout and the public
|
|
23
|
+
* route `generateMetadata` functions.
|
|
24
|
+
*/
|
|
25
|
+
export function createStaticSupabaseClient() {
|
|
26
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
27
|
+
const supabaseKey =
|
|
28
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
29
|
+
|
|
30
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
31
|
+
throw new Error('Missing Supabase environment variables for public layout data');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return createSupabaseJsClient<Database>(supabaseUrl, supabaseKey, {
|
|
35
|
+
auth: {
|
|
36
|
+
persistSession: false,
|
|
37
|
+
autoRefreshToken: false,
|
|
38
|
+
detectSessionInUrl: false,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cached read of the site-wide SEO identity (title / description / keywords)
|
|
45
|
+
* from the `site_settings` key-value store. This is the single source of truth
|
|
46
|
+
* used for both `<title>`/OpenGraph metadata and the header brand.
|
|
47
|
+
*/
|
|
48
|
+
export const getSiteSettings = unstable_cache(
|
|
49
|
+
async (): Promise<SiteSettings> => {
|
|
50
|
+
const fallback: SiteSettings = {
|
|
51
|
+
siteTitle: DEFAULT_SITE_TITLE,
|
|
52
|
+
siteDescription: DEFAULT_SITE_DESCRIPTION,
|
|
53
|
+
siteKeywords: DEFAULT_SITE_KEYWORDS,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const supabase = createStaticSupabaseClient();
|
|
58
|
+
const { data, error } = await supabase
|
|
59
|
+
.from('site_settings')
|
|
60
|
+
.select('key, value')
|
|
61
|
+
.in('key', ['site_title', 'site_description', 'site_keywords']);
|
|
62
|
+
|
|
63
|
+
if (error || !data) {
|
|
64
|
+
console.error('Error fetching cached site settings:', error);
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const settings: Record<string, string> = {};
|
|
69
|
+
data.forEach((item) => {
|
|
70
|
+
if (typeof item.value === 'string') {
|
|
71
|
+
settings[item.key] = item.value;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
siteTitle: settings.site_title?.trim() || fallback.siteTitle,
|
|
77
|
+
siteDescription: settings.site_description?.trim() || fallback.siteDescription,
|
|
78
|
+
siteKeywords: settings.site_keywords?.trim() || fallback.siteKeywords,
|
|
79
|
+
};
|
|
80
|
+
} catch (caught) {
|
|
81
|
+
console.error('Unexpected error fetching site settings:', caught);
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
['public-site-settings'],
|
|
86
|
+
{ revalidate: SITE_SETTINGS_REVALIDATE_SECONDS, tags: [SITE_SETTINGS_CACHE_TAG] }
|
|
87
|
+
);
|
|
@@ -1,68 +1,253 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
1
|
+
import { getSsgSupabaseClient } from '@nextblock-cms/db/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A single, language-aware entry destined for the XML sitemap.
|
|
5
|
+
*
|
|
6
|
+
* Paths are kept site-root-relative (e.g. "/about", "/article/my-post"); the
|
|
7
|
+
* `app/sitemap.ts` route prefixes the absolute origin so the base URL lives in
|
|
8
|
+
* exactly one place.
|
|
9
|
+
*/
|
|
10
|
+
export interface SitemapEntry {
|
|
11
|
+
/** Root-relative path for this row, e.g. "/about" or "/article/hello". */
|
|
12
|
+
path: string;
|
|
13
|
+
/** ISO-8601 timestamp mapped from the row's `updated_at`. */
|
|
14
|
+
lastModified: string;
|
|
15
|
+
/**
|
|
16
|
+
* hreflang map of `languageCode -> root-relative path` covering every
|
|
17
|
+
* published translation that shares this row's `translation_group_id`, plus
|
|
18
|
+
* an `x-default` key pointing at the default-language version. Left
|
|
19
|
+
* `undefined` for content that only exists in a single language.
|
|
20
|
+
*
|
|
21
|
+
* NextBlock's i18n model gives each language its own slug (there is no
|
|
22
|
+
* `/en` `/fr` path prefix), so alternates are resolved by grouping sibling
|
|
23
|
+
* rows on `translation_group_id` rather than by swapping a locale segment.
|
|
24
|
+
*/
|
|
25
|
+
alternates?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Page slugs that are rendered by their own canonical route and must not be
|
|
30
|
+
* re-advertised under the generic `/{slug}` catch-all:
|
|
31
|
+
* - `product-template` backs the product layout (app/product/[slug]); it is
|
|
32
|
+
* not a public page.
|
|
33
|
+
* - `home` / `accueil` back the locale-resolved homepage served at "/"
|
|
34
|
+
* (see getHomepageSlugForLocale in app/page.tsx); listing them as `/home`
|
|
35
|
+
* and `/accueil` would duplicate the canonical "/" entry.
|
|
36
|
+
*/
|
|
37
|
+
const EXCLUDED_PAGE_SLUGS = new Set(['product-template', 'home', 'accueil']);
|
|
38
|
+
|
|
39
|
+
type SupabaseLikeClient = ReturnType<typeof getSsgSupabaseClient>;
|
|
40
|
+
|
|
41
|
+
/** Minimal shape every content table exposes for sitemap purposes. */
|
|
42
|
+
interface ContentRow {
|
|
43
|
+
slug: string | null;
|
|
44
|
+
updated_at: string | null;
|
|
45
|
+
created_at?: string | null;
|
|
46
|
+
language_id: number | null;
|
|
47
|
+
translation_group_id: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toIsoDate(value: string | null | undefined): string {
|
|
51
|
+
if (value) {
|
|
52
|
+
const parsed = new Date(value);
|
|
53
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
54
|
+
return parsed.toISOString();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return new Date().toISOString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Loads the language set once so callers can resolve a row's `language_id` to
|
|
62
|
+
* its BCP-47 `code` and identify the default language used for `x-default`.
|
|
63
|
+
*/
|
|
64
|
+
async function fetchLanguageMap(
|
|
65
|
+
supabase: SupabaseLikeClient,
|
|
66
|
+
): Promise<{ codeById: Map<number, string>; defaultCode: string | null }> {
|
|
67
|
+
const codeById = new Map<number, string>();
|
|
68
|
+
let defaultCode: string | null = null;
|
|
69
|
+
|
|
70
|
+
const { data, error } = await supabase
|
|
71
|
+
.from('languages')
|
|
72
|
+
.select('id, code, is_default');
|
|
73
|
+
|
|
74
|
+
if (error) {
|
|
75
|
+
console.error('Error fetching languages for sitemap alternates:', error);
|
|
76
|
+
return { codeById, defaultCode };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const lang of data ?? []) {
|
|
80
|
+
if (lang.code) {
|
|
81
|
+
codeById.set(lang.id, lang.code);
|
|
82
|
+
if (lang.is_default) {
|
|
83
|
+
defaultCode = lang.code;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { codeById, defaultCode };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Turns raw content rows into language-aware sitemap entries. Rows that share a
|
|
93
|
+
* `translation_group_id` are emitted as sibling URLs that cross-link to one
|
|
94
|
+
* another via `alternates`.
|
|
95
|
+
*/
|
|
96
|
+
function rowsToEntries(
|
|
97
|
+
rows: ContentRow[],
|
|
98
|
+
pathForSlug: (slug: string) => string,
|
|
99
|
+
codeById: Map<number, string>,
|
|
100
|
+
defaultCode: string | null,
|
|
101
|
+
): SitemapEntry[] {
|
|
102
|
+
// Bucket rows by their translation group so each URL can advertise its siblings.
|
|
103
|
+
const groups = new Map<string, ContentRow[]>();
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
if (!row.slug) continue;
|
|
106
|
+
const key = row.translation_group_id ?? `solo:${row.slug}`;
|
|
107
|
+
const bucket = groups.get(key);
|
|
108
|
+
if (bucket) {
|
|
109
|
+
bucket.push(row);
|
|
110
|
+
} else {
|
|
111
|
+
groups.set(key, [row]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const entries: SitemapEntry[] = [];
|
|
116
|
+
for (const group of groups.values()) {
|
|
117
|
+
// languageCode -> path for every translated sibling in this group.
|
|
118
|
+
const byCode: Record<string, string> = {};
|
|
119
|
+
let defaultPath: string | undefined;
|
|
120
|
+
for (const sibling of group) {
|
|
121
|
+
if (!sibling.slug || sibling.language_id == null) continue;
|
|
122
|
+
const code = codeById.get(sibling.language_id);
|
|
123
|
+
if (!code) continue;
|
|
124
|
+
const path = pathForSlug(sibling.slug);
|
|
125
|
+
byCode[code] = path;
|
|
126
|
+
if (code === defaultCode) {
|
|
127
|
+
defaultPath = path;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// hreflang is only meaningful when 2+ *distinct* URLs exist. NextBlock has no
|
|
132
|
+
// locale path prefix, so two translations can share a slug (e.g. `/articles`
|
|
133
|
+
// is the slug in both en and fr); that collapses to a single URL with nothing
|
|
134
|
+
// to cross-link, so no alternates are emitted.
|
|
135
|
+
const hasAlternates = new Set(Object.values(byCode)).size > 1;
|
|
136
|
+
const alternates = hasAlternates
|
|
137
|
+
? { ...byCode, 'x-default': defaultPath ?? Object.values(byCode)[0] }
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
for (const row of group) {
|
|
141
|
+
if (!row.slug) continue;
|
|
142
|
+
entries.push({
|
|
143
|
+
path: pathForSlug(row.slug),
|
|
144
|
+
lastModified: toIsoDate(row.updated_at ?? row.created_at),
|
|
145
|
+
alternates,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return entries;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fetches all published pages from Supabase and formats them, with language
|
|
155
|
+
* alternates, for the sitemap.
|
|
156
|
+
*/
|
|
12
157
|
export async function fetchAllPublishedPages(): Promise<SitemapEntry[]> {
|
|
13
|
-
const supabase =
|
|
158
|
+
const supabase = getSsgSupabaseClient();
|
|
14
159
|
try {
|
|
15
|
-
const { data: pages, error } = await
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
160
|
+
const [{ data: pages, error }, languageMap] = await Promise.all([
|
|
161
|
+
supabase
|
|
162
|
+
.from('pages')
|
|
163
|
+
.select('slug, updated_at, language_id, translation_group_id')
|
|
164
|
+
.eq('status', 'published'),
|
|
165
|
+
fetchLanguageMap(supabase),
|
|
166
|
+
]);
|
|
19
167
|
|
|
20
168
|
if (error) {
|
|
21
169
|
console.error('Error fetching published pages:', error);
|
|
22
170
|
return [];
|
|
23
171
|
}
|
|
24
172
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
173
|
+
const rows = (pages ?? []).filter(
|
|
174
|
+
(page) => page.slug && !EXCLUDED_PAGE_SLUGS.has(page.slug),
|
|
175
|
+
);
|
|
28
176
|
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
177
|
+
return rowsToEntries(
|
|
178
|
+
rows,
|
|
179
|
+
(slug) => `/${slug}`,
|
|
180
|
+
languageMap.codeById,
|
|
181
|
+
languageMap.defaultCode,
|
|
182
|
+
);
|
|
33
183
|
} catch (err) {
|
|
34
184
|
console.error('An unexpected error occurred while fetching pages:', err);
|
|
35
185
|
return [];
|
|
36
186
|
}
|
|
37
187
|
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Fetches all published posts
|
|
41
|
-
*
|
|
42
|
-
*/
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Fetches all published posts (respecting scheduled `published_at`) from
|
|
191
|
+
* Supabase and formats them, with language alternates, for the sitemap.
|
|
192
|
+
*/
|
|
43
193
|
export async function fetchAllPublishedPosts(): Promise<SitemapEntry[]> {
|
|
44
|
-
const supabase =
|
|
194
|
+
const supabase = getSsgSupabaseClient();
|
|
45
195
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
196
|
+
const nowIso = new Date().toISOString();
|
|
197
|
+
const [{ data: posts, error }, languageMap] = await Promise.all([
|
|
198
|
+
supabase
|
|
199
|
+
.from('posts')
|
|
200
|
+
.select('slug, updated_at, language_id, translation_group_id')
|
|
201
|
+
.eq('status', 'published')
|
|
202
|
+
.or(`published_at.is.null,published_at.lte.${nowIso}`),
|
|
203
|
+
fetchLanguageMap(supabase),
|
|
204
|
+
]);
|
|
50
205
|
|
|
51
206
|
if (error) {
|
|
52
207
|
console.error('Error fetching published posts:', error);
|
|
53
208
|
return [];
|
|
54
209
|
}
|
|
55
210
|
|
|
56
|
-
|
|
211
|
+
return rowsToEntries(
|
|
212
|
+
posts ?? [],
|
|
213
|
+
(slug) => `/article/${slug}`,
|
|
214
|
+
languageMap.codeById,
|
|
215
|
+
languageMap.defaultCode,
|
|
216
|
+
);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error('An unexpected error occurred while fetching posts:', err);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Fetches all active storefront products from Supabase and formats them, with
|
|
225
|
+
* language alternates, for the sitemap.
|
|
226
|
+
*/
|
|
227
|
+
export async function fetchAllActiveProducts(): Promise<SitemapEntry[]> {
|
|
228
|
+
const supabase = getSsgSupabaseClient();
|
|
229
|
+
try {
|
|
230
|
+
const [{ data: products, error }, languageMap] = await Promise.all([
|
|
231
|
+
supabase
|
|
232
|
+
.from('products')
|
|
233
|
+
.select('slug, updated_at, created_at, language_id, translation_group_id')
|
|
234
|
+
.eq('status', 'active'),
|
|
235
|
+
fetchLanguageMap(supabase),
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
if (error) {
|
|
239
|
+
console.error('Error fetching active products:', error);
|
|
57
240
|
return [];
|
|
58
241
|
}
|
|
59
242
|
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
243
|
+
return rowsToEntries(
|
|
244
|
+
products ?? [],
|
|
245
|
+
(slug) => `/product/${slug}`,
|
|
246
|
+
languageMap.codeById,
|
|
247
|
+
languageMap.defaultCode,
|
|
248
|
+
);
|
|
64
249
|
} catch (err) {
|
|
65
|
-
console.error('An unexpected error occurred while fetching
|
|
250
|
+
console.error('An unexpected error occurred while fetching products:', err);
|
|
66
251
|
return [];
|
|
67
252
|
}
|
|
68
253
|
}
|