blume 2.1.0 → 2.1.1

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 (397) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/{chunk-5cg2rzgf.js → chunk-073rdvgb.js} +19 -48
  3. package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-073rdvgb.js.map} +4 -5
  4. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  5. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  6. package/dist/cli/chunk-150jk7km.js +52 -0
  7. package/dist/cli/chunk-150jk7km.js.map +10 -0
  8. package/dist/cli/{chunk-rekqz4qg.js → chunk-313vnn40.js} +16 -15
  9. package/dist/cli/chunk-313vnn40.js.map +10 -0
  10. package/dist/cli/{chunk-gp84za0h.js → chunk-3v3ffbrj.js} +2 -2
  11. package/dist/cli/{chunk-9xr4dgee.js → chunk-3x6x3pph.js} +29 -16
  12. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  13. package/dist/cli/{chunk-0gz36akr.js → chunk-42t4jxdx.js} +21 -5
  14. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  15. package/dist/cli/{chunk-ta27feev.js → chunk-54ztw1nj.js} +21 -2
  16. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  17. package/dist/cli/{chunk-jnwj5kjj.js → chunk-5vjtgs6e.js} +20 -7
  18. package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-5vjtgs6e.js.map} +3 -3
  19. package/dist/cli/chunk-66w9j7p6.js +322 -0
  20. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  21. package/dist/cli/{chunk-nd1821sk.js → chunk-7ppf1rva.js} +32 -18
  22. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  23. package/dist/cli/{chunk-p804f3pe.js → chunk-8y2kmjv8.js} +6 -6
  24. package/dist/cli/{chunk-2f7n4qzt.js → chunk-c1y538kd.js} +1 -1
  25. package/dist/cli/{chunk-1n228xqb.js → chunk-c887fj0s.js} +2 -2
  26. package/dist/cli/{chunk-y5h4r2y0.js → chunk-cfyg47zg.js} +15 -5
  27. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  28. package/dist/cli/{chunk-cmtcnh9f.js → chunk-dzwehpy7.js} +2 -2
  29. package/dist/cli/{chunk-h2a48yhd.js → chunk-e6atmn4h.js} +119 -15
  30. package/dist/cli/{chunk-h2a48yhd.js.map → chunk-e6atmn4h.js.map} +10 -10
  31. package/dist/cli/{chunk-5ffywsmb.js → chunk-e8xw9qkh.js} +8 -6
  32. package/dist/cli/{chunk-5ffywsmb.js.map → chunk-e8xw9qkh.js.map} +3 -3
  33. package/dist/cli/{chunk-ntv4qrt7.js → chunk-e9q7a2ak.js} +14 -14
  34. package/dist/cli/{chunk-xkz5xbtv.js → chunk-ehweg4fr.js} +2366 -1114
  35. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  36. package/dist/cli/{chunk-zf748avv.js → chunk-ep6nd0rn.js} +176 -281
  37. package/dist/cli/{chunk-zf748avv.js.map → chunk-ep6nd0rn.js.map} +11 -12
  38. package/dist/cli/{chunk-yxmk4pec.js → chunk-fdrkhpjy.js} +16 -15
  39. package/dist/cli/{chunk-yxmk4pec.js.map → chunk-fdrkhpjy.js.map} +3 -3
  40. package/dist/cli/{chunk-5wj2qwn2.js → chunk-ft9vfys6.js} +4 -4
  41. package/dist/cli/{chunk-4h8z9vv5.js → chunk-gb420vvz.js} +89 -46
  42. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  43. package/dist/cli/{chunk-713mngdq.js → chunk-hebn0tpx.js} +76 -10
  44. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  45. package/dist/cli/{chunk-2n4ps1b3.js → chunk-htrwgabr.js} +40 -22
  46. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  47. package/dist/cli/{chunk-zhvd6adm.js → chunk-hx3xqnk9.js} +13 -13
  48. package/dist/cli/chunk-j134vrp2.js +65 -0
  49. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  50. package/dist/cli/{chunk-y4ykbark.js → chunk-kma9f6sf.js} +2 -2
  51. package/dist/cli/{chunk-py62fewh.js → chunk-mrcccgnh.js} +3 -3
  52. package/dist/cli/{chunk-2f3ndkpw.js → chunk-n0v7m8ry.js} +3 -3
  53. package/dist/cli/{chunk-43zcf4gz.js → chunk-ncj7w596.js} +522 -353
  54. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  55. package/dist/cli/{chunk-9geymqns.js → chunk-nq1d20w2.js} +14 -14
  56. package/dist/cli/{chunk-g2z9x01v.js → chunk-nrns10mj.js} +3 -3
  57. package/dist/cli/{chunk-e7vvk5ya.js → chunk-sds923fa.js} +3 -3
  58. package/dist/cli/{chunk-cy5bm0j0.js → chunk-t2m5j4pr.js} +2 -2
  59. package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-t2m5j4pr.js.map} +1 -1
  60. package/dist/cli/{chunk-yj17vs39.js → chunk-tdqhyxfz.js} +5 -5
  61. package/dist/cli/{chunk-q3wxxq39.js → chunk-tphac74f.js} +5 -5
  62. package/dist/cli/{chunk-6bm0xpgd.js → chunk-w1r4vase.js} +2 -2
  63. package/dist/cli/index.js +24 -19
  64. package/dist/cli/index.js.map +3 -3
  65. package/dist/types/ai/ask-context.d.ts +6 -2
  66. package/dist/types/ai/component-markdown.d.ts +2 -0
  67. package/dist/types/ai/openapi-components.d.ts +4 -1
  68. package/dist/types/astro/integration.d.ts +5 -4
  69. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  70. package/dist/types/astro/pages.d.ts +10 -0
  71. package/dist/types/cli/init/scaffold.d.ts +1 -1
  72. package/dist/types/components/content/api-field.d.ts +4 -1
  73. package/dist/types/components/content/api-page.d.ts +6 -1
  74. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  75. package/dist/types/components/openapi/helpers.d.ts +22 -1
  76. package/dist/types/components/openapi/security.d.ts +10 -0
  77. package/dist/types/components/openapi/snippets.d.ts +60 -0
  78. package/dist/types/core/changelog-index.d.ts +8 -0
  79. package/dist/types/core/config-input.d.ts +30 -3
  80. package/dist/types/core/data.d.ts +4 -0
  81. package/dist/types/core/i18n-ui.d.ts +2 -0
  82. package/dist/types/core/links.d.ts +8 -0
  83. package/dist/types/core/probe.d.ts +2 -0
  84. package/dist/types/core/project-graph.d.ts +6 -0
  85. package/dist/types/core/redirect-patterns.d.ts +11 -3
  86. package/dist/types/core/schema.d.ts +119 -0
  87. package/dist/types/core/sources/cache.d.ts +3 -0
  88. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  89. package/dist/types/core/sources/contentful.d.ts +17 -1
  90. package/dist/types/core/sources/lexical.d.ts +5 -0
  91. package/dist/types/core/sources/normalize.d.ts +20 -1
  92. package/dist/types/core/sources/remote.d.ts +37 -10
  93. package/dist/types/core/sources/sanity.d.ts +1 -1
  94. package/dist/types/core/types.d.ts +17 -0
  95. package/dist/types/deploy/adapter-output.d.ts +2 -1
  96. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  97. package/dist/types/deploy/adapters/node.d.ts +58 -23
  98. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  99. package/dist/types/deploy/adapters/types.d.ts +3 -2
  100. package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
  101. package/dist/types/deploy/platforms/node.d.ts +3 -1
  102. package/dist/types/deploy/platforms/types.d.ts +18 -7
  103. package/dist/types/deploy/platforms/vercel.d.ts +20 -1
  104. package/dist/types/deploy/redirects.d.ts +17 -1
  105. package/dist/types/deploy/sitemap.d.ts +2 -1
  106. package/dist/types/deploy/vercel-negotiation.d.ts +42 -21
  107. package/dist/types/openapi/checks.d.ts +27 -0
  108. package/dist/types/openapi/graphql.d.ts +6 -0
  109. package/dist/types/openapi/model.d.ts +15 -4
  110. package/dist/types/openapi/parse.d.ts +3 -0
  111. package/dist/types/openapi/references.d.ts +16 -0
  112. package/dist/types/ratelimit/index.d.ts +1 -1
  113. package/dist/types/ratelimit/schema.d.ts +8 -0
  114. package/dist/types/ratelimit/upstash.d.ts +39 -14
  115. package/dist/types/reference/graphql.d.ts +76 -1
  116. package/dist/types/reference/index.d.ts +1 -1
  117. package/dist/types/reference/openapi.d.ts +2 -2
  118. package/dist/types/reference/options.d.ts +37 -1
  119. package/dist/types/reference/schema.d.ts +84 -0
  120. package/dist/types/search/adapters/registry.d.ts +3 -0
  121. package/dist/types/search/adapters/typesense.d.ts +14 -4
  122. package/dist/types/search/build.d.ts +3 -1
  123. package/dist/types/search/orama-index.d.ts +9 -9
  124. package/dist/types/search/sync/algolia.d.ts +5 -2
  125. package/dist/types/search/sync/index.d.ts +3 -1
  126. package/dist/types/search/sync/typesense.d.ts +11 -7
  127. package/dist/types/sources/contentful.d.ts +16 -0
  128. package/dist/types/sources/github-releases.d.ts +8 -1
  129. package/dist/types/sources/payload.d.ts +2 -0
  130. package/dist/types/sources/registry.d.ts +54 -0
  131. package/dist/types/sources/shared.d.ts +3 -0
  132. package/dist/types/sources/strapi.d.ts +2 -0
  133. package/docs/01-quickstart.mdx +4 -2
  134. package/docs/02-deployment.mdx +31 -13
  135. package/docs/advanced/changelog.mdx +11 -0
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +30 -7
  138. package/docs/cli/doctor.mdx +5 -1
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/translate.mdx +1 -0
  141. package/docs/cli/validate.mdx +4 -3
  142. package/docs/configuration/analytics.mdx +1 -1
  143. package/docs/configuration/assistant.mdx +6 -2
  144. package/docs/configuration/customization.mdx +4 -2
  145. package/docs/configuration/export.mdx +5 -3
  146. package/docs/configuration/index.mdx +3 -2
  147. package/docs/configuration/rate-limiting.mdx +23 -2
  148. package/docs/configuration/search.mdx +37 -14
  149. package/docs/content/components.mdx +2 -2
  150. package/docs/content/frontmatter.mdx +5 -3
  151. package/docs/content/i18n.mdx +2 -2
  152. package/docs/content/includes.mdx +1 -1
  153. package/docs/content/navigation.mdx +4 -4
  154. package/docs/content/sources/contentful.mdx +7 -4
  155. package/docs/content/sources/custom.mdx +7 -3
  156. package/docs/content/sources/github-releases.mdx +10 -2
  157. package/docs/content/sources/index.mdx +3 -3
  158. package/docs/content/sources/notion.mdx +1 -1
  159. package/docs/content/sources/obsidian.mdx +1 -1
  160. package/docs/content/sources/payload.mdx +3 -2
  161. package/docs/content/sources/remote-mdx.mdx +4 -0
  162. package/docs/content/sources/sanity.mdx +2 -2
  163. package/docs/content/sources/strapi.mdx +3 -2
  164. package/docs/content/syntax.mdx +2 -2
  165. package/docs/discoverability/agent-discovery.mdx +1 -1
  166. package/docs/discoverability/index.mdx +1 -1
  167. package/docs/discoverability/json-api.mdx +2 -0
  168. package/docs/discoverability/llms-txt.mdx +1 -1
  169. package/docs/discoverability/markdown.mdx +3 -1
  170. package/docs/discoverability/mcp.mdx +4 -0
  171. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  172. package/docs/references/api-pages.mdx +1 -1
  173. package/docs/references/asyncapi.mdx +3 -1
  174. package/docs/references/graphql.mdx +13 -1
  175. package/docs/references/openapi.mdx +50 -8
  176. package/package.json +9 -9
  177. package/skills/blume-migrate/SKILL.md +1 -1
  178. package/skills/blume-migrate/references/docusaurus.md +4 -4
  179. package/skills/blume-migrate/references/fumadocs.md +17 -13
  180. package/skills/blume-migrate/references/nextra.md +8 -5
  181. package/skills/blume-migrate/references/starlight.md +9 -7
  182. package/src/ai/ask-context.ts +33 -3
  183. package/src/ai/ask-limits.ts +13 -0
  184. package/src/ai/changelog-markdown.ts +11 -6
  185. package/src/ai/component-markdown.ts +1 -1
  186. package/src/ai/cors.ts +12 -3
  187. package/src/ai/mcp/query.ts +14 -6
  188. package/src/ai/openapi-components.ts +28 -4
  189. package/src/ai/relative-links.ts +4 -38
  190. package/src/astro/generate.ts +78 -48
  191. package/src/astro/integration.ts +16 -13
  192. package/src/astro/markdown-negotiation.ts +3 -0
  193. package/src/astro/module-types.ts +3 -1
  194. package/src/astro/pages.ts +23 -0
  195. package/src/astro/templates.ts +125 -80
  196. package/src/audit/catalog.ts +19 -3
  197. package/src/audit/checks/links.ts +14 -1
  198. package/src/audit/checks/network.ts +110 -6
  199. package/src/audit/checks/redirects.ts +6 -4
  200. package/src/audit/checks/robots.ts +58 -2
  201. package/src/audit/run.ts +3 -0
  202. package/src/audit/types.ts +2 -0
  203. package/src/audit/url.ts +14 -5
  204. package/src/blume-modules.d.ts +5 -1
  205. package/src/cli/args.ts +51 -0
  206. package/src/cli/build-failure.ts +23 -0
  207. package/src/cli/commands/audit.ts +10 -1
  208. package/src/cli/commands/dev.ts +18 -2
  209. package/src/cli/commands/doctor.ts +45 -12
  210. package/src/cli/commands/eval.ts +36 -4
  211. package/src/cli/commands/init.ts +6 -4
  212. package/src/cli/commands/validate.ts +10 -1
  213. package/src/cli/init/scaffold.ts +3 -3
  214. package/src/cli/prepare.ts +6 -3
  215. package/src/cli/required-secrets.ts +1 -1
  216. package/src/components/content/ApiPlayground.astro +6 -2
  217. package/src/components/content/api-field.ts +4 -1
  218. package/src/components/content/api-page.ts +31 -7
  219. package/src/components/content/mermaid-element.ts +15 -1
  220. package/src/components/islands/hooks.ts +27 -1
  221. package/src/components/layout/PageActions.astro +139 -24
  222. package/src/components/layout/RootLayout.astro +10 -2
  223. package/src/components/layout/Search.astro +17 -3
  224. package/src/components/layout/TableOfContents.astro +24 -17
  225. package/src/components/layout/head-scripts.ts +6 -1
  226. package/src/components/layout/search/algolia.ts +10 -1
  227. package/src/components/layout/search/analytics.ts +42 -14
  228. package/src/components/layout/search/endpoint.ts +48 -5
  229. package/src/components/layout/search/pagefind.ts +114 -5
  230. package/src/components/layout/search/types.ts +12 -0
  231. package/src/components/layout/search/typesense.ts +18 -6
  232. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  233. package/src/components/openapi/Authorization.astro +10 -0
  234. package/src/components/openapi/GraphqlOperation.astro +5 -0
  235. package/src/components/openapi/Operation.astro +24 -1
  236. package/src/components/openapi/ParametersTable.astro +8 -4
  237. package/src/components/openapi/Playground.astro +7 -1
  238. package/src/components/openapi/RequestBody.astro +19 -2
  239. package/src/components/openapi/Responses.astro +1 -0
  240. package/src/components/openapi/SchemaTable.astro +15 -3
  241. package/src/components/openapi/async-snippets.ts +61 -5
  242. package/src/components/openapi/async.ts +123 -29
  243. package/src/components/openapi/graphql-helpers.ts +35 -12
  244. package/src/components/openapi/helpers.ts +36 -2
  245. package/src/components/openapi/message-model.ts +54 -11
  246. package/src/components/openapi/message.ts +14 -4
  247. package/src/components/openapi/operation-model.ts +16 -7
  248. package/src/components/openapi/playground-client.ts +24 -5
  249. package/src/components/openapi/playground-schema.ts +1 -1
  250. package/src/components/openapi/schema-tree.ts +44 -14
  251. package/src/components/openapi/security.ts +10 -0
  252. package/src/components/openapi/snippets.ts +5 -3
  253. package/src/components/openapi/validate-json.ts +4 -3
  254. package/src/core/changelog-index.ts +31 -1
  255. package/src/core/component-diagnostics.ts +53 -0
  256. package/src/core/component-overrides.ts +32 -0
  257. package/src/core/config-input.ts +31 -3
  258. package/src/core/data.ts +2 -0
  259. package/src/core/diagnostics.ts +5 -0
  260. package/src/core/i18n-ui.ts +2 -0
  261. package/src/core/includes.ts +1 -1
  262. package/src/core/links.ts +47 -17
  263. package/src/core/probe.ts +26 -6
  264. package/src/core/project-graph.ts +59 -18
  265. package/src/core/redirect-patterns.ts +36 -7
  266. package/src/core/schema.ts +21 -2
  267. package/src/core/sources/cache.ts +6 -0
  268. package/src/core/sources/contentful-rich-text.ts +29 -6
  269. package/src/core/sources/contentful.ts +135 -25
  270. package/src/core/sources/filesystem.ts +26 -4
  271. package/src/core/sources/github-releases.ts +12 -1
  272. package/src/core/sources/lexical.ts +15 -10
  273. package/src/core/sources/mdx-remote.ts +62 -10
  274. package/src/core/sources/normalize.ts +227 -41
  275. package/src/core/sources/notion.ts +7 -1
  276. package/src/core/sources/obsidian.ts +16 -3
  277. package/src/core/sources/payload.ts +38 -15
  278. package/src/core/sources/remote.ts +72 -15
  279. package/src/core/sources/resolve.ts +11 -1
  280. package/src/core/sources/sanity.ts +43 -25
  281. package/src/core/sources/strapi.ts +2 -2
  282. package/src/core/types.ts +18 -0
  283. package/src/core/ui-packs/ar.ts +1 -0
  284. package/src/core/ui-packs/bg.ts +1 -0
  285. package/src/core/ui-packs/bn.ts +1 -0
  286. package/src/core/ui-packs/ca.ts +1 -0
  287. package/src/core/ui-packs/cs.ts +1 -0
  288. package/src/core/ui-packs/da.ts +1 -0
  289. package/src/core/ui-packs/de.ts +1 -0
  290. package/src/core/ui-packs/el.ts +1 -0
  291. package/src/core/ui-packs/es.ts +1 -0
  292. package/src/core/ui-packs/fa.ts +1 -0
  293. package/src/core/ui-packs/fi.ts +1 -0
  294. package/src/core/ui-packs/fr.ts +1 -0
  295. package/src/core/ui-packs/he.ts +1 -0
  296. package/src/core/ui-packs/hi.ts +1 -0
  297. package/src/core/ui-packs/hr.ts +1 -0
  298. package/src/core/ui-packs/hu.ts +1 -0
  299. package/src/core/ui-packs/id.ts +1 -0
  300. package/src/core/ui-packs/it.ts +1 -0
  301. package/src/core/ui-packs/ja.ts +1 -0
  302. package/src/core/ui-packs/ko.ts +1 -0
  303. package/src/core/ui-packs/nl.ts +1 -0
  304. package/src/core/ui-packs/no.ts +1 -0
  305. package/src/core/ui-packs/pl.ts +1 -0
  306. package/src/core/ui-packs/pt-br.ts +1 -0
  307. package/src/core/ui-packs/pt.ts +1 -0
  308. package/src/core/ui-packs/ro.ts +1 -0
  309. package/src/core/ui-packs/ru.ts +1 -0
  310. package/src/core/ui-packs/sk.ts +1 -0
  311. package/src/core/ui-packs/sr.ts +1 -0
  312. package/src/core/ui-packs/sv.ts +1 -0
  313. package/src/core/ui-packs/th.ts +1 -0
  314. package/src/core/ui-packs/tr.ts +1 -0
  315. package/src/core/ui-packs/uk.ts +1 -0
  316. package/src/core/ui-packs/vi.ts +1 -0
  317. package/src/core/ui-packs/zh-tw.ts +1 -0
  318. package/src/core/ui-packs/zh.ts +1 -0
  319. package/src/deploy/adapter-output.ts +7 -2
  320. package/src/deploy/adapters/cloudflare.ts +7 -5
  321. package/src/deploy/adapters/node.ts +47 -9
  322. package/src/deploy/adapters/types.ts +7 -3
  323. package/src/deploy/artifacts.ts +2 -1
  324. package/src/deploy/platforms/cloudflare.ts +13 -9
  325. package/src/deploy/platforms/netlify.ts +2 -1
  326. package/src/deploy/platforms/node.ts +12 -2
  327. package/src/deploy/platforms/types.ts +21 -7
  328. package/src/deploy/platforms/vercel.ts +113 -16
  329. package/src/deploy/redirects.ts +66 -6
  330. package/src/deploy/rss.ts +3 -1
  331. package/src/deploy/sitemap.ts +39 -5
  332. package/src/deploy/vercel-negotiation.ts +290 -102
  333. package/src/eval/agents.ts +18 -1
  334. package/src/eval/prompts.ts +6 -3
  335. package/src/eval/report.ts +34 -13
  336. package/src/eval/run.ts +5 -0
  337. package/src/markdown/relative-links.ts +3 -2
  338. package/src/openapi/checks.ts +273 -0
  339. package/src/openapi/graphql-build.ts +21 -0
  340. package/src/openapi/graphql.ts +11 -0
  341. package/src/openapi/model.ts +15 -3
  342. package/src/openapi/parse.ts +127 -2
  343. package/src/openapi/references.ts +43 -3
  344. package/src/openapi/source.ts +39 -14
  345. package/src/ratelimit/index.ts +1 -1
  346. package/src/ratelimit/runtime.ts +37 -19
  347. package/src/ratelimit/upstash.ts +40 -10
  348. package/src/reference/graphql.ts +15 -1
  349. package/src/reference/index.ts +1 -0
  350. package/src/reference/openapi.ts +2 -2
  351. package/src/reference/options.ts +29 -1
  352. package/src/registry/eject.ts +3 -1
  353. package/src/search/adapters/locale-scope.ts +30 -0
  354. package/src/search/adapters/typesense.ts +14 -4
  355. package/src/search/build.ts +21 -3
  356. package/src/search/orama-index.ts +163 -31
  357. package/src/search/source-pages.ts +29 -0
  358. package/src/search/sync/algolia.ts +99 -9
  359. package/src/search/sync/index.ts +52 -13
  360. package/src/search/sync/typesense.ts +72 -27
  361. package/src/sources/contentful.ts +12 -0
  362. package/src/sources/github-releases.ts +7 -1
  363. package/src/sources/shared.ts +3 -0
  364. package/src/theme/entry.ts +29 -3
  365. package/src/translate/validate.ts +23 -1
  366. package/dist/cli/chunk-0gz36akr.js.map +0 -12
  367. package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
  368. package/dist/cli/chunk-43zcf4gz.js.map +0 -36
  369. package/dist/cli/chunk-4ejxbzee.js +0 -39
  370. package/dist/cli/chunk-4ejxbzee.js.map +0 -10
  371. package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
  372. package/dist/cli/chunk-713mngdq.js.map +0 -14
  373. package/dist/cli/chunk-9xr4dgee.js.map +0 -10
  374. package/dist/cli/chunk-amrb6hgh.js +0 -145
  375. package/dist/cli/chunk-amrb6hgh.js.map +0 -11
  376. package/dist/cli/chunk-nd1821sk.js.map +0 -10
  377. package/dist/cli/chunk-rekqz4qg.js.map +0 -10
  378. package/dist/cli/chunk-ta27feev.js.map +0 -10
  379. package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
  380. package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
  381. /package/dist/cli/{chunk-gp84za0h.js.map → chunk-3v3ffbrj.js.map} +0 -0
  382. /package/dist/cli/{chunk-p804f3pe.js.map → chunk-8y2kmjv8.js.map} +0 -0
  383. /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-c1y538kd.js.map} +0 -0
  384. /package/dist/cli/{chunk-1n228xqb.js.map → chunk-c887fj0s.js.map} +0 -0
  385. /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-dzwehpy7.js.map} +0 -0
  386. /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-e9q7a2ak.js.map} +0 -0
  387. /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-ft9vfys6.js.map} +0 -0
  388. /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-hx3xqnk9.js.map} +0 -0
  389. /package/dist/cli/{chunk-y4ykbark.js.map → chunk-kma9f6sf.js.map} +0 -0
  390. /package/dist/cli/{chunk-py62fewh.js.map → chunk-mrcccgnh.js.map} +0 -0
  391. /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-n0v7m8ry.js.map} +0 -0
  392. /package/dist/cli/{chunk-9geymqns.js.map → chunk-nq1d20w2.js.map} +0 -0
  393. /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-nrns10mj.js.map} +0 -0
  394. /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-sds923fa.js.map} +0 -0
  395. /package/dist/cli/{chunk-yj17vs39.js.map → chunk-tdqhyxfz.js.map} +0 -0
  396. /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-tphac74f.js.map} +0 -0
  397. /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-w1r4vase.js.map} +0 -0
