blume 2.1.0 → 2.1.1

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 (397) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/{chunk-5cg2rzgf.js → chunk-073rdvgb.js} +19 -48
  3. package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-073rdvgb.js.map} +4 -5
  4. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  5. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  6. package/dist/cli/chunk-150jk7km.js +52 -0
  7. package/dist/cli/chunk-150jk7km.js.map +10 -0
  8. package/dist/cli/{chunk-rekqz4qg.js → chunk-313vnn40.js} +16 -15
  9. package/dist/cli/chunk-313vnn40.js.map +10 -0
  10. package/dist/cli/{chunk-gp84za0h.js → chunk-3v3ffbrj.js} +2 -2
  11. package/dist/cli/{chunk-9xr4dgee.js → chunk-3x6x3pph.js} +29 -16
  12. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  13. package/dist/cli/{chunk-0gz36akr.js → chunk-42t4jxdx.js} +21 -5
  14. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  15. package/dist/cli/{chunk-ta27feev.js → chunk-54ztw1nj.js} +21 -2
  16. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  17. package/dist/cli/{chunk-jnwj5kjj.js → chunk-5vjtgs6e.js} +20 -7
  18. package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-5vjtgs6e.js.map} +3 -3
  19. package/dist/cli/chunk-66w9j7p6.js +322 -0
  20. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  21. package/dist/cli/{chunk-nd1821sk.js → chunk-7ppf1rva.js} +32 -18
  22. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  23. package/dist/cli/{chunk-p804f3pe.js → chunk-8y2kmjv8.js} +6 -6
  24. package/dist/cli/{chunk-2f7n4qzt.js → chunk-c1y538kd.js} +1 -1
  25. package/dist/cli/{chunk-1n228xqb.js → chunk-c887fj0s.js} +2 -2
  26. package/dist/cli/{chunk-y5h4r2y0.js → chunk-cfyg47zg.js} +15 -5
  27. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  28. package/dist/cli/{chunk-cmtcnh9f.js → chunk-dzwehpy7.js} +2 -2
  29. package/dist/cli/{chunk-h2a48yhd.js → chunk-e6atmn4h.js} +119 -15
  30. package/dist/cli/{chunk-h2a48yhd.js.map → chunk-e6atmn4h.js.map} +10 -10
  31. package/dist/cli/{chunk-5ffywsmb.js → chunk-e8xw9qkh.js} +8 -6
  32. package/dist/cli/{chunk-5ffywsmb.js.map → chunk-e8xw9qkh.js.map} +3 -3
  33. package/dist/cli/{chunk-ntv4qrt7.js → chunk-e9q7a2ak.js} +14 -14
  34. package/dist/cli/{chunk-xkz5xbtv.js → chunk-ehweg4fr.js} +2366 -1114
  35. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  36. package/dist/cli/{chunk-zf748avv.js → chunk-ep6nd0rn.js} +176 -281
  37. package/dist/cli/{chunk-zf748avv.js.map → chunk-ep6nd0rn.js.map} +11 -12
  38. package/dist/cli/{chunk-yxmk4pec.js → chunk-fdrkhpjy.js} +16 -15
  39. package/dist/cli/{chunk-yxmk4pec.js.map → chunk-fdrkhpjy.js.map} +3 -3
  40. package/dist/cli/{chunk-5wj2qwn2.js → chunk-ft9vfys6.js} +4 -4
  41. package/dist/cli/{chunk-4h8z9vv5.js → chunk-gb420vvz.js} +89 -46
  42. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  43. package/dist/cli/{chunk-713mngdq.js → chunk-hebn0tpx.js} +76 -10
  44. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  45. package/dist/cli/{chunk-2n4ps1b3.js → chunk-htrwgabr.js} +40 -22
  46. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  47. package/dist/cli/{chunk-zhvd6adm.js → chunk-hx3xqnk9.js} +13 -13
  48. package/dist/cli/chunk-j134vrp2.js +65 -0
  49. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  50. package/dist/cli/{chunk-y4ykbark.js → chunk-kma9f6sf.js} +2 -2
  51. package/dist/cli/{chunk-py62fewh.js → chunk-mrcccgnh.js} +3 -3
  52. package/dist/cli/{chunk-2f3ndkpw.js → chunk-n0v7m8ry.js} +3 -3
  53. package/dist/cli/{chunk-43zcf4gz.js → chunk-ncj7w596.js} +522 -353
  54. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  55. package/dist/cli/{chunk-9geymqns.js → chunk-nq1d20w2.js} +14 -14
  56. package/dist/cli/{chunk-g2z9x01v.js → chunk-nrns10mj.js} +3 -3
  57. package/dist/cli/{chunk-e7vvk5ya.js → chunk-sds923fa.js} +3 -3
  58. package/dist/cli/{chunk-cy5bm0j0.js → chunk-t2m5j4pr.js} +2 -2
  59. package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-t2m5j4pr.js.map} +1 -1
  60. package/dist/cli/{chunk-yj17vs39.js → chunk-tdqhyxfz.js} +5 -5
  61. package/dist/cli/{chunk-q3wxxq39.js → chunk-tphac74f.js} +5 -5
  62. package/dist/cli/{chunk-6bm0xpgd.js → chunk-w1r4vase.js} +2 -2
  63. package/dist/cli/index.js +24 -19
  64. package/dist/cli/index.js.map +3 -3
  65. package/dist/types/ai/ask-context.d.ts +6 -2
  66. package/dist/types/ai/component-markdown.d.ts +2 -0
  67. package/dist/types/ai/openapi-components.d.ts +4 -1
  68. package/dist/types/astro/integration.d.ts +5 -4
  69. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  70. package/dist/types/astro/pages.d.ts +10 -0
  71. package/dist/types/cli/init/scaffold.d.ts +1 -1
  72. package/dist/types/components/content/api-field.d.ts +4 -1
  73. package/dist/types/components/content/api-page.d.ts +6 -1
  74. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  75. package/dist/types/components/openapi/helpers.d.ts +22 -1
  76. package/dist/types/components/openapi/security.d.ts +10 -0
  77. package/dist/types/components/openapi/snippets.d.ts +60 -0
  78. package/dist/types/core/changelog-index.d.ts +8 -0
  79. package/dist/types/core/config-input.d.ts +30 -3
  80. package/dist/types/core/data.d.ts +4 -0
  81. package/dist/types/core/i18n-ui.d.ts +2 -0
  82. package/dist/types/core/links.d.ts +8 -0
  83. package/dist/types/core/probe.d.ts +2 -0
  84. package/dist/types/core/project-graph.d.ts +6 -0
  85. package/dist/types/core/redirect-patterns.d.ts +11 -3
  86. package/dist/types/core/schema.d.ts +119 -0
  87. package/dist/types/core/sources/cache.d.ts +3 -0
  88. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  89. package/dist/types/core/sources/contentful.d.ts +17 -1
  90. package/dist/types/core/sources/lexical.d.ts +5 -0
  91. package/dist/types/core/sources/normalize.d.ts +20 -1
  92. package/dist/types/core/sources/remote.d.ts +37 -10
  93. package/dist/types/core/sources/sanity.d.ts +1 -1
  94. package/dist/types/core/types.d.ts +17 -0
  95. package/dist/types/deploy/adapter-output.d.ts +2 -1
  96. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  97. package/dist/types/deploy/adapters/node.d.ts +58 -23
  98. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  99. package/dist/types/deploy/adapters/types.d.ts +3 -2
  100. package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
  101. package/dist/types/deploy/platforms/node.d.ts +3 -1
  102. package/dist/types/deploy/platforms/types.d.ts +18 -7
  103. package/dist/types/deploy/platforms/vercel.d.ts +20 -1
  104. package/dist/types/deploy/redirects.d.ts +17 -1
  105. package/dist/types/deploy/sitemap.d.ts +2 -1
  106. package/dist/types/deploy/vercel-negotiation.d.ts +42 -21
  107. package/dist/types/openapi/checks.d.ts +27 -0
  108. package/dist/types/openapi/graphql.d.ts +6 -0
  109. package/dist/types/openapi/model.d.ts +15 -4
  110. package/dist/types/openapi/parse.d.ts +3 -0
  111. package/dist/types/openapi/references.d.ts +16 -0
  112. package/dist/types/ratelimit/index.d.ts +1 -1
  113. package/dist/types/ratelimit/schema.d.ts +8 -0
  114. package/dist/types/ratelimit/upstash.d.ts +39 -14
  115. package/dist/types/reference/graphql.d.ts +76 -1
  116. package/dist/types/reference/index.d.ts +1 -1
  117. package/dist/types/reference/openapi.d.ts +2 -2
  118. package/dist/types/reference/options.d.ts +37 -1
  119. package/dist/types/reference/schema.d.ts +84 -0
  120. package/dist/types/search/adapters/registry.d.ts +3 -0
  121. package/dist/types/search/adapters/typesense.d.ts +14 -4
  122. package/dist/types/search/build.d.ts +3 -1
  123. package/dist/types/search/orama-index.d.ts +9 -9
  124. package/dist/types/search/sync/algolia.d.ts +5 -2
  125. package/dist/types/search/sync/index.d.ts +3 -1
  126. package/dist/types/search/sync/typesense.d.ts +11 -7
  127. package/dist/types/sources/contentful.d.ts +16 -0
  128. package/dist/types/sources/github-releases.d.ts +8 -1
  129. package/dist/types/sources/payload.d.ts +2 -0
  130. package/dist/types/sources/registry.d.ts +54 -0
  131. package/dist/types/sources/shared.d.ts +3 -0
  132. package/dist/types/sources/strapi.d.ts +2 -0
  133. package/docs/01-quickstart.mdx +4 -2
  134. package/docs/02-deployment.mdx +31 -13
  135. package/docs/advanced/changelog.mdx +11 -0
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +30 -7
  138. package/docs/cli/doctor.mdx +5 -1
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/translate.mdx +1 -0
  141. package/docs/cli/validate.mdx +4 -3
  142. package/docs/configuration/analytics.mdx +1 -1
  143. package/docs/configuration/assistant.mdx +6 -2
  144. package/docs/configuration/customization.mdx +4 -2
  145. package/docs/configuration/export.mdx +5 -3
  146. package/docs/configuration/index.mdx +3 -2
  147. package/docs/configuration/rate-limiting.mdx +23 -2
  148. package/docs/configuration/search.mdx +37 -14
  149. package/docs/content/components.mdx +2 -2
  150. package/docs/content/frontmatter.mdx +5 -3
  151. package/docs/content/i18n.mdx +2 -2
  152. package/docs/content/includes.mdx +1 -1
  153. package/docs/content/navigation.mdx +4 -4
  154. package/docs/content/sources/contentful.mdx +7 -4
  155. package/docs/content/sources/custom.mdx +7 -3
  156. package/docs/content/sources/github-releases.mdx +10 -2
  157. package/docs/content/sources/index.mdx +3 -3
  158. package/docs/content/sources/notion.mdx +1 -1
  159. package/docs/content/sources/obsidian.mdx +1 -1
  160. package/docs/content/sources/payload.mdx +3 -2
  161. package/docs/content/sources/remote-mdx.mdx +4 -0
  162. package/docs/content/sources/sanity.mdx +2 -2
  163. package/docs/content/sources/strapi.mdx +3 -2
  164. package/docs/content/syntax.mdx +2 -2
  165. package/docs/discoverability/agent-discovery.mdx +1 -1
  166. package/docs/discoverability/index.mdx +1 -1
  167. package/docs/discoverability/json-api.mdx +2 -0
  168. package/docs/discoverability/llms-txt.mdx +1 -1
  169. package/docs/discoverability/markdown.mdx +3 -1
  170. package/docs/discoverability/mcp.mdx +4 -0
  171. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  172. package/docs/references/api-pages.mdx +1 -1
  173. package/docs/references/asyncapi.mdx +3 -1
  174. package/docs/references/graphql.mdx +13 -1
  175. package/docs/references/openapi.mdx +50 -8
  176. package/package.json +9 -9
  177. package/skills/blume-migrate/SKILL.md +1 -1
  178. package/skills/blume-migrate/references/docusaurus.md +4 -4
  179. package/skills/blume-migrate/references/fumadocs.md +17 -13
  180. package/skills/blume-migrate/references/nextra.md +8 -5
  181. package/skills/blume-migrate/references/starlight.md +9 -7
  182. package/src/ai/ask-context.ts +33 -3
  183. package/src/ai/ask-limits.ts +13 -0
  184. package/src/ai/changelog-markdown.ts +11 -6
  185. package/src/ai/component-markdown.ts +1 -1
  186. package/src/ai/cors.ts +12 -3
  187. package/src/ai/mcp/query.ts +14 -6
  188. package/src/ai/openapi-components.ts +28 -4
  189. package/src/ai/relative-links.ts +4 -38
  190. package/src/astro/generate.ts +78 -48
  191. package/src/astro/integration.ts +16 -13
  192. package/src/astro/markdown-negotiation.ts +3 -0
  193. package/src/astro/module-types.ts +3 -1
  194. package/src/astro/pages.ts +23 -0
  195. package/src/astro/templates.ts +125 -80
  196. package/src/audit/catalog.ts +19 -3
  197. package/src/audit/checks/links.ts +14 -1
  198. package/src/audit/checks/network.ts +110 -6
  199. package/src/audit/checks/redirects.ts +6 -4
  200. package/src/audit/checks/robots.ts +58 -2
  201. package/src/audit/run.ts +3 -0
  202. package/src/audit/types.ts +2 -0
  203. package/src/audit/url.ts +14 -5
  204. package/src/blume-modules.d.ts +5 -1
  205. package/src/cli/args.ts +51 -0
  206. package/src/cli/build-failure.ts +23 -0
  207. package/src/cli/commands/audit.ts +10 -1
  208. package/src/cli/commands/dev.ts +18 -2
  209. package/src/cli/commands/doctor.ts +45 -12
  210. package/src/cli/commands/eval.ts +36 -4
  211. package/src/cli/commands/init.ts +6 -4
  212. package/src/cli/commands/validate.ts +10 -1
  213. package/src/cli/init/scaffold.ts +3 -3
  214. package/src/cli/prepare.ts +6 -3
  215. package/src/cli/required-secrets.ts +1 -1
  216. package/src/components/content/ApiPlayground.astro +6 -2
  217. package/src/components/content/api-field.ts +4 -1
  218. package/src/components/content/api-page.ts +31 -7
  219. package/src/components/content/mermaid-element.ts +15 -1
  220. package/src/components/islands/hooks.ts +27 -1
  221. package/src/components/layout/PageActions.astro +139 -24
  222. package/src/components/layout/RootLayout.astro +10 -2
  223. package/src/components/layout/Search.astro +17 -3
  224. package/src/components/layout/TableOfContents.astro +24 -17
  225. package/src/components/layout/head-scripts.ts +6 -1
  226. package/src/components/layout/search/algolia.ts +10 -1
  227. package/src/components/layout/search/analytics.ts +42 -14
  228. package/src/components/layout/search/endpoint.ts +48 -5
  229. package/src/components/layout/search/pagefind.ts +114 -5
  230. package/src/components/layout/search/types.ts +12 -0
  231. package/src/components/layout/search/typesense.ts +18 -6
  232. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  233. package/src/components/openapi/Authorization.astro +10 -0
  234. package/src/components/openapi/GraphqlOperation.astro +5 -0
  235. package/src/components/openapi/Operation.astro +24 -1
  236. package/src/components/openapi/ParametersTable.astro +8 -4
  237. package/src/components/openapi/Playground.astro +7 -1
  238. package/src/components/openapi/RequestBody.astro +19 -2
  239. package/src/components/openapi/Responses.astro +1 -0
  240. package/src/components/openapi/SchemaTable.astro +15 -3
  241. package/src/components/openapi/async-snippets.ts +61 -5
  242. package/src/components/openapi/async.ts +123 -29
  243. package/src/components/openapi/graphql-helpers.ts +35 -12
  244. package/src/components/openapi/helpers.ts +36 -2
  245. package/src/components/openapi/message-model.ts +54 -11
  246. package/src/components/openapi/message.ts +14 -4
  247. package/src/components/openapi/operation-model.ts +16 -7
  248. package/src/components/openapi/playground-client.ts +24 -5
  249. package/src/components/openapi/playground-schema.ts +1 -1
  250. package/src/components/openapi/schema-tree.ts +44 -14
  251. package/src/components/openapi/security.ts +10 -0
  252. package/src/components/openapi/snippets.ts +5 -3
  253. package/src/components/openapi/validate-json.ts +4 -3
  254. package/src/core/changelog-index.ts +31 -1
  255. package/src/core/component-diagnostics.ts +53 -0
  256. package/src/core/component-overrides.ts +32 -0
  257. package/src/core/config-input.ts +31 -3
  258. package/src/core/data.ts +2 -0
  259. package/src/core/diagnostics.ts +5 -0
  260. package/src/core/i18n-ui.ts +2 -0
  261. package/src/core/includes.ts +1 -1
  262. package/src/core/links.ts +47 -17
  263. package/src/core/probe.ts +26 -6
  264. package/src/core/project-graph.ts +59 -18
  265. package/src/core/redirect-patterns.ts +36 -7
  266. package/src/core/schema.ts +21 -2
  267. package/src/core/sources/cache.ts +6 -0
  268. package/src/core/sources/contentful-rich-text.ts +29 -6
  269. package/src/core/sources/contentful.ts +135 -25
  270. package/src/core/sources/filesystem.ts +26 -4
  271. package/src/core/sources/github-releases.ts +12 -1
  272. package/src/core/sources/lexical.ts +15 -10
  273. package/src/core/sources/mdx-remote.ts +62 -10
  274. package/src/core/sources/normalize.ts +227 -41
  275. package/src/core/sources/notion.ts +7 -1
  276. package/src/core/sources/obsidian.ts +16 -3
  277. package/src/core/sources/payload.ts +38 -15
  278. package/src/core/sources/remote.ts +72 -15
  279. package/src/core/sources/resolve.ts +11 -1
  280. package/src/core/sources/sanity.ts +43 -25
  281. package/src/core/sources/strapi.ts +2 -2
  282. package/src/core/types.ts +18 -0
  283. package/src/core/ui-packs/ar.ts +1 -0
  284. package/src/core/ui-packs/bg.ts +1 -0
  285. package/src/core/ui-packs/bn.ts +1 -0
  286. package/src/core/ui-packs/ca.ts +1 -0
  287. package/src/core/ui-packs/cs.ts +1 -0
  288. package/src/core/ui-packs/da.ts +1 -0
  289. package/src/core/ui-packs/de.ts +1 -0
  290. package/src/core/ui-packs/el.ts +1 -0
  291. package/src/core/ui-packs/es.ts +1 -0
  292. package/src/core/ui-packs/fa.ts +1 -0
  293. package/src/core/ui-packs/fi.ts +1 -0
  294. package/src/core/ui-packs/fr.ts +1 -0
  295. package/src/core/ui-packs/he.ts +1 -0
  296. package/src/core/ui-packs/hi.ts +1 -0
  297. package/src/core/ui-packs/hr.ts +1 -0
  298. package/src/core/ui-packs/hu.ts +1 -0
  299. package/src/core/ui-packs/id.ts +1 -0
  300. package/src/core/ui-packs/it.ts +1 -0
  301. package/src/core/ui-packs/ja.ts +1 -0
  302. package/src/core/ui-packs/ko.ts +1 -0
  303. package/src/core/ui-packs/nl.ts +1 -0
  304. package/src/core/ui-packs/no.ts +1 -0
  305. package/src/core/ui-packs/pl.ts +1 -0
  306. package/src/core/ui-packs/pt-br.ts +1 -0
  307. package/src/core/ui-packs/pt.ts +1 -0
  308. package/src/core/ui-packs/ro.ts +1 -0
  309. package/src/core/ui-packs/ru.ts +1 -0
  310. package/src/core/ui-packs/sk.ts +1 -0
  311. package/src/core/ui-packs/sr.ts +1 -0
  312. package/src/core/ui-packs/sv.ts +1 -0
  313. package/src/core/ui-packs/th.ts +1 -0
  314. package/src/core/ui-packs/tr.ts +1 -0
  315. package/src/core/ui-packs/uk.ts +1 -0
  316. package/src/core/ui-packs/vi.ts +1 -0
  317. package/src/core/ui-packs/zh-tw.ts +1 -0
  318. package/src/core/ui-packs/zh.ts +1 -0
  319. package/src/deploy/adapter-output.ts +7 -2
  320. package/src/deploy/adapters/cloudflare.ts +7 -5
  321. package/src/deploy/adapters/node.ts +47 -9
  322. package/src/deploy/adapters/types.ts +7 -3
  323. package/src/deploy/artifacts.ts +2 -1
  324. package/src/deploy/platforms/cloudflare.ts +13 -9
  325. package/src/deploy/platforms/netlify.ts +2 -1
  326. package/src/deploy/platforms/node.ts +12 -2
  327. package/src/deploy/platforms/types.ts +21 -7
  328. package/src/deploy/platforms/vercel.ts +113 -16
  329. package/src/deploy/redirects.ts +66 -6
  330. package/src/deploy/rss.ts +3 -1
  331. package/src/deploy/sitemap.ts +39 -5
  332. package/src/deploy/vercel-negotiation.ts +290 -102
  333. package/src/eval/agents.ts +18 -1
  334. package/src/eval/prompts.ts +6 -3
  335. package/src/eval/report.ts +34 -13
  336. package/src/eval/run.ts +5 -0
  337. package/src/markdown/relative-links.ts +3 -2
  338. package/src/openapi/checks.ts +273 -0
  339. package/src/openapi/graphql-build.ts +21 -0
  340. package/src/openapi/graphql.ts +11 -0
  341. package/src/openapi/model.ts +15 -3
  342. package/src/openapi/parse.ts +127 -2
  343. package/src/openapi/references.ts +43 -3
  344. package/src/openapi/source.ts +39 -14
  345. package/src/ratelimit/index.ts +1 -1
  346. package/src/ratelimit/runtime.ts +37 -19
  347. package/src/ratelimit/upstash.ts +40 -10
  348. package/src/reference/graphql.ts +15 -1
  349. package/src/reference/index.ts +1 -0
  350. package/src/reference/openapi.ts +2 -2
  351. package/src/reference/options.ts +29 -1
  352. package/src/registry/eject.ts +3 -1
  353. package/src/search/adapters/locale-scope.ts +30 -0
  354. package/src/search/adapters/typesense.ts +14 -4
  355. package/src/search/build.ts +21 -3
  356. package/src/search/orama-index.ts +163 -31
  357. package/src/search/source-pages.ts +29 -0
  358. package/src/search/sync/algolia.ts +99 -9
  359. package/src/search/sync/index.ts +52 -13
  360. package/src/search/sync/typesense.ts +72 -27
  361. package/src/sources/contentful.ts +12 -0
  362. package/src/sources/github-releases.ts +7 -1
  363. package/src/sources/shared.ts +3 -0
  364. package/src/theme/entry.ts +29 -3
  365. package/src/translate/validate.ts +23 -1
  366. package/dist/cli/chunk-0gz36akr.js.map +0 -12
  367. package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
  368. package/dist/cli/chunk-43zcf4gz.js.map +0 -36
  369. package/dist/cli/chunk-4ejxbzee.js +0 -39
  370. package/dist/cli/chunk-4ejxbzee.js.map +0 -10
  371. package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
  372. package/dist/cli/chunk-713mngdq.js.map +0 -14
  373. package/dist/cli/chunk-9xr4dgee.js.map +0 -10
  374. package/dist/cli/chunk-amrb6hgh.js +0 -145
  375. package/dist/cli/chunk-amrb6hgh.js.map +0 -11
  376. package/dist/cli/chunk-nd1821sk.js.map +0 -10
  377. package/dist/cli/chunk-rekqz4qg.js.map +0 -10
  378. package/dist/cli/chunk-ta27feev.js.map +0 -10
  379. package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
  380. package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
  381. /package/dist/cli/{chunk-gp84za0h.js.map → chunk-3v3ffbrj.js.map} +0 -0
  382. /package/dist/cli/{chunk-p804f3pe.js.map → chunk-8y2kmjv8.js.map} +0 -0
  383. /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-c1y538kd.js.map} +0 -0
  384. /package/dist/cli/{chunk-1n228xqb.js.map → chunk-c887fj0s.js.map} +0 -0
  385. /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-dzwehpy7.js.map} +0 -0
  386. /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-e9q7a2ak.js.map} +0 -0
  387. /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-ft9vfys6.js.map} +0 -0
  388. /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-hx3xqnk9.js.map} +0 -0
  389. /package/dist/cli/{chunk-y4ykbark.js.map → chunk-kma9f6sf.js.map} +0 -0
  390. /package/dist/cli/{chunk-py62fewh.js.map → chunk-mrcccgnh.js.map} +0 -0
  391. /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-n0v7m8ry.js.map} +0 -0
  392. /package/dist/cli/{chunk-9geymqns.js.map → chunk-nq1d20w2.js.map} +0 -0
  393. /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-nrns10mj.js.map} +0 -0
  394. /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-sds923fa.js.map} +0 -0
  395. /package/dist/cli/{chunk-yj17vs39.js.map → chunk-tdqhyxfz.js.map} +0 -0
  396. /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-tphac74f.js.map} +0 -0
  397. /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-w1r4vase.js.map} +0 -0
