blume 2.0.3 → 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 (607) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +142 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-5f86nr5m.js → chunk-073rdvgb.js} +21 -48
  5. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-073rdvgb.js.map} +4 -5
  6. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  7. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  8. package/dist/cli/chunk-150jk7km.js +52 -0
  9. package/dist/cli/chunk-150jk7km.js.map +10 -0
  10. package/dist/cli/{chunk-9t7a85s3.js → chunk-313vnn40.js} +30 -19
  11. package/dist/cli/chunk-313vnn40.js.map +10 -0
  12. package/dist/cli/{chunk-tkacnehg.js → chunk-3v3ffbrj.js} +2 -2
  13. package/dist/cli/{chunk-q58y5e6a.js → chunk-3x6x3pph.js} +46 -24
  14. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  15. package/dist/cli/{chunk-1d7ve1dm.js → chunk-42t4jxdx.js} +37 -5
  16. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  17. package/dist/cli/{chunk-8ktnccpt.js → chunk-54ztw1nj.js} +21 -2
  18. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  19. package/dist/cli/{chunk-hm3vjy5s.js → chunk-5vjtgs6e.js} +28 -10
  20. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-5vjtgs6e.js.map} +4 -4
  21. package/dist/cli/chunk-66w9j7p6.js +322 -0
  22. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  23. package/dist/cli/{chunk-2hsdwb9n.js → chunk-7ppf1rva.js} +34 -18
  24. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  25. package/dist/cli/{chunk-r9rcc4w7.js → chunk-8y2kmjv8.js} +7 -7
  26. package/dist/cli/{chunk-r20tn01b.js → chunk-c1y538kd.js} +8 -2
  27. package/dist/cli/{chunk-r20tn01b.js.map → chunk-c1y538kd.js.map} +3 -3
  28. package/dist/cli/{chunk-2eytanqx.js → chunk-c887fj0s.js} +2 -2
  29. package/dist/cli/{chunk-p3v96n38.js → chunk-cfyg47zg.js} +16 -6
  30. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  31. package/dist/cli/chunk-cm89scdv.js +443 -0
  32. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  33. package/dist/cli/{chunk-akbpwfxc.js → chunk-dzwehpy7.js} +2 -2
  34. package/dist/cli/{chunk-wrr3j9w9.js → chunk-e6atmn4h.js} +299 -72
  35. package/dist/cli/chunk-e6atmn4h.js.map +36 -0
  36. package/dist/cli/{chunk-tzmab476.js → chunk-e8xw9qkh.js} +8 -6
  37. package/dist/cli/{chunk-tzmab476.js.map → chunk-e8xw9qkh.js.map} +3 -3
  38. package/dist/cli/{chunk-6dsbexzp.js → chunk-e9q7a2ak.js} +17 -15
  39. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-e9q7a2ak.js.map} +2 -2
  40. package/dist/cli/{chunk-pv29h0wf.js → chunk-ehweg4fr.js} +5155 -1249
  41. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  42. package/dist/cli/{chunk-c8chx29p.js → chunk-ep6nd0rn.js} +390 -318
  43. package/dist/cli/chunk-ep6nd0rn.js.map +36 -0
  44. package/dist/cli/{chunk-p73c0m7w.js → chunk-fdrkhpjy.js} +18 -15
  45. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-fdrkhpjy.js.map} +3 -3
  46. package/dist/cli/{chunk-2z928egk.js → chunk-ft9vfys6.js} +5 -5
  47. package/dist/cli/{chunk-5m5nmvyq.js → chunk-gb420vvz.js} +316 -79
  48. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  49. package/dist/cli/{chunk-vg9r4eb9.js → chunk-hebn0tpx.js} +86 -13
  50. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  51. package/dist/cli/{chunk-35d4wj9f.js → chunk-htrwgabr.js} +41 -23
  52. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  53. package/dist/cli/{chunk-g4hq16wv.js → chunk-hx3xqnk9.js} +16 -14
  54. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-hx3xqnk9.js.map} +2 -2
  55. package/dist/cli/chunk-j134vrp2.js +65 -0
  56. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  57. package/dist/cli/{chunk-a58773jm.js → chunk-kma9f6sf.js} +2 -2
  58. package/dist/cli/chunk-kwrg63tm.js +31 -0
  59. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  60. package/dist/cli/{chunk-ey84smr6.js → chunk-mrcccgnh.js} +6 -6
  61. package/dist/cli/{chunk-ey84smr6.js.map → chunk-mrcccgnh.js.map} +1 -1
  62. package/dist/cli/{chunk-yfyb25rh.js → chunk-n0v7m8ry.js} +24 -8
  63. package/dist/cli/chunk-n0v7m8ry.js.map +14 -0
  64. package/dist/cli/{chunk-j85vccga.js → chunk-ncj7w596.js} +576 -367
  65. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  66. package/dist/cli/{chunk-5xvm6tfj.js → chunk-nq1d20w2.js} +17 -15
  67. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-nq1d20w2.js.map} +2 -2
  68. package/dist/cli/{chunk-pehfxfta.js → chunk-nrns10mj.js} +6 -98
  69. package/dist/cli/chunk-nrns10mj.js.map +10 -0
  70. package/dist/cli/{chunk-crgn1q09.js → chunk-sds923fa.js} +4 -4
  71. package/dist/cli/chunk-t2m5j4pr.js +104 -0
  72. package/dist/cli/chunk-t2m5j4pr.js.map +11 -0
  73. package/dist/cli/{chunk-acanzt5p.js → chunk-tdqhyxfz.js} +6 -6
  74. package/dist/cli/chunk-tphac74f.js +126 -0
  75. package/dist/cli/chunk-tphac74f.js.map +11 -0
  76. package/dist/cli/chunk-w1r4vase.js +18 -0
  77. package/dist/cli/chunk-w1r4vase.js.map +10 -0
  78. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  79. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  80. package/dist/cli/index.js +31 -20
  81. package/dist/cli/index.js.map +5 -5
  82. package/dist/types/ai/agent-surface.d.ts +8 -7
  83. package/dist/types/ai/ai-catalog.d.ts +7 -0
  84. package/dist/types/ai/ask-context.d.ts +26 -0
  85. package/dist/types/ai/ask.d.ts +271 -58
  86. package/dist/types/ai/component-markdown.d.ts +2 -0
  87. package/dist/types/ai/index.d.ts +2 -2
  88. package/dist/types/ai/openapi-components.d.ts +4 -1
  89. package/dist/types/ai/site-skill.d.ts +26 -0
  90. package/dist/types/ai/skills.d.ts +16 -0
  91. package/dist/types/astro/integration.d.ts +9 -0
  92. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  93. package/dist/types/astro/pages.d.ts +10 -0
  94. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  95. package/dist/types/captcha/index.d.ts +28 -0
  96. package/dist/types/captcha/schema.d.ts +29 -0
  97. package/dist/types/captcha/turnstile.d.ts +28 -0
  98. package/dist/types/cli/init/scaffold.d.ts +1 -1
  99. package/dist/types/components/content/api-field.d.ts +62 -0
  100. package/dist/types/components/content/api-page.d.ts +100 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  103. package/dist/types/components/openapi/helpers.d.ts +158 -0
  104. package/dist/types/components/openapi/security.d.ts +95 -0
  105. package/dist/types/components/openapi/snippets.d.ts +60 -0
  106. package/dist/types/consent/ethyca.d.ts +48 -0
  107. package/dist/types/consent/index.d.ts +37 -0
  108. package/dist/types/consent/native.d.ts +28 -0
  109. package/dist/types/consent/osano.d.ts +42 -0
  110. package/dist/types/consent/schema.d.ts +110 -0
  111. package/dist/types/core/changelog-index.d.ts +8 -0
  112. package/dist/types/core/config-input.d.ts +217 -23
  113. package/dist/types/core/data.d.ts +53 -4
  114. package/dist/types/core/footer.d.ts +8 -0
  115. package/dist/types/core/i18n-ui.d.ts +88 -0
  116. package/dist/types/core/includes.d.ts +8 -2
  117. package/dist/types/core/links.d.ts +8 -0
  118. package/dist/types/core/localizable.d.ts +7 -0
  119. package/dist/types/core/page-modes.d.ts +29 -0
  120. package/dist/types/core/probe.d.ts +2 -0
  121. package/dist/types/core/project-graph.d.ts +6 -0
  122. package/dist/types/core/redirect-patterns.d.ts +152 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +670 -56
  125. package/dist/types/core/sources/cache.d.ts +3 -0
  126. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  127. package/dist/types/core/sources/contentful.d.ts +17 -1
  128. package/dist/types/core/sources/lexical.d.ts +5 -0
  129. package/dist/types/core/sources/normalize.d.ts +20 -1
  130. package/dist/types/core/sources/read.d.ts +5 -0
  131. package/dist/types/core/sources/remote.d.ts +37 -10
  132. package/dist/types/core/sources/sanity.d.ts +1 -1
  133. package/dist/types/core/types.d.ts +18 -1
  134. package/dist/types/core/variables.d.ts +42 -0
  135. package/dist/types/deploy/adapter-output.d.ts +2 -1
  136. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  137. package/dist/types/deploy/adapters/node.d.ts +58 -23
  138. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  139. package/dist/types/deploy/adapters/types.d.ts +3 -2
  140. package/dist/types/deploy/node-headers.d.ts +13 -2
  141. package/dist/types/deploy/platforms/cloudflare.d.ts +15 -7
  142. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  143. package/dist/types/deploy/platforms/node.d.ts +3 -1
  144. package/dist/types/deploy/platforms/types.d.ts +18 -7
  145. package/dist/types/deploy/platforms/vercel.d.ts +26 -1
  146. package/dist/types/deploy/redirects.d.ts +55 -11
  147. package/dist/types/deploy/sitemap.d.ts +2 -1
  148. package/dist/types/deploy/vercel-negotiation.d.ts +51 -21
  149. package/dist/types/markdown/code-title.d.ts +17 -0
  150. package/dist/types/markdown/features.d.ts +3 -2
  151. package/dist/types/narration/build.d.ts +70 -0
  152. package/dist/types/narration/provider.d.ts +26 -0
  153. package/dist/types/narration/script.d.ts +154 -0
  154. package/dist/types/openapi/checks.d.ts +27 -0
  155. package/dist/types/openapi/graphql.d.ts +6 -0
  156. package/dist/types/openapi/model.d.ts +27 -10
  157. package/dist/types/openapi/overlay.d.ts +54 -0
  158. package/dist/types/openapi/parse.d.ts +16 -0
  159. package/dist/types/openapi/references.d.ts +30 -4
  160. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  161. package/dist/types/ratelimit/index.d.ts +27 -0
  162. package/dist/types/ratelimit/memory.d.ts +36 -0
  163. package/dist/types/ratelimit/schema.d.ts +119 -0
  164. package/dist/types/ratelimit/upstash.d.ts +46 -0
  165. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  166. package/dist/types/reference/asyncapi.d.ts +10 -10
  167. package/dist/types/reference/graphql.d.ts +89 -11
  168. package/dist/types/reference/index.d.ts +1 -1
  169. package/dist/types/reference/openapi.d.ts +33 -12
  170. package/dist/types/reference/options.d.ts +82 -6
  171. package/dist/types/reference/scalar.d.ts +13 -0
  172. package/dist/types/reference/schema.d.ts +144 -36
  173. package/dist/types/search/adapters/registry.d.ts +3 -0
  174. package/dist/types/search/adapters/typesense.d.ts +14 -4
  175. package/dist/types/search/build.d.ts +3 -1
  176. package/dist/types/search/documents.d.ts +12 -1
  177. package/dist/types/search/orama-index.d.ts +13 -9
  178. package/dist/types/search/sync/algolia.d.ts +5 -2
  179. package/dist/types/search/sync/index.d.ts +3 -1
  180. package/dist/types/search/sync/typesense.d.ts +11 -7
  181. package/dist/types/seo/metatags.d.ts +20 -0
  182. package/dist/types/sources/contentful.d.ts +16 -0
  183. package/dist/types/sources/github-releases.d.ts +8 -1
  184. package/dist/types/sources/payload.d.ts +2 -0
  185. package/dist/types/sources/registry.d.ts +54 -0
  186. package/dist/types/sources/shared.d.ts +3 -0
  187. package/dist/types/sources/strapi.d.ts +2 -0
  188. package/docs/01-quickstart.mdx +4 -2
  189. package/docs/02-deployment.mdx +71 -17
  190. package/docs/03-upgrading.mdx +4 -4
  191. package/docs/04-migrating.mdx +1 -1
  192. package/docs/advanced/changelog.mdx +13 -2
  193. package/docs/advanced/skills.mdx +15 -1
  194. package/docs/cli/audit.mdx +51 -8
  195. package/docs/cli/doctor.mdx +5 -1
  196. package/docs/cli/evals.mdx +4 -4
  197. package/docs/cli/index.mdx +1 -0
  198. package/docs/cli/translate.mdx +1 -0
  199. package/docs/cli/validate.mdx +4 -3
  200. package/docs/configuration/analytics.mdx +3 -1
  201. package/docs/configuration/assistant.mdx +194 -38
  202. package/docs/configuration/consent.mdx +100 -0
  203. package/docs/configuration/customization.mdx +8 -6
  204. package/docs/configuration/export.mdx +5 -3
  205. package/docs/configuration/index.mdx +64 -5
  206. package/docs/configuration/meta.ts +3 -0
  207. package/docs/configuration/narration.mdx +119 -0
  208. package/docs/configuration/rate-limiting.mdx +90 -0
  209. package/docs/configuration/search.mdx +63 -12
  210. package/docs/configuration/theming.mdx +3 -1
  211. package/docs/content/components.mdx +108 -2
  212. package/docs/content/frontmatter.mdx +80 -2
  213. package/docs/content/i18n.mdx +39 -4
  214. package/docs/content/includes.mdx +16 -0
  215. package/docs/content/meta.mdx +1 -0
  216. package/docs/content/meta.ts +1 -0
  217. package/docs/content/navigation.mdx +32 -10
  218. package/docs/content/sources/contentful.mdx +35 -0
  219. package/docs/content/sources/custom.mdx +43 -0
  220. package/docs/content/sources/github-releases.mdx +39 -0
  221. package/docs/content/sources/index.mdx +91 -0
  222. package/docs/content/sources/meta.ts +16 -0
  223. package/docs/content/sources/notion.mdx +32 -0
  224. package/docs/content/sources/obsidian.mdx +37 -0
  225. package/docs/content/sources/payload.mdx +31 -0
  226. package/docs/content/sources/remote-mdx.mdx +44 -0
  227. package/docs/content/sources/sanity.mdx +29 -0
  228. package/docs/content/sources/strapi.mdx +32 -0
  229. package/docs/content/syntax.mdx +69 -4
  230. package/docs/content/variables.mdx +41 -0
  231. package/docs/discoverability/agent-discovery.mdx +29 -2
  232. package/docs/discoverability/index.mdx +2 -2
  233. package/docs/discoverability/json-api.mdx +2 -0
  234. package/docs/discoverability/llms-txt.mdx +2 -2
  235. package/docs/discoverability/markdown.mdx +3 -1
  236. package/docs/discoverability/mcp.mdx +4 -0
  237. package/docs/discoverability/metadata.mdx +17 -0
  238. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  239. package/docs/references/api-pages.mdx +74 -0
  240. package/docs/references/asyncapi.mdx +4 -2
  241. package/docs/references/graphql.mdx +14 -2
  242. package/docs/references/meta.ts +1 -1
  243. package/docs/references/openapi.mdx +148 -7
  244. package/docs/references/scalar.mdx +3 -1
  245. package/package.json +43 -8
  246. package/skills/blume/SKILL.md +3 -0
  247. package/skills/blume-migrate/SKILL.md +6 -6
  248. package/skills/blume-migrate/references/docusaurus.md +7 -7
  249. package/skills/blume-migrate/references/fumadocs.md +17 -13
  250. package/skills/blume-migrate/references/mintlify.md +37 -19
  251. package/skills/blume-migrate/references/nextra.md +10 -7
  252. package/skills/blume-migrate/references/starlight.md +11 -8
  253. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  254. package/skills/blume-write-skill/SKILL.md +82 -0
  255. package/src/ai/agent-readability.ts +2 -1
  256. package/src/ai/agent-surface.ts +9 -7
  257. package/src/ai/ai-catalog.ts +6 -3
  258. package/src/ai/ask-context.ts +103 -20
  259. package/src/ai/ask-data.ts +6 -0
  260. package/src/ai/ask-limits.ts +13 -0
  261. package/src/ai/ask-tools.ts +142 -0
  262. package/src/ai/ask.ts +354 -76
  263. package/src/ai/changelog-markdown.ts +11 -6
  264. package/src/ai/component-markdown.ts +76 -1
  265. package/src/ai/cors.ts +14 -3
  266. package/src/ai/index.ts +12 -0
  267. package/src/ai/llms.ts +6 -2
  268. package/src/ai/markdown.ts +2 -1
  269. package/src/ai/mcp/data.ts +6 -0
  270. package/src/ai/mcp/query.ts +14 -6
  271. package/src/ai/openapi-components.ts +36 -6
  272. package/src/ai/relative-links.ts +4 -38
  273. package/src/ai/site-skill.ts +274 -0
  274. package/src/ai/skills.ts +35 -2
  275. package/src/ai/write-skill.ts +97 -0
  276. package/src/astro/generate.ts +210 -66
  277. package/src/astro/index.ts +1 -0
  278. package/src/astro/integration.ts +42 -4
  279. package/src/astro/markdown-negotiation.ts +3 -0
  280. package/src/astro/module-types.ts +9 -1
  281. package/src/astro/pages.ts +23 -0
  282. package/src/astro/templates.ts +406 -108
  283. package/src/astro/variables.ts +31 -0
  284. package/src/audit/catalog.ts +38 -3
  285. package/src/audit/checks/contrast.ts +183 -0
  286. package/src/audit/checks/i18n.ts +2 -3
  287. package/src/audit/checks/indexability.ts +2 -3
  288. package/src/audit/checks/links.ts +19 -13
  289. package/src/audit/checks/network.ts +110 -6
  290. package/src/audit/checks/redirects.ts +6 -4
  291. package/src/audit/checks/robots.ts +58 -2
  292. package/src/audit/checks/sitemap.ts +2 -3
  293. package/src/audit/redirects.ts +46 -15
  294. package/src/audit/run.ts +5 -0
  295. package/src/audit/types.ts +4 -1
  296. package/src/audit/url.ts +14 -5
  297. package/src/blume-modules.d.ts +12 -1
  298. package/src/captcha/client.ts +152 -0
  299. package/src/captcha/hcaptcha.ts +36 -0
  300. package/src/captcha/index.ts +28 -0
  301. package/src/captcha/schema.ts +32 -0
  302. package/src/captcha/turnstile.ts +38 -0
  303. package/src/captcha/verify.ts +71 -0
  304. package/src/cli/args.ts +51 -0
  305. package/src/cli/build-failure.ts +23 -0
  306. package/src/cli/command-meta.ts +5 -0
  307. package/src/cli/commands/audit.ts +10 -1
  308. package/src/cli/commands/dev.ts +18 -2
  309. package/src/cli/commands/doctor.ts +67 -28
  310. package/src/cli/commands/eval.ts +36 -4
  311. package/src/cli/commands/init.ts +6 -4
  312. package/src/cli/commands/skill.ts +96 -0
  313. package/src/cli/commands/validate.ts +10 -1
  314. package/src/cli/index.ts +5 -0
  315. package/src/cli/init/scaffold.ts +3 -3
  316. package/src/cli/prepare.ts +6 -3
  317. package/src/cli/required-secrets.ts +72 -30
  318. package/src/components/BlumePage.astro +2 -0
  319. package/src/components/content/ApiEndpoint.astro +18 -0
  320. package/src/components/content/ApiField.astro +79 -0
  321. package/src/components/content/ApiPlayground.astro +66 -0
  322. package/src/components/content/ApiRail.astro +19 -0
  323. package/src/components/content/Color.astro +1 -0
  324. package/src/components/content/FileTree.astro +1 -0
  325. package/src/components/content/GithubInfo.astro +1 -0
  326. package/src/components/content/Panel.astro +1 -0
  327. package/src/components/content/ParamField.astro +25 -0
  328. package/src/components/content/RequestExample.astro +17 -0
  329. package/src/components/content/ResponseExample.astro +17 -0
  330. package/src/components/content/ResponseField.astro +32 -0
  331. package/src/components/content/Step.astro +1 -0
  332. package/src/components/content/Tile.astro +1 -0
  333. package/src/components/content/Tooltip.astro +1 -0
  334. package/src/components/content/Tree.astro +1 -0
  335. package/src/components/content/TypeTable.astro +4 -1
  336. package/src/components/content/View.astro +21 -0
  337. package/src/components/content/ViewSwitcher.astro +168 -0
  338. package/src/components/content/api-field.ts +103 -0
  339. package/src/components/content/api-page.ts +248 -0
  340. package/src/components/content/mermaid-element.ts +15 -1
  341. package/src/components/content/view-switcher.ts +71 -0
  342. package/src/components/index.ts +2 -0
  343. package/src/components/islands/Assistant.astro +10 -1
  344. package/src/components/islands/assistant.tsx +104 -14
  345. package/src/components/islands/code-question.ts +62 -0
  346. package/src/components/islands/hooks.ts +136 -21
  347. package/src/components/islands/support-link.ts +68 -0
  348. package/src/components/layout/Analytics.astro +11 -2
  349. package/src/components/layout/Banner.astro +14 -10
  350. package/src/components/layout/Breadcrumbs.astro +6 -2
  351. package/src/components/layout/ConsentBanner.astro +62 -0
  352. package/src/components/layout/ConsentHead.astro +38 -0
  353. package/src/components/layout/Directory.astro +94 -0
  354. package/src/components/layout/DrawerSwitchers.astro +51 -0
  355. package/src/components/layout/Empty.astro +2 -2
  356. package/src/components/layout/Header.astro +122 -80
  357. package/src/components/layout/LanguageSwitcher.astro +48 -15
  358. package/src/components/layout/NarrationPlayer.astro +1085 -0
  359. package/src/components/layout/NavSelector.astro +33 -7
  360. package/src/components/layout/PageActions.astro +139 -24
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +71 -3
  363. package/src/components/layout/ReferenceLayout.astro +17 -6
  364. package/src/components/layout/RelatedPages.astro +42 -0
  365. package/src/components/layout/RootLayout.astro +318 -47
  366. package/src/components/layout/Search.astro +47 -19
  367. package/src/components/layout/SiteFooter.astro +124 -0
  368. package/src/components/layout/SiteMetatags.astro +16 -0
  369. package/src/components/layout/TableOfContents.astro +24 -17
  370. package/src/components/layout/consent/native.ts +79 -0
  371. package/src/components/layout/consent/types.ts +37 -0
  372. package/src/components/layout/footer-links.ts +53 -0
  373. package/src/components/layout/footer-socials.ts +87 -0
  374. package/src/components/layout/head-scripts.ts +6 -1
  375. package/src/components/layout/locale-redirect.ts +107 -0
  376. package/src/components/layout/nav-utils.ts +83 -0
  377. package/src/components/layout/page-feedback.ts +96 -0
  378. package/src/components/layout/search/algolia.ts +10 -1
  379. package/src/components/layout/search/analytics.ts +126 -0
  380. package/src/components/layout/search/endpoint.ts +48 -5
  381. package/src/components/layout/search/flexsearch.ts +17 -5
  382. package/src/components/layout/search/orama-cloud.ts +26 -14
  383. package/src/components/layout/search/pagefind.ts +114 -5
  384. package/src/components/layout/search/types.ts +32 -0
  385. package/src/components/layout/search/typesense.ts +19 -3
  386. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  387. package/src/components/openapi/Authorization.astro +10 -0
  388. package/src/components/openapi/GraphqlOperation.astro +5 -0
  389. package/src/components/openapi/Operation.astro +184 -13
  390. package/src/components/openapi/ParametersTable.astro +8 -4
  391. package/src/components/openapi/Playground.astro +7 -1
  392. package/src/components/openapi/RequestBody.astro +24 -5
  393. package/src/components/openapi/RequestPanel.astro +59 -7
  394. package/src/components/openapi/Responses.astro +7 -4
  395. package/src/components/openapi/SchemaTable.astro +15 -3
  396. package/src/components/openapi/async-snippets.ts +65 -9
  397. package/src/components/openapi/async.ts +123 -29
  398. package/src/components/openapi/code-samples.ts +97 -0
  399. package/src/components/openapi/graphql-helpers.ts +35 -12
  400. package/src/components/openapi/helpers.ts +36 -2
  401. package/src/components/openapi/message-model.ts +54 -11
  402. package/src/components/openapi/message.ts +14 -4
  403. package/src/components/openapi/operation-model.ts +16 -7
  404. package/src/components/openapi/playground-client.ts +24 -5
  405. package/src/components/openapi/playground-schema.ts +1 -1
  406. package/src/components/openapi/schema-tree.ts +44 -14
  407. package/src/components/openapi/security.ts +16 -3
  408. package/src/components/openapi/snippets.ts +873 -10
  409. package/src/components/openapi/validate-json.ts +4 -3
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/builtin-tags.ts +5 -0
  421. package/src/core/changelog-index.ts +31 -1
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/component-diagnostics.ts +53 -0
  424. package/src/core/component-overrides.ts +32 -0
  425. package/src/core/config-input.ts +225 -23
  426. package/src/core/data.ts +46 -4
  427. package/src/core/diagnostics.ts +8 -2
  428. package/src/core/footer.ts +26 -0
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/i18n-ui.ts +84 -2
  431. package/src/core/includes.ts +21 -4
  432. package/src/core/links.ts +63 -19
  433. package/src/core/localizable.ts +26 -0
  434. package/src/core/navigation.ts +43 -12
  435. package/src/core/page-modes.ts +56 -0
  436. package/src/core/probe.ts +26 -6
  437. package/src/core/project-graph.ts +163 -16
  438. package/src/core/redirect-patterns.ts +520 -0
  439. package/src/core/related.ts +131 -0
  440. package/src/core/schema.ts +339 -66
  441. package/src/core/sources/cache.ts +6 -0
  442. package/src/core/sources/contentful-rich-text.ts +29 -6
  443. package/src/core/sources/contentful.ts +135 -25
  444. package/src/core/sources/filesystem.ts +26 -4
  445. package/src/core/sources/github-releases.ts +12 -1
  446. package/src/core/sources/lexical.ts +15 -10
  447. package/src/core/sources/mdx-remote.ts +62 -10
  448. package/src/core/sources/normalize.ts +228 -41
  449. package/src/core/sources/notion.ts +7 -1
  450. package/src/core/sources/obsidian.ts +16 -3
  451. package/src/core/sources/payload.ts +38 -15
  452. package/src/core/sources/read.ts +8 -3
  453. package/src/core/sources/remote.ts +72 -15
  454. package/src/core/sources/resolve.ts +11 -1
  455. package/src/core/sources/sanity.ts +43 -25
  456. package/src/core/sources/strapi.ts +2 -2
  457. package/src/core/types.ts +19 -1
  458. package/src/core/ui-packs/ar.ts +44 -0
  459. package/src/core/ui-packs/bg.ts +46 -0
  460. package/src/core/ui-packs/bn.ts +44 -0
  461. package/src/core/ui-packs/ca.ts +47 -0
  462. package/src/core/ui-packs/cs.ts +45 -0
  463. package/src/core/ui-packs/da.ts +46 -0
  464. package/src/core/ui-packs/de.ts +47 -0
  465. package/src/core/ui-packs/el.ts +46 -0
  466. package/src/core/ui-packs/es.ts +47 -0
  467. package/src/core/ui-packs/fa.ts +46 -0
  468. package/src/core/ui-packs/fi.ts +46 -0
  469. package/src/core/ui-packs/fr.ts +47 -0
  470. package/src/core/ui-packs/he.ts +44 -0
  471. package/src/core/ui-packs/hi.ts +45 -0
  472. package/src/core/ui-packs/hr.ts +46 -0
  473. package/src/core/ui-packs/hu.ts +45 -0
  474. package/src/core/ui-packs/id.ts +46 -0
  475. package/src/core/ui-packs/it.ts +46 -0
  476. package/src/core/ui-packs/ja.ts +46 -0
  477. package/src/core/ui-packs/ko.ts +45 -0
  478. package/src/core/ui-packs/nl.ts +47 -0
  479. package/src/core/ui-packs/no.ts +45 -0
  480. package/src/core/ui-packs/pl.ts +46 -0
  481. package/src/core/ui-packs/pt-br.ts +47 -0
  482. package/src/core/ui-packs/pt.ts +46 -0
  483. package/src/core/ui-packs/ro.ts +46 -0
  484. package/src/core/ui-packs/ru.ts +46 -0
  485. package/src/core/ui-packs/sk.ts +45 -0
  486. package/src/core/ui-packs/sr.ts +46 -0
  487. package/src/core/ui-packs/sv.ts +45 -0
  488. package/src/core/ui-packs/th.ts +44 -0
  489. package/src/core/ui-packs/tr.ts +46 -0
  490. package/src/core/ui-packs/uk.ts +46 -0
  491. package/src/core/ui-packs/vi.ts +46 -0
  492. package/src/core/ui-packs/zh-tw.ts +44 -0
  493. package/src/core/ui-packs/zh.ts +44 -0
  494. package/src/core/variables.ts +103 -0
  495. package/src/deploy/adapter-output.ts +7 -2
  496. package/src/deploy/adapters/cloudflare.ts +7 -5
  497. package/src/deploy/adapters/node.ts +47 -9
  498. package/src/deploy/adapters/types.ts +7 -3
  499. package/src/deploy/artifacts.ts +79 -9
  500. package/src/deploy/cloudflare-negotiation.ts +35 -5
  501. package/src/deploy/headers.ts +2 -1
  502. package/src/deploy/node-headers.ts +63 -9
  503. package/src/deploy/platforms/cloudflare.ts +45 -9
  504. package/src/deploy/platforms/netlify.ts +35 -2
  505. package/src/deploy/platforms/node.ts +12 -2
  506. package/src/deploy/platforms/types.ts +21 -7
  507. package/src/deploy/platforms/vercel.ts +149 -17
  508. package/src/deploy/redirects.ts +151 -24
  509. package/src/deploy/rss.ts +3 -1
  510. package/src/deploy/sitemap.ts +39 -5
  511. package/src/deploy/vercel-negotiation.ts +326 -102
  512. package/src/eval/agents.ts +18 -1
  513. package/src/eval/prompts.ts +6 -3
  514. package/src/eval/report.ts +34 -13
  515. package/src/eval/run.ts +5 -0
  516. package/src/markdown/api-rail.ts +259 -0
  517. package/src/markdown/code-title.ts +35 -9
  518. package/src/markdown/features.ts +3 -2
  519. package/src/markdown/fence-meta.ts +2 -0
  520. package/src/markdown/include.ts +11 -1
  521. package/src/markdown/index.ts +28 -5
  522. package/src/markdown/math.ts +4 -4
  523. package/src/markdown/relative-links.ts +3 -2
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +7 -6
  531. package/src/openapi/checks.ts +273 -0
  532. package/src/openapi/graphql-build.ts +21 -0
  533. package/src/openapi/graphql.ts +11 -0
  534. package/src/openapi/model.ts +72 -27
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +182 -4
  537. package/src/openapi/references.ts +64 -8
  538. package/src/openapi/render-mdx.ts +5 -1
  539. package/src/openapi/scalar.ts +40 -6
  540. package/src/openapi/source.ts +40 -15
  541. package/src/ratelimit/cloudflare.ts +54 -0
  542. package/src/ratelimit/index.ts +30 -0
  543. package/src/ratelimit/memory.ts +45 -0
  544. package/src/ratelimit/runtime.ts +247 -0
  545. package/src/ratelimit/schema.ts +28 -0
  546. package/src/ratelimit/upstash.ts +60 -0
  547. package/src/ratelimit/wrangler.ts +70 -0
  548. package/src/reference/asyncapi.ts +2 -2
  549. package/src/reference/graphql.ts +20 -3
  550. package/src/reference/index.ts +1 -0
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +80 -8
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +23 -5
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/locale-scope.ts +30 -0
  557. package/src/search/adapters/typesense.ts +14 -4
  558. package/src/search/build.ts +21 -3
  559. package/src/search/documents.ts +26 -2
  560. package/src/search/orama-index.ts +191 -34
  561. package/src/search/pagefind-ranking.ts +39 -0
  562. package/src/search/source-pages.ts +29 -0
  563. package/src/search/sync/algolia.ts +111 -10
  564. package/src/search/sync/index.ts +52 -13
  565. package/src/search/sync/orama-cloud.ts +3 -0
  566. package/src/search/sync/typesense.ts +76 -26
  567. package/src/seo/metatags.ts +67 -0
  568. package/src/sources/contentful.ts +12 -0
  569. package/src/sources/github-releases.ts +7 -1
  570. package/src/sources/shared.ts +3 -0
  571. package/src/theme/chrome-icons.ts +1 -0
  572. package/src/theme/color.ts +500 -0
  573. package/src/theme/entry.ts +85 -6
  574. package/src/theme/palette.ts +121 -45
  575. package/src/translate/prompts.ts +1 -0
  576. package/src/translate/validate.ts +31 -3
  577. package/src/upgrade/upgrade.ts +4 -171
  578. package/dist/cli/chunk-1d7ve1dm.js.map +0 -12
  579. package/dist/cli/chunk-2hsdwb9n.js.map +0 -10
  580. package/dist/cli/chunk-35d4wj9f.js.map +0 -15
  581. package/dist/cli/chunk-364znk6q.js +0 -75
  582. package/dist/cli/chunk-364znk6q.js.map +0 -11
  583. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  584. package/dist/cli/chunk-8ktnccpt.js.map +0 -10
  585. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  586. package/dist/cli/chunk-b07cmahc.js +0 -145
  587. package/dist/cli/chunk-b07cmahc.js.map +0 -11
  588. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  589. package/dist/cli/chunk-e04dxsz1.js +0 -39
  590. package/dist/cli/chunk-e04dxsz1.js.map +0 -10
  591. package/dist/cli/chunk-j85vccga.js.map +0 -36
  592. package/dist/cli/chunk-p3v96n38.js.map +0 -10
  593. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  594. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  595. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  596. package/dist/cli/chunk-vg9r4eb9.js.map +0 -14
  597. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  598. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  599. package/docs/content/sources.mdx +0 -334
  600. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-3v3ffbrj.js.map} +0 -0
  601. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-8y2kmjv8.js.map} +0 -0
  602. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-c887fj0s.js.map} +0 -0
  603. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-dzwehpy7.js.map} +0 -0
  604. /package/dist/cli/{chunk-2z928egk.js.map → chunk-ft9vfys6.js.map} +0 -0
  605. /package/dist/cli/{chunk-a58773jm.js.map → chunk-kma9f6sf.js.map} +0 -0
  606. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-sds923fa.js.map} +0 -0
  607. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-tdqhyxfz.js.map} +0 -0