@@ -16,7 +16,16 @@ sidebar:
16
16
  | Output directory | `dist` |
17
17
  | Node version | 22.12 or newer |
18
18
 
19
- These settings work on Vercel, Netlify, Cloudflare Pages, GitHub Pages, Amazon S3 + CloudFront, or any bucket or CDN. Make sure `blume` is a dependency so the host can run the build.
19
+ These settings work on Vercel, Netlify, Cloudflare Pages, GitHub Pages, or any bucket or CDN. Make sure `blume` is a dependency so the host can run the build.
20
+
21
+ Amazon S3 behind CloudFront, with a private bucket read through origin access control, needs two more pieces, because the S3 REST endpoint looks up each key exactly and serves each file with the type it was uploaded with:
22
+
23
+ - A CloudFront Function on viewer requests that maps each page URL to the `index.html` Blume writes for it: `/quickstart` to `/quickstart/index.html`, and `/` (or any path ending in `/`) to its `index.html`. Leave a path whose last segment has a dot (`/quickstart.md`, `/_astro/app.js`) unchanged, and keep the function off `/.well-known/*`, whose `api-catalog` has no extension. Without it, S3 answers `/quickstart` with a `403` or `404`.
24
+ - Uploads that set the types a guess from the file extension gets wrong or leaves without a charset: `text/markdown; charset=utf-8` for `.md` and `.mdx`, `text/plain; charset=utf-8` for `.txt`, and `application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"` for `.well-known/api-catalog`. `dist/_headers` lists every rule your build needs.
25
+
26
+ The [S3 and CloudFront guide](/guides/s3-cloudfront-documentation-site) has a CloudFormation template with the function and a deploy script that uploads with these types.
27
+
28
+ On Cloudflare Workers, which serves a static build as static assets, set `"html_handling": "drop-trailing-slash"` under `assets` in your Wrangler config. Blume writes each page as `<route>/index.html` and links to it without a trailing slash, and Cloudflare's default (`auto-trailing-slash`) answers `/quickstart` with a `307` to `/quickstart/`. A [`cloudflare()` server build](#server-rendering) routes every page through its Worker, which serves `/quickstart` under either setting, so there it only matters when the build warns `Could not wire Accept: text/markdown negotiation` and the static layer serves the pages itself.
20
29
 
