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
@@ -16,23 +16,50 @@
16
16
  /** How long the copied confirmation holds before reverting. */
17
17
  const HOLD_MS = 1500;
18
18
 
19
+ /**
20
+ * How long a just-inserted live region waits before its first message. Screen
21
+ * readers start watching a region once it is in the document, so text written
22
+ * in the same task as the insertion is often never announced.
23
+ */
24
+ const REGISTER_MS = 100;
25
+
19
26
  /** The shared visually-hidden live region, created on first announcement. */
20
27
  let region: HTMLElement | null = null;
28
+ /** Pending while a fresh region registers; its first message waits for it. */
29
+ let registering: ReturnType<typeof setTimeout> | undefined;
30
+ /** The newest message asked for while the region was registering. */
31
+ let queued = "";
32
+
33
+ const write = (target: HTMLElement, message: string): void => {
34
+ // Clear first so repeating the same message is re-announced.
35
+ target.textContent = "";
36
+ target.textContent = message;
37
+ };
21
38
 
22
39
  /**
23
40
  * Announce `message` to screen readers. The region is re-created if a swap
24
- * (view transition, client router) disconnected it.
41
+ * (view transition, client router) disconnected it — the router replaces
42
+ * `<body>` on every navigation — and a fresh region gets its first message
43
+ * only after it has had time to register.
25
44
  */
26
45
  export const announceCopied = (message: string): void => {
27
46
  if (!region?.isConnected) {
28
- region = document.createElement("div");
29
- region.setAttribute("role", "status");
30
- region.className = "sr-only";
31
- document.body.append(region);
47
+ const fresh = document.createElement("div");
48
+ fresh.setAttribute("role", "status");
49
+ fresh.className = "sr-only";
50
+ document.body.append(fresh);
51
+ region = fresh;
52
+ clearTimeout(registering);
53
+ registering = setTimeout(() => {
54
+ registering = undefined;
55
+ write(fresh, queued);
56
+ }, REGISTER_MS);
57
+ }
58
+ if (registering === undefined) {
59
+ write(region, message);
60
+ } else {
61
+ queued = message;
32
62
  }
33
- // Clear first so repeating the same message is re-announced.
34
- region.textContent = "";
35
- region.textContent = message;
36
63
  };
37
64
 
38
65
  /**
@@ -6,7 +6,7 @@ import { createPortal } from "react-dom";
6
6
 
7
7
  import type { UIStrings } from "../../core/i18n-ui.ts";
8
8
  import { copyText } from "../copy-feedback.ts";
9
- import { joinBase, prefixBase } from "./base-path.ts";
9
+ import { joinBase, mountBase } from "./base-path.ts";
10
10
  import { useAssistant } from "./hooks.ts";
11
11
 
12
12
  /** A resolved empty-state prompt; `icon` is ready-to-inline SVG (or null). */
@@ -65,11 +65,12 @@ const markdown = new Marked({
65
65
  breaks: true,
66
66
  gfm: true,
67
67
  // The model cites pages as base-less logical routes (`[Title](/route)`); rewrite
68
- // link targets to served URLs so citations resolve under `deployment.base`.
69
- // `prefixBase` leaves external URLs and fragments untouched and is idempotent.
68
+ // link targets to served URLs so citations resolve under `deployment.base`,
69
+ // even a route that starts with the base's own name. `mountBase` leaves
70
+ // external URLs and fragments untouched.
70
71
  walkTokens: (token) => {
71
72
  if (token.type === "link") {
72
- token.href = prefixBase(import.meta.env.BASE_URL, token.href);
73
+ token.href = mountBase(import.meta.env.BASE_URL, token.href);
73
74
  }
74
75
  },
75
76
  });
