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
@@ -0,0 +1,244 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * The page-level SEO audit, as it appears on the page and post edit screens.
5
+ *
6
+ * WHAT THIS IS FOR. `SeoAuditPanel` grades whatever document it is handed; this
7
+ * component is the thing that decides *which* document that is on an edit screen
8
+ * and how much room the result is allowed to take. It reads the shared
9
+ * `PageSeoProvider` state — the live block array published by `BlockEditorArea`
10
+ * and the meta title and description published by the page or post form — merges
11
+ * them with `buildPageSeoDocument`, and hands the result to the panel at page
12
+ * scope. That merge is the entire point of the feature: headings live both in
13
+ * standalone `heading` blocks and inside a text block's HTML, and section blocks
14
+ * carry more blocks in their columns, so "does this page have exactly one H1"
15
+ * simply cannot be answered from inside one block editor.
16
+ *
17
+ * WHY IT IS COLLAPSED BY DEFAULT. The block editor is the reason anyone opens
18
+ * this screen. A full analysis panel expanded above it would push the first
19
+ * block below the fold on a laptop every time, which is a tax paid on every
20
+ * edit for a report most sessions never read. Collapsed, the section is a single
21
+ * row: the score, the number of findings, and a button. That row is enough to
22
+ * decide whether the report is worth opening, which is all a summary owes you.
23
+ *
24
+ * WHY THE PANEL STAYS MOUNTED WHILE COLLAPSED. The summary row shows a live
25
+ * score, and the only honest way to have one is to let the panel keep computing
26
+ * it — so the collapsed state hides the detail with `hidden` rather than
27
+ * unmounting the panel, and the panel reports each result back through
28
+ * `onAuditChange`. Unmounting instead would either freeze the summary at its
29
+ * last value or force this component to run a second, competing audit that could
30
+ * disagree with the one behind the toggle.
31
+ *
32
+ * WHY THE DOCUMENT IS REBUILT ON A DELAY. `blocks` is a new array on every
33
+ * keystroke in any block editor, and rebuilding the page document means walking
34
+ * every block and parsing every rich-text body. Doing that during the render of
35
+ * each keystroke would be pure waste, since only the last one is ever graded. The
36
+ * rebuild is therefore deferred to the same trailing pause the panel already
37
+ * waits for, which does mean the page score settles roughly two debounce
38
+ * intervals after typing stops — a deliberate trade of latency nobody is watching
39
+ * for work nobody needs.
40
+ *
41
+ * Rendering nothing without a provider is intentional, not defensive padding:
42
+ * the same edit-screen components are reachable from the product editor, which
43
+ * has no page-level audit, and `usePageSeo()` returning `null` is the supported
44
+ * way that surface says so.
45
+ */
46
+
47
+ import * as React from 'react';
48
+
49
+ import { Button } from '@nextblock-cms/ui';
50
+ import { cn } from '@nextblock-cms/utils';
51
+ import type { SeoAuditResult, SeoScoreBand } from '@nextblock-cms/utils/seo';
52
+ import { ChevronDown, ChevronUp, Gauge } from 'lucide-react';
53
+
54
+ import { useCortexAiActive } from '../../app/cms/components/CortexAiActiveContext';
55
+ import { usePageSeo } from '../../lib/seo/page-audit-context';
56
+ import { buildPageSeoDocument } from '../../lib/seo/page-document';
57
+ import { SEO_LIVE_AUDIT_DEBOUNCE_MS, SeoAuditPanel } from './SeoAuditPanel';
58
+
59
+ /**
60
+ * Where the open/closed choice is remembered.
61
+ *
62
+ * Per-browser like the block editor's own panel toggle, and for the same reason:
63
+ * it is a workspace preference in the family of a collapsed sidebar, not content,
64
+ * and persisting it server-side would mean a write on every click.
65
+ */
66
+ const PAGE_SEO_PANEL_STORAGE_KEY = 'nextblock_page_seo_audit_open';
67
+
68
+ /**
69
+ * Badge colours for the collapsed summary, following the CMS convention: emerald
70
+ * for fine, amber for a caution, red for a failure. `excellent` and `good` share
71
+ * emerald because the distinction between them is carried by the number itself.
72
+ */
73
+ const BAND_BADGE_STYLE: Record<SeoScoreBand, string> = {
74
+ excellent: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400',
75
+ fair: 'bg-amber-500/15 text-amber-700 dark:text-amber-400',
76
+ good: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400',
77
+ poor: 'bg-red-500/15 text-red-700 dark:text-red-400',
78
+ };
79
+
80
+ export interface PageSeoAuditSectionProps {
81
+ className?: string;
82
+ }
83
+
84
+ export function PageSeoAuditSection({ className }: PageSeoAuditSectionProps) {
85
+ const pageSeo = usePageSeo();
86
+ const detailsId = React.useId();
87
+ /**
88
+ * Forwarded so the panel can explain where the one-click fixes went.
89
+ *
90
+ * There is no Fix button at page scope — a page has no single editor to write
91
+ * a rewrite into — and the panel only says so when Cortex AI is actually
92
+ * activated. Without this flag an activated install would show nothing at all
93
+ * where a Fix button sits at block scope, which reads as a bug rather than as
94
+ * a deliberate absence.
95
+ */
96
+ const isCortexAiActive = useCortexAiActive();
97
+
98
+ // Read through optional chaining so every hook below runs unconditionally: the
99
+ // "no provider" case is handled by returning null *after* the hooks, never by
100
+ // skipping them, which would break the rules of hooks the moment a provider
101
+ // appeared or disappeared above this component.
102
+ const liveBlocks = pageSeo?.snapshot.blocks;
103
+
104
+ const [audit, setAudit] = React.useState<SeoAuditResult | null>(null);
105
+ const [isOpen, setIsOpen] = React.useState(false);
106
+ const [settledBlocks, setSettledBlocks] = React.useState<unknown[]>(() => liveBlocks ?? []);
107
+
108
+ /**
109
+ * Adopt the remembered preference once, after mount.
110
+ *
111
+ * Every access is wrapped because `localStorage` does not merely come back
112
+ * empty in a private window or with site data blocked — the property access
113
+ * itself throws, and an uncaught throw here would take the whole edit screen
114
+ * down over a panel toggle. A first visit stays collapsed, which is the state
115
+ * that keeps the block editor where the author expects to find it.
116
+ */
117
+ React.useEffect(() => {
118
+ try {
119
+ setIsOpen(window.localStorage.getItem(PAGE_SEO_PANEL_STORAGE_KEY) === 'true');
120
+ } catch {
121
+ setIsOpen(false);
122
+ }
123
+ }, []);
124
+
125
+ const toggleOpen = React.useCallback(() => {
126
+ setIsOpen((previous) => {
127
+ const next = !previous;
128
+ try {
129
+ window.localStorage.setItem(PAGE_SEO_PANEL_STORAGE_KEY, String(next));
130
+ } catch {
131
+ // The preference is a convenience; losing it is not worth an error.
132
+ }
133
+ return next;
134
+ });
135
+ }, []);
136
+
137
+ // The deferred rebuild described in the module docblock. `liveBlocks` changes
138
+ // identity on every keystroke anywhere in the page, so each change restarts
139
+ // this timer and only the array that survives a pause is ever walked.
140
+ React.useEffect(() => {
141
+ if (liveBlocks === undefined) {
142
+ return;
143
+ }
144
+
145
+ const timeoutId = window.setTimeout(() => {
146
+ setSettledBlocks(liveBlocks);
147
+ }, SEO_LIVE_AUDIT_DEBOUNCE_MS);
148
+
149
+ return () => window.clearTimeout(timeoutId);
150
+ }, [liveBlocks]);
151
+
152
+ // Memoised because the document's identity is one of the panel's debounce
153
+ // dependencies: a fresh object on every render would reset the panel's timer
154
+ // forever and no score would ever appear.
155
+ const pageDocument = React.useMemo(() => buildPageSeoDocument(settledBlocks), [settledBlocks]);
156
+
157
+ const handleAuditChange = React.useCallback((next: SeoAuditResult | null) => {
158
+ setAudit(next);
159
+ }, []);
160
+
161
+ if (!pageSeo) {
162
+ return null;
163
+ }
164
+
165
+ const issueCount = audit?.issues.length ?? 0;
166
+ const errorCount = audit?.issues.filter((issue) => issue.severity === 'error').length ?? 0;
167
+ const summary = !audit
168
+ ? 'Reading every block on this page…'
169
+ : issueCount === 0
170
+ ? 'No issues found across this page.'
171
+ : `${issueCount} ${issueCount === 1 ? 'finding' : 'findings'} across this page` +
172
+ (errorCount > 0 ? `, ${errorCount} of them ${errorCount === 1 ? 'an error' : 'errors'}.` : '.');
173
+
174
+ return (
175
+ <section className={cn('rounded-lg border bg-background', className)}>
176
+ <div className="flex flex-wrap items-center justify-between gap-3 p-3">
177
+ <div className="flex min-w-0 items-center gap-3">
178
+ <Gauge aria-hidden="true" className="h-5 w-5 shrink-0 text-muted-foreground" />
179
+ <div className="min-w-0">
180
+ <h2 className="text-sm font-semibold mt-0">Page SEO analysis</h2>
181
+ <p className="text-xs text-muted-foreground">{summary}</p>
182
+ </div>
183
+ </div>
184
+
185
+ <div className="flex shrink-0 items-center gap-3">
186
+ {audit && (
187
+ /* The band's colour repeats what the number already says, so it is
188
+ hidden from assistive technology and spelled out in the button's
189
+ accessible name instead of being announced twice. */
190
+ <span
191
+ aria-hidden="true"
192
+ className={cn(
193
+ 'rounded-full px-2.5 py-1 text-xs font-semibold tabular-nums',
194
+ BAND_BADGE_STYLE[audit.scoreBand]
195
+ )}
196
+ >
197
+ {audit.score}/100
198
+ </span>
199
+ )}
200
+ <Button
201
+ aria-controls={detailsId}
202
+ aria-expanded={isOpen}
203
+ className="h-8 text-xs"
204
+ onClick={toggleOpen}
205
+ size="sm"
206
+ type="button"
207
+ variant="outline"
208
+ >
209
+ {isOpen ? (
210
+ <ChevronUp aria-hidden="true" className="mr-1.5 h-4 w-4" />
211
+ ) : (
212
+ <ChevronDown aria-hidden="true" className="mr-1.5 h-4 w-4" />
213
+ )}
214
+ {isOpen ? 'Hide SEO analysis' : 'Show SEO analysis'}
215
+ <span className="sr-only">
216
+ {audit ? ` — currently scoring ${audit.score} out of 100.` : ''}
217
+ </span>
218
+ </Button>
219
+ </div>
220
+ </div>
221
+
222
+ {/*
223
+ `hidden` rather than a conditional render: the panel has to stay mounted
224
+ for the summary above to keep updating. Tailwind's `hidden` is
225
+ `display: none`, so a collapsed section costs no layout and the block
226
+ editor below it does not move.
227
+ */}
228
+ <div className={cn('border-t', !isOpen && 'hidden')} id={detailsId}>
229
+ <SeoAuditPanel
230
+ document={pageDocument}
231
+ isCortexAiActive={isCortexAiActive}
232
+ keyword={pageSeo.keyword}
233
+ metaDescription={pageSeo.snapshot.metaDescription}
234
+ metaTitle={pageSeo.snapshot.metaTitle}
235
+ onAuditChange={handleAuditChange}
236
+ onKeywordChange={pageSeo.setKeyword}
237
+ scope="page"
238
+ />
239
+ </div>
240
+ </section>
241
+ );
242
+ }
243
+
244
+ export default PageSeoAuditSection;