21
30
  A static build includes:
22
31
 
@@ -40,10 +49,14 @@ deployment: {
40
49
  }
41
50
  ```
42
51
 
43
- On Vercel and Netlify, automatic detection prefers your stable production domain over per-deploy preview URLs, so the canonical origin stays put across deploys. Cloudflare Pages exposes only the current deployment's URL (`CF_PAGES_URL`), which changes with every deploy, so set `site` there.
52
+ On Vercel and Netlify, automatic detection prefers your stable production domain over per-deploy preview URLs, so the canonical origin stays put across deploys. Cloudflare Pages exposes only the current deployment's URL (`CF_PAGES_URL`), which changes with every deploy, so set `site` there. Cloudflare Workers exposes none, so set `site` on Workers too.
44
53
 
45
54
  During `blume dev`, the site URL falls back to your local dev server (e.g. `http://localhost:4321`) when none is set, so site-gated features — Open Graph images, canonicals, the sitemap — work out of the box. Builds never use this fallback, so production output is never pointed at localhost.
46
55
 
56
+ ### Monorepos
57
+
58
+ Deploy the docs workspace as its own project, with the host's root directory set to it (Vercel's **Root Directory**, Netlify's **Base directory**) and the settings above. On Vercel, leave **Include source files outside of the Root Directory in the Build Step** on, which is the default, whenever the docs read files outside their workspace: [component examples](/docs/content/components#component) or a workspace package they import, an `@source` path in `theme.css`, or a spec elsewhere in the repository. With it off, the build can't reach those files. The [monorepo guide](/guides/monorepo-documentation-site) walks through a pnpm and Turborepo setup.
59
+
47
60
  ## Preview locally
48
61
 
49
62
  Before you ship, preview the production build exactly as a static host would serve it:
@@ -65,7 +78,7 @@ deployment: {
65
78
  }
66
79
  ```
67
80
 
68
- `base` (and `site`) are options of every host adapter too: `vercel({ base: "/docs" })`.
81
+ `base` (and `site`) are options of every host adapter too: `vercel({ base: "/docs" })`. `@astrojs/vercel` ignores the base, so a `vercel()` server build moves the static files to `.vercel/output/static/docs/` and the routes and redirects in `.vercel/output/config.json` under `/docs` itself.
69
82
 
70
83
  Every page keeps its own path under the base, even in a content folder that shares the base's name: with `base: "/docs"`, `docs/setup.md` is served at `/docs/docs/setup`, and its canonical URL, sidebar link, and `llms.txt` entry point there. A root-relative link you write that already starts with the base is taken to include it and left as written, so link to that page with a relative link (`./docs/setup`) or its full path (`/docs/docs/setup`).
71
84
 
@@ -112,9 +125,9 @@ deployment: vercel({
112
125
  | `vercel()` | `@astrojs/vercel` | Vercel — the most polished path |
113
126
  | `netlify()` | `@astrojs/netlify` | Netlify Functions |
114
127
  | `node()` | `@astrojs/node` | Self-hosted Node servers, containers |
115
- | `cloudflare()` | `@astrojs/cloudflare` | Cloudflare Workers and Pages |
128
+ | `cloudflare()` | `@astrojs/cloudflare` | Cloudflare Workers |
116
129
 
117
- `vercel()` and `node()` ship with Blume — choosing one just works. `netlify()` and `cloudflare()` need their package installed in your project (e.g. `bun add -d @astrojs/netlify`). If it's missing, `blume build` stops with the install command for your package manager, `blume dev` warns, and [`blume doctor`](/docs/cli/doctor) reports it. With pnpm 10 or later, also approve the build scripts those packages bring (`sharp`, and `workerd` for Cloudflare) under `allowBuilds` in `pnpm-workspace.yaml`, or pnpm stops the install.
130
+ `vercel()` and `node()` ship with Blume — choosing one just works. `netlify()` and `cloudflare()` need their package installed in your project (e.g. `bun add -d @astrojs/netlify`). If it's missing, `blume build` stops with the install command for your package manager, `blume dev` warns, and [`blume doctor`](/docs/cli/doctor) reports it. With pnpm, also approve the build scripts those packages bring under `allowBuilds` in `pnpm-workspace.yaml`, beside the `esbuild` entry `blume init` writes: `sharp` for Netlify, whose adapter pulls in its own copy, and `workerd` for Cloudflare. pnpm 11 and later stop the install with `ERR_PNPM_IGNORED_BUILDS` until you approve them; pnpm 10 skips the scripts with a warning. `allowBuilds` needs pnpm 10.26 or later.
118
131
 
119
132
  Naming an adapter switches the build to server output. To keep a static build on that host — with its site detection and the platform files it reads — pass `output: "static"`:
120
133
 
@@ -122,9 +135,9 @@ Naming an adapter switches the build to server output. To keep a static build on
122
135
  deployment: netlify({ output: "static" }),
123
136
  ```
