blume 2.0.0 → 2.0.2

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 (454) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +110 -0
  3. package/README.md +2 -2
  4. package/dist/cli/{chunk-f2972sbt.js → chunk-00gs3wqs.js} +1 -1
  5. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  6. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  7. package/dist/cli/{chunk-pat2zzwc.js → chunk-3yce002v.js} +21 -8
  8. package/dist/cli/{chunk-pat2zzwc.js.map → chunk-3yce002v.js.map} +4 -4
  9. package/dist/cli/{chunk-d1tadaw7.js → chunk-4e9b9ra6.js} +17 -7
  10. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  11. package/dist/cli/{chunk-zg2gtj10.js → chunk-5r8g91qn.js} +352 -676
  12. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  13. package/dist/cli/{chunk-11j0384y.js → chunk-6dtt0zfn.js} +14 -14
  14. package/dist/cli/{chunk-11j0384y.js.map → chunk-6dtt0zfn.js.map} +3 -3
  15. package/dist/cli/{chunk-bw22s759.js → chunk-6k4ftwze.js} +38 -19
  16. package/dist/cli/{chunk-bw22s759.js.map → chunk-6k4ftwze.js.map} +4 -4
  17. package/dist/cli/{chunk-sqn5t4q0.js → chunk-7mbqtmgb.js} +16 -7
  18. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  19. package/dist/cli/{chunk-79njf86q.js → chunk-7vtckvaw.js} +21 -19
  20. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  21. package/dist/cli/{chunk-fh5hj5jt.js → chunk-8g8ytmgx.js} +17 -18
  22. package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-8g8ytmgx.js.map} +2 -2
  23. package/dist/cli/{chunk-1w8dp3qb.js → chunk-91ws1n6j.js} +18 -15
  24. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  25. package/dist/cli/{chunk-7ez8ny0t.js → chunk-bbnwccaz.js} +2 -2
  26. package/dist/cli/{chunk-5a2z0198.js → chunk-bfwp9vp6.js} +18 -10
  27. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  28. package/dist/cli/{chunk-ernrthtr.js → chunk-d1v5rhy0.js} +14 -15
  29. package/dist/cli/{chunk-ernrthtr.js.map → chunk-d1v5rhy0.js.map} +2 -2
  30. package/dist/cli/{chunk-a9kptbw5.js → chunk-ddndchfr.js} +21 -6
  31. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  32. package/dist/cli/{chunk-zxccj738.js → chunk-esh98wmb.js} +1 -1
  33. package/dist/cli/{chunk-88cpgt6h.js → chunk-fsmrqk8a.js} +1 -1
  34. package/dist/cli/{chunk-b5aj94ah.js → chunk-g698a744.js} +5 -5
  35. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  36. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  37. package/dist/cli/{chunk-41za066z.js → chunk-h2ez8dzb.js} +4 -4
  38. package/dist/cli/{chunk-by2290sx.js → chunk-h7k3nq3v.js} +2 -2
  39. package/dist/cli/{chunk-j8mw0za6.js → chunk-hqp2ajnh.js} +252 -123
  40. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  41. package/dist/cli/{chunk-mnqj32sj.js → chunk-hr8ne106.js} +119 -50
  42. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  43. package/dist/cli/{chunk-mwt1k8n7.js → chunk-j85scx15.js} +74 -30
  44. package/dist/cli/chunk-j85scx15.js.map +10 -0
  45. package/dist/cli/{chunk-z01ze5c1.js → chunk-k7pj68a8.js} +63 -22
  46. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  47. package/dist/cli/{chunk-2q1dwty4.js → chunk-mqc662a6.js} +8 -8
  48. package/dist/cli/{chunk-2q1dwty4.js.map → chunk-mqc662a6.js.map} +4 -4
  49. package/dist/cli/{chunk-y3e45rc8.js → chunk-n1yg3tj3.js} +4 -4
  50. package/dist/cli/{chunk-bctazmbk.js → chunk-nfcyttvj.js} +17 -6
  51. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  52. package/dist/cli/{chunk-6k8vp3ta.js → chunk-pbg5a4s3.js} +60 -28
  53. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  54. package/dist/cli/{chunk-d80hr03s.js → chunk-ppzjqwx2.js} +24 -17
  55. package/dist/cli/{chunk-d80hr03s.js.map → chunk-ppzjqwx2.js.map} +4 -4
  56. package/dist/cli/{chunk-beat36xx.js → chunk-qkb5a8sa.js} +25 -10
  57. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  58. package/dist/cli/{chunk-bnbmcwfb.js → chunk-sqw4ekg1.js} +5 -5
  59. package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-sqw4ekg1.js.map} +3 -3
  60. package/dist/cli/{chunk-xaz13gwg.js → chunk-v6ya5kcb.js} +3268 -1150
  61. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  62. package/dist/cli/{chunk-tzne8qfq.js → chunk-w4bxdvsa.js} +18 -15
  63. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  64. package/dist/cli/{chunk-f7t03s3g.js → chunk-wdrt2k2v.js} +2 -2
  65. package/dist/cli/{chunk-z1f5arsg.js → chunk-xh43dwgw.js} +170 -55
  66. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  67. package/dist/cli/{chunk-pnnvybbk.js → chunk-zp79m0ts.js} +5 -5
  68. package/dist/cli/{chunk-pnnvybbk.js.map → chunk-zp79m0ts.js.map} +2 -2
  69. package/dist/cli/index.js +160 -35
  70. package/dist/cli/index.js.map +4 -4
  71. package/dist/types/ai/agent-readability.d.ts +1 -1
  72. package/dist/types/ai/agent-surface.d.ts +32 -0
  73. package/dist/types/ai/api/paths.d.ts +1 -1
  74. package/dist/types/ai/api-catalog.d.ts +7 -1
  75. package/dist/types/ai/ask-context.d.ts +14 -7
  76. package/dist/types/ai/ask.d.ts +43 -43
  77. package/dist/types/ai/component-markdown.d.ts +4 -4
  78. package/dist/types/ai/index.d.ts +3 -3
  79. package/dist/types/ai/openapi-components.d.ts +1 -1
  80. package/dist/types/ai/relative-links.d.ts +9 -4
  81. package/dist/types/ai/serializers.d.ts +1 -1
  82. package/dist/types/ai/static-expression.d.ts +28 -0
  83. package/dist/types/ai/visibility.d.ts +1 -1
  84. package/dist/types/analytics/databuddy.d.ts +43 -0
  85. package/dist/types/analytics/index.d.ts +2 -0
  86. package/dist/types/analytics/schema.d.ts +14 -0
  87. package/dist/types/cli/env.d.ts +5 -0
  88. package/dist/types/cli/init/scaffold.d.ts +19 -3
  89. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  90. package/dist/types/core/base-path.d.ts +9 -0
  91. package/dist/types/core/config-input.d.ts +21 -21
  92. package/dist/types/core/config.d.ts +5 -5
  93. package/dist/types/core/data.d.ts +3 -3
  94. package/dist/types/core/graph.d.ts +2 -0
  95. package/dist/types/core/i18n-ui.d.ts +37 -8
  96. package/dist/types/core/i18n.d.ts +7 -1
  97. package/dist/types/core/links.d.ts +3 -1
  98. package/dist/types/core/load-module.d.ts +10 -0
  99. package/dist/types/core/locale-links.d.ts +12 -2
  100. package/dist/types/core/meta.d.ts +5 -1
  101. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  102. package/dist/types/core/navigation.d.ts +38 -0
  103. package/dist/types/core/ordering-prefix.d.ts +4 -0
  104. package/dist/types/core/safe-links.d.ts +3 -1
  105. package/dist/types/core/schema.d.ts +57 -18
  106. package/dist/types/core/sources/github-releases.d.ts +5 -0
  107. package/dist/types/core/sources/lower.d.ts +14 -2
  108. package/dist/types/core/sources/normalize.d.ts +10 -1
  109. package/dist/types/core/sources/remote.d.ts +11 -1
  110. package/dist/types/core/sources/resolve.d.ts +12 -0
  111. package/dist/types/core/sources/types.d.ts +31 -0
  112. package/dist/types/core/types.d.ts +9 -0
  113. package/dist/types/core/unrecognized-keys.d.ts +1 -1
  114. package/dist/types/deploy/adapters/node.d.ts +5 -2
  115. package/dist/types/deploy/adapters/types.d.ts +7 -0
  116. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  117. package/dist/types/deploy/headers.d.ts +31 -7
  118. package/dist/types/deploy/node-headers.d.ts +43 -8
  119. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  120. package/dist/types/deploy/platforms/node.d.ts +6 -5
  121. package/dist/types/deploy/platforms/types.d.ts +7 -0
  122. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  123. package/dist/types/deploy/redirects.d.ts +7 -2
  124. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  125. package/dist/types/openapi/model.d.ts +20 -6
  126. package/dist/types/search/documents.d.ts +1 -1
  127. package/dist/types/search/orama-index.d.ts +1 -1
  128. package/dist/types/search/sync/algolia.d.ts +3 -1
  129. package/docs/01-quickstart.mdx +3 -2
  130. package/docs/02-deployment.mdx +21 -12
  131. package/docs/03-upgrading.mdx +22 -9
  132. package/docs/04-migrating.mdx +4 -4
  133. package/docs/08-faq.mdx +12 -5
  134. package/docs/advanced/changelog.mdx +1 -1
  135. package/docs/advanced/custom-pages.mdx +5 -3
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +5 -5
  138. package/docs/cli/doctor.mdx +4 -4
  139. package/docs/cli/evals.mdx +9 -9
  140. package/docs/cli/index.mdx +6 -3
  141. package/docs/cli/translate.mdx +8 -8
  142. package/docs/configuration/analytics.mdx +23 -2
  143. package/docs/configuration/{ask-ai.mdx → assistant.mdx} +20 -20
  144. package/docs/configuration/customization.mdx +5 -4
  145. package/docs/configuration/index.mdx +7 -5
  146. package/docs/configuration/meta.ts +1 -1
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +1 -1
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -3
  151. package/docs/content/index.mdx +4 -2
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +5 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -12
  156. package/docs/content/versioning.mdx +1 -0
  157. package/docs/discoverability/agent-discovery.mdx +22 -8
  158. package/docs/discoverability/index.mdx +3 -3
  159. package/docs/discoverability/llms-txt.mdx +2 -5
  160. package/docs/discoverability/markdown.mdx +5 -3
  161. package/docs/discoverability/mcp.mdx +4 -0
  162. package/docs/discoverability/metadata.mdx +3 -2
  163. package/docs/discoverability/open-graph.mdx +6 -4
  164. package/docs/discoverability/rss.mdx +3 -1
  165. package/docs/index.mdx +2 -2
  166. package/docs/references/asyncapi.mdx +1 -1
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +3 -3
  169. package/package.json +1 -1
  170. package/skills/blume/SKILL.md +5 -5
  171. package/skills/blume-migrate/SKILL.md +6 -6
  172. package/skills/blume-migrate/references/docusaurus.md +6 -6
  173. package/skills/blume-migrate/references/mintlify.md +3 -3
  174. package/skills/blume-migrate/references/monorepo.md +1 -1
  175. package/skills/blume-migrate/references/nextra.md +1 -1
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +4 -4
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/api/paths.ts +1 -1
  182. package/src/ai/api/spec.ts +15 -2
  183. package/src/ai/api-catalog.ts +7 -1
  184. package/src/ai/ask-context.ts +21 -9
  185. package/src/ai/ask-data.ts +28 -14
  186. package/src/ai/ask.ts +84 -71
  187. package/src/ai/component-markdown.ts +43 -37
  188. package/src/ai/cors.ts +3 -3
  189. package/src/ai/index.ts +16 -16
  190. package/src/ai/link-headers.ts +8 -3
  191. package/src/ai/llms.ts +4 -3
  192. package/src/ai/markdown.ts +23 -11
  193. package/src/ai/mcp/server.ts +78 -7
  194. package/src/ai/openapi-components.ts +1 -1
  195. package/src/ai/relative-links.ts +78 -10
  196. package/src/ai/serializers.ts +1 -1
  197. package/src/ai/static-expression.ts +416 -0
  198. package/src/ai/visibility.ts +46 -15
  199. package/src/analytics/databuddy.ts +67 -0
  200. package/src/analytics/head.ts +4 -0
  201. package/src/analytics/index.ts +2 -0
  202. package/src/analytics/posthog.ts +21 -3
  203. package/src/analytics/schema.ts +2 -0
  204. package/src/astro/generate.ts +90 -45
  205. package/src/astro/module-types.ts +1 -1
  206. package/src/astro/runtime-deps.ts +6 -6
  207. package/src/astro/templates.ts +87 -49
  208. package/src/audit/catalog.ts +2 -2
  209. package/src/audit/checks/assets.ts +25 -5
  210. package/src/audit/checks/content.ts +20 -2
  211. package/src/audit/checks/i18n.ts +46 -8
  212. package/src/audit/checks/indexability.ts +39 -19
  213. package/src/audit/checks/links.ts +14 -0
  214. package/src/audit/checks/llms.ts +6 -3
  215. package/src/audit/checks/network.ts +3 -1
  216. package/src/audit/checks/og-image.ts +10 -0
  217. package/src/audit/checks/robots.ts +6 -1
  218. package/src/audit/checks/sitemap.ts +56 -31
  219. package/src/audit/checks/social.ts +21 -2
  220. package/src/audit/crawl.ts +88 -15
  221. package/src/audit/report.ts +54 -17
  222. package/src/audit/run.ts +1 -0
  223. package/src/audit/types.ts +18 -0
  224. package/src/audit/url.ts +37 -6
  225. package/src/blume-modules.d.ts +2 -2
  226. package/src/cli/build-failure.ts +50 -0
  227. package/src/cli/commands/audit.ts +8 -1
  228. package/src/cli/commands/build.ts +19 -5
  229. package/src/cli/commands/check.ts +2 -0
  230. package/src/cli/commands/doctor.ts +7 -5
  231. package/src/cli/commands/eval.ts +4 -10
  232. package/src/cli/commands/init.ts +15 -5
  233. package/src/cli/commands/migrate.ts +2 -2
  234. package/src/cli/commands/preview.ts +15 -0
  235. package/src/cli/commands/sync.ts +2 -0
  236. package/src/cli/commands/translate.ts +12 -9
  237. package/src/cli/commands/upgrade.ts +13 -2
  238. package/src/cli/eject-scripts.ts +32 -7
  239. package/src/cli/env.ts +12 -1
  240. package/src/cli/init/scaffold.ts +117 -11
  241. package/src/cli/init/starter-spec.ts +235 -0
  242. package/src/cli/required-secrets.ts +3 -3
  243. package/src/components/content/AccordionItem.astro +26 -22
  244. package/src/components/content/Badge.astro +2 -9
  245. package/src/components/content/Card.astro +2 -2
  246. package/src/components/content/Component.astro +9 -3
  247. package/src/components/content/Expandable.astro +5 -1
  248. package/src/components/content/Frame.astro +2 -7
  249. package/src/components/content/GithubInfo.astro +12 -2
  250. package/src/components/content/Prompt.astro +2 -7
  251. package/src/components/content/Tabs.astro +54 -5
  252. package/src/components/content/Tile.astro +1 -1
  253. package/src/components/content/Tooltip.astro +69 -7
  254. package/src/components/content/Tree.astro +7 -2
  255. package/src/components/content/TypeTable.astro +10 -5
  256. package/src/components/content/Update.astro +8 -2
  257. package/src/components/content/auto-type-table.ts +4 -1
  258. package/src/components/content/badge-color.ts +17 -0
  259. package/src/components/content/base-href.ts +18 -3
  260. package/src/components/content/inline-markdown.ts +27 -7
  261. package/src/components/copy-feedback.ts +36 -9
  262. package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
  263. package/src/components/islands/{ask-ai.tsx → assistant.tsx} +39 -25
  264. package/src/components/islands/hooks.ts +21 -14
  265. package/src/components/islands/webmcp.ts +12 -8
  266. package/src/components/layout/Banner.astro +23 -4
  267. package/src/components/layout/Header.astro +32 -19
  268. package/src/components/layout/Logo.astro +5 -0
  269. package/src/components/layout/NavSelector.astro +6 -1
  270. package/src/components/layout/NavTabMenu.astro +133 -0
  271. package/src/components/layout/NavTree.astro +15 -6
  272. package/src/components/layout/NavTreeCache.astro +5 -2
  273. package/src/components/layout/NavTreeScript.astro +45 -5
  274. package/src/components/layout/PageLayout.astro +28 -14
  275. package/src/components/layout/Pagination.astro +7 -7
  276. package/src/components/layout/ReferenceLayout.astro +5 -2
  277. package/src/components/layout/RootLayout.astro +27 -14
  278. package/src/components/layout/Search.astro +18 -14
  279. package/src/components/layout/analytics-client.ts +3 -1
  280. package/src/components/layout/drawer-inert.ts +1 -1
  281. package/src/components/openapi/GraphqlType.astro +11 -3
  282. package/src/components/openapi/MessageComposer.astro +1 -1
  283. package/src/components/openapi/Operation.astro +21 -5
  284. package/src/components/openapi/PanelTabs.astro +4 -1
  285. package/src/components/openapi/Playground.astro +8 -2
  286. package/src/components/openapi/RequestPanel.astro +9 -5
  287. package/src/components/openapi/SchemaProperty.astro +11 -33
  288. package/src/components/openapi/SchemaTable.astro +27 -52
  289. package/src/components/openapi/description.ts +2 -2
  290. package/src/components/openapi/helpers.ts +90 -12
  291. package/src/components/openapi/message-composer.ts +8 -0
  292. package/src/components/openapi/message-model.ts +12 -2
  293. package/src/components/openapi/message.ts +4 -1
  294. package/src/components/openapi/operation-model.ts +40 -8
  295. package/src/components/openapi/panel.ts +29 -5
  296. package/src/components/openapi/playground-client.ts +37 -7
  297. package/src/components/openapi/playground-schema.ts +25 -6
  298. package/src/components/openapi/request.ts +2 -0
  299. package/src/components/openapi/schema-tree.ts +203 -0
  300. package/src/components/openapi/snippets.ts +30 -15
  301. package/src/components/openapi/validate-json.ts +1 -1
  302. package/src/core/base-path.ts +15 -0
  303. package/src/core/code-fences.ts +1 -1
  304. package/src/core/config-input.ts +23 -23
  305. package/src/core/config.ts +21 -7
  306. package/src/core/data.ts +3 -3
  307. package/src/core/diagnostics.ts +217 -30
  308. package/src/core/graph.ts +120 -11
  309. package/src/core/i18n-ui.ts +74 -11
  310. package/src/core/i18n.ts +13 -2
  311. package/src/core/links.ts +7 -4
  312. package/src/core/load-module.ts +20 -0
  313. package/src/core/locale-links.ts +17 -17
  314. package/src/core/manifest.ts +3 -2
  315. package/src/core/meta.ts +69 -6
  316. package/src/core/nav-diagnostics.ts +56 -1
  317. package/src/core/navigation.ts +245 -76
  318. package/src/core/ordering-prefix.ts +27 -0
  319. package/src/core/project-graph.ts +23 -1
  320. package/src/core/request-body.ts +1 -1
  321. package/src/core/safe-href.ts +53 -1
  322. package/src/core/safe-links.ts +11 -2
  323. package/src/core/schema.ts +95 -34
  324. package/src/core/server-features.ts +2 -2
  325. package/src/core/sources/assets.ts +83 -41
  326. package/src/core/sources/contentful-rich-text.ts +3 -2
  327. package/src/core/sources/contentful.ts +25 -12
  328. package/src/core/sources/filesystem.ts +5 -1
  329. package/src/core/sources/github-releases.ts +106 -5
  330. package/src/core/sources/lexical.ts +9 -4
  331. package/src/core/sources/lower.ts +79 -26
  332. package/src/core/sources/mdx-remote.ts +1 -0
  333. package/src/core/sources/normalize.ts +132 -30
  334. package/src/core/sources/notion.ts +26 -10
  335. package/src/core/sources/obsidian.ts +17 -3
  336. package/src/core/sources/payload.ts +1 -0
  337. package/src/core/sources/portable-text.ts +63 -44
  338. package/src/core/sources/remote.ts +18 -2
  339. package/src/core/sources/resolve.ts +52 -33
  340. package/src/core/sources/sanity.ts +1 -0
  341. package/src/core/sources/strapi-blocks.ts +9 -4
  342. package/src/core/sources/strapi.ts +1 -0
  343. package/src/core/sources/types.ts +31 -0
  344. package/src/core/types.ts +9 -0
  345. package/src/core/ui-packs/ar.ts +17 -5
  346. package/src/core/ui-packs/bg.ts +17 -5
  347. package/src/core/ui-packs/bn.ts +17 -5
  348. package/src/core/ui-packs/ca.ts +17 -5
  349. package/src/core/ui-packs/cs.ts +17 -5
  350. package/src/core/ui-packs/da.ts +17 -5
  351. package/src/core/ui-packs/de.ts +17 -5
  352. package/src/core/ui-packs/el.ts +17 -5
  353. package/src/core/ui-packs/es.ts +17 -5
  354. package/src/core/ui-packs/fa.ts +17 -5
  355. package/src/core/ui-packs/fi.ts +17 -5
  356. package/src/core/ui-packs/fr.ts +17 -5
  357. package/src/core/ui-packs/he.ts +17 -5
  358. package/src/core/ui-packs/hi.ts +17 -5
  359. package/src/core/ui-packs/hr.ts +17 -5
  360. package/src/core/ui-packs/hu.ts +17 -5
  361. package/src/core/ui-packs/id.ts +17 -5
  362. package/src/core/ui-packs/it.ts +17 -5
  363. package/src/core/ui-packs/ja.ts +17 -5
  364. package/src/core/ui-packs/ko.ts +17 -5
  365. package/src/core/ui-packs/nl.ts +17 -5
  366. package/src/core/ui-packs/no.ts +17 -5
  367. package/src/core/ui-packs/pl.ts +17 -5
  368. package/src/core/ui-packs/pt-br.ts +17 -5
  369. package/src/core/ui-packs/pt.ts +17 -5
  370. package/src/core/ui-packs/ro.ts +17 -5
  371. package/src/core/ui-packs/ru.ts +17 -5
  372. package/src/core/ui-packs/sk.ts +17 -5
  373. package/src/core/ui-packs/sr.ts +17 -5
  374. package/src/core/ui-packs/sv.ts +17 -5
  375. package/src/core/ui-packs/th.ts +17 -5
  376. package/src/core/ui-packs/tr.ts +17 -5
  377. package/src/core/ui-packs/uk.ts +17 -5
  378. package/src/core/ui-packs/vi.ts +17 -5
  379. package/src/core/ui-packs/zh-tw.ts +17 -5
  380. package/src/core/ui-packs/zh.ts +17 -5
  381. package/src/core/unrecognized-keys.ts +1 -1
  382. package/src/core/version-cut.ts +17 -2
  383. package/src/core/versions.ts +4 -1
  384. package/src/deploy/adapters/node.ts +5 -2
  385. package/src/deploy/adapters/registry.ts +2 -1
  386. package/src/deploy/adapters/types.ts +13 -1
  387. package/src/deploy/artifacts.ts +25 -5
  388. package/src/deploy/cloudflare-negotiation.ts +23 -4
  389. package/src/deploy/headers.ts +67 -51
  390. package/src/deploy/node-headers.ts +148 -27
  391. package/src/deploy/platforms/cloudflare.ts +1 -0
  392. package/src/deploy/platforms/netlify.ts +82 -5
  393. package/src/deploy/platforms/node.ts +7 -5
  394. package/src/deploy/platforms/static.ts +1 -0
  395. package/src/deploy/platforms/types.ts +7 -0
  396. package/src/deploy/platforms/vercel.ts +9 -3
  397. package/src/deploy/redirects.ts +14 -3
  398. package/src/deploy/vercel-negotiation.ts +35 -3
  399. package/src/eval/agents.ts +10 -2
  400. package/src/eval/run.ts +25 -0
  401. package/src/markdown/base-links.ts +55 -8
  402. package/src/markdown/index.ts +6 -3
  403. package/src/markdown/relative-links.ts +3 -23
  404. package/src/markdown/route-snapshot.ts +37 -0
  405. package/src/og/card.ts +20 -4
  406. package/src/og/derive.ts +145 -4
  407. package/src/openapi/graphql-build.ts +28 -2
  408. package/src/openapi/model.ts +77 -13
  409. package/src/openapi/render-mdx.ts +10 -1
  410. package/src/registry/eject.ts +135 -37
  411. package/src/search/documents.ts +24 -3
  412. package/src/search/orama-index.ts +1 -1
  413. package/src/search/sync/algolia.ts +36 -2
  414. package/src/sources/registry.ts +5 -0
  415. package/src/theme/entry.ts +11 -4
  416. package/src/translate/agents.ts +6 -1
  417. package/src/translate/ledger.ts +26 -3
  418. package/src/translate/meta.ts +11 -3
  419. package/src/translate/report.ts +1 -1
  420. package/src/translate/run.ts +11 -5
  421. package/src/translate/validate.ts +10 -1
  422. package/src/translate/work-list.ts +36 -3
  423. package/src/upgrade/upgrade.ts +37 -5
  424. package/dist/cli/chunk-1w8dp3qb.js.map +0 -10
  425. package/dist/cli/chunk-5a2z0198.js.map +0 -10
  426. package/dist/cli/chunk-6k8vp3ta.js.map +0 -19
  427. package/dist/cli/chunk-79njf86q.js.map +0 -11
  428. package/dist/cli/chunk-a9kptbw5.js.map +0 -14
  429. package/dist/cli/chunk-abh8yjkn.js +0 -31
  430. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  431. package/dist/cli/chunk-bctazmbk.js.map +0 -10
  432. package/dist/cli/chunk-beat36xx.js.map +0 -10
  433. package/dist/cli/chunk-d1tadaw7.js.map +0 -10
  434. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  435. package/dist/cli/chunk-j8mw0za6.js.map +0 -35
  436. package/dist/cli/chunk-mnqj32sj.js.map +0 -12
  437. package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
  438. package/dist/cli/chunk-nk3ts2xk.js +0 -51
  439. package/dist/cli/chunk-nk3ts2xk.js.map +0 -10
  440. package/dist/cli/chunk-sqn5t4q0.js.map +0 -10
  441. package/dist/cli/chunk-tzne8qfq.js.map +0 -10
  442. package/dist/cli/chunk-xaz13gwg.js.map +0 -182
  443. package/dist/cli/chunk-z01ze5c1.js.map +0 -10
  444. package/dist/cli/chunk-z1f5arsg.js.map +0 -36
  445. package/dist/cli/chunk-zg2gtj10.js.map +0 -35
  446. /package/dist/cli/{chunk-f2972sbt.js.map → chunk-00gs3wqs.js.map} +0 -0
  447. /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-bbnwccaz.js.map} +0 -0
  448. /package/dist/cli/{chunk-zxccj738.js.map → chunk-esh98wmb.js.map} +0 -0
  449. /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-fsmrqk8a.js.map} +0 -0
  450. /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-g698a744.js.map} +0 -0
  451. /package/dist/cli/{chunk-41za066z.js.map → chunk-h2ez8dzb.js.map} +0 -0
  452. /package/dist/cli/{chunk-by2290sx.js.map → chunk-h7k3nq3v.js.map} +0 -0
  453. /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-n1yg3tj3.js.map} +0 -0
  454. /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-wdrt2k2v.js.map} +0 -0
