@prenta/core 0.118.0

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 (1408) hide show
  1. package/CHANGELOG.md +2974 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/a11y/index.d.ts +25 -0
  5. package/dist/a11y/index.d.ts.map +1 -0
  6. package/dist/a11y/index.js +91 -0
  7. package/dist/a11y/index.js.map +1 -0
  8. package/dist/actions.d.ts +130 -0
  9. package/dist/actions.d.ts.map +1 -0
  10. package/dist/actions.js +1324 -0
  11. package/dist/actions.js.map +1 -0
  12. package/dist/ai/brand-voice-alignment.d.ts +17 -0
  13. package/dist/ai/brand-voice-alignment.d.ts.map +1 -0
  14. package/dist/ai/brand-voice-alignment.js +40 -0
  15. package/dist/ai/brand-voice-alignment.js.map +1 -0
  16. package/dist/ai/brand-voice-analyze.d.ts +71 -0
  17. package/dist/ai/brand-voice-analyze.d.ts.map +1 -0
  18. package/dist/ai/brand-voice-analyze.js +151 -0
  19. package/dist/ai/brand-voice-analyze.js.map +1 -0
  20. package/dist/ai/brand-voice-document.d.ts +56 -0
  21. package/dist/ai/brand-voice-document.d.ts.map +1 -0
  22. package/dist/ai/brand-voice-document.js +335 -0
  23. package/dist/ai/brand-voice-document.js.map +1 -0
  24. package/dist/ai/brand-voice-fix.d.ts +26 -0
  25. package/dist/ai/brand-voice-fix.d.ts.map +1 -0
  26. package/dist/ai/brand-voice-fix.js +97 -0
  27. package/dist/ai/brand-voice-fix.js.map +1 -0
  28. package/dist/ai/brand-voice-save.d.ts +11 -0
  29. package/dist/ai/brand-voice-save.d.ts.map +1 -0
  30. package/dist/ai/brand-voice-save.js +51 -0
  31. package/dist/ai/brand-voice-save.js.map +1 -0
  32. package/dist/ai/brand-voice-sources.d.ts +16 -0
  33. package/dist/ai/brand-voice-sources.d.ts.map +1 -0
  34. package/dist/ai/brand-voice-sources.js +106 -0
  35. package/dist/ai/brand-voice-sources.js.map +1 -0
  36. package/dist/ai/brand-voice-structured.d.ts +87 -0
  37. package/dist/ai/brand-voice-structured.d.ts.map +1 -0
  38. package/dist/ai/brand-voice-structured.js +438 -0
  39. package/dist/ai/brand-voice-structured.js.map +1 -0
  40. package/dist/ai/brand-voice.d.ts +40 -0
  41. package/dist/ai/brand-voice.d.ts.map +1 -0
  42. package/dist/ai/brand-voice.js +103 -0
  43. package/dist/ai/brand-voice.js.map +1 -0
  44. package/dist/ai/catalog.d.ts +35 -0
  45. package/dist/ai/catalog.d.ts.map +1 -0
  46. package/dist/ai/catalog.js +370 -0
  47. package/dist/ai/catalog.js.map +1 -0
  48. package/dist/ai/config-store.d.ts +55 -0
  49. package/dist/ai/config-store.d.ts.map +1 -0
  50. package/dist/ai/config-store.js +177 -0
  51. package/dist/ai/config-store.js.map +1 -0
  52. package/dist/ai/content-writing-generate.d.ts +19 -0
  53. package/dist/ai/content-writing-generate.d.ts.map +1 -0
  54. package/dist/ai/content-writing-generate.js +64 -0
  55. package/dist/ai/content-writing-generate.js.map +1 -0
  56. package/dist/ai/feature-defaults.d.ts +20 -0
  57. package/dist/ai/feature-defaults.d.ts.map +1 -0
  58. package/dist/ai/feature-defaults.js +104 -0
  59. package/dist/ai/feature-defaults.js.map +1 -0
  60. package/dist/ai/index.d.ts +24 -0
  61. package/dist/ai/index.d.ts.map +1 -0
  62. package/dist/ai/index.js +24 -0
  63. package/dist/ai/index.js.map +1 -0
  64. package/dist/ai/models.d.ts +37 -0
  65. package/dist/ai/models.d.ts.map +1 -0
  66. package/dist/ai/models.js +126 -0
  67. package/dist/ai/models.js.map +1 -0
  68. package/dist/ai/providers/anthropic.d.ts +10 -0
  69. package/dist/ai/providers/anthropic.d.ts.map +1 -0
  70. package/dist/ai/providers/anthropic.js +63 -0
  71. package/dist/ai/providers/anthropic.js.map +1 -0
  72. package/dist/ai/providers/custom.d.ts +12 -0
  73. package/dist/ai/providers/custom.d.ts.map +1 -0
  74. package/dist/ai/providers/custom.js +76 -0
  75. package/dist/ai/providers/custom.js.map +1 -0
  76. package/dist/ai/providers/http.d.ts +66 -0
  77. package/dist/ai/providers/http.d.ts.map +1 -0
  78. package/dist/ai/providers/http.js +185 -0
  79. package/dist/ai/providers/http.js.map +1 -0
  80. package/dist/ai/providers/index.d.ts +18 -0
  81. package/dist/ai/providers/index.d.ts.map +1 -0
  82. package/dist/ai/providers/index.js +28 -0
  83. package/dist/ai/providers/index.js.map +1 -0
  84. package/dist/ai/providers/openai.d.ts +11 -0
  85. package/dist/ai/providers/openai.d.ts.map +1 -0
  86. package/dist/ai/providers/openai.js +44 -0
  87. package/dist/ai/providers/openai.js.map +1 -0
  88. package/dist/ai/providers/xai.d.ts +10 -0
  89. package/dist/ai/providers/xai.d.ts.map +1 -0
  90. package/dist/ai/providers/xai.js +38 -0
  91. package/dist/ai/providers/xai.js.map +1 -0
  92. package/dist/ai/resolve.d.ts +41 -0
  93. package/dist/ai/resolve.d.ts.map +1 -0
  94. package/dist/ai/resolve.js +143 -0
  95. package/dist/ai/resolve.js.map +1 -0
  96. package/dist/ai/runtime.d.ts +88 -0
  97. package/dist/ai/runtime.d.ts.map +1 -0
  98. package/dist/ai/runtime.js +192 -0
  99. package/dist/ai/runtime.js.map +1 -0
  100. package/dist/ai/sampling.d.ts +17 -0
  101. package/dist/ai/sampling.d.ts.map +1 -0
  102. package/dist/ai/sampling.js +51 -0
  103. package/dist/ai/sampling.js.map +1 -0
  104. package/dist/ai/types.d.ts +193 -0
  105. package/dist/ai/types.d.ts.map +1 -0
  106. package/dist/ai/types.js +11 -0
  107. package/dist/ai/types.js.map +1 -0
  108. package/dist/ai/usage.d.ts +18 -0
  109. package/dist/ai/usage.d.ts.map +1 -0
  110. package/dist/ai/usage.js +70 -0
  111. package/dist/ai/usage.js.map +1 -0
  112. package/dist/api/collection-introspection.d.ts +46 -0
  113. package/dist/api/collection-introspection.d.ts.map +1 -0
  114. package/dist/api/collection-introspection.js +82 -0
  115. package/dist/api/collection-introspection.js.map +1 -0
  116. package/dist/api/guarded-db.d.ts +14 -0
  117. package/dist/api/guarded-db.d.ts.map +1 -0
  118. package/dist/api/guarded-db.js +62 -0
  119. package/dist/api/guarded-db.js.map +1 -0
  120. package/dist/api/handler-factory.d.ts +7 -0
  121. package/dist/api/handler-factory.d.ts.map +1 -0
  122. package/dist/api/handler-factory.js +380 -0
  123. package/dist/api/handler-factory.js.map +1 -0
  124. package/dist/api/handlers.d.ts +8 -0
  125. package/dist/api/handlers.d.ts.map +1 -0
  126. package/dist/api/handlers.js +62 -0
  127. package/dist/api/handlers.js.map +1 -0
  128. package/dist/api/index.d.ts +8 -0
  129. package/dist/api/index.d.ts.map +1 -0
  130. package/dist/api/index.js +8 -0
  131. package/dist/api/index.js.map +1 -0
  132. package/dist/api/openapi.d.ts +3 -0
  133. package/dist/api/openapi.d.ts.map +1 -0
  134. package/dist/api/openapi.js +539 -0
  135. package/dist/api/openapi.js.map +1 -0
  136. package/dist/api/route-helpers.d.ts +369 -0
  137. package/dist/api/route-helpers.d.ts.map +1 -0
  138. package/dist/api/route-helpers.js +1574 -0
  139. package/dist/api/route-helpers.js.map +1 -0
  140. package/dist/api/router.d.ts +21 -0
  141. package/dist/api/router.d.ts.map +1 -0
  142. package/dist/api/router.js +75 -0
  143. package/dist/api/router.js.map +1 -0
  144. package/dist/api/routes/ai-seo.d.ts +3 -0
  145. package/dist/api/routes/ai-seo.d.ts.map +1 -0
  146. package/dist/api/routes/ai-seo.js +482 -0
  147. package/dist/api/routes/ai-seo.js.map +1 -0
  148. package/dist/api/routes/ai-settings.d.ts +3 -0
  149. package/dist/api/routes/ai-settings.d.ts.map +1 -0
  150. package/dist/api/routes/ai-settings.js +1078 -0
  151. package/dist/api/routes/ai-settings.js.map +1 -0
  152. package/dist/api/routes/auth.d.ts +3 -0
  153. package/dist/api/routes/auth.d.ts.map +1 -0
  154. package/dist/api/routes/auth.js +877 -0
  155. package/dist/api/routes/auth.js.map +1 -0
  156. package/dist/api/routes/collab.d.ts +3 -0
  157. package/dist/api/routes/collab.d.ts.map +1 -0
  158. package/dist/api/routes/collab.js +426 -0
  159. package/dist/api/routes/collab.js.map +1 -0
  160. package/dist/api/routes/dashboard.d.ts +7 -0
  161. package/dist/api/routes/dashboard.d.ts.map +1 -0
  162. package/dist/api/routes/dashboard.js +540 -0
  163. package/dist/api/routes/dashboard.js.map +1 -0
  164. package/dist/api/routes/documents.d.ts +11 -0
  165. package/dist/api/routes/documents.d.ts.map +1 -0
  166. package/dist/api/routes/documents.js +340 -0
  167. package/dist/api/routes/documents.js.map +1 -0
  168. package/dist/api/routes/email-settings.d.ts +3 -0
  169. package/dist/api/routes/email-settings.d.ts.map +1 -0
  170. package/dist/api/routes/email-settings.js +248 -0
  171. package/dist/api/routes/email-settings.js.map +1 -0
  172. package/dist/api/routes/folders.d.ts +3 -0
  173. package/dist/api/routes/folders.d.ts.map +1 -0
  174. package/dist/api/routes/folders.js +195 -0
  175. package/dist/api/routes/folders.js.map +1 -0
  176. package/dist/api/routes/forms.d.ts +3 -0
  177. package/dist/api/routes/forms.d.ts.map +1 -0
  178. package/dist/api/routes/forms.js +776 -0
  179. package/dist/api/routes/forms.js.map +1 -0
  180. package/dist/api/routes/globals.d.ts +3 -0
  181. package/dist/api/routes/globals.d.ts.map +1 -0
  182. package/dist/api/routes/globals.js +157 -0
  183. package/dist/api/routes/globals.js.map +1 -0
  184. package/dist/api/routes/media.d.ts +3 -0
  185. package/dist/api/routes/media.d.ts.map +1 -0
  186. package/dist/api/routes/media.js +807 -0
  187. package/dist/api/routes/media.js.map +1 -0
  188. package/dist/api/routes/meta.d.ts +3 -0
  189. package/dist/api/routes/meta.d.ts.map +1 -0
  190. package/dist/api/routes/meta.js +97 -0
  191. package/dist/api/routes/meta.js.map +1 -0
  192. package/dist/api/routes/navigations.d.ts +7 -0
  193. package/dist/api/routes/navigations.d.ts.map +1 -0
  194. package/dist/api/routes/navigations.js +81 -0
  195. package/dist/api/routes/navigations.js.map +1 -0
  196. package/dist/api/routes/page-templates.d.ts +3 -0
  197. package/dist/api/routes/page-templates.d.ts.map +1 -0
  198. package/dist/api/routes/page-templates.js +265 -0
  199. package/dist/api/routes/page-templates.js.map +1 -0
  200. package/dist/api/routes/presence.d.ts +3 -0
  201. package/dist/api/routes/presence.d.ts.map +1 -0
  202. package/dist/api/routes/presence.js +35 -0
  203. package/dist/api/routes/presence.js.map +1 -0
  204. package/dist/api/routes/preview.d.ts +3 -0
  205. package/dist/api/routes/preview.d.ts.map +1 -0
  206. package/dist/api/routes/preview.js +111 -0
  207. package/dist/api/routes/preview.js.map +1 -0
  208. package/dist/api/routes/redirects.d.ts +3 -0
  209. package/dist/api/routes/redirects.d.ts.map +1 -0
  210. package/dist/api/routes/redirects.js +937 -0
  211. package/dist/api/routes/redirects.js.map +1 -0
  212. package/dist/api/routes/saved-sections.d.ts +3 -0
  213. package/dist/api/routes/saved-sections.d.ts.map +1 -0
  214. package/dist/api/routes/saved-sections.js +1071 -0
  215. package/dist/api/routes/saved-sections.js.map +1 -0
  216. package/dist/api/routes/scheduling.d.ts +3 -0
  217. package/dist/api/routes/scheduling.d.ts.map +1 -0
  218. package/dist/api/routes/scheduling.js +391 -0
  219. package/dist/api/routes/scheduling.js.map +1 -0
  220. package/dist/api/routes/sections.d.ts +3 -0
  221. package/dist/api/routes/sections.d.ts.map +1 -0
  222. package/dist/api/routes/sections.js +512 -0
  223. package/dist/api/routes/sections.js.map +1 -0
  224. package/dist/api/routes/security-center.d.ts +3 -0
  225. package/dist/api/routes/security-center.d.ts.map +1 -0
  226. package/dist/api/routes/security-center.js +71 -0
  227. package/dist/api/routes/security-center.js.map +1 -0
  228. package/dist/api/routes/seo-public.d.ts +14 -0
  229. package/dist/api/routes/seo-public.d.ts.map +1 -0
  230. package/dist/api/routes/seo-public.js +1366 -0
  231. package/dist/api/routes/seo-public.js.map +1 -0
  232. package/dist/api/routes/seo.d.ts +8 -0
  233. package/dist/api/routes/seo.d.ts.map +1 -0
  234. package/dist/api/routes/seo.js +2256 -0
  235. package/dist/api/routes/seo.js.map +1 -0
  236. package/dist/api/routes/system.d.ts +3 -0
  237. package/dist/api/routes/system.d.ts.map +1 -0
  238. package/dist/api/routes/system.js +353 -0
  239. package/dist/api/routes/system.js.map +1 -0
  240. package/dist/api/routes/url-resolution.d.ts +3 -0
  241. package/dist/api/routes/url-resolution.d.ts.map +1 -0
  242. package/dist/api/routes/url-resolution.js +522 -0
  243. package/dist/api/routes/url-resolution.js.map +1 -0
  244. package/dist/api/routes/users.d.ts +3 -0
  245. package/dist/api/routes/users.d.ts.map +1 -0
  246. package/dist/api/routes/users.js +1177 -0
  247. package/dist/api/routes/users.js.map +1 -0
  248. package/dist/api/routes/webhooks.d.ts +3 -0
  249. package/dist/api/routes/webhooks.d.ts.map +1 -0
  250. package/dist/api/routes/webhooks.js +422 -0
  251. package/dist/api/routes/webhooks.js.map +1 -0
  252. package/dist/appearance/color.d.ts +24 -0
  253. package/dist/appearance/color.d.ts.map +1 -0
  254. package/dist/appearance/color.js +90 -0
  255. package/dist/appearance/color.js.map +1 -0
  256. package/dist/appearance/favicon-icon.d.ts +12 -0
  257. package/dist/appearance/favicon-icon.d.ts.map +1 -0
  258. package/dist/appearance/favicon-icon.js +58 -0
  259. package/dist/appearance/favicon-icon.js.map +1 -0
  260. package/dist/appearance/favicon-paths.d.ts +6 -0
  261. package/dist/appearance/favicon-paths.d.ts.map +1 -0
  262. package/dist/appearance/favicon-paths.js +6 -0
  263. package/dist/appearance/favicon-paths.js.map +1 -0
  264. package/dist/appearance/index.d.ts +15 -0
  265. package/dist/appearance/index.d.ts.map +1 -0
  266. package/dist/appearance/index.js +7 -0
  267. package/dist/appearance/index.js.map +1 -0
  268. package/dist/appearance/logo-shape.d.ts +26 -0
  269. package/dist/appearance/logo-shape.d.ts.map +1 -0
  270. package/dist/appearance/logo-shape.js +62 -0
  271. package/dist/appearance/logo-shape.js.map +1 -0
  272. package/dist/appearance/resolve.d.ts +52 -0
  273. package/dist/appearance/resolve.d.ts.map +1 -0
  274. package/dist/appearance/resolve.js +219 -0
  275. package/dist/appearance/resolve.js.map +1 -0
  276. package/dist/appearance/theme-tokens.d.ts +49 -0
  277. package/dist/appearance/theme-tokens.d.ts.map +1 -0
  278. package/dist/appearance/theme-tokens.js +229 -0
  279. package/dist/appearance/theme-tokens.js.map +1 -0
  280. package/dist/appearance/types.d.ts +110 -0
  281. package/dist/appearance/types.d.ts.map +1 -0
  282. package/dist/appearance/types.js +16 -0
  283. package/dist/appearance/types.js.map +1 -0
  284. package/dist/astro-cache.d.ts +80 -0
  285. package/dist/astro-cache.d.ts.map +1 -0
  286. package/dist/astro-cache.js +108 -0
  287. package/dist/astro-cache.js.map +1 -0
  288. package/dist/astro-content.d.ts +81 -0
  289. package/dist/astro-content.d.ts.map +1 -0
  290. package/dist/astro-content.js +151 -0
  291. package/dist/astro-content.js.map +1 -0
  292. package/dist/astro-redirects.d.ts +38 -0
  293. package/dist/astro-redirects.d.ts.map +1 -0
  294. package/dist/astro-redirects.js +45 -0
  295. package/dist/astro-redirects.js.map +1 -0
  296. package/dist/astro-security.d.ts +34 -0
  297. package/dist/astro-security.d.ts.map +1 -0
  298. package/dist/astro-security.js +31 -0
  299. package/dist/astro-security.js.map +1 -0
  300. package/dist/astro-site.d.ts +69 -0
  301. package/dist/astro-site.d.ts.map +1 -0
  302. package/dist/astro-site.js +81 -0
  303. package/dist/astro-site.js.map +1 -0
  304. package/dist/auth/index.d.ts +17 -0
  305. package/dist/auth/index.d.ts.map +1 -0
  306. package/dist/auth/index.js +12 -0
  307. package/dist/auth/index.js.map +1 -0
  308. package/dist/auth/invite-email.d.ts +12 -0
  309. package/dist/auth/invite-email.d.ts.map +1 -0
  310. package/dist/auth/invite-email.js +57 -0
  311. package/dist/auth/invite-email.js.map +1 -0
  312. package/dist/auth/invites.d.ts +72 -0
  313. package/dist/auth/invites.d.ts.map +1 -0
  314. package/dist/auth/invites.js +279 -0
  315. package/dist/auth/invites.js.map +1 -0
  316. package/dist/auth/mfa-pending.d.ts +24 -0
  317. package/dist/auth/mfa-pending.d.ts.map +1 -0
  318. package/dist/auth/mfa-pending.js +38 -0
  319. package/dist/auth/mfa-pending.js.map +1 -0
  320. package/dist/auth/oauth.d.ts +139 -0
  321. package/dist/auth/oauth.d.ts.map +1 -0
  322. package/dist/auth/oauth.js +376 -0
  323. package/dist/auth/oauth.js.map +1 -0
  324. package/dist/auth/password.d.ts +57 -0
  325. package/dist/auth/password.d.ts.map +1 -0
  326. package/dist/auth/password.js +150 -0
  327. package/dist/auth/password.js.map +1 -0
  328. package/dist/auth/providers/github.d.ts +9 -0
  329. package/dist/auth/providers/github.d.ts.map +1 -0
  330. package/dist/auth/providers/github.js +10 -0
  331. package/dist/auth/providers/github.js.map +1 -0
  332. package/dist/auth/providers/google.d.ts +9 -0
  333. package/dist/auth/providers/google.d.ts.map +1 -0
  334. package/dist/auth/providers/google.js +10 -0
  335. package/dist/auth/providers/google.js.map +1 -0
  336. package/dist/auth/providers/microsoft.d.ts +9 -0
  337. package/dist/auth/providers/microsoft.d.ts.map +1 -0
  338. package/dist/auth/providers/microsoft.js +11 -0
  339. package/dist/auth/providers/microsoft.js.map +1 -0
  340. package/dist/auth/reset-email.d.ts +10 -0
  341. package/dist/auth/reset-email.d.ts.map +1 -0
  342. package/dist/auth/reset-email.js +30 -0
  343. package/dist/auth/reset-email.js.map +1 -0
  344. package/dist/auth/reset.d.ts +42 -0
  345. package/dist/auth/reset.d.ts.map +1 -0
  346. package/dist/auth/reset.js +142 -0
  347. package/dist/auth/reset.js.map +1 -0
  348. package/dist/auth/roles.d.ts +51 -0
  349. package/dist/auth/roles.d.ts.map +1 -0
  350. package/dist/auth/roles.js +159 -0
  351. package/dist/auth/roles.js.map +1 -0
  352. package/dist/auth/session.d.ts +38 -0
  353. package/dist/auth/session.d.ts.map +1 -0
  354. package/dist/auth/session.js +106 -0
  355. package/dist/auth/session.js.map +1 -0
  356. package/dist/auth/team.d.ts +99 -0
  357. package/dist/auth/team.d.ts.map +1 -0
  358. package/dist/auth/team.js +327 -0
  359. package/dist/auth/team.js.map +1 -0
  360. package/dist/auth/totp.d.ts +31 -0
  361. package/dist/auth/totp.d.ts.map +1 -0
  362. package/dist/auth/totp.js +134 -0
  363. package/dist/auth/totp.js.map +1 -0
  364. package/dist/auth/user-admin.d.ts +119 -0
  365. package/dist/auth/user-admin.d.ts.map +1 -0
  366. package/dist/auth/user-admin.js +292 -0
  367. package/dist/auth/user-admin.js.map +1 -0
  368. package/dist/backup/index.d.ts +19 -0
  369. package/dist/backup/index.d.ts.map +1 -0
  370. package/dist/backup/index.js +24 -0
  371. package/dist/backup/index.js.map +1 -0
  372. package/dist/billing/stripe-meter.d.ts +22 -0
  373. package/dist/billing/stripe-meter.d.ts.map +1 -0
  374. package/dist/billing/stripe-meter.js +101 -0
  375. package/dist/billing/stripe-meter.js.map +1 -0
  376. package/dist/brand-voice/index.d.ts +7 -0
  377. package/dist/brand-voice/index.d.ts.map +1 -0
  378. package/dist/brand-voice/index.js +7 -0
  379. package/dist/brand-voice/index.js.map +1 -0
  380. package/dist/cache/http.d.ts +140 -0
  381. package/dist/cache/http.d.ts.map +1 -0
  382. package/dist/cache/http.js +213 -0
  383. package/dist/cache/http.js.map +1 -0
  384. package/dist/cache/index.d.ts +15 -0
  385. package/dist/cache/index.d.ts.map +1 -0
  386. package/dist/cache/index.js +32 -0
  387. package/dist/cache/index.js.map +1 -0
  388. package/dist/client.d.ts +51 -0
  389. package/dist/client.d.ts.map +1 -0
  390. package/dist/client.js +59 -0
  391. package/dist/client.js.map +1 -0
  392. package/dist/codegen/index.d.ts +4 -0
  393. package/dist/codegen/index.d.ts.map +1 -0
  394. package/dist/codegen/index.js +366 -0
  395. package/dist/codegen/index.js.map +1 -0
  396. package/dist/collections/index.d.ts +19 -0
  397. package/dist/collections/index.d.ts.map +1 -0
  398. package/dist/collections/index.js +30 -0
  399. package/dist/collections/index.js.map +1 -0
  400. package/dist/collections/presets.d.ts +90 -0
  401. package/dist/collections/presets.d.ts.map +1 -0
  402. package/dist/collections/presets.js +573 -0
  403. package/dist/collections/presets.js.map +1 -0
  404. package/dist/config/define.d.ts +8 -0
  405. package/dist/config/define.d.ts.map +1 -0
  406. package/dist/config/define.js +7 -0
  407. package/dist/config/define.js.map +1 -0
  408. package/dist/config/index.d.ts +31 -0
  409. package/dist/config/index.d.ts.map +1 -0
  410. package/dist/config/index.js +196 -0
  411. package/dist/config/index.js.map +1 -0
  412. package/dist/config/runtime.d.ts +107 -0
  413. package/dist/config/runtime.d.ts.map +1 -0
  414. package/dist/config/runtime.js +77 -0
  415. package/dist/config/runtime.js.map +1 -0
  416. package/dist/config/types.d.ts +924 -0
  417. package/dist/config/types.d.ts.map +1 -0
  418. package/dist/config/types.js +2 -0
  419. package/dist/config/types.js.map +1 -0
  420. package/dist/content/ai-api.d.ts +21 -0
  421. package/dist/content/ai-api.d.ts.map +1 -0
  422. package/dist/content/ai-api.js +25 -0
  423. package/dist/content/ai-api.js.map +1 -0
  424. package/dist/content/content-graph.d.ts +25 -0
  425. package/dist/content/content-graph.d.ts.map +1 -0
  426. package/dist/content/content-graph.js +40 -0
  427. package/dist/content/content-graph.js.map +1 -0
  428. package/dist/content/extract.d.ts +7 -0
  429. package/dist/content/extract.d.ts.map +1 -0
  430. package/dist/content/extract.js +33 -0
  431. package/dist/content/extract.js.map +1 -0
  432. package/dist/content/index.d.ts +9 -0
  433. package/dist/content/index.d.ts.map +1 -0
  434. package/dist/content/index.js +6 -0
  435. package/dist/content/index.js.map +1 -0
  436. package/dist/content/slug.d.ts +40 -0
  437. package/dist/content/slug.d.ts.map +1 -0
  438. package/dist/content/slug.js +78 -0
  439. package/dist/content/slug.js.map +1 -0
  440. package/dist/content/structured-data.d.ts +163 -0
  441. package/dist/content/structured-data.d.ts.map +1 -0
  442. package/dist/content/structured-data.js +479 -0
  443. package/dist/content/structured-data.js.map +1 -0
  444. package/dist/content-health/cron.d.ts +68 -0
  445. package/dist/content-health/cron.d.ts.map +1 -0
  446. package/dist/content-health/cron.js +167 -0
  447. package/dist/content-health/cron.js.map +1 -0
  448. package/dist/content-health/scanner.d.ts +119 -0
  449. package/dist/content-health/scanner.d.ts.map +1 -0
  450. package/dist/content-health/scanner.js +257 -0
  451. package/dist/content-health/scanner.js.map +1 -0
  452. package/dist/cron/index.d.ts +90 -0
  453. package/dist/cron/index.d.ts.map +1 -0
  454. package/dist/cron/index.js +299 -0
  455. package/dist/cron/index.js.map +1 -0
  456. package/dist/db/adapters/mysql.d.ts +5 -0
  457. package/dist/db/adapters/mysql.d.ts.map +1 -0
  458. package/dist/db/adapters/mysql.js +26 -0
  459. package/dist/db/adapters/mysql.js.map +1 -0
  460. package/dist/db/adapters/postgres.d.ts +7 -0
  461. package/dist/db/adapters/postgres.d.ts.map +1 -0
  462. package/dist/db/adapters/postgres.js +25 -0
  463. package/dist/db/adapters/postgres.js.map +1 -0
  464. package/dist/db/adapters/sqlite.d.ts +5 -0
  465. package/dist/db/adapters/sqlite.d.ts.map +1 -0
  466. package/dist/db/adapters/sqlite.js +27 -0
  467. package/dist/db/adapters/sqlite.js.map +1 -0
  468. package/dist/db/client-types.d.ts +75 -0
  469. package/dist/db/client-types.d.ts.map +1 -0
  470. package/dist/db/client-types.js +2 -0
  471. package/dist/db/client-types.js.map +1 -0
  472. package/dist/db/create-adapter.d.ts +11 -0
  473. package/dist/db/create-adapter.d.ts.map +1 -0
  474. package/dist/db/create-adapter.js +43 -0
  475. package/dist/db/create-adapter.js.map +1 -0
  476. package/dist/db/find-in-batches.d.ts +12 -0
  477. package/dist/db/find-in-batches.d.ts.map +1 -0
  478. package/dist/db/find-in-batches.js +20 -0
  479. package/dist/db/find-in-batches.js.map +1 -0
  480. package/dist/db/index.d.ts +9 -0
  481. package/dist/db/index.d.ts.map +1 -0
  482. package/dist/db/index.js +5 -0
  483. package/dist/db/index.js.map +1 -0
  484. package/dist/db/model-types.d.ts +602 -0
  485. package/dist/db/model-types.d.ts.map +1 -0
  486. package/dist/db/model-types.js +14 -0
  487. package/dist/db/model-types.js.map +1 -0
  488. package/dist/db.d.ts +20 -0
  489. package/dist/db.d.ts.map +1 -0
  490. package/dist/db.js +35 -0
  491. package/dist/db.js.map +1 -0
  492. package/dist/diagnostics/api-metrics.d.ts +135 -0
  493. package/dist/diagnostics/api-metrics.d.ts.map +1 -0
  494. package/dist/diagnostics/api-metrics.js +376 -0
  495. package/dist/diagnostics/api-metrics.js.map +1 -0
  496. package/dist/diagnostics/env.d.ts +44 -0
  497. package/dist/diagnostics/env.d.ts.map +1 -0
  498. package/dist/diagnostics/env.js +354 -0
  499. package/dist/diagnostics/env.js.map +1 -0
  500. package/dist/diagnostics/logger.d.ts +38 -0
  501. package/dist/diagnostics/logger.d.ts.map +1 -0
  502. package/dist/diagnostics/logger.js +89 -0
  503. package/dist/diagnostics/logger.js.map +1 -0
  504. package/dist/diagnostics/sentry.d.ts +8 -0
  505. package/dist/diagnostics/sentry.d.ts.map +1 -0
  506. package/dist/diagnostics/sentry.js +54 -0
  507. package/dist/diagnostics/sentry.js.map +1 -0
  508. package/dist/email/resolve.d.ts +27 -0
  509. package/dist/email/resolve.d.ts.map +1 -0
  510. package/dist/email/resolve.js +68 -0
  511. package/dist/email/resolve.js.map +1 -0
  512. package/dist/email/sender.d.ts +71 -0
  513. package/dist/email/sender.d.ts.map +1 -0
  514. package/dist/email/sender.js +41 -0
  515. package/dist/email/sender.js.map +1 -0
  516. package/dist/email/settings-store.d.ts +116 -0
  517. package/dist/email/settings-store.d.ts.map +1 -0
  518. package/dist/email/settings-store.js +222 -0
  519. package/dist/email/settings-store.js.map +1 -0
  520. package/dist/fields/component-block.d.ts +23 -0
  521. package/dist/fields/component-block.d.ts.map +1 -0
  522. package/dist/fields/component-block.js +137 -0
  523. package/dist/fields/component-block.js.map +1 -0
  524. package/dist/fields/index.d.ts +19 -0
  525. package/dist/fields/index.d.ts.map +1 -0
  526. package/dist/fields/index.js +95 -0
  527. package/dist/fields/index.js.map +1 -0
  528. package/dist/fields/presets.d.ts +144 -0
  529. package/dist/fields/presets.d.ts.map +1 -0
  530. package/dist/fields/presets.js +350 -0
  531. package/dist/fields/presets.js.map +1 -0
  532. package/dist/fields/relations.d.ts +87 -0
  533. package/dist/fields/relations.d.ts.map +1 -0
  534. package/dist/fields/relations.js +231 -0
  535. package/dist/fields/relations.js.map +1 -0
  536. package/dist/forms/analytics.d.ts +62 -0
  537. package/dist/forms/analytics.d.ts.map +1 -0
  538. package/dist/forms/analytics.js +95 -0
  539. package/dist/forms/analytics.js.map +1 -0
  540. package/dist/forms/attribution.d.ts +175 -0
  541. package/dist/forms/attribution.d.ts.map +1 -0
  542. package/dist/forms/attribution.js +577 -0
  543. package/dist/forms/attribution.js.map +1 -0
  544. package/dist/forms/client-validation.d.ts +50 -0
  545. package/dist/forms/client-validation.d.ts.map +1 -0
  546. package/dist/forms/client-validation.js +215 -0
  547. package/dist/forms/client-validation.js.map +1 -0
  548. package/dist/forms/delivery.d.ts +89 -0
  549. package/dist/forms/delivery.d.ts.map +1 -0
  550. package/dist/forms/delivery.js +477 -0
  551. package/dist/forms/delivery.js.map +1 -0
  552. package/dist/forms/encryption.d.ts +20 -0
  553. package/dist/forms/encryption.d.ts.map +1 -0
  554. package/dist/forms/encryption.js +80 -0
  555. package/dist/forms/encryption.js.map +1 -0
  556. package/dist/forms/entries.d.ts +68 -0
  557. package/dist/forms/entries.d.ts.map +1 -0
  558. package/dist/forms/entries.js +557 -0
  559. package/dist/forms/entries.js.map +1 -0
  560. package/dist/forms/files.d.ts +38 -0
  561. package/dist/forms/files.d.ts.map +1 -0
  562. package/dist/forms/files.js +134 -0
  563. package/dist/forms/files.js.map +1 -0
  564. package/dist/forms/index.d.ts +21 -0
  565. package/dist/forms/index.d.ts.map +1 -0
  566. package/dist/forms/index.js +20 -0
  567. package/dist/forms/index.js.map +1 -0
  568. package/dist/forms/notify.d.ts +80 -0
  569. package/dist/forms/notify.d.ts.map +1 -0
  570. package/dist/forms/notify.js +357 -0
  571. package/dist/forms/notify.js.map +1 -0
  572. package/dist/forms/prefetch.d.ts +17 -0
  573. package/dist/forms/prefetch.d.ts.map +1 -0
  574. package/dist/forms/prefetch.js +36 -0
  575. package/dist/forms/prefetch.js.map +1 -0
  576. package/dist/forms/public-html.d.ts +22 -0
  577. package/dist/forms/public-html.d.ts.map +1 -0
  578. package/dist/forms/public-html.js +145 -0
  579. package/dist/forms/public-html.js.map +1 -0
  580. package/dist/forms/service.d.ts +102 -0
  581. package/dist/forms/service.d.ts.map +1 -0
  582. package/dist/forms/service.js +625 -0
  583. package/dist/forms/service.js.map +1 -0
  584. package/dist/forms/spam.d.ts +33 -0
  585. package/dist/forms/spam.d.ts.map +1 -0
  586. package/dist/forms/spam.js +93 -0
  587. package/dist/forms/spam.js.map +1 -0
  588. package/dist/forms/submission.d.ts +132 -0
  589. package/dist/forms/submission.d.ts.map +1 -0
  590. package/dist/forms/submission.js +348 -0
  591. package/dist/forms/submission.js.map +1 -0
  592. package/dist/forms/types.d.ts +259 -0
  593. package/dist/forms/types.d.ts.map +1 -0
  594. package/dist/forms/types.js +20 -0
  595. package/dist/forms/types.js.map +1 -0
  596. package/dist/forms/validation.d.ts +19 -0
  597. package/dist/forms/validation.d.ts.map +1 -0
  598. package/dist/forms/validation.js +166 -0
  599. package/dist/forms/validation.js.map +1 -0
  600. package/dist/forms/webhooks.d.ts +60 -0
  601. package/dist/forms/webhooks.d.ts.map +1 -0
  602. package/dist/forms/webhooks.js +360 -0
  603. package/dist/forms/webhooks.js.map +1 -0
  604. package/dist/forms/webmcp.d.ts +36 -0
  605. package/dist/forms/webmcp.d.ts.map +1 -0
  606. package/dist/forms/webmcp.js +67 -0
  607. package/dist/forms/webmcp.js.map +1 -0
  608. package/dist/forms/xlsx.d.ts +10 -0
  609. package/dist/forms/xlsx.d.ts.map +1 -0
  610. package/dist/forms/xlsx.js +207 -0
  611. package/dist/forms/xlsx.js.map +1 -0
  612. package/dist/generated/browser.d.ts +220 -0
  613. package/dist/generated/browser.js +17 -0
  614. package/dist/generated/client.d.ts +239 -0
  615. package/dist/generated/client.js +34 -0
  616. package/dist/generated/commonInputTypes.d.ts +887 -0
  617. package/dist/generated/commonInputTypes.js +10 -0
  618. package/dist/generated/enums.d.ts +83 -0
  619. package/dist/generated/enums.js +81 -0
  620. package/dist/generated/internal/class.d.ts +517 -0
  621. package/dist/generated/internal/class.js +52 -0
  622. package/dist/generated/internal/prismaNamespace.d.ts +4184 -0
  623. package/dist/generated/internal/prismaNamespace.js +932 -0
  624. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +968 -0
  625. package/dist/generated/internal/prismaNamespaceBrowser.js +903 -0
  626. package/dist/generated/models/AiCrawlerHit.d.ts +1115 -0
  627. package/dist/generated/models/AiCrawlerHit.js +1 -0
  628. package/dist/generated/models/ApiKey.d.ts +1388 -0
  629. package/dist/generated/models/ApiKey.js +1 -0
  630. package/dist/generated/models/ApiRequestMetric.d.ts +1229 -0
  631. package/dist/generated/models/ApiRequestMetric.js +1 -0
  632. package/dist/generated/models/AuditLog.d.ts +1062 -0
  633. package/dist/generated/models/AuditLog.js +1 -0
  634. package/dist/generated/models/BackupRecord.d.ts +1100 -0
  635. package/dist/generated/models/BackupRecord.js +1 -0
  636. package/dist/generated/models/ContentIssue.d.ts +1318 -0
  637. package/dist/generated/models/ContentIssue.js +1 -0
  638. package/dist/generated/models/ContentLock.d.ts +1295 -0
  639. package/dist/generated/models/ContentLock.js +1 -0
  640. package/dist/generated/models/ContentTemplate.d.ts +1254 -0
  641. package/dist/generated/models/ContentTemplate.js +1 -0
  642. package/dist/generated/models/Document.d.ts +3443 -0
  643. package/dist/generated/models/Document.js +1 -0
  644. package/dist/generated/models/Folder.d.ts +1689 -0
  645. package/dist/generated/models/Folder.js +1 -0
  646. package/dist/generated/models/FormDeliveryAttempt.d.ts +1705 -0
  647. package/dist/generated/models/FormDeliveryAttempt.js +1 -0
  648. package/dist/generated/models/FormSchemaVersion.d.ts +1105 -0
  649. package/dist/generated/models/FormSchemaVersion.js +1 -0
  650. package/dist/generated/models/FormSubmission.d.ts +2125 -0
  651. package/dist/generated/models/FormSubmission.js +1 -0
  652. package/dist/generated/models/FormSubmissionFile.d.ts +1438 -0
  653. package/dist/generated/models/FormSubmissionFile.js +1 -0
  654. package/dist/generated/models/FormWebhook.d.ts +1271 -0
  655. package/dist/generated/models/FormWebhook.js +1 -0
  656. package/dist/generated/models/InAppNotification.d.ts +1295 -0
  657. package/dist/generated/models/InAppNotification.js +1 -0
  658. package/dist/generated/models/Invite.d.ts +1240 -0
  659. package/dist/generated/models/Invite.js +1 -0
  660. package/dist/generated/models/Media.d.ts +2759 -0
  661. package/dist/generated/models/Media.js +1 -0
  662. package/dist/generated/models/MediaUsage.d.ts +1301 -0
  663. package/dist/generated/models/MediaUsage.js +1 -0
  664. package/dist/generated/models/OAuthAccount.d.ts +1300 -0
  665. package/dist/generated/models/OAuthAccount.js +1 -0
  666. package/dist/generated/models/PageTemplate.d.ts +1118 -0
  667. package/dist/generated/models/PageTemplate.js +1 -0
  668. package/dist/generated/models/PasswordResetToken.d.ts +1225 -0
  669. package/dist/generated/models/PasswordResetToken.js +1 -0
  670. package/dist/generated/models/Redirect.d.ts +1342 -0
  671. package/dist/generated/models/Redirect.js +1 -0
  672. package/dist/generated/models/Redirect404Hit.d.ts +1107 -0
  673. package/dist/generated/models/Redirect404Hit.js +1 -0
  674. package/dist/generated/models/RedirectSuggestion.d.ts +1185 -0
  675. package/dist/generated/models/RedirectSuggestion.js +1 -0
  676. package/dist/generated/models/SavedSection.d.ts +1156 -0
  677. package/dist/generated/models/SavedSection.js +1 -0
  678. package/dist/generated/models/ScriptTag.d.ts +1291 -0
  679. package/dist/generated/models/ScriptTag.js +1 -0
  680. package/dist/generated/models/SeoAuditRun.d.ts +1570 -0
  681. package/dist/generated/models/SeoAuditRun.js +1 -0
  682. package/dist/generated/models/SeoIssue.d.ts +1873 -0
  683. package/dist/generated/models/SeoIssue.js +1 -0
  684. package/dist/generated/models/Session.d.ts +1330 -0
  685. package/dist/generated/models/Session.js +1 -0
  686. package/dist/generated/models/Site.d.ts +1062 -0
  687. package/dist/generated/models/Site.js +1 -0
  688. package/dist/generated/models/User.d.ts +4344 -0
  689. package/dist/generated/models/User.js +1 -0
  690. package/dist/generated/models/Version.d.ts +1334 -0
  691. package/dist/generated/models/Version.js +1 -0
  692. package/dist/generated/models/WebhookDeliveryLog.d.ts +1479 -0
  693. package/dist/generated/models/WebhookDeliveryLog.js +1 -0
  694. package/dist/generated/models/WebhookEndpoint.d.ts +1331 -0
  695. package/dist/generated/models/WebhookEndpoint.js +1 -0
  696. package/dist/generated/models/WorkflowState.d.ts +1436 -0
  697. package/dist/generated/models/WorkflowState.js +1 -0
  698. package/dist/generated/models.d.ts +37 -0
  699. package/dist/generated/models.js +1 -0
  700. package/dist/graphql/index.d.ts +11 -0
  701. package/dist/graphql/index.d.ts.map +1 -0
  702. package/dist/graphql/index.js +74 -0
  703. package/dist/graphql/index.js.map +1 -0
  704. package/dist/graphql/resolvers.d.ts +8 -0
  705. package/dist/graphql/resolvers.d.ts.map +1 -0
  706. package/dist/graphql/resolvers.js +89 -0
  707. package/dist/graphql/resolvers.js.map +1 -0
  708. package/dist/graphql/schema-builder.d.ts +3 -0
  709. package/dist/graphql/schema-builder.d.ts.map +1 -0
  710. package/dist/graphql/schema-builder.js +103 -0
  711. package/dist/graphql/schema-builder.js.map +1 -0
  712. package/dist/health/index.d.ts +27 -0
  713. package/dist/health/index.d.ts.map +1 -0
  714. package/dist/health/index.js +43 -0
  715. package/dist/health/index.js.map +1 -0
  716. package/dist/i18n/index.d.ts +22 -0
  717. package/dist/i18n/index.d.ts.map +1 -0
  718. package/dist/i18n/index.js +37 -0
  719. package/dist/i18n/index.js.map +1 -0
  720. package/dist/index.d.ts +125 -0
  721. package/dist/index.d.ts.map +1 -0
  722. package/dist/index.js +132 -0
  723. package/dist/index.js.map +1 -0
  724. package/dist/media/content-hash.d.ts +3 -0
  725. package/dist/media/content-hash.d.ts.map +1 -0
  726. package/dist/media/content-hash.js +11 -0
  727. package/dist/media/content-hash.js.map +1 -0
  728. package/dist/media/denormalize.d.ts +23 -0
  729. package/dist/media/denormalize.d.ts.map +1 -0
  730. package/dist/media/denormalize.js +170 -0
  731. package/dist/media/denormalize.js.map +1 -0
  732. package/dist/media/index.d.ts +8 -0
  733. package/dist/media/index.d.ts.map +1 -0
  734. package/dist/media/index.js +5 -0
  735. package/dist/media/index.js.map +1 -0
  736. package/dist/media/merge-document-media.d.ts +23 -0
  737. package/dist/media/merge-document-media.d.ts.map +1 -0
  738. package/dist/media/merge-document-media.js +146 -0
  739. package/dist/media/merge-document-media.js.map +1 -0
  740. package/dist/media/metadata-patch.d.ts +21 -0
  741. package/dist/media/metadata-patch.d.ts.map +1 -0
  742. package/dist/media/metadata-patch.js +37 -0
  743. package/dist/media/metadata-patch.js.map +1 -0
  744. package/dist/media/optimize.d.ts +61 -0
  745. package/dist/media/optimize.d.ts.map +1 -0
  746. package/dist/media/optimize.js +168 -0
  747. package/dist/media/optimize.js.map +1 -0
  748. package/dist/media/seo.d.ts +110 -0
  749. package/dist/media/seo.d.ts.map +1 -0
  750. package/dist/media/seo.js +263 -0
  751. package/dist/media/seo.js.map +1 -0
  752. package/dist/media/usage-sync.d.ts +37 -0
  753. package/dist/media/usage-sync.d.ts.map +1 -0
  754. package/dist/media/usage-sync.js +234 -0
  755. package/dist/media/usage-sync.js.map +1 -0
  756. package/dist/middleware.d.ts +17 -0
  757. package/dist/middleware.d.ts.map +1 -0
  758. package/dist/middleware.js +159 -0
  759. package/dist/middleware.js.map +1 -0
  760. package/dist/multisite/index.d.ts +20 -0
  761. package/dist/multisite/index.d.ts.map +1 -0
  762. package/dist/multisite/index.js +26 -0
  763. package/dist/multisite/index.js.map +1 -0
  764. package/dist/navigation/unresolved-targets.d.ts +35 -0
  765. package/dist/navigation/unresolved-targets.d.ts.map +1 -0
  766. package/dist/navigation/unresolved-targets.js +109 -0
  767. package/dist/navigation/unresolved-targets.js.map +1 -0
  768. package/dist/next/preview.d.ts +10 -0
  769. package/dist/next/preview.d.ts.map +1 -0
  770. package/dist/next/preview.js +17 -0
  771. package/dist/next/preview.js.map +1 -0
  772. package/dist/next/script-tags.d.ts +15 -0
  773. package/dist/next/script-tags.d.ts.map +1 -0
  774. package/dist/next/script-tags.js +58 -0
  775. package/dist/next/script-tags.js.map +1 -0
  776. package/dist/next-site.d.ts +29 -0
  777. package/dist/next-site.d.ts.map +1 -0
  778. package/dist/next-site.js +60 -0
  779. package/dist/next-site.js.map +1 -0
  780. package/dist/next.d.ts +33 -0
  781. package/dist/next.d.ts.map +1 -0
  782. package/dist/next.js +142 -0
  783. package/dist/next.js.map +1 -0
  784. package/dist/notifications/index.d.ts +20 -0
  785. package/dist/notifications/index.d.ts.map +1 -0
  786. package/dist/notifications/index.js +22 -0
  787. package/dist/notifications/index.js.map +1 -0
  788. package/dist/page-builder/a11y-fix.d.ts +20 -0
  789. package/dist/page-builder/a11y-fix.d.ts.map +1 -0
  790. package/dist/page-builder/a11y-fix.js +182 -0
  791. package/dist/page-builder/a11y-fix.js.map +1 -0
  792. package/dist/page-builder/ai-pipeline.d.ts +43 -0
  793. package/dist/page-builder/ai-pipeline.d.ts.map +1 -0
  794. package/dist/page-builder/ai-pipeline.js +165 -0
  795. package/dist/page-builder/ai-pipeline.js.map +1 -0
  796. package/dist/page-builder/blocks.d.ts +27 -0
  797. package/dist/page-builder/blocks.d.ts.map +1 -0
  798. package/dist/page-builder/blocks.js +313 -0
  799. package/dist/page-builder/blocks.js.map +1 -0
  800. package/dist/page-builder/design-scorer.d.ts +27 -0
  801. package/dist/page-builder/design-scorer.d.ts.map +1 -0
  802. package/dist/page-builder/design-scorer.js +612 -0
  803. package/dist/page-builder/design-scorer.js.map +1 -0
  804. package/dist/page-builder/index.d.ts +18 -0
  805. package/dist/page-builder/index.d.ts.map +1 -0
  806. package/dist/page-builder/index.js +20 -0
  807. package/dist/page-builder/index.js.map +1 -0
  808. package/dist/page-builder/schema.d.ts +107 -0
  809. package/dist/page-builder/schema.d.ts.map +1 -0
  810. package/dist/page-builder/schema.js +90 -0
  811. package/dist/page-builder/schema.js.map +1 -0
  812. package/dist/page-builder/seo-analyzer.d.ts +61 -0
  813. package/dist/page-builder/seo-analyzer.d.ts.map +1 -0
  814. package/dist/page-builder/seo-analyzer.js +778 -0
  815. package/dist/page-builder/seo-analyzer.js.map +1 -0
  816. package/dist/page-builder/templates.d.ts +9 -0
  817. package/dist/page-builder/templates.d.ts.map +1 -0
  818. package/dist/page-builder/templates.js +742 -0
  819. package/dist/page-builder/templates.js.map +1 -0
  820. package/dist/page-builder/tree.d.ts +17 -0
  821. package/dist/page-builder/tree.d.ts.map +1 -0
  822. package/dist/page-builder/tree.js +160 -0
  823. package/dist/page-builder/tree.js.map +1 -0
  824. package/dist/page-builder/types.d.ts +112 -0
  825. package/dist/page-builder/types.d.ts.map +1 -0
  826. package/dist/page-builder/types.js +4 -0
  827. package/dist/page-builder/types.js.map +1 -0
  828. package/dist/page-builder/validate.d.ts +6 -0
  829. package/dist/page-builder/validate.d.ts.map +1 -0
  830. package/dist/page-builder/validate.js +87 -0
  831. package/dist/page-builder/validate.js.map +1 -0
  832. package/dist/plan/features.d.ts +17 -0
  833. package/dist/plan/features.d.ts.map +1 -0
  834. package/dist/plan/features.js +73 -0
  835. package/dist/plan/features.js.map +1 -0
  836. package/dist/posts/query.d.ts +50 -0
  837. package/dist/posts/query.d.ts.map +1 -0
  838. package/dist/posts/query.js +105 -0
  839. package/dist/posts/query.js.map +1 -0
  840. package/dist/presence/index.d.ts +24 -0
  841. package/dist/presence/index.d.ts.map +1 -0
  842. package/dist/presence/index.js +99 -0
  843. package/dist/presence/index.js.map +1 -0
  844. package/dist/preview/event-bus.d.ts +39 -0
  845. package/dist/preview/event-bus.d.ts.map +1 -0
  846. package/dist/preview/event-bus.js +73 -0
  847. package/dist/preview/event-bus.js.map +1 -0
  848. package/dist/preview/index.d.ts +36 -0
  849. package/dist/preview/index.d.ts.map +1 -0
  850. package/dist/preview/index.js +68 -0
  851. package/dist/preview/index.js.map +1 -0
  852. package/dist/preview/sse-stream.d.ts +47 -0
  853. package/dist/preview/sse-stream.d.ts.map +1 -0
  854. package/dist/preview/sse-stream.js +111 -0
  855. package/dist/preview/sse-stream.js.map +1 -0
  856. package/dist/privacy/index.d.ts +33 -0
  857. package/dist/privacy/index.d.ts.map +1 -0
  858. package/dist/privacy/index.js +15 -0
  859. package/dist/privacy/index.js.map +1 -0
  860. package/dist/public-redirects.d.ts +78 -0
  861. package/dist/public-redirects.d.ts.map +1 -0
  862. package/dist/public-redirects.js +187 -0
  863. package/dist/public-redirects.js.map +1 -0
  864. package/dist/public-security-headers.d.ts +27 -0
  865. package/dist/public-security-headers.d.ts.map +1 -0
  866. package/dist/public-security-headers.js +40 -0
  867. package/dist/public-security-headers.js.map +1 -0
  868. package/dist/publishing/publish-sync.d.ts +50 -0
  869. package/dist/publishing/publish-sync.d.ts.map +1 -0
  870. package/dist/publishing/publish-sync.js +124 -0
  871. package/dist/publishing/publish-sync.js.map +1 -0
  872. package/dist/purge/adapters.d.ts +60 -0
  873. package/dist/purge/adapters.d.ts.map +1 -0
  874. package/dist/purge/adapters.js +81 -0
  875. package/dist/purge/adapters.js.map +1 -0
  876. package/dist/purge/index.d.ts +12 -0
  877. package/dist/purge/index.d.ts.map +1 -0
  878. package/dist/purge/index.js +10 -0
  879. package/dist/purge/index.js.map +1 -0
  880. package/dist/purge/receiver.d.ts +95 -0
  881. package/dist/purge/receiver.d.ts.map +1 -0
  882. package/dist/purge/receiver.js +176 -0
  883. package/dist/purge/receiver.js.map +1 -0
  884. package/dist/realtime/comments.d.ts +217 -0
  885. package/dist/realtime/comments.d.ts.map +1 -0
  886. package/dist/realtime/comments.js +282 -0
  887. package/dist/realtime/comments.js.map +1 -0
  888. package/dist/realtime/gateway.d.ts +107 -0
  889. package/dist/realtime/gateway.d.ts.map +1 -0
  890. package/dist/realtime/gateway.js +215 -0
  891. package/dist/realtime/gateway.js.map +1 -0
  892. package/dist/realtime/index.d.ts +21 -0
  893. package/dist/realtime/index.d.ts.map +1 -0
  894. package/dist/realtime/index.js +27 -0
  895. package/dist/realtime/index.js.map +1 -0
  896. package/dist/realtime/notification-bus.d.ts +60 -0
  897. package/dist/realtime/notification-bus.d.ts.map +1 -0
  898. package/dist/realtime/notification-bus.js +77 -0
  899. package/dist/realtime/notification-bus.js.map +1 -0
  900. package/dist/realtime/notification-stream.d.ts +50 -0
  901. package/dist/realtime/notification-stream.d.ts.map +1 -0
  902. package/dist/realtime/notification-stream.js +148 -0
  903. package/dist/realtime/notification-stream.js.map +1 -0
  904. package/dist/realtime/notifications.d.ts +201 -0
  905. package/dist/realtime/notifications.d.ts.map +1 -0
  906. package/dist/realtime/notifications.js +233 -0
  907. package/dist/realtime/notifications.js.map +1 -0
  908. package/dist/realtime/persistence.d.ts +72 -0
  909. package/dist/realtime/persistence.d.ts.map +1 -0
  910. package/dist/realtime/persistence.js +53 -0
  911. package/dist/realtime/persistence.js.map +1 -0
  912. package/dist/realtime/sse.d.ts +20 -0
  913. package/dist/realtime/sse.d.ts.map +1 -0
  914. package/dist/realtime/sse.js +31 -0
  915. package/dist/realtime/sse.js.map +1 -0
  916. package/dist/redirects/auto.d.ts +45 -0
  917. package/dist/redirects/auto.d.ts.map +1 -0
  918. package/dist/redirects/auto.js +60 -0
  919. package/dist/redirects/auto.js.map +1 -0
  920. package/dist/redirects/fix-link.d.ts +15 -0
  921. package/dist/redirects/fix-link.d.ts.map +1 -0
  922. package/dist/redirects/fix-link.js +66 -0
  923. package/dist/redirects/fix-link.js.map +1 -0
  924. package/dist/redirects/graph.d.ts +92 -0
  925. package/dist/redirects/graph.d.ts.map +1 -0
  926. package/dist/redirects/graph.js +176 -0
  927. package/dist/redirects/graph.js.map +1 -0
  928. package/dist/redirects/index.d.ts +17 -0
  929. package/dist/redirects/index.d.ts.map +1 -0
  930. package/dist/redirects/index.js +10 -0
  931. package/dist/redirects/index.js.map +1 -0
  932. package/dist/redirects/legitimacy.d.ts +23 -0
  933. package/dist/redirects/legitimacy.d.ts.map +1 -0
  934. package/dist/redirects/legitimacy.js +64 -0
  935. package/dist/redirects/legitimacy.js.map +1 -0
  936. package/dist/redirects/recovery.d.ts +92 -0
  937. package/dist/redirects/recovery.d.ts.map +1 -0
  938. package/dist/redirects/recovery.js +210 -0
  939. package/dist/redirects/recovery.js.map +1 -0
  940. package/dist/redirects/rerank.d.ts +24 -0
  941. package/dist/redirects/rerank.d.ts.map +1 -0
  942. package/dist/redirects/rerank.js +82 -0
  943. package/dist/redirects/rerank.js.map +1 -0
  944. package/dist/redirects/suggest.d.ts +23 -0
  945. package/dist/redirects/suggest.d.ts.map +1 -0
  946. package/dist/redirects/suggest.js +80 -0
  947. package/dist/redirects/suggest.js.map +1 -0
  948. package/dist/redirects/typo.d.ts +10 -0
  949. package/dist/redirects/typo.d.ts.map +1 -0
  950. package/dist/redirects/typo.js +51 -0
  951. package/dist/redirects/typo.js.map +1 -0
  952. package/dist/redirects/validate-destination.d.ts +13 -0
  953. package/dist/redirects/validate-destination.d.ts.map +1 -0
  954. package/dist/redirects/validate-destination.js +51 -0
  955. package/dist/redirects/validate-destination.js.map +1 -0
  956. package/dist/relationships/index.d.ts +13 -0
  957. package/dist/relationships/index.d.ts.map +1 -0
  958. package/dist/relationships/index.js +12 -0
  959. package/dist/relationships/index.js.map +1 -0
  960. package/dist/scheduling/index.d.ts +45 -0
  961. package/dist/scheduling/index.d.ts.map +1 -0
  962. package/dist/scheduling/index.js +141 -0
  963. package/dist/scheduling/index.js.map +1 -0
  964. package/dist/script-tags/index.d.ts +3 -0
  965. package/dist/script-tags/index.d.ts.map +1 -0
  966. package/dist/script-tags/index.js +3 -0
  967. package/dist/script-tags/index.js.map +1 -0
  968. package/dist/script-tags/invalidate.d.ts +3 -0
  969. package/dist/script-tags/invalidate.d.ts.map +1 -0
  970. package/dist/script-tags/invalidate.js +16 -0
  971. package/dist/script-tags/invalidate.js.map +1 -0
  972. package/dist/script-tags/resolve.d.ts +41 -0
  973. package/dist/script-tags/resolve.d.ts.map +1 -0
  974. package/dist/script-tags/resolve.js +130 -0
  975. package/dist/script-tags/resolve.js.map +1 -0
  976. package/dist/search/index.d.ts +25 -0
  977. package/dist/search/index.d.ts.map +1 -0
  978. package/dist/search/index.js +189 -0
  979. package/dist/search/index.js.map +1 -0
  980. package/dist/sections/bridge.d.ts +8 -0
  981. package/dist/sections/bridge.d.ts.map +1 -0
  982. package/dist/sections/bridge.js +211 -0
  983. package/dist/sections/bridge.js.map +1 -0
  984. package/dist/sections/define.d.ts +21 -0
  985. package/dist/sections/define.d.ts.map +1 -0
  986. package/dist/sections/define.js +160 -0
  987. package/dist/sections/define.js.map +1 -0
  988. package/dist/sections/embed.d.ts +24 -0
  989. package/dist/sections/embed.d.ts.map +1 -0
  990. package/dist/sections/embed.js +78 -0
  991. package/dist/sections/embed.js.map +1 -0
  992. package/dist/sections/fields.d.ts +37 -0
  993. package/dist/sections/fields.d.ts.map +1 -0
  994. package/dist/sections/fields.js +50 -0
  995. package/dist/sections/fields.js.map +1 -0
  996. package/dist/sections/helpers.d.ts +149 -0
  997. package/dist/sections/helpers.d.ts.map +1 -0
  998. package/dist/sections/helpers.js +373 -0
  999. package/dist/sections/helpers.js.map +1 -0
  1000. package/dist/sections/index.d.ts +22 -0
  1001. package/dist/sections/index.d.ts.map +1 -0
  1002. package/dist/sections/index.js +18 -0
  1003. package/dist/sections/index.js.map +1 -0
  1004. package/dist/sections/media-enrich.d.ts +45 -0
  1005. package/dist/sections/media-enrich.d.ts.map +1 -0
  1006. package/dist/sections/media-enrich.js +137 -0
  1007. package/dist/sections/media-enrich.js.map +1 -0
  1008. package/dist/sections/migrate.d.ts +42 -0
  1009. package/dist/sections/migrate.d.ts.map +1 -0
  1010. package/dist/sections/migrate.js +66 -0
  1011. package/dist/sections/migrate.js.map +1 -0
  1012. package/dist/sections/registry.d.ts +46 -0
  1013. package/dist/sections/registry.d.ts.map +1 -0
  1014. package/dist/sections/registry.js +598 -0
  1015. package/dist/sections/registry.js.map +1 -0
  1016. package/dist/sections/types.d.ts +192 -0
  1017. package/dist/sections/types.d.ts.map +1 -0
  1018. package/dist/sections/types.js +21 -0
  1019. package/dist/sections/types.js.map +1 -0
  1020. package/dist/sections/validate.d.ts +12 -0
  1021. package/dist/sections/validate.d.ts.map +1 -0
  1022. package/dist/sections/validate.js +186 -0
  1023. package/dist/sections/validate.js.map +1 -0
  1024. package/dist/sections/widget-embed.d.ts +45 -0
  1025. package/dist/sections/widget-embed.d.ts.map +1 -0
  1026. package/dist/sections/widget-embed.js +129 -0
  1027. package/dist/sections/widget-embed.js.map +1 -0
  1028. package/dist/security/access.d.ts +35 -0
  1029. package/dist/security/access.d.ts.map +1 -0
  1030. package/dist/security/access.js +133 -0
  1031. package/dist/security/access.js.map +1 -0
  1032. package/dist/security/anomaly-detection.d.ts +17 -0
  1033. package/dist/security/anomaly-detection.d.ts.map +1 -0
  1034. package/dist/security/anomaly-detection.js +39 -0
  1035. package/dist/security/anomaly-detection.js.map +1 -0
  1036. package/dist/security/api-key-enhanced.d.ts +68 -0
  1037. package/dist/security/api-key-enhanced.d.ts.map +1 -0
  1038. package/dist/security/api-key-enhanced.js +76 -0
  1039. package/dist/security/api-key-enhanced.js.map +1 -0
  1040. package/dist/security/audit.d.ts +46 -0
  1041. package/dist/security/audit.d.ts.map +1 -0
  1042. package/dist/security/audit.js +67 -0
  1043. package/dist/security/audit.js.map +1 -0
  1044. package/dist/security/breach-check.d.ts +3 -0
  1045. package/dist/security/breach-check.d.ts.map +1 -0
  1046. package/dist/security/breach-check.js +27 -0
  1047. package/dist/security/breach-check.js.map +1 -0
  1048. package/dist/security/captcha.d.ts +67 -0
  1049. package/dist/security/captcha.d.ts.map +1 -0
  1050. package/dist/security/captcha.js +186 -0
  1051. package/dist/security/captcha.js.map +1 -0
  1052. package/dist/security/center/gather.d.ts +20 -0
  1053. package/dist/security/center/gather.d.ts.map +1 -0
  1054. package/dist/security/center/gather.js +240 -0
  1055. package/dist/security/center/gather.js.map +1 -0
  1056. package/dist/security/center/index.d.ts +13 -0
  1057. package/dist/security/center/index.d.ts.map +1 -0
  1058. package/dist/security/center/index.js +4 -0
  1059. package/dist/security/center/index.js.map +1 -0
  1060. package/dist/security/center/settings.d.ts +48 -0
  1061. package/dist/security/center/settings.d.ts.map +1 -0
  1062. package/dist/security/center/settings.js +218 -0
  1063. package/dist/security/center/settings.js.map +1 -0
  1064. package/dist/security/center/status.d.ts +10 -0
  1065. package/dist/security/center/status.d.ts.map +1 -0
  1066. package/dist/security/center/status.js +249 -0
  1067. package/dist/security/center/status.js.map +1 -0
  1068. package/dist/security/center/summaries.d.ts +21 -0
  1069. package/dist/security/center/summaries.d.ts.map +1 -0
  1070. package/dist/security/center/summaries.js +65 -0
  1071. package/dist/security/center/summaries.js.map +1 -0
  1072. package/dist/security/center/types.d.ts +142 -0
  1073. package/dist/security/center/types.d.ts.map +1 -0
  1074. package/dist/security/center/types.js +12 -0
  1075. package/dist/security/center/types.js.map +1 -0
  1076. package/dist/security/client-ip.d.ts +23 -0
  1077. package/dist/security/client-ip.d.ts.map +1 -0
  1078. package/dist/security/client-ip.js +97 -0
  1079. package/dist/security/client-ip.js.map +1 -0
  1080. package/dist/security/cors.d.ts +11 -0
  1081. package/dist/security/cors.d.ts.map +1 -0
  1082. package/dist/security/cors.js +33 -0
  1083. package/dist/security/cors.js.map +1 -0
  1084. package/dist/security/csp-nonces.d.ts +5 -0
  1085. package/dist/security/csp-nonces.d.ts.map +1 -0
  1086. package/dist/security/csp-nonces.js +24 -0
  1087. package/dist/security/csp-nonces.js.map +1 -0
  1088. package/dist/security/csrf.d.ts +5 -0
  1089. package/dist/security/csrf.d.ts.map +1 -0
  1090. package/dist/security/csrf.js +20 -0
  1091. package/dist/security/csrf.js.map +1 -0
  1092. package/dist/security/encrypted-fields.d.ts +14 -0
  1093. package/dist/security/encrypted-fields.d.ts.map +1 -0
  1094. package/dist/security/encrypted-fields.js +94 -0
  1095. package/dist/security/encrypted-fields.js.map +1 -0
  1096. package/dist/security/headers.d.ts +11 -0
  1097. package/dist/security/headers.d.ts.map +1 -0
  1098. package/dist/security/headers.js +32 -0
  1099. package/dist/security/headers.js.map +1 -0
  1100. package/dist/security/index.d.ts +40 -0
  1101. package/dist/security/index.d.ts.map +1 -0
  1102. package/dist/security/index.js +26 -0
  1103. package/dist/security/index.js.map +1 -0
  1104. package/dist/security/internal-keys.d.ts +15 -0
  1105. package/dist/security/internal-keys.d.ts.map +1 -0
  1106. package/dist/security/internal-keys.js +33 -0
  1107. package/dist/security/internal-keys.js.map +1 -0
  1108. package/dist/security/ip-allowlist.d.ts +15 -0
  1109. package/dist/security/ip-allowlist.d.ts.map +1 -0
  1110. package/dist/security/ip-allowlist.js +141 -0
  1111. package/dist/security/ip-allowlist.js.map +1 -0
  1112. package/dist/security/ip-canon.d.ts +71 -0
  1113. package/dist/security/ip-canon.d.ts.map +1 -0
  1114. package/dist/security/ip-canon.js +352 -0
  1115. package/dist/security/ip-canon.js.map +1 -0
  1116. package/dist/security/middleware.d.ts +20 -0
  1117. package/dist/security/middleware.d.ts.map +1 -0
  1118. package/dist/security/middleware.js +45 -0
  1119. package/dist/security/middleware.js.map +1 -0
  1120. package/dist/security/rate-limit-policy.d.ts +40 -0
  1121. package/dist/security/rate-limit-policy.d.ts.map +1 -0
  1122. package/dist/security/rate-limit-policy.js +57 -0
  1123. package/dist/security/rate-limit-policy.js.map +1 -0
  1124. package/dist/security/rate-limit.d.ts +55 -0
  1125. package/dist/security/rate-limit.d.ts.map +1 -0
  1126. package/dist/security/rate-limit.js +298 -0
  1127. package/dist/security/rate-limit.js.map +1 -0
  1128. package/dist/security/reauth.d.ts +16 -0
  1129. package/dist/security/reauth.d.ts.map +1 -0
  1130. package/dist/security/reauth.js +56 -0
  1131. package/dist/security/reauth.js.map +1 -0
  1132. package/dist/security/redact.d.ts +12 -0
  1133. package/dist/security/redact.d.ts.map +1 -0
  1134. package/dist/security/redact.js +44 -0
  1135. package/dist/security/redact.js.map +1 -0
  1136. package/dist/security/safe-fetch.d.ts +57 -0
  1137. package/dist/security/safe-fetch.d.ts.map +1 -0
  1138. package/dist/security/safe-fetch.js +71 -0
  1139. package/dist/security/safe-fetch.js.map +1 -0
  1140. package/dist/security/sanitize-document.d.ts +14 -0
  1141. package/dist/security/sanitize-document.d.ts.map +1 -0
  1142. package/dist/security/sanitize-document.js +117 -0
  1143. package/dist/security/sanitize-document.js.map +1 -0
  1144. package/dist/security/sanitize.d.ts +13 -0
  1145. package/dist/security/sanitize.d.ts.map +1 -0
  1146. package/dist/security/sanitize.js +64 -0
  1147. package/dist/security/sanitize.js.map +1 -0
  1148. package/dist/security/secret-storage.d.ts +22 -0
  1149. package/dist/security/secret-storage.d.ts.map +1 -0
  1150. package/dist/security/secret-storage.js +75 -0
  1151. package/dist/security/secret-storage.js.map +1 -0
  1152. package/dist/security/security-txt.d.ts +12 -0
  1153. package/dist/security/security-txt.d.ts.map +1 -0
  1154. package/dist/security/security-txt.js +19 -0
  1155. package/dist/security/security-txt.js.map +1 -0
  1156. package/dist/security/session-limits.d.ts +17 -0
  1157. package/dist/security/session-limits.d.ts.map +1 -0
  1158. package/dist/security/session-limits.js +14 -0
  1159. package/dist/security/session-limits.js.map +1 -0
  1160. package/dist/security/upload.d.ts +32 -0
  1161. package/dist/security/upload.d.ts.map +1 -0
  1162. package/dist/security/upload.js +129 -0
  1163. package/dist/security/upload.js.map +1 -0
  1164. package/dist/security/webhook.d.ts +30 -0
  1165. package/dist/security/webhook.d.ts.map +1 -0
  1166. package/dist/security/webhook.js +133 -0
  1167. package/dist/security/webhook.js.map +1 -0
  1168. package/dist/seo/ai-bots.d.ts +56 -0
  1169. package/dist/seo/ai-bots.d.ts.map +1 -0
  1170. package/dist/seo/ai-bots.js +125 -0
  1171. package/dist/seo/ai-bots.js.map +1 -0
  1172. package/dist/seo/ai-crawler-log.d.ts +14 -0
  1173. package/dist/seo/ai-crawler-log.d.ts.map +1 -0
  1174. package/dist/seo/ai-crawler-log.js +48 -0
  1175. package/dist/seo/ai-crawler-log.js.map +1 -0
  1176. package/dist/seo/alt-text.d.ts +39 -0
  1177. package/dist/seo/alt-text.d.ts.map +1 -0
  1178. package/dist/seo/alt-text.js +67 -0
  1179. package/dist/seo/alt-text.js.map +1 -0
  1180. package/dist/seo/analysis.d.ts +75 -0
  1181. package/dist/seo/analysis.d.ts.map +1 -0
  1182. package/dist/seo/analysis.js +641 -0
  1183. package/dist/seo/analysis.js.map +1 -0
  1184. package/dist/seo/audit-engine.d.ts +124 -0
  1185. package/dist/seo/audit-engine.d.ts.map +1 -0
  1186. package/dist/seo/audit-engine.js +534 -0
  1187. package/dist/seo/audit-engine.js.map +1 -0
  1188. package/dist/seo/audit-runner.d.ts +84 -0
  1189. package/dist/seo/audit-runner.d.ts.map +1 -0
  1190. package/dist/seo/audit-runner.js +372 -0
  1191. package/dist/seo/audit-runner.js.map +1 -0
  1192. package/dist/seo/audit-signals.d.ts +28 -0
  1193. package/dist/seo/audit-signals.d.ts.map +1 -0
  1194. package/dist/seo/audit-signals.js +133 -0
  1195. package/dist/seo/audit-signals.js.map +1 -0
  1196. package/dist/seo/config-store.d.ts +99 -0
  1197. package/dist/seo/config-store.d.ts.map +1 -0
  1198. package/dist/seo/config-store.js +194 -0
  1199. package/dist/seo/config-store.js.map +1 -0
  1200. package/dist/seo/confirm-broken-links.d.ts +48 -0
  1201. package/dist/seo/confirm-broken-links.d.ts.map +1 -0
  1202. package/dist/seo/confirm-broken-links.js +135 -0
  1203. package/dist/seo/confirm-broken-links.js.map +1 -0
  1204. package/dist/seo/content-markdown.d.ts +26 -0
  1205. package/dist/seo/content-markdown.d.ts.map +1 -0
  1206. package/dist/seo/content-markdown.js +235 -0
  1207. package/dist/seo/content-markdown.js.map +1 -0
  1208. package/dist/seo/document-content.d.ts +18 -0
  1209. package/dist/seo/document-content.d.ts.map +1 -0
  1210. package/dist/seo/document-content.js +80 -0
  1211. package/dist/seo/document-content.js.map +1 -0
  1212. package/dist/seo/document-path.d.ts +33 -0
  1213. package/dist/seo/document-path.d.ts.map +1 -0
  1214. package/dist/seo/document-path.js +44 -0
  1215. package/dist/seo/document-path.js.map +1 -0
  1216. package/dist/seo/feeds.d.ts +44 -0
  1217. package/dist/seo/feeds.d.ts.map +1 -0
  1218. package/dist/seo/feeds.js +95 -0
  1219. package/dist/seo/feeds.js.map +1 -0
  1220. package/dist/seo/fix-brief-export.d.ts +47 -0
  1221. package/dist/seo/fix-brief-export.d.ts.map +1 -0
  1222. package/dist/seo/fix-brief-export.js +224 -0
  1223. package/dist/seo/fix-brief-export.js.map +1 -0
  1224. package/dist/seo/index-status.d.ts +56 -0
  1225. package/dist/seo/index-status.d.ts.map +1 -0
  1226. package/dist/seo/index-status.js +193 -0
  1227. package/dist/seo/index-status.js.map +1 -0
  1228. package/dist/seo/index.d.ts +68 -0
  1229. package/dist/seo/index.d.ts.map +1 -0
  1230. package/dist/seo/index.js +36 -0
  1231. package/dist/seo/index.js.map +1 -0
  1232. package/dist/seo/issue-batch-briefs.d.ts +37 -0
  1233. package/dist/seo/issue-batch-briefs.d.ts.map +1 -0
  1234. package/dist/seo/issue-batch-briefs.js +85 -0
  1235. package/dist/seo/issue-batch-briefs.js.map +1 -0
  1236. package/dist/seo/issue-content-brief.d.ts +51 -0
  1237. package/dist/seo/issue-content-brief.d.ts.map +1 -0
  1238. package/dist/seo/issue-content-brief.js +267 -0
  1239. package/dist/seo/issue-content-brief.js.map +1 -0
  1240. package/dist/seo/issue-content-research.d.ts +51 -0
  1241. package/dist/seo/issue-content-research.d.ts.map +1 -0
  1242. package/dist/seo/issue-content-research.js +303 -0
  1243. package/dist/seo/issue-content-research.js.map +1 -0
  1244. package/dist/seo/issue-fix-ai.d.ts +39 -0
  1245. package/dist/seo/issue-fix-ai.d.ts.map +1 -0
  1246. package/dist/seo/issue-fix-ai.js +320 -0
  1247. package/dist/seo/issue-fix-ai.js.map +1 -0
  1248. package/dist/seo/issue-fix-redirect.d.ts +18 -0
  1249. package/dist/seo/issue-fix-redirect.d.ts.map +1 -0
  1250. package/dist/seo/issue-fix-redirect.js +153 -0
  1251. package/dist/seo/issue-fix-redirect.js.map +1 -0
  1252. package/dist/seo/issue-fix.d.ts +72 -0
  1253. package/dist/seo/issue-fix.d.ts.map +1 -0
  1254. package/dist/seo/issue-fix.js +266 -0
  1255. package/dist/seo/issue-fix.js.map +1 -0
  1256. package/dist/seo/link-graph.d.ts +54 -0
  1257. package/dist/seo/link-graph.d.ts.map +1 -0
  1258. package/dist/seo/link-graph.js +0 -0
  1259. package/dist/seo/link-graph.js.map +1 -0
  1260. package/dist/seo/link-probe.d.ts +49 -0
  1261. package/dist/seo/link-probe.d.ts.map +1 -0
  1262. package/dist/seo/link-probe.js +147 -0
  1263. package/dist/seo/link-probe.js.map +1 -0
  1264. package/dist/seo/llms-select.d.ts +34 -0
  1265. package/dist/seo/llms-select.d.ts.map +1 -0
  1266. package/dist/seo/llms-select.js +67 -0
  1267. package/dist/seo/llms-select.js.map +1 -0
  1268. package/dist/seo/llms-txt.d.ts +40 -0
  1269. package/dist/seo/llms-txt.d.ts.map +1 -0
  1270. package/dist/seo/llms-txt.js +177 -0
  1271. package/dist/seo/llms-txt.js.map +1 -0
  1272. package/dist/seo/meta-tags.d.ts +108 -0
  1273. package/dist/seo/meta-tags.d.ts.map +1 -0
  1274. package/dist/seo/meta-tags.js +353 -0
  1275. package/dist/seo/meta-tags.js.map +1 -0
  1276. package/dist/seo/migrate-meta.d.ts +23 -0
  1277. package/dist/seo/migrate-meta.d.ts.map +1 -0
  1278. package/dist/seo/migrate-meta.js +114 -0
  1279. package/dist/seo/migrate-meta.js.map +1 -0
  1280. package/dist/seo/migrate-schema.d.ts +28 -0
  1281. package/dist/seo/migrate-schema.d.ts.map +1 -0
  1282. package/dist/seo/migrate-schema.js +119 -0
  1283. package/dist/seo/migrate-schema.js.map +1 -0
  1284. package/dist/seo/page-meta.d.ts +179 -0
  1285. package/dist/seo/page-meta.d.ts.map +1 -0
  1286. package/dist/seo/page-meta.js +710 -0
  1287. package/dist/seo/page-meta.js.map +1 -0
  1288. package/dist/seo/public-site-url.d.ts +16 -0
  1289. package/dist/seo/public-site-url.d.ts.map +1 -0
  1290. package/dist/seo/public-site-url.js +39 -0
  1291. package/dist/seo/public-site-url.js.map +1 -0
  1292. package/dist/seo/resolve-path.d.ts +22 -0
  1293. package/dist/seo/resolve-path.d.ts.map +1 -0
  1294. package/dist/seo/resolve-path.js +53 -0
  1295. package/dist/seo/resolve-path.js.map +1 -0
  1296. package/dist/seo/rewrite-public-origin.d.ts +12 -0
  1297. package/dist/seo/rewrite-public-origin.d.ts.map +1 -0
  1298. package/dist/seo/rewrite-public-origin.js +17 -0
  1299. package/dist/seo/rewrite-public-origin.js.map +1 -0
  1300. package/dist/seo/robots.d.ts +45 -0
  1301. package/dist/seo/robots.d.ts.map +1 -0
  1302. package/dist/seo/robots.js +90 -0
  1303. package/dist/seo/robots.js.map +1 -0
  1304. package/dist/seo/schema-sync.d.ts +65 -0
  1305. package/dist/seo/schema-sync.d.ts.map +1 -0
  1306. package/dist/seo/schema-sync.js +163 -0
  1307. package/dist/seo/schema-sync.js.map +1 -0
  1308. package/dist/seo/score.d.ts +132 -0
  1309. package/dist/seo/score.d.ts.map +1 -0
  1310. package/dist/seo/score.js +216 -0
  1311. package/dist/seo/score.js.map +1 -0
  1312. package/dist/seo/serving-origin.d.ts +42 -0
  1313. package/dist/seo/serving-origin.d.ts.map +1 -0
  1314. package/dist/seo/serving-origin.js +83 -0
  1315. package/dist/seo/serving-origin.js.map +1 -0
  1316. package/dist/seo/site-identity.d.ts +60 -0
  1317. package/dist/seo/site-identity.d.ts.map +1 -0
  1318. package/dist/seo/site-identity.js +109 -0
  1319. package/dist/seo/site-identity.js.map +1 -0
  1320. package/dist/seo/sitemap-notify.d.ts +123 -0
  1321. package/dist/seo/sitemap-notify.d.ts.map +1 -0
  1322. package/dist/seo/sitemap-notify.js +270 -0
  1323. package/dist/seo/sitemap-notify.js.map +1 -0
  1324. package/dist/seo/sitemap-public.d.ts +43 -0
  1325. package/dist/seo/sitemap-public.d.ts.map +1 -0
  1326. package/dist/seo/sitemap-public.js +213 -0
  1327. package/dist/seo/sitemap-public.js.map +1 -0
  1328. package/dist/seo/title-templates.d.ts +80 -0
  1329. package/dist/seo/title-templates.d.ts.map +1 -0
  1330. package/dist/seo/title-templates.js +175 -0
  1331. package/dist/seo/title-templates.js.map +1 -0
  1332. package/dist/server-site.d.ts +55 -0
  1333. package/dist/server-site.d.ts.map +1 -0
  1334. package/dist/server-site.js +184 -0
  1335. package/dist/server-site.js.map +1 -0
  1336. package/dist/setup/index.d.ts +68 -0
  1337. package/dist/setup/index.d.ts.map +1 -0
  1338. package/dist/setup/index.js +153 -0
  1339. package/dist/setup/index.js.map +1 -0
  1340. package/dist/site.d.ts +178 -0
  1341. package/dist/site.d.ts.map +1 -0
  1342. package/dist/site.js +222 -0
  1343. package/dist/site.js.map +1 -0
  1344. package/dist/storage/auto-wire.d.ts +14 -0
  1345. package/dist/storage/auto-wire.d.ts.map +1 -0
  1346. package/dist/storage/auto-wire.js +60 -0
  1347. package/dist/storage/auto-wire.js.map +1 -0
  1348. package/dist/storage/index.d.ts +21 -0
  1349. package/dist/storage/index.d.ts.map +1 -0
  1350. package/dist/storage/index.js +14 -0
  1351. package/dist/storage/index.js.map +1 -0
  1352. package/dist/templates/index.d.ts +16 -0
  1353. package/dist/templates/index.d.ts.map +1 -0
  1354. package/dist/templates/index.js +23 -0
  1355. package/dist/templates/index.js.map +1 -0
  1356. package/dist/upgrade/changelog.d.ts +18 -0
  1357. package/dist/upgrade/changelog.d.ts.map +1 -0
  1358. package/dist/upgrade/changelog.js +60 -0
  1359. package/dist/upgrade/changelog.js.map +1 -0
  1360. package/dist/upgrade/index.d.ts +7 -0
  1361. package/dist/upgrade/index.d.ts.map +1 -0
  1362. package/dist/upgrade/index.js +4 -0
  1363. package/dist/upgrade/index.js.map +1 -0
  1364. package/dist/upgrade/upgrade-pr.d.ts +16 -0
  1365. package/dist/upgrade/upgrade-pr.d.ts.map +1 -0
  1366. package/dist/upgrade/upgrade-pr.js +128 -0
  1367. package/dist/upgrade/upgrade-pr.js.map +1 -0
  1368. package/dist/upgrade/version-check.d.ts +59 -0
  1369. package/dist/upgrade/version-check.d.ts.map +1 -0
  1370. package/dist/upgrade/version-check.js +142 -0
  1371. package/dist/upgrade/version-check.js.map +1 -0
  1372. package/dist/validation/publish.d.ts +53 -0
  1373. package/dist/validation/publish.d.ts.map +1 -0
  1374. package/dist/validation/publish.js +75 -0
  1375. package/dist/validation/publish.js.map +1 -0
  1376. package/dist/webhooks/index.d.ts +99 -0
  1377. package/dist/webhooks/index.d.ts.map +1 -0
  1378. package/dist/webhooks/index.js +387 -0
  1379. package/dist/webhooks/index.js.map +1 -0
  1380. package/dist/webhooks/signing.d.ts +68 -0
  1381. package/dist/webhooks/signing.d.ts.map +1 -0
  1382. package/dist/webhooks/signing.js +98 -0
  1383. package/dist/webhooks/signing.js.map +1 -0
  1384. package/dist/workflow/index.d.ts +8 -0
  1385. package/dist/workflow/index.d.ts.map +1 -0
  1386. package/dist/workflow/index.js +56 -0
  1387. package/dist/workflow/index.js.map +1 -0
  1388. package/dist/workflows/index.d.ts +30 -0
  1389. package/dist/workflows/index.d.ts.map +1 -0
  1390. package/dist/workflows/index.js +14 -0
  1391. package/dist/workflows/index.js.map +1 -0
  1392. package/package.json +250 -0
  1393. package/prisma/migrations/0001_init/migration.sql +941 -0
  1394. package/prisma/migrations/0002_media_usage_field_path/migration.sql +11 -0
  1395. package/prisma/migrations/0003_ai_crawler_hits/migration.sql +21 -0
  1396. package/prisma/migrations/0004_drop_search_vector/migration.sql +4 -0
  1397. package/prisma/migrations/0005_script_tag_delivery_controls/migration.sql +8 -0
  1398. package/prisma/migrations/0006_totp_last_step/migration.sql +4 -0
  1399. package/prisma/migrations/0007_workflow_fk_drop_dead_enums/migration.sql +27 -0
  1400. package/prisma/migrations/0008_form_delivery_attempts/migration.sql +29 -0
  1401. package/prisma/migrations/0009_webhook_secret_rotation/migration.sql +6 -0
  1402. package/prisma/migrations/0010_media_is_decorative/migration.sql +3 -0
  1403. package/prisma/migrations/0011_media_content_hash/migration.sql +4 -0
  1404. package/prisma/migrations/0012_rename_to_prenta/migration.sql +89 -0
  1405. package/prisma/migrations/migration_lock.toml +3 -0
  1406. package/prisma/schema.prisma +995 -0
  1407. package/prisma/seed.ts +82 -0
  1408. package/schema-fragments/cms-schema.prisma +526 -0
