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
@@ -3,7 +3,7 @@ import { cp, readdir, readFile, rm } from "node:fs/promises";
3
3
 
4
4
  import { join, relative } from "pathe";
5
5
 
6
- import { stripBasePath } from "./base-path.ts";
6
+ import { mountBasePath, stripBasePath } from "./base-path.ts";
7
7
  import { nextFenceState } from "./code-fences.ts";
8
8
  import type { FenceState } from "./code-fences.ts";
9
9
  import { writeTextAtomic } from "./fs-atomic.ts";
@@ -45,18 +45,22 @@ export class CutError extends Error {
45
45
  // matching so replacements can splice into the real line by index (the mask
46
46
  // preserves length). Mirrors the content-assets rewriter.
47
47
  const INLINE_CODE = /`[^`]*`/gu;
48
- // A markdown link/image target or an HTML href/src attribute whose value is
49
- // root-absolute. Named groups carry the prefix (kept) and target (rewritten).
50
- const ROOT_LINK = /(?<prefix>\]\(|href="|src=")(?<target>\/[^\s"')]*)/gu;
48
+ // A root-absolute markdown link/image target, reference-style link definition
49
+ // (`[setup]: /guides/setup`, up to three spaces in, never a `[^note]:`
50
+ // footnote), or HTML href/src attribute in either quote style. Named groups
51
+ // carry the prefix (kept) and target (rewritten).
52
+ const ROOT_LINK =
53
+ /(?<prefix>\]\(|^ {0,3}\[(?!\^)[^\]]+\]:[ \t]*<?|(?:href|src)=["'])(?<target>\/[^\s"'<>)]*)/gu;
51
54
 
52
55
  /**
53
56
  * Build the link-rewrite table: every current-version route (basePath
54
- * stripped, since authors write root-absolute links as if mounted at root)
55
- * mapped to the same page's route inside the new snapshot. Only pages the
56
- * snapshot actually contains qualify — filesystem pages under the content
57
- * root. Spec-rendered references (`/api`, `/events`) and remote sources
58
- * aren't copied, so links to them keep pointing at the live pages instead of
59
- * a 404 inside the snapshot.
57
+ * stripped, since authors write root-absolute links as if mounted at root;
58
+ * a link that spells the base out is looked up without it, see
59
+ * `snapshotTarget`) mapped to the same page's route inside the new snapshot.
60
+ * Only pages the snapshot actually contains qualify — filesystem pages under
61
+ * the content root. Spec-rendered references (`/api`, `/events`) and remote
62
+ * sources aren't copied, so links to them keep pointing at the live pages
63
+ * instead of a 404 inside the snapshot.
60
64
  */
61
65
  const buildRouteRewrites = (
62
66
  project: BlumeProject,
@@ -105,8 +109,35 @@ const buildRouteRewrites = (
105
109
  return rewrites;
106
110
  };
107
111
 
112
+ /**
113
+ * The snapshot link a root-absolute path rewrites to, or undefined when the
114
+ * snapshot has no copy of its page. A path written with the base path by hand
115
+ * (`/docs/guides/x` under `basePath: "/docs"`) names the same page as
116
+ * `/guides/x` — rendering leaves an already-based link alone (see
117
+ * `withBasePath`) — so it is looked up without the base and keeps it.
118
+ */
119
+ const snapshotTarget = (
120
+ path: string,
121
+ rewrites: Map<string, string>,
122
+ basePath: string
123
+ ): string | undefined => {
124
+ const based =
125
+ basePath !== "" && (path === basePath || path.startsWith(`${basePath}/`));
126
+ if (!based) {
127
+ return rewrites.get(path);
128
+ }
129
+ const replacement = rewrites.get(stripBasePath(basePath, path));
130
+ return replacement === undefined
131
+ ? undefined
132
+ : mountBasePath(basePath, replacement);
133
+ };
134
+
108
135
  /** Rewrite one line's root-absolute internal links via the rewrite table. */
109
- const rewriteLine = (line: string, rewrites: Map<string, string>): string => {
136
+ const rewriteLine = (
137
+ line: string,
138
+ rewrites: Map<string, string>,
139
+ basePath: string
140
+ ): string => {
110
141
  const masked = line.replaceAll(INLINE_CODE, (span) =>
111
142
  " ".repeat(span.length)
112
143
  );
@@ -120,7 +151,7 @@ const rewriteLine = (line: string, rewrites: Map<string, string>): string => {
120
151
  const path = hash === -1 ? target : target.slice(0, hash);
121
152
  const suffix = hash === -1 ? "" : target.slice(hash);
122
153
  const bare = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
123
- const replacement = rewrites.get(bare);
154
+ const replacement = snapshotTarget(bare, rewrites, basePath);
124
155
  if (replacement === undefined) {
125
156
  continue;
126
157
  }
@@ -142,10 +173,12 @@ export interface SnapshotRewrite {
142
173
  * Rewrite a copied page's root-absolute internal links to their snapshot
143
174
  * equivalents, skipping fenced and inline code. Relative links need no
144
175
  * rewriting — the whole tree copies together, so they stay self-contained.
176
+ * `basePath` is the site's, for links that spell it out by hand.
145
177
  */
146
178
  export const rewriteSnapshotLinks = (
147
179
  source: string,
148
- rewrites: Map<string, string>
180
+ rewrites: Map<string, string>,
181
+ basePath = ""
149
182
  ): SnapshotRewrite => {
150
183
  let fence: FenceState = null;
151
184
  let count = 0;
@@ -156,7 +189,7 @@ export const rewriteSnapshotLinks = (
156
189
  if (inFence) {
157
190
  return line;
158
191
  }
159
- const rewrittenLine = rewriteLine(line, rewrites);
192
+ const rewrittenLine = rewriteLine(line, rewrites, basePath);
160
193
  if (rewrittenLine !== line) {
161
194
  count += 1;
162
195
  }
@@ -416,7 +449,11 @@ export const cutVersion = async (
416
449
  return;
417
450
  }
418
451
  const source = await readFile(abs, "utf-8");
419
- const { text, count } = rewriteSnapshotLinks(source, rewrites);
452
+ const { text, count } = rewriteSnapshotLinks(
453
+ source,
454
+ rewrites,
455
+ project.config.basePath
456
+ );
420
457
  if (count > 0) {
421
458
  await writeTextAtomic(abs, text);
422
459
  rewritten.push({ count, file: relative(dir, abs) });
@@ -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,
@@ -25,9 +30,11 @@ import {
25
30
  buildSignaturesDirectory,
26
31
  SIGNATURES_DIRECTORY_PATH,
27
32
  } from "../ai/web-bot-auth.ts";
33
+ import { normalizeBasePath } from "../core/base-path.ts";
28
34
  import { discoverPagesSync } from "../core/custom-pages.ts";
29
35
  import type { BlumeProject } from "../core/project-graph.ts";
30
36
  import type { ResolvedConfig } from "../core/schema.ts";
37
+ import { buildNarration } from "../narration/build.ts";
31
38
  import { buildSearchIndex } from "../search/build.ts";
32
39
  import { syncSearchProvider } from "../search/sync/index.ts";
33
40
  import { readsHeaderFiles } from "./adapter-output.ts";
@@ -51,10 +58,12 @@ import { buildSitemapFiles, describeSitemapFiles } from "./sitemap.ts";
51
58
  * Written from the integration's `astro:build:done` hook — Astro's channel
52
59
  * for exactly this kind of post-build work — into the directory Astro
53
60
  * reports as the client output (`dist/`, or `dist/client` for a server
54
- * build). The Vercel adapter copies that directory into its Build Output
55
- * static tree in a later hook, so the artifacts ride along; every other
56
- * adapter serves it directly. Running inside the hook rather than after
57
- * `astro build` returns means an ejected project keeps producing them.
61
+ * build, or `dist/client/<base>/` on Cloudflare, whose `_headers` goes to
62
+ * the root the platform serves above it). The Vercel adapter copies that
63
+ * directory into its Build Output static tree in a later hook, so the
64
+ * artifacts ride along; every other adapter serves it directly. Running
65
+ * inside the hook rather than after `astro build` returns means an ejected
66
+ * project keeps producing them.
58
67
  *
59
68
  * Every file yields to one the user ships in `public/` (Astro copied it into
60
69
  * the output before this runs).
@@ -88,7 +97,7 @@ const emitRedirectFiles = async (
88
97
  return;
89
98
  }
90
99
  const platform = deployPlatform(config.deployment);
91
- const redirects = platformRedirects(config);
100
+ const redirects = platformRedirects(project);
92
101
  const vercelHeaders = platform.redirectFiles.includes(VERCEL_JSON_FILE)
93
102
  ? buildVercelHeaders(
94
103
  config,
@@ -127,6 +136,24 @@ const emitRedirectFiles = async (
127
136
  logger.info(`Emitted redirect files for ${redirects.length} redirect(s)`);
128
137
  };
129
138
 
139
+ /**
140
+ * The root of the static assets the platform serves, where it reads
141
+ * `_headers`: `distDir` itself, except on a server build whose adapter moved
142
+ * the client output under `deployment.base` (`@astrojs/cloudflare` hands
143
+ * `astro:build:done` `dist/client/<base>/` but serves `dist/client`). A file
144
+ * left in `distDir` there would never be read, and would be served publicly
145
+ * at `<base>/_headers`.
146
+ */
147
+ const assetsRoot = (config: ResolvedConfig, distDir: string): string => {
148
+ const base = normalizeBasePath(config.deployment.options.base);
149
+ const nested =
150
+ config.deployment.options.output === "server" &&
151
+ deployPlatform(config.deployment).serverClientUnderBase;
152
+ // One `..` per base segment climbs back out of `dist/client/<base>/`.
153
+ const depth = base.split("/").length - 1;
154
+ return nested && base ? join(distDir, "../".repeat(depth)) : distDir;
155
+ };
156
+
130
157
  /**
131
158
  * Emit a `_headers` file so Netlify / Cloudflare serve the raw AI-ready
132
159
  * endpoints (`*.md`, `*.mdx`, `*.txt`) with an explicit `charset=utf-8`. Without
@@ -179,7 +206,7 @@ export const emitHeaderFiles = async (
179
206
  // An adapter may have written its own rules here already (Cloudflare adds an
180
207
  // immutable Cache-Control for /_astro/*). Keep them and append ours: both
181
208
  // sets are wanted, and `_headers` has no merge semantics beyond order.
182
- const target = join(distDir, "_headers");
209
+ const target = join(assetsRoot(config, distDir), "_headers");
183
210
  const existing = existsSync(target) ? await readFile(target, "utf-8") : "";
184
211
  await writeFile(
185
212
  target,
@@ -191,7 +218,8 @@ export const emitHeaderFiles = async (
191
218
 
192
219
  /**
193
220
  * Collect the Agent Skills `agents.skills` publishes, once per build, so both the
194
- * 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
195
223
  * when the feature is off, the directory is missing, nothing in it is
196
224
  * publishable (each with a warning), or a user-shipped
197
225
  * `public/.well-known/agent-skills/index.json` already owns the surface.
@@ -227,6 +255,57 @@ const collectConfiguredSkills = async (
227
255
  return skills;
228
256
  };
229
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
+
230
309
  /**
231
310
  * Publish the collected Agent Skills: copy each skill artifact under
232
311
  * `.well-known/agent-skills/` and emit the discovery index. A user-shipped
@@ -361,6 +440,10 @@ export const publishBuildArtifacts = async (
361
440
  logger.info(`Indexed ${indexed} page(s) for search`);
362
441
  }
363
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
+
364
447
  // Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
365
448
  // Skipped with a warning when its admin key isn't configured.
366
449
  await syncSearchProvider(project, {
@@ -373,18 +456,24 @@ export const publishBuildArtifacts = async (
373
456
  const userSkillsIndex = existsSync(
374
457
  join(distDir, AGENT_SKILLS_INDEX_PATH.slice(1))
375
458
  );
376
- 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
+ );
377
471
  // The discovery documents advertise what this build serves: no MCP server
378
472
  // when a page took its route, no skills index when nothing was published.
379
473
  const advertised: BlumeProject = {
380
474
  ...project,
381
475
  config: advertisedConfig(project.config, {
382
- mcp: servesMcp(
383
- project,
384
- project.context.pagesRoot
385
- ? discoverPagesSync(project.context.pagesRoot)
386
- : []
387
- ),
476
+ mcp,
388
477
  skills: skills.length > 0 || userSkillsIndex,
389
478
  }),
390
479
  };
@@ -423,6 +512,7 @@ export const publishBuildArtifacts = async (
423
512
 
424
513
  await emitWellKnownFiles(advertised.config, distDir, skills, logger);
425
514
  await emitAgentSkills(project, distDir, skills, logger);
515
+ await emitSkillMd(project, distDir, skills, logger);
426
516
 
427
517
  await emitRedirectFiles(advertised, distDir, logger);
428
518
  await emitHeaderFiles(advertised, distDir, logger);
@@ -84,7 +84,8 @@ import {
84
84
  API_PAGES_PATH,
85
85
  OPENAPI_PATH,
86
86
  } from "../ai/api/paths.ts";
87
- import { normalizePath } from "../core/base-path.ts";
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
 
@@ -132,14 +133,6 @@ const STATIC_EXEMPTIONS = [
132
133
  API_NAVIGATION_PATH,
133
134
  ];
134
135
 
135
- /**
136
- * The deployment base as a rule/URL prefix: trailing slash stripped, empty
137
- * for a root deploy — the same normalization the dev middleware applies in
138
- * `astro/markdown-negotiation.ts`.
139
- */
140
- const basePrefix = (base?: string): string =>
141
- base && base !== "/" ? base.replace(/\/$/u, "") : "";
142
-
143
136
  /** A value as `JSON.parse` produces it (the wrangler config's value space). */
144
137
  type JsonValue =
145
138
  | string
@@ -180,7 +173,7 @@ const ruleBody = (rule: string): string =>
180
173
  * status (see the module comment).
181
174
  */
182
175
  export const buildRunWorkerFirstRules = (base?: string): string[] => {
183
- const prefix = encodeURI(basePrefix(base));
176
+ const prefix = encodeURI(normalizeBasePath(base));
184
177
  const exemptions = STATIC_EXEMPTIONS.map((path) => `!${prefix}${path}`);
185
178
  return prefix
186
179
  ? [prefix, `${prefix}/*`, ...exemptions]
@@ -282,7 +275,7 @@ export const buildNegotiationWorker = (
282
275
  const routes = JSON.stringify(options.routePaths);
283
276
  const pageJson = JSON.stringify(options.pageJsonPaths ?? []);
284
277
  const binding = JSON.stringify(options.assetsBinding);
285
- const prefix = JSON.stringify(encodeURI(basePrefix(options.base)));
278
+ const prefix = JSON.stringify(encodeURI(normalizeBasePath(options.base)));
286
279
  const homeLinkHeader = JSON.stringify(options.homeLinkHeader ?? null);
287
280
  const homeTokens = JSON.stringify(
288
281
  options.homeTokens === undefined ? null : String(options.homeTokens)
@@ -297,12 +290,18 @@ export const buildNegotiationWorker = (
297
290
  // header, which cannot carry non-ASCII.
298
291
  const redirects = JSON.stringify(
299
292
  Object.fromEntries(
300
- (options.redirects ?? []).map((redirect) => [
301
- normalizePath(redirect.from),
302
- [encodeURI(redirect.to), redirect.status],
303
- ])
293
+ (options.redirects ?? [])
294
+ .filter((redirect) => !isPatternPath(redirect.from))
295
+ .map((redirect) => [
296
+ normalizePath(redirect.from),
297
+ [encodeURI(redirect.to), redirect.status],
298
+ ])
304
299
  )
305
300
  );
301
+ // Patterns (`/beta/:slug*`) are tried in order after the exact paths.
302
+ const patternRedirects = JSON.stringify(
303
+ compileRedirects(options.redirects ?? [])
304
+ );
306
305
  return `// Generated by Blume. Do not edit; this file is recreated on each build.
307
306
  //
308
307
  // Request-time \`Accept: text/markdown\` negotiation for a Cloudflare server
@@ -327,6 +326,9 @@ const ASSETS_BINDING = ${binding};
327
326
  const HOME_LINK_HEADER = ${homeLinkHeader};
328
327
  const HOME_TOKENS = ${homeTokens};
329
328
  const REDIRECTS = ${redirects};
329
+ const PATTERN_REDIRECTS = ${patternRedirects}.map(
330
+ ([source, location, status]) => [new RegExp(source, "u"), location, status]
331
+ );
330
332
  const NOT_FOUND = ${notFound};
331
333
  const SVG_ASSET_HEADERS = ${JSON.stringify(SVG_ASSET_HEADERS)};
332
334
 
@@ -343,11 +345,31 @@ const isSvgAsset = (pathname) => {
343
345
  // reads — a worker-first route never reaches it. Answering from this baked-in
344
346
  // copy keeps the exact configured status; delegating instead would let Astro's
345
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.
346
351
  const redirectFor = (pathname) => {
347
352
  const trimmed =
348
353
  pathname !== "/" && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
349
354
  const path = safeDecode(trimmed);
350
- 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;
351
373
  };
352
374
 
353
375
  // The request path with the deployment base removed: the whole path on a
@@ -680,7 +702,7 @@ export const injectWorkerNegotiation = (
680
702
  // The content-route guard compares against redirect `from`s, which carry the
681
703
  // full `{deployment.base}{basePath}` stack; the routes carry only
682
704
  // `basePath`, so the deployment base is applied here.
683
- const deployPrefix = basePrefix(options.base);
705
+ const deployPrefix = normalizeBasePath(options.base);
684
706
  const guardRoutes = new Set(
685
707
  contentRoutePaths.map((route) =>
686
708
  normalizePath(
@@ -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",
@@ -200,6 +201,13 @@ export interface VercelHeader {
200
201
  /** The characters `path-to-regexp` would read as syntax in a literal path. */
