blume 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (455) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-1d7ve1dm.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-1d7ve1dm.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-2eytanqx.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-yfyb25rh.js → chunk-2f3ndkpw.js} +24 -8
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-r20tn01b.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-r20tn01b.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-35d4wj9f.js → chunk-2n4ps1b3.js} +7 -7
  12. package/dist/cli/{chunk-j85vccga.js → chunk-43zcf4gz.js} +97 -57
  13. package/dist/cli/{chunk-j85vccga.js.map → chunk-43zcf4gz.js.map} +6 -6
  14. package/dist/cli/{chunk-e04dxsz1.js → chunk-4ejxbzee.js} +1 -1
  15. package/dist/cli/{chunk-5m5nmvyq.js → chunk-4h8z9vv5.js} +233 -39
  16. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  17. package/dist/cli/{chunk-5f86nr5m.js → chunk-5cg2rzgf.js} +18 -16
  18. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-5cg2rzgf.js.map} +2 -2
  19. package/dist/cli/{chunk-tzmab476.js → chunk-5ffywsmb.js} +4 -4
  20. package/dist/cli/{chunk-2z928egk.js → chunk-5wj2qwn2.js} +5 -5
  21. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  22. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  23. package/dist/cli/{chunk-vg9r4eb9.js → chunk-713mngdq.js} +12 -5
  24. package/dist/cli/{chunk-vg9r4eb9.js.map → chunk-713mngdq.js.map} +3 -3
  25. package/dist/cli/{chunk-5xvm6tfj.js → chunk-9geymqns.js} +17 -15
  26. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-9geymqns.js.map} +2 -2
  27. package/dist/cli/{chunk-q58y5e6a.js → chunk-9xr4dgee.js} +26 -17
  28. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  29. package/dist/cli/{chunk-b07cmahc.js → chunk-amrb6hgh.js} +2 -2
  30. package/dist/cli/chunk-cm89scdv.js +443 -0
  31. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  32. package/dist/cli/{chunk-akbpwfxc.js → chunk-cmtcnh9f.js} +2 -2
  33. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  34. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  35. package/dist/cli/{chunk-crgn1q09.js → chunk-e7vvk5ya.js} +4 -4
  36. package/dist/cli/{chunk-pehfxfta.js → chunk-g2z9x01v.js} +6 -98
  37. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  38. package/dist/cli/{chunk-tkacnehg.js → chunk-gp84za0h.js} +2 -2
  39. package/dist/cli/{chunk-wrr3j9w9.js → chunk-h2a48yhd.js} +186 -63
  40. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  41. package/dist/cli/{chunk-hm3vjy5s.js → chunk-jnwj5kjj.js} +14 -9
  42. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-jnwj5kjj.js.map} +4 -4
  43. package/dist/cli/chunk-kwrg63tm.js +31 -0
  44. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  45. package/dist/cli/{chunk-2hsdwb9n.js → chunk-nd1821sk.js} +18 -16
  46. package/dist/cli/{chunk-2hsdwb9n.js.map → chunk-nd1821sk.js.map} +2 -2
  47. package/dist/cli/{chunk-6dsbexzp.js → chunk-ntv4qrt7.js} +17 -15
  48. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-ntv4qrt7.js.map} +2 -2
  49. package/dist/cli/{chunk-r9rcc4w7.js → chunk-p804f3pe.js} +7 -7
  50. package/dist/cli/{chunk-ey84smr6.js → chunk-py62fewh.js} +6 -6
  51. package/dist/cli/{chunk-ey84smr6.js.map → chunk-py62fewh.js.map} +1 -1
  52. package/dist/cli/chunk-q3wxxq39.js +126 -0
  53. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  54. package/dist/cli/{chunk-9t7a85s3.js → chunk-rekqz4qg.js} +23 -13
  55. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  56. package/dist/cli/{chunk-8ktnccpt.js → chunk-ta27feev.js} +1 -1
  57. package/dist/cli/{chunk-pv29h0wf.js → chunk-xkz5xbtv.js} +3476 -822
  58. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  59. package/dist/cli/{chunk-a58773jm.js → chunk-y4ykbark.js} +2 -2
  60. package/dist/cli/{chunk-p3v96n38.js → chunk-y5h4r2y0.js} +4 -4
  61. package/dist/cli/{chunk-acanzt5p.js → chunk-yj17vs39.js} +6 -6
  62. package/dist/cli/{chunk-p73c0m7w.js → chunk-yxmk4pec.js} +16 -14
  63. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-yxmk4pec.js.map} +2 -2
  64. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  65. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  66. package/dist/cli/{chunk-c8chx29p.js → chunk-zf748avv.js} +384 -207
  67. package/dist/cli/chunk-zf748avv.js.map +37 -0
  68. package/dist/cli/{chunk-g4hq16wv.js → chunk-zhvd6adm.js} +16 -14
  69. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-zhvd6adm.js.map} +2 -2
  70. package/dist/cli/index.js +26 -20
  71. package/dist/cli/index.js.map +5 -5
  72. package/dist/types/ai/agent-surface.d.ts +8 -7
  73. package/dist/types/ai/ai-catalog.d.ts +7 -0
  74. package/dist/types/ai/ask-context.d.ts +22 -0
  75. package/dist/types/ai/ask.d.ts +271 -58
  76. package/dist/types/ai/index.d.ts +2 -2
  77. package/dist/types/ai/site-skill.d.ts +26 -0
  78. package/dist/types/ai/skills.d.ts +16 -0
  79. package/dist/types/astro/integration.d.ts +8 -0
  80. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  81. package/dist/types/captcha/index.d.ts +28 -0
  82. package/dist/types/captcha/schema.d.ts +29 -0
  83. package/dist/types/captcha/turnstile.d.ts +28 -0
  84. package/dist/types/components/content/api-field.d.ts +59 -0
  85. package/dist/types/components/content/api-page.d.ts +95 -0
  86. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  87. package/dist/types/components/openapi/helpers.d.ts +137 -0
  88. package/dist/types/components/openapi/security.d.ts +85 -0
  89. package/dist/types/consent/ethyca.d.ts +48 -0
  90. package/dist/types/consent/index.d.ts +37 -0
  91. package/dist/types/consent/native.d.ts +28 -0
  92. package/dist/types/consent/osano.d.ts +42 -0
  93. package/dist/types/consent/schema.d.ts +110 -0
  94. package/dist/types/core/config-input.d.ts +187 -20
  95. package/dist/types/core/data.d.ts +49 -4
  96. package/dist/types/core/footer.d.ts +8 -0
  97. package/dist/types/core/i18n-ui.d.ts +86 -0
  98. package/dist/types/core/includes.d.ts +8 -2
  99. package/dist/types/core/localizable.d.ts +7 -0
  100. package/dist/types/core/page-modes.d.ts +29 -0
  101. package/dist/types/core/redirect-patterns.d.ts +144 -0
  102. package/dist/types/core/related.d.ts +40 -0
  103. package/dist/types/core/schema.d.ts +551 -56
  104. package/dist/types/core/sources/read.d.ts +5 -0
  105. package/dist/types/core/types.d.ts +1 -1
  106. package/dist/types/core/variables.d.ts +42 -0
  107. package/dist/types/deploy/node-headers.d.ts +13 -2
  108. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  109. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  110. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  111. package/dist/types/deploy/redirects.d.ts +39 -11
  112. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  113. package/dist/types/markdown/code-title.d.ts +17 -0
  114. package/dist/types/markdown/features.d.ts +3 -2
  115. package/dist/types/narration/build.d.ts +70 -0
  116. package/dist/types/narration/provider.d.ts +26 -0
  117. package/dist/types/narration/script.d.ts +154 -0
  118. package/dist/types/openapi/model.d.ts +12 -6
  119. package/dist/types/openapi/overlay.d.ts +54 -0
  120. package/dist/types/openapi/parse.d.ts +13 -0
  121. package/dist/types/openapi/references.d.ts +14 -4
  122. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  123. package/dist/types/ratelimit/index.d.ts +27 -0
  124. package/dist/types/ratelimit/memory.d.ts +36 -0
  125. package/dist/types/ratelimit/schema.d.ts +111 -0
  126. package/dist/types/ratelimit/upstash.d.ts +21 -0
  127. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  128. package/dist/types/reference/asyncapi.d.ts +10 -10
  129. package/dist/types/reference/graphql.d.ts +13 -10
  130. package/dist/types/reference/openapi.d.ts +33 -12
  131. package/dist/types/reference/options.d.ts +45 -5
  132. package/dist/types/reference/scalar.d.ts +13 -0
  133. package/dist/types/reference/schema.d.ts +60 -36
  134. package/dist/types/search/documents.d.ts +12 -1
  135. package/dist/types/search/orama-index.d.ts +4 -0
  136. package/dist/types/seo/metatags.d.ts +20 -0
  137. package/docs/02-deployment.mdx +41 -5
  138. package/docs/03-upgrading.mdx +4 -4
  139. package/docs/04-migrating.mdx +1 -1
  140. package/docs/advanced/changelog.mdx +2 -2
  141. package/docs/advanced/skills.mdx +14 -0
  142. package/docs/cli/audit.mdx +21 -1
  143. package/docs/cli/index.mdx +1 -0
  144. package/docs/configuration/analytics.mdx +3 -1
  145. package/docs/configuration/assistant.mdx +188 -36
  146. package/docs/configuration/consent.mdx +100 -0
  147. package/docs/configuration/customization.mdx +4 -4
  148. package/docs/configuration/index.mdx +63 -5
  149. package/docs/configuration/meta.ts +3 -0
  150. package/docs/configuration/narration.mdx +119 -0
  151. package/docs/configuration/rate-limiting.mdx +69 -0
  152. package/docs/configuration/search.mdx +29 -1
  153. package/docs/configuration/theming.mdx +3 -1
  154. package/docs/content/components.mdx +106 -0
  155. package/docs/content/frontmatter.mdx +76 -0
  156. package/docs/content/i18n.mdx +38 -3
  157. package/docs/content/includes.mdx +16 -0
  158. package/docs/content/meta.mdx +1 -0
  159. package/docs/content/meta.ts +1 -0
  160. package/docs/content/navigation.mdx +28 -6
  161. package/docs/content/sources/contentful.mdx +32 -0
  162. package/docs/content/sources/custom.mdx +39 -0
  163. package/docs/content/sources/github-releases.mdx +31 -0
  164. package/docs/content/sources/index.mdx +91 -0
  165. package/docs/content/sources/meta.ts +16 -0
  166. package/docs/content/sources/notion.mdx +32 -0
  167. package/docs/content/sources/obsidian.mdx +37 -0
  168. package/docs/content/sources/payload.mdx +30 -0
  169. package/docs/content/sources/remote-mdx.mdx +40 -0
  170. package/docs/content/sources/sanity.mdx +29 -0
  171. package/docs/content/sources/strapi.mdx +31 -0
  172. package/docs/content/syntax.mdx +67 -2
  173. package/docs/content/variables.mdx +41 -0
  174. package/docs/discoverability/agent-discovery.mdx +29 -2
  175. package/docs/discoverability/index.mdx +1 -1
  176. package/docs/discoverability/llms-txt.mdx +1 -1
  177. package/docs/discoverability/metadata.mdx +17 -0
  178. package/docs/references/api-pages.mdx +74 -0
  179. package/docs/references/asyncapi.mdx +1 -1
  180. package/docs/references/graphql.mdx +2 -2
  181. package/docs/references/meta.ts +1 -1
  182. package/docs/references/openapi.mdx +102 -3
  183. package/docs/references/scalar.mdx +3 -1
  184. package/package.json +36 -1
  185. package/skills/blume/SKILL.md +3 -0
  186. package/skills/blume-migrate/SKILL.md +5 -5
  187. package/skills/blume-migrate/references/docusaurus.md +3 -3
  188. package/skills/blume-migrate/references/mintlify.md +37 -19
  189. package/skills/blume-migrate/references/nextra.md +2 -2
  190. package/skills/blume-migrate/references/starlight.md +2 -1
  191. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  192. package/skills/blume-write-skill/SKILL.md +82 -0
  193. package/src/ai/agent-readability.ts +2 -1
  194. package/src/ai/agent-surface.ts +9 -7
  195. package/src/ai/ai-catalog.ts +6 -3
  196. package/src/ai/ask-context.ts +73 -20
  197. package/src/ai/ask-data.ts +6 -0
  198. package/src/ai/ask-tools.ts +142 -0
  199. package/src/ai/ask.ts +354 -76
  200. package/src/ai/component-markdown.ts +75 -0
  201. package/src/ai/cors.ts +2 -0
  202. package/src/ai/index.ts +12 -0
  203. package/src/ai/llms.ts +6 -2
  204. package/src/ai/markdown.ts +2 -1
  205. package/src/ai/mcp/data.ts +6 -0
  206. package/src/ai/openapi-components.ts +9 -3
  207. package/src/ai/site-skill.ts +274 -0
  208. package/src/ai/skills.ts +35 -2
  209. package/src/ai/write-skill.ts +97 -0
  210. package/src/astro/generate.ts +132 -18
  211. package/src/astro/index.ts +1 -0
  212. package/src/astro/integration.ts +35 -0
  213. package/src/astro/module-types.ts +6 -0
  214. package/src/astro/templates.ts +283 -30
  215. package/src/astro/variables.ts +31 -0
  216. package/src/audit/catalog.ts +19 -0
  217. package/src/audit/checks/contrast.ts +183 -0
  218. package/src/audit/checks/i18n.ts +2 -3
  219. package/src/audit/checks/indexability.ts +2 -3
  220. package/src/audit/checks/links.ts +5 -12
  221. package/src/audit/checks/sitemap.ts +2 -3
  222. package/src/audit/redirects.ts +46 -15
  223. package/src/audit/run.ts +2 -0
  224. package/src/audit/types.ts +2 -1
  225. package/src/blume-modules.d.ts +7 -0
  226. package/src/captcha/client.ts +152 -0
  227. package/src/captcha/hcaptcha.ts +36 -0
  228. package/src/captcha/index.ts +28 -0
  229. package/src/captcha/schema.ts +32 -0
  230. package/src/captcha/turnstile.ts +38 -0
  231. package/src/captcha/verify.ts +71 -0
  232. package/src/cli/command-meta.ts +5 -0
  233. package/src/cli/commands/doctor.ts +22 -16
  234. package/src/cli/commands/skill.ts +96 -0
  235. package/src/cli/index.ts +5 -0
  236. package/src/cli/required-secrets.ts +71 -29
  237. package/src/components/BlumePage.astro +2 -0
  238. package/src/components/content/ApiEndpoint.astro +18 -0
  239. package/src/components/content/ApiField.astro +79 -0
  240. package/src/components/content/ApiPlayground.astro +62 -0
  241. package/src/components/content/ApiRail.astro +19 -0
  242. package/src/components/content/Color.astro +1 -0
  243. package/src/components/content/FileTree.astro +1 -0
  244. package/src/components/content/GithubInfo.astro +1 -0
  245. package/src/components/content/Panel.astro +1 -0
  246. package/src/components/content/ParamField.astro +25 -0
  247. package/src/components/content/RequestExample.astro +17 -0
  248. package/src/components/content/ResponseExample.astro +17 -0
  249. package/src/components/content/ResponseField.astro +32 -0
  250. package/src/components/content/Step.astro +1 -0
  251. package/src/components/content/Tile.astro +1 -0
  252. package/src/components/content/Tooltip.astro +1 -0
  253. package/src/components/content/Tree.astro +1 -0
  254. package/src/components/content/TypeTable.astro +4 -1
  255. package/src/components/content/View.astro +21 -0
  256. package/src/components/content/ViewSwitcher.astro +168 -0
  257. package/src/components/content/api-field.ts +100 -0
  258. package/src/components/content/api-page.ts +224 -0
  259. package/src/components/content/view-switcher.ts +71 -0
  260. package/src/components/index.ts +2 -0
  261. package/src/components/islands/Assistant.astro +10 -1
  262. package/src/components/islands/assistant.tsx +104 -14
  263. package/src/components/islands/code-question.ts +62 -0
  264. package/src/components/islands/hooks.ts +110 -21
  265. package/src/components/islands/support-link.ts +68 -0
  266. package/src/components/layout/Analytics.astro +11 -2
  267. package/src/components/layout/Banner.astro +14 -10
  268. package/src/components/layout/Breadcrumbs.astro +6 -2
  269. package/src/components/layout/ConsentBanner.astro +62 -0
  270. package/src/components/layout/ConsentHead.astro +38 -0
  271. package/src/components/layout/Directory.astro +94 -0
  272. package/src/components/layout/DrawerSwitchers.astro +51 -0
  273. package/src/components/layout/Empty.astro +2 -2
  274. package/src/components/layout/Header.astro +122 -80
  275. package/src/components/layout/LanguageSwitcher.astro +48 -15
  276. package/src/components/layout/NarrationPlayer.astro +1085 -0
  277. package/src/components/layout/NavSelector.astro +33 -7
  278. package/src/components/layout/PageFeedback.astro +54 -49
  279. package/src/components/layout/PageLayout.astro +71 -3
  280. package/src/components/layout/ReferenceLayout.astro +17 -6
  281. package/src/components/layout/RelatedPages.astro +42 -0
  282. package/src/components/layout/RootLayout.astro +308 -45
  283. package/src/components/layout/Search.astro +31 -17
  284. package/src/components/layout/SiteFooter.astro +124 -0
  285. package/src/components/layout/SiteMetatags.astro +16 -0
  286. package/src/components/layout/consent/native.ts +79 -0
  287. package/src/components/layout/consent/types.ts +37 -0
  288. package/src/components/layout/footer-links.ts +53 -0
  289. package/src/components/layout/footer-socials.ts +87 -0
  290. package/src/components/layout/locale-redirect.ts +107 -0
  291. package/src/components/layout/nav-utils.ts +83 -0
  292. package/src/components/layout/page-feedback.ts +96 -0
  293. package/src/components/layout/search/analytics.ts +98 -0
  294. package/src/components/layout/search/flexsearch.ts +17 -5
  295. package/src/components/layout/search/orama-cloud.ts +26 -14
  296. package/src/components/layout/search/types.ts +20 -0
  297. package/src/components/layout/search/typesense.ts +5 -1
  298. package/src/components/openapi/Operation.astro +161 -13
  299. package/src/components/openapi/RequestBody.astro +6 -4
  300. package/src/components/openapi/RequestPanel.astro +59 -7
  301. package/src/components/openapi/Responses.astro +6 -4
  302. package/src/components/openapi/async-snippets.ts +4 -4
  303. package/src/components/openapi/code-samples.ts +97 -0
  304. package/src/components/openapi/security.ts +6 -3
  305. package/src/components/openapi/snippets.ts +869 -8
  306. package/src/components/props.ts +6 -0
  307. package/src/consent/client.ts +95 -0
  308. package/src/consent/clients.ts +19 -0
  309. package/src/consent/ethyca.ts +71 -0
  310. package/src/consent/head.ts +24 -0
  311. package/src/consent/index.ts +37 -0
  312. package/src/consent/init.ts +29 -0
  313. package/src/consent/native.ts +36 -0
  314. package/src/consent/osano.ts +59 -0
  315. package/src/consent/schema.ts +30 -0
  316. package/src/core/builtin-tags.ts +5 -0
  317. package/src/core/client-address.ts +19 -0
  318. package/src/core/config-input.ts +194 -20
  319. package/src/core/data.ts +44 -4
  320. package/src/core/diagnostics.ts +3 -2
  321. package/src/core/footer.ts +26 -0
  322. package/src/core/graph.ts +39 -36
  323. package/src/core/i18n-ui.ts +82 -2
  324. package/src/core/includes.ts +20 -3
  325. package/src/core/links.ts +16 -2
  326. package/src/core/localizable.ts +26 -0
  327. package/src/core/navigation.ts +43 -12
  328. package/src/core/page-modes.ts +56 -0
  329. package/src/core/project-graph.ts +111 -5
  330. package/src/core/redirect-patterns.ts +491 -0
  331. package/src/core/related.ts +131 -0
  332. package/src/core/schema.ts +318 -64
  333. package/src/core/sources/normalize.ts +2 -1
  334. package/src/core/sources/read.ts +8 -3
  335. package/src/core/types.ts +1 -1
  336. package/src/core/ui-packs/ar.ts +43 -0
  337. package/src/core/ui-packs/bg.ts +45 -0
  338. package/src/core/ui-packs/bn.ts +43 -0
  339. package/src/core/ui-packs/ca.ts +46 -0
  340. package/src/core/ui-packs/cs.ts +44 -0
  341. package/src/core/ui-packs/da.ts +45 -0
  342. package/src/core/ui-packs/de.ts +46 -0
  343. package/src/core/ui-packs/el.ts +45 -0
  344. package/src/core/ui-packs/es.ts +46 -0
  345. package/src/core/ui-packs/fa.ts +45 -0
  346. package/src/core/ui-packs/fi.ts +45 -0
  347. package/src/core/ui-packs/fr.ts +46 -0
  348. package/src/core/ui-packs/he.ts +43 -0
  349. package/src/core/ui-packs/hi.ts +44 -0
  350. package/src/core/ui-packs/hr.ts +45 -0
  351. package/src/core/ui-packs/hu.ts +44 -0
  352. package/src/core/ui-packs/id.ts +45 -0
  353. package/src/core/ui-packs/it.ts +45 -0
  354. package/src/core/ui-packs/ja.ts +45 -0
  355. package/src/core/ui-packs/ko.ts +44 -0
  356. package/src/core/ui-packs/nl.ts +46 -0
  357. package/src/core/ui-packs/no.ts +44 -0
  358. package/src/core/ui-packs/pl.ts +45 -0
  359. package/src/core/ui-packs/pt-br.ts +46 -0
  360. package/src/core/ui-packs/pt.ts +45 -0
  361. package/src/core/ui-packs/ro.ts +45 -0
  362. package/src/core/ui-packs/ru.ts +45 -0
  363. package/src/core/ui-packs/sk.ts +44 -0
  364. package/src/core/ui-packs/sr.ts +45 -0
  365. package/src/core/ui-packs/sv.ts +44 -0
  366. package/src/core/ui-packs/th.ts +43 -0
  367. package/src/core/ui-packs/tr.ts +45 -0
  368. package/src/core/ui-packs/uk.ts +45 -0
  369. package/src/core/ui-packs/vi.ts +45 -0
  370. package/src/core/ui-packs/zh-tw.ts +43 -0
  371. package/src/core/ui-packs/zh.ts +43 -0
  372. package/src/core/variables.ts +103 -0
  373. package/src/deploy/artifacts.ts +77 -8
  374. package/src/deploy/cloudflare-negotiation.ts +35 -5
  375. package/src/deploy/headers.ts +2 -1
  376. package/src/deploy/node-headers.ts +63 -9
  377. package/src/deploy/platforms/cloudflare.ts +32 -0
  378. package/src/deploy/platforms/netlify.ts +33 -1
  379. package/src/deploy/platforms/vercel.ts +37 -2
  380. package/src/deploy/redirects.ts +91 -24
  381. package/src/deploy/vercel-negotiation.ts +36 -0
  382. package/src/markdown/api-rail.ts +259 -0
  383. package/src/markdown/code-title.ts +35 -9
  384. package/src/markdown/features.ts +3 -2
  385. package/src/markdown/fence-meta.ts +2 -0
  386. package/src/markdown/include.ts +11 -1
  387. package/src/markdown/index.ts +28 -5
  388. package/src/markdown/math.ts +4 -4
  389. package/src/markdown/variables.ts +56 -0
  390. package/src/markdown/views.ts +82 -0
  391. package/src/narration/build.ts +412 -0
  392. package/src/narration/player.ts +249 -0
  393. package/src/narration/provider.ts +29 -0
  394. package/src/narration/script.ts +592 -0
  395. package/src/og/card.ts +7 -6
  396. package/src/openapi/model.ts +57 -24
  397. package/src/openapi/overlay.ts +256 -0
  398. package/src/openapi/parse.ts +55 -2
  399. package/src/openapi/references.ts +21 -5
  400. package/src/openapi/render-mdx.ts +5 -1
  401. package/src/openapi/scalar.ts +40 -6
  402. package/src/openapi/source.ts +1 -1
  403. package/src/ratelimit/cloudflare.ts +54 -0
  404. package/src/ratelimit/index.ts +30 -0
  405. package/src/ratelimit/memory.ts +45 -0
  406. package/src/ratelimit/runtime.ts +229 -0
  407. package/src/ratelimit/schema.ts +28 -0
  408. package/src/ratelimit/upstash.ts +30 -0
  409. package/src/ratelimit/wrangler.ts +70 -0
  410. package/src/reference/asyncapi.ts +2 -2
  411. package/src/reference/graphql.ts +5 -2
  412. package/src/reference/openapi.ts +21 -7
  413. package/src/reference/options.ts +51 -7
  414. package/src/reference/scalar.ts +33 -8
  415. package/src/registry/eject.ts +20 -4
  416. package/src/registry/registry.ts +7 -0
  417. package/src/search/documents.ts +26 -2
  418. package/src/search/orama-index.ts +28 -3
  419. package/src/search/pagefind-ranking.ts +39 -0
  420. package/src/search/sync/algolia.ts +15 -4
  421. package/src/search/sync/orama-cloud.ts +3 -0
  422. package/src/search/sync/typesense.ts +5 -0
  423. package/src/seo/metatags.ts +67 -0
  424. package/src/theme/chrome-icons.ts +1 -0
  425. package/src/theme/color.ts +500 -0
  426. package/src/theme/entry.ts +56 -3
  427. package/src/theme/palette.ts +121 -45
  428. package/src/translate/prompts.ts +1 -0
  429. package/src/translate/validate.ts +8 -2
  430. package/src/upgrade/upgrade.ts +4 -171
  431. package/dist/cli/chunk-364znk6q.js +0 -75
  432. package/dist/cli/chunk-364znk6q.js.map +0 -11
  433. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  434. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  435. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  436. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  437. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  438. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  439. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  440. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  441. package/docs/content/sources.mdx +0 -334
  442. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-1n228xqb.js.map} +0 -0
  443. /package/dist/cli/{chunk-35d4wj9f.js.map → chunk-2n4ps1b3.js.map} +0 -0
  444. /package/dist/cli/{chunk-e04dxsz1.js.map → chunk-4ejxbzee.js.map} +0 -0
  445. /package/dist/cli/{chunk-tzmab476.js.map → chunk-5ffywsmb.js.map} +0 -0
  446. /package/dist/cli/{chunk-2z928egk.js.map → chunk-5wj2qwn2.js.map} +0 -0
  447. /package/dist/cli/{chunk-b07cmahc.js.map → chunk-amrb6hgh.js.map} +0 -0
  448. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-cmtcnh9f.js.map} +0 -0
  449. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-e7vvk5ya.js.map} +0 -0
  450. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-gp84za0h.js.map} +0 -0
  451. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-p804f3pe.js.map} +0 -0
  452. /package/dist/cli/{chunk-8ktnccpt.js.map → chunk-ta27feev.js.map} +0 -0
  453. /package/dist/cli/{chunk-a58773jm.js.map → chunk-y4ykbark.js.map} +0 -0
  454. /package/dist/cli/{chunk-p3v96n38.js.map → chunk-y5h4r2y0.js.map} +0 -0
  455. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-yj17vs39.js.map} +0 -0
