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,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
+ }