124
137
 
125
- A server build includes everything a static build does, plus any Astro endpoints or middleware you add. The `node()` adapter produces a standalone server you can run directly with `node dist/server/entry.mjs`. It listens on `localhost:4321` unless you set the `HOST` and `PORT` environment variables when you start it (`HOST=0.0.0.0 PORT=8080 node dist/server/entry.mjs`); `host` and `port` passed to `node()` have no effect, because `@astrojs/node` replaces them with Astro's own server settings. The server resolves its packages through links into your project's `node_modules`, so deploy the project with its installed dependencies, not `dist/` alone. When the site publishes discovery files, serves images a content source downloaded, or configures redirects, Blume puts a small wrapper in front of Astro's entry (moved to `astro-entry.mjs` beside it). It sends the `.well-known` discovery files with their media types and CORS headers and downloaded SVGs sandboxed, which the standalone server's static handler can't do on its own, and answers each redirect with its configured status.
138
+ A server build includes everything a static build does, plus any Astro endpoints or middleware you add. The `node()` adapter produces a standalone server you can run directly with `node dist/server/entry.mjs`. It listens on `localhost:4321` unless you set the `HOST` and `PORT` environment variables when you start it (`HOST=0.0.0.0 PORT=8080 node dist/server/entry.mjs`); `host` and `port` passed to `node()` have no effect, because `@astrojs/node` replaces them with Astro's own server settings. Behind a reverse proxy, set `allowedDomains` so the [rate limit](/docs/configuration/rate-limiting#behind-a-reverse-proxy) counts each reader apart. The server resolves its packages through links into your project's `node_modules`, so deploy the project with its installed dependencies, not `dist/` alone. When the site publishes discovery files, serves images a content source downloaded, or configures redirects, Blume puts a small wrapper in front of Astro's entry (moved to `astro-entry.mjs` beside it). It sends the `.well-known` discovery files with their media types and CORS headers and downloaded SVGs sandboxed, which the standalone server's static handler can't do on its own, and answers each redirect with its configured status.
126
139
 
127
- A `cloudflare()` server build deploys with `npx wrangler deploy` from the project root after `blume build`: Blume writes the redirected Wrangler config to `.wrangler/deploy/` (and adds `.wrangler/` to `.gitignore`), and names the Worker after your project — the `package.json` `name`, else the site's hostname, else the folder — unless a `wrangler.jsonc` of your own at the project root sets `name`.
140
+ A `cloudflare()` server build runs on Cloudflare Workers only, since `@astrojs/cloudflare` dropped Cloudflare Pages in version 13; a static build doesn't load the package, so its `dist/` still deploys to Pages. The server build deploys with `npx wrangler deploy` from the project root after `blume build`: Blume writes the redirected Wrangler config to `.wrangler/deploy/` (and adds `.wrangler/` to `.gitignore`), and names the Worker after your project — the `package.json` `name`, else the site's hostname, else the folder — unless a `wrangler.jsonc` of your own at the project root sets `name`.
128
141
 