@@ -868,13 +868,20 @@ const localeRedirect = localeTargets
868
868
  />
869
869
  )
870
870
  }
871
+ {/* Below `xl`, where the rail is hidden, the page actions ride in
872
+ the "On this page" dropdown, so it renders wherever the rail
873
+ would, with or without headings. They render once, in the rail:
874
+ the dropdown gets an empty mount the actions script moves them
875
+ into (see PageActions.astro). */}
871
876
  {
872
- hasToc && (
877
+ showToc && (
873
878
  <TableOfContentsSlot
874
879
  headings={tocHeadings}
875
880
  title={strings.toc.title}
876
881
  variant="mobile"
877
- />
882
+ >
883
+ <div data-blume-page-actions-mount />
884
+ </TableOfContentsSlot>
878
885
  )
879
886
  }
880
887
  <PageHeaderSlot headings={tocHeadings} page={page} route={page.route} />
@@ -911,6 +918,7 @@ const localeRedirect = localeTargets
911
918
  (apiRail || modeLayout.width === "full") && "[&>*]:max-w-none",
912
919
  modeLayout.width === "center" && "[&>*]:max-w-[54rem]",
913
920
  ]}
921
+ data-blume-page-end
914
922
  >
915
923
  {directoryGroup && <Directory group={directoryGroup} />}
