blume 1.7.2 → 2.0.0

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 (677) hide show
  1. package/AGENTS.md +19 -0
  2. package/CHANGELOG.md +227 -0
  3. package/README.md +34 -21
  4. package/dist/cli/chunk-11j0384y.js +148 -0
  5. package/dist/cli/chunk-11j0384y.js.map +10 -0
  6. package/dist/cli/{chunk-xhtpx3ff.js → chunk-1w8dp3qb.js} +17 -16
  7. package/dist/cli/{chunk-xhtpx3ff.js.map → chunk-1w8dp3qb.js.map} +3 -3
  8. package/dist/cli/chunk-2q1dwty4.js +75 -0
  9. package/dist/cli/chunk-2q1dwty4.js.map +11 -0
  10. package/dist/cli/chunk-41za066z.js +122 -0
  11. package/dist/cli/chunk-41za066z.js.map +11 -0
  12. package/dist/cli/chunk-5a2z0198.js +133 -0
  13. package/dist/cli/chunk-5a2z0198.js.map +10 -0
  14. package/dist/cli/{chunk-9he6crym.js → chunk-6k8vp3ta.js} +25 -13
  15. package/dist/cli/{chunk-9he6crym.js.map → chunk-6k8vp3ta.js.map} +4 -4
  16. package/dist/cli/{chunk-mt76t7dj.js → chunk-79njf86q.js} +133 -54
  17. package/dist/cli/chunk-79njf86q.js.map +11 -0
  18. package/dist/cli/{chunk-3w7b2vcx.js → chunk-7ez8ny0t.js} +2 -2
  19. package/dist/cli/{chunk-688e0dde.js → chunk-88cpgt6h.js} +1 -1
  20. package/dist/cli/chunk-a9kptbw5.js +361 -0
  21. package/dist/cli/chunk-a9kptbw5.js.map +14 -0
  22. package/dist/cli/chunk-abh8yjkn.js +31 -0
  23. package/dist/cli/chunk-abh8yjkn.js.map +10 -0
  24. package/dist/cli/chunk-b5aj94ah.js +91 -0
  25. package/dist/cli/chunk-b5aj94ah.js.map +10 -0
  26. package/dist/cli/{chunk-hs3gbh8p.js → chunk-bctazmbk.js} +9 -5
  27. package/dist/cli/chunk-bctazmbk.js.map +10 -0
  28. package/dist/cli/chunk-beat36xx.js +279 -0
  29. package/dist/cli/chunk-beat36xx.js.map +10 -0
  30. package/dist/cli/chunk-bnbmcwfb.js +145 -0
  31. package/dist/cli/chunk-bnbmcwfb.js.map +11 -0
  32. package/dist/cli/{chunk-9bkjd11x.js → chunk-bw22s759.js} +15 -5
  33. package/dist/cli/{chunk-9bkjd11x.js.map → chunk-bw22s759.js.map} +4 -4
  34. package/dist/cli/chunk-by2290sx.js +39 -0
  35. package/dist/cli/chunk-by2290sx.js.map +10 -0
  36. package/dist/cli/chunk-d1tadaw7.js +79 -0
  37. package/dist/cli/chunk-d1tadaw7.js.map +10 -0
  38. package/dist/cli/{chunk-t3tj0dgr.js → chunk-d80hr03s.js} +24 -19
  39. package/dist/cli/chunk-d80hr03s.js.map +15 -0
  40. package/dist/cli/chunk-ernrthtr.js +97 -0
  41. package/dist/cli/chunk-ernrthtr.js.map +10 -0
  42. package/dist/cli/chunk-f2972sbt.js +374 -0
  43. package/dist/cli/chunk-f2972sbt.js.map +10 -0
  44. package/dist/cli/{chunk-exeeb35e.js → chunk-f7t03s3g.js} +2 -2
  45. package/dist/cli/{chunk-2z47ypj8.js → chunk-fa25z98p.js} +16 -3
  46. package/dist/cli/chunk-fa25z98p.js.map +11 -0
  47. package/dist/cli/{chunk-12dxjqk7.js → chunk-fh5hj5jt.js} +44 -21
  48. package/dist/cli/chunk-fh5hj5jt.js.map +10 -0
  49. package/dist/cli/{chunk-8cd8tj54.js → chunk-j8mw0za6.js} +86 -57
  50. package/dist/cli/chunk-j8mw0za6.js.map +35 -0
  51. package/dist/cli/chunk-jts8mvcz.js +106 -0
  52. package/dist/cli/{chunk-2mzebbbz.js.map → chunk-jts8mvcz.js.map} +6 -4
  53. package/dist/cli/{chunk-ejjx8znq.js → chunk-mnqj32sj.js} +505 -536
  54. package/dist/cli/chunk-mnqj32sj.js.map +12 -0
  55. package/dist/cli/{chunk-196vjxp9.js → chunk-mwt1k8n7.js} +100 -372
  56. package/dist/cli/chunk-mwt1k8n7.js.map +10 -0
  57. package/dist/cli/chunk-nk3ts2xk.js +51 -0
  58. package/dist/cli/chunk-nk3ts2xk.js.map +10 -0
  59. package/dist/cli/{chunk-n9sra6sy.js → chunk-pat2zzwc.js} +10 -14
  60. package/dist/cli/{chunk-n9sra6sy.js.map → chunk-pat2zzwc.js.map} +2 -2
  61. package/dist/cli/chunk-pnnvybbk.js +176 -0
  62. package/dist/cli/chunk-pnnvybbk.js.map +11 -0
  63. package/dist/cli/{chunk-cvky9gb2.js → chunk-sqn5t4q0.js} +81 -87
  64. package/dist/cli/chunk-sqn5t4q0.js.map +10 -0
  65. package/dist/cli/{chunk-eevwt1sc.js → chunk-tzne8qfq.js} +15 -15
  66. package/dist/cli/{chunk-eevwt1sc.js.map → chunk-tzne8qfq.js.map} +1 -1
  67. package/dist/cli/chunk-xaz13gwg.js +12449 -0
  68. package/dist/cli/chunk-xaz13gwg.js.map +182 -0
  69. package/dist/cli/chunk-y3e45rc8.js +102 -0
  70. package/dist/cli/chunk-y3e45rc8.js.map +10 -0
  71. package/dist/cli/chunk-z01ze5c1.js +261 -0
  72. package/dist/cli/chunk-z01ze5c1.js.map +10 -0
  73. package/dist/cli/{chunk-5n7t497w.js → chunk-z1f5arsg.js} +247 -755
  74. package/dist/cli/chunk-z1f5arsg.js.map +36 -0
  75. package/dist/cli/{chunk-hdm2dkd2.js → chunk-zg2gtj10.js} +1086 -2596
  76. package/dist/cli/chunk-zg2gtj10.js.map +35 -0
  77. package/dist/cli/{chunk-ppfvdcd4.js → chunk-zxccj738.js} +1 -1
  78. package/dist/cli/index.js +214 -57
  79. package/dist/cli/index.js.map +8 -7
  80. package/dist/types/ai/agent-readability.d.ts +52 -0
  81. package/dist/types/ai/ai-catalog.d.ts +42 -0
  82. package/dist/types/ai/api/paths.d.ts +17 -0
  83. package/dist/types/ai/api-catalog.d.ts +18 -0
  84. package/dist/types/ai/ask.d.ts +368 -0
  85. package/dist/types/ai/changelog-markdown.d.ts +2 -0
  86. package/dist/types/ai/component-markdown.d.ts +2 -2
  87. package/dist/types/ai/index.d.ts +24 -0
  88. package/dist/types/ai/link-headers.d.ts +24 -0
  89. package/dist/types/ai/llms.d.ts +25 -0
  90. package/dist/types/ai/markdown.d.ts +45 -0
  91. package/dist/types/ai/mcp/discovery.d.ts +68 -0
  92. package/dist/types/ai/mcp/tools.d.ts +16 -0
  93. package/dist/types/ai/openapi-components.d.ts +43 -0
  94. package/dist/types/ai/relative-links.d.ts +26 -0
  95. package/dist/types/ai/serializers.d.ts +15 -0
  96. package/dist/types/ai/skills.d.ts +42 -0
  97. package/dist/types/ai/tar.d.ts +25 -0
  98. package/dist/types/ai/visibility.d.ts +17 -0
  99. package/dist/types/ai/web-bot-auth.d.ts +16 -0
  100. package/dist/types/analytics/adobe.d.ts +35 -0
  101. package/dist/types/analytics/amplitude.d.ts +50 -0
  102. package/dist/types/analytics/clarity.d.ts +31 -0
  103. package/dist/types/analytics/clearbit.d.ts +29 -0
  104. package/dist/types/analytics/cloudflare.d.ts +41 -0
  105. package/dist/types/analytics/fathom.d.ts +41 -0
  106. package/dist/types/analytics/google-analytics.d.ts +44 -0
  107. package/dist/types/analytics/google-tag-manager.d.ts +40 -0
  108. package/dist/types/analytics/head.d.ts +30 -0
  109. package/dist/types/analytics/heap.d.ts +40 -0
  110. package/dist/types/analytics/hightouch.d.ts +44 -0
  111. package/dist/types/analytics/hotjar.d.ts +33 -0
  112. package/dist/types/analytics/index.d.ts +61 -0
  113. package/dist/types/analytics/inline.d.ts +17 -0
  114. package/dist/types/analytics/logrocket.d.ts +44 -0
  115. package/dist/types/analytics/mixpanel.d.ts +67 -0
  116. package/dist/types/analytics/pirsch.d.ts +42 -0
  117. package/dist/types/analytics/plausible.d.ts +56 -0
  118. package/dist/types/analytics/posthog.d.ts +45 -0
  119. package/dist/types/analytics/schema.d.ts +320 -0
  120. package/dist/types/analytics/script.d.ts +46 -0
  121. package/dist/types/analytics/segment.d.ts +50 -0
  122. package/dist/types/analytics/vercel.d.ts +48 -0
  123. package/dist/types/astro/integration.d.ts +76 -0
  124. package/dist/types/astro/markdown-negotiation.d.ts +23 -0
  125. package/dist/types/astro/module-types.d.ts +14 -0
  126. package/dist/types/astro/pages.d.ts +44 -0
  127. package/dist/types/cli/env.d.ts +12 -0
  128. package/dist/types/cli/init/scaffold.d.ts +154 -0
  129. package/dist/types/components/layout/nav-utils.d.ts +11 -0
  130. package/dist/types/core/adapter.d.ts +47 -0
  131. package/dist/types/core/api-name.d.ts +7 -0
  132. package/dist/types/core/changelog-index.d.ts +13 -0
  133. package/dist/types/core/config-input.d.ts +209 -505
  134. package/dist/types/core/config.d.ts +60 -34
  135. package/dist/types/core/content-assets.d.ts +76 -0
  136. package/dist/types/core/custom-pages.d.ts +33 -0
  137. package/dist/types/core/data.d.ts +28 -11
  138. package/dist/types/core/define-components.d.ts +12 -9
  139. package/dist/types/core/deployment-env.d.ts +6 -11
  140. package/dist/types/core/frontmatter.d.ts +10 -0
  141. package/dist/types/core/graph.d.ts +18 -0
  142. package/dist/types/core/heading-markers.d.ts +54 -0
  143. package/dist/types/core/i18n-ui.d.ts +6 -2
  144. package/dist/types/core/i18n.d.ts +87 -0
  145. package/dist/types/core/includes.d.ts +138 -0
  146. package/dist/types/core/last-modified.d.ts +47 -0
  147. package/dist/types/core/links.d.ts +95 -0
  148. package/dist/types/core/locale-links.d.ts +60 -0
  149. package/dist/types/core/manifest.d.ts +17 -0
  150. package/dist/types/core/meta.d.ts +38 -0
  151. package/dist/types/core/nav-diagnostics.d.ts +26 -0
  152. package/dist/types/core/navigation.d.ts +6 -0
  153. package/dist/types/core/node-require.d.ts +19 -0
  154. package/dist/types/core/package-json.d.ts +13 -0
  155. package/dist/types/core/probe.d.ts +42 -0
  156. package/dist/types/core/project-graph.d.ts +51 -0
  157. package/dist/types/core/project.d.ts +2 -0
  158. package/dist/types/core/safe-href.d.ts +2 -0
  159. package/dist/types/core/safe-links.d.ts +26 -0
  160. package/dist/types/core/schema.d.ts +2291 -441
  161. package/dist/types/core/site-url.d.ts +16 -0
  162. package/dist/types/core/sources/assets.d.ts +36 -0
  163. package/dist/types/core/sources/cache.d.ts +37 -0
  164. package/dist/types/core/sources/collection.d.ts +28 -0
  165. package/dist/types/core/sources/contentful-rich-text.d.ts +31 -0
  166. package/dist/types/core/sources/contentful.d.ts +40 -0
  167. package/dist/types/core/sources/filesystem.d.ts +24 -0
  168. package/dist/types/core/sources/github-releases.d.ts +31 -0
  169. package/dist/types/core/sources/json.d.ts +30 -0
  170. package/dist/types/core/sources/lexical.d.ts +19 -0
  171. package/dist/types/core/sources/lower.d.ts +75 -0
  172. package/dist/types/core/sources/mdx-remote.d.ts +28 -0
  173. package/dist/types/core/sources/normalize.d.ts +147 -0
  174. package/dist/types/core/sources/notion.d.ts +131 -0
  175. package/dist/types/core/sources/obsidian.d.ts +46 -0
  176. package/dist/types/core/sources/payload.d.ts +39 -0
  177. package/dist/types/core/sources/portable-text.d.ts +42 -0
  178. package/dist/types/core/sources/read.d.ts +23 -0
  179. package/dist/types/core/sources/remote.d.ts +74 -0
  180. package/dist/types/core/sources/resolve.d.ts +19 -0
  181. package/dist/types/core/sources/sanity.d.ts +43 -0
  182. package/dist/types/core/sources/strapi-blocks.d.ts +12 -0
  183. package/dist/types/core/sources/strapi.d.ts +33 -0
  184. package/dist/types/core/sources/types.d.ts +7 -0
  185. package/dist/types/core/sources/watch.d.ts +45 -0
  186. package/dist/types/core/text-width.d.ts +11 -0
  187. package/dist/types/core/types.d.ts +20 -1
  188. package/dist/types/core/unrecognized-keys.d.ts +7 -0
  189. package/dist/types/core/versions.d.ts +72 -0
  190. package/dist/types/core/yaml.d.ts +9 -0
  191. package/dist/types/deploy/adapter-output.d.ts +45 -0
  192. package/dist/types/deploy/adapters/cloudflare.d.ts +40 -0
  193. package/dist/types/deploy/adapters/index.d.ts +29 -0
  194. package/dist/types/deploy/adapters/netlify.d.ts +37 -0
  195. package/dist/types/deploy/adapters/node.d.ts +37 -0
  196. package/dist/types/deploy/adapters/registry.d.ts +133 -0
  197. package/dist/types/deploy/adapters/types.d.ts +71 -0
  198. package/dist/types/deploy/adapters/vercel.d.ts +38 -0
  199. package/dist/types/deploy/artifacts.d.ts +65 -0
  200. package/dist/types/deploy/cloudflare-negotiation.d.ts +196 -0
  201. package/dist/types/deploy/function-bundle.d.ts +80 -0
  202. package/dist/types/deploy/headers.d.ts +50 -0
  203. package/dist/types/deploy/node-headers.d.ts +42 -0
  204. package/dist/types/deploy/platforms/cloudflare.d.ts +40 -0
  205. package/dist/types/deploy/platforms/index.d.ts +18 -0
  206. package/dist/types/deploy/platforms/netlify.d.ts +13 -0
  207. package/dist/types/deploy/platforms/node.d.ts +11 -0
  208. package/dist/types/deploy/platforms/paths.d.ts +27 -0
  209. package/dist/types/deploy/platforms/static.d.ts +10 -0
  210. package/dist/types/deploy/platforms/types.d.ts +104 -0
  211. package/dist/types/deploy/platforms/vercel.d.ts +32 -0
  212. package/dist/types/deploy/redirects.d.ts +53 -0
  213. package/dist/types/deploy/robots.d.ts +8 -0
  214. package/dist/types/deploy/rss.d.ts +31 -0
  215. package/dist/types/deploy/sitemap.d.ts +21 -0
  216. package/dist/types/deploy/vercel-negotiation.d.ts +109 -0
  217. package/dist/types/markdown/code-title.d.ts +32 -0
  218. package/dist/types/markdown/fence-meta.d.ts +23 -0
  219. package/dist/types/markdown/themes.d.ts +3 -3
  220. package/dist/types/openapi/asyncapi.d.ts +129 -0
  221. package/dist/types/openapi/graphql-build.d.ts +8 -0
  222. package/dist/types/openapi/graphql.d.ts +122 -0
  223. package/dist/types/openapi/model.d.ts +158 -0
  224. package/dist/types/openapi/parse.d.ts +57 -0
  225. package/dist/types/openapi/references.d.ts +37 -25
  226. package/dist/types/openapi/render-mdx.d.ts +33 -0
  227. package/dist/types/openapi/sentence.d.ts +7 -0
  228. package/dist/types/openapi/signature.d.ts +10 -0
  229. package/dist/types/openapi/source.d.ts +22 -0
  230. package/dist/types/openapi/spec-dependency-error.d.ts +10 -0
  231. package/dist/types/reference/asyncapi.d.ts +166 -0
  232. package/dist/types/reference/graphql.d.ts +181 -0
  233. package/dist/types/reference/index.d.ts +32 -0
  234. package/dist/types/reference/openapi.d.ts +165 -0
  235. package/dist/types/reference/options.d.ts +157 -0
  236. package/dist/types/reference/scalar.d.ts +136 -0
  237. package/dist/types/reference/schema.d.ts +630 -0
  238. package/dist/types/search/adapters/algolia.d.ts +32 -0
  239. package/dist/types/search/adapters/flexsearch.d.ts +12 -0
  240. package/dist/types/search/adapters/index.d.ts +31 -0
  241. package/dist/types/search/adapters/mixedbread.d.ts +22 -0
  242. package/dist/types/search/adapters/orama-cloud.d.ts +33 -0
  243. package/dist/types/search/adapters/orama.d.ts +13 -0
  244. package/dist/types/search/adapters/pagefind.d.ts +12 -0
  245. package/dist/types/search/adapters/registry.d.ts +228 -0
  246. package/dist/types/search/adapters/types.d.ts +34 -0
  247. package/dist/types/search/adapters/typesense.d.ts +42 -0
  248. package/dist/types/search/build.d.ts +23 -0
  249. package/dist/types/search/documents.d.ts +90 -0
  250. package/dist/types/search/facets.d.ts +3 -0
  251. package/dist/types/search/sync/algolia.d.ts +14 -0
  252. package/dist/types/search/sync/index.d.ts +14 -0
  253. package/dist/types/search/sync/orama-cloud.d.ts +10 -0
  254. package/dist/types/search/sync/typesense.d.ts +14 -0
  255. package/dist/types/sources/contentful.d.ts +68 -0
  256. package/dist/types/sources/custom.d.ts +20 -0
  257. package/dist/types/sources/filesystem.d.ts +42 -0
  258. package/dist/types/sources/github-releases.d.ts +46 -0
  259. package/dist/types/sources/index.d.ts +48 -0
  260. package/dist/types/sources/mdx-remote.d.ts +63 -0
  261. package/dist/types/sources/notion.d.ts +65 -0
  262. package/dist/types/sources/obsidian.d.ts +34 -0
  263. package/dist/types/sources/payload.d.ts +68 -0
  264. package/dist/types/sources/registry.d.ts +1414 -0
  265. package/dist/types/sources/sanity.d.ts +69 -0
  266. package/dist/types/sources/shared.d.ts +46 -0
  267. package/dist/types/sources/strapi.d.ts +66 -0
  268. package/dist/types/theme/icon-kind.d.ts +11 -0
  269. package/dist/types/theme/icons.d.ts +20 -0
  270. package/docs/01-quickstart.mdx +18 -18
  271. package/docs/02-deployment.mdx +50 -26
  272. package/docs/03-upgrading.mdx +351 -0
  273. package/docs/04-migrating.mdx +58 -0
  274. package/docs/08-faq.mdx +3 -3
  275. package/docs/advanced/blog.mdx +13 -6
  276. package/docs/advanced/changelog.mdx +23 -35
  277. package/docs/advanced/custom-pages.mdx +20 -8
  278. package/docs/advanced/meta.ts +1 -8
  279. package/docs/advanced/skills.mdx +8 -0
  280. package/docs/cli/audit.mdx +646 -0
  281. package/docs/cli/doctor.mdx +29 -0
  282. package/docs/{reference/eval.mdx → cli/evals.mdx} +9 -8
  283. package/docs/cli/index.mdx +105 -0
  284. package/docs/cli/meta.ts +7 -0
  285. package/docs/{reference → cli}/translate.mdx +1 -1
  286. package/docs/cli/validate.mdx +41 -0
  287. package/docs/cli/version.mdx +40 -0
  288. package/docs/configuration/analytics.mdx +350 -59
  289. package/docs/configuration/ask-ai.mdx +176 -61
  290. package/docs/configuration/customization.mdx +15 -9
  291. package/docs/configuration/index.mdx +52 -31
  292. package/docs/configuration/search.mdx +75 -54
  293. package/docs/configuration/theming.mdx +24 -15
  294. package/docs/content/components.mdx +21 -5
  295. package/docs/{reference → content}/frontmatter.mdx +37 -1
  296. package/docs/content/i18n.mdx +8 -6
  297. package/docs/content/includes.mdx +2 -4
  298. package/docs/content/index.mdx +1 -1
  299. package/docs/content/islands.mdx +10 -5
  300. package/docs/content/meta.mdx +1 -1
  301. package/docs/content/meta.ts +1 -0
  302. package/docs/content/navigation.mdx +12 -7
  303. package/docs/content/sources.mdx +145 -50
  304. package/docs/content/syntax.mdx +26 -12
  305. package/docs/content/versioning.mdx +3 -3
  306. package/docs/discoverability/agent-discovery.mdx +91 -10
  307. package/docs/discoverability/index.mdx +5 -5
  308. package/docs/discoverability/json-api.mdx +4 -4
  309. package/docs/discoverability/llms-txt.mdx +6 -6
  310. package/docs/discoverability/markdown.mdx +4 -4
  311. package/docs/discoverability/mcp.mdx +11 -11
  312. package/docs/discoverability/sitemap-and-robots.mdx +3 -3
  313. package/docs/index.mdx +2 -2
  314. package/docs/references/asyncapi.mdx +59 -0
  315. package/docs/{advanced → references}/graphql.mdx +45 -32
  316. package/docs/{reference → references}/meta.ts +2 -2
  317. package/docs/references/openapi.mdx +171 -0
  318. package/docs/references/scalar.mdx +64 -0
  319. package/package.json +42 -8
  320. package/skills/blume/SKILL.md +21 -8
  321. package/skills/blume-migrate/SKILL.md +22 -21
  322. package/skills/blume-migrate/assets/oxfmt@0.67.0.patch +49 -0
  323. package/skills/blume-migrate/references/docusaurus.md +5 -4
  324. package/skills/blume-migrate/references/fumadocs.md +4 -4
  325. package/skills/blume-migrate/references/mintlify.md +23 -8
  326. package/skills/blume-migrate/references/monorepo.md +6 -6
  327. package/skills/blume-migrate/references/starlight.md +4 -4
  328. package/src/ai/agent-readability.ts +26 -22
  329. package/src/ai/ai-catalog.ts +259 -0
  330. package/src/ai/api/handlers.ts +46 -11
  331. package/src/ai/api-catalog.ts +8 -8
  332. package/src/ai/ask-data.ts +1 -1
  333. package/src/ai/ask.ts +624 -100
  334. package/src/ai/changelog-markdown.ts +91 -0
  335. package/src/ai/component-markdown.ts +328 -12
  336. package/src/ai/index.ts +45 -0
  337. package/src/ai/link-headers.ts +19 -6
  338. package/src/ai/llms.ts +26 -15
  339. package/src/ai/markdown.ts +35 -5
  340. package/src/ai/mcp/data.ts +9 -5
  341. package/src/ai/mcp/discovery.ts +1 -1
  342. package/src/ai/openapi-components.ts +4 -1
  343. package/src/ai/relative-links.ts +170 -0
  344. package/src/ai/serializers.ts +2 -2
  345. package/src/ai/skills.ts +1 -1
  346. package/src/ai/web-bot-auth.ts +2 -2
  347. package/src/analytics/adobe.ts +46 -0
  348. package/src/analytics/amplitude.ts +79 -0
  349. package/src/analytics/clarity.ts +46 -0
  350. package/src/analytics/clearbit.ts +47 -0
  351. package/src/analytics/cloudflare.ts +67 -0
  352. package/src/analytics/fathom.ts +64 -0
  353. package/src/analytics/google-analytics.ts +84 -0
  354. package/src/analytics/google-tag-manager.ts +61 -0
  355. package/src/analytics/head.ts +130 -0
  356. package/src/analytics/heap.ts +65 -0
  357. package/src/analytics/hightouch.ts +77 -0
  358. package/src/analytics/hotjar.ts +47 -0
  359. package/src/analytics/index.ts +71 -0
  360. package/src/analytics/inline.ts +21 -0
  361. package/src/analytics/logrocket.ts +71 -0
  362. package/src/analytics/mixpanel.ts +92 -0
  363. package/src/analytics/pirsch.ts +66 -0
  364. package/src/analytics/plausible.ts +83 -0
  365. package/src/analytics/posthog.ts +78 -0
  366. package/src/analytics/schema.ts +60 -0
  367. package/src/analytics/script.ts +60 -0
  368. package/src/analytics/segment.ts +85 -0
  369. package/src/analytics/vercel.ts +50 -0
  370. package/src/astro/adapter-root.ts +7 -9
  371. package/src/astro/component-slots.ts +131 -91
  372. package/src/astro/generate.ts +114 -345
  373. package/src/astro/integration.ts +2 -6
  374. package/src/astro/pages.ts +13 -101
  375. package/src/astro/render-deps.ts +379 -0
  376. package/src/astro/runtime-deps.ts +201 -0
  377. package/src/astro/templates.ts +554 -541
  378. package/src/audit/agent.ts +24 -0
  379. package/src/audit/catalog.ts +2 -2
  380. package/src/audit/checks/assets.ts +2 -2
  381. package/src/audit/checks/dns-aid.ts +1 -1
  382. package/src/audit/checks/duplicates.ts +3 -1
  383. package/src/audit/checks/i18n.ts +1 -1
  384. package/src/audit/checks/indexability.ts +7 -7
  385. package/src/audit/checks/links.ts +2 -2
  386. package/src/audit/checks/llms.ts +6 -6
  387. package/src/audit/checks/network.ts +3 -3
  388. package/src/audit/checks/og-image.ts +2 -2
  389. package/src/audit/checks/robots.ts +1 -1
  390. package/src/audit/checks/sitemap.ts +2 -2
  391. package/src/audit/checks/social.ts +1 -1
  392. package/src/audit/run.ts +4 -3
  393. package/src/audit/terms.ts +31 -0
  394. package/src/audit/url.ts +13 -13
  395. package/src/cli/command-meta.ts +10 -0
  396. package/src/cli/commands/audit.ts +39 -20
  397. package/src/cli/commands/build.ts +71 -314
  398. package/src/cli/commands/check.ts +1 -0
  399. package/src/cli/commands/dev.ts +40 -1
  400. package/src/cli/commands/doctor.ts +90 -14
  401. package/src/cli/commands/eject.ts +44 -7
  402. package/src/cli/commands/eval.ts +2 -2
  403. package/src/cli/commands/init.ts +161 -41
  404. package/src/cli/commands/migrate.ts +121 -0
  405. package/src/cli/commands/preview.ts +7 -1
  406. package/src/cli/commands/translate.ts +2 -2
  407. package/src/cli/commands/upgrade.ts +141 -0
  408. package/src/cli/commands/version.ts +57 -44
  409. package/src/cli/eject-scripts.ts +121 -6
  410. package/src/cli/index.ts +11 -1
  411. package/src/cli/init/install.ts +70 -0
  412. package/src/cli/init/questions.ts +7 -0
  413. package/src/cli/init/scaffold.ts +459 -75
  414. package/src/cli/lazy-command.ts +37 -1
  415. package/src/cli/prepare.ts +40 -6
  416. package/src/cli/required-secrets.ts +38 -11
  417. package/src/cli/unknown-flags.ts +266 -0
  418. package/src/cli/yarn-pnp.ts +52 -0
  419. package/src/components/content/AccordionItem.astro +7 -1
  420. package/src/components/content/Card.astro +4 -3
  421. package/src/components/content/ColorItem.astro +22 -4
  422. package/src/components/content/GithubInfo.astro +2 -2
  423. package/src/components/content/Prompt.astro +25 -25
  424. package/src/components/content/Tabs.astro +3 -1
  425. package/src/components/content/Tile.astro +2 -3
  426. package/src/components/content/Tooltip.astro +57 -9
  427. package/src/components/content/content-strings.ts +34 -0
  428. package/src/components/content/diff.ts +1 -1
  429. package/src/components/content/mermaid-element.ts +13 -2
  430. package/src/components/content/prompt-markdown.ts +292 -0
  431. package/src/components/content/tooltip-id.ts +41 -0
  432. package/src/components/islands/hooks.ts +75 -3
  433. package/src/components/layout/Analytics.astro +21 -79
  434. package/src/components/layout/Banner.astro +3 -1
  435. package/src/components/layout/DiscoveryLinks.astro +69 -0
  436. package/src/components/layout/Header.astro +83 -20
  437. package/src/components/layout/LanguageSwitcher.astro +4 -1
  438. package/src/components/layout/Logo.astro +23 -2
  439. package/src/components/layout/NavSelector.astro +13 -2
  440. package/src/components/layout/NavTree.astro +12 -3
  441. package/src/components/layout/NavTreeScript.astro +17 -3
  442. package/src/components/layout/PageActions.astro +3 -3
  443. package/src/components/layout/PageFeedback.astro +11 -1
  444. package/src/components/layout/PageLayout.astro +52 -5
  445. package/src/components/layout/ReferenceLayout.astro +21 -12
  446. package/src/components/layout/RootLayout.astro +69 -43
  447. package/src/components/layout/Search.astro +30 -6
  448. package/src/components/layout/WebMcp.astro +1 -1
  449. package/src/components/layout/analytics-client.ts +101 -15
  450. package/src/components/layout/drawer-inert.ts +113 -15
  451. package/src/components/layout/dropdown-clamp.ts +105 -0
  452. package/src/components/layout/head-scripts.ts +15 -6
  453. package/src/components/layout/nav-utils.ts +17 -0
  454. package/src/components/layout/search/algolia.ts +8 -8
  455. package/src/components/layout/search/orama-cloud.ts +9 -7
  456. package/src/components/layout/search/typesense.ts +11 -17
  457. package/src/components/openapi/MessageComposer.astro +2 -2
  458. package/src/components/openapi/Playground.astro +2 -2
  459. package/src/components/openapi/playground-client.ts +79 -10
  460. package/src/core/changelog-index.ts +24 -0
  461. package/src/core/component-overrides.ts +399 -154
  462. package/src/core/config-input.ts +212 -543
  463. package/src/core/config.ts +130 -41
  464. package/src/core/custom-pages.ts +105 -0
  465. package/src/core/data.ts +32 -11
  466. package/src/core/define-components.ts +12 -9
  467. package/src/core/deployment-env.ts +18 -74
  468. package/src/core/diagnostics.ts +14 -7
  469. package/src/core/graph.ts +69 -25
  470. package/src/core/i18n-ui.ts +6 -2
  471. package/src/core/i18n.ts +17 -0
  472. package/src/core/includes.ts +156 -38
  473. package/src/core/last-modified.ts +6 -11
  474. package/src/core/links.ts +166 -2
  475. package/src/core/manifest.ts +10 -3
  476. package/src/core/navigation.ts +137 -19
  477. package/src/core/new-tab.ts +35 -0
  478. package/src/core/node-require.ts +21 -0
  479. package/src/core/project-graph.ts +42 -26
  480. package/src/core/project.ts +9 -4
  481. package/src/core/request-body.ts +61 -0
  482. package/src/core/safe-href.ts +28 -0
  483. package/src/core/safe-links.ts +68 -0
  484. package/src/core/schema.ts +531 -721
  485. package/src/core/server-features.ts +8 -9
  486. package/src/core/sources/assets.ts +47 -11
  487. package/src/core/sources/collection.ts +67 -0
  488. package/src/core/sources/contentful-rich-text.ts +285 -0
  489. package/src/core/sources/contentful.ts +173 -0
  490. package/src/core/sources/github-releases.ts +51 -1
  491. package/src/core/sources/json.ts +71 -0
  492. package/src/core/sources/lexical.ts +195 -0
  493. package/src/core/sources/lower.ts +226 -0
  494. package/src/core/sources/normalize.ts +52 -2
  495. package/src/core/sources/notion.ts +39 -28
  496. package/src/core/sources/payload.ts +135 -0
  497. package/src/core/sources/portable-text.ts +11 -16
  498. package/src/core/sources/remote.ts +226 -0
  499. package/src/core/sources/resolve.ts +104 -166
  500. package/src/core/sources/sanity.ts +16 -49
  501. package/src/core/sources/strapi-blocks.ts +124 -0
  502. package/src/core/sources/strapi.ts +191 -0
  503. package/src/core/sources/types.ts +12 -1
  504. package/src/core/types.ts +20 -1
  505. package/src/core/ui-packs/ar.ts +6 -2
  506. package/src/core/ui-packs/bg.ts +6 -2
  507. package/src/core/ui-packs/bn.ts +6 -2
  508. package/src/core/ui-packs/ca.ts +3 -1
  509. package/src/core/ui-packs/cs.ts +6 -2
  510. package/src/core/ui-packs/da.ts +6 -2
  511. package/src/core/ui-packs/de.ts +6 -2
  512. package/src/core/ui-packs/el.ts +3 -1
  513. package/src/core/ui-packs/es.ts +3 -1
  514. package/src/core/ui-packs/fa.ts +6 -2
  515. package/src/core/ui-packs/fi.ts +6 -2
  516. package/src/core/ui-packs/fr.ts +3 -1
  517. package/src/core/ui-packs/he.ts +6 -2
  518. package/src/core/ui-packs/hi.ts +6 -2
  519. package/src/core/ui-packs/hr.ts +6 -2
  520. package/src/core/ui-packs/hu.ts +6 -2
  521. package/src/core/ui-packs/id.ts +6 -2
  522. package/src/core/ui-packs/it.ts +3 -1
  523. package/src/core/ui-packs/ja.ts +3 -1
  524. package/src/core/ui-packs/ko.ts +3 -1
  525. package/src/core/ui-packs/nl.ts +6 -2
  526. package/src/core/ui-packs/no.ts +6 -2
  527. package/src/core/ui-packs/pl.ts +6 -2
  528. package/src/core/ui-packs/pt-br.ts +3 -1
  529. package/src/core/ui-packs/pt.ts +3 -1
  530. package/src/core/ui-packs/ro.ts +6 -2
  531. package/src/core/ui-packs/ru.ts +6 -2
  532. package/src/core/ui-packs/sk.ts +6 -2
  533. package/src/core/ui-packs/sr.ts +6 -2
  534. package/src/core/ui-packs/sv.ts +6 -2
  535. package/src/core/ui-packs/th.ts +3 -1
  536. package/src/core/ui-packs/tr.ts +6 -2
  537. package/src/core/ui-packs/uk.ts +6 -2
  538. package/src/core/ui-packs/vi.ts +3 -1
  539. package/src/core/ui-packs/zh-tw.ts +3 -1
  540. package/src/core/ui-packs/zh.ts +3 -1
  541. package/src/core/unrecognized-keys.ts +10 -0
  542. package/src/core/version-cut.ts +116 -8
  543. package/src/deploy/adapter-output.ts +57 -97
  544. package/src/deploy/adapters/cloudflare.ts +39 -0
  545. package/src/deploy/adapters/index.ts +41 -0
  546. package/src/deploy/adapters/netlify.ts +34 -0
  547. package/src/deploy/adapters/node.ts +34 -0
  548. package/src/deploy/adapters/registry.ts +127 -0
  549. package/src/deploy/adapters/types.ts +92 -0
  550. package/src/deploy/adapters/vercel.ts +35 -0
  551. package/src/deploy/artifacts.ts +67 -28
  552. package/src/deploy/cloudflare-negotiation.ts +179 -100
  553. package/src/deploy/function-bundle.ts +18 -3
  554. package/src/deploy/headers.ts +129 -22
  555. package/src/deploy/node-headers.ts +198 -0
  556. package/src/deploy/platforms/cloudflare.ts +312 -0
  557. package/src/deploy/platforms/index.ts +67 -0
  558. package/src/deploy/platforms/netlify.ts +52 -0
  559. package/src/deploy/platforms/node.ts +40 -0
  560. package/src/deploy/platforms/paths.ts +42 -0
  561. package/src/deploy/platforms/static.ts +29 -0
  562. package/src/deploy/platforms/types.ts +112 -0
  563. package/src/deploy/platforms/vercel.ts +193 -0
  564. package/src/deploy/redirects.ts +30 -18
  565. package/src/deploy/robots.ts +3 -3
  566. package/src/deploy/rss.ts +2 -2
  567. package/src/deploy/sitemap.ts +2 -2
  568. package/src/deploy/vercel-negotiation.ts +27 -4
  569. package/src/eval/agents.ts +32 -1
  570. package/src/eval/findings.ts +19 -11
  571. package/src/eval/report.ts +10 -2
  572. package/src/markdown/external-links.ts +65 -0
  573. package/src/markdown/include.ts +45 -27
  574. package/src/markdown/index.ts +34 -9
  575. package/src/markdown/inline-code.ts +12 -6
  576. package/src/markdown/relative-links.ts +324 -0
  577. package/src/markdown/themes.ts +3 -3
  578. package/src/migrate/migrate.ts +149 -0
  579. package/src/openapi/parse.ts +64 -16
  580. package/src/openapi/proxy.ts +62 -10
  581. package/src/openapi/references.ts +147 -147
  582. package/src/openapi/render-mdx.ts +32 -10
  583. package/src/openapi/scalar.ts +15 -13
  584. package/src/openapi/sentence.ts +14 -0
  585. package/src/openapi/source.ts +37 -21
  586. package/src/openapi/spec-dependency-error.ts +15 -0
  587. package/src/reference/asyncapi.ts +83 -0
  588. package/src/reference/graphql.ts +89 -0
  589. package/src/reference/index.ts +40 -0
  590. package/src/reference/openapi.ts +83 -0
  591. package/src/reference/options.ts +201 -0
  592. package/src/reference/scalar.ts +136 -0
  593. package/src/reference/schema.ts +88 -0
  594. package/src/registry/eject.ts +97 -42
  595. package/src/search/adapters/algolia.ts +46 -0
  596. package/src/search/adapters/flexsearch.ts +29 -0
  597. package/src/search/adapters/index.ts +39 -0
  598. package/src/search/adapters/mixedbread.ts +39 -0
  599. package/src/search/adapters/orama-cloud.ts +51 -0
  600. package/src/search/adapters/orama.ts +24 -0
  601. package/src/search/adapters/pagefind.ts +27 -0
  602. package/src/search/adapters/registry.ts +131 -0
  603. package/src/search/adapters/types.ts +46 -0
  604. package/src/search/adapters/typesense.ts +57 -0
  605. package/src/search/build.ts +33 -7
  606. package/src/search/documents.ts +6 -0
  607. package/src/search/sync/algolia.ts +12 -11
  608. package/src/search/sync/index.ts +35 -20
  609. package/src/search/sync/orama-cloud.ts +12 -9
  610. package/src/search/sync/typesense.ts +15 -13
  611. package/src/sources/contentful.ts +63 -0
  612. package/src/sources/custom.ts +38 -0
  613. package/src/sources/filesystem.ts +51 -0
  614. package/src/sources/github-releases.ts +52 -0
  615. package/src/sources/index.ts +60 -0
  616. package/src/sources/mdx-remote.ts +76 -0
  617. package/src/sources/notion.ts +63 -0
  618. package/src/sources/obsidian.ts +38 -0
  619. package/src/sources/payload.ts +60 -0
  620. package/src/sources/registry.ts +182 -0
  621. package/src/sources/sanity.ts +66 -0
  622. package/src/sources/shared.ts +52 -0
  623. package/src/sources/strapi.ts +58 -0
  624. package/src/theme/entry.ts +47 -2
  625. package/src/translate/report.ts +40 -7
  626. package/src/upgrade/upgrade.ts +499 -0
  627. package/dist/cli/chunk-12dxjqk7.js.map +0 -10
  628. package/dist/cli/chunk-196vjxp9.js.map +0 -13
  629. package/dist/cli/chunk-2mzebbbz.js +0 -69
  630. package/dist/cli/chunk-2z47ypj8.js.map +0 -11
  631. package/dist/cli/chunk-30e87n55.js +0 -108
  632. package/dist/cli/chunk-30e87n55.js.map +0 -10
  633. package/dist/cli/chunk-450a7rcr.js +0 -185
  634. package/dist/cli/chunk-450a7rcr.js.map +0 -11
  635. package/dist/cli/chunk-5n7t497w.js.map +0 -40
  636. package/dist/cli/chunk-61j18dwk.js +0 -5342
  637. package/dist/cli/chunk-61j18dwk.js.map +0 -58
  638. package/dist/cli/chunk-88by27n5.js +0 -17
  639. package/dist/cli/chunk-88by27n5.js.map +0 -10
  640. package/dist/cli/chunk-8cd8tj54.js.map +0 -34
  641. package/dist/cli/chunk-aztttvb3.js +0 -381
  642. package/dist/cli/chunk-aztttvb3.js.map +0 -12
  643. package/dist/cli/chunk-cvky9gb2.js.map +0 -11
  644. package/dist/cli/chunk-ejjx8znq.js.map +0 -15
  645. package/dist/cli/chunk-fmceyezb.js +0 -1007
  646. package/dist/cli/chunk-fmceyezb.js.map +0 -13
  647. package/dist/cli/chunk-hdm2dkd2.js.map +0 -48
  648. package/dist/cli/chunk-hs3gbh8p.js.map +0 -10
  649. package/dist/cli/chunk-jbj4qhfw.js +0 -30
  650. package/dist/cli/chunk-jbj4qhfw.js.map +0 -10
  651. package/dist/cli/chunk-mqb2ka8m.js +0 -68
  652. package/dist/cli/chunk-mqb2ka8m.js.map +0 -10
  653. package/dist/cli/chunk-mt76t7dj.js.map +0 -11
  654. package/dist/cli/chunk-q4rae3bg.js +0 -60
  655. package/dist/cli/chunk-q4rae3bg.js.map +0 -10
  656. package/dist/cli/chunk-ra1v2nc2.js +0 -35
  657. package/dist/cli/chunk-ra1v2nc2.js.map +0 -10
  658. package/dist/cli/chunk-t3tj0dgr.js.map +0 -15
  659. package/dist/cli/chunk-tqa1s0k8.js +0 -69
  660. package/dist/cli/chunk-tqa1s0k8.js.map +0 -11
  661. package/dist/cli/chunk-vh9w1sgp.js +0 -73
  662. package/dist/cli/chunk-vh9w1sgp.js.map +0 -10
  663. package/dist/cli/chunk-vkrsvbr5.js +0 -107
  664. package/dist/cli/chunk-vkrsvbr5.js.map +0 -11
  665. package/dist/cli/chunk-vrfp10qk.js +0 -81
  666. package/dist/cli/chunk-vrfp10qk.js.map +0 -10
  667. package/dist/cli/chunk-wjt80jps.js +0 -1049
  668. package/dist/cli/chunk-wjt80jps.js.map +0 -24
  669. package/docs/advanced/api-reference.mdx +0 -240
  670. package/docs/reference/cli.mdx +0 -197
  671. package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +0 -20
  672. package/src/components/content/changelog-element.ts +0 -69
  673. package/src/search/providers.ts +0 -91
  674. /package/dist/cli/{chunk-3w7b2vcx.js.map → chunk-7ez8ny0t.js.map} +0 -0
  675. /package/dist/cli/{chunk-688e0dde.js.map → chunk-88cpgt6h.js.map} +0 -0
  676. /package/dist/cli/{chunk-exeeb35e.js.map → chunk-f7t03s3g.js.map} +0 -0
  677. /package/dist/cli/{chunk-ppfvdcd4.js.map → chunk-zxccj738.js.map} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