@@ -4,27 +4,44 @@ import { pathToFileURL } from "node:url";
4
4
  import { dirname, isAbsolute, join, relative } from "pathe";
5
5
 
6
6
  import type { AskRetrievalOptions } from "../ai/ask-context.ts";
7
+ import { ASK_MAX_MESSAGES, ASK_MAX_MESSAGES_CHARS } from "../ai/ask-limits.ts";
7
8
  import type { AskBackend } from "../ai/ask.ts";
8
9
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
10
+ import type { CaptchaAdapter } from "../captcha/schema.ts";
11
+ import { CONSENT_CLIENT_MODULES } from "../consent/clients.ts";
9
12
  import { normalizeBasePath } from "../core/base-path.ts";
10
13
  import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
14
+ import {
15
+ compileEveryRedirect,
16
+ isPatternPath,
17
+ } from "../core/redirect-patterns.ts";
18
+ import type { CompiledRedirect } from "../core/redirect-patterns.ts";
11
19
  import type { ResolvedConfig } from "../core/schema.ts";
12
20
  import { resolveDocsCollection } from "../core/sources/collection.ts";
13
21
  import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
14
22
  import { trimChar } from "../core/trim.ts";
15
23
  import type { ProjectContext } from "../core/types.ts";
24
+ import { hasVariables } from "../core/variables.ts";
16
25
  import { deployPassthrough } from "../deploy/adapters/types.ts";
