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
@@ -0,0 +1,107 @@
1
+ import { withBasePath } from "../../core/base-path.ts";
2
+ import type { BlumeDataI18n } from "../../core/data.ts";
3
+ import { localizeRoute } from "../../core/i18n.ts";
4
+ import type { RouteSet } from "../../core/locale-links.ts";
5
+
6
+ /**
7
+ * Routing by browser language (`i18n.routeByBrowserLanguage`): a visitor who
8
+ * lands on the default language's home page from outside the site goes to
9
+ * the home page in the first language their browser prefers that the site
10
+ * has, until they pick a language with the switcher. It runs in the browser,
11
+ * from the home page's head, so it works on a static host, and the redirect
12
+ * happens before anything paints.
13
+ */
14
+
15
+ /**
16
+ * Where a language picked with the switcher is remembered. The inline script
17
+ * below spells it out.
18
+ */
19
+ export const LOCALE_STORAGE_KEY = "blume-locale";
20
+
21
+ /** What the home page's routing needs from the site. */
22
+ export interface LocaleRedirectContext {
23
+ basePath: string;
24
+ i18n: BlumeDataI18n | null;
25
+ /** The page being rendered. */
26
+ route: string;
27
+ routes: RouteSet;
28
+ }
29
+
30
+ /**
31
+ * The other languages' home pages a visitor could be sent to from `route`,
32
+ * by locale code, or `null` when this page doesn't route: the option is off,
33
+ * this isn't the default language's home, or no other home is served.
34
+ */
35
+ export const browserLanguageTargets = (
36
+ context: LocaleRedirectContext
37
+ ): Record<string, string> | null => {
38
+ const { i18n } = context;
39
+ if (!i18n?.routeByBrowserLanguage) {
40
+ return null;
41
+ }
42
+ const home = (code: string): string =>
43
+ withBasePath(context.basePath, localizeRoute("/", code, i18n));
44
+ if (context.route !== home(i18n.defaultLocale)) {
45
+ return null;
46
+ }
47
+ const targets = i18n.locales
48
+ .filter(({ code }) => code !== i18n.defaultLocale)
49
+ .map(({ code }) => [code, home(code)] as const)
50
+ .filter(([, path]) => context.routes.has(path));
51
+ return targets.length > 0 ? Object.fromEntries(targets) : null;
52
+ };
53
+
54
+ /**
55
+ * The inline script, given the default locale (`data-default`) and the other
56
+ * homes (`data-targets`). For each language the browser prefers, in order, it
57
+ * matches a locale by exact code, then by base language (`fr-CA` finds `fr`,
58
+ * `pt` finds `pt-br`): the default language stays put, another one's home is
59
+ * where the visitor goes. A visitor who picked a language, arrived from
60
+ * another page of the site, or whose storage can't be read stays too.
61
+ */
62
+ export const LOCALE_REDIRECT_SCRIPT = `(()=>{const s=document.currentScript;if(!s)return;try{if(localStorage.getItem("blume-locale")!==null)return}catch{return}if(document.referrer.startsWith(location.origin+"/"))return;let t;try{t=JSON.parse(s.dataset.targets||"")}catch{return}const d=s.dataset.default||"";const c=[d,...Object.keys(t)];const low=(v)=>v.toLowerCase();const base=(v)=>low(v).split("-")[0];for(const l of navigator.languages||[]){const m=c.find((v)=>low(v)===low(l))||c.find((v)=>low(v)===base(l))||c.find((v)=>base(v)===base(l));if(m===d)return;if(m){location.replace(t[m]+location.search+location.hash);return}}})();`;
63
+
64
+ /** A click target that can look up its ancestors: an element. */
65
+ interface ClosestTarget {
66
+ closest: (selector: string) => {
67
+ getAttribute: (name: string) => string | null;
68
+ } | null;
69
+ }
70
+
71
+ /** A click, as the listener reads it. */
72
+ interface Click {
73
+ target: EventTarget | ClosestTarget | null;
74
+ }
75
+
76
+ /** What {@link rememberLocaleChoice} listens on: the document. */
77
+ export interface ChoiceTarget {
78
+ addEventListener: (type: "click", listener: (event: Click) => void) => void;
79
+ }
80
+
81
+ const hasClosest = (target: Click["target"]): target is ClosestTarget =>
82
+ target !== null && "closest" in target;
83
+
84
+ /** The code of the switcher link a click landed on, if any. */
85
+ const chosenCode = (target: Click["target"]): string | null | undefined =>
86
+ hasClosest(target)
87
+ ? target.closest("a[data-blume-locale-choice]")?.getAttribute("hreflang")
88
+ : null;
89
+
90
+ /**
91
+ * Remember a language picked with the switcher, so routing by browser
92
+ * language stops sending the reader elsewhere. One listener on the document
93
+ * covers every switcher, on every page the client router swaps in.
94
+ */
95
+ export const rememberLocaleChoice = (target: ChoiceTarget): void => {
96
+ target.addEventListener("click", (event) => {
97
+ const code = chosenCode(event.target);
98
+ if (!code) {
99
+ return;
100
+ }
101
+ try {
102
+ localStorage.setItem(LOCALE_STORAGE_KEY, code);
103
+ } catch {
104
+ // Storage can be off; routing then stays off too (see the script).
105
+ }
106
+ });
107
+ };
@@ -270,6 +270,89 @@ export const getPagination = (flat: FlatPage[], route: string) => {
270
270
  };