@@ -13,12 +13,13 @@ import {
13
13
  SIGNATURES_DIRECTORY_PATH,
14
14
  SIGNATURES_DIRECTORY_TYPE,
15
15
  } from "../ai/web-bot-auth.ts";
16
- import { normalizeBasePath } from "../core/base-path.ts";
16
+ import { normalizeBasePath, normalizePath } from "../core/base-path.ts";
17
17
  import type { BlumeProject } from "../core/project-graph.ts";
18
18
  import type { ResolvedConfig } from "../core/schema.ts";
19
- import { SVG_ASSET_POLICY } from "./headers.ts";
19
+ import { SVG_ASSET_HEADERS, svgAssetPath } from "./headers.ts";
20
20
  import { distDir } from "./platforms/paths.ts";
21
21
  import type { BuildLog } from "./platforms/types.ts";
22
+ import { platformRedirects } from "./redirects.ts";
22
23
 
23
24
  /**
24
25
  * The response headers the Node standalone server can't set itself. Its static
@@ -29,6 +30,12 @@ import type { BuildLog } from "./platforms/types.ts";
29
30
  * (`deploy/headers.ts`) that a Node server has no file to read from. The Markdown
30
31
  * and text charsets need no rule here: `send` already adds `charset=UTF-8` to
31
32
  * `text/*` types.
33
+ *
34
+ * The same wrapper answers the configured redirects. Astro's own handler
35
+ * honors a redirect's configured status only when its destination resolves
36
+ * to a discrete route, and Blume serves every page from `[...slug]`, so a
37
+ * `302` or `307` would go out as a `301` or `308` — permanent redirects that
38
+ * browsers cache.
32
39
  */
