blume 2.0.1 → 2.0.3

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 (492) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/cli/{chunk-qwsrynx5.js → chunk-1d7ve1dm.js} +38 -19
  3. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-1d7ve1dm.js.map} +4 -4
  4. package/dist/cli/{chunk-s6jhgk0q.js → chunk-2eytanqx.js} +2 -2
  5. package/dist/cli/{chunk-kdp5q7ke.js → chunk-2hsdwb9n.js} +18 -19
  6. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-2hsdwb9n.js.map} +2 -2
  7. package/dist/cli/{chunk-hdpx1tax.js → chunk-2z928egk.js} +5 -5
  8. package/dist/cli/{chunk-jwyddg7y.js → chunk-35d4wj9f.js} +30 -24
  9. package/dist/cli/chunk-35d4wj9f.js.map +15 -0
  10. package/dist/cli/{chunk-ah61y8py.js → chunk-364znk6q.js} +2 -2
  11. package/dist/cli/{chunk-jts8mvcz.js → chunk-3em5wd2y.js} +25 -6
  12. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-3em5wd2y.js.map} +3 -3
  13. package/dist/cli/{chunk-s1p84fyh.js → chunk-5f86nr5m.js} +63 -22
  14. package/dist/cli/chunk-5f86nr5m.js.map +11 -0
  15. package/dist/cli/{chunk-2hn4b8z7.js → chunk-5m5nmvyq.js} +170 -76
  16. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  17. package/dist/cli/{chunk-epjnccmv.js → chunk-5xvm6tfj.js} +18 -15
  18. package/dist/cli/chunk-5xvm6tfj.js.map +10 -0
  19. package/dist/cli/{chunk-fz5wtpmh.js → chunk-6dsbexzp.js} +18 -15
  20. package/dist/cli/chunk-6dsbexzp.js.map +10 -0
  21. package/dist/cli/{chunk-vtk4a6dg.js → chunk-8ktnccpt.js} +1 -1
  22. package/dist/cli/{chunk-6hsn950k.js → chunk-9t7a85s3.js} +178 -48
  23. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  24. package/dist/cli/{chunk-fxypxtvm.js → chunk-a58773jm.js} +2 -2
  25. package/dist/cli/{chunk-mb2919y2.js → chunk-acanzt5p.js} +17 -6
  26. package/dist/cli/chunk-acanzt5p.js.map +10 -0
  27. package/dist/cli/{chunk-27g6wdth.js → chunk-akbpwfxc.js} +90 -26
  28. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  29. package/dist/cli/{chunk-wm7js3j9.js → chunk-b07cmahc.js} +2 -2
  30. package/dist/cli/{chunk-wgm7m9qk.js → chunk-c8chx29p.js} +179 -51
  31. package/dist/cli/chunk-c8chx29p.js.map +36 -0
  32. package/dist/cli/{chunk-qs4q5p4e.js → chunk-crgn1q09.js} +32 -14
  33. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  34. package/dist/cli/chunk-e04dxsz1.js +39 -0
  35. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  36. package/dist/cli/{chunk-yt5n7ppj.js → chunk-ey84smr6.js} +17 -7
  37. package/dist/cli/chunk-ey84smr6.js.map +10 -0
  38. package/dist/cli/{chunk-f2z5v128.js → chunk-g4hq16wv.js} +14 -15
  39. package/dist/cli/{chunk-f2z5v128.js.map → chunk-g4hq16wv.js.map} +2 -2
  40. package/dist/cli/{chunk-fa25z98p.js → chunk-ga0pf4aj.js} +9 -8
  41. package/dist/cli/chunk-ga0pf4aj.js.map +11 -0
  42. package/dist/cli/{chunk-kpf8rrjc.js → chunk-hm3vjy5s.js} +109 -58
  43. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  44. package/dist/cli/{chunk-fs23ddbb.js → chunk-j85vccga.js} +625 -750
  45. package/dist/cli/chunk-j85vccga.js.map +36 -0
  46. package/dist/cli/{chunk-ch6g3ar0.js → chunk-p3v96n38.js} +6 -6
  47. package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-p3v96n38.js.map} +3 -3
  48. package/dist/cli/{chunk-q5163e60.js → chunk-p73c0m7w.js} +21 -19
  49. package/dist/cli/chunk-p73c0m7w.js.map +11 -0
  50. package/dist/cli/{chunk-dh8cwk36.js → chunk-pehfxfta.js} +24 -9
  51. package/dist/cli/chunk-pehfxfta.js.map +10 -0
  52. package/dist/cli/{chunk-qkqwkpte.js → chunk-pv29h0wf.js} +3688 -1313
  53. package/dist/cli/chunk-pv29h0wf.js.map +190 -0
  54. package/dist/cli/{chunk-6vm74dry.js → chunk-q58y5e6a.js} +10 -10
  55. package/dist/cli/{chunk-6vm74dry.js.map → chunk-q58y5e6a.js.map} +3 -3
  56. package/dist/cli/{chunk-zxcczpyx.js → chunk-r20tn01b.js} +1 -1
  57. package/dist/cli/{chunk-m3vmjgmq.js → chunk-r9rcc4w7.js} +16 -8
  58. package/dist/cli/chunk-r9rcc4w7.js.map +10 -0
  59. package/dist/cli/{chunk-5shv93fd.js → chunk-tkacnehg.js} +2 -2
  60. package/dist/cli/{chunk-zxh4d9vy.js → chunk-tzmab476.js} +4 -4
  61. package/dist/cli/{chunk-yw7dm696.js → chunk-vg9r4eb9.js} +24 -11
  62. package/dist/cli/chunk-vg9r4eb9.js.map +14 -0
  63. package/dist/cli/{chunk-79jhk4py.js → chunk-wrr3j9w9.js} +260 -132
  64. package/dist/cli/chunk-wrr3j9w9.js.map +35 -0
  65. package/dist/cli/{chunk-6crbhc3x.js → chunk-yfyb25rh.js} +54 -27
  66. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  67. package/dist/cli/index.js +162 -35
  68. package/dist/cli/index.js.map +4 -4
  69. package/dist/types/ai/agent-surface.d.ts +32 -0
  70. package/dist/types/ai/api-catalog.d.ts +7 -1
  71. package/dist/types/ai/ask-context.d.ts +7 -0
  72. package/dist/types/ai/component-markdown.d.ts +4 -4
  73. package/dist/types/ai/link-headers.d.ts +8 -1
  74. package/dist/types/ai/openapi-components.d.ts +5 -2
  75. package/dist/types/ai/relative-links.d.ts +11 -4
  76. package/dist/types/ai/skills.d.ts +4 -1
  77. package/dist/types/ai/static-expression.d.ts +28 -0
  78. package/dist/types/ai/tar.d.ts +1 -3
  79. package/dist/types/analytics/databuddy.d.ts +43 -0
  80. package/dist/types/analytics/index.d.ts +4 -0
  81. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  82. package/dist/types/analytics/schema.d.ts +28 -0
  83. package/dist/types/astro/integration.d.ts +3 -2
  84. package/dist/types/cli/env.d.ts +5 -0
  85. package/dist/types/cli/init/scaffold.d.ts +19 -3
  86. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  87. package/dist/types/core/base-path.d.ts +24 -1
  88. package/dist/types/core/config-input.d.ts +13 -11
  89. package/dist/types/core/config.d.ts +2 -2
  90. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  91. package/dist/types/core/graph.d.ts +2 -0
  92. package/dist/types/core/heading-markers.d.ts +5 -7
  93. package/dist/types/core/i18n-ui.d.ts +31 -0
  94. package/dist/types/core/i18n.d.ts +9 -1
  95. package/dist/types/core/last-modified.d.ts +10 -0
  96. package/dist/types/core/links.d.ts +3 -1
  97. package/dist/types/core/load-module.d.ts +10 -0
  98. package/dist/types/core/locale-links.d.ts +12 -2
  99. package/dist/types/core/meta.d.ts +13 -1
  100. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  101. package/dist/types/core/navigation.d.ts +38 -0
  102. package/dist/types/core/ordering-prefix.d.ts +4 -0
  103. package/dist/types/core/safe-links.d.ts +3 -1
  104. package/dist/types/core/schema.d.ts +59 -13
  105. package/dist/types/core/sources/github-releases.d.ts +5 -0
  106. package/dist/types/core/sources/lower.d.ts +38 -13
  107. package/dist/types/core/sources/normalize.d.ts +23 -2
  108. package/dist/types/core/sources/remote.d.ts +11 -1
  109. package/dist/types/core/sources/resolve.d.ts +12 -0
  110. package/dist/types/core/sources/types.d.ts +31 -0
  111. package/dist/types/core/sources/watch.d.ts +12 -5
  112. package/dist/types/core/standard-schema.d.ts +5 -0
  113. package/dist/types/core/types.d.ts +9 -0
  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/artifacts.d.ts +6 -4
  117. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  118. package/dist/types/deploy/headers.d.ts +36 -7
  119. package/dist/types/deploy/node-headers.d.ts +43 -8
  120. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  121. package/dist/types/deploy/platforms/node.d.ts +6 -5
  122. package/dist/types/deploy/platforms/types.d.ts +15 -0
  123. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  124. package/dist/types/deploy/redirects.d.ts +31 -15
  125. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  126. package/dist/types/markdown/directives.d.ts +62 -0
  127. package/dist/types/markdown/features.d.ts +21 -0
  128. package/dist/types/markdown/mdast.d.ts +63 -0
  129. package/dist/types/openapi/asyncapi.d.ts +4 -2
  130. package/dist/types/openapi/model.d.ts +20 -6
  131. package/dist/types/search/sync/algolia.d.ts +3 -1
  132. package/docs/01-quickstart.mdx +3 -2
  133. package/docs/02-deployment.mdx +20 -9
  134. package/docs/08-faq.mdx +10 -3
  135. package/docs/advanced/changelog.mdx +1 -1
  136. package/docs/advanced/custom-pages.mdx +7 -5
  137. package/docs/cli/audit.mdx +19 -3
  138. package/docs/cli/doctor.mdx +2 -2
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/index.mdx +4 -1
  141. package/docs/cli/translate.mdx +4 -4
  142. package/docs/cli/version.mdx +1 -1
  143. package/docs/configuration/analytics.mdx +42 -2
  144. package/docs/configuration/assistant.mdx +1 -1
  145. package/docs/configuration/customization.mdx +6 -3
  146. package/docs/configuration/index.mdx +5 -3
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +2 -2
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -1
  151. package/docs/content/index.mdx +8 -4
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +7 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -16
  156. package/docs/content/syntax.mdx +14 -0
  157. package/docs/content/versioning.mdx +2 -1
  158. package/docs/discoverability/agent-discovery.mdx +22 -8
  159. package/docs/discoverability/index.mdx +4 -3
  160. package/docs/discoverability/llms-txt.mdx +2 -5
  161. package/docs/discoverability/markdown.mdx +6 -4
  162. package/docs/discoverability/mcp.mdx +4 -0
  163. package/docs/discoverability/metadata.mdx +3 -2
  164. package/docs/discoverability/open-graph.mdx +8 -4
  165. package/docs/discoverability/rss.mdx +4 -2
  166. package/docs/references/asyncapi.mdx +2 -2
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +6 -4
  169. package/package.json +1 -1
  170. package/skills/blume-migrate/SKILL.md +6 -6
  171. package/skills/blume-migrate/references/docusaurus.md +6 -6
  172. package/skills/blume-migrate/references/fumadocs.md +1 -1
  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 +2 -2
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +9 -4
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +2 -2
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/ai-catalog.ts +6 -2
  182. package/src/ai/api/handlers.ts +2 -2
  183. package/src/ai/api/spec.ts +17 -4
  184. package/src/ai/api-catalog.ts +13 -3
  185. package/src/ai/ask-context.ts +14 -2
  186. package/src/ai/ask-data.ts +26 -12
  187. package/src/ai/changelog-markdown.ts +2 -2
  188. package/src/ai/component-markdown.ts +43 -37
  189. package/src/ai/link-headers.ts +22 -8
  190. package/src/ai/llms.ts +22 -10
  191. package/src/ai/markdown.ts +23 -11
  192. package/src/ai/mcp/discovery.ts +2 -2
  193. package/src/ai/mcp/query.ts +2 -2
  194. package/src/ai/mcp/server.ts +80 -9
  195. package/src/ai/openapi-components.ts +22 -6
  196. package/src/ai/relative-links.ts +99 -15
  197. package/src/ai/serializers.ts +2 -1
  198. package/src/ai/skills.ts +14 -1
  199. package/src/ai/static-expression.ts +416 -0
  200. package/src/ai/tar.ts +139 -12
  201. package/src/ai/visibility.ts +45 -14
  202. package/src/analytics/databuddy.ts +67 -0
  203. package/src/analytics/head.ts +8 -0
  204. package/src/analytics/index.ts +7 -0
  205. package/src/analytics/one-dollar-stats.ts +86 -0
  206. package/src/analytics/posthog.ts +21 -3
  207. package/src/analytics/schema.ts +4 -0
  208. package/src/astro/generate.ts +72 -28
  209. package/src/astro/include-hmr.ts +31 -12
  210. package/src/astro/include-refresh.ts +19 -8
  211. package/src/astro/integration.ts +108 -14
  212. package/src/astro/runtime-modules.ts +28 -13
  213. package/src/astro/templates.ts +139 -57
  214. package/src/audit/catalog.ts +2 -2
  215. package/src/audit/checks/assets.ts +25 -5
  216. package/src/audit/checks/content.ts +20 -2
  217. package/src/audit/checks/i18n.ts +46 -8
  218. package/src/audit/checks/indexability.ts +39 -19
  219. package/src/audit/checks/links.ts +22 -1
  220. package/src/audit/checks/llms.ts +6 -3
  221. package/src/audit/checks/network.ts +3 -1
  222. package/src/audit/checks/og-image.ts +10 -0
  223. package/src/audit/checks/robots.ts +6 -1
  224. package/src/audit/checks/sitemap.ts +56 -31
  225. package/src/audit/checks/social.ts +21 -2
  226. package/src/audit/crawl.ts +88 -15
  227. package/src/audit/graph.ts +3 -1
  228. package/src/audit/report.ts +54 -17
  229. package/src/audit/run.ts +13 -9
  230. package/src/audit/snapshot.ts +5 -0
  231. package/src/audit/types.ts +18 -0
  232. package/src/audit/url.ts +37 -6
  233. package/src/cli/args.ts +32 -0
  234. package/src/cli/build-failure.ts +50 -0
  235. package/src/cli/commands/audit.ts +11 -1
  236. package/src/cli/commands/build.ts +19 -5
  237. package/src/cli/commands/check.ts +2 -0
  238. package/src/cli/commands/doctor.ts +3 -1
  239. package/src/cli/commands/eval.ts +20 -18
  240. package/src/cli/commands/init.ts +15 -5
  241. package/src/cli/commands/preview.ts +15 -0
  242. package/src/cli/commands/sync.ts +2 -0
  243. package/src/cli/commands/translate.ts +18 -18
  244. package/src/cli/commands/upgrade.ts +11 -0
  245. package/src/cli/commands/validate.ts +3 -1
  246. package/src/cli/dev-lock.ts +157 -37
  247. package/src/cli/eject-scripts.ts +32 -7
  248. package/src/cli/env.ts +12 -1
  249. package/src/cli/init/scaffold.ts +117 -11
  250. package/src/cli/init/starter-spec.ts +235 -0
  251. package/src/cli/report-format.ts +11 -6
  252. package/src/components/colors.ts +19 -0
  253. package/src/components/content/AccordionItem.astro +26 -22
  254. package/src/components/content/Badge.astro +7 -12
  255. package/src/components/content/Card.astro +2 -2
  256. package/src/components/content/Component.astro +11 -5
  257. package/src/components/content/Expandable.astro +5 -1
  258. package/src/components/content/Frame.astro +2 -7
  259. package/src/components/content/GithubInfo.astro +12 -2
  260. package/src/components/content/Prompt.astro +2 -7
  261. package/src/components/content/Tab.astro +0 -1
  262. package/src/components/content/Tabs.astro +58 -5
  263. package/src/components/content/Tile.astro +1 -1
  264. package/src/components/content/Tooltip.astro +69 -7
  265. package/src/components/content/Tree.astro +7 -2
  266. package/src/components/content/TypeTable.astro +10 -5
  267. package/src/components/content/Update.astro +8 -2
  268. package/src/components/content/auto-type-table.ts +4 -1
  269. package/src/components/content/badge-color.ts +20 -0
  270. package/src/components/content/base-href.ts +14 -22
  271. package/src/components/content/inline-markdown.ts +27 -7
  272. package/src/components/copy-feedback.ts +35 -8
  273. package/src/components/islands/assistant.tsx +37 -6
  274. package/src/components/islands/base-path.ts +47 -10
  275. package/src/components/islands/hooks.ts +42 -21
  276. package/src/components/islands/webmcp.ts +16 -10
  277. package/src/components/layout/Banner.astro +23 -4
  278. package/src/components/layout/Breadcrumbs.astro +5 -2
  279. package/src/components/layout/DiscoveryLinks.astro +9 -5
  280. package/src/components/layout/Header.astro +23 -10
  281. package/src/components/layout/LanguageSwitcher.astro +2 -2
  282. package/src/components/layout/Logo.astro +5 -0
  283. package/src/components/layout/NavSelector.astro +8 -3
  284. package/src/components/layout/NavTabMenu.astro +133 -0
  285. package/src/components/layout/NavTree.astro +31 -12
  286. package/src/components/layout/NavTreeCache.astro +5 -2
  287. package/src/components/layout/NavTreeScript.astro +45 -5
  288. package/src/components/layout/PageActions.astro +4 -3
  289. package/src/components/layout/PageLayout.astro +28 -13
  290. package/src/components/layout/Pagination.astro +3 -3
  291. package/src/components/layout/ReferenceLayout.astro +4 -1
  292. package/src/components/layout/RootLayout.astro +25 -12
  293. package/src/components/layout/Search.astro +38 -14
  294. package/src/components/layout/VersionBanner.astro +2 -2
  295. package/src/components/layout/analytics-client.ts +14 -0
  296. package/src/components/layout/toc-active.ts +41 -0
  297. package/src/components/layout/toc-element.ts +8 -14
  298. package/src/components/openapi/ApiTagOperations.astro +2 -2
  299. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  300. package/src/components/openapi/GraphqlChip.astro +2 -2
  301. package/src/components/openapi/GraphqlType.astro +11 -3
  302. package/src/components/openapi/MessageComposer.astro +1 -1
  303. package/src/components/openapi/Operation.astro +23 -5
  304. package/src/components/openapi/PanelTabs.astro +4 -1
  305. package/src/components/openapi/Playground.astro +8 -2
  306. package/src/components/openapi/RequestPanel.astro +13 -6
  307. package/src/components/openapi/SchemaProperty.astro +11 -33
  308. package/src/components/openapi/SchemaTable.astro +34 -52
  309. package/src/components/openapi/async.ts +38 -6
  310. package/src/components/openapi/helpers.ts +100 -12
  311. package/src/components/openapi/message-composer.ts +14 -1
  312. package/src/components/openapi/message-model.ts +12 -2
  313. package/src/components/openapi/message.ts +21 -3
  314. package/src/components/openapi/operation-model.ts +119 -17
  315. package/src/components/openapi/panel.ts +31 -5
  316. package/src/components/openapi/param-style.ts +181 -0
  317. package/src/components/openapi/playground-client.ts +110 -23
  318. package/src/components/openapi/playground-schema.ts +25 -6
  319. package/src/components/openapi/request.ts +192 -26
  320. package/src/components/openapi/schema-tree.ts +209 -0
  321. package/src/components/openapi/snippets.ts +113 -19
  322. package/src/components/openapi/validate-json.ts +1 -1
  323. package/src/components/openapi/ws-client.ts +18 -2
  324. package/src/core/base-path.ts +69 -8
  325. package/src/core/config-input.ts +13 -11
  326. package/src/core/config.ts +18 -4
  327. package/src/core/diagnostics.ts +219 -30
  328. package/src/core/directive-diagnostics.ts +99 -0
  329. package/src/core/frontmatter.ts +21 -18
  330. package/src/core/graph.ts +120 -11
  331. package/src/core/heading-markers.ts +5 -18
  332. package/src/core/i18n-ui.ts +39 -2
  333. package/src/core/i18n.ts +44 -5
  334. package/src/core/last-modified.ts +25 -3
  335. package/src/core/links.ts +7 -4
  336. package/src/core/load-module.ts +20 -0
  337. package/src/core/locale-links.ts +22 -18
  338. package/src/core/manifest.ts +3 -2
  339. package/src/core/meta.ts +89 -12
  340. package/src/core/nav-diagnostics.ts +56 -1
  341. package/src/core/navigation.ts +272 -83
  342. package/src/core/ordering-prefix.ts +27 -0
  343. package/src/core/project-graph.ts +50 -6
  344. package/src/core/safe-href.ts +53 -1
  345. package/src/core/safe-links.ts +11 -2
  346. package/src/core/schema.ts +94 -18
  347. package/src/core/sources/assets.ts +83 -41
  348. package/src/core/sources/contentful-rich-text.ts +28 -20
  349. package/src/core/sources/contentful.ts +25 -12
  350. package/src/core/sources/filesystem.ts +25 -3
  351. package/src/core/sources/github-releases.ts +131 -11
  352. package/src/core/sources/lexical.ts +30 -20
  353. package/src/core/sources/lower.ts +276 -56
  354. package/src/core/sources/mdx-remote.ts +52 -16
  355. package/src/core/sources/normalize.ts +456 -119
  356. package/src/core/sources/notion.ts +77 -34
  357. package/src/core/sources/obsidian.ts +40 -9
  358. package/src/core/sources/payload.ts +1 -0
  359. package/src/core/sources/portable-text.ts +85 -49
  360. package/src/core/sources/remote.ts +18 -2
  361. package/src/core/sources/resolve.ts +52 -33
  362. package/src/core/sources/sanity.ts +1 -0
  363. package/src/core/sources/strapi-blocks.ts +28 -19
  364. package/src/core/sources/strapi.ts +1 -0
  365. package/src/core/sources/types.ts +31 -0
  366. package/src/core/sources/watch.ts +20 -7
  367. package/src/core/standard-schema.ts +10 -6
  368. package/src/core/types.ts +9 -0
  369. package/src/core/ui-packs/ar.ts +13 -0
  370. package/src/core/ui-packs/bg.ts +13 -0
  371. package/src/core/ui-packs/bn.ts +13 -0
  372. package/src/core/ui-packs/ca.ts +13 -0
  373. package/src/core/ui-packs/cs.ts +13 -0
  374. package/src/core/ui-packs/da.ts +13 -0
  375. package/src/core/ui-packs/de.ts +13 -0
  376. package/src/core/ui-packs/el.ts +13 -0
  377. package/src/core/ui-packs/es.ts +13 -0
  378. package/src/core/ui-packs/fa.ts +13 -0
  379. package/src/core/ui-packs/fi.ts +13 -0
  380. package/src/core/ui-packs/fr.ts +13 -0
  381. package/src/core/ui-packs/he.ts +13 -0
  382. package/src/core/ui-packs/hi.ts +13 -0
  383. package/src/core/ui-packs/hr.ts +13 -0
  384. package/src/core/ui-packs/hu.ts +13 -0
  385. package/src/core/ui-packs/id.ts +13 -0
  386. package/src/core/ui-packs/it.ts +13 -0
  387. package/src/core/ui-packs/ja.ts +13 -0
  388. package/src/core/ui-packs/ko.ts +13 -0
  389. package/src/core/ui-packs/nl.ts +13 -0
  390. package/src/core/ui-packs/no.ts +13 -0
  391. package/src/core/ui-packs/pl.ts +13 -0
  392. package/src/core/ui-packs/pt-br.ts +13 -0
  393. package/src/core/ui-packs/pt.ts +13 -0
  394. package/src/core/ui-packs/ro.ts +13 -0
  395. package/src/core/ui-packs/ru.ts +13 -0
  396. package/src/core/ui-packs/sk.ts +13 -0
  397. package/src/core/ui-packs/sr.ts +13 -0
  398. package/src/core/ui-packs/sv.ts +13 -0
  399. package/src/core/ui-packs/th.ts +13 -0
  400. package/src/core/ui-packs/tr.ts +13 -0
  401. package/src/core/ui-packs/uk.ts +13 -0
  402. package/src/core/ui-packs/vi.ts +13 -0
  403. package/src/core/ui-packs/zh-tw.ts +13 -0
  404. package/src/core/ui-packs/zh.ts +13 -0
  405. package/src/core/version-cut.ts +69 -17
  406. package/src/core/versions.ts +4 -1
  407. package/src/deploy/adapters/node.ts +5 -2
  408. package/src/deploy/adapters/registry.ts +2 -1
  409. package/src/deploy/adapters/types.ts +13 -1
  410. package/src/deploy/artifacts.ts +52 -11
  411. package/src/deploy/cloudflare-negotiation.ts +27 -16
  412. package/src/deploy/headers.ts +75 -55
  413. package/src/deploy/node-headers.ts +148 -27
  414. package/src/deploy/platforms/cloudflare.ts +13 -3
  415. package/src/deploy/platforms/netlify.ts +83 -5
  416. package/src/deploy/platforms/node.ts +8 -5
  417. package/src/deploy/platforms/static.ts +2 -0
  418. package/src/deploy/platforms/types.ts +15 -0
  419. package/src/deploy/platforms/vercel.ts +10 -3
  420. package/src/deploy/redirects.ts +68 -21
  421. package/src/deploy/robots.ts +2 -2
  422. package/src/deploy/rss.ts +4 -3
  423. package/src/deploy/sitemap.ts +7 -5
  424. package/src/deploy/vercel-negotiation.ts +35 -3
  425. package/src/eval/agents.ts +10 -2
  426. package/src/eval/run.ts +25 -0
  427. package/src/markdown/base-links.ts +74 -29
  428. package/src/markdown/directives.ts +242 -36
  429. package/src/markdown/features.ts +17 -0
  430. package/src/markdown/index.ts +13 -9
  431. package/src/markdown/mdast.ts +5 -2
  432. package/src/markdown/relative-links.ts +15 -27
  433. package/src/markdown/route-snapshot.ts +37 -0
  434. package/src/og/card.ts +149 -9
  435. package/src/og/derive.ts +156 -5
  436. package/src/og/index.ts +1 -0
  437. package/src/openapi/asyncapi.ts +4 -2
  438. package/src/openapi/graphql-build.ts +28 -2
  439. package/src/openapi/model.ts +99 -27
  440. package/src/openapi/proxy.ts +63 -10
  441. package/src/openapi/render-mdx.ts +51 -3
  442. package/src/registry/eject.ts +297 -43
  443. package/src/search/adapters/version-scope.ts +30 -0
  444. package/src/search/documents.ts +89 -27
  445. package/src/search/popular.ts +2 -1
  446. package/src/search/sync/algolia.ts +36 -2
  447. package/src/seo/jsonld.ts +7 -3
  448. package/src/sources/registry.ts +5 -0
  449. package/src/theme/entry.ts +11 -4
  450. package/src/translate/agents.ts +6 -1
  451. package/src/translate/ledger.ts +26 -3
  452. package/src/translate/meta.ts +76 -24
  453. package/src/translate/run.ts +14 -8
  454. package/src/translate/validate.ts +14 -2
  455. package/src/translate/work-list.ts +91 -19
  456. package/src/upgrade/upgrade.ts +36 -4
  457. package/dist/cli/chunk-27g6wdth.js.map +0 -10
  458. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  459. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  460. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  461. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  462. package/dist/cli/chunk-82bbrxdn.js +0 -51
  463. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  464. package/dist/cli/chunk-abh8yjkn.js +0 -31
  465. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  466. package/dist/cli/chunk-ce574jw2.js +0 -23
  467. package/dist/cli/chunk-ce574jw2.js.map +0 -10
  468. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  469. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  470. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  471. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  472. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  473. package/dist/cli/chunk-jwyddg7y.js.map +0 -15
  474. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  475. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  476. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  477. package/dist/cli/chunk-q5163e60.js.map +0 -11
  478. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  479. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  480. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  481. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  482. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  483. package/dist/cli/chunk-yw7dm696.js.map +0 -14
  484. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-2eytanqx.js.map} +0 -0
  485. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-2z928egk.js.map} +0 -0
  486. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-364znk6q.js.map} +0 -0
  487. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-8ktnccpt.js.map} +0 -0
  488. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-a58773jm.js.map} +0 -0
  489. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-b07cmahc.js.map} +0 -0
  490. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-r20tn01b.js.map} +0 -0
  491. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-tkacnehg.js.map} +0 -0
  492. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-tzmab476.js.map} +0 -0
