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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "The open-source docs framework for humans and agents.",
5
5
  "keywords": [
6
6
  "agents",
@@ -92,7 +92,7 @@ Trailing heading markers — `[#custom-id]` (pinned anchor), `[!toc]` (hide from
92
92
 
93
93
  ## i18n
94
94
 
95
- A `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's `dir` parser as-is; locale **file suffixes** (`page.cn.mdx`) need restructuring into locale folders. Report whichever transform you apply.
95
+ A `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's default `dir` parser as-is; locale **file suffixes** (`page.cn.mdx` beside an unsuffixed default-locale `page.mdx`) match the `dot` parser as-is — add `parser: "dot"`, no file moves. Under `dot` a folder's `meta.ts` serves every locale, so build it from the default locale's `meta.json` and report any translated folder titles in `meta.<lang>.json`. Report whichever transform you apply.
96
96
 
97
97
  ## Package.json & teardown
98
98
 
@@ -64,7 +64,7 @@ Nextra enables math via `nextra({ latex: true })` (KaTeX or MathJax). In Blume,
64
64
 
65
65
  ## i18n
66
66
 
67
- - **v2/v3:** locale **file suffixes** (`index.en.mdx`, `index.zh.mdx`) + `i18n` in `next.config` → restructure into locale **folders** (default locale at the content root, others under `<code>/`), then `i18n: { defaultLocale, locales: [{ code, label }] }`.
67
+ - **v2/v3:** locale **file suffixes** (`index.en.mdx`, `index.zh.mdx`) + `i18n` in `next.config` → match Blume's `dot` parser as-is, default-locale suffix included: `i18n: { defaultLocale, locales: [{ code, label }], parser: "dot" }`, no file moves. Page titles land in each file's own frontmatter, so they stay per-locale, but under `dot` a folder's `meta.ts` serves every locale — build it from the default locale's `_meta` and report translated folder titles from the other locales' `_meta` files.
68
68
  - **v4:** `content/<lang>/` dirs already match Blume's `dir` parser — map the locale list, no file moves.
69
69
 
70
70
  ## Package.json & teardown
@@ -219,10 +219,15 @@ const topKey = (line) => {
219
219
  return { key: m.groups.key, value: (m.groups.rest ?? "").trim() };
220
220
  };
221
221
 
222
- // The line index range [start, endExclusive) of a top-level key's block.
222
+ // The line index range [start, endExclusive) of a top-level key's block: its
223
+ // indented lines plus any `- ` sequence items written flush at column 0,
224
+ // which YAML also reads as the key's value (`keywords:` then `- one`).
223
225
  const blockRange = (fm, start) => {
224
226
  let end = start + 1;
225
- while (end < fm.length && (fm[end] === "" || /^\s/u.test(fm[end]))) {
227
+ while (
228
+ end < fm.length &&
229
+ (fm[end] === "" || /^\s/u.test(fm[end]) || /^-(?:\s|$)/u.test(fm[end]))
230
+ ) {
226
231
  end += 1;
227
232
  }
228
233
  // Trim trailing blank lines back out so the gap before the next key survives.
@@ -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 { repoUrl } from "../core/github.ts";
3
3
  import type { BlumeProject } from "../core/project-graph.ts";
4
4
  import type { ContentSignalPolicy, ContentSignals } from "../core/schema.ts";
@@ -173,7 +173,7 @@ export const buildAgentReadability = (
173
173
  // `site`; concatenate rather than `new URL()` so the subpath is preserved.
174
174
  const deployBase = normalizeBasePath(config.deployment.options.base);
175
175
  const abs = (path: string): string => {
176
- const based = withBasePath(deployBase, path);
176
+ const based = mountBasePath(deployBase, path);
177
177
  return site ? absoluteUrl(site, based) : based;
178
178
  };
179
179
 
@@ -1,5 +1,9 @@
1
1
  import { apiNamePhrase } from "../core/api-name.ts";
2
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
2
+ import {
3
+ mountBasePath,
4
+ normalizeBasePath,
5
+ withBasePath,
6
+ } from "../core/base-path.ts";
3
7
  import type { ResolvedConfig } from "../core/schema.ts";
4
8
  import { absoluteUrl } from "../core/site-url.ts";
5
9
  import { resolveReferences } from "../openapi/references.ts";
@@ -227,7 +231,7 @@ export const buildAiCatalog = (
227
231
  }
228
232
  const deployBase = normalizeBasePath(config.deployment.options.base);
229
233
  const abs = (path: string): string =>
230
- absoluteUrl(site, withBasePath(deployBase, path));
234
+ absoluteUrl(site, mountBasePath(deployBase, path));
231
235
  const host = publisherHost(site);
232
236
  const entries = entrySeeds(config, skills, abs).map((seed): CatalogEntry => {
233
237
  const entry: CatalogEntry = {
@@ -1,4 +1,4 @@
1
- import { withBasePath } from "../../core/base-path.ts";
1
+ import { mountBasePath } from "../../core/base-path.ts";
2
2
  import { absoluteUrl } from "../../core/site-url.ts";
3
3
  import type { Navigation } from "../../core/types.ts";
4
4
  import type { McpData, McpRoute } from "../mcp/data.ts";
@@ -88,7 +88,7 @@ export const jsonResponse = (payload: ApiPayload, status = 200): Response =>
88
88
 
89
89
  /** The absolute (or root-relative) URL for a base-less path. */
90
90
  const siteUrl = (path: string, context: ApiSiteContext): string => {
91
- const based = withBasePath(context.base, path);
91
+ const based = mountBasePath(context.base, path);
92
92
  return context.site ? absoluteUrl(context.site, based) : based;
93
93
  };
94
94
 
@@ -1,5 +1,5 @@
1
1
  import { apiNamePhrase } from "../../core/api-name.ts";
2
- import { withBasePath } from "../../core/base-path.ts";
2
+ import { mountBasePath } from "../../core/base-path.ts";
3
3
  import { absoluteUrl, siteRoot } from "../../core/site-url.ts";
4
4
  import {
5
5
  API_NAVIGATION_PATH,
@@ -688,7 +688,7 @@ export const buildApiSpec = (input: ApiSpecInput): ApiSpecDocument => {
688
688
  if (input.site) {
689
689
  document.externalDocs = {
690
690
  description: `${input.name} documentation`,
691
- url: absoluteUrl(input.site, withBasePath(input.base, "/")),
691
+ url: absoluteUrl(input.site, mountBasePath(input.base, "/")),
692
692
  };
693
693
  }
694
694
  return document;
@@ -1,4 +1,8 @@
1
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
1
+ import {
2
+ mountBasePath,
3
+ normalizeBasePath,
4
+ withBasePath,
5
+ } from "../core/base-path.ts";
2
6
  import type { ResolvedConfig } from "../core/schema.ts";
3
7
  import { absoluteUrl } from "../core/site-url.ts";
4
8
  import { resolveReferences } from "../openapi/references.ts";
@@ -39,7 +43,7 @@ const linksetEntries = (config: ResolvedConfig): LinksetEntry[] => {
39
43
  const site = config.deployment.options.site ?? null;
40
44
  const deployBase = normalizeBasePath(config.deployment.options.base);
41
45
  const abs = (path: string): string => {
42
- const based = withBasePath(deployBase, path);
46
+ const based = mountBasePath(deployBase, path);
43
47
  return site ? absoluteUrl(site, based) : based;
44
48
  };
45
49
 
@@ -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 { EN_UI, resolveUIStrings } from "../core/i18n-ui.ts";
3
3
  import type { BlumeProject } from "../core/project-graph.ts";
4
4
  import { absoluteUrl } from "../core/site-url.ts";
@@ -29,7 +29,7 @@ const parseDate = (value: string | undefined): Date | null => {
29
29
  /** A row's link: absolute under a configured site, like llms.txt. */
30
30
  const entryUrl = (project: BlumeProject, route: string): string => {
31
31
  const { base, site } = project.config.deployment.options;
32
- const path = withBasePath(normalizeBasePath(base), route);
32
+ const path = mountBasePath(normalizeBasePath(base), route);
33
33
  return encodeURI(site ? absoluteUrl(site, path) : path);
34
34
  };
35
35
 
@@ -33,24 +33,33 @@ import { OPENAPI_PATH } from "./api/paths.ts";
33
33
  * headers. Targets are
34
34
  * root-relative under `deployment.base` — RFC 8288 resolves them against the
35
35
  * request URL. Returns null when nothing is advertisable.
36
+ *
37
+ * `surface` is where the header is sent. A deployed site (`"build"`) serves
38
+ * every target. The dev server (`"dev"`) serves the routes — the JSON API's
39
+ * OpenAPI document and the Markdown mirror — but not the files `blume build`
40
+ * writes after the build (the API and AI catalogs, `agent-readability.json`,
41
+ * `llms.txt`), so its header leaves those out rather than point agents at
42
+ * 404s.
36
43
  */
37
44
  export const buildHomeLinkHeader = (
38
45
  config: ResolvedConfig,
39
- routePaths: readonly string[]
46
+ routePaths: readonly string[],
47
+ surface: "build" | "dev" = "build"
40
48
  ): string | null => {
41
49
  const deployBase = normalizeBasePath(config.deployment.options.base);
50
+ const artifacts = surface === "build";
42
51
  const links: string[] = [];
43
52
  // RFC 9727 §3: the api-catalog relation is how a homepage advertises the
44
53
  // well-known catalog. Its type carries the RFC 9727 profile, whose quotes
45
54
  // are escaped inside the quoted `type` value (RFC 8288 quoted-string).
46
- if (hasApiCatalog(config)) {
55
+ if (artifacts && hasApiCatalog(config)) {
47
56
  links.push(
48
57
  `<${deployBase}${API_CATALOG_PATH}>; rel="api-catalog"; type="${API_CATALOG_TYPE.replaceAll('"', String.raw`\"`)}"`
49
58
  );
50
59
  }
51
60
  // The ai-catalog spec's own relation for its well-known document, the
52
61
  // header form of the `<link rel="ai-catalog">` every page carries.
53
- if (hasAiCatalog(config)) {
62
+ if (artifacts && hasAiCatalog(config)) {
54
63
  links.push(
55
64
  `<${deployBase}${AI_CATALOG_PATH}>; rel="ai-catalog"; type="${AI_CATALOG_TYPE}"`
56
65
  );
@@ -62,12 +71,12 @@ export const buildHomeLinkHeader = (
62
71
  `<${deployBase}${OPENAPI_PATH}>; rel="service-desc"; type="application/json"`
63
72
  );
64
73
  }
65
- if (config.agents.agentReadability) {
74
+ if (artifacts && config.agents.agentReadability) {
66
75
  links.push(
67
76
  `<${deployBase}/agent-readability.json>; rel="describedby"; type="application/json"`
68
77
  );
69
78
  }
70
- if (config.agents.llmsTxt.enabled) {
79
+ if (artifacts && config.agents.llmsTxt.enabled) {
71
80
  links.push(
72
81
  `<${deployBase}/llms.txt>; rel="describedby"; type="text/plain"`
73
82
  );
package/src/ai/llms.ts CHANGED
@@ -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 { rewriteRelativeImages } from "../core/content-assets.ts";
3
3
  import matter from "../core/frontmatter.ts";
4
4
  import type { BlumeProject } from "../core/project-graph.ts";
@@ -17,14 +17,22 @@ import type { SkillArtifact } from "./skills.ts";
17
17
  import { applyAgentVisibility } from "./visibility.ts";
18
18
 
19
19
  // Routes carry `basePath`; a `deployment.base` subdirectory is layered on top —
20
- // with or without a `site` (the mcp.json convention) — so the emitted URL
21
- // matches where the page is served. Encoded like the sitemap: a route with
22
- // spaces or non-ASCII must still yield a valid Markdown link.
20
+ // with or without a `site` (the mcp.json convention), and even over a route
21
+ // that starts with the base's own name — so the emitted URL matches where the
22
+ // page is served. Encoded like the sitemap: a route with spaces or non-ASCII
23
+ // must still yield a valid Markdown link.
23
24
  const pageUrl = (route: string, site?: string, base = ""): string => {
24
- const path = withBasePath(base, route);
25
+ const path = mountBasePath(base, route);
25
26
  return encodeURI(site ? absoluteUrl(site, path) : path);
26
27
  };
27
28
 
29
+ /**
30
+ * A title as Markdown link text. An unescaped bracket in it — `[Beta] Webhooks`
31
+ * — closes or nests the text early, and the line stops being a link.
32
+ */
33
+ const linkText = (title: string): string =>
34
+ title.replaceAll(/[\\[\]]/gu, String.raw`\$&`);
35
+
28
36
  /** Inputs only the build has: the published skills, collected once per build. */
29
37
  export interface LlmsIndexOptions {
30
38
  /**
@@ -176,7 +184,7 @@ export const buildLlmsIndex = (
176
184
  const line = (page: PageRecord): string => {
177
185
  seen.add(page.route);
178
186
  const summary = page.description ? `: ${page.description}` : "";
179
- return `- [${page.title}](${pageUrl(page.route, site, base)})${summary}`;
187
+ return `- [${linkText(page.title)}](${pageUrl(page.route, site, base)})${summary}`;
180
188
  };
181
189
 
182
190
  // One nav level -> Markdown blocks: the level's loose pages as a link list,
@@ -259,7 +267,10 @@ export const buildLlmsIndex = (
259
267
  blocks.push(
260
268
  "## RSS Feeds",
261
269
  feeds
262
- .map((feed) => `- [${feed.title}](${pageUrl(feed.path, site, base)})`)
270
+ .map(
271
+ (feed) =>
272
+ `- [${linkText(feed.title)}](${pageUrl(feed.path, site, base)})`
273
+ )
263
274
  .join("\n")
264
275
  );
265
276
  }
@@ -1,4 +1,4 @@
1
- import { withBasePath } from "../../core/base-path.ts";
1
+ import { mountBasePath } from "../../core/base-path.ts";
2
2
  import { absoluteUrl, siteRoot } from "../../core/site-url.ts";
3
3
  import { trimChar } from "../../core/trim.ts";
4
4
  import { MCP_TOOLS } from "./tools.ts";
@@ -18,7 +18,7 @@ export interface McpDiscoveryInput {
18
18
  const serverUrl = (input: McpDiscoveryInput): string => {
19
19
  // The endpoint is a generated Astro page, so it's served under
20
20
  // `deployment.base` like every other route (the sitemap/llms.txt convention).
21
- const path = withBasePath(input.base, input.route);
21
+ const path = mountBasePath(input.base, input.route);
22
22
  // Concatenate rather than `new URL(path, site)` — a root-absolute path
23
23
  // would drop the base path of a subpath deployment (`acme.com/docs`).
24
24
  return input.site ? absoluteUrl(input.site, path) : path;
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import {
4
+ mountBasePath,
4
5
  normalizeRoute as normalizePageRoute,
5
6
  stripBasePath,
6
- withBasePath,
7
7
  } from "../../core/base-path.ts";
8
8
  import { absoluteUrl } from "../../core/site-url.ts";
9
9
  import { trimEnd } from "../../core/trim.ts";
@@ -244,7 +244,7 @@ export const normalizeRoute = (input: string, data: McpData): string => {
244
244
  export const urlFor = (route: string, data: McpData): string => {
245
245
  // Routes are base-less manifest paths; layer `deployment.base` on top so the
246
246
  // URL matches where the page is served (the sitemap/llms.txt convention).
247
- const path = withBasePath(data.base, route);
247
+ const path = mountBasePath(data.base, route);
248
248
  // Concatenate rather than `new URL(path, site)` — a root-absolute path
249
249
  // would drop the base path of a subpath deployment (`acme.com/docs`).
250
250
  return data.site ? absoluteUrl(data.site, path) : path;
@@ -10,7 +10,7 @@ import {
10
10
  } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { z } from "zod";
12
12
 
13
- import { withBasePath } from "../../core/base-path.ts";
13
+ import { mountBasePath } from "../../core/base-path.ts";
14
14
  import { readCappedText } from "../../core/request-body.ts";
15
15
  import type { McpData } from "./data.ts";
16
16
  import {
@@ -104,7 +104,7 @@ interface PageResource {
104
104
  const resourceUri = (route: string, data: McpData): string =>
105
105
  data.site
106
106
  ? urlFor(route, data)
107
- : `${LOCAL_RESOURCE_SCHEME}${withBasePath(data.base, route)}`;
107
+ : `${LOCAL_RESOURCE_SCHEME}${mountBasePath(data.base, route)}`;
108
108
 
109
109
  /** The `pages` key a resource URI (either form, or a bare route) names. */
110
110
  const resourceRoute = (uri: string, data: McpData): string =>
@@ -1,3 +1,4 @@
1
+ import { mountBasePath, normalizeBasePath } from "../core/base-path.ts";
1
2
  import type { ApiOperationRef, OpenApiData } from "../openapi/model.ts";
2
3
  import { operationOf, specAddresses, specOf } from "../openapi/model.ts";
3
4
  import { asSentence } from "../openapi/sentence.ts";
@@ -20,10 +21,14 @@ const inlineText = (text: string): string =>
20
21
  .replaceAll(/\s+/gu, " ")
21
22
  .replaceAll(/[\\`*_[\]<>~]/gu, String.raw`\$&`);
22
23
 
23
- /** One operation as a link, for the tag listings. */
24
+ /**
25
+ * One operation as a link, for the tag listings. `href` is the operation's
26
+ * served URL: its route under the deployment base.
27
+ */
24
28
  const listItem = (
25
29
  signature: string,
26
- operation: Pick<ApiOperationRef, "deprecated" | "route" | "summary">
30
+ href: string,
31
+ operation: Pick<ApiOperationRef, "deprecated" | "summary">
27
32
  ): string => {
28
33
  const tail = [
29
34
  // Title-like summaries ("Get a flag") need their own period before the
@@ -33,7 +38,7 @@ const listItem = (
33
38
  ]
34
39
  .filter(Boolean)
35
40
  .join(" ");
36
- return `- [${inlineCode(signature)}](${linkDestination(operation.route)})${tail ? ` — ${tail}` : ""}`;
41
+ return `- [${inlineCode(signature)}](${linkDestination(href)})${tail ? ` — ${tail}` : ""}`;
37
42
  };
38
43
 
39
44
  /**
@@ -60,9 +65,16 @@ const listItem = (
60
65
  * endpoint and what it does is the part that was missing altogether.
61
66
  *
62
67
  * `specs` is the parsed `blume:openapi` data — empty when the project has no
63
- * API reference, in which case every serializer declines.
68
+ * API reference, in which case every serializer declines. `deployBase` is the
69
+ * configured `deployment.base`: operation routes carry `basePath` but not it,
70
+ * and the links written here are made after the agent surfaces rebase the
71
+ * page's own links, so they take it here, as the rendered page's do.
64
72
  */
65
- export const openapiComponentSerializers = (specs: OpenApiData) => {
73
+ export const openapiComponentSerializers = (
74
+ specs: OpenApiData,
75
+ deployBase?: string
76
+ ) => {
77
+ const base = normalizeBasePath(deployBase);
66
78
  const spec = (source: EvaluatedValue) =>
67
79
  isString(source) ? specOf(specs, source) : undefined;
68
80
 
@@ -97,7 +109,11 @@ export const openapiComponentSerializers = (specs: OpenApiData) => {
97
109
  const items = Object.values(data.operations)
98
110
  .filter((operation) => operation.tagSlug === tag)
99
111
  .map((operation) =>
100
- listItem(operationSignature(data, operation), operation)
112
+ listItem(
113
+ operationSignature(data, operation),
114
+ mountBasePath(base, operation.route),
115
+ operation
116
+ )
101
117
  );
102
118
  return items.length > 0 ? items.join("\n") : null;
103
119
  },
@@ -2,9 +2,10 @@ import { basename } from "pathe";
2
2
 
3
3
  import {
4
4
  isInternalPath,
5
+ mountBasePath,
5
6
  normalizeBasePath,
7
+ withAuthoredBasePath,
6
8
  withBasePath,
7
- withComposedBasePath,
8
9
  } from "../core/base-path.ts";
9
10
  import { nextFenceState } from "../core/code-fences.ts";
10
11
  import type { FenceState } from "../core/code-fences.ts";
@@ -16,7 +17,7 @@ import {
16
17
  routeOfLinkedFile,
17
18
  } from "../core/links.ts";
18
19
  import type { RelativeLinkBase } from "../core/links.ts";
19
- import { localizeHref } from "../core/locale-links.ts";
20
+ import { localizeHref, servesRoute } from "../core/locale-links.ts";
20
21
  import type { BlumeProject } from "../core/project-graph.ts";
21
22
  import { extractLinks } from "../core/sources/normalize.ts";
22
23
 
@@ -27,14 +28,16 @@ import { extractLinks } from "../core/sources/normalize.ts";
27
28
  * link lands on `/install`; the rendered page rewrites it to the route it
28
29
  * means (`markdown/relative-links.ts`), and the Markdown an agent reads gets
29
30
  * the same rewrite from the same resolver (`resolveRelativeHref`), so both
30
- * point at one page. Root-relative page links (`[x](/guide)`,
31
- * `<Card href="/guide">`) get what the rendered page gives them too: the
32
- * `deployment.base` + `basePath` prefix (`markdown/base-links.ts`,
33
- * `components/content/base-href.ts`) and, on a page in a prefixed locale,
31
+ * point at one page. Root-relative links (`[x](/guide)`,
32
+ * `<Card href="/guide">`, `![logo](/logo.png)`) get what the rendered page
33
+ * gives them too (`markdown/base-links.ts`,
34
+ * `components/content/base-href.ts`): a page link the `deployment.base` +
35
+ * `basePath` prefix, a public file (`/spec.pdf`) or image the
36
+ * `deployment.base` alone, and, on a page in a prefixed locale, a page link
34
37
  * that locale's copy of the route when it is served
35
- * (`components/layout/LocaleLinks.astro`). Inline links, component `href`s,
36
- * and reference definitions are rewritten; fenced and inline code, images,
37
- * asset links, and external URLs are left as written.
38
+ * (`components/layout/LocaleLinks.astro`). Inline links, images, component
39
+ * `href`s, and reference definitions are rewritten; fenced and inline code,
40
+ * relative images and asset links, and external URLs are left as written.
38
41
  */
39
42
 
40
43
  /** The page a Markdown text was written for. */
@@ -67,13 +70,6 @@ const MAYBE_RELATIVE =
67
70
  const MAYBE_PAGE_LINK =
68
71
  /\]\([\t ]*<?(?![a-z][\d+.a-z-]*:|\/\/|#)|^ {0,3}\[[^\]\n]+\]:[\t ]*<?(?![a-z][\d+.a-z-]*:|\/\/|#)|\shref=["'](?![a-z][\d+.a-z-]*:|\/\/|#)/imu;
69
72
 
70
- /**
71
- * A path whose final segment carries a file extension — a `public/` asset or
72
- * a raw `.md` twin, which the rendered page neither bases nor localizes (see
73
- * `markdown/base-links.ts`).
74
- */
75
- const ASSET_PATH = /\.[\da-z]+$/iu;
76
-
77
73
  interface Splice {
78
74
  column: number;
79
75
  length: number;
@@ -109,18 +105,23 @@ const definitionSplice = (
109
105
  return { column: lead.length + angled, length: target.length, text: route };
110
106
  };
111
107
 
112
- /** Every routed splice in `text`, by 0-based line: links, then definitions. */
108
+ /**
109
+ * Every routed splice in `text`, by 0-based line: links and images, then
110
+ * definitions. An image target goes through `imaged`, everything else through
111
+ * `routed`.
112
+ */
113
113
  const collectSplices = (
114
114
  text: string,
115
115
  lines: readonly string[],
116
- routed: (target: string) => string | undefined
116
+ routed: (target: string) => string | undefined,
117
+ imaged: (target: string) => string | undefined
117
118
  ): Map<number, Splice[]> => {
118
119
  const splices = new Map<number, Splice[]>();
119
120
  const add = (index: number, splice: Splice): void => {
120
121
  splices.set(index, [...(splices.get(index) ?? []), splice]);
121
122
  };
122
123
  for (const link of extractLinks(text)) {
123
- const route = link.image ? undefined : routed(link.target);
124
+ const route = (link.image ? imaged : routed)(link.target);
124
125
  if (route !== undefined) {
125
126
  add(link.line - 1, {
126
127
  column: link.column - 1,
@@ -186,14 +187,19 @@ export const relativeLinkRewriter = (
186
187
  const navPath = navPathBySource.get(sourcePath) ?? basename(sourcePath);
187
188
  const resolveFile = (path: string): string | undefined =>
188
189
  routeOfLinkedFile(fileRoutes, { navPath, sourcePath }, path);
190
+ // A resolved route is one Blume serves, so the deployment base goes on
191
+ // unconditionally — even over a route that starts with the base's name.
189
192
  return (target) => {
190
193
  const route = resolveRelativeHref(target, from, resolveFile, (path) =>
191
194
  routes.has(path)
192
195
  );
193
- return route === undefined ? undefined : withBasePath(deployBase, route);
196
+ return route === undefined ? undefined : mountBasePath(deployBase, route);
194
197
  };
195
198
  };
196
199
 
200
+ /** Whether a page is served at a based, fragment-less path. */
201
+ const servesPage = (route: string): boolean => servesRoute(routes, route);
202
+
197
203
  return (text, page) => {
198
204
  const locale = localeByRoute.get(page.route);
199
205
  // The page's locale moves root links only when it has a URL prefix.
@@ -212,21 +218,31 @@ export const relativeLinkRewriter = (
212
218
  return text;
213
219
  }
214
220
  const relative = relativeRoute(page);
215
- // What the rendered page makes of `/guide`: based, then moved into the
221
+ // What the rendered page makes of `/guide`: based (a public file like
222
+ // `/spec.pdf` under the deployment base alone), then moved into the
216
223
  // page's locale when that route is served.
217
224
  const rooted = (target: string): string | undefined => {
218
- if (ASSET_PATH.test(target.replace(/[#?].*$/u, ""))) {
219
- return undefined;
220
- }
221
- const based = withComposedBasePath(deployBase, basePath, target);
225
+ const based = withAuthoredBasePath(
226
+ deployBase,
227
+ basePath,
228
+ target,
229
+ servesPage
230
+ );
222
231
  const href = localize ? localizeHref(based, localize) : based;
223
232
  return href === target ? undefined : href;
224
233
  };
225
234
  const routed = (target: string): string | undefined =>
226
235
  isInternalPath(target) ? rooted(target) : relative?.(target);
236
+ // An image is always a file: `public/` (or a generated asset endpoint),
237
+ // served under the deployment base but never `basePath`. A relative one
238
+ // is left to `core/content-assets.ts`.
239
+ const imaged = (target: string): string | undefined => {
240
+ const based = withBasePath(deployBase, target);
241
+ return based === target ? undefined : based;
242
+ };
227
243
 
228
244
  const lines = text.split("\n");
229
- const splices = collectSplices(text, lines, routed);
245
+ const splices = collectSplices(text, lines, routed, imaged);
230
246
 
231
247
  return lines
232
248
  .map((line, index) => {
@@ -18,7 +18,8 @@ export const projectComponentSerializers = (
18
18
  ({
19
19
  ...exampleComponentSerializers(project.examples ?? {}),
20
20
  ...openapiComponentSerializers(
21
- project.sources.find(isOpenApiSource)?.openApiData() ?? {}
21
+ project.sources.find(isOpenApiSource)?.openApiData() ?? {},
22
+ project.config.deployment.options.base
22
23
  ),
23
24
  ...project.config.agents.markdownComponents,
24
25
  }) satisfies Record<string, ComponentMarkdown>;
package/src/ai/skills.ts CHANGED
@@ -151,11 +151,16 @@ const skillMeta = (raw: string, dirName: string): SkillMetaResult => {
151
151
  * hold a `SKILL.md` (the layout `npx skills add` consumes). Subdirectories
152
152
  * without one are ignored silently — the directory may hold other assets —
153
153
  * while a present-but-invalid skill earns a warning so it isn't dropped
154
- * behind the publisher's back.
154
+ * behind the publisher's back. Two directories declaring the same `name`
155
+ * would publish to the same artifact path under two index entries, so the
156
+ * first directory in sorted order keeps the name and the other is skipped
157
+ * with a warning naming both.
155
158
  */
156
159
  export const collectSkills = async (dir: string): Promise<CollectedSkills> => {
157
160
  const skills: SkillArtifact[] = [];
158
161
  const warnings: string[] = [];
162
+ /** Skill name → the directory that claimed it first. */
163
+ const owners = new Map<string, string>();
159
164
  const items = await readdir(dir, { withFileTypes: true });
160
165
  for (const item of items.toSorted((a, b) => (a.name < b.name ? -1 : 1))) {
161
166
  if (!item.isDirectory() || item.name.startsWith(".")) {
@@ -177,6 +182,14 @@ export const collectSkills = async (dir: string): Promise<CollectedSkills> => {
177
182
  }
178
183
  continue;
179
184
  }
185
+ const owner = owners.get(meta.name);
186
+ if (owner) {
187
+ warnings.push(
188
+ `Skills "${owner}" and "${item.name}" both declare the name "${meta.name}"; publishing "${owner}" and skipping "${item.name}".`
189
+ );
190
+ continue;
191
+ }
192
+ owners.set(meta.name, item.name);
180
193
  // A lone SKILL.md ships verbatim; supporting resources ship as an
181
194
  // archive so the skill's relative references resolve after unpacking.
182
195
  const single = entries.length === 1;