create-nextblock 0.15.10 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-template/.dockerignore +1 -1
- 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 +50 -50
- package/project.json +19 -19
- package/templates/nextblock-template/.dockerignore +1 -1
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/AGENTS.md +9 -9
- package/templates/nextblock-template/CLAUDE.md +1 -1
- 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/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
- package/templates/nextblock-template/app/actions/email.ts +241 -241
- package/templates/nextblock-template/app/actions/formActions.ts +245 -245
- package/templates/nextblock-template/app/actions/interactions.ts +489 -489
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
- package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
- 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/api/view/route.ts +114 -114
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- 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/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/checkout/page.tsx +57 -57
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/layout.tsx +101 -101
- 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 +177 -2
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- 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/revisions/actions.ts +332 -332
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
- package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
- package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
- package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
- package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
- package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
- 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/components/EmailForm.tsx +227 -227
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
- 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/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
- 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/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/lib/seo.ts +319 -319
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
- 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/providers.tsx +96 -96
- package/templates/nextblock-template/app/robots.ts +123 -0
- package/templates/nextblock-template/app/sitemap.ts +1 -1
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
- package/templates/nextblock-template/app/thread/layout.tsx +15 -15
- package/templates/nextblock-template/app/thread/page.tsx +98 -98
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
- 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/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
- 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/seo/GenerateMetaButton.tsx +137 -0
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -78
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
- package/templates/nextblock-template/docs/README.md +42 -42
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
- 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/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
- package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
- package/templates/nextblock-template/lib/messages/threads.ts +579 -579
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
- package/templates/nextblock-template/lib/site-url.test.ts +89 -89
- package/templates/nextblock-template/lib/site-url.ts +102 -102
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +0 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/proxy.ts +240 -20
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
- package/templates/nextblock-template/tools/update.mjs +1303 -1303
- package/tsconfig.base.json +3 -3
- package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import { redirect } from 'next/navigation';
|
|
2
|
-
import { createClient } from '@nextblock-cms/db/server';
|
|
3
|
-
|
|
4
|
-
import { isEmailConfigured } from '../../../lib/config/email-settings';
|
|
5
|
-
import {
|
|
6
|
-
getStoreContactEmail,
|
|
7
|
-
resolveSellerContactEmail,
|
|
8
|
-
} from '../../../lib/commerce/seller-contact';
|
|
9
|
-
import { loadInbox, loadThreadDetail, type ThreadDetail } from './loadInbox';
|
|
10
|
-
import MessagesClient from './MessagesClient';
|
|
11
|
-
|
|
12
|
-
export const metadata = {
|
|
13
|
-
title: 'Messages | NextBlock™ CMS',
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Messages arrive from the public site at any moment; a cached page would show an admin
|
|
17
|
-
// an empty inbox that is not.
|
|
18
|
-
export const dynamic = 'force-dynamic';
|
|
19
|
-
|
|
20
|
-
export default async function MessagesPage({
|
|
21
|
-
searchParams,
|
|
22
|
-
}: {
|
|
23
|
-
searchParams: Promise<{ source?: string; page?: string; thread?: string; handled?: string }>;
|
|
24
|
-
}) {
|
|
25
|
-
const params = await searchParams;
|
|
26
|
-
|
|
27
|
-
const supabase = createClient();
|
|
28
|
-
const {
|
|
29
|
-
data: { user },
|
|
30
|
-
} = await supabase.auth.getUser();
|
|
31
|
-
if (!user) redirect('/sign-in');
|
|
32
|
-
|
|
33
|
-
const { data: profile } = await supabase
|
|
34
|
-
.from('profiles')
|
|
35
|
-
.select('role')
|
|
36
|
-
.eq('id', user.id)
|
|
37
|
-
.maybeSingle();
|
|
38
|
-
|
|
39
|
-
if (profile?.role !== 'ADMIN' && profile?.role !== 'WRITER') {
|
|
40
|
-
redirect('/unauthorized');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const isAdmin = profile?.role === 'ADMIN';
|
|
44
|
-
|
|
45
|
-
const [inbox, storeContactEmail, resolvedFallback, smtpConfigured] = await Promise.all([
|
|
46
|
-
loadInbox({
|
|
47
|
-
source: params.source,
|
|
48
|
-
page: params.page ? Number(params.page) : 0,
|
|
49
|
-
isAdmin,
|
|
50
|
-
showHandled: params.handled === '1',
|
|
51
|
-
}),
|
|
52
|
-
isAdmin ? getStoreContactEmail() : Promise.resolve(''),
|
|
53
|
-
isAdmin
|
|
54
|
-
? resolveSellerContactEmail()
|
|
55
|
-
: Promise.resolve({ email: null, source: 'none' as const }),
|
|
56
|
-
isEmailConfigured().catch(() => false),
|
|
57
|
-
]);
|
|
58
|
-
|
|
59
|
-
// Only the open conversation's history is loaded, not every thread's.
|
|
60
|
-
let openThread: ThreadDetail | null = null;
|
|
61
|
-
if (isAdmin && params.thread) {
|
|
62
|
-
openThread = await loadThreadDetail(params.thread);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<div className="space-y-6">
|
|
67
|
-
<div>
|
|
68
|
-
<h1 className="text-2xl font-bold">Messages</h1>
|
|
69
|
-
<p className="text-sm text-muted-foreground">
|
|
70
|
-
Everything people send you — product enquiries, contact forms, reviews and
|
|
71
|
-
comments — in one place.
|
|
72
|
-
</p>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<MessagesClient
|
|
76
|
-
inbox={inbox}
|
|
77
|
-
openThread={openThread}
|
|
78
|
-
isAdmin={isAdmin}
|
|
79
|
-
activeSource={params.source ?? ''}
|
|
80
|
-
showHandled={params.handled === '1'}
|
|
81
|
-
storeContactEmail={storeContactEmail}
|
|
82
|
-
resolvedFallback={{ email: resolvedFallback.email, source: resolvedFallback.source }}
|
|
83
|
-
smtpConfigured={smtpConfigured}
|
|
84
|
-
/>
|
|
85
|
-
</div>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
3
|
+
|
|
4
|
+
import { isEmailConfigured } from '../../../lib/config/email-settings';
|
|
5
|
+
import {
|
|
6
|
+
getStoreContactEmail,
|
|
7
|
+
resolveSellerContactEmail,
|
|
8
|
+
} from '../../../lib/commerce/seller-contact';
|
|
9
|
+
import { loadInbox, loadThreadDetail, type ThreadDetail } from './loadInbox';
|
|
10
|
+
import MessagesClient from './MessagesClient';
|
|
11
|
+
|
|
12
|
+
export const metadata = {
|
|
13
|
+
title: 'Messages | NextBlock™ CMS',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Messages arrive from the public site at any moment; a cached page would show an admin
|
|
17
|
+
// an empty inbox that is not.
|
|
18
|
+
export const dynamic = 'force-dynamic';
|
|
19
|
+
|
|
20
|
+
export default async function MessagesPage({
|
|
21
|
+
searchParams,
|
|
22
|
+
}: {
|
|
23
|
+
searchParams: Promise<{ source?: string; page?: string; thread?: string; handled?: string }>;
|
|
24
|
+
}) {
|
|
25
|
+
const params = await searchParams;
|
|
26
|
+
|
|
27
|
+
const supabase = createClient();
|
|
28
|
+
const {
|
|
29
|
+
data: { user },
|
|
30
|
+
} = await supabase.auth.getUser();
|
|
31
|
+
if (!user) redirect('/sign-in');
|
|
32
|
+
|
|
33
|
+
const { data: profile } = await supabase
|
|
34
|
+
.from('profiles')
|
|
35
|
+
.select('role')
|
|
36
|
+
.eq('id', user.id)
|
|
37
|
+
.maybeSingle();
|
|
38
|
+
|
|
39
|
+
if (profile?.role !== 'ADMIN' && profile?.role !== 'WRITER') {
|
|
40
|
+
redirect('/unauthorized');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isAdmin = profile?.role === 'ADMIN';
|
|
44
|
+
|
|
45
|
+
const [inbox, storeContactEmail, resolvedFallback, smtpConfigured] = await Promise.all([
|
|
46
|
+
loadInbox({
|
|
47
|
+
source: params.source,
|
|
48
|
+
page: params.page ? Number(params.page) : 0,
|
|
49
|
+
isAdmin,
|
|
50
|
+
showHandled: params.handled === '1',
|
|
51
|
+
}),
|
|
52
|
+
isAdmin ? getStoreContactEmail() : Promise.resolve(''),
|
|
53
|
+
isAdmin
|
|
54
|
+
? resolveSellerContactEmail()
|
|
55
|
+
: Promise.resolve({ email: null, source: 'none' as const }),
|
|
56
|
+
isEmailConfigured().catch(() => false),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// Only the open conversation's history is loaded, not every thread's.
|
|
60
|
+
let openThread: ThreadDetail | null = null;
|
|
61
|
+
if (isAdmin && params.thread) {
|
|
62
|
+
openThread = await loadThreadDetail(params.thread);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-6">
|
|
67
|
+
<div>
|
|
68
|
+
<h1 className="text-2xl font-bold">Messages</h1>
|
|
69
|
+
<p className="text-sm text-muted-foreground">
|
|
70
|
+
Everything people send you — product enquiries, contact forms, reviews and
|
|
71
|
+
comments — in one place.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<MessagesClient
|
|
76
|
+
inbox={inbox}
|
|
77
|
+
openThread={openThread}
|
|
78
|
+
isAdmin={isAdmin}
|
|
79
|
+
activeSource={params.source ?? ''}
|
|
80
|
+
showHandled={params.handled === '1'}
|
|
81
|
+
storeContactEmail={storeContactEmail}
|
|
82
|
+
resolvedFallback={{ email: resolvedFallback.email, source: resolvedFallback.source }}
|
|
83
|
+
smtpConfigured={smtpConfigured}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { createClient } from '@nextblock-cms/db/server';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Admin gate shared by the Messages server actions.
|
|
5
|
-
*
|
|
6
|
-
* Lives outside `actions.ts` because that file is `'use server'`, where every export
|
|
7
|
-
* must itself be a valid server action — a helper returning a Supabase client cannot be
|
|
8
|
-
* exported from there.
|
|
9
|
-
*
|
|
10
|
-
* This is the check standing between a WRITER and every private conversation on the
|
|
11
|
-
* site: visitor names, addresses, and free text. RLS enforces the same rule
|
|
12
|
-
* independently, so a bug here fails closed rather than open, but the redundancy is the
|
|
13
|
-
* point.
|
|
14
|
-
*/
|
|
15
|
-
export async function requireAdminSupabaseClient() {
|
|
16
|
-
const supabase = createClient();
|
|
17
|
-
const {
|
|
18
|
-
data: { user },
|
|
19
|
-
error: userError,
|
|
20
|
-
} = await supabase.auth.getUser();
|
|
21
|
-
|
|
22
|
-
if (userError || !user) {
|
|
23
|
-
throw new Error('You must be logged in to manage messages.');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const { data: profile, error: profileError } = await supabase
|
|
27
|
-
.from('profiles')
|
|
28
|
-
.select('role, full_name')
|
|
29
|
-
.eq('id', user.id)
|
|
30
|
-
.single();
|
|
31
|
-
|
|
32
|
-
if (profileError || !profile || profile.role !== 'ADMIN') {
|
|
33
|
-
throw new Error('You do not have permission to manage messages.');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return { supabase, userId: user.id, fullName: profile.full_name as string | null };
|
|
37
|
-
}
|
|
1
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin gate shared by the Messages server actions.
|
|
5
|
+
*
|
|
6
|
+
* Lives outside `actions.ts` because that file is `'use server'`, where every export
|
|
7
|
+
* must itself be a valid server action — a helper returning a Supabase client cannot be
|
|
8
|
+
* exported from there.
|
|
9
|
+
*
|
|
10
|
+
* This is the check standing between a WRITER and every private conversation on the
|
|
11
|
+
* site: visitor names, addresses, and free text. RLS enforces the same rule
|
|
12
|
+
* independently, so a bug here fails closed rather than open, but the redundancy is the
|
|
13
|
+
* point.
|
|
14
|
+
*/
|
|
15
|
+
export async function requireAdminSupabaseClient() {
|
|
16
|
+
const supabase = createClient();
|
|
17
|
+
const {
|
|
18
|
+
data: { user },
|
|
19
|
+
error: userError,
|
|
20
|
+
} = await supabase.auth.getUser();
|
|
21
|
+
|
|
22
|
+
if (userError || !user) {
|
|
23
|
+
throw new Error('You must be logged in to manage messages.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { data: profile, error: profileError } = await supabase
|
|
27
|
+
.from('profiles')
|
|
28
|
+
.select('role, full_name')
|
|
29
|
+
.eq('id', user.id)
|
|
30
|
+
.single();
|
|
31
|
+
|
|
32
|
+
if (profileError || !profile || profile.role !== 'ADMIN') {
|
|
33
|
+
throw new Error('You do not have permission to manage messages.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { supabase, userId: user.id, fullName: profile.full_name as string | null };
|
|
37
|
+
}
|
|
@@ -176,9 +176,9 @@ interface NavigationMenuDndProps {
|
|
|
176
176
|
initialItems: NavigationItem[];
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
export default function NavigationMenuDnd({ menuKey, languageCode, initialItems }: NavigationMenuDndProps) {
|
|
180
|
-
void menuKey;
|
|
181
|
-
// Prevent SSR/hydration mismatches from dnd-kit by rendering on client only
|
|
179
|
+
export default function NavigationMenuDnd({ menuKey, languageCode, initialItems }: NavigationMenuDndProps) {
|
|
180
|
+
void menuKey;
|
|
181
|
+
// Prevent SSR/hydration mismatches from dnd-kit by rendering on client only
|
|
182
182
|
// Keep hooks order stable; move early return after hooks
|
|
183
183
|
const [isMounted, setIsMounted] = useState(false);
|
|
184
184
|
useEffect(() => { setIsMounted(true); }, []);
|
|
@@ -14,6 +14,8 @@ import CopyContentFromLanguage from "../../../components/CopyContentFromLanguage
|
|
|
14
14
|
import RevisionHistoryButton from "../../../revisions/RevisionHistoryButton";
|
|
15
15
|
import { UploadFolderProvider } from '../../../media/UploadFolderContext';
|
|
16
16
|
import { CortexAiPageContextRegistrar } from "../../../components/CortexAiPageContext";
|
|
17
|
+
import { PageSeoAuditSection } from "../../../../../components/seo/PageSeoAuditSection";
|
|
18
|
+
import { PageSeoProvider } from "../../../../../lib/seo/page-audit-context";
|
|
17
19
|
import type { Database } from "@nextblock-cms/db";
|
|
18
20
|
import DraftStatusActions from "../../../components/DraftStatusActions";
|
|
19
21
|
|
|
@@ -76,6 +78,20 @@ export default function EditPageClient({
|
|
|
76
78
|
translationGroupId: page.translation_group_id,
|
|
77
79
|
}}
|
|
78
80
|
/>
|
|
81
|
+
{/*
|
|
82
|
+
The provider has to enclose both `PageForm` and `BlockEditorArea`,
|
|
83
|
+
because the page-level SEO audit is assembled from halves they each own:
|
|
84
|
+
the form publishes the meta title and description, the block editor
|
|
85
|
+
publishes the live block array, and neither knows the other exists. It is
|
|
86
|
+
seeded from the server-rendered row so the panel has a real document to
|
|
87
|
+
grade on first paint rather than an empty one that scores zero until the
|
|
88
|
+
block editor finishes mounting.
|
|
89
|
+
*/}
|
|
90
|
+
<PageSeoProvider
|
|
91
|
+
initialBlocks={page.blocks}
|
|
92
|
+
initialMetaDescription={page.meta_description}
|
|
93
|
+
initialMetaTitle={page.meta_title}
|
|
94
|
+
>
|
|
79
95
|
<div className="space-y-8 w-full mx-auto px-6">
|
|
80
96
|
<div className="flex justify-between items-center flex-wrap gap-4 w-full">
|
|
81
97
|
<div className="flex items-center gap-3">
|
|
@@ -151,6 +167,9 @@ export default function EditPageClient({
|
|
|
151
167
|
<PageForm
|
|
152
168
|
page={page}
|
|
153
169
|
formAction={updatePageAction}
|
|
170
|
+
// Read-only: the form never writes blocks, it only lets Cortex AI read them so it
|
|
171
|
+
// can summarize the page when drafting meta title and description.
|
|
172
|
+
contentBlocks={page.blocks}
|
|
154
173
|
actionButtonText="Update Page Metadata"
|
|
155
174
|
isEditing={true}
|
|
156
175
|
availableLanguagesProp={allSiteLanguages}
|
|
@@ -158,6 +177,14 @@ export default function EditPageClient({
|
|
|
158
177
|
initialFeatureImageId={initialFeatureImageId}
|
|
159
178
|
/>
|
|
160
179
|
|
|
180
|
+
{/*
|
|
181
|
+
Sits between the metadata form and the blocks because that is what it
|
|
182
|
+
grades: everything above it and everything below it, together. It
|
|
183
|
+
collapses to a single summary row so the block editor keeps its place
|
|
184
|
+
on the screen.
|
|
185
|
+
*/}
|
|
186
|
+
<PageSeoAuditSection />
|
|
187
|
+
|
|
161
188
|
<Separator className="my-8" />
|
|
162
189
|
|
|
163
190
|
<div className="w-full mx-auto px-6">
|
|
@@ -170,6 +197,7 @@ export default function EditPageClient({
|
|
|
170
197
|
/>
|
|
171
198
|
</div>
|
|
172
199
|
</div>
|
|
200
|
+
</PageSeoProvider>
|
|
173
201
|
</UploadFolderProvider>
|
|
174
202
|
);
|
|
175
203
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// app/cms/pages/components/PageForm.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { useEffect, useRef, useState, useTransition } from "react";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState, useTransition } from "react";
|
|
5
5
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
6
6
|
import { Button } from "@nextblock-cms/ui";
|
|
7
7
|
import { Spinner, Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
@@ -19,11 +19,27 @@ import type { Database } from "@nextblock-cms/db";
|
|
|
19
19
|
import { useAuth } from '../../../../context/AuthContext';
|
|
20
20
|
import { useHotkeys } from '../../../../hooks/use-hotkeys';
|
|
21
21
|
import FeatureImageField from "../../components/FeatureImageField";
|
|
22
|
+
import { useCortexAiActive } from "../../components/CortexAiActiveContext";
|
|
23
|
+
import GenerateMetaButton, {
|
|
24
|
+
type GeneratedSeoMetadata,
|
|
25
|
+
} from "../../../../components/seo/GenerateMetaButton";
|
|
26
|
+
import SocialPreviewDialog from "../../../../components/seo/SocialPreviewDialog";
|
|
27
|
+
import { buildSeoContentForGeneration } from "../../../../lib/seo/block-content";
|
|
28
|
+
import { usePageSeo } from "../../../../lib/seo/page-audit-context";
|
|
29
|
+
import { resolveSiteUrl } from "../../../../lib/site-url";
|
|
22
30
|
|
|
23
31
|
type Page = Database['public']['Tables']['pages']['Row'];
|
|
24
32
|
type Language = Database['public']['Tables']['languages']['Row'];
|
|
25
33
|
// Remove: import { getActiveLanguagesClientSide } from "@nextblock-cms/db";
|
|
26
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Google truncates a title around 60 characters and a description around 160. These are
|
|
37
|
+
* soft advice, not validation — the same numbers `SiteSeoSettingsForm` shows, and the same
|
|
38
|
+
* amber-past-the-limit treatment, so the counter means one thing everywhere in the CMS.
|
|
39
|
+
*/
|
|
40
|
+
const META_TITLE_RECOMMENDED_MAX = 60;
|
|
41
|
+
const META_DESCRIPTION_RECOMMENDED_MAX = 160;
|
|
42
|
+
|
|
27
43
|
interface PageFormProps {
|
|
28
44
|
page?: (Page & { feature_image_id?: string | null }) | null;
|
|
29
45
|
formAction: (formData: FormData) => Promise<{ error?: string } | void>;
|
|
@@ -34,6 +50,16 @@ interface PageFormProps {
|
|
|
34
50
|
target_lang_id?: string;
|
|
35
51
|
initialFeatureImageUrl?: string | null;
|
|
36
52
|
initialFeatureImageId?: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The page's block rows, read-only, purely so Cortex AI can be asked to summarize the
|
|
55
|
+
* page it is writing metadata for.
|
|
56
|
+
*
|
|
57
|
+
* Deliberately NOT a form field and NOT persisted by this form: nothing here is added to
|
|
58
|
+
* the FormData, to `hasChanges`, or to the autosave dependency list. It flows one way,
|
|
59
|
+
* parent → form → AI request. Blocks have their own editor and their own save path
|
|
60
|
+
* (`BlockEditorArea`); this form must never become a second writer of them.
|
|
61
|
+
*/
|
|
62
|
+
contentBlocks?: unknown;
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
export default function PageForm({
|
|
@@ -46,6 +72,7 @@ export default function PageForm({
|
|
|
46
72
|
target_lang_id,
|
|
47
73
|
initialFeatureImageUrl,
|
|
48
74
|
initialFeatureImageId,
|
|
75
|
+
contentBlocks,
|
|
49
76
|
}: PageFormProps) {
|
|
50
77
|
const router = useRouter();
|
|
51
78
|
const searchParams = useSearchParams();
|
|
@@ -83,6 +110,58 @@ export default function PageForm({
|
|
|
83
110
|
const [featureImageId, setFeatureImageId] = useState<string | null>(
|
|
84
111
|
initialFeatureImageId || page?.feature_image_id || null
|
|
85
112
|
);
|
|
113
|
+
/**
|
|
114
|
+
* The resolved URL of the feature image, for the share preview only.
|
|
115
|
+
*
|
|
116
|
+
* This one is exempt from everything the autosave path looks at, and the exemption is
|
|
117
|
+
* deliberate rather than an oversight: it is absent from the FormData, from `hasChanges`,
|
|
118
|
+
* and from the autosave effect's dependency array. `feature_image_id` is the persisted
|
|
119
|
+
* field — `FeatureImageField` renders it as the hidden input and the diff above already
|
|
120
|
+
* watches it — and the URL is nothing but that id resolved against the `media` row, which
|
|
121
|
+
* the server redoes on the next render. Persisting it would store a duplicate that can go
|
|
122
|
+
* stale, and adding it to the autosave deps would only schedule a second save for the
|
|
123
|
+
* change `featureImageId` already covers.
|
|
124
|
+
*
|
|
125
|
+
* It exists because the preview was previously bound to `initialFeatureImageUrl`, which
|
|
126
|
+
* is fixed for the life of the render — so picking an image left the share card showing
|
|
127
|
+
* the old one (or a placeholder) until a full reload, which defeats a live preview.
|
|
128
|
+
*/
|
|
129
|
+
const [featureImageUrl, setFeatureImageUrl] = useState<string | null>(
|
|
130
|
+
initialFeatureImageUrl || null
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const isCortexAiActive = useCortexAiActive();
|
|
134
|
+
/**
|
|
135
|
+
* The social-specific copy Cortex AI returns alongside the meta fields.
|
|
136
|
+
*
|
|
137
|
+
* This is held in component state and never written anywhere. There are no
|
|
138
|
+
* `og_title` / `og_description` columns on `pages`, and adding one is not a one-line
|
|
139
|
+
* change — a new persisted field needs the useState, the sync-from-prop effect AND its
|
|
140
|
+
* dependency array, `hasChanges`, the autosave deps, the FormData fallback list, the JSX
|
|
141
|
+
* `name`, the server action's rawFormData/updateData/payload type, and the draft overlay
|
|
142
|
+
* in the edit RSC, and missing any one of those makes the field silently never save.
|
|
143
|
+
* So the OG copy exists to inform the meta fields, which DO persist, and the operator can
|
|
144
|
+
* fold it in with one click. The live share card is built from the meta title and
|
|
145
|
+
* description below plus the feature image.
|
|
146
|
+
*/
|
|
147
|
+
const [socialSuggestion, setSocialSuggestion] = useState<{
|
|
148
|
+
ogDescription: string;
|
|
149
|
+
ogTitle: string;
|
|
150
|
+
} | null>(null);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Prose for the metadata call. Memoized because this form re-renders on every keystroke
|
|
154
|
+
* (each one re-arms the autosave timer) and flattening a page's whole block tree on each
|
|
155
|
+
* of those would be real, pointless work — the blocks themselves cannot change while
|
|
156
|
+
* this form is mounted, since they are edited by `BlockEditorArea` below.
|
|
157
|
+
*
|
|
158
|
+
* On a brand-new page with no blocks the title is a thin but genuine brief, which beats
|
|
159
|
+
* a permanently disabled button on day one.
|
|
160
|
+
*/
|
|
161
|
+
const seoContent = useMemo(
|
|
162
|
+
() => buildSeoContentForGeneration(contentBlocks, title),
|
|
163
|
+
[contentBlocks, title]
|
|
164
|
+
);
|
|
86
165
|
|
|
87
166
|
// Use the passed-in languages
|
|
88
167
|
const [availableLanguages] = useState<Language[]>(availableLanguagesProp);
|
|
@@ -120,8 +199,13 @@ export default function PageForm({
|
|
|
120
199
|
setMetaDescription(page.meta_description || "");
|
|
121
200
|
setCustomCanonical(page.custom_canonical || "");
|
|
122
201
|
setFeatureImageId(initialFeatureImageId || page.feature_image_id || null);
|
|
202
|
+
// Re-seeded in lockstep with the id above. Both come from the same server-resolved
|
|
203
|
+
// media row, so letting the id snap back to the props while the URL kept a newer value
|
|
204
|
+
// would put the preview and the field it previews out of step.
|
|
205
|
+
setFeatureImageUrl(initialFeatureImageUrl || null);
|
|
123
206
|
}, [
|
|
124
207
|
initialFeatureImageId,
|
|
208
|
+
initialFeatureImageUrl,
|
|
125
209
|
page?.id,
|
|
126
210
|
page?.custom_canonical,
|
|
127
211
|
page?.language_id,
|
|
@@ -225,6 +309,32 @@ export default function PageForm({
|
|
|
225
309
|
// meant editing only the canonical URL never scheduled a save.
|
|
226
310
|
}, [title, slug, languageId, metaTitle, metaDescription, customCanonical, featureImageId, page, isEditing]);
|
|
227
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Hand the live meta fields to the page-level SEO audit.
|
|
314
|
+
*
|
|
315
|
+
* The panel that grades a page cannot read this form's state directly — it renders as a
|
|
316
|
+
* sibling, not a child — so the title and description have to be published outward. This
|
|
317
|
+
* is a one-way broadcast of values this form ALREADY owns and already persists; it adds
|
|
318
|
+
* no field, changes nothing about what `hasChanges` above compares, and puts nothing in
|
|
319
|
+
* the FormData. The autosave diff is deliberately untouched, because publishing a value
|
|
320
|
+
* is not the same event as changing one: were `setMeta` wired into that diff, merely
|
|
321
|
+
* mounting the audit panel would look like an edit and schedule a draft write.
|
|
322
|
+
*
|
|
323
|
+
* `usePageSeo()` returns null when no provider is above this form, which is a supported
|
|
324
|
+
* arrangement rather than a bug — some surfaces embed these fields without a page-level
|
|
325
|
+
* panel — so the call is optional all the way through.
|
|
326
|
+
*
|
|
327
|
+
* The dependency is `setPageSeoMeta`, not the whole context value: the setter is a stable
|
|
328
|
+
* `useCallback`, whereas the context object is rebuilt whenever the blocks array changes,
|
|
329
|
+
* which would re-run this effect on every keystroke elsewhere in the editor. The setter
|
|
330
|
+
* also bails when the values are unchanged, so there is no path from this effect back into
|
|
331
|
+
* a render of this form and therefore no loop.
|
|
332
|
+
*/
|
|
333
|
+
const setPageSeoMeta = usePageSeo()?.setMeta;
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
setPageSeoMeta?.({ metaDescription, metaTitle });
|
|
336
|
+
}, [metaDescription, metaTitle, setPageSeoMeta]);
|
|
337
|
+
|
|
228
338
|
// Removed languagesLoading from this condition
|
|
229
339
|
if (authLoading) {
|
|
230
340
|
return <div>Loading form...</div>;
|
|
@@ -234,6 +344,46 @@ export default function PageForm({
|
|
|
234
344
|
return <div>Please log in to manage pages.</div>;
|
|
235
345
|
}
|
|
236
346
|
|
|
347
|
+
const selectedLanguageCode =
|
|
348
|
+
availableLanguages.find((lang) => lang.id.toString() === languageId)?.code ?? null;
|
|
349
|
+
// What the share card will actually show. Meta title wins when set, because that is what
|
|
350
|
+
// `buildSocialMetadata` uses; otherwise the page title, exactly as the public site does.
|
|
351
|
+
const previewTitle = metaTitle.trim() || title.trim();
|
|
352
|
+
const previewUrl = `${resolveSiteUrl()}/${(slug || "").replace(/^\/+/, "")}`;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Fold a generation into the form.
|
|
356
|
+
*
|
|
357
|
+
* Setting state is the whole persistence story here: the autosave effect above diffs
|
|
358
|
+
* this state against the `page` prop and calls `formAction` a second later, writing into
|
|
359
|
+
* `content_drafts`. A direct row UPDATE from this handler would bypass the draft and
|
|
360
|
+
* mutate the live page — precisely the thing draft mode exists to prevent.
|
|
361
|
+
*/
|
|
362
|
+
/**
|
|
363
|
+
* Track the picked image for the preview as well as for the save.
|
|
364
|
+
*
|
|
365
|
+
* The id half feeds `hasChanges` and therefore the autosave; the URL half is purely
|
|
366
|
+
* presentational (see the state declaration above) and stops here.
|
|
367
|
+
*/
|
|
368
|
+
const handleFeatureImageChange = (imageId: string | null, imageUrl: string | null) => {
|
|
369
|
+
setFeatureImageId(imageId);
|
|
370
|
+
setFeatureImageUrl(imageUrl);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const handleGeneratedMetadata = (result: GeneratedSeoMetadata) => {
|
|
374
|
+
if (result.metaTitle) {
|
|
375
|
+
setMetaTitle(result.metaTitle);
|
|
376
|
+
}
|
|
377
|
+
if (result.metaDescription) {
|
|
378
|
+
setMetaDescription(result.metaDescription);
|
|
379
|
+
}
|
|
380
|
+
setSocialSuggestion(
|
|
381
|
+
result.ogTitle || result.ogDescription
|
|
382
|
+
? { ogDescription: result.ogDescription, ogTitle: result.ogTitle }
|
|
383
|
+
: null
|
|
384
|
+
);
|
|
385
|
+
};
|
|
386
|
+
|
|
237
387
|
return (
|
|
238
388
|
<form ref={formRef} onSubmit={handleSubmit} className="space-y-4 w-full mx-auto px-6">
|
|
239
389
|
{isEditing && (
|
|
@@ -330,11 +480,52 @@ export default function PageForm({
|
|
|
330
480
|
</div>
|
|
331
481
|
</div>
|
|
332
482
|
|
|
333
|
-
{/* Row 2: SEO Settings. Canonical override (optional): blank = self-referencing canonical.
|
|
483
|
+
{/* Row 2: SEO Settings. Canonical override (optional): blank = self-referencing canonical.
|
|
484
|
+
|
|
485
|
+
This header is no longer gated on `isCortexAiActive`. It now carries the share-card
|
|
486
|
+
trigger, which has nothing to do with the AI package and must be reachable on every
|
|
487
|
+
install; only the generate button remains premium-gated, inside the row. */}
|
|
488
|
+
<div className="flex flex-wrap items-center justify-between gap-2 pt-1">
|
|
489
|
+
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80 font-semibold">
|
|
490
|
+
Search & social
|
|
491
|
+
</span>
|
|
492
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
493
|
+
{isCortexAiActive && (
|
|
494
|
+
<GenerateMetaButton
|
|
495
|
+
content={seoContent}
|
|
496
|
+
locale={selectedLanguageCode}
|
|
497
|
+
onGenerated={handleGeneratedMetadata}
|
|
498
|
+
title={title}
|
|
499
|
+
/>
|
|
500
|
+
)}
|
|
501
|
+
{/* The share-card rehearsal lives in a modal rather than inline: it is a glance,
|
|
502
|
+
not a field, and inline it added roughly four hundred pixels to a form that
|
|
503
|
+
already scrolls. Sitting here it is adjacent to the two inputs it previews.
|
|
504
|
+
Every prop is live form state, so the card repaints while the dialog is open,
|
|
505
|
+
and none of it is written anywhere — see `SocialPreviewDialog`. */}
|
|
506
|
+
<SocialPreviewDialog
|
|
507
|
+
description={metaDescription}
|
|
508
|
+
imageUrl={featureImageUrl}
|
|
509
|
+
title={previewTitle}
|
|
510
|
+
url={previewUrl}
|
|
511
|
+
/>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
334
514
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
|
|
335
515
|
{/* Meta Title */}
|
|
336
516
|
<div className="md:col-span-4 flex flex-col gap-1">
|
|
337
|
-
<
|
|
517
|
+
<div className="flex items-center justify-between gap-2">
|
|
518
|
+
<Label htmlFor="meta_title" className="text-xs font-medium">Meta Title (SEO)</Label>
|
|
519
|
+
<span
|
|
520
|
+
className={`text-[10px] ${
|
|
521
|
+
metaTitle.length > META_TITLE_RECOMMENDED_MAX
|
|
522
|
+
? 'text-amber-600'
|
|
523
|
+
: 'text-muted-foreground'
|
|
524
|
+
}`}
|
|
525
|
+
>
|
|
526
|
+
{metaTitle.length}/{META_TITLE_RECOMMENDED_MAX}
|
|
527
|
+
</span>
|
|
528
|
+
</div>
|
|
338
529
|
<Input
|
|
339
530
|
id="meta_title"
|
|
340
531
|
name="meta_title"
|
|
@@ -346,7 +537,18 @@ export default function PageForm({
|
|
|
346
537
|
|
|
347
538
|
{/* Meta Description */}
|
|
348
539
|
<div className="md:col-span-4 flex flex-col gap-1">
|
|
349
|
-
<
|
|
540
|
+
<div className="flex items-center justify-between gap-2">
|
|
541
|
+
<Label htmlFor="meta_description" className="text-xs font-medium">Meta Description (SEO)</Label>
|
|
542
|
+
<span
|
|
543
|
+
className={`text-[10px] ${
|
|
544
|
+
metaDescription.length > META_DESCRIPTION_RECOMMENDED_MAX
|
|
545
|
+
? 'text-amber-600'
|
|
546
|
+
: 'text-muted-foreground'
|
|
547
|
+
}`}
|
|
548
|
+
>
|
|
549
|
+
{metaDescription.length}/{META_DESCRIPTION_RECOMMENDED_MAX}
|
|
550
|
+
</span>
|
|
551
|
+
</div>
|
|
350
552
|
<Textarea
|
|
351
553
|
id="meta_description"
|
|
352
554
|
name="meta_description"
|
|
@@ -372,10 +574,51 @@ export default function PageForm({
|
|
|
372
574
|
</div>
|
|
373
575
|
</div>
|
|
374
576
|
|
|
577
|
+
{/* Social-specific copy Cortex AI suggested. It is shown, not stored: pages have no
|
|
578
|
+
Open Graph columns, so the only way it can survive is by being folded into the
|
|
579
|
+
meta fields — hence the one-click apply rather than a read-only curiosity. */}
|
|
580
|
+
{socialSuggestion && (
|
|
581
|
+
<div className="rounded-md border border-border bg-muted/30 p-3 space-y-2">
|
|
582
|
+
<p className="text-[11px] font-medium text-muted-foreground">
|
|
583
|
+
Cortex AI also drafted social-specific copy. Pages have no separate Open Graph
|
|
584
|
+
fields, so this is only a suggestion for the meta fields above.
|
|
585
|
+
</p>
|
|
586
|
+
{socialSuggestion.ogTitle && (
|
|
587
|
+
<p className="text-xs text-foreground"><strong>Social title:</strong> {socialSuggestion.ogTitle}</p>
|
|
588
|
+
)}
|
|
589
|
+
{socialSuggestion.ogDescription && (
|
|
590
|
+
<p className="text-xs text-foreground"><strong>Social description:</strong> {socialSuggestion.ogDescription}</p>
|
|
591
|
+
)}
|
|
592
|
+
<div className="flex gap-2">
|
|
593
|
+
<Button
|
|
594
|
+
type="button"
|
|
595
|
+
variant="outline"
|
|
596
|
+
size="sm"
|
|
597
|
+
className="h-7 px-2 text-[11px]"
|
|
598
|
+
onClick={() => {
|
|
599
|
+
if (socialSuggestion.ogTitle) setMetaTitle(socialSuggestion.ogTitle);
|
|
600
|
+
if (socialSuggestion.ogDescription) setMetaDescription(socialSuggestion.ogDescription);
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
Use for meta fields
|
|
604
|
+
</Button>
|
|
605
|
+
<Button
|
|
606
|
+
type="button"
|
|
607
|
+
variant="ghost"
|
|
608
|
+
size="sm"
|
|
609
|
+
className="h-7 px-2 text-[11px]"
|
|
610
|
+
onClick={() => setSocialSuggestion(null)}
|
|
611
|
+
>
|
|
612
|
+
Dismiss
|
|
613
|
+
</Button>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
)}
|
|
617
|
+
|
|
375
618
|
<FeatureImageField
|
|
376
619
|
initialImageId={initialFeatureImageId || page?.feature_image_id || null}
|
|
377
620
|
initialImageUrl={initialFeatureImageUrl || null}
|
|
378
|
-
onImageIdChange={
|
|
621
|
+
onImageIdChange={handleFeatureImageChange}
|
|
379
622
|
uploadFolder={`pages/${(slug || 'untitled').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '')}/`}
|
|
380
623
|
/>
|
|
381
624
|
|