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