201
202
  const SOURCE_SYNTAX = /[\\(){}?+:]/gu;
202
203
 
204
+ /**
205
+ * A literal path as `path-to-regexp` source text — what a `vercel.json`
206
+ * `source` is — with each syntax character escaped, so it matches only itself.
207
+ */
208
+ export const escapeVercelSource = (path: string): string =>
209
+ path.replaceAll(SOURCE_SYNTAX, String.raw`\$&`);
210
+
203
211
  /**
204
212
  * A `_headers` path as a `vercel.json` `source`, which `path-to-regexp` reads:
205
213
  * the literal parts are escaped, and a `*` glob becomes the `(.*)` group that
@@ -209,10 +217,7 @@ const SOURCE_SYNTAX = /[\\(){}?+:]/gu;
209
217
  const vercelSource = (path: string): string => {
210
218
  const trimmed =
211
219
  path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
212
- return trimmed
213
- .split("*")
214
- .map((part) => part.replaceAll(SOURCE_SYNTAX, String.raw`\$&`))
215
- .join("(.*)");
220
+ return trimmed.split("*").map(escapeVercelSource).join("(.*)");
216
221
  };
217
222
 
218
223
  /**
@@ -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;
@@ -124,8 +127,14 @@ export const nodeRedirects = (project: BlumeProject): NodeRedirects => {
124
127
  )
125
128
  );
126
129
  return Object.fromEntries(
127
- platformRedirects(config)
128
- .filter((redirect) => !pages.has(normalizePath(redirect.from)))
130
+ platformRedirects(project)
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"