blume 2.0.3 → 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 (607) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +142 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-5f86nr5m.js → chunk-073rdvgb.js} +21 -48
  5. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-073rdvgb.js.map} +4 -5
  6. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  7. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  8. package/dist/cli/chunk-150jk7km.js +52 -0
  9. package/dist/cli/chunk-150jk7km.js.map +10 -0
  10. package/dist/cli/{chunk-9t7a85s3.js → chunk-313vnn40.js} +30 -19
  11. package/dist/cli/chunk-313vnn40.js.map +10 -0
  12. package/dist/cli/{chunk-tkacnehg.js → chunk-3v3ffbrj.js} +2 -2
  13. package/dist/cli/{chunk-q58y5e6a.js → chunk-3x6x3pph.js} +46 -24
  14. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  15. package/dist/cli/{chunk-1d7ve1dm.js → chunk-42t4jxdx.js} +37 -5
  16. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  17. package/dist/cli/{chunk-8ktnccpt.js → chunk-54ztw1nj.js} +21 -2
  18. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  19. package/dist/cli/{chunk-hm3vjy5s.js → chunk-5vjtgs6e.js} +28 -10
  20. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-5vjtgs6e.js.map} +4 -4
  21. package/dist/cli/chunk-66w9j7p6.js +322 -0
  22. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  23. package/dist/cli/{chunk-2hsdwb9n.js → chunk-7ppf1rva.js} +34 -18
  24. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  25. package/dist/cli/{chunk-r9rcc4w7.js → chunk-8y2kmjv8.js} +7 -7
  26. package/dist/cli/{chunk-r20tn01b.js → chunk-c1y538kd.js} +8 -2
  27. package/dist/cli/{chunk-r20tn01b.js.map → chunk-c1y538kd.js.map} +3 -3
  28. package/dist/cli/{chunk-2eytanqx.js → chunk-c887fj0s.js} +2 -2
  29. package/dist/cli/{chunk-p3v96n38.js → chunk-cfyg47zg.js} +16 -6
  30. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  31. package/dist/cli/chunk-cm89scdv.js +443 -0
  32. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  33. package/dist/cli/{chunk-akbpwfxc.js → chunk-dzwehpy7.js} +2 -2
  34. package/dist/cli/{chunk-wrr3j9w9.js → chunk-e6atmn4h.js} +299 -72
  35. package/dist/cli/chunk-e6atmn4h.js.map +36 -0
  36. package/dist/cli/{chunk-tzmab476.js → chunk-e8xw9qkh.js} +8 -6
  37. package/dist/cli/{chunk-tzmab476.js.map → chunk-e8xw9qkh.js.map} +3 -3
  38. package/dist/cli/{chunk-6dsbexzp.js → chunk-e9q7a2ak.js} +17 -15
  39. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-e9q7a2ak.js.map} +2 -2
  40. package/dist/cli/{chunk-pv29h0wf.js → chunk-ehweg4fr.js} +5155 -1249
  41. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  42. package/dist/cli/{chunk-c8chx29p.js → chunk-ep6nd0rn.js} +390 -318
  43. package/dist/cli/chunk-ep6nd0rn.js.map +36 -0
  44. package/dist/cli/{chunk-p73c0m7w.js → chunk-fdrkhpjy.js} +18 -15
  45. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-fdrkhpjy.js.map} +3 -3
  46. package/dist/cli/{chunk-2z928egk.js → chunk-ft9vfys6.js} +5 -5
  47. package/dist/cli/{chunk-5m5nmvyq.js → chunk-gb420vvz.js} +316 -79
  48. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  49. package/dist/cli/{chunk-vg9r4eb9.js → chunk-hebn0tpx.js} +86 -13
  50. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  51. package/dist/cli/{chunk-35d4wj9f.js → chunk-htrwgabr.js} +41 -23
  52. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  53. package/dist/cli/{chunk-g4hq16wv.js → chunk-hx3xqnk9.js} +16 -14
  54. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-hx3xqnk9.js.map} +2 -2
  55. package/dist/cli/chunk-j134vrp2.js +65 -0
  56. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  57. package/dist/cli/{chunk-a58773jm.js → chunk-kma9f6sf.js} +2 -2
  58. package/dist/cli/chunk-kwrg63tm.js +31 -0
  59. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  60. package/dist/cli/{chunk-ey84smr6.js → chunk-mrcccgnh.js} +6 -6
  61. package/dist/cli/{chunk-ey84smr6.js.map → chunk-mrcccgnh.js.map} +1 -1
  62. package/dist/cli/{chunk-yfyb25rh.js → chunk-n0v7m8ry.js} +24 -8
  63. package/dist/cli/chunk-n0v7m8ry.js.map +14 -0
  64. package/dist/cli/{chunk-j85vccga.js → chunk-ncj7w596.js} +576 -367
  65. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  66. package/dist/cli/{chunk-5xvm6tfj.js → chunk-nq1d20w2.js} +17 -15
  67. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-nq1d20w2.js.map} +2 -2
  68. package/dist/cli/{chunk-pehfxfta.js → chunk-nrns10mj.js} +6 -98
  69. package/dist/cli/chunk-nrns10mj.js.map +10 -0
  70. package/dist/cli/{chunk-crgn1q09.js → chunk-sds923fa.js} +4 -4
  71. package/dist/cli/chunk-t2m5j4pr.js +104 -0
  72. package/dist/cli/chunk-t2m5j4pr.js.map +11 -0
  73. package/dist/cli/{chunk-acanzt5p.js → chunk-tdqhyxfz.js} +6 -6
  74. package/dist/cli/chunk-tphac74f.js +126 -0
  75. package/dist/cli/chunk-tphac74f.js.map +11 -0
  76. package/dist/cli/chunk-w1r4vase.js +18 -0
  77. package/dist/cli/chunk-w1r4vase.js.map +10 -0
  78. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  79. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  80. package/dist/cli/index.js +31 -20
  81. package/dist/cli/index.js.map +5 -5
  82. package/dist/types/ai/agent-surface.d.ts +8 -7
  83. package/dist/types/ai/ai-catalog.d.ts +7 -0
  84. package/dist/types/ai/ask-context.d.ts +26 -0
  85. package/dist/types/ai/ask.d.ts +271 -58
  86. package/dist/types/ai/component-markdown.d.ts +2 -0
  87. package/dist/types/ai/index.d.ts +2 -2
  88. package/dist/types/ai/openapi-components.d.ts +4 -1
  89. package/dist/types/ai/site-skill.d.ts +26 -0
  90. package/dist/types/ai/skills.d.ts +16 -0
  91. package/dist/types/astro/integration.d.ts +9 -0
  92. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  93. package/dist/types/astro/pages.d.ts +10 -0
  94. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  95. package/dist/types/captcha/index.d.ts +28 -0
  96. package/dist/types/captcha/schema.d.ts +29 -0
  97. package/dist/types/captcha/turnstile.d.ts +28 -0
  98. package/dist/types/cli/init/scaffold.d.ts +1 -1
  99. package/dist/types/components/content/api-field.d.ts +62 -0
  100. package/dist/types/components/content/api-page.d.ts +100 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  103. package/dist/types/components/openapi/helpers.d.ts +158 -0
  104. package/dist/types/components/openapi/security.d.ts +95 -0
  105. package/dist/types/components/openapi/snippets.d.ts +60 -0
  106. package/dist/types/consent/ethyca.d.ts +48 -0
  107. package/dist/types/consent/index.d.ts +37 -0
  108. package/dist/types/consent/native.d.ts +28 -0
  109. package/dist/types/consent/osano.d.ts +42 -0
  110. package/dist/types/consent/schema.d.ts +110 -0
  111. package/dist/types/core/changelog-index.d.ts +8 -0
  112. package/dist/types/core/config-input.d.ts +217 -23
  113. package/dist/types/core/data.d.ts +53 -4
  114. package/dist/types/core/footer.d.ts +8 -0
  115. package/dist/types/core/i18n-ui.d.ts +88 -0
  116. package/dist/types/core/includes.d.ts +8 -2
  117. package/dist/types/core/links.d.ts +8 -0
  118. package/dist/types/core/localizable.d.ts +7 -0
  119. package/dist/types/core/page-modes.d.ts +29 -0
  120. package/dist/types/core/probe.d.ts +2 -0
  121. package/dist/types/core/project-graph.d.ts +6 -0
  122. package/dist/types/core/redirect-patterns.d.ts +152 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +670 -56
  125. package/dist/types/core/sources/cache.d.ts +3 -0
  126. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  127. package/dist/types/core/sources/contentful.d.ts +17 -1
  128. package/dist/types/core/sources/lexical.d.ts +5 -0
  129. package/dist/types/core/sources/normalize.d.ts +20 -1
  130. package/dist/types/core/sources/read.d.ts +5 -0
  131. package/dist/types/core/sources/remote.d.ts +37 -10
  132. package/dist/types/core/sources/sanity.d.ts +1 -1
  133. package/dist/types/core/types.d.ts +18 -1
  134. package/dist/types/core/variables.d.ts +42 -0
  135. package/dist/types/deploy/adapter-output.d.ts +2 -1
  136. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  137. package/dist/types/deploy/adapters/node.d.ts +58 -23
  138. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  139. package/dist/types/deploy/adapters/types.d.ts +3 -2
  140. package/dist/types/deploy/node-headers.d.ts +13 -2
  141. package/dist/types/deploy/platforms/cloudflare.d.ts +15 -7
  142. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  143. package/dist/types/deploy/platforms/node.d.ts +3 -1
  144. package/dist/types/deploy/platforms/types.d.ts +18 -7
  145. package/dist/types/deploy/platforms/vercel.d.ts +26 -1
  146. package/dist/types/deploy/redirects.d.ts +55 -11
  147. package/dist/types/deploy/sitemap.d.ts +2 -1
  148. package/dist/types/deploy/vercel-negotiation.d.ts +51 -21
  149. package/dist/types/markdown/code-title.d.ts +17 -0
  150. package/dist/types/markdown/features.d.ts +3 -2
  151. package/dist/types/narration/build.d.ts +70 -0
  152. package/dist/types/narration/provider.d.ts +26 -0
  153. package/dist/types/narration/script.d.ts +154 -0
  154. package/dist/types/openapi/checks.d.ts +27 -0
  155. package/dist/types/openapi/graphql.d.ts +6 -0
  156. package/dist/types/openapi/model.d.ts +27 -10
  157. package/dist/types/openapi/overlay.d.ts +54 -0
  158. package/dist/types/openapi/parse.d.ts +16 -0
  159. package/dist/types/openapi/references.d.ts +30 -4
  160. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  161. package/dist/types/ratelimit/index.d.ts +27 -0
  162. package/dist/types/ratelimit/memory.d.ts +36 -0
  163. package/dist/types/ratelimit/schema.d.ts +119 -0
  164. package/dist/types/ratelimit/upstash.d.ts +46 -0
  165. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  166. package/dist/types/reference/asyncapi.d.ts +10 -10
  167. package/dist/types/reference/graphql.d.ts +89 -11
  168. package/dist/types/reference/index.d.ts +1 -1
  169. package/dist/types/reference/openapi.d.ts +33 -12
  170. package/dist/types/reference/options.d.ts +82 -6
  171. package/dist/types/reference/scalar.d.ts +13 -0
  172. package/dist/types/reference/schema.d.ts +144 -36
  173. package/dist/types/search/adapters/registry.d.ts +3 -0
  174. package/dist/types/search/adapters/typesense.d.ts +14 -4
  175. package/dist/types/search/build.d.ts +3 -1
  176. package/dist/types/search/documents.d.ts +12 -1
  177. package/dist/types/search/orama-index.d.ts +13 -9
  178. package/dist/types/search/sync/algolia.d.ts +5 -2
  179. package/dist/types/search/sync/index.d.ts +3 -1
  180. package/dist/types/search/sync/typesense.d.ts +11 -7
  181. package/dist/types/seo/metatags.d.ts +20 -0
  182. package/dist/types/sources/contentful.d.ts +16 -0
  183. package/dist/types/sources/github-releases.d.ts +8 -1
  184. package/dist/types/sources/payload.d.ts +2 -0
  185. package/dist/types/sources/registry.d.ts +54 -0
  186. package/dist/types/sources/shared.d.ts +3 -0
  187. package/dist/types/sources/strapi.d.ts +2 -0
  188. package/docs/01-quickstart.mdx +4 -2
  189. package/docs/02-deployment.mdx +71 -17
  190. package/docs/03-upgrading.mdx +4 -4
  191. package/docs/04-migrating.mdx +1 -1
  192. package/docs/advanced/changelog.mdx +13 -2
  193. package/docs/advanced/skills.mdx +15 -1
  194. package/docs/cli/audit.mdx +51 -8
  195. package/docs/cli/doctor.mdx +5 -1
  196. package/docs/cli/evals.mdx +4 -4
  197. package/docs/cli/index.mdx +1 -0
  198. package/docs/cli/translate.mdx +1 -0
  199. package/docs/cli/validate.mdx +4 -3
  200. package/docs/configuration/analytics.mdx +3 -1
  201. package/docs/configuration/assistant.mdx +194 -38
  202. package/docs/configuration/consent.mdx +100 -0
  203. package/docs/configuration/customization.mdx +8 -6
  204. package/docs/configuration/export.mdx +5 -3
  205. package/docs/configuration/index.mdx +64 -5
  206. package/docs/configuration/meta.ts +3 -0
  207. package/docs/configuration/narration.mdx +119 -0
  208. package/docs/configuration/rate-limiting.mdx +90 -0
  209. package/docs/configuration/search.mdx +63 -12
  210. package/docs/configuration/theming.mdx +3 -1
  211. package/docs/content/components.mdx +108 -2
  212. package/docs/content/frontmatter.mdx +80 -2
  213. package/docs/content/i18n.mdx +39 -4
  214. package/docs/content/includes.mdx +16 -0
  215. package/docs/content/meta.mdx +1 -0
  216. package/docs/content/meta.ts +1 -0
  217. package/docs/content/navigation.mdx +32 -10
  218. package/docs/content/sources/contentful.mdx +35 -0
  219. package/docs/content/sources/custom.mdx +43 -0
  220. package/docs/content/sources/github-releases.mdx +39 -0
  221. package/docs/content/sources/index.mdx +91 -0
  222. package/docs/content/sources/meta.ts +16 -0
  223. package/docs/content/sources/notion.mdx +32 -0
  224. package/docs/content/sources/obsidian.mdx +37 -0
  225. package/docs/content/sources/payload.mdx +31 -0
  226. package/docs/content/sources/remote-mdx.mdx +44 -0
  227. package/docs/content/sources/sanity.mdx +29 -0
  228. package/docs/content/sources/strapi.mdx +32 -0
  229. package/docs/content/syntax.mdx +69 -4
  230. package/docs/content/variables.mdx +41 -0
  231. package/docs/discoverability/agent-discovery.mdx +29 -2
  232. package/docs/discoverability/index.mdx +2 -2
  233. package/docs/discoverability/json-api.mdx +2 -0
  234. package/docs/discoverability/llms-txt.mdx +2 -2
  235. package/docs/discoverability/markdown.mdx +3 -1
  236. package/docs/discoverability/mcp.mdx +4 -0
  237. package/docs/discoverability/metadata.mdx +17 -0
  238. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  239. package/docs/references/api-pages.mdx +74 -0
  240. package/docs/references/asyncapi.mdx +4 -2
  241. package/docs/references/graphql.mdx +14 -2
  242. package/docs/references/meta.ts +1 -1
  243. package/docs/references/openapi.mdx +148 -7
  244. package/docs/references/scalar.mdx +3 -1
  245. package/package.json +43 -8
  246. package/skills/blume/SKILL.md +3 -0
  247. package/skills/blume-migrate/SKILL.md +6 -6
  248. package/skills/blume-migrate/references/docusaurus.md +7 -7
  249. package/skills/blume-migrate/references/fumadocs.md +17 -13
  250. package/skills/blume-migrate/references/mintlify.md +37 -19
  251. package/skills/blume-migrate/references/nextra.md +10 -7
  252. package/skills/blume-migrate/references/starlight.md +11 -8
  253. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  254. package/skills/blume-write-skill/SKILL.md +82 -0
  255. package/src/ai/agent-readability.ts +2 -1
  256. package/src/ai/agent-surface.ts +9 -7
  257. package/src/ai/ai-catalog.ts +6 -3
  258. package/src/ai/ask-context.ts +103 -20
  259. package/src/ai/ask-data.ts +6 -0
  260. package/src/ai/ask-limits.ts +13 -0
  261. package/src/ai/ask-tools.ts +142 -0
  262. package/src/ai/ask.ts +354 -76
  263. package/src/ai/changelog-markdown.ts +11 -6
  264. package/src/ai/component-markdown.ts +76 -1
  265. package/src/ai/cors.ts +14 -3
  266. package/src/ai/index.ts +12 -0
  267. package/src/ai/llms.ts +6 -2
  268. package/src/ai/markdown.ts +2 -1
  269. package/src/ai/mcp/data.ts +6 -0
  270. package/src/ai/mcp/query.ts +14 -6
  271. package/src/ai/openapi-components.ts +36 -6
  272. package/src/ai/relative-links.ts +4 -38
  273. package/src/ai/site-skill.ts +274 -0
  274. package/src/ai/skills.ts +35 -2
  275. package/src/ai/write-skill.ts +97 -0
  276. package/src/astro/generate.ts +210 -66
  277. package/src/astro/index.ts +1 -0
  278. package/src/astro/integration.ts +42 -4
  279. package/src/astro/markdown-negotiation.ts +3 -0
  280. package/src/astro/module-types.ts +9 -1
  281. package/src/astro/pages.ts +23 -0
  282. package/src/astro/templates.ts +406 -108
  283. package/src/astro/variables.ts +31 -0
  284. package/src/audit/catalog.ts +38 -3
  285. package/src/audit/checks/contrast.ts +183 -0
  286. package/src/audit/checks/i18n.ts +2 -3
  287. package/src/audit/checks/indexability.ts +2 -3
  288. package/src/audit/checks/links.ts +19 -13
  289. package/src/audit/checks/network.ts +110 -6
  290. package/src/audit/checks/redirects.ts +6 -4
  291. package/src/audit/checks/robots.ts +58 -2
  292. package/src/audit/checks/sitemap.ts +2 -3
  293. package/src/audit/redirects.ts +46 -15
  294. package/src/audit/run.ts +5 -0
  295. package/src/audit/types.ts +4 -1
  296. package/src/audit/url.ts +14 -5
  297. package/src/blume-modules.d.ts +12 -1
  298. package/src/captcha/client.ts +152 -0
  299. package/src/captcha/hcaptcha.ts +36 -0
  300. package/src/captcha/index.ts +28 -0
  301. package/src/captcha/schema.ts +32 -0
  302. package/src/captcha/turnstile.ts +38 -0
  303. package/src/captcha/verify.ts +71 -0
  304. package/src/cli/args.ts +51 -0
  305. package/src/cli/build-failure.ts +23 -0
  306. package/src/cli/command-meta.ts +5 -0
  307. package/src/cli/commands/audit.ts +10 -1
  308. package/src/cli/commands/dev.ts +18 -2
  309. package/src/cli/commands/doctor.ts +67 -28
  310. package/src/cli/commands/eval.ts +36 -4
  311. package/src/cli/commands/init.ts +6 -4
  312. package/src/cli/commands/skill.ts +96 -0
  313. package/src/cli/commands/validate.ts +10 -1
  314. package/src/cli/index.ts +5 -0
  315. package/src/cli/init/scaffold.ts +3 -3
  316. package/src/cli/prepare.ts +6 -3
  317. package/src/cli/required-secrets.ts +72 -30
  318. package/src/components/BlumePage.astro +2 -0
  319. package/src/components/content/ApiEndpoint.astro +18 -0
  320. package/src/components/content/ApiField.astro +79 -0
  321. package/src/components/content/ApiPlayground.astro +66 -0
  322. package/src/components/content/ApiRail.astro +19 -0
  323. package/src/components/content/Color.astro +1 -0
  324. package/src/components/content/FileTree.astro +1 -0
  325. package/src/components/content/GithubInfo.astro +1 -0
  326. package/src/components/content/Panel.astro +1 -0
  327. package/src/components/content/ParamField.astro +25 -0
  328. package/src/components/content/RequestExample.astro +17 -0
  329. package/src/components/content/ResponseExample.astro +17 -0
  330. package/src/components/content/ResponseField.astro +32 -0
  331. package/src/components/content/Step.astro +1 -0
  332. package/src/components/content/Tile.astro +1 -0
  333. package/src/components/content/Tooltip.astro +1 -0
  334. package/src/components/content/Tree.astro +1 -0
  335. package/src/components/content/TypeTable.astro +4 -1
  336. package/src/components/content/View.astro +21 -0
  337. package/src/components/content/ViewSwitcher.astro +168 -0
  338. package/src/components/content/api-field.ts +103 -0
  339. package/src/components/content/api-page.ts +248 -0
  340. package/src/components/content/mermaid-element.ts +15 -1
  341. package/src/components/content/view-switcher.ts +71 -0
  342. package/src/components/index.ts +2 -0
  343. package/src/components/islands/Assistant.astro +10 -1
  344. package/src/components/islands/assistant.tsx +104 -14
  345. package/src/components/islands/code-question.ts +62 -0
  346. package/src/components/islands/hooks.ts +136 -21
  347. package/src/components/islands/support-link.ts +68 -0
  348. package/src/components/layout/Analytics.astro +11 -2
  349. package/src/components/layout/Banner.astro +14 -10
  350. package/src/components/layout/Breadcrumbs.astro +6 -2
  351. package/src/components/layout/ConsentBanner.astro +62 -0
  352. package/src/components/layout/ConsentHead.astro +38 -0
  353. package/src/components/layout/Directory.astro +94 -0
  354. package/src/components/layout/DrawerSwitchers.astro +51 -0
  355. package/src/components/layout/Empty.astro +2 -2
  356. package/src/components/layout/Header.astro +122 -80
  357. package/src/components/layout/LanguageSwitcher.astro +48 -15
  358. package/src/components/layout/NarrationPlayer.astro +1085 -0
  359. package/src/components/layout/NavSelector.astro +33 -7
  360. package/src/components/layout/PageActions.astro +139 -24
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +71 -3
  363. package/src/components/layout/ReferenceLayout.astro +17 -6
  364. package/src/components/layout/RelatedPages.astro +42 -0
  365. package/src/components/layout/RootLayout.astro +318 -47
  366. package/src/components/layout/Search.astro +47 -19
  367. package/src/components/layout/SiteFooter.astro +124 -0
  368. package/src/components/layout/SiteMetatags.astro +16 -0
  369. package/src/components/layout/TableOfContents.astro +24 -17
  370. package/src/components/layout/consent/native.ts +79 -0
  371. package/src/components/layout/consent/types.ts +37 -0
  372. package/src/components/layout/footer-links.ts +53 -0
  373. package/src/components/layout/footer-socials.ts +87 -0
  374. package/src/components/layout/head-scripts.ts +6 -1
  375. package/src/components/layout/locale-redirect.ts +107 -0
  376. package/src/components/layout/nav-utils.ts +83 -0
  377. package/src/components/layout/page-feedback.ts +96 -0
  378. package/src/components/layout/search/algolia.ts +10 -1
  379. package/src/components/layout/search/analytics.ts +126 -0
  380. package/src/components/layout/search/endpoint.ts +48 -5
  381. package/src/components/layout/search/flexsearch.ts +17 -5
  382. package/src/components/layout/search/orama-cloud.ts +26 -14
  383. package/src/components/layout/search/pagefind.ts +114 -5
  384. package/src/components/layout/search/types.ts +32 -0
  385. package/src/components/layout/search/typesense.ts +19 -3
  386. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  387. package/src/components/openapi/Authorization.astro +10 -0
  388. package/src/components/openapi/GraphqlOperation.astro +5 -0
  389. package/src/components/openapi/Operation.astro +184 -13
  390. package/src/components/openapi/ParametersTable.astro +8 -4
  391. package/src/components/openapi/Playground.astro +7 -1
  392. package/src/components/openapi/RequestBody.astro +24 -5
  393. package/src/components/openapi/RequestPanel.astro +59 -7
  394. package/src/components/openapi/Responses.astro +7 -4
  395. package/src/components/openapi/SchemaTable.astro +15 -3
  396. package/src/components/openapi/async-snippets.ts +65 -9
  397. package/src/components/openapi/async.ts +123 -29
  398. package/src/components/openapi/code-samples.ts +97 -0
  399. package/src/components/openapi/graphql-helpers.ts +35 -12
  400. package/src/components/openapi/helpers.ts +36 -2
  401. package/src/components/openapi/message-model.ts +54 -11
  402. package/src/components/openapi/message.ts +14 -4
  403. package/src/components/openapi/operation-model.ts +16 -7
  404. package/src/components/openapi/playground-client.ts +24 -5
  405. package/src/components/openapi/playground-schema.ts +1 -1
  406. package/src/components/openapi/schema-tree.ts +44 -14
  407. package/src/components/openapi/security.ts +16 -3
  408. package/src/components/openapi/snippets.ts +873 -10
  409. package/src/components/openapi/validate-json.ts +4 -3
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/builtin-tags.ts +5 -0
  421. package/src/core/changelog-index.ts +31 -1
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/component-diagnostics.ts +53 -0
  424. package/src/core/component-overrides.ts +32 -0
  425. package/src/core/config-input.ts +225 -23
  426. package/src/core/data.ts +46 -4
  427. package/src/core/diagnostics.ts +8 -2
  428. package/src/core/footer.ts +26 -0
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/i18n-ui.ts +84 -2
  431. package/src/core/includes.ts +21 -4
  432. package/src/core/links.ts +63 -19
  433. package/src/core/localizable.ts +26 -0
  434. package/src/core/navigation.ts +43 -12
  435. package/src/core/page-modes.ts +56 -0
  436. package/src/core/probe.ts +26 -6
  437. package/src/core/project-graph.ts +163 -16
  438. package/src/core/redirect-patterns.ts +520 -0
  439. package/src/core/related.ts +131 -0
  440. package/src/core/schema.ts +339 -66
  441. package/src/core/sources/cache.ts +6 -0
  442. package/src/core/sources/contentful-rich-text.ts +29 -6
  443. package/src/core/sources/contentful.ts +135 -25
  444. package/src/core/sources/filesystem.ts +26 -4
  445. package/src/core/sources/github-releases.ts +12 -1
  446. package/src/core/sources/lexical.ts +15 -10
  447. package/src/core/sources/mdx-remote.ts +62 -10
  448. package/src/core/sources/normalize.ts +228 -41
  449. package/src/core/sources/notion.ts +7 -1
  450. package/src/core/sources/obsidian.ts +16 -3
  451. package/src/core/sources/payload.ts +38 -15
  452. package/src/core/sources/read.ts +8 -3
  453. package/src/core/sources/remote.ts +72 -15
  454. package/src/core/sources/resolve.ts +11 -1
  455. package/src/core/sources/sanity.ts +43 -25
  456. package/src/core/sources/strapi.ts +2 -2
  457. package/src/core/types.ts +19 -1
  458. package/src/core/ui-packs/ar.ts +44 -0
  459. package/src/core/ui-packs/bg.ts +46 -0
  460. package/src/core/ui-packs/bn.ts +44 -0
  461. package/src/core/ui-packs/ca.ts +47 -0
  462. package/src/core/ui-packs/cs.ts +45 -0
  463. package/src/core/ui-packs/da.ts +46 -0
  464. package/src/core/ui-packs/de.ts +47 -0
  465. package/src/core/ui-packs/el.ts +46 -0
  466. package/src/core/ui-packs/es.ts +47 -0
  467. package/src/core/ui-packs/fa.ts +46 -0
  468. package/src/core/ui-packs/fi.ts +46 -0
  469. package/src/core/ui-packs/fr.ts +47 -0
  470. package/src/core/ui-packs/he.ts +44 -0
  471. package/src/core/ui-packs/hi.ts +45 -0
  472. package/src/core/ui-packs/hr.ts +46 -0
  473. package/src/core/ui-packs/hu.ts +45 -0
  474. package/src/core/ui-packs/id.ts +46 -0
  475. package/src/core/ui-packs/it.ts +46 -0
  476. package/src/core/ui-packs/ja.ts +46 -0
  477. package/src/core/ui-packs/ko.ts +45 -0
  478. package/src/core/ui-packs/nl.ts +47 -0
  479. package/src/core/ui-packs/no.ts +45 -0
  480. package/src/core/ui-packs/pl.ts +46 -0
  481. package/src/core/ui-packs/pt-br.ts +47 -0
  482. package/src/core/ui-packs/pt.ts +46 -0
  483. package/src/core/ui-packs/ro.ts +46 -0
  484. package/src/core/ui-packs/ru.ts +46 -0
  485. package/src/core/ui-packs/sk.ts +45 -0
  486. package/src/core/ui-packs/sr.ts +46 -0
  487. package/src/core/ui-packs/sv.ts +45 -0
  488. package/src/core/ui-packs/th.ts +44 -0
  489. package/src/core/ui-packs/tr.ts +46 -0
  490. package/src/core/ui-packs/uk.ts +46 -0
  491. package/src/core/ui-packs/vi.ts +46 -0
  492. package/src/core/ui-packs/zh-tw.ts +44 -0
  493. package/src/core/ui-packs/zh.ts +44 -0
  494. package/src/core/variables.ts +103 -0
  495. package/src/deploy/adapter-output.ts +7 -2
  496. package/src/deploy/adapters/cloudflare.ts +7 -5
  497. package/src/deploy/adapters/node.ts +47 -9
  498. package/src/deploy/adapters/types.ts +7 -3
  499. package/src/deploy/artifacts.ts +79 -9
  500. package/src/deploy/cloudflare-negotiation.ts +35 -5
  501. package/src/deploy/headers.ts +2 -1
  502. package/src/deploy/node-headers.ts +63 -9
  503. package/src/deploy/platforms/cloudflare.ts +45 -9
  504. package/src/deploy/platforms/netlify.ts +35 -2
  505. package/src/deploy/platforms/node.ts +12 -2
  506. package/src/deploy/platforms/types.ts +21 -7
  507. package/src/deploy/platforms/vercel.ts +149 -17
  508. package/src/deploy/redirects.ts +151 -24
  509. package/src/deploy/rss.ts +3 -1
  510. package/src/deploy/sitemap.ts +39 -5
  511. package/src/deploy/vercel-negotiation.ts +326 -102
  512. package/src/eval/agents.ts +18 -1
  513. package/src/eval/prompts.ts +6 -3
  514. package/src/eval/report.ts +34 -13
  515. package/src/eval/run.ts +5 -0
  516. package/src/markdown/api-rail.ts +259 -0
  517. package/src/markdown/code-title.ts +35 -9
  518. package/src/markdown/features.ts +3 -2
  519. package/src/markdown/fence-meta.ts +2 -0
  520. package/src/markdown/include.ts +11 -1
  521. package/src/markdown/index.ts +28 -5
  522. package/src/markdown/math.ts +4 -4
  523. package/src/markdown/relative-links.ts +3 -2
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +7 -6
  531. package/src/openapi/checks.ts +273 -0
  532. package/src/openapi/graphql-build.ts +21 -0
  533. package/src/openapi/graphql.ts +11 -0
  534. package/src/openapi/model.ts +72 -27
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +182 -4
  537. package/src/openapi/references.ts +64 -8
  538. package/src/openapi/render-mdx.ts +5 -1
  539. package/src/openapi/scalar.ts +40 -6
  540. package/src/openapi/source.ts +40 -15
  541. package/src/ratelimit/cloudflare.ts +54 -0
  542. package/src/ratelimit/index.ts +30 -0
  543. package/src/ratelimit/memory.ts +45 -0
  544. package/src/ratelimit/runtime.ts +247 -0
  545. package/src/ratelimit/schema.ts +28 -0
  546. package/src/ratelimit/upstash.ts +60 -0
  547. package/src/ratelimit/wrangler.ts +70 -0
  548. package/src/reference/asyncapi.ts +2 -2
  549. package/src/reference/graphql.ts +20 -3
  550. package/src/reference/index.ts +1 -0
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +80 -8
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +23 -5
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/locale-scope.ts +30 -0
  557. package/src/search/adapters/typesense.ts +14 -4
  558. package/src/search/build.ts +21 -3
  559. package/src/search/documents.ts +26 -2
  560. package/src/search/orama-index.ts +191 -34
  561. package/src/search/pagefind-ranking.ts +39 -0
  562. package/src/search/source-pages.ts +29 -0
  563. package/src/search/sync/algolia.ts +111 -10
  564. package/src/search/sync/index.ts +52 -13
  565. package/src/search/sync/orama-cloud.ts +3 -0
  566. package/src/search/sync/typesense.ts +76 -26
  567. package/src/seo/metatags.ts +67 -0
  568. package/src/sources/contentful.ts +12 -0
  569. package/src/sources/github-releases.ts +7 -1
  570. package/src/sources/shared.ts +3 -0
  571. package/src/theme/chrome-icons.ts +1 -0
  572. package/src/theme/color.ts +500 -0
  573. package/src/theme/entry.ts +85 -6
  574. package/src/theme/palette.ts +121 -45
  575. package/src/translate/prompts.ts +1 -0
  576. package/src/translate/validate.ts +31 -3
  577. package/src/upgrade/upgrade.ts +4 -171
  578. package/dist/cli/chunk-1d7ve1dm.js.map +0 -12
  579. package/dist/cli/chunk-2hsdwb9n.js.map +0 -10
  580. package/dist/cli/chunk-35d4wj9f.js.map +0 -15
  581. package/dist/cli/chunk-364znk6q.js +0 -75
  582. package/dist/cli/chunk-364znk6q.js.map +0 -11
  583. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  584. package/dist/cli/chunk-8ktnccpt.js.map +0 -10
  585. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  586. package/dist/cli/chunk-b07cmahc.js +0 -145
  587. package/dist/cli/chunk-b07cmahc.js.map +0 -11
  588. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  589. package/dist/cli/chunk-e04dxsz1.js +0 -39
  590. package/dist/cli/chunk-e04dxsz1.js.map +0 -10
  591. package/dist/cli/chunk-j85vccga.js.map +0 -36
  592. package/dist/cli/chunk-p3v96n38.js.map +0 -10
  593. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  594. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  595. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  596. package/dist/cli/chunk-vg9r4eb9.js.map +0 -14
  597. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  598. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  599. package/docs/content/sources.mdx +0 -334
  600. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-3v3ffbrj.js.map} +0 -0
  601. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-8y2kmjv8.js.map} +0 -0
  602. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-c887fj0s.js.map} +0 -0
  603. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-dzwehpy7.js.map} +0 -0
  604. /package/dist/cli/{chunk-2z928egk.js.map → chunk-ft9vfys6.js.map} +0 -0
  605. /package/dist/cli/{chunk-a58773jm.js.map → chunk-kma9f6sf.js.map} +0 -0
  606. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-sds923fa.js.map} +0 -0
  607. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-tdqhyxfz.js.map} +0 -0