129
142
  On Vercel and Cloudflare, a server build also turns on [`Accept: text/markdown` content negotiation](/docs/discoverability/markdown#content-negotiation), so an agent requesting any content page with that header receives its raw-Markdown mirror at the same URL. On Vercel, Blume splices header-conditional rewrites into the deploy's routing config; on Cloudflare, it generates a small Worker in front of the Astro one and points `assets.run_worker_first` at every path except the fingerprinted build assets and the raw `.md`, `.txt`, `.json`, and `.well-known` files, since the platform would otherwise serve the prerendered pages — and its `404.html` for a URL no page backs — before any server code runs; those exempted files keep their zero-Worker fast path. That Worker also answers the prerendered per-page JSON documents (`/api/docs/pages/{route}.json`) from the asset binding when a request for one reaches it, because Astro would otherwise route it to the `/api/` catch-all.
130
143
 
@@ -161,7 +174,9 @@ Map old URLs to new ones in `blume.config.ts`:
161
174
  redirects: [{ from: "/old", to: "/new", status: 301 }],
162
175
  ```
163
176
 
164
- `status` accepts `301`, `302`, `307`, or `308` (default `301`). Server builds answer redirects at request time with the configured status. Static builds emit redirect pages **and** the platform files your host reads, so it issues a real HTTP redirect: `_redirects` for `netlify()` and `cloudflare()`, `vercel.json` for `vercel()`, and — when no host is named — both of those plus `blume-redirects.json`, a structured manifest for anything else (nginx/Apache rules, an edge worker). A `_redirects` or `vercel.json` you ship in `public/` is left untouched.
177
+ `status` accepts `301`, `302`, `307`, or `308` (default `301`). Server builds answer redirects at request time with the configured status, and so does `blume dev`. Static builds emit redirect pages **and** the platform files your host reads, so it issues a real HTTP redirect: `_redirects` for `netlify()` and `cloudflare()`, `vercel.json` for `vercel()`, and — when no host is named — both of those plus `blume-redirects.json`, a structured manifest for anything else (nginx/Apache rules, an edge worker). A `_redirects` or `vercel.json` you ship in `public/` is left untouched.
178
+
179
+ A redirect that moves one page to another moves the page's [raw Markdown](/docs/discoverability/markdown#raw-markdown) with it: `/old.md` and `/old.mdx` redirect to `/new.md` and `/new.mdx` with the same status, in `blume dev` and every build.
165
180
 
166
181
  Two hosts need more than the file:
167
182
 
@@ -193,7 +208,7 @@ redirects: [
193
208
 
194
209
  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
210
 
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.
211
+ 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. An exact redirect from a page's own URL answers the same way on every host, taking the URL over so the page never publishes, so the same code is a warning there: delete the page if it moved, or drop the redirect.
197
212
 
198
213
  ## Content types
199
214
 
@@ -201,16 +216,19 @@ Where the host reads one, a build also emits a `_headers` file that pins `charse
201
216
 
202
217
  ## Environment variables
203
218
 
204
- When a feature needs a runtime secret, Blume warns at `blume dev`/`build` if it's missing — so the problem surfaces early instead of at the first request:
219
+ When a feature needs a secret, Blume warns at `blume dev`/`build` if it's missing — so the problem surfaces early instead of at the first request:
205
220
 
206
221
  | Feature | Variable |
207
222
  | --- | --- |
208
223
  | Assistant (AI Gateway) | `AI_GATEWAY_API_KEY` (or Vercel OIDC) |
209
224
  | 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 |
225
+ | [Assistant bot protection](/docs/configuration/assistant#bot-protection) | `TURNSTILE_SECRET_KEY` (`turnstile()`), `HCAPTCHA_SECRET_KEY` (`hcaptcha()`) |
226
+ | [Rate limiting](/docs/configuration/rate-limiting) (`upstash()`) | `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` |
210
227
  | Mixedbread search | `MIXEDBREAD_API_KEY` |
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) |
228
+ | [Narration](/docs/configuration/narration) (generated voices) | `AI_GATEWAY_API_KEY` (or Vercel OIDC), or the `apiKeyEnv` you passed `gateway()` |
229
+ | [Content sources](/docs/content/sources) | `NOTION_TOKEN` (`notion()`), `SANITY_TOKEN` (`sanity()`), `CONTENTFUL_ACCESS_TOKEN` (`contentful()`, plus `CONTENTFUL_PREVIEW_TOKEN` for `--preview`, which fails without it), `PAYLOAD_API_KEY` (`payload()`), `STRAPI_API_TOKEN` (`strapi()`), `GITHUB_TOKEN` (`githubReleases()`, and `mdxRemote()` reading from GitHub) |
212
230
 
213
- Set them in `.env.local` for local dev and in your host's environment for production. A content source reads its token when it fetches, during `blume dev` and `blume build`, so set it where your site builds. Build-time secrets for search-index sync (Algolia, Orama Cloud, Typesense) are warned about separately during the sync step.
231
+ Set them in `.env.local` for local dev and in your host's environment for production. A content source reads its token when it fetches, during `blume dev` and `blume build`, and narration reads its key during `blume build`, so set those where your site builds. Build-time secrets for search-index sync (`ALGOLIA_ADMIN_API_KEY`, `ORAMA_PRIVATE_API_KEY`, `TYPESENSE_ADMIN_API_KEY`) are checked by the sync step itself: when one is unset, the build warns and skips the sync, and when it's set, a failed sync [fails the build](/docs/configuration/search#adapters). The [GitHub info](/docs/content/components#github-info) card also sends `GITHUB_TOKEN`, when it's set, to lift GitHub's API rate limit.
214
232
 
215
233
  ## Build cache
216
234
 
@@ -222,7 +240,7 @@ Blume keeps two caches a build can reuse. Astro's and Vite's caches live under `
222
240
  - **GitHub Actions** and other runners you manage keep nothing unless you cache the directory yourself:
223
241
 
224
242
  ```yaml
225
- - uses: actions/cache@v4
243
+ - uses: actions/cache@v6
226
244
  with:
227
245
  path: node_modules/.cache/blume/og
228
246
  key: blume-og-${{ runner.os }}-${{ hashFiles('**/bun.lock', '**/package-lock.json', '**/pnpm-lock.yaml') }}
@@ -62,6 +62,17 @@ Once you have at least one `type: changelog` entry, Blume generates a **`/change
62
62
  - The date follows the configured [`dateFormat`](/docs/configuration#date-format), minus the year the row's group already shows.
63
63
  - Drafts and `sidebar.hidden` entries are skipped.
64
64
 
65
+ The page is headed **Changelog** (in English), with a one-line description below. Set your own with `changelog` in your config; they also become the page's `<title>`, meta description, and OG card, and the top of its Markdown mirror at `/changelog.md`:
66
+
67
+ ```ts blume.config.ts
68
+ changelog: {
69
+ title: "Release notes",
70
+ description: "Every Acme SDK release, newest first.",
71
+ },
72
+ ```
73
+
74
+ Each takes a map of locale code to text too, like a [header tab label](/docs/content/i18n#per-locale-navigation). The index renders in the default locale, so it shows that locale's entry. Leave either unset and it keeps the built-in text, which a [UI string override](/docs/content/i18n#translated-ui) (`changelog.title` and `changelog.description` under `i18n.ui`) replaces.
75
+
65
76
  The page appears only when nothing already occupies the `/changelog` route. To replace it with your own design, add a [custom page](/docs/advanced/custom-pages) at `pages/changelog.astro` — it takes over and Blume stops generating the default index. The `<Update>` component the previous timeline was built from still ships for a custom page that wants inline release notes. It isn't one of the MDX components, so import it in the `.astro` page itself: `import Update from "blume/components/content/Update.astro";`.
66
77
 
67
78
  A header [tab](/docs/content/navigation#tabs) pointing at `/changelog` opens this index — no `href` needed.
@@ -23,7 +23,7 @@ npx skills add haydenbleasel/blume --skill blume-migrate
23
23
 
24
24
  ## Self-updating docs
25
25
 
26
- `blume-update-docs` keeps your docs in sync with the product they document. On each run — typically from a schedule you configure in your agent runner — it audits recently merged PRs, changelogs, config schemas, and CLI help against the docs content, updates only pages that are factually stale (feature-flagged work is ignored), verifies with `blume build`, and opens or updates a `blume/*` pull request. If nothing drifted, it reports a clean no-op instead of opening a noisy PR.
26
+ `blume-update-docs` keeps your docs in sync with the product they document. On each run — typically from a schedule you configure in your agent runner — it audits recently merged PRs, changelogs, config schemas, and CLI help against the docs content, updates only pages that are factually stale (feature-flagged work is ignored), verifies with `blume build` and `blume validate`, and opens or updates a `blume/*` pull request. If nothing drifted, it reports a clean no-op instead of opening a noisy PR.
27
27
 
28
28
  ```bash
29
29
  npx skills add haydenbleasel/blume --skill blume-update-docs
@@ -25,8 +25,9 @@ Findings are grouped by check rather than listed per page, so the report reads a
25
25
  ## Flags
26
26
 
27
27
  - `--fail-on error|warning|info` — the CI gate: exit non-zero at this severity or above. Defaults to `error`; `--strict` is an alias for `--fail-on warning`.
28
- - `--url <origin>` — also probe a live deployment for status codes, response headers, and redirect chains.
28
+ - `--url <origin>` — also probe a live deployment for status codes, response headers, and configured redirects.
29
29
  - `--external` — probe outbound links over the network.
30
+ - `--ignore <glob>` — with `--external`, don't probe an outbound link whose URL matches the glob, like `--ignore "https://api.acme.example/**"`. Repeat the flag for more patterns. It matches the way [`blume validate --ignore`](/docs/cli/validate#flags) does, and internal links are always checked.
30
31
  - `--only <check|category>` / `--skip <check|category>` — narrow the report to, or away from, these checks or categories (comma-separated). A term that names no check or category is an error that suggests the closest one, so a typo can't quietly empty the report.
31
32
  - `--list-checks` — print every check the audit can report, then exit.
32
33
  - `--verbose` — list every affected page with each finding's full detail, instead of the first few.
@@ -44,14 +45,18 @@ blume audit --fail-on warning # also fails on warnings
44
45
 
45
46
  ## Checking a live deployment
46
47
 
47
- Some things only the real server can tell you: whether a page that exists in `dist/` actually 404s behind a bad rewrite, whether responses are compressed, and whether an `X-Robots-Tag` header is quietly deindexing a page whose HTML looks perfectly fine. Point the audit at a deployment to add those checks:
48
+ Some things only the real server can tell you: whether a page that exists in `dist/` actually 404s behind a bad rewrite, whether responses are compressed, whether an `X-Robots-Tag` header is quietly deindexing a page whose HTML looks perfectly fine, and whether each old URL in [`redirects`](/docs/deployment#redirects) really redirects where you configured it. Point the audit at a deployment to add those checks:
48
49
 
49
50
  ```bash
50
51
  blume audit --url https://docs.example.com
51
52
  blume audit --url https://docs.example.com --external # also probe outbound links
52
53
  ```
53
54
 
54
- Outbound links are graded rather than flatly failed: a 404 is a broken link you can fix, while a 403 or 5xx is usually rate limiting or someone else's outage and is reported as a warning.
55
+ These requests go out as a plain HTTP client, not as a crawler: the audit doesn't send Googlebot's or an AI crawler's user agent, since a host's bot protection can block a client pretending to be one. A firewall or CDN rule that treats crawlers differently isn't exercised, so check it with the crawler's own tools, such as Google Search Console's URL Inspection.
56
+
57
+ The audit requests every exact redirect's old URL (a pattern names no single URL to request). One that 404s is an error. One that answers 200 is a warning: a host that doesn't read the redirect file the build wrote serves the build's meta-refresh page there instead of an HTTP redirect, which readers follow but crawlers may not. See [Redirects](/docs/deployment#redirects) for what each host reads.
58
+
59
+ Outbound links are graded rather than flatly failed: a 404 or 410, or a host name that doesn't resolve, is a broken link you can fix, while any other error status (a 401 or 403 from a site that turns bots away, a 429, a 5xx), a refused connection, or a timeout is usually rate limiting or someone else's outage and is reported as a warning. Leave out URLs the audit can't check from where it runs, like a placeholder domain or a local server, with `--ignore`.
55
60
 
56
61
  ## Fixing the findings with an agent
57
62
 
@@ -78,7 +83,7 @@ Limits worth stating plainly:
78
83
  Anything the audit did not run is reported as skipped rather than silently passing:
79
84
 
80
85
  ```
81
- ⊘ network skipped — pass --url <origin> (11 checks)
86
+ ⊘ network skipped — pass --url <origin> (12 checks)
82
87
  ⊘ external skipped — pass --external (2 checks)
83
88
  ```
84
89
 
@@ -195,7 +200,7 @@ Fix: Give each page a distinct `description`.
195
200
 
196
201
  `BLUME_AUDIT_DUPLICATE_CONTENT` · warning · from the built HTML
197
202
 
198
- Fix: Merge the pages, or set `seo.canonical` on all but one.
203
+ Fix: Merge the pages, or point `seo.canonical` on all but one at the page to index.
199
204
 
200
205
  ### Indexability
201
206
 
@@ -273,6 +278,8 @@ Fix: Remove the X-Robots-Tag header, or align it with the page's robots meta.
273
278
 
274
279
  Fix: Fix the link target, or create the page it points at.
275
280
 
281
+ Under [`deployment.base`](/docs/deployment#subpath-deploys), a root-relative link without the base is reported here, since the host serves nothing of this build there. A full URL on the same host outside the base isn't: that's how a page links to another app the host serves beside the docs.
282
+
276
283
  #### Page has links to a redirect [#link-to-redirect]
277
284
 
278
285
  `BLUME_AUDIT_LINK_TO_REDIRECT` · warning · from the built HTML
@@ -345,7 +352,7 @@ Fix: Use a real redirect in `blume.config.ts` instead of a meta refresh.
345
352
 
346
353
  `BLUME_AUDIT_REDIRECT_SOURCE_IS_PAGE` · error · from the built HTML
347
354
 
348
- Fix: Remove the redirect, or delete the page it shadows — the page wins and the redirect never fires.
355
+ Fix: Remove the redirect, or delete the page it shadows — a path can't be both, and which one readers get depends on the host.
349
356
 
350
357
  #### HTTPS to HTTP redirect [#redirect-to-http]
351
358
 
@@ -353,6 +360,14 @@ Fix: Remove the redirect, or delete the page it shadows — the page wins and th
353
360
 
354
361
  Fix: Redirect to the HTTPS URL.
355
362
 
363
+ #### Live site doesn't redirect as configured [#redirect-not-served]
364
+
365
+ `BLUME_AUDIT_REDIRECT_NOT_SERVED` · error · needs `--url`
366
+
367
+ Fix: Deploy the redirect file your host reads (see Redirects in the deployment docs), or name the host in `deployment`.
368
+
369
+ A warning instead when the old URL answers 200 (the meta-refresh page) or redirects somewhere other than the configured destination.
370
+
356
371
  ### Social cards
357
372
 
358
373
  #### Open Graph tags missing or incomplete [#og-incomplete]
@@ -509,7 +524,7 @@ Fix: Load the subresource over HTTPS — browsers block mixed content.
509
524
 
510
525
  `BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP` · warning · from the built HTML
511
526
 
512
- Fix: Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed.
527
+ Fix: Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed. A `public/sitemap.xml` replaces the generated sitemap, so add the page there or delete that file.
513
528
 
514
529
  #### Noindex page in sitemap [#noindex-in-sitemap]
515
530
 
@@ -579,6 +594,14 @@ Fix: Every robots.txt line must be a `Field: value` directive or a comment.
579
594
 
580
595
  Fix: A page can't be both disallowed in robots.txt and advertised in the sitemap.
581
596
 
597
+ #### robots.txt blocks one crawler from pages in the sitemap [#robots-blocks-crawler]
598
+
599
+ `BLUME_AUDIT_ROBOTS_BLOCKS_CRAWLER` · warning · from the built HTML
600
+
601
+ Fix: Narrow or remove the rule in that crawler's `User-agent` group if it should read these pages. Blocking an AI crawler on purpose is fine; skip this check.
602
+
603
+ A crawler named in its own `User-agent` group follows only that group, so a rule there (`User-agent: GPTBot` then `Disallow: /`) shuts it out while every other crawler reads the page. The audit reports each crawler once, at the rule that blocks its first page.
604
+
582
605
  #### robots.txt does not reference the sitemap [#robots-sitemap-missing]
583
606
 
584
607
  `BLUME_AUDIT_ROBOTS_SITEMAP_MISSING` · info · from the built HTML
@@ -9,16 +9,20 @@ description: blume doctor diagnoses configuration and content problems before yo
9
9
  blume doctor
10
10
  ```
11
11
 
12
+ `blume init` also adds it to `package.json` as a `doctor` script. With pnpm, run that as `pnpm run doctor` (`pnpm --filter <workspace> run doctor` from a monorepo root): a bare `pnpm doctor` runs pnpm's own built-in `doctor` command instead of the script.
13
+
12
14
  ## What it checks
13
15
 
14
16
  - **The Node version** against the range the installed `blume` package supports, read from its own `engines` field. A version outside it is a warning: things may work, but it isn't a combination Blume tests.
15
17
  - **`blume.config.ts`**, with the same validation a build runs. A removed or renamed key fails with a hint naming its replacement rather than a bare "unrecognized key".
16
18
  - **Every content page and folder meta**: the diagnostics `blume dev` and `blume build` print as they load the project — invalid frontmatter, navigation problems, missing include targets, and the rest — collected in one report.
19
+ - **Navigation that leads nowhere**: a tab, selector item, featured link, header action, or call to action whose path matches no page, custom `.astro` page, generated route, or reference: a warning (`BLUME_NAV_MISSING_PAGE`), the same one `blume dev` and `blume build` print.
20
+ - **[`<Component>`](/docs/content/components#component) examples that don't exist**: a `path` that names no file under `examples`, which the page would show as a "No example found" box: a warning (`BLUME_EXAMPLE_NOT_FOUND`) at its line, as `blume dev` and `blume build` print it. A `components.ts` override or island named `Component` replaces the built-in, and isn't checked.
17
21
  - **Features that need a server** on a site configured for static output — the assistant, the MCP server, the Try it playground's built-in proxy, or server-mode search (Mixedbread): an error naming the feature, with the deployment adapter to switch to (or, when a host adapter is set to `output: "static"`, telling you to drop that option).
18
22
  - **Packages your config needs** that aren't installed — the SDK a search, content source, or assistant adapter imports, a deployment adapter's `@astrojs/*` package, or the renderer for Vue or Svelte islands: an error naming each package, with the install command for your package manager. `blume build` stops on the same check.
19
23
  - **`components.ts` overrides** Blume can't plan — an inline or computed entry, or an import whose file doesn't exist: an error for each, at its line.
20
24
  - **A version-shaped folder** (`v1.0/`) on a site with no `versions` configured, which would otherwise build as ordinary content: a warning pointing at [`blume version`](/docs/cli/version).
21
- - **Secrets an enabled feature reads** that aren't set, such as `MIXEDBREAD_API_KEY` or `OPENROUTER_API_KEY`: a warning naming the variable. Doctor loads `.env` and `.env.local` first, like `blume dev` and `blume build`.
25
+ - **Secrets an enabled feature reads** that aren't set, such as `MIXEDBREAD_API_KEY` or `OPENROUTER_API_KEY`: a warning naming the variable. Doctor loads `.env` and `.env.local` first, like `blume dev` and `blume build`, and checks before any remote content source fetches, so the warning still prints when a source then fails without its token.
22
26
 
23
27
  ## The summary
24
28
 
@@ -25,7 +25,7 @@ blume eval 4 question(s) · Codex
25
25
 
26
26
  ## How it works
27
27
 
28
- Each question runs through two agent sessions, using an agent CLI you already have installed — [Codex](https://developers.openai.com/codex/cli) by default, or [Claude Code](https://claude.com/claude-code) with `--agent claude`. Blume holds no API keys and calls no model itself. With Codex, both sessions also run without Codex's shell, command, and image tools, and inherit none of your environment variables.
28
+ Each question runs through two agent sessions, using an agent CLI you already have installed — [Codex](https://developers.openai.com/codex/cli) by default, or [Claude Code](https://claude.com/claude-code) with `--agent claude`. Blume holds no API keys and calls no model itself. With Codex, both sessions also run without Codex's shell, command, and image tools, and inherit none of your environment variables. With Claude Code, both sessions run with every built-in tool turned off and skip your Claude Code settings files and `CLAUDE.md`, so your hooks, plugins, and memory can't tell the reader what the docs don't. Your Claude Code login still works; set anything else it needs, such as `ANTHROPIC_API_KEY`, in your shell rather than in `settings.json`.
29
29
 
30
30
  1. **The reader** answers the question using _only_ your documentation. It runs in an empty directory with its file, shell, and web tools disabled, connected to a private [MCP server](/docs/discoverability/mcp) that serves your docs — the same `search_docs`/`get_page` tools a real agent uses against your deployed site. It cannot read your repo, so it experiences the docs exactly like a fresh user: what isn't written doesn't exist.
31
31
  2. **The judge** grades the answer against the facts you listed, with no tools at all. Paraphrase passes; a missing or contradicted fact fails — and so does "the documentation doesn't say."
@@ -82,9 +82,9 @@ blume eval --threshold 0.8 # at least 80% must pass
82
82
  blume eval --json # machine-readable report on stdout
83
83
  ```
84
84
 
85
- The JSON report carries the same `diagnostics` + `summary` shape as `blume validate --json` and `blume audit --json`, with the per-question results (answer, score, missing facts, cost) alongside.
85
+ The JSON report carries the same `diagnostics` + `summary` shape as `blume validate --json` and `blume audit --json`, with the per-question results (answer, score, missing facts, and with Claude Code, cost) alongside.
86
86
 
87
- Because each question is two model sessions, an eval run costs real money and minutes — the per-question spend is printed as it runs. A sensible CI setup runs `blume eval` on docs changes rather than every push.
87
+ Because each question is two model sessions, an eval run costs real money and minutes. With Claude Code, the per-question spend is printed as it runs and totaled in the summary; Codex doesn't report its spend, so a Codex run shows none. A sensible CI setup runs `blume eval` on docs changes rather than every push.
88
88
 
89
89
  ## Fixing the findings
90
90
 
@@ -94,7 +94,7 @@ Each failure names the missing facts and the page that should state them. To han
94
94
  blume eval --fix
95
95
  ```
96
96
 
97
- This writes the full JSON report to a file and opens the agent interactively with a prompt that walks it through each failed question: read the named page, add the missing facts in the page's voice, and rerun `blume eval` until everything passes. The session is interactive by design — you review the edits through the agent's own permission flow — and the agent is told never to delete questions or weaken expected facts to get to green.
97
+ This writes the full JSON report to a file and opens the agent interactively with a prompt that walks it through each failed question: read the named page, add the missing facts in the page's voice, and rerun `blume eval` with the run's own `--agent`, `--file`, `--threshold`, and `--timeout` until everything passes. The session is interactive by design — you review the edits through the agent's own permission flow — and the agent is told never to delete questions or weaken expected facts to get to green.
98
98
 
99
99
  ## Flags
100
100
 
@@ -43,6 +43,7 @@ Translations you wrote by hand are **adopted, never overwritten**: a translation
43
43
  The agent is never trusted with structure. Before writing, Blume checks each reply and rebuilds the file from the source:
44
44
 
45
45
  - The frontmatter is reconstructed from the source file's data, with only the six translatable values overlaid — keys the agent invented are dropped, keys it deleted are restored, and `slug`, `icon`, `order`, and dates are source-verbatim by construction.
46
+ - `seo.canonical` is the one source key left out. It names a page in the source language, and a translation that canonicalizes to another language drops out of search in its own, so the translation gets the default canonical, its own URL.
46
47
  - The number of code fences must match the source, the body must be non-empty, and the frontmatter must parse.
47
48
  - Every heading is pinned to its source heading's anchor id with a trailing [`[#id]` marker](/docs/content/syntax#custom-anchors), unless the translation already pins one, so `#fragment` links resolve identically in every language. Headings pair up positionally, so a translation whose heading structure doesn't match the source gets no pins.
48
49
 
@@ -9,10 +9,10 @@ description: blume validate checks every link in your content — pages, anchors
9
9
  blume validate
10
10
  ```
11
11
 
12
- - **Internal page links** (`/guides/intro`, `./sibling`) must resolve to a real page — broken ones are reported as errors. A component's string `href` (`<Card href="./install">`) is checked the same way; an `href={…}` expression isn't.
12
+ - **Internal page links** (`/guides/intro`, `./sibling`) must resolve to a real page — broken ones are reported as errors. Every link form Markdown and MDX render is read: inline links, including one whose label wraps onto the next line; reference-style links, checked at their `[label]: /target` definition; autolinks (`<https://…>`, in `.md`); and a component's string `href` (`<Card href="./install">`). A lowercase HTML `<a href>` ships exactly as written, so it's checked the way a browser reads it: a relative path resolves against the page's URL, and no [`basePath`](/docs/deployment#mount-the-docs-under-a-path) is added. An `href={…}` expression isn't checked.
13
13
  - **Anchor links** (`#section`, `/guides/intro#setup`) must match an anchor on the target page — a heading's id (generated or [pinned](/docs/content/syntax#custom-anchors)) or a raw HTML element's `id` attribute — with misses reported as warnings. Ids inside code blocks, inline code, HTML comments, and `<Prompt>` blocks don't count.
14
14
  - **Asset links** are checked where the file lives: an absolute path (`/logo.png`) against the `public/` directory, a relative image embed (`![](./diagram.png)`) against the page's own folder. A plain link to a relative path still resolves as a site route — only image embeds go through the image pipeline.
15
- - **External links** are only checked with [`--external`](#flags) (off by default since it requires the network); dead links (404/410/unreachable) are errors, while rate-limited or transient responses (403/429/5xx/timeout) are warnings.
15
+ - **External links** are only checked with [`--external`](#flags) (off by default since it requires the network). A dead link is an error: a 404 or 410, or a host name that doesn't resolve. Anything else that fails is a warning, since it's more often the other site or the network than the link: any other error status (a 401 or 403 from a site that turns bots away, a 429 rate limit, a 5xx outage), a refused or dropped connection, or no answer within 10 seconds. To leave out URLs that can't be checked from where the command runs, like a placeholder domain, a local server, or a site that turns bots away, pass [`--ignore`](#flags). A bare URL that GitHub-flavored Markdown links on its own, with no link syntax around it, isn't read; [`blume audit --external`](/docs/cli/audit) checks it in the built site.
16
16
 
17
17
  ## What counts as a page
18
18
 
@@ -21,6 +21,7 @@ Links resolve against everything the built site serves, not just Markdown pages:
21
21
  ## Flags
22
22
 
23
23
  - `--external` — also check external `http(s)` links over the network.
24
+ - `--ignore <glob>` — with `--external`, don't request an external link whose URL matches the glob, and report nothing for it. The glob is matched against the link's full URL, scheme included: `*` matches within one path segment and `**` across segments, so `--ignore "https://api.acme.example/**"` skips every link to that host and `--ignore "http://localhost:*/**"` every local server. Repeat the flag for more patterns, and quote each one so your shell doesn't expand it. Internal links are always checked: they resolve against your own pages, so fix a broken one or add a [redirect](/docs/deployment#redirects).
24
25
  - `--strict` — exit non-zero on warnings too. Info-level notes stay advisory.
25
26
  - `--json` — emit the diagnostics as JSON on stdout instead of the terminal report.
26
27
 
@@ -32,7 +33,7 @@ Links resolve against everything the built site serves, not just Markdown pages:
32
33
  | `BLUME_BROKEN_ANCHOR` | warning | The page exists but has no anchor matching the fragment. |
33
34
  | `BLUME_BROKEN_ASSET` | warning | An absolute path isn't in `public/`, or a relative image embed isn't beside the page. |
34
35
  | `BLUME_ASSETS_UNCHECKED` | info | There is no `public/` directory, so absolute asset paths weren't checked. |
35
- | `BLUME_DEAD_LINK` | error or warning | An external link (with `--external`) returned 404/410 or was unreachable (error), or a transient 403/429/5xx/timeout (warning). |
36
+ | `BLUME_DEAD_LINK` | error or warning | An external link (with `--external`) returned 404/410 or its host doesn't resolve (error), or it got any other error status, no connection, or no answer in time (warning). |
36
37
 
37
38
  The exit code is the contract: errors exit non-zero, and `--strict` makes warnings do the same.
38
39
 
@@ -431,4 +431,4 @@ analytics: [
431
431
 
432
432
  ## Custom events
433
433
 
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).
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`), [assistant](/docs/configuration/assistant#analytics) (`ask`, `ask_answer`, `ask_error`), 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. Fathom, Clarity, and Hotjar take an event's name alone, so they count each event without its properties. 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. The assistant never sends a question's text to a provider, and search sends its queries as typed unless you set [`search.analytics.queries`](/docs/configuration/search#search-analytics) to `false`, which sends each query's length instead; either way the text still reaches the `blume:track` event, for a listener you write to forward. For deploying your built site, see [Deployment](/docs/deployment).
@@ -75,7 +75,7 @@ Your text is **appended to** the built-in instructions rather than replacing the
75
75
 
76
76
  The assistant is **grounded in your docs**. For each question it retrieves the most relevant pages — using the same lexical [Orama](/docs/configuration/search) index that powers on-page search — and injects them into the model's system prompt, so answers come from your content instead of the model's own knowledge. The assistant is told to answer only from the retrieved pages, to say when something isn't covered, and to cite the pages it drew from.
77
77
 
78
- The page the reader is currently on is added to the context first and used to scope retrieval to that page's language — and, on a [versioned](/docs/content/versioning) site, to its docs version — so answers stay relevant to where they are in the docs. Retrieval runs at request time from a snapshot baked into the build, so it works regardless of your [search](/docs/configuration/search) provider — even with `search: false` — and needs no configuration.
78
+ The page the reader is currently on is added to the context first and used to scope retrieval to that page's language — and, on a [versioned](/docs/content/versioning) site, to its docs version — so answers stay relevant to where they are in the docs. A question from outside the docs, such as a landing page, keeps to the language the URL's locale prefix names (`/ja`), or to the default language. Retrieval runs at request time from a snapshot baked into the build, so it works regardless of your [search](/docs/configuration/search) provider — even with `search: false` — and needs no configuration.
79
79
 
80
80
  Grounding is on for every adapter except **[Inkeep](#inkeep)**, which runs its own retrieval over the content you've indexed in its dashboard.
81
81
 
@@ -124,6 +124,8 @@ The three aren't interchangeable. `contextBudget` caps the whole injection, `exc
124
124
 
125
125
  The defaults suit a hosted model. Lower them when you're serving from your own hardware and time-to-first-token matters more than recall; answers stay grounded either way, and the assistant is told to say when something isn't covered rather than fill the gap.
126
126
 
127
+ On your own hardware, also size the model's context window for the whole prompt, not only the excerpts: the conversation so far adds as many as 24,000 characters, and with `tools` on, one `read_page` result as many as 20,000, about 5,000 tokens on its own. Ollama, for one, defaults to a 4k-token window on smaller GPUs, and a prompt that outgrows it doesn't fail: Ollama drops the start of the prompt, where the instructions and excerpts are, and the model answers without your docs.
128
+
127
129
  ## External endpoint
128
130
 
129
131
  Already have an API backend for AI? Point the panel at it and keep the docs build static:
@@ -146,6 +148,8 @@ Blume sends the same `POST` body as its built-in route:
146
148
  }
147
149
  ```
148
150
 
151
+ `messages` is the conversation so far, ending with the new question. The built-in route takes at most 40 messages and 24,000 characters of them as JSON, so once a conversation outgrows either, the panel sends its latest turns that fit and drops the oldest.
152
+
149
153
  Return a successful response whose body is a plain UTF-8 text stream. If the endpoint is on another origin, allow the docs origin with CORS: accept `OPTIONS` and `POST`, permit the `content-type` request header, and return the CORS headers on both the preflight and streamed response. With `endpoint` set, Blume generates the chat UI but no server route, grounding snapshot, provider dependency, or provider-secret warning; your backend owns retrieval, authentication, rate limiting, model access, and citations. An adapter set alongside it is ignored.
150
154
 
151
155
  ## Cross-origin callers
@@ -161,7 +165,7 @@ ai: {
161
165
  }
162
166
  ```
163
167
 
164
- The route then answers the browser's `OPTIONS` preflight and names a listed origin on every response — the streamed answer and the error statuses alike, so the caller can tell a rejected body from a provider failure. Origins that aren't listed get no header and stay subject to the browser's same-origin rule. Each entry is reduced to its origin, so `https://www.example.com/docs/` and `https://www.example.com` mean the same thing. To let any page call the route, list `"*"` instead of origins.
168
+ The route then answers the browser's `OPTIONS` preflight and names a listed origin on every response — the streamed answer and the error statuses alike, so the caller can tell a rejected body from a provider failure. Those responses expose `Retry-After` too, so after a [rate limit's](/docs/configuration/rate-limiting) `429` the caller can read how many seconds to wait. Origins that aren't listed get no header and stay subject to the browser's same-origin rule. Each entry is reduced to its origin, so `https://www.example.com/docs/` and `https://www.example.com` mean the same thing. To let any page call the route, list `"*"` instead of origins.
165
169
 