@@ -39,8 +39,9 @@
39
39
  *
40
40
  * Cloudflare does not apply `_headers` to worker-first routes, so the wrapper
41
41
  * also re-stamps what the static layer would otherwise add on the routes it
42
- * takes over: the homepage agent-discovery `Link` header and the Markdown
43
- * `charset=utf-8` (see `deploy/headers.ts`). The raw `.md`/`.mdx` URLs are
42
+ * takes over: the homepage agent-discovery `Link` header, the Markdown
43
+ * `charset=utf-8`, and the sandbox on the SVGs a content source downloaded
44
+ * (see `deploy/headers.ts`). The raw `.md`/`.mdx` URLs are
44
45
  * exempted from worker-first routing with negative rules, keeping their
45
46
  * `_headers` treatment and their zero-Worker serving path.
46
47
  *
@@ -83,7 +84,8 @@ import {
83
84
  API_PAGES_PATH,
84
85
  OPENAPI_PATH,
85
86
  } from "../ai/api/paths.ts";
86
- import { normalizePath } from "../core/base-path.ts";
87
+ import { normalizeBasePath, normalizePath } from "../core/base-path.ts";
88
+ import { CONTENT_ASSETS_ROOT, SVG_ASSET_HEADERS } from "./headers.ts";
87
89
  import type { NotFoundVariants } from "./vercel-negotiation.ts";
