blume 2.0.3 → 2.1.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 (455) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-1d7ve1dm.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-1d7ve1dm.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-2eytanqx.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-yfyb25rh.js → chunk-2f3ndkpw.js} +24 -8
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-r20tn01b.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-r20tn01b.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-35d4wj9f.js → chunk-2n4ps1b3.js} +7 -7
  12. package/dist/cli/{chunk-j85vccga.js → chunk-43zcf4gz.js} +97 -57
  13. package/dist/cli/{chunk-j85vccga.js.map → chunk-43zcf4gz.js.map} +6 -6
  14. package/dist/cli/{chunk-e04dxsz1.js → chunk-4ejxbzee.js} +1 -1
  15. package/dist/cli/{chunk-5m5nmvyq.js → chunk-4h8z9vv5.js} +233 -39
  16. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  17. package/dist/cli/{chunk-5f86nr5m.js → chunk-5cg2rzgf.js} +18 -16
  18. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-5cg2rzgf.js.map} +2 -2
  19. package/dist/cli/{chunk-tzmab476.js → chunk-5ffywsmb.js} +4 -4
  20. package/dist/cli/{chunk-2z928egk.js → chunk-5wj2qwn2.js} +5 -5
  21. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  22. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  23. package/dist/cli/{chunk-vg9r4eb9.js → chunk-713mngdq.js} +12 -5
  24. package/dist/cli/{chunk-vg9r4eb9.js.map → chunk-713mngdq.js.map} +3 -3
  25. package/dist/cli/{chunk-5xvm6tfj.js → chunk-9geymqns.js} +17 -15
  26. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-9geymqns.js.map} +2 -2
  27. package/dist/cli/{chunk-q58y5e6a.js → chunk-9xr4dgee.js} +26 -17
  28. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  29. package/dist/cli/{chunk-b07cmahc.js → chunk-amrb6hgh.js} +2 -2
  30. package/dist/cli/chunk-cm89scdv.js +443 -0
  31. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  32. package/dist/cli/{chunk-akbpwfxc.js → chunk-cmtcnh9f.js} +2 -2
  33. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  34. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  35. package/dist/cli/{chunk-crgn1q09.js → chunk-e7vvk5ya.js} +4 -4
  36. package/dist/cli/{chunk-pehfxfta.js → chunk-g2z9x01v.js} +6 -98
  37. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  38. package/dist/cli/{chunk-tkacnehg.js → chunk-gp84za0h.js} +2 -2
  39. package/dist/cli/{chunk-wrr3j9w9.js → chunk-h2a48yhd.js} +186 -63
  40. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  41. package/dist/cli/{chunk-hm3vjy5s.js → chunk-jnwj5kjj.js} +14 -9
  42. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-jnwj5kjj.js.map} +4 -4
  43. package/dist/cli/chunk-kwrg63tm.js +31 -0
  44. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  45. package/dist/cli/{chunk-2hsdwb9n.js → chunk-nd1821sk.js} +18 -16
  46. package/dist/cli/{chunk-2hsdwb9n.js.map → chunk-nd1821sk.js.map} +2 -2
  47. package/dist/cli/{chunk-6dsbexzp.js → chunk-ntv4qrt7.js} +17 -15
  48. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-ntv4qrt7.js.map} +2 -2
  49. package/dist/cli/{chunk-r9rcc4w7.js → chunk-p804f3pe.js} +7 -7
  50. package/dist/cli/{chunk-ey84smr6.js → chunk-py62fewh.js} +6 -6
  51. package/dist/cli/{chunk-ey84smr6.js.map → chunk-py62fewh.js.map} +1 -1
  52. package/dist/cli/chunk-q3wxxq39.js +126 -0
  53. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  54. package/dist/cli/{chunk-9t7a85s3.js → chunk-rekqz4qg.js} +23 -13
  55. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  56. package/dist/cli/{chunk-8ktnccpt.js → chunk-ta27feev.js} +1 -1
  57. package/dist/cli/{chunk-pv29h0wf.js → chunk-xkz5xbtv.js} +3476 -822
  58. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  59. package/dist/cli/{chunk-a58773jm.js → chunk-y4ykbark.js} +2 -2
  60. package/dist/cli/{chunk-p3v96n38.js → chunk-y5h4r2y0.js} +4 -4
  61. package/dist/cli/{chunk-acanzt5p.js → chunk-yj17vs39.js} +6 -6
  62. package/dist/cli/{chunk-p73c0m7w.js → chunk-yxmk4pec.js} +16 -14
  63. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-yxmk4pec.js.map} +2 -2
  64. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  65. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  66. package/dist/cli/{chunk-c8chx29p.js → chunk-zf748avv.js} +384 -207
  67. package/dist/cli/chunk-zf748avv.js.map +37 -0
  68. package/dist/cli/{chunk-g4hq16wv.js → chunk-zhvd6adm.js} +16 -14
  69. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-zhvd6adm.js.map} +2 -2
  70. package/dist/cli/index.js +26 -20
  71. package/dist/cli/index.js.map +5 -5
  72. package/dist/types/ai/agent-surface.d.ts +8 -7
  73. package/dist/types/ai/ai-catalog.d.ts +7 -0
  74. package/dist/types/ai/ask-context.d.ts +22 -0
  75. package/dist/types/ai/ask.d.ts +271 -58
  76. package/dist/types/ai/index.d.ts +2 -2
  77. package/dist/types/ai/site-skill.d.ts +26 -0
  78. package/dist/types/ai/skills.d.ts +16 -0
  79. package/dist/types/astro/integration.d.ts +8 -0
  80. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  81. package/dist/types/captcha/index.d.ts +28 -0
  82. package/dist/types/captcha/schema.d.ts +29 -0
  83. package/dist/types/captcha/turnstile.d.ts +28 -0
  84. package/dist/types/components/content/api-field.d.ts +59 -0
  85. package/dist/types/components/content/api-page.d.ts +95 -0
  86. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  87. package/dist/types/components/openapi/helpers.d.ts +137 -0
  88. package/dist/types/components/openapi/security.d.ts +85 -0
  89. package/dist/types/consent/ethyca.d.ts +48 -0
  90. package/dist/types/consent/index.d.ts +37 -0
  91. package/dist/types/consent/native.d.ts +28 -0
  92. package/dist/types/consent/osano.d.ts +42 -0
  93. package/dist/types/consent/schema.d.ts +110 -0
  94. package/dist/types/core/config-input.d.ts +187 -20
  95. package/dist/types/core/data.d.ts +49 -4
  96. package/dist/types/core/footer.d.ts +8 -0
  97. package/dist/types/core/i18n-ui.d.ts +86 -0
  98. package/dist/types/core/includes.d.ts +8 -2
  99. package/dist/types/core/localizable.d.ts +7 -0
  100. package/dist/types/core/page-modes.d.ts +29 -0
  101. package/dist/types/core/redirect-patterns.d.ts +144 -0
  102. package/dist/types/core/related.d.ts +40 -0
  103. package/dist/types/core/schema.d.ts +551 -56
  104. package/dist/types/core/sources/read.d.ts +5 -0
  105. package/dist/types/core/types.d.ts +1 -1
  106. package/dist/types/core/variables.d.ts +42 -0
  107. package/dist/types/deploy/node-headers.d.ts +13 -2
  108. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  109. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  110. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  111. package/dist/types/deploy/redirects.d.ts +39 -11
  112. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  113. package/dist/types/markdown/code-title.d.ts +17 -0
  114. package/dist/types/markdown/features.d.ts +3 -2
  115. package/dist/types/narration/build.d.ts +70 -0
  116. package/dist/types/narration/provider.d.ts +26 -0
  117. package/dist/types/narration/script.d.ts +154 -0
  118. package/dist/types/openapi/model.d.ts +12 -6
  119. package/dist/types/openapi/overlay.d.ts +54 -0
  120. package/dist/types/openapi/parse.d.ts +13 -0
  121. package/dist/types/openapi/references.d.ts +14 -4
  122. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  123. package/dist/types/ratelimit/index.d.ts +27 -0
  124. package/dist/types/ratelimit/memory.d.ts +36 -0
  125. package/dist/types/ratelimit/schema.d.ts +111 -0
  126. package/dist/types/ratelimit/upstash.d.ts +21 -0
  127. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  128. package/dist/types/reference/asyncapi.d.ts +10 -10
  129. package/dist/types/reference/graphql.d.ts +13 -10
  130. package/dist/types/reference/openapi.d.ts +33 -12
  131. package/dist/types/reference/options.d.ts +45 -5
  132. package/dist/types/reference/scalar.d.ts +13 -0
  133. package/dist/types/reference/schema.d.ts +60 -36
  134. package/dist/types/search/documents.d.ts +12 -1
  135. package/dist/types/search/orama-index.d.ts +4 -0
  136. package/dist/types/seo/metatags.d.ts +20 -0
  137. package/docs/02-deployment.mdx +41 -5
  138. package/docs/03-upgrading.mdx +4 -4
  139. package/docs/04-migrating.mdx +1 -1
  140. package/docs/advanced/changelog.mdx +2 -2
  141. package/docs/advanced/skills.mdx +14 -0
  142. package/docs/cli/audit.mdx +21 -1
  143. package/docs/cli/index.mdx +1 -0
  144. package/docs/configuration/analytics.mdx +3 -1
  145. package/docs/configuration/assistant.mdx +188 -36
  146. package/docs/configuration/consent.mdx +100 -0
  147. package/docs/configuration/customization.mdx +4 -4
  148. package/docs/configuration/index.mdx +63 -5
  149. package/docs/configuration/meta.ts +3 -0
  150. package/docs/configuration/narration.mdx +119 -0
  151. package/docs/configuration/rate-limiting.mdx +69 -0
  152. package/docs/configuration/search.mdx +29 -1
  153. package/docs/configuration/theming.mdx +3 -1
  154. package/docs/content/components.mdx +106 -0
  155. package/docs/content/frontmatter.mdx +76 -0
  156. package/docs/content/i18n.mdx +38 -3
  157. package/docs/content/includes.mdx +16 -0
  158. package/docs/content/meta.mdx +1 -0
  159. package/docs/content/meta.ts +1 -0
  160. package/docs/content/navigation.mdx +28 -6
  161. package/docs/content/sources/contentful.mdx +32 -0
  162. package/docs/content/sources/custom.mdx +39 -0
  163. package/docs/content/sources/github-releases.mdx +31 -0
  164. package/docs/content/sources/index.mdx +91 -0
  165. package/docs/content/sources/meta.ts +16 -0
  166. package/docs/content/sources/notion.mdx +32 -0
  167. package/docs/content/sources/obsidian.mdx +37 -0
  168. package/docs/content/sources/payload.mdx +30 -0
  169. package/docs/content/sources/remote-mdx.mdx +40 -0
  170. package/docs/content/sources/sanity.mdx +29 -0
  171. package/docs/content/sources/strapi.mdx +31 -0
  172. package/docs/content/syntax.mdx +67 -2
  173. package/docs/content/variables.mdx +41 -0
  174. package/docs/discoverability/agent-discovery.mdx +29 -2
  175. package/docs/discoverability/index.mdx +1 -1
  176. package/docs/discoverability/llms-txt.mdx +1 -1
  177. package/docs/discoverability/metadata.mdx +17 -0
  178. package/docs/references/api-pages.mdx +74 -0
  179. package/docs/references/asyncapi.mdx +1 -1
  180. package/docs/references/graphql.mdx +2 -2
  181. package/docs/references/meta.ts +1 -1
  182. package/docs/references/openapi.mdx +102 -3
  183. package/docs/references/scalar.mdx +3 -1
  184. package/package.json +36 -1
  185. package/skills/blume/SKILL.md +3 -0
  186. package/skills/blume-migrate/SKILL.md +5 -5
  187. package/skills/blume-migrate/references/docusaurus.md +3 -3
  188. package/skills/blume-migrate/references/mintlify.md +37 -19
  189. package/skills/blume-migrate/references/nextra.md +2 -2
  190. package/skills/blume-migrate/references/starlight.md +2 -1
  191. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  192. package/skills/blume-write-skill/SKILL.md +82 -0
  193. package/src/ai/agent-readability.ts +2 -1
  194. package/src/ai/agent-surface.ts +9 -7
  195. package/src/ai/ai-catalog.ts +6 -3
  196. package/src/ai/ask-context.ts +73 -20
  197. package/src/ai/ask-data.ts +6 -0
  198. package/src/ai/ask-tools.ts +142 -0
  199. package/src/ai/ask.ts +354 -76
  200. package/src/ai/component-markdown.ts +75 -0
  201. package/src/ai/cors.ts +2 -0
  202. package/src/ai/index.ts +12 -0
  203. package/src/ai/llms.ts +6 -2
  204. package/src/ai/markdown.ts +2 -1
  205. package/src/ai/mcp/data.ts +6 -0
  206. package/src/ai/openapi-components.ts +9 -3
  207. package/src/ai/site-skill.ts +274 -0
  208. package/src/ai/skills.ts +35 -2
  209. package/src/ai/write-skill.ts +97 -0
  210. package/src/astro/generate.ts +132 -18
  211. package/src/astro/index.ts +1 -0
  212. package/src/astro/integration.ts +35 -0
  213. package/src/astro/module-types.ts +6 -0
  214. package/src/astro/templates.ts +283 -30
  215. package/src/astro/variables.ts +31 -0
  216. package/src/audit/catalog.ts +19 -0
  217. package/src/audit/checks/contrast.ts +183 -0
  218. package/src/audit/checks/i18n.ts +2 -3
  219. package/src/audit/checks/indexability.ts +2 -3
  220. package/src/audit/checks/links.ts +5 -12
  221. package/src/audit/checks/sitemap.ts +2 -3
  222. package/src/audit/redirects.ts +46 -15
  223. package/src/audit/run.ts +2 -0
  224. package/src/audit/types.ts +2 -1
  225. package/src/blume-modules.d.ts +7 -0
  226. package/src/captcha/client.ts +152 -0
  227. package/src/captcha/hcaptcha.ts +36 -0
  228. package/src/captcha/index.ts +28 -0
  229. package/src/captcha/schema.ts +32 -0
  230. package/src/captcha/turnstile.ts +38 -0
  231. package/src/captcha/verify.ts +71 -0
  232. package/src/cli/command-meta.ts +5 -0
  233. package/src/cli/commands/doctor.ts +22 -16
  234. package/src/cli/commands/skill.ts +96 -0
  235. package/src/cli/index.ts +5 -0
  236. package/src/cli/required-secrets.ts +71 -29
  237. package/src/components/BlumePage.astro +2 -0
  238. package/src/components/content/ApiEndpoint.astro +18 -0
  239. package/src/components/content/ApiField.astro +79 -0
  240. package/src/components/content/ApiPlayground.astro +62 -0
  241. package/src/components/content/ApiRail.astro +19 -0
  242. package/src/components/content/Color.astro +1 -0
  243. package/src/components/content/FileTree.astro +1 -0
  244. package/src/components/content/GithubInfo.astro +1 -0
  245. package/src/components/content/Panel.astro +1 -0
  246. package/src/components/content/ParamField.astro +25 -0
  247. package/src/components/content/RequestExample.astro +17 -0
  248. package/src/components/content/ResponseExample.astro +17 -0
  249. package/src/components/content/ResponseField.astro +32 -0
  250. package/src/components/content/Step.astro +1 -0
  251. package/src/components/content/Tile.astro +1 -0
  252. package/src/components/content/Tooltip.astro +1 -0
  253. package/src/components/content/Tree.astro +1 -0
  254. package/src/components/content/TypeTable.astro +4 -1
  255. package/src/components/content/View.astro +21 -0
  256. package/src/components/content/ViewSwitcher.astro +168 -0
  257. package/src/components/content/api-field.ts +100 -0
  258. package/src/components/content/api-page.ts +224 -0
  259. package/src/components/content/view-switcher.ts +71 -0
  260. package/src/components/index.ts +2 -0
  261. package/src/components/islands/Assistant.astro +10 -1
  262. package/src/components/islands/assistant.tsx +104 -14
  263. package/src/components/islands/code-question.ts +62 -0
  264. package/src/components/islands/hooks.ts +110 -21
  265. package/src/components/islands/support-link.ts +68 -0
  266. package/src/components/layout/Analytics.astro +11 -2
  267. package/src/components/layout/Banner.astro +14 -10
  268. package/src/components/layout/Breadcrumbs.astro +6 -2
  269. package/src/components/layout/ConsentBanner.astro +62 -0
  270. package/src/components/layout/ConsentHead.astro +38 -0
  271. package/src/components/layout/Directory.astro +94 -0
  272. package/src/components/layout/DrawerSwitchers.astro +51 -0
  273. package/src/components/layout/Empty.astro +2 -2
  274. package/src/components/layout/Header.astro +122 -80
  275. package/src/components/layout/LanguageSwitcher.astro +48 -15
  276. package/src/components/layout/NarrationPlayer.astro +1085 -0
  277. package/src/components/layout/NavSelector.astro +33 -7
  278. package/src/components/layout/PageFeedback.astro +54 -49
  279. package/src/components/layout/PageLayout.astro +71 -3
  280. package/src/components/layout/ReferenceLayout.astro +17 -6
  281. package/src/components/layout/RelatedPages.astro +42 -0
  282. package/src/components/layout/RootLayout.astro +308 -45
  283. package/src/components/layout/Search.astro +31 -17
  284. package/src/components/layout/SiteFooter.astro +124 -0
  285. package/src/components/layout/SiteMetatags.astro +16 -0
  286. package/src/components/layout/consent/native.ts +79 -0
  287. package/src/components/layout/consent/types.ts +37 -0
  288. package/src/components/layout/footer-links.ts +53 -0
  289. package/src/components/layout/footer-socials.ts +87 -0
  290. package/src/components/layout/locale-redirect.ts +107 -0
  291. package/src/components/layout/nav-utils.ts +83 -0
  292. package/src/components/layout/page-feedback.ts +96 -0
  293. package/src/components/layout/search/analytics.ts +98 -0
  294. package/src/components/layout/search/flexsearch.ts +17 -5
  295. package/src/components/layout/search/orama-cloud.ts +26 -14
  296. package/src/components/layout/search/types.ts +20 -0
  297. package/src/components/layout/search/typesense.ts +5 -1
  298. package/src/components/openapi/Operation.astro +161 -13
  299. package/src/components/openapi/RequestBody.astro +6 -4
  300. package/src/components/openapi/RequestPanel.astro +59 -7
  301. package/src/components/openapi/Responses.astro +6 -4
  302. package/src/components/openapi/async-snippets.ts +4 -4
  303. package/src/components/openapi/code-samples.ts +97 -0
  304. package/src/components/openapi/security.ts +6 -3
  305. package/src/components/openapi/snippets.ts +869 -8
  306. package/src/components/props.ts +6 -0
  307. package/src/consent/client.ts +95 -0
  308. package/src/consent/clients.ts +19 -0
  309. package/src/consent/ethyca.ts +71 -0
  310. package/src/consent/head.ts +24 -0
  311. package/src/consent/index.ts +37 -0
  312. package/src/consent/init.ts +29 -0
  313. package/src/consent/native.ts +36 -0
  314. package/src/consent/osano.ts +59 -0
  315. package/src/consent/schema.ts +30 -0
  316. package/src/core/builtin-tags.ts +5 -0
  317. package/src/core/client-address.ts +19 -0
  318. package/src/core/config-input.ts +194 -20
  319. package/src/core/data.ts +44 -4
  320. package/src/core/diagnostics.ts +3 -2
  321. package/src/core/footer.ts +26 -0
  322. package/src/core/graph.ts +39 -36
  323. package/src/core/i18n-ui.ts +82 -2
  324. package/src/core/includes.ts +20 -3
  325. package/src/core/links.ts +16 -2
  326. package/src/core/localizable.ts +26 -0
  327. package/src/core/navigation.ts +43 -12
  328. package/src/core/page-modes.ts +56 -0
  329. package/src/core/project-graph.ts +111 -5
  330. package/src/core/redirect-patterns.ts +491 -0
  331. package/src/core/related.ts +131 -0
  332. package/src/core/schema.ts +318 -64
  333. package/src/core/sources/normalize.ts +2 -1
  334. package/src/core/sources/read.ts +8 -3
  335. package/src/core/types.ts +1 -1
  336. package/src/core/ui-packs/ar.ts +43 -0
  337. package/src/core/ui-packs/bg.ts +45 -0
  338. package/src/core/ui-packs/bn.ts +43 -0
  339. package/src/core/ui-packs/ca.ts +46 -0
  340. package/src/core/ui-packs/cs.ts +44 -0
  341. package/src/core/ui-packs/da.ts +45 -0
  342. package/src/core/ui-packs/de.ts +46 -0
  343. package/src/core/ui-packs/el.ts +45 -0
  344. package/src/core/ui-packs/es.ts +46 -0
  345. package/src/core/ui-packs/fa.ts +45 -0
  346. package/src/core/ui-packs/fi.ts +45 -0
  347. package/src/core/ui-packs/fr.ts +46 -0
  348. package/src/core/ui-packs/he.ts +43 -0
  349. package/src/core/ui-packs/hi.ts +44 -0
  350. package/src/core/ui-packs/hr.ts +45 -0
  351. package/src/core/ui-packs/hu.ts +44 -0
  352. package/src/core/ui-packs/id.ts +45 -0
  353. package/src/core/ui-packs/it.ts +45 -0
  354. package/src/core/ui-packs/ja.ts +45 -0
  355. package/src/core/ui-packs/ko.ts +44 -0
  356. package/src/core/ui-packs/nl.ts +46 -0
  357. package/src/core/ui-packs/no.ts +44 -0
  358. package/src/core/ui-packs/pl.ts +45 -0
  359. package/src/core/ui-packs/pt-br.ts +46 -0
  360. package/src/core/ui-packs/pt.ts +45 -0
  361. package/src/core/ui-packs/ro.ts +45 -0
  362. package/src/core/ui-packs/ru.ts +45 -0
  363. package/src/core/ui-packs/sk.ts +44 -0
  364. package/src/core/ui-packs/sr.ts +45 -0
  365. package/src/core/ui-packs/sv.ts +44 -0
  366. package/src/core/ui-packs/th.ts +43 -0
  367. package/src/core/ui-packs/tr.ts +45 -0
  368. package/src/core/ui-packs/uk.ts +45 -0
  369. package/src/core/ui-packs/vi.ts +45 -0
  370. package/src/core/ui-packs/zh-tw.ts +43 -0
  371. package/src/core/ui-packs/zh.ts +43 -0
  372. package/src/core/variables.ts +103 -0
  373. package/src/deploy/artifacts.ts +77 -8
  374. package/src/deploy/cloudflare-negotiation.ts +35 -5
  375. package/src/deploy/headers.ts +2 -1
  376. package/src/deploy/node-headers.ts +63 -9
  377. package/src/deploy/platforms/cloudflare.ts +32 -0
  378. package/src/deploy/platforms/netlify.ts +33 -1
  379. package/src/deploy/platforms/vercel.ts +37 -2
  380. package/src/deploy/redirects.ts +91 -24
  381. package/src/deploy/vercel-negotiation.ts +36 -0
  382. package/src/markdown/api-rail.ts +259 -0
  383. package/src/markdown/code-title.ts +35 -9
  384. package/src/markdown/features.ts +3 -2
  385. package/src/markdown/fence-meta.ts +2 -0
  386. package/src/markdown/include.ts +11 -1
  387. package/src/markdown/index.ts +28 -5
  388. package/src/markdown/math.ts +4 -4
  389. package/src/markdown/variables.ts +56 -0
  390. package/src/markdown/views.ts +82 -0
  391. package/src/narration/build.ts +412 -0
  392. package/src/narration/player.ts +249 -0
  393. package/src/narration/provider.ts +29 -0
  394. package/src/narration/script.ts +592 -0
  395. package/src/og/card.ts +7 -6
  396. package/src/openapi/model.ts +57 -24
  397. package/src/openapi/overlay.ts +256 -0
  398. package/src/openapi/parse.ts +55 -2
  399. package/src/openapi/references.ts +21 -5
  400. package/src/openapi/render-mdx.ts +5 -1
  401. package/src/openapi/scalar.ts +40 -6
  402. package/src/openapi/source.ts +1 -1
  403. package/src/ratelimit/cloudflare.ts +54 -0
  404. package/src/ratelimit/index.ts +30 -0
  405. package/src/ratelimit/memory.ts +45 -0
  406. package/src/ratelimit/runtime.ts +229 -0
  407. package/src/ratelimit/schema.ts +28 -0
  408. package/src/ratelimit/upstash.ts +30 -0
  409. package/src/ratelimit/wrangler.ts +70 -0
  410. package/src/reference/asyncapi.ts +2 -2
  411. package/src/reference/graphql.ts +5 -2
  412. package/src/reference/openapi.ts +21 -7
  413. package/src/reference/options.ts +51 -7
  414. package/src/reference/scalar.ts +33 -8
  415. package/src/registry/eject.ts +20 -4
  416. package/src/registry/registry.ts +7 -0
  417. package/src/search/documents.ts +26 -2
  418. package/src/search/orama-index.ts +28 -3
  419. package/src/search/pagefind-ranking.ts +39 -0
  420. package/src/search/sync/algolia.ts +15 -4
  421. package/src/search/sync/orama-cloud.ts +3 -0
  422. package/src/search/sync/typesense.ts +5 -0
  423. package/src/seo/metatags.ts +67 -0
  424. package/src/theme/chrome-icons.ts +1 -0
  425. package/src/theme/color.ts +500 -0
  426. package/src/theme/entry.ts +56 -3
  427. package/src/theme/palette.ts +121 -45
  428. package/src/translate/prompts.ts +1 -0
  429. package/src/translate/validate.ts +8 -2
  430. package/src/upgrade/upgrade.ts +4 -171
  431. package/dist/cli/chunk-364znk6q.js +0 -75
  432. package/dist/cli/chunk-364znk6q.js.map +0 -11
  433. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  434. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  435. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  436. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  437. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  438. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  439. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  440. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  441. package/docs/content/sources.mdx +0 -334
  442. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-1n228xqb.js.map} +0 -0
  443. /package/dist/cli/{chunk-35d4wj9f.js.map → chunk-2n4ps1b3.js.map} +0 -0
  444. /package/dist/cli/{chunk-e04dxsz1.js.map → chunk-4ejxbzee.js.map} +0 -0
  445. /package/dist/cli/{chunk-tzmab476.js.map → chunk-5ffywsmb.js.map} +0 -0
  446. /package/dist/cli/{chunk-2z928egk.js.map → chunk-5wj2qwn2.js.map} +0 -0
  447. /package/dist/cli/{chunk-b07cmahc.js.map → chunk-amrb6hgh.js.map} +0 -0
  448. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-cmtcnh9f.js.map} +0 -0
  449. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-e7vvk5ya.js.map} +0 -0
  450. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-gp84za0h.js.map} +0 -0
  451. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-p804f3pe.js.map} +0 -0
  452. /package/dist/cli/{chunk-8ktnccpt.js.map → chunk-ta27feev.js.map} +0 -0
  453. /package/dist/cli/{chunk-a58773jm.js.map → chunk-y4ykbark.js.map} +0 -0
  454. /package/dist/cli/{chunk-p3v96n38.js.map → chunk-y5h4r2y0.js.map} +0 -0
  455. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-yj17vs39.js.map} +0 -0