916
924
  <RelatedPages links={relatedItems} title={strings.page.related} />
@@ -4,6 +4,7 @@ import data from "blume:data";
4
4
  import { EN_UI } from "../../core/i18n-ui.ts";
5
5
  import type { UIStrings } from "../../core/i18n-ui.ts";
6
6
  import type { Navigation } from "../../core/types.ts";
7
+ import { scopesLocales } from "../../search/adapters/locale-scope.ts";
7
8
  import { scopesVersions } from "../../search/adapters/version-scope.ts";
8
9
  import { resolvePopularIconMarkup } from "../../search/popular-icon.ts";
9
10
  import Icon from "../Icon.astro";
@@ -36,9 +37,15 @@ const {
36
37
  navigation,
37
38
  popularPages,
38
39
  strings,
39
- locale,
40
+ locale: viewedLocale,
40
41
  version: viewedVersion = null,
41
42
  } = Astro.props;
43
+ // Mixedbread searches every language whatever the dialog asks, so on it an
44
+ // "All languages" toggle would change nothing: treat the site as
45
+ // single-language for search instead.
46
+ const locale = scopesLocales(data.config.search.provider)
47
+ ? viewedLocale
48
+ : undefined;
42
49
  // Pagefind, Orama Cloud, and Mixedbread search every version whatever the