33
40
 
34
41
  /** The headers one exact served path (deployment base included) gets. */
@@ -93,35 +100,130 @@ const patternRules = (
93
100
  });
94
101
 
95
102
  /**
96
- * The entry that replaces Astro's: it stamps the rules' headers on the
97
- * response, then hands the request to Astro's handler. `send` leaves a
98
- * Content-Type that is already set alone, so the registered media type wins.
103
+ * The configured redirects the wrapper answers, keyed by served path (the
104
+ * trailing slash trimmed), each to its `Location` and exact status.
105
+ */
106
+ export type NodeRedirects = Record<string, [string, number]>;
107
+
108
+ /**
109
+ * The configured redirects, based the way the host matches them (see
110
+ * `platformRedirects`) and keyed for the wrapper's lookup, the `Location`
111
+ * percent-encoded as Astro sends it. A redirect at a content route's own path
112
+ * is left out: the prerendered page owns that URL, and the static handler
113
+ * serves it before Astro's redirect ever could.
114
+ */
115
+ export const nodeRedirects = (project: BlumeProject): NodeRedirects => {
116
+ const { config } = project;
117
+ if (config.redirects.length === 0) {
118
+ return {};
119
+ }
120
+ const base = normalizeBasePath(config.deployment.options.base);
121
+ const pages = new Set(
122
+ project.manifest.routes.map((route) =>
123
+ normalizePath(`${base}${route.path}`)
124
+ )
125
+ );
126
+ return Object.fromEntries(
127
+ platformRedirects(config)
128
+ .filter((redirect) => !pages.has(normalizePath(redirect.from)))
129
+ .map((redirect) => [
130
+ normalizePath(redirect.from),
131
+ [encodeURI(redirect.to), redirect.status],
132
+ ])
133
+ );
134
+ };
135
+
136
+ /** What the wrapper needs beyond the header rules. */
137
+ export interface NodeEntryOptions {
138
+ /** `deployment.base`, normalized (`""` at the root). */
139
+ base?: string;
140
+ /** The configured redirects, from {@link nodeRedirects}. */
141
+ redirects?: NodeRedirects;
142
+ }
143
+
144
+ /**
145
+ * The entry that replaces Astro's: it answers a configured redirect with its
146
+ * exact status, and otherwise stamps the rules' headers on the response and
147
+ * hands the request to Astro's handler. `send` leaves a Content-Type that is
148
+ * already set alone, so the registered media type wins.
149
+ *
150
+ * A rule matches the file the static handler will serve, not the raw URL:
151
+ * Astro strips the base when it leads the path and serves the rest from the
152
+ * client directory either way, and `send` decodes and normalizes the path —
153
+ * so `/docs/blume-assets/a.sv%67`, `/blume-assets/a.svg`, and
154
+ * `/docs//x/../blume-assets/a.svg` are all the same SVG. The comparison
155
+ * ignores case, as a case-insensitive disk and `send`'s MIME lookup do.
99
156
  *
100
157
  * It keeps Astro's contract — the `handler`, `options`, and `startServer`
101
158
  * exports `astro preview` and a middleware-mode host import — and its
102
159
  * autostart: Astro's entry is imported with autostart off (the env var is read
103
160
  * when that module evaluates, so the import has to be dynamic), and the
104
- * wrapper starts the server itself with its listener in front of Astro's.
161
+ * wrapper starts the server itself, taking over its request listener so a
162
+ * redirect is answered before Astro sees the request.
105
163
  */