17
26
  import { SVG_ASSET_HEADERS } from "../deploy/headers.ts";
18
27
  import { deployPlatform } from "../deploy/platforms/index.ts";
19
28
  import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
20
- import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
29
+ import {
30
+ applyBaseToAstroRedirects,
31
+ withMirrorRedirects,
32
+ } from "../deploy/redirects.ts";
33
+ import { API_RAIL_KEY } from "../markdown/api-rail.ts";
34
+ import { VIEWS_KEY } from "../markdown/views.ts";
21
35
  import type { OgCache } from "../og/cache.ts";
22
36
  import type { OgFont, OgFontFamilies, OgGoogleFont } from "../og/card.ts";
37
+ import { RATE_LIMIT_BINDING } from "../ratelimit/cloudflare.ts";
38
+ import type { RateLimitAdapter } from "../ratelimit/schema.ts";
23
39
  import type { MixedbreadOptions } from "../search/adapters/mixedbread.ts";
24
40
  import type {
25
41
  ResolvedSearchAdapter,
26
42
  SearchAdapterKind,
27
43
  } from "../search/adapters/registry.ts";
44
+ import type { SourcePage } from "../search/source-pages.ts";
28
45
  import { buildFontEntries, fontLocaleCodes } from "../theme/fonts.ts";