166
170
  The caller sends the same `POST` body the [external endpoint](#external-endpoint) contract describes and reads back the same text stream. Send it as JSON with a `content-type: application/json` header:
167
171
 
@@ -84,7 +84,7 @@ export default defineComponents({
84
84
  });
85
85
  ```
86
86
 
87
- Wired slots:
87
+ Wired slots (names are case-sensitive; any other key under `layout` renders nowhere, and Blume warns about it):
88
88
 
89
89
  | Slot | Replaces | Props |
90
90
  | --- | --- | --- |
@@ -95,13 +95,15 @@ Wired slots:
95
95
  | `Sidebar` | The primary navigation tree | `items`, `currentRoute` |
96
96
  | `MobileNav` | The nav inside the mobile drawer (defaults to `Sidebar`) | `items`, `currentRoute` |
97
97
  | `Breadcrumbs` | The breadcrumb trail | `crumbs` |
98
- | `TableOfContents` | The on-this-page outline | `headings`, `title`, `variant` |
98
+ | `TableOfContents` | The on-this-page outline, in the right rail (`variant: "desktop"`) and in the dropdown above the content on narrower screens (`"mobile"`) | `headings`, `title`, `variant` |
99
99
  | `Pagination` | The prev/next footer links | `prev`, `next`, `strings` |
100
100
  | `Feedback` | The "Was this page helpful?" rating below the article (rendered only when [`feedback`](/docs/configuration#page-feedback) is on) | `strings`, `comments` |
101
101
  | `PageHeader` | An injection point above the article (no built-in) | `page`, `headings`, `route` |
102
102
  | `PageFooter` | An injection point below the article (no built-in) | `page`, `headings`, `route` |
103
103
  | `Footer` | The [site footer](/docs/configuration#footer) after the content grid: links, social profiles, the repository link, and Cookie settings | `footer`, `locale`, `site`, `navigation`, `ui` |
104
104
 
105
+ The `mobile` variant also receives the [page actions](/docs/content/navigation#page-actions) as its children (an empty element the actions move into from the right rail below 1,280px), so render a `<slot />` in it to keep them on narrower screens. It renders on a page with no headings as well, with `headings` empty, so the actions still show there.
106
+
105
107
  `PageHeader` and `PageFooter` have no built-in component — they render nothing until you set them, which makes them handy injection points for a promo banner or a "last updated" note. The built-in `Footer` holds the only links to your repository and social profiles, so a `Footer` override (the place for a marketing footer the config can't express) should keep them. With `consent` set, also keep a way to reopen it: any element with `data-blume-consent-open` works as the [Cookie settings](/docs/configuration/consent#changing-an-answer) link.
106
108
 
107
109
  Layout slots accept the same [three reference forms](#reference-form) as MDX overrides, so a slot can be a path string or a hydrated descriptor (`{ component, client }`) when you want an interactive header or footer.
@@ -26,7 +26,7 @@ Any format you omit (or set to `false`) is left out of the menu. With both off
26
26
 
27
27
  ## PDF
28
28
 
29
- **Export to PDF** opens your browser's print dialog with a print stylesheet that strips the site chrome — header, sidebars, navigation — down to just the article. Choose **Save as PDF** to finish.
29
+ **Export to PDF** opens your browser's print dialog with a print stylesheet that strips the site chrome — header, sidebars, navigation, and everything after the article (last updated, feedback, previous and next links, the site footer), plus the copy buttons on code blocks — down to just the article. Collapsed [`expandable`](/docs/content/syntax#long-lines-and-long-blocks) code blocks print in full, and a page read in the dark theme prints in the light one. Choose **Save as PDF** to finish. The same stylesheet applies when a reader prints from the browser's own menu.
30
30
 
31
31
  Because it prints the live, fully-styled page, the result keeps crisp, selectable text, real fonts, and syntax-highlighted code. It needs no dependencies and works the same in dev, in production, and on static hosts.
32
32
 
@@ -38,11 +38,13 @@ PDF export goes through the browser's native print dialog, so the exact "Save as
38
38
 
39
39
  **Export to EPUB** generates a self-contained `.epub` of the current page in the browser and downloads it — ready for Apple Books, Calibre, or any e-reader. The generator is loaded only when a reader clicks Export, so it never weighs down the rest of your site.
40
40
 
41
- To keep the file readable on a device with no stylesheet or JavaScript of its own, Blume rewrites the page into clean, standalone HTML: syntax-highlighted code becomes plain monospace, decorative icons and copy buttons are dropped, and a built-in e-reader stylesheet handles spacing, code blocks, and tables.
41
+ To keep the file readable on a device with no stylesheet or JavaScript of its own, Blume rewrites the page into clean, standalone HTML: syntax-highlighted code becomes plain monospace, decorative icons and copy buttons are dropped, and a built-in e-reader stylesheet handles spacing, code blocks, and tables. Links to other pages point at your live site, since the book holds only this page.
42
+
43
+ The book is titled after the page's heading, takes the page's language, and names your site's [`title`](/docs/configuration#site) as its author and publisher. Images are fetched into the book; one the browser can't fetch, such as an image on another host that doesn't allow cross-origin requests, is left out, with a warning in the browser console.
42
44
 
43
45
  ## What gets exported
44
46
 
45
- Both formats export the **single page** a reader is viewing — not the whole site — which matches where the action lives, beneath that page's table of contents.
47
+ Both formats export the **single page** a reader is viewing — not the whole site — which matches where the action lives, beneath that page's table of contents. Like the other [page actions](/docs/content/navigation#page-actions), it sits in the right-hand column in windows at least 1,280px wide, and in the “On this page” dropdown above the content in narrower windows and on phones.
46
48
 
47
49
  :::note
48
50
  In an EPUB there's no JavaScript to switch tabs, so tabbed content — like [package-install](/docs/content/syntax) blocks and code groups — is expanded to show every panel at once. Client-rendered embeds such as Mermaid diagrams aren't included.
@@ -459,7 +459,7 @@ export default defineConfig({
459
459
  });
460
460
  ```
461
461
 
462
- A reader's answer is sent as a `feedback` [custom event](/docs/configuration/analytics#custom-events) — with `helpful` (`"yes"` or `"no"`), `path`, and `title` — through every configured analytics adapter that has an event API, and as a `blume:track` event on `window`. Without an analytics adapter, the answer isn't recorded anywhere, and with [cookie consent](/docs/configuration/consent) on, the rating only shows for readers who allowed analytics, since from anyone else an answer would go nowhere. The question and thank-you text are UI strings you can translate with [`i18n.ui`](/docs/content/i18n#translated-ui).
462
+ A reader's answer is sent as a `feedback` [custom event](/docs/configuration/analytics#custom-events) — with `helpful` (`"yes"` or `"no"`), `path`, and `title` — through every configured analytics adapter that has an event API, and as a `blume:track` event on `window`. Fathom, Microsoft Clarity, and Hotjar take an event's name alone, without its properties, so they count ratings but can't tell a yes from a no. Without an analytics adapter, the answer isn't recorded anywhere, and with [cookie consent](/docs/configuration/consent) on, the rating only shows for readers who allowed analytics, since from anyone else an answer would go nowhere. The question and thank-you text are UI strings you can translate with [`i18n.ui`](/docs/content/i18n#translated-ui).
463
463
 
464
464
  ### Written feedback
465
465
 
@@ -471,7 +471,7 @@ export default defineConfig({
471
471
  });
472
472
  ```
473
473
 
474
- A comment is sent as a `feedback_comment` event with the same `helpful`, `path`, and `title` as the rating, plus the reader's `comment` (up to 1,000 characters). It goes through the same adapters as the rating, so read comments where you read events. Providers cap how long a property value can be, so a long comment may arrive cut short: Google Analytics keeps 100 characters and Mixpanel 255, while PostHog and Segment pass comments whole. Plausible groups identical values rather than listing each comment, so it suits ratings better than comments. Readers can write anything in the box, so treat comments as they might contain personal details.
474
+ A comment is sent as a `feedback_comment` event with the same `helpful`, `path`, and `title` as the rating, plus the reader's `comment` (up to 1,000 characters). It goes through the same adapters as the rating, so read comments where you read events. Providers cap how long a property value can be, so a long comment may not arrive whole: Google Analytics keeps 100 characters, Mixpanel 255 bytes (fewer characters in a script that takes several bytes a character, like Japanese), and Vercel Web Analytics takes values up to 255 characters, while PostHog and Segment pass comments whole. Fathom, Clarity, and Hotjar get no comment text at all. Plausible groups identical values rather than listing each comment, so it suits ratings better than comments. Readers can write anything in the box, so treat comments as they might contain personal details.
475
475
 
476
476
  The box's label and button are UI strings too (`feedback.comment` and `feedback.send`).
477
477
 
@@ -505,6 +505,7 @@ Each of these has its own guide. The config field is the entry point:
505
505
  | `react` | React island behavior — the React Compiler's auto-memoization | [Islands](/docs/content/islands#frameworks) |
506
506
  | `variables` | Values `{{name}}` reads in any page | [Variables](/docs/content/variables) |
507
507
  | `feedback` | The "Was this page helpful?" rating at the foot of each page (default `true`), and optional written comments after it | [Page feedback](#page-feedback) |
508
+ | `changelog` | The title and description of the generated `/changelog` index, each a string or a per-locale map | [Changelogs](/docs/advanced/changelog#the-index-page) |
508
509
 
509
510
  ## Precedence
510
511
 
@@ -5,7 +5,7 @@ description: Limit how often one reader can call the assistant, the API playgrou
5
5
 
6
6
  The server routes a reader can call cost you something each time: the [assistant](/docs/configuration/assistant) spends model tokens, the [API playground](/docs/references/openapi#cors-and-the-proxy) proxy relays requests to your API, and [Mixedbread search](/docs/configuration/search#mixedbread) runs a paid query. Blume limits how often one reader, identified by IP address, can call each of them. A reader over the limit gets `429 Too Many Requests` with a `Retry-After` header, and the assistant tells them to try again in a few minutes.
7
7
 
8
- Rate limiting is on by default, at 30 requests per reader per route every 10 minutes: enough that a person never meets it, low enough to stop a script. It only matters for a server build; a static site has no server routes to limit.
8
+ Rate limiting is on by default, at 30 requests per reader per route every 10 minutes: well above what a person sends to the assistant or the playground, and low enough to stop a script. Search counts differently. The search dialog sends a query each time the reader pauses while typing, so one search can take several requests, and a reader who searches steadily for a few minutes can reach the limit. If your site uses Mixedbread search, raise `requests`, which applies to every route. Rate limiting only matters for a server build; a static site has no server routes to limit.
9
9
 
10
10
  ```ts blume.config.ts lineNumbers
11
11
  import { defineConfig } from "blume";
@@ -38,7 +38,7 @@ rateLimit: memory({ requests: 60, window: 600 }),
38
38
 
39
39
  ### Upstash
40
40
 
41
- `upstash()` keeps the count in Upstash Redis, which every instance shares. Create a database (on Vercel, add Upstash from the Marketplace), then set its REST endpoint and token as `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`. Blume talks to Upstash's REST API, so there's nothing to install. Until both are set, the routes count in memory and `blume build` warns.
41
+ `upstash()` keeps the count in Upstash Redis, which every instance shares. Create a database, then set its REST endpoint and token as `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`. Blume talks to Upstash's REST API, so there's nothing to install. Until both are set, the routes count in memory and `blume build` warns.
42
42
 
43
43
  ```ts blume.config.ts lineNumbers
44
44
  import { upstash } from "blume/ratelimit";
@@ -46,6 +46,12 @@ import { upstash } from "blume/ratelimit";
46
46
  rateLimit: upstash(),
47
47
  ```
48
48
 
49
+ A database added from Vercel's Marketplace sets its endpoint and token as `KV_REST_API_URL` and `KV_REST_API_TOKEN` instead, with any prefix you chose when you connected it. Name the variables with `urlEnv` and `tokenEnv`:
50
+
51
+ ```ts blume.config.ts lineNumbers
52
+ rateLimit: upstash({ urlEnv: "KV_REST_API_URL", tokenEnv: "KV_REST_API_TOKEN" }),
53
+ ```
54
+
49
55
  ### Cloudflare
50
56
 
51
57
  `cloudflare()` counts with [Workers rate limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/), for a site deployed with `deployment: cloudflare()`. Blume declares the binding in the Worker's config when it builds, so there's nothing to set up. Cloudflare counts per location and takes a window of 10 or 60 seconds, so its defaults are 10 requests per 60 seconds.
@@ -62,6 +68,21 @@ export default defineConfig({
62
68
 
63
69
  The binding's namespace comes from the Worker's name, so two sites on one account don't share a count. Set `namespaceId` to choose it yourself.
64
70
 
71
+ ## Behind a reverse proxy
72
+
73
+ Vercel, Netlify, and Cloudflare tell Blume each reader's address. A `node()` server behind a reverse proxy, like nginx or Caddy, sees every request come from the proxy, so every reader shares one count and a busy site hits the limit for everyone. List the host the proxy answers for in `allowedDomains`:
74
+
75
+ ```ts blume.config.ts lineNumbers
76
+ import { node } from "blume/deploy";
77
+
78
+ deployment: node({
79
+ site: "https://docs.example.com",
80
+ allowedDomains: [{ hostname: "docs.example.com" }],
81
+ }),
82
+ ```
83
+
84
+ This is Astro's [`security.allowedDomains`](https://docs.astro.build/en/reference/configuration-reference/#securityalloweddomains): for a request whose `Host` or `X-Forwarded-Host` header matches an entry, Astro takes the reader's address from the first entry of `X-Forwarded-For`. Have the proxy pass the site's host through and replace any `X-Forwarded-For` the reader sent with the address it sees, like nginx's `proxy_set_header X-Forwarded-For $remote_addr;`. A proxy that appends to the header instead lets a reader pick their own address and get a fresh count with every request. Leave `allowedDomains` unset when readers reach the server directly.
85
+
65
86
  ## Turning it off
66
87
 
67
88
  Set `rateLimit: false` to let every request through, when your host's firewall already limits these routes, say. A request whose address the host can't tell is always let through, and so is one a shared store fails to count: the failure is logged, and readers never get locked out by a store outage.