@@ -77,9 +78,18 @@ const markdown = new Marked({
77
78
  const renderMarkdown = (content: string): string =>
78
79
  DOMPurify.sanitize(markdown.parse(content, { async: false }));
79
80
 
80
- const Glyph = ({ path, size = 16 }: { path: string; size?: number }) => (
81
+ const Glyph = ({
82
+ className,
83
+ path,
84
+ size = 16,
85
+ }: {
86
+ className?: string;
87
+ path: string;
88
+ size?: number;
89
+ }) => (
81
90
  <svg
82
91
  aria-hidden="true"
92
+ className={className}
83
93
  // `path` is a trusted, server-resolved Lucide glyph body (inline SVG),
84
94
  // not user content; it must be injected as markup to render the icon.
85
95
  // oxlint-disable-next-line react/no-danger -- trusted server-resolved inline SVG glyph
@@ -253,6 +263,22 @@ const Assistant = ({
253
263
  };
254
264
  }, [open]);
255
265
 
266
+ // Stamp the incoming body before the swap installs it, as the theme and
267
+ // banner scripts do for <html>. Stamped only after the swap (below, in a
268
+ // passive effect), the new body would first paint without the push padding,
269
+ // replaying its transition on every navigation with the panel docked.
270
+ useEffect(() => {
271
+ if (!open) {
272
+ return;
273
+ }
274
+ const onBeforeSwap = (event: DocumentEventMap["astro:before-swap"]) => {
275
+ event.newDocument.body.dataset.blumeAssistant = "open";
276
+ };
277
+ document.addEventListener("astro:before-swap", onBeforeSwap);
278
+ return () =>
279
+ document.removeEventListener("astro:before-swap", onBeforeSwap);
280
+ }, [open]);
281
+
256
282
  // Re-stamp the push attribute after a swap while the panel is open — the new
257
283
  // body arrives without it. Deliberately separate from the effect above: a
258
284
  // navigation must not re-run the focus handling and yank focus out of the
@@ -383,6 +409,9 @@ const Assistant = ({
383
409
  <aside
384
410
  aria-hidden={open ? undefined : "true"}
385
411
  aria-label={t.title}
412
+ // Print hides the chrome by this marker, not by tag: content renders
413
+ // <aside> too (Callout, Panel), and that must print.
414
+ data-blume-assistant-panel=""
386
415
  ref={panelRef}
387
416
  // The closed panel is only translated off-screen; `inert` drops its
388
417
  // buttons/textarea from the tab order and the accessibility tree.
@@ -418,7 +447,9 @@ const Assistant = ({
418
447
  onClick={() => setOpen(false)}
419
448
  type="button"
420
449
  >
421
- <Glyph path={icons.close} size={18} />
450
+ {/* Chevrons toward the inline end, where the panel docks: they
451
+ mirror with the panel in a right-to-left locale. */}
452
+ <Glyph className="rtl:-scale-x-100" path={icons.close} size={18} />
422
453
  </button>
423
454
  </div>
424
455
  </header>
@@ -13,20 +13,50 @@ export const withTrailingSlash = (base: string): string =>
13
13
  export const joinBase = (base: string, path: string): string =>
14
14
  `${withTrailingSlash(base)}${path}`;
15
15
 
16
+ /** A root-relative internal target (`/x`) — never `//host` or a URL. */
17
+ const isRootRelative = (route: string): boolean =>
18
+ route.startsWith("/") && !route.startsWith("//");
19
+
20
+ /** A target's path, without its `?query` or `#fragment`. */
21
+ const pathOf = (route: string): string => {
22
+ const at = route.search(/[#?]/u);
23
+ return at === -1 ? route : route.slice(0, at);
24
+ };
25
+
16
26
  /**
17
- * Prefix a root-relative internal route with the deployment base
18
- * (`/guide` under base `/sub` -> `/sub/guide`), so a rendered link points at the
27
+ * Prefix a root-relative internal link an author wrote with the deployment
28
+ * base (`/guide` under base `/sub` -> `/sub/guide`), so it points at the
19
29
  * page's real served URL. External URLs, protocol-relative URLs, and fragments
20
- * pass through untouched, and it's idempotent (a route already under the base is
21
- * returned unchanged). This is applied only where a URL is *emitted* — the
22
- * navigation model and active-route matching stay in base-less logical space.
30
+ * pass through untouched, and it's idempotent: a link whose path is already
31
+ * under the base (`/sub/guide`, `/sub#install`) is returned unchanged, so a
32
+ * base written by hand isn't doubled. A route Blume generates takes
33
+ * {@link mountBase} instead. This is applied only where a URL is *emitted* —
34
+ * the navigation model and active-route matching stay in base-less logical
35
+ * space.
23
36
  */
24
37
  export const prefixBase = (base: string, route: string): string => {
25
- if (!route.startsWith("/") || route.startsWith("//")) {
38
+ if (!isRootRelative(route)) {
26
39
  return route;
27
40
  }
28
41
  const trimmed = base.replace(/\/+$/u, "");
29
- if (!trimmed || route === trimmed || route.startsWith(`${trimmed}/`)) {
42
+ const path = pathOf(route);
43
+ if (!trimmed || path === trimmed || path.startsWith(`${trimmed}/`)) {
44
+ return route;
45
+ }
46
+ return route === "/" ? trimmed : `${trimmed}${route}`;
47
+ };
48
+
49
+ /**
50
+ * Prefix a route Blume generates (a page, a sidebar entry, a Markdown mirror,
51
+ * an agent artifact) with the deployment base, unconditionally. Unlike
52
+ * {@link prefixBase}, a route whose first segment matches the base is still
53
+ * prefixed: under base `/guides`, the page `guides/setup.md` is served at
54
+ * `/guides/guides/setup`, not at `/guides/setup`. External and
55
+ * protocol-relative URLs pass through untouched.
56
+ */
57
+ export const mountBase = (base: string, route: string): string => {
58
+ const trimmed = base.replace(/\/+$/u, "");
59
+ if (!(trimmed && isRootRelative(route))) {
30
60
  return route;
31
61
  }
32
62
  return route === "/" ? trimmed : `${trimmed}${route}`;
@@ -34,13 +64,20 @@ export const prefixBase = (base: string, route: string): string => {
34
64
 
35
65
  /**
36
66
  * {@link prefixBase} bound to the build-time `BASE_URL` (the resolved
37
- * `deployment.base`). The ergonomic form for `.astro` templates — `href={
38
- * withBase(route)}` — since `BASE_URL` is inlined by Vite wherever this module
39
- * is bundled into the site.
67
+ * `deployment.base`), for links an author wrote. The ergonomic form for
68
+ * `.astro` templates — `href={withBase(href)}` — since `BASE_URL` is inlined
69
+ * by Vite wherever this module is bundled into the site.
40
70
  */
41
71
  export const withBase = (route: string): string =>
42
72
  prefixBase(import.meta.env.BASE_URL ?? "/", route);
43
73
 
74
+ /**
75
+ * {@link mountBase} bound to the build-time `BASE_URL`, for routes Blume
76
+ * generates: `href={withMountedBase(page.route)}`.
77
+ */
78
+ export const withMountedBase = (route: string): string =>
79
+ mountBase(import.meta.env.BASE_URL ?? "/", route);
80
+
44
81
  /**
45
82
  * A pathname with the deployment base stripped (`/docs/guide` -> `/guide`),
46
83
  * for page-context lookups against base-less document routes.
@@ -23,27 +23,29 @@ import { joinBase, stripBase } from "./base-path.ts";
23
23
 
24
24
  export type { BlumeClientData } from "../../core/data.ts";
25
25
 
26
- let cachedData: BlumeClientData | null = null;
26
+ /**
27
+ * The last parsed snapshot, keyed by the script text it came from. Each page
28
+ * renders its own snapshot (its route and title, its locale's navigation), and
29
+ * a client-router navigation swaps in the new page's tag, so the cache is only
30
+ * reused while the tag's text is unchanged.
31
+ */
32
+ let cached: { data: BlumeClientData; text: string } | null = null;
27
33
 
28
- /** Read + parse the injected snapshot (memoized); null on the server. */
34
+ /** Read + parse the current page's injected snapshot (memoized per text). */
29
35
  const readClientData = (): BlumeClientData | null => {
30
- if (cachedData) {
31
- return cachedData;
32
- }
33
- // `document` is undeclared on the server; probing `globalThis` avoids both
34
- // the bare-reference ReferenceError and a `typeof` sniff.
35
- if (!("document" in globalThis)) {
36
+ const text = document.querySelector("#blume-client-data")?.textContent;
37
+ if (!text) {
36
38
  return null;
37
39
  }
38
- const element = document.querySelector("#blume-client-data");
39
- if (!element?.textContent) {
40
- return null;
40
+ if (cached?.text === text) {
41
+ return cached.data;
41
42
  }
42
43
  try {
43
44
  // SAFETY: the layout serialized this script tag's JSON from the same
44
45
  // `BlumeClientData` snapshot this reads back.
45
- cachedData = JSON.parse(element.textContent) as BlumeClientData;
46
- return cachedData;
46
+ const data = JSON.parse(text) as BlumeClientData;
47
+ cached = { data, text };
48
+ return data;
47
49
  } catch {
48
50
  return null;
49
51
  }
@@ -51,15 +53,29 @@ const readClientData = (): BlumeClientData | null => {
51
53
 
52
54
  /**
53
55
  * Read the injected snapshot after mount. `null` on the server and on the first
54
- * client render (so hydration matches), then the data once mounted.
56
+ * client render (so hydration matches), then the data once mounted, re-read
57
+ * after every client-router swap.
55
58
  */
56
59
  const useClientData = (): BlumeClientData | null => {
57
60
  const [data, setData] = useState<BlumeClientData | null>(null);
58
- // Intentional post-mount hydration guard: `null` on the server and first
59
- // client render so hydration matches, then the snapshot once mounted. The
60
- // extra render is required; do not seed the initial value from the DOM.
61
- // oxlint-disable-next-line react/react-compiler, react/set-state-in-effect, react-doctor/no-initialize-state -- deliberate SSR hydration guard
62
- useEffect(() => setData(readClientData()), []);
61
+ useEffect(() => {
62
+ // `document` is undeclared on the server; probing `globalThis` avoids both
63
+ // the bare-reference ReferenceError and a `typeof` sniff.
64
+ if (!("document" in globalThis)) {
65
+ return;
66
+ }
67
+ const sync = () => setData(readClientData());
68
+ // Intentional post-mount hydration guard: `null` on the server and first
69
+ // client render so hydration matches, then the snapshot once mounted. The
70
+ // extra render is required; do not seed the initial value from the DOM.
71
+ // oxlint-disable-next-line react/react-compiler, react/set-state-in-effect, react-doctor/no-initialize-state -- deliberate SSR hydration guard
72
+ sync();
73
+ // An island kept across navigations (`transition:persist`) outlives the
74
+ // page it mounted on; the swap brings the next page's snapshot, so read
75
+ // it again or the hook keeps answering with the first page's data.
76
+ document.addEventListener("astro:after-swap", sync);
77
+ return () => document.removeEventListener("astro:after-swap", sync);
78
+ }, []);
63
79
  return data;
64
80
  };
65
81
 
@@ -308,12 +324,17 @@ export const useAssistant = (
308
324
  }
309
325
  }
310
326
  if (live()) {
311
- // A 200 with nothing in it (no body, an empty stream) leaves the
312
- // reader a blank bubble — that is not an answer.
327
+ // A 200 with nothing in it (no body, an empty stream) is not an
328
+ // answer. It is also how a provider failure after the 200 arrives
329
+ // (a bad key, a rate limit, an unknown model): the route's text
330
+ // stream carries only text, so the error ends it empty. Show the
331
+ // error notice instead of leaving a pulsing blank bubble.
313
332
  if (assistant.content) {
314
333
  outcome("ask_answer", { chars: assistant.content.length });
315
334
  } else {
316
335
  outcome("ask_error", { status });
336
+ assistant.content = errorMessage;
337
+ setMessages([...history, { ...assistant }]);
317
338
  }
318
339
  }
319
340
  } catch {
@@ -1,5 +1,7 @@
1
+ import { unescape } from "html-escaper";
2
+
1
3
  import type { SearchFn } from "../layout/search/types.ts";
2
- import { joinBase, prefixBase } from "./base-path.ts";
4
+ import { joinBase, mountBase, prefixBase } from "./base-path.ts";
3
5
 
4
6
  /**
5
7
  * WebMCP (W3C Web Machine Learning CG): in-page tools an agentic browser can
@@ -80,13 +82,15 @@ const isCallable = <T extends (...args: never[]) => void>(
80
82
  const TAG = /<[^>]*>?/gu;
81
83
 
82
84
  /**
83
- * Drop the `<mark>` highlighting (and any other markup) search hits carry.
84
- * The closing `>` is optional so every `<` starts a strip: a dangling
85
- * `<script` fragment can't survive the way it would if a full `<...>` pair
86
- * were required. The input is HTML, where a literal `<` is `&lt;`, so
87
- * consuming from every raw `<` loses nothing legitimate. Stripping repeats
88
- * to a fixed point so the no-fragment guarantee is explicit rather than an
89
- * artifact of the regex shape.
85
+ * Search-hit HTML as the plain text an agent reads: the `<mark>`
86
+ * highlighting (and any other markup) dropped, then the entities the
87
+ * highlighter escaped decoded (`&lt;T&gt; &amp;` → `<T> &`). The closing `>`
88
+ * is optional so every `<` starts a strip: a dangling `<script` fragment
89
+ * can't survive the way it would if a full `<...>` pair were required. The
90
+ * input is HTML, where a literal `<` is `&lt;`, so consuming from every raw
91
+ * `<` loses nothing legitimate. Stripping repeats to a fixed point so the
92
+ * no-fragment guarantee is explicit rather than an artifact of the regex
93
+ * shape; decoding comes last, so a decoded `<` is text, never a tag.
90
94
  */
91
95
  const plain = (html: string): string => {
92
96
  let stripped = html;
@@ -95,7 +99,7 @@ const plain = (html: string): string => {
95
99
  previous = stripped;
96
100
  stripped = stripped.replaceAll(TAG, "");
97
101
  } while (stripped !== previous);
98
- return stripped;
102
+ return unescape(stripped);
99
103
  };
100
104
 
101
105
  export interface WebMcpToolOptions {
@@ -140,7 +144,7 @@ export const buildWebMcpTools = (options: WebMcpToolOptions): WebMcpTool[] => {
140
144
  hits
141
145
  .map(
142
146
  (hit) =>
143
- `${plain(hit.title)} — ${prefixBase(options.base, hit.url)}\n${plain(hit.excerpt)}`
147
+ `${plain(hit.title)} — ${mountBase(options.base, hit.url)}\n${plain(hit.excerpt)}`
144
148
  )
145
149
  .join("\n\n")
146
150
  );
@@ -171,6 +175,8 @@ export const buildWebMcpTools = (options: WebMcpToolOptions): WebMcpTool[] => {
171
175
  }
172
176
  const trimmed = route.length > 1 ? route.replace(/\/+$/u, "") : route;
173
177
  const target = trimmed === "/" ? "/index" : trimmed;
178
+ // The agent may hand back a served URL `search_docs` returned, base
179
+ // included, so a path already under the base keeps it (`prefixBase`).
174
180
  const response = await fetchFn(
175
181
  `${prefixBase(options.base, target)}.md`
176
182
  ).catch(() => null);
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import data from "blume:data";
2
3
  import { EN_UI } from "../../core/i18n-ui.ts";
3
4
  import type { UIStrings } from "../../core/i18n-ui.ts";
4
5
  import { withBase } from "../islands/base-path.ts";
@@ -18,13 +19,31 @@ interface Props {
18
19
  | null;
19
20
  /** Localized banner labels. */
20
21
  strings?: UIStrings["banner"];
22
+ /**
23
+ * Active locale of the page. The link follows it the way the sidebar's
24
+ * featured links do (`/guide` -> `/fr/guide` when French serves it).
25
+ * Absent, the link resolves for the default locale.
26
+ */
27
+ locale?: string;
21
28
  }
22
29
 
23
- const { banner, strings } = Astro.props;
30
+ const { banner, strings, locale } = Astro.props;
24
31
  // Merge over the English baseline per key (rather than `strings ?? …`) so a
25
32
  // partial — or missing — strings object still resolves every label to a
26
33
  // default, matching the pattern PageFeedback and Search use.
27
34
  const b = { ...EN_UI.banner, ...strings };
35
+ // The configured `banner.link.href` is authored as if the site were mounted at
36
+ // root, in the default locale's path space; the locale's navigation carries it
37
+ // localized and mounted under `basePath` (the default navigation on a
38
+ // single-locale site). A banner a custom page passes with a link of its own
39
+ // renders that link as given.
40
+ const navigation =
41
+ (locale === undefined ? undefined : data.navigationByLocale[locale]) ??
42
+ data.navigation;
43
+ const linkHref =
44
+ banner?.link && banner.link.href === data.config.banner?.link?.href
45
+ ? (navigation.bannerHref ?? banner.link.href)
46
+ : banner?.link?.href;
28
47
  ---
29
48
 
30
49
  {
@@ -37,13 +56,13 @@ const b = { ...EN_UI.banner, ...strings };
37
56
  {/* The message is authored in one language, whatever the page's
38
57
  direction: `dir="auto"` keeps its punctuation where it was typed. */}
39
58
  <span dir="auto">{banner.content}</span>
40
- {banner.link && (
59
+ {banner.link && linkHref && (
41
60
  <a
42
61
  class="inline-flex items-center gap-1 font-medium underline underline-offset-2"
43
- href={withBase(banner.link.href)}
62
+ href={withBase(linkHref)}
44
63
  >
45
64
  {banner.link.text}
46
- <Icon name="arrow-right" size={14} />
65
+ <Icon class="rtl:-scale-x-100" name="arrow-right" size={14} />
47
66
  </a>
48
67
  )}
49
68
  {banner.dismissible && (
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import { EN_UI } from "../../core/i18n-ui.ts";
3
3
  import type { UIStrings } from "../../core/i18n-ui.ts";
4
- import { withBase } from "../islands/base-path.ts";
4
+ import { withMountedBase } from "../islands/base-path.ts";
5
5
  import type { Crumb } from "./nav-utils.ts";
6
6
 
7
7
  interface Props {
@@ -35,7 +35,10 @@ const eyebrowCrumb = crumbs.length > 1 ? crumbs[crumbs.length - 2] : null;
35
35
  ]}
36
36
  >
37
37
  {eyebrowCrumb.route ? (
38
- <a class="hover:text-foreground" href={withBase(eyebrowCrumb.route)}>
38
+ <a
39
+ class="hover:text-foreground"
40
+ href={withMountedBase(eyebrowCrumb.route)}
41
+ >
39
42
  {eyebrowCrumb.label}
40
43
  </a>
41
44
  ) : (
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import type { DiscoveryHeadLinks } from "../../core/data.ts";
3
- import { withBase } from "../islands/base-path.ts";
3
+ import { withMountedBase } from "../islands/base-path.ts";
4
4
  // Emits the agent-discovery <link>s for the document <head>, on every page and
5
5
  // not just the root: an agent that enters on a deep page (a search result, a
6
6
  // shared link) never sees the homepage HTTP `Link` header (see
@@ -32,7 +32,7 @@ const { discovery, markdownMirror } = Astro.props;
32
32
  {
33
33
  discovery?.agentReadability && (
34
34
  <link
35
- href={withBase("/agent-readability.json")}
35
+ href={withMountedBase("/agent-readability.json")}
36
36
  rel="describedby"
37
37
  type="application/json"
38
38
  />
@@ -40,7 +40,11 @@ const { discovery, markdownMirror } = Astro.props;
40
40
  }
41
41
  {
42
42
  discovery?.llmsTxt && (
43
- <link href={withBase("/llms.txt")} rel="describedby" type="text/plain" />
43
+ <link
44
+ href={withMountedBase("/llms.txt")}
45
+ rel="describedby"
46
+ type="text/plain"
47
+ />
44
48
  )
45
49
  }
46
50
  {/* The AI Catalog / ARD manifest under both relations its two specs define:
@@ -50,12 +54,12 @@ const { discovery, markdownMirror } = Astro.props;
50
54
  discovery?.aiCatalog && (
51
55
  <>
52
56
  <link
53
- href={withBase("/.well-known/ai-catalog.json")}
57
+ href={withMountedBase("/.well-known/ai-catalog.json")}
54
58
  rel="ai-catalog"
55
59
  type="application/ai-catalog+json"
56
60
  />
57
61
  <link
58
- href={withBase("/.well-known/ard.json")}
62
+ href={withMountedBase("/.well-known/ard.json")}
59
63
  rel="ard"
60
64
  type="application/json"
61
65
  />
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import Ask from "blume:ask";
3
3
  import data from "blume:data";
4
- import { withBase } from "../islands/base-path.ts";
4
+ import { withBase, withMountedBase } from "../islands/base-path.ts";
5
5
  import { NEW_TAB_ATTRS, newTabAttrs } from "../../core/new-tab.ts";
6
6
  import type { ComponentOverride } from "../../core/define-components.ts";
7
7
  import { EN_UI } from "../../core/i18n-ui.ts";
@@ -17,6 +17,7 @@ import LanguageSwitcher from "./LanguageSwitcher.astro";
17
17
  import Logo from "./Logo.astro";
18
18
  import { currentTabForRoute } from "./nav-utils.ts";
19
19
  import NavSelector from "./NavSelector.astro";
20
+ import NavTabMenu from "./NavTabMenu.astro";
20
21
  import { resolveSlot } from "./overrides.ts";
21
22
  import Search from "./Search.astro";
22
23
 
@@ -216,15 +217,27 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
216
217
  {
217
218
  navigation.tabs.length > 0 && (
218
219
  <nav aria-label={n.sections} class={tabsNavClass}>
219
- {navigation.tabs.map((tab) => (
220
- <a
221
- aria-current={tab === activeTab ? "page" : undefined}
222
- class="rounded-full px-3 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:text-foreground"
223
- href={withBase(tab.href ?? tab.path)}
224
- >
225
- {tab.label}
226
- </a>
227
- ))}
220
+ {navigation.tabs.map((tab) =>
221
+ tab.items && tab.items.length > 0 ? (
222
+ <NavTabMenu
223
+ active={tab === activeTab}
224
+ route={route}
225
+ tab={tab}
226
+ variant="bar"
227
+ />
228
+ ) : (
229
+ <a
230
+ aria-current={tab === activeTab ? "page" : undefined}
231
+ class="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:text-foreground"
232
+ href={withMountedBase(tab.href ?? tab.path)}
233
+ >
234
+ {tab.icon && (
235
+ <Icon class="shrink-0" name={tab.icon} size={14} />
236
+ )}
237
+ {tab.label}
238
+ </a>
239
+ )
240
+ )}
228
241
  </nav>
229
242
  )
230
243
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { withBase } from "../islands/base-path.ts";
2
+ import { withMountedBase } from "../islands/base-path.ts";
3
3
  import type { LocaleSwitchOption } from "../../core/types.ts";
4
4
  import Icon from "../Icon.astro";
5
5
 
@@ -42,7 +42,7 @@ const menuRowClass =
42
42
  <a
43
43
  aria-current={option.current ? "true" : undefined}
44
44
  class={menuRowClass}
45
- href={withBase(option.href)}
45
+ href={withMountedBase(option.href)}
46
46
  hreflang={option.code}
47
47
  lang={option.code}
48
48
  >
@@ -47,9 +47,14 @@ const brandHref =
47
47
  // it's omitted. An explicit `""` hides it (an image-only logo whose mark already
48
48
  // carries the wordmark).
49
49
  const brandText = logo?.text ?? site.title;
50
+ // Without a wordmark the link's only content is the mark, and an inline SVG
51
+ // mark is decorative (`aria-hidden`), so the link would have no accessible
52
+ // name. Name it after the image's `alt`, else the site title.
53
+ const linkLabel = brandText ? undefined : logoAlt || site.title;
50
54
  ---
51
55
 
52
56
  <a
57
+ aria-label={linkLabel}
53
58
  class="inline-flex min-w-0 items-center gap-2 font-semibold text-base text-foreground"
54
59
  href={withBase(brandHref)}
55
60
  >
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { withBase } from "../islands/base-path.ts";
2
+ import { withMountedBase } from "../islands/base-path.ts";
3
3
  // A top-level navigation selector: a dropdown that switches
4
4
  // between partitions of the site — a product, a version, or any grouped set of
5
5
  // destinations (`navigation.selectors` in the config). Built on
@@ -38,6 +38,11 @@ const active =
38
38
  // it switches. Longer product names still collapse to the icon there.
39
39
  const label = active?.label ?? selector.label;
40
40
  const labelClass = label.length <= 6 ? undefined : "max-sm:hidden";
41
+ // The accessible name carries the visible active label too (WCAG 2.5.3,
42
+ // label in name), so a voice-control user can say "v2" to open it — the
43
+ // language switcher's pattern.
44
+ const summaryLabel =
45
+ label === selector.label ? selector.label : `${selector.label}: ${label}`;
41
46
 
42
47
  const iconButton =
43
48
  "inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground";
@@ -49,7 +54,7 @@ const menuRowClass =
49
54
  selector.items.length > 0 && (
50
55
  <details class="group relative" data-blume-dropdown>
51
56
  <summary
52
- aria-label={selector.label}
57
+ aria-label={summaryLabel}
53
58
  class={`${iconButton} list-none [&::-webkit-details-marker]:hidden`}
54
59
  >
55
60
  {active?.icon && <Icon name={active.icon} size={16} />}
@@ -68,7 +73,7 @@ const menuRowClass =
68
73
  <a
69
74
  aria-current={item.path === active?.path ? "true" : undefined}
70
75
  class={menuRowClass}
71
- href={withBase(item.path)}
76
+ href={withMountedBase(item.path)}
72
77
  >
73
78
  {item.icon && <Icon class="mt-0.5" name={item.icon} size={16} />}
74
79
  <span class="flex-1">