create-nextblock 0.16.1 → 0.16.3

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 (264) 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/route.ts +4 -4
  30. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +4542 -2904
  31. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  32. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  33. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  34. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  35. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  37. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  38. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  39. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  40. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  41. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
  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 +406 -406
  46. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  47. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  48. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  49. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  50. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  51. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  52. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +40 -0
  53. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
  54. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  55. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  56. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  58. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  59. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  60. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  61. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  63. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  64. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  65. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  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/components/PageForm.tsx +649 -649
  74. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +624 -618
  77. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
  78. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  79. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  80. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  81. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  82. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  83. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  84. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  85. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  86. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  90. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  91. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  92. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  93. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  96. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  98. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  99. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  100. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  101. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  102. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  103. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  104. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  105. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  106. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  107. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  108. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  109. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  110. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  111. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  112. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  113. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  114. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  115. package/templates/nextblock-template/app/layout.tsx +671 -671
  116. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  117. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  118. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  119. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  120. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  121. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  122. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  123. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  124. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  125. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  126. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  127. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  128. package/templates/nextblock-template/app/providers.tsx +96 -96
  129. package/templates/nextblock-template/app/robots.ts +123 -123
  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 -137
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +251 -244
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  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 -86
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  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 -154
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +157 -140
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +384 -350
  231. package/templates/nextblock-template/lib/seo/page-document.ts +463 -412
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -22
  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 +1 -2
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/scripts/backup.js +115 -115
  256. package/templates/nextblock-template/scripts/restore.js +385 -385
  257. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  258. package/templates/nextblock-template/tailwind.config.js +25 -25
  259. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  260. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  261. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  262. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  263. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  264. package/tsconfig.base.json +3 -3
