blume 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +110 -0
  3. package/README.md +2 -2
  4. package/dist/cli/{chunk-f2972sbt.js → chunk-00gs3wqs.js} +1 -1
  5. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  6. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  7. package/dist/cli/{chunk-pat2zzwc.js → chunk-3yce002v.js} +21 -8
  8. package/dist/cli/{chunk-pat2zzwc.js.map → chunk-3yce002v.js.map} +4 -4
  9. package/dist/cli/{chunk-d1tadaw7.js → chunk-4e9b9ra6.js} +17 -7
  10. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  11. package/dist/cli/{chunk-zg2gtj10.js → chunk-5r8g91qn.js} +352 -676
  12. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  13. package/dist/cli/{chunk-11j0384y.js → chunk-6dtt0zfn.js} +14 -14
  14. package/dist/cli/{chunk-11j0384y.js.map → chunk-6dtt0zfn.js.map} +3 -3
  15. package/dist/cli/{chunk-bw22s759.js → chunk-6k4ftwze.js} +38 -19
  16. package/dist/cli/{chunk-bw22s759.js.map → chunk-6k4ftwze.js.map} +4 -4
  17. package/dist/cli/{chunk-sqn5t4q0.js → chunk-7mbqtmgb.js} +16 -7
  18. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  19. package/dist/cli/{chunk-79njf86q.js → chunk-7vtckvaw.js} +21 -19
  20. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  21. package/dist/cli/{chunk-fh5hj5jt.js → chunk-8g8ytmgx.js} +17 -18
  22. package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-8g8ytmgx.js.map} +2 -2
  23. package/dist/cli/{chunk-1w8dp3qb.js → chunk-91ws1n6j.js} +18 -15
  24. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  25. package/dist/cli/{chunk-7ez8ny0t.js → chunk-bbnwccaz.js} +2 -2
  26. package/dist/cli/{chunk-5a2z0198.js → chunk-bfwp9vp6.js} +18 -10
  27. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  28. package/dist/cli/{chunk-ernrthtr.js → chunk-d1v5rhy0.js} +14 -15
  29. package/dist/cli/{chunk-ernrthtr.js.map → chunk-d1v5rhy0.js.map} +2 -2
  30. package/dist/cli/{chunk-a9kptbw5.js → chunk-ddndchfr.js} +21 -6
  31. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  32. package/dist/cli/{chunk-zxccj738.js → chunk-esh98wmb.js} +1 -1
  33. package/dist/cli/{chunk-88cpgt6h.js → chunk-fsmrqk8a.js} +1 -1
  34. package/dist/cli/{chunk-b5aj94ah.js → chunk-g698a744.js} +5 -5
  35. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  36. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  37. package/dist/cli/{chunk-41za066z.js → chunk-h2ez8dzb.js} +4 -4
  38. package/dist/cli/{chunk-by2290sx.js → chunk-h7k3nq3v.js} +2 -2
  39. package/dist/cli/{chunk-j8mw0za6.js → chunk-hqp2ajnh.js} +252 -123
  40. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  41. package/dist/cli/{chunk-mnqj32sj.js → chunk-hr8ne106.js} +119 -50
  42. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  43. package/dist/cli/{chunk-mwt1k8n7.js → chunk-j85scx15.js} +74 -30
  44. package/dist/cli/chunk-j85scx15.js.map +10 -0
  45. package/dist/cli/{chunk-z01ze5c1.js → chunk-k7pj68a8.js} +63 -22
  46. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  47. package/dist/cli/{chunk-2q1dwty4.js → chunk-mqc662a6.js} +8 -8
  48. package/dist/cli/{chunk-2q1dwty4.js.map → chunk-mqc662a6.js.map} +4 -4
  49. package/dist/cli/{chunk-y3e45rc8.js → chunk-n1yg3tj3.js} +4 -4
  50. package/dist/cli/{chunk-bctazmbk.js → chunk-nfcyttvj.js} +17 -6
  51. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  52. package/dist/cli/{chunk-6k8vp3ta.js → chunk-pbg5a4s3.js} +60 -28
  53. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  54. package/dist/cli/{chunk-d80hr03s.js → chunk-ppzjqwx2.js} +24 -17
  55. package/dist/cli/{chunk-d80hr03s.js.map → chunk-ppzjqwx2.js.map} +4 -4
  56. package/dist/cli/{chunk-beat36xx.js → chunk-qkb5a8sa.js} +25 -10
  57. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  58. package/dist/cli/{chunk-bnbmcwfb.js → chunk-sqw4ekg1.js} +5 -5
  59. package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-sqw4ekg1.js.map} +3 -3
  60. package/dist/cli/{chunk-xaz13gwg.js → chunk-v6ya5kcb.js} +3268 -1150
  61. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  62. package/dist/cli/{chunk-tzne8qfq.js → chunk-w4bxdvsa.js} +18 -15
  63. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  64. package/dist/cli/{chunk-f7t03s3g.js → chunk-wdrt2k2v.js} +2 -2
  65. package/dist/cli/{chunk-z1f5arsg.js → chunk-xh43dwgw.js} +170 -55
  66. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  67. package/dist/cli/{chunk-pnnvybbk.js → chunk-zp79m0ts.js} +5 -5
  68. package/dist/cli/{chunk-pnnvybbk.js.map → chunk-zp79m0ts.js.map} +2 -2
  69. package/dist/cli/index.js +160 -35
  70. package/dist/cli/index.js.map +4 -4
  71. package/dist/types/ai/agent-readability.d.ts +1 -1
  72. package/dist/types/ai/agent-surface.d.ts +32 -0
  73. package/dist/types/ai/api/paths.d.ts +1 -1
  74. package/dist/types/ai/api-catalog.d.ts +7 -1
  75. package/dist/types/ai/ask-context.d.ts +14 -7
  76. package/dist/types/ai/ask.d.ts +43 -43
  77. package/dist/types/ai/component-markdown.d.ts +4 -4
  78. package/dist/types/ai/index.d.ts +3 -3
  79. package/dist/types/ai/openapi-components.d.ts +1 -1
  80. package/dist/types/ai/relative-links.d.ts +9 -4
  81. package/dist/types/ai/serializers.d.ts +1 -1
  82. package/dist/types/ai/static-expression.d.ts +28 -0
  83. package/dist/types/ai/visibility.d.ts +1 -1
  84. package/dist/types/analytics/databuddy.d.ts +43 -0
  85. package/dist/types/analytics/index.d.ts +2 -0
  86. package/dist/types/analytics/schema.d.ts +14 -0
  87. package/dist/types/cli/env.d.ts +5 -0
  88. package/dist/types/cli/init/scaffold.d.ts +19 -3
  89. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  90. package/dist/types/core/base-path.d.ts +9 -0
  91. package/dist/types/core/config-input.d.ts +21 -21
  92. package/dist/types/core/config.d.ts +5 -5
  93. package/dist/types/core/data.d.ts +3 -3
  94. package/dist/types/core/graph.d.ts +2 -0
  95. package/dist/types/core/i18n-ui.d.ts +37 -8
  96. package/dist/types/core/i18n.d.ts +7 -1
  97. package/dist/types/core/links.d.ts +3 -1
  98. package/dist/types/core/load-module.d.ts +10 -0
  99. package/dist/types/core/locale-links.d.ts +12 -2
  100. package/dist/types/core/meta.d.ts +5 -1
  101. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  102. package/dist/types/core/navigation.d.ts +38 -0
  103. package/dist/types/core/ordering-prefix.d.ts +4 -0
  104. package/dist/types/core/safe-links.d.ts +3 -1
  105. package/dist/types/core/schema.d.ts +57 -18
  106. package/dist/types/core/sources/github-releases.d.ts +5 -0
  107. package/dist/types/core/sources/lower.d.ts +14 -2
  108. package/dist/types/core/sources/normalize.d.ts +10 -1
  109. package/dist/types/core/sources/remote.d.ts +11 -1
  110. package/dist/types/core/sources/resolve.d.ts +12 -0
  111. package/dist/types/core/sources/types.d.ts +31 -0
  112. package/dist/types/core/types.d.ts +9 -0
  113. package/dist/types/core/unrecognized-keys.d.ts +1 -1
  114. package/dist/types/deploy/adapters/node.d.ts +5 -2
  115. package/dist/types/deploy/adapters/types.d.ts +7 -0
  116. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  117. package/dist/types/deploy/headers.d.ts +31 -7
  118. package/dist/types/deploy/node-headers.d.ts +43 -8
  119. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  120. package/dist/types/deploy/platforms/node.d.ts +6 -5
  121. package/dist/types/deploy/platforms/types.d.ts +7 -0
  122. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  123. package/dist/types/deploy/redirects.d.ts +7 -2
  124. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  125. package/dist/types/openapi/model.d.ts +20 -6
  126. package/dist/types/search/documents.d.ts +1 -1
  127. package/dist/types/search/orama-index.d.ts +1 -1
  128. package/dist/types/search/sync/algolia.d.ts +3 -1
  129. package/docs/01-quickstart.mdx +3 -2
  130. package/docs/02-deployment.mdx +21 -12
  131. package/docs/03-upgrading.mdx +22 -9
  132. package/docs/04-migrating.mdx +4 -4
  133. package/docs/08-faq.mdx +12 -5
  134. package/docs/advanced/changelog.mdx +1 -1
  135. package/docs/advanced/custom-pages.mdx +5 -3
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +5 -5
  138. package/docs/cli/doctor.mdx +4 -4
  139. package/docs/cli/evals.mdx +9 -9
  140. package/docs/cli/index.mdx +6 -3
  141. package/docs/cli/translate.mdx +8 -8
  142. package/docs/configuration/analytics.mdx +23 -2
  143. package/docs/configuration/{ask-ai.mdx → assistant.mdx} +20 -20
  144. package/docs/configuration/customization.mdx +5 -4
  145. package/docs/configuration/index.mdx +7 -5
  146. package/docs/configuration/meta.ts +1 -1
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +1 -1
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -3
  151. package/docs/content/index.mdx +4 -2
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +5 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -12
  156. package/docs/content/versioning.mdx +1 -0
  157. package/docs/discoverability/agent-discovery.mdx +22 -8
  158. package/docs/discoverability/index.mdx +3 -3
  159. package/docs/discoverability/llms-txt.mdx +2 -5
  160. package/docs/discoverability/markdown.mdx +5 -3
  161. package/docs/discoverability/mcp.mdx +4 -0
  162. package/docs/discoverability/metadata.mdx +3 -2
  163. package/docs/discoverability/open-graph.mdx +6 -4
  164. package/docs/discoverability/rss.mdx +3 -1
  165. package/docs/index.mdx +2 -2
  166. package/docs/references/asyncapi.mdx +1 -1
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +3 -3
  169. package/package.json +1 -1
  170. package/skills/blume/SKILL.md +5 -5
  171. package/skills/blume-migrate/SKILL.md +6 -6
  172. package/skills/blume-migrate/references/docusaurus.md +6 -6
  173. package/skills/blume-migrate/references/mintlify.md +3 -3
  174. package/skills/blume-migrate/references/monorepo.md +1 -1
  175. package/skills/blume-migrate/references/nextra.md +1 -1
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +4 -4
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/api/paths.ts +1 -1
  182. package/src/ai/api/spec.ts +15 -2
  183. package/src/ai/api-catalog.ts +7 -1
  184. package/src/ai/ask-context.ts +21 -9
  185. package/src/ai/ask-data.ts +28 -14
  186. package/src/ai/ask.ts +84 -71
  187. package/src/ai/component-markdown.ts +43 -37
  188. package/src/ai/cors.ts +3 -3
  189. package/src/ai/index.ts +16 -16
  190. package/src/ai/link-headers.ts +8 -3
  191. package/src/ai/llms.ts +4 -3
  192. package/src/ai/markdown.ts +23 -11
  193. package/src/ai/mcp/server.ts +78 -7
  194. package/src/ai/openapi-components.ts +1 -1
  195. package/src/ai/relative-links.ts +78 -10
  196. package/src/ai/serializers.ts +1 -1
  197. package/src/ai/static-expression.ts +416 -0
  198. package/src/ai/visibility.ts +46 -15
  199. package/src/analytics/databuddy.ts +67 -0
  200. package/src/analytics/head.ts +4 -0
  201. package/src/analytics/index.ts +2 -0
  202. package/src/analytics/posthog.ts +21 -3
  203. package/src/analytics/schema.ts +2 -0
  204. package/src/astro/generate.ts +90 -45
  205. package/src/astro/module-types.ts +1 -1
  206. package/src/astro/runtime-deps.ts +6 -6
  207. package/src/astro/templates.ts +87 -49
  208. package/src/audit/catalog.ts +2 -2
  209. package/src/audit/checks/assets.ts +25 -5
  210. package/src/audit/checks/content.ts +20 -2
  211. package/src/audit/checks/i18n.ts +46 -8
  212. package/src/audit/checks/indexability.ts +39 -19
  213. package/src/audit/checks/links.ts +14 -0
  214. package/src/audit/checks/llms.ts +6 -3
  215. package/src/audit/checks/network.ts +3 -1
  216. package/src/audit/checks/og-image.ts +10 -0
  217. package/src/audit/checks/robots.ts +6 -1
  218. package/src/audit/checks/sitemap.ts +56 -31
  219. package/src/audit/checks/social.ts +21 -2
  220. package/src/audit/crawl.ts +88 -15
  221. package/src/audit/report.ts +54 -17
  222. package/src/audit/run.ts +1 -0
  223. package/src/audit/types.ts +18 -0
  224. package/src/audit/url.ts +37 -6
  225. package/src/blume-modules.d.ts +2 -2
  226. package/src/cli/build-failure.ts +50 -0
  227. package/src/cli/commands/audit.ts +8 -1
  228. package/src/cli/commands/build.ts +19 -5
  229. package/src/cli/commands/check.ts +2 -0
  230. package/src/cli/commands/doctor.ts +7 -5
  231. package/src/cli/commands/eval.ts +4 -10
  232. package/src/cli/commands/init.ts +15 -5
  233. package/src/cli/commands/migrate.ts +2 -2
  234. package/src/cli/commands/preview.ts +15 -0
  235. package/src/cli/commands/sync.ts +2 -0
  236. package/src/cli/commands/translate.ts +12 -9
  237. package/src/cli/commands/upgrade.ts +13 -2
  238. package/src/cli/eject-scripts.ts +32 -7
  239. package/src/cli/env.ts +12 -1
  240. package/src/cli/init/scaffold.ts +117 -11
  241. package/src/cli/init/starter-spec.ts +235 -0
  242. package/src/cli/required-secrets.ts +3 -3
  243. package/src/components/content/AccordionItem.astro +26 -22
  244. package/src/components/content/Badge.astro +2 -9
  245. package/src/components/content/Card.astro +2 -2
  246. package/src/components/content/Component.astro +9 -3
  247. package/src/components/content/Expandable.astro +5 -1
  248. package/src/components/content/Frame.astro +2 -7
  249. package/src/components/content/GithubInfo.astro +12 -2
  250. package/src/components/content/Prompt.astro +2 -7
  251. package/src/components/content/Tabs.astro +54 -5
  252. package/src/components/content/Tile.astro +1 -1
  253. package/src/components/content/Tooltip.astro +69 -7
  254. package/src/components/content/Tree.astro +7 -2
  255. package/src/components/content/TypeTable.astro +10 -5
  256. package/src/components/content/Update.astro +8 -2
  257. package/src/components/content/auto-type-table.ts +4 -1
  258. package/src/components/content/badge-color.ts +17 -0
  259. package/src/components/content/base-href.ts +18 -3
  260. package/src/components/content/inline-markdown.ts +27 -7
  261. package/src/components/copy-feedback.ts +36 -9
  262. package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
  263. package/src/components/islands/{ask-ai.tsx → assistant.tsx} +39 -25
  264. package/src/components/islands/hooks.ts +21 -14
  265. package/src/components/islands/webmcp.ts +12 -8
  266. package/src/components/layout/Banner.astro +23 -4
  267. package/src/components/layout/Header.astro +32 -19
  268. package/src/components/layout/Logo.astro +5 -0
  269. package/src/components/layout/NavSelector.astro +6 -1
  270. package/src/components/layout/NavTabMenu.astro +133 -0
  271. package/src/components/layout/NavTree.astro +15 -6
  272. package/src/components/layout/NavTreeCache.astro +5 -2
  273. package/src/components/layout/NavTreeScript.astro +45 -5
  274. package/src/components/layout/PageLayout.astro +28 -14
  275. package/src/components/layout/Pagination.astro +7 -7
  276. package/src/components/layout/ReferenceLayout.astro +5 -2
  277. package/src/components/layout/RootLayout.astro +27 -14
  278. package/src/components/layout/Search.astro +18 -14
  279. package/src/components/layout/analytics-client.ts +3 -1
  280. package/src/components/layout/drawer-inert.ts +1 -1
  281. package/src/components/openapi/GraphqlType.astro +11 -3
  282. package/src/components/openapi/MessageComposer.astro +1 -1
  283. package/src/components/openapi/Operation.astro +21 -5
  284. package/src/components/openapi/PanelTabs.astro +4 -1
  285. package/src/components/openapi/Playground.astro +8 -2
  286. package/src/components/openapi/RequestPanel.astro +9 -5
  287. package/src/components/openapi/SchemaProperty.astro +11 -33
  288. package/src/components/openapi/SchemaTable.astro +27 -52
  289. package/src/components/openapi/description.ts +2 -2
  290. package/src/components/openapi/helpers.ts +90 -12
  291. package/src/components/openapi/message-composer.ts +8 -0
  292. package/src/components/openapi/message-model.ts +12 -2
  293. package/src/components/openapi/message.ts +4 -1
  294. package/src/components/openapi/operation-model.ts +40 -8
  295. package/src/components/openapi/panel.ts +29 -5
  296. package/src/components/openapi/playground-client.ts +37 -7
  297. package/src/components/openapi/playground-schema.ts +25 -6
  298. package/src/components/openapi/request.ts +2 -0
  299. package/src/components/openapi/schema-tree.ts +203 -0
  300. package/src/components/openapi/snippets.ts +30 -15
  301. package/src/components/openapi/validate-json.ts +1 -1
  302. package/src/core/base-path.ts +15 -0
  303. package/src/core/code-fences.ts +1 -1
  304. package/src/core/config-input.ts +23 -23
  305. package/src/core/config.ts +21 -7
  306. package/src/core/data.ts +3 -3
  307. package/src/core/diagnostics.ts +217 -30
  308. package/src/core/graph.ts +120 -11
  309. package/src/core/i18n-ui.ts +74 -11
  310. package/src/core/i18n.ts +13 -2
  311. package/src/core/links.ts +7 -4
  312. package/src/core/load-module.ts +20 -0
  313. package/src/core/locale-links.ts +17 -17
  314. package/src/core/manifest.ts +3 -2
  315. package/src/core/meta.ts +69 -6
  316. package/src/core/nav-diagnostics.ts +56 -1
  317. package/src/core/navigation.ts +245 -76
  318. package/src/core/ordering-prefix.ts +27 -0
  319. package/src/core/project-graph.ts +23 -1
  320. package/src/core/request-body.ts +1 -1
  321. package/src/core/safe-href.ts +53 -1
  322. package/src/core/safe-links.ts +11 -2
  323. package/src/core/schema.ts +95 -34
  324. package/src/core/server-features.ts +2 -2
  325. package/src/core/sources/assets.ts +83 -41
  326. package/src/core/sources/contentful-rich-text.ts +3 -2
  327. package/src/core/sources/contentful.ts +25 -12
  328. package/src/core/sources/filesystem.ts +5 -1
  329. package/src/core/sources/github-releases.ts +106 -5
  330. package/src/core/sources/lexical.ts +9 -4
  331. package/src/core/sources/lower.ts +79 -26
  332. package/src/core/sources/mdx-remote.ts +1 -0
  333. package/src/core/sources/normalize.ts +132 -30
  334. package/src/core/sources/notion.ts +26 -10
  335. package/src/core/sources/obsidian.ts +17 -3
  336. package/src/core/sources/payload.ts +1 -0
  337. package/src/core/sources/portable-text.ts +63 -44
  338. package/src/core/sources/remote.ts +18 -2
  339. package/src/core/sources/resolve.ts +52 -33
  340. package/src/core/sources/sanity.ts +1 -0
  341. package/src/core/sources/strapi-blocks.ts +9 -4
  342. package/src/core/sources/strapi.ts +1 -0
  343. package/src/core/sources/types.ts +31 -0
  344. package/src/core/types.ts +9 -0
  345. package/src/core/ui-packs/ar.ts +17 -5
  346. package/src/core/ui-packs/bg.ts +17 -5
  347. package/src/core/ui-packs/bn.ts +17 -5
  348. package/src/core/ui-packs/ca.ts +17 -5
  349. package/src/core/ui-packs/cs.ts +17 -5
  350. package/src/core/ui-packs/da.ts +17 -5
  351. package/src/core/ui-packs/de.ts +17 -5
  352. package/src/core/ui-packs/el.ts +17 -5
  353. package/src/core/ui-packs/es.ts +17 -5
  354. package/src/core/ui-packs/fa.ts +17 -5
  355. package/src/core/ui-packs/fi.ts +17 -5
  356. package/src/core/ui-packs/fr.ts +17 -5
  357. package/src/core/ui-packs/he.ts +17 -5
  358. package/src/core/ui-packs/hi.ts +17 -5
  359. package/src/core/ui-packs/hr.ts +17 -5
  360. package/src/core/ui-packs/hu.ts +17 -5
  361. package/src/core/ui-packs/id.ts +17 -5
  362. package/src/core/ui-packs/it.ts +17 -5
  363. package/src/core/ui-packs/ja.ts +17 -5
  364. package/src/core/ui-packs/ko.ts +17 -5
  365. package/src/core/ui-packs/nl.ts +17 -5
  366. package/src/core/ui-packs/no.ts +17 -5
  367. package/src/core/ui-packs/pl.ts +17 -5
  368. package/src/core/ui-packs/pt-br.ts +17 -5
  369. package/src/core/ui-packs/pt.ts +17 -5
  370. package/src/core/ui-packs/ro.ts +17 -5
  371. package/src/core/ui-packs/ru.ts +17 -5
  372. package/src/core/ui-packs/sk.ts +17 -5
  373. package/src/core/ui-packs/sr.ts +17 -5
  374. package/src/core/ui-packs/sv.ts +17 -5
  375. package/src/core/ui-packs/th.ts +17 -5
  376. package/src/core/ui-packs/tr.ts +17 -5
  377. package/src/core/ui-packs/uk.ts +17 -5
  378. package/src/core/ui-packs/vi.ts +17 -5
  379. package/src/core/ui-packs/zh-tw.ts +17 -5
  380. package/src/core/ui-packs/zh.ts +17 -5
  381. package/src/core/unrecognized-keys.ts +1 -1
  382. package/src/core/version-cut.ts +17 -2
  383. package/src/core/versions.ts +4 -1
  384. package/src/deploy/adapters/node.ts +5 -2
  385. package/src/deploy/adapters/registry.ts +2 -1
  386. package/src/deploy/adapters/types.ts +13 -1
  387. package/src/deploy/artifacts.ts +25 -5
  388. package/src/deploy/cloudflare-negotiation.ts +23 -4
  389. package/src/deploy/headers.ts +67 -51
  390. package/src/deploy/node-headers.ts +148 -27
  391. package/src/deploy/platforms/cloudflare.ts +1 -0
  392. package/src/deploy/platforms/netlify.ts +82 -5
  393. package/src/deploy/platforms/node.ts +7 -5
  394. package/src/deploy/platforms/static.ts +1 -0
  395. package/src/deploy/platforms/types.ts +7 -0
  396. package/src/deploy/platforms/vercel.ts +9 -3
  397. package/src/deploy/redirects.ts +14 -3
  398. package/src/deploy/vercel-negotiation.ts +35 -3
  399. package/src/eval/agents.ts +10 -2
  400. package/src/eval/run.ts +25 -0
  401. package/src/markdown/base-links.ts +55 -8
  402. package/src/markdown/index.ts +6 -3
  403. package/src/markdown/relative-links.ts +3 -23
  404. package/src/markdown/route-snapshot.ts +37 -0
  405. package/src/og/card.ts +20 -4
  406. package/src/og/derive.ts +145 -4
  407. package/src/openapi/graphql-build.ts +28 -2
  408. package/src/openapi/model.ts +77 -13
  409. package/src/openapi/render-mdx.ts +10 -1
  410. package/src/registry/eject.ts +135 -37
  411. package/src/search/documents.ts +24 -3
  412. package/src/search/orama-index.ts +1 -1
  413. package/src/search/sync/algolia.ts +36 -2
  414. package/src/sources/registry.ts +5 -0
  415. package/src/theme/entry.ts +11 -4
  416. package/src/translate/agents.ts +6 -1
  417. package/src/translate/ledger.ts +26 -3
  418. package/src/translate/meta.ts +11 -3
  419. package/src/translate/report.ts +1 -1
  420. package/src/translate/run.ts +11 -5
  421. package/src/translate/validate.ts +10 -1
  422. package/src/translate/work-list.ts +36 -3
  423. package/src/upgrade/upgrade.ts +37 -5
  424. package/dist/cli/chunk-1w8dp3qb.js.map +0 -10
  425. package/dist/cli/chunk-5a2z0198.js.map +0 -10
  426. package/dist/cli/chunk-6k8vp3ta.js.map +0 -19
  427. package/dist/cli/chunk-79njf86q.js.map +0 -11
  428. package/dist/cli/chunk-a9kptbw5.js.map +0 -14
  429. package/dist/cli/chunk-abh8yjkn.js +0 -31
  430. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  431. package/dist/cli/chunk-bctazmbk.js.map +0 -10
  432. package/dist/cli/chunk-beat36xx.js.map +0 -10
  433. package/dist/cli/chunk-d1tadaw7.js.map +0 -10
  434. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  435. package/dist/cli/chunk-j8mw0za6.js.map +0 -35
  436. package/dist/cli/chunk-mnqj32sj.js.map +0 -12
  437. package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
  438. package/dist/cli/chunk-nk3ts2xk.js +0 -51
  439. package/dist/cli/chunk-nk3ts2xk.js.map +0 -10
  440. package/dist/cli/chunk-sqn5t4q0.js.map +0 -10
  441. package/dist/cli/chunk-tzne8qfq.js.map +0 -10
  442. package/dist/cli/chunk-xaz13gwg.js.map +0 -182
  443. package/dist/cli/chunk-z01ze5c1.js.map +0 -10
  444. package/dist/cli/chunk-z1f5arsg.js.map +0 -36
  445. package/dist/cli/chunk-zg2gtj10.js.map +0 -35
  446. /package/dist/cli/{chunk-f2972sbt.js.map → chunk-00gs3wqs.js.map} +0 -0
  447. /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-bbnwccaz.js.map} +0 -0
  448. /package/dist/cli/{chunk-zxccj738.js.map → chunk-esh98wmb.js.map} +0 -0
  449. /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-fsmrqk8a.js.map} +0 -0
  450. /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-g698a744.js.map} +0 -0
  451. /package/dist/cli/{chunk-41za066z.js.map → chunk-h2ez8dzb.js.map} +0 -0
  452. /package/dist/cli/{chunk-by2290sx.js.map → chunk-h7k3nq3v.js.map} +0 -0
  453. /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-n1yg3tj3.js.map} +0 -0
  454. /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-wdrt2k2v.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import type { UIStrings } from "../../core/i18n-ui.ts";