@@ -6,20 +6,29 @@ import { dirname, isAbsolute, join, relative } from "pathe";
6
6
  import type { AskRetrievalOptions } from "../ai/ask-context.ts";
7
7
  import type { AskBackend } from "../ai/ask.ts";
8
8
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
9
+ import type { CaptchaAdapter } from "../captcha/schema.ts";
10
+ import { CONSENT_CLIENT_MODULES } from "../consent/clients.ts";
9
11
  import { normalizeBasePath } from "../core/base-path.ts";
10
12
  import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
13
+ import { compileRedirects, isPatternPath } from "../core/redirect-patterns.ts";
14
+ import type { CompiledRedirect } from "../core/redirect-patterns.ts";
11
15
  import type { ResolvedConfig } from "../core/schema.ts";
12
16
  import { resolveDocsCollection } from "../core/sources/collection.ts";
13
17
  import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
14
18
  import { trimChar } from "../core/trim.ts";
15
19
  import type { ProjectContext } from "../core/types.ts";
20
+ import { hasVariables } from "../core/variables.ts";
16
21
  import { deployPassthrough } from "../deploy/adapters/types.ts";
17
22
  import { SVG_ASSET_HEADERS } from "../deploy/headers.ts";
18
23
  import { deployPlatform } from "../deploy/platforms/index.ts";
