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
@@ -5,6 +5,7 @@ export const zh: UIStringsOverride = {
5
5
  actions: {
6
6
  addToCursor: "添加到 Cursor",
7
7
  addToVscode: "添加到 VS Code",
8
+ askAboutCode: "询问这段代码",
8
9
  connectMcp: "连接到 MCP",
9
10
  copied: "已复制!",
10
11
  copyClaudeCode: "复制 Claude Code 命令",
@@ -30,11 +31,18 @@ export const zh: UIStringsOverride = {
30
31
  copy: "复制对话",
31
32
  empty: "就文档提出问题。",
32
33
  error: "抱歉,出了点问题。",
34
+ explainCode: "解释这段代码。",
33
35
  label: "提出问题",
36
+ open: "打开助手",
34
37
  placeholder: "提出问题…",
38
+ rateLimited: "你提的问题太多了,请几分钟后再试。",
39
+ removeCode: "移除代码",
35
40
  send: "发送",
41
+ support: "联系支持团队",
42
+ supportSubject: "来自文档的问题",
36
43
  tip: "提示:可通过以下快捷键打开和关闭聊天",
37
44
  title: "助手",
45
+ verifyFailed: "无法确认你是真人,请重试。",
38
46
  you: "你",
39
47
  },
40
48
  banner: { dismiss: "关闭公告" },
@@ -42,6 +50,14 @@ export const zh: UIStringsOverride = {
42
50
  description: "产品更新与版本说明。",
43
51
  title: "更新日志",
44
52
  },
53
+ consent: {
54
+ accept: "接受",
55
+ decline: "拒绝",
56
+ label: "Cookie 同意",
57
+ message: "我们希望使用 Cookie 来了解本文档的使用情况。",
58
+ policy: "隐私政策",
59
+ settings: "Cookie 设置",
60
+ },
45
61
  content: {
46
62
  code: "代码",
47
63
  copyColor: "复制颜色 {name} {value}",
@@ -52,6 +68,8 @@ export const zh: UIStringsOverride = {
52
68
  preview: "预览",
53
69
  prop: "属性",
54
70
  selectTab: "选择标签页",
71
+ selectView: "选择视图",
72
+ showLess: "显示更少",
55
73
  showMore: "显示更多",
56
74
  stars: "星标",
57
75
  tab: "标签页 {n}",
@@ -59,12 +77,36 @@ export const zh: UIStringsOverride = {
59
77
  update: "更新",
60
78
  },
61
79
  feedback: {
80
+ comment: "告诉我们更多(选填)",
62
81
  no: "没有帮助",
63
82
  question: "这个页面有帮助吗?",
83
+ send: "发送",
64
84
  thanks: "感谢你的反馈!",
65
85
  yes: "有帮助",
66
86
  },
67
87
  languageSwitcher: { label: "语言", untranslated: "未翻译" },
88
+ narration: {
89
+ cueDanger: "危险。",
90
+ cueInfo: "信息。",
91
+ cueNote: "备注。",
92
+ cueSection: "可展开部分。",
93
+ cueStep: "第 {n} 步。",
94
+ cueSuccess: "成功。",
95
+ cueTab: "{title} 标签页。",
96
+ cueTip: "提示。",
97
+ cueWarning: "警告。",
98
+ error: "无法加载朗读音频。",
99
+ follow: "跟随朗读",
100
+ label: "收听此页面",
101
+ minutes: "{n} 分钟",
102
+ next: "下一句",
103
+ pause: "暂停",
104
+ play: "播放",
105
+ previous: "上一句",
106
+ progress: "朗读进度",
107
+ speed: "播放速度",
108
+ stop: "停止收听",
109
+ },
68
110
  nav: {
69
111
  back: "返回",
70
112
  breadcrumb: "面包屑导航",
@@ -89,6 +131,7 @@ export const zh: UIStringsOverride = {
89
131
  next: "下一页",
90
132
  pagination: "分页",
91
133
  previous: "上一页",
134
+ related: "相关页面",
92
135
  skipToContent: "跳到内容",
93
136
  },
94
137
  search: {
@@ -0,0 +1,103 @@
1
+ import { nextFenceState } from "./code-fences.ts";
2
+ import type { FenceState } from "./code-fences.ts";
3
+
4
+ /**
5
+ * Content variables: `variables: { version: "2.1.0" }` in `blume.config.ts`
6
+ * makes `{{version}}` in a page read `2.1.0` wherever it appears in the body,
7
+ * code and links included. The syntax is Mintlify's, so migrated pages keep
8
+ * working.
9
+ *
10
+ * Substitution is textual, before any parsing, so a page's HTML, its search
11
+ * entry, its `.md` mirror, and `llms-full.txt` all agree: the MDX pipeline
12
+ * substitutes in a Vite transform ahead of the compiler (an MDX parser would
13
+ * read `{{version}}` as a JavaScript expression), the Markdown pipeline in a
14
+ * plugin over the parsed text, includes as their files are read, and the raw
15
+ * readers through {@link substituteVariables}. Only defined names are
16
+ * replaced; an undefined one in prose is a build error
17
+ * (`BLUME_UNDEFINED_VARIABLE`), and inside code it is left alone, since
18
+ * templating examples (`{{name}}` in a Handlebars snippet) are common there.
19
+ */
20
+
21
+ /** The variables a site defines, name to value. */
22
+ export type ContentVariables = Readonly<Record<string, string>>;
23
+
24
+ /** A variable name: letters, digits, `_`, and `-` (Mintlify's `api-url`). */
25
+ export const VARIABLE_NAME = /^[\w-]+$/u;
26
+
27
+ /** A `{{name}}` reference, spaces inside the braces allowed. */
28
+ const REFERENCE = /\{\{\s*(?<name>[\w-]+)\s*\}\}/gu;
29
+
30
+ /** Whether a site defines any variables, so the rest can be skipped. */
31
+ export const hasVariables = (
32
+ variables?: ContentVariables
33
+ ): variables is ContentVariables =>
34
+ variables !== undefined && Object.keys(variables).length > 0;
35
+
36
+ /** `text` with every reference to a defined variable replaced by its value. */
37
+ export const substituteVariables = (
38
+ text: string,
39
+ variables?: ContentVariables
40
+ ): string => {
41
+ if (!(hasVariables(variables) && text.includes("{{"))) {
42
+ return text;
43
+ }
44
+ return text.replaceAll(REFERENCE, (match, name: string) =>
45
+ Object.hasOwn(variables, name) ? (variables[name] ?? match) : match
46
+ );
47
+ };
48
+
49
+ // A leading front matter block: variables apply to the body, not to it.
50
+ const FRONT_MATTER = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u;
51
+
52
+ /**
53
+ * {@link substituteVariables} over a page's raw source, leaving its front
54
+ * matter as written: the rendered page reads its title and description from
55
+ * the unsubstituted front matter, so the other surfaces must too.
56
+ */
57
+ export const substituteVariablesInBody = (
58
+ source: string,
59
+ variables?: ContentVariables
60
+ ): string => {
61
+ const head = FRONT_MATTER.exec(source)?.[0] ?? "";
62
+ return head + substituteVariables(source.slice(head.length), variables);
63
+ };
64
+
65
+ /** An undefined variable referenced in prose, with its 1-based line. */
66
+ export interface UndefinedVariable {
67
+ line: number;
68
+ name: string;
69
+ }
70
+
71
+ // A backtick code span: the same run of backticks opens and closes it.
72
+ const CODE_SPAN = /(?<ticks>`+)[^`][\s\S]*?\k<ticks>/gu;
73
+
74
+ /**
75
+ * The undefined variables `text` references outside code: fenced blocks and
76
+ * inline code spans are skipped, so a templating example never fails a
77
+ * build. Empty when the site defines no variables at all, so a page that
78
+ * shows `{{…}}` literally keeps working until the site opts in.
79
+ */
80
+ export const undefinedVariables = (
81
+ text: string,
82
+ variables?: ContentVariables
83
+ ): UndefinedVariable[] => {
84
+ if (!(hasVariables(variables) && text.includes("{{"))) {
85
+ return [];
86
+ }
87
+ const found: UndefinedVariable[] = [];
88
+ let fence: FenceState = null;
89
+ for (const [index, line] of text.split("\n").entries()) {
90
+ const wasInFence = fence !== null;
91
+ fence = nextFenceState(line, fence);
92
+ if (wasInFence || fence !== null) {
93
+ continue;
94
+ }
95
+ for (const match of line.replaceAll(CODE_SPAN, "").matchAll(REFERENCE)) {
96
+ const name = match.groups?.name ?? "";
97
+ if (!Object.hasOwn(variables, name)) {
98
+ found.push({ line: index + 1, name });
99
+ }
100
+ }
101
+ }
102
+ return found;
103
+ };
@@ -14,6 +14,11 @@ import { API_CATALOG_PATH, buildApiCatalog } from "../ai/api-catalog.ts";
14
14
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
15
15
  import { buildLlmsFiles } from "../ai/llms.ts";
16
16
  import { markdownRoutePaths } from "../ai/markdown.ts";
17
+ import {
18
+ buildSiteSkill,
19
+ SKILL_MD_PATH,
20
+ siteSkillName,
21
+ } from "../ai/site-skill.ts";
17
22
  import {
18
23
  AGENT_SKILLS_DIR,
19
24
  AGENT_SKILLS_INDEX_PATH,
@@ -29,6 +34,7 @@ import { normalizeBasePath } from "../core/base-path.ts";
29
34
  import { discoverPagesSync } from "../core/custom-pages.ts";
30
35
  import type { BlumeProject } from "../core/project-graph.ts";
31
36
  import type { ResolvedConfig } from "../core/schema.ts";
37
+ import { buildNarration } from "../narration/build.ts";
32
38
  import { buildSearchIndex } from "../search/build.ts";
33
39
  import { syncSearchProvider } from "../search/sync/index.ts";
34
40
  import { readsHeaderFiles } from "./adapter-output.ts";
@@ -212,7 +218,8 @@ export const emitHeaderFiles = async (
212
218
 
213
219
  /**
214
220
  * Collect the Agent Skills `agents.skills` publishes, once per build, so both the
215
- * skills surface and llms.txt (which lists them) read the same set. Empty
221
+ * skills surface and llms.txt (which lists them) read the same set (the
222
+ * generated site skill joins them in {@link publishedSkills}). Empty
216
223
  * when the feature is off, the directory is missing, nothing in it is
217
224
  * publishable (each with a warning), or a user-shipped
218
225
  * `public/.well-known/agent-skills/index.json` already owns the surface.
@@ -248,6 +255,57 @@ const collectConfiguredSkills = async (
248
255
  return skills;
249
256
  };
250
257
 
258
+ /**
259
+ * Every skill the build publishes: the `agents.skills` ones, plus the
260
+ * generated site skill unless one of those shares its name (the hand-written
261
+ * one wins) or a user-shipped index owns the surface.
262
+ */
263
+ const publishedSkills = async (
264
+ project: BlumeProject,
265
+ distDir: string,
266
+ surface: { mcp: boolean; userIndex: boolean },
267
+ logger: ArtifactLogger
268
+ ): Promise<SkillArtifact[]> => {
269
+ const configured = await collectConfiguredSkills(project, distDir, logger);
270
+ // The skill lists the MCP server only when the build serves it.
271
+ const site = surface.userIndex
272
+ ? null
273
+ : buildSiteSkill({
274
+ ...project,
275
+ config: advertisedConfig(project.config, {
276
+ mcp: surface.mcp,
277
+ skills: true,
278
+ }),
279
+ });
280
+ return site && !configured.some((skill) => skill.name === site.name)
281
+ ? [site, ...configured]
282
+ : configured;
283
+ };
284
+
285
+ /**
286
+ * Serve the site skill at `/skill.md` too, the path agents try first: the
287
+ * generated one, or a same-named hand-written skill that is a lone
288
+ * `SKILL.md` (an archive's relative references need its other files). A
289
+ * `public/skill.md` wins.
290
+ */
291
+ const emitSkillMd = async (
292
+ project: BlumeProject,
293
+ distDir: string,
294
+ skills: readonly SkillArtifact[],
295
+ logger: ArtifactLogger
296
+ ): Promise<void> => {
297
+ const target = join(distDir, SKILL_MD_PATH.slice(1));
298
+ const name = siteSkillName(project);
299
+ const skill = skills.find(
300
+ (candidate) => candidate.name === name && candidate.type === "skill-md"
301
+ );
302
+ if (!(project.config.agents.skillMd && skill) || existsSync(target)) {
303
+ return;
304
+ }
305
+ await writeFile(target, skill.content);
306
+ logger.info(`Generated ${SKILL_MD_PATH.slice(1)} (the "${name}" skill)`);
307
+ };
308
+
251
309
  /**
252
310
  * Publish the collected Agent Skills: copy each skill artifact under
253
311
  * `.well-known/agent-skills/` and emit the discovery index. A user-shipped
@@ -382,6 +440,10 @@ export const publishBuildArtifacts = async (
382
440
  logger.info(`Indexed ${indexed} page(s) for search`);
383
441
  }
384
442
 
443
+ // Generated narration reads the built pages, so it runs once they exist.
444
+ // Nothing happens without `narration.provider`.
445
+ await buildNarration(project, distDir, logger);
446
+
385
447
  // Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
386
448
  // Skipped with a warning when its admin key isn't configured.
387
449
  await syncSearchProvider(project, {
@@ -394,18 +456,24 @@ export const publishBuildArtifacts = async (
394
456
  const userSkillsIndex = existsSync(
395
457
  join(distDir, AGENT_SKILLS_INDEX_PATH.slice(1))
396
458
  );
397
- const skills = await collectConfiguredSkills(project, distDir, logger);
459
+ const mcp = servesMcp(
460
+ project,
461
+ project.context.pagesRoot
462
+ ? discoverPagesSync(project.context.pagesRoot)
463
+ : []
464
+ );
465
+ const skills = await publishedSkills(
466
+ project,
467
+ distDir,
468
+ { mcp, userIndex: userSkillsIndex },
469
+ logger
470
+ );
398
471
  // The discovery documents advertise what this build serves: no MCP server
399
472
  // when a page took its route, no skills index when nothing was published.
400
473
  const advertised: BlumeProject = {
401
474
  ...project,
402
475
  config: advertisedConfig(project.config, {
403
- mcp: servesMcp(
404
- project,
405
- project.context.pagesRoot
406
- ? discoverPagesSync(project.context.pagesRoot)
407
- : []
408
- ),
476
+ mcp,
409
477
  skills: skills.length > 0 || userSkillsIndex,
410
478
  }),
411
479
  };
@@ -444,6 +512,7 @@ export const publishBuildArtifacts = async (
444
512
 
445
513
  await emitWellKnownFiles(advertised.config, distDir, skills, logger);
446
514
  await emitAgentSkills(project, distDir, skills, logger);
515
+ await emitSkillMd(project, distDir, skills, logger);
447
516
 
448
517
  await emitRedirectFiles(advertised, distDir, logger);
449
518
  await emitHeaderFiles(advertised, distDir, logger);
@@ -85,6 +85,7 @@ import {
85
85
  OPENAPI_PATH,
86
86
  } from "../ai/api/paths.ts";
87
87
  import { normalizeBasePath, normalizePath } from "../core/base-path.ts";
88
+ import { compileRedirects, isPatternPath } from "../core/redirect-patterns.ts";
88
89
  import { CONTENT_ASSETS_ROOT, SVG_ASSET_HEADERS } from "./headers.ts";
89
90
  import type { NotFoundVariants } from "./vercel-negotiation.ts";
90
91
 
@@ -289,12 +290,18 @@ export const buildNegotiationWorker = (
289
290
  // header, which cannot carry non-ASCII.
290
291
  const redirects = JSON.stringify(
291
292
  Object.fromEntries(
292
- (options.redirects ?? []).map((redirect) => [
293
- normalizePath(redirect.from),
294
- [encodeURI(redirect.to), redirect.status],
295
- ])
293
+ (options.redirects ?? [])
294
+ .filter((redirect) => !isPatternPath(redirect.from))
295
+ .map((redirect) => [
296
+ normalizePath(redirect.from),
297
+ [encodeURI(redirect.to), redirect.status],
298
+ ])
296
299
  )
297
300
  );
301
+ // Patterns (`/beta/:slug*`) are tried in order after the exact paths.
302
+ const patternRedirects = JSON.stringify(
303
+ compileRedirects(options.redirects ?? [])
304
+ );
298
305
  return `// Generated by Blume. Do not edit; this file is recreated on each build.
299
306
  //
300
307
  // Request-time \`Accept: text/markdown\` negotiation for a Cloudflare server
@@ -319,6 +326,9 @@ const ASSETS_BINDING = ${binding};
319
326
  const HOME_LINK_HEADER = ${homeLinkHeader};
320
327
  const HOME_TOKENS = ${homeTokens};
321
328
  const REDIRECTS = ${redirects};
329
+ const PATTERN_REDIRECTS = ${patternRedirects}.map(
330
+ ([source, location, status]) => [new RegExp(source, "u"), location, status]
331
+ );
322
332
  const NOT_FOUND = ${notFound};
323
333
  const SVG_ASSET_HEADERS = ${JSON.stringify(SVG_ASSET_HEADERS)};
324
334
 
@@ -335,11 +345,31 @@ const isSvgAsset = (pathname) => {
335
345
  // reads — a worker-first route never reaches it. Answering from this baked-in
336
346
  // copy keeps the exact configured status; delegating instead would let Astro's
337
347
  // SSR handler default a GET to a permanent 301.
348
+ // An exact path is looked up first, then each pattern is tried in order, its
349
+ // Location template filled with the captures, encoded so a capture can't grow
350
+ // a query or fragment.
338
351
  const redirectFor = (pathname) => {
339
352
  const trimmed =
340
353
  pathname !== "/" && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
341
354
  const path = safeDecode(trimmed);
342
- return Object.hasOwn(REDIRECTS, path) ? REDIRECTS[path] : null;
355
+ if (Object.hasOwn(REDIRECTS, path)) {
356
+ return REDIRECTS[path];
357
+ }
358
+ const decoded = safeDecode(pathname);
359
+ for (const [pattern, location, status] of PATTERN_REDIRECTS) {
360
+ const match = pattern.exec(decoded);
361
+ if (match) {
362
+ return [
363
+ location.replace(/\\$(\\d+)/gu, (_reference, index) =>
364
+ encodeURI(match[Number(index)] ?? "")
365
+ .replaceAll("?", "%3F")
366
+ .replaceAll("#", "%23")
367
+ ),
368
+ status,
369
+ ];
370
+ }
371
+ }
372
+ return null;
343
373
  };
344
374
 
345
375
  // The request path with the deployment base removed: the whole path on a
@@ -4,6 +4,7 @@ import {
4
4
  API_CATALOG_TYPE,
5
5
  hasApiCatalog,
6
6
  } from "../ai/api-catalog.ts";
7
+ import { servesSkillsIndex } from "../ai/skills.ts";
7
8
  import {
8
9
  SIGNATURES_DIRECTORY_PATH,
9
10
  SIGNATURES_DIRECTORY_TYPE,
@@ -128,7 +129,7 @@ export const headerRules = (
128
129
  }
129
130
  // Published skills live at the deployment base, and the RFC wants their
130
131
  // archives served as application/gzip explicitly.
131
- if (config.agents.skills) {
132
+ if (servesSkillsIndex(config)) {
132
133
  rules.push(
133
134
  {
134
135
  name: "Content-Type",
@@ -15,6 +15,8 @@ import {
15
15
  } from "../ai/web-bot-auth.ts";
16
16
  import { normalizeBasePath, normalizePath } from "../core/base-path.ts";
17
17
  import type { BlumeProject } from "../core/project-graph.ts";
18
+ import { compileRedirects, isPatternPath } from "../core/redirect-patterns.ts";
19
+ import type { CompiledRedirect } from "../core/redirect-patterns.ts";
18
20
  import type { ResolvedConfig } from "../core/schema.ts";
19
21
  import { SVG_ASSET_HEADERS, svgAssetPath } from "./headers.ts";
20
22
  import { distDir } from "./platforms/paths.ts";
@@ -106,11 +108,12 @@ const patternRules = (
106
108
  export type NodeRedirects = Record<string, [string, number]>;
107
109
 
108
110
  /**
109
- * The configured redirects, based the way the host matches them (see
111
+ * The configured exact redirects, based the way the host matches them (see
110
112
  * `platformRedirects`) and keyed for the wrapper's lookup, the `Location`
111
113
  * percent-encoded as Astro sends it. A redirect at a content route's own path
112
114
  * is left out: the prerendered page owns that URL, and the static handler
113
- * serves it before Astro's redirect ever could.
115
+ * serves it before Astro's redirect ever could. Patterns are compiled apart
116
+ * ({@link nodePatternRedirects}).
114
117
  */
115
118
  export const nodeRedirects = (project: BlumeProject): NodeRedirects => {
116
119
  const { config } = project;
@@ -125,7 +128,13 @@ export const nodeRedirects = (project: BlumeProject): NodeRedirects => {
125
128
  );
126
129
  return Object.fromEntries(
127
130
  platformRedirects(project)
128
- .filter((redirect) => !pages.has(normalizePath(redirect.from)))
131
+ .filter(
132
+ (redirect) =>
133
+ !(
134
+ isPatternPath(redirect.from) ||
135
+ pages.has(normalizePath(redirect.from))
136
+ )
137
+ )
129
138
  .map((redirect) => [
130
139
  normalizePath(redirect.from),
131
140
  [encodeURI(redirect.to), redirect.status],
@@ -133,12 +142,27 @@ export const nodeRedirects = (project: BlumeProject): NodeRedirects => {
133
142
  );
134
143
  };
135
144
 
145
+ /**
146
+ * The configured pattern redirects (`/beta/:slug*`), based the way the host
147
+ * matches them and compiled for the wrapper, which tries them in order after
148
+ * the exact ones. None can match a page: the scan rejects a pattern that
149
+ * does (`BLUME_REDIRECT_MATCHES_PAGE`).
150
+ */
151
+ export const nodePatternRedirects = (
152
+ project: BlumeProject
153
+ ): CompiledRedirect[] =>
154
+ project.config.redirects.length === 0
155
+ ? []
156
+ : compileRedirects(platformRedirects(project));
157
+
136
158
  /** What the wrapper needs beyond the header rules. */
137
159
  export interface NodeEntryOptions {
138
160
  /** `deployment.base`, normalized (`""` at the root). */
139
161
  base?: string;
140
162
  /** The configured redirects, from {@link nodeRedirects}. */
141
163
  redirects?: NodeRedirects;
164
+ /** The configured pattern redirects, from {@link nodePatternRedirects}. */
165
+ patternRedirects?: CompiledRedirect[];
142
166
  }
143
167
 
144
168
  /**
@@ -177,6 +201,9 @@ const BASE = ${JSON.stringify(options.base ?? "")};
177
201
  const RULES = new Map(${JSON.stringify(exactRules(rules))});
178
202
  const PATTERNS = ${JSON.stringify(patternRules(rules))};
179
203
  const REDIRECTS = ${JSON.stringify(options.redirects ?? {})};
204
+ const PATTERN_REDIRECTS = ${JSON.stringify(options.patternRedirects ?? [])}.map(
205
+ ([source, location, status]) => [new RegExp(source, "u"), location, status]
206
+ );
180
207
  const safeDecode = (path) => {
181
208
  try {
182
209
  return decodeURIComponent(path);
@@ -210,16 +237,37 @@ const applyHeaders = (req, res) => {
210
237
  }
211
238
  }
212
239
  };
240
+ // A pattern redirect's Location: its template, each $n filled with that
241
+ // capture, encoded so it can't grow a query or fragment.
242
+ const fillLocation = (location, match) =>
243
+ location.replace(/\\$(\\d+)/gu, (_reference, index) =>
244
+ encodeURI(match[Number(index)] ?? "")
245
+ .replaceAll("?", "%3F")
246
+ .replaceAll("#", "%23")
247
+ );
248
+ // The configured redirect for a decoded path: an exact one, else the first
249
+ // pattern that matches it.
250
+ const redirectFor = (path) => {
251
+ const trimmed = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
252
+ if (Object.hasOwn(REDIRECTS, trimmed)) {
253
+ return REDIRECTS[trimmed];
254
+ }
255
+ for (const [pattern, location, status] of PATTERN_REDIRECTS) {
256
+ const match = pattern.exec(path);
257
+ if (match) {
258
+ return [fillLocation(location, match), status];
259
+ }
260
+ }
261
+ return null;
262
+ };
213
263
  // Answer a configured redirect with its exact status; Astro's handler would
214
264
  // send a 301 (a 308 for other methods) whatever the configured one.
215
265
  const answerRedirect = (req, res) => {
216
- const path = urlPath(req);
217
- const trimmed = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
218
- const key = safeDecode(trimmed);
219
- if (!Object.hasOwn(REDIRECTS, key)) {
266
+ const redirect = redirectFor(safeDecode(urlPath(req)));
267
+ if (redirect === null) {
220
268
  return false;
221
269
  }
222
- const [location, status] = REDIRECTS[key];
270
+ const [location, status] = redirect;
223
271
  res.writeHead(status, { location });
224
272
  res.end();
225
273
  return true;
@@ -291,7 +339,12 @@ export const wrapNodeEntry = async (
291
339
  : []),
292
340
  ];
293
341
  const redirects = nodeRedirects(project);
294
- if (rules.length === 0 && Object.keys(redirects).length === 0) {
342
+ const patternRedirects = nodePatternRedirects(project);
343
+ if (
344
+ rules.length === 0 &&
345
+ Object.keys(redirects).length === 0 &&
346
+ patternRedirects.length === 0
347
+ ) {
295
348
  return;
296
349
  }
297
350
  const serverDir = join(distDir(project.context), "server");
@@ -311,6 +364,7 @@ export const wrapNodeEntry = async (
311
364
  entry,
312
365
  nodeEntryWrapper(rules, {
313
366
  base: normalizeBasePath(project.config.deployment.options.base),
367
+ patternRedirects,
314
368
  redirects,
315
369
  }),
316
370
  "utf-8"
@@ -14,6 +14,7 @@ import {
14
14
  import { normalizeBasePath } from "../../core/base-path.ts";
15
15
  import type { BlumeProject } from "../../core/project-graph.ts";
16
16
  import type { ProjectContext } from "../../core/types.ts";
17
+ import { withRateLimitBinding } from "../../ratelimit/wrangler.ts";
17
18
  import { CLOUDFLARE_ADAPTER_PACKAGE } from "../adapters/cloudflare.ts";
18
19
  import {
19
20
  injectWorkerNegotiation,
@@ -273,6 +274,36 @@ export const nameCloudflareWorker = async (
273
274
  );
274
275
  };
275
276
 
277
+ /**
278
+ * Declare the Workers rate limiting binding `rateLimit: cloudflare()` counts
279
+ * with in the built Worker's config, which `wrangler deploy` reads.
280
+ */
281
+ export const bindCloudflareRateLimit = async (
282
+ project: BlumeProject,
283
+ log: BuildLog
284
+ ): Promise<void> => {
285
+ const { rateLimit } = project.config;
286
+ if (rateLimit?.kind !== "cloudflare") {
287
+ return;
288
+ }
289
+ const wranglerPath = join(
290
+ distDir(project.context),
291
+ "server",
292
+ "wrangler.json"
293
+ );
294
+ const bound = existsSync(wranglerPath)
295
+ ? withRateLimitBinding(await readFile(wranglerPath, "utf-8"), rateLimit)
296
+ : null;
297
+ if (bound === null) {
298
+ log.warn(
299
+ "Could not declare the rate limiting binding in dist/server/wrangler.json — the routes count in memory instead."
300
+ );
301
+ return;
302
+ }
303
+ await writeFile(wranglerPath, bound, "utf-8");
304
+ log.success("Declared the Workers rate limiting binding");
305
+ };
306
+
276
307
  /**
277
308
  * Cloudflare Workers and Pages. A server build emits the Worker into
278
309
  * `dist/server` and serves `dist/client` through the ASSETS binding, which
@@ -298,6 +329,7 @@ export const cloudflarePlatform: DeployPlatform = {
298
329
  if (!isolated) {
299
330
  await nameCloudflareWorker(project, log);
300
331
  await emitCloudflareNegotiation(project, log);
332
+ await bindCloudflareRateLimit(project, log);
301
333
  await emitCloudflareDeployConfig(project.context);
302
334
  }
303
335
  return true;
@@ -9,7 +9,11 @@ import type { BlumeProject } from "../../core/project-graph.ts";
9
9
  import { NETLIFY_ADAPTER_PACKAGE } from "../adapters/netlify.ts";
10
10
  import { buildNetlifyConfigHeaders } from "../headers.ts";
11
11
  import type { NetlifyConfigHeaders } from "../headers.ts";
12
- import { buildNetlifyRedirects } from "../redirects.ts";
12
+ import {
13
+ buildNetlifyRedirects,
14
+ netlifyPatternRedirects,
15
+ platformRedirects,
16
+ } from "../redirects.ts";
13
17
  import { distDir, toSiteUrl } from "./paths.ts";
14
18
  import type { BuildLog, DeployPlatform, RedirectFile } from "./types.ts";
15
19
 
@@ -76,6 +80,33 @@ export const emitNetlifyHeaders = async (
76
80
  await writeFile(configPath, JSON.stringify(frameworks), "utf-8");
77
81
  };
78
82
 
83
+ /**
84
+ * Carry the pattern redirects (`/beta/:slug*`) into a server build, as
85
+ * `redirects` in the Frameworks API config: Astro's `redirects` carry only
86
+ * the exact ones, since it can't prerender a pattern's pages, so the adapter
87
+ * never writes these. Netlify appends them after any rules of the user's own.
88
+ */
89
+ export const emitNetlifyPatternRedirects = async (
90
+ project: BlumeProject
91
+ ): Promise<void> => {
92
+ const ours = netlifyPatternRedirects(platformRedirects(project));
93
+ const configPath = join(project.context.root, NETLIFY_CONFIG_FILE);
94
+ if (ours.length === 0 || !existsSync(configPath)) {
95
+ return;
96
+ }
97
+ // Only `redirects` is read; every other key rides through as parsed.
98
+ const frameworks: { redirects?: { from: string }[] } = JSON.parse(
99
+ await readFile(configPath, "utf-8")
100
+ );
101
+ // A second pass over the same build replaces its own entries.
102
+ const froms = new Set(ours.map((entry) => entry.from));
103
+ frameworks.redirects = [
104
+ ...(frameworks.redirects ?? []).filter((entry) => !froms.has(entry.from)),
105
+ ...ours,
106
+ ];
107
+ await writeFile(configPath, JSON.stringify(frameworks), "utf-8");
108
+ };
109
+
79
110
  /**
80
111
  * Netlify. A server build runs on Netlify Functions from the Frameworks API
81
112
  * tree the adapter writes to `.netlify/v1`, relative to the Astro root —
@@ -106,6 +137,7 @@ export const netlifyPlatform: DeployPlatform = {
106
137
  // never surfaces it.
107
138
  if (!isolated) {
108
139
  await emitNetlifyHeaders(project, log);
140
+ await emitNetlifyPatternRedirects(project);
109
141
  }
110
142
  return true;
111
143
  },