3
  import type { BlumeClientData } from "../../core/data.ts";
4
+ import { track } from "../layout/analytics-client.ts";
4
5
  import type { SearchFn, SearchResult } from "../layout/search/types.ts";
5
6
  import { joinBase, stripBase } from "./base-path.ts";
6
7
 
@@ -160,6 +161,28 @@ export interface UseAskAIOptions {
160
161
  /** Shown as the assistant's answer when the request fails or throws. */
161
162
  const ASK_ERROR = "Something went wrong answering that. Please try again.";
162
163
 
164
+ /** How the generated Ask AI route opens its missing-credential notice. */
165
+ const NOT_CONFIGURED = /^Ask AI is not configured/u;
166
+
167
+ /**
168
+ * The route's own explanation of a 503 — the generated route's "Ask AI is not
169
+ * configured: set AI_GATEWAY_API_KEY." — when the response is that notice, or
170
+ * null. It names only the variable to set, never a value, so it's safe to
171
+ * show, and it tells a site owner trying a fresh deploy what's missing instead
172
+ * of the generic failure; any other error body (an HTML error page, a proxy's
173
+ * message) still gets the generic notice.
174
+ */
175
+ const unavailableNotice = async (
176
+ response: Response
177
+ ): Promise<string | null> => {
178
+ if (response.status !== 503) {
179
+ return null;
180
+ }
181
+ const body = await response.text();
182
+ const text = body.trim();
183
+ return NOT_CONFIGURED.test(text) && text.length <= 200 ? text : null;
184
+ };
185
+
163
186
  /** The current route with the deployment base stripped, for page grounding. */
164
187
  const currentPath = (): string =>
165
188
  stripBase(import.meta.env.BASE_URL, window.location.pathname);
@@ -196,6 +219,42 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
196
219
  const controller = new AbortController();
197
220
  abortRef.current = controller;
198
221
  const live = () => current === generation.current;
222
+ const path = currentPath();
223
+ // Usage reaches the configured analytics providers the same way page
224
+ // feedback does: the question now, its outcome once the stream settles.
225
+ // A reset mid-answer revokes the outcome along with the UI update.
226
+ // Analytics keys on the raw pathname, like page feedback and the
227
+ // providers' own pageviews, so the events join under a `base`; the
228
+ // endpoint gets the base-stripped route for grounding. Providers receive
229
+ // the question's length only: its text is free-form reader input (pasted
230
+ // keys, error logs) that would breach their PII terms and their
231
+ // per-value size caps, so it rides the `blume:track` event alone for a
232
+ // site to bridge on its own terms.
233
+ const { pathname } = window.location;
234
+ const report = (
235
+ event: "ask" | "ask_answer" | "ask_error",
236
+ props: Record<string, number>
237
+ ) =>
238
+ track(
239
+ event,
240
+ { ...props, path: pathname, questionChars: trimmed.length },
241
+ { question: trimmed }
242
+ );
243
+ report("ask", {});
244
+ // A monotonic clock: the wall clock can jump mid-stream (NTP, sleep).
245
+ const startedAt = performance.now();
246
+ const outcome = (
247
+ event: "ask_answer" | "ask_error",
248
+ props: Record<string, number>
249
+ ) =>
250
+ report(event, {
251
+ ...props,
252
+ ms: Math.round(performance.now() - startedAt),
253
+ });
254
+ // The HTTP status once a response exists. `streamText` defers provider
255
+ // errors to stream consumption, so a 200 can still break mid-flight;
256
+ // that reports as a 200 error, not as "no response".
257
+ let status = 0;
199
258
  const history: AskMessage[] = [
200
259
  ...messages,
201
260
  { content: trimmed, role: "user" },
@@ -207,17 +266,20 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
207
266
  const response = await fetch(endpoint, {
208
267
  body: JSON.stringify({
209
268
  messages: history,
210
- page: { path: currentPath() },
269
+ page: { path },
211
270
  }),
212
271
  headers: { "content-type": "application/json" },
213
272
  method: "POST",
214
273
  signal: controller.signal,
215
274
  });
275
+ ({ status } = response);
216
276
  if (!response.ok) {
217
277
  // An error body (JSON, HTML error page) must not stream in as the
218
- // assistant's answer.
278
+ // assistant's answer — only the route's own not-configured notice.
279
+ const notice = await unavailableNotice(response).catch(() => null);
219
280
  if (live()) {
220
- assistant.content = errorMessage;
281
+ outcome("ask_error", { status });
282
+ assistant.content = notice ?? errorMessage;
221
283
  setMessages([...history, { ...assistant }]);
222
284
  }
223
285
  return;
@@ -243,11 +305,21 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
243
305
  }
244
306
  }
245
307
  }