19
24
  import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
20
25
  import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
26
+ import { API_RAIL_KEY } from "../markdown/api-rail.ts";
27
+ import { VIEWS_KEY } from "../markdown/views.ts";
21
28
  import type { OgCache } from "../og/cache.ts";
22
29
  import type { OgFont, OgFontFamilies, OgGoogleFont } from "../og/card.ts";
30
+ import { RATE_LIMIT_BINDING } from "../ratelimit/cloudflare.ts";
31
+ import type { RateLimitAdapter } from "../ratelimit/schema.ts";
23
32
  import type { MixedbreadOptions } from "../search/adapters/mixedbread.ts";
24
33
  import type {
25
34
  ResolvedSearchAdapter,
@@ -188,7 +197,8 @@ export const runtimeDependencies = (options: {
188
197
  deps.push(
189
198
  ...new Set(config.reference.flatMap((adapter) => adapter.runtimeDeps)),
190
199
  ...config.search.provider.runtimeDeps,
191
- ...config.analytics.flatMap((adapter) => adapter.runtimeDeps)
200
+ ...config.analytics.flatMap((adapter) => adapter.runtimeDeps),
201
+ ...(config.consent?.runtimeDeps ?? [])
192
202
  );
193
203
  // Each content source adapter declares the SDK its fetch imports (Notion,
194
204
  // Sanity); the descriptor is the one place that knows.
@@ -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;
@@ -617,6 +635,7 @@ export const astroConfigTemplate = (options: {
617
635
  );
618
636
  const {
619
637
  askPath,
638
+ consentClientPath,
620
639
  contentRoutes,
621
640
  examplesPath,
622
641
  examplesThemePath,
@@ -687,11 +706,17 @@ export const astroConfigTemplate = (options: {
687
706
  deployment.options.base ?? "",
688
707
  new Set(contentRoutes)
689
708
  );
709
+ // Only exact redirects: Astro can't prerender a pattern's redirect pages
710
+ // (it would need every path the pattern covers), so a pattern reaches the
711
+ // dev server through the integration and each host through its own rules.
712
+ const exactRedirects = basedRedirects.filter(
713
+ (redirect) => !isPatternPath(redirect.from)
714
+ );
690
715
  const redirectsOption =
691
- basedRedirects.length > 0
716
+ exactRedirects.length > 0
692
717
  ? `\n redirects: ${JSON.stringify(
693
718
  Object.fromEntries(
694
- basedRedirects.map((redirect) => [
719
+ exactRedirects.map((redirect) => [
695
720
  redirect.from,
696
721
  { destination: redirect.to, status: redirect.status },
697
722
  ])
@@ -759,12 +784,19 @@ export const astroConfigTemplate = (options: {
759
784
  const svelteImport = needsSvelte
760
785
  ? `import svelte from "@astrojs/svelte";\n`
761
786
  : "";
787
+ // Content variables in `.mdx` are replaced before the MDX compiler parses
788
+ // the source, which reads `{{name}}` as a JavaScript expression.
789
+ const substitutesVariables = hasVariables(config.variables);
790
+ const variablesPluginEntry = substitutesVariables
791
+ ? `variablesVitePlugin(${JSON.stringify(config.variables)}), `
792
+ : "";
762
793
  const blumeImports = [
763
794
  "blumeIntegration",
764
795
  "includeHmrPlugin",
765
796
  "prerenderDepsPlugin",
766
797
  ...runtimeModuleImports,
767
798
  ...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
799
+ ...(substitutesVariables ? ["variablesVitePlugin"] : []),
768
800
  ];
769
801
  const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
770
802
 
@@ -794,6 +826,7 @@ export const astroConfigTemplate = (options: {
794
826
  deployBase,
795
827
  externalLinks: config.markdown.externalLinks,
796
828
  headingAnchors: config.markdown.headingAnchors,
829
+ variables: config.variables,
797
830
  });
798
831
  const processorOptions =
799
832
  options.generatedModulesDir === undefined
@@ -822,6 +855,7 @@ export const astroConfigTemplate = (options: {
822
855
  contentRoutes,
823
856
  ejected,
824
857
  pages,
858
+ redirects: compileRedirects(basedRedirects),
825
859
  })
826
860
  )})`
827
861
  );
@@ -885,7 +919,7 @@ ${userConfigSetup}export default defineConfig({
885
919
  // the overlap only adds memory.
886
920
  build: { concurrency: Math.min(8, availableParallelism()) },
887
921
  vite: {${viteCacheOption}
888
- plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
922
+ plugins: [${runtimeModulesPluginEntry}${variablesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
889
923
  `${context.outDir}/src/generated/includes.json`,
890
924
  ejected
891
925
  )}), prerenderDepsPlugin()],
@@ -944,6 +978,7 @@ ${userConfigSetup}export default defineConfig({
944
978
  resolve: {
945
979
  alias: {
946
980
  "blume:ask": ${configPath(askPath, ejected)},
981
+ "blume:consent-client": ${configPath(consentClientPath, ejected)},
947
982
  "blume:examples": ${configPath(examplesPath, ejected)},
948
983
  "blume:examples-theme": ${configPath(examplesThemePath, ejected)},
949
984
  "blume:features": ${configPath(featuresPath, ejected)},
@@ -1095,12 +1130,21 @@ const ASK_FALLBACK_PROMPT =
1095
1130
 
1096
1131
  /** The `ai.assistant` values the generated endpoint has to carry with it. */
1097
1132
  export interface AskEndpointOptions {
1133
+ /** `ai.assistant.captcha` — the bot check the route verifies first. */
1134
+ captcha?: CaptchaAdapter;
1098
1135
  /** `ai.assistant.cors` — origins allowed to call the route from another site. */
1099
1136
  cors?: string[];
1137
+ /** `rateLimit` — the limiter the route checks before any work. */
1138
+ rateLimit?: RateLimitAdapter | null;
1100
1139
  /** `ai.assistant.instructions` — extra system-prompt text. */
1101
1140
  instructions?: string;
1102
1141
  /** `ai.assistant.retrieval` — how much documentation each question carries. */
1103
1142
  retrieval?: AskRetrievalOptions;
1143
+ /**
1144
+ * `ai.assistant.tools`, resolved against the adapter's default: give the
1145
+ * model the docs search and read-page tools.
1146
+ */
1147
+ tools?: boolean;
1104
1148
  }
1105
1149
 
1106
1150
  /** The pieces `askEndpointTemplate` splices in for `ai.assistant.cors`. */
@@ -1132,7 +1176,7 @@ const askCorsTemplate = (cors: readonly string[] = []): AskCorsTemplate =>
1132
1176
  imports: [
1133
1177
  'import { preflightResponse, withCors } from "blume/ai/cors.ts";',
1134
1178
  ],
1135
- open: "withCors(ALLOWED_ORIGINS, async ({ request }) => {",
1179
+ open: "withCors(ALLOWED_ORIGINS, async (context) => {",
1136
1180
  setup: `
1137
1181
  const ALLOWED_ORIGINS = ${JSON.stringify(cors)};
1138
1182
 
@@ -1140,7 +1184,89 @@ export const OPTIONS: APIRoute = ({ request }) =>
1140
1184
  preflightResponse(request, ALLOWED_ORIGINS);
1141
1185
  `,
1142
1186
  }
1143
- : { close: "};", imports: [], open: "async ({ request }) => {", setup: "" };
1187
+ : { close: "};", imports: [], open: "async (context) => {", setup: "" };
1188
+
1189
+ /** The pieces a server route splices in to check `rateLimit` first. */
1190
+ interface RateLimitTemplate {
1191
+ /** The check at the top of the handler; reads `context`. */
1192
+ check: string;
1193
+ /** The runtime import, plus what the adapter's store needs. */
1194
+ imports: string[];
1195
+ /** The route's limiter, built once at module scope. */
1196
+ setup: string;
1197
+ }
1198
+
1199
+ /**
1200
+ * `rateLimit` for one server route: a limiter built at module scope from the
1201
+ * configured adapter, checked against every request, keyed by the reader's
1202
+ * address and `scope` so each route keeps its own budget (see
1203
+ * `ratelimit/runtime.ts`). Upstash reads its secrets through `getSecret`;
1204
+ * Cloudflare's binding comes from the Worker's env. Nothing at all when
1205
+ * rate limiting is off.
1206
+ */
1207
+ export const rateLimitTemplate = (
1208
+ adapter: RateLimitAdapter | null | undefined,
1209
+ scope: string
1210
+ ): RateLimitTemplate => {
1211
+ if (!adapter) {
1212
+ return { check: "", imports: [], setup: "" };
1213
+ }
1214
+ const imports = [
1215
+ 'import { createLimiter, rateLimited } from "blume/ratelimit/runtime.ts";',
1216
+ ];
1217
+ let runtime = "";
1218
+ if (adapter.kind === "upstash") {
1219
+ imports.push('import { getSecret } from "astro:env/server";');
1220
+ runtime = ", { secret: getSecret }";
1221
+ } else if (adapter.kind === "cloudflare") {
1222
+ imports.push(
1223
+ "// @ts-ignore `cloudflare:workers` is typed once `wrangler types` has run.",
1224
+ 'import { env } from "cloudflare:workers";'
1225
+ );
1226
+ runtime = `, { binding: Reflect.get(env, ${JSON.stringify(RATE_LIMIT_BINDING)}) }`;
1227
+ }
1228
+ return {
1229
+ check: ` const limited = await rateLimited(limiter, context, ${JSON.stringify(scope)});
1230
+ if (limited) {
1231
+ return limited;
1232
+ }
1233
+ `,
1234
+ imports,
1235
+ setup: `\nconst limiter = createLimiter(${JSON.stringify(adapter)}${runtime});\n`,
1236
+ };
1237
+ };
1238
+
1239
+ /**
1240
+ * `ai.assistant.captcha` for the ask route: verify the question's token with
1241
+ * the provider before the model runs (see `captcha/verify.ts`). A missing
1242
+ * secret answers with the same "not configured" notice as a missing
1243
+ * provider key, which the panel shows as is; a failed check answers `403`.
1244
+ */
1245
+ const askCaptchaTemplate = (adapter?: CaptchaAdapter): RateLimitTemplate => {
1246
+ if (!adapter) {
1247
+ return { check: "", imports: [], setup: "" };
1248
+ }
1249
+ const [secret = ""] = adapter.requiredSecrets;
1250
+ return {
1251
+ check: ` const captchaSecret = getSecret(${JSON.stringify(secret)});
1252
+ if (!captchaSecret) {
1253
+ return new Response(
1254
+ ${JSON.stringify(`The assistant is not configured: set ${secret}.`)},
1255
+ { status: 503 }
1256
+ );
1257
+ }
1258
+ const captchaToken =
1259
+ typeof body.captcha === "string" ? body.captcha : undefined;
1260
+ if (!(await verifyCaptcha(CAPTCHA, captchaToken, context, { secret: captchaSecret }))) {
1261
+ return new Response("Verification failed: the bot check didn't pass.", {
1262
+ status: 403,
1263
+ });
1264
+ }
1265
+ `,
1266
+ imports: ['import { verifyCaptcha } from "blume/captcha/verify.ts";'],
1267
+ setup: `\nconst CAPTCHA = ${JSON.stringify(adapter)};\n`,
1268
+ };
1269
+ };
1144
1270
 
1145
1271
  /**
1146
1272
  * Largest request body the assistant route reads: 64 KB, well above the
@@ -1174,6 +1300,8 @@ export const askEndpointTemplate = (
1174
1300
  ): string => {
1175
1301
  const { instructions, retrieval } = options ?? {};
1176
1302
  const { grounded } = backend;
1303
+ // The tools read the grounding snapshot, so an ungrounded backend has none.
1304
+ const tools = grounded && Boolean(options?.tools);
1177
1305
  const fallbackPrompt = instructions
1178
1306
  ? `${ASK_FALLBACK_PROMPT}\n\n${instructions}`
1179
1307
  : ASK_FALLBACK_PROMPT;
@@ -1201,12 +1329,32 @@ export const askEndpointTemplate = (
1201
1329
  if (retrieval) {
1202
1330
  groundFields.push(`retrieval: ${JSON.stringify(retrieval)}`);
1203
1331
  }
1332
+ if (tools) {
1333
+ groundFields.push("tools: true");
1334
+ }
1204
1335
  const groundOptions =
1205
1336
  groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
1206
1337
  setup += `\nconst ground = createAskContext(askData${groundOptions});\n`;
1338
+ if (tools) {
1339
+ imports.push(
1340
+ 'import { stepCountIs } from "ai";',
1341
+ 'import { ASK_MAX_STEPS, createAskTools } from "blume/ai/ask-tools.ts";'
1342
+ );
1343
+ setup += "const askTools = createAskTools(askData);\n";
1344
+ }
1207
1345
  }
1208
1346
  const cors = askCorsTemplate(options?.cors);
1209
1347
  imports.push(...cors.imports);
1348
+ const limit = rateLimitTemplate(options?.rateLimit, "ask");
1349
+ for (const line of limit.imports) {
1350
+ if (!imports.includes(line)) {
1351
+ imports.push(line);
1352
+ }
1353
+ }
1354
+ setup += limit.setup;
1355
+ const captcha = askCaptchaTemplate(options?.captcha);
1356
+ imports.push(...captcha.imports);
1357
+ setup += captcha.setup;
1210
1358
  // Validate the client-supplied body and cap its size. The endpoint is
1211
1359
  // unauthenticated, so bounding message count/length limits how much a caller
1212
1360
  // can spend against the model per request, and restricting roles to
@@ -1215,7 +1363,8 @@ export const askEndpointTemplate = (
1215
1363
  // limiter (or your provider's limits) for stronger protection. The body is
1216
1364
  // read under a 64 KB cap before any parsing, since a self-hosted Node server
1217
1365
  // would otherwise buffer an arbitrarily large POST in memory first.
1218
- const validate = ` const text = await readCappedText(request, ${ASK_BODY_LIMIT_BYTES});
1366
+ const validate = ` const { request } = context;
1367
+ ${limit.check} const text = await readCappedText(request, ${ASK_BODY_LIMIT_BYTES});
1219
1368
  if (text === undefined) {
1220
1369
  return new Response("Request too large: the body must be at most 64 KB.", {
1221
1370
  status: 413,
@@ -1273,6 +1422,11 @@ export const askEndpointTemplate = (
1273
1422
  ? "instructions"
1274
1423
  : `instructions:\n ${JSON.stringify(fallbackPrompt)}`,
1275
1424
  "messages",
1425
+ // The model may search and read pages, then answer, within one request:
1426
+ // tool calls run here on the server and only the text reaches the reader.
1427
+ ...(tools
1428
+ ? ["tools: askTools(body.page)", "stopWhen: stepCountIs(ASK_MAX_STEPS)"]
1429
+ : []),
1276
1430
  ...backend.template.fields,
1277
1431
  ];
1278
1432
  // The request's signal aborts when the reader closes the panel mid-answer
@@ -1294,7 +1448,7 @@ ${call}`
1294
1448
  const handler = `export const POST: APIRoute = ${cors.open}
1295
1449
  ${validate}
1296
1450
  ${keyCheck}
1297
- try {
1451
+ ${captcha.check} try {
1298
1452
  ${stream}
1299
1453
  return createTextStreamResponse({
1300
1454
  stream: toTextStream({ stream: result.stream }),
@@ -1339,9 +1493,11 @@ const { strings } = Astro.props;
1339
1493
  ---
1340
1494
 
1341
1495
  <Assistant
1496
+ captcha={data.config.assistant?.captcha ?? undefined}
1342
1497
  endpoint={data.config.assistant?.endpoint ?? undefined}
1343
1498
  strings={strings ?? data.ui.assistant}
1344
1499
  suggestions={data.config.assistant?.suggestions ?? []}
1500
+ support={data.config.assistant?.support ?? undefined}
1345
1501
  />
1346
1502
  `
1347
1503
  : `---
@@ -1441,6 +1597,36 @@ ${
1441
1597
  }
1442
1598
  `;
1443
1599
 
1600
+ /**
1601
+ * Generate `.blume/src/generated/consent-client.ts`, behind the
1602
+ * `blume:consent-client` alias: the configured consent adapter's browser
1603
+ * module (see `consent/clients.ts`), started by `ConsentHead.astro` with the
1604
+ * adapter's options baked in. Only that adapter's module is referenced, so a
1605
+ * site bundles no other adapter's code. An adapter without one (a hosted
1606
+ * manager), or no `consent` at all, gets a no-op so the alias always resolves.
1607
+ */
1608
+ export const consentClientTemplate = (
1609
+ consent: ResolvedConfig["consent"]
1610
+ ): string => {
1611
+ const module = consent ? CONSENT_CLIENT_MODULES.get(consent.kind) : undefined;
1612
+ if (!(consent && module)) {
1613
+ return `// Generated by Blume. Do not edit.
1614
+ // The consent adapter has no browser module, or consent is off.
1615
+ export const startConsentClient = (): void => {};
1616
+ `;
1617
+ }
1618
+ return `// Generated by Blume. Do not edit.
1619
+ // The \`${consent.kind}\` consent adapter's browser module, with its options.
1620
+ import { start } from ${JSON.stringify(module)};
1621
+
1622
+ export const startConsentClient = (
1623
+ consent: Parameters<typeof start>[0]
1624
+ ): void => {
1625
+ start(consent, ${JSON.stringify(consent.options)});
1626
+ };
1627
+ `;
1628
+ };
1629
+
1444
1630
  /**
1445
1631
  * Generate `.blume/src/generated/search-client.ts` — the provider-specific
1446
1632
  * loader the `<Search>` component lazy-imports via the `blume:search-client`
@@ -1493,13 +1679,19 @@ export const createSearch = () => create({ url });
1493
1679
  * content was synced (see the Mixedbread sync step / \`mxbai vs sync\`).
1494
1680
  */
1495
1681
  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";
1682
+ options: MixedbreadOptions,
1683
+ rateLimit?: RateLimitAdapter | null
1684
+ ): string => {
1685
+ const limit = rateLimitTemplate(rateLimit, "search");
1686
+ const imports = [
1687
+ 'import type { APIRoute } from "astro";',
1688
+ 'import { getSecret } from "astro:env/server";',
1689
+ 'import Mixedbread from "@mixedbread/sdk";',
1690
+ 'import { readCappedText } from "blume/core/request-body.ts";',
1691
+ ...limit.imports.filter((line) => !line.includes('"astro:env/server"')),
1692
+ ];
1693
+ return `// Generated by Blume. Do not edit.
1694
+ ${imports.join("\n")}
1503
1695
 
1504
1696
  export const prerender = false;
1505
1697
 
@@ -1507,9 +1699,10 @@ const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" })
1507
1699
  const OPTIONS = ${JSON.stringify(options)};
1508
1700
  // Every option besides the store reaches the search call verbatim.
1509
1701
  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
1702
+ ${limit.setup}
1703
+ export const POST: APIRoute = async (context) => {
1704
+ const { request } = context;
1705
+ ${limit.check} // A search body is one short query: read it under a 16 KB cap, so a
1513
1706
  // self-hosted server never buffers an arbitrarily large POST first.
1514
1707
  const text = await readCappedText(request, 16_384);
1515
1708
  if (text === undefined) {
@@ -1554,6 +1747,7 @@ export const POST: APIRoute = async ({ request }) => {
1554
1747
  });
1555
1748
  };
1556
1749
  `;
1750
+ };
1557
1751
 
1558
1752
  /**
1559
1753
  * Generate the raw-Markdown endpoints (`[...slug].md.ts` and `[...slug].mdx.ts`).
@@ -1756,17 +1950,26 @@ export const ALL: APIRoute = ({ request }) => handler(request);
1756
1950
  * client-side data: that is the whole trust boundary keeping the endpoint from
1757
1951
  * being an open proxy onto the deployment's own network.
1758
1952
  */
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";
1953
+ export const playgroundProxyTemplate = (
1954
+ origins: string[],
1955
+ rateLimit?: RateLimitAdapter | null
1956
+ ): string => {
1957
+ const limit = rateLimitTemplate(rateLimit, "api-proxy");
1958
+ const route = limit.check
1959
+ ? `export const ALL: APIRoute = async (context) => {
1960
+ ${limit.check} return handler(context.request);
1961
+ };`
1962
+ : "export const ALL: APIRoute = ({ request }) => handler(request);";
1963
+ return `// Generated by Blume. Do not edit.
1964
+ ${['import type { APIRoute } from "astro";', 'import { createPlaygroundProxyHandler } from "blume/openapi/proxy.ts";', ...limit.imports].join("\n")}
1763
1965
 
1764
1966
  export const prerender = false;
1765
1967
 
1766
1968
  const handler = createPlaygroundProxyHandler(${JSON.stringify(origins)});
1767
-
1768
- export const ALL: APIRoute = ({ request }) => handler(request);
1969
+ ${limit.setup}
1970
+ ${route}
1769
1971
  `;
1972
+ };
1770
1973
 
1771
1974
  /** Generate a prerendered endpoint that serves a fixed JSON payload. */
1772
1975
  export const staticJsonEndpointTemplate = <Payload extends object>(
@@ -2090,6 +2293,9 @@ const contentComponentsSource = (mathEnabled: boolean) => {
2090
2293
  return {
2091
2294
  imports: `import Accordion from "blume/components/content/Accordion.astro";
2092
2295
  import AccordionItem from "blume/components/content/AccordionItem.astro";
2296
+ import ApiEndpoint from "blume/components/content/ApiEndpoint.astro";
2297
+ import ApiPlayground from "blume/components/content/ApiPlayground.astro";
2298
+ import ApiRail from "blume/components/content/ApiRail.astro";
2093
2299
  import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
2094
2300
  import Badge from "blume/components/content/Badge.astro";
2095
2301
  import Callout from "blume/components/content/Callout.astro";
@@ -2109,7 +2315,11 @@ import FileTree from "blume/components/content/FileTree.astro";
2109
2315
  import Frame from "blume/components/content/Frame.astro";
2110
2316
  import GithubInfo from "blume/components/content/GithubInfo.astro";
2111
2317
  import Panel from "blume/components/content/Panel.astro";
2318
+ import ParamField from "blume/components/content/ParamField.astro";
2112
2319
  import Prompt from "blume/components/content/Prompt.astro";
2320
+ import RequestExample from "blume/components/content/RequestExample.astro";
2321
+ import ResponseExample from "blume/components/content/ResponseExample.astro";
2322
+ import ResponseField from "blume/components/content/ResponseField.astro";
2113
2323
  import Step from "blume/components/content/Step.astro";
2114
2324
  import Steps from "blume/components/content/Steps.astro";
2115
2325
  import Tab from "blume/components/content/Tab.astro";
@@ -2120,10 +2330,13 @@ import TreeRoot from "blume/components/content/Tree.astro";
2120
2330
  import TreeFile from "blume/components/content/TreeFile.astro";
2121
2331
  import TreeFolder from "blume/components/content/TreeFolder.astro";
2122
2332
  import TypeTable from "blume/components/content/TypeTable.astro";
2333
+ import View from "blume/components/content/View.astro";
2123
2334
  import Visibility from "blume/components/content/Visibility.astro";
2124
2335
  import YouTube from "blume/components/content/YouTube.astro";
2125
2336
  import Icon from "blume/components/Icon.astro";
2126
2337
  import LocaleLinks from "blume/components/layout/LocaleLinks.astro";
2338
+ import NarrationPlayer from "blume/components/layout/NarrationPlayer.astro";
2339
+ import ViewSwitcher from "blume/components/content/ViewSwitcher.astro";
2127
2340
  import ApiOverview from "blume/components/openapi/ApiOverview.astro";
2128
2341
  import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
2129
2342
  import Operation from "blume/components/openapi/Operation.astro";
@@ -2131,7 +2344,10 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2131
2344
  map: `{
2132
2345
  Accordion,
2133
2346
  AccordionItem,
2347
+ ApiEndpoint,
2134
2348
  ApiOverview,
2349
+ ApiPlayground,
2350
+ ApiRail,
2135
2351
  ApiTagOperations,
2136
2352
  AutoTypeTable,
2137
2353
  Badge,
@@ -2152,7 +2368,11 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2152
2368
  Icon,
2153
2369
  Operation,
2154
2370
  Panel,
2371
+ ParamField,
2155
2372
  Prompt,
2373
+ RequestExample,
2374
+ ResponseExample,
2375
+ ResponseField,
2156
2376
  Step,
2157
2377
  Steps,
2158
2378
  Tab,
@@ -2161,6 +2381,7 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2161
2381
  Tooltip,
2162
2382
  Tree,
2163
2383
  TypeTable,
2384
+ View,
2164
2385
  Visibility,
2165
2386
  YouTube,
2166
2387
  ${mathEntry}...userMdx,
@@ -2191,6 +2412,7 @@ export const catchAllPageTemplate = (options: {
2191
2412
  return `---
2192
2413
  // Generated by Blume. Do not edit.
2193
2414
  import { getEntry, render } from "astro:content";
2415
+ import { pageModeLayout } from "blume/core/page-modes.ts";
2194
2416
  import type { CollectionKey } from "astro:content";
2195
2417
  import RootLayout from "blume/components/layout/RootLayout.astro";
2196
2418
  import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
@@ -2242,6 +2464,13 @@ const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render
2242
2464
  // frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
2243
2465
  // counts: \`frontmatter.extend\` can declare the same key, and on a page with
2244
2466
  // no headings that user-supplied value would pass straight through.
2467
+ // A page written in \`<View>\` blocks gets their picker above the content;
2468
+ // the views plugin lists them through the render's frontmatter (see
2469
+ // markdown/views.ts), and only a list of titled entries counts.
2470
+ const viewsRaw = remarkPluginFrontmatter?.${VIEWS_KEY};
2471
+ const views = Array.isArray(viewsRaw)
2472
+ ? viewsRaw.filter((view) => typeof view?.title === "string")
2473
+ : [];
2245
2474
  const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
2246
2475
  const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
2247
2476
  const headings =
@@ -2320,6 +2549,14 @@ const htmlLang = i18n ? locale : "en";
2320
2549
  // follows that language — not the (mirrored) page locale.
2321
2550
  const contentLocale =
2322
2551
  fallback && i18n?.fallbackLocale ? i18n.fallbackLocale : locale;
2552
+ // "Listen to this page", unless the page opts out. The spoken cues are read
2553
+ // between the content's own sentences, so they come from the content's
2554
+ // language (a fallback page's), while the player's labels follow the page.
2555
+ // A \`custom\` or \`frame\` page brings its own heading: no title, description,
2556
+ // or narration player above its content.
2557
+ const pageChrome = pageModeLayout(frontmatter.mode).chrome;
2558
+ const narration = pageChrome && frontmatter.narration ? data.config.narration : null;
2559
+ const narrationCues = (i18n ? (data.uiByLocale[contentLocale] ?? data.ui) : data.ui).narration;
2323
2560
  const contentDir = i18n
2324
2561
  ? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
2325
2562
  : "ltr";
@@ -2503,6 +2740,10 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2503
2740
  canonical={canonical}
2504
2741
  editUrl={editUrl}
2505
2742
  feedback={data.config.feedback}
2743
+ feedbackComments={data.config.feedbackComments}
2744
+ pagination={frontmatter.pagination}
2745
+ related={frontmatter.related}
2746
+ search={frontmatter.search}
2506
2747
  exportPdf={${options.exportPdf}}
2507
2748
  exportEpub={${options.exportEpub}}${
2508
2749
  options.navFragments
@@ -2515,13 +2756,25 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2515
2756
  discovery={data.config.discovery}
2516
2757
  siteUrl={data.config.site}
2517
2758
  pageType={frontmatter.type}
2759
+ apiRail={remarkPluginFrontmatter?.${API_RAIL_KEY} === true}
2760
+ pageMode={frontmatter.mode}
2518
2761
  published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
2519
2762
  lastModified={lastModified}
2520
2763
  noindex={effectiveNoindex}
2521
2764
  structuredDataEnabled={data.config.structuredData}
2522
2765
  >
2523
- <h1>{title}</h1>
2524
- {frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
2766
+ {pageChrome && <h1>{title}</h1>}
2767
+ {pageChrome && frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
2768
+ {narration && (
2769
+ <NarrationPlayer
2770
+ audioBase={narration.generated ? withMountedBase("/blume-narration/audio/") : undefined}
2771
+ cues={narrationCues}
2772
+ lang={i18n ? contentLocale : htmlLang}
2773
+ manifest={narration.generated ? withMountedBase(encodeURI(\`/blume-narration/\${route === "/" ? "index" : route.slice(1)}.json\`)) : undefined}
2774
+ strings={ui.narration}
2775
+ />
2776
+ )}
2777
+ {views.length > 1 && <ViewSwitcher label={ui.content.selectView} views={views} />}
2525
2778
  <LocaleLinks locale={locale}>
2526
2779
  <Content components={components} />
2527
2780
  </LocaleLinks>
@@ -0,0 +1,31 @@
1
+ import { substituteVariables } from "../core/variables.ts";
2
+ import type { ContentVariables } from "../core/variables.ts";
3
+
4
+ /**
5
+ * The MDX half of content variables (see `core/variables.ts`): a Vite plugin
6
+ * that replaces `{{name}}` in an `.mdx` module's source before the MDX
7
+ * compiler reads it. It has to run first: the compiler parses `{{version}}`
8
+ * as a JavaScript expression (a `ReferenceError` at render) and rejects
9
+ * `{{api-url}}` outright, so no plugin after parsing could see either.
10
+ */
11
+ /** The Vite plugin slice this needs (structurally typed, like Blume's other plugins). */
12
+ export interface VariablesVitePlugin {
13
+ enforce: "pre";
14
+ name: string;
15
+ transform: (code: string, id: string) => { code: string; map: null } | null;
16
+ }
17
+
18
+ export const variablesVitePlugin = (
19
+ variables: ContentVariables
20
+ ): VariablesVitePlugin => ({
21
+ enforce: "pre",
22
+ name: "blume:variables",
23
+ transform(code, id) {
24
+ const [path = ""] = id.split("?");
25
+ if (!path.endsWith(".mdx")) {
26
+ return null;
27
+ }
28
+ const next = substituteVariables(code, variables);
29
+ return next === code ? null : { code: next, map: null };
30
+ },
31
+ });
@@ -498,6 +498,25 @@ export const CHECKS = [
498
498
  title: "Hreflang group has a language conflict",
499
499
  },
500
500
 
501
+ // Accessibility. Theme colors are one per site, not per page: these run
502
+ // on the config, the way the build resolves it.
503
+ {
504
+ category: "accessibility",
505
+ fix: "Pick a darker shade for light mode or a lighter one for dark mode, per mode with `theme.accent: { light, dark }`.",
506
+ id: "BLUME_AUDIT_THEME_CONTRAST_LOW",
507
+ severity: "warning",
508
+ tier: "static",
509
+ title: "Theme colors miss WCAG AA contrast",
510
+ },
511
+ {
512
+ category: "accessibility",
513
+ fix: "Write the color as hex, `rgb()`, `hsl()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, or a named color so its contrast can be checked.",
514
+ id: "BLUME_AUDIT_THEME_COLOR_UNCHECKED",
515
+ severity: "info",
516
+ tier: "static",
517
+ title: "Theme color can't be checked for contrast",
518
+ },
519
+
501
520
  // Assets
502
521
  {
503
522
  category: "assets",