271
271
  };
272
272
 
273
+ /** A group node in the sidebar tree. */
274
+ export type NavGroup = Extract<NavNode, { kind: "group" }>;
275
+
276
+ /**
277
+ * The group whose own page `route` is, when that page lists the group's
278
+ * pages: its `directory` (inherited when the group sets none) is `card` or
279
+ * `accordion`. `null` on every other page.
280
+ */
281
+ export const directoryGroupForRoute = (
282
+ nodes: NavNode[],
283
+ route: string
284
+ ): NavGroup | null => {
285
+ for (const node of nodes) {
286
+ if (node.kind !== "group") {
287
+ continue;
288
+ }
289
+ if (
290
+ node.route === route &&
291
+ (node.directory === "card" || node.directory === "accordion")
292
+ ) {
293
+ return node;
294
+ }
295
+ const nested = directoryGroupForRoute(node.children, route);
296
+ if (nested) {
297
+ return nested;
298
+ }
299
+ }
300
+ return null;
301
+ };
302
+
303
+ /** One entry in a group page's directory: a page, or a subgroup and its pages. */
304
+ export interface DirectoryEntry {
305
+ description?: string;
306
+ /** The page, or the subgroup's own page (its first page when it has none). */
307
+ href: string;
308
+ icon?: string;
309
+ label: string;
310
+ /** A subgroup's pages in sidebar order, its own page first; absent for a page. */
311
+ pages?: FlatPage[];
312
+ }
313
+
314
+ /**
315
+ * What a group's page lists: the group's pages and subgroups, in sidebar
316
+ * order, without the page itself or external links. A subgroup is described
317
+ * by its own page, and one with no page to link to is left out.
318
+ */
319
+ export const directoryEntries = (group: NavGroup): DirectoryEntry[] =>
320
+ group.children.flatMap((child): DirectoryEntry[] => {
321
+ if (child.kind === "page") {
322
+ return child.pageId && child.route !== group.route
323
+ ? [
324
+ {
325
+ description: child.description,
326
+ href: child.route,
327
+ icon: child.icon,
328
+ label: child.label,
329
+ },
330
+ ]
331
+ : [];
332
+ }
333
+ const listed = flattenPages(child.children);
334
+ const pages =
335
+ child.route && !listed.some((page) => page.route === child.route)
336
+ ? [{ label: child.label, route: child.route }, ...listed]
337
+ : listed;
338
+ const [first] = pages;
339
+ if (!first) {
340
+ return [];
341
+ }
342
+ const own = child.children.find(
343
+ (node) => node.kind === "page" && node.route === child.route
344
+ );
345
+ return [
346
+ {
347
+ description: own?.kind === "page" ? own.description : undefined,
348
+ href: child.route ?? first.route,
349
+ icon: child.icon,
350
+ label: child.label,
351
+ pages,
352
+ },
353
+ ];
354
+ });
355
+
273
356
  /**
274
357
  * A stable id for every group in a sidebar — `g<n>` by pre-order position in
275
358
  * the full tree. The layout hands `NavTree` a scoped view of that tree (a
@@ -0,0 +1,96 @@
1
+ import { track } from "./analytics-client.ts";
2
+ /**
3
+ * The "Was this page helpful?" rating (`PageFeedback.astro`). A rating is
4
+ * sent as a `feedback` analytics event, the buttons give way to a thank-you,
5
+ * and, with `feedback.comments` on, a box below asks the reader to say more.
6
+ * A comment they send goes out as a `feedback_comment` event with the same
7
+ * rating, path, and title, so the two line up in a dashboard.
8
+ *
9
+ * Both go through analytics, so under a consent layer (`consent`) the widget
10
+ * renders hidden and only shows once the reader allows analytics: from
11
+ * anyone else, an answer would go nowhere.
12
+ *
13
+ * Runs on the initial load and again after every client-router swap, which
14
+ * rebuilds the widget from server-rendered markup, so the handlers always
15
+ * bind to the freshly swapped-in elements.
16
+ */
17
+ import type { BlumeConsent } from "./consent/types.ts";
18
+
19
+ /** The longest comment the box takes. */
20
+ export const COMMENT_MAX_LENGTH = 1000;
21
+
22
+ /**
23
+ * Whether an answer would reach analytics: the site has no consent layer, or
24
+ * the reader has allowed analytics.
25
+ */
26
+ const answersReachAnalytics = (): boolean => {
27
+ // SAFETY: the consent init script (`consent/init.ts`) is the only writer of
28
+ // `window.blumeConsent`; without a consent layer it's absent.
29
+ const { blumeConsent } = window as Window & { blumeConsent?: BlumeConsent };
30
+ return blumeConsent === undefined || blumeConsent.analytics === true;
31
+ };
32
+
33
+ /**
34
+ * Show the page's rating only while its answers would reach analytics. Runs
35
+ * when the widget is wired and again on every `blume:consent` change, so a
36
+ * reader who accepts the banner gets the rating right away.
37
+ */
38
+ export const syncFeedbackVisibility = (): void => {
39
+ const root = document.querySelector<HTMLElement>(
40
+ "[data-blume-page-feedback]"
41
+ );
42
+ if (root) {
43
+ root.hidden = !answersReachAnalytics();
44
+ }
45
+ };
46
+
47
+ /** Wire the page's rating, when it has one. */
48
+ export const initFeedback = (): void => {
49
+ const root = document.querySelector("[data-blume-page-feedback]");
50
+ if (!root) {
51
+ return;
52
+ }
53
+ syncFeedbackVisibility();
54
+ const actions = root.querySelector("[data-feedback-actions]");
55
+ const thanks = root.querySelector<HTMLElement>("[data-feedback-thanks]");
56
+ const form = root.querySelector<HTMLFormElement>("[data-feedback-form]");
57
+ let helpful = "";
58
+ const rate = (value: string) => {
59
+ helpful = value;
60
+ track("feedback", {
61
+ helpful,
62
+ path: location.pathname,
63
+ title: document.title,
64
+ });
65
+ actions?.classList.add("hidden");
66
+ thanks?.classList.remove("hidden");
67
+ if (form) {
68
+ form.hidden = false;
69
+ }
70
+ // The clicked button just disappeared under focus; hand focus to the
71
+ // (live) thanks line so the reader isn't dropped at the top of the page.
72
+ // The comment box is the next stop from there.
73
+ thanks?.focus();
74
+ };
75
+ for (const button of root.querySelectorAll<HTMLElement>(
76
+ "[data-feedback-value]"
77
+ )) {
78
+ button.addEventListener("click", () =>
79
+ rate(button.dataset.feedbackValue ?? "")
80
+ );
81
+ }
82
+ form?.addEventListener("submit", (event) => {
83
+ event.preventDefault();
84
+ const comment = form.querySelector("textarea")?.value.trim() ?? "";
85
+ if (comment) {
86
+ track("feedback_comment", {
87
+ comment: comment.slice(0, COMMENT_MAX_LENGTH),
88
+ helpful,
89
+ path: location.pathname,
90
+ title: document.title,
91
+ });
92
+ }
93
+ form.hidden = true;
94
+ thanks?.focus();
95
+ });
96
+ };
@@ -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),
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Search analytics: what readers search for, what they find, and what they
3
+ * open, sent through the configured analytics adapters (`track`).
4
+ *
5
+ * The dialog searches on every keystroke, so a query is only recorded once it
6
+ * settles: a second without typing, or the reader picking a result or closing
7
+ * the dialog. Each settled query is one `search` event with the number of
8
+ * results, so a dashboard can list the queries that found nothing. Picking a
9
+ * result is a `search_select` event with its position, for click-through. The
10
+ * same query settling twice in a row (a filter toggled back and forth) counts
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.
16
+ */
17
+ import type { TrackProps } from "../analytics-client.ts";
18
+
19
+ /** The longest query sent: past this it's rarely a search term. */
20
+ export const MAX_QUERY_CHARS = 100;
21
+
22
+ /** How long a query sits untouched before it counts as settled. */
23
+ export const SETTLE_MS = 1000;
24
+
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;
34
+
35
+ /** The timer a tracker waits on; injectable for tests. */
36
+ export interface SettleTimer {
37
+ cancel: (handle: number) => void;
38
+ start: (callback: () => void, ms: number) => number;
39
+ }
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
+
49
+ /** What the dialog tells the tracker. */
50
+ export interface SearchTracker {
51
+ /** The dialog closed; record the query it was showing. */
52
+ closed: () => void;
53
+ /** The reader picked result `position` (1-based) for `query`. */
54
+ selected: (query: string, position: number, url: string) => void;
55
+ /** Results for `query` are on screen. */
56
+ settled: (query: string, results: number) => void;
57
+ }
58
+
59
+ /** A query and how many results it found. */
60
+ interface PendingSearch {
61
+ query: string;
62
+ results: number;
63
+ }
64
+
65
+ const browserTimer: SettleTimer = {
66
+ cancel: (handle) => clearTimeout(handle),
67
+ start: (callback, ms) => Number(setTimeout(callback, ms)),
68
+ };
69
+
70
+ /** A tracker for one search dialog. */
71
+ export const createSearchTracker = (
72
+ send: SendEvent,
73
+ { queries = true, timer = browserTimer }: SearchTrackerOptions = {}
74
+ ): SearchTracker => {
75
+ let pending: PendingSearch | undefined;
76
+ let handle: number | undefined;
77
+ let last: string | undefined;
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
+
89
+ const flush = (): void => {
90
+ if (handle !== undefined) {
91
+ timer.cancel(handle);
92
+ handle = undefined;
93
+ }
94
+ if (pending && pending.query !== last) {
95
+ report(
96
+ "search",
97
+ { path: location.pathname, results: pending.results },
98
+ pending.query
99
+ );
100
+ last = pending.query;
101
+ }
102
+ pending = undefined;
103
+ };
104
+
105
+ return {
106
+ closed: () => {
107
+ flush();
108
+ last = undefined;
109
+ },
110
+ selected: (query, position, url) => {
111
+ flush();
112
+ report(
113
+ "search_select",
114
+ { path: location.pathname, position, url },
115
+ query.slice(0, MAX_QUERY_CHARS)
116
+ );
117
+ },
118
+ settled: (query, results) => {
119
+ if (handle !== undefined) {
120
+ timer.cancel(handle);
121
+ }
122
+ pending = { query: query.slice(0, MAX_QUERY_CHARS), results };
123
+ handle = timer.start(flush, SETTLE_MS);
124
+ },
125
+ };
126
+ };
@@ -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
+ };
@@ -1,6 +1,6 @@
1
1
  import { Document } from "flexsearch";
