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
|
@@ -21,6 +21,8 @@ import { CortexAiPageContextRegistrar } from "../../../components/CortexAiPageCo
|
|
|
21
21
|
import DraftStatusActions from "../../../components/DraftStatusActions";
|
|
22
22
|
import VisibilityControl from "../../../components/VisibilityControl";
|
|
23
23
|
import { buildViewUrl } from "../../../../../lib/publishing/viewUrl";
|
|
24
|
+
import { PageSeoAuditSection } from "../../../../../components/seo/PageSeoAuditSection";
|
|
25
|
+
import { PageSeoProvider } from "../../../../../lib/seo/page-audit-context";
|
|
24
26
|
|
|
25
27
|
type PostType = Database['public']['Tables']['posts']['Row'];
|
|
26
28
|
type BlockType = Database['public']['Tables']['blocks']['Row'];
|
|
@@ -172,6 +174,25 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
172
174
|
translationGroupId: postWithBlocks.translation_group_id,
|
|
173
175
|
}}
|
|
174
176
|
/>
|
|
177
|
+
{/*
|
|
178
|
+
A server component may render a client provider around client children,
|
|
179
|
+
which is the whole reason this file does not need converting: everything
|
|
180
|
+
inside the provider that touches the context — the form, the block editor
|
|
181
|
+
and the audit section — is already a client component, and this file
|
|
182
|
+
stays an RSC that can keep awaiting Supabase above them.
|
|
183
|
+
|
|
184
|
+
The provider must enclose both `PostForm` and `BlockEditorArea` because
|
|
185
|
+
the audit is assembled from halves they each own: the form publishes the
|
|
186
|
+
meta title and description, the block editor publishes the live blocks.
|
|
187
|
+
Seeding it from the row (draft overlay included, since `postWithBlocks`
|
|
188
|
+
already carries it) means the panel grades the real article on first
|
|
189
|
+
paint instead of an empty document.
|
|
190
|
+
*/}
|
|
191
|
+
<PageSeoProvider
|
|
192
|
+
initialBlocks={postWithBlocks.blocks}
|
|
193
|
+
initialMetaDescription={postWithBlocks.meta_description}
|
|
194
|
+
initialMetaTitle={postWithBlocks.meta_title}
|
|
195
|
+
>
|
|
175
196
|
<div className="space-y-8 w-full mx-auto px-6">
|
|
176
197
|
<div className="flex justify-between items-center flex-wrap gap-4 w-full">
|
|
177
198
|
<div className="flex items-center gap-3">
|
|
@@ -235,6 +256,9 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
235
256
|
<PostForm
|
|
236
257
|
post={postWithBlocks as PostType & { feature_image_id?: string | null }}
|
|
237
258
|
formAction={updatePostWithId}
|
|
259
|
+
// Read-only: the form never writes blocks, it only lets Cortex AI read them so it
|
|
260
|
+
// can summarize the article when drafting meta title and description.
|
|
261
|
+
contentBlocks={postWithBlocks.blocks}
|
|
238
262
|
actionButtonText="Update Post Metadata"
|
|
239
263
|
isEditing={true}
|
|
240
264
|
availableLanguagesProp={allSiteLanguages}
|
|
@@ -242,6 +266,14 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
242
266
|
initialFeatureImageId={initialFeatureImageIdProp}
|
|
243
267
|
/>
|
|
244
268
|
|
|
269
|
+
{/*
|
|
270
|
+
Between the metadata form and the blocks because that is exactly what
|
|
271
|
+
it grades: everything above it and everything below it, together. It
|
|
272
|
+
collapses to a single summary row so the block editor keeps its place
|
|
273
|
+
on the screen.
|
|
274
|
+
*/}
|
|
275
|
+
<PageSeoAuditSection />
|
|
276
|
+
|
|
245
277
|
<Separator className="my-8" />
|
|
246
278
|
|
|
247
279
|
<div className="w-full mx-auto px-6">
|
|
@@ -254,6 +286,7 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
254
286
|
/>
|
|
255
287
|
</div>
|
|
256
288
|
</div>
|
|
289
|
+
</PageSeoProvider>
|
|
257
290
|
</UploadFolderProvider>
|
|
258
291
|
);
|
|
259
292
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// app/cms/posts/components/PostForm.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 { Input } from "@nextblock-cms/ui";
|
|
@@ -18,11 +18,25 @@ import { Textarea } from "@nextblock-cms/ui";
|
|
|
18
18
|
import type { Database } from "@nextblock-cms/db";
|
|
19
19
|
import { useAuth } from '../../../../context/AuthContext';
|
|
20
20
|
import FeatureImageField from "../../components/FeatureImageField";
|
|
21
|
+
import { useCortexAiActive } from "../../components/CortexAiActiveContext";
|
|
22
|
+
import GenerateMetaButton, {
|
|
23
|
+
type GeneratedSeoMetadata,
|
|
24
|
+
} from "../../../../components/seo/GenerateMetaButton";
|
|
25
|
+
import SocialPreviewDialog from "../../../../components/seo/SocialPreviewDialog";
|
|
26
|
+
import { buildSeoContentForGeneration } from "../../../../lib/seo/block-content";
|
|
27
|
+
import { usePageSeo } from "../../../../lib/seo/page-audit-context";
|
|
28
|
+
import { resolveSiteUrl } from "../../../../lib/site-url";
|
|
21
29
|
|
|
22
30
|
type Post = Database['public']['Tables']['posts']['Row'];
|
|
23
31
|
type Language = Database['public']['Tables']['languages']['Row'];
|
|
24
32
|
import { useHotkeys } from '../../../../hooks/use-hotkeys';
|
|
25
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The same soft limits `SiteSeoSettingsForm` and `PageForm` show, with the same
|
|
36
|
+
* amber-past-the-limit treatment, so a counter means one thing everywhere in the CMS.
|
|
37
|
+
*/
|
|
38
|
+
const META_TITLE_RECOMMENDED_MAX = 60;
|
|
39
|
+
const META_DESCRIPTION_RECOMMENDED_MAX = 160;
|
|
26
40
|
|
|
27
41
|
interface PostFormProps {
|
|
28
42
|
post?: Post & { feature_image_id?: string | null };
|
|
@@ -32,6 +46,16 @@ interface PostFormProps {
|
|
|
32
46
|
availableLanguagesProp?: Language[]; // Make optional
|
|
33
47
|
initialFeatureImageUrl?: string | null;
|
|
34
48
|
initialFeatureImageId?: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* The post's block rows, read-only, so Cortex AI can be asked to summarize the article
|
|
51
|
+
* it is writing metadata for.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately NOT a form field and NOT persisted: it is absent from the FormData, from
|
|
54
|
+
* `hasChanges`, and from the autosave dependency list. Blocks are owned by
|
|
55
|
+
* `BlockEditorArea` and have their own save path; this form must never become a second
|
|
56
|
+
* writer of them.
|
|
57
|
+
*/
|
|
58
|
+
contentBlocks?: unknown;
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
export default function PostForm({
|
|
@@ -42,6 +66,7 @@ export default function PostForm({
|
|
|
42
66
|
availableLanguagesProp = [], // Default to empty array
|
|
43
67
|
initialFeatureImageUrl,
|
|
44
68
|
initialFeatureImageId,
|
|
69
|
+
contentBlocks,
|
|
45
70
|
}: PostFormProps) {
|
|
46
71
|
const router = useRouter();
|
|
47
72
|
const searchParams = useSearchParams();
|
|
@@ -64,6 +89,55 @@ export default function PostForm({
|
|
|
64
89
|
const [featureImageId, setFeatureImageId] = useState<string | null>(
|
|
65
90
|
initialFeatureImageId || post?.feature_image_id || null
|
|
66
91
|
);
|
|
92
|
+
/**
|
|
93
|
+
* The resolved URL of the feature image, for the share preview only.
|
|
94
|
+
*
|
|
95
|
+
* Exempt from the whole autosave path on purpose, not by omission: it is absent from the
|
|
96
|
+
* FormData, from `hasChanges`, and from the autosave effect's dependency array.
|
|
97
|
+
* `feature_image_id` is the field that persists — `FeatureImageField` renders it as the
|
|
98
|
+
* hidden input and the diff below already watches it — and this URL is only that id
|
|
99
|
+
* resolved against the `media` row, which the server redoes on every render. Storing it
|
|
100
|
+
* would store a duplicate that can go stale, and putting it in the autosave deps would
|
|
101
|
+
* schedule a second save for a change `featureImageId` already covers.
|
|
102
|
+
*
|
|
103
|
+
* It exists because the preview used to read `initialFeatureImageUrl`, which is fixed for
|
|
104
|
+
* the life of the render, so picking an image left the share card showing the previous one
|
|
105
|
+
* (or a placeholder) until a reload — which is the opposite of a live preview.
|
|
106
|
+
*/
|
|
107
|
+
const [featureImageUrl, setFeatureImageUrl] = useState<string | null>(
|
|
108
|
+
initialFeatureImageUrl || null
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const isCortexAiActive = useCortexAiActive();
|
|
112
|
+
/**
|
|
113
|
+
* Social-specific copy from Cortex AI, held in state and never stored.
|
|
114
|
+
*
|
|
115
|
+
* `posts` has no `og_title` / `og_description` columns, and adding one is not a one-line
|
|
116
|
+
* change — a new persisted field needs the useState, the sync-from-prop effect AND its
|
|
117
|
+
* dependency array, `hasChanges`, the autosave deps, the FormData fallback list, the JSX
|
|
118
|
+
* `name`, the server action's rawFormData/updateData/payload type, and the draft overlay
|
|
119
|
+
* in the edit RSC; miss one and the field silently never saves. So the OG copy informs
|
|
120
|
+
* the meta fields, which do persist, and the share card is built from those plus the
|
|
121
|
+
* feature image.
|
|
122
|
+
*/
|
|
123
|
+
const [socialSuggestion, setSocialSuggestion] = useState<{
|
|
124
|
+
ogDescription: string;
|
|
125
|
+
ogTitle: string;
|
|
126
|
+
} | null>(null);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Prose for the metadata call: the article body first, then the editorial summaries the
|
|
130
|
+
* author has already written, so a post that is still an outline can still be described.
|
|
131
|
+
*
|
|
132
|
+
* Memoized because this form re-renders on every keystroke (each one re-arms the autosave
|
|
133
|
+
* timer), and re-flattening the whole article on each of those would be real, pointless
|
|
134
|
+
* work — the blocks cannot change while this form is mounted, since `BlockEditorArea`
|
|
135
|
+
* below owns them.
|
|
136
|
+
*/
|
|
137
|
+
const seoContent = useMemo(
|
|
138
|
+
() => buildSeoContentForGeneration(contentBlocks, excerpt, subtitle, title),
|
|
139
|
+
[contentBlocks, excerpt, subtitle, title]
|
|
140
|
+
);
|
|
67
141
|
|
|
68
142
|
// Use the passed-in languages directly
|
|
69
143
|
const [availableLanguages] = useState<Language[]>(availableLanguagesProp);
|
|
@@ -103,8 +177,13 @@ export default function PostForm({
|
|
|
103
177
|
setMetaDescription(post.meta_description || "");
|
|
104
178
|
setCustomCanonical(post.custom_canonical || "");
|
|
105
179
|
setFeatureImageId(initialFeatureImageId || post.feature_image_id || null);
|
|
180
|
+
// Re-seeded in lockstep with the id above: both are derived from the same
|
|
181
|
+
// server-resolved media row, so snapping the id back to the props while the URL kept a
|
|
182
|
+
// newer value would leave the preview describing an image the field no longer holds.
|
|
183
|
+
setFeatureImageUrl(initialFeatureImageUrl || null);
|
|
106
184
|
}, [
|
|
107
185
|
initialFeatureImageId,
|
|
186
|
+
initialFeatureImageUrl,
|
|
108
187
|
post?.custom_canonical,
|
|
109
188
|
post?.excerpt,
|
|
110
189
|
post?.id,
|
|
@@ -236,6 +315,32 @@ export default function PostForm({
|
|
|
236
315
|
isEditing,
|
|
237
316
|
]);
|
|
238
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Hand the live meta fields to the page-level SEO audit.
|
|
320
|
+
*
|
|
321
|
+
* The panel that grades an article renders as a sibling of this form, not a child, so it
|
|
322
|
+
* cannot read this state directly — the values have to be published outward. This is a
|
|
323
|
+
* one-way broadcast of fields this form ALREADY owns and already persists: it adds no
|
|
324
|
+
* field, changes nothing about what `hasChanges` above compares, and puts nothing in the
|
|
325
|
+
* FormData. Leaving the autosave diff alone is the point, because publishing a value is a
|
|
326
|
+
* different event from changing one — wiring `setMeta` into that diff would make merely
|
|
327
|
+
* mounting the audit panel look like an edit and schedule a draft write.
|
|
328
|
+
*
|
|
329
|
+
* `usePageSeo()` returns null when no provider sits above this form. That is a supported
|
|
330
|
+
* arrangement rather than a bug — these fields are reachable from surfaces with no
|
|
331
|
+
* page-level panel — so the call stays optional the whole way through.
|
|
332
|
+
*
|
|
333
|
+
* The dependency is `setPageSeoMeta` rather than the context value: the setter is a stable
|
|
334
|
+
* `useCallback`, while the context object is rebuilt whenever the blocks array changes,
|
|
335
|
+
* which would re-run this effect on every keystroke elsewhere in the editor. The setter
|
|
336
|
+
* also bails when the values are unchanged, so nothing here can loop back into a render of
|
|
337
|
+
* this form.
|
|
338
|
+
*/
|
|
339
|
+
const setPageSeoMeta = usePageSeo()?.setMeta;
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
setPageSeoMeta?.({ metaDescription, metaTitle });
|
|
342
|
+
}, [metaDescription, metaTitle, setPageSeoMeta]);
|
|
343
|
+
|
|
239
344
|
// Remove languagesLoading from this condition
|
|
240
345
|
if (authLoading) {
|
|
241
346
|
return <div>Loading form...</div>;
|
|
@@ -244,6 +349,44 @@ export default function PostForm({
|
|
|
244
349
|
return <div>Please log in to manage posts.</div>;
|
|
245
350
|
}
|
|
246
351
|
|
|
352
|
+
const selectedLanguageCode =
|
|
353
|
+
availableLanguages.find((lang) => lang.id.toString() === languageId)?.code ?? null;
|
|
354
|
+
const previewTitle = metaTitle.trim() || title.trim();
|
|
355
|
+
const previewUrl = `${resolveSiteUrl()}/article/${(slug || "").replace(/^\/+/, "")}`;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Fold a generation into the form.
|
|
359
|
+
*
|
|
360
|
+
* Setting state is the entire persistence story: the autosave effect diffs this state
|
|
361
|
+
* against the `post` prop and calls `formAction` a second later, writing into
|
|
362
|
+
* `content_drafts`. A direct row UPDATE here would bypass the draft and mutate the live
|
|
363
|
+
* post — exactly what draft mode exists to prevent.
|
|
364
|
+
*/
|
|
365
|
+
/**
|
|
366
|
+
* Track the picked image for the preview as well as for the save.
|
|
367
|
+
*
|
|
368
|
+
* The id half feeds `hasChanges` and therefore the autosave; the URL half is purely
|
|
369
|
+
* presentational (see the state declaration above) and goes no further than the card.
|
|
370
|
+
*/
|
|
371
|
+
const handleFeatureImageChange = (imageId: string | null, imageUrl: string | null) => {
|
|
372
|
+
setFeatureImageId(imageId);
|
|
373
|
+
setFeatureImageUrl(imageUrl);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const handleGeneratedMetadata = (result: GeneratedSeoMetadata) => {
|
|
377
|
+
if (result.metaTitle) {
|
|
378
|
+
setMetaTitle(result.metaTitle);
|
|
379
|
+
}
|
|
380
|
+
if (result.metaDescription) {
|
|
381
|
+
setMetaDescription(result.metaDescription);
|
|
382
|
+
}
|
|
383
|
+
setSocialSuggestion(
|
|
384
|
+
result.ogTitle || result.ogDescription
|
|
385
|
+
? { ogDescription: result.ogDescription, ogTitle: result.ogTitle }
|
|
386
|
+
: null
|
|
387
|
+
);
|
|
388
|
+
};
|
|
389
|
+
|
|
247
390
|
return (
|
|
248
391
|
<form ref={formRef} onSubmit={handleSubmit} className="space-y-4 w-full mx-auto px-6">
|
|
249
392
|
{isEditing && (
|
|
@@ -336,17 +479,69 @@ export default function PostForm({
|
|
|
336
479
|
</div>
|
|
337
480
|
</div>
|
|
338
481
|
|
|
339
|
-
{/* Row 4: SEO Settings. Canonical override (optional): blank = self-referencing canonical.
|
|
482
|
+
{/* Row 4: SEO Settings. Canonical override (optional): blank = self-referencing canonical.
|
|
483
|
+
|
|
484
|
+
This header is no longer gated on `isCortexAiActive`. It now carries the share-card
|
|
485
|
+
trigger, which has nothing to do with the AI package and must be reachable on every
|
|
486
|
+
install; only the generate button stays premium-gated, inside the row. */}
|
|
487
|
+
<div className="flex flex-wrap items-center justify-between gap-2 pt-1">
|
|
488
|
+
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80 font-semibold">
|
|
489
|
+
Search & social
|
|
490
|
+
</span>
|
|
491
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
492
|
+
{isCortexAiActive && (
|
|
493
|
+
<GenerateMetaButton
|
|
494
|
+
content={seoContent}
|
|
495
|
+
locale={selectedLanguageCode}
|
|
496
|
+
onGenerated={handleGeneratedMetadata}
|
|
497
|
+
title={title}
|
|
498
|
+
/>
|
|
499
|
+
)}
|
|
500
|
+
{/* The share-card rehearsal moved into a modal: it is a glance, not a field, and
|
|
501
|
+
inline it added roughly four hundred pixels to a form that already scrolls.
|
|
502
|
+
Here it sits beside the two inputs it previews. Every prop is live form state,
|
|
503
|
+
so the card repaints while the dialog is open, and none of it is written
|
|
504
|
+
anywhere — see `SocialPreviewDialog`. */}
|
|
505
|
+
<SocialPreviewDialog
|
|
506
|
+
description={metaDescription}
|
|
507
|
+
imageUrl={featureImageUrl}
|
|
508
|
+
title={previewTitle}
|
|
509
|
+
url={previewUrl}
|
|
510
|
+
/>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
340
513
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
|
|
341
514
|
{/* Meta Title */}
|
|
342
515
|
<div className="md:col-span-4 flex flex-col gap-1">
|
|
343
|
-
<
|
|
516
|
+
<div className="flex items-center justify-between gap-2">
|
|
517
|
+
<Label htmlFor="meta_title" className="text-xs font-medium">Meta Title (SEO)</Label>
|
|
518
|
+
<span
|
|
519
|
+
className={`text-[10px] ${
|
|
520
|
+
metaTitle.length > META_TITLE_RECOMMENDED_MAX
|
|
521
|
+
? 'text-amber-600'
|
|
522
|
+
: 'text-muted-foreground'
|
|
523
|
+
}`}
|
|
524
|
+
>
|
|
525
|
+
{metaTitle.length}/{META_TITLE_RECOMMENDED_MAX}
|
|
526
|
+
</span>
|
|
527
|
+
</div>
|
|
344
528
|
<Input id="meta_title" name="meta_title" value={metaTitle} onChange={(e) => setMetaTitle(e.target.value)} className="h-9" />
|
|
345
529
|
</div>
|
|
346
530
|
|
|
347
531
|
{/* Meta Description */}
|
|
348
532
|
<div className="md:col-span-4 flex flex-col gap-1">
|
|
349
|
-
<
|
|
533
|
+
<div className="flex items-center justify-between gap-2">
|
|
534
|
+
<Label htmlFor="meta_description" className="text-xs font-medium">Meta Description (SEO)</Label>
|
|
535
|
+
<span
|
|
536
|
+
className={`text-[10px] ${
|
|
537
|
+
metaDescription.length > META_DESCRIPTION_RECOMMENDED_MAX
|
|
538
|
+
? 'text-amber-600'
|
|
539
|
+
: 'text-muted-foreground'
|
|
540
|
+
}`}
|
|
541
|
+
>
|
|
542
|
+
{metaDescription.length}/{META_DESCRIPTION_RECOMMENDED_MAX}
|
|
543
|
+
</span>
|
|
544
|
+
</div>
|
|
350
545
|
<Textarea id="meta_description" name="meta_description" value={metaDescription} onChange={(e) => setMetaDescription(e.target.value)} className="min-h-[36px] h-9 py-1.5 resize-y text-sm leading-normal" rows={1} placeholder="Meta description for search engines..." />
|
|
351
546
|
</div>
|
|
352
547
|
|
|
@@ -357,10 +552,50 @@ export default function PostForm({
|
|
|
357
552
|
</div>
|
|
358
553
|
</div>
|
|
359
554
|
|
|
555
|
+
{/* Suggested social copy. Shown, not stored: posts have no Open Graph columns, so the
|
|
556
|
+
only way this survives is by being folded into the meta fields — hence apply. */}
|
|
557
|
+
{socialSuggestion && (
|
|
558
|
+
<div className="rounded-md border border-border bg-muted/30 p-3 space-y-2">
|
|
559
|
+
<p className="text-[11px] font-medium text-muted-foreground">
|
|
560
|
+
Cortex AI also drafted social-specific copy. Posts have no separate Open Graph
|
|
561
|
+
fields, so this is only a suggestion for the meta fields above.
|
|
562
|
+
</p>
|
|
563
|
+
{socialSuggestion.ogTitle && (
|
|
564
|
+
<p className="text-xs text-foreground"><strong>Social title:</strong> {socialSuggestion.ogTitle}</p>
|
|
565
|
+
)}
|
|
566
|
+
{socialSuggestion.ogDescription && (
|
|
567
|
+
<p className="text-xs text-foreground"><strong>Social description:</strong> {socialSuggestion.ogDescription}</p>
|
|
568
|
+
)}
|
|
569
|
+
<div className="flex gap-2">
|
|
570
|
+
<Button
|
|
571
|
+
type="button"
|
|
572
|
+
variant="outline"
|
|
573
|
+
size="sm"
|
|
574
|
+
className="h-7 px-2 text-[11px]"
|
|
575
|
+
onClick={() => {
|
|
576
|
+
if (socialSuggestion.ogTitle) setMetaTitle(socialSuggestion.ogTitle);
|
|
577
|
+
if (socialSuggestion.ogDescription) setMetaDescription(socialSuggestion.ogDescription);
|
|
578
|
+
}}
|
|
579
|
+
>
|
|
580
|
+
Use for meta fields
|
|
581
|
+
</Button>
|
|
582
|
+
<Button
|
|
583
|
+
type="button"
|
|
584
|
+
variant="ghost"
|
|
585
|
+
size="sm"
|
|
586
|
+
className="h-7 px-2 text-[11px]"
|
|
587
|
+
onClick={() => setSocialSuggestion(null)}
|
|
588
|
+
>
|
|
589
|
+
Dismiss
|
|
590
|
+
</Button>
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
)}
|
|
594
|
+
|
|
360
595
|
<FeatureImageField
|
|
361
596
|
initialImageId={initialFeatureImageId || post?.feature_image_id || null}
|
|
362
597
|
initialImageUrl={initialFeatureImageUrl || null}
|
|
363
|
-
onImageIdChange={
|
|
598
|
+
onImageIdChange={handleFeatureImageChange}
|
|
364
599
|
uploadFolder={`posts/${(slug || 'untitled').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '')}/`}
|
|
365
600
|
/>
|
|
366
601
|
|