@@ -1,204 +1,204 @@
1
- /**
2
- * Turns an SEO finding into the two strings the Cortex AI rewrite endpoint takes.
3
- *
4
- * The audit panel that calls this is a React component with a network call, a
5
- * debounce and a Tiptap instance in it, and this repository has no DOM test
6
- * environment (`@testing-library/react` is deliberately not a dependency), so
7
- * the panel itself cannot be exercised by a test. Everything in it that can get
8
- * the *content* of a request wrong therefore lives here instead, as ordinary
9
- * functions over plain data: which issues may offer a Fix button at all, what
10
- * instruction the model is given, and what it is told about the surrounding
11
- * document. What is left in the component is wiring — state, fetch, insertion —
12
- * which fails loudly in the browser rather than silently producing a bad prompt.
13
- *
14
- * The hard constraint this module exists to honour is the request schema on
15
- * `POST /api/ai/generate-blocks`, which is a `z.strictObject`:
16
- *
17
- * { context?: string (max 2000); prompt: string (min 3, max 4000) }
18
- *
19
- * Strict means an unrecognised key is a 400 with no useful message, and the
20
- * length caps are enforced server-side, so both strings are clamped here rather
21
- * than hoped about at the call site. Every limit below mirrors that schema; if
22
- * the route's schema ever changes, these constants change with it.
23
- */
24
-
25
- import type { SeoIssue } from '@nextblock-cms/utils/seo';
26
-
27
- /** Mirrors `context: z.string().max(2000)` on the generate-blocks schema. */
28
- export const SEO_FIX_CONTEXT_MAX_LENGTH = 2000;
29
-
30
- /** Mirrors `prompt: z.string().max(4000)` on the generate-blocks schema. */
31
- export const SEO_FIX_PROMPT_MAX_LENGTH = 4000;
32
-
33
- /** Mirrors `prompt: z.string().min(3)` on the generate-blocks schema. */
34
- export const SEO_FIX_PROMPT_MIN_LENGTH = 3;
35
-
36
- /**
37
- * How much of the document body to quote back to the model.
38
- *
39
- * Sized so the quoted body plus the framing lines around it still fit inside
40
- * `SEO_FIX_CONTEXT_MAX_LENGTH` without the final clamp having to bite, because
41
- * a clamp that lands in the *middle* of the assembled context would cut the
42
- * trailing sections off entirely and leave the model reading a truncated
43
- * excerpt as if it were the whole page.
44
- */
45
- export const SEO_FIX_CONTEXT_BODY_BUDGET = 1200;
46
-
47
- /** How much of the author's selection to quote when fixing a selected passage. */
48
- export const SEO_FIX_CONTEXT_SELECTION_BUDGET = 600;
49
-
50
- /**
51
- * Where the rewritten fragment is going to land, which changes what the model
52
- * should produce: a whole document, a replacement for the highlighted passage,
53
- * or a continuation appended after everything that already exists.
54
- *
55
- * The three values match the branches the editor's own prompt bar already uses
56
- * (`buildAiEditorContext` in `libs/editor/src/lib/NotionEditor.tsx`), so a
57
- * one-click fix and a hand-typed prompt are framed to the model identically.
58
- */
59
- export type SeoFixInsertionMode =
60
- | 'append-to-end'
61
- | 'replace-empty-document'
62
- | 'replace-selection';
63
-
64
- export interface SeoFixContextInput {
65
- /** Plain text of the document being audited, used to ground the rewrite. */
66
- documentText: string;
67
- /** Headings in document order, already formatted as e.g. `H2 Ingredients`. */
68
- headingOutline?: string[];
69
- insertionMode: SeoFixInsertionMode;
70
- /** The text the author had highlighted, when the fix targets a selection. */
71
- selectedText?: string | null;
72
- /** Word count from the audit, so the model is told the real number. */
73
- wordCount?: number | null;
74
- }
75
-
76
- export interface SeoFixPromptInput {
77
- issue: SeoIssue;
78
- /** The focus keyphrase, when one is set; blank and `null` are equivalent. */
79
- keyword?: string | null;
80
- /** Word count from the audit, quoted for the findings that are about length. */
81
- wordCount?: number | null;
82
- }
83
-
84
- /**
85
- * Cuts a string to `limit` characters without ever exceeding it.
86
- *
87
- * Deliberately a hard slice rather than a word-boundary trim: the caps are a
88
- * server-side validation boundary, and "nearly 2000" is a rejected request. A
89
- * ragged final word costs the model nothing; a 400 costs the author their click.
90
- */
91
- function clampLength(value: string, limit: number): string {
92
- return value.length <= limit ? value : value.slice(0, limit);
93
- }
94
-
95
- /**
96
- * Whether the "Fix with Cortex AI" button should appear for this finding.
97
- *
98
- * Two conditions, and both matter. `fixable` is the audit engine's own verdict,
99
- * and it says no to more than you might expect: a missing H1 and a missing
100
- * keyphrase are unfixable because choosing them is an editorial decision, and
101
- * missing image alt text is unfixable *here* because describing pixels needs the
102
- * vision-backed generator in the media library rather than a prose rewrite. A
103
- * non-empty `fixPrompt` is then required because it is the entire instruction
104
- * handed to the model — an issue marked fixable without one would send a prompt
105
- * assembled from nothing but the problem statement, and the model would answer
106
- * by restating the problem instead of repairing it.
107
- */
108
- export function canFixIssueWithCortexAi(issue: SeoIssue): boolean {
109
- return issue.fixable && typeof issue.fixPrompt === 'string' && issue.fixPrompt.trim() !== '';
110
- }
111
-
112
- /**
113
- * Builds the `prompt` field: the audit's own repair instruction, plus the
114
- * concrete numbers behind the finding, plus the output contract.
115
- *
116
- * The numbers come from `issue.detail`, which every auditor writes as a sentence
117
- * that already contains them ("This page has 118 words; 300 or more…"), so
118
- * quoting it verbatim keeps the figure on screen and the figure in the request
119
- * from ever drifting apart. Restating the message alongside the instruction is
120
- * not redundancy: `fixPrompt` is phrased as a bare command, and a model told
121
- * only "shorten the longest sentences" has no idea by how much.
122
- *
123
- * The output contract at the end exists because the response is inserted
124
- * straight into a Tiptap document. A model that answered with a markdown code
125
- * fence, a preamble, or a full HTML document would have all of that rendered as
126
- * literal content in the page the author is editing.
127
- */
128
- export function buildSeoFixPrompt(input: SeoFixPromptInput): string {
129
- const { issue, keyword, wordCount } = input;
130
- const instruction = (issue.fixPrompt ?? '').trim();
131
- const trimmedKeyword = (keyword ?? '').trim();
132
-
133
- const lines: Array<string | null> = [
134
- instruction,
135
- '',
136
- `The SEO audit reported: ${issue.message}${issue.detail ? ` ${issue.detail}` : ''}`,
137
- trimmedKeyword === '' ? null : `The focus keyphrase for this page is "${trimmedKeyword}".`,
138
- typeof wordCount === 'number' && Number.isFinite(wordCount)
139
- ? `The document currently runs to ${wordCount} words.`
140
- : null,
141
- '',
142
- 'Return only the corrected content as an HTML fragment using semantic tags such as <h2>, <p>, <ul> and <li>. Do not wrap it in a code fence, do not add commentary, and do not emit <html>, <head> or <body>.',
143
- ];
144
-
145
- const prompt = lines
146
- .filter((line): line is string => line !== null)
147
- .join('\n')
148
- .trim();
149
-
150
- return clampLength(prompt, SEO_FIX_PROMPT_MAX_LENGTH);
151
- }
152
-
153
- /**
154
- * Builds the `context` field: what the model needs to know about the document
155
- * it is editing, framed by where its answer is going to be inserted.
156
- *
157
- * This mirrors `buildAiEditorContext` in the editor's prompt bar on purpose.
158
- * Both feed the same endpoint, and a fix that described the document
159
- * differently from a hand-typed prompt would get systematically different
160
- * output from the same model for no reason an author could see.
161
- *
162
- * The body excerpt is taken from the *end* of the document when appending and
163
- * from the *start* otherwise, because those are the parts adjacent to where the
164
- * answer lands: a continuation needs to know what it is continuing, while a
165
- * rewrite anchored at the top is best grounded by the opening.
166
- */
167
- export function buildSeoFixContext(input: SeoFixContextInput): string {
168
- const { documentText, headingOutline, insertionMode, selectedText, wordCount } = input;
169
-
170
- // An empty document has no context worth sending, and saying so is more
171
- // useful to the model than an excerpt of nothing followed by an outline of
172
- // nothing. This matches the editor's own early return for the same case.
173
- if (insertionMode === 'replace-empty-document') {
174
- return 'Insertion target: empty editor. Create the initial content for this document.';
175
- }
176
-
177
- const body = (documentText ?? '').trim();
178
- const selection = (selectedText ?? '').trim();
179
- const excerpt =
180
- insertionMode === 'append-to-end'
181
- ? body.slice(-SEO_FIX_CONTEXT_BODY_BUDGET)
182
- : body.slice(0, SEO_FIX_CONTEXT_BODY_BUDGET);
183
-
184
- const parts: Array<string | null> = [
185
- `Insertion target: ${insertionMode}.`,
186
- insertionMode === 'append-to-end'
187
- ? 'Continue after the existing content. Do not repeat existing headings, paragraphs, or lists unless the fix explicitly calls for a rewrite.'
188
- : 'Replace only the selected content. Keep the surrounding editor content in mind for continuity.',
189
- typeof wordCount === 'number' && Number.isFinite(wordCount)
190
- ? `The document has ${wordCount} words.`
191
- : null,
192
- headingOutline && headingOutline.length > 0
193
- ? `Heading outline:\n${headingOutline.join('\n')}`
194
- : null,
195
- selection === ''
196
- ? null
197
- : `Selected text:\n${selection.slice(0, SEO_FIX_CONTEXT_SELECTION_BUDGET)}`,
198
- excerpt === '' ? null : `Existing editor text:\n${excerpt}`,
199
- ];
200
-
201
- const context = parts.filter((part): part is string => part !== null).join('\n\n');
202
-
203
- return clampLength(context, SEO_FIX_CONTEXT_MAX_LENGTH);
204
- }
1
+ /**
2
+ * Turns an SEO finding into the two strings the Cortex AI rewrite endpoint takes.
3
+ *
4
+ * The audit panel that calls this is a React component with a network call, a
5
+ * debounce and a Tiptap instance in it, and this repository has no DOM test
6
+ * environment (`@testing-library/react` is deliberately not a dependency), so
7
+ * the panel itself cannot be exercised by a test. Everything in it that can get
8
+ * the *content* of a request wrong therefore lives here instead, as ordinary
9
+ * functions over plain data: which issues may offer a Fix button at all, what
10
+ * instruction the model is given, and what it is told about the surrounding
11
+ * document. What is left in the component is wiring — state, fetch, insertion —
12
+ * which fails loudly in the browser rather than silently producing a bad prompt.
13
+ *
14
+ * The hard constraint this module exists to honour is the request schema on
15
+ * `POST /api/ai/generate-blocks`, which is a `z.strictObject`:
16
+ *
17
+ * { context?: string (max 2000); prompt: string (min 3, max 4000) }
18
+ *
19
+ * Strict means an unrecognised key is a 400 with no useful message, and the
20
+ * length caps are enforced server-side, so both strings are clamped here rather
21
+ * than hoped about at the call site. Every limit below mirrors that schema; if
22
+ * the route's schema ever changes, these constants change with it.
23
+ */
24
+
25
+ import type { SeoIssue } from '@nextblock-cms/utils/seo';
26
+
27
+ /** Mirrors `context: z.string().max(2000)` on the generate-blocks schema. */
28
+ export const SEO_FIX_CONTEXT_MAX_LENGTH = 2000;
29
+
30
+ /** Mirrors `prompt: z.string().max(4000)` on the generate-blocks schema. */
31
+ export const SEO_FIX_PROMPT_MAX_LENGTH = 4000;
32
+
33
+ /** Mirrors `prompt: z.string().min(3)` on the generate-blocks schema. */
34
+ export const SEO_FIX_PROMPT_MIN_LENGTH = 3;
35
+
36
+ /**
37
+ * How much of the document body to quote back to the model.
38
+ *
39
+ * Sized so the quoted body plus the framing lines around it still fit inside
40
+ * `SEO_FIX_CONTEXT_MAX_LENGTH` without the final clamp having to bite, because
41
+ * a clamp that lands in the *middle* of the assembled context would cut the
42
+ * trailing sections off entirely and leave the model reading a truncated
43
+ * excerpt as if it were the whole page.
44
+ */
45
+ export const SEO_FIX_CONTEXT_BODY_BUDGET = 1200;
46
+
47
+ /** How much of the author's selection to quote when fixing a selected passage. */
48
+ export const SEO_FIX_CONTEXT_SELECTION_BUDGET = 600;
49
+
50
+ /**
51
+ * Where the rewritten fragment is going to land, which changes what the model
52
+ * should produce: a whole document, a replacement for the highlighted passage,
53
+ * or a continuation appended after everything that already exists.
54
+ *
55
+ * The three values match the branches the editor's own prompt bar already uses
56
+ * (`buildAiEditorContext` in `libs/editor/src/lib/NotionEditor.tsx`), so a
57
+ * one-click fix and a hand-typed prompt are framed to the model identically.
58
+ */
59
+ export type SeoFixInsertionMode =
60
+ | 'append-to-end'
61
+ | 'replace-empty-document'
62
+ | 'replace-selection';
63
+
64
+ export interface SeoFixContextInput {
65
+ /** Plain text of the document being audited, used to ground the rewrite. */
66
+ documentText: string;
67
+ /** Headings in document order, already formatted as e.g. `H2 Ingredients`. */
68
+ headingOutline?: string[];
69
+ insertionMode: SeoFixInsertionMode;
70
+ /** The text the author had highlighted, when the fix targets a selection. */
71
+ selectedText?: string | null;
72
+ /** Word count from the audit, so the model is told the real number. */
73
+ wordCount?: number | null;
74
+ }
75
+
76
+ export interface SeoFixPromptInput {
77
+ issue: SeoIssue;
78
+ /** The focus keyphrase, when one is set; blank and `null` are equivalent. */
79
+ keyword?: string | null;
80
+ /** Word count from the audit, quoted for the findings that are about length. */
81
+ wordCount?: number | null;
82
+ }
83
+
84
+ /**
85
+ * Cuts a string to `limit` characters without ever exceeding it.
86
+ *
87
+ * Deliberately a hard slice rather than a word-boundary trim: the caps are a
88
+ * server-side validation boundary, and "nearly 2000" is a rejected request. A
89
+ * ragged final word costs the model nothing; a 400 costs the author their click.
90
+ */
91
+ function clampLength(value: string, limit: number): string {
92
+ return value.length <= limit ? value : value.slice(0, limit);
93
+ }
94
+
95
+ /**
96
+ * Whether the "Fix with Cortex AI" button should appear for this finding.
97
+ *
98
+ * Two conditions, and both matter. `fixable` is the audit engine's own verdict,
99
+ * and it says no to more than you might expect: a missing H1 and a missing
100
+ * keyphrase are unfixable because choosing them is an editorial decision, and
101
+ * missing image alt text is unfixable *here* because describing pixels needs the
102
+ * vision-backed generator in the media library rather than a prose rewrite. A
103
+ * non-empty `fixPrompt` is then required because it is the entire instruction
104
+ * handed to the model — an issue marked fixable without one would send a prompt
105
+ * assembled from nothing but the problem statement, and the model would answer
106
+ * by restating the problem instead of repairing it.
107
+ */
108
+ export function canFixIssueWithCortexAi(issue: SeoIssue): boolean {
109
+ return issue.fixable && typeof issue.fixPrompt === 'string' && issue.fixPrompt.trim() !== '';
110
+ }
111
+
112
+ /**
113
+ * Builds the `prompt` field: the audit's own repair instruction, plus the
114
+ * concrete numbers behind the finding, plus the output contract.
115
+ *
116
+ * The numbers come from `issue.detail`, which every auditor writes as a sentence
117
+ * that already contains them ("This page has 118 words; 300 or more…"), so
118
+ * quoting it verbatim keeps the figure on screen and the figure in the request
119
+ * from ever drifting apart. Restating the message alongside the instruction is
120
+ * not redundancy: `fixPrompt` is phrased as a bare command, and a model told
121
+ * only "shorten the longest sentences" has no idea by how much.
122
+ *
123
+ * The output contract at the end exists because the response is inserted
124
+ * straight into a Tiptap document. A model that answered with a markdown code
125
+ * fence, a preamble, or a full HTML document would have all of that rendered as
126
+ * literal content in the page the author is editing.
127
+ */
128
+ export function buildSeoFixPrompt(input: SeoFixPromptInput): string {
129
+ const { issue, keyword, wordCount } = input;
130
+ const instruction = (issue.fixPrompt ?? '').trim();
131
+ const trimmedKeyword = (keyword ?? '').trim();
132
+
133
+ const lines: Array<string | null> = [
134
+ instruction,
135
+ '',
136
+ `The SEO audit reported: ${issue.message}${issue.detail ? ` ${issue.detail}` : ''}`,
137
+ trimmedKeyword === '' ? null : `The focus keyphrase for this page is "${trimmedKeyword}".`,
138
+ typeof wordCount === 'number' && Number.isFinite(wordCount)
139
+ ? `The document currently runs to ${wordCount} words.`
140
+ : null,
141
+ '',
142
+ 'Return only the corrected content as an HTML fragment using semantic tags such as <h2>, <p>, <ul> and <li>. Do not wrap it in a code fence, do not add commentary, and do not emit <html>, <head> or <body>.',
143
+ ];
144
+
145
+ const prompt = lines
146
+ .filter((line): line is string => line !== null)
147
+ .join('\n')
148
+ .trim();
149
+
150
+ return clampLength(prompt, SEO_FIX_PROMPT_MAX_LENGTH);
151
+ }
152
+
153
+ /**
154
+ * Builds the `context` field: what the model needs to know about the document
155
+ * it is editing, framed by where its answer is going to be inserted.
156
+ *
157
+ * This mirrors `buildAiEditorContext` in the editor's prompt bar on purpose.
158
+ * Both feed the same endpoint, and a fix that described the document
159
+ * differently from a hand-typed prompt would get systematically different
160
+ * output from the same model for no reason an author could see.
161
+ *
162
+ * The body excerpt is taken from the *end* of the document when appending and
163
+ * from the *start* otherwise, because those are the parts adjacent to where the
164
+ * answer lands: a continuation needs to know what it is continuing, while a
165
+ * rewrite anchored at the top is best grounded by the opening.
166
+ */
167
+ export function buildSeoFixContext(input: SeoFixContextInput): string {
168
+ const { documentText, headingOutline, insertionMode, selectedText, wordCount } = input;
169
+
170
+ // An empty document has no context worth sending, and saying so is more
171
+ // useful to the model than an excerpt of nothing followed by an outline of
172
+ // nothing. This matches the editor's own early return for the same case.
173
+ if (insertionMode === 'replace-empty-document') {
174
+ return 'Insertion target: empty editor. Create the initial content for this document.';
175
+ }
176
+
177
+ const body = (documentText ?? '').trim();
178
+ const selection = (selectedText ?? '').trim();
179
+ const excerpt =
180
+ insertionMode === 'append-to-end'
181
+ ? body.slice(-SEO_FIX_CONTEXT_BODY_BUDGET)
182
+ : body.slice(0, SEO_FIX_CONTEXT_BODY_BUDGET);
183
+
184
+ const parts: Array<string | null> = [
185
+ `Insertion target: ${insertionMode}.`,
186
+ insertionMode === 'append-to-end'
187
+ ? 'Continue after the existing content. Do not repeat existing headings, paragraphs, or lists unless the fix explicitly calls for a rewrite.'
188
+ : 'Replace only the selected content. Keep the surrounding editor content in mind for continuity.',
189
+ typeof wordCount === 'number' && Number.isFinite(wordCount)
190
+ ? `The document has ${wordCount} words.`
191
+ : null,
192
+ headingOutline && headingOutline.length > 0
193
+ ? `Heading outline:\n${headingOutline.join('\n')}`
194
+ : null,
195
+ selection === ''
196
+ ? null
197
+ : `Selected text:\n${selection.slice(0, SEO_FIX_CONTEXT_SELECTION_BUDGET)}`,
198
+ excerpt === '' ? null : `Existing editor text:\n${excerpt}`,
199
+ ];
200
+
201
+ const context = parts.filter((part): part is string => part !== null).join('\n\n');
202
+
203
+ return clampLength(context, SEO_FIX_CONTEXT_MAX_LENGTH);
204
+ }