2
2
 
3
- import { buildResult, RESULT_POOL } from "./types.ts";
3
+ import { buildResult, byBoost, RESULT_POOL } from "./types.ts";
4
4
  import type { IndexedDocument, SearchFn } from "./types.ts";
5
5
 
6
6
  /**
@@ -11,7 +11,7 @@ import type { IndexedDocument, SearchFn } from "./types.ts";
11
11
  type SearchDocument = Pick<
12
12
  IndexedDocument,
13
13
  "content" | "description" | "route" | "title"
14
- >;
14
+ > & { keywords: string };
15
15
 
16
16
  /**
17
17
  * FlexSearch: reuse the same static `blume-search.json` Orama ships, but build
@@ -29,11 +29,14 @@ export const createSearch = async (opts: {
29
29
  const byRoute = new Map(documents.map((doc) => [doc.route, doc]));
30
30
 
31
31
  const index = new Document<SearchDocument>({
32
- document: { id: "route", index: ["title", "description", "content"] },
32
+ document: {
33
+ id: "route",
34
+ index: ["title", "keywords", "description", "content"],
35
+ },
33
36
  tokenize: "forward",
34
37
  });
35
38
  for (const doc of documents) {
36
- index.add(doc);
39
+ index.add({ ...doc, keywords: doc.keywords?.join(" ") ?? "" });
37
40
  }
38
41
 
39
42
  // FlexSearch's in-memory search is synchronous, so the SearchFn resolves
@@ -63,6 +66,15 @@ export const createSearch = async (opts: {
63
66
  }
64
67
  }
65
68
  }
66
- return Promise.resolve(buildResult(matched, query, options?.section));
69
+ // FlexSearch returns an order, not scores: rank by position, then apply
70
+ // each page's `search.boost` to that.
71
+ const ranked = byBoost(
72
+ matched.map((doc, position) => ({
73
+ boost: doc.boost,
74
+ match: doc,
75
+ score: 1 / (position + 1),
76
+ }))
77
+ );
78
+ return Promise.resolve(buildResult(ranked, query, options?.section));
67
79
  };
68
80
  };
@@ -1,7 +1,13 @@
1
1
  import { OramaClient } from "@oramacloud/client";
2
2
 
3
3
  import type { OramaCloudOptions } from "../../../search/adapters/orama-cloud.ts";
4
- import { excerptFor, highlight, SEARCH_LIMIT } from "./types.ts";
4
+ import {
5
+ byBoost,
6
+ excerptFor,
7
+ highlight,
8
+ RESULT_POOL,
9
+ SEARCH_LIMIT,
10
+ } from "./types.ts";
5
11
  import type { SearchFn } from "./types.ts";
6
12
 
7
13
  interface OramaCloudRecord {
@@ -9,6 +15,7 @@ interface OramaCloudRecord {
9
15
  title: string;
10
16
  description?: string;
11
17
  content?: string;
18
+ boost?: number;
12
19
  }
13
20
 
14
21
  /**
@@ -25,25 +32,30 @@ export const createSearch = (opts: OramaCloudOptions): SearchFn => {
25
32
  endpoint,
26
33
  });
27
34
  return async (query, options) => {
35
+ // A pool beyond the visible limit, so each page's `search.boost` can
36
+ // lift a hit into view before the list is cut.
28
37
  const results = await client.search({
29
- limit: SEARCH_LIMIT,
38
+ limit: RESULT_POOL,
30
39
  term: query,
31
40
  // The sync carries `locale` on every record so an i18n site can scope
32
41
  // hosted results to the active language.
33
42
  ...(options?.locale && { where: { locale: options.locale } }),
34
43
  });
35
- const hits = (results?.hits ?? []).map((hit) => {
36
- const doc: OramaCloudRecord = hit.document;
37
- return {
38
- content: doc.content ?? "",
39
- excerpt: highlight(
40
- excerptFor(doc.description ?? "", doc.content ?? "", query),
41
- query
42
- ),
43
- title: highlight(doc.title, query),
44
- url: doc.url,
45
- };
46
- });
44
+ const ranked = byBoost(
45
+ (results?.hits ?? []).map((hit) => {
46
+ const doc: OramaCloudRecord = hit.document;
47
+ return { boost: doc.boost, match: doc, score: hit.score };
48
+ })
49
+ );
50
+ const hits = ranked.slice(0, SEARCH_LIMIT).map((doc) => ({
51
+ content: doc.content ?? "",
52
+ excerpt: highlight(
53
+ excerptFor(doc.description ?? "", doc.content ?? "", query),
54
+ query
55
+ ),
56
+ title: highlight(doc.title, query),
57
+ url: doc.url,
58
+ }));
47
59
  return { hits, sections: [] };
48
60
  };
49
61
  };