43
50
  // dialog asks, so on those an "All versions" toggle would change nothing:
44
51
  // treat the site as unversioned for search instead.
@@ -90,6 +97,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
90
97
  data-i18n-popular={s.popular}
91
98
  data-i18n-results={s.results}
92
99
  data-locale={locale || undefined}
100
+ data-redact-queries={data.config.search.analytics.queries ? undefined : ""}
93
101
  data-version={version ?? undefined}
94
102
  data-versioned={version === null ? undefined : ""}
95
103
  >
@@ -240,6 +248,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
240
248
 
241
249
  import { track } from "./analytics-client.ts";
242
250
  import { createSearchTracker } from "./search/analytics.ts";
251
+ import type { SearchTracker } from "./search/analytics.ts";
243
252
  import { highlight, matchSnippet } from "./search/types.ts";
244
253
  import type { SearchFn, SearchHit } from "./search/types.ts";
245
254
 
@@ -306,7 +315,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
306
315
  searchClient ??= (async () => {
307
316
  try {
308
317
  const { createSearch } = await import("blume:search-client");
309
- return await createSearch();
318
+ // The reader types each query, so each supersedes the last.
319
+ return await createSearch({ typing: true });
310
320
  } catch (error) {
311
321
  searchClient = undefined;
312
322
  throw error;
@@ -325,7 +335,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
325
335
  preview!: HTMLElement;
326
336
  searchFn: SearchFn | null = null;
327
337
  /** Records settled queries and picked results (search/analytics.ts). */
328
- tracker = createSearchTracker(track);
338
+ tracker!: SearchTracker;
329
339
  loaded = false;
330
340
  loadFailed = false;
331
341
  /** First-open client/index load still in flight. */
@@ -419,6 +429,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
419
429
  this.message = this.querySelector("[data-blume-search-message]")!;
420
430
  this.preview = this.querySelector("[data-blume-search-preview]")!;
421
431
  this.assistantEnabled = this.hasAttribute("data-ask");
432
+ // `search.analytics.queries: false` keeps query text off providers.
433
+ this.tracker = createSearchTracker(track, {
434
+ queries: !this.hasAttribute("data-redact-queries"),
435
+ });
422
436
 
423
437
  const popularScript = this.querySelector(
424
438
  "[data-blume-search-popular]"
@@ -15,10 +15,14 @@ interface Props {
15
15
  }
16
16
 
17
17
  const { headings, title, variant } = Astro.props;
18
+ // The mobile dropdown also holds what the layout passes as children (the
19
+ // mount the page actions move into below `xl`, where the rail that renders
20
+ // them is hidden), so a page with actions but no headings still gets it.
21
+ const hasChildren = Astro.slots.has("default");
18
22
  ---
19
23
 
20
24
  {
21
- headings.length > 0 && variant === "mobile" && (
25
+ (headings.length > 0 || hasChildren) && variant === "mobile" && (
22
26
  <details class="group mx-auto mb-6 max-w-content rounded-blume border border-border xl:hidden">
23
27
  <summary class="flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 font-medium text-foreground text-sm [&::-webkit-details-marker]:hidden">
24
28
  <span>{title}</span>
@@ -28,23 +32,26 @@ const { headings, title, variant } = Astro.props;
28
32
  size={16}
29
33
  />
30
34
  </summary>
31
- <blume-toc class="block">
32
- <ul class="m-0 list-none border-border border-t p-2">
33
- {headings.map((heading) => (
34
- <li
35
- data-depth={heading.depth}
36
- style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
37
- >
38
- <a
39
- class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
40
- href={`#${heading.slug}`}
35
+ {headings.length > 0 && (
36
+ <blume-toc class="block">
37
+ <ul class="m-0 list-none border-border border-t p-2">
38
+ {headings.map((heading) => (
39
+ <li
40
+ data-depth={heading.depth}
41
+ style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
41
42
  >
42
- {heading.text}
43
- </a>
44
- </li>
45
- ))}
46
- </ul>
47
- </blume-toc>
43
+ <a
44
+ class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
45
+ href={`#${heading.slug}`}
46
+ >
47
+ {heading.text}
48
+ </a>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ </blume-toc>
53
+ )}
54
+ <slot />
48
55
  </details>
49
56
  )
50
57
  }
@@ -44,8 +44,13 @@
44
44
  * stored preference instead of aborting before the swap listeners exist, and
45
45
  * after a swap the theme the `before-swap` stamp carried over stands, so a
46
46
  * toggle the reader made still holds for the rest of their visit.
47
+ *
48
+ * Paper is white, so a dark page prints light: `beforeprint` switches the root
49
+ * to the light theme (every token, Shiki color, and `dark:` utility follows
50
+ * it) and `afterprint` puts the dark theme back. The listeners sit on
51
+ * `window`, so the one registration outlives client-router swaps.
47
52
  */
48
- export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{const r=document.documentElement;let s=null;let blocked=false;try{s=localStorage.getItem("blume-theme");}catch{blocked=true;}const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.dataset.theme=s??(blocked&&r.dataset.theme?r.dataset.theme:m==="system"?sys:m);};apply();document.addEventListener("astro:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);})();`;
53
+ export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{const r=document.documentElement;let s=null;let blocked=false;try{s=localStorage.getItem("blume-theme");}catch{blocked=true;}const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.dataset.theme=s??(blocked&&r.dataset.theme?r.dataset.theme:m==="system"?sys:m);};apply();document.addEventListener("astro:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);let printed=null;addEventListener("beforeprint",()=>{const r=document.documentElement;if(r.dataset.theme==="dark"){printed="dark";r.dataset.theme="light";}});addEventListener("afterprint",()=>{if(printed){document.documentElement.dataset.theme=printed;printed=null;}});})();`;
49
54
 
50
55
  /**
51
56
  * Keep an embedded Scalar reference on Blume's theme. Scalar picks its own
@@ -48,7 +48,16 @@ export const createSearch = (opts: AlgoliaOptions): SearchFn => {
48
48
  // shape, so hits returned by that index carry those fields.
49
49
  const records =
50
50
  first && "hits" in first ? (first.hits as AlgoliaRecord[]) : [];
51
- const hits = records.map((record) => ({
51
+ // The sync splits a long page across records that share its `url`. The
52
+ // index's `distinct` returns one of them, unless the site set its own
53
+ // `attributeForDistinct`; either way a page is listed once.
54
+ const pages = new Set<string>();
55
+ const unique = records.filter((record) => {
56
+ const seen = pages.has(record.url);
57
+ pages.add(record.url);
58
+ return !seen;
59
+ });
60
+ const hits = unique.map((record) => ({
52
61
  content: record.content ?? "",
53
62
  excerpt: highlight(
54
63
  excerptFor(record.description ?? "", record.content ?? "", query),
@@ -9,6 +9,10 @@
9
9
  * result is a `search_select` event with its position, for click-through. The
10
10
  * same query settling twice in a row (a filter toggled back and forth) counts
11
11
  * once.
12
+ *
13
+ * With `search.analytics.queries: false`, providers get each query's length
14
+ * (`queryChars`) instead of its text, and the text rides only the
15
+ * `blume:track` DOM event, the way the assistant reports questions.
12
16
  */
13
17
  import type { TrackProps } from "../analytics-client.ts";
14
18
 
@@ -18,8 +22,15 @@ export const MAX_QUERY_CHARS = 100;
18
22
  /** How long a query sits untouched before it counts as settled. */
19
23
  export const SETTLE_MS = 1000;
20
24
 
21
- /** Sends one analytics event. */
22
- export type SendEvent = (event: string, props: TrackProps) => void;
25
+ /**
26
+ * Sends one analytics event; `local` rides only the `blume:track` DOM event
27
+ * (see `track`).
28
+ */
29
+ export type SendEvent = (
30
+ event: string,
31
+ props: TrackProps,
32
+ local?: TrackProps
33
+ ) => void;
23
34
 
24
35
  /** The timer a tracker waits on; injectable for tests. */
25
36
  export interface SettleTimer {
@@ -27,6 +38,14 @@ export interface SettleTimer {
27
38
  start: (callback: () => void, ms: number) => number;
28
39
  }
29
40
 
41
+ /** How a tracker reports. */
42
+ export interface SearchTrackerOptions {
43
+ /** Whether providers receive each query's text (`search.analytics.queries`). */
44
+ queries?: boolean;
45
+ /** The timer a query waits on to settle; injectable for tests. */
46
+ timer?: SettleTimer;
47
+ }
48
+
30
49
  /** What the dialog tells the tracker. */
31
50
  export interface SearchTracker {
32
51
  /** The dialog closed; record the query it was showing. */
@@ -51,23 +70,33 @@ const browserTimer: SettleTimer = {
51
70
  /** A tracker for one search dialog. */
52
71
  export const createSearchTracker = (
53
72
  send: SendEvent,
54
- timer: SettleTimer = browserTimer
73
+ { queries = true, timer = browserTimer }: SearchTrackerOptions = {}
55
74
  ): SearchTracker => {
56
75
  let pending: PendingSearch | undefined;
57
76
  let handle: number | undefined;
58
77
  let last: string | undefined;
59
78
 
79
+ // Providers get the query's text, or only its length, in which case the
80
+ // text goes to the `blume:track` DOM event alone.
81
+ const report = (event: string, props: TrackProps, query: string): void => {
82
+ if (queries) {
83
+ send(event, { ...props, query });
84
+ } else {
85
+ send(event, { ...props, queryChars: query.length }, { query });
86
+ }
87
+ };
88
+
60
89
  const flush = (): void => {
61
90
  if (handle !== undefined) {
62
91
  timer.cancel(handle);
63
92
  handle = undefined;
64
93
  }
65
94
  if (pending && pending.query !== last) {
66
- send("search", {
67
- path: location.pathname,
68
- query: pending.query,
69
- results: pending.results,
70
- });
95
+ report(
96
+ "search",
97
+ { path: location.pathname, results: pending.results },
98
+ pending.query
99
+ );
71
100
  last = pending.query;
72
101
  }
73
102
  pending = undefined;
@@ -80,12 +109,11 @@ export const createSearchTracker = (
80
109
  },
81
110
  selected: (query, position, url) => {
82
111
  flush();
83
- send("search_select", {
84
- path: location.pathname,
85
- position,
86
- query: query.slice(0, MAX_QUERY_CHARS),
87
- url,
88
- });
112
+ report(
113
+ "search_select",
114
+ { path: location.pathname, position, url },
115
+ query.slice(0, MAX_QUERY_CHARS)
116
+ );
89
117
  },
90
118
  settled: (query, results) => {
91
119
  if (handle !== undefined) {
@@ -1,22 +1,64 @@
1
1
  import { highlight, SEARCH_LIMIT } from "./types.ts";
2
- import type { SearchFn, SearchHit } from "./types.ts";
2
+ import type { SearchClientOptions, SearchFn, SearchHit } from "./types.ts";
3
+
4
+ /**
5
+ * How long a typed query waits for the reader to stop typing before it's
6
+ * sent, in milliseconds. The dialog searches on every keystroke, and each
7
+ * request counts toward the endpoint's `rateLimit`.
8
+ */
9
+ export const SEARCH_DEBOUNCE_MS = 250;
10
+
11
+ /** Resolve after `ms`, or reject with the abort reason once `signal` aborts. */
12
+ const pause = (ms: number, signal: AbortSignal): Promise<void> =>
13
+ // oxlint-disable-next-line promise/avoid-new -- adapt setTimeout and the abort event
14
+ new Promise((resolve, reject) => {
15
+ const timer = setTimeout(resolve, ms);
16
+ signal.addEventListener(
17
+ "abort",
18
+ () => {
19
+ clearTimeout(timer);
20
+ reject(signal.reason);
21
+ },
22
+ { once: true }
23
+ );
24
+ });
3
25
 
4
26
  /**
5
27
  * Server-proxied search (Mixedbread): POST the query to a generated endpoint
6
28
  * that holds the secret key and talks to the service. The returned hits carry
7
29
  * service-derived text and the dialog injects title/excerpt as HTML, so both
8
30
  * are escaped (and query matches marked) here, like every other provider.
31
+ * A failed request rejects, as a hosted provider's does, so the dialog shows
32
+ * its error rather than an empty "no results".
33
+ *
34
+ * With `typing` (the search dialog), a query waits {@link SEARCH_DEBOUNCE_MS}
35
+ * before it's sent, and a newer one supersedes it: the earlier call's wait or
36
+ * request is aborted and it rejects with an `AbortError`. Typing a word then
37
+ * costs one request rather than one per letter, and a slow earlier response
38
+ * can't land after a later one. Without it, every call is sent at once.
9
39
  */
10
- export const createSearch =
11
- (opts: { api: string }): SearchFn =>
12
- async (query) => {
40
+ export const createSearch = (
41
+ opts: { api: string } & SearchClientOptions
42
+ ): SearchFn => {
43
+ let pending: AbortController | undefined;
44
+ return async (query) => {
45
+ let signal: AbortSignal | undefined;
46
+ if (opts.typing) {
47
+ pending?.abort();
48
+ pending = new AbortController();
49
+ ({ signal } = pending);
50
+ await pause(SEARCH_DEBOUNCE_MS, signal);
51
+ }
13
52
  const response = await fetch(opts.api, {
14
53
  body: JSON.stringify({ query }),
15
54
  headers: { "Content-Type": "application/json" },
16
55
  method: "POST",
56
+ signal,
17
57
  });
18
58
  if (!response.ok) {
19
- return { hits: [], sections: [] };
59
+ throw new Error(
60
+ `Search failed: ${opts.api} answered ${response.status}.`
61
+ );
20
62
  }
21
63
  // SAFETY: the endpoint is Blume-generated (`search-endpoint` template) and
22
64
  // responds with the SearchHit list it built; title/excerpt are still
@@ -29,3 +71,4 @@ export const createSearch =
29
71
  }));
30
72
  return { hits, sections: [] };
31
73
  };
74
+ };
@@ -9,14 +9,63 @@ interface PagefindResult {
9
9
  }>;
10
10
  }
11
11
 
12
- interface PagefindModule {
12
+ /** One Pagefind instance: a page language's index, plus any merged into it. */
13
+ interface PagefindInstance {
14
+ mergeIndex: (
15
+ url: string,
16
+ options: { baseUrl: string; language: string }
17
+ ) => Promise<void>;
13
18
  search: (query: string) => Promise<{ results: PagefindResult[] }>;
14
19
  }
15
20
 
21
+ interface PagefindModule {
22
+ createInstance: (options: {
23
+ basePath: string;
24
+ baseUrl: string;
25
+ }) => PagefindInstance;
26
+ }
27
+
28
+ /** `pagefind-entry.json`: the bundle's index for each language. */
29
+ interface PagefindEntry {
30
+ languages: Record<string, { page_count: number }>;
31
+ }
32
+
33
+ // Pagefind names each page after its built file (`quickstart/index.html`), so
34
+ // its URLs end in a slash. Blume serves pages without one (`trailingSlash:
35
+ // "never"`): hosts redirect the slashed URL, and `blume preview` 404s it.
36
+ const TRAILING_SLASH = /(?<=.)\/(?=[#?]|$)/u;
37
+
38
+ /**
39
+ * The language whose index Pagefind loads for a page, picked the way its
40
+ * `findIndex` does: the page's own, else its base language (`pt` for
41
+ * `pt-br`), else the language with the most pages (a locale with no
42
+ * translated pages has no index of its own).
43
+ */
44
+ const loadedLanguage = (
45
+ languages: PagefindEntry["languages"],
46
+ language: string
47
+ ): string | undefined => {
48
+ const [base = language] = language.split("-");
49
+ if (languages[language]) {
50
+ return language;
51
+ }
52
+ if (languages[base]) {
53
+ return base;
54
+ }
55
+ return Object.entries(languages).toSorted(
56
+ ([, a], [, b]) => b.page_count - a.page_count
57
+ )[0]?.[0];
58
+ };
59
+
16
60
  /**
17
61
  * Pagefind: load the index emitted into the built site and query it. The bundle
18
62
  * lives in the output (not `node_modules`), so it is imported at runtime by URL
19
63
  * — which is why this only works in the production build, not `dev`.
64
+ *
65
+ * Pagefind keeps an index per language and searches the one for the page's
66
+ * `<html lang>`, which Blume sets to the locale the dialog scopes to. A
67
+ * search scoped to a locale is that index alone; a search across every
68
+ * language (no `locale`) merges every other language's index into it.
20
69
  */
21
70
  export const createSearch = async (opts: {
22
71
  url: string;
@@ -24,25 +73,85 @@ export const createSearch = async (opts: {
24
73
  // The pagefind bundle lives in the built site (not node_modules) and is
25
74
  // resolved at runtime by URL — it can't be a static, code-splittable path.
26
75
  // SAFETY: the URL points at the `pagefind.js` module our own build emitted,
27
- // whose export contract (`search()`) is fixed by pagefind.
76
+ // whose export contract (`createInstance()`) is fixed by pagefind.
28
77
  // oxlint-disable-next-line react-doctor/no-dynamic-import-path
29
78
  const pagefind = (await import(
30
79
  /* @vite-ignore */
31
80
  opts.url
32
81
  )) as PagefindModule;
82
+ // The bundle's folder, which holds the entry file and every language's index.
83
+ const bundle = new URL("./", new URL(opts.url, document.baseURI));
84
+ // `baseUrl` "/" keeps result URLs base-less routes, like every other
85
+ // provider's: by default Pagefind prefixes the folder the bundle is served
86
+ // under, and the dialog adds the deployment base itself.
87
+ const create = (): PagefindInstance =>
88
+ pagefind.createInstance({ basePath: bundle.pathname, baseUrl: "/" });
89
+
90
+ // Every language's index: the page's own, and every other one merged in.
91
+ const everyLanguage = async (language: string): Promise<PagefindInstance> => {
92
+ const instance = create();
93
+ const response = await fetch(new URL("pagefind-entry.json", bundle));
94
+ // SAFETY: the entry file is the one Pagefind wrote into this bundle.
95
+ const { languages } = (await response.json()) as PagefindEntry;
96
+ const own = loadedLanguage(languages, language);
97
+ // Pagefind skips a merge whose path its own `basePath` starts with, as
98
+ // the same index twice, so the bundle is merged by its full URL.
99
+ await Promise.all(
100
+ Object.keys(languages)
101
+ .filter((code) => code !== own)
102
+ .map((code) =>
103
+ instance.mergeIndex(bundle.href, { baseUrl: "/", language: code })
104
+ )
105
+ );
106
+ return instance;
107
+ };
108
+
109
+ // Pagefind reads the page's language once, when an instance is created. A
110
+ // client-side language switch changes the page's language without a
111
+ // reload, so each language gets its own instance, created on its first
112
+ // search: after the switch, search moves to the new page's index. The
113
+ // every-language instance is one for the whole page load.
114
+ const instances = new Map<string, Promise<PagefindInstance>>();
115
+ const cached = (
116
+ key: string,
117
+ load: () => Promise<PagefindInstance>
118
+ ): Promise<PagefindInstance> => {
119
+ const existing = instances.get(key);
120
+ if (existing) {
121
+ return existing;
122
+ }
123
+ const loading = (async () => {
124
+ try {
125
+ return await load();
126
+ } catch (error) {
127
+ // A failed load (the entry file's fetch, say) is forgotten, so the
128
+ // next search retries it.
129
+ instances.delete(key);
130
+ throw error;
131
+ }
132
+ })();
133
+ instances.set(key, loading);
134
+ return loading;
135
+ };
136
+
33
137
  // Pagefind builds its own marked-up excerpt; we keep its `<mark>` highlights
34
138
  // (dropping any other markup — the excerpt is rendered via innerHTML) and
35
139
  // only highlight the title ourselves. It carries no section/breadcrumb data,
36
140
  // so pills stay hidden and the preview pane falls back to the excerpt.
37
- return async (query) => {
38
- const response = await pagefind.search(query);
141
+ return async (query, options) => {
142
+ // Lowercased like Pagefind's own reading, which names the entry's indexes.
143
+ const language = document.documentElement.lang.toLowerCase() || "unknown";
144
+ const instance = await (options?.locale
145
+ ? cached(language, () => Promise.resolve(create()))
146
+ : cached("*", () => everyLanguage(language)));
147
+ const response = await instance.search(query);
39
148
  const docs = await Promise.all(
40
149
  response.results.slice(0, SEARCH_LIMIT).map((result) => result.data())
41
150
  );
42
151
  const hits = docs.map((doc) => ({
43
152
  excerpt: sanitizeExcerpt(doc.excerpt),
44
153
  title: highlight(doc.meta?.title ?? doc.url, query),
45
- url: doc.url,
154
+ url: doc.url.replace(TRAILING_SLASH, ""),
46
155
  }));
47
156
  return { hits, sections: [] };
48
157
  };
@@ -43,6 +43,18 @@ export type SearchFn = (
43
43
  }
44
44
  ) => Promise<SearchResult>;
45
45
 
46
+ /** What a caller tells `createSearch` (`blume:search-client`) about its queries. */
47
+ export interface SearchClientOptions {
48
+ /**
49
+ * The queries come from a reader typing, as in the search dialog, so each
50
+ * one supersedes the last. The server-proxied client (Mixedbread), whose
51
+ * endpoint is rate-limited, then waits for a pause in typing before it
52
+ * sends a query and aborts the one a newer query supersedes. Other
53
+ * providers ignore it.
54
+ */
55
+ typing?: boolean;
56
+ }
57
+
46
58
  /** A document in the client-loaded `blume-search.json` index. */
47
59
  export interface IndexedDocument {
48
60
  route: string;
@@ -17,10 +17,19 @@ interface TypesenseRecord extends Record<string, unknown> {
17
17
  * key. Documents are imported at build time by the sync step. Every adapter
18
18
  * option Blume doesn't read (`connectionTimeoutSeconds`, `numRetries`, …) is
19
19
  * the site's own client option and goes to the `Client` untouched; the named
20
- * connection options still decide `apiKey` and `nodes`.
20
+ * connection options still decide `apiKey` and `nodes`. `locale` is the
21
+ * sync's: Typesense tokenizes a query for the fields it searches.
21
22
  */
22
23
  export const createSearch = (opts: TypesenseOptions): SearchFn => {
23
- const { apiKey, collection, host, port, protocol, ...clientOptions } = opts;
24
+ const {
25
+ apiKey,
26
+ collection,
27
+ host,
28
+ locale: _locale,
29
+ port,
30
+ protocol,
31
+ ...clientOptions
32
+ } = opts;
24
33
  const client = new Client({
25
34
  ...clientOptions,
26
35
  apiKey,
@@ -35,10 +44,13 @@ export const createSearch = (opts: TypesenseOptions): SearchFn => {
35
44
  per_page: SEARCH_LIMIT,
36
45
  q: query,
37
46
  query_by: "title,keywords,description,content",
38
- // Relevance first, in ten bands, then each page's `search.boost`
39
- // within a band: a boost lifts a page past similar matches without
40
- // floating it over clearly better ones.
41
- sort_by: "_text_match(buckets: 10):desc,boost:desc",
47
+ // Relevance first, then each page's `search.boost` among pages that
48
+ // match equally well: a boost settles a tie without floating a page
49
+ // over a better match. Bucketing the relevance instead
50
+ // (`_text_match(buckets: 10)`) ranks by boost alone inside each
51
+ // bucket, so at equal boosts a bucket falls back to indexing order
52
+ // and the best match can land behind weaker ones.
53
+ sort_by: "_text_match:desc,boost:desc",
42
54
  // The sync marks `locale` and `version` as facets so hosted results
43
55
  // scope to the active language and the viewed docs version (the
44
56
  // current docs upload as "current").
@@ -7,11 +7,12 @@ import { asyncApiOperationObject } from "../../openapi/asyncapi.ts";
7
7
  import { operationOf, specOf } from "../../openapi/model.ts";
8
8
  import { highlightCode } from "../../markdown/index.ts";
9
9
  import {
10
- asyncApiSecurityEntries,
10
+ asyncApiSecurity,
11
11
  bindingGroups,
12
12
  channelParameters,
13
- channelServers,
13
+ kafkaTopic,
14
14
  messageLabel,
15
+ namedChannelServers,
15
16
  operationMessages,
16
17
  payloadSchema,
17
18
  protocolOf,
@@ -33,7 +34,6 @@ import PanelTabs from "./PanelTabs.astro";
33
34
  import ParametersTable from "./ParametersTable.astro";
34
35
  import SchemaTable from "./SchemaTable.astro";
35
36
  import type { SecuritySchemeLike } from "./security.ts";
36
- import { resolveAsyncApiSecurity } from "./security.ts";
37
37
  import OperationPanel from "./OperationPanel.astro";
38
38
 
39
39
  /**
@@ -61,9 +61,11 @@ const schemas = (document.components?.schemas ?? {}) as Record<
61
61
  string,
62
62
  SchemaLike
63
63
  >;
64
- const servers = channelServers(channel, document);
65
- const security = resolveAsyncApiSecurity(
66
- asyncApiSecurityEntries(operation, servers),
64
+ const namedServers = namedChannelServers(channel, document);
65
+ const servers = namedServers.map(({ server }) => server);
66
+ const security = asyncApiSecurity(
67
+ operation,
68
+ namedServers,
67
69
  document.components?.securitySchemes as
68
70
  | Record<string, SecuritySchemeLike>
69
71
  | undefined
@@ -96,10 +98,15 @@ const messagePanels = await Promise.all(
96
98
 
97
99
  // One derived model feeds the static default sample, the composer form, and
98
100
  // (client-side) the live samples + WebSocket frame — they can never drift.
101
+ // A Kafka channel binding's `topic` names the real topic when the address
102
+ // doesn't, so the samples use it.
103
+ const topic =
104
+ protocol === "kafka" ? kafkaTopic(channel, document.components) : undefined;
99
105
  const model = ref
100
106
  ? messageModel({
101
107
  action: ref.method === "send" ? "send" : "receive",
102
- address: ref.path,
108
+ address: topic ?? ref.path,
109
+ components: document.components,
103
110
  messages,
104
111
  parameters,
105
112
  protocol,
@@ -28,6 +28,13 @@ const { security } = Astro.props;
28
28
  Optional — this operation also accepts unauthenticated requests.
29
29
  </p>
30
30
  )}
31
+ {security.unauthenticatedServers &&
32
+ security.unauthenticatedServers.length > 0 && (
33
+ <p class="mb-2 text-muted-foreground text-sm">
34
+ No authorization needed on{" "}
35
+ {security.unauthenticatedServers.join(", ")}.
36
+ </p>
37
+ )}
31
38
  {security.alternatives.map((alternative, index) => (
32
39
  <>
33
40
  {index > 0 && (
@@ -47,6 +54,9 @@ const { security } = Astro.props;
47
54
  <span class="text-muted-foreground text-xs">
48
55
  {schemeLabel(resolved)}
49
56
  {carrier ? ` · ${carrier.in}` : ""}
57
+ {resolved.servers
58
+ ? ` · on ${resolved.servers.join(", ")}`
59
+ : ""}
50
60
  </span>
51
61
  {!security.optional && (
52
62
  <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">