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.
Files changed (266) hide show
  1. package/docker-template/.dockerignore +1 -1
  2. package/libs/db/tsconfig.lib.json +3 -3
  3. package/libs/editor/tsconfig.lib.json +3 -3
  4. package/libs/ui/tsconfig.lib.json +3 -3
  5. package/libs/utils/tsconfig.json +3 -3
  6. package/package.json +50 -50
  7. package/project.json +19 -19
  8. package/templates/nextblock-template/.dockerignore +1 -1
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/AGENTS.md +9 -9
  11. package/templates/nextblock-template/CLAUDE.md +1 -1
  12. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  13. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  14. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  15. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
  16. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  17. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
  18. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
  19. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
  20. package/templates/nextblock-template/app/actions/email.ts +241 -241
  21. package/templates/nextblock-template/app/actions/formActions.ts +245 -245
  22. package/templates/nextblock-template/app/actions/interactions.ts +489 -489
  23. package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
  24. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
  25. package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
  27. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  28. package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
  29. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
  30. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  31. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  32. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  33. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  34. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  35. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  37. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  38. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  39. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  40. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  43. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  44. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  45. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
  46. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
  47. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  48. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  49. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  50. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
  51. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  52. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  53. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  54. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  55. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  56. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  58. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  59. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  60. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  63. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  64. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  65. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
  66. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  67. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  68. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  69. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  70. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  71. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  72. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  73. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
  74. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
  77. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  78. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  79. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  80. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  81. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  82. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  83. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  84. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  85. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  86. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  90. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  91. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  92. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  93. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  96. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  98. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  99. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  100. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  101. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  102. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  103. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
  104. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
  105. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
  106. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
  107. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
  108. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
  109. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
  110. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  111. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  112. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  113. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  114. package/templates/nextblock-template/app/layout.tsx +671 -671
  115. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  116. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  117. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  118. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  119. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  120. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  121. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  122. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  123. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  124. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  125. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  126. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  127. package/templates/nextblock-template/app/providers.tsx +96 -96
  128. package/templates/nextblock-template/app/robots.ts +123 -0
  129. package/templates/nextblock-template/app/sitemap.ts +1 -1
  130. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  131. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  132. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  133. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  134. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  135. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  136. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  137. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  138. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  139. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  140. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  141. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  142. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  143. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  144. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  145. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  146. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  147. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  148. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  149. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  150. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  151. package/templates/nextblock-template/components/Header.tsx +38 -38
  152. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  153. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  154. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  155. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  156. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  157. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  158. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  159. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  160. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  161. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  162. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  163. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  164. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  165. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  166. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  167. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  168. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  169. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  170. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  171. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  172. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  173. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  174. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  175. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  176. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  177. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  178. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  179. package/templates/nextblock-template/components/form-message.tsx +32 -32
  180. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  189. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  192. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  193. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  194. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  195. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  196. package/templates/nextblock-template/docs/README.md +42 -42
  197. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  198. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  199. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  200. package/templates/nextblock-template/index.d.ts +7 -7
  201. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  202. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  203. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  204. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  205. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  206. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  207. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  208. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  209. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  210. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  211. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  212. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  213. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
  215. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  216. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  217. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  218. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  219. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  220. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  221. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  222. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  223. package/templates/nextblock-template/lib/search/types.ts +27 -27
  224. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
  231. package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
  239. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  240. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  241. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  242. package/templates/nextblock-template/lib/site-url.ts +102 -102
  243. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  244. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  245. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  246. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  247. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  248. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  249. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  250. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  251. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  252. package/templates/nextblock-template/next-env.d.ts +0 -1
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/proxy.ts +240 -20
  256. package/templates/nextblock-template/scripts/backup.js +115 -115
  257. package/templates/nextblock-template/scripts/restore.js +385 -385
  258. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  259. package/templates/nextblock-template/tailwind.config.js +25 -25
  260. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  261. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  262. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  263. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  264. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  265. package/tsconfig.base.json +3 -3
  266. 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 &amp; 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
- <Label htmlFor="meta_title" className="text-xs font-medium">Meta Title (SEO)</Label>
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
- <Label htmlFor="meta_description" className="text-xs font-medium">Meta Description (SEO)</Label>
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={setFeatureImageId}
598
+ onImageIdChange={handleFeatureImageChange}
364
599
  uploadFolder={`posts/${(slug || 'untitled').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '')}/`}
365
600
  />
366
601