blume 2.0.2 → 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 (601) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +113 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-6k4ftwze.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-6k4ftwze.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-zp79m0ts.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-ddndchfr.js → chunk-2f3ndkpw.js} +57 -29
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-fsmrqk8a.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-ppzjqwx2.js → chunk-2n4ps1b3.js} +17 -18
  12. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-2n4ps1b3.js.map} +3 -3
  13. package/dist/cli/{chunk-5r8g91qn.js → chunk-43zcf4gz.js} +398 -159
  14. package/dist/cli/chunk-43zcf4gz.js.map +36 -0
  15. package/dist/cli/chunk-4ejxbzee.js +39 -0
  16. package/dist/cli/chunk-4ejxbzee.js.map +10 -0
  17. package/dist/cli/{chunk-hr8ne106.js → chunk-4h8z9vv5.js} +296 -77
  18. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  19. package/dist/cli/{chunk-k7pj68a8.js → chunk-5cg2rzgf.js} +18 -16
  20. package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5cg2rzgf.js.map} +2 -2
  21. package/dist/cli/{chunk-h2ez8dzb.js → chunk-5ffywsmb.js} +4 -4
  22. package/dist/cli/{chunk-g698a744.js → chunk-5wj2qwn2.js} +5 -5
  23. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  24. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  25. package/dist/cli/{chunk-3yce002v.js → chunk-713mngdq.js} +15 -8
  26. package/dist/cli/chunk-713mngdq.js.map +14 -0
  27. package/dist/cli/{chunk-91ws1n6j.js → chunk-9geymqns.js} +17 -15
  28. package/dist/cli/{chunk-91ws1n6j.js.map → chunk-9geymqns.js.map} +2 -2
  29. package/dist/cli/{chunk-6dtt0zfn.js → chunk-9xr4dgee.js} +27 -18
  30. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  31. package/dist/cli/{chunk-sqw4ekg1.js → chunk-amrb6hgh.js} +2 -2
  32. package/dist/cli/chunk-cm89scdv.js +443 -0
  33. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  34. package/dist/cli/{chunk-wdrt2k2v.js → chunk-cmtcnh9f.js} +90 -26
  35. package/dist/cli/chunk-cmtcnh9f.js.map +10 -0
  36. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  37. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  38. package/dist/cli/{chunk-7mbqtmgb.js → chunk-e7vvk5ya.js} +21 -12
  39. package/dist/cli/chunk-e7vvk5ya.js.map +10 -0
  40. package/dist/cli/{chunk-qkb5a8sa.js → chunk-g2z9x01v.js} +6 -98
  41. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  42. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  43. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  44. package/dist/cli/{chunk-h7k3nq3v.js → chunk-gp84za0h.js} +2 -2
  45. package/dist/cli/{chunk-hqp2ajnh.js → chunk-h2a48yhd.js} +195 -73
  46. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  47. package/dist/cli/{chunk-pbg5a4s3.js → chunk-jnwj5kjj.js} +71 -47
  48. package/dist/cli/chunk-jnwj5kjj.js.map +19 -0
  49. package/dist/cli/chunk-kwrg63tm.js +31 -0
  50. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  51. package/dist/cli/{chunk-8g8ytmgx.js → chunk-nd1821sk.js} +21 -19
  52. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-nd1821sk.js.map} +2 -2
  53. package/dist/cli/{chunk-w4bxdvsa.js → chunk-ntv4qrt7.js} +17 -15
  54. package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-ntv4qrt7.js.map} +2 -2
  55. package/dist/cli/{chunk-bfwp9vp6.js → chunk-p804f3pe.js} +7 -7
  56. package/dist/cli/{chunk-4e9b9ra6.js → chunk-py62fewh.js} +6 -6
  57. package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-py62fewh.js.map} +1 -1
  58. package/dist/cli/chunk-q3wxxq39.js +126 -0
  59. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  60. package/dist/cli/{chunk-j85scx15.js → chunk-rekqz4qg.js} +142 -46
  61. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  62. package/dist/cli/{chunk-00gs3wqs.js → chunk-ta27feev.js} +1 -1
  63. package/dist/cli/{chunk-v6ya5kcb.js → chunk-xkz5xbtv.js} +4031 -1132
  64. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  65. package/dist/cli/{chunk-bbnwccaz.js → chunk-y4ykbark.js} +2 -2
  66. package/dist/cli/{chunk-n1yg3tj3.js → chunk-y5h4r2y0.js} +6 -6
  67. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-y5h4r2y0.js.map} +3 -3
  68. package/dist/cli/{chunk-nfcyttvj.js → chunk-yj17vs39.js} +9 -9
  69. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-yj17vs39.js.map} +1 -1
  70. package/dist/cli/{chunk-7vtckvaw.js → chunk-yxmk4pec.js} +16 -14
  71. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-yxmk4pec.js.map} +2 -2
  72. package/dist/cli/{chunk-gs7r695n.js → chunk-zccbjcj0.js} +22 -8
  73. package/dist/cli/{chunk-gs7r695n.js.map → chunk-zccbjcj0.js.map} +3 -3
  74. package/dist/cli/{chunk-xh43dwgw.js → chunk-zf748avv.js} +415 -225
  75. package/dist/cli/chunk-zf748avv.js.map +37 -0
  76. package/dist/cli/{chunk-d1v5rhy0.js → chunk-zhvd6adm.js} +16 -14
  77. package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-zhvd6adm.js.map} +2 -2
  78. package/dist/cli/index.js +28 -20
  79. package/dist/cli/index.js.map +5 -5
  80. package/dist/types/ai/agent-surface.d.ts +8 -7
  81. package/dist/types/ai/ai-catalog.d.ts +7 -0
  82. package/dist/types/ai/ask-context.d.ts +22 -0
  83. package/dist/types/ai/ask.d.ts +271 -58
  84. package/dist/types/ai/index.d.ts +2 -2
  85. package/dist/types/ai/link-headers.d.ts +8 -1
  86. package/dist/types/ai/openapi-components.d.ts +5 -2
  87. package/dist/types/ai/relative-links.d.ts +9 -7
  88. package/dist/types/ai/site-skill.d.ts +26 -0
  89. package/dist/types/ai/skills.d.ts +20 -1
  90. package/dist/types/ai/tar.d.ts +1 -3
  91. package/dist/types/analytics/index.d.ts +2 -0
  92. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  93. package/dist/types/analytics/schema.d.ts +14 -0
  94. package/dist/types/astro/integration.d.ts +11 -2
  95. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  96. package/dist/types/captcha/index.d.ts +28 -0
  97. package/dist/types/captcha/schema.d.ts +29 -0
  98. package/dist/types/captcha/turnstile.d.ts +28 -0
  99. package/dist/types/components/content/api-field.d.ts +59 -0
  100. package/dist/types/components/content/api-page.d.ts +95 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/helpers.d.ts +137 -0
  103. package/dist/types/components/openapi/security.d.ts +85 -0
  104. package/dist/types/consent/ethyca.d.ts +48 -0
  105. package/dist/types/consent/index.d.ts +37 -0
  106. package/dist/types/consent/native.d.ts +28 -0
  107. package/dist/types/consent/osano.d.ts +42 -0
  108. package/dist/types/consent/schema.d.ts +110 -0
  109. package/dist/types/core/base-path.d.ts +21 -7
  110. package/dist/types/core/config-input.d.ts +196 -27
  111. package/dist/types/core/data.d.ts +49 -4
  112. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  113. package/dist/types/core/footer.d.ts +8 -0
  114. package/dist/types/core/heading-markers.d.ts +5 -7
  115. package/dist/types/core/i18n-ui.d.ts +86 -0
  116. package/dist/types/core/i18n.d.ts +2 -0
  117. package/dist/types/core/includes.d.ts +8 -2
  118. package/dist/types/core/last-modified.d.ts +10 -0
  119. package/dist/types/core/localizable.d.ts +7 -0
  120. package/dist/types/core/meta.d.ts +8 -0
  121. package/dist/types/core/page-modes.d.ts +29 -0
  122. package/dist/types/core/redirect-patterns.d.ts +144 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +558 -56
  125. package/dist/types/core/sources/lower.d.ts +29 -16
  126. package/dist/types/core/sources/normalize.d.ts +13 -1
  127. package/dist/types/core/sources/read.d.ts +5 -0
  128. package/dist/types/core/sources/watch.d.ts +12 -5
  129. package/dist/types/core/standard-schema.d.ts +5 -0
  130. package/dist/types/core/types.d.ts +1 -1
  131. package/dist/types/core/variables.d.ts +42 -0
  132. package/dist/types/deploy/artifacts.d.ts +6 -4
  133. package/dist/types/deploy/headers.d.ts +5 -0
  134. package/dist/types/deploy/node-headers.d.ts +13 -2
  135. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  136. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  137. package/dist/types/deploy/platforms/types.d.ts +8 -0
  138. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  139. package/dist/types/deploy/redirects.d.ts +61 -22
  140. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  141. package/dist/types/markdown/code-title.d.ts +17 -0
  142. package/dist/types/markdown/directives.d.ts +62 -0
  143. package/dist/types/markdown/features.d.ts +24 -2
  144. package/dist/types/markdown/mdast.d.ts +63 -0
  145. package/dist/types/narration/build.d.ts +70 -0
  146. package/dist/types/narration/provider.d.ts +26 -0
  147. package/dist/types/narration/script.d.ts +154 -0
  148. package/dist/types/openapi/asyncapi.d.ts +4 -2
  149. package/dist/types/openapi/model.d.ts +12 -6
  150. package/dist/types/openapi/overlay.d.ts +54 -0
  151. package/dist/types/openapi/parse.d.ts +13 -0
  152. package/dist/types/openapi/references.d.ts +14 -4
  153. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  154. package/dist/types/ratelimit/index.d.ts +27 -0
  155. package/dist/types/ratelimit/memory.d.ts +36 -0
  156. package/dist/types/ratelimit/schema.d.ts +111 -0
  157. package/dist/types/ratelimit/upstash.d.ts +21 -0
  158. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  159. package/dist/types/reference/asyncapi.d.ts +10 -10
  160. package/dist/types/reference/graphql.d.ts +13 -10
  161. package/dist/types/reference/openapi.d.ts +33 -12
  162. package/dist/types/reference/options.d.ts +45 -5
  163. package/dist/types/reference/scalar.d.ts +13 -0
  164. package/dist/types/reference/schema.d.ts +60 -36
  165. package/dist/types/search/documents.d.ts +12 -1
  166. package/dist/types/search/orama-index.d.ts +4 -0
  167. package/dist/types/seo/metatags.d.ts +20 -0
  168. package/docs/02-deployment.mdx +44 -6
  169. package/docs/03-upgrading.mdx +4 -4
  170. package/docs/04-migrating.mdx +1 -1
  171. package/docs/08-faq.mdx +1 -1
  172. package/docs/advanced/changelog.mdx +2 -2
  173. package/docs/advanced/custom-pages.mdx +3 -3
  174. package/docs/advanced/skills.mdx +14 -0
  175. package/docs/cli/audit.mdx +38 -2
  176. package/docs/cli/evals.mdx +3 -3
  177. package/docs/cli/index.mdx +1 -0
  178. package/docs/cli/translate.mdx +3 -3
  179. package/docs/cli/version.mdx +1 -1
  180. package/docs/configuration/analytics.mdx +22 -1
  181. package/docs/configuration/assistant.mdx +188 -36
  182. package/docs/configuration/consent.mdx +100 -0
  183. package/docs/configuration/customization.mdx +6 -4
  184. package/docs/configuration/index.mdx +63 -5
  185. package/docs/configuration/meta.ts +3 -0
  186. package/docs/configuration/narration.mdx +119 -0
  187. package/docs/configuration/rate-limiting.mdx +69 -0
  188. package/docs/configuration/search.mdx +30 -2
  189. package/docs/configuration/theming.mdx +3 -1
  190. package/docs/content/components.mdx +107 -1
  191. package/docs/content/frontmatter.mdx +77 -1
  192. package/docs/content/i18n.mdx +40 -3
  193. package/docs/content/includes.mdx +16 -0
  194. package/docs/content/index.mdx +4 -2
  195. package/docs/content/islands.mdx +1 -1
  196. package/docs/content/meta.mdx +4 -1
  197. package/docs/content/meta.ts +1 -0
  198. package/docs/content/navigation.mdx +28 -6
  199. package/docs/content/sources/contentful.mdx +32 -0
  200. package/docs/content/sources/custom.mdx +39 -0
  201. package/docs/content/sources/github-releases.mdx +31 -0
  202. package/docs/content/sources/index.mdx +91 -0
  203. package/docs/content/sources/meta.ts +16 -0
  204. package/docs/content/sources/notion.mdx +32 -0
  205. package/docs/content/sources/obsidian.mdx +37 -0
  206. package/docs/content/sources/payload.mdx +30 -0
  207. package/docs/content/sources/remote-mdx.mdx +40 -0
  208. package/docs/content/sources/sanity.mdx +29 -0
  209. package/docs/content/sources/strapi.mdx +31 -0
  210. package/docs/content/syntax.mdx +81 -2
  211. package/docs/content/variables.mdx +41 -0
  212. package/docs/content/versioning.mdx +1 -1
  213. package/docs/discoverability/agent-discovery.mdx +30 -3
  214. package/docs/discoverability/index.mdx +3 -2
  215. package/docs/discoverability/llms-txt.mdx +1 -1
  216. package/docs/discoverability/markdown.mdx +2 -2
  217. package/docs/discoverability/metadata.mdx +17 -0
  218. package/docs/discoverability/open-graph.mdx +5 -3
  219. package/docs/discoverability/rss.mdx +1 -1
  220. package/docs/references/api-pages.mdx +74 -0
  221. package/docs/references/asyncapi.mdx +2 -2
  222. package/docs/references/graphql.mdx +3 -3
  223. package/docs/references/meta.ts +1 -1
  224. package/docs/references/openapi.mdx +106 -5
  225. package/docs/references/scalar.mdx +3 -1
  226. package/package.json +36 -1
  227. package/skills/blume/SKILL.md +3 -0
  228. package/skills/blume-migrate/SKILL.md +5 -5
  229. package/skills/blume-migrate/references/docusaurus.md +3 -3
  230. package/skills/blume-migrate/references/fumadocs.md +1 -1
  231. package/skills/blume-migrate/references/mintlify.md +37 -19
  232. package/skills/blume-migrate/references/nextra.md +3 -3
  233. package/skills/blume-migrate/references/starlight.md +2 -1
  234. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +67 -16
  235. package/skills/blume-write-skill/SKILL.md +82 -0
  236. package/src/ai/agent-readability.ts +4 -3
  237. package/src/ai/agent-surface.ts +9 -7
  238. package/src/ai/ai-catalog.ts +12 -5
  239. package/src/ai/api/handlers.ts +2 -2
  240. package/src/ai/api/spec.ts +2 -2
  241. package/src/ai/api-catalog.ts +6 -2
  242. package/src/ai/ask-context.ts +73 -20
  243. package/src/ai/ask-data.ts +6 -0
  244. package/src/ai/ask-tools.ts +142 -0
  245. package/src/ai/ask.ts +354 -76
  246. package/src/ai/changelog-markdown.ts +2 -2
  247. package/src/ai/component-markdown.ts +75 -0
  248. package/src/ai/cors.ts +2 -0
  249. package/src/ai/index.ts +12 -0
  250. package/src/ai/link-headers.ts +14 -5
  251. package/src/ai/llms.ts +24 -9
  252. package/src/ai/markdown.ts +2 -1
  253. package/src/ai/mcp/data.ts +6 -0
  254. package/src/ai/mcp/discovery.ts +2 -2
  255. package/src/ai/mcp/query.ts +2 -2
  256. package/src/ai/mcp/server.ts +2 -2
  257. package/src/ai/openapi-components.ts +31 -9
  258. package/src/ai/relative-links.ts +42 -26
  259. package/src/ai/serializers.ts +2 -1
  260. package/src/ai/site-skill.ts +274 -0
  261. package/src/ai/skills.ts +49 -3
  262. package/src/ai/tar.ts +139 -12
  263. package/src/ai/write-skill.ts +97 -0
  264. package/src/analytics/head.ts +4 -0
  265. package/src/analytics/index.ts +5 -0
  266. package/src/analytics/one-dollar-stats.ts +86 -0
  267. package/src/analytics/schema.ts +2 -0
  268. package/src/astro/generate.ts +133 -19
  269. package/src/astro/include-hmr.ts +31 -12
  270. package/src/astro/include-refresh.ts +19 -8
  271. package/src/astro/index.ts +1 -0
  272. package/src/astro/integration.ts +143 -14
  273. package/src/astro/module-types.ts +6 -0
  274. package/src/astro/runtime-modules.ts +28 -13
  275. package/src/astro/templates.ts +364 -67
  276. package/src/astro/variables.ts +31 -0
  277. package/src/audit/catalog.ts +19 -0
  278. package/src/audit/checks/contrast.ts +183 -0
  279. package/src/audit/checks/i18n.ts +2 -3
  280. package/src/audit/checks/indexability.ts +2 -3
  281. package/src/audit/checks/links.ts +13 -13
  282. package/src/audit/checks/sitemap.ts +2 -3
  283. package/src/audit/graph.ts +3 -1
  284. package/src/audit/redirects.ts +46 -15
  285. package/src/audit/run.ts +14 -9
  286. package/src/audit/snapshot.ts +5 -0
  287. package/src/audit/types.ts +2 -1
  288. package/src/blume-modules.d.ts +7 -0
  289. package/src/captcha/client.ts +152 -0
  290. package/src/captcha/hcaptcha.ts +36 -0
  291. package/src/captcha/index.ts +28 -0
  292. package/src/captcha/schema.ts +32 -0
  293. package/src/captcha/turnstile.ts +38 -0
  294. package/src/captcha/verify.ts +71 -0
  295. package/src/cli/args.ts +32 -0
  296. package/src/cli/command-meta.ts +5 -0
  297. package/src/cli/commands/audit.ts +4 -1
  298. package/src/cli/commands/doctor.ts +25 -17
  299. package/src/cli/commands/eval.ts +19 -11
  300. package/src/cli/commands/skill.ts +96 -0
  301. package/src/cli/commands/translate.ts +9 -12
  302. package/src/cli/commands/validate.ts +3 -1
  303. package/src/cli/dev-lock.ts +157 -37
  304. package/src/cli/index.ts +5 -0
  305. package/src/cli/report-format.ts +11 -6
  306. package/src/cli/required-secrets.ts +71 -29
  307. package/src/components/BlumePage.astro +2 -0
  308. package/src/components/colors.ts +19 -0
  309. package/src/components/content/ApiEndpoint.astro +18 -0
  310. package/src/components/content/ApiField.astro +79 -0
  311. package/src/components/content/ApiPlayground.astro +62 -0
  312. package/src/components/content/ApiRail.astro +19 -0
  313. package/src/components/content/Badge.astro +5 -3
  314. package/src/components/content/Color.astro +1 -0
  315. package/src/components/content/Component.astro +2 -2
  316. package/src/components/content/FileTree.astro +1 -0
  317. package/src/components/content/GithubInfo.astro +1 -0
  318. package/src/components/content/Panel.astro +1 -0
  319. package/src/components/content/ParamField.astro +25 -0
  320. package/src/components/content/RequestExample.astro +17 -0
  321. package/src/components/content/ResponseExample.astro +17 -0
  322. package/src/components/content/ResponseField.astro +32 -0
  323. package/src/components/content/Step.astro +1 -0
  324. package/src/components/content/Tab.astro +0 -1
  325. package/src/components/content/Tabs.astro +4 -0
  326. package/src/components/content/Tile.astro +1 -0
  327. package/src/components/content/Tooltip.astro +1 -0
  328. package/src/components/content/Tree.astro +1 -0
  329. package/src/components/content/TypeTable.astro +4 -1
  330. package/src/components/content/View.astro +21 -0
  331. package/src/components/content/ViewSwitcher.astro +168 -0
  332. package/src/components/content/api-field.ts +100 -0
  333. package/src/components/content/api-page.ts +224 -0
  334. package/src/components/content/badge-color.ts +5 -2
  335. package/src/components/content/base-href.ts +13 -36
  336. package/src/components/content/view-switcher.ts +71 -0
  337. package/src/components/index.ts +2 -0
  338. package/src/components/islands/Assistant.astro +10 -1
  339. package/src/components/islands/assistant.tsx +125 -18
  340. package/src/components/islands/base-path.ts +47 -10
  341. package/src/components/islands/code-question.ts +62 -0
  342. package/src/components/islands/hooks.ts +145 -40
  343. package/src/components/islands/support-link.ts +68 -0
  344. package/src/components/islands/webmcp.ts +4 -2
  345. package/src/components/layout/Analytics.astro +11 -2
  346. package/src/components/layout/Banner.astro +14 -10
  347. package/src/components/layout/Breadcrumbs.astro +11 -4
  348. package/src/components/layout/ConsentBanner.astro +62 -0
  349. package/src/components/layout/ConsentHead.astro +38 -0
  350. package/src/components/layout/Directory.astro +94 -0
  351. package/src/components/layout/DiscoveryLinks.astro +9 -5
  352. package/src/components/layout/DrawerSwitchers.astro +51 -0
  353. package/src/components/layout/Empty.astro +2 -2
  354. package/src/components/layout/Header.astro +124 -82
  355. package/src/components/layout/LanguageSwitcher.astro +50 -17
  356. package/src/components/layout/NarrationPlayer.astro +1085 -0
  357. package/src/components/layout/NavSelector.astro +35 -9
  358. package/src/components/layout/NavTabMenu.astro +3 -3
  359. package/src/components/layout/NavTree.astro +16 -6
  360. package/src/components/layout/PageActions.astro +4 -3
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +78 -9
  363. package/src/components/layout/Pagination.astro +3 -3
  364. package/src/components/layout/ReferenceLayout.astro +17 -6
  365. package/src/components/layout/RelatedPages.astro +42 -0
  366. package/src/components/layout/RootLayout.astro +313 -50
  367. package/src/components/layout/Search.astro +64 -30
  368. package/src/components/layout/SiteFooter.astro +124 -0
  369. package/src/components/layout/SiteMetatags.astro +16 -0
  370. package/src/components/layout/VersionBanner.astro +2 -2
  371. package/src/components/layout/analytics-client.ts +12 -0
  372. package/src/components/layout/consent/native.ts +79 -0
  373. package/src/components/layout/consent/types.ts +37 -0
  374. package/src/components/layout/footer-links.ts +53 -0
  375. package/src/components/layout/footer-socials.ts +87 -0
  376. package/src/components/layout/locale-redirect.ts +107 -0
  377. package/src/components/layout/nav-utils.ts +83 -0
  378. package/src/components/layout/page-feedback.ts +96 -0
  379. package/src/components/layout/search/analytics.ts +98 -0
  380. package/src/components/layout/search/flexsearch.ts +17 -5
  381. package/src/components/layout/search/orama-cloud.ts +26 -14
  382. package/src/components/layout/search/types.ts +20 -0
  383. package/src/components/layout/search/typesense.ts +5 -1
  384. package/src/components/layout/toc-active.ts +41 -0
  385. package/src/components/layout/toc-element.ts +8 -14
  386. package/src/components/openapi/ApiTagOperations.astro +2 -2
  387. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  388. package/src/components/openapi/GraphqlChip.astro +2 -2
  389. package/src/components/openapi/Operation.astro +163 -13
  390. package/src/components/openapi/Playground.astro +4 -4
  391. package/src/components/openapi/RequestBody.astro +6 -4
  392. package/src/components/openapi/RequestPanel.astro +63 -8
  393. package/src/components/openapi/Responses.astro +6 -4
  394. package/src/components/openapi/SchemaTable.astro +7 -0
  395. package/src/components/openapi/async-snippets.ts +4 -4
  396. package/src/components/openapi/async.ts +38 -6
  397. package/src/components/openapi/code-samples.ts +97 -0
  398. package/src/components/openapi/helpers.ts +19 -9
  399. package/src/components/openapi/message-composer.ts +6 -1
  400. package/src/components/openapi/message.ts +17 -2
  401. package/src/components/openapi/operation-model.ts +81 -11
  402. package/src/components/openapi/panel.ts +4 -2
  403. package/src/components/openapi/param-style.ts +181 -0
  404. package/src/components/openapi/playground-client.ts +73 -16
  405. package/src/components/openapi/request.ts +190 -26
  406. package/src/components/openapi/schema-tree.ts +30 -24
  407. package/src/components/openapi/security.ts +6 -3
  408. package/src/components/openapi/snippets.ts +953 -13
  409. package/src/components/openapi/ws-client.ts +18 -2
  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/base-path.ts +62 -16
  421. package/src/core/builtin-tags.ts +5 -0
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/config-input.ts +203 -27
  424. package/src/core/data.ts +44 -4
  425. package/src/core/diagnostics.ts +5 -2
  426. package/src/core/directive-diagnostics.ts +99 -0
  427. package/src/core/footer.ts +26 -0
  428. package/src/core/frontmatter.ts +21 -18
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/heading-markers.ts +5 -18
  431. package/src/core/i18n-ui.ts +82 -2
  432. package/src/core/i18n.ts +31 -3
  433. package/src/core/includes.ts +20 -3
  434. package/src/core/last-modified.ts +25 -3
  435. package/src/core/links.ts +16 -2
  436. package/src/core/locale-links.ts +5 -1
  437. package/src/core/localizable.ts +26 -0
  438. package/src/core/meta.ts +33 -19
  439. package/src/core/navigation.ts +71 -20
  440. package/src/core/page-modes.ts +56 -0
  441. package/src/core/project-graph.ts +138 -10
  442. package/src/core/redirect-patterns.ts +491 -0
  443. package/src/core/related.ts +131 -0
  444. package/src/core/schema.ts +342 -68
  445. package/src/core/sources/contentful-rich-text.ts +27 -20
  446. package/src/core/sources/filesystem.ts +20 -2
  447. package/src/core/sources/github-releases.ts +25 -6
  448. package/src/core/sources/lexical.ts +23 -18
  449. package/src/core/sources/lower.ts +201 -34
  450. package/src/core/sources/mdx-remote.ts +51 -16
  451. package/src/core/sources/normalize.ts +327 -91
  452. package/src/core/sources/notion.ts +52 -25
  453. package/src/core/sources/obsidian.ts +23 -6
  454. package/src/core/sources/portable-text.ts +38 -21
  455. package/src/core/sources/read.ts +8 -3
  456. package/src/core/sources/strapi-blocks.ts +20 -16
  457. package/src/core/sources/watch.ts +20 -7
  458. package/src/core/standard-schema.ts +10 -6
  459. package/src/core/types.ts +1 -1
  460. package/src/core/ui-packs/ar.ts +43 -0
  461. package/src/core/ui-packs/bg.ts +45 -0
  462. package/src/core/ui-packs/bn.ts +43 -0
  463. package/src/core/ui-packs/ca.ts +46 -0
  464. package/src/core/ui-packs/cs.ts +44 -0
  465. package/src/core/ui-packs/da.ts +45 -0
  466. package/src/core/ui-packs/de.ts +46 -0
  467. package/src/core/ui-packs/el.ts +45 -0
  468. package/src/core/ui-packs/es.ts +46 -0
  469. package/src/core/ui-packs/fa.ts +45 -0
  470. package/src/core/ui-packs/fi.ts +45 -0
  471. package/src/core/ui-packs/fr.ts +46 -0
  472. package/src/core/ui-packs/he.ts +43 -0
  473. package/src/core/ui-packs/hi.ts +44 -0
  474. package/src/core/ui-packs/hr.ts +45 -0
  475. package/src/core/ui-packs/hu.ts +44 -0
  476. package/src/core/ui-packs/id.ts +45 -0
  477. package/src/core/ui-packs/it.ts +45 -0
  478. package/src/core/ui-packs/ja.ts +45 -0
  479. package/src/core/ui-packs/ko.ts +44 -0
  480. package/src/core/ui-packs/nl.ts +46 -0
  481. package/src/core/ui-packs/no.ts +44 -0
  482. package/src/core/ui-packs/pl.ts +45 -0
  483. package/src/core/ui-packs/pt-br.ts +46 -0
  484. package/src/core/ui-packs/pt.ts +45 -0
  485. package/src/core/ui-packs/ro.ts +45 -0
  486. package/src/core/ui-packs/ru.ts +45 -0
  487. package/src/core/ui-packs/sk.ts +44 -0
  488. package/src/core/ui-packs/sr.ts +45 -0
  489. package/src/core/ui-packs/sv.ts +44 -0
  490. package/src/core/ui-packs/th.ts +43 -0
  491. package/src/core/ui-packs/tr.ts +45 -0
  492. package/src/core/ui-packs/uk.ts +45 -0
  493. package/src/core/ui-packs/vi.ts +45 -0
  494. package/src/core/ui-packs/zh-tw.ts +43 -0
  495. package/src/core/ui-packs/zh.ts +43 -0
  496. package/src/core/variables.ts +103 -0
  497. package/src/core/version-cut.ts +52 -15
  498. package/src/deploy/artifacts.ts +104 -14
  499. package/src/deploy/cloudflare-negotiation.ts +39 -17
  500. package/src/deploy/headers.ts +10 -5
  501. package/src/deploy/node-headers.ts +64 -10
  502. package/src/deploy/platforms/cloudflare.ts +44 -3
  503. package/src/deploy/platforms/netlify.ts +34 -1
  504. package/src/deploy/platforms/node.ts +1 -0
  505. package/src/deploy/platforms/static.ts +1 -0
  506. package/src/deploy/platforms/types.ts +8 -0
  507. package/src/deploy/platforms/vercel.ts +38 -2
  508. package/src/deploy/redirects.ts +139 -36
  509. package/src/deploy/robots.ts +2 -2
  510. package/src/deploy/rss.ts +4 -3
  511. package/src/deploy/sitemap.ts +7 -5
  512. package/src/deploy/vercel-negotiation.ts +36 -0
  513. package/src/markdown/api-rail.ts +259 -0
  514. package/src/markdown/base-links.ts +34 -36
  515. package/src/markdown/code-title.ts +35 -9
  516. package/src/markdown/directives.ts +242 -36
  517. package/src/markdown/features.ts +20 -2
  518. package/src/markdown/fence-meta.ts +2 -0
  519. package/src/markdown/include.ts +11 -1
  520. package/src/markdown/index.ts +35 -11
  521. package/src/markdown/math.ts +4 -4
  522. package/src/markdown/mdast.ts +5 -2
  523. package/src/markdown/relative-links.ts +12 -4
  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 +136 -11
  531. package/src/og/derive.ts +41 -31
  532. package/src/og/index.ts +1 -0
  533. package/src/openapi/asyncapi.ts +4 -2
  534. package/src/openapi/model.ts +77 -36
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +55 -2
  537. package/src/openapi/proxy.ts +63 -10
  538. package/src/openapi/references.ts +21 -5
  539. package/src/openapi/render-mdx.ts +46 -3
  540. package/src/openapi/scalar.ts +40 -6
  541. package/src/openapi/source.ts +1 -1
  542. package/src/ratelimit/cloudflare.ts +54 -0
  543. package/src/ratelimit/index.ts +30 -0
  544. package/src/ratelimit/memory.ts +45 -0
  545. package/src/ratelimit/runtime.ts +229 -0
  546. package/src/ratelimit/schema.ts +28 -0
  547. package/src/ratelimit/upstash.ts +30 -0
  548. package/src/ratelimit/wrangler.ts +70 -0
  549. package/src/reference/asyncapi.ts +2 -2
  550. package/src/reference/graphql.ts +5 -2
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +51 -7
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +201 -28
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/version-scope.ts +30 -0
  557. package/src/search/documents.ts +93 -28
  558. package/src/search/orama-index.ts +28 -3
  559. package/src/search/pagefind-ranking.ts +39 -0
  560. package/src/search/popular.ts +2 -1
  561. package/src/search/sync/algolia.ts +15 -4
  562. package/src/search/sync/orama-cloud.ts +3 -0
  563. package/src/search/sync/typesense.ts +5 -0
  564. package/src/seo/jsonld.ts +7 -3
  565. package/src/seo/metatags.ts +67 -0
  566. package/src/theme/chrome-icons.ts +1 -0
  567. package/src/theme/color.ts +500 -0
  568. package/src/theme/entry.ts +56 -3
  569. package/src/theme/palette.ts +121 -45
  570. package/src/translate/meta.ts +68 -24
  571. package/src/translate/prompts.ts +1 -0
  572. package/src/translate/run.ts +3 -3
  573. package/src/translate/validate.ts +12 -3
  574. package/src/translate/work-list.ts +56 -17
  575. package/src/upgrade/upgrade.ts +4 -171
  576. package/dist/cli/chunk-3yce002v.js.map +0 -14
  577. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  578. package/dist/cli/chunk-6dtt0zfn.js.map +0 -10
  579. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  580. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  581. package/dist/cli/chunk-esh98wmb.js +0 -23
  582. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  583. package/dist/cli/chunk-hqp2ajnh.js.map +0 -35
  584. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  585. package/dist/cli/chunk-j85scx15.js.map +0 -10
  586. package/dist/cli/chunk-mqc662a6.js +0 -75
  587. package/dist/cli/chunk-mqc662a6.js.map +0 -11
  588. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  589. package/dist/cli/chunk-qkb5a8sa.js.map +0 -10
  590. package/dist/cli/chunk-v6ya5kcb.js.map +0 -189
  591. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  592. package/dist/cli/chunk-xh43dwgw.js.map +0 -36
  593. package/docs/content/sources.mdx +0 -338
  594. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-1n228xqb.js.map} +0 -0
  595. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-5ffywsmb.js.map} +0 -0
  596. /package/dist/cli/{chunk-g698a744.js.map → chunk-5wj2qwn2.js.map} +0 -0
  597. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-amrb6hgh.js.map} +0 -0
  598. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-gp84za0h.js.map} +0 -0
  599. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-p804f3pe.js.map} +0 -0
  600. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-ta27feev.js.map} +0 -0
  601. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-y4ykbark.js.map} +0 -0