3
3
  import { resolveIcon } from "../../theme/icons.ts";
4
- import AskAI from "./ask-ai.tsx";
4
+ import Assistant from "./assistant.tsx";
5
5
 
6
6
  interface Suggestion {
7
7
  icon?: string;
@@ -10,7 +10,7 @@ interface Suggestion {
10
10
 
11
11
  interface Props {
12
12
  endpoint?: string;
13
- strings?: UIStrings["ask"];
13
+ strings?: UIStrings["assistant"];
14
14
  suggestions?: Suggestion[];
15
15
  }
16
16
 
@@ -48,19 +48,19 @@ const icons = {
48
48
  the draft input, and the open panel — across client-router navigations
49
49
  instead of remounting it empty on every page. The island re-portals its
50
50
  panel and re-stamps the body push attribute itself on astro:after-swap
51
- (see ask-ai.tsx), since the swap replaces <body> wholesale. */}
52
- <AskAI
51
+ (see assistant.tsx), since the swap replaces <body> wholesale. */}
52
+ <Assistant
53
53
  client:load
54
54
  endpoint={endpoint}
55
55
  icons={icons}
56
56
  strings={strings}
57
57
  suggestions={items}
58
- transition:persist="blume-ask"
58
+ transition:persist="blume-assistant"
59
59
  />
60
60
 
61
61
  <style is:global>
62
62
  :root {
63
- --blume-ask-width: min(30rem, 100vw);
63
+ --blume-assistant-width: min(30rem, 100vw);
64
64
  }
65
65
 
66
66
  /* Desktop docks the panel and shrinks the page to fit, like vercel.com/docs.
@@ -70,8 +70,8 @@ const icons = {
70
70
  transition: padding-inline-end 0.2s ease-out;
71
71
  }
72
72
 
73
- body[data-blume-ask="open"] {
74
- padding-inline-end: var(--blume-ask-width);
73
+ body[data-blume-assistant="open"] {
74
+ padding-inline-end: var(--blume-assistant-width);
75
75
  }
76
76
  }
77
77
 
@@ -81,11 +81,11 @@ const icons = {
81
81
  changelog index) is single-column, and forcing the sidebar tracks onto it
82
82
  would squeeze its only item — <main> — into the 17.5rem track. */
83
83
  @media (min-width: 1280px) {
84
- body[data-blume-ask="open"] [data-blume-toc] {
84
+ body[data-blume-assistant="open"] [data-blume-toc] {
85
85
  display: none;
86
86
  }
87
87
 
88
- body[data-blume-ask="open"] [data-blume-doc-grid]:has([data-blume-toc]) {
88
+ body[data-blume-assistant="open"] [data-blume-doc-grid]:has([data-blume-toc]) {
89
89
  grid-template-columns: 17.5rem minmax(0, 1fr);
90
90
  }
91
91
  }
@@ -7,7 +7,7 @@ import { createPortal } from "react-dom";
7
7
  import type { UIStrings } from "../../core/i18n-ui.ts";
8
8
  import { copyText } from "../copy-feedback.ts";
9
9
  import { joinBase, prefixBase } from "./base-path.ts";
10
- import { useAskAI } from "./hooks.ts";
10
+ import { useAssistant } from "./hooks.ts";
11
11
 
12
12
  /** A resolved empty-state prompt; `icon` is ready-to-inline SVG (or null). */
13
13
  interface Suggestion {
@@ -16,10 +16,10 @@ interface Suggestion {
16
16
  }
17
17
 
18
18
  /**
19
- * The panel's chrome glyphs, resolved server-side in `AskAI.astro` and passed in
19
+ * The panel's chrome glyphs, resolved server-side in `Assistant.astro` and passed in
20
20
  * as ready-to-inline Lucide bodies so this client island ships no icon data.
21
21
  */
22
- interface AskIcons {
22
+ interface AssistantIcons {
23
23
  arrowUp: string;
24
24
  chat: string;
25
25
  clear: string;
@@ -29,7 +29,7 @@ interface AskIcons {
29
29
 
30
30
  // Empty bodies so the island still renders (iconless) if instantiated without
31
31
  // the Astro wrapper that resolves the real Lucide glyphs.
32
- const EMPTY_ICONS: AskIcons = {
32
+ const EMPTY_ICONS: AssistantIcons = {
33
33
  arrowUp: "",
34
34
  chat: "",
35
35
  clear: "",
@@ -38,7 +38,7 @@ const EMPTY_ICONS: AskIcons = {
38
38
  };
39
39
 
40
40
  // English fallback so the island renders even if no dictionary is passed.
41
- const DEFAULT_ASK: UIStrings["ask"] = {
41
+ const DEFAULT_ASK: UIStrings["assistant"] = {
42
42
  ai: "AI",
43
43
  clear: "Clear conversation",
44
44
  close: "Close",
@@ -49,7 +49,7 @@ const DEFAULT_ASK: UIStrings["ask"] = {
49
49
  placeholder: "Ask a question…",
50
50
  send: "Send",
51
51
  tip: "Tip: You can open and close chat with",
52
- title: "Ask AI",
52
+ title: "Assistant",
53
53
  you: "You",
54
54
  };
55
55
 
@@ -77,9 +77,18 @@ const markdown = new Marked({
77
77
  const renderMarkdown = (content: string): string =>
78
78
  DOMPurify.sanitize(markdown.parse(content, { async: false }));
79
79
 
80
- const Glyph = ({ path, size = 16 }: { path: string; size?: number }) => (
80
+ const Glyph = ({
81
+ className,
82
+ path,
83
+ size = 16,
84
+ }: {
85
+ className?: string;
86
+ path: string;
87
+ size?: number;
88
+ }) => (
81
89
  <svg
82
90
  aria-hidden="true"
91
+ className={className}
83
92
  // `path` is a trusted, server-resolved Lucide glyph body (inline SVG),
84
93
  // not user content; it must be injected as markup to render the icon.
85
94
  // oxlint-disable-next-line react/no-danger -- trusted server-resolved inline SVG glyph
@@ -130,15 +139,15 @@ const ICON_BUTTON_CLASS =
130
139
  const ANSWER_CLASS =
131
140
  "prose prose-sm max-w-none text-foreground [&_a]:inline-flex [&_a]:items-center [&_a]:gap-1 [&_a]:rounded-full [&_a]:bg-muted [&_a]:px-2 [&_a]:py-1 [&_a]:align-middle [&_a]:font-medium [&_a]:text-[0.7rem] [&_a]:leading-none [&_a]:text-muted-foreground! [&_a]:no-underline! [&_a:hover]:text-foreground!";
132
141
 
133
- const AskAI = ({
142
+ const Assistant = ({
134
143
  endpoint = DEFAULT_ASK_ENDPOINT,
135
144
  icons = EMPTY_ICONS,
136
145
  strings,
137
146
  suggestions = EMPTY_SUGGESTIONS,
138
147
  }: {
139
148
  endpoint?: string;
140
- icons?: AskIcons;
141
- strings?: UIStrings["ask"];
149
+ icons?: AssistantIcons;
150
+ strings?: UIStrings["assistant"];
142
151
  suggestions?: Suggestion[];
143
152
  }) => {
144
153
  // Merge per key (not `strings ?? …`) so a dictionary from a stale snapshot
@@ -147,14 +156,14 @@ const AskAI = ({
147
156
  const [open, setOpen] = useState(false);
148
157
  const [input, setInput] = useState("");
149
158
  // The streaming client — request shaping, optimistic assistant bubble,
150
- // stale-stream/abort guards, error-body handling — is the public useAskAI
159
+ // stale-stream/abort guards, error-body handling — is the public useAssistant
151
160
  // hook, so the built-in panel and custom UIs share one implementation.
152
161
  const {
153
162
  ask,
154
163
  loading: busy,
155
164
  messages,
156
165
  reset,
157
- } = useAskAI({ endpoint, errorMessage: t.error });
166
+ } = useAssistant({ endpoint, errorMessage: t.error });
158
167
  const inputRef = useRef<HTMLTextAreaElement>(null);
159
168
  const scrollRef = useRef<HTMLDivElement>(null);
160
169
  const triggerRef = useRef<HTMLButtonElement>(null);
@@ -167,7 +176,7 @@ const AskAI = ({
167
176
  // Null until mount (guards SSR), then refreshed on every client-router swap:
168
177
  // the island rides across navigations via transition:persist, but each swap
169
178
  // installs a NEW <body>, discarding the portaled panel with the old one and
170
- // resetting the `data-blume-ask` push attribute to the incoming page's
179
+ // resetting the `data-blume-assistant` push attribute to the incoming page's
171
180
  // server-rendered set. Reading document.body inline in render would NOT
172
181
  // recover from that — it isn't a reactive value, so the memoized portal
173
182
  // keeps its stale (detached) container. State identity is what re-anchors
@@ -183,10 +192,10 @@ const AskAI = ({
183
192
  return () => document.removeEventListener("astro:after-swap", onSwap);
184
193
  }, []);
185
194
 
186
- // The search modal forwards its query so "Ask AI: <query>" carries straight in.
195
+ // The search modal forwards its query so "Assistant: <query>" carries straight in.
187
196
  useEffect(() => {
188
197
  const handler = (event: Event) => {
189
- // SAFETY: `blume:open-ask-ai` is only ever dispatched as a CustomEvent
198
+ // SAFETY: `blume:open-assistant` is only ever dispatched as a CustomEvent
190
199
  // whose optional detail carries the search query.
191
200
  const query = (event as CustomEvent<{ query?: string }>).detail?.query;
192
201
  if (query) {
@@ -194,8 +203,8 @@ const AskAI = ({
194
203
  }
195
204
  setOpen(true);
196
205
  };
197
- window.addEventListener("blume:open-ask-ai", handler);
198
- return () => window.removeEventListener("blume:open-ask-ai", handler);
206
+ window.addEventListener("blume:open-assistant", handler);
207
+ return () => window.removeEventListener("blume:open-assistant", handler);
199
208
  }, []);
200
209
 
201
210
  // ⌘I / Ctrl+I toggles the panel; Escape closes it. Shift/Alt chords are
@@ -226,17 +235,17 @@ const AskAI = ({
226
235
  return () => window.removeEventListener("keydown", onKey);
227
236
  }, [open]);
228
237
 
229
- // Drive the desktop content push from a body attribute (see AskAI.astro CSS).
238
+ // Drive the desktop content push from a body attribute (see Assistant.astro CSS).
230
239
  useEffect(() => {
231
240
  if (open) {
232
241
  returnFocusRef.current =
233
242
  document.activeElement instanceof HTMLElement
234
243
  ? document.activeElement
235
244
  : null;
236
- document.body.dataset.blumeAsk = "open";
245
+ document.body.dataset.blumeAssistant = "open";
237
246
  inputRef.current?.focus();
238
247
  } else {
239
- delete document.body.dataset.blumeAsk;
248
+ delete document.body.dataset.blumeAssistant;
240
249
  // Return focus to the element that opened the panel (or the trigger when
241
250
  // it's gone), so closing doesn't strand keyboard focus in an inert tree.
242
251
  // `returnFocusRef` is only set on open, so initial mount is a no-op.
@@ -249,7 +258,7 @@ const AskAI = ({
249
258
  }
250
259
  }
251
260
  return () => {
252
- delete document.body.dataset.blumeAsk;
261
+ delete document.body.dataset.blumeAssistant;
253
262
  };
254
263
  }, [open]);
255
264
 
@@ -261,7 +270,7 @@ const AskAI = ({
261
270
  // document.body (not portalTarget) so the compiler doesn't flag a state
262
271
  // mutation; by the time this runs for a swap, they are the same element.
263
272
  if (open && portalTarget) {
264
- document.body.dataset.blumeAsk = "open";
273
+ document.body.dataset.blumeAssistant = "open";
265
274
  }
266
275
  // oxlint-disable-next-line react/exhaustive-effect-dependencies -- see above
267
276
  }, [open, portalTarget]);
@@ -383,11 +392,14 @@ const AskAI = ({
383
392
  <aside
384
393
  aria-hidden={open ? undefined : "true"}
385
394
  aria-label={t.title}
395
+ // Print hides the chrome by this marker, not by tag: content renders
396
+ // <aside> too (Callout, Panel), and that must print.
397
+ data-blume-assistant-panel=""
386
398
  ref={panelRef}
387
399
  // The closed panel is only translated off-screen; `inert` drops its
388
400
  // buttons/textarea from the tab order and the accessibility tree.
389
401
  inert={!open}
390
- className={`border-border bg-background fixed inset-y-0 end-0 z-[60] flex w-[var(--blume-ask-width)] flex-col border-s shadow-2xl transition-transform duration-200 ease-out ${
402
+ className={`border-border bg-background fixed inset-y-0 end-0 z-[60] flex w-[var(--blume-assistant-width)] flex-col border-s shadow-2xl transition-transform duration-200 ease-out ${
391
403
  open ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"
392
404
  }`}
393
405
  >
@@ -418,7 +430,9 @@ const AskAI = ({
418
430
  onClick={() => setOpen(false)}
419
431
  type="button"
420
432
  >
421
- <Glyph path={icons.close} size={18} />
433
+ {/* Chevrons toward the inline end, where the panel docks: they
434
+ mirror with the panel in a right-to-left locale. */}
435
+ <Glyph className="rtl:-scale-x-100" path={icons.close} size={18} />
422
436
  </button>
423
437
  </div>
424
438
  </header>
@@ -539,4 +553,4 @@ const AskAI = ({
539
553
  );
540
554
  };
541
555
 
542
- export default AskAI;
556
+ export default Assistant;
@@ -12,7 +12,7 @@ import { joinBase, stripBase } from "./base-path.ts";
12
12
  * project data can't come through context. Instead the layout serializes a small
13
13
  * snapshot into a `<script type="application/json" id="blume-client-data">` tag,
14
14
  * and {@link useBlume}/{@link usePage} read it after mount. {@link useSearch} and
15
- * {@link useAskAI} wrap the generated search client and the Ask AI endpoint.
15
+ * {@link useAssistant} wrap the generated search client and the assistant endpoint.
16
16
  *
17
17
  * Import them from `blume/hooks`:
18
18
  *
@@ -131,14 +131,14 @@ export const useSearch = (): UseSearch => {
131
131
  return { loading, results, search };
132
132
  };
133
133
 
134
- /** A single Ask AI chat message. */
134
+ /** A single assistant chat message. */
135
135
  export interface AskMessage {
136
136
  content: string;
137
137
  role: "assistant" | "user";
138
138
  }
139
139
 
140
- /** State + actions returned by {@link useAskAI}. */
141
- export interface UseAskAI {
140
+ /** State + actions returned by {@link useAssistant}. */
141
+ export interface UseAssistant {
142
142
  ask: (question: string) => Promise<void>;
143
143
  loading: boolean;
144
144
  messages: AskMessage[];
@@ -147,8 +147,8 @@ export interface UseAskAI {
147
147
 
148
148
  const DEFAULT_ASK_ENDPOINT = joinBase(import.meta.env.BASE_URL, "api/ask");
149
149
 
150
- export interface UseAskAIOptions {
151
- /** Existing Ask AI endpoint; defaults to Blume's generated `/api/ask`. */
150
+ export interface UseAssistantOptions {
151
+ /** Existing assistant endpoint; defaults to Blume's generated `/api/ask`. */
152
152
  endpoint?: string;
153
153
  /**
154
154
  * Shown as the assistant's answer when the request fails or throws.
@@ -161,11 +161,11 @@ export interface UseAskAIOptions {
161
161
  /** Shown as the assistant's answer when the request fails or throws. */
162
162
  const ASK_ERROR = "Something went wrong answering that. Please try again.";
163
163
 
164
- /** How the generated Ask AI route opens its missing-credential notice. */
165
- const NOT_CONFIGURED = /^Ask AI is not configured/u;
164
+ /** How the assistant's generated route opens its missing-credential notice. */
165
+ const NOT_CONFIGURED = /^The assistant is not configured/u;
166
166
 
167
167
  /**
168
- * The route's own explanation of a 503 — the generated route's "Ask AI is not
168
+ * The route's own explanation of a 503 — the generated route's "The assistant is not
169
169
  * configured: set AI_GATEWAY_API_KEY." — when the response is that notice, or
170
170
  * null. It names only the variable to set, never a value, so it's safe to
171
171
  * show, and it tells a site owner trying a fresh deploy what's missing instead
@@ -188,10 +188,12 @@ const currentPath = (): string =>
188
188
  stripBase(import.meta.env.BASE_URL, window.location.pathname);
189
189
 
190
190
  /**
191
- * Stream answers from the Ask AI endpoint. Mirrors the built-in Ask AI island so
191
+ * Stream answers from the assistant endpoint. Mirrors the built-in assistant island so
192
192
  * a custom chat UI shares the same grounded, page-aware backend.
193
193
  */
194
- export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
194
+ export const useAssistant = (
195
+ options: UseAssistantOptions = {}
196
+ ): UseAssistant => {
195
197
  const endpoint = options.endpoint ?? DEFAULT_ASK_ENDPOINT;
196
198
  const errorMessage = options.errorMessage ?? ASK_ERROR;
197
199
  const [messages, setMessages] = useState<AskMessage[]>([]);
@@ -208,7 +210,7 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
208
210
  // preserves a stable `ask` identity for consumers that depend on it. With the
209
211
  // compiler on it's redundant but inert.
210
212
  // oxlint-disable-next-line react-doctor/react-compiler-no-manual-memoization -- see above
211
- const ask = useCallback<UseAskAI["ask"]>(
213
+ const ask = useCallback<UseAssistant["ask"]>(
212
214
  async (question) => {
213
215
  const trimmed = question.trim();
214
216
  if (!trimmed || loading) {
@@ -306,12 +308,17 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
306
308
  }
307
309
  }
308
310
  if (live()) {
309
- // A 200 with nothing in it (no body, an empty stream) leaves the
310
- // reader a blank bubble — that is not an answer.
311
+ // A 200 with nothing in it (no body, an empty stream) is not an
312
+ // answer. It is also how a provider failure after the 200 arrives
313
+ // (a bad key, a rate limit, an unknown model): the route's text
314
+ // stream carries only text, so the error ends it empty. Show the
315
+ // error notice instead of leaving a pulsing blank bubble.
311
316
  if (assistant.content) {
312
317
  outcome("ask_answer", { chars: assistant.content.length });
313
318
  } else {
314
319
  outcome("ask_error", { status });
320
+ assistant.content = errorMessage;
321
+ setMessages([...history, { ...assistant }]);
315
322
  }
316
323
  }
317
324
  } catch {
@@ -1,3 +1,5 @@
1
+ import { unescape } from "html-escaper";
2
+
1
3
  import type { SearchFn } from "../layout/search/types.ts";
2
4
  import { joinBase, prefixBase } from "./base-path.ts";
3
5
 
@@ -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 {
@@ -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 && (
@@ -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
 
@@ -39,13 +40,13 @@ interface Props {
39
40
  */
40
41
  locale?: string;
41
42
  /**
42
- * Whether the search modal offers an "Ask AI" hand-off. Defaults to whether
43
- * Ask AI is configured, so no page has to pass it; a layout can still opt a
43
+ * Whether the search modal offers a hand-off to the assistant. Defaults to whether
44
+ * the assistant is configured, so no page has to pass it; a layout can still opt a
44
45
  * shell out explicitly.
45
46
  */
46
- askEnabled?: boolean;
47
- /** Localized Ask AI strings for the active locale. */
48
- askStrings?: UIStrings["ask"];
47
+ assistantEnabled?: boolean;
48
+ /** Localized assistant strings for the active locale. */
49
+ assistantStrings?: UIStrings["assistant"];
49
50
  // The mobile menu button toggles the docs sidebar drawer; custom pages
50
51
  // without a sidebar (e.g. a landing page) pass `false` to hide it.
51
52
  hasSidebar?: boolean;
@@ -91,11 +92,11 @@ const {
91
92
  route,
92
93
  searchEnabled,
93
94
  locale,
94
- // `config.ask` is null whenever Ask AI is off, so the header is the one place
95
+ // `config.assistant` is null whenever the assistant is off, so the header is the one place
95
96
  // that has to know — the Ask trigger below and the search modal's hand-off to
96
97
  // it both switch on this, and every page gets both for free.
97
- askEnabled = Boolean(data.config.ask),
98
- askStrings,
98
+ assistantEnabled = Boolean(data.config.assistant),
99
+ assistantStrings,
99
100
  hasSidebar = true,
100
101
  hasDrawer = true,
101
102
  searchStrings,
@@ -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={withBase(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
  }
@@ -250,7 +263,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
250
263
  {
251
264
  searchEnabled && (
252
265
  <SearchSlot
253
- askEnabled={askEnabled}
266
+ assistantEnabled={assistantEnabled}
254
267
  locale={searchLocale}
255
268
  navigation={navigation}
256
269
  popularPages={data.config.search.popular}
@@ -314,7 +327,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
314
327
  <span class="inline-flex dark:hidden"><Icon name="sun" size={18} /></span>
315
328
  <span class="hidden dark:inline-flex"><Icon name="moon" size={18} /></span>
316
329
  </button>
317
- {askEnabled && <Ask strings={askStrings} />}
330
+ {assistantEnabled && <Ask strings={assistantStrings} />}
318
331
  </div>
319
332
  </header>
320
333
  <script is:inline set:html={clickScript} />
@@ -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
  >
@@ -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} />}