@@ -0,0 +1,2256 @@
1
+ import { updateDocumentSeoFields } from '../../actions.js';
2
+ import { logEvent } from '../../security/audit.js';
3
+ import { processSeoScan } from '../../cron/index.js';
4
+ import { isResolvedIp } from '../../security/client-ip.js';
5
+ import { safeFetch, SsrfBlockedError } from '../../security/safe-fetch.js';
6
+ import { extractOutboundUrlsFromDocumentData, isLinkProbeIssueStatus, LINK_PROBE_HEADERS, } from '../../seo/link-probe.js';
7
+ import { getPrentaConfig } from '../../config/runtime.js';
8
+ import { assertPlanFeature, hasPlanFeature } from '../../plan/features.js';
9
+ import { syncSchemaTypeFields } from '../../seo/schema-sync.js';
10
+ import { guardedDb } from '../guarded-db.js';
11
+ import { recordAiCrawlerFromRequest, recordAiCrawlerHit } from '../../seo/ai-crawler-log.js';
12
+ import { detectAiBot } from '../../seo/ai-bots.js';
13
+ import { json, errorResponse, internalError, asRecord, normalizeSeoPage, hasModel, formatCompactNumber, modelNotAvailable, requireAuth, buildActionContext, WRITE_ROLES, ADMIN_ROLES, requireRole, linkHealthLimiter, seoAuditLimiter, aiGenerateLimiter, checkRateLimitAsync, clientIp, enforceDocumentReadAccess, importAIPlugin, } from '../route-helpers.js';
14
+ // Explicit risk order. A plain `severity: 'asc'` DB sort orders the values
15
+ // alphabetically (critical, info, warning) which wrongly ranks `info` above
16
+ // `warning`. Unknown severities sort last.
17
+ const SEVERITY_RANK = { critical: 0, warning: 1, info: 2 };
18
+ function severityRank(severity) {
19
+ return SEVERITY_RANK[String(severity)] ?? 99;
20
+ }
21
+ /**
22
+ * Collect internal link targets from global navigation menus (the `navigations`
23
+ * collection) so the link-graph can treat nav-linked pages as non-orphaned.
24
+ * Best-effort: returns `[]` when the navigation plugin isn't installed or on any
25
+ * read error.
26
+ */
27
+ async function gatherNavigationLinkTargets(db) {
28
+ try {
29
+ if (!hasModel(db, 'document'))
30
+ return [];
31
+ const { coercePublicNavItems } = await import('../../site.js');
32
+ const navDocs = await db.document
33
+ .findMany({
34
+ where: { collection: 'navigations', deletedAt: null, status: { not: 'ARCHIVED' } },
35
+ select: { data: true },
36
+ take: 50,
37
+ })
38
+ .catch(() => []);
39
+ const targets = [];
40
+ const walk = (items) => {
41
+ for (const item of items) {
42
+ if (typeof item.url === 'string' && item.url.trim())
43
+ targets.push(item.url);
44
+ if (item.children)
45
+ walk(item.children);
46
+ }
47
+ };
48
+ for (const doc of navDocs) {
49
+ const data = doc?.data && typeof doc.data === 'object' ? doc.data : {};
50
+ walk(coercePublicNavItems(data.items));
51
+ }
52
+ return targets;
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ function planRequiredResponse(denial) {
59
+ return json({
60
+ error: denial.message,
61
+ code: 'plan_required',
62
+ requiredTier: denial.requiredTier,
63
+ feature: denial.feature,
64
+ }, 403);
65
+ }
66
+ function seoAiGenerationError(result) {
67
+ if (result.code === 'unavailable' || result.code === 'feature_disabled') {
68
+ return errorResponse(result.message, 503);
69
+ }
70
+ if (result.code === 'rate_limited' || result.code === 'budget_exceeded') {
71
+ return errorResponse(result.message, 429);
72
+ }
73
+ return errorResponse(result.message, 422);
74
+ }
75
+ /** Sort issues by risk (critical → warning → info), then most-recent first. */
76
+ export function sortIssuesBySeverity(issues) {
77
+ return [...issues].sort((a, b) => {
78
+ const rankDiff = severityRank(a.severity) - severityRank(b.severity);
79
+ if (rankDiff !== 0)
80
+ return rankDiff;
81
+ const at = a.lastDetectedAt ? new Date(a.lastDetectedAt).getTime() : 0;
82
+ const bt = b.lastDetectedAt ? new Date(b.lastDetectedAt).getTime() : 0;
83
+ return bt - at;
84
+ });
85
+ }
86
+ export function registerSeoRoutes(router) {
87
+ const db = guardedDb;
88
+ // ---------------------------------------------------------------------------
89
+ // SEO routes
90
+ // ---------------------------------------------------------------------------
91
+ router.get('/seo/pages', async (request) => {
92
+ try {
93
+ const auth = await requireAuth(request);
94
+ if (auth.error)
95
+ return auth.error;
96
+ // Bound the scan: a large site can have tens of thousands of published
97
+ // docs, and each carries a full `data` JSONB. Default to a high cap so
98
+ // typical sites are unaffected; expose `?limit`/`?offset` so clients can
99
+ // page rather than pull everything into memory at once. A full page
100
+ // (data.length === limit) signals there may be more.
101
+ const params = new URL(request.url).searchParams;
102
+ const limit = Math.min(Math.max(Number(params.get('limit')) || 1000, 1), 5000);
103
+ const offset = Math.max(Number(params.get('offset')) || 0, 0);
104
+ const pages = await db().document.findMany({
105
+ where: { deletedAt: null, status: 'PUBLISHED' },
106
+ select: {
107
+ id: true,
108
+ collection: true,
109
+ data: true,
110
+ updatedAt: true,
111
+ structuredData: true,
112
+ },
113
+ orderBy: { updatedAt: 'desc' },
114
+ take: limit,
115
+ skip: offset,
116
+ });
117
+ return json({ data: pages.map(normalizeSeoPage), limit, offset });
118
+ }
119
+ catch (err) {
120
+ return internalError(err);
121
+ }
122
+ });
123
+ router.get('/seo/link-health', async (request) => {
124
+ try {
125
+ const auth = await requireAuth(request);
126
+ if (auth.error)
127
+ return auth.error;
128
+ // EDITOR+ only — this endpoint hits arbitrary URLs and can be expensive.
129
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
130
+ if (roleErr)
131
+ return roleErr;
132
+ // Tight rate limit because each call fans out to many outbound requests.
133
+ const ip = clientIp(request);
134
+ const rateKey = isResolvedIp(ip)
135
+ ? `link-health:${ip}`
136
+ : `link-health-user:${auth.session.userId}`;
137
+ if (!(await checkRateLimitAsync(linkHealthLimiter, rateKey))) {
138
+ return errorResponse('Too many link-health scans. Please try again later.', 429);
139
+ }
140
+ const MAX_LINKS_PER_PAGE = 50;
141
+ const MAX_TOTAL_LINKS = 500;
142
+ const DOC_BATCH_SIZE = 100;
143
+ const PER_LINK_TIMEOUT_MS = 4000;
144
+ const CONCURRENCY = 8;
145
+ const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL ?? '').replace(/\/+$/, '');
146
+ const queue = [];
147
+ const seenGlobal = new Set();
148
+ let docsScanned = 0;
149
+ let skip = 0;
150
+ scanLoop: while (queue.length < MAX_TOTAL_LINKS) {
151
+ const batch = await db().document.findMany({
152
+ where: { deletedAt: null, status: 'PUBLISHED' },
153
+ select: { id: true, title: true, data: true, collection: true },
154
+ orderBy: { updatedAt: 'desc' },
155
+ skip,
156
+ take: DOC_BATCH_SIZE,
157
+ });
158
+ if (batch.length === 0)
159
+ break;
160
+ docsScanned += batch.length;
161
+ skip += batch.length;
162
+ for (const doc of batch) {
163
+ const pageTitle = doc.title ?? doc.data?.title ?? doc.id;
164
+ // Walk unescaped string fields — never JSON.stringify (captures `\`
165
+ // from `\"` and produces mass false positives).
166
+ const urls = extractOutboundUrlsFromDocumentData(doc.data ?? {});
167
+ let countInPage = 0;
168
+ for (const clean of urls) {
169
+ if (seenGlobal.has(clean))
170
+ continue;
171
+ seenGlobal.add(clean);
172
+ if (countInPage >= MAX_LINKS_PER_PAGE)
173
+ break;
174
+ if (queue.length >= MAX_TOTAL_LINKS)
175
+ break scanLoop;
176
+ const isInternal = !!siteUrl && clean.startsWith(siteUrl);
177
+ queue.push({ docId: doc.id, pageTitle, clean, isInternal });
178
+ countInPage++;
179
+ }
180
+ }
181
+ }
182
+ const linkResults = [];
183
+ let cursor = 0;
184
+ const worker = async () => {
185
+ while (cursor < queue.length) {
186
+ const idx = cursor++;
187
+ const job = queue[idx];
188
+ let status = 0;
189
+ try {
190
+ // GET + browser UA: many hosts reject HEAD or non-browser agents
191
+ // (403/405/500) while the same URL is fine in a browser.
192
+ // safeFetch rejects private/loopback IPs and disables redirect
193
+ // following so 302->internal can't smuggle the scanner past SSRF.
194
+ const resp = await safeFetch(job.clean, {
195
+ method: 'GET',
196
+ headers: { ...LINK_PROBE_HEADERS },
197
+ timeoutMs: PER_LINK_TIMEOUT_MS,
198
+ });
199
+ status = resp.status;
200
+ // Cancel the body — we only need the status line.
201
+ try {
202
+ await resp.body?.cancel();
203
+ }
204
+ catch {
205
+ /* noop */
206
+ }
207
+ }
208
+ catch (err) {
209
+ status = err instanceof SsrfBlockedError ? -1 : 0;
210
+ }
211
+ // 2xx/3xx = healthy; 403/405/429/999 = unverifiable (not issues).
212
+ if (isLinkProbeIssueStatus(status)) {
213
+ linkResults.push({
214
+ id: `${job.docId}-${idx}`,
215
+ page: job.pageTitle,
216
+ url: job.clean,
217
+ status,
218
+ type: job.isInternal ? 'internal' : 'external',
219
+ });
220
+ }
221
+ }
222
+ };
223
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, queue.length) }, worker));
224
+ return json({
225
+ data: {
226
+ truncated: queue.length >= MAX_TOTAL_LINKS,
227
+ docsScanned,
228
+ checked: queue.length,
229
+ issues: linkResults,
230
+ },
231
+ });
232
+ }
233
+ catch (err) {
234
+ return internalError(err);
235
+ }
236
+ });
237
+ router.get('/seo/links/snapshot', async (request) => {
238
+ try {
239
+ const auth = await requireAuth(request);
240
+ if (auth.error)
241
+ return auth.error;
242
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
243
+ if (roleErr)
244
+ return roleErr;
245
+ const { gatherAuditEntities, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
246
+ const { buildLinkGraphSnapshot } = await import('../../seo/link-graph.js');
247
+ const { getSeoOverrides, resolveSeoModuleSettings } = await import('../../seo/config-store.js');
248
+ const cfg = getPrentaConfig();
249
+ const collectionTypes = frontEndContentCollectionTypes(cfg);
250
+ const siteUrl = cfg?.seo?.siteUrl?.replace(/\/+$/, '') ?? '';
251
+ const entities = await gatherAuditEntities(db(), { collectionTypes });
252
+ const docRows = await db()
253
+ .document.findMany({
254
+ where: {
255
+ deletedAt: null,
256
+ status: 'PUBLISHED',
257
+ collection: { in: Object.keys(collectionTypes) },
258
+ },
259
+ select: { id: true, collection: true },
260
+ })
261
+ .catch(() => []);
262
+ const collections = Object.fromEntries(docRows.map((d) => [d.id, d.collection]));
263
+ let brokenLinkCount = 0;
264
+ if (hasModel(db(), 'seoIssue')) {
265
+ brokenLinkCount = await db()
266
+ .seoIssue.count({
267
+ where: {
268
+ status: { in: ['open', 'recurring'] },
269
+ type: 'broken-internal-link',
270
+ },
271
+ })
272
+ .catch(() => 0);
273
+ }
274
+ const overrides = await getSeoOverrides(db()).catch(() => null);
275
+ const moduleSettings = resolveSeoModuleSettings(overrides);
276
+ // Global nav/footer menus link to top-level pages from site chrome, not
277
+ // from document bodies. Feed those targets in so nav-only reachable pages
278
+ // are not mis-reported as orphaned. Best-effort: no-op when the navigation
279
+ // plugin isn't installed.
280
+ const navLinks = await gatherNavigationLinkTargets(db());
281
+ const snapshot = buildLinkGraphSnapshot({
282
+ entities,
283
+ siteUrl,
284
+ collections,
285
+ brokenLinkCount,
286
+ aiIndexLastRunAt: moduleSettings.linkAiIndexLastRunAt,
287
+ navLinks,
288
+ });
289
+ return json({ data: snapshot });
290
+ }
291
+ catch (err) {
292
+ return internalError(err, 'seo/links/snapshot');
293
+ }
294
+ });
295
+ router.post('/seo/links/ai-index', async (request) => {
296
+ try {
297
+ const auth = await requireAuth(request);
298
+ if (auth.error)
299
+ return auth.error;
300
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
301
+ if (roleErr)
302
+ return roleErr;
303
+ if (!(await checkRateLimitAsync(aiGenerateLimiter, `link-ai-index:${auth.session.userId}`))) {
304
+ return errorResponse('AI link index rate limit reached. Try again later.', 429);
305
+ }
306
+ const { gatherAuditEntities, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
307
+ const { patchSeoModuleSettings } = await import('../../seo/config-store.js');
308
+ const cfg = getPrentaConfig();
309
+ const collectionTypes = frontEndContentCollectionTypes(cfg);
310
+ const entities = await gatherAuditEntities(db(), { collectionTypes });
311
+ let aiAvailable = false;
312
+ try {
313
+ await importAIPlugin();
314
+ aiAvailable = true;
315
+ }
316
+ catch {
317
+ aiAvailable = false;
318
+ }
319
+ const now = new Date().toISOString();
320
+ await patchSeoModuleSettings(db(), { linkAiIndexLastRunAt: now }, auth.session.userId);
321
+ await logEvent({
322
+ event: 'seo_link_ai_index',
323
+ userId: auth.session.userId,
324
+ details: { entitiesIndexed: entities.length, aiAvailable },
325
+ });
326
+ return json({
327
+ data: {
328
+ indexedAt: now,
329
+ entitiesIndexed: entities.length,
330
+ aiAvailable,
331
+ // No standing index/embeddings are persisted here — /seo/links/suggest
332
+ // rebuilds the corpus on demand and works with or without this run.
333
+ // Keep the copy honest: this refreshes the link corpus and records the
334
+ // run; it does not "build" anything suggestions depend on.
335
+ message: aiAvailable
336
+ ? 'Link corpus refreshed. AI link suggestions are generated on demand — run them on orphaned or under-linked pages.'
337
+ : 'Link corpus refreshed. Install @prenta/plugin-ai to enable semantic AI link suggestions.',
338
+ },
339
+ });
340
+ }
341
+ catch (err) {
342
+ return internalError(err, 'seo/links/ai-index');
343
+ }
344
+ });
345
+ router.post('/seo/links/suggest', async (request) => {
346
+ try {
347
+ const auth = await requireAuth(request);
348
+ if (auth.error)
349
+ return auth.error;
350
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
351
+ if (roleErr)
352
+ return roleErr;
353
+ if (!(await checkRateLimitAsync(aiGenerateLimiter, `link-suggest:${auth.session.userId}`))) {
354
+ return errorResponse('Link suggestion rate limit reached. Try again later.', 429);
355
+ }
356
+ const body = (await request.json());
357
+ if (!body.targetEntityId || !body.targetEntityType) {
358
+ return errorResponse('targetEntityId and targetEntityType are required', 400);
359
+ }
360
+ const limit = Math.max(1, Math.min(20, typeof body.limit === 'number' ? body.limit : 8));
361
+ const { gatherAuditEntities, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
362
+ const cfg = getPrentaConfig();
363
+ const collectionTypes = frontEndContentCollectionTypes(cfg);
364
+ const entities = await gatherAuditEntities(db(), { collectionTypes });
365
+ const target = entities.find((e) => e.entityId === body.targetEntityId);
366
+ if (!target)
367
+ return errorResponse('Target document not found in link graph', 404);
368
+ const sources = entities.filter((e) => e.entityId !== target.entityId);
369
+ const query = [target.title, target.focusKeyword, target.metaDescription]
370
+ .filter(Boolean)
371
+ .join(' — ');
372
+ const docRows = await db()
373
+ .document.findMany({
374
+ where: { id: { in: sources.map((s) => s.entityId) } },
375
+ select: { id: true, collection: true },
376
+ })
377
+ .catch(() => []);
378
+ const collections = Object.fromEntries(docRows.map((d) => [d.id, d.collection]));
379
+ let suggestions = [];
380
+ try {
381
+ const aiModule = await importAIPlugin();
382
+ if (typeof aiModule?.semanticSearch === 'function') {
383
+ const corpus = sources.map((s) => ({
384
+ id: s.entityId,
385
+ collectionSlug: collections[s.entityId] ?? 'pages',
386
+ title: s.title,
387
+ content: (s.content ?? '').slice(0, 8000),
388
+ }));
389
+ const results = await aiModule.semanticSearch(query, { limit, threshold: 0.35, corpus });
390
+ suggestions = results.map((r) => ({
391
+ sourceEntityId: r.id,
392
+ sourceTitle: r.title,
393
+ sourceUrl: sources.find((s) => s.entityId === r.id)?.url ?? '',
394
+ anchorText: target.title,
395
+ confidence: Math.round(r.score * 100) / 100,
396
+ excerpt: r.excerpt,
397
+ }));
398
+ }
399
+ else if (typeof aiModule?.suggestInternalLinks === 'function') {
400
+ for (const source of sources.slice(0, 40)) {
401
+ const found = await aiModule.suggestInternalLinks(source.content ?? '', [
402
+ {
403
+ id: target.entityId,
404
+ title: target.title,
405
+ url: target.url,
406
+ keywords: target.focusKeyword ? [target.focusKeyword] : [],
407
+ },
408
+ ]);
409
+ for (const f of found.slice(0, 2)) {
410
+ suggestions.push({
411
+ sourceEntityId: source.entityId,
412
+ sourceTitle: source.title,
413
+ sourceUrl: source.url,
414
+ anchorText: f.anchorText,
415
+ confidence: f.confidence,
416
+ excerpt: (source.content ?? '').slice(f.position.start, f.position.end + 40),
417
+ });
418
+ }
419
+ }
420
+ suggestions.sort((a, b) => b.confidence - a.confidence);
421
+ suggestions = suggestions.slice(0, limit);
422
+ }
423
+ }
424
+ catch {
425
+ suggestions = [];
426
+ }
427
+ return json({
428
+ data: {
429
+ target: { id: target.entityId, title: target.title, url: target.url },
430
+ suggestions,
431
+ },
432
+ });
433
+ }
434
+ catch (err) {
435
+ return internalError(err, 'seo/links/suggest');
436
+ }
437
+ });
438
+ router.post('/seo/scan', async (request) => {
439
+ try {
440
+ const auth = await requireAuth(request);
441
+ if (auth.error)
442
+ return auth.error;
443
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
444
+ if (roleErr)
445
+ return roleErr;
446
+ // Expensive: loads + regex-scans every published doc. Rate-limit it (same
447
+ // budget as the full audit) so it can't be hammered into an OOM/CPU spike.
448
+ const scanIp = clientIp(request);
449
+ const scanKey = isResolvedIp(scanIp)
450
+ ? `seo-scan:${scanIp}`
451
+ : `seo-scan-user:${auth.session.userId}`;
452
+ if (!(await checkRateLimitAsync(seoAuditLimiter, scanKey))) {
453
+ return errorResponse('Too many scans. Please try again later.', 429);
454
+ }
455
+ // Scope the scan to configured front-end content collections so structural
456
+ // (navigation/footer) and benchmark documents are never reported. Shares
457
+ // the exact scan logic with the cron path via processSeoScan.
458
+ const { frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
459
+ const allowedCollections = Object.keys(frontEndContentCollectionTypes(getPrentaConfig()));
460
+ const result = await processSeoScan(db(), { allowedCollections, maxDocuments: 5000 });
461
+ return json({ data: result });
462
+ }
463
+ catch (err) {
464
+ return internalError(err);
465
+ }
466
+ });
467
+ // ---------------------------------------------------------------------------
468
+ // SEO Operations Center — audit engine, issues, overview, content records
469
+ // ---------------------------------------------------------------------------
470
+ function seoIssueToApi(i) {
471
+ return {
472
+ id: i.id,
473
+ auditRunId: i.auditRunId ?? null,
474
+ entityType: i.entityType ?? null,
475
+ entityId: i.entityId ?? null,
476
+ entityTitle: i.entityTitle ?? null,
477
+ url: i.url ?? null,
478
+ severity: i.severity,
479
+ category: i.category,
480
+ type: i.type,
481
+ title: i.title,
482
+ description: i.description ?? null,
483
+ impact: i.impact ?? null,
484
+ recommendation: i.recommendation ?? null,
485
+ status: i.status,
486
+ fixable: i.fixable ?? false,
487
+ fixActionType: i.fixActionType ?? null,
488
+ relatedEntityIds: Array.isArray(i.relatedEntityIds) ? i.relatedEntityIds : [],
489
+ source: i.source ?? 'audit',
490
+ firstDetectedAt: i.firstDetectedAt ?? null,
491
+ lastDetectedAt: i.lastDetectedAt ?? null,
492
+ resolvedAt: i.resolvedAt ?? null,
493
+ };
494
+ }
495
+ /**
496
+ * Compute the set of front-end content collections the caller may read.
497
+ * Mirrors the per-document `enforceDocumentReadAccess` check the sibling
498
+ * `/seo/analysis|readability|…` endpoints run, but resolved once for the
499
+ * collection list so the aggregate `/seo/content` view can never surface SEO
500
+ * fields for a collection the caller's scope/role forbids.
501
+ */
502
+ async function readableSeoCollections(session) {
503
+ const { frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
504
+ const collectionTypes = frontEndContentCollectionTypes(getPrentaConfig());
505
+ const allowed = new Set();
506
+ for (const slug of Object.keys(collectionTypes)) {
507
+ const err = await enforceDocumentReadAccess(session, slug);
508
+ if (!err)
509
+ allowed.add(slug);
510
+ }
511
+ return allowed;
512
+ }
513
+ async function buildSeoContentRecords(allowedCollections) {
514
+ const { gatherAuditEntities, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
515
+ const { applySeoOverrides, getSeoOverrides } = await import('../../seo/config-store.js');
516
+ const { calculatePageSeoScore } = await import('../../seo/score.js');
517
+ const allTypes = frontEndContentCollectionTypes(getPrentaConfig());
518
+ // When a readable-collection allow-list is supplied, scope the gather to it
519
+ // so records for forbidden collections are never even built.
520
+ const collectionTypes = allowedCollections
521
+ ? Object.fromEntries(Object.entries(allTypes).filter(([slug]) => allowedCollections.has(slug)))
522
+ : allTypes;
523
+ const overrides = await getSeoOverrides(db()).catch(() => null);
524
+ const siteSeo = applySeoOverrides(getPrentaConfig(), overrides)?.seo;
525
+ const entities = await gatherAuditEntities(db(), { collectionTypes });
526
+ return entities.map((e) => {
527
+ const score = calculatePageSeoScore(e);
528
+ return {
529
+ id: `${e.entityType}:${e.entityId}`,
530
+ entityType: e.entityType,
531
+ entityId: e.entityId,
532
+ title: e.title,
533
+ url: e.url,
534
+ canonicalUrl: e.canonicalUrl ?? null,
535
+ /** Rendered public title (after site title template). */
536
+ metaTitle: e.metaTitle ?? null,
537
+ /** Stored page-specific portion (`%page%` token). */
538
+ metaTitlePage: e.metaTitlePage ?? null,
539
+ siteName: siteSeo?.siteName ?? null,
540
+ titleTemplate: siteSeo?.titleTemplate ?? null,
541
+ metaDescription: e.metaDescription ?? null,
542
+ focusKeyword: e.focusKeyword ?? null,
543
+ keyTakeaway: e.keyTakeaway ?? null,
544
+ ogTitle: e.ogTitle ?? null,
545
+ ogDescription: e.ogDescription ?? null,
546
+ ogImageId: null,
547
+ ogImage: e.ogImage ?? null,
548
+ twitterTitle: null,
549
+ twitterDescription: null,
550
+ twitterImageId: null,
551
+ noindex: e.noindex ?? false,
552
+ nofollow: false,
553
+ structuredDataType: e.structuredDataType ?? null,
554
+ structuredData: null,
555
+ // llms.txt curation defaults; the single-record GET overlays the real
556
+ // stored values (the list endpoint doesn't surface these).
557
+ llmsInclude: 'AUTO',
558
+ llmsDescription: null,
559
+ llmsSection: null,
560
+ llmsOptional: false,
561
+ seoScore: score.score,
562
+ seoGrade: score.grade,
563
+ updatedAt: null,
564
+ };
565
+ });
566
+ }
567
+ // Demo/seed search-performance rows live in a hidden collection so they can
568
+ // be inserted by the dev seed script and never ship to npm. In production
569
+ // with a connected Search Console these would come from the GSC API.
570
+ const SEO_PERF_COLLECTION = '__seo_perf__';
571
+ async function loadSearchPerformance(limit = 10) {
572
+ try {
573
+ const docs = await db().document.findMany({
574
+ where: { collection: SEO_PERF_COLLECTION, deletedAt: null },
575
+ select: { data: true },
576
+ });
577
+ const rows = docs
578
+ .map((d) => {
579
+ const data = asRecord(d.data);
580
+ return {
581
+ url: String(data.url ?? ''),
582
+ title: String(data.title ?? ''),
583
+ impressions: Number(data.impressions ?? 0),
584
+ clicks: Number(data.clicks ?? 0),
585
+ ctr: Number(data.ctr ?? 0),
586
+ averagePosition: Number(data.averagePosition ?? 0),
587
+ };
588
+ })
589
+ .filter((r) => r.url);
590
+ rows.sort((a, b) => b.impressions - a.impressions);
591
+ return rows.slice(0, limit);
592
+ }
593
+ catch {
594
+ return [];
595
+ }
596
+ }
597
+ router.post('/seo/audit', async (request) => {
598
+ try {
599
+ const auth = await requireAuth(request);
600
+ if (auth.error)
601
+ return auth.error;
602
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
603
+ if (roleErr)
604
+ return roleErr;
605
+ if (!hasModel(db(), 'seoAuditRun') || !hasModel(db(), 'seoIssue')) {
606
+ return modelNotAvailable('SeoAuditRun');
607
+ }
608
+ const ip = clientIp(request);
609
+ const rateKey = isResolvedIp(ip) ? `seo-audit:${ip}` : `seo-audit-user:${auth.session.userId}`;
610
+ if (!(await checkRateLimitAsync(seoAuditLimiter, rateKey))) {
611
+ return errorResponse('Too many audits. Please try again later.', 429);
612
+ }
613
+ const body = (await request.json().catch(() => ({})));
614
+ const scope = body?.scope ?? 'full';
615
+ const { runAndPersistAudit, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
616
+ const run = await runAndPersistAudit(db(), {
617
+ scope,
618
+ startedById: auth.session.userId,
619
+ collectionTypes: frontEndContentCollectionTypes(getPrentaConfig()),
620
+ });
621
+ try {
622
+ await logEvent({
623
+ event: 'settings_changed',
624
+ userId: auth.session.userId,
625
+ details: { action: 'seo_audit', auditRunId: run.id, scope },
626
+ });
627
+ }
628
+ catch {
629
+ /* audit-log failures never block */
630
+ }
631
+ return json({ data: run });
632
+ }
633
+ catch (err) {
634
+ return internalError(err, 'seo/audit');
635
+ }
636
+ });
637
+ router.get('/seo/audits', async (request) => {
638
+ try {
639
+ const auth = await requireAuth(request);
640
+ if (auth.error)
641
+ return auth.error;
642
+ if (!hasModel(db(), 'seoAuditRun'))
643
+ return json({ data: { runs: [] } });
644
+ const runs = await db().seoAuditRun.findMany({
645
+ orderBy: { startedAt: 'desc' },
646
+ take: 50,
647
+ });
648
+ return json({ data: { runs } });
649
+ }
650
+ catch (err) {
651
+ return internalError(err, 'seo/audits');
652
+ }
653
+ });
654
+ router.get('/seo/audits/:id', async (request, params) => {
655
+ try {
656
+ const auth = await requireAuth(request);
657
+ if (auth.error)
658
+ return auth.error;
659
+ if (!hasModel(db(), 'seoAuditRun'))
660
+ return modelNotAvailable('SeoAuditRun');
661
+ const run = await db().seoAuditRun.findUnique({ where: { id: params.id } });
662
+ if (!run)
663
+ return errorResponse('Audit run not found', 404);
664
+ return json({ data: run });
665
+ }
666
+ catch (err) {
667
+ return internalError(err, 'seo/audits/:id');
668
+ }
669
+ });
670
+ router.get('/seo/issues', async (request) => {
671
+ try {
672
+ const auth = await requireAuth(request);
673
+ if (auth.error)
674
+ return auth.error;
675
+ if (!hasModel(db(), 'seoIssue'))
676
+ return json({ data: { issues: [] } });
677
+ const url = new URL(request.url);
678
+ const where = {};
679
+ const severity = url.searchParams.get('severity');
680
+ const category = url.searchParams.get('category');
681
+ const status = url.searchParams.get('status');
682
+ const auditRunId = url.searchParams.get('auditRunId');
683
+ if (severity)
684
+ where.severity = severity;
685
+ if (category)
686
+ where.category = category;
687
+ if (status)
688
+ where.status = status;
689
+ else
690
+ where.status = { in: ['open', 'recurring'] };
691
+ if (auditRunId)
692
+ where.auditRunId = auditRunId;
693
+ const issues = await db().seoIssue.findMany({
694
+ where,
695
+ orderBy: { lastDetectedAt: 'desc' },
696
+ take: 500,
697
+ });
698
+ return json({ data: { issues: sortIssuesBySeverity(issues).map(seoIssueToApi) } });
699
+ }
700
+ catch (err) {
701
+ return internalError(err, 'seo/issues');
702
+ }
703
+ });
704
+ router.get('/seo/issues/export-brief', async (request) => {
705
+ try {
706
+ const auth = await requireAuth(request);
707
+ if (auth.error)
708
+ return auth.error;
709
+ if (!hasModel(db(), 'seoIssue')) {
710
+ const { buildSeoFixBriefMarkdown, fixBriefFilename } = await import('../../seo/fix-brief-export.js');
711
+ return json({
712
+ data: {
713
+ markdown: buildSeoFixBriefMarkdown({ issues: [], manualOnly: true }),
714
+ filename: fixBriefFilename(),
715
+ issueCount: 0,
716
+ },
717
+ });
718
+ }
719
+ const url = new URL(request.url);
720
+ const severity = url.searchParams.get('severity');
721
+ const status = url.searchParams.get('status');
722
+ const auditRunId = url.searchParams.get('auditRunId');
723
+ const manualOnly = url.searchParams.get('manualOnly') !== 'false';
724
+ const includeContentBriefs = url.searchParams.get('includeContentBriefs') !== 'false';
725
+ const includeContentResearch = url.searchParams.get('includeContentResearch') !== 'false';
726
+ const where = {};
727
+ if (severity)
728
+ where.severity = severity;
729
+ if (status)
730
+ where.status = status;
731
+ else
732
+ where.status = { in: ['open', 'recurring'] };
733
+ if (auditRunId)
734
+ where.auditRunId = auditRunId;
735
+ const issues = sortIssuesBySeverity(await db().seoIssue.findMany({
736
+ where,
737
+ orderBy: { lastDetectedAt: 'desc' },
738
+ take: 500,
739
+ }));
740
+ let runAuditDate = null;
741
+ let runId = auditRunId;
742
+ if (auditRunId && hasModel(db(), 'seoAuditRun')) {
743
+ const run = await db().seoAuditRun.findUnique({ where: { id: auditRunId } });
744
+ runAuditDate = run?.finishedAt ?? run?.startedAt ?? null;
745
+ }
746
+ else if (hasModel(db(), 'seoAuditRun')) {
747
+ const lastRun = await db().seoAuditRun.findFirst({
748
+ where: { status: 'COMPLETED' },
749
+ orderBy: { startedAt: 'desc' },
750
+ });
751
+ runId = lastRun?.id ?? null;
752
+ runAuditDate = lastRun?.finishedAt ?? lastRun?.startedAt ?? null;
753
+ }
754
+ const { applySeoOverrides, getSeoOverrides } = await import('../../seo/config-store.js');
755
+ const overrides = await getSeoOverrides(db()).catch(() => null);
756
+ const mergedConfig = applySeoOverrides(getPrentaConfig(), overrides);
757
+ const siteSeo = mergedConfig?.seo;
758
+ const { buildSeoFixBriefMarkdown, filterFixBriefIssues, fixBriefFilename } = await import('../../seo/fix-brief-export.js');
759
+ const entityIds = [
760
+ ...new Set(issues
761
+ .map((i) => i.entityId)
762
+ .filter((id) => typeof id === 'string' && id.length > 0)),
763
+ ];
764
+ const updatedAtByEntity = new Map();
765
+ if ((includeContentBriefs || includeContentResearch) && entityIds.length > 0) {
766
+ const docs = await db().document.findMany({
767
+ where: { id: { in: entityIds }, deletedAt: null },
768
+ select: { id: true, updatedAt: true },
769
+ });
770
+ for (const doc of docs) {
771
+ updatedAtByEntity.set(doc.id, doc.updatedAt instanceof Date ? doc.updatedAt : new Date(doc.updatedAt));
772
+ }
773
+ }
774
+ const { contentBriefFromMetadata } = await import('../../seo/issue-content-brief.js');
775
+ const { contentResearchFromMetadata } = await import('../../seo/issue-content-research.js');
776
+ const briefIssues = issues.map((i) => {
777
+ let contentBrief = null;
778
+ let contentResearch = null;
779
+ if (i.entityId) {
780
+ const updatedAt = updatedAtByEntity.get(i.entityId);
781
+ if (updatedAt) {
782
+ if (includeContentBriefs) {
783
+ contentBrief = contentBriefFromMetadata(i.metadata, i.type, i.entityId, updatedAt);
784
+ }
785
+ if (includeContentResearch) {
786
+ contentResearch = contentResearchFromMetadata(i.metadata, i.type, i.entityId, updatedAt);
787
+ }
788
+ }
789
+ }
790
+ return {
791
+ id: i.id,
792
+ type: i.type,
793
+ title: i.title,
794
+ description: i.description ?? null,
795
+ impact: i.impact ?? null,
796
+ recommendation: i.recommendation ?? null,
797
+ severity: i.severity,
798
+ category: i.category,
799
+ status: i.status,
800
+ entityType: i.entityType ?? null,
801
+ entityId: i.entityId ?? null,
802
+ entityTitle: i.entityTitle ?? null,
803
+ url: i.url ?? null,
804
+ fixActionType: i.fixActionType ?? null,
805
+ fixable: i.fixable ?? false,
806
+ contentBrief,
807
+ contentResearch,
808
+ };
809
+ });
810
+ const markdown = buildSeoFixBriefMarkdown({
811
+ siteName: siteSeo?.siteName ?? null,
812
+ siteUrl: siteSeo?.siteUrl ?? null,
813
+ auditRunId: runId,
814
+ auditDate: runAuditDate,
815
+ issues: briefIssues,
816
+ manualOnly,
817
+ });
818
+ const issueCount = filterFixBriefIssues(briefIssues, manualOnly).length;
819
+ return json({
820
+ data: {
821
+ markdown,
822
+ filename: fixBriefFilename(siteSeo?.siteName, runAuditDate ?? undefined),
823
+ issueCount,
824
+ },
825
+ });
826
+ }
827
+ catch (err) {
828
+ return internalError(err, 'seo/issues/export-brief');
829
+ }
830
+ });
831
+ router.post('/seo/issues/batch-content-briefs', async (request) => {
832
+ try {
833
+ const auth = await requireAuth(request);
834
+ if (auth.error)
835
+ return auth.error;
836
+ const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
837
+ if (roleErr)
838
+ return roleErr;
839
+ const batchPlanErr = assertPlanFeature('seo.batchContentBriefs');
840
+ if (batchPlanErr)
841
+ return planRequiredResponse(batchPlanErr);
842
+ if (!hasModel(db(), 'seoIssue'))
843
+ return modelNotAvailable('SeoIssue');
844
+ if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-seo-batch-briefs:${auth.session.userId}`))) {
845
+ return errorResponse('AI rate limit reached. Try again in an hour.', 429);
846
+ }
847
+ const body = (await request.json().catch(() => ({})));
848
+ const severity = typeof body?.severity === 'string' ? body.severity : null;
849
+ const status = typeof body?.status === 'string' ? body.status : null;
850
+ const skipExisting = body?.skipExisting !== false;
851
+ const includeResearch = body?.includeResearch === true;
852
+ const competitorUrls = Array.isArray(body?.competitorUrls)
853
+ ? body.competitorUrls.filter((u) => typeof u === 'string')
854
+ : undefined;
855
+ const issueIds = Array.isArray(body?.issueIds)
856
+ ? body.issueIds.filter((id) => typeof id === 'string' && id.length > 0)
857
+ : null;
858
+ const where = {};
859
+ if (severity)
860
+ where.severity = severity;
861
+ if (status)
862
+ where.status = status;
863
+ else
864
+ where.status = { in: ['open', 'recurring'] };
865
+ let issues = await db().seoIssue.findMany({
866
+ where,
867
+ orderBy: { lastDetectedAt: 'desc' },
868
+ take: 500,
869
+ });
870
+ if (issueIds?.length) {
871
+ const idSet = new Set(issueIds);
872
+ issues = issues.filter((i) => idSet.has(i.id));
873
+ }
874
+ const { supportsContentBrief } = await import('../../seo/issue-content-brief.js');
875
+ const eligible = issues.filter((i) => supportsContentBrief(i.type));
876
+ const entityIds = [
877
+ ...new Set(eligible
878
+ .map((i) => i.entityId)
879
+ .filter((id) => typeof id === 'string' && id.length > 0)),
880
+ ];
881
+ const { documentToAuditEntity, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
882
+ const { applySeoOverrides, getSeoOverrides } = await import('../../seo/config-store.js');
883
+ const collectionTypes = frontEndContentCollectionTypes(getPrentaConfig());
884
+ const overrides = await getSeoOverrides(db()).catch(() => null);
885
+ const mergedConfig = applySeoOverrides(getPrentaConfig(), overrides);
886
+ const contexts = new Map();
887
+ if (entityIds.length > 0) {
888
+ const docs = await db().document.findMany({
889
+ where: { id: { in: entityIds }, deletedAt: null },
890
+ select: {
891
+ id: true,
892
+ collection: true,
893
+ title: true,
894
+ slug: true,
895
+ data: true,
896
+ structuredData: true,
897
+ plainText: true,
898
+ publishedAt: true,
899
+ updatedAt: true,
900
+ },
901
+ });
902
+ for (const doc of docs) {
903
+ contexts.set(doc.id, {
904
+ collection: doc.collection,
905
+ entity: documentToAuditEntity(doc, collectionTypes, mergedConfig?.seo),
906
+ updatedAt: doc.updatedAt instanceof Date ? doc.updatedAt : new Date(doc.updatedAt),
907
+ });
908
+ }
909
+ }
910
+ const { batchGenerateContentBriefs } = await import('../../seo/issue-batch-briefs.js');
911
+ const result = await batchGenerateContentBriefs(eligible.map((i) => ({
912
+ id: i.id,
913
+ type: i.type,
914
+ title: i.title,
915
+ description: i.description,
916
+ recommendation: i.recommendation,
917
+ entityId: i.entityId,
918
+ entityType: i.entityType,
919
+ metadata: i.metadata,
920
+ })), contexts, { db: db(), userId: auth.session.userId, collection: 'pages' }, { skipExisting, includeResearch, competitorUrls });
921
+ return json({ data: result });
922
+ }
923
+ catch (err) {
924
+ return internalError(err, 'seo/issues/batch-content-briefs');
925
+ }
926
+ });
927
+ router.get('/seo/issues/:id/content-brief', async (request, params) => {
928
+ try {
929
+ const auth = await requireAuth(request);
930
+ if (auth.error)
931
+ return auth.error;
932
+ if (!hasModel(db(), 'seoIssue'))
933
+ return modelNotAvailable('SeoIssue');
934
+ const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
935
+ if (!issue)
936
+ return errorResponse('Issue not found', 404);
937
+ const { supportsContentBrief, contentBriefFromMetadata } = await import('../../seo/issue-content-brief.js');
938
+ if (!supportsContentBrief(issue.type)) {
939
+ return json({ data: { supported: false, brief: null, stale: false } });
940
+ }
941
+ if (!issue.entityId) {
942
+ return json({ data: { supported: true, brief: null, stale: false } });
943
+ }
944
+ const loaded = await loadIssueFixContext(issue);
945
+ if ('error' in loaded)
946
+ return loaded.error;
947
+ const brief = contentBriefFromMetadata(issue.metadata, issue.type, issue.entityId, loaded.updatedAt);
948
+ const raw = asRecord(issue.metadata)?.contentBrief;
949
+ const stale = !!raw &&
950
+ typeof raw === 'object' &&
951
+ !Array.isArray(raw) &&
952
+ typeof raw.fingerprint === 'string' &&
953
+ !brief;
954
+ return json({ data: { supported: true, brief, stale } });
955
+ }
956
+ catch (err) {
957
+ return internalError(err, 'seo/issues/:id/content-brief');
958
+ }
959
+ });
960
+ router.post('/seo/issues/:id/content-brief', async (request, params) => {
961
+ try {
962
+ const auth = await requireAuth(request);
963
+ if (auth.error)
964
+ return auth.error;
965
+ const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
966
+ if (roleErr)
967
+ return roleErr;
968
+ const planErr = assertPlanFeature('seo.contentBrief');
969
+ if (planErr)
970
+ return planRequiredResponse(planErr);
971
+ if (!hasModel(db(), 'seoIssue'))
972
+ return modelNotAvailable('SeoIssue');
973
+ if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-seo-content-brief:${auth.session.userId}`))) {
974
+ return errorResponse('AI rate limit reached. Try again in an hour.', 429);
975
+ }
976
+ const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
977
+ if (!issue)
978
+ return errorResponse('Issue not found', 404);
979
+ const loaded = await loadIssueFixContext(issue);
980
+ if ('error' in loaded)
981
+ return loaded.error;
982
+ const { generateContentBrief, contentBriefToMetadata, supportsContentBrief } = await import('../../seo/issue-content-brief.js');
983
+ if (!supportsContentBrief(issue.type)) {
984
+ return errorResponse('This issue type does not support content briefs.', 422);
985
+ }
986
+ const result = await generateContentBrief({
987
+ id: issue.id,
988
+ type: issue.type,
989
+ title: issue.title,
990
+ description: issue.description,
991
+ recommendation: issue.recommendation,
992
+ entityId: issue.entityId,
993
+ }, { entity: loaded.entity, updatedAt: loaded.updatedAt }, { db: db(), userId: auth.session.userId, collection: loaded.collection });
994
+ if (!result.ok) {
995
+ return seoAiGenerationError(result);
996
+ }
997
+ const existingMeta = asRecord(issue.metadata);
998
+ await db().seoIssue.update({
999
+ where: { id: issue.id },
1000
+ data: { metadata: { ...existingMeta, ...contentBriefToMetadata(result.brief) } },
1001
+ });
1002
+ return json({ data: { brief: result.brief } });
1003
+ }
1004
+ catch (err) {
1005
+ return internalError(err, 'seo/issues/:id/content-brief');
1006
+ }
1007
+ });
1008
+ router.get('/seo/issues/:id/content-research', async (request, params) => {
1009
+ try {
1010
+ const auth = await requireAuth(request);
1011
+ if (auth.error)
1012
+ return auth.error;
1013
+ if (!hasModel(db(), 'seoIssue'))
1014
+ return modelNotAvailable('SeoIssue');
1015
+ const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
1016
+ if (!issue)
1017
+ return errorResponse('Issue not found', 404);
1018
+ const { supportsContentResearch, contentResearchFromMetadata } = await import('../../seo/issue-content-research.js');
1019
+ if (!supportsContentResearch(issue.type)) {
1020
+ return json({ data: { supported: false, research: null, stale: false } });
1021
+ }
1022
+ if (!issue.entityId) {
1023
+ return json({ data: { supported: true, research: null, stale: false } });
1024
+ }
1025
+ const loaded = await loadIssueFixContext(issue);
1026
+ if ('error' in loaded)
1027
+ return loaded.error;
1028
+ const research = contentResearchFromMetadata(issue.metadata, issue.type, issue.entityId, loaded.updatedAt);
1029
+ const raw = asRecord(issue.metadata)?.contentResearch;
1030
+ const stale = !!raw &&
1031
+ typeof raw === 'object' &&
1032
+ !Array.isArray(raw) &&
1033
+ typeof raw.fingerprint === 'string' &&
1034
+ !research;
1035
+ return json({ data: { supported: true, research, stale } });
1036
+ }
1037
+ catch (err) {
1038
+ return internalError(err, 'seo/issues/:id/content-research');
1039
+ }
1040
+ });
1041
+ router.post('/seo/issues/:id/content-research', async (request, params) => {
1042
+ try {
1043
+ const auth = await requireAuth(request);
1044
+ if (auth.error)
1045
+ return auth.error;
1046
+ const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
1047
+ if (roleErr)
1048
+ return roleErr;
1049
+ const researchPlanErr = assertPlanFeature('seo.contentResearch');
1050
+ if (researchPlanErr)
1051
+ return planRequiredResponse(researchPlanErr);
1052
+ if (!hasModel(db(), 'seoIssue'))
1053
+ return modelNotAvailable('SeoIssue');
1054
+ if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-seo-content-research:${auth.session.userId}`))) {
1055
+ return errorResponse('AI rate limit reached. Try again in an hour.', 429);
1056
+ }
1057
+ const body = (await request.json().catch(() => ({})));
1058
+ const competitorUrls = Array.isArray(body?.competitorUrls)
1059
+ ? body.competitorUrls.filter((u) => typeof u === 'string')
1060
+ : undefined;
1061
+ const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
1062
+ if (!issue)
1063
+ return errorResponse('Issue not found', 404);
1064
+ const loaded = await loadIssueFixContext(issue);
1065
+ if ('error' in loaded)
1066
+ return loaded.error;
1067
+ const { generateContentResearch, contentResearchToMetadata, supportsContentResearch } = await import('../../seo/issue-content-research.js');
1068
+ if (!supportsContentResearch(issue.type)) {
1069
+ return errorResponse('This issue type does not support topic research.', 422);
1070
+ }
1071
+ const result = await generateContentResearch({
1072
+ id: issue.id,
1073
+ type: issue.type,
1074
+ title: issue.title,
1075
+ description: issue.description,
1076
+ recommendation: issue.recommendation,
1077
+ entityId: issue.entityId,
1078
+ }, { entity: loaded.entity, updatedAt: loaded.updatedAt }, { db: db(), userId: auth.session.userId, collection: loaded.collection }, { competitorUrls });
1079
+ if (!result.ok) {
1080
+ return seoAiGenerationError(result);
1081
+ }
1082
+ const existingMeta = asRecord(issue.metadata);
1083
+ await db().seoIssue.update({
1084
+ where: { id: issue.id },
1085
+ data: { metadata: { ...existingMeta, ...contentResearchToMetadata(result.research) } },
1086
+ });
1087
+ return json({ data: { research: result.research } });
1088
+ }
1089
+ catch (err) {
1090
+ return internalError(err, 'seo/issues/:id/content-research');
1091
+ }
1092
+ });
1093
+ router.post('/seo/issues/:id/research-and-brief', async (request, params) => {
1094
+ try {
1095
+ const auth = await requireAuth(request);
1096
+ if (auth.error)
1097
+ return auth.error;
1098
+ const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
1099
+ if (roleErr)
1100
+ return roleErr;
1101
+ const comboPlanErr = assertPlanFeature('seo.contentResearch');
1102
+ if (comboPlanErr)
1103
+ return planRequiredResponse(comboPlanErr);
1104
+ if (!hasModel(db(), 'seoIssue'))
1105
+ return modelNotAvailable('SeoIssue');
1106
+ if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-seo-research-brief:${auth.session.userId}`))) {
1107
+ return errorResponse('AI rate limit reached. Try again in an hour.', 429);
1108
+ }
1109
+ const body = (await request.json().catch(() => ({})));
1110
+ const competitorUrls = Array.isArray(body?.competitorUrls)
1111
+ ? body.competitorUrls.filter((u) => typeof u === 'string')
1112
+ : undefined;
1113
+ const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
1114
+ if (!issue)
1115
+ return errorResponse('Issue not found', 404);
1116
+ const loaded = await loadIssueFixContext(issue);
1117
+ if ('error' in loaded)
1118
+ return loaded.error;
1119
+ const { generateContentResearch, contentResearchToMetadata, supportsContentResearch, contentResearchFromMetadata, } = await import('../../seo/issue-content-research.js');
1120
+ const { generateContentBrief, contentBriefToMetadata, supportsContentBrief } = await import('../../seo/issue-content-brief.js');
1121
+ if (!supportsContentBrief(issue.type)) {
1122
+ return errorResponse('This issue type does not support content briefs.', 422);
1123
+ }
1124
+ const issueInput = {
1125
+ id: issue.id,
1126
+ type: issue.type,
1127
+ title: issue.title,
1128
+ description: issue.description,
1129
+ recommendation: issue.recommendation,
1130
+ entityId: issue.entityId,
1131
+ };
1132
+ const briefCtx = { entity: loaded.entity, updatedAt: loaded.updatedAt };
1133
+ const deps = { db: db(), userId: auth.session.userId, collection: loaded.collection };
1134
+ let research = supportsContentResearch(issue.type)
1135
+ ? contentResearchFromMetadata(issue.metadata, issue.type, issue.entityId, loaded.updatedAt)
1136
+ : null;
1137
+ if (supportsContentResearch(issue.type) && !research) {
1138
+ const researchResult = await generateContentResearch(issueInput, briefCtx, deps, {
1139
+ competitorUrls,
1140
+ });
1141
+ if (!researchResult.ok) {
1142
+ return seoAiGenerationError(researchResult);
1143
+ }
1144
+ research = researchResult.research;
1145
+ }
1146
+ const briefResult = await generateContentBrief(issueInput, briefCtx, deps, { research });
1147
+ if (!briefResult.ok) {
1148
+ return seoAiGenerationError(briefResult);
1149
+ }
1150
+ const existingMeta = asRecord(issue.metadata);
1151
+ const metadata = {
1152
+ ...existingMeta,
1153
+ ...contentBriefToMetadata(briefResult.brief),
1154
+ ...(research ? contentResearchToMetadata(research) : {}),
1155
+ };
1156
+ await db().seoIssue.update({
1157
+ where: { id: issue.id },
1158
+ data: { metadata },
1159
+ });
1160
+ return json({ data: { research, brief: briefResult.brief } });
1161
+ }
1162
+ catch (err) {
1163
+ return internalError(err, 'seo/issues/:id/research-and-brief');
1164
+ }
1165
+ });
1166
+ router.get('/seo/issues/:id', async (request, params) => {
1167
+ try {
1168
+ const auth = await requireAuth(request);
1169
+ if (auth.error)
1170
+ return auth.error;
1171
+ if (!hasModel(db(), 'seoIssue'))
1172
+ return modelNotAvailable('SeoIssue');
1173
+ const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
1174
+ if (!issue)
1175
+ return errorResponse('Issue not found', 404);
1176
+ return json({ data: seoIssueToApi(issue) });
1177
+ }
1178
+ catch (err) {
1179
+ return internalError(err, 'seo/issues/:id');
1180
+ }
1181
+ });
1182
+ async function setIssueStatus(request, issueId, status) {
1183
+ const auth = await requireAuth(request);
1184
+ if (auth.error)
1185
+ return auth.error;
1186
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
1187
+ if (roleErr)
1188
+ return roleErr;
1189
+ if (!hasModel(db(), 'seoIssue'))
1190
+ return modelNotAvailable('SeoIssue');
1191
+ const existing = await db().seoIssue.findUnique({ where: { id: issueId } });
1192
+ if (!existing)
1193
+ return errorResponse('Issue not found', 404);
1194
+ const issue = await db().seoIssue.update({
1195
+ where: { id: issueId },
1196
+ data: {
1197
+ status,
1198
+ resolvedAt: status === 'resolved' ? new Date() : null,
1199
+ resolvedById: status === 'resolved' ? auth.session.userId : null,
1200
+ },
1201
+ });
1202
+ try {
1203
+ await logEvent({
1204
+ event: 'settings_changed',
1205
+ userId: auth.session.userId,
1206
+ details: { action: 'seo_issue_status', issueId, status },
1207
+ });
1208
+ }
1209
+ catch {
1210
+ /* noop */
1211
+ }
1212
+ return json({ data: seoIssueToApi(issue) });
1213
+ }
1214
+ router.post('/seo/issues/:id/resolve', (request, params) => setIssueStatus(request, params.id, 'resolved').catch((e) => internalError(e)));
1215
+ router.post('/seo/issues/:id/ignore', (request, params) => setIssueStatus(request, params.id, 'ignored').catch((e) => internalError(e)));
1216
+ router.post('/seo/issues/:id/reopen', (request, params) => setIssueStatus(request, params.id, 'open').catch((e) => internalError(e)));
1217
+ /** Load the audit entity + collection for an issue's linked document. */
1218
+ async function loadIssueFixContext(issue) {
1219
+ if (!issue.entityId) {
1220
+ return { error: errorResponse('This issue is not linked to a content item.', 422) };
1221
+ }
1222
+ const doc = await db().document.findFirst({
1223
+ where: { id: issue.entityId, deletedAt: null },
1224
+ select: {
1225
+ id: true,
1226
+ collection: true,
1227
+ title: true,
1228
+ slug: true,
1229
+ data: true,
1230
+ structuredData: true,
1231
+ plainText: true,
1232
+ publishedAt: true,
1233
+ updatedAt: true,
1234
+ },
1235
+ });
1236
+ if (!doc)
1237
+ return { error: errorResponse('Content not found', 404) };
1238
+ const { documentToAuditEntity, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
1239
+ const { applySeoOverrides, getSeoOverrides } = await import('../../seo/config-store.js');
1240
+ const collectionTypes = frontEndContentCollectionTypes(getPrentaConfig());
1241
+ const overrides = await getSeoOverrides(db()).catch(() => null);
1242
+ const mergedConfig = applySeoOverrides(getPrentaConfig(), overrides);
1243
+ const entity = documentToAuditEntity(doc, collectionTypes, mergedConfig?.seo);
1244
+ if (issue.entityType && entity.entityType !== issue.entityType) {
1245
+ return { error: errorResponse('Issue entity type does not match the linked document.', 409) };
1246
+ }
1247
+ return {
1248
+ collection: doc.collection,
1249
+ entity,
1250
+ updatedAt: doc.updatedAt instanceof Date ? doc.updatedAt : new Date(doc.updatedAt),
1251
+ siteSeo: mergedConfig?.seo,
1252
+ };
1253
+ }
1254
+ router.post('/seo/issues/:id/suggest-fix', async (request, params) => {
1255
+ try {
1256
+ const auth = await requireAuth(request);
1257
+ if (auth.error)
1258
+ return auth.error;
1259
+ if (!hasModel(db(), 'seoIssue'))
1260
+ return modelNotAvailable('SeoIssue');
1261
+ const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
1262
+ if (!issue)
1263
+ return errorResponse('Issue not found', 404);
1264
+ const loaded = await loadIssueFixContext(issue);
1265
+ if ('error' in loaded)
1266
+ return loaded.error;
1267
+ const issueInput = {
1268
+ type: issue.type,
1269
+ title: issue.title,
1270
+ recommendation: issue.recommendation,
1271
+ entityType: issue.entityType,
1272
+ entityId: issue.entityId,
1273
+ };
1274
+ const fixCtx = {
1275
+ entity: loaded.entity,
1276
+ siteSeo: loaded.siteSeo,
1277
+ updatedAt: loaded.updatedAt,
1278
+ metadata: asRecord(issue.metadata),
1279
+ };
1280
+ let aiDeps;
1281
+ const { REDIRECT_FIX_TYPES } = await import('../../seo/issue-fix-redirect.js');
1282
+ if (REDIRECT_FIX_TYPES.has(issue.type)) {
1283
+ aiDeps = {
1284
+ db: db(),
1285
+ userId: auth.session.userId,
1286
+ collection: loaded.collection,
1287
+ };
1288
+ }
1289
+ else {
1290
+ const adminErr = requireRole(auth.session.role, ADMIN_ROLES);
1291
+ if (!adminErr &&
1292
+ hasPlanFeature('seo.inlineApply') &&
1293
+ (await checkRateLimitAsync(aiGenerateLimiter, `ai-seo-fix-suggest:${auth.session.userId}`))) {
1294
+ aiDeps = {
1295
+ db: db(),
1296
+ userId: auth.session.userId,
1297
+ collection: loaded.collection,
1298
+ };
1299
+ }
1300
+ }
1301
+ const { suggestIssueFix } = await import('../../seo/issue-fix.js');
1302
+ const { pendingFixToMetadata } = await import('../../seo/issue-fix-ai.js');
1303
+ const result = await suggestIssueFix(issueInput, fixCtx, aiDeps);
1304
+ const existingMeta = asRecord(issue.metadata);
1305
+ const nextMetadata = result.cacheForApply
1306
+ ? { ...existingMeta, ...pendingFixToMetadata(result.suggestion) }
1307
+ : { ...existingMeta, pendingFix: null };
1308
+ await db().seoIssue.update({
1309
+ where: { id: issue.id },
1310
+ data: { metadata: nextMetadata },
1311
+ });
1312
+ return json({ data: { suggestion: result.suggestion } });
1313
+ }
1314
+ catch (err) {
1315
+ return internalError(err, 'seo/issues/:id/suggest-fix');
1316
+ }
1317
+ });
1318
+ router.post('/seo/issues/:id/apply-fix', async (request, params) => {
1319
+ try {
1320
+ const auth = await requireAuth(request);
1321
+ if (auth.error)
1322
+ return auth.error;
1323
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
1324
+ if (roleErr)
1325
+ return roleErr;
1326
+ if (!hasModel(db(), 'seoIssue'))
1327
+ return modelNotAvailable('SeoIssue');
1328
+ const body = (await request.json().catch(() => ({})));
1329
+ const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
1330
+ if (!issue)
1331
+ return errorResponse('Issue not found', 404);
1332
+ if (issue.status === 'ignored') {
1333
+ return errorResponse('Cannot apply a fix to an ignored issue. Reopen it first.', 409);
1334
+ }
1335
+ const loaded = await loadIssueFixContext(issue);
1336
+ if ('error' in loaded)
1337
+ return loaded.error;
1338
+ const issueInput = {
1339
+ type: issue.type,
1340
+ title: issue.title,
1341
+ recommendation: issue.recommendation,
1342
+ entityType: issue.entityType,
1343
+ entityId: issue.entityId,
1344
+ };
1345
+ const fixCtx = {
1346
+ entity: loaded.entity,
1347
+ siteSeo: loaded.siteSeo,
1348
+ updatedAt: loaded.updatedAt,
1349
+ metadata: asRecord(issue.metadata),
1350
+ };
1351
+ const { REDIRECT_FIX_TYPES, applyBrokenLinkRedirectFix, applyBrokenLinkContentFix } = await import('../../seo/issue-fix-redirect.js');
1352
+ if (REDIRECT_FIX_TYPES.has(issue.type)) {
1353
+ const { pendingFixFromMetadata } = await import('../../seo/issue-fix-ai.js');
1354
+ const { buildFixFingerprint } = await import('../../seo/issue-fix.js');
1355
+ const cached = pendingFixFromMetadata(issue.metadata);
1356
+ const liveFingerprint = buildFixFingerprint(loaded.entity.entityId, issue.type, loaded.updatedAt);
1357
+ if (!cached?.autoFixable) {
1358
+ return errorResponse('This issue does not support an automatic fix.', 422);
1359
+ }
1360
+ if (body?.fingerprint && body.fingerprint !== cached.fingerprint) {
1361
+ return errorResponse('This fix is stale — the page changed since the suggestion was loaded. Expand the row again.', 409);
1362
+ }
1363
+ if (cached.fingerprint !== liveFingerprint) {
1364
+ return errorResponse('This fix is stale — the page changed since the suggestion was loaded. Expand the row again.', 409);
1365
+ }
1366
+ const fixStrategy = cached.patch.fixStrategy === 'fix-link' ? 'fix-link' : 'redirect';
1367
+ try {
1368
+ if (fixStrategy === 'fix-link') {
1369
+ const ctx = buildActionContext(auth.session, db());
1370
+ await applyBrokenLinkContentFix(loaded.collection, loaded.entity.entityId, cached.patch, ctx);
1371
+ }
1372
+ else {
1373
+ await applyBrokenLinkRedirectFix(db(), cached.patch, auth.session.userId);
1374
+ }
1375
+ }
1376
+ catch (err) {
1377
+ const msg = err instanceof Error ? err.message : 'Fix application failed';
1378
+ return errorResponse(msg, 400);
1379
+ }
1380
+ const existingMeta = asRecord(issue.metadata);
1381
+ const resolved = await db().seoIssue.update({
1382
+ where: { id: issue.id },
1383
+ data: {
1384
+ status: 'resolved',
1385
+ resolvedAt: new Date(),
1386
+ resolvedById: auth.session.userId,
1387
+ metadata: { ...existingMeta, pendingFix: null },
1388
+ },
1389
+ });
1390
+ try {
1391
+ await logEvent({
1392
+ event: 'document_updated',
1393
+ userId: auth.session.userId,
1394
+ details: {
1395
+ action: fixStrategy === 'fix-link'
1396
+ ? 'seo_issue_link_fix_applied'
1397
+ : 'seo_issue_redirect_applied',
1398
+ issueId: issue.id,
1399
+ issueType: issue.type,
1400
+ ...(fixStrategy === 'fix-link'
1401
+ ? {
1402
+ fixLinkFrom: cached.patch.fixLinkFrom,
1403
+ fixLinkTo: cached.patch.fixLinkTo,
1404
+ }
1405
+ : {
1406
+ redirectFrom: cached.patch.redirectFrom,
1407
+ redirectTo: cached.patch.redirectTo,
1408
+ }),
1409
+ },
1410
+ });
1411
+ }
1412
+ catch {
1413
+ /* noop */
1414
+ }
1415
+ return json({
1416
+ data: {
1417
+ issue: seoIssueToApi(resolved),
1418
+ applied: cached.changes,
1419
+ },
1420
+ });
1421
+ }
1422
+ const { suggestDeterministicFix, buildFixFingerprint } = await import('../../seo/issue-fix.js');
1423
+ const { pendingFixFromMetadata } = await import('../../seo/issue-fix-ai.js');
1424
+ const deterministic = suggestDeterministicFix(issueInput, fixCtx);
1425
+ let suggestion = deterministic;
1426
+ if (!deterministic.autoFixable || Object.keys(deterministic.patch).length === 0) {
1427
+ const inlinePlanErr = assertPlanFeature('seo.inlineApply');
1428
+ if (inlinePlanErr)
1429
+ return planRequiredResponse(inlinePlanErr);
1430
+ const cached = pendingFixFromMetadata(issue.metadata);
1431
+ const liveFingerprint = buildFixFingerprint(loaded.entity.entityId, issue.type, loaded.updatedAt);
1432
+ if (!cached || !cached.autoFixable) {
1433
+ return errorResponse('This issue does not support an automatic fix.', 422);
1434
+ }
1435
+ if (body?.fingerprint && body.fingerprint !== cached.fingerprint) {
1436
+ return errorResponse('This fix is stale — the page changed since the suggestion was loaded. Expand the row again.', 409);
1437
+ }
1438
+ if (cached.fingerprint !== liveFingerprint) {
1439
+ return errorResponse('This fix is stale — the page changed since the suggestion was loaded. Expand the row again.', 409);
1440
+ }
1441
+ suggestion = {
1442
+ autoFixable: true,
1443
+ source: 'ai',
1444
+ fingerprint: cached.fingerprint,
1445
+ patch: cached.patch,
1446
+ changes: cached.changes,
1447
+ justification: cached.justification,
1448
+ };
1449
+ }
1450
+ else if (body?.fingerprint && body.fingerprint !== suggestion.fingerprint) {
1451
+ return errorResponse('This fix is stale — the page changed since the suggestion was loaded. Expand the row again.', 409);
1452
+ }
1453
+ if (!suggestion.autoFixable || Object.keys(suggestion.patch).length === 0) {
1454
+ return errorResponse('This issue does not support an automatic fix.', 422);
1455
+ }
1456
+ const ctx = buildActionContext(auth.session, db());
1457
+ try {
1458
+ await updateDocumentSeoFields(loaded.collection, loaded.entity.entityId, suggestion.patch, ctx);
1459
+ }
1460
+ catch (err) {
1461
+ const msg = err instanceof Error ? err.message : '';
1462
+ if (msg.includes('Access denied'))
1463
+ return errorResponse('Insufficient permissions', 403);
1464
+ if (msg.includes('not found'))
1465
+ return errorResponse('Document not found', 404);
1466
+ throw err;
1467
+ }
1468
+ const existingMeta = asRecord(issue.metadata);
1469
+ const resolved = await db().seoIssue.update({
1470
+ where: { id: issue.id },
1471
+ data: {
1472
+ status: 'resolved',
1473
+ resolvedAt: new Date(),
1474
+ resolvedById: auth.session.userId,
1475
+ metadata: { ...existingMeta, pendingFix: null },
1476
+ },
1477
+ });
1478
+ try {
1479
+ await logEvent({
1480
+ event: 'document_updated',
1481
+ userId: auth.session.userId,
1482
+ details: {
1483
+ action: 'seo_issue_fix_applied',
1484
+ issueId: issue.id,
1485
+ issueType: issue.type,
1486
+ entityId: loaded.entity.entityId,
1487
+ fields: Object.keys(suggestion.patch),
1488
+ source: suggestion.source ?? 'deterministic',
1489
+ },
1490
+ });
1491
+ }
1492
+ catch {
1493
+ /* noop */
1494
+ }
1495
+ return json({
1496
+ data: {
1497
+ issue: seoIssueToApi(resolved),
1498
+ applied: suggestion.changes,
1499
+ },
1500
+ });
1501
+ }
1502
+ catch (err) {
1503
+ return internalError(err, 'seo/issues/:id/apply-fix');
1504
+ }
1505
+ });
1506
+ router.get('/seo/overview', async (request) => {
1507
+ try {
1508
+ const auth = await requireAuth(request);
1509
+ if (auth.error)
1510
+ return auth.error;
1511
+ const { resolveSeoModuleSettings, getSeoOverrides } = await import('../../seo/config-store.js');
1512
+ const { buildQuickWins } = await import('../../seo/audit-engine.js');
1513
+ const { gradeForSeoScore } = await import('../../seo/score.js');
1514
+ const overrides = await getSeoOverrides(db()).catch(() => null);
1515
+ const moduleSettings = resolveSeoModuleSettings(overrides);
1516
+ let lastRun = null;
1517
+ let openIssues = [];
1518
+ if (hasModel(db(), 'seoAuditRun')) {
1519
+ lastRun = await db().seoAuditRun.findFirst({
1520
+ where: { status: 'COMPLETED' },
1521
+ orderBy: { startedAt: 'desc' },
1522
+ });
1523
+ }
1524
+ if (hasModel(db(), 'seoIssue')) {
1525
+ const rawIssues = await db().seoIssue.findMany({
1526
+ where: { status: { in: ['open', 'recurring'] } },
1527
+ orderBy: { lastDetectedAt: 'desc' },
1528
+ take: 200,
1529
+ });
1530
+ openIssues = sortIssuesBySeverity(rawIssues);
1531
+ }
1532
+ const records = await buildSeoContentRecords().catch(() => []);
1533
+ const indexable = records.filter((r) => !r.noindex).length;
1534
+ const perf = await loadSearchPerformance(10);
1535
+ const summary = {
1536
+ total: openIssues.length,
1537
+ critical: openIssues.filter((i) => i.severity === 'critical').length,
1538
+ warning: openIssues.filter((i) => i.severity === 'warning').length,
1539
+ info: openIssues.filter((i) => i.severity === 'info').length,
1540
+ };
1541
+ const scoreAfter = lastRun?.siteScoreAfter ?? null;
1542
+ const scoreBefore = lastRun?.siteScoreBefore ?? null;
1543
+ const breakdown = lastRun?.metadata?.breakdown ?? [];
1544
+ const avgPosition = perf.length > 0
1545
+ ? perf.reduce((s, r) => s + r.averagePosition, 0) / perf.length
1546
+ : null;
1547
+ const weeklyImpressions = perf.reduce((s, r) => s + r.impressions, 0);
1548
+ const metrics = [
1549
+ {
1550
+ label: 'Pages Indexed',
1551
+ value: String(indexable),
1552
+ helper: `${records.length} total pages`,
1553
+ trend: 'flat',
1554
+ },
1555
+ {
1556
+ label: 'Open Issues',
1557
+ value: String(summary.total),
1558
+ helper: `${summary.critical} critical`,
1559
+ trend: summary.critical > 0 ? 'down' : 'flat',
1560
+ },
1561
+ {
1562
+ label: 'Avg. Position',
1563
+ value: avgPosition !== null ? avgPosition.toFixed(1) : '—',
1564
+ helper: moduleSettings.searchConsoleConnected ? 'last 28 days' : 'Connect Search Console',
1565
+ trend: 'flat',
1566
+ },
1567
+ {
1568
+ label: 'Weekly Impressions',
1569
+ value: weeklyImpressions > 0 ? formatCompactNumber(weeklyImpressions) : '—',
1570
+ helper: moduleSettings.searchConsoleConnected ? 'last 28 days' : 'Connect Search Console',
1571
+ trend: 'flat',
1572
+ },
1573
+ ];
1574
+ const quickWins = buildQuickWins(openIssues.map((i) => ({
1575
+ issueKey: i.issueKey,
1576
+ entityType: i.entityType,
1577
+ entityId: i.entityId,
1578
+ entityTitle: i.entityTitle,
1579
+ url: i.url,
1580
+ severity: i.severity,
1581
+ category: i.category,
1582
+ type: i.type,
1583
+ title: i.title,
1584
+ description: i.description ?? '',
1585
+ impact: i.impact ?? 'Low',
1586
+ recommendation: i.recommendation ?? '',
1587
+ fixable: i.fixable ?? false,
1588
+ fixActionType: i.fixActionType ?? null,
1589
+ relatedEntityIds: Array.isArray(i.relatedEntityIds) ? i.relatedEntityIds : [],
1590
+ }))).map((w) => ({
1591
+ id: w.id,
1592
+ title: w.title,
1593
+ explanation: w.explanation,
1594
+ impact: w.impact,
1595
+ issueId: openIssues.find((i) => i.type === w.issueType)?.id ?? null,
1596
+ fixActionType: w.fixActionType,
1597
+ }));
1598
+ return json({
1599
+ data: {
1600
+ siteScore: {
1601
+ score: scoreAfter ?? 0,
1602
+ grade: gradeForSeoScore(scoreAfter ?? 0),
1603
+ delta: scoreAfter !== null && scoreBefore !== null ? scoreAfter - scoreBefore : null,
1604
+ breakdown,
1605
+ },
1606
+ metrics,
1607
+ issues: openIssues.slice(0, 7).map(seoIssueToApi),
1608
+ issuesSummary: summary,
1609
+ quickWins,
1610
+ searchPerformance: perf,
1611
+ searchConsoleConnected: moduleSettings.searchConsoleConnected,
1612
+ lastAuditRunId: lastRun?.id ?? null,
1613
+ },
1614
+ });
1615
+ }
1616
+ catch (err) {
1617
+ return internalError(err, 'seo/overview');
1618
+ }
1619
+ });
1620
+ router.get('/seo/search-performance', async (request) => {
1621
+ try {
1622
+ const auth = await requireAuth(request);
1623
+ if (auth.error)
1624
+ return auth.error;
1625
+ const url = new URL(request.url);
1626
+ const limit = Math.min(50, Math.max(1, Number(url.searchParams.get('limit') ?? 10)));
1627
+ const rows = await loadSearchPerformance(limit);
1628
+ return json({ data: { rows } });
1629
+ }
1630
+ catch (err) {
1631
+ return internalError(err, 'seo/search-performance');
1632
+ }
1633
+ });
1634
+ router.get('/seo/content', async (request) => {
1635
+ try {
1636
+ const auth = await requireAuth(request);
1637
+ if (auth.error)
1638
+ return auth.error;
1639
+ // Scope the list to the collections this caller may read — otherwise a
1640
+ // collection-scoped API key (or a collection-restricted role) could pull
1641
+ // metaTitle/description/canonical/OG for every collection, the exact
1642
+ // bypass the sibling document-level SEO endpoints already prevent.
1643
+ const allowed = await readableSeoCollections(auth.session);
1644
+ const records = await buildSeoContentRecords(allowed);
1645
+ return json({ data: { records } });
1646
+ }
1647
+ catch (err) {
1648
+ return internalError(err, 'seo/content');
1649
+ }
1650
+ });
1651
+ router.get('/seo/content/:entityType/:entityId', async (request, params) => {
1652
+ try {
1653
+ const auth = await requireAuth(request);
1654
+ if (auth.error)
1655
+ return auth.error;
1656
+ // Load the target first so we can enforce the collection's read-access
1657
+ // rule (requireCollectionScope + assertCollectionAccess) before returning
1658
+ // any SEO fields — parity with /seo/analysis and friends.
1659
+ const doc = await db().document.findFirst({
1660
+ where: { id: params.entityId, deletedAt: null },
1661
+ select: { data: true, collection: true },
1662
+ });
1663
+ if (!doc)
1664
+ return errorResponse('Content record not found', 404);
1665
+ const accessErr = await enforceDocumentReadAccess(auth.session, doc.collection);
1666
+ if (accessErr)
1667
+ return accessErr;
1668
+ const records = await buildSeoContentRecords();
1669
+ const record = records.find((r) => r.entityType === params.entityType && r.entityId === params.entityId);
1670
+ if (!record)
1671
+ return errorResponse('Content record not found', 404);
1672
+ // Overlay the real llms.txt curation values from the stored document
1673
+ // (the audit-entity pipeline that builds the base record doesn't carry
1674
+ // these convention keys).
1675
+ const data = asRecord(doc.data);
1676
+ const includeRaw = String(data.llmsInclude ?? '').toUpperCase();
1677
+ const merged = {
1678
+ ...record,
1679
+ llmsInclude: includeRaw === 'ALWAYS' || includeRaw === 'NEVER'
1680
+ ? includeRaw
1681
+ : 'AUTO',
1682
+ llmsDescription: typeof data.llmsDescription === 'string' ? data.llmsDescription : null,
1683
+ llmsSection: typeof data.llmsSection === 'string' ? data.llmsSection : null,
1684
+ llmsOptional: data.llmsOptional === true,
1685
+ };
1686
+ return json({ data: merged });
1687
+ }
1688
+ catch (err) {
1689
+ return internalError(err, 'seo/content/:id');
1690
+ }
1691
+ });
1692
+ router.put('/seo/content/:entityType/:entityId', async (request, params) => {
1693
+ try {
1694
+ const auth = await requireAuth(request);
1695
+ if (auth.error)
1696
+ return auth.error;
1697
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
1698
+ if (roleErr)
1699
+ return roleErr;
1700
+ const body = (await request.json().catch(() => null));
1701
+ if (!body || typeof body !== 'object') {
1702
+ return errorResponse('Request body must be an object', 400);
1703
+ }
1704
+ const doc = await db().document.findFirst({
1705
+ where: { id: params.entityId, deletedAt: null },
1706
+ select: { id: true, collection: true },
1707
+ });
1708
+ if (!doc)
1709
+ return errorResponse('Document not found', 404);
1710
+ // String SEO fields: payload key -> stored data key, each with a max
1711
+ // length so we never persist unbounded/megabyte values into the content
1712
+ // JSON. Canonical is stored as `canonical` to match normalizeSeoPage.
1713
+ const TEXT_FIELDS = {
1714
+ metaTitle: { target: 'metaTitle', max: 300 },
1715
+ metaDescription: { target: 'metaDescription', max: 1000 },
1716
+ canonicalUrl: { target: 'canonical', max: 2000 },
1717
+ focusKeyword: { target: 'focusKeyword', max: 200 },
1718
+ keyTakeaway: { target: 'keyTakeaway', max: 500 },
1719
+ ogTitle: { target: 'ogTitle', max: 300 },
1720
+ ogDescription: { target: 'ogDescription', max: 1000 },
1721
+ ogImage: { target: 'ogImage', max: 2000 },
1722
+ twitterTitle: { target: 'twitterTitle', max: 300 },
1723
+ twitterDescription: { target: 'twitterDescription', max: 1000 },
1724
+ twitterImage: { target: 'twitterImage', max: 2000 },
1725
+ structuredDataType: { target: 'structuredDataType', max: 100 },
1726
+ schemaType: { target: 'schemaType', max: 100 },
1727
+ llmsDescription: { target: 'llmsDescription', max: 500 },
1728
+ llmsSection: { target: 'llmsSection', max: 100 },
1729
+ };
1730
+ const patch = {};
1731
+ for (const [key, { target, max }] of Object.entries(TEXT_FIELDS)) {
1732
+ if (!(key in body))
1733
+ continue;
1734
+ const value = body[key];
1735
+ if (value === null || value === '') {
1736
+ patch[target] = '';
1737
+ continue;
1738
+ }
1739
+ if (typeof value !== 'string') {
1740
+ return errorResponse(`Field "${key}" must be a string`, 400);
1741
+ }
1742
+ if (value.length > max) {
1743
+ return errorResponse(`Field "${key}" exceeds the ${max}-character limit`, 400);
1744
+ }
1745
+ patch[target] = value;
1746
+ }
1747
+ if ('noindex' in body)
1748
+ patch.noindex = body.noindex === true;
1749
+ if ('nofollow' in body)
1750
+ patch.nofollow = body.nofollow === true;
1751
+ if ('llmsOptional' in body)
1752
+ patch.llmsOptional = body.llmsOptional === true;
1753
+ if ('llmsInclude' in body) {
1754
+ const v = String(body.llmsInclude ?? '').toUpperCase();
1755
+ if (v !== 'AUTO' && v !== 'ALWAYS' && v !== 'NEVER') {
1756
+ return errorResponse('llmsInclude must be one of AUTO, ALWAYS, NEVER', 400);
1757
+ }
1758
+ // AUTO is the default — store empty so the field falls back cleanly.
1759
+ patch.llmsInclude = v === 'AUTO' ? '' : v;
1760
+ }
1761
+ if ('llmsPriority' in body) {
1762
+ const n = body.llmsPriority;
1763
+ if (n !== null && typeof n !== 'number') {
1764
+ return errorResponse('llmsPriority must be a number', 400);
1765
+ }
1766
+ patch.llmsPriority = n ?? 0;
1767
+ }
1768
+ if ('structuredData' in body) {
1769
+ const sd = body.structuredData;
1770
+ if (sd !== null && (typeof sd !== 'object' || Array.isArray(sd))) {
1771
+ return errorResponse('structuredData must be a JSON object', 400);
1772
+ }
1773
+ if (sd && JSON.stringify(sd).length > 50_000) {
1774
+ return errorResponse('structuredData payload is too large', 400);
1775
+ }
1776
+ patch.structuredData = sd;
1777
+ }
1778
+ if (Object.keys(patch).length === 0) {
1779
+ return errorResponse('No recognized SEO fields in request body', 400);
1780
+ }
1781
+ if ('structuredDataType' in patch || 'schemaType' in patch) {
1782
+ Object.assign(patch, syncSchemaTypeFields(patch));
1783
+ }
1784
+ // Route through the data layer so the edit gets per-collection access
1785
+ // control, a Version snapshot, search re-indexing and the afterUpdate
1786
+ // hook — never write document.data directly. (updateDocumentSeoFields
1787
+ // preserves the undeclared SEO convention keys that the generic
1788
+ // updateDocument field-access layer would strip.)
1789
+ const ctx = buildActionContext(auth.session, db());
1790
+ try {
1791
+ await updateDocumentSeoFields(doc.collection, doc.id, patch, ctx);
1792
+ }
1793
+ catch (err) {
1794
+ const msg = err instanceof Error ? err.message : '';
1795
+ if (msg.includes('Access denied'))
1796
+ return errorResponse('Insufficient permissions', 403);
1797
+ if (msg.includes('not found'))
1798
+ return errorResponse('Document not found', 404);
1799
+ throw err;
1800
+ }
1801
+ try {
1802
+ await logEvent({
1803
+ event: 'document_updated',
1804
+ userId: auth.session.userId,
1805
+ details: { action: 'seo_fields_update', documentId: doc.id },
1806
+ });
1807
+ }
1808
+ catch {
1809
+ /* noop */
1810
+ }
1811
+ const records = await buildSeoContentRecords();
1812
+ const record = records.find((r) => r.entityId === doc.id);
1813
+ return json({ data: record ?? { ok: true } });
1814
+ }
1815
+ catch (err) {
1816
+ return internalError(err, 'seo/content PUT');
1817
+ }
1818
+ });
1819
+ // ---------------------------------------------------------------------------
1820
+ // SEO analysis, readability, internal links, schema, meta
1821
+ // ---------------------------------------------------------------------------
1822
+ router.get('/seo/analysis/:documentId', async (request, params) => {
1823
+ try {
1824
+ const auth = await requireAuth(request);
1825
+ if (auth.error)
1826
+ return auth.error;
1827
+ const doc = await db().document.findFirst({
1828
+ where: { id: params.documentId, deletedAt: null },
1829
+ });
1830
+ if (!doc)
1831
+ return errorResponse('Not found', 404);
1832
+ const accessErr = await enforceDocumentReadAccess(auth.session, doc.collection);
1833
+ if (accessErr)
1834
+ return accessErr;
1835
+ const { analyzeContent } = await import('../../seo/analysis.js');
1836
+ const { documentToAuditEntity, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
1837
+ const { applySeoOverrides, getSeoOverrides } = await import('../../seo/config-store.js');
1838
+ const data = doc.data || {};
1839
+ // Same content assembly as readability + audit entity (`documentContentForSeoAnalysis`)
1840
+ // so MCP/admin scores agree and section-built pages still get plainText/sections.
1841
+ const collectionTypes = frontEndContentCollectionTypes(getPrentaConfig());
1842
+ const overrides = await getSeoOverrides(db()).catch(() => null);
1843
+ const siteSeo = applySeoOverrides(getPrentaConfig(), overrides)?.seo;
1844
+ const entity = documentToAuditEntity(doc, collectionTypes, siteSeo);
1845
+ const result = analyzeContent({
1846
+ title: entity.title,
1847
+ slug: doc.slug || data.slug || '',
1848
+ content: entity.content ?? '',
1849
+ // Rendered title (site template applied) — the same value the page
1850
+ // score measures, so both agree instead of one reading the page portion.
1851
+ metaTitle: entity.metaTitle ?? undefined,
1852
+ metaDescription: entity.metaDescription ?? undefined,
1853
+ focusKeyphrase: entity.focusKeyword ?? undefined,
1854
+ canonical: entity.canonicalUrl ?? undefined,
1855
+ ogTitle: entity.ogTitle ?? undefined,
1856
+ ogDescription: entity.ogDescription ?? undefined,
1857
+ ogImage: entity.ogImage ?? undefined,
1858
+ isCornerstone: data.isCornerstone,
1859
+ });
1860
+ return new Response(JSON.stringify(result), {
1861
+ status: 200,
1862
+ headers: { 'Content-Type': 'application/json' },
1863
+ });
1864
+ }
1865
+ catch (err) {
1866
+ return internalError(err, 'seo/analysis');
1867
+ }
1868
+ });
1869
+ router.get('/seo/readability/:documentId', async (request, params) => {
1870
+ try {
1871
+ const auth = await requireAuth(request);
1872
+ if (auth.error)
1873
+ return auth.error;
1874
+ const doc = await db().document.findFirst({
1875
+ where: { id: params.documentId, deletedAt: null },
1876
+ });
1877
+ if (!doc)
1878
+ return errorResponse('Not found', 404);
1879
+ const accessErr = await enforceDocumentReadAccess(auth.session, doc.collection);
1880
+ if (accessErr)
1881
+ return accessErr;
1882
+ const { calculateReadability, stripHtmlTags } = await import('../../seo/analysis.js');
1883
+ const { documentContentForSeoAnalysis } = await import('../../seo/document-content.js');
1884
+ const text = stripHtmlTags(documentContentForSeoAnalysis(doc));
1885
+ const result = calculateReadability(text);
1886
+ return new Response(JSON.stringify(result), {
1887
+ status: 200,
1888
+ headers: { 'Content-Type': 'application/json' },
1889
+ });
1890
+ }
1891
+ catch (err) {
1892
+ return internalError(err, 'seo/readability');
1893
+ }
1894
+ });
1895
+ router.get('/seo/internal-links/:documentId', async (request, params) => {
1896
+ try {
1897
+ const auth = await requireAuth(request);
1898
+ if (auth.error)
1899
+ return auth.error;
1900
+ const doc = await db().document.findFirst({
1901
+ where: { id: params.documentId, deletedAt: null },
1902
+ });
1903
+ if (!doc)
1904
+ return errorResponse('Not found', 404);
1905
+ const accessErr = await enforceDocumentReadAccess(auth.session, doc.collection);
1906
+ if (accessErr)
1907
+ return accessErr;
1908
+ const data = doc.data || {};
1909
+ const title = doc.title || data.title || '';
1910
+ const keywords = title
1911
+ .split(/\s+/)
1912
+ .filter((w) => w.length > 3)
1913
+ .slice(0, 5);
1914
+ if (keywords.length === 0) {
1915
+ return new Response(JSON.stringify({ suggestions: [] }), {
1916
+ status: 200,
1917
+ headers: { 'Content-Type': 'application/json' },
1918
+ });
1919
+ }
1920
+ const suggestions = await db().document.findMany({
1921
+ where: {
1922
+ id: { not: params.documentId },
1923
+ status: 'PUBLISHED',
1924
+ deletedAt: null,
1925
+ OR: keywords.map((kw) => ({
1926
+ title: { contains: kw, mode: 'insensitive' },
1927
+ })),
1928
+ },
1929
+ select: { id: true, title: true, slug: true, collection: true },
1930
+ take: 10,
1931
+ orderBy: { updatedAt: 'desc' },
1932
+ });
1933
+ return new Response(JSON.stringify({ suggestions }), {
1934
+ status: 200,
1935
+ headers: { 'Content-Type': 'application/json' },
1936
+ });
1937
+ }
1938
+ catch (err) {
1939
+ return internalError(err, 'seo/internal-links');
1940
+ }
1941
+ });
1942
+ router.get('/llms.txt', async (request) => {
1943
+ try {
1944
+ // Public surface: must exclude soft-deleted docs and internal/system
1945
+ // collections (e.g. `__seo_config__`). Over-fetch then filter so the
1946
+ // 50-page cap still lands on real content after exclusion.
1947
+ const { isInternalCollection } = await import('../../seo/audit-runner.js');
1948
+ const { siteUrlFromRequest, loadEffectiveConfig } = await import('./seo-public.js');
1949
+ // Layer DB overrides (Settings → SEO) so an admin-edited siteName /
1950
+ // defaultMetaDescription flows into the llms heading + summary too, exactly
1951
+ // as sitemap.xml, robots.txt, /og.png and the feeds already do.
1952
+ const cfg = (await loadEffectiveConfig()) ?? getPrentaConfig();
1953
+ const llmsCfg = cfg?.seo?.llms;
1954
+ const base = siteUrlFromRequest(request, cfg);
1955
+ // Operators can disable the file entirely (it's optionality, not a
1956
+ // guaranteed traffic channel).
1957
+ if (llmsCfg?.disabled) {
1958
+ return new Response('Not found', { status: 404 });
1959
+ }
1960
+ const maxEntries = llmsCfg?.maxEntries ?? 50;
1961
+ const maxPerSection = llmsCfg?.maxPerSection ?? 25;
1962
+ // Restrict AUTO selection to the configured high-value collections when
1963
+ // set, so blog archives don't flood the curated guide.
1964
+ const eligible = Array.isArray(llmsCfg?.eligibleCollections) && llmsCfg.eligibleCollections.length > 0
1965
+ ? new Set(llmsCfg.eligibleCollections)
1966
+ : null;
1967
+ const rawDocs = await db().document.findMany({
1968
+ where: { status: 'PUBLISHED', deletedAt: null },
1969
+ select: { title: true, slug: true, collection: true, updatedAt: true, data: true },
1970
+ orderBy: { updatedAt: 'desc' },
1971
+ take: 500,
1972
+ });
1973
+ // Per-doc curation + privacy boundary (noindex exclusion) live in the
1974
+ // shared selector so `/llms.txt` and `/llms-full.txt` can never diverge.
1975
+ const { selectLlmsDocs, toLlmsPage } = await import('../../seo/llms-select.js');
1976
+ const selected = selectLlmsDocs(rawDocs, { isInternalCollection, eligible });
1977
+ const { generateLlmsTxt } = await import('../../seo/llms-txt.js');
1978
+ const pages = selected.map((d) => toLlmsPage(d, { base, urlPrefix: cfg?.collections?.[d.collection]?.urlPrefix }));
1979
+ const txt = generateLlmsTxt({
1980
+ siteName: cfg?.seo?.siteName || 'Prenta CMS',
1981
+ siteUrl: base,
1982
+ siteDescription: llmsCfg?.summary || cfg?.seo?.defaultMetaDescription || 'Content managed by Prenta CMS',
1983
+ intro: llmsCfg?.intro,
1984
+ maxPages: maxEntries,
1985
+ maxPerSection,
1986
+ }, pages);
1987
+ // Opt-in honeypot: append a unique trap link. No human reads llms.txt, so
1988
+ // a fetch of this URL is an automated reader — recorded under the
1989
+ // "honeypot" surface in the AI-crawler panel.
1990
+ let body = txt;
1991
+ if (llmsCfg?.honeypot) {
1992
+ const trapUrl = `${base.replace(/\/$/, '')}/api/cms/ai-crawlers/trap`;
1993
+ body += `\n## Reference\n\n- [Complete content index](${trapUrl}): Full machine-readable content map.\n`;
1994
+ }
1995
+ // Measurement: if a known AI crawler fetched the index, count it.
1996
+ await recordAiCrawlerFromRequest(db(), request, 'llms');
1997
+ return new Response(body, {
1998
+ status: 200,
1999
+ headers: {
2000
+ 'Content-Type': 'text/plain; charset=utf-8',
2001
+ // The file itself is a machine-facing index, not a page to rank.
2002
+ 'X-Robots-Tag': 'noindex',
2003
+ },
2004
+ });
2005
+ }
2006
+ catch (err) {
2007
+ return internalError(err, 'llms.txt');
2008
+ }
2009
+ });
2010
+ // `/llms-full.txt` — same curated selection as `/llms.txt`, but inlines each
2011
+ // page's content rendered to clean Markdown so an agent can read the actual
2012
+ // body without fetching every URL. Opt-in (these files get large) and bounded
2013
+ // by a byte budget. noindex + never in the sitemap; HTML stays canonical.
2014
+ router.get('/llms-full.txt', async (request) => {
2015
+ try {
2016
+ const { isInternalCollection } = await import('../../seo/audit-runner.js');
2017
+ const { siteUrlFromRequest, loadEffectiveConfig } = await import('./seo-public.js');
2018
+ // Honor admin overrides (siteName / summary / defaultMetaDescription) the
2019
+ // same way `/llms.txt` and the other public SEO surfaces do.
2020
+ const cfg = (await loadEffectiveConfig()) ?? getPrentaConfig();
2021
+ const llmsCfg = cfg?.seo?.llms;
2022
+ const base = siteUrlFromRequest(request, cfg);
2023
+ // Gated independently from `/llms.txt`: must be both enabled overall and
2024
+ // have the full variant turned on.
2025
+ if (llmsCfg?.disabled || !llmsCfg?.fullEnabled) {
2026
+ return new Response('Not found', { status: 404 });
2027
+ }
2028
+ const maxEntries = llmsCfg?.maxEntries ?? 50;
2029
+ const maxBytes = llmsCfg?.maxFullBytes ?? 2_000_000;
2030
+ const eligible = Array.isArray(llmsCfg?.eligibleCollections) && llmsCfg.eligibleCollections.length > 0
2031
+ ? new Set(llmsCfg.eligibleCollections)
2032
+ : null;
2033
+ const rawDocs = await db().document.findMany({
2034
+ where: { status: 'PUBLISHED', deletedAt: null },
2035
+ select: {
2036
+ title: true,
2037
+ slug: true,
2038
+ collection: true,
2039
+ updatedAt: true,
2040
+ data: true,
2041
+ plainText: true,
2042
+ },
2043
+ orderBy: { updatedAt: 'desc' },
2044
+ take: 500,
2045
+ });
2046
+ const { selectLlmsDocs, toLlmsPage } = await import('../../seo/llms-select.js');
2047
+ const { documentToMarkdown } = await import('../../seo/content-markdown.js');
2048
+ const selected = selectLlmsDocs(rawDocs, { isInternalCollection, eligible }).slice(0, maxEntries);
2049
+ const header = [
2050
+ `# ${cfg?.seo?.siteName || 'Prenta CMS'}`,
2051
+ '',
2052
+ `> ${llmsCfg?.summary || cfg?.seo?.defaultMetaDescription || 'Full content export for AI agents.'}`,
2053
+ '',
2054
+ ].join('\n');
2055
+ const chunks = [header];
2056
+ let bytes = Buffer.byteLength(header, 'utf8');
2057
+ let truncated = false;
2058
+ for (const d of selected) {
2059
+ const page = toLlmsPage(d, {
2060
+ base,
2061
+ urlPrefix: cfg?.collections?.[d.collection]?.urlPrefix,
2062
+ });
2063
+ const md = documentToMarkdown({
2064
+ title: d.title,
2065
+ data: d.data,
2066
+ plainText: d.plainText,
2067
+ }, { baseUrl: base });
2068
+ // Each entry is delimited by a horizontal rule and carries its canonical
2069
+ // URL so the inlined content stays traceable back to the live page.
2070
+ const block = `\n---\n\nURL: ${page.url}\n\n${md}\n`;
2071
+ const blockBytes = Buffer.byteLength(block, 'utf8');
2072
+ if (bytes + blockBytes > maxBytes) {
2073
+ truncated = true;
2074
+ break;
2075
+ }
2076
+ chunks.push(block);
2077
+ bytes += blockBytes;
2078
+ }
2079
+ if (truncated) {
2080
+ chunks.push('\n---\n\n> Output truncated at the configured size limit.\n');
2081
+ }
2082
+ await recordAiCrawlerFromRequest(db(), request, 'llms-full');
2083
+ return new Response(chunks.join(''), {
2084
+ status: 200,
2085
+ headers: {
2086
+ 'Content-Type': 'text/plain; charset=utf-8',
2087
+ 'X-Robots-Tag': 'noindex',
2088
+ },
2089
+ });
2090
+ }
2091
+ catch (err) {
2092
+ return internalError(err, 'llms-full.txt');
2093
+ }
2094
+ });
2095
+ // Per-page Markdown: `GET /md?path=/about` returns a clean Markdown rendering
2096
+ // of a single published page so an agent can fetch the content, not just the
2097
+ // link. Fresh installs expose this at clean `/{path}.md` URLs via a scaffolded
2098
+ // rewrite. CRITICAL: always noindex, never in the sitemap — the HTML page
2099
+ // stays canonical (per-page `.md` is the most common duplicate-content trap).
2100
+ router.get('/md', async (request) => {
2101
+ try {
2102
+ const cfg = getPrentaConfig();
2103
+ const url = new URL(request.url);
2104
+ const pathParam = url.searchParams.get('path');
2105
+ if (!pathParam) {
2106
+ return errorResponse('Missing required "path" query parameter', 400);
2107
+ }
2108
+ // Part of the AI/agent layer — honor the same master switch as llms.txt.
2109
+ if (cfg?.seo?.llms?.disabled) {
2110
+ return new Response('Not found', { status: 404 });
2111
+ }
2112
+ const { matchPathToCollection } = await import('../../seo/resolve-path.js');
2113
+ const match = matchPathToCollection(cfg, pathParam);
2114
+ if (!match) {
2115
+ return new Response('Not found', { status: 404 });
2116
+ }
2117
+ const slugFilter = match.isRoot
2118
+ ? {
2119
+ OR: [
2120
+ { data: { path: ['slug'], equals: 'home' } },
2121
+ { data: { path: ['slug'], equals: 'index' } },
2122
+ { slug: 'home' },
2123
+ { slug: 'index' },
2124
+ ],
2125
+ }
2126
+ : {
2127
+ OR: [{ data: { path: ['slug'], equals: match.slug } }, { slug: match.slug }],
2128
+ };
2129
+ const doc = await db().document.findFirst({
2130
+ where: {
2131
+ collection: match.collection,
2132
+ deletedAt: null,
2133
+ status: 'PUBLISHED',
2134
+ ...slugFilter,
2135
+ },
2136
+ select: { title: true, data: true, plainText: true },
2137
+ });
2138
+ if (!doc) {
2139
+ return new Response('Not found', { status: 404 });
2140
+ }
2141
+ const data = doc.data || {};
2142
+ // The selection filter is the privacy boundary: noindexed or explicitly
2143
+ // excluded pages must not be fetchable as Markdown either.
2144
+ const { isLlmsNoindex, readLlmsInclude } = await import('../../seo/llms-select.js');
2145
+ if (isLlmsNoindex(data) || readLlmsInclude(data) === 'NEVER') {
2146
+ return new Response('Not found', { status: 404 });
2147
+ }
2148
+ const { documentToMarkdown } = await import('../../seo/content-markdown.js');
2149
+ const { siteUrlFromRequest } = await import('./seo-public.js');
2150
+ const base = siteUrlFromRequest(request, cfg);
2151
+ const md = documentToMarkdown({ title: doc.title, data, plainText: doc.plainText }, { baseUrl: base });
2152
+ await recordAiCrawlerFromRequest(db(), request, 'md');
2153
+ return new Response(md, {
2154
+ status: 200,
2155
+ headers: {
2156
+ 'Content-Type': 'text/markdown; charset=utf-8',
2157
+ 'X-Robots-Tag': 'noindex',
2158
+ },
2159
+ });
2160
+ }
2161
+ catch (err) {
2162
+ return internalError(err, 'md');
2163
+ }
2164
+ });
2165
+ // Honeypot: a unique trap URL appended to llms.txt only when
2166
+ // `seo.llms.honeypot` is enabled. No human visits it, so any fetch is an
2167
+ // automated reader that followed a link from the file. Always records (even
2168
+ // if the flag was later turned off, a cached llms.txt may still point here)
2169
+ // and returns a tiny noindex body.
2170
+ router.get('/ai-crawlers/trap', async (request) => {
2171
+ try {
2172
+ const detected = detectAiBot(request.headers.get('user-agent'));
2173
+ await recordAiCrawlerHit(db(), {
2174
+ bot: detected?.token ?? 'unknown',
2175
+ surface: 'honeypot',
2176
+ category: detected?.category ?? 'other',
2177
+ });
2178
+ return new Response('This endpoint exists to detect automated indexers.\n', {
2179
+ status: 200,
2180
+ headers: {
2181
+ 'Content-Type': 'text/plain; charset=utf-8',
2182
+ 'X-Robots-Tag': 'noindex',
2183
+ },
2184
+ });
2185
+ }
2186
+ catch (err) {
2187
+ return internalError(err, 'ai-crawlers/trap');
2188
+ }
2189
+ });
2190
+ // Admin: aggregated AI-crawler activity for the SEO → Technical panel.
2191
+ // Any authenticated admin may read it (analytics, not a mutation).
2192
+ router.get('/ai-crawlers', async (request) => {
2193
+ try {
2194
+ const auth = await requireAuth(request);
2195
+ if (auth.error)
2196
+ return auth.error;
2197
+ if (!hasModel(db(), 'aiCrawlerHit')) {
2198
+ return json({
2199
+ data: { enabled: false, rows: [], totals: { hits: 0, bots: 0, honeypot: 0 } },
2200
+ });
2201
+ }
2202
+ const rows = await db().aiCrawlerHit.findMany({
2203
+ orderBy: { lastSeenAt: 'desc' },
2204
+ take: 500,
2205
+ });
2206
+ const totalHits = rows.reduce((sum, r) => sum + Number(r.hits ?? 0), 0);
2207
+ const honeypotHits = rows
2208
+ .filter((r) => r.surface === 'honeypot')
2209
+ .reduce((sum, r) => sum + Number(r.hits ?? 0), 0);
2210
+ const uniqueBots = new Set(rows.map((r) => r.bot)).size;
2211
+ // Recency windowing. `hits` is a lifetime cumulative counter (one row per
2212
+ // bot×surface), so we can't derive per-window hit *counts* from it — but
2213
+ // `lastSeenAt` lets us answer the actionable question "which bots/surfaces
2214
+ // were active in the last 24h / 7d". Per-window hit-count trends would
2215
+ // need a (bot, surface, day) bucket table (a future additive migration);
2216
+ // this keeps storage bounded while making recency visible today.
2217
+ const nowMs = Date.now();
2218
+ const DAY_MS = 24 * 60 * 60 * 1000;
2219
+ const within = (v, ms) => {
2220
+ if (!v)
2221
+ return false;
2222
+ const t = v instanceof Date ? v.getTime() : new Date(v).getTime();
2223
+ return Number.isFinite(t) && nowMs - t <= ms;
2224
+ };
2225
+ const windowSummary = (ms) => {
2226
+ const active = rows.filter((r) => within(r.lastSeenAt, ms));
2227
+ return {
2228
+ bots: new Set(active.map((r) => r.bot)).size,
2229
+ surfaces: new Set(active.map((r) => r.surface)).size,
2230
+ rows: active.length,
2231
+ };
2232
+ };
2233
+ return json({
2234
+ data: {
2235
+ enabled: true,
2236
+ rows: rows.map((r) => ({
2237
+ bot: r.bot,
2238
+ surface: r.surface,
2239
+ category: r.category,
2240
+ hits: Number(r.hits ?? 0),
2241
+ firstSeenAt: r.firstSeenAt,
2242
+ lastSeenAt: r.lastSeenAt,
2243
+ activeLast24h: within(r.lastSeenAt, DAY_MS),
2244
+ activeLast7d: within(r.lastSeenAt, 7 * DAY_MS),
2245
+ })),
2246
+ totals: { hits: totalHits, bots: uniqueBots, honeypot: honeypotHits },
2247
+ windows: { last24h: windowSummary(DAY_MS), last7d: windowSummary(7 * DAY_MS) },
2248
+ },
2249
+ });
2250
+ }
2251
+ catch (err) {
2252
+ return internalError(err, 'ai-crawlers');
2253
+ }
2254
+ });
2255
+ }
2256
+ //# sourceMappingURL=seo.js.map