@@ -6,22 +6,34 @@ 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
- import type { OgFont, OgFontFamilies } from "../og/card.ts";
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
- import type { ResolvedSearchAdapter } from "../search/adapters/registry.ts";
33
+ import type {
34
+ ResolvedSearchAdapter,
35
+ SearchAdapterKind,
36
+ } from "../search/adapters/registry.ts";
25
37
  import { buildFontEntries, fontLocaleCodes } from "../theme/fonts.ts";
26
38
  import { importSpecifier, wrapperPropsType } from "./component-slots.ts";
27
39
  import type { ExampleSpec } from "./examples.ts";
@@ -185,7 +197,8 @@ export const runtimeDependencies = (options: {
185
197
  deps.push(
186
198
  ...new Set(config.reference.flatMap((adapter) => adapter.runtimeDeps)),
187
199
  ...config.search.provider.runtimeDeps,
188
- ...config.analytics.flatMap((adapter) => adapter.runtimeDeps)
200
+ ...config.analytics.flatMap((adapter) => adapter.runtimeDeps),
201
+ ...(config.consent?.runtimeDeps ?? [])
189
202
  );
190
203
  // Each content source adapter declares the SDK its fetch imports (Notion,
191
204
  // Sanity); the descriptor is the one place that knows.
@@ -398,14 +411,32 @@ export interface ClientFeatures {
398
411
  /** Every feature on: what a checkout that predates the detection shipped. */
399
412
  const ALL_CLIENT_FEATURES: ClientFeatures = { epub: true, mermaid: true };
400
413
 
414
+ /**
415
+ * The npm module each browser-queried search adapter's client imports (see
416
+ * `components/layout/search/`). The `<Search>` dialog lazy-imports that client
417
+ * on first open, after the dev dep optimizer's startup run, so the first
418
+ * search of a fresh `blume dev` session discovered the library, re-ran the
419
+ * optimizer, and reloaded the page. Pagefind loads its script from the build
420
+ * output and Mixedbread queries through the server, so neither has one.
421
+ */
422
+ const SEARCH_CLIENT_DEPS: Partial<Record<SearchAdapterKind, string>> = {
423
+ algolia: "algoliasearch/lite",
424
+ flexsearch: "flexsearch",
425
+ orama: "@orama/orama",
426
+ "orama-cloud": "@oramacloud/client",
427
+ typesense: "typesense",
428
+ };
429
+
401
430
  const resolveOptimizeDeps = (options: {
402
431
  aliases: Record<string, string> | undefined;
403
432
  context: ProjectContext;
404
433
  features: ClientFeatures;
405
434
  needsReact: boolean;
406
435
  reactCompilerPath: string | null | undefined;
436
+ searchKind: SearchAdapterKind;
407
437
  }): OptimizeDepsConfig => {
408
438
  const { context, features } = options;
439
+ const searchDep = SEARCH_CLIENT_DEPS[options.searchKind];
409
440
  const optimizeDepsEntries = [
410
441
  ...(context.pagesRoot ? [`${context.pagesRoot}/**/*.astro`] : []),
411
442
  `${context.root}/islands/**/*.{jsx,svelte,tsx,vue}`,
@@ -418,6 +449,8 @@ const resolveOptimizeDeps = (options: {
418
449
  // Mermaid costs the dev server seconds at startup for nothing otherwise.
419
450
  ...(features.mermaid ? ["blume > mermaid"] : []),
420
451
  ...(features.epub ? ["blume > epub-gen-memory/bundle"] : []),
452
+ // Only the configured search adapter's client library.
453
+ ...(searchDep ? [`blume > ${searchDep}`] : []),
421
454
  // Astro's own client-router/prefetch virtual modules are deliberately NOT
422
455
  // forced in here: they read Vite `define`-injected constants
423
456
  // (__PREFETCH_PREFETCH_ALL__ and friends) that a pre-bundled copy loses,
@@ -532,17 +565,23 @@ const blumeIntegrationOptions = (options: {
532
565
  contentRoutes: string[];
533
566
  ejected: boolean;
534
567
  pages: BlumePageRoute[];
535
- }): BlumeIntegrationOptions =>
536
- 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
537
575
  ? {
576
+ ...shared,
538
577
  buildArtifactsRoot: ".",
539
578
  contentRoutes: options.contentRoutes,
540
579
  homeLinkHeader:
541
- buildHomeLinkHeader(options.config, options.contentRoutes) ??
580
+ buildHomeLinkHeader(options.config, options.contentRoutes, "dev") ??
542
581
  undefined,
543
- pages: options.pages,
544
582
  }
545
- : { pages: options.pages };
583
+ : shared;
584
+ };
546
585
 
547
586
  export const astroConfigTemplate = (options: {
548
587
  context: ProjectContext;
@@ -558,6 +597,8 @@ export const astroConfigTemplate = (options: {
558
597
  /** The example-preview Tailwind entry (`blume:examples-theme`). */
559
598
  examplesThemePath: string;
560
599
  themePath: string;
600
+ /** The configured consent adapter's browser module (`blume:consent-client`). */
601
+ consentClientPath: string;
561
602
  searchClientPath: string;
562
603
  /** The generated client-feature loaders (`blume:features`). */
563
604
  featuresPath: string;
@@ -594,6 +635,7 @@ export const astroConfigTemplate = (options: {
594
635
  );
595
636
  const {
596
637
  askPath,
638
+ consentClientPath,
597
639
  contentRoutes,
598
640
  examplesPath,
599
641
  examplesThemePath,
@@ -623,6 +665,7 @@ export const astroConfigTemplate = (options: {
623
665
  features,
624
666
  needsReact,
625
667
  reactCompilerPath: options.reactCompilerPath,
668
+ searchKind: config.search.provider.kind,
626
669
  });
627
670
 
628
671
  const {
@@ -660,13 +703,20 @@ export const astroConfigTemplate = (options: {
660
703
  const basedRedirects = applyBaseToAstroRedirects(
661
704
  config.redirects,
662
705
  config.basePath,
663
- deployment.options.base ?? ""
706
+ deployment.options.base ?? "",
707
+ new Set(contentRoutes)
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)
664
714
  );
665
715
  const redirectsOption =
666
- basedRedirects.length > 0
716
+ exactRedirects.length > 0
667
717
  ? `\n redirects: ${JSON.stringify(
668
718
  Object.fromEntries(
669
- basedRedirects.map((redirect) => [
719
+ exactRedirects.map((redirect) => [
670
720
  redirect.from,
671
721
  { destination: redirect.to, status: redirect.status },
672
722
  ])
@@ -734,12 +784,19 @@ export const astroConfigTemplate = (options: {
734
784
  const svelteImport = needsSvelte
735
785
  ? `import svelte from "@astrojs/svelte";\n`
736
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
+ : "";
737
793
  const blumeImports = [
738
794
  "blumeIntegration",
739
795
  "includeHmrPlugin",
740
796
  "prerenderDepsPlugin",
741
797
  ...runtimeModuleImports,
742
798
  ...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
799
+ ...(substitutesVariables ? ["variablesVitePlugin"] : []),
743
800
  ];
744
801
  const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
745
802
 
@@ -769,6 +826,7 @@ export const astroConfigTemplate = (options: {
769
826
  deployBase,
770
827
  externalLinks: config.markdown.externalLinks,
771
828
  headingAnchors: config.markdown.headingAnchors,
829
+ variables: config.variables,
772
830
  });
773
831
  const processorOptions =
774
832
  options.generatedModulesDir === undefined
@@ -797,6 +855,7 @@ export const astroConfigTemplate = (options: {
797
855
  contentRoutes,
798
856
  ejected,
799
857
  pages,
858
+ redirects: compileRedirects(basedRedirects),
800
859
  })
801
860
  )})`
802
861
  );
@@ -860,7 +919,7 @@ ${userConfigSetup}export default defineConfig({
860
919
  // the overlap only adds memory.
861
920
  build: { concurrency: Math.min(8, availableParallelism()) },
862
921
  vite: {${viteCacheOption}
863
- plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
922
+ plugins: [${runtimeModulesPluginEntry}${variablesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
864
923
  `${context.outDir}/src/generated/includes.json`,
865
924
  ejected
866
925
  )}), prerenderDepsPlugin()],
@@ -892,6 +951,10 @@ ${userConfigSetup}export default defineConfig({
892
951
  // \`/bundle\` subpath that is actually imported: optimizing the package
893
952
  // root leaves that entry out. Production (Rollup) already handles the
894
953
  // interop, so all of this only affects dev.
954
+ //
955
+ // The search adapter's client library rides the list for the same reason
956
+ // as the compiler runtime: the search dialog lazy-imports it on first
957
+ // open, and discovering it then re-optimizes and reloads the page.
895
958
  optimizeDeps: {
896
959
  entries: ${JSON.stringify(optimizeDepsEntries)},
897
960
  include: ${JSON.stringify(optimizeDepsInclude)},
@@ -915,6 +978,7 @@ ${userConfigSetup}export default defineConfig({
915
978
  resolve: {
916
979
  alias: {
917
980
  "blume:ask": ${configPath(askPath, ejected)},
981
+ "blume:consent-client": ${configPath(consentClientPath, ejected)},
918
982
  "blume:examples": ${configPath(examplesPath, ejected)},
919
983
  "blume:examples-theme": ${configPath(examplesThemePath, ejected)},
920
984
  "blume:features": ${configPath(featuresPath, ejected)},
@@ -1066,12 +1130,21 @@ const ASK_FALLBACK_PROMPT =
1066
1130
 
1067
1131
  /** The `ai.assistant` values the generated endpoint has to carry with it. */
1068
1132
  export interface AskEndpointOptions {
1133
+ /** `ai.assistant.captcha` — the bot check the route verifies first. */
1134
+ captcha?: CaptchaAdapter;
1069
1135
  /** `ai.assistant.cors` — origins allowed to call the route from another site. */
1070
1136
  cors?: string[];
1137
+ /** `rateLimit` — the limiter the route checks before any work. */
1138
+ rateLimit?: RateLimitAdapter | null;
1071
1139
  /** `ai.assistant.instructions` — extra system-prompt text. */
1072
1140
  instructions?: string;
1073
1141
  /** `ai.assistant.retrieval` — how much documentation each question carries. */
1074
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;
1075
1148
  }
1076
1149
 
1077
1150
  /** The pieces `askEndpointTemplate` splices in for `ai.assistant.cors`. */
@@ -1103,7 +1176,7 @@ const askCorsTemplate = (cors: readonly string[] = []): AskCorsTemplate =>
1103
1176
  imports: [
1104
1177
  'import { preflightResponse, withCors } from "blume/ai/cors.ts";',
1105
1178
  ],
1106
- open: "withCors(ALLOWED_ORIGINS, async ({ request }) => {",
1179
+ open: "withCors(ALLOWED_ORIGINS, async (context) => {",
1107
1180
  setup: `
1108
1181
  const ALLOWED_ORIGINS = ${JSON.stringify(cors)};
1109
1182
 
@@ -1111,7 +1184,89 @@ export const OPTIONS: APIRoute = ({ request }) =>
1111
1184
  preflightResponse(request, ALLOWED_ORIGINS);
1112
1185
  `,
1113
1186
  }
1114
- : { 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
+ };
1115
1270
 
1116
1271
  /**
1117
1272
  * Largest request body the assistant route reads: 64 KB, well above the
@@ -1145,6 +1300,8 @@ export const askEndpointTemplate = (
1145
1300
  ): string => {
1146
1301
  const { instructions, retrieval } = options ?? {};
1147
1302
  const { grounded } = backend;
1303
+ // The tools read the grounding snapshot, so an ungrounded backend has none.
1304
+ const tools = grounded && Boolean(options?.tools);
1148
1305
  const fallbackPrompt = instructions
1149
1306
  ? `${ASK_FALLBACK_PROMPT}\n\n${instructions}`
1150
1307
  : ASK_FALLBACK_PROMPT;
@@ -1172,12 +1329,32 @@ export const askEndpointTemplate = (
1172
1329
  if (retrieval) {
1173
1330
  groundFields.push(`retrieval: ${JSON.stringify(retrieval)}`);
1174
1331
  }
1332
+ if (tools) {
1333
+ groundFields.push("tools: true");
1334
+ }
1175
1335
  const groundOptions =
1176
1336
  groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
1177
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
+ }
1178
1345
  }
1179
1346
  const cors = askCorsTemplate(options?.cors);
1180
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;
1181
1358
  // Validate the client-supplied body and cap its size. The endpoint is
1182
1359
  // unauthenticated, so bounding message count/length limits how much a caller
1183
1360
  // can spend against the model per request, and restricting roles to
@@ -1186,7 +1363,8 @@ export const askEndpointTemplate = (
1186
1363
  // limiter (or your provider's limits) for stronger protection. The body is
1187
1364
  // read under a 64 KB cap before any parsing, since a self-hosted Node server
1188
1365
  // would otherwise buffer an arbitrarily large POST in memory first.
1189
- 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});
1190
1368
  if (text === undefined) {
1191
1369
  return new Response("Request too large: the body must be at most 64 KB.", {
1192
1370
  status: 413,
@@ -1244,11 +1422,22 @@ export const askEndpointTemplate = (
1244
1422
  ? "instructions"
1245
1423
  : `instructions:\n ${JSON.stringify(fallbackPrompt)}`,
1246
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
+ : []),
1247
1430
  ...backend.template.fields,
1248
1431
  ];
1432
+ // The request's signal aborts when the reader closes the panel mid-answer
1433
+ // (the client drops the connection): passing it on stops the model call,
1434
+ // which otherwise kept generating, and billing, to completion into a closed
1435
+ // connection. `streamText` ends an aborted call through its abort path, not
1436
+ // `onError`, so the reader leaving is never logged as a provider error.
1249
1437
  const call = ` const result = streamText({
1250
1438
  ${streamFields.join(",\n ")},
1251
1439
  ${onError}
1440
+ abortSignal: request.signal,
1252
1441
  });`;
1253
1442
  const stream = grounded
1254
1443
  ? ` const instructions =
@@ -1259,7 +1448,7 @@ ${call}`
1259
1448
  const handler = `export const POST: APIRoute = ${cors.open}
1260
1449
  ${validate}
1261
1450
  ${keyCheck}
1262
- try {
1451
+ ${captcha.check} try {
1263
1452
  ${stream}
1264
1453
  return createTextStreamResponse({
1265
1454
  stream: toTextStream({ stream: result.stream }),
@@ -1304,9 +1493,11 @@ const { strings } = Astro.props;
1304
1493
  ---
1305
1494
 
1306
1495
  <Assistant
1496
+ captcha={data.config.assistant?.captcha ?? undefined}
1307
1497
  endpoint={data.config.assistant?.endpoint ?? undefined}
1308
1498
  strings={strings ?? data.ui.assistant}
1309
1499
  suggestions={data.config.assistant?.suggestions ?? []}
1500
+ support={data.config.assistant?.support ?? undefined}
1310
1501
  />
1311
1502
  `
1312
1503
  : `---
@@ -1406,6 +1597,36 @@ ${
1406
1597
  }
1407
1598
  `;
1408
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
+
1409
1630
  /**
1410
1631
  * Generate `.blume/src/generated/search-client.ts` — the provider-specific
1411
1632
  * loader the `<Search>` component lazy-imports via the `blume:search-client`
@@ -1458,13 +1679,19 @@ export const createSearch = () => create({ url });
1458
1679
  * content was synced (see the Mixedbread sync step / \`mxbai vs sync\`).
1459
1680
  */
1460
1681
  export const mixedbreadSearchEndpointTemplate = (
1461
- options: MixedbreadOptions
1462
- ): string =>
1463
- `// Generated by Blume. Do not edit.
1464
- import type { APIRoute } from "astro";
1465
- import { getSecret } from "astro:env/server";
1466
- import Mixedbread from "@mixedbread/sdk";
1467
- 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")}
1468
1695
 
1469
1696
  export const prerender = false;
1470
1697
 
@@ -1472,9 +1699,10 @@ const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" })
1472
1699
  const OPTIONS = ${JSON.stringify(options)};
1473
1700
  // Every option besides the store reaches the search call verbatim.
1474
1701
  const { storeId: STORE_ID, ...SEARCH_OPTIONS } = OPTIONS;
1475
-
1476
- export const POST: APIRoute = async ({ request }) => {
1477
- // 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
1478
1706
  // self-hosted server never buffers an arbitrarily large POST first.
1479
1707
  const text = await readCappedText(request, 16_384);
1480
1708
  if (text === undefined) {
@@ -1519,6 +1747,7 @@ export const POST: APIRoute = async ({ request }) => {
1519
1747
  });
1520
1748
  };
1521
1749
  `;
1750
+ };
1522
1751
 
1523
1752
  /**
1524
1753
  * Generate the raw-Markdown endpoints (`[...slug].md.ts` and `[...slug].mdx.ts`).
@@ -1721,17 +1950,26 @@ export const ALL: APIRoute = ({ request }) => handler(request);
1721
1950
  * client-side data: that is the whole trust boundary keeping the endpoint from
1722
1951
  * being an open proxy onto the deployment's own network.
1723
1952
  */
1724
- export const playgroundProxyTemplate = (origins: string[]): string =>
1725
- `// Generated by Blume. Do not edit.
1726
- import type { APIRoute } from "astro";
1727
- 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")}
1728
1965
 
1729
1966
  export const prerender = false;
1730
1967
 
1731
1968
  const handler = createPlaygroundProxyHandler(${JSON.stringify(origins)});
1732
-
1733
- export const ALL: APIRoute = ({ request }) => handler(request);
1969
+ ${limit.setup}
1970
+ ${route}
1734
1971
  `;
1972
+ };
1735
1973
 
1736
1974
  /** Generate a prerendered endpoint that serves a fixed JSON payload. */
1737
1975
  export const staticJsonEndpointTemplate = <Payload extends object>(
@@ -1792,7 +2030,7 @@ export const navFragmentTemplate = (): string =>
1792
2030
  `---
1793
2031
  // Generated by Blume. Do not edit.
1794
2032
  import NavTree from "blume/components/layout/NavTree.astro";
1795
- import { withBase } from "blume/components/islands/base-path.ts";
2033
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
1796
2034
  import {
1797
2035
  hiddenDefaultLocale,
1798
2036
  navGroupIds,
@@ -1831,7 +2069,7 @@ if (!(variant && node && node.kind === "group")) {
1831
2069
  }
1832
2070
  const strings =
1833
2071
  locale === "default" ? data.ui.nav : (data.uiByLocale[locale] ?? data.ui).nav;
1834
- const fragmentBase = withBase(\`/blume-nav/\${version}/\${locale}\`);
2072
+ const fragmentBase = withMountedBase(\`/blume-nav/\${version}/\${locale}\`);
1835
2073
  ---
1836
2074
 
1837
2075
  <NavTree
@@ -1855,6 +2093,7 @@ export const ogEndpointTemplate = (
1855
2093
  * Absent, every card renders on every build.
1856
2094
  */
1857
2095
  cache?: OgCache;
2096
+ fallbacks?: OgGoogleFont[];
1858
2097
  families?: OgFontFamilies;
1859
2098
  fonts?: OgFont[];
1860
2099
  /**
@@ -1868,7 +2107,7 @@ export const ogEndpointTemplate = (
1868
2107
  ): string =>
1869
2108
  `// Generated by Blume. Do not edit.
1870
2109
  import { cachedOgImage } from "blume/og";
1871
- import type { OgCache, OgFont, OgFontFamilies } from "blume/og";
2110
+ import type { OgCache, OgFont, OgFontFamilies, OgGoogleFont } from "blume/og";
1872
2111
  import data from "blume:data";
1873
2112
 
1874
2113
  export const prerender = true;
@@ -1886,6 +2125,9 @@ const fonts: OgFont[] = ${JSON.stringify(og.fonts ?? [])};
1886
2125
  const families: OgFontFamilies | undefined = ${
1887
2126
  og.families ? JSON.stringify(og.families) : "undefined"
1888
2127
  };
2128
+ // Script fallbacks (Noto families, the configured locales' first). A card
2129
+ // fetches them only when its text has a glyph Takumi's built-in font lacks.
2130
+ const fallbacks: OgGoogleFont[] = ${JSON.stringify(og.fallbacks ?? [])};
1889
2131
 
1890
2132
  // Rendered cards are kept on disk between builds, keyed by everything that
1891
2133
  // decides their pixels, so a rebuild renders only the cards whose title,
@@ -1958,6 +2200,7 @@ export async function GET({ props }: { props: CardProps }) {
1958
2200
  accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
1959
2201
  brand: data.config.title,
1960
2202
  description: props.description ?? data.config.og.description,
2203
+ fallbacks,
1961
2204
  families,
1962
2205
  fonts,
1963
2206
  logo: data.config.og.logo,
@@ -2050,6 +2293,9 @@ const contentComponentsSource = (mathEnabled: boolean) => {
2050
2293
  return {
2051
2294
  imports: `import Accordion from "blume/components/content/Accordion.astro";
2052
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";
2053
2299
  import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
2054
2300
  import Badge from "blume/components/content/Badge.astro";
2055
2301
  import Callout from "blume/components/content/Callout.astro";
@@ -2069,7 +2315,11 @@ import FileTree from "blume/components/content/FileTree.astro";
2069
2315
  import Frame from "blume/components/content/Frame.astro";
2070
2316
  import GithubInfo from "blume/components/content/GithubInfo.astro";
2071
2317
  import Panel from "blume/components/content/Panel.astro";
2318
+ import ParamField from "blume/components/content/ParamField.astro";
2072
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";
2073
2323
  import Step from "blume/components/content/Step.astro";
2074
2324
  import Steps from "blume/components/content/Steps.astro";
2075
2325
  import Tab from "blume/components/content/Tab.astro";
@@ -2080,10 +2330,13 @@ import TreeRoot from "blume/components/content/Tree.astro";
2080
2330
  import TreeFile from "blume/components/content/TreeFile.astro";
2081
2331
  import TreeFolder from "blume/components/content/TreeFolder.astro";
2082
2332
  import TypeTable from "blume/components/content/TypeTable.astro";
2333
+ import View from "blume/components/content/View.astro";
2083
2334
  import Visibility from "blume/components/content/Visibility.astro";
2084
2335
  import YouTube from "blume/components/content/YouTube.astro";
2085
2336
  import Icon from "blume/components/Icon.astro";
2086
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";
2087
2340
  import ApiOverview from "blume/components/openapi/ApiOverview.astro";
2088
2341
  import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
2089
2342
  import Operation from "blume/components/openapi/Operation.astro";
@@ -2091,7 +2344,10 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2091
2344
  map: `{
2092
2345
  Accordion,
2093
2346
  AccordionItem,
2347
+ ApiEndpoint,
2094
2348
  ApiOverview,
2349
+ ApiPlayground,
2350
+ ApiRail,
2095
2351
  ApiTagOperations,
2096
2352
  AutoTypeTable,
2097
2353
  Badge,
@@ -2112,7 +2368,11 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2112
2368
  Icon,
2113
2369
  Operation,
2114
2370
  Panel,
2371
+ ParamField,
2115
2372
  Prompt,
2373
+ RequestExample,
2374
+ ResponseExample,
2375
+ ResponseField,
2116
2376
  Step,
2117
2377
  Steps,
2118
2378
  Tab,
@@ -2121,6 +2381,7 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2121
2381
  Tooltip,
2122
2382
  Tree,
2123
2383
  TypeTable,
2384
+ View,
2124
2385
  Visibility,
2125
2386
  YouTube,
2126
2387
  ${mathEntry}...userMdx,
@@ -2151,10 +2412,11 @@ export const catchAllPageTemplate = (options: {
2151
2412
  return `---
2152
2413
  // Generated by Blume. Do not edit.
2153
2414
  import { getEntry, render } from "astro:content";
2415
+ import { pageModeLayout } from "blume/core/page-modes.ts";
2154
2416
  import type { CollectionKey } from "astro:content";
2155
2417
  import RootLayout from "blume/components/layout/RootLayout.astro";
2156
- import { withBase } from "blume/components/islands/base-path.ts";
2157
- import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
2418
+ import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
2419
+ import { mountBasePath, stripBasePath } from "blume/core/base-path.ts";
2158
2420
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2159
2421
  ${componentImports}
2160
2422
  import data from "blume:data";
@@ -2202,6 +2464,13 @@ const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render
2202
2464
  // frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
2203
2465
  // counts: \`frontmatter.extend\` can declare the same key, and on a page with
2204
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
+ : [];
2205
2474
  const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
2206
2475
  const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
2207
2476
  const headings =
@@ -2219,12 +2488,16 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
2219
2488
  const ogPath = data.config.og.enabled
2220
2489
  ? encodeURI(\`/og/\${route === "/" ? "index" : route.slice(1)}.png\`)
2221
2490
  : null;
2222
- const ogRel = seo.image ?? ogPath;
2223
2491
  // Absolute URLs also carry the deployment base (the page is served under it):
2224
2492
  // \`site + base + path\`. Only absolutize root-relative paths: \`seo.image\` may be
2225
- // an external URL, which passes through verbatim (mirrors PageLayout).
2226
- const ogImage =
2227
- ogRel && base && ogRel.startsWith("/") ? \`\${base}\${withBase(ogRel)}\` : ogRel;
2493
+ // an external URL, which passes through verbatim (mirrors PageLayout). An
2494
+ // authored \`seo.image\` keeps a base written by hand; the generated card is
2495
+ // always mounted under it.
2496
+ const absoluteOg = (path: string, based: (path: string) => string) =>
2497
+ base && path.startsWith("/") ? \`\${base}\${based(path)}\` : path;
2498
+ const ogImage = seo.image
2499
+ ? absoluteOg(seo.image, withBase)
2500
+ : ogPath && absoluteOg(ogPath, withMountedBase);
2228
2501
  // Blume's generated card has known dimensions the layout can declare; a user's
2229
2502
  // \`seo.image\` could be any size or format, so it gets none.
2230
2503
  const ogGenerated = !seo.image && Boolean(ogPath);
@@ -2233,7 +2506,7 @@ const ogGenerated = !seo.image && Boolean(ogPath);
2233
2506
  // itself (a guest post crediting its own author) over the configured default.
2234
2507
  const x = { ...data.config.x, ...(seo.x?.creator ? { creator: seo.x.creator } : {}) };
2235
2508
 
2236
- const basedRoute = withBase(route);
2509
+ const basedRoute = withMountedBase(route);
2237
2510
 
2238
2511
  // Locale resolution. With i18n on, pick the active locale's nav + dictionary,
2239
2512
  // build hreflang alternates, and derive the language-switcher targets.
@@ -2276,12 +2549,20 @@ const htmlLang = i18n ? locale : "en";
2276
2549
  // follows that language — not the (mirrored) page locale.
2277
2550
  const contentLocale =
2278
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;
2279
2560
  const contentDir = i18n
2280
2561
  ? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
2281
2562
  : "ltr";
2282
2563
  // The root route keeps its trailing slash (\`https://site/\`) so canonical and
2283
2564
  // hreflang URLs byte-match the sitemap's <loc> for the home page.
2284
- const absolute = (path: string) => base + withBase(path);
2565
+ const absolute = (path: string) => base + withMountedBase(path);
2285
2566
 
2286
2567
  // A fallback page renders the fallback locale's page at this locale's URL, so
2287
2568
  // its canonical names the page it copies, and search engines never rank the
@@ -2326,7 +2607,7 @@ const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
2326
2607
  // carries sits *after* the base (\`/docs/ja/guide\`), while \`localePrefix\` is
2327
2608
  // base-less (\`/ja\`). Stripping and re-adding a locale therefore happen in
2328
2609
  // base-less space, with the base re-applied at the end — the same
2329
- // \`withBasePath(basePath, localizeRoute(...))\` composition the manifest uses to
2610
+ // \`mountBasePath(basePath, localizeRoute(...))\` composition the manifest uses to
2330
2611
  // build every real route. Done in based space, \`stripLocale\` matches nothing
2331
2612
  // and \`localizeRoute\` prepends a second prefix (\`/ja/docs/ja/guide\`). Only a
2332
2613
  // switcher entry for a locale with no real translation reaches this fallback:
@@ -2334,7 +2615,7 @@ const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
2334
2615
  // generated reference (which has no \`alternates\` at all) for every locale
2335
2616
  // but its own.
2336
2617
  const mountLocalized = (logical: string, codeArg: string) =>
2337
- withBasePath(data.config.basePath, localizeRoute(logical, codeArg));
2618
+ mountBasePath(data.config.basePath, localizeRoute(logical, codeArg));
2338
2619
  const logicalRoute = i18n
2339
2620
  ? stripLocale(stripBasePath(data.config.basePath, route), locale)
2340
2621
  : route;
@@ -2363,7 +2644,7 @@ const versionRootFor = (id: string) => {
2363
2644
  const logical = id ? \`/\${id}\` : "/";
2364
2645
  return i18n
2365
2646
  ? mountLocalized(logical, locale)
2366
- : withBasePath(data.config.basePath, logical);
2647
+ : mountBasePath(data.config.basePath, logical);
2367
2648
  };
2368
2649
  const samePageSwitch = versionsConfig
2369
2650
  ? versionsConfig.switcher.redirect === "same-page"
@@ -2459,11 +2740,15 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2459
2740
  canonical={canonical}
2460
2741
  editUrl={editUrl}
2461
2742
  feedback={data.config.feedback}
2743
+ feedbackComments={data.config.feedbackComments}
2744
+ pagination={frontmatter.pagination}
2745
+ related={frontmatter.related}
2746
+ search={frontmatter.search}
2462
2747
  exportPdf={${options.exportPdf}}
2463
2748
  exportEpub={${options.exportEpub}}${
2464
2749
  options.navFragments
2465
2750
  ? `
2466
- navFragmentBase={withBase(\`/blume-nav/\${version || "current"}/\${i18n && localePrefix(locale) ? locale : "default"}\`)}`
2751
+ navFragmentBase={withMountedBase(\`/blume-nav/\${version || "current"}/\${i18n && localePrefix(locale) ? locale : "default"}\`)}`
2467
2752
  : ""
2468
2753
  }
2469
2754
  openInChat={data.config.openInChat}
@@ -2471,13 +2756,25 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2471
2756
  discovery={data.config.discovery}
2472
2757
  siteUrl={data.config.site}
2473
2758
  pageType={frontmatter.type}
2759
+ apiRail={remarkPluginFrontmatter?.${API_RAIL_KEY} === true}
2760
+ pageMode={frontmatter.mode}
2474
2761
  published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
2475
2762
  lastModified={lastModified}
2476
2763
  noindex={effectiveNoindex}
2477
2764
  structuredDataEnabled={data.config.structuredData}
2478
2765
  >
2479
- <h1>{title}</h1>
2480
- {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} />}
2481
2778
  <LocaleLinks locale={locale}>
2482
2779
  <Content components={components} />
2483
2780
  </LocaleLinks>
@@ -2516,7 +2813,7 @@ export const changelogIndexTemplate = (options: {
2516
2813
  // Generated by Blume. Do not edit.
2517
2814
  import { getCollection } from "astro:content";
2518
2815
  import RootLayout from "blume/components/layout/RootLayout.astro";
2519
- import { withBase } from "blume/components/islands/base-path.ts";
2816
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
2520
2817
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2521
2818
  import { resolveDateFormatOptions } from "blume/core/date-format.ts";
2522
2819
  import { layoutOverrides } from "../generated/components.ts";
@@ -2615,7 +2912,7 @@ const items = changelogEntries.map((entry) => {
2615
2912
  return {
2616
2913
  date: date ? formatRowDate(date) : null,
2617
2914
  dateTime: date ? date.toISOString().slice(0, 10) : null,
2618
- href: route ? withBase(route) : null,
2915
+ href: route ? withMountedBase(route) : null,
2619
2916
  id: slugify(label),
2620
2917
  label,
2621
2918
  tag: entry.data.changelog?.category ?? null,
@@ -2651,14 +2948,14 @@ for (const item of items) {
2651
2948
 
2652
2949
  const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
2653
2950
  // The canonical URL carries the deployment base (the page is served under it),
2654
- // matching how the catch-all canonicalizes via \`withBase(route)\`.
2655
- const basedRoute = withBase("/changelog");
2951
+ // matching how the catch-all canonicalizes via \`withMountedBase(route)\`.
2952
+ const basedRoute = withMountedBase("/changelog");
2656
2953
  const canonical = base ? base + basedRoute : null;
2657
2954
 
2658
2955
  // The generated OG card for this route (the /og endpoint emits it alongside
2659
2956
  // the content-route cards), absolutized like the catch-all's so crawlers get
2660
2957
  // a full URL when the site is known.
2661
- const ogPath = data.config.og.enabled ? withBase("/og/changelog.png") : null;
2958
+ const ogPath = data.config.og.enabled ? withMountedBase("/og/changelog.png") : null;
2662
2959
  const ogImage = ogPath && base ? base + ogPath : ogPath;
2663
2960
 
2664
2961
  // The page chrome (h1, title, description) comes from the translatable
@@ -2780,8 +3077,8 @@ export const notFoundPageTemplate = (): string => `---
2780
3077
  // Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
2781
3078
  import Icon from "blume/components/Icon.astro";
2782
3079
  import PageLayout from "blume/components/layout/PageLayout.astro";
2783
- import { withBase } from "blume/components/islands/base-path.ts";
2784
- import { withBasePath } from "blume/core/base-path.ts";
3080
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
3081
+ import { mountBasePath } from "blume/core/base-path.ts";
2785
3082
  import data from "blume:data";
2786
3083
 
2787
3084
  export const prerender = true;
@@ -2805,14 +3102,14 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
2805
3102
  // their resolved target when the section has no index page of its own.
2806
3103
  const suggestions = [
2807
3104
  ...data.navigation.tabs.map((tab) => ({
2808
- href: withBase(tab.href ?? tab.path),
3105
+ href: withMountedBase(tab.href ?? tab.path),
2809
3106
  label: tab.label,
2810
3107
  })),
2811
3108
  ...(data.config.discovery.sitemap
2812
- ? [{ href: withBase("/sitemap.xml"), label: nf.sitemap }]
3109
+ ? [{ href: withMountedBase("/sitemap.xml"), label: nf.sitemap }]
2813
3110
  : []),
2814
3111
  ...(data.config.discovery.llmsTxt
2815
- ? [{ href: withBase("/llms.txt"), label: nf.llms }]
3112
+ ? [{ href: withMountedBase("/llms.txt"), label: nf.llms }]
2816
3113
  : []),
2817
3114
  ];
2818
3115
 
@@ -2824,7 +3121,7 @@ const suggestions = [
2824
3121
  // Localized routes sit under both bases (\`{deployment.base}/{basePath}/ar/…\`),
2825
3122
  // so the script strips the two together, trailing slash included.
2826
3123
  const { basePath } = data.config;
2827
- const siteRoot = withBase(withBasePath(basePath, "/"));
3124
+ const siteRoot = withMountedBase(mountBasePath(basePath, "/"));
2828
3125
  const localeBase = siteRoot.endsWith("/") ? siteRoot : siteRoot + "/";
2829
3126
  const localized = Object.fromEntries(
2830
3127
  (i18n?.locales ?? [])
@@ -2837,7 +3134,7 @@ const localized = Object.fromEntries(
2837
3134
  description: strings.description,
2838
3135
  dir: l.dir ?? "ltr",
2839
3136
  home: strings.home,
2840
- homeHref: withBase(withBasePath(basePath, "/" + l.code)),
3137
+ homeHref: withMountedBase(mountBasePath(basePath, "/" + l.code)),
2841
3138
  suggestions: strings.suggestions,
2842
3139
  title: strings.title,
2843
3140
  },
@@ -2887,7 +3184,7 @@ const localizedJson = JSON.stringify(localized).replaceAll("<", "\\\\u003c");
2887
3184
  <a
2888
3185
  class="mt-8 inline-flex items-center gap-1.5 rounded-full bg-accent py-2 pe-4 ps-3.5 text-sm font-medium text-accent-foreground transition-opacity hover:opacity-90"
2889
3186
  data-nf-home
2890
- href={withBase("/")}
3187
+ href={withMountedBase("/")}
2891
3188
  >
2892
3189
  <Icon class="rtl:-scale-x-100" name="arrow-left" size={14} />
2893
3190
  <span data-nf="home">{nf.home}</span>
@@ -2969,7 +3266,7 @@ const localizedJson = JSON.stringify(localized).replaceAll("<", "\\\\u003c");
2969
3266
  */
2970
3267
  export const notFoundMarkdownTemplate =
2971
3268
  (): string => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
2972
- import { withBase } from "blume/components/islands/base-path.ts";
3269
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
2973
3270
  import { absoluteUrl } from "blume/core/site-url.ts";
2974
3271
  import data from "blume:data";
2975
3272
 
@@ -2980,7 +3277,7 @@ const nf = data.ui.notFound;
2980
3277
  // Absolute for internal routes when the site is known; an external tab href
2981
3278
  // passes through untouched.
2982
3279
  const href = (path: string): string => {
2983
- const based = withBase(path);
3280
+ const based = withMountedBase(path);
2984
3281
  return data.config.site && based.startsWith("/") && !based.startsWith("//")
2985
3282
  ? absoluteUrl(data.config.site, based)
2986
3283
  : based;
@@ -3039,7 +3336,7 @@ export function GET() {
3039
3336
  export const notFoundJsonTemplate =
3040
3337
  (): string => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
3041
3338
  import { problem } from "blume/ai/api/problem.ts";
3042
- import { withBase } from "blume/components/islands/base-path.ts";
3339
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
3043
3340
  import { absoluteUrl } from "blume/core/site-url.ts";
3044
3341
  import data from "blume:data";
3045
3342
 
@@ -3050,7 +3347,7 @@ const nf = data.ui.notFound;
3050
3347
  // Absolute for internal routes when the site is known; an external tab href
3051
3348
  // passes through untouched.
3052
3349
  const href = (path: string): string => {
3053
- const based = withBase(path);
3350
+ const based = withMountedBase(path);
3054
3351
  return data.config.site && based.startsWith("/") && !based.startsWith("//")
3055
3352
  ? absoluteUrl(data.config.site, based)
3056
3353
  : based;
@@ -3225,7 +3522,7 @@ ${wrapperPropsType("Example")}
3225
3522
  /**
3226
3523
  * The route prefix `<Component />` preview frames are served under:
3227
3524
  * `{basePath}/blume-examples/<example path>`. `deployment.base` is layered on
3228
- * top by Astro (components apply it with `withBase`).
3525
+ * top by Astro (components apply it with `withMountedBase`).
3229
3526
  */
3230
3527
  export const examplesRouteBase = (basePath: string): string =>
3231
3528
  `${basePath}/blume-examples`;