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
@@ -5,7 +5,7 @@ description: Client-side search that works out of the box with no API keys, plus
5
5
 
6
6
  Blume ships local search with no hosted infrastructure and no API keys. It runs in the browser, works in both `blume dev` and `blume build`, and indexes only your real content — navigation chrome and excluded pages are skipped. When you outgrow it, you can switch to a hosted or semantic backend without changing how search looks or behaves — only the adapter you pass to `search` changes.
7
7
 
8
- Every backend is an **adapter** imported from `blume/search`: **Orama** (the default), **FlexSearch**, **Pagefind**, **Algolia**, **Orama Cloud**, **Typesense**, and **Mixedbread**. Each adapter owns its options, its runtime dependency, and the secrets it needs, so only the configured adapter's SDK is installed into your project — picking one backend never pulls in the others.
8
+ Every backend is an **adapter** imported from `blume/search`: **Orama** (the default), **FlexSearch**, **Pagefind**, **Algolia**, **Orama Cloud**, **Typesense**, and **Mixedbread**. Each adapter owns its options, its runtime dependency, and the secrets it needs. Orama and Pagefind ship with Blume; every other adapter's SDK is an optional peer dependency that you install yourself, so picking one backend never pulls in the others. 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.
9
9
 
10
10
  ```ts blume.config.ts lineNumbers
11
11
  import { defineConfig } from "blume";
@@ -20,7 +20,7 @@ export default defineConfig({
20
20
  });
21
21
  ```
22
22
 
