blume 2.0.2 → 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 (293) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
  3. package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
  4. package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
  5. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
  6. package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
  7. package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
  8. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
  9. package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
  10. package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
  11. package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
  12. package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
  13. package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
  14. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  15. package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
  16. package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
  17. package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
  18. package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
  19. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  20. package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
  21. package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
  22. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
  23. package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
  24. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  25. package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
  26. package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
  27. package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
  28. package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
  29. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  30. package/dist/cli/chunk-e04dxsz1.js +39 -0
  31. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  32. package/dist/cli/{chunk-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
  33. package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
  34. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  35. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  36. package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
  37. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  38. package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
  39. package/dist/cli/chunk-j85vccga.js.map +36 -0
  40. package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
  41. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
  42. package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
  43. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
  44. package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
  45. package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
  46. package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
  47. package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
  48. package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
  49. package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
  50. package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
  51. package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
  52. package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
  53. package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
  54. package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
  55. package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
  56. package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
  57. package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
  58. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  59. package/dist/cli/index.js +20 -18
  60. package/dist/cli/index.js.map +3 -3
  61. package/dist/types/ai/link-headers.d.ts +8 -1
  62. package/dist/types/ai/openapi-components.d.ts +5 -2
  63. package/dist/types/ai/relative-links.d.ts +9 -7
  64. package/dist/types/ai/skills.d.ts +4 -1
  65. package/dist/types/ai/tar.d.ts +1 -3
  66. package/dist/types/analytics/index.d.ts +2 -0
  67. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  68. package/dist/types/analytics/schema.d.ts +14 -0
  69. package/dist/types/astro/integration.d.ts +3 -2
  70. package/dist/types/core/base-path.d.ts +21 -7
  71. package/dist/types/core/config-input.d.ts +9 -7
  72. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  73. package/dist/types/core/heading-markers.d.ts +5 -7
  74. package/dist/types/core/i18n.d.ts +2 -0
  75. package/dist/types/core/last-modified.d.ts +10 -0
  76. package/dist/types/core/meta.d.ts +8 -0
  77. package/dist/types/core/schema.d.ts +7 -0
  78. package/dist/types/core/sources/lower.d.ts +29 -16
  79. package/dist/types/core/sources/normalize.d.ts +13 -1
  80. package/dist/types/core/sources/watch.d.ts +12 -5
  81. package/dist/types/core/standard-schema.d.ts +5 -0
  82. package/dist/types/deploy/artifacts.d.ts +6 -4
  83. package/dist/types/deploy/headers.d.ts +5 -0
  84. package/dist/types/deploy/platforms/types.d.ts +8 -0
  85. package/dist/types/deploy/redirects.d.ts +24 -13
  86. package/dist/types/markdown/directives.d.ts +62 -0
  87. package/dist/types/markdown/features.d.ts +21 -0
  88. package/dist/types/markdown/mdast.d.ts +63 -0
  89. package/dist/types/openapi/asyncapi.d.ts +4 -2
  90. package/docs/02-deployment.mdx +4 -2
  91. package/docs/08-faq.mdx +1 -1
  92. package/docs/advanced/custom-pages.mdx +3 -3
  93. package/docs/cli/audit.mdx +17 -1
  94. package/docs/cli/evals.mdx +3 -3
  95. package/docs/cli/translate.mdx +3 -3
  96. package/docs/cli/version.mdx +1 -1
  97. package/docs/configuration/analytics.mdx +20 -1
  98. package/docs/configuration/customization.mdx +2 -0
  99. package/docs/configuration/search.mdx +1 -1
  100. package/docs/content/components.mdx +1 -1
  101. package/docs/content/frontmatter.mdx +1 -1
  102. package/docs/content/i18n.mdx +2 -0
  103. package/docs/content/index.mdx +4 -2
  104. package/docs/content/islands.mdx +1 -1
  105. package/docs/content/meta.mdx +3 -1
  106. package/docs/content/sources.mdx +1 -5
  107. package/docs/content/syntax.mdx +14 -0
  108. package/docs/content/versioning.mdx +1 -1
  109. package/docs/discoverability/agent-discovery.mdx +1 -1
  110. package/docs/discoverability/index.mdx +2 -1
  111. package/docs/discoverability/markdown.mdx +2 -2
  112. package/docs/discoverability/open-graph.mdx +5 -3
  113. package/docs/discoverability/rss.mdx +1 -1
  114. package/docs/references/asyncapi.mdx +1 -1
  115. package/docs/references/graphql.mdx +1 -1
  116. package/docs/references/openapi.mdx +5 -3
  117. package/package.json +1 -1
  118. package/skills/blume-migrate/references/fumadocs.md +1 -1
  119. package/skills/blume-migrate/references/nextra.md +1 -1
  120. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
  121. package/src/ai/agent-readability.ts +2 -2
  122. package/src/ai/ai-catalog.ts +6 -2
  123. package/src/ai/api/handlers.ts +2 -2
  124. package/src/ai/api/spec.ts +2 -2
  125. package/src/ai/api-catalog.ts +6 -2
  126. package/src/ai/changelog-markdown.ts +2 -2
  127. package/src/ai/link-headers.ts +14 -5
  128. package/src/ai/llms.ts +18 -7
  129. package/src/ai/mcp/discovery.ts +2 -2
  130. package/src/ai/mcp/query.ts +2 -2
  131. package/src/ai/mcp/server.ts +2 -2
  132. package/src/ai/openapi-components.ts +22 -6
  133. package/src/ai/relative-links.ts +42 -26
  134. package/src/ai/serializers.ts +2 -1
  135. package/src/ai/skills.ts +14 -1
  136. package/src/ai/tar.ts +139 -12
  137. package/src/analytics/head.ts +4 -0
  138. package/src/analytics/index.ts +5 -0
  139. package/src/analytics/one-dollar-stats.ts +86 -0
  140. package/src/analytics/schema.ts +2 -0
  141. package/src/astro/generate.ts +1 -1
  142. package/src/astro/include-hmr.ts +31 -12
  143. package/src/astro/include-refresh.ts +19 -8
  144. package/src/astro/integration.ts +108 -14
  145. package/src/astro/runtime-modules.ts +28 -13
  146. package/src/astro/templates.ts +81 -37
  147. package/src/audit/checks/links.ts +8 -1
  148. package/src/audit/graph.ts +3 -1
  149. package/src/audit/run.ts +12 -9
  150. package/src/audit/snapshot.ts +5 -0
  151. package/src/cli/args.ts +32 -0
  152. package/src/cli/commands/audit.ts +4 -1
  153. package/src/cli/commands/doctor.ts +3 -1
  154. package/src/cli/commands/eval.ts +19 -11
  155. package/src/cli/commands/translate.ts +9 -12
  156. package/src/cli/commands/validate.ts +3 -1
  157. package/src/cli/dev-lock.ts +157 -37
  158. package/src/cli/report-format.ts +11 -6
  159. package/src/components/colors.ts +19 -0
  160. package/src/components/content/Badge.astro +5 -3
  161. package/src/components/content/Component.astro +2 -2
  162. package/src/components/content/Tab.astro +0 -1
  163. package/src/components/content/Tabs.astro +4 -0
  164. package/src/components/content/badge-color.ts +5 -2
  165. package/src/components/content/base-href.ts +13 -36
  166. package/src/components/islands/assistant.tsx +21 -4
  167. package/src/components/islands/base-path.ts +47 -10
  168. package/src/components/islands/hooks.ts +35 -19
  169. package/src/components/islands/webmcp.ts +4 -2
  170. package/src/components/layout/Breadcrumbs.astro +5 -2
  171. package/src/components/layout/DiscoveryLinks.astro +9 -5
  172. package/src/components/layout/Header.astro +2 -2
  173. package/src/components/layout/LanguageSwitcher.astro +2 -2
  174. package/src/components/layout/NavSelector.astro +2 -2
  175. package/src/components/layout/NavTabMenu.astro +3 -3
  176. package/src/components/layout/NavTree.astro +16 -6
  177. package/src/components/layout/PageActions.astro +4 -3
  178. package/src/components/layout/PageLayout.astro +7 -6
  179. package/src/components/layout/Pagination.astro +3 -3
  180. package/src/components/layout/RootLayout.astro +5 -5
  181. package/src/components/layout/Search.astro +33 -13
  182. package/src/components/layout/VersionBanner.astro +2 -2
  183. package/src/components/layout/analytics-client.ts +12 -0
  184. package/src/components/layout/toc-active.ts +41 -0
  185. package/src/components/layout/toc-element.ts +8 -14
  186. package/src/components/openapi/ApiTagOperations.astro +2 -2
  187. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  188. package/src/components/openapi/GraphqlChip.astro +2 -2
  189. package/src/components/openapi/Operation.astro +2 -0
  190. package/src/components/openapi/Playground.astro +4 -4
  191. package/src/components/openapi/RequestPanel.astro +5 -2
  192. package/src/components/openapi/SchemaTable.astro +7 -0
  193. package/src/components/openapi/async.ts +38 -6
  194. package/src/components/openapi/helpers.ts +19 -9
  195. package/src/components/openapi/message-composer.ts +6 -1
  196. package/src/components/openapi/message.ts +17 -2
  197. package/src/components/openapi/operation-model.ts +81 -11
  198. package/src/components/openapi/panel.ts +4 -2
  199. package/src/components/openapi/param-style.ts +181 -0
  200. package/src/components/openapi/playground-client.ts +73 -16
  201. package/src/components/openapi/request.ts +190 -26
  202. package/src/components/openapi/schema-tree.ts +30 -24
  203. package/src/components/openapi/snippets.ts +85 -6
  204. package/src/components/openapi/ws-client.ts +18 -2
  205. package/src/core/base-path.ts +62 -16
  206. package/src/core/config-input.ts +9 -7
  207. package/src/core/diagnostics.ts +2 -0
  208. package/src/core/directive-diagnostics.ts +99 -0
  209. package/src/core/frontmatter.ts +21 -18
  210. package/src/core/heading-markers.ts +5 -18
  211. package/src/core/i18n.ts +31 -3
  212. package/src/core/last-modified.ts +25 -3
  213. package/src/core/locale-links.ts +5 -1
  214. package/src/core/meta.ts +33 -19
  215. package/src/core/navigation.ts +28 -8
  216. package/src/core/project-graph.ts +27 -5
  217. package/src/core/schema.ts +24 -4
  218. package/src/core/sources/contentful-rich-text.ts +27 -20
  219. package/src/core/sources/filesystem.ts +20 -2
  220. package/src/core/sources/github-releases.ts +25 -6
  221. package/src/core/sources/lexical.ts +23 -18
  222. package/src/core/sources/lower.ts +201 -34
  223. package/src/core/sources/mdx-remote.ts +51 -16
  224. package/src/core/sources/normalize.ts +325 -90
  225. package/src/core/sources/notion.ts +52 -25
  226. package/src/core/sources/obsidian.ts +23 -6
  227. package/src/core/sources/portable-text.ts +38 -21
  228. package/src/core/sources/strapi-blocks.ts +20 -16
  229. package/src/core/sources/watch.ts +20 -7
  230. package/src/core/standard-schema.ts +10 -6
  231. package/src/core/version-cut.ts +52 -15
  232. package/src/deploy/artifacts.ts +27 -6
  233. package/src/deploy/cloudflare-negotiation.ts +4 -12
  234. package/src/deploy/headers.ts +8 -4
  235. package/src/deploy/node-headers.ts +1 -1
  236. package/src/deploy/platforms/cloudflare.ts +12 -3
  237. package/src/deploy/platforms/netlify.ts +1 -0
  238. package/src/deploy/platforms/node.ts +1 -0
  239. package/src/deploy/platforms/static.ts +1 -0
  240. package/src/deploy/platforms/types.ts +8 -0
  241. package/src/deploy/platforms/vercel.ts +1 -0
  242. package/src/deploy/redirects.ts +54 -18
  243. package/src/deploy/robots.ts +2 -2
  244. package/src/deploy/rss.ts +4 -3
  245. package/src/deploy/sitemap.ts +7 -5
  246. package/src/markdown/base-links.ts +34 -36
  247. package/src/markdown/directives.ts +242 -36
  248. package/src/markdown/features.ts +17 -0
  249. package/src/markdown/index.ts +7 -6
  250. package/src/markdown/mdast.ts +5 -2
  251. package/src/markdown/relative-links.ts +12 -4
  252. package/src/og/card.ts +129 -5
  253. package/src/og/derive.ts +41 -31
  254. package/src/og/index.ts +1 -0
  255. package/src/openapi/asyncapi.ts +4 -2
  256. package/src/openapi/model.ts +22 -14
  257. package/src/openapi/proxy.ts +63 -10
  258. package/src/openapi/render-mdx.ts +41 -2
  259. package/src/registry/eject.ts +181 -24
  260. package/src/search/adapters/version-scope.ts +30 -0
  261. package/src/search/documents.ts +67 -26
  262. package/src/search/popular.ts +2 -1
  263. package/src/seo/jsonld.ts +7 -3
  264. package/src/translate/meta.ts +68 -24
  265. package/src/translate/run.ts +3 -3
  266. package/src/translate/validate.ts +4 -1
  267. package/src/translate/work-list.ts +56 -17
  268. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  269. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  270. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  271. package/dist/cli/chunk-esh98wmb.js +0 -23
  272. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  273. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  274. package/dist/cli/chunk-j85scx15.js.map +0 -10
  275. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  276. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  277. /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
  278. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
  279. /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
  280. /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
  281. /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
  282. /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
  283. /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
  284. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
  285. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
  286. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
  287. /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
  288. /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
  289. /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
  290. /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
  291. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
  292. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
  293. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
@@ -200,6 +200,13 @@ export interface VercelHeader {
200
200
  /** The characters `path-to-regexp` would read as syntax in a literal path. */
201
201
  const SOURCE_SYNTAX = /[\\(){}?+:]/gu;
202
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
+
203
210
  /**
204
211
  * A `_headers` path as a `vercel.json` `source`, which `path-to-regexp` reads:
205
212
  * the literal parts are escaped, and a `*` glob becomes the `(.*)` group that
@@ -209,10 +216,7 @@ const SOURCE_SYNTAX = /[\\(){}?+:]/gu;
209
216
  const vercelSource = (path: string): string => {
210
217
  const trimmed =
211
218
  path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
212
- return trimmed
213
- .split("*")
214
- .map((part) => part.replaceAll(SOURCE_SYNTAX, String.raw`\$&`))
215
- .join("(.*)");
219
+ return trimmed.split("*").map(escapeVercelSource).join("(.*)");
216
220
  };
217
221
 
218
222
  /**
@@ -124,7 +124,7 @@ export const nodeRedirects = (project: BlumeProject): NodeRedirects => {
124
124
  )
125
125
  );
126
126
  return Object.fromEntries(
127
- platformRedirects(config)
127
+ platformRedirects(project)
128
128
  .filter((redirect) => !pages.has(normalizePath(redirect.from)))
129
129
  .map((redirect) => [
130
130
  normalizePath(redirect.from),
@@ -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
  );
@@ -308,6 +314,9 @@ export const cloudflarePlatform: DeployPlatform = {
308
314
  previewDeploy: null,
309
315
  readsHeaderFiles: { server: true, static: true },
310
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,
311
320
  serverOutputDir: distDir,
312
321
  serverStaticDir: clientDir,
313
322
  };
@@ -124,6 +124,7 @@ export const netlifyPlatform: DeployPlatform = {
124
124
  // same rules into that tree's config.
125
125
  readsHeaderFiles: { server: false, static: true },
126
126
  redirectFiles: [NETLIFY_REDIRECTS_FILE],
127
+ serverClientUnderBase: false,
127
128
  serverOutputDir: distDir,
128
129
  serverStaticDir: distDir,
129
130
  };
@@ -37,6 +37,7 @@ export const nodePlatform: DeployPlatform = {
37
37
  previewDeploy: null,
38
38
  readsHeaderFiles: { server: false, static: false },
39
39
  redirectFiles: [],
40
+ serverClientUnderBase: false,
40
41
  serverOutputDir: distDir,
41
42
  serverStaticDir: clientDir,
42
43
  };
@@ -25,6 +25,7 @@ export const staticPlatform: DeployPlatform = {
25
25
  previewDeploy: null,
26
26
  readsHeaderFiles: { server: false, static: true },
27
27
  redirectFiles: [REDIRECTS_FILE, VERCEL_JSON_FILE],
28
+ serverClientUnderBase: false,
28
29
  serverOutputDir: distDir,
29
30
  serverStaticDir: distDir,
30
31
  };
@@ -112,6 +112,14 @@ export interface DeployPlatform {
112
112
  readsHeaderFiles: Record<DeployOutput, boolean>;
113
113
  /** The redirect files a static build writes, beside the manifest. */
114
114
  redirectFiles: RedirectFile[];
115
+ /**
116
+ * Whether the adapter moves a server build's client output under
117
+ * `deployment.base` (`dist/client/<base>/`, the directory Astro then hands
118
+ * `astro:build:done`) while the platform keeps serving `serverStaticDir`
119
+ * as the assets root. The files the platform reads there, like `_headers`,
120
+ * belong above the directory Astro reports.
121
+ */
122
+ serverClientUnderBase: boolean;
115
123
  /** Where a server build's deploy bundle lands. */
116
124
  serverOutputDir: (context: ProjectContext) => string;
117
125
  /** The directory a server build serves as static files. */
@@ -194,6 +194,7 @@ export const vercelPlatform: DeployPlatform = {
194
194
  previewDeploy: "vercel deploy",
195
195
  readsHeaderFiles: { server: false, static: false },
196
196
  redirectFiles: [VERCEL_JSON_FILE],
197
+ serverClientUnderBase: false,
197
198
  serverOutputDir: buildOutputDir,
198
199
  serverStaticDir: (context) => join(buildOutputDir(context), "static"),
199
200
  };
@@ -1,9 +1,14 @@
1
1
  import {
2
2
  normalizeBasePath,
3
+ withAuthoredBasePath,
3
4
  withBasePath,
4
5
  withComposedBasePath,
5
6
  } from "../core/base-path.ts";
7
+ import { routeSetFor, servesRoute } from "../core/locale-links.ts";
8
+ import type { RouteSet } from "../core/locale-links.ts";
9
+ import type { BlumeProject } from "../core/project-graph.ts";
6
10
  import type { ResolvedConfig } from "../core/schema.ts";
11
+ import { escapeVercelSource } from "./headers.ts";
7
12
  import type { VercelHeader } from "./headers.ts";
8
13
 
9
14
  /**
@@ -16,6 +21,23 @@ import type { VercelHeader } from "./headers.ts";
16
21
 
17
22
  type Redirect = ResolvedConfig["redirects"][number];
18
23
 
24
+ /**
25
+ * Base a redirect destination authored as if mounted at root by what it
26
+ * names (see `withAuthoredBasePath`): a page gains the full
27
+ * `{deployment.base}{basePath}` stack, a public file (`/files/spec.pdf`, with
28
+ * no page in `routes`) only the deployment base — `public/` is served there
29
+ * whatever `basePath` is. External URLs pass through.
30
+ */
31
+ const baseRedirectTarget = (
32
+ to: string,
33
+ basePath: string,
34
+ base: string,
35
+ routes: RouteSet
36
+ ): string =>
37
+ withAuthoredBasePath(base, basePath, to, (route) =>
38
+ servesRoute(routes, route)
39
+ );
40
+
19
41
  /**
20
42
  * Base a redirect for Astro's `redirects` config, where the two sides are not
21
43
  * symmetric:
@@ -23,19 +45,23 @@ type Redirect = ResolvedConfig["redirects"][number];
23
45
  * - `from` gains only `basePath`. Astro builds the match pattern with
24
46
  * `deployment.base` already applied (`getPattern(segments, config.base)`), so
25
47
  * adding it here would serve the redirect at `{base}{base}/from`.
26
- * - `to` gains the full `{deployment.base}{basePath}` stack. Astro resolves a
27
- * destination that matches a known route by regenerating it from that route's
28
- * segments, which carry no base — and passes an unmatched destination through
29
- * verbatim. Neither path prepends `base`, so a root-relative `to` escapes the
30
- * base entirely (withastro/astro#7774, still the behavior in Astro 7).
48
+ * - `to` gains the full `{deployment.base}{basePath}` stack, or the deployment
49
+ * base alone for a public file (see {@link baseRedirectTarget}). Astro
50
+ * resolves a destination that matches a known route by regenerating it from
51
+ * that route's segments, which carry no base — and passes an unmatched
52
+ * destination through verbatim. Neither path prepends `base`, so a
53
+ * root-relative `to` escapes the base entirely (withastro/astro#7774, still
54
+ * the behavior in Astro 7).
31
55
  *
32
- * Both sides are authored as if mounted at root; external `to` URLs pass
33
- * through. Idempotent, so a hand-written base isn't doubled.
56
+ * Both sides are authored as if mounted at root; `routes` (the served page
57
+ * routes, carrying `basePath`) tell a dotted page route like `/releases/v1.2`
58
+ * from a file. Idempotent, so a hand-written base isn't doubled.
34
59
  */
35
60
  export const applyBaseToAstroRedirects = (
36
61
  redirects: Redirect[],
37
62
  basePath: string,
38
- deployBase: string
63
+ deployBase: string,
64
+ routes: RouteSet
39
65
  ): Redirect[] => {
40
66
  // `deployment.base` arrives as the user wrote it (Astro accepts `/base/`,
41
67
  // even `base`); normalizing here keeps the composed paths well-formed.
@@ -44,27 +70,29 @@ export const applyBaseToAstroRedirects = (
44
70
  ? redirects.map((redirect) => ({
45
71
  ...redirect,
46
72
  from: withBasePath(basePath, redirect.from),
47
- to: withComposedBasePath(base, basePath, redirect.to),
73
+ to: baseRedirectTarget(redirect.to, basePath, base, routes),
48
74
  }))
49
75
  : redirects;
50
76
  };
51
77
 
52
78
  /**
53
79
  * Base a redirect for the host platform files below. Unlike Astro's config,
54
- * these are matched against the real served URL, so both sides carry the full
55
- * `{deployment.base}{basePath}` stack.
80
+ * these are matched against the real served URL, so `from` carries the full
81
+ * `{deployment.base}{basePath}` stack, and so does `to` unless it names a
82
+ * public file (see {@link baseRedirectTarget}).
56
83
  */
57
84
  export const applyBaseToPlatformRedirects = (
58
85
  redirects: Redirect[],
59
86
  basePath: string,
60
- deployBase: string
87
+ deployBase: string,
88
+ routes: RouteSet
61
89
  ): Redirect[] => {
62
90
  const base = normalizeBasePath(deployBase);
63
91
  return basePath || base
64
92
  ? redirects.map((redirect) => ({
65
93
  ...redirect,
66
94
  from: withComposedBasePath(base, basePath, redirect.from),
67
- to: withComposedBasePath(base, basePath, redirect.to),
95
+ to: baseRedirectTarget(redirect.to, basePath, base, routes),
68
96
  }))
69
97
  : redirects;
70
98
  };
@@ -75,12 +103,18 @@ export const applyBaseToPlatformRedirects = (
75
103
  * share: the emitted redirect files and the Cloudflare worker-first redirect
76
104
  * exemptions both compare these paths against real served URLs.
77
105
  */
78
- export const platformRedirects = (config: ResolvedConfig): Redirect[] =>
79
- applyBaseToPlatformRedirects(
106
+ export const platformRedirects = (project: {
107
+ config: ResolvedConfig;
108
+ manifest: Pick<BlumeProject["manifest"], "routes">;
109
+ }): Redirect[] => {
110
+ const { config } = project;
111
+ return applyBaseToPlatformRedirects(
80
112
  config.redirects,
81
113
  config.basePath,
82
- config.deployment.options.base ?? ""
114
+ config.deployment.options.base ?? "",
115
+ routeSetFor(project.manifest.routes)
83
116
  );
117
+ };
84
118
 
85
119
  /**
86
120
  * `_redirects` text (Netlify + Cloudflare Pages): `from to status` per line.
@@ -110,7 +144,9 @@ interface VercelConfig {
110
144
  * header rules are given (see `buildVercelHeaders`). Uses `statusCode` (Vercel's
111
145
  * alternative to the boolean `permanent`) so the configured code ships exactly:
112
146
  * `permanent` would silently coerce a 301 to 308 and a 302 to 307, diverging
113
- * from the `_redirects` file, which preserves exact codes.
147
+ * from the `_redirects` file, which preserves exact codes. A `source` is a
148
+ * `path-to-regexp` pattern, so each exact `from` path is escaped: unescaped,
149
+ * `/c++-guide` fails the whole config and `/faq(old)` never matches.
114
150
  */
115
151
  export const buildVercelConfig = (
116
152
  redirects: Redirect[],
@@ -119,7 +155,7 @@ export const buildVercelConfig = (
119
155
  const config: VercelConfig = {
120
156
  redirects: redirects.map((redirect) => ({
121
157
  destination: redirect.to,
122
- source: redirect.from,
158
+ source: escapeVercelSource(redirect.from),
123
159
  statusCode: redirect.status,
124
160
  })),
125
161
  };
@@ -1,4 +1,4 @@
1
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
1
+ import { mountBasePath, normalizeBasePath } from "../core/base-path.ts";
2
2
  import type { BlumeProject } from "../core/project-graph.ts";
3
3
  import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
4
4
  import { absoluteUrl } from "../core/site-url.ts";
@@ -49,7 +49,7 @@ export const buildRobots = (project: BlumeProject): string | null => {
49
49
 
50
50
  const { site } = config.deployment.options;
51
51
  if (site && config.seo.sitemap) {
52
- const sitemapPath = withBasePath(
52
+ const sitemapPath = mountBasePath(
53
53
  normalizeBasePath(config.deployment.options.base),
54
54
  "/sitemap.xml"
55
55
  );
package/src/deploy/rss.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // `&#39;` reference rather than `&apos;`, which XML accepts equally.
3
3
  import { escape as escapeXml } from "html-escaper";
4
4
 
5
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
5
+ import { mountBasePath, normalizeBasePath } from "../core/base-path.ts";
6
6
  import type { BlumeProject } from "../core/project-graph.ts";
7
7
  import { siteRoot } from "../core/site-url.ts";
8
8
  import type { PageRecord } from "../core/types.ts";
@@ -57,7 +57,8 @@ export const buildRssFeeds = (project: BlumeProject): RssFeed[] => {
57
57
  return [];
58
58
  }
59
59
  const base = siteRoot(site);
60
- // Routes carry `basePath`; a `deployment.base` subdirectory is layered on top.
60
+ // Routes carry `basePath`; a `deployment.base` subdirectory is mounted on top
61
+ // of every one, even a route whose first segment matches it (see sitemap.ts).
61
62
  // The feed's own `link`/self URL points at the docs root under that base, while
62
63
  // `path` stays base-less (it's also the on-disk output location).
63
64
  const deployBase = normalizeBasePath(config.deployment.options.base);
@@ -80,7 +81,7 @@ export const buildRssFeeds = (project: BlumeProject): RssFeed[] => {
80
81
  description: page.description,
81
82
  // Encode like the sitemap does: a route with spaces or non-ASCII
82
83
  // must still yield a valid <link>/<guid> URL after XML decoding.
83
- link: encodeURI(`${base}${withBasePath(deployBase, page.route)}`),
84
+ link: encodeURI(`${base}${mountBasePath(deployBase, page.route)}`),
84
85
  title: page.title,
85
86
  }))
86
87
  .toSorted((a, b) => (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0))
@@ -6,7 +6,7 @@ import {
6
6
  discoverPagesSync,
7
7
  hasGeneratedChangelog,
8
8
  } from "../astro/pages.ts";
9
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
9
+ import { mountBasePath, normalizeBasePath } from "../core/base-path.ts";
10
10
  import type { BlumeProject } from "../core/project-graph.ts";
11
11
  import { siteRoot } from "../core/site-url.ts";
12
12
 
@@ -78,7 +78,9 @@ export const buildSitemapFiles = (
78
78
  }
79
79
 
80
80
  const base = siteRoot(site);
81
- // Routes carry `basePath`; a `deployment.base` subdirectory is layered on top.
81
+ // Routes carry `basePath`; a `deployment.base` subdirectory is mounted on
82
+ // top of every one, even a route whose first segment matches it: under base
83
+ // `/guides`, `guides/setup.md` is served at `/guides/guides/setup`.
82
84
  const deployBase = normalizeBasePath(project.config.deployment.options.base);
83
85
 
84
86
  // Archived-version pages leave the sitemap when the version is noindexed,
@@ -135,7 +137,7 @@ export const buildSitemapFiles = (
135
137
  ) {
136
138
  continue;
137
139
  }
138
- pushUrl(withBasePath(deployBase, page.route), page.lastModified);
140
+ pushUrl(mountBasePath(deployBase, page.route), page.lastModified);
139
141
  }
140
142
  // Custom `.astro` pages and the generated changelog index mount outside
141
143
  // `basePath` (they're injected at their pattern — see `blumeIntegration`), so
@@ -150,7 +152,7 @@ export const buildSitemapFiles = (
150
152
  extraRoutes.push("/changelog");
151
153
  }
152
154
  for (const route of extraRoutes) {
153
- pushUrl(withBasePath(deployBase, route));
155
+ pushUrl(mountBasePath(deployBase, route));
154
156
  }
155
157
  urls.sort();
156
158
 
@@ -169,7 +171,7 @@ export const buildSitemapFiles = (
169
171
  // Chunks sit next to sitemap.xml, so their URLs layer the same deployment
170
172
  // base robots.txt uses for the index.
171
173
  const loc = escapeXml(
172
- encodeURI(`${base}${withBasePath(deployBase, `/${name}`)}`)
174
+ encodeURI(`${base}${mountBasePath(deployBase, `/${name}`)}`)
173
175
  );
174
176
  references.push(` <sitemap><loc>${loc}</loc></sitemap>`);
175
177
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  isInternalPath,
3
+ withAuthoredBasePath,
3
4
  withBasePath,
4
- withComposedBasePath,
5
5
  } from "../core/base-path.ts";
6
6
  import { servesRoute } from "../core/locale-links.ts";
7
7
  import type { MdastNode } from "./mdast.ts";
@@ -20,18 +20,6 @@ interface MdastUrlContext {
20
20
  setProperty: (node: MdastNode, key: "url", value: string) => void;
21
21
  }
22
22
 
23
- /**
24
- * A path whose final segment carries a file extension (`/spec.pdf`, `/logo.svg`)
25
- * — a public asset, which Blume serves from `public/` at the site root and
26
- * does *not* move under `basePath`, unless a page is served there: a dotted
27
- * route (`/releases/v1.2`) is based like any other page link, the same
28
- * served-route test the link checker and the locale rewrite apply.
29
- */
30
- const DOTTED_PATH = /\.[a-z0-9]+$/iu;
31
-
32
- /** Strip any `#fragment`/`?query` so only the path is extension-tested. */
33
- const pathOf = (url: string): string => url.replace(/[#?].*$/u, "");
34
-
35
23
  /** Only a string URL can be rebased; MDAST allows null or absent urls. */
36
24
  const isUrl = (url: string | null | undefined): url is string =>
37
25
  typeof url === "string";
@@ -45,13 +33,15 @@ export interface BaseLinksPluginOptions {
45
33
  }
46
34
 
47
35
  /**
48
- * Satteri MDAST plugin that prepends the served-URL base — `deployment.base`
49
- * layered over the site-wide `basePath` — to root-relative internal page links
50
- * (`[x](/guide)` -> `/base/docs/guide`), so authors write links as if mounted
51
- * at root. Idempotent per layer (via `withComposedBasePath`, so a hand-written
52
- * `/docs/x` isn't double-prefixed) and inert for external URLs, fragments,
53
- * relative paths, images, and asset links. Only constructed when a base is set
54
- * (see `markdown/index.ts`).
36
+ * Satteri MDAST plugin that prepends the served-URL base to root-relative
37
+ * internal links, so authors write links as if mounted at root. A page link
38
+ * gains `deployment.base` layered over the site-wide `basePath` (`[x](/guide)`
39
+ * -> `/base/docs/guide`); a public asset — an image, or a link to a file like
40
+ * `/spec.pdf` (see `withAuthoredBasePath`) — gains `deployment.base` alone,
41
+ * since Astro serves `public/` under it but never under `basePath`.
42
+ * Idempotent per layer (a hand-written `/docs/x` isn't double-prefixed) and
43
+ * inert for external URLs, fragments, and relative paths. Only constructed
44
+ * when a base is set (see `markdown/index.ts`).
55
45
  */
56
46
  export const baseLinksPlugin = (
57
47
  deployBase: string,
@@ -78,30 +68,38 @@ export const baseLinksPlugin = (
78
68
  return cached.routes;
79
69
  };
80
70
 
81
- /** Whether an internal `url` links a page rather than a public asset. */
82
- const isPageLink = (url: string): boolean => {
83
- const path = pathOf(url);
84
- return (
85
- !DOTTED_PATH.test(path) ||
86
- servesRoute(servedRoutes(), withBasePath(basePath, path))
87
- );
88
- };
71
+ /** Whether a page is served at a based, fragment-less path. */
72
+ const servesPage = (route: string): boolean =>
73
+ servesRoute(servedRoutes(), route);
89
74
 
90
- const rebase = (node: UrlNode, ctx: MdastUrlContext): void => {
75
+ /** Record `based(url)` on the node when its root-relative URL changes. */
76
+ const rebase = (
77
+ node: UrlNode,
78
+ ctx: MdastUrlContext,
79
+ based: (url: string) => string
80
+ ): void => {
91
81
  const { url } = node;
92
- if (isUrl(url) && isInternalPath(url) && isPageLink(url)) {
93
- const next = withComposedBasePath(deployBase, basePath, url);
82
+ if (isUrl(url) && isInternalPath(url)) {
83
+ const next = based(url);
94
84
  if (next !== url) {
95
85
  ctx.setProperty(node, "url", next);
96
86
  }
97
87
  }
98
88
  };
99
- // `link` covers inline links; `definition` covers reference-style link
100
- // definitions (`[x]: /guide`). `image` is intentionally excluded — images are
101
- // public assets served at the site root, unaffected by `basePath`.
89
+ const rebaseLink = (node: UrlNode, ctx: MdastUrlContext): void =>
90
+ rebase(node, ctx, (url) =>
91
+ withAuthoredBasePath(deployBase, basePath, url, servesPage)
92
+ );
93
+ // An image is always a file, never a page: `public/` (or a generated asset
94
+ // endpoint), served under the deployment base but not `basePath`.
95
+ const rebaseImage = (node: UrlNode, ctx: MdastUrlContext): void =>
96
+ rebase(node, ctx, (url) => withBasePath(deployBase, url));
97
+ // `link` covers inline links; `definition` covers reference-style
98
+ // definitions (`[x]: /guide`), which a link or an image may cite.
102
99
  return {
103
- definition: rebase,
104
- link: rebase,
100
+ definition: rebaseLink,
101
+ image: rebaseImage,
102
+ link: rebaseLink,
105
103
  name: "blume-base-links",
106
104
  };
107
105
  };