29
46
  import { importSpecifier, wrapperPropsType } from "./component-slots.ts";
30
47
  import type { ExampleSpec } from "./examples.ts";
@@ -113,7 +130,7 @@ const renderAstroAdapter = (
113
130
  const { astro } = platform;
114
131
  const args = {
115
132
  ...astro.options(context),
116
- ...deployPassthrough(deployment.options),
133
+ ...deployPassthrough(deployment.options, astro.configOptions),
117
134
  };
118
135
  const argsLiteral = Object.keys(args).length > 0 ? JSON.stringify(args) : "";
119
136
  const construct = `adapter(${argsLiteral})`;
@@ -124,7 +141,7 @@ const renderAstroAdapter = (
124
141
  ? `withAdapterRoot(${construct}, ${JSON.stringify(adapterRoot(context))})`
125
142
  : construct;
126
143
  return {
127
- configEntries: Object.entries(astro.config)
144
+ configEntries: Object.entries(astro.config(deployment.options))
128
145
  .map(([key, value]) => `\n ${key}: ${JSON.stringify(value)},`)
129
146
  .join(""),
130
147
  importLine: `import adapter from "${astro.package}";\n`,
@@ -188,7 +205,8 @@ export const runtimeDependencies = (options: {
188
205
  deps.push(
189
206
  ...new Set(config.reference.flatMap((adapter) => adapter.runtimeDeps)),
190
207
  ...config.search.provider.runtimeDeps,
191
- ...config.analytics.flatMap((adapter) => adapter.runtimeDeps)
208
+ ...config.analytics.flatMap((adapter) => adapter.runtimeDeps),
209
+ ...(config.consent?.runtimeDeps ?? [])
192
210
  );
193
211
  // Each content source adapter declares the SDK its fetch imports (Notion,
194
212
  // Sanity); the descriptor is the one place that knows.
@@ -273,18 +291,14 @@ const renderUserAliases = (
273
291
  .join("");
274
292
 
275
293
  /**
276
- * Excludes Vite's pre-bundled dep cache from @vitejs/plugin-react. Astro's
277
- * react() replaces the plugin's default `/node_modules/` exclude with just
278
- * `/\.astro$/`, so without this Babel re-parses every optimized dep chunk
279
- * served from `.vite/deps` — a 500KB+ vendor bundle per chunk, re-done on each
280
- * re-optimization. A blanket `/node_modules/` exclude would instead switch the
281
- * React Compiler off for Blume's own components in published installs (they
282
- * resolve under `node_modules/blume/src`, and exclude beats include in the
283
- * plugin's filter), so only the pre-bundle cache is excluded. The hidden runtime
284
- * relocates that cache to `<runtime>/.cache/vite` (see `cacheOptions`), so both
285
- * the default and the relocated path are excluded.
294
+ * Excludes the hidden runtime's pre-bundled dep cache from @vitejs/plugin-react.
295
+ * Astro's react() already excludes `/node_modules/`, which covers Vite's
296
+ * default `node_modules/.vite` cache, but the hidden runtime relocates that
297
+ * cache to `<runtime>/.cache/vite` (see `cacheOptions`). Without this the React
298
+ * Compiler re-transforms every optimized dep chunk served from there — a 500KB+
299
+ * vendor bundle per chunk, re-done on each re-optimization.
286
300
  */
287
- const REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//, /\/\.cache\/vite\//]`;
301
+ const REACT_EXCLUDE = String.raw`exclude: [/\/\.cache\/vite\//]`;
288
302
 
289
303
  /**
290
304
  * The `cacheDir` entries for the generated config's top level and its `vite`
@@ -314,17 +328,14 @@ const runtimeCacheOptions = (
314
328
  };
315
329
 
316
330
  /**
317
- * The `react()` integration call. When `compilerPath` is set (the resolved
318
- * absolute path to `babel-plugin-react-compiler`), react() carries the compiler
319
- * as the first babel plugin — an absolute path, because @vitejs/plugin-react
320
- * resolves babel plugins from the *project* root, not `.blume/`, so a bare
321
- * specifier wouldn't resolve in a user project. `target: "19"` matches Blume's
322
- * React pin. `null`/`undefined` (compiler off or unresolvable) omits the babel
323
- * block. Both variants carry the pre-bundle exclude above.
331
+ * The `react()` integration call. `compiler` turns on @astrojs/react's React
332
+ * Compiler, which runs on `oxc-transform-react` and targets the installed
333
+ * React major; false/absent (compiler off or unresolvable) leaves it out. Both
334
+ * variants carry the pre-bundle exclude above.
324
335
  */
325
- const reactIntegration = (compilerPath: string | null | undefined): string =>
326
- compilerPath
327
- ? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })`
336
+ const reactIntegration = (compiler: boolean | undefined): string =>
337
+ compiler
338
+ ? `react({ compiler: true, ${REACT_EXCLUDE} })`
328
339
  : `react({ ${REACT_EXCLUDE} })`;
329
340
 
330
341
  interface IntegrationBridgeOptions {
@@ -378,11 +389,10 @@ interface OptimizeDepsConfig {
378
389
  * the Vite root is the generated runtime, so user pages, convention islands,
379
390
  * and alias-reachable components all live outside it and are otherwise only
380
391
  * crawled when first requested. The compiler runtime rides the include list
381
- * because it is Babel-injected and no source scan can see it. @vitejs/plugin-react
382
- * would add it itself, but only when the babel plugin is passed by its bare
383
- * name (`getReactCompilerPlugin` is an exact string match) — Blume passes an
384
- * absolute path (see `reactIntegration`), which that check never matches. See
385
- * the optimizeDeps comment in the generated config for the failure this prevents.
392
+ * because the compiler injects its import and no source scan can see it. The
393
+ * compiler plugin in `@vitejs/plugin-react` lists it too; Blume keeps it
394
+ * explicit so the guard doesn't hinge on that plugin's internals. See the
395
+ * optimizeDeps comment in the generated config for the failure this prevents.
386
396
  */
387
397
  /**
388
398
  * The client-side libraries a site needs, decided at generation time. A
@@ -422,7 +432,7 @@ const resolveOptimizeDeps = (options: {
422
432
  context: ProjectContext;
423
433
  features: ClientFeatures;
424
434
  needsReact: boolean;
425
- reactCompilerPath: string | null | undefined;
435
+ reactCompiler: boolean | undefined;
426
436
  searchKind: SearchAdapterKind;
427
437
  }): OptimizeDepsConfig => {
428
438
  const { context, features } = options;
@@ -446,7 +456,7 @@ const resolveOptimizeDeps = (options: {
446
456
  // (__PREFETCH_PREFETCH_ALL__ and friends) that a pre-bundled copy loses,
447
457
  // throwing ReferenceError on every page. Astro manages their optimization
448
458
  // itself, without a mid-session reload.
449
- ...(options.needsReact && options.reactCompilerPath
459
+ ...(options.needsReact && options.reactCompiler
450
460
  ? ["react/compiler-runtime"]
451
461
  : []),
452
462
  ];
@@ -555,17 +565,23 @@ const blumeIntegrationOptions = (options: {
555
565
  contentRoutes: string[];
556
566
  ejected: boolean;
557
567
  pages: BlumePageRoute[];
558
- }): BlumeIntegrationOptions =>
559
- options.ejected
568
+ redirects: CompiledRedirect[];
569
+ }): BlumeIntegrationOptions => {
570
+ const shared: BlumeIntegrationOptions = { pages: options.pages };
571
+ if (options.redirects.length > 0) {
572
+ shared.redirects = options.redirects;
573
+ }
574
+ return options.ejected
560
575
  ? {
576
+ ...shared,
561
577
  buildArtifactsRoot: ".",
562
578
  contentRoutes: options.contentRoutes,
563
579
  homeLinkHeader:
564
580
  buildHomeLinkHeader(options.config, options.contentRoutes, "dev") ??
565
581
  undefined,
566
- pages: options.pages,
567
582
  }
568
- : { pages: options.pages };
583
+ : shared;
584
+ };
569
585
 
570
586
  export const astroConfigTemplate = (options: {
571
587
  context: ProjectContext;
@@ -581,6 +597,8 @@ export const astroConfigTemplate = (options: {
581
597
  /** The example-preview Tailwind entry (`blume:examples-theme`). */
582
598
  examplesThemePath: string;
583
599
  themePath: string;
600
+ /** The configured consent adapter's browser module (`blume:consent-client`). */
601
+ consentClientPath: string;
584
602
  searchClientPath: string;
585
603
  /** The generated client-feature loaders (`blume:features`). */
586
604
  featuresPath: string;
@@ -594,11 +612,10 @@ export const astroConfigTemplate = (options: {
594
612
  */
595
613
  generatedModulesDir?: string;
596
614
  /**
597
- * Absolute path to `babel-plugin-react-compiler` when the React Compiler is
598
- * enabled (resolved from Blume's package root by the caller); null/absent
599
- * disables the compiler and emits a bare `react()`.
615
+ * Turn on the React Compiler (the caller checked that `oxc-transform-react`
616
+ * resolves); false/absent emits `react()` without it.
600
617
  */
601
- reactCompilerPath?: string | null;
618
+ reactCompiler?: boolean;
602
619
  /** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
603
620
  aliases?: Record<string, string>;
604
621
  /**
@@ -617,6 +634,7 @@ export const astroConfigTemplate = (options: {
617
634
  );
618
635
  const {
619
636
  askPath,
637
+ consentClientPath,
620
638
  contentRoutes,
621
639
  examplesPath,
622
640
  examplesThemePath,
@@ -645,7 +663,7 @@ export const astroConfigTemplate = (options: {
645
663
  context,
646
664
  features,
647
665
  needsReact,
648
- reactCompilerPath: options.reactCompilerPath,
666
+ reactCompiler: options.reactCompiler,
649
667
  searchKind: config.search.provider.kind,
650
668
  });
651
669
 
@@ -681,17 +699,29 @@ export const astroConfigTemplate = (options: {
681
699
  // Base the redirect paths the same way routes are based, so a redirect lands
682
700
  // under `basePath` too. Astro layers its own `base` (deployment.base) onto
683
701
  // `from` when matching, but never onto `to` — see applyBaseToAstroRedirects.
684
- const basedRedirects = applyBaseToAstroRedirects(
685
- config.redirects,
686
- config.basePath,
687
- deployment.options.base ?? "",
688
- new Set(contentRoutes)
702
+ // A moved page's Markdown copies move with it (see withMirrorRedirects).
703
+ const redirectPages = new Set(contentRoutes);
704
+ const basedRedirects = withMirrorRedirects(
705
+ applyBaseToAstroRedirects(
706
+ config.redirects,
707
+ config.basePath,
708
+ deployment.options.base ?? "",
709
+ redirectPages
710
+ ),
711
+ redirectPages,
712
+ { from: "", to: normalizeBasePath(deployment.options.base) }
713
+ );
714
+ // Only exact redirects: Astro can't prerender a pattern's redirect pages
715
+ // (it would need every path the pattern covers), so a pattern reaches the
716
+ // dev server through the integration and each host through its own rules.
717
+ const exactRedirects = basedRedirects.filter(
718
+ (redirect) => !isPatternPath(redirect.from)
689
719
  );
690
720
  const redirectsOption =
691
- basedRedirects.length > 0
721
+ exactRedirects.length > 0
692
722
  ? `\n redirects: ${JSON.stringify(
693
723
  Object.fromEntries(
694
- basedRedirects.map((redirect) => [
724
+ exactRedirects.map((redirect) => [
695
725
  redirect.from,
696
726
  { destination: redirect.to, status: redirect.status },
697
727
  ])
@@ -759,12 +789,19 @@ export const astroConfigTemplate = (options: {
759
789
  const svelteImport = needsSvelte
760
790
  ? `import svelte from "@astrojs/svelte";\n`
761
791
  : "";
792
+ // Content variables in `.mdx` are replaced before the MDX compiler parses
793
+ // the source, which reads `{{name}}` as a JavaScript expression.
794
+ const substitutesVariables = hasVariables(config.variables);
795
+ const variablesPluginEntry = substitutesVariables
796
+ ? `variablesVitePlugin(${JSON.stringify(config.variables)}), `
797
+ : "";
762
798
  const blumeImports = [
763
799
  "blumeIntegration",
764
800
  "includeHmrPlugin",
765
801
  "prerenderDepsPlugin",
766
802
  ...runtimeModuleImports,
767
803
  ...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
804
+ ...(substitutesVariables ? ["variablesVitePlugin"] : []),
768
805
  ];
769
806
  const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
770
807
 
@@ -794,6 +831,7 @@ export const astroConfigTemplate = (options: {
794
831
  deployBase,
795
832
  externalLinks: config.markdown.externalLinks,
796
833
  headingAnchors: config.markdown.headingAnchors,
834
+ variables: config.variables,
797
835
  });
798
836
  const processorOptions =
799
837
  options.generatedModulesDir === undefined
@@ -804,7 +842,7 @@ export const astroConfigTemplate = (options: {
804
842
  `mdx({ processor: blumeMdxProcessor(${processorOptions}) })`,
805
843
  ];
806
844
  if (needsReact) {
807
- integrations.push(reactIntegration(options.reactCompilerPath));
845
+ integrations.push(reactIntegration(options.reactCompiler));
808
846
  }
809
847
  if (needsVue) {
810
848
  integrations.push("vue()");
@@ -822,6 +860,7 @@ export const astroConfigTemplate = (options: {
822
860
  contentRoutes,
823
861
  ejected,
824
862
  pages,
863
+ redirects: compileEveryRedirect(basedRedirects),
825
864
  })
826
865
  )})`
827
866
  );
@@ -885,7 +924,7 @@ ${userConfigSetup}export default defineConfig({
885
924
  // the overlap only adds memory.
886
925
  build: { concurrency: Math.min(8, availableParallelism()) },
887
926
  vite: {${viteCacheOption}
888
- plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
927
+ plugins: [${runtimeModulesPluginEntry}${variablesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
889
928
  `${context.outDir}/src/generated/includes.json`,
890
929
  ejected
891
930
  )}), prerenderDepsPlugin()],
@@ -897,8 +936,8 @@ ${userConfigSetup}export default defineConfig({
897
936
  // Everything hydration can reach must be part of the dev dep optimizer's
898
937
  // FIRST run. The Vite root is the generated runtime, so user pages,
899
938
  // islands, and aliased components live outside it and are only crawled
900
- // when first requested — and \`react/compiler-runtime\` is Babel-injected,
901
- // so no source scan can ever see it. A dependency discovered after
939
+ // when first requested — and \`react/compiler-runtime\` is injected by the
940
+ // React Compiler, so no source scan can ever see it. A dependency discovered after
902
941
  // hydration begins triggers a mid-session re-optimization whose new
903
942
  // generation imports React through new \`?v=\` URLs; the browser then
904
943
  // evaluates a second React copy and every island tears down with
@@ -944,6 +983,7 @@ ${userConfigSetup}export default defineConfig({
944
983
  resolve: {
945
984
  alias: {
946
985
  "blume:ask": ${configPath(askPath, ejected)},
986
+ "blume:consent-client": ${configPath(consentClientPath, ejected)},
947
987
  "blume:examples": ${configPath(examplesPath, ejected)},
948
988
  "blume:examples-theme": ${configPath(examplesThemePath, ejected)},
949
989
  "blume:features": ${configPath(featuresPath, ejected)},
@@ -1095,12 +1135,21 @@ const ASK_FALLBACK_PROMPT =
1095
1135
 
1096
1136
  /** The `ai.assistant` values the generated endpoint has to carry with it. */
1097
1137
  export interface AskEndpointOptions {
1138
+ /** `ai.assistant.captcha` — the bot check the route verifies first. */
1139
+ captcha?: CaptchaAdapter;
1098
1140
  /** `ai.assistant.cors` — origins allowed to call the route from another site. */
1099
1141
  cors?: string[];
1142
+ /** `rateLimit` — the limiter the route checks before any work. */
1143
+ rateLimit?: RateLimitAdapter | null;
1100
1144
  /** `ai.assistant.instructions` — extra system-prompt text. */
1101
1145
  instructions?: string;
1102
1146
  /** `ai.assistant.retrieval` — how much documentation each question carries. */
1103
1147
  retrieval?: AskRetrievalOptions;
1148
+ /**
1149
+ * `ai.assistant.tools`, resolved against the adapter's default: give the
1150
+ * model the docs search and read-page tools.
1151
+ */
1152
+ tools?: boolean;
1104
1153
  }
1105
1154
 
1106
1155
  /** The pieces `askEndpointTemplate` splices in for `ai.assistant.cors`. */
@@ -1132,7 +1181,7 @@ const askCorsTemplate = (cors: readonly string[] = []): AskCorsTemplate =>
1132
1181
  imports: [
1133
1182
  'import { preflightResponse, withCors } from "blume/ai/cors.ts";',
1134
1183
  ],
1135
- open: "withCors(ALLOWED_ORIGINS, async ({ request }) => {",
1184
+ open: "withCors(ALLOWED_ORIGINS, async (context) => {",
1136
1185
  setup: `
1137
1186
  const ALLOWED_ORIGINS = ${JSON.stringify(cors)};
1138
1187
 
@@ -1140,12 +1189,94 @@ export const OPTIONS: APIRoute = ({ request }) =>
1140
1189
  preflightResponse(request, ALLOWED_ORIGINS);
1141
1190
  `,
1142
1191
  }
1143
- : { close: "};", imports: [], open: "async ({ request }) => {", setup: "" };
1192
+ : { close: "};", imports: [], open: "async (context) => {", setup: "" };
1193
+
1194
+ /** The pieces a server route splices in to check `rateLimit` first. */
1195
+ interface RateLimitTemplate {
1196
+ /** The check at the top of the handler; reads `context`. */
1197
+ check: string;
1198
+ /** The runtime import, plus what the adapter's store needs. */
1199
+ imports: string[];
1200
+ /** The route's limiter, built once at module scope. */
1201
+ setup: string;
1202
+ }
1203
+
1204
+ /**
1205
+ * `rateLimit` for one server route: a limiter built at module scope from the
1206
+ * configured adapter, checked against every request, keyed by the reader's
1207
+ * address and `scope` so each route keeps its own budget (see
1208
+ * `ratelimit/runtime.ts`). Upstash reads its secrets through `getSecret`;
1209
+ * Cloudflare's binding comes from the Worker's env. Nothing at all when
1210
+ * rate limiting is off.
1211
+ */
1212
+ export const rateLimitTemplate = (
1213
+ adapter: RateLimitAdapter | null | undefined,
1214
+ scope: string
1215
+ ): RateLimitTemplate => {
1216
+ if (!adapter) {
1217
+ return { check: "", imports: [], setup: "" };
1218
+ }
1219
+ const imports = [
1220
+ 'import { createLimiter, rateLimited } from "blume/ratelimit/runtime.ts";',
1221
+ ];
1222
+ let runtime = "";
1223
+ if (adapter.kind === "upstash") {
1224
+ imports.push('import { getSecret } from "astro:env/server";');
1225
+ runtime = ", { secret: getSecret }";
1226
+ } else if (adapter.kind === "cloudflare") {
1227
+ imports.push(
1228
+ "// @ts-ignore `cloudflare:workers` is typed once `wrangler types` has run.",
1229
+ 'import { env } from "cloudflare:workers";'
1230
+ );
1231
+ runtime = `, { binding: Reflect.get(env, ${JSON.stringify(RATE_LIMIT_BINDING)}) }`;
1232
+ }
1233
+ return {
1234
+ check: ` const limited = await rateLimited(limiter, context, ${JSON.stringify(scope)});
1235
+ if (limited) {
1236
+ return limited;
1237
+ }
1238
+ `,
1239
+ imports,
1240
+ setup: `\nconst limiter = createLimiter(${JSON.stringify(adapter)}${runtime});\n`,
1241
+ };
1242
+ };
1243
+
1244
+ /**
1245
+ * `ai.assistant.captcha` for the ask route: verify the question's token with
1246
+ * the provider before the model runs (see `captcha/verify.ts`). A missing
1247
+ * secret answers with the same "not configured" notice as a missing
1248
+ * provider key, which the panel shows as is; a failed check answers `403`.
1249
+ */
1250
+ const askCaptchaTemplate = (adapter?: CaptchaAdapter): RateLimitTemplate => {
1251
+ if (!adapter) {
1252
+ return { check: "", imports: [], setup: "" };
1253
+ }
1254
+ const [secret = ""] = adapter.requiredSecrets;
1255
+ return {
1256
+ check: ` const captchaSecret = getSecret(${JSON.stringify(secret)});
1257
+ if (!captchaSecret) {
1258
+ return new Response(
1259
+ ${JSON.stringify(`The assistant is not configured: set ${secret}.`)},
1260
+ { status: 503 }
1261
+ );
1262
+ }
1263
+ const captchaToken =
1264
+ typeof body.captcha === "string" ? body.captcha : undefined;
1265
+ if (!(await verifyCaptcha(CAPTCHA, captchaToken, context, { secret: captchaSecret }))) {
1266
+ return new Response("Verification failed: the bot check didn't pass.", {
1267
+ status: 403,
1268
+ });
1269
+ }
1270
+ `,
1271
+ imports: ['import { verifyCaptcha } from "blume/captcha/verify.ts";'],
1272
+ setup: `\nconst CAPTCHA = ${JSON.stringify(adapter)};\n`,
1273
+ };
1274
+ };
1144
1275
 
1145
1276
  /**
1146
1277
  * Largest request body the assistant route reads: 64 KB, well above the
1147
- * 24,000-character message budget it validates next, so a real conversation
1148
- * never meets it.
1278
+ * message budget it validates next (`ai/ask-limits.ts`), so a real
1279
+ * conversation never meets it.
1149
1280
  */
1150
1281
  const ASK_BODY_LIMIT_BYTES = 65_536;
1151
1282
 
@@ -1174,6 +1305,8 @@ export const askEndpointTemplate = (
1174
1305
  ): string => {
1175
1306
  const { instructions, retrieval } = options ?? {};
1176
1307
  const { grounded } = backend;
1308
+ // The tools read the grounding snapshot, so an ungrounded backend has none.
1309
+ const tools = grounded && Boolean(options?.tools);
1177
1310
  const fallbackPrompt = instructions
1178
1311
  ? `${ASK_FALLBACK_PROMPT}\n\n${instructions}`
1179
1312
  : ASK_FALLBACK_PROMPT;
@@ -1201,12 +1334,32 @@ export const askEndpointTemplate = (
1201
1334
  if (retrieval) {
1202
1335
  groundFields.push(`retrieval: ${JSON.stringify(retrieval)}`);
1203
1336
  }
1337
+ if (tools) {
1338
+ groundFields.push("tools: true");
1339
+ }
1204
1340
  const groundOptions =
1205
1341
  groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
1206
1342
  setup += `\nconst ground = createAskContext(askData${groundOptions});\n`;
1343
+ if (tools) {
1344
+ imports.push(
1345
+ 'import { stepCountIs } from "ai";',
1346
+ 'import { ASK_MAX_STEPS, createAskTools } from "blume/ai/ask-tools.ts";'
1347
+ );
1348
+ setup += "const askTools = createAskTools(askData);\n";
1349
+ }
1207
1350
  }
1208
1351
  const cors = askCorsTemplate(options?.cors);
1209
1352
  imports.push(...cors.imports);
1353
+ const limit = rateLimitTemplate(options?.rateLimit, "ask");
1354
+ for (const line of limit.imports) {
1355
+ if (!imports.includes(line)) {
1356
+ imports.push(line);
1357
+ }
1358
+ }
1359
+ setup += limit.setup;
1360
+ const captcha = askCaptchaTemplate(options?.captcha);
1361
+ imports.push(...captcha.imports);
1362
+ setup += captcha.setup;
1210
1363
  // Validate the client-supplied body and cap its size. The endpoint is
1211
1364
  // unauthenticated, so bounding message count/length limits how much a caller
1212
1365
  // can spend against the model per request, and restricting roles to
@@ -1215,7 +1368,8 @@ export const askEndpointTemplate = (
1215
1368
  // limiter (or your provider's limits) for stronger protection. The body is
1216
1369
  // read under a 64 KB cap before any parsing, since a self-hosted Node server
1217
1370
  // would otherwise buffer an arbitrarily large POST in memory first.
1218
- const validate = ` const text = await readCappedText(request, ${ASK_BODY_LIMIT_BYTES});
1371
+ const validate = ` const { request } = context;
1372
+ ${limit.check} const text = await readCappedText(request, ${ASK_BODY_LIMIT_BYTES});
1219
1373
  if (text === undefined) {
1220
1374
  return new Response("Request too large: the body must be at most 64 KB.", {
1221
1375
  status: 413,
@@ -1231,7 +1385,7 @@ export const askEndpointTemplate = (
1231
1385
  const valid =
1232
1386
  Array.isArray(raw) &&
1233
1387
  raw.length > 0 &&
1234
- raw.length <= 40 &&
1388
+ raw.length <= ${ASK_MAX_MESSAGES} &&
1235
1389
  raw.every(
1236
1390
  (m: unknown) =>
1237
1391
  typeof m === "object" &&
@@ -1239,10 +1393,10 @@ export const askEndpointTemplate = (
1239
1393
  ("role" in m && (m.role === "user" || m.role === "assistant")) &&
1240
1394
  ("content" in m && typeof m.content === "string")
1241
1395
  ) &&
1242
- JSON.stringify(raw).length <= 24_000;
1396
+ JSON.stringify(raw).length <= ${ASK_MAX_MESSAGES_CHARS};
1243
1397
  if (!valid) {
1244
1398
  return new Response(
1245
- "Invalid request: send 1-40 user/assistant messages with string content.",
1399
+ "Invalid request: send 1-${ASK_MAX_MESSAGES} user/assistant messages with string content.",
1246
1400
  { status: 400 }
1247
1401
  );
1248
1402
  }
@@ -1273,6 +1427,11 @@ export const askEndpointTemplate = (
1273
1427
  ? "instructions"
1274
1428
  : `instructions:\n ${JSON.stringify(fallbackPrompt)}`,
1275
1429
  "messages",
1430
+ // The model may search and read pages, then answer, within one request:
1431
+ // tool calls run here on the server and only the text reaches the reader.
1432
+ ...(tools
1433
+ ? ["tools: askTools(body.page)", "stopWhen: stepCountIs(ASK_MAX_STEPS)"]
1434
+ : []),
1276
1435
  ...backend.template.fields,
1277
1436
  ];
1278
1437
  // The request's signal aborts when the reader closes the panel mid-answer
@@ -1294,7 +1453,7 @@ ${call}`
1294
1453
  const handler = `export const POST: APIRoute = ${cors.open}
1295
1454
  ${validate}
1296
1455
  ${keyCheck}
1297
- try {
1456
+ ${captcha.check} try {
1298
1457
  ${stream}
1299
1458
  return createTextStreamResponse({
1300
1459
  stream: toTextStream({ stream: result.stream }),
@@ -1339,9 +1498,11 @@ const { strings } = Astro.props;
1339
1498
  ---
1340
1499
 
1341
1500
  <Assistant
1501
+ captcha={data.config.assistant?.captcha ?? undefined}
1342
1502
  endpoint={data.config.assistant?.endpoint ?? undefined}
1343
1503
  strings={strings ?? data.ui.assistant}
1344
1504
  suggestions={data.config.assistant?.suggestions ?? []}
1505
+ support={data.config.assistant?.support ?? undefined}
1345
1506
  />
1346
1507
  `
1347
1508
  : `---
@@ -1441,6 +1602,36 @@ ${
1441
1602
  }
1442
1603
  `;
1443
1604
 
1605
+ /**
1606
+ * Generate `.blume/src/generated/consent-client.ts`, behind the
1607
+ * `blume:consent-client` alias: the configured consent adapter's browser
1608
+ * module (see `consent/clients.ts`), started by `ConsentHead.astro` with the
1609
+ * adapter's options baked in. Only that adapter's module is referenced, so a
1610
+ * site bundles no other adapter's code. An adapter without one (a hosted
1611
+ * manager), or no `consent` at all, gets a no-op so the alias always resolves.
1612
+ */
1613
+ export const consentClientTemplate = (
1614
+ consent: ResolvedConfig["consent"]
1615
+ ): string => {
1616
+ const module = consent ? CONSENT_CLIENT_MODULES.get(consent.kind) : undefined;
1617
+ if (!(consent && module)) {
1618
+ return `// Generated by Blume. Do not edit.
1619
+ // The consent adapter has no browser module, or consent is off.
1620
+ export const startConsentClient = (): void => {};
1621
+ `;
1622
+ }
1623
+ return `// Generated by Blume. Do not edit.
1624
+ // The \`${consent.kind}\` consent adapter's browser module, with its options.
1625
+ import { start } from ${JSON.stringify(module)};
1626
+
1627
+ export const startConsentClient = (
1628
+ consent: Parameters<typeof start>[0]
1629
+ ): void => {
1630
+ start(consent, ${JSON.stringify(consent.options)});
1631
+ };
1632
+ `;
1633
+ };
1634
+
1444
1635
  /**
1445
1636
  * Generate `.blume/src/generated/search-client.ts` — the provider-specific
1446
1637
  * loader the `<Search>` component lazy-imports via the `blume:search-client`
@@ -1463,10 +1654,12 @@ export const searchClientTemplate = (config: ResolvedConfig): string => {
1463
1654
  return hostedSearchClient(provider);
1464
1655
  }
1465
1656
  case "server": {
1466
- return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}
1657
+ // The dialog passes `typing`, which paces its queries (see endpoint.ts).
1658
+ return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}import type { SearchClientOptions } from "blume/components/layout/search/types.ts";
1467
1659
  const api = joinBase(import.meta.env.BASE_URL, "api/search");
1468
1660
 
1469
- export const createSearch = () => create({ api });
1661
+ export const createSearch = (options: SearchClientOptions = {}) =>
1662
+ create({ ...options, api });
1470
1663
  `;
1471
1664
  }
1472
1665
  case "pagefind": {
@@ -1488,28 +1681,55 @@ export const createSearch = () => create({ url });
1488
1681
  /**
1489
1682
  * Generate the Mixedbread search endpoint (`/api/search`). It holds the secret
1490
1683
  * key server-side and proxies semantic queries to the configured store. The
1491
- * adapter's options are inlined as a literal so the route never imports the
1492
- * config. The result mapping is best-effort and may need tuning to how your
1493
- * content was synced (see the Mixedbread sync step / \`mxbai vs sync\`).
1684
+ * adapter's options are inlined as literals so the route never imports the
1685
+ * config. `pages` (see `sourcePages`) maps each source file to its page: a
1686
+ * chunk links to the page whose file `mxbai store sync` uploaded it from,
1687
+ * found by the longest trailing run of the path the CLI recorded.
1494
1688
  */
1495
1689
  export const mixedbreadSearchEndpointTemplate = (
1496
- options: MixedbreadOptions
1497
- ): string =>
1498
- `// Generated by Blume. Do not edit.
1499
- import type { APIRoute } from "astro";
1500
- import { getSecret } from "astro:env/server";
1501
- import Mixedbread from "@mixedbread/sdk";
1502
- import { readCappedText } from "blume/core/request-body.ts";
1690
+ options: MixedbreadOptions,
1691
+ pages: [string, SourcePage][],
1692
+ rateLimit?: RateLimitAdapter | null
1693
+ ): string => {
1694
+ const { search_options: tuning, storeId, ...searchOptions } = options;
1695
+ const limit = rateLimitTemplate(rateLimit, "search");
1696
+ const imports = [
1697
+ 'import type { APIRoute } from "astro";',
1698
+ 'import { getSecret } from "astro:env/server";',
1699
+ 'import Mixedbread from "@mixedbread/sdk";',
1700
+ 'import { readCappedText } from "blume/core/request-body.ts";',
1701
+ ...limit.imports.filter((line) => !line.includes('"astro:env/server"')),
1702
+ ];
1703
+ return `// Generated by Blume. Do not edit.
1704
+ ${imports.join("\n")}
1503
1705
 
1504
1706
  export const prerender = false;
1505
1707
 
1506
1708
  const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" });
1507
- const OPTIONS = ${JSON.stringify(options)};
1508
- // Every option besides the store reaches the search call verbatim.
1509
- const { storeId: STORE_ID, ...SEARCH_OPTIONS } = OPTIONS;
1510
-
1511
- export const POST: APIRoute = async ({ request }) => {
1512
- // A search body is one short query: read it under a 16 KB cap, so a
1709
+ const STORE_ID = ${JSON.stringify(storeId)};
1710
+ // Every other option reaches the search call verbatim.
1711
+ const SEARCH_OPTIONS = ${JSON.stringify(searchOptions)};
1712
+ const SEARCH_TUNING = ${JSON.stringify(tuning ?? {})};
1713
+ // Each page's source file, relative to the project root, and the page it renders.
1714
+ const PAGES = new Map<string, { title: string; url: string }>(${JSON.stringify(pages)});
1715
+
1716
+ // The page a synced file renders. \`mxbai store sync\` records the path
1717
+ // relative to where it ran (the project root, or a monorepo root above it), so
1718
+ // the longest trailing run of that path that names a page's file wins.
1719
+ const pageFor = (path: string) => {
1720
+ const segments = path.replaceAll("\\\\", "/").split("/");
1721
+ for (let start = 0; start < segments.length; start += 1) {
1722
+ const page = PAGES.get(segments.slice(start).join("/"));
1723
+ if (page) {
1724
+ return page;
1725
+ }
1726
+ }
1727
+ return undefined;
1728
+ };
1729
+ ${limit.setup}
1730
+ export const POST: APIRoute = async (context) => {
1731
+ const { request } = context;
1732
+ ${limit.check} // A search body is one short query: read it under a 16 KB cap, so a
1513
1733
  // self-hosted server never buffers an arbitrarily large POST first.
1514
1734
  const text = await readCappedText(request, 16_384);
1515
1735
  if (text === undefined) {
@@ -1531,29 +1751,39 @@ export const POST: APIRoute = async ({ request }) => {
1531
1751
  });
1532
1752
  }
1533
1753
  // \`top_k\` defaults to 8 unless an option sets it; the query and store
1534
- // always come from the request and \`storeId\`.
1754
+ // always come from the request and \`storeId\`. File metadata is always
1755
+ // returned: it holds the path \`mxbai store sync\` recorded for the file.
1535
1756
  const response = await client.stores.search({
1536
1757
  top_k: 8,
1537
1758
  ...SEARCH_OPTIONS,
1538
1759
  query,
1760
+ search_options: { ...SEARCH_TUNING, return_metadata: true },
1539
1761
  store_identifiers: [STORE_ID],
1540
1762
  });
1541
- const hits = (response.data ?? []).map((chunk) => {
1763
+ const seen = new Set<string>();
1764
+ const hits = (response.data ?? []).flatMap((chunk) => {
1765
+ const { file_path: path } = (chunk.metadata ?? {}) as { file_path?: unknown };
1766
+ const page = typeof path === "string" ? pageFor(path) : undefined;
1767
+ // A file no page renders has nowhere to link, and a page's later chunks
1768
+ // would repeat it: one hit per page, at its best chunk.
1769
+ if (!page || seen.has(page.url)) {
1770
+ return [];
1771
+ }
1772
+ seen.add(page.url);
1542
1773
  const meta = chunk.generated_metadata ?? {};
1543
1774
  // Only a text chunk carries \`text\`; image, audio, and video chunks fall
1544
1775
  // back to the excerpt the store generated for them.
1545
1776
  const text = "text" in chunk ? chunk.text : undefined;
1546
- return {
1547
- excerpt: text ?? meta.excerpt ?? "",
1548
- title: meta.title ?? chunk.filename ?? "",
1549
- url: meta.url ?? "",
1550
- };
1777
+ return [
1778
+ { excerpt: text ?? meta.excerpt ?? "", title: page.title, url: page.url },
1779
+ ];
1551
1780
  });
1552
1781
  return new Response(JSON.stringify(hits), {
1553
1782
  headers: { "Content-Type": "application/json" },
1554
1783
  });
1555
1784
  };
1556
1785
  `;
1786
+ };
1557
1787
 
1558
1788
  /**
1559
1789
  * Generate the raw-Markdown endpoints (`[...slug].md.ts` and `[...slug].mdx.ts`).
@@ -1756,17 +1986,26 @@ export const ALL: APIRoute = ({ request }) => handler(request);
1756
1986
  * client-side data: that is the whole trust boundary keeping the endpoint from
1757
1987
  * being an open proxy onto the deployment's own network.
1758
1988
  */
1759
- export const playgroundProxyTemplate = (origins: string[]): string =>
1760
- `// Generated by Blume. Do not edit.
1761
- import type { APIRoute } from "astro";
1762
- import { createPlaygroundProxyHandler } from "blume/openapi/proxy.ts";
1989
+ export const playgroundProxyTemplate = (
1990
+ origins: string[],
1991
+ rateLimit?: RateLimitAdapter | null
1992
+ ): string => {
1993
+ const limit = rateLimitTemplate(rateLimit, "api-proxy");
1994
+ const route = limit.check
1995
+ ? `export const ALL: APIRoute = async (context) => {
1996
+ ${limit.check} return handler(context.request);
1997
+ };`
1998
+ : "export const ALL: APIRoute = ({ request }) => handler(request);";
1999
+ return `// Generated by Blume. Do not edit.
2000
+ ${['import type { APIRoute } from "astro";', 'import { createPlaygroundProxyHandler } from "blume/openapi/proxy.ts";', ...limit.imports].join("\n")}
1763
2001
 
1764
2002
  export const prerender = false;
1765
2003
 
1766
2004
  const handler = createPlaygroundProxyHandler(${JSON.stringify(origins)});
1767
-
1768
- export const ALL: APIRoute = ({ request }) => handler(request);
2005
+ ${limit.setup}
2006
+ ${route}
1769
2007
  `;
2008
+ };
1770
2009
 
1771
2010
  /** Generate a prerendered endpoint that serves a fixed JSON payload. */
1772
2011
  export const staticJsonEndpointTemplate = <Payload extends object>(
@@ -2090,6 +2329,9 @@ const contentComponentsSource = (mathEnabled: boolean) => {
2090
2329
  return {
2091
2330
  imports: `import Accordion from "blume/components/content/Accordion.astro";
2092
2331
  import AccordionItem from "blume/components/content/AccordionItem.astro";
2332
+ import ApiEndpoint from "blume/components/content/ApiEndpoint.astro";
2333
+ import ApiPlayground from "blume/components/content/ApiPlayground.astro";
2334
+ import ApiRail from "blume/components/content/ApiRail.astro";
2093
2335
  import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
2094
2336
  import Badge from "blume/components/content/Badge.astro";
2095
2337
  import Callout from "blume/components/content/Callout.astro";
@@ -2109,7 +2351,11 @@ import FileTree from "blume/components/content/FileTree.astro";
2109
2351
  import Frame from "blume/components/content/Frame.astro";
2110
2352
  import GithubInfo from "blume/components/content/GithubInfo.astro";
2111
2353
  import Panel from "blume/components/content/Panel.astro";
2354
+ import ParamField from "blume/components/content/ParamField.astro";
2112
2355
  import Prompt from "blume/components/content/Prompt.astro";
2356
+ import RequestExample from "blume/components/content/RequestExample.astro";
2357
+ import ResponseExample from "blume/components/content/ResponseExample.astro";
2358
+ import ResponseField from "blume/components/content/ResponseField.astro";
2113
2359
  import Step from "blume/components/content/Step.astro";
2114
2360
  import Steps from "blume/components/content/Steps.astro";
2115
2361
  import Tab from "blume/components/content/Tab.astro";
@@ -2120,10 +2366,13 @@ import TreeRoot from "blume/components/content/Tree.astro";
2120
2366
  import TreeFile from "blume/components/content/TreeFile.astro";
2121
2367
  import TreeFolder from "blume/components/content/TreeFolder.astro";
2122
2368
  import TypeTable from "blume/components/content/TypeTable.astro";
2369
+ import View from "blume/components/content/View.astro";
2123
2370
  import Visibility from "blume/components/content/Visibility.astro";
2124
2371
  import YouTube from "blume/components/content/YouTube.astro";
2125
2372
  import Icon from "blume/components/Icon.astro";
2126
2373
  import LocaleLinks from "blume/components/layout/LocaleLinks.astro";
2374
+ import NarrationPlayer from "blume/components/layout/NarrationPlayer.astro";
2375
+ import ViewSwitcher from "blume/components/content/ViewSwitcher.astro";
2127
2376
  import ApiOverview from "blume/components/openapi/ApiOverview.astro";
2128
2377
  import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
2129
2378
  import Operation from "blume/components/openapi/Operation.astro";
@@ -2131,7 +2380,10 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2131
2380
  map: `{
2132
2381
  Accordion,
2133
2382
  AccordionItem,
2383
+ ApiEndpoint,
2134
2384
  ApiOverview,
2385
+ ApiPlayground,
2386
+ ApiRail,
2135
2387
  ApiTagOperations,
2136
2388
  AutoTypeTable,
2137
2389
  Badge,
@@ -2152,7 +2404,11 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2152
2404
  Icon,
2153
2405
  Operation,
2154
2406
  Panel,
2407
+ ParamField,
2155
2408
  Prompt,
2409
+ RequestExample,
2410
+ ResponseExample,
2411
+ ResponseField,
2156
2412
  Step,
2157
2413
  Steps,
2158
2414
  Tab,
@@ -2161,6 +2417,7 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2161
2417
  Tooltip,
2162
2418
  Tree,
2163
2419
  TypeTable,
2420
+ View,
2164
2421
  Visibility,
2165
2422
  YouTube,
2166
2423
  ${mathEntry}...userMdx,
@@ -2191,10 +2448,12 @@ export const catchAllPageTemplate = (options: {
2191
2448
  return `---
2192
2449
  // Generated by Blume. Do not edit.
2193
2450
  import { getEntry, render } from "astro:content";
2451
+ import { pageModeLayout } from "blume/core/page-modes.ts";
2194
2452
  import type { CollectionKey } from "astro:content";
2195
2453
  import RootLayout from "blume/components/layout/RootLayout.astro";
2196
2454
  import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
2197
2455
  import { mountBasePath, stripBasePath } from "blume/core/base-path.ts";
2456
+ import { routeSetFor, servesRoute } from "blume/core/locale-links.ts";
2198
2457
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2199
2458
  ${componentImports}
2200
2459
  import data from "blume:data";
@@ -2242,6 +2501,13 @@ const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render
2242
2501
  // frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
2243
2502
  // counts: \`frontmatter.extend\` can declare the same key, and on a page with
2244
2503
  // no headings that user-supplied value would pass straight through.
2504
+ // A page written in \`<View>\` blocks gets their picker above the content;
2505
+ // the views plugin lists them through the render's frontmatter (see
2506
+ // markdown/views.ts), and only a list of titled entries counts.
2507
+ const viewsRaw = remarkPluginFrontmatter?.${VIEWS_KEY};
2508
+ const views = Array.isArray(viewsRaw)
2509
+ ? viewsRaw.filter((view) => typeof view?.title === "string")
2510
+ : [];
2245
2511
  const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
2246
2512
  const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
2247
2513
  const headings =
@@ -2320,6 +2586,14 @@ const htmlLang = i18n ? locale : "en";
2320
2586
  // follows that language — not the (mirrored) page locale.
2321
2587
  const contentLocale =
2322
2588
  fallback && i18n?.fallbackLocale ? i18n.fallbackLocale : locale;
2589
+ // "Listen to this page", unless the page opts out. The spoken cues are read
2590
+ // between the content's own sentences, so they come from the content's
2591
+ // language (a fallback page's), while the player's labels follow the page.
2592
+ // A \`custom\` or \`frame\` page brings its own heading: no title, description,
2593
+ // or narration player above its content.
2594
+ const pageChrome = pageModeLayout(frontmatter.mode).chrome;
2595
+ const narration = pageChrome && frontmatter.narration ? data.config.narration : null;
2596
+ const narrationCues = (i18n ? (data.uiByLocale[contentLocale] ?? data.ui) : data.ui).narration;
2323
2597
  const contentDir = i18n
2324
2598
  ? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
2325
2599
  : "ltr";
@@ -2383,18 +2657,24 @@ const logicalRoute = i18n
2383
2657
  ? stripLocale(stripBasePath(data.config.basePath, route), locale)
2384
2658
  : route;
2385
2659
  // A page from a one-language source (GitHub Releases) gets no switcher: every
2386
- // other locale would only repeat the same text.
2660
+ // other locale would only repeat the same text. A locale with no real
2661
+ // translation links the page's fallback copy, which exists only while
2662
+ // fallbacks are on; where nothing is served at that URL (\`fallbackLocale:
2663
+ // null\`), the locale is left out rather than linked to a 404.
2387
2664
  const localeSwitch = i18n && !monolingual
2388
- ? i18n.locales.map((l) => {
2665
+ ? i18n.locales.flatMap((l) => {
2389
2666
  const alt = (alternates ?? []).find((x) => x.locale === l.code);
2390
- return {
2391
- code: l.code,
2392
- current: l.code === locale,
2393
- dir: l.dir,
2394
- href: alt ? alt.path : mountLocalized(logicalRoute, l.code),
2395
- label: l.label,
2396
- untranslated: !alt,
2397
- };
2667
+ const href = alt ? alt.path : mountLocalized(logicalRoute, l.code);
2668
+ return alt || servesRoute(routeSetFor(data.routes), href)
2669
+ ? [{
2670
+ code: l.code,
2671
+ current: l.code === locale,
2672
+ dir: l.dir,
2673
+ href,
2674
+ label: l.label,
2675
+ untranslated: !alt,
2676
+ }]
2677
+ : [];
2398
2678
  })
2399
2679
  : [];
2400
2680
 
@@ -2503,6 +2783,10 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2503
2783
  canonical={canonical}
2504
2784
  editUrl={editUrl}
2505
2785
  feedback={data.config.feedback}
2786
+ feedbackComments={data.config.feedbackComments}
2787
+ pagination={frontmatter.pagination}
2788
+ related={frontmatter.related}
2789
+ search={frontmatter.search}
2506
2790
  exportPdf={${options.exportPdf}}
2507
2791
  exportEpub={${options.exportEpub}}${
2508
2792
  options.navFragments
@@ -2515,13 +2799,25 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2515
2799
  discovery={data.config.discovery}
2516
2800
  siteUrl={data.config.site}
2517
2801
  pageType={frontmatter.type}
2802
+ apiRail={remarkPluginFrontmatter?.${API_RAIL_KEY} === true}
2803
+ pageMode={frontmatter.mode}
2518
2804
  published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
2519
2805
  lastModified={lastModified}
2520
2806
  noindex={effectiveNoindex}
2521
2807
  structuredDataEnabled={data.config.structuredData}
2522
2808
  >
2523
- <h1>{title}</h1>
2524
- {frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
2809
+ {pageChrome && <h1>{title}</h1>}
2810
+ {pageChrome && frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
2811
+ {narration && (
2812
+ <NarrationPlayer
2813
+ audioBase={narration.generated ? withMountedBase("/blume-narration/audio/") : undefined}
2814
+ cues={narrationCues}
2815
+ lang={i18n ? contentLocale : htmlLang}
2816
+ manifest={narration.generated ? withMountedBase(encodeURI(\`/blume-narration/\${route === "/" ? "index" : route.slice(1)}.json\`)) : undefined}
2817
+ strings={ui.narration}
2818
+ />
2819
+ )}
2820
+ {views.length > 1 && <ViewSwitcher label={ui.content.selectView} views={views} />}
2525
2821
  <LocaleLinks locale={locale}>
2526
2822
  <Content components={components} />
2527
2823
  </LocaleLinks>
@@ -2705,13 +3001,15 @@ const canonical = base ? base + basedRoute : null;
2705
3001
  const ogPath = data.config.og.enabled ? withMountedBase("/og/changelog.png") : null;
2706
3002
  const ogImage = ogPath && base ? base + ogPath : ogPath;
2707
3003
 
2708
- // The page chrome (h1, title, description) comes from the translatable
2709
- // \`changelog\` group; optional chaining tolerates a not-yet-regenerated data
2710
- // snapshot from before these keys existed.
3004
+ // The page chrome (h1, title, description, empty state) comes from the
3005
+ // translatable \`changelog\` group, which carries the \`changelog\` config's title and
3006
+ // description when it sets them; optional chaining tolerates a
3007
+ // not-yet-regenerated data snapshot from before these keys existed.
2711
3008
  const changelogTitle = data.ui.changelog?.title ?? "Changelog";
2712
3009
  const changelogDescription =
2713
3010
  data.ui.changelog?.description ??
2714
3011
  "Product updates, new features, and fixes from every release.";
3012
+ const changelogEmpty = data.ui.changelog?.empty ?? "No changelog entries yet.";
2715
3013
  // The layout suffixes "- {site title}" itself, so the page title is just the
2716
3014
  // changelog's own name — prefixing the site title too would double it
2717
3015
  // ("Acme Changelog - Acme").
@@ -2764,7 +3062,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2764
3062
  <p class="text-lg text-muted-foreground">{changelogDescription}</p>
2765
3063
  {
2766
3064
  items.length === 0 ? (
2767
- <p>No changelog entries yet.</p>
3065
+ <p>{changelogEmpty}</p>
2768
3066
  ) : (
2769
3067
  <div class="not-prose mt-10 divide-y divide-border border-border border-y">
2770
3068
  {groups.map((group) => (