23
- An adapter is a plain description of the backend — not a live client — so Blume can inline it into the generated site and into an ejected project. Pass the adapter directly, or use the object form when you also want to set the [popular links](#popular-pages) or [indexing](#whats-indexed) options beside it:
23
+ An adapter is a plain description of the backend — not a live client — so Blume can inline it into the generated site and into an ejected project. Pass the adapter directly, or use the object form when you also want to set the [popular links](#popular-pages), [indexing](#whats-indexed), or [analytics](#search-analytics) options beside it:
24
24
 
25
25
  ```ts blume.config.ts lineNumbers
26
26
  import { pagefind } from "blume/search";
@@ -77,19 +77,23 @@ search: {
77
77
  },
78
78
  ```
79
79
 
80
- Each fence's body and title (`blume.config.ts` above) become searchable; the language and fence markers don't. On `.mdx` pages the index reads components as the text they show — a Card's title, a Tab's label, a TypeTable's descriptions — using the same serializers as the [agent surfaces](/docs/discoverability/markdown), so an `agents.markdownComponents` entry covers your own components too. The option has no effect on Pagefind or Mixedbread. Expect the index to grow with your fenced content — the client index ships to every reader, hosted adapters cap record size (Algolia rejects the sync batch when one page's record exceeds its plan's limit, leaving the previous index live), and a hit inside a fence shows flattened code in the result excerpt.
80
+ Each fence's body and title (`blume.config.ts` above) become searchable; the language and fence markers don't. On `.mdx` pages the index reads components as the text they show — a Card's title, a Tab's label, a TypeTable's descriptions — using the same serializers as the [agent surfaces](/docs/discoverability/markdown), so an `agents.markdownComponents` entry covers your own components too. The option has no effect on Pagefind or Mixedbread. Expect the index to grow with your fenced content — the client index ships to every reader, hosted adapters store more (the [Algolia](#algolia) sync splits a long page across several records), and a hit inside a fence shows flattened code in the result excerpt.
81
81
 
82
82
  On a [versioned](/docs/content/versioning) site, results default to the version being viewed, with an "All versions" toggle in the dialog footer (remembered per reader). Cross-version hits name their version on the row. Orama, FlexSearch, Algolia, and Typesense honor the scoping — hosted records carry a `version` facet, with the current docs uploaded as `"current"`. Pagefind, Orama Cloud, and Mixedbread don't scope by version: their results span every version, and the dialog leaves out the toggle.
83
83
 
84
+ On a site with [`i18n`](/docs/content/i18n), results default to the language of the page being viewed, with an "All languages" toggle beside it (also remembered per reader). Orama and FlexSearch filter their index in the browser, and Algolia, Orama Cloud, and Typesense filter on the `locale` each record carries. Pagefind keeps an index per language: it searches the page's own, and with "All languages" it merges in the others, which it then searches with the page language's word stemming. Mixedbread doesn't scope by language: its results span every language, and the dialog leaves out the toggle.
85
+
84
86
  ## Tags
85
87
 
86
- Add `search.tags` to a page's frontmatter to group it under a filter in the search dialog — readers can narrow results to a tag with a click. Tags also become a facet on the hosted adapters.
88
+ Add `search.tags` to a page's frontmatter to label it for a hosted adapter. The page's first tag is uploaded as a `tag` field on its record in Algolia, Orama Cloud, and Typesense, where you can filter on it in the provider's dashboard or your own search UI. Typesense declares `tag` as a facet; on Algolia, add it to the index's `attributesForFaceting` yourself, and the sync keeps it there.
87
89
 
88
90
  ```yaml
89
91
  search:
90
92
  tags: [api, reference]
91
93
  ```
92
94
 
95
+ The search dialog doesn't read tags. Its filters group results by the sidebar section each page is in, and they show with Orama and FlexSearch, whose indexes carry each page's section.
96
+
93
97
  ## Ranking
94
98
 
95
99
  Two frontmatter fields shape how a page ranks. `search.keywords` lists extra terms the page is found by, even when its text doesn't use them. It suits a page readers search for by an old name, an acronym, or a competitor's term. `search.boost` multiplies the page's relevance: above 1 moves it up, below 1 moves it down.
@@ -106,7 +110,8 @@ How much a boost moves a page depends on the adapter:
106
110
 
107
111
  - **Orama** (the default) and **FlexSearch** multiply every match by its page's boost before sorting, so a boost of 3 counts exactly three times. The [MCP server](/docs/discoverability/mcp) and the [assistant](/docs/configuration/assistant)'s search rank the same way.
108
112
  - **Pagefind** weighs a boosted page's text more heavily. It damps repeated matches, so the lift is smaller than the multiplier: a boost of 5 ranks the page higher, but closer to twice as high. Pagefind counts a boost over 10 as 10.
109
- - **Algolia** and **Typesense** sort by boost among results that match about equally well, so a boost settles close calls without lifting a weaker match over a strong one. On Algolia, the sync adds `desc(boost)` to the index's custom ranking, after any ranking you set.
113
+ - **Algolia** sorts by boost among results that match about equally well, so a boost settles close calls without lifting a weaker match over a strong one. The sync adds `desc(boost)` to the index's custom ranking, after any ranking you set.
114
+ - **Typesense** sorts by relevance, then by boost among results with the same relevance score, so a boost settles ties without lifting a weaker match over a strong one.
110
115
  - **Orama Cloud** fetches a wider set of results and re-sorts them by relevance times boost.
111
116
  - **Mixedbread** searches your own store, which Blume doesn't upload to, so boosts and keywords don't reach it.
112
117
 
@@ -114,14 +119,24 @@ Keywords are searched on every adapter except Mixedbread.
114
119
 
115
120
  ## Search analytics
116
121
 
117
- With an [analytics adapter](/docs/configuration/analytics) configured, search reports what readers look for. A query is recorded once it settles, after a second without typing or when the reader picks a result or closes the dialog, as a `search` event with the `query`, the number of `results`, and the `path` it was searched from. A query with no results shows up with `results: 0`, so filtering on it lists what your docs don't cover yet. Picking a result sends a `search_select` event with the `query`, the result's 1-based `position`, and its `url`, for click-through.
122
+ With an [analytics adapter](/docs/configuration/analytics) configured, search reports what readers look for. A query is recorded once it settles, after a second without typing or when the reader picks a result or closes the dialog, as a `search` event with the `query`, the number of `results`, and the `path` it was searched from. A query with no results shows up with `results: 0`, so filtering on it lists what your docs don't cover yet. Picking a result sends a `search_select` event with the `query`, the result's 1-based `position`, its `url`, and the `path` it was picked from, for click-through.
118
123
 
119
- Queries are sent as typed, up to 100 characters. Your analytics sees them the way it sees any page's search terms, so treat them as reader input. With [cookie consent](/docs/configuration/consent) on, only readers who allowed analytics are counted.
124
+ Queries are sent as typed, up to 100 characters. Your analytics sees them the way it sees any page's search terms, so treat them as reader input: a reader can paste an email address or a key into the box, and Google Analytics and Plausible forbid sending personal data. To keep query text away from your providers, turn it off:
125
+
126
+ ```ts blume.config.ts lineNumbers
127
+ search: {
128
+ analytics: { queries: false },
129
+ },
130
+ ```
131
+
132
+ The `search` and `search_select` events then carry `queryChars`, the query's length, in place of `query`. The text travels only on the [`blume:track`](/docs/configuration/analytics#custom-events) DOM event, as `query` in `detail.props`, so a listener you write can forward it wherever you decide it belongs, the way the [assistant](/docs/configuration/assistant#analytics) handles questions. With [cookie consent](/docs/configuration/consent) on, only readers who allowed analytics are counted.
120
133
 
121
134
  ## Adapters
122
135
 
123
136
  The client-side adapters are keyless and need no options — their clients are configured from `i18n` (or, for Pagefind, from the built HTML), so an unknown key is rejected by config validation rather than ignored. The hosted ones take **public** credentials (safe to ship to the browser) and read their **secret** admin key from an environment variable at build time — the secret never lands in the config or the client bundle. Every option you pass to a hosted adapter is kept verbatim and handed to its search client in the browser (or, for Mixedbread, to the search endpoint), so an option Blume doesn't name still reaches the SDK — Algolia's `liteClient`, the Typesense `Client`, or `OramaClient`. Those options must be JSON values — the descriptor is inlined into the generated project as a literal, so a function, `undefined`, or a bigint fails config validation with a path instead of vanishing on the way.
124
137
 
138
+ Algolia, Orama Cloud, and Typesense upload your pages to their index at the end of each `blume build`. When the admin key isn't set, the build warns and skips the upload, so a build without secrets, such as a local one, still works. When the key is set and the upload fails (a wrong key, an unreachable host, a rejected record), the build fails with `BLUME_SEARCH_SYNC_FAILED` instead of deploying against an index it didn't update.
139
+
125
140
  ### Orama (default)
126
141
 
127
142
  Blume's default engine. It builds a JSON index served at `/blume-search.json` and queries it in the browser — instant, client-side, and live in `blume dev` as you edit. No keys, no service. Omitting `search` entirely selects it; spell it out only when you want to be explicit:
@@ -134,7 +149,7 @@ search: orama(),
134
149
 
135
150
  #### Non-Latin scripts
136
151
 
137
- Orama's standard tokenizer keeps only basic Latin letters, digits and a handful of accented vowels, so text in any other script — Japanese, Chinese, Korean and Thai, but equally Russian, Greek, Hebrew and Hindi — would otherwise produce no matches at all. Blume handles this for you: when [`i18n.defaultLocale`](/docs/content/i18n) resolves to a non-Latin script, the index switches to a word-segmenting tokenizer (built on the browser- and Node-native `Intl.Segmenter`). Declaring your site's language is all it takes:
152
+ Orama's standard tokenizer keeps only basic Latin letters, digits and a handful of accented vowels, so text in any other script — Japanese, Chinese, Korean and Thai, but equally Russian, Greek, Hebrew and Hindi — would otherwise produce no matches at all. Blume handles this for you: pages in a locale that resolves to a non-Latin script are indexed with a word-segmenting tokenizer (built on the browser- and Node-native `Intl.Segmenter`). Declaring your site's languages is all it takes:
138
153
 
139
154
  ```ts blume.config.ts lineNumbers
140
155
  i18n: {
@@ -143,13 +158,13 @@ i18n: {
143
158
  }
144
159
  ```
145
160
 
146
- The same tokenizer serves the search dialog, the MCP server's `search_docs` tool, and assistant grounding. The script is what decides, not the language name — `az-Cyrl` is segmented while `sr-Latn` is not — and it is the default locale that decides for the whole index: on a mixed-language site every page shares the default locale's tokenizer. With a non-Latin default that's safe, because Latin words survive segmentation intact, so pages in English stay searchable alongside the default language. The reverse doesn't hold: non-Latin translations on a Latin-default site aren't searchable. Latin-script languages that lean heavily on diacritics (Vietnamese, or Serbian in Latin script) also fare worse on the standard tokenizer, which folds only a few accented vowels and splits words on the rest.
161
+ The same tokenizers serve the search dialog, the MCP server's `search_docs` tool, and assistant grounding. The script is what decides, not the language name — `az-Cyrl` is segmented while `sr-Latn` is not. With a non-Latin default, every page shares the default locale's tokenizer: Latin words survive segmentation intact, so pages in English stay searchable alongside the default language. With a Latin default, Latin-script pages keep the standard tokenizer and each translation in a non-Latin script gets its own locale's, so an English site's Japanese and Hindi pages are searchable from their own language and from a search across every language. Latin-script languages that lean heavily on diacritics (Vietnamese, or Serbian in Latin script) also fare worse on the standard tokenizer, which folds only a few accented vowels and splits words on the rest.
147
162
 
148
163
  Japanese and Chinese go one step further. Segmenting alone indexes a compound term as its parts — 資金決済法 as 資金, 決済 and 法 — which lets a page mentioning each part somewhere outrank the page the term is actually about. Han, Hiragana and Katakana are therefore indexed as overlapping character pairs, and queries on those indexes prefer pages carrying a term's pairs together, loosening to any-pair matching when no page carries them all, so typing a whole sentence still returns its closest pages. Korean and Thai keep their segmented words.
149
164
 
150
165
  ### FlexSearch
151
166
 
152
- A second keyless, client-side option. It reuses the same `/blume-search.json` index Orama ships and builds a [FlexSearch](https://github.com/nextapps-de/flexsearch) document index in the browser. Works in `blume dev` and `blume build`.
167
+ A second keyless, client-side option (install `flexsearch`). It reuses the same `/blume-search.json` index Orama ships and builds a [FlexSearch](https://github.com/nextapps-de/flexsearch) document index in the browser. Works in `blume dev` and `blume build`.
153
168
 
154
169
  FlexSearch has no equivalent segmentation hook, so for sites in a non-Latin script prefer Orama (the default) or [Pagefind](#pagefind), whose `pagefind_extended` binary indexes a broad set of languages and segments Chinese, Japanese and Korean natively.
155
170
 
@@ -173,7 +188,9 @@ Pagefind only runs during `blume build`, so search isn't available in `blume dev
173
188
 
174
189
  ### Algolia
175
190
 
176
- The browser queries [Algolia](https://www.algolia.com) directly with your search-only key. Each `blume build` replaces the index using the admin key from `ALGOLIA_ADMIN_API_KEY` (the build warns and skips the upload if it's unset). The whole index is replaced on every sync, so pages you delete or rename don't linger as stale results. The sync also adds `filterOnly(locale)` and `filterOnly(version)` to the index's `attributesForFaceting`, keeping any facets you declared yourself, because the dialog scopes results by language and docs version, and in Algolia a filter on an attribute that isn't declared for faceting matches nothing.
191
+ Install `algoliasearch`. The browser queries [Algolia](https://www.algolia.com) directly with your search-only key. Each `blume build` replaces the index using the admin key from `ALGOLIA_ADMIN_API_KEY` (the build warns and skips the upload if it's unset). The whole index is replaced on every sync, so pages you delete or rename don't linger as stale results. The sync also adds `filterOnly(locale)` and `filterOnly(version)` to the index's `attributesForFaceting`, keeping any facets you declared yourself, because the dialog scopes results by language and docs version, and in Algolia a filter on an attribute that isn't declared for faceting matches nothing.
192
+
193
+ Algolia caps a record at 10 KB on its Build and Grow plans, so the sync splits a longer page into several records, each carrying the page's title, description, and URL with a stretch of its text. It then sets the index's `attributeForDistinct` to `url`, unless you set your own, so a split page appears once in results, at its best-matching record.
177
194
 
178
195
  ```ts blume.config.ts lineNumbers
179
196
  import { algolia } from "blume/search";
@@ -187,7 +204,7 @@ search: algolia({
187
204
 
188
205
  ### Orama Cloud
189
206
 
190
- Hosted Orama. The browser queries your index endpoint with the public API key; `blume build` pushes records to the index using `ORAMA_PRIVATE_API_KEY`. Set `indexId` to enable the sync.
207
+ Hosted Orama (install `@oramacloud/client`). The browser queries your index endpoint with the public API key; `blume build` pushes records to the index using `ORAMA_PRIVATE_API_KEY`. Set `indexId` to enable the sync; without it, the build warns and skips the sync even with the key set.
191
208
 
192
209
  ```ts blume.config.ts lineNumbers
193
210
  import { oramaCloud } from "blume/search";
@@ -201,7 +218,7 @@ search: oramaCloud({
201
218
 
202
219
  ### Typesense
203
220
 
204
- Self-hosted or cloud [Typesense](https://typesense.org). The browser queries the collection with the search-only key; `blume build` recreates the collection and imports documents using `TYPESENSE_ADMIN_API_KEY`. The collection is dropped and rebuilt on every sync so deleted or renamed pages don't linger as stale results — if you hand-tune the collection's settings, reapply them after a build.
221
+ Self-hosted or cloud [Typesense](https://typesense.org). Install `typesense`. The browser queries the collection with the search-only key. Each `blume build` imports your pages into a new collection using `TYPESENSE_ADMIN_API_KEY`, then points an [alias](https://typesense.org/docs/latest/api/collection-alias.html) named after `collection` at it and drops the collection it replaced. Searches keep reading the previous collection until the new one is complete, and deleted or renamed pages don't linger as stale results. If the sync fails, the build fails and the alias keeps serving the last complete sync. The admin key needs to manage collections and aliases. On the first sync, a collection that already has the `collection` name (from an earlier Blume version) is replaced by the alias, and search keys scoped to that name keep working. Settings you hand-tune on a collection don't carry over to the next one, so reapply them after a build.
205
222
 
206
223
  ```ts blume.config.ts lineNumbers
207
224
  import { typesense } from "blume/search";
@@ -214,9 +231,13 @@ search: typesense({
214
231
  }),
215
232
  ```
216
233
 
234
+ Typesense's default tokenizer splits text on spaces, so in Chinese, Japanese, or Thai a query only matches from the start of a run of text. Set `locale` to your pages' language as a two-letter code (`locale: "ja"`, `"zh"`, `"th"`) and the sync tokenizes the searched fields for it; Latin-script words in those fields still match. One collection holds every language, so the setting applies to all of them.
235
+
217
236
  ### Mixedbread
218
237
 
219
- Semantic search via [Mixedbread](https://www.mixedbread.com). Queries are proxied through a generated `/api/search` endpoint that holds your key, so this adapter **requires server output**: a host adapter such as `deployment: vercel()` from `blume/deploy` (see [Server rendering](/docs/deployment#server-rendering)). The endpoint reads `MIXEDBREAD_API_KEY`. Sync your content to the store with the Mixedbread CLI in your build, e.g. `mxbai vs sync <STORE_ID> ./content --ci`.
238
+ Semantic search via [Mixedbread](https://www.mixedbread.com) (install `@mixedbread/sdk`). Queries are proxied through a generated `/api/search` endpoint that holds your key, so this adapter **requires server output**: a host adapter such as `deployment: vercel()` from `blume/deploy` (see [Server rendering](/docs/deployment#server-rendering)). The endpoint reads `MIXEDBREAD_API_KEY`. Blume doesn't upload to the store: sync your content folder with the [Mixedbread CLI](https://www.npmjs.com/package/@mixedbread/cli) as part of your build, e.g. `mxbai store sync <STORE_ID> ./docs --yes` (the CLI reads its key from `MXBAI_API_KEY`).
239
+
240
+ Each result links to the page its file renders, found by the path the CLI records for the file, so run the sync from your project root or a folder above it, such as a monorepo root. A page appears once, at its best-matching passage, and passages from files that aren't pages, or from pages excluded from search, are left out.
220
241
 
221
242
  ```ts blume.config.ts lineNumbers
222
243
  import { mixedbread } from "blume/search";
@@ -226,6 +247,8 @@ search: mixedbread({
226
247
  }),
227
248
  ```
228
249
 
250
+ The search dialog waits for the reader to pause typing before it sends a query, and drops a query the reader has typed past. Each query sent counts toward the endpoint's [rate limit](/docs/configuration/rate-limiting).
251
+
229
252
  ### Disabling search
230
253
 
231
254
  ```ts blume.config.ts lineNumbers
@@ -544,7 +544,7 @@ A **Prop / Type** grid where each row expands to reveal its description and deta
544
544
 
545
545
  ### Auto type table
546
546
 
547
- Generate a type table from a TypeScript type so the docs stay in sync with the source. Point `AutoTypeTable` at a file with `path` (resolved from your project root) and a type `name`. Descriptions come from JSDoc comments, defaults from `@default` tags, and optional properties (`?`) are marked accordingly.
547
+ Generate a type table from a TypeScript type so the docs stay in sync with the source. Point `AutoTypeTable` at a file with `path` (resolved from your project root, the folder you run `blume` in) and a type `name`. Descriptions come from JSDoc comments, defaults from `@default` tags, and optional properties (`?`) are marked accordingly.
548
548
 
549
549
  ```astro
550
550
  <AutoTypeTable path="./src/button.ts" name="ButtonProps" />
@@ -685,7 +685,7 @@ The card reads the instance from [`github.host`](/docs/configuration#github-ente
685
685
 
686
686
  ## Component
687
687
 
688
- `Component` renders an example file from your project's `examples/` directory as a live preview alongside its highlighted source, in tabs. Point it at a file with `path` — its location under `examples/`, without the extension (so `examples/counter.tsx` is `path="counter"`). React, Vue, Svelte, and Astro examples are all supported; framework examples hydrate, Astro ones render statically. It keeps the preview and the code in sync from a single file.
688
+ `Component` renders an example file from your project's `examples/` directory as a live preview alongside its highlighted source, in tabs. Point it at a file with `path` — its location under `examples/`, without the extension (so `examples/counter.tsx` is `path="counter"`). React, Vue, Svelte, and Astro examples are all supported; framework examples hydrate, Astro ones render statically. It keeps the preview and the code in sync from a single file. A `path` that names no example renders a "No example found" box instead, and `blume dev`, `blume build`, and [`blume doctor`](/docs/cli/doctor) warn about it (`BLUME_EXAMPLE_NOT_FOUND`) at its line.
689
689
 
690
690
  The preview renders in an isolated frame that the docs styles never reach — no prose margins, typography, or theme chrome bleed into your component. The frame gets Tailwind (preflight + utilities scanned from your project and your examples directory), Blume's design tokens so classes like `bg-background` follow the site palette by default, and it follows the site's light/dark toggle live. The pane sizes itself to the rendered example — and keeps tracking it if the example grows or shrinks after load — with the Preview and Code tabs sharing one height so toggling them never shifts the page.
691
691
 
@@ -5,6 +5,8 @@ description: Every frontmatter field a page accepts, all optional — title, des
5
5
 
6
6
  Every page accepts the following frontmatter. All fields are optional.
7
7
 
8
+ Frontmatter is YAML, so quote a value that holds a colon followed by a space, like `description: "Setup: the easy way"`. Unquoted, YAML reads the second colon as the start of a nested key, and Blume reports the file and line as `BLUME_FRONTMATTER_INVALID`.
9
+
8
10
  <TypeTable
9
11
  type={{
10
12
  title: { type: "string", description: "Page title." },
@@ -163,7 +165,7 @@ seo:
163
165
  creator: "@jane"
164
166
  ```
165
167
 
166
- `noindex` emits a robots `noindex`, drops the page from the sitemap, and skips its structured data. `x.creator` credits the page to an X account (`twitter:creator`) — a guest post's author, say. See [Metadata](/docs/discoverability/metadata#per-page-overrides) for every field.
168
+ `noindex` emits a robots `noindex`, drops the page from the sitemap, and skips its structured data. A `canonical` that names another URL drops the page from the sitemap too, since the sitemap lists canonical URLs only. `x.creator` credits the page to an X account (`twitter:creator`) — a guest post's author, say. See [Metadata](/docs/discoverability/metadata#per-page-overrides) for every field.
167
169
 
168
170
  ## Search
169
171
 
@@ -175,7 +177,7 @@ search:
175
177
  boost: 3
176
178
  ```
177
179
 
178
- `exclude` keeps the page out of search, and `tags` groups it under a filter in the search dialog. `keywords` are extra terms the page is found by, beyond its own text. `boost` multiplies the page's relevance: above 1 ranks it higher, below 1 lower. See [Ranking](/docs/configuration/search#ranking).
180
+ `exclude` keeps the page out of search, and `tags` labels its record for a hosted search adapter to filter on (see [Tags](/docs/configuration/search#tags)). `keywords` are extra terms the page is found by, beyond its own text. `boost` multiplies the page's relevance: above 1 ranks it higher, below 1 lower. See [Ranking](/docs/configuration/search#ranking).
179
181
 
180
182
  ## AI
181
183
 
@@ -184,7 +186,7 @@ ai:
184
186
  exclude: true
185
187
  ```
186
188
 
187
- `ai.exclude` keeps the page out of [`llms.txt` and `llms-full.txt`](/docs/discoverability/llms-txt#excluding-a-page). The page still renders, stays in search, and keeps its place in the sitemap.
189
+ `ai.exclude` keeps the page out of [`llms.txt` and `llms-full.txt`](/docs/discoverability/llms-txt#excluding-a-page) and out of the page map in your [site's skill](/docs/discoverability/agent-discovery#your-sites-skill). The page still renders, stays in search, the [MCP server](/docs/discoverability/mcp), and the [JSON API](/docs/discoverability/json-api), and keeps its place in the sitemap.
188
190
 
189
191
  ## Changelog
190
192
 
@@ -140,7 +140,7 @@ Blume validates each result's structure — frontmatter, code fences, links —
140
140
 
141
141
  ## The language switcher
142
142
 
143
- When i18n is on, a language switcher appears in the header automatically, generated from your `locales`. For each page it links the matching translation in every language; where a translation is missing it links the fallback page and marks it as not translated. There's nothing to configure. Pages from a [GitHub Releases](/docs/content/sources/github-releases) source are the exception: release notes exist in one language only, so those pages render without a switcher.
143
+ When i18n is on, a language switcher appears in the header automatically, generated from your `locales`. For each page it links the matching translation in every language; where a translation is missing it links the fallback page and marks it as not translated. With fallbacks off (`fallbackLocale: null`) there's no page to link, so the switcher leaves that language out. There's nothing to configure. Pages from a [GitHub Releases](/docs/content/sources/github-releases) source are the exception: release notes exist in one language only, so those pages render without a switcher.
144
144
 
145
145
  ## Routing by browser language
146
146
 
@@ -193,7 +193,7 @@ Set [`deployment.site`](/docs/deployment) so these can be emitted as absolute UR
193
193
 
194
194
  ## Search
195
195
 
196
- Search is scoped to the active language: on a `/fr/…` page the dialog returns French results, with an **All languages** toggle to search across every locale at once. The default (Orama) and FlexSearch indexes filter in the browser; hosted providers carry a `locale` facet on each record.
196
+ Search is scoped to the active language: on a `/fr/…` page the dialog returns French results, with an **All languages** toggle to search across every locale at once. The default (Orama) and FlexSearch indexes filter in the browser, Pagefind keeps an index per language, and Algolia, Orama Cloud, and Typesense carry a `locale` field on each record. Mixedbread can't scope by language, so with it every search spans every locale and the dialog has no toggle. See [Search](/docs/configuration/search) for how each adapter handles it.
197
197
 
198
198
  ## Right-to-left
199
199
 
@@ -55,7 +55,7 @@ Upgrade to the **{{plan}}** plan to use {{feature}}.
55
55
  </include>
56
56
  ```
57
57
 
58
- A prop applies to the included file and to anything it includes in turn, and takes precedence over a site-wide variable with the same name. Every attribute except `lang` and `meta` is a prop; attribute values are plain text.
58
+ A prop applies to the included file and to anything it includes in turn, and takes precedence over a site-wide variable with the same name. Every attribute except `lang` and `meta` is a prop; attribute names are lowercase, and values are quoted plain text. Props reach Markdown and MDX partials only: a `{{name}}` in an included [code file](#including-code-files) isn't replaced by a prop.
59
59
 
60
60
  ## Including code files
61
61
 
@@ -267,7 +267,7 @@ navigation: {
267
267
  }
268
268
  ```
269
269
 
270
- Each link takes a `label`, an `href`, and an optional `icon` (a [built-in icon](/docs/content/components#icon) name, image path/URL, or inline SVG — the same as anywhere else). An `href` may point anywhere: an external URL opens in a new tab, while an internal route (`/contact`) is validated against your pages at build time, warning you if nothing matches.
270
+ Each link takes a `label`, an `href`, and an optional `icon` (a [built-in icon](/docs/content/components#icon) name, image path/URL, or inline SVG — the same as anywhere else). An `href` may point anywhere: an external URL opens in a new tab, while an internal route (`/contact`) is validated against your pages at build time and by [`blume doctor`](/docs/cli/doctor), warning you if nothing matches.
271
271
 
272
272
  ## Explicit sidebar
273
273
 
@@ -339,14 +339,14 @@ These come for free from the sidebar tree — no configuration:
339
339
 
340
340
  ## On this page
341
341
 
342
- A right-rail outline is generated automatically from each page's headings — `##` and `###` by default — so long pages stay scannable. Set [`toc`](/docs/configuration#table-of-contents) to change the heading range or turn the outline off. On narrower screens, where the right rail is hidden, it collapses into an “On this page” dropdown above the content.
342
+ A right-rail outline is generated automatically from each page's headings — `##` and `###` by default — so long pages stay scannable. Set [`toc`](/docs/configuration#table-of-contents) to change the heading range or turn the outline off. On narrower screens, where the right rail is hidden, it collapses into an “On this page” dropdown above the content, which also holds the [page actions](#page-actions).
343
343
 
344
344
  ## Page actions
345
345
 
346
- Below the table of contents, every page shows a set of quick actions:
346
+ Below the table of contents, every page shows a set of quick actions. In windows at least 1,280px wide they sit in the right-hand column with the outline. In narrower windows and on phones they sit at the bottom of the “On this page” dropdown above the content, which shows on a page with no headings too:
347
347
 
348
348
  - **Edit on GitHub** — links straight to the source file. Appears once you set [`github`](/docs/configuration) in your config.
349
- - **Scroll to top** — smoothly returns to the top of long pages.
349
+ - **Scroll to top** — smoothly returns to the top of long pages. It's in the right-hand column only, since the dropdown already sits at the top of the page.
350
350
 
351
351
  Others hand the page to AI tools — **Copy as Markdown** and **Open in chat** — covered in [Markdown for agents](/docs/discoverability/markdown#copy-as-markdown).
352
352
 
@@ -3,7 +3,7 @@ title: Contentful
3
3
  description: Read a Contentful content type through the Delivery API with the contentful() source, lowering rich text bodies to Markdown with nothing to install.
4
4
  ---
5
5
 
6
- The built-in `contentful()` adapter reads the entries of one content type through the Content Delivery API and lowers each entry's rich text body to Markdown: headings, marks, links, lists, quotes, tables, and embedded assets map to their Markdown equivalents, and a body held in a Markdown long-text field passes through as written, except that a link that isn't a web, mail, phone, or relative address (a `javascript:` URL, say) keeps only its label, as it does in rich text. Nothing to install — the adapter speaks the REST API directly.
6
+ The built-in `contentful()` adapter reads the entries of one content type through the Content Delivery API and lowers each entry's rich text body to Markdown: headings, marks, links, lists, quotes, and tables map to their Markdown equivalents, an embedded image asset to an image, and any other embedded file (a PDF, say) to a link to it. A body held in a Markdown long-text field passes through as written, except that a link that isn't a web, mail, phone, or relative address (a `javascript:` URL, say) keeps only its label, as it does in rich text. Nothing to install — the adapter speaks the REST API directly.
7
7
 
8
8
  ```ts blume.config.ts
9
9
  import { defineConfig } from "blume";
@@ -19,14 +19,17 @@ export default defineConfig({
19
19
  contentType: "guide",
20
20
  // environment: "master", locale: "en-US"
21
21
  // Field ids default to title / description / slug / body, and the
22
- // date to sys.updatedAt
23
- fields: { body: "content" },
22
+ // date to sys.updatedAt; order (no default) sets the sidebar order
23
+ fields: { body: "content", order: "position" },
24
24
  // Extra Delivery API query parameters
25
25
  params: { "fields.section": "sdk" },
26
+ // A space with EU data residency:
27
+ // host: "cdn.eu.contentful.com",
28
+ // previewHost: "preview.eu.contentful.com",
26
29
  }),
27
30
  ],
28
31
  },
29
32
  });
30
33
  ```
31
34
 
32
- The Delivery API token comes from the `CONTENTFUL_ACCESS_TOKEN` environment variable, which the adapter declares. Under `--preview` the adapter reads drafts through the Preview API with `CONTENTFUL_PREVIEW_TOKEN`. The Preview API rejects delivery tokens, so `--preview` without a preview token fails with a clear error rather than falling back to `CONTENTFUL_ACCESS_TOKEN`. Assets are referenced from Contentful's CDN rather than downloaded — their URLs are stable. An embedded entry maps to a Blume component through the engine's `serializers` option, keyed by content type id, when you construct `contentfulSource` directly and pass it to [`custom()`](/docs/content/sources/custom); setting `serializers` writes the source's pages as MDX so the returned components render, and an embedded entry without a serializer is noted in a comment. A link to another entry renders as plain text, since there is no route to point at.
35
+ The Delivery API token comes from the `CONTENTFUL_ACCESS_TOKEN` environment variable, which the adapter declares. Without it, the source stops before its first request with a `BLUME_MISSING_SECRET` error rather than Contentful's `401`, unless it has a [cached snapshot](/docs/content/sources#caching-and-offline-builds) to serve instead. Under `--preview` the adapter reads drafts through the Preview API with `CONTENTFUL_PREVIEW_TOKEN`. The Preview API rejects delivery tokens, so `--preview` without a preview token fails with a clear error rather than falling back to `CONTENTFUL_ACCESS_TOKEN`. A space with EU data residency is served from other hosts: set `host` to `cdn.eu.contentful.com` and `previewHost` to `preview.eu.contentful.com`. Map `fields.order` to a number field to set each page's sidebar order; it has no default. Assets are referenced from Contentful's CDN rather than downloaded — their URLs are stable. A link to another entry of the source's `contentType` points at that entry's page, at the route the page is built at (its slug under the source's `prefix`). A link to an entry the source doesn't turn into a page, one of another content type or one your `params` filter out, keeps only its text. The Delivery API leaves an unpublished, archived, or deleted asset or entry out of its response, so a page that embeds or links one renders without it (a link keeps its text) and the source warns with `BLUME_SOURCE_UNRESOLVED_LINK`. An embedded entry maps to a Blume component through the engine's `serializers` option, keyed by content type id, when you construct `contentfulSource` directly and pass it to [`custom()`](/docs/content/sources/custom); setting `serializers` writes the source's pages as MDX so the returned components render, and an embedded entry without a serializer is noted in a comment.
@@ -8,6 +8,7 @@ Any object implementing the `ContentSource` interface can be passed to `custom()
8
8
  ```ts blume.config.ts
9
9
  import { defineConfig } from "blume";
10
10
  import { custom, filesystem } from "blume/sources";
11
+ import { escapeMarkdownText } from "blume/sources/lower.ts";
11
12
  import { sanitySource } from "blume/sources/sanity.ts";
12
13
 
13
14
  export default defineConfig({
@@ -23,7 +24,8 @@ export default defineConfig({
23
24
  query: `*[_type == "guide"]`,
24
25
  // Map custom Portable Text blocks to Blume components
25
26
  serializers: {
26
- callout: (block) => `<Callout>${block.text}</Callout>`,
27
+ callout: (block) =>
28
+ `<Callout>${escapeMarkdownText(String(block.text ?? ""))}</Callout>`,
27
29
  },
28
30
  })
29
31
  ),
@@ -32,8 +34,10 @@ export default defineConfig({
32
34
  });
33
35
  ```
34
36
 
35
- A source built with one of Blume's engine factories, like `sanitySource` above, is rebuilt on the running command's context, so it reads drafts under `--preview` and keeps its snapshot in `.blume/cache` the way the built-in adapter does. A source of your own can do the same by implementing `withContext(ctx)` and returning itself rebuilt on that context.
37
+ A serializer's return value is written into the page as MDX, as is, so text it takes from the CMS must be escaped: a `{` in an editor's text would otherwise run as an expression and fail the build, and a `<b>` would render as HTML. `escapeMarkdownText` escapes it the way Blume's built-in lowering does, so the text renders as written.
36
38
 
37
- `custom()` takes a `ContentSource` instance rather than an options object, so there is nothing to pass the [shared options](/docs/content/sources#adapters) `prefix` or `pollInterval` to. The source sets its own `prefix` property, and re-fetching is whatever its `watch` method implements. It carries a live instance rather than plain data, so it declares no runtime dependency or secret of its own — the instance manages those itself.
39
+ A source built with one of Blume's engine factories, like `sanitySource` above, is rebuilt on the running command's context, so it reads drafts under `--preview` and keeps its snapshot in `.blume/cache` the way the built-in adapter does. A source of your own can do the same by implementing `withContext(ctx)` and returning itself rebuilt on that context. Unlike a built-in adapter's, a `custom()` source's snapshot is kept per preview mode only, not per set of options, since Blume sees an instance rather than its options. `blume dev` serves that snapshot after you edit the source's query, fields, or serializers, so run `blume sync` to fetch afresh.
40
+
41
+ `custom()` takes a `ContentSource` instance rather than an options object, so there is nothing to pass the [shared options](/docs/content/sources#adapters) `prefix` or `pollInterval` to. The source sets its own `prefix` property, and re-fetching is whatever its `watch` method implements. It carries a live instance rather than plain data, so it declares no runtime dependency or secret of its own — the instance manages those itself. `blume dev` and `blume build` don't warn about an unset variable a `custom()` source reads; an engine whose API requires its token (`notionSource`, `contentfulSource`) stops before its first request with `BLUME_MISSING_SECRET` instead.
38
42
 
39
43
  A custom source that reads local files should set `sourcePath` on each entry and `contentRoot` on the source itself. `sourcePath` names the file in diagnostics and resolves relative images beside it; `contentRoot` bounds the git `log` that dates pages, so without it the source's pages get no git-derived ["Last updated" date](/docs/configuration#last-modified).
@@ -20,12 +20,20 @@ export default defineConfig({
20
20
  // prereleases: false, // include prereleases (default off)
21
21
  // drafts: false, // include drafts (needs a write token)
22
22
  // limit: 100, // cap releases, newest-first
23
+ // baseUrl: "https://github.acme.com/api/v3", // GitHub Enterprise Server
23
24
  }),
24
25
  ],
25
26
  },
26
27
  });
27
28
  ```
28
29
 
29
- Each release maps to the changelog fields automatically: its name (or tag) becomes the title, its published date drives the timeline order, the tag becomes `changelog.version`, and prereleases are tagged `Prerelease` (others `Release`). The notes render as the entry body, with two changes to their links: a link that isn't a web, mail, phone, or relative address (a `javascript:` URL, say) keeps only its label, and a link back to your own [`deployment.site`](/docs/deployment) is rewritten to its root-relative path, so it follows preview deploys and your deployment base. Give the source a `prefix` so its release pages nest under a route like `/changelog/v1-2-0`.
30
+ Each release maps to the changelog fields automatically: its name (or tag) becomes the title, its published date drives the timeline order, the tag becomes `changelog.version`, and prereleases are tagged `Prerelease`, drafts (when `drafts` includes them) `Draft`, and the rest `Release`. A draft is published like any other release, so include drafts only in a build readers don't see. A summary of the notes becomes the page's meta description, which the changelog's RSS feed uses as the item's description too. The notes render as the entry body, with two changes to their links: a link that isn't a web, mail, phone, or relative address (a `javascript:` URL, say) keeps only its label, and a link back to your own [`deployment.site`](/docs/deployment) is rewritten to its root-relative path, so it follows preview deploys and your deployment base. Give the source a `prefix` so its release pages nest under a route like `/changelog/v1-2-0`. The timeline at `/changelog` takes its title and description from the top-level [`changelog`](/docs/advanced/changelog#the-index-page) config, not from the source:
30
31
 
31
- A private repo authenticates with the `GITHUB_TOKEN` environment variable — the same token the other GitHub features use, never inlined into your config; the adapter declares it, so a build without it warns. Like [every remote source](/docs/content/sources#caching-and-offline-builds) it's cached under `.blume/cache/<source>/` and served offline if the API is unreachable. Because a changelog is supplementary, a fetch failure with no cache (say a CI build without a token) degrades to an empty changelog with a warning rather than failing the build — set `GITHUB_TOKEN` in your CI and deploy environments to populate it.
32
+ ```ts blume.config.ts
33
+ changelog: {
34
+ title: "SDK releases",
35
+ description: "Every release of the Acme SDK, from GitHub.",
36
+ },
37
+ ```
38
+
39
+ A private repo authenticates with the `GITHUB_TOKEN` environment variable — the same token the other GitHub features use, never inlined into your config; the adapter declares it, so a build without it warns. For a repository on GitHub Enterprise Server, set `baseUrl` to its REST API (`https://<host>/api/v3`); `GITHUB_TOKEN` is sent there, so it must be a token for that server. Like [every remote source](/docs/content/sources#caching-and-offline-builds) it's cached under `.blume/cache/<source>/` and served offline if the API is unreachable. Because a changelog is supplementary, a fetch failure with no cache (say a CI build without a token) degrades to an empty changelog with a warning rather than failing the build — set `GITHUB_TOKEN` in your CI and deploy environments to populate it.
@@ -62,13 +62,13 @@ Every adapter that takes an options object accepts two shared options:
62
62
  - **`prefix`** namespaces the source's routes under `/<prefix>/…`, which also becomes the source's name in diagnostics and its cache directory. If two sources resolve to the same route, Blume reports a `BLUME_DUPLICATE_ROUTE` build error — give each source a distinct `prefix`.
63
63
  - **`pollInterval`** (seconds) makes a remote source re-fetch on that interval in dev, reloading only when the content actually changes. Leave it unset to fetch once and freeze for the session. Local sources (`filesystem()`, `obsidian()`) watch the filesystem instead and ignore it.
64
64
 
65
- An adapter's descriptor also declares the SDK it needs and the environment variables it reads, so the generated project declares that package, `blume dev` and `blume build` warn when a variable is unset, and `blume doctor` lists the configured sources. Options are validated when the config loads: a missing required option, an unknown key, or a leftover 1.x `{ type: "…" }` object fails with a message naming the fix.
65
+ An adapter's descriptor also declares the SDK it needs and the environment variables it reads, so the generated project declares that package, `blume dev`, `blume build`, and `blume doctor` warn when a variable is unset, and `blume doctor` lists the configured sources. The warning prints before any source fetches, so it isn't lost when the fetch then fails. `notion()` and `contentful()` can't read anything without their token, so with it unset they stop before their first request with a `BLUME_MISSING_SECRET` error, and a source whose SDK isn't installed stops the same way with `BLUME_SOURCE_SDK_MISSING`; either serves its cached snapshot instead when it has one. Options are validated when the config loads: a missing required option, an unknown key, or a leftover 1.x `{ type: "…" }` object fails with a message naming the fix.
66
66
 
67
67
  A single `filesystem()` source roots the generated docs collection at its own directory. Several filesystem sources must share one root and partition it with `include` globs — a second source rooted elsewhere is reported as `BLUME_ENTRY_ID_MISMATCH` so its pages can't silently 404.
68
68
 
69
69
  ## Caching and offline builds
70
70
 
71
- Each remote source keeps a snapshot under `.blume/cache/<source>/`. If a fetch fails — a network blip or a CMS outage — Blume serves the last-known-good snapshot with a warning rather than failing the build. Preview and published content get separate snapshots, and so does each set of source options, so a build never falls back to drafts fetched under `--preview`, and editing a source's `query` or `fields` fetches afresh. The cache lives inside `.blume/` and is regenerated, never committed.
71
+ Each remote source keeps a snapshot under `.blume/cache/<source>/`. If a fetch fails — a network blip or a CMS outage — Blume serves the last-known-good snapshot with a warning rather than failing the build. Preview and published content get separate snapshots, and so does each set of source options, so a build never falls back to drafts fetched under `--preview`, and editing a source's `query` or `fields` fetches afresh. A [`custom()`](/docs/content/sources/custom) source is the exception: its snapshot is kept per preview mode only, so after editing its options run `blume sync`. The cache lives inside `.blume/` and is regenerated, never committed.
72
72
 
73
73
  In dev, a remote source is served from its snapshot when it has one, so restarting the dev server doesn't refetch it. Run `blume sync` to pull the latest content (a running dev server hot-reloads), or `blume sync --force` to drop the snapshots first. Local filesystem sources hot-reload as usual. To poll a remote source for changes instead, set the shared `pollInterval` option (seconds) on it — the dev server re-fetches on that interval and reloads only when the content actually changes. Leave it unset to avoid hitting the API while you work.
74
74
 
@@ -76,7 +76,7 @@ In dev, a remote source is served from its snapshot when it has one, so restarti
76
76
 
77
77
  Two flags control how remote content is fetched and what's included:
78
78
 
79
- - **`--preview`** on `blume dev` or `blume build` renders drafts and pulls unpublished CMS content — Sanity switches to its `previewDrafts` perspective, Notion stops filtering by `Status`, Contentful reads through the Preview API, and Payload and Strapi request drafts. Production builds without the flag exclude drafts as usual, so a preview build is a safe way to review unpublished work before it ships.
79
+ - **`--preview`** on `blume dev` or `blume build` renders drafts and pulls unpublished CMS content — Sanity switches to its `previewDrafts` perspective, Contentful reads through the Preview API, and Payload and Strapi request drafts. Notion always reads every page and stages the ones whose `Status` isn't published as drafts, so `blume dev` shows them with or without the flag, and `blume build --preview` renders them. Production builds without the flag exclude drafts as usual, so a preview build is a safe way to review unpublished work before it ships.
80
80
  - **`blume sync`** re-fetches every remote source and regenerates the runtime. Dev is cache-first — a remote source is fetched once and served from `.blume/cache` on restart (fast and offline-tolerant), so `blume sync` is how you pull the latest CMS content without restarting the dev server (a running server hot-reloads). Add `--force` to drop the cache first, or set `pollInterval` on a source to refresh automatically.
81
81
 
82
82
  ```sh
@@ -25,7 +25,7 @@ export default defineConfig({
25
25
  });
26
26
  ```
27
27
 
28
- The integration token comes from the `NOTION_TOKEN` environment variable (share the database with your integration); the adapter declares it, so a build without it warns.
28
+ The integration token comes from the `NOTION_TOKEN` environment variable (share the database with your integration). Without it, the source stops before calling Notion with a `BLUME_MISSING_SECRET` error naming the variable, unless it has a [cached snapshot](/docs/content/sources#caching-and-offline-builds) to serve instead.
29
29
 
30
30
  The `Status` property is a publish gate by default. A page whose Status (a status or select property) holds any value other than `publishedValue`, which defaults to `Published`, imports with `draft: true`, and production builds drop drafts. A page with no Status value is published, and a database without the property publishes every page. Notion's default status options are Not started, In progress, and Done, so a database that uses them has no `Published` value and publishes nothing until you set `publishedValue: "Done"` (or whichever option means published). `properties.status` names a differently named property; to import every page whatever its status, point it at a property the database doesn't have.
31
31
 
@@ -32,6 +32,6 @@ Locale directories and version snapshots inside the vault are read the same way
32
32
 
33
33
  A link to an `index` note lands on its folder's route rather than a phantom `/index`. **An unresolved wikilink degrades to plain text with a build warning instead of failing the build**, so a vault mid-refactor still publishes. Single-line `%%comments%%` are stripped, a wikilink inside an HTML comment (`<!-- [[Draft]] -->`) is left alone since Obsidian hides it too, and a note with no `title` in its frontmatter is titled by its filename — the same rule Obsidian itself applies. An `index` note is the one exception: it names a route rather than a note, so its title falls through to Blume's usual derivation (first heading, then the humanized segment). Fenced, indented, and inline code passes through verbatim, so a note documenting the syntax survives.
34
34
 
35
- Dot-folders are skipped, including Obsidian's own `.obsidian` config directory and `.trash` — which the dev watcher also ignores, so moving a pane in the app or deleting a note to the trash doesn't rebuild your site. Editing a note does. The directories no content scan reads (`node_modules`, `dist`, `.git`, …) are skipped too, so a vault rooted at the project itself doesn't publish dependency READMEs. A note with `#` or `?` in its path is left out with an error, the way a [content file](/docs/content#files-and-routes) is: Astro can't load its copy, so rename it. Symlinks inside the vault are followed, the way the filesystem source follows them, so a shared folder linked into the vault publishes with it. A vault that sits inside the filesystem source's root must be excluded from it (`filesystem({ root: "docs", exclude: ["**/_*", "**/.*", "vault/**"] })` — an `exclude` replaces the default `["**/_*", "**/.*"]` rather than adding to it, so keep those two to leave `_`-prefixed partials and dot-files unpublished); [`blume version <id>`](/docs/cli/version) then leaves it out of the snapshot, since the vault keeps publishing its own notes as current.
35
+ Dot-folders are skipped, including Obsidian's own `.obsidian` config directory and `.trash` — which the dev watcher also ignores, so moving a pane in the app doesn't rebuild your site. Editing a note does, and so does deleting one to the trash, since the note leaves the vault. The directories no content scan reads (`node_modules`, `dist`, `.git`, …) are skipped too, so a vault rooted at the project itself doesn't publish dependency READMEs. A note with `#` or `?` in its path is left out with an error, the way a [content file](/docs/content#files-and-routes) is: Astro can't load its copy, so rename it. Symlinks inside the vault are followed, the way the filesystem source follows them, so a shared folder linked into the vault publishes with it. A vault that sits inside the filesystem source's root must be excluded from it (`filesystem({ root: "docs", exclude: ["**/_*", "**/.*", "vault/**"] })` — an `exclude` replaces the default `["**/_*", "**/.*"]` rather than adding to it, so keep those two to leave `_`-prefixed partials and dot-files unpublished); [`blume version <id>`](/docs/cli/version) then leaves it out of the snapshot, since the vault keeps publishing its own notes as current.
36
36
 
37
37
  Not yet lowered: callouts (`> [!note]`) render as plain blockquotes, embeds (`![[image.png]]`) pass through untouched, multi-line `%%comments%%` are left in place, and there is no backlink graph.
@@ -17,7 +17,8 @@ export default defineConfig({
17
17
  prefix: "handbook",
18
18
  url: "https://cms.example.com",
19
19
  collection: "docs",
20
- // Field paths default to title / description / slug / content / updatedAt
20
+ // Field paths default to title / description / slug / content /
21
+ // updatedAt; order (no default) sets the sidebar order
21
22
  fields: { body: "richText" },
22
23
  // Extra query parameters: where[...], sort
23
24
  params: { sort: "title" },
@@ -27,4 +28,4 @@ export default defineConfig({
27
28
  });
28
29
  ```
29
30
 
30
- The API key comes from the `PAYLOAD_API_KEY` environment variable and is sent as `users API-Key <key>`; set `authCollection` when the key belongs to another auth-enabled collection. Only published documents are imported — `--preview` requests drafts and stages them with `draft: true`. Documents are fetched with `depth: 1` so uploads carry their URLs, and a relative upload path (`/media/x.png`) resolves against `url`. A `block` or `inlineBlock` node maps to a Blume component through the engine's `serializers` option, keyed by `blockType`, when you construct `payloadSource` directly and pass it to [`custom()`](/docs/content/sources/custom). Setting `serializers` writes the source's pages as MDX so the returned components render; a body held in a Markdown text field stays Markdown.
31
+ The API key comes from the `PAYLOAD_API_KEY` environment variable and is sent as `users API-Key <key>`; set `authCollection` when the key belongs to another auth-enabled collection. Only published documents are imported — `--preview` requests drafts and stages them with `draft: true`. Documents are fetched with `depth: 1` so uploads carry their URLs, and a relative upload path (`/media/x.png`) resolves against `url`. Uploads are referenced at that URL rather than downloaded, since it doesn't expire the way Notion's do, so readers' browsers load them from your Payload server or its storage; to have Blume download and optimize them at build instead, authorize the host in [`image.domains`](/docs/configuration#images). A `block` or `inlineBlock` node maps to a Blume component through the engine's `serializers` option, keyed by `blockType`, when you construct `payloadSource` directly and pass it to [`custom()`](/docs/content/sources/custom). Setting `serializers` writes the source's pages as MDX so the returned components render; a body held in a Markdown text field stays Markdown. A node with no Markdown equivalent, or a block with no serializer, is left out as a comment, and the source warns with `BLUME_SOURCE_UNSUPPORTED_NODE` naming it and the page.
@@ -33,6 +33,10 @@ mdxRemote({
33
33
  });
34
34
  ```
35
35
 
36
+ `include` (default `["**/*.{md,mdx}"]`) filters the files by their path under `path`, the way a `filesystem()` source's `include` does: a pattern starting with `!` excludes what it matches (`["**/*.mdx", "!drafts/**"]`). With `github`, the source lists the repository's whole tree at `ref` and keeps the files under `path`. If nothing is under `path` at that ref, the source reads no pages and warns with `BLUME_SOURCE_PATH_MISSING`. GitHub caps the listing (100,000 entries or 7 MB for the whole repository, whatever `path` is), and past it the source warns with `BLUME_SOURCE_TRUNCATED`: list the files with `url` and `files` instead.
37
+
38
+ A relative link between remote files (`[Errors](./02-errors.mdx)`) lands on the page the linked file publishes at, with its ordering prefix dropped from the route, and `blume validate` checks it the same way.
39
+
36
40
  A private repo's token is read from the `GITHUB_TOKEN` environment variable — the adapter declares it, so `blume dev` and `blume build` warn when it's unset. It is never inlined into your config or generated output, and it is only ever sent to GitHub's own hosts (`api.github.com`, `raw.githubusercontent.com`), never to a custom `url` base. A public repo works without it.
37
41
 
38
42
  Remote pages are rendered with full MDX-plus-component fidelity: their bodies are materialized into a hidden staging directory and rendered through Astro alongside your local docs, so callouts, tabs, and every other Blume component keep working.
@@ -3,7 +3,7 @@ title: Sanity
3
3
  description: Pull documents from a Sanity dataset with the sanity() source — a GROQ query picks the documents and their Portable Text bodies become Markdown.
4
4
  ---
5
5
 
6
- The built-in `sanity()` adapter runs a GROQ query and maps each document's fields to frontmatter and its Portable Text body to Markdown. The adapter declares `@sanity/client` as its runtime dependency — an optional peer, so install it only if you use this source.
6
+ The built-in `sanity()` adapter runs a GROQ query and maps each document's fields to frontmatter and its Portable Text body to Markdown. A body held in a Markdown string field passes through as written, except that a link that isn't a web, mail, phone, or relative address (a `javascript:` URL, say) keeps only its label. The adapter declares `@sanity/client` as its runtime dependency — an optional peer, so install it only if you use this source.
7
7
 
8
8
  ```ts blume.config.ts
9
9
  import { defineConfig } from "blume";
@@ -26,4 +26,4 @@ export default defineConfig({
26
26
  });
27
27
  ```
28
28
 
29
- A read token for a private dataset comes from the `SANITY_TOKEN` environment variable, which the adapter declares. Custom Portable Text block types map to Blume components through the engine's `serializers` option, available when you construct `sanitySource` directly and pass it to [`custom()`](/docs/content/sources/custom). Setting `serializers` writes the source's pages as MDX, so a component or directive a serializer returns renders.
29
+ A read token for a private dataset comes from the `SANITY_TOKEN` environment variable. The adapter declares it, so `blume dev` and `blume build` warn when it's unset; a public dataset works without it. Sanity answers a private dataset's query without a token with no documents rather than an error, so when a query finds nothing and the token is unset, the source adds a `BLUME_MISSING_SECRET` warning saying so. Under `--preview` the adapter reads drafts with the `previewDrafts` perspective, which `@sanity/client` 8 reports as renamed to `drafts`; the old name still works, and `@sanity/client` 6 accepts only the old one. Custom Portable Text block types map to Blume components through the engine's `serializers` option, available when you construct `sanitySource` directly and pass it to [`custom()`](/docs/content/sources/custom). Setting `serializers` writes the source's pages as MDX, so a component or directive a serializer returns renders.
@@ -18,7 +18,8 @@ export default defineConfig({
18
18
  url: "https://cms.example.com",
19
19
  contentType: "guides",
20
20
  // locale: "en"
21
- // Field paths default to title / description / slug / content / updatedAt
21
+ // Field paths default to title / description / slug / content /
22
+ // updatedAt; order (no default) sets the sidebar order
22
23
  fields: { body: "body" },
23
24
  // Extra query parameters: filters[...], sort
24
25
  params: { "filters[section][$eq]": "sdk" },
@@ -28,4 +29,4 @@ export default defineConfig({
28
29
  });
29
30
  ```
30
31
 
31
- The API token comes from the `STRAPI_API_TOKEN` environment variable, sent as a bearer token. Entries are fetched with `populate=*` so images carry their URLs (set `populate` to narrow it), and a relative upload path (`/uploads/x.png`) resolves against `url`. Only published entries are imported — `--preview` requests drafts (`status=draft` on Strapi 5, `publicationState=preview` on Strapi 4) and stages each document that has never been published with `draft: true`; a published document previews its latest draft.
32
+ The API token comes from the `STRAPI_API_TOKEN` environment variable, sent as a bearer token. Entries are fetched with `populate=*` so images carry their URLs (set `populate` to narrow it), and a relative upload path (`/uploads/x.png`) in a Blocks field resolves against `url`. A Markdown field passes through as written, so a relative path there stays relative to your docs site and breaks: use absolute image URLs in Markdown fields, as Strapi's Markdown editor inserts them. Only published entries are imported — `--preview` requests drafts (`status=draft` on Strapi 5, `publicationState=preview` on Strapi 4) and stages each document that has never been published with `draft: true`; a published document previews its latest draft.
@@ -27,7 +27,7 @@ Anchor ids are generated from the heading text, so a reworded heading gets a new
27
27
 
28
28
  Link to it as `/page#setup`. The syntax matches Fumadocs, so migrated content works verbatim.
29
29
 
30
- The `{#custom-id}` form used by Pandoc, kramdown, and Markdown-based specification toolchains is accepted as an equivalent in `.md` files. In `.mdx`, a bare `{…}` is a JSX expression and the page fails to compile — `blume check` reports the marker as `BLUME_MDX_CURLY_ANCHOR` — so write `[#custom-id]` there, or escape the braces. The escaped form pins the same anchor in both formats, which makes it the right spelling for a partial that `.mdx` pages [include](/docs/content/includes):
30
+ The `{#custom-id}` form used by Pandoc, kramdown, and Markdown-based specification toolchains is accepted as an equivalent in `.md` files. Only that unspaced spelling pins an anchor: the spaced `{ #custom-id }` and kramdown's `{: #custom-id }` (both from MkDocs' `attr_list`) stay in the heading text. In `.mdx`, a bare `{…}` is a JSX expression and the page fails to compile — `blume check` reports all three spellings as `BLUME_MDX_CURLY_ANCHOR` — so write `[#custom-id]` there, or escape the braces. The escaped form pins the same anchor in both formats, which makes it the right spelling for a partial that `.mdx` pages [include](/docs/content/includes):
31
31
 
32
32
  ```md
33
33
  ## Getting started \{#setup\}
@@ -457,7 +457,7 @@ flowchart LR
457
457
  ```
458
458
  ````
459
459
 
460
- Diagrams render on the client, so this is an MDX-only feature, and the Mermaid library loads only on pages that include one; a site with no diagram doesn't ship it at all. Diagrams use Mermaid's dagre layout and classic look by default; opt a single diagram into another layout or look through Mermaid front matter (a `config:` block with `layout: elk` or `look: neo`), and the ELK engine loads only for diagrams that ask for it. The rest of this section is a gallery of common types — see the [Mermaid docs](https://mermaid.js.org/intro/) for the full list.
460
+ Diagrams render on the client, so this is an MDX-only feature, and the Mermaid library loads only on pages that include one; a site with no diagram doesn't ship it at all. Diagrams use Mermaid's dagre layout and classic look by default; opt a single diagram into another layout or look through Mermaid front matter (a `config:` block with `layout: elk` or `look: neo`), and the ELK engine loads only for diagrams that ask for it. A diagram Mermaid can't parse shows "Could not render this diagram." in its place; Mermaid's parser error, with the line it stopped on, goes to the browser console, and `blume dev` also prints it under the message. The rest of this section is a gallery of common types — see the [Mermaid docs](https://mermaid.js.org/intro/) for the full list.
461
461
 
462
462
  ### Flowchart
463
463
 
@@ -237,7 +237,7 @@ Every build writes an [agent skill](https://agentskills.io) for your docs, a `SK
237
237
  - your API references
238
238
  - a map of the docs in sidebar order, linking each page's Markdown with its one-line description
239
239
 
240
- The skill is named after your site's `title` (`Acme Docs` becomes `acme-docs`). It covers the current version in the default language, leaves out API operation pages and changelog entries (each has one line of its own above the map), and lists up to 200 pages, pointing at `llms.txt` for the rest. The links are absolute, so the skill needs [`deployment.site`](/docs/deployment); a build without one skips it.
240
+ The skill is named after your site's `title` (`Acme Docs` becomes `acme-docs`). It covers the current version in the default language, leaves out API operation pages and changelog entries (each has one line of its own above the map), skips the pages `llms.txt` does (drafts, hidden and `noindex` pages, and pages with [`ai.exclude`](/docs/discoverability/llms-txt#excluding-a-page)), and lists up to 200 pages, pointing at `llms.txt` for the rest. The links are absolute, so the skill needs [`deployment.site`](/docs/deployment); a build without one skips it.
241
241
 
242
242
  The generated skill is a guide to your docs, not a summary of your product. For one that teaches an agent your product — setup, core concepts, common tasks, and gotchas — have your coding agent write it from the docs:
243
243
 
@@ -46,4 +46,4 @@ The in-page **assistant** is the one AI feature documented elsewhere: it's a rea
46
46
 
47
47
  ## Checking your work
48
48
 
49
- [`blume audit`](/docs/cli/audit) crawls the built site and reports on titles, descriptions, canonicals, Open Graph and X cards, hreflang, the sitemap, `robots.txt`, and structured data. Point it at a deployment with `--url` to also check response headers and [DNS-based agent discovery](/docs/discoverability/agent-discovery#dns-based-discovery-dns-aid).
49
+ [`blume audit`](/docs/cli/audit) crawls the built site and reports on titles, descriptions, canonicals, Open Graph and X cards, hreflang, the sitemap, `robots.txt`, and structured data. Point it at a deployment with `--url` to also check response headers, configured redirects, and [DNS-based agent discovery](/docs/discoverability/agent-discovery#dns-based-discovery-dns-aid).
@@ -13,6 +13,8 @@ Every Blume site also serves its docs as a small read-only **JSON API** — the
13
13
  | `/api/docs/search?q=` | Full-text search, with the same `limit`, `contentTypes`, `locale`, `version`, and `filters[key]` scoping as `search_docs`. Server output only. |
14
14
  | `/openapi.json` | The OpenAPI 3.1 description of the whole machine-readable surface. |
15
15
 
16
+ The page index leaves out [hidden pages](/docs/content/navigation#hidden-pages), which get no per-page document either; `get_page` and the page's `.md` mirror still serve one, since hidden isn't private. Pages with `seo.noindex`, `ai.exclude`, or `search.exclude` are listed like any other. A page document's `markdown` is the page's source as agents read it, with its frontmatter block kept at the top.
17
+
16
18
  The page index, per-page documents, and navigation are prerendered, so a static site serves them as files from any host. Search is a live endpoint and exists only under [server output](/docs/deployment#server-rendering), where it runs the same index `search_docs` does.
17
19
 
18
20
  ## Errors