106
- export const nodeEntryWrapper = (rules: readonly NodeHeaderRule[]): string =>
164
+ export const nodeEntryWrapper = (
165
+ rules: readonly NodeHeaderRule[],
166
+ options: NodeEntryOptions = {}
167
+ ): string =>
107
168
  `${WRAPPER_MARKER}
108
169
  // Written by \`blume build\`: Astro's server entry is ./${NODE_ASTRO_ENTRY_FILE}.
109
- // This wrapper sets the media types and CORS headers of the .well-known
110
- // discovery files, which the standalone server's static handler can't, then
111
- // hands every request to Astro.
170
+ // This wrapper answers the configured redirects with their exact status and
171
+ // sets the headers the standalone server's static handler can't (the media
172
+ // types and CORS headers of the .well-known discovery files, the sandbox on
173
+ // downloaded SVGs), then hands every other request to Astro.
174
+ import { posix } from "node:path";
175
+
176
+ const BASE = ${JSON.stringify(options.base ?? "")};
112
177
  const RULES = new Map(${JSON.stringify(exactRules(rules))});
113
178
  const PATTERNS = ${JSON.stringify(patternRules(rules))};
179
+ const REDIRECTS = ${JSON.stringify(options.redirects ?? {})};
180
+ const safeDecode = (path) => {
181
+ try {
182
+ return decodeURIComponent(path);
183
+ } catch {
184
+ return path;
185
+ }
186
+ };
187
+ const urlPath = (req) => (req.url ?? "").split("#")[0].split("?")[0];
188
+ // The file the static handler serves for a request, spelled with the base.
189
+ const servedPath = (req) => {
190
+ const path = posix.normalize("/" + safeDecode(urlPath(req)));
191
+ const rest =
192
+ BASE && (path === BASE || path.startsWith(BASE + "/"))
193
+ ? path.slice(BASE.length)
194
+ : path;
195
+ return BASE + (rest || "/");
196
+ };
114
197
  const applyHeaders = (req, res) => {
115
- const path = (req.url ?? "").split("?")[0];
198
+ const path = servedPath(req);
199
+ const lower = path.toLowerCase();
116
200
  const headers =
117
201
  RULES.get(path) ??
118
- PATTERNS.find(([prefix, suffix]) => path.startsWith(prefix) && path.endsWith(suffix))?.[2];
202
+ PATTERNS.find(
203
+ ([prefix, suffix]) =>
204
+ lower.startsWith(prefix.toLowerCase()) &&
205
+ lower.endsWith(suffix.toLowerCase())
206
+ )?.[2];
119
207
  if (headers) {
120
208
  for (const [name, value] of Object.entries(headers)) {
121
209
  res.setHeader(name, value);
122
210
  }
123
211
  }
124
212
  };
213
+ // Answer a configured redirect with its exact status; Astro's handler would
214
+ // send a 301 (a 308 for other methods) whatever the configured one.
215
+ 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)) {
220
+ return false;
221
+ }
222
+ const [location, status] = REDIRECTS[key];
223
+ res.writeHead(status, { location });
224
+ res.end();
225
+ return true;
226
+ };
125
227
  const previous = process.env.ASTRO_NODE_AUTOSTART;
126
228
  process.env.ASTRO_NODE_AUTOSTART = "disabled";
127
229
  const astro = await import("./${NODE_ASTRO_ENTRY_FILE}");
@@ -132,17 +234,30 @@ if (previous === undefined) {
132
234
  }
133
235
  export const { options } = astro;
134
236
  export const handler = (req, res, ...rest) => {
237
+ if (answerRedirect(req, res)) {
238
+ return;
239
+ }
135
240
  applyHeaders(req, res);
136
241
  return astro.handler(req, res, ...rest);
137
242
  };
138
243
  export const startServer = () => {
139
244
  const started = astro.startServer();
140
- // Stamp the headers only where the adapter exposes its HTTP server as
141
- // expected; a different shape serves the files without them rather than
142
- // failing to start.
245
+ // Take over the adapter's request listener only where it exposes its HTTP
246
+ // server as expected; a different shape serves without the headers and
247
+ // redirect statuses rather than failing to start.
143
248
  const httpServer = started?.server?.server;
144
- if (typeof httpServer?.prependListener === "function") {
145
- httpServer.prependListener("request", applyHeaders);
249
+ if (typeof httpServer?.listeners === "function") {
250
+ const astroListeners = httpServer.listeners("request");
251
+ httpServer.removeAllListeners("request");
252
+ httpServer.on("request", (req, res) => {
253
+ if (answerRedirect(req, res)) {
254
+ return;
255
+ }
256
+ applyHeaders(req, res);
257
+ for (const listener of astroListeners) {
258
+ listener.call(httpServer, req, res);
259
+ }
260
+ });
146
261
  }
147
262
  return started;
148
263
  };
@@ -152,9 +267,9 @@ if (options.mode === "standalone" && previous !== "disabled") {
152
267
  `;