@@ -54,6 +54,10 @@ export interface SearchDocument {
54
54
  * Filterable through the MCP tools' `filters` input.
55
55
  */
56
56
  facets?: Record<string, string>;
57
+ /** Frontmatter `search.boost`: the page's relevance multiplier, when set. */
58
+ boost?: number;
59
+ /** Frontmatter `search.keywords`: extra terms the page is found by. */
60
+ keywords?: string[];
57
61
  }
58
62
 
59
63
  /**
@@ -77,6 +81,13 @@ export interface SearchRecord {
77
81
  version: string;
78
82
  /** Single faceting tag (the first frontmatter tag, when present). */
79
83
  tag?: string;
84
+ /**
85
+ * The page's relevance multiplier, 1 unless `search.boost` sets it: a
86
+ * ranking attribute every record carries, so the backend can sort by it.
87
+ */
88
+ boost: number;
89
+ /** Extra terms the page is found by (`search.keywords`). */
90
+ keywords?: string[];
80
91
  }
81
92
 
82
93
  // Tag-shaped only: a name (or closing slash/fragment) right after `<`, and no
@@ -414,6 +425,17 @@ const pageBody = async (
414
425
  });
415
426
  };
416
427
 
428
+ /** The ranking a page asks for: its `search.boost` and `search.keywords`. */
429
+ const rankingFields = (
430
+ search: PageRecord["meta"]["search"] | undefined
431
+ ): Pick<SearchDocument, "boost" | "keywords"> => {
432
+ const { boost, keywords } = search ?? {};
433
+ return {
434
+ ...(boost !== undefined && boost !== 1 && { boost }),
435
+ ...(keywords && keywords.length > 0 && { keywords }),
436
+ };
437
+ };
438
+
417
439
  /**
418
440
  * Build search documents from the content graph. Only indexable pages are
419
441
  * included (per the route manifest), and content comes from the source files,
@@ -531,7 +553,7 @@ export const buildSearchDocuments = async (
531
553
  if (facets) {
532
554
  document.facets = facets;
533
555
  }
534
- return document;
556
+ return { ...document, ...rankingFields(page?.meta?.search) };
535
557
  })
536
558
  );
537
559
  };
@@ -539,13 +561,15 @@ export const buildSearchDocuments = async (
539
561
  /**
540
562
  * Map per-page search documents to the flat record shape hosted backends
541
563
  * ingest. One record per page, keyed by route; the first tag becomes the
542
- * faceting `tag`.
564
+ * faceting `tag`, and every record carries a `boost` (1 by default).
543
565
  */