308
+ 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
+ if (assistant.content) {
312
+ outcome("ask_answer", { chars: assistant.content.length });
313
+ } else {
314
+ outcome("ask_error", { status });
315
+ }
316
+ }
246
317
  } catch {
247
318
  // A thrown fetch (offline, DNS failure, CORS) must not strand the
248
319
  // pre-appended empty assistant message as a stuck placeholder. A
249
320
  // reset's abort lands here too — the guard keeps it silent.
250
321
  if (live()) {
322
+ outcome("ask_error", { status });
251
323
  assistant.content = errorMessage;
252
324
  setMessages([...history, { ...assistant }]);
253
325
  }
@@ -1,90 +1,32 @@
1
1
  ---
2
2
  import VercelAnalytics from "@vercel/analytics/astro";
3
3
 
4
- // Analytics scripts injected into <head>. PostHog and custom providers use the
5
- // inline-snippet house style (banner/theme/JSON-LD); Vercel uses its official
6
- // Astro component; Cloudflare is its beacon script tag. Loads ONLY in production builds so `blume dev` stays clean
7
- // and local traffic never reaches your analytics.
8
- interface AnalyticsScript {
9
- attributes?: Record<string, string>;
10
- content?: string;
11
- src?: string;
12
- strategy?: "async" | "defer";
13
- }
14
-
4
+ import { analyticsHead } from "../../analytics/head.ts";
5
+ import type { BlumeDataConfig } from "../../core/data.ts";
6
+
7
+ // The analytics adapters' <head> output. Each adapter in `analytics` owns the
8
+ // snippet it emits (src/analytics/*); `analyticsHead` collects them into one
9
+ // list in declared order and this component renders it: the official Vercel
10
+ // component for `vercel()`, one <script> for every other adapter. Loads ONLY
11
+ // in production builds so `blume dev` stays clean and local traffic never
12
+ // reaches your analytics.
15
13
  interface Props {
16
- analytics?: {
17
- cloudflare?: { token: string };
18
- posthog?: { host?: string; key: string };
19
- scripts?: AnalyticsScript[];
20
- vercel?: boolean;
21
- } | null;
14
+ analytics?: BlumeDataConfig["analytics"] | null;
22
15
  }
23
16
 
24
17
  const { analytics } = Astro.props;
25
- const enabled = import.meta.env.PROD && Boolean(analytics);
26
-
27
- // Vercel Web Analytics: rendered via the official @vercel/analytics/astro
28
- // component, which injects the first-party script Vercel serves at
29
- // /_vercel/insights once Web Analytics is enabled for the project.
30
- const vercelEnabled = enabled && analytics?.vercel === true;
31
-
32
- // PostHog: the official array.js loader snippet, keyed off config. The host
33
- // defaults to PostHog Cloud US; EU/self-hosted users override `posthog.host`.
34
- const POSTHOG_LOADER =
35
- '!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);';
36
- const posthog = enabled ? analytics?.posthog : undefined;
37
- // PostHog's loader only captures a pageview per real page load, but the client
38
- // router turns link clicks into in-place swaps — capture those too, keyed off
39
- // `astro:page-load` with a pathname guard so the initial load (which the
40
- // loader already counted) and same-page hash moves aren't double-counted.
41
- // Vercel Web Analytics and GA4's enhanced measurement track history changes
42
- // on their own.
43
- const POSTHOG_SPA_PAGEVIEWS =
44
- 'document.addEventListener("astro:page-load",function(){var p=window.__blumePhPath;window.__blumePhPath=location.pathname;if(p!==undefined&&p!==location.pathname){posthog.capture("$pageview");}});';
45
- const posthogSnippet = posthog
46
- ? `${POSTHOG_LOADER}posthog.init(${JSON.stringify(posthog.key)},{api_host:${JSON.stringify(posthog.host ?? "https://us.i.posthog.com")}});${POSTHOG_SPA_PAGEVIEWS}`
47
- : null;
48
-
49
- // Cloudflare Web Analytics (manual setup): the beacon script keyed by the
50
- // site token, exactly as the dashboard's snippet renders it. It tracks
51
- // history changes itself, so client-router navigations need no extra hook.
52
- const cloudflareBeacon =
53
- enabled && analytics?.cloudflare
54
- ? JSON.stringify({ token: analytics.cloudflare.token })
55
- : null;
56
-
57
- // Custom scripts: any other provider. Each is either external (`src`) or inline
58
- // (`content`). Explicit fields win over spread `attributes`.
59
- const customScripts = enabled ? (analytics?.scripts ?? []) : [];
60
- const externalScripts = customScripts
61
- .filter((script) => script.src)
62
- .map((script) => ({
63
- ...script.attributes,
64
- ...(script.strategy === "async" ? { async: true } : {}),
65
- ...(script.strategy === "defer" ? { defer: true } : {}),
66
- src: script.src,
67
- }));
68
- const inlineScripts = customScripts
69
- .filter((script) => script.content)
70
- .map((script) => ({ attributes: script.attributes ?? {}, content: script.content }));
18
+ const nodes = analyticsHead(import.meta.env.PROD ? (analytics ?? []) : []);
71
19
  ---
72
20
 
73
- {vercelEnabled && <VercelAnalytics />}
74
- {
75
- cloudflareBeacon && (
76
- <script
77
- is:inline
78
- defer
79
- src="https://static.cloudflareinsights.com/beacon.min.js"
80
- data-cf-beacon={cloudflareBeacon}
81
- />
82
- )
83
- }
84
- {posthogSnippet && <script is:inline set:html={posthogSnippet} />}
85
- {externalScripts.map((attrs) => <script is:inline {...attrs} />)}
86
21
  {
87
- inlineScripts.map((script) => (
88
- <script is:inline set:html={script.content} {...script.attributes} />
89
- ))
22
+ nodes.map((node) => {
23
+ if (node.type === "vercel") {
24
+ return <VercelAnalytics {...node.props} />;
25
+ }
26
+ return node.content === null ? (
27
+ <script is:inline {...node.attributes} />
28
+ ) : (
29
+ <script is:inline set:html={node.content} {...node.attributes} />
30
+ );
31
+ })
90
32
  }
@@ -34,7 +34,9 @@ const b = { ...EN_UI.banner, ...strings };
34
34
  data-banner-key={banner.key}
35
35
  data-blume-banner
36
36
  >
37
- <span>{banner.content}</span>
37
+ {/* The message is authored in one language, whatever the page's
38
+ direction: `dir="auto"` keeps its punctuation where it was typed. */}
39
+ <span dir="auto">{banner.content}</span>
38
40
  {banner.link && (
39
41
  <a
40
42
  class="inline-flex items-center gap-1 font-medium underline underline-offset-2"
@@ -0,0 +1,69 @@
1
+ ---
2
+ import type { DiscoveryHeadLinks } from "../../core/data.ts";
3
+ import { withBase } from "../islands/base-path.ts";
4
+ // Emits the agent-discovery <link>s for the document <head>, on every page and
5
+ // not just the root: an agent that enters on a deep page (a search result, a
6
+ // shared link) never sees the homepage HTTP `Link` header (see
7
+ // `ai/link-headers.ts`), so the head carries the same `describedby` resources,
8
+ // the AI Catalog / ARD manifest pair, and — when the page has one — its own
9
+ // raw-Markdown mirror as an `alternate`. This also reaches hosts where Blume
10
+ // can't set response headers. Every shell (RootLayout, PageLayout,
11
+ // ReferenceLayout) renders this one partial, so the "every page's head"
12
+ // promise has a single definition. Both `describedby` and `alternate` are
13
+ // IANA-registered relations.
14
+ interface Props {
15
+ /**
16
+ * Which resources exist, from the `blume:data` snapshot; `null` drops every
17
+ * link this partial renders, the Markdown alternate included (a page opting
18
+ * out of the agent-facing surface).
19
+ */
20
+ discovery?: DiscoveryHeadLinks | null;
21
+ /**
22
+ * This page's raw-Markdown mirror (already under the deployment base), or
23
+ * `null` when the route has none — only content routes and the homepage do
24
+ * (see `markdownRoutePaths`). Advertised only while `discovery` is set.
25
+ */
26
+ markdownMirror?: string | null;
27
+ }
28
+
29
+ const { discovery, markdownMirror } = Astro.props;
30
+ ---
31
+
32
+ {
33
+ discovery?.agentReadability && (
34
+ <link
35
+ href={withBase("/agent-readability.json")}
36
+ rel="describedby"
37
+ type="application/json"
38
+ />
39
+ )
40
+ }
41
+ {
42
+ discovery?.llmsTxt && (
43
+ <link href={withBase("/llms.txt")} rel="describedby" type="text/plain" />
44
+ )
45
+ }
46
+ {/* The AI Catalog / ARD manifest under both relations its two specs define:
47
+ `ai-catalog` (ai-catalog spec) and `ard` (ARD v0.91), each pointing at that
48
+ spec's own well-known path. */}
49
+ {
50
+ discovery?.aiCatalog && (
51
+ <>
52
+ <link
53
+ href={withBase("/.well-known/ai-catalog.json")}
54
+ rel="ai-catalog"
55
+ type="application/ai-catalog+json"
56
+ />
57
+ <link
58
+ href={withBase("/.well-known/ard.json")}
59
+ rel="ard"
60
+ type="application/json"
61
+ />
62
+ </>
63
+ )
64
+ }
65
+ {
66
+ discovery && markdownMirror && (
67
+ <link href={markdownMirror} rel="alternate" type="text/markdown" />
68
+ )
69
+ }
@@ -2,7 +2,7 @@
2
2
  import Ask from "blume:ask";
3
3
  import data from "blume:data";
4
4
  import { withBase } from "../islands/base-path.ts";
5
- import { isExternalUrl } from "../../core/base-path.ts";
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";
8
8
  import type { UIStrings } from "../../core/i18n-ui.ts";
@@ -33,6 +33,11 @@ interface Props {
33
33
  navigation: Navigation;
34
34
  route: string;
35
35
  searchEnabled: boolean;
36
+ /**
37
+ * Active locale of the page, forwarded to the `Logo` slot so the brand link
38
+ * resolves to the locale's root like the tabs beside it.
39
+ */
40
+ locale?: string;
36
41
  /**
37
42
  * Whether the search modal offers an "Ask AI" hand-off. Defaults to whether
38
43
  * Ask AI is configured, so no page has to pass it; a layout can still opt a
@@ -71,6 +76,12 @@ interface Props {
71
76
  * the whole header.
72
77
  */
73
78
  layout?: Record<string, ComponentOverride>;
79
+ /**
80
+ * Start the header see-through, with its chrome in white, so it can sit over
81
+ * a dark hero at the top of the page; it returns to the frosted bar as soon
82
+ * as the page scrolls (`PageLayout`'s `transparentHeader`).
83
+ */
84
+ transparent?: boolean;
74
85
  }
75
86
 
76
87
  const {
@@ -79,6 +90,7 @@ const {
79
90
  navigation,
80
91
  route,
81
92
  searchEnabled,
93
+ locale,
82
94
  // `config.ask` is null whenever Ask AI is off, so the header is the one place
83
95
  // that has to know — the Ask trigger below and the search modal's hand-off to
84
96
  // it both switch on this, and every page gets both for free.
@@ -94,6 +106,7 @@ const {
94
106
  searchLocale,
95
107
  searchVersion = null,
96
108
  layout = {},
109
+ transparent = false,
97
110
  } = Astro.props;
98
111
 
99
112
  // Merge over the English defaults so a label missing from a translation (or
@@ -143,6 +156,12 @@ const ctaClass = [
143
156
  // for the swap (next-themes' disableTransitionOnChange trick), force a
144
157
  // synchronous style flush, then restore them on the next tick.
145
158
  //
159
+ // Both the theme toggle and the banner dismiss change the page first and only
160
+ // then remember the choice: localStorage throws where storage is blocked
161
+ // (Safari "Block All Cookies", sandboxed iframes), and a throw before the DOM
162
+ // change would leave the toggle dead with transitions switched off for good.
163
+ // There the choice simply isn't remembered.
164
+ //
146
165
  // The mobile drawer sits below the sticky header, but the banner above the
147
166
  // header has no fixed height (its text wraps), so the drawer's top offset can't
148
167
  // be static: on open we measure the header's on-screen bottom edge into
@@ -150,16 +169,31 @@ const ctaClass = [
150
169
  // so the offset can't drift while open, and re-measure when a banner dismiss or
151
170
  // a resize/rotation moves the header. Crossing into the desktop breakpoint
152
171
  // (64rem, Tailwind `lg`) closes the drawer so the scroll lock can't outlive it.
153
- const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-blume-header]");if(h){document.documentElement.style.setProperty("--blume-drawer-top",h.getBoundingClientRect().bottom+"px");}};document.addEventListener("click",(e)=>{const t=e.target.closest("[data-blume-theme-toggle]");if(t){const d=document.documentElement;const next=d.dataset.theme==="dark"?"light":"dark";const s=document.createElement("style");s.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important}"));document.head.appendChild(s);d.dataset.theme=next;localStorage.setItem("blume-theme",next);window.getComputedStyle(d).opacity;setTimeout(()=>document.head.removeChild(s),1);}const n=e.target.closest("[data-blume-nav-toggle]");if(n){const d=document.documentElement;const open=d.toggleAttribute("data-blume-nav-open");if(open){dr();}}const b=e.target.closest("[data-blume-banner-dismiss]");if(b){const bar=b.closest("[data-blume-banner]");const k=bar&&bar.getAttribute("data-banner-key");if(k){localStorage.setItem("blume-banner:"+k,"1");}document.documentElement.setAttribute("data-blume-banner-hidden","");requestAnimationFrame(dr);}});addEventListener("resize",()=>{const d=document.documentElement;if(!d.hasAttribute("data-blume-nav-open")){return;}if(matchMedia("(min-width:64rem)").matches){d.removeAttribute("data-blume-nav-open");}else{dr();}});})();`;
172
+ // The rest of the drawer's behavior — the toggle's `aria-expanded`, Escape to
173
+ // close, and the inert page behind it — follows the same attribute from the
174
+ // layouts' drawer-inert.ts.
175
+ // Transparent header: flag the bar as scrolled once the page moves off the
176
+ // top so the stylesheet below can swap it back to the frosted look. `sync`
177
+ // re-queries the header each time, so the one listener a real load installs
178
+ // keeps working across client-router swaps (and no-ops on pages without it).
179
+ // The router re-runs this inline script on every swap into a transparent-header
180
+ // page, so it syncs that page each time but installs the listeners only once
181
+ // per real load, behind a window flag.
182
+ const transparentScript = `(()=>{const sync=()=>{const h=document.querySelector("[data-blume-header-transparent]");if(h){h.toggleAttribute("data-blume-header-scrolled",scrollY>8);}};sync();if(window.__blumeTransparentHeader){return;}window.__blumeTransparentHeader=true;addEventListener("scroll",sync,{passive:true});document.addEventListener("astro:after-swap",sync);})();`;
183
+
184
+ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-blume-header]");if(h){document.documentElement.style.setProperty("--blume-drawer-top",h.getBoundingClientRect().bottom+"px");}};const save=(k,v)=>{try{localStorage.setItem(k,v);}catch{}};document.addEventListener("click",(e)=>{const t=e.target.closest("[data-blume-theme-toggle]");if(t){const d=document.documentElement;const next=d.dataset.theme==="dark"?"light":"dark";const s=document.createElement("style");s.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important}"));document.head.appendChild(s);d.dataset.theme=next;window.getComputedStyle(d).opacity;setTimeout(()=>document.head.removeChild(s),1);save("blume-theme",next);}const n=e.target.closest("[data-blume-nav-toggle]");if(n){const d=document.documentElement;const open=d.toggleAttribute("data-blume-nav-open");if(open){dr();}}const b=e.target.closest("[data-blume-banner-dismiss]");if(b){document.documentElement.setAttribute("data-blume-banner-hidden","");requestAnimationFrame(dr);const bar=b.closest("[data-blume-banner]");const k=bar&&bar.getAttribute("data-banner-key");if(k){save("blume-banner:"+k,"1");}}});addEventListener("resize",()=>{const d=document.documentElement;if(!d.hasAttribute("data-blume-nav-open")){return;}if(matchMedia("(min-width:64rem)").matches){d.removeAttribute("data-blume-nav-open");}else{dr();}});})();`;
154
185
  ---