153
268
 
154
269
  /**
155
- * Put the header wrapper in front of a Node server build's entry. A build
156
- * with no rule (no API catalog, AI catalog, MCP server, signatures directory,
157
- * or downloaded content assets) leaves Astro's entry alone.
270
+ * Put the wrapper in front of a Node server build's entry. A build with no
271
+ * rule (no API catalog, AI catalog, MCP server, signatures directory, or
272
+ * downloaded content assets) and no redirect leaves Astro's entry alone.
158
273
  */
159
274
  export const wrapNodeEntry = async (
160
275
  project: BlumeProject,
@@ -169,20 +284,21 @@ export const wrapNodeEntry = async (
169
284
  ...(existsSync(assetsDir)
170
285
  ? [
171
286
  {
172
- headers: { "Content-Security-Policy": SVG_ASSET_POLICY },
173
- path: `${normalizeBasePath(project.config.deployment.options.base)}/blume-assets/*.svg`,
287
+ headers: { ...SVG_ASSET_HEADERS },
288
+ path: svgAssetPath(project.config),
174
289
  },
175
290
  ]
176
291
  : []),
177
292
  ];
178
- if (rules.length === 0) {
293
+ const redirects = nodeRedirects(project);
294
+ if (rules.length === 0 && Object.keys(redirects).length === 0) {
179
295
  return;
180
296
  }
181
297
  const serverDir = join(distDir(project.context), "server");
182
298
  const entry = join(serverDir, NODE_ENTRY_FILE);
183
299
  if (!existsSync(entry)) {
184
300
  log.warn(
185
- `Could not find the Node server entry at ${entry}, so the .well-known discovery files are served without their media types and CORS headers.`
301
+ `Could not find the Node server entry at ${entry}, so the server runs without the headers and redirect statuses Blume sets there.`
186
302
  );
187
303
  return;
188
304
  }
@@ -191,8 +307,13 @@ export const wrapNodeEntry = async (
191
307
  return;
192
308
  }
193
309
  await rename(entry, join(serverDir, NODE_ASTRO_ENTRY_FILE));
194
- await writeFile(entry, nodeEntryWrapper(rules), "utf-8");
195
- log.success(
196
- "Wired the .well-known discovery headers into the Node server entry"
310
+ await writeFile(
311
+ entry,
312
+ nodeEntryWrapper(rules, {
313
+ base: normalizeBasePath(project.config.deployment.options.base),
314
+ redirects,
315
+ }),
316
+ "utf-8"
197
317
  );
318
+ log.success("Wired headers and redirects into the Node server entry");
198
319
  };
@@ -305,6 +305,7 @@ export const cloudflarePlatform: DeployPlatform = {
305
305
  },
306
306
  kind: "cloudflare",
307
307
  negotiatesMarkdown: true,
308
+ previewDeploy: null,
308
309
  readsHeaderFiles: { server: true, static: true },
309
310
  redirectFiles: [REDIRECTS_FILE],
310
311
  serverOutputDir: distDir,
@@ -1,14 +1,81 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+
4
+ import { join } from "pathe";
5
+
6
+ import { buildHomeLinkHeader } from "../../ai/link-headers.ts";
7
+ import { markdownRoutePaths } from "../../ai/markdown.ts";
8
+ import type { BlumeProject } from "../../core/project-graph.ts";
1
9
  import { NETLIFY_ADAPTER_PACKAGE } from "../adapters/netlify.ts";
10
+ import { buildNetlifyConfigHeaders } from "../headers.ts";
11
+ import type { NetlifyConfigHeaders } from "../headers.ts";
2
12
  import { buildNetlifyRedirects } from "../redirects.ts";
3
13
  import { distDir, toSiteUrl } from "./paths.ts";
4
- import type { DeployPlatform, RedirectFile } from "./types.ts";
14
+ import type { BuildLog, DeployPlatform, RedirectFile } from "./types.ts";
5
15
 
6
- /** `_redirects`, the format Netlify and Cloudflare Pages/Workers read. */
16
+ /**
17
+ * `_redirects`, the format Netlify and Cloudflare Pages/Workers read, in the
18
+ * form both accept: a static build for Cloudflare, or for no named host.
19
+ */
7
20
  export const REDIRECTS_FILE: RedirectFile = {
8
21
  build: buildNetlifyRedirects,
9
22
  name: "_redirects",
10
23
  };
11
24
 
25
+ /**
26
+ * `_redirects` for a `netlify()` static build, every rule forced (`301!`).
27
+ * Astro writes a redirect page at each `from` (`dist/old/index.html`), and
28
+ * Netlify serves a file that exists ahead of an unforced rule, so without the
29
+ * flag that page would answer with a 200 instead of the redirect.
30
+ */
31
+ export const NETLIFY_REDIRECTS_FILE: RedirectFile = {
32
+ build: (redirects) => buildNetlifyRedirects(redirects, true),
33
+ name: "_redirects",
34
+ };
35
+
36
+ /** The Frameworks API config, once the build surfaced it to the project root. */
37
+ export const NETLIFY_CONFIG_FILE = join(".netlify", "v1", "config.json");
38
+
39
+ /**
40
+ * Carry the static header rules into a server build. A static `netlify()`
41
+ * build gets them from `_headers` (see `deploy/artifacts.ts`), but a server
42
+ * build's header rules go in the Frameworks API config the adapter writes
43
+ * (`.netlify/v1/config.json`, beside its own `Cache-Control` rule for
44
+ * `/_astro/*`), which follows the same rules as `netlify.toml`'s
45
+ * `[[headers]]`: the charset on the raw Markdown and text files, the homepage
46
+ * `Link` header, the discovery files' media types and CORS header, and the
47
+ * sandbox on downloaded SVGs, which Netlify serves from the publish directory
48
+ * as static files, so the prerendered endpoint's own headers never ship.
49
+ */
50
+ export const emitNetlifyHeaders = async (
51
+ project: BlumeProject,
52
+ log: BuildLog
53
+ ): Promise<void> => {
54
+ const { config, context } = project;
55
+ const configPath = join(context.root, NETLIFY_CONFIG_FILE);
56
+ if (!existsSync(configPath)) {
57
+ log.warn(
58
+ `Could not find Netlify's deploy config at ${configPath}, so the site is served without its header rules (the homepage Link header, the discovery files' media types and CORS header, and the sandbox on downloaded SVGs).`
59
+ );
60
+ return;
61
+ }
62
+ // Only `headers` is read; every other key rides through as parsed.
63
+ const frameworks: { headers?: NetlifyConfigHeaders[] } = JSON.parse(
64
+ await readFile(configPath, "utf-8")
65
+ );
66
+ const ours = buildNetlifyConfigHeaders(
67
+ config,
68
+ buildHomeLinkHeader(config, markdownRoutePaths(project))
69
+ );
70
+ // A second pass over the same build replaces its own entries.
71
+ const paths = new Set(ours.map((entry) => entry.for));
72
+ frameworks.headers = [
73
+ ...(frameworks.headers ?? []).filter((entry) => !paths.has(entry.for)),
74
+ ...ours,
75
+ ];
76
+ await writeFile(configPath, JSON.stringify(frameworks), "utf-8");
77
+ };
78
+
12
79
  /**
13
80
  * Netlify. A server build runs on Netlify Functions from the Frameworks API
14
81
  * tree the adapter writes to `.netlify/v1`, relative to the Astro root —
@@ -34,7 +101,14 @@ export const netlifyPlatform: DeployPlatform = {
34
101
  toSiteUrl(env.DEPLOY_PRIME_URL) ??
35
102
  toSiteUrl(env.DEPLOY_URL),
36
103
  },
37
- finalizeBuild: null,
104
+ finalizeBuild: async ({ isolated, log, project }) => {
105
+ // The Frameworks API config is a deploy artifact; an isolated verify
106
+ // never surfaces it.
107
+ if (!isolated) {
108
+ await emitNetlifyHeaders(project, log);
109
+ }
110
+ return true;
111
+ },
38
112
  hiddenRuntime: {
39
113
  ignoreDir: ".netlify/",
40
114
  showProjectRoot: false,
@@ -42,11 +116,14 @@ export const netlifyPlatform: DeployPlatform = {
42
116
  },
43
117
  kind: "netlify",
44
118
  negotiatesMarkdown: false,
119
+ // @astrojs/netlify declares no preview entrypoint.
120
+ previewDeploy: "netlify deploy",
45
121
  // Netlify reads `_headers` from the publish directory of a static deploy.
46
122
  // A server build's static assets ride the Frameworks API tree instead,
47
- // where the file has never been applied.
123
+ // where the file has never been applied, so `finalizeBuild` writes the
124
+ // same rules into that tree's config.
48
125
  readsHeaderFiles: { server: false, static: true },
49
- redirectFiles: [REDIRECTS_FILE],
126
+ redirectFiles: [NETLIFY_REDIRECTS_FILE],
50
127
  serverOutputDir: distDir,
51
128
  serverStaticDir: distDir,
52
129
  };
@@ -6,11 +6,12 @@ import type { DeployPlatform } from "./types.ts";
6
6
  /**
7
7
  * A self-hosted Node server. The standalone server in `dist/server` serves
8
8
  * `dist/client` through its own static handler, which has no `_headers` or
9
- * `_redirects` support — redirects are answered at request time from the
10
- * Astro config, and a header file written there would be inert. The headers
11
- * the discovery files need come from a wrapper around the server entry
12
- * instead (`deploy/node-headers.ts`). No platform env to detect: the site URL
13
- * has to be configured.
9
+ * `_redirects` support, so a file written there would be inert. A wrapper
10
+ * around the server entry (`deploy/node-headers.ts`) sets the headers the
11
+ * discovery files and downloaded SVGs need instead, and answers the
12
+ * configured redirects with their exact status, which Astro's own handler
13
+ * would default to 301 or 308. No platform env to detect: the site URL has to
14
+ * be configured.
14
15
  */
15
16
  export const nodePlatform: DeployPlatform = {
16
17
  astro: {
@@ -33,6 +34,7 @@ export const nodePlatform: DeployPlatform = {
33
34
  },
34
35
  kind: "node",
35
36
  negotiatesMarkdown: false,
37
+ previewDeploy: null,
36
38
  readsHeaderFiles: { server: false, static: false },
37
39
  redirectFiles: [],
38
40
  serverOutputDir: distDir,
@@ -22,6 +22,7 @@ export const staticPlatform: DeployPlatform = {
22
22
  },
23
23
  kind: "static",
24
24
  negotiatesMarkdown: false,
25
+ previewDeploy: null,
25
26
  readsHeaderFiles: { server: false, static: true },
26
27
  redirectFiles: [REDIRECTS_FILE, VERCEL_JSON_FILE],
27
28
  serverOutputDir: distDir,
@@ -98,6 +98,13 @@ export interface DeployPlatform {
98
98
  kind: DeployAdapterKind;
99
99
  /** Whether a server build honors `Accept: text/markdown` at the content URLs. */
100
100
  negotiatesMarkdown: boolean;
101
+ /**
102
+ * The host CLI command that deploys a preview, for a platform whose
103
+ * `@astrojs/*` adapter has no local preview server: Astro's `preview`
104
+ * throws for its server build, so `blume preview` points here instead. Null
105
+ * when the adapter can preview a server build locally, or there is none.
106
+ */
107
+ previewDeploy: string | null;
101
108
  /**
102
109
  * Whether the platform applies a `_headers` file to the static assets it
103
110
  * serves, per output mode.
@@ -124,7 +124,10 @@ export const emitVercelNegotiation = async (
124
124
  json: existsSync(join(staticDir, "404.json")),
125
125
  markdown: existsSync(join(staticDir, "404.md")),
126
126
  },
127
- crossOriginDiscoveryPaths(config)
127
+ crossOriginDiscoveryPaths(config),
128
+ // Downloaded content assets are prerendered static files; only a build
129
+ // that has them needs the SVG sandbox route.
130
+ existsSync(join(staticDir, "blume-assets"))
128
131
  );
129
132
  if (injected === null) {
130
133
  log.warn(
@@ -144,8 +147,9 @@ export const emitVercelNegotiation = async (
144
147
  * `@vercel/nft` dependency trace is rooted there too, and tracing from the
145
148
  * hidden runtime silently drops the function's chunks and `node_modules`.
146
149
  * Static assets are served from the tree's `static/` half, so the deploy
147
- * artifacts are written there; headers arrive through the routing config
148
- * rather than a `_headers` file, which Vercel never reads.
150
+ * artifacts are written there; headers (the discovery files', the sandbox on
151
+ * downloaded SVGs) arrive through the routing config rather than a
152
+ * `_headers` file, which Vercel never reads.
149
153
  */
150
154
  export const vercelPlatform: DeployPlatform = {
151
155
  astro: {
@@ -186,6 +190,8 @@ export const vercelPlatform: DeployPlatform = {
186
190
  },
187
191
  kind: "vercel",
188
192
  negotiatesMarkdown: true,
193
+ // @astrojs/vercel declares no preview entrypoint.
194
+ previewDeploy: "vercel deploy",
189
195
  readsHeaderFiles: { server: false, static: false },
190
196
  redirectFiles: [VERCEL_JSON_FILE],
191
197
  serverOutputDir: buildOutputDir,
@@ -82,10 +82,21 @@ export const platformRedirects = (config: ResolvedConfig): Redirect[] =>
82
82
  config.deployment.options.base ?? ""
83
83
  );
84
84
 
85
- /** `_redirects` text (Netlify + Cloudflare Pages): `from to status` per line. */
86
- export const buildNetlifyRedirects = (redirects: Redirect[]): string =>
85
+ /**
86
+ * `_redirects` text (Netlify + Cloudflare Pages): `from to status` per line.
87
+ * `force` appends Netlify's `!` to each status (`301!`), so the rule wins over
88
+ * the redirect page Astro writes at `from`; Cloudflare, which always applies
89
+ * its rules first, rejects a line carrying it.
90
+ */
91
+ export const buildNetlifyRedirects = (
92
+ redirects: Redirect[],
93
+ force = false
94
+ ): string =>
87
95
  `${redirects
88
- .map((redirect) => `${redirect.from} ${redirect.to} ${redirect.status}`)
96
+ .map(
97
+ (redirect) =>
98
+ `${redirect.from} ${redirect.to} ${redirect.status}${force ? "!" : ""}`
99
+ )
89
100
  .join("\n")}\n`;
90
101
 
91
102
  /** The `vercel.json` Blume writes for a static deploy of `dist/`. */
@@ -16,6 +16,8 @@
16
16
  * prerendered problem-details 404.
17
17
  */
18
18
 
19
+ import { SVG_ASSET_HEADERS } from "./headers.ts";
20
+
19
21
  /**
20
22
  * Regex for the `accept` header condition. Written to hold under both matching
21
23
  * semantics a router may apply — full-string and substring — by anchoring the
@@ -248,6 +250,30 @@ const corsRoute = (path: string): VercelRoute => ({
248
250
  src: `^${routePattern(path)}$`,
249
251
  });
250
252
 
253
+ /**
254
+ * The `src` of the route stamping the content-source SVG headers: every `.svg`
255
+ * under `/blume-assets/`. The adapter copies the static files into the Build
256
+ * Output tree without the deployment base, like every path matched here.
257
+ */
258
+ const SVG_ASSET_SRC = String.raw`^/blume-assets/.+\.svg$`;
259
+
260
+ /**
261
+ * A main-phase `continue` route stamping the sandbox headers
262
+ * (`deploy/headers.ts`) on a downloaded SVG, which Vercel serves from the
263
+ * static layer — where the prerendered asset endpoint's own headers never
264
+ * reach.
265
+ */
266
+ const svgAssetRoute = (): VercelRoute => ({
267
+ continue: true,
268
+ headers: Object.fromEntries(
269
+ Object.entries(SVG_ASSET_HEADERS).map(([name, value]) => [
270
+ name.toLowerCase(),
271
+ value,
272
+ ])
273
+ ),
274
+ src: SVG_ASSET_SRC,
275
+ });
276
+
251
277
  /** Whether a route is a `corsRoute` — the same three-field shape test as the others. */
252
278
  const isCorsRoute = (route: VercelRoute): boolean =>
253
279
  route.continue === true &&
@@ -279,7 +305,8 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
279
305
  isString(route.headers?.link) &&
280
306
  route.src === HOME_SRC &&
281
307
  Object.keys(route).length === 3) ||
282
- isCorsRoute(route);
308
+ isCorsRoute(route) ||
309
+ (route.continue === true && route.src === SVG_ASSET_SRC);
283
310
 
284
311
  /**
285
312
  * Splice the negotiation routes into a Build Output `config.json`, plus — when
@@ -297,7 +324,8 @@ const isNegotiationRoute = (route: VercelRoute): boolean =>
297
324
  * `404.md`, `notFound.json` for `404.json`), its routes go into the miss
298
325
  * phase right before the adapter's `/404.html` fallback — and nowhere when
299
326
  * that fallback is absent, since a `dest` with no file behind it would serve
300
- * nothing. Returns the updated JSON
327
+ * nothing. With `svgAssets`, a main-phase route also sandboxes the SVGs a
328
+ * content source downloaded (see {@link svgAssetRoute}). Returns the updated JSON
301
329
  * text (tab-indented, like the adapter's own output), or `null` when there is
302
330
  * nowhere safe to splice: an unparsable config, no `routes` array, or no
303
331
  * `handle: "filesystem"` marker to anchor the splice.
@@ -309,7 +337,8 @@ export const injectNegotiationRoutes = (
309
337
  contentTypeOverrides?: Record<string, string>,
310
338
  homeTokens?: number,
311
339
  notFound: NotFoundVariants = {},
312
- corsPaths: readonly string[] = []
340
+ corsPaths: readonly string[] = [],
341
+ svgAssets = false
313
342
  ): string | null => {
314
343
  const overrideEntries = Object.entries(contentTypeOverrides ?? {});
315
344
  let config: {
@@ -348,6 +377,9 @@ export const injectNegotiationRoutes = (
348
377
  });
349
378
  }
350
379
  headerRoutes.push(...corsPaths.map(corsRoute));
380
+ if (svgAssets) {
381
+ headerRoutes.push(svgAssetRoute());
382
+ }
351
383
  // Headers first: `continue` routes accumulate, so a request the rewrite
352
384
  // route then terminates (Markdown negotiation on the homepage) still carries
353
385
  // the Link header.
@@ -124,6 +124,14 @@ export const runAgentHeadless = (
124
124
  }
125
125
  });
126
126
 
127
+ // An agent that exits before reading the whole prompt (it rejected a flag,
128
+ // say) closes the pipe mid-write, and the resulting EPIPE would otherwise
129
+ // be an uncaught exception that aborts the run. The exit itself is the
130
+ // outcome: `close` resolves with the agent's code and stderr, which
131
+ // `readAgentOutput` reports.
132
+ child.stdin?.on("error", () => {
133
+ // Deliberately ignored — see above.
134
+ });
127
135
  child.stdin?.end(options.prompt);
128
136
  });
129
137
 
@@ -208,7 +216,7 @@ const claudeArgs = (context: InvocationContext): string[] => {
208
216
 
209
217
  /**
210
218
  * Codex config overrides that leave an eval run nothing but its prompt and the
211
- * docs MCP tools. `--sandbox read-only` blocks writes and the network but still
219
+ * docs MCP tools (and a `blume translate` run nothing but its prompt). `--sandbox read-only` blocks writes and the network but still
212
220
  * lets the shell tool read any file the user can, and the docs a reader
213
221
  * searches — or the answer a judge grades — can come from a remote source, so
214
222
  * an instruction planted there could otherwise pull a local secret into the
@@ -216,7 +224,7 @@ const claudeArgs = (context: InvocationContext): string[] => {
216
224
  * local-image tool off, and any subprocess started with no inherited
217
225
  * environment, there is nothing on the machine for a run to read.
218
226
  */
219
- const CODEX_LOCKDOWN = [
227
+ export const CODEX_LOCKDOWN = [
220
228
  "-c",
221
229
  "features.shell_tool=false",
222
230
  "-c",
package/src/eval/run.ts CHANGED
@@ -181,6 +181,31 @@ const runQuestion = async (
181
181
  };
182
182
  };
183
183
 
184
+ /** The findings a missed question produces: failed, or its agent run errored. */
185
+ const MISS_CODES: ReadonlySet<string> = new Set([
186
+ "BLUME_EVAL_QUESTION_ERROR",
187
+ "BLUME_EVAL_QUESTION_FAILED",
188
+ ]);
189
+
190
+ /**
191
+ * The fraction of run (non-skipped) questions that pass the CI gate. A miss on
192
+ * a `severity: warning` question is reported as a warning and never counts
193
+ * against the gate — each miss's finding carries its question's severity, so
194
+ * the gate reads the same findings the JSON summary counts, and the exit code
195
+ * can't disagree with the summary's error total.
196
+ */
197
+ export const passFraction = (result: EvalResult): number => {
198
+ const ran = result.results.length - result.counts.skip;
199
+ if (ran === 0) {
200
+ return 1;
201
+ }
202
+ const misses = result.diagnostics.filter(
203
+ (diagnostic) =>
204
+ MISS_CODES.has(diagnostic.code) && diagnostic.severity === "error"
205
+ ).length;
206
+ return (ran - misses) / ran;
207
+ };
208
+
184
209
  /**
185
210
  * Run every question through the reader/judge pair, sequentially: each
186
211
  * question is already two agent sessions, and a serial run keeps progress