544
566
  export const toSearchRecords = (documents: SearchDocument[]): SearchRecord[] =>
545
567
  documents.map((doc) => ({
546
568
  _id: doc.route,
569
+ boost: doc.boost ?? 1,
547
570
  content: doc.content,
548
571
  description: doc.description,
572
+ ...(doc.keywords && { keywords: doc.keywords }),
549
573
  locale: doc.locale,
550
574
  tag: doc.tags?.[0],
551
575
  title: doc.title,
@@ -1,8 +1,16 @@
1
- import { create, insertMultiple, search } from "@orama/orama";
1
+ import {
2
+ components,
3
+ create,
4
+ insert,
5
+ insertMultiple,
6
+ search,
7
+ } from "@orama/orama";
2
8
  import type {
3
9
  AnyOrama,
4
10
  EnumArrComparisonOperator,
5
11
  EnumComparisonOperator,
12
+ Result,
13
+ SearchParamsFullText,
6
14
  Tokenizer,
7
15
  } from "@orama/orama";
8
16
 
@@ -31,6 +39,10 @@ export interface OramaDoc {
31
39
  * but not indexed, so they ride along on the returned document untouched. */
32
40
  breadcrumb?: string[];
33
41
  section?: string;
42
+ /** The page's relevance multiplier (`search.boost`); stored, not indexed. */
43
+ boost?: number;
44
+ /** Extra terms the page is found by (`search.keywords`). */
45
+ keywords?: string[];
34
46
  }
35
47
 
36
48
  const SCHEMA = {
@@ -42,6 +54,7 @@ const SCHEMA = {
42
54
  // serves every project's facet keys, with `containsAll` matching a filter
43
55
  // set. Derived from `facets` at insert time.
44
56
  facetTerms: "enum[]",
57
+ keywords: "string[]",
45
58
  locale: "enum",
46
59
  route: "string",
47
60
  title: "string",
@@ -52,8 +65,21 @@ const SCHEMA = {
52
65
  const toFacetTerms = (facets: Record<string, string>): string[] =>
53
66
  Object.entries(facets).map(([key, value]) => `${key}:${value}`);
54
67
 
55
- /** Title and description outrank body text, matching the search dialog. */
56
- const BOOST = { description: 2, title: 3 };
68
+ /**
69
+ * Title and description outrank body text, matching the search dialog; a
70
+ * page's keywords count as much as its title, since its author chose them.
71
+ */
72
+ const BOOST = { description: 2, keywords: 3, title: 3 };
73
+
74
+ /** Databases holding a boosted page, which rank by boosted score. */
75
+ const boostedDatabases = new WeakSet<AnyOrama>();
76
+
77
+ /** A search match as Orama's sort function sees it: id, score, document. */
78
+ type RankedMatch = [unknown, number, OramaDoc];
79
+
80
+ /** Rank matches by relevance times the page's `search.boost`. */
81
+ const byBoostedScore = (a: RankedMatch, b: RankedMatch): number =>
82
+ b[1] * (b[2].boost ?? 1) - a[1] * (a[2].boost ?? 1);
57
83
 
58
84
  /**
59
85
  * The script whose text Orama's default tokenizer keeps mostly intact. Its
@@ -296,32 +322,92 @@ const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
296
322
  };
297
323
  };
298
324
 
325
+ /**
326
+ * On a Latin-default index holding pages in non-Latin locales, the tokenizer
327
+ * each of those locales' pages was indexed with, keyed by locale code. A query
328
+ * reads it to tokenize its term the way the pages it searches were.
329
+ */
330
+ const translationTokenizers = new WeakMap<AnyOrama, Map<string, Tokenizer>>();
331
+
332
+ /** A segmenting tokenizer for each document locale Orama's can't serve. */
333
+ const localeTokenizers = (documents: OramaDoc[]): Map<string, Tokenizer> => {
334
+ const tokenizers = new Map<string, Tokenizer>();
335
+ for (const code of new Set(documents.map((doc) => doc.locale ?? ""))) {
336
+ const tokenizer = segmentingTokenizer(code);
337
+ if (tokenizer) {
338
+ tokenizers.set(code, tokenizer);
339
+ }
340
+ }
341
+ return tokenizers;
342
+ };
343
+
344
+ /**
345
+ * One tokenizer that hands each text to its locale's: Orama passes the
346
+ * language given to `insert` and `search` through to `tokenize`, so a page
347
+ * inserted under `ja` and a query searched under `ja` both reach the Japanese
348
+ * tokenizer, and everything else reaches Orama's own, exactly as on an index
349
+ * with no translations.
350
+ */
351
+ const dispatchingTokenizer = (
352
+ tokenizers: Map<string, Tokenizer>
353
+ ): Tokenizer => {
354
+ const standard = components.tokenizer.createTokenizer();
355
+ return {
356
+ language: standard.language,
357
+ normalizationCache: standard.normalizationCache,
358
+ tokenize: (raw, language, prop, withCache) =>
359
+ // Orama's tokenizer rejects a language other than its own, so the
360
+ // locale that picked it is not passed on.
361
+ (tokenizers.get(language ?? "") ?? standard).tokenize(
362
+ raw,
363
+ undefined,
364
+ prop,
365
+ withCache
366
+ ),
367
+ };
368
+ };
369
+
299
370
  /**
300
371
  * Build an in-memory Orama full-text index from search documents. Shared by the
301
372
  * Orama client loader (browser), the MCP server, and assistant grounding (Node),
302
373
  * so ranking is identical wherever docs are queried. `locale` — the site's
303
374
  * `i18n.defaultLocale` — swaps in a word-segmenting tokenizer for every
304
375
  * non-Latin script, all of which Orama's default tokenizer reduces to zero
305
- * tokens; the tokenizer belongs to the database, so on a mixed-locale site it
306
- * applies to every document. That is safe in one direction only: Latin words
376
+ * tokens. A non-Latin default's tokenizer serves every page: Latin words
307
377
  * survive segmentation intact, so English pages on a segmented index stay
308
- * searchable, but non-Latin translations on a Latin-default index still
309
- * collapse to zero tokens.
378
+ * searchable. A Latin default keeps Orama's tokenizer for its Latin-script
379
+ * pages, and each page in a non-Latin locale (the Japanese and Hindi
380
+ * translations of an English site) is indexed with its own locale's tokenizer.
310
381
  */
311
382
  export const buildOramaIndex = async (
312
383
  documents: OramaDoc[],
313
384
  locale?: string
314
385
  ): Promise<AnyOrama> => {
315
- const tokenizer = segmentingTokenizer(locale);
316
- const db = tokenizer
317
- ? create({ components: { tokenizer }, schema: SCHEMA })
318
- : create({ schema: SCHEMA });
319
- await insertMultiple(
320
- db,
321
- documents.map((doc) =>
322
- doc.facets ? { ...doc, facetTerms: toFacetTerms(doc.facets) } : doc
323
- )
386
+ const rows = documents.map((doc) =>
387
+ doc.facets ? { ...doc, facetTerms: toFacetTerms(doc.facets) } : doc
324
388
  );
389
+ const tokenizer = segmentingTokenizer(locale);
390
+ const translations = tokenizer ? undefined : localeTokenizers(documents);
391
+ let db: AnyOrama;
392
+ if (translations && translations.size > 0) {
393
+ db = create({
394
+ components: { tokenizer: dispatchingTokenizer(translations) },
395
+ schema: SCHEMA,
396
+ });
397
+ for (const row of rows) {
398
+ // oxlint-disable-next-line no-await-in-loop -- one at a time, in order, so each page is tokenized under its own locale and ties still rank in document order.
399
+ await insert(db, row, row.locale);
400
+ }
401
+ translationTokenizers.set(db, translations);
402
+ } else {
403
+ db = tokenizer
404
+ ? create({ components: { tokenizer }, schema: SCHEMA })
405
+ : create({ schema: SCHEMA });
406
+ await insertMultiple(db, rows);
407
+ }
408
+ if (documents.some((doc) => (doc.boost ?? 1) !== 1)) {
409
+ boostedDatabases.add(db);
410
+ }
325
411
  return db;
326
412
  };
327
413
 
@@ -358,17 +444,56 @@ interface OramaWhereClause {
358
444
  | undefined;
359
445
  }
360
446
 
447
+ /** A ranked match, scored and carrying its document. */
448
+ type Hit = Result<OramaDoc>;
449
+
450
+ /**
451
+ * One search, with the term tokenized under `language`. On a bigrammed
452
+ * tokenizer the strict pass runs first: a term is only meant to match where
453
+ * its bigrams sit together, and scoring them independently lets a page
454
+ * sharing a couple of windows outrank the page the term is about. Terms
455
+ * spanning several words rarely appear in full on one page, so an empty
456
+ * strict result falls back to the default pass rather than reporting no
457
+ * matches.
458
+ */
459
+ const rankedHits = async (
460
+ db: AnyOrama,
461
+ params: SearchParamsFullText<AnyOrama, OramaDoc>,
462
+ language: string | undefined,
463
+ bigrammed: boolean
464
+ ): Promise<Hit[]> => {
465
+ // The result-document generic is OramaDoc because `buildOramaIndex` is the
466
+ // only writer to this database and inserts OramaDoc records (plus the
467
+ // derived `facetTerms`).
468
+ const strict = bigrammed
469
+ ? await search<AnyOrama, OramaDoc>(
470
+ db,
471
+ { ...params, threshold: ALL_TOKENS },
472
+ language
473
+ )
474
+ : undefined;
475
+ const found =
476
+ strict && strict.hits.length > 0
477
+ ? strict
478
+ : await search<AnyOrama, OramaDoc>(db, params, language);
479
+ return found.hits;
480
+ };
481
+
482
+ /** A hit's rank across searches: its relevance times the page's boost. */
483
+ const boostedScore = (hit: Hit): number =>
484
+ hit.score * (hit.document.boost ?? 1);
485
+
361
486
  /**
362
487
  * Query the index, returning the matching documents (highest-ranked first).
363
488
  * `filters` narrows results by exact `where` matches on the enum fields:
364
489
  * `locale` to one language, `contentTypes` to a set of page types, `facets`
365
490
  * to documents carrying every requested `key:value` term.
366
491
  *
367
- * On a bigrammed index the strict pass runs first: a term is only meant to
368
- * match where its bigrams sit together, and scoring them independently lets a
369
- * page sharing a couple of windows outrank the page the term is about. Terms
370
- * spanning several words rarely appear in full on one page, so an empty strict
371
- * result falls back to the default pass rather than reporting no matches.
492
+ * On an index whose translations have tokenizers of their own, a query scoped
493
+ * to a locale is tokenized with that locale's tokenizer. An unscoped query
494
+ * searches each locale's pages with its own tokenizer, and the Latin-script
495
+ * pages with Orama's, then merges the matches by score: every search runs over
496
+ * the one index, so their scores share its term statistics.
372
497
  */
373
498
  export const queryOramaIndex = async (
374
499
  db: AnyOrama,
@@ -391,24 +516,56 @@ export const queryOramaIndex = async (
391
516
  if (facetTerms.length > 0) {
392
517
  where.facetTerms = { containsAll: facetTerms };
393
518
  }
519
+ // Boosts multiply every match's score before the limit applies (Orama
520
+ // sorts the whole match set with a sort function), so a boosted page can
521
+ // rise past matches outside the first page.
394
522
  const unfiltered = {
395
523
  boost: BOOST,
396
524
  limit,
397
- properties: ["title", "description", "content"],
525
+ properties: ["title", "keywords", "description", "content"],
526
+ ...(boostedDatabases.has(db) && { sortBy: byBoostedScore }),
398
527
  term,
399
528
  };
400
529
  const params =
401
530
  Object.keys(where).length > 0 ? { ...unfiltered, where } : unfiltered;
402
- const bigrammed = isBigramLanguage(db.tokenizer?.language ?? "");
403
- // The result-document generic is OramaDoc because `buildOramaIndex` is the
404
- // only writer to this database and inserts OramaDoc records (plus the
405
- // derived `facetTerms`).
406
- const strict = bigrammed
407
- ? await search<AnyOrama, OramaDoc>(db, { ...params, threshold: ALL_TOKENS })
408
- : undefined;
409
- const found =
410
- strict && strict.hits.length > 0
411
- ? strict
412
- : await search<AnyOrama, OramaDoc>(db, params);
413
- return found.hits.map((hit) => hit.document);
531
+ const translations = translationTokenizers.get(db);
532
+ if (!translations) {
533
+ const bigrammed = isBigramLanguage(db.tokenizer?.language ?? "");
534
+ const hits = await rankedHits(db, params, undefined, bigrammed);
535
+ return hits.map((hit) => hit.document);
536
+ }
537
+ if (filters?.locale) {
538
+ const { language = "" } = translations.get(filters.locale) ?? {};
539
+ const bigrammed = isBigramLanguage(language);
540
+ const hits = await rankedHits(db, params, filters.locale, bigrammed);
541
+ return hits.map((hit) => hit.document);
542
+ }
543
+ // `nin` matches only pages with a locale, which every Blume-built index
544
+ // gives each page. (Orama's `not` would admit locale-less pages, but it
545
+ // drops the sibling filters beside it.)
546
+ const searches = [
547
+ rankedHits(
548
+ db,
549
+ {
550
+ ...params,
551
+ where: { ...where, locale: { nin: [...translations.keys()] } },
552
+ },
553
+ undefined,
554
+ false
555
+ ),
556
+ ...[...translations].map(([code, tokenizer]) =>
557
+ rankedHits(
558
+ db,
559
+ { ...params, where: { ...where, locale: { eq: code } } },
560
+ code,
561
+ isBigramLanguage(tokenizer.language)
562
+ )
563
+ ),
564
+ ];
565
+ const found = await Promise.all(searches);
566
+ return found
567
+ .flat()
568
+ .toSorted((a, b) => boostedScore(b) - boostedScore(a))
569
+ .slice(0, limit)
570
+ .map((hit) => hit.document);
414
571
  };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A page's `search.boost` and `search.keywords`, as Pagefind reads them from
3
+ * the built page. Pagefind has no page-level boost, only element weights, so
4
+ * the page's body carries its boost as its weight: a boosted page's matches
5
+ * count for more and it ranks higher. Pagefind damps repeated matches, so the
6
+ * lift is smaller than the full multiplier: a boost of 5 scored its page about
7
+ * 1.5 to 2 times higher in testing. A boost past Pagefind's largest weight,
8
+ * 10, counts as 10.
9
+ * Keywords go on an empty element as an indexed attribute, found like text
10
+ * and never shown.
11
+ */
12
+
13
+ /** The largest weight Pagefind takes. */
14
+ const MAX_WEIGHT = 10;
15
+
16
+ /** The attribute a page's keywords ride on. */
17
+ export const KEYWORDS_ATTRIBUTE = "data-blume-keywords";
18
+
19
+ /** What the page body carries for Pagefind. */
20
+ export interface PagefindRanking {
21
+ /** `data-pagefind-weight` for the body, when the page is boosted. */
22
+ weight?: string;
23
+ /** The keywords, space-separated, when the page sets any. */
24
+ keywords?: string;
25
+ }
26
+
27
+ export const pagefindRanking = (search: {
28
+ boost?: number;
29
+ keywords?: string[];
30
+ }): PagefindRanking => {
31
+ const { boost, keywords } = search;
32
+ return {
33
+ ...(boost !== undefined &&
34
+ boost !== 1 && {
35
+ weight: String(Math.min(boost, MAX_WEIGHT)),
36
+ }),
37
+ ...(keywords && keywords.length > 0 && { keywords: keywords.join(" ") }),
38
+ };
39
+ };
@@ -0,0 +1,29 @@
1
+ import { relative } from "pathe";
2
+
3
+ import type { BlumeProject } from "../core/project-graph.ts";
4
+
5
+ /** Where a page is served, and its title: what a search hit links to. */
6
+ export interface SourcePage {
7
+ title: string;
8
+ url: string;
9
+ }
10
+
11
+ /**
12
+ * Each search-indexable page's source file, relative to the project root,
13
+ * paired with the page it renders. The Mixedbread endpoint links a hit to its
14
+ * page through the path `mxbai store sync` recorded for the chunk's file,
15
+ * which is relative to wherever the CLI ran: the project root, or a directory
16
+ * above it in a monorepo. Pages from a source without local files have no
17
+ * path to match and are left out.
18
+ */
19
+ export const sourcePages = (project: BlumeProject): [string, SourcePage][] =>
20
+ project.manifest.routes.flatMap((route): [string, SourcePage][] =>
21
+ route.indexable && route.sourcePath
22
+ ? [
23
+ [
24
+ relative(project.context.root, route.sourcePath),
25
+ { title: route.title, url: route.path },
26
+ ],
27
+ ]
28
+ : []
29
+ );
@@ -16,10 +16,94 @@ export type AlgoliaSyncConfig = Pick<AlgoliaOptions, "appId" | "indexName">;
16
16
  */
17
17
  const FILTER_ATTRIBUTES = ["locale", "version"] as const;
18
18
 
19
+ /** The custom ranking rule that orders close matches by `search.boost`. */
20
+ const BOOST_RANKING = "desc(boost)";
21
+
19
22
  /** A faceting declaration's attribute: `filterOnly(locale)` → `locale`. */
20
23
  const facetAttribute = (declaration: string): string =>
21
24
  declaration.replace(/^\w+\((?<name>.*)\)$/u, "$<name>");
22
25
 
26
+ /**
27
+ * The largest record Algolia accepts on its Build and Grow plans, in bytes of
28
+ * the record's JSON. One larger record fails the whole upload.
29
+ */
30
+ const MAX_RECORD_BYTES = 10_000;
31
+
32
+ /**
33
+ * A record as uploaded: the search record's fields plus Algolia's `objectID`.
34
+ * (Mapped rather than the interface itself, which the SDK's
35
+ * `Record<string, unknown>` objects don't accept.)
36
+ */
37
+ type AlgoliaObject = Pick<SearchRecord, keyof SearchRecord> & {
38
+ objectID: string;
39
+ };
40
+
41
+ /** A string's or record's size as Algolia measures it: UTF-8 bytes of its JSON. */
42
+ const jsonBytes = (value: string | AlgoliaObject): number =>
43
+ Buffer.byteLength(JSON.stringify(value), "utf-8");
44
+
45
+ /** The JSON of the widest single character: a control character's `\u` escape. */
46
+ const WIDEST_CHARACTER = jsonBytes("\u0000");
47
+
48
+ /** The length of `text`'s longest prefix whose JSON string fits `budget`. */
49
+ const fittingLength = (text: string, budget: number): number => {
50
+ let size = jsonBytes("");
51
+ let end = 0;
52
+ [...text].some((character) => {
53
+ size += jsonBytes(character) - jsonBytes("");
54
+ if (size > budget) {
55
+ return true;
56
+ }
57
+ end += character.length;
58
+ return false;
59
+ });
60
+ return end;
61
+ };
62
+
63
+ /**
64
+ * Cut `text` into pieces whose JSON strings fit `budget` bytes each, at the
65
+ * last space that fits so no word is split; a run with no space in reach (an
66
+ * unspaced script, a long URL) is cut between characters.
67
+ */
68
+ const splitText = (text: string, budget: number): string[] => {
69
+ const pieces: string[] = [];
70
+ let rest = text;
71
+ while (jsonBytes(rest) > budget) {
72
+ const end = fittingLength(rest, budget);
73
+ const space = rest.lastIndexOf(" ", end);
74
+ const cut = space > 0 ? space : end;
75
+ pieces.push(rest.slice(0, cut));
76
+ rest = rest.slice(cut).trimStart();
77
+ }
78
+ pieces.push(rest);
79
+ return pieces;
80
+ };
81
+
82
+ /**
83
+ * A page's record, split to fit Algolia's cap the way Algolia recommends for
84
+ * long documents: every piece carries the page's title, description, URL and
85
+ * facets with a stretch of its body, and the index's `distinct` on `url`
86
+ * shows the page once, at its best-matching piece. The first piece keeps the
87
+ * page's own `objectID`; the rest add `#1`, `#2`… A record whose other fields
88
+ * leave no room for a single character of body can't be split to fit, and
89
+ * uploads whole for Algolia to reject by name.
90
+ */
91
+ const toObjects = (record: SearchRecord): AlgoliaObject[] => {
92
+ const whole = { ...record, objectID: record._id };
93
+ const budget =
94
+ MAX_RECORD_BYTES -
95
+ jsonBytes({ ...whole, content: "", objectID: `${record._id}#000` }) +
96
+ jsonBytes("");
97
+ if (jsonBytes(whole) <= MAX_RECORD_BYTES || budget < WIDEST_CHARACTER) {
98
+ return [whole];
99
+ }
100
+ return splitText(record.content, budget).map((content, index) => ({
101
+ ...record,
102
+ content,
103
+ objectID: index === 0 ? record._id : `${record._id}#${index}`,
104
+ }));
105
+ };
106
+
23
107
  /**
24
108
  * Upload the search records to Algolia. Uses the admin key from
25
109
  * `ALGOLIA_ADMIN_API_KEY` (never the adapter options, which hold only the
@@ -27,9 +111,12 @@ const facetAttribute = (declaration: string): string =>
27
111
  *
28
112
  * Uses `replaceAllObjects`, which atomically replaces the index contents, so
29
113
  * pages deleted or renamed since the last sync don't linger as stale search
30
- * hits that 404 when clicked. Then adds `filterOnly(locale)` and
114
+ * hits that 404 when clicked. A page too long for one record is split across
115
+ * several (see {@link toObjects}). Then adds `filterOnly(locale)` and
31
116
  * `filterOnly(version)` to the index's `attributesForFaceting`, keeping any
32
- * the site declared itself, so the dialog's locale and version filters match.
117
+ * the site declared itself, so the dialog's locale and version filters match,
118
+ * and, once a page is split, `url` as the index's `attributeForDistinct`
119
+ * unless the site set its own.
33
120
  */
34
121
  export const syncAlgolia = async (
35
122
  records: SearchRecord[],
@@ -44,16 +131,26 @@ export const syncAlgolia = async (
44
131
  const { algoliasearch }: typeof AlgoliaSdk = nodeRequire("algoliasearch");
45
132
  const client = algoliasearch(config.appId, adminKey);
46
133
  const { indexName } = config;
47
- await client.replaceAllObjects({
48
- indexName,
49
- objects: records.map((record) => ({ ...record, objectID: record._id })),
50
- });
51
- const { attributesForFaceting = [] } = await client.getSettings({
52
- indexName,
53
- });
134
+ const objects = records.flatMap(toObjects);
135
+ await client.replaceAllObjects({ indexName, objects });
136
+ const {
137
+ attributeForDistinct,
138
+ attributesForFaceting = [],
139
+ customRanking = [],
140
+ } = await client.getSettings({ indexName });
54
141
  const declared = new Set(attributesForFaceting.map(facetAttribute));
55
142
  const missing = FILTER_ATTRIBUTES.filter((name) => !declared.has(name));
56
- if (missing.length === 0) {
143
+ // Every record carries `boost` (search.boost, 1 by default): ranking by it
144
+ // after textual relevance puts boosted pages first among close matches.
145
+ // A ranking the site set in the dashboard keeps its place ahead of it.
146
+ const ranksByBoost = customRanking.some((rule) =>
147
+ /^(?:asc|desc)\(boost\)$/u.test(rule)
148
+ );
149
+ // A split page's records share its `url`: distinct on it lists the page
150
+ // once, at its best-matching record.
151
+ const setsDistinct =
152
+ objects.length > records.length && attributeForDistinct === undefined;
153
+ if (missing.length === 0 && ranksByBoost && !setsDistinct) {
57
154
  return;
58
155
  }
59
156
  const { taskID } = await client.setSettings({
@@ -63,6 +160,10 @@ export const syncAlgolia = async (
63
160
  ...attributesForFaceting,
64
161
  ...missing.map((name) => `filterOnly(${name})`),
65
162
  ],
163
+ customRanking: ranksByBoost
164
+ ? customRanking
165
+ : [...customRanking, BOOST_RANKING],
166
+ ...(setsDistinct && { attributeForDistinct: "url", distinct: true }),
66
167
  },
67
168
  });
68
169
  await client.waitForTask({ indexName, taskID });