155
186
 
156
187
  <header
157
188
  class="sticky top-0 z-40 flex h-16 items-center gap-3 bg-background/90 px-4 backdrop-blur md:px-6"
158
189
  data-blume-header
190
+ data-blume-header-transparent={transparent ? "" : undefined}
159
191
  >
160
192
  {
161
193
  showNavToggle && (
162
194
  <button
195
+ aria-controls="blume-nav-drawer"
196
+ aria-expanded="false"
163
197
  aria-label={n.toggleNavigation}
164
198
  class={`${iconButton} lg:hidden`}
165
199
  data-blume-nav-toggle
@@ -169,7 +203,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
169
203
  </button>
170
204
  )
171
205
  }
172
- <LogoSlot logo={logo} site={site} />
206
+ <LogoSlot locale={locale} logo={logo} site={site} />
173
207
  {
174
208
  navigation.selectors.length > 0 && (
175
209
  <div class="flex items-center gap-1.5">
@@ -229,27 +263,22 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
229
263
  {
230
264
  /* Plain links first, then the one filled button: the reader's eye lands
231
265
  on the button because nothing beside it competes. */
232
- (navigation.actions ?? []).map((action) => {
233
- const external = isExternalUrl(action.href);
234
- return (
235
- <a
236
- class={actionClass}
237
- href={withBase(action.href)}
238
- rel={external ? "noreferrer" : undefined}
239
- target={external ? "_blank" : undefined}
240
- >
241
- {action.label}
242
- </a>
243
- );
244
- })
266
+ (navigation.actions ?? []).map((action) => (
267
+ <a
268
+ class={actionClass}
269
+ href={withBase(action.href)}
270
+ {...newTabAttrs(action.href)}
271
+ >
272
+ {action.label}
273
+ </a>
274
+ ))
245
275
  }
246
276
  {
247
277
  navigation.cta && (
248
278
  <a
249
279
  class={ctaClass}
250
280
  href={withBase(navigation.cta.href)}
251
- rel={isExternalUrl(navigation.cta.href) ? "noreferrer" : undefined}
252
- target={isExternalUrl(navigation.cta.href) ? "_blank" : undefined}
281
+ {...newTabAttrs(navigation.cta.href)}
253
282
  >
254
283
  {navigation.cta.label}
255
284
  <Icon name="chevron-right" size={14} />
@@ -262,8 +291,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
262
291
  aria-label={n.githubRepository}
263
292
  class={iconButton}
264
293
  href={navigation.repoUrl}
265
- rel="noreferrer"
266
- target="_blank"
294
+ {...NEW_TAB_ATTRS}
267
295
  >
268
296
  <svg
269
297
  aria-hidden="true"
@@ -290,3 +318,38 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
290
318
  </div>
291
319
  </header>
292
320
  <script is:inline set:html={clickScript} />
321
+ {transparent && <script is:inline set:html={transparentScript} />}
322
+
323
+ <style is:global>
324
+ /* Transparent header (PageLayout `transparentHeader`): at the top of the
325
+ page the bar has no background and its chrome renders in white so it can
326
+ sit over a dark hero; once scrolled it is the frosted bar again. The color
327
+ tokens are re-declared on the header so every utility inside follows, and
328
+ the search dialog gets the page's own values back so it isn't inverted. */
329
+ :root {
330
+ --blume-header-background: var(--blume-background);
331
+ --blume-header-foreground: var(--blume-foreground);
332
+ --blume-header-muted: var(--blume-muted);
333
+ --blume-header-muted-foreground: var(--blume-muted-foreground);
334
+ --blume-header-border: var(--blume-border);
335
+ }
336
+ [data-blume-header-transparent] {
337
+ transition: background-color 200ms;
338
+ }
339
+ [data-blume-header-transparent]:not([data-blume-header-scrolled]) {
340
+ --blume-background: transparent;
341
+ --blume-foreground: #fff;
342
+ --blume-muted: rgb(255 255 255 / 0.12);
343
+ --blume-muted-foreground: rgb(255 255 255 / 0.75);
344
+ --blume-border: rgb(255 255 255 / 0.25);
345
+ background-color: transparent;
346
+ backdrop-filter: none;
347
+ }
348
+ [data-blume-header-transparent]:not([data-blume-header-scrolled]) dialog {
349
+ --blume-background: var(--blume-header-background);
350
+ --blume-foreground: var(--blume-header-foreground);
351
+ --blume-muted: var(--blume-header-muted);
352
+ --blume-muted-foreground: var(--blume-header-muted-foreground);
353
+ --blume-border: var(--blume-header-border);
354
+ }
355
+ </style>
@@ -11,6 +11,9 @@ interface Props {
11
11
 
12
12
  const { options, label, untranslatedLabel } = Astro.props;
13
13
  const current = options.find((option) => option.current);
14
+ // The accessible name carries the visible language name too (WCAG 2.5.3,
15
+ // label in name), so a voice-control user can say "English" to open it.
16
+ const summaryLabel = current ? `${label}: ${current.label}` : label;
14
17
 
15
18
  const iconButton =
16
19
  "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";
@@ -22,7 +25,7 @@ const menuRowClass =
22
25
  options.length > 1 && (
23
26
  <details class="group relative" data-blume-dropdown>
24
27
  <summary
25
- aria-label={label}
28
+ aria-label={summaryLabel}
26
29
  class={`${iconButton} list-none [&::-webkit-details-marker]:hidden`}
27
30
  >
28
31
  <Icon name="globe" size={16} />
@@ -1,17 +1,26 @@
1
1
  ---
2
+ import data from "blume:data";
2
3
  import { withBase } from "../islands/base-path.ts";
3
4
  // The site brand: the logo mark (inline SVG so a `currentColor` mark follows the
4
5
  // theme, or light/dark `<img>` variants) followed by the site title, wrapped in
5
6
  // a link to the brand href. Extracted from the header so it can be replaced on
6
7
  // its own through the `Logo` layout slot (`defineComponents({ layout: { Logo } })`).
7
8
  import type { BlumeLogo } from "../../core/data.ts";
9
+ import { localizeInternalPath } from "../../core/i18n.ts";
8
10
 
9
11
  interface Props {
10
12
  site: { title: string };
11
13
  logo?: BlumeLogo | null;
14
+ /**
15
+ * Active locale of the page. The brand link follows it the way the header
16
+ * tabs do (`/` -> `/en` under a prefixed locale), so clicking the logo never
17
+ * drops a reader out of the language they were reading. Absent, the link is
18
+ * emitted as configured.
19
+ */
20
+ locale?: string;
12
21
  }
13
22
 
14
- const { site, logo } = Astro.props;
23
+ const { site, logo, locale } = Astro.props;
15
24
 
16
25
  // The logo is resolved upstream: an inline `svg` (so a currentColor mark follows
17
26
  // the theme) or image URL(s) for an <img>.
@@ -21,7 +30,19 @@ const logoDark = logo?.dark ?? logo?.light;
21
30
  const logoLightDimensions = logo?.dimensions?.light;
22
31
  const logoDarkDimensions = logo?.dimensions?.dark ?? logo?.dimensions?.light;
23
32
  const logoAlt = logo?.alt ?? "";
24
- const brandHref = logo?.href ?? "/";
33
+ // The configured `logo.href` (default `/`) is authored in the default locale's
34
+ // path space, like a tab path; localize it for the page's locale. The locale's
35
+ // navigation carries it resolved against the routes the build serves (a custom
36
+ // home page exists only at `/`, so it isn't moved to `/fr`). External and
37
+ // protocol-relative hrefs pass through, and `withBase` layers the deployment
38
+ // base on afterwards, so the locale prefix sits inside the base like every
39
+ // other localized route.
40
+ const configuredHref = logo?.href ?? "/";
41
+ const brandHref =
42
+ locale === undefined
43
+ ? configuredHref
44
+ : (data.navigationByLocale[locale]?.brandHref ??
45
+ localizeInternalPath(configuredHref, locale, data.config.i18n));
25
46
  // Wordmark beside the mark: the configured `logo.text`, or the site title when
26
47
  // it's omitted. An explicit `""` hides it (an image-only logo whose mark already
27
48
  // carries the wordmark).
@@ -33,6 +33,12 @@ const active =
33
33
  .toSorted((a, b) => b.path.length - a.path.length)[0] ??
34
34
  selector.items[0];
35
35
 
36
+ // The active label rides in the trigger on every screen when it's short (a
37
+ // version like "v2" or "Latest"): on a phone a bare chevron doesn't say what
38
+ // it switches. Longer product names still collapse to the icon there.
39
+ const label = active?.label ?? selector.label;
40
+ const labelClass = label.length <= 6 ? undefined : "max-sm:hidden";
41
+
36
42
  const iconButton =
37
43
  "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";
38
44
  const menuRowClass =
@@ -47,7 +53,7 @@ const menuRowClass =
47
53
  class={`${iconButton} list-none [&::-webkit-details-marker]:hidden`}
48
54
  >
49
55
  {active?.icon && <Icon name={active.icon} size={16} />}
50
- <span class="max-sm:hidden">{active?.label ?? selector.label}</span>
56
+ <span class={labelClass}>{label}</span>
51
57
  <Icon
52
58
  class="transition-transform group-open:rotate-180"
53
59
  name="chevron-down"
@@ -55,7 +61,8 @@ const menuRowClass =
55
61
  />
56
62
  </summary>
57
63
  <div
58
- class={`absolute z-50 mt-2 min-w-56 rounded-blume border border-border bg-background p-1 shadow-xl ${align === "end" ? "end-0" : "start-0"}`}
64
+ class={`absolute z-50 mt-2 min-w-56 max-w-[calc(100vw-1rem)] rounded-blume border border-border bg-background p-1 shadow-xl ${align === "end" ? "end-0" : "start-0"}`}
65
+ data-blume-dropdown-panel
59
66
  >
60
67
  {selector.items.map((item) => (
61
68
  <a
@@ -89,8 +96,12 @@ const menuRowClass =
89
96
 
90
97
  <script>
91
98
  import { installDropdownDismiss } from "../dropdown-dismiss.ts";
99
+ import { installDropdownClamp } from "./dropdown-clamp.ts";
92
100
 
93
101
  // Outside-click / Escape / focus-out dismissal, shared with the language
94
102
  // switcher and the page actions.
95
103
  installDropdownDismiss();
104
+ // On a phone the trigger sits mid-header, so an edge-anchored panel can
105
+ // run off the screen; shift it back inside when it opens.
106
+ installDropdownClamp();
96
107
  </script>