88
90
 
89
91
  /** Filename of the generated wrapper Worker, next to the adapter's entry. */
@@ -130,14 +132,6 @@ const STATIC_EXEMPTIONS = [
130
132
  API_NAVIGATION_PATH,
131
133
  ];
132
134
 
133
- /**
134
- * The deployment base as a rule/URL prefix: trailing slash stripped, empty
135
- * for a root deploy — the same normalization the dev middleware applies in
136
- * `astro/markdown-negotiation.ts`.
137
- */
138
- const basePrefix = (base?: string): string =>
139
- base && base !== "/" ? base.replace(/\/$/u, "") : "";
140
-
141
135
  /** A value as `JSON.parse` produces it (the wrangler config's value space). */
142
136
  type JsonValue =
143
137
  | string
@@ -178,7 +172,7 @@ const ruleBody = (rule: string): string =>
178
172
  * status (see the module comment).
179
173
  */
180
174
  export const buildRunWorkerFirstRules = (base?: string): string[] => {
181
- const prefix = encodeURI(basePrefix(base));
175
+ const prefix = encodeURI(normalizeBasePath(base));
182
176
  const exemptions = STATIC_EXEMPTIONS.map((path) => `!${prefix}${path}`);
183
177
  return prefix
184
178
  ? [prefix, `${prefix}/*`, ...exemptions]
@@ -280,7 +274,7 @@ export const buildNegotiationWorker = (
280
274
  const routes = JSON.stringify(options.routePaths);
281
275
  const pageJson = JSON.stringify(options.pageJsonPaths ?? []);
282
276
  const binding = JSON.stringify(options.assetsBinding);
283
- const prefix = JSON.stringify(encodeURI(basePrefix(options.base)));
277
+ const prefix = JSON.stringify(encodeURI(normalizeBasePath(options.base)));
284
278
  const homeLinkHeader = JSON.stringify(options.homeLinkHeader ?? null);
285
279
  const homeTokens = JSON.stringify(
286
280
  options.homeTokens === undefined ? null : String(options.homeTokens)
@@ -314,8 +308,8 @@ export const buildNegotiationWorker = (
314
308
  // request is delegated to the Astro Worker untouched — except a missing
315
309
  // page, whose HTML 404 shell is swapped for the prerendered Markdown or JSON
316
310
  // 404 twin when the client prefers one. \`_headers\` does not apply to
317
- // worker-first routes, so the homepage Link header and the Markdown charset
318
- // are re-stamped here.
311
+ // worker-first routes, so the homepage Link header, the Markdown charset, and
312
+ // the sandbox on downloaded SVGs are re-stamped here.
319
313
  import server from ${JSON.stringify(options.mainSpecifier)};
320
314
 
321
315
  const ROUTES = new Set(${routes});
@@ -326,6 +320,16 @@ const HOME_LINK_HEADER = ${homeLinkHeader};
326
320
  const HOME_TOKENS = ${homeTokens};
327
321
  const REDIRECTS = ${redirects};
328
322
  const NOT_FOUND = ${notFound};
323
+ const SVG_ASSET_HEADERS = ${JSON.stringify(SVG_ASSET_HEADERS)};
324
+
325
+ // An SVG a content source downloaded: a document that could run script as the
326
+ // docs site if opened directly, so it goes out sandboxed. Matched on the
327
+ // decoded path, ignoring case — the file the assets binding serves.
328
+ const isSvgAsset = (pathname) => {
329
+ const path = safeDecode(pathname).toLowerCase();
330
+ const prefix = (safeDecode(BASE_PREFIX) + "${CONTENT_ASSETS_ROOT}/").toLowerCase();
331
+ return path.startsWith(prefix) && path.endsWith(".svg");
332
+ };
329
333
 
330
334
  // Configured redirects live in \`_redirects\`, which only the static layer
331
335
  // reads — a worker-first route never reaches it. Answering from this baked-in
@@ -526,6 +530,13 @@ export default {
526
530
  status: redirect[1],
527
531
  });
528
532
  }
533
+ if (isSvgAsset(url.pathname)) {
534
+ return withHeaders(await server.fetch(request, env, context), (headers) => {
535
+ for (const [name, value] of Object.entries(SVG_ASSET_HEADERS)) {
536
+ headers.set(name, value);
537
+ }
538
+ });
539
+ }
529
540
  if (request.method !== "GET" && request.method !== "HEAD") {
530
541
  return server.fetch(request, env, context);
531
542
  }
@@ -661,7 +672,7 @@ export const injectWorkerNegotiation = (
661
672
  // The content-route guard compares against redirect `from`s, which carry the
662
673
  // full `{deployment.base}{basePath}` stack; the routes carry only
663
674
  // `basePath`, so the deployment base is applied here.
664
- const deployPrefix = basePrefix(options.base);
675
+ const deployPrefix = normalizeBasePath(options.base);
665
676
  const guardRoutes = new Set(
666
677
  contentRoutePaths.map((route) =>
667
678
  normalizePath(
@@ -29,45 +29,44 @@ import type { ResolvedConfig } from "../core/schema.ts";
29
29
 
30
30
  /**
31
31
  * One rule per served extension. `.mdx` uses `text/markdown` to match the
32
- * runtime endpoint, which serves both variants as `text/markdown`. The raw
33
- * Markdown mirrors live under the page routes (which carry `basePath`), while
34
- * the `.txt` files (`llms.txt`, `llms-full.txt`) are written to the dist root
35
- * and served at the deployment base — so only the `.md`/`.mdx` rules take the
36
- * `basePath` layer.
32
+ * runtime endpoint, which serves both variants as `text/markdown`. Every rule
33
+ * sits at the deployment base, not under `basePath`: most Markdown mirrors
34
+ * live under the page routes, but the section home's mirror (`/docs.md`) sits
35
+ * beside `basePath`, and the synthesized `/index.md`, `/changelog.md`, and
36
+ * `/404.md` sit at the root, as do the `.txt` files (`llms.txt`,
37
+ * `llms-full.txt`).
37
38
  */
38
- const HEADER_RULES: readonly {
39
- contentType: string;
40
- ext: string;
41
- underBasePath: boolean;
42
- }[] = [
43
- {
44
- contentType: "text/markdown; charset=utf-8",
45
- ext: "md",
46
- underBasePath: true,
47
- },
48
- {
49
- contentType: "text/markdown; charset=utf-8",
50
- ext: "mdx",
51
- underBasePath: true,
52
- },
53
- {
54
- contentType: "text/plain; charset=utf-8",
55
- ext: "txt",
56
- underBasePath: false,
57
- },
39
+ const HEADER_RULES: readonly { contentType: string; ext: string }[] = [
40
+ { contentType: "text/markdown; charset=utf-8", ext: "md" },
41
+ { contentType: "text/markdown; charset=utf-8", ext: "mdx" },
42
+ { contentType: "text/plain; charset=utf-8", ext: "txt" },
58
43
  ];
59
44
 
60
45
  /** Where the generated endpoint serves content-source images from. */
61
- const CONTENT_ASSETS_ROOT = "/blume-assets";
46
+ export const CONTENT_ASSETS_ROOT = "/blume-assets";
62
47
 
63
48
  /**
64
49
  * The policy a content-source SVG is served with: an opaque origin with
65
50
  * scripts off, so an uploaded SVG that carries a `<script>` stays inert when
66
- * opened directly. The endpoint sends it from the server and dev runtimes;
67
- * {@link headerRules} carries it to static hosts.
51
+ * opened directly.
68
52
  */
69
53
  export const SVG_ASSET_POLICY = "sandbox";
70
54
 
55
+ /**
56
+ * The headers a content-source SVG is served with: the sandbox policy, and no
57
+ * MIME sniffing. The endpoint sends them from the dev runtime; every host
58
+ * gets them for the built files — static hosts through {@link headerRules},
59
+ * server builds through each platform's own mechanism (`deploy/platforms`).
60
+ */
61
+ export const SVG_ASSET_HEADERS = {
62
+ "Content-Security-Policy": SVG_ASSET_POLICY,
63
+ "X-Content-Type-Options": "nosniff",
64
+ } satisfies Record<string, string>;
65
+
66
+ /** The served glob of content-source SVGs, under the deployment base. */
67
+ export const svgAssetPath = (config: ResolvedConfig): string =>
68
+ `${normalizeBasePath(config.deployment.options.base)}${CONTENT_ASSETS_ROOT}/*.svg`;
69
+
71
70
  /**
72
71
  * One static-host header rule: a served path (a `*` glob spanning path
73
72
  * segments, or an exact path) and the header it sets there.
@@ -80,11 +79,9 @@ export interface HeaderRule {
80
79
 
81
80
  /**
82
81
  * The header rules every static host should apply, in `_headers` glob syntax.
83
- * The glob carries the served prefix (`{deployment.base}{basePath}` for the
84
- * Markdown mirrors, `{deployment.base}` for the root `.txt` files) so the rules
85
- * still match once the site is mounted under a subpath (`/docs/*.md`); the
86
- * wildcard spans path segments, so a nested route like `/docs/ja/intro.md`
87
- * matches too.
82
+ * The glob carries the deployment base so the rules still match once the site
83
+ * is mounted under a subpath (`/docs/*.md`); the wildcard spans path
84
+ * segments, so a nested route like `/docs/ja/intro.md` matches too.
88
85
  *
89
86
  * When a homepage `Link` header is provided (see `ai/link-headers.ts`), an
90
87
  * exact-path rule for the root page advertises the agent-discovery resources —
@@ -95,16 +92,11 @@ export const headerRules = (
95
92
  homeLinkHeader?: string | null
96
93
  ): HeaderRule[] => {
97
94
  const deployBase = normalizeBasePath(config.deployment.options.base);
98
- const rules: HeaderRule[] = HEADER_RULES.map((rule) => {
99
- const prefix = rule.underBasePath
100
- ? `${deployBase}${config.basePath}`
101
- : deployBase;
102
- return {
103
- name: "Content-Type",
104
- path: `${prefix}/*.${rule.ext}`,
105
- value: rule.contentType,
106
- };
107
- });
95
+ const rules: HeaderRule[] = HEADER_RULES.map((rule) => ({
96
+ name: "Content-Type",
97
+ path: `${deployBase}/*.${rule.ext}`,
98
+ value: rule.contentType,
99
+ }));
108
100
  if (homeLinkHeader) {
109
101
  rules.push({ name: "Link", path: `${deployBase}/`, value: homeLinkHeader });
110
102
  }
@@ -134,9 +126,8 @@ export const headerRules = (
134
126
  value: "*",
135
127
  });
136
128
  }
137
- // Published skills live at the deployment base, outside `basePath` — the
138
- // `.md` charset rule above misses them whenever a basePath is set, and the
139
- // RFC wants archives served as application/gzip explicitly.
129
+ // Published skills live at the deployment base, and the RFC wants their
130
+ // archives served as application/gzip explicitly.
140
131
  if (config.agents.skills) {
141
132
  rules.push(
142
133
  {
@@ -155,11 +146,9 @@ export const headerRules = (
155
146
  // are served from the docs origin, and an SVG opened directly is a document
156
147
  // that can run script. A sandbox policy keeps it inert; an `<img>` that
157
148
  // embeds it is unaffected.
158
- rules.push({
159
- name: "Content-Security-Policy",
160
- path: `${deployBase}${CONTENT_ASSETS_ROOT}/*.svg`,
161
- value: SVG_ASSET_POLICY,
162
- });
149
+ for (const [name, value] of Object.entries(SVG_ASSET_HEADERS)) {
150
+ rules.push({ name, path: svgAssetPath(config), value });
151
+ }
163
152
  return rules;
164
153
  };
165
154
 
@@ -175,6 +164,33 @@ export const buildNetlifyHeaders = (
175
164
  .map((rule) => `${rule.path}\n ${rule.name}: ${rule.value}`)
176
165
  .join("\n")}\n`;
177
166
 
167
+ /** One entry of Netlify's Frameworks API config `headers` array. */
168
+ export interface NetlifyConfigHeaders {
169
+ for: string;
170
+ values: Record<string, string>;
171
+ }
172
+
173
+ /**
174
+ * The {@link headerRules} as Netlify Frameworks API `headers` entries
175
+ * (`.netlify/v1/config.json`), one per path with every header it sets. A
176
+ * `netlify()` server build reads no `_headers` file, so it carries the same
177
+ * rules here, in the `for`/`values` form that follows `netlify.toml`'s
178
+ * `[[headers]]` and the same path syntax as `_headers`.
179
+ */
180
+ export const buildNetlifyConfigHeaders = (
181
+ config: ResolvedConfig,
182
+ homeLinkHeader?: string | null
183
+ ): NetlifyConfigHeaders[] => {
184
+ const byPath = new Map<string, Record<string, string>>();
185
+ for (const rule of headerRules(config, homeLinkHeader)) {
186
+ byPath.set(rule.path, {
187
+ ...byPath.get(rule.path),
188
+ [rule.name]: rule.value,
189
+ });
190
+ }
191
+ return [...byPath].map(([path, values]) => ({ for: path, values }));
192
+ };
193
+
178
194
  /** One entry of `vercel.json`'s `headers` array. */
179
195
  export interface VercelHeader {
180
196
  headers: { key: string; value: string }[];
@@ -184,6 +200,13 @@ export interface VercelHeader {
184
200
  /** The characters `path-to-regexp` would read as syntax in a literal path. */
185
201
  const SOURCE_SYNTAX = /[\\(){}?+:]/gu;
186
202
 
203
+ /**
204
+ * A literal path as `path-to-regexp` source text — what a `vercel.json`
205
+ * `source` is — with each syntax character escaped, so it matches only itself.
206
+ */
207
+ export const escapeVercelSource = (path: string): string =>
208
+ path.replaceAll(SOURCE_SYNTAX, String.raw`\$&`);
209
+
187
210
  /**
188
211
  * A `_headers` path as a `vercel.json` `source`, which `path-to-regexp` reads:
189
212
  * the literal parts are escaped, and a `*` glob becomes the `(.*)` group that
@@ -193,10 +216,7 @@ const SOURCE_SYNTAX = /[\\(){}?+:]/gu;
193
216
  const vercelSource = (path: string): string => {
194
217
  const trimmed =
195
218
  path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
196
- return trimmed
197
- .split("*")
198
- .map((part) => part.replaceAll(SOURCE_SYNTAX, String.raw`\$&`))
199
- .join("(.*)");
219
+ return trimmed.split("*").map(escapeVercelSource).join("(.*)");
200
220
  };
201
221
 
202
222
  /**
@@ -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(project)
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
  };
@@ -11,6 +11,7 @@ import {
11
11
  markdownRoutePaths,
12
12
  markdownTokenCount,
13
13
  } from "../../ai/markdown.ts";
14
+ import { normalizeBasePath } from "../../core/base-path.ts";
14
15
  import type { BlumeProject } from "../../core/project-graph.ts";
15
16
  import type { ProjectContext } from "../../core/types.ts";
16
17
  import { CLOUDFLARE_ADAPTER_PACKAGE } from "../adapters/cloudflare.ts";
@@ -90,8 +91,13 @@ export const emitCloudflareNegotiation = async (
90
91
  const home = rawMarkdown["/"];
91
92
  // The 404 twins the wrapper may substitute for the HTML shell; only wired
92
93
  // when the build actually emitted them (a project that owns `/404` gets
93
- // neither), like the Vercel routing config.
94
- const staticDir = clientDir(context);
94
+ // neither), like the Vercel routing config. The adapter writes the
95
+ // prerendered files under the deployment base (`dist/client/<base>/`),
96
+ // where the wrapper fetches them from.
97
+ const staticDir = join(
98
+ clientDir(context),
99
+ normalizeBasePath(config.deployment.options.base)
100
+ );
95
101
  const injected = injectWorkerNegotiation(
96
102
  await readFile(wranglerPath, "utf-8"),
97
103
  {
@@ -119,7 +125,7 @@ export const emitCloudflareNegotiation = async (
119
125
  // based the same way the platform files are — it answers any the
120
126
  // worker-first rules claim, where `_redirects` is never consulted and
121
127
  // Astro would default their status.
122
- redirects: platformRedirects(config),
128
+ redirects: platformRedirects(project),
123
129
  routePaths,
124
130
  }
125
131
  );
@@ -305,8 +311,12 @@ export const cloudflarePlatform: DeployPlatform = {
305
311
  },
306
312
  kind: "cloudflare",
307
313
  negotiatesMarkdown: true,
314
+ previewDeploy: null,
308
315
  readsHeaderFiles: { server: true, static: true },
309
316
  redirectFiles: [REDIRECTS_FILE],
317
+ // The adapter points `build.client` at `dist/client/<base>/` for a server
318
+ // build, and hoists its own `_headers`/`_redirects` back up to `dist/client`.
319
+ serverClientUnderBase: true,
310
320
  serverOutputDir: distDir,
311
321
  serverStaticDir: clientDir,
312
322
  };