@@ -136,6 +136,23 @@ On Vercel and Cloudflare, a server build also turns on [`Accept: text/markdown`
136
136
  Server features have their own configuration — for example, the assistant needs a model API key. See the [assistant guide](/docs/configuration/assistant) for setup.
137
137
  :::
138
138
 
139
+ ## Private docs
140
+
141
+ Blume has no sign-in of its own. A static build is plain files, and anyone who can reach the host can fetch anything in `dist/`. To keep a site private, turn on your host's access protection, which checks every request before a file is served:
142
+
143
+ - **Vercel**: [Deployment Protection](https://vercel.com/docs/deployment-protection) with the **All Deployments** scope, so production is covered and not only previews. Vercel Authentication lets in members of your Vercel team; Password Protection lets in anyone with a shared password, on paid plans.
144
+ - **Netlify**: [password protection](https://docs.netlify.com/manage/security/secure-access-to-sites/password-protection/) on all deploys, with a shared password, or with team login on Enterprise plans.
145
+ - **Cloudflare**: [Cloudflare Access](https://developers.cloudflare.com/workers/configuration/cloudflare-access/) on the Worker that serves the site (**Workers & Pages**, then your Worker, then **Access**), which covers every domain it answers on. Choose who can sign in by email address, email domain, or Cloudflare account membership.
146
+ - **GitHub Pages**: publish the site privately from an organization on GitHub Enterprise Cloud, and only people with read access to the repository can view it.
147
+ - **Your own server**: put authentication in front of `dist/`, such as nginx's `auth_basic` or an identity-aware proxy.
148
+
149
+ Host protection works the same for [server builds](#server-rendering). It covers the whole site: to keep some pages public, publish them as a separate site.
150
+
151
+ Everything Blume writes to `dist/` sits behind the protection, including pages, the search index, the `.md` copies, `llms.txt`, and Open Graph images. A few features reach outside it:
152
+
153
+ - A hosted [search provider](/docs/configuration/search) (Algolia, Orama Cloud, Typesense, or Mixedbread) keeps its own copy of your content on that service. The built-in Orama, FlexSearch, and Pagefind indexes are files in `dist/`, so they stay private.
154
+ - Services outside the protection can't read your pages. **Open in chat** hands ChatGPT or Claude a link it can't open, link previews in Slack or X can't show your Open Graph cards, and agents can't read `llms.txt` or the MCP server. Set `ai.openInChat: false` to hide the chat actions.
155
+
139
156
  ## Redirects
140
157
 
141
158
  Map old URLs to new ones in `blume.config.ts`:
@@ -151,14 +168,33 @@ Two hosts need more than the file:
151
168
  - **Netlify** serves a file that exists ahead of a redirect rule unless the rule is forced, and a static build has a redirect page at every `from`. The `_redirects` for `netlify()` forces its rules (`/old /new 301!`). Cloudflare rejects that flag, so the file a build for no named host writes leaves it off, and on Netlify that build answers with the redirect page instead; name the host with `netlify({ output: "static" })` to get the HTTP redirect.
152
169
  - **Vercel** reads `vercel.json` from the project's root directory, never from the output directory, so the copy in `dist/` applies only when you deploy that folder itself with the Vercel CLI (`vercel deploy dist`). A Git-connected project never reads it: it serves the redirect pages and none of the headers from [Content types](#content-types). Copy the `redirects` and `headers` from `dist/vercel.json` into the `vercel.json` in your project's root directory, or use `vercel()` for a server build, whose routing config carries the redirects and the discovery headers.
153
170
 
154
- :::note
155
- `from` and `to` are exact paths — a `:param` segment or `*` wildcard (e.g. `/blog/:slug` or `/old/*`) fails config validation, since hosts disagree on patterns. If you need pattern-based rules, handle them in an infrastructure file like `vercel.json` (which supports wildcard `source` patterns) or your host's redirect config instead. A `vercel.json` you ship in `public/` is preserved as-is.
156
- :::
157
-
158
171
  :::note
159
172
  Write both `from` and `to` as if mounted at root, starting with `/` (`to` can also be a full `https://` URL) — under [`base`](#subpath-deploys) and [`basePath`](#mount-the-docs-under-a-path) alike, Blume rewrites both sides for you, so a redirect lands inside the base. A `to` that names a file in `public/` (`/files/guide.pdf`) gains `base` but not `basePath`, since that's where the file is served. A base you've already written into `to` by hand is preserved rather than doubled.
160
173
  :::
161
174
 
175
+ ### Pattern redirects
176
+
177
+ A `from` can cover many paths at once, written the way Mintlify and `vercel.json` write them:
178
+
179
+ ```ts blume.config.ts
180
+ redirects: [
181
+ { from: "/beta/:slug*", to: "/v2/:slug*" },
182
+ { from: "/blog/:slug", to: "/articles/:slug" },
183
+ { from: "/articles/concepts-*", to: "/concepts" },
184
+ { from: "/old/article-*", to: "/new/article-*" },
185
+ ],
186
+ ```
187
+
188
+ - `:name` matches one whole segment: `/blog/:slug` covers `/blog/hello`, not `/blog/a/b`.
189
+ - `:name*` as the last segment matches the rest of the path, any number of segments: `/beta/:slug*` sends `/beta/guides/setup` to `/v2/guides/setup`, and `/beta` itself to `/v2`. A last segment of `*` does the same.
190
+ - `*` at the end of a segment matches the rest of the path from there: `/articles/concepts-*` covers `/articles/concepts-overview`.
191
+
192
+ `to` reads a capture by name, as a whole segment (`/:slug*`, `/:slug`), or a `*` for what the `*` in `from` matched (`:splat`, the way `_redirects` spells it, reads it too). A `to` that reads no capture sends every path the pattern covers to one page. An exact redirect wins over a pattern that covers the same path, and patterns are tried in the order you list them.
193
+
194
+ Each host gets the patterns in its own syntax: splats in `_redirects`, `path-to-regexp` sources in `vercel.json`, and the routing config of a `vercel()` or `netlify()` server build. The `node()` and `cloudflare()` servers match them themselves, and so does `blume dev`. A static build writes no redirect page for a pattern, since it would need one for every path the pattern covers, so a host that reads none of these files (GitHub Pages, say) applies only the exact redirects; `blume-redirects.json` lists patterns as you wrote them.
195
+
196
+ A pattern can't also match a page: hosts disagree on which of the two answers, so the build stops with `BLUME_REDIRECT_MATCHES_PAGE`, naming the page. Narrow the pattern to the paths that moved.
197
+
162
198
  ## Content types
163
199
 
164
200
  Where the host reads one, a build also emits a `_headers` file that pins `charset=utf-8` onto the raw AI-ready endpoints — `/<route>.md`, `/<route>.mdx`, and the `.txt` files (`llms.txt`, `llms-full.txt`). Those responses are valid UTF-8, but many static hosts serve them as `text/markdown` / `text/plain` with **no** charset, and browsers then fall back to Windows-1252 — so non-ASCII docs (Japanese, accented Latin, …) render as mojibake when the raw URL is opened directly. HTML pages are unaffected because they carry `<meta charset>`. Netlify reads `_headers` on a static deploy and Cloudflare (Pages, and Workers static assets) on both static and server builds, so those adapters and an unnamed static host get the file; Vercel (whose headers ride the routing config) and Node (whose server ignores the file) don't. A Netlify server build writes the same rules into the `headers` of its Frameworks API config (`.netlify/v1/config.json`) instead. A static Vercel build writes the same rules into `dist/vercel.json` instead, which a Git-connected project doesn't read (see [Redirects](#redirects)). A `_headers` you ship in `public/` is left untouched.
@@ -170,7 +206,7 @@ When a feature needs a runtime secret, Blume warns at `blume dev`/`build` if it'
170
206
  | Feature | Variable |
171
207
  | --- | --- |
172
208
  | Assistant (AI Gateway) | `AI_GATEWAY_API_KEY` (or Vercel OIDC) |
173
- | Assistant (other adapters) | the adapter's default key env var (`OPENROUTER_API_KEY`, `LLMGATEWAY_API_KEY`, `INKEEP_API_KEY`), or the `apiKeyEnv` you passed it |
209
+ | Assistant (other adapters) | the adapter's default key env var (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, `OPENROUTER_API_KEY`, `LLMGATEWAY_API_KEY`, `INKEEP_API_KEY`), or the `apiKeyEnv` you passed it |
174
210
  | Mixedbread search | `MIXEDBREAD_API_KEY` |
175
211
  | [Content sources](/docs/content/sources) | `NOTION_TOKEN` (`notion()`), `SANITY_TOKEN` (`sanity()`), `CONTENTFUL_ACCESS_TOKEN` (`contentful()`), `PAYLOAD_API_KEY` (`payload()`), `STRAPI_API_TOKEN` (`strapi()`), `GITHUB_TOKEN` (`githubReleases()`, and `mdxRemote()` reading from GitHub) |
176
212
 
@@ -6,7 +6,7 @@ sidebar:
6
6
  order: 2.5
7
7
  ---
8
8
 
9
- Blume 2 changes configuration, not content: your Markdown and MDX pages need no edits, unless one sets the removed `search.boost` frontmatter field (see [Frontmatter](#frontmatter)). Settings that used to be a named string or a keyed block — the search provider, the deployment target, content sources, API references, analytics, and the assistant's model backend — are now **adapters** you import from a `blume/*` subpath and call. Ask AI is renamed the assistant, so `ai.ask` becomes `ai.assistant`. The machine-readable settings move from `ai` to a new `agents` key, and `components.ts` overrides are checked before the build. A zero-config site, or one that sets none of these, only needs the version bump.
9
+ Blume 2 changes configuration, not content: your Markdown and MDX pages need no edits (see [Frontmatter](#frontmatter) for one field that now does something). Settings that used to be a named string or a keyed block — the search provider, the deployment target, content sources, API references, analytics, and the assistant's model backend — are now **adapters** you import from a `blume/*` subpath and call. Ask AI is renamed the assistant, so `ai.ask` becomes `ai.assistant`. The machine-readable settings move from `ai` to a new `agents` key, and `components.ts` overrides are checked before the build. A zero-config site, or one that sets none of these, only needs the version bump.
10
10
 
11
11
  ## Upgrade with one command
12
12
 
@@ -87,7 +87,7 @@ export default defineConfig({
87
87
 
88
88
  - `netlify()`, `cloudflare()`, and `node()` work the same way. Naming a host adapter switches to server output; pass `output: "static"` to keep a static build with that host's platform files.
89
89
  - A config that only sets `site` or `base` stays as it is: `deployment: { site, base }` is still the static form.
90
- - `redirects` take exact paths. A `from` or `to` with a `:param` segment or a `*` wildcard now fails validation; Blume 1 never supported patterns, and hosts treated them differently. Move pattern rules into your host's own config (`vercel.json`, `_redirects`).
90
+ - `redirects` read `:name` segments and a trailing `*` as patterns, matched the same way on every host (see [Pattern redirects](/docs/deployment#pattern-redirects)). Blume 1 never supported patterns and handed them to each host as written, so check any `from` or `to` that holds one.
91
91
  - The `--adapter`, `--output`, and `--base` flags on `blume build` are gone, and passing one stops the build with an error naming the `deployment` setting that replaces it. Set the adapter in `blume.config.ts`, and name it explicitly: server output is no longer inferred from the platform's environment.
92
92
 
93
93
  See [Deployment](/docs/deployment) for each adapter's options.
@@ -239,7 +239,7 @@ export default defineConfig({
239
239
 
240
240
  </CodeGroup>
241
241
 
242
- The adapters are `gateway()`, `openrouter()`, `llmgateway()`, `inkeep()`, and `openaiCompatible({ baseUrl, name, model, apiKeyEnv })`. `model`, `apiKeyEnv`, `baseUrl`, `headers`, and `reasoning` move into the adapter; `enabled`, `instructions`, `retrieval`, `suggestions`, `cors`, and `endpoint` move to `ai.assistant` unchanged. Leaving `provider` unset still uses the AI Gateway.
242
+ A 1.x `provider` name maps onto `gateway()`, `openrouter()`, `llmgateway()`, or `inkeep()`, and `openai-compatible` onto `openai({ baseUrl, name, model, apiKeyEnv })`; the [adapter list](/docs/configuration/assistant#adapters) has the rest. `model`, `apiKeyEnv`, `baseUrl`, `headers`, and `reasoning` move into the adapter; `enabled`, `instructions`, `retrieval`, `suggestions`, `cors`, and `endpoint` move to `ai.assistant` unchanged. Leaving `provider` unset still uses the AI Gateway.
243
243
 
244
244
  The rename reaches every name that said Ask AI:
245
245
 
@@ -290,7 +290,7 @@ export default defineConfig({
290
290
 
291
291
  ## Frontmatter
292
292
 
293
- One frontmatter field is gone: `search.boost`. Blume 1 accepted it, but search never read it, so a page ranked the same without it. Delete it wherever it appears; a page that still sets it fails validation with a hint, and `blume upgrade` lists each one with its file and line.
293
+ Pages keep their frontmatter. One field changes what it does: `search.boost`. Blume 1 accepted it, but search never read it, so a page ranked the same without it. It now multiplies the page's search relevance (see [Ranking](/docs/configuration/search#ranking)). Check any page that sets it: a boost you added and forgot now moves the page up.
294
294
 
295
295
  ## Component overrides
296
296
 
@@ -43,7 +43,7 @@ The agent follows the playbook's workflow from the source's config to a passing
43
43
  5. Points your `package.json` scripts at `blume dev` and `blume build`, and swaps the old framework's dependencies for `blume`.
44
44
  6. Runs `blume build` and `blume validate` until both pass.
45
45
 
46
- It finishes with a summary of what it migrated, dropped, or approximated, such as footer links or dynamic redirects with no Blume equivalent, so you can decide what to do with each.
46
+ It finishes with a summary of what it migrated, dropped, or approximated, such as footer links or navbar buttons with no Blume equivalent, so you can decide what to do with each.
47
47
 
48
48
  ## Other agents
49
49
 
@@ -68,7 +68,7 @@ A header [tab](/docs/content/navigation#tabs) pointing at `/changelog` opens thi
68
68
 
69
69
  ## From GitHub Releases
70
70
 
71
- Rather than authoring entries by hand, point the built-in [`githubReleases()` source](/docs/content/sources#github-releases) at a repo and every release becomes a `type: changelog` entry — the same timeline and feed, fed straight from the releases you already publish. Blume's own [changelog](/changelog) is built this way:
71
+ Rather than authoring entries by hand, point the built-in [`githubReleases()` source](/docs/content/sources/github-releases) at a repo and every release becomes a `type: changelog` entry — the same timeline and feed, fed straight from the releases you already publish. Blume's own [changelog](/changelog) is built this way:
72
72
 
73
73
  ```ts blume.config.ts
74
74
  import { filesystem, githubReleases } from "blume/sources";
@@ -85,7 +85,7 @@ content: {
85
85
  }
86
86
  ```
87
87
 
88
- The release name becomes the title, its tag becomes `changelog.version`, and its published date sorts the timeline. Each release page also gets a unique meta description summarized from its notes — markdown stripped, section headings and changeset commit-hash prefixes dropped, trimmed to the search-snippet length [`blume audit`](/docs/cli/audit) checks for — instead of falling back to the site description. A private repo authenticates with the `GITHUB_TOKEN` environment variable. See [Content sources](/docs/content/sources#github-releases) for every option.
88
+ The release name becomes the title, its tag becomes `changelog.version`, and its published date sorts the timeline. Each release page also gets a unique meta description summarized from its notes — markdown stripped, section headings and changeset commit-hash prefixes dropped, trimmed to the search-snippet length [`blume audit`](/docs/cli/audit) checks for — instead of falling back to the site description. A private repo authenticates with the `GITHUB_TOKEN` environment variable. See [GitHub Releases](/docs/content/sources/github-releases) for every option.
89
89
 
90
90
  ## The RSS feed
91
91
 
@@ -34,3 +34,17 @@ Blume doesn't host the automation — wire the skill into a scheduled task in Cl
34
34
  ```text
35
35
  Use the blume-update-docs skill. Review the PRs merged within the last 7 days and compare them to the docs content. Ignore work behind feature flags. If docs need updates, make them, verify the docs build, and open a blume/* PR. If not, report what you checked and do not open a PR.
36
36
  ```
37
+
38
+ ## Writing your site's skill
39
+
40
+ `blume-write-skill` writes the [agent skill for your own docs](/docs/discoverability/agent-discovery#your-sites-skill): one `SKILL.md`, grounded in the docs, that teaches a coding agent to use your product — setup, core concepts, common tasks, and gotchas — with each topic linked to its page. It takes the place of the page map Blume generates at `/skill.md`. The quickest way to run it is `blume skill`, which works out where the skill goes and opens Codex or Claude Code on the copy bundled in the package:
41
+
42
+ ```bash
43
+ blume skill --claude # or --codex
44
+ ```
45
+
46
+ To use it from another agent, install it:
47
+
48
+ ```bash
49
+ npx skills add haydenbleasel/blume --skill blume-write-skill
50
+ ```
@@ -69,9 +69,10 @@ This writes the complete JSON report — every affected page, not the terminal's
69
69
 
70
70
  The check set is deliberately narrower than a general-purpose SEO crawler's. Much of what such a crawler reports cannot happen to a Blume site — it never emits `rel=nofollow`, and Vite's content-hashed bundles are never missing or redirecting — and reporting those as permanent zeroes would just teach you to ignore the report.
71
71
 
72
- Two limits worth stating plainly:
72
+ Limits worth stating plainly:
73
73
 
74
74
  - **Structured data** is validated for well-formedness (valid JSON, a `@context`, a `@type` on every node). Blume does not validate against the full schema.org vocabulary or Google's rich-results rules.
75
+ - **Contrast** is checked for the colors your theme config sets, not element by element. The audit doesn't render pages, so a color you write by hand in MDX or `theme.css` isn't measured.
75
76
  - **Core Web Vitals** are not checked. They need a real browser, and a flag that quietly measured nothing would be worse than not having one — so `blume audit` reports the layout-shift causes it _can_ see offline (images with no `width`/`height`, oversized assets) and leaves the rest alone for now.
76
77
 
77
78
  Anything the audit did not run is reported as skipped rather than silently passing:
@@ -87,6 +88,7 @@ Every check `blume audit` can report, grouped by category, with its default seve
87
88
 
88
89
  | Category | Key |
89
90
  | -------------------- | ----------------- |
91
+ | Accessibility | `accessibility` |
90
92
  | Content | `content` |
91
93
  | Duplicates | `duplicates` |
92
94
  | Indexability | `indexability` |
@@ -445,6 +447,24 @@ Fix: Point the hreflang alternate at a page that exists and is canonical.
445
447
 
446
448
  Fix: Each language in an hreflang group must name exactly one page.
447
449
 
450
+ ### Accessibility
451
+
452
+ These run on your theme config, not on each page: a theme color is one choice for the whole site. The check resolves colors the way the build does (a preset's shade for each mode, and white or dark labels on fills) and measures every text color the theme draws against the WCAG AA ratio for body text, 4.5:1, in light and dark mode.
453
+
454
+ #### Theme colors miss WCAG AA contrast [#theme-contrast-low]
455
+
456
+ `BLUME_AUDIT_THEME_CONTRAST_LOW` · warning · from the theme config
457
+
458
+ Fix: Pick a darker shade for light mode or a lighter one for dark mode, per mode with `theme.accent: { light, dark }`.
459
+
460
+ It checks accent text on the page, labels on accent and `action` fills, and body and secondary text on a custom `background`. Each finding names the pair, the mode, and its ratio.
461
+
462
+ #### Theme color can't be checked for contrast [#theme-color-unchecked]
463
+
464
+ `BLUME_AUDIT_THEME_COLOR_UNCHECKED` · info · from the theme config
465
+
466
+ Fix: Write the color as hex, `rgb()`, `hsl()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, or a named color so its contrast can be checked.
467
+
448
468
  ### Assets
449
469
 
450
470
  #### Missing alt text [#image-alt-missing]
@@ -25,6 +25,7 @@ blume <command> [options]
25
25
  | [`blume eval`](/docs/cli/evals) | Test the docs: an agent answers your questions using only the documentation. |
26
26
  | [`blume translate`](/docs/cli/translate) | Translate docs into the configured locales with a local agent CLI. |
27
27
  | [`blume version [id]`](/docs/cli/version) | Freeze the current docs as an archived version (no id lists configured versions). |
28
+ | [`blume skill`](/docs/discoverability/agent-discovery#your-sites-skill) | Write your docs' agent skill with Codex or Claude Code, in place of the generated one. |
28
29
  | [`blume migrate [source]`](/docs/migrating) | Move a Mintlify, Fumadocs, Docusaurus, Starlight, or Nextra site to Blume with Codex or Claude Code. |
29
30
  | [`blume upgrade`](/docs/upgrading) | Move to a new major: bump `blume`, then list the config changes left or hand them to Codex or Claude Code. |
30
31
 
@@ -27,6 +27,8 @@ Analytics loads in **production builds only**. The scripts are emitted by `blume
27
27
  Analytics is opt-in. With no `analytics` list (or an empty one), Blume injects nothing.
28
28
  :::
29
29
 
30
+ To ask readers first, set [`consent`](/docs/configuration/consent): every adapter here then waits until the reader allows analytics.
31
+
30
32
  Every identifier below — a project key, a measurement ID, a site token — is the public, browser-side one from the provider's own install snippet. Each adapter renders that snippet, so it's safe to commit to your docs repository; Blume never asks for a secret.
31
33
 
32
34
  ## Adapters
@@ -429,4 +431,4 @@ analytics: [
429
431
 
430
432
  ## Custom events
431
433
 
432
- Blume's page feedback sends a custom event through every configured adapter that has a client API — PostHog, Mixpanel, Heap, Segment, Hightouch, Amplitude, LogRocket, Adobe, Google Analytics, Google Tag Manager (as a `{ event }` push on `window.dataLayer`), Plausible, Databuddy, Fathom, OneDollarStats (with each property value as a string), Pirsch, Clarity, Hotjar, and Vercel. Cloudflare and Clearbit have no event API. Every event also fires as a `blume:track` CustomEvent on `window` with `{ event, props }` in `detail`, so a `script()` adapter can forward it anywhere else. For deploying your built site, see [Deployment](/docs/deployment).
434
+ Blume's page feedback (`feedback`, and `feedback_comment` when [written feedback](/docs/configuration#written-feedback) is on), [search](/docs/configuration/search#search-analytics) (`search`, `search_select`), and [narration](/docs/configuration/narration#analytics) player (`narration_play`, `narration_complete`) send custom events through every configured adapter that has a client API — PostHog, Mixpanel, Heap, Segment, Hightouch, Amplitude, LogRocket, Adobe, Google Analytics, Google Tag Manager (as a `{ event }` push on `window.dataLayer`), Plausible, Databuddy, Fathom, OneDollarStats (with each property value as a string), Pirsch, Clarity, Hotjar, and Vercel. Cloudflare and Clearbit have no event API. Every event also fires as a `blume:track` CustomEvent on `window` with `{ event, props }` in `detail`, so a `script()` adapter can forward it anywhere else. For deploying your built site, see [Deployment](/docs/deployment).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Assistant
3
- description: An in-page assistant grounded in your docs — suggested questions, custom instructions, retrieval sizing, provider adapters from the Vercel AI Gateway to any OpenAI-compatible endpoint, and the server output it needs.
3
+ description: An in-page assistant grounded in your docs — suggested questions, custom instructions, retrieval sizing, provider adapters for OpenAI, Anthropic, Gemini, Grok, the Vercel AI Gateway, and any OpenAI-compatible endpoint, and the server output it needs.
4
4
  ---
5
5
 
6
6
  Add an assistant that answers reader questions in an in-page chat panel, backed by a streaming server endpoint and the [AI SDK](https://ai-sdk.dev). It's opt-in, and static docs stay fully static until you turn it on:
@@ -34,6 +34,27 @@ ai: {
34
34
 
35
35
  `label` is the question that gets asked; `icon` is optional. Leave `suggestions` unset (or empty) and the panel opens to a plain input.
36
36
 
37
+ ## Asking about code
38
+
39
+ With the assistant on, every code block gets an **Ask** button beside its copy button. It opens the assistant with the block attached, shown as a chip above the input, so the reader can ask what it does, why it fails, or how to adapt it. Sent without a question, it asks the assistant to explain the code. The code goes to the model as a fenced block after the question, up to 6,000 characters.
40
+
41
+ A custom chat UI built on `useAssistant` can do the same: listen for `blume:open-assistant` on `window`, whose `detail.code` holds the block's `language`, `source`, and `title`.
42
+
43
+ ## Contact support
44
+
45
+ When the docs can't answer, hand the reader to a person. Set `support`, and once there's a conversation the panel shows a **Contact support** link under it:
46
+
47
+ ```ts blume.config.ts lineNumbers
48
+ ai: {
49
+ assistant: {
50
+ enabled: true,
51
+ support: "mailto:help@example.com", // or "https://example.com/support", or "/support"
52
+ },
53
+ },
54
+ ```
55
+
56
+ A `mailto:` address starts an email with the conversation as its body, so the reader doesn't have to explain again (a long conversation keeps its latest turns). A URL or a page on your site gets the conversation's id as a `thread` query parameter. The assistant's `ask`, `ask_answer`, and `ask_error` [analytics events](#analytics) carry the same `thread`, so your support team can find the conversation in your analytics.
57
+
37
58
  ## Custom instructions
38
59
 
39
60
  Add your own system-prompt text with `instructions` — identity, language, tone, or anything else the assistant should keep in mind:
@@ -58,6 +79,24 @@ The page the reader is currently on is added to the context first and used to sc
58
79
 
59
80
  Grounding is on for every adapter except **[Inkeep](#inkeep)**, which runs its own retrieval over the content you've indexed in its dashboard.
60
81
 
82
+ ## Searching and reading pages
83
+
84
+ The retrieved pages are a head start. The assistant can also look things up itself: it has a `search_docs` tool that searches your docs, and a `read_page` tool that reads a whole page, for when an excerpt stops short of the answer. It can search, read a page or two, search again, and then answer, all within one question. Tool calls run on your server against the same snapshot as the grounding, keep to the reader's language and docs version, and never reach the reader; they only see the answer.
85
+
86
+ The tools are on by default for every adapter whose models support tool calling. They're off for [OpenAI-compatible endpoints](#openai-compatible-endpoints), since a self-hosted model may not. Set `tools` to change either default:
87
+
88
+ ```ts blume.config.ts lineNumbers
89
+ ai: {
90
+ assistant: {
91
+ enabled: true,
92
+ provider: openai({ baseUrl, model, apiKeyEnv }),
93
+ tools: true, // this model supports tool calling
94
+ },
95
+ }
96
+ ```
97
+
98
+ Each question takes at most five model steps: up to four rounds of tool calls, then the answer. With `tools: false`, the assistant answers from the retrieved pages alone, as it did before tools existed.
99
+
61
100
  ## Retrieval size
62
101
 
63
102
  How much documentation a question carries is the biggest lever on how long the reader waits for the first word: the model reads every injected character before it emits a token. On a hosted frontier model that's invisible, but on a self-hosted backend it dominates. `retrieval` sizes it:
@@ -160,13 +199,13 @@ A static build with the assistant enabled and no external `endpoint` fails fast
160
199
 
161
200
  ```ts blume.config.ts lineNumbers
162
201
  import { defineConfig } from "blume";
163
- import { openrouter } from "blume/ai";
202
+ import { anthropic } from "blume/ai";
164
203
 
165
204
  export default defineConfig({
166
205
  ai: {
167
206
  assistant: {
168
207
  enabled: true,
169
- provider: openrouter({ model: "anthropic/claude-sonnet-4-5" }),
208
+ provider: anthropic({ model: "claude-sonnet-5" }),
170
209
  },
171
210
  },
172
211
  });
@@ -174,16 +213,120 @@ export default defineConfig({
174
213
 
175
214
  | Adapter | Answers with | API key env var | SDK to install |
176
215
  | --- | --- | --- | --- |
216
+ | [`openai()`](#openai) | an [OpenAI](https://platform.openai.com) model, or any [OpenAI-compatible endpoint](#openai-compatible-endpoints) | `OPENAI_API_KEY` | `@ai-sdk/openai` (`@ai-sdk/openai-compatible` with a `baseUrl`) |
217
+ | [`anthropic()`](#anthropic) | a [Claude](https://platform.claude.com) model | `ANTHROPIC_API_KEY` | `@ai-sdk/anthropic` |
218
+ | [`gemini()`](#gemini) | a [Gemini](https://ai.google.dev) model | `GEMINI_API_KEY` | `@ai-sdk/google` |
219
+ | [`grok()`](#grok) | an [xAI](https://x.ai/api) Grok model | `XAI_API_KEY` | `@ai-sdk/xai` |
177
220
  | [`gateway()`](#vercel-ai-gateway) (default) | a `provider/model` string via the Vercel AI Gateway | `AI_GATEWAY_API_KEY` | none — ships with Blume |
178
221
  | [`openrouter()`](#openrouter) | any [OpenRouter](https://openrouter.ai) model | `OPENROUTER_API_KEY` | `@openrouter/ai-sdk-provider` |
179
222
  | [`llmgateway()`](#llmgateway) | any [LLMGateway](https://llmgateway.io) model | `LLMGATEWAY_API_KEY` | `@ai-sdk/openai-compatible` |
180
223
  | [`inkeep()`](#inkeep) | an [Inkeep](https://inkeep.com) QA model | `INKEEP_API_KEY` | `@ai-sdk/openai-compatible` |
181
- | [`openaiCompatible()`](#openai-compatible-endpoints) | whatever your endpoint serves | the `apiKeyEnv` you name | `@ai-sdk/openai-compatible` |
182
224
 
183
- The SDKs are optional peer dependencies, so add the one your adapter needs to your project (`npm install @openrouter/ai-sdk-provider`, say). If it's missing, `blume build` stops before Vite runs, naming the package and the command that installs it, and [`blume doctor`](/docs/cli/doctor) reports it too.
225
+ `openai()`, `anthropic()`, `gemini()`, and `grok()` call the provider's own API with your key — nothing sits in between. The SDKs are optional peer dependencies, so add the one your adapter needs to your project (`npm install @ai-sdk/anthropic`, say). If it's missing, `blume build` stops before Vite runs, naming the package and the command that installs it, and [`blume doctor`](/docs/cli/doctor) reports it too.
184
226
 
185
227
  The descriptor an adapter returns is plain data — its kind, its options, the env vars it reads, and the SDK it needs — so the generated route (and the [ejected](/docs/configuration/customization#eject) one) inlines it as literals and imports the provider SDK by name. Nothing reads `blume.config.ts` at request time, and no secret is ever written into a route: adapters take the **name** of the env var holding the key, and the route reads the value through Astro's [`getSecret()`](https://docs.astro.build/en/guides/environment-variables/#retrieving-secrets-programmatically), so each deployment adapter supplies it its own way — environment variables on Node, Vercel, and Netlify, and the Worker's [bindings](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets) on Cloudflare.
186
228
 
229
+ ### OpenAI
230
+
231
+ Any [OpenAI](https://platform.openai.com) model, called directly through OpenAI's own AI SDK provider:
232
+
233
+ ```ts blume.config.ts lineNumbers
234
+ import { defineConfig } from "blume";
235
+ import { openai } from "blume/ai";
236
+
237
+ export default defineConfig({
238
+ ai: {
239
+ assistant: {
240
+ enabled: true,
241
+ provider: openai({ model: "gpt-5.5" }),
242
+ },
243
+ },
244
+ });
245
+ ```
246
+
247
+ #### OpenAI-compatible endpoints
248
+
249
+ Any endpoint that speaks the OpenAI API — a self-hosted model, an internal gateway — works through `openai()` too: add its `baseUrl`, and usually `apiKeyEnv` for the env var holding its key (it defaults to `OPENAI_API_KEY`). `name` is the provider name the AI SDK reports, and the key the endpoint reads [`providerOptions`](#options-every-adapter-takes) under; it defaults to `openai-compatible`:
250
+
251
+ ```ts blume.config.ts lineNumbers
252
+ import { defineConfig } from "blume";
253
+ import { openai } from "blume/ai";
254
+
255
+ export default defineConfig({
256
+ ai: {
257
+ assistant: {
258
+ enabled: true,
259
+ provider: openai({
260
+ baseUrl: "https://my-gateway.example.com/v1",
261
+ apiKeyEnv: "MY_GATEWAY_API_KEY",
262
+ model: "gpt-4o",
263
+ name: "my-gateway",
264
+ }),
265
+ },
266
+ },
267
+ });
268
+ ```
269
+
270
+ With a `baseUrl`, the route calls the endpoint's Chat Completions API through `@ai-sdk/openai-compatible`, so install that package instead of `@ai-sdk/openai`: most compatible endpoints don't serve OpenAI's Responses API. The [docs tools](#searching-and-reading-pages) are off by default, since a self-hosted model may not support tool calling.
271
+
272
+ :::note
273
+ `openaiCompatible()` from earlier Blume versions still works: it's the same adapter as `openai()` with a `baseUrl`.
274
+ :::
275
+
276
+ ### Anthropic
277
+
278
+ Any [Claude](https://platform.claude.com) model, called directly through Anthropic's AI SDK provider:
279
+
280
+ ```ts blume.config.ts lineNumbers
281
+ import { defineConfig } from "blume";
282
+ import { anthropic } from "blume/ai";
283
+
284
+ export default defineConfig({
285
+ ai: {
286
+ assistant: {
287
+ enabled: true,
288
+ provider: anthropic({ model: "claude-sonnet-5" }),
289
+ },
290
+ },
291
+ });
292
+ ```
293
+
294
+ ### Gemini
295
+
296
+ Any [Gemini](https://ai.google.dev) model, called directly through Google's AI SDK provider. The key is an API key from Google AI Studio:
297
+
298
+ ```ts blume.config.ts lineNumbers
299
+ import { defineConfig } from "blume";
300
+ import { gemini } from "blume/ai";
301
+
302
+ export default defineConfig({
303
+ ai: {
304
+ assistant: {
305
+ enabled: true,
306
+ provider: gemini({ model: "gemini-3.5-flash" }),
307
+ },
308
+ },
309
+ });
310
+ ```
311
+
312
+ ### Grok
313
+
314
+ Any [xAI](https://x.ai/api) Grok model, called directly through xAI's AI SDK provider:
315
+
316
+ ```ts blume.config.ts lineNumbers
317
+ import { defineConfig } from "blume";
318
+ import { grok } from "blume/ai";
319
+
320
+ export default defineConfig({
321
+ ai: {
322
+ assistant: {
323
+ enabled: true,
324
+ provider: grok({ model: "grok-4.7" }),
325
+ },
326
+ },
327
+ });
328
+ ```
329
+
187
330
  ### Vercel AI Gateway
188
331
 
189
332
  The default. `model` is a `provider/model` string, so you switch models by changing it (`openai/gpt-5.5`, `anthropic/claude-sonnet-4-5`, and so on) with no provider SDK to install. The gateway reads `AI_GATEWAY_API_KEY` from your environment and is wired up automatically when you deploy on Vercel, where it can also authenticate with the deployment's OIDC token:
@@ -265,29 +408,6 @@ export default defineConfig({
265
408
 
266
409
  `baseUrl` overrides the preset endpoint (`https://api.inkeep.com/v1`).
267
410
 
268
- ### OpenAI-compatible endpoints
269
-
270
- Any endpoint that speaks the OpenAI API works through `openaiCompatible()` — supply its `baseUrl`, the `model` it serves, and the env var holding its key. A generic endpoint has no preset for any of these, so all three are required; `name` is the provider name the AI SDK reports and defaults to `openai-compatible`:
271
-
272
- ```ts blume.config.ts lineNumbers
273
- import { defineConfig } from "blume";
274
- import { openaiCompatible } from "blume/ai";
275
-
276
- export default defineConfig({
277
- ai: {
278
- assistant: {
279
- enabled: true,
280
- provider: openaiCompatible({
281
- baseUrl: "https://my-gateway.example.com/v1",
282
- apiKeyEnv: "MY_GATEWAY_API_KEY",
283
- model: "gpt-4o",
284
- name: "my-gateway",
285
- }),
286
- },
287
- },
288
- });
289
- ```
290
-
291
411
  ### Options every adapter takes
292
412
 
293
413
  **`apiKeyEnv`** points an adapter at a different env var than its default — `gateway({ apiKeyEnv: "DOCS_GATEWAY_KEY" })` reads that variable instead of `AI_GATEWAY_API_KEY`, and the missing-secret warning at `blume dev`/`build` checks it too. Until the key is set, the deployed route answers `503` with a message naming the variable. The route reads a request body only up to 64 KB and answers anything larger with `413`.
@@ -295,7 +415,7 @@ export default defineConfig({
295
415
  **`headers`** sends static request headers with every call — a caller-identifying header for a shared backend, say, so its own observability or rate limiting can tell your docs apart from other traffic:
296
416
 
297
417
  ```ts blume.config.ts lineNumbers
298
- provider: openaiCompatible({
418
+ provider: openai({
299
419
  baseUrl: "https://llm.internal.example.com/v1",
300
420
  apiKeyEnv: "INTERNAL_LLM_API_KEY",
301
421
  model: "gpt-4o",
@@ -314,7 +434,7 @@ provider: gateway({
314
434
  }),
315
435
  ```
316
436
 
317
- Blume maps only the options it names (`model`, `reasoning`, `apiKeyEnv`, `headers`) and forwards `providerOptions` verbatim, so it has to be JSON — it's inlined into the route — and it has to use the key the underlying provider expects (`openai` for an OpenAI model behind the gateway, `openrouter` on OpenRouter). Enabling the assistant also turns on React for the in-page island — see [Customization](/docs/configuration/customization#interactive-islands).
437
+ Blume maps only the options it names (`model`, `reasoning`, `apiKeyEnv`, `headers`) and forwards `providerOptions` verbatim, so it has to be JSON — it's inlined into the route — and it has to use the key the underlying provider expects (`openai` for `openai()` or an OpenAI model behind the gateway, `anthropic` for `anthropic()`, `google` for `gemini()`, `xai` for `grok()`, `openrouter` on OpenRouter, and the adapter's `name` on an OpenAI-compatible endpoint). Enabling the assistant also turns on React for the in-page island — see [Customization](/docs/configuration/customization#interactive-islands).
318
438
 
319
439
  ## Reasoning
320
440
 
@@ -328,10 +448,13 @@ Each adapter sends the level as its backend's own reasoning control, which is wh
328
448
 
329
449
  | Adapter | What the level becomes |
330
450
  | --- | --- |
451
+ | `openai()` | OpenAI's `reasoning_effort`. With a `baseUrl`, it's sent as `reasoning_effort` in the request, so the endpoint has to accept that parameter. |
452
+ | `anthropic()` | Claude's thinking: an effort level on models with adaptive thinking, a thinking budget on older ones, and thinking off for `"none"`. |
453
+ | `gemini()` | Gemini's thinking level, or a thinking budget on models that take one. |
454
+ | `grok()` | xAI's reasoning effort, on the models that offer one. |
331
455
  | `gateway()` | The AI SDK's [`reasoning`](https://ai-sdk.dev/docs/ai-sdk-core/reasoning) call option, which the gateway maps to the model's own setting — OpenAI's `reasoning_effort`, for example. |
332
456
  | `openrouter()` | OpenRouter's `reasoning.effort`, set on the model. Its provider ignores the AI SDK's call option, so the level is placed where OpenRouter reads it. |
333
457
  | `llmgateway()` | `reasoning_effort` in the request, through the AI SDK's call option. |
334
- | `openaiCompatible()` | `reasoning_effort` in the request, so the endpoint has to accept that parameter. |
335
458
  | `inkeep()` | Not available. Inkeep runs its own QA pipeline with no reasoning control, so the adapter has no `reasoning` option and setting one is a config error. |
336
459
 
337
460
  The model has to support the level you pick: OpenAI rejects a level a model doesn't offer (`"none"` and `"xhigh"` exist only on some), so check the model's documentation before setting one. Leave it unset to keep the model's default. Like [retrieval size](#retrieval-size), it trades thoroughness for time-to-first-token, and answers stay grounded either way.
@@ -342,16 +465,45 @@ With an [analytics provider](/docs/configuration/analytics) configured, the assi
342
465
 
343
466
  | Event | When | Properties |
344
467
  | --- | --- | --- |
345
- | `ask` | A question is sent | `path`, `questionChars` |
346
- | `ask_answer` | The answer finishes streaming | `path`, `questionChars`, `ms`, `chars` |
347
- | `ask_error` | The request fails, breaks, or comes back empty | `path`, `questionChars`, `ms`, `status` |
468
+ | `ask` | A question is sent | `path`, `questionChars`, `thread` |
469
+ | `ask_answer` | The answer finishes streaming | `path`, `questionChars`, `thread`, `ms`, `chars` |
470
+ | `ask_error` | The request fails, breaks, or comes back empty | `path`, `questionChars`, `thread`, `ms`, `status` |
348
471
 
349
- `path` is the page the reader asked from (the served pathname, so it matches the feedback widget and your pageviews under a `base`), `questionChars` the question's length, `ms` the time from sending the question to the last chunk, and `chars` the answer's length. `status` is the HTTP status: `0` when no response arrived at all (offline, DNS, CORS), and `200` when the response was fine but its stream broke mid-answer — how a provider or credential error surfaces, since the backend has already sent its headers — or delivered nothing. Clearing the conversation mid-answer reports neither outcome.
472
+ `path` is the page the reader asked from (the served pathname, so it matches the feedback widget and your pageviews under a `base`), `questionChars` the question's length, `thread` the conversation's id (new when the reader clears it), `ms` the time from sending the question to the last chunk, and `chars` the answer's length. `status` is the HTTP status: `0` when no response arrived at all (offline, DNS, CORS), and `200` when the response was fine but its stream broke mid-answer — how a provider or credential error surfaces, since the backend has already sent its headers — or delivered nothing. Clearing the conversation mid-answer reports neither outcome.
350
473
 
351
474
  The question's text never reaches a provider: it is free-form reader input (pasted keys, error logs, names) that would breach most providers' terms and per-value size limits. It travels only on the `blume:track` DOM event, as `question` in `detail.props`, so a listener you write can forward it wherever you decide it belongs. A custom chat UI built on `useAssistant` from `blume/hooks` reports the same events. With no provider configured the built-in provider calls are no-ops, but the `blume:track` event still fires, so a custom integration listening for it receives them.
352
475
 
353
476
  ## Rate limiting
354
477
 
355
- The `POST /api/ask` endpoint is **unauthenticated** — it has to be, so the in-page assistant can call it. Blume validates each request — rejecting malformed bodies, capping it to 1–40 messages, and accepting only `user`/`assistant` roles so a caller can't inject their own system prompt and repurpose the route as a general LLM proxy — to bound how much a single call can spend against your model, but it can't stop someone from calling the endpoint repeatedly. If cost abuse is a concern, put the route behind a rate limiter — your host's (e.g. Vercel's) edge rate limiting, a middleware, or your model provider's per-key spend limits.
478
+ The `POST /api/ask` endpoint is **unauthenticated** — it has to be, so the in-page assistant can call it. Blume validates each request — rejecting malformed bodies, capping it to 1–40 messages, and accepting only `user`/`assistant` roles so a caller can't inject their own system prompt and repurpose the route as a general LLM proxy — to bound how much a single call can spend against your model. [Rate limiting](/docs/configuration/rate-limiting), on by default, then caps how many questions one reader can ask: past it, the panel tells them to try again in a few minutes.
479
+
480
+ ## Bot protection
481
+
482
+ Rate limiting counts by IP address, so a script that spreads its questions across many addresses slips past it. To stop scripts, add a bot check from `blume/captcha`: before each question, the panel gets a token from the check, and the route verifies it with the provider before the model runs. Most readers never see a challenge; the check only asks for a click when it's unsure.
483
+
484
+ ```ts blume.config.ts lineNumbers
485
+ import { defineConfig } from "blume";
486
+ import { turnstile } from "blume/captcha";
487
+
488
+ export default defineConfig({
489
+ ai: {
490
+ assistant: {
491
+ enabled: true,
492
+ captcha: turnstile({ siteKey: "0x4AAAAAAA…" }),
493
+ },
494
+ },
495
+ });
496
+ ```
497
+
498
+ | Adapter | Provider | Secret key env var |
499
+ | --- | --- | --- |
500
+ | `turnstile()` | [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) | `TURNSTILE_SECRET_KEY` |
501
+ | `hcaptcha()` | [hCaptcha](https://www.hcaptcha.com), run invisibly | `HCAPTCHA_SECRET_KEY` |
502
+
503
+ The site key goes to the browser and is safe to commit; the secret key stays on the server. For Turnstile, create a widget in the Cloudflare dashboard and choose **Managed** or **Invisible** mode. Until the secret is set, the assistant answers that it isn't configured, and `blume build` warns. The provider's script loads with a reader's first question, not with the page.
504
+
505
+ A failed check shows the reader a translated "we couldn't check that you're human" notice, and the route answers `403`. Both providers publish test keys that always pass, for trying the check locally: Turnstile's `1x00000000000000000000BB` with the secret `1x0000000000000000000000000000000AA`, and hCaptcha's `10000000-ffff-ffff-ffff-000000000001` with `0x0000000000000000000000000000000000000000`.
506
+
507
+ With an [external endpoint](#external-endpoint), the panel still sends the token, as `captcha` in the request body, for your backend to verify.
356
508
 
357
509
  The endpoint is advertised in the [agent readability manifest](/docs/discoverability/agent-discovery#agent-readability) alongside the rest of the site's machine-readable surface.