blume 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (601) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +113 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-6k4ftwze.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-6k4ftwze.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-zp79m0ts.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-ddndchfr.js → chunk-2f3ndkpw.js} +57 -29
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-fsmrqk8a.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-ppzjqwx2.js → chunk-2n4ps1b3.js} +17 -18
  12. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-2n4ps1b3.js.map} +3 -3
  13. package/dist/cli/{chunk-5r8g91qn.js → chunk-43zcf4gz.js} +398 -159
  14. package/dist/cli/chunk-43zcf4gz.js.map +36 -0
  15. package/dist/cli/chunk-4ejxbzee.js +39 -0
  16. package/dist/cli/chunk-4ejxbzee.js.map +10 -0
  17. package/dist/cli/{chunk-hr8ne106.js → chunk-4h8z9vv5.js} +296 -77
  18. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  19. package/dist/cli/{chunk-k7pj68a8.js → chunk-5cg2rzgf.js} +18 -16
  20. package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5cg2rzgf.js.map} +2 -2
  21. package/dist/cli/{chunk-h2ez8dzb.js → chunk-5ffywsmb.js} +4 -4
  22. package/dist/cli/{chunk-g698a744.js → chunk-5wj2qwn2.js} +5 -5
  23. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  24. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  25. package/dist/cli/{chunk-3yce002v.js → chunk-713mngdq.js} +15 -8
  26. package/dist/cli/chunk-713mngdq.js.map +14 -0
  27. package/dist/cli/{chunk-91ws1n6j.js → chunk-9geymqns.js} +17 -15
  28. package/dist/cli/{chunk-91ws1n6j.js.map → chunk-9geymqns.js.map} +2 -2
  29. package/dist/cli/{chunk-6dtt0zfn.js → chunk-9xr4dgee.js} +27 -18
  30. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  31. package/dist/cli/{chunk-sqw4ekg1.js → chunk-amrb6hgh.js} +2 -2
  32. package/dist/cli/chunk-cm89scdv.js +443 -0
  33. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  34. package/dist/cli/{chunk-wdrt2k2v.js → chunk-cmtcnh9f.js} +90 -26
  35. package/dist/cli/chunk-cmtcnh9f.js.map +10 -0
  36. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  37. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  38. package/dist/cli/{chunk-7mbqtmgb.js → chunk-e7vvk5ya.js} +21 -12
  39. package/dist/cli/chunk-e7vvk5ya.js.map +10 -0
  40. package/dist/cli/{chunk-qkb5a8sa.js → chunk-g2z9x01v.js} +6 -98
  41. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  42. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  43. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  44. package/dist/cli/{chunk-h7k3nq3v.js → chunk-gp84za0h.js} +2 -2
  45. package/dist/cli/{chunk-hqp2ajnh.js → chunk-h2a48yhd.js} +195 -73
  46. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  47. package/dist/cli/{chunk-pbg5a4s3.js → chunk-jnwj5kjj.js} +71 -47
  48. package/dist/cli/chunk-jnwj5kjj.js.map +19 -0
  49. package/dist/cli/chunk-kwrg63tm.js +31 -0
  50. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  51. package/dist/cli/{chunk-8g8ytmgx.js → chunk-nd1821sk.js} +21 -19
  52. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-nd1821sk.js.map} +2 -2
  53. package/dist/cli/{chunk-w4bxdvsa.js → chunk-ntv4qrt7.js} +17 -15
  54. package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-ntv4qrt7.js.map} +2 -2
  55. package/dist/cli/{chunk-bfwp9vp6.js → chunk-p804f3pe.js} +7 -7
  56. package/dist/cli/{chunk-4e9b9ra6.js → chunk-py62fewh.js} +6 -6
  57. package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-py62fewh.js.map} +1 -1
  58. package/dist/cli/chunk-q3wxxq39.js +126 -0
  59. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  60. package/dist/cli/{chunk-j85scx15.js → chunk-rekqz4qg.js} +142 -46
  61. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  62. package/dist/cli/{chunk-00gs3wqs.js → chunk-ta27feev.js} +1 -1
  63. package/dist/cli/{chunk-v6ya5kcb.js → chunk-xkz5xbtv.js} +4031 -1132
  64. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  65. package/dist/cli/{chunk-bbnwccaz.js → chunk-y4ykbark.js} +2 -2
  66. package/dist/cli/{chunk-n1yg3tj3.js → chunk-y5h4r2y0.js} +6 -6
  67. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-y5h4r2y0.js.map} +3 -3
  68. package/dist/cli/{chunk-nfcyttvj.js → chunk-yj17vs39.js} +9 -9
  69. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-yj17vs39.js.map} +1 -1
  70. package/dist/cli/{chunk-7vtckvaw.js → chunk-yxmk4pec.js} +16 -14
  71. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-yxmk4pec.js.map} +2 -2
  72. package/dist/cli/{chunk-gs7r695n.js → chunk-zccbjcj0.js} +22 -8
  73. package/dist/cli/{chunk-gs7r695n.js.map → chunk-zccbjcj0.js.map} +3 -3
  74. package/dist/cli/{chunk-xh43dwgw.js → chunk-zf748avv.js} +415 -225
  75. package/dist/cli/chunk-zf748avv.js.map +37 -0
  76. package/dist/cli/{chunk-d1v5rhy0.js → chunk-zhvd6adm.js} +16 -14
  77. package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-zhvd6adm.js.map} +2 -2
  78. package/dist/cli/index.js +28 -20
  79. package/dist/cli/index.js.map +5 -5
  80. package/dist/types/ai/agent-surface.d.ts +8 -7
  81. package/dist/types/ai/ai-catalog.d.ts +7 -0
  82. package/dist/types/ai/ask-context.d.ts +22 -0
  83. package/dist/types/ai/ask.d.ts +271 -58
  84. package/dist/types/ai/index.d.ts +2 -2
  85. package/dist/types/ai/link-headers.d.ts +8 -1
  86. package/dist/types/ai/openapi-components.d.ts +5 -2
  87. package/dist/types/ai/relative-links.d.ts +9 -7
  88. package/dist/types/ai/site-skill.d.ts +26 -0
  89. package/dist/types/ai/skills.d.ts +20 -1
  90. package/dist/types/ai/tar.d.ts +1 -3
  91. package/dist/types/analytics/index.d.ts +2 -0
  92. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  93. package/dist/types/analytics/schema.d.ts +14 -0
  94. package/dist/types/astro/integration.d.ts +11 -2
  95. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  96. package/dist/types/captcha/index.d.ts +28 -0
  97. package/dist/types/captcha/schema.d.ts +29 -0
  98. package/dist/types/captcha/turnstile.d.ts +28 -0
  99. package/dist/types/components/content/api-field.d.ts +59 -0
  100. package/dist/types/components/content/api-page.d.ts +95 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/helpers.d.ts +137 -0
  103. package/dist/types/components/openapi/security.d.ts +85 -0
  104. package/dist/types/consent/ethyca.d.ts +48 -0
  105. package/dist/types/consent/index.d.ts +37 -0
  106. package/dist/types/consent/native.d.ts +28 -0
  107. package/dist/types/consent/osano.d.ts +42 -0
  108. package/dist/types/consent/schema.d.ts +110 -0
  109. package/dist/types/core/base-path.d.ts +21 -7
  110. package/dist/types/core/config-input.d.ts +196 -27
  111. package/dist/types/core/data.d.ts +49 -4
  112. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  113. package/dist/types/core/footer.d.ts +8 -0
  114. package/dist/types/core/heading-markers.d.ts +5 -7
  115. package/dist/types/core/i18n-ui.d.ts +86 -0
  116. package/dist/types/core/i18n.d.ts +2 -0
  117. package/dist/types/core/includes.d.ts +8 -2
  118. package/dist/types/core/last-modified.d.ts +10 -0
  119. package/dist/types/core/localizable.d.ts +7 -0
  120. package/dist/types/core/meta.d.ts +8 -0
  121. package/dist/types/core/page-modes.d.ts +29 -0
  122. package/dist/types/core/redirect-patterns.d.ts +144 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +558 -56
  125. package/dist/types/core/sources/lower.d.ts +29 -16
  126. package/dist/types/core/sources/normalize.d.ts +13 -1
  127. package/dist/types/core/sources/read.d.ts +5 -0
  128. package/dist/types/core/sources/watch.d.ts +12 -5
  129. package/dist/types/core/standard-schema.d.ts +5 -0
  130. package/dist/types/core/types.d.ts +1 -1
  131. package/dist/types/core/variables.d.ts +42 -0
  132. package/dist/types/deploy/artifacts.d.ts +6 -4
  133. package/dist/types/deploy/headers.d.ts +5 -0
  134. package/dist/types/deploy/node-headers.d.ts +13 -2
  135. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  136. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  137. package/dist/types/deploy/platforms/types.d.ts +8 -0
  138. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  139. package/dist/types/deploy/redirects.d.ts +61 -22
  140. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  141. package/dist/types/markdown/code-title.d.ts +17 -0
  142. package/dist/types/markdown/directives.d.ts +62 -0
  143. package/dist/types/markdown/features.d.ts +24 -2
  144. package/dist/types/markdown/mdast.d.ts +63 -0
  145. package/dist/types/narration/build.d.ts +70 -0
  146. package/dist/types/narration/provider.d.ts +26 -0
  147. package/dist/types/narration/script.d.ts +154 -0
  148. package/dist/types/openapi/asyncapi.d.ts +4 -2
  149. package/dist/types/openapi/model.d.ts +12 -6
  150. package/dist/types/openapi/overlay.d.ts +54 -0
  151. package/dist/types/openapi/parse.d.ts +13 -0
  152. package/dist/types/openapi/references.d.ts +14 -4
  153. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  154. package/dist/types/ratelimit/index.d.ts +27 -0
  155. package/dist/types/ratelimit/memory.d.ts +36 -0
  156. package/dist/types/ratelimit/schema.d.ts +111 -0
  157. package/dist/types/ratelimit/upstash.d.ts +21 -0
  158. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  159. package/dist/types/reference/asyncapi.d.ts +10 -10
  160. package/dist/types/reference/graphql.d.ts +13 -10
  161. package/dist/types/reference/openapi.d.ts +33 -12
  162. package/dist/types/reference/options.d.ts +45 -5
  163. package/dist/types/reference/scalar.d.ts +13 -0
  164. package/dist/types/reference/schema.d.ts +60 -36
  165. package/dist/types/search/documents.d.ts +12 -1
  166. package/dist/types/search/orama-index.d.ts +4 -0
  167. package/dist/types/seo/metatags.d.ts +20 -0
  168. package/docs/02-deployment.mdx +44 -6
  169. package/docs/03-upgrading.mdx +4 -4
  170. package/docs/04-migrating.mdx +1 -1
  171. package/docs/08-faq.mdx +1 -1
  172. package/docs/advanced/changelog.mdx +2 -2
  173. package/docs/advanced/custom-pages.mdx +3 -3
  174. package/docs/advanced/skills.mdx +14 -0
  175. package/docs/cli/audit.mdx +38 -2
  176. package/docs/cli/evals.mdx +3 -3
  177. package/docs/cli/index.mdx +1 -0
  178. package/docs/cli/translate.mdx +3 -3
  179. package/docs/cli/version.mdx +1 -1
  180. package/docs/configuration/analytics.mdx +22 -1
  181. package/docs/configuration/assistant.mdx +188 -36
  182. package/docs/configuration/consent.mdx +100 -0
  183. package/docs/configuration/customization.mdx +6 -4
  184. package/docs/configuration/index.mdx +63 -5
  185. package/docs/configuration/meta.ts +3 -0
  186. package/docs/configuration/narration.mdx +119 -0
  187. package/docs/configuration/rate-limiting.mdx +69 -0
  188. package/docs/configuration/search.mdx +30 -2
  189. package/docs/configuration/theming.mdx +3 -1
  190. package/docs/content/components.mdx +107 -1
  191. package/docs/content/frontmatter.mdx +77 -1
  192. package/docs/content/i18n.mdx +40 -3
  193. package/docs/content/includes.mdx +16 -0
  194. package/docs/content/index.mdx +4 -2
  195. package/docs/content/islands.mdx +1 -1
  196. package/docs/content/meta.mdx +4 -1
  197. package/docs/content/meta.ts +1 -0
  198. package/docs/content/navigation.mdx +28 -6
  199. package/docs/content/sources/contentful.mdx +32 -0
  200. package/docs/content/sources/custom.mdx +39 -0
  201. package/docs/content/sources/github-releases.mdx +31 -0
  202. package/docs/content/sources/index.mdx +91 -0
  203. package/docs/content/sources/meta.ts +16 -0
  204. package/docs/content/sources/notion.mdx +32 -0
  205. package/docs/content/sources/obsidian.mdx +37 -0
  206. package/docs/content/sources/payload.mdx +30 -0
  207. package/docs/content/sources/remote-mdx.mdx +40 -0
  208. package/docs/content/sources/sanity.mdx +29 -0
  209. package/docs/content/sources/strapi.mdx +31 -0
  210. package/docs/content/syntax.mdx +81 -2
  211. package/docs/content/variables.mdx +41 -0
  212. package/docs/content/versioning.mdx +1 -1
  213. package/docs/discoverability/agent-discovery.mdx +30 -3
  214. package/docs/discoverability/index.mdx +3 -2
  215. package/docs/discoverability/llms-txt.mdx +1 -1
  216. package/docs/discoverability/markdown.mdx +2 -2
  217. package/docs/discoverability/metadata.mdx +17 -0
  218. package/docs/discoverability/open-graph.mdx +5 -3
  219. package/docs/discoverability/rss.mdx +1 -1
  220. package/docs/references/api-pages.mdx +74 -0
  221. package/docs/references/asyncapi.mdx +2 -2
  222. package/docs/references/graphql.mdx +3 -3
  223. package/docs/references/meta.ts +1 -1
  224. package/docs/references/openapi.mdx +106 -5
  225. package/docs/references/scalar.mdx +3 -1
  226. package/package.json +36 -1
  227. package/skills/blume/SKILL.md +3 -0
  228. package/skills/blume-migrate/SKILL.md +5 -5
  229. package/skills/blume-migrate/references/docusaurus.md +3 -3
  230. package/skills/blume-migrate/references/fumadocs.md +1 -1
  231. package/skills/blume-migrate/references/mintlify.md +37 -19
  232. package/skills/blume-migrate/references/nextra.md +3 -3
  233. package/skills/blume-migrate/references/starlight.md +2 -1
  234. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +67 -16
  235. package/skills/blume-write-skill/SKILL.md +82 -0
  236. package/src/ai/agent-readability.ts +4 -3
  237. package/src/ai/agent-surface.ts +9 -7
  238. package/src/ai/ai-catalog.ts +12 -5
  239. package/src/ai/api/handlers.ts +2 -2
  240. package/src/ai/api/spec.ts +2 -2
  241. package/src/ai/api-catalog.ts +6 -2
  242. package/src/ai/ask-context.ts +73 -20
  243. package/src/ai/ask-data.ts +6 -0
  244. package/src/ai/ask-tools.ts +142 -0
  245. package/src/ai/ask.ts +354 -76
  246. package/src/ai/changelog-markdown.ts +2 -2
  247. package/src/ai/component-markdown.ts +75 -0
  248. package/src/ai/cors.ts +2 -0
  249. package/src/ai/index.ts +12 -0
  250. package/src/ai/link-headers.ts +14 -5
  251. package/src/ai/llms.ts +24 -9
  252. package/src/ai/markdown.ts +2 -1
  253. package/src/ai/mcp/data.ts +6 -0
  254. package/src/ai/mcp/discovery.ts +2 -2
  255. package/src/ai/mcp/query.ts +2 -2
  256. package/src/ai/mcp/server.ts +2 -2
  257. package/src/ai/openapi-components.ts +31 -9
  258. package/src/ai/relative-links.ts +42 -26
  259. package/src/ai/serializers.ts +2 -1
  260. package/src/ai/site-skill.ts +274 -0
  261. package/src/ai/skills.ts +49 -3
  262. package/src/ai/tar.ts +139 -12
  263. package/src/ai/write-skill.ts +97 -0
  264. package/src/analytics/head.ts +4 -0
  265. package/src/analytics/index.ts +5 -0
  266. package/src/analytics/one-dollar-stats.ts +86 -0
  267. package/src/analytics/schema.ts +2 -0
  268. package/src/astro/generate.ts +133 -19
  269. package/src/astro/include-hmr.ts +31 -12
  270. package/src/astro/include-refresh.ts +19 -8
  271. package/src/astro/index.ts +1 -0
  272. package/src/astro/integration.ts +143 -14
  273. package/src/astro/module-types.ts +6 -0
  274. package/src/astro/runtime-modules.ts +28 -13
  275. package/src/astro/templates.ts +364 -67
  276. package/src/astro/variables.ts +31 -0
  277. package/src/audit/catalog.ts +19 -0
  278. package/src/audit/checks/contrast.ts +183 -0
  279. package/src/audit/checks/i18n.ts +2 -3
  280. package/src/audit/checks/indexability.ts +2 -3
  281. package/src/audit/checks/links.ts +13 -13
  282. package/src/audit/checks/sitemap.ts +2 -3
  283. package/src/audit/graph.ts +3 -1
  284. package/src/audit/redirects.ts +46 -15
  285. package/src/audit/run.ts +14 -9
  286. package/src/audit/snapshot.ts +5 -0
  287. package/src/audit/types.ts +2 -1
  288. package/src/blume-modules.d.ts +7 -0
  289. package/src/captcha/client.ts +152 -0
  290. package/src/captcha/hcaptcha.ts +36 -0
  291. package/src/captcha/index.ts +28 -0
  292. package/src/captcha/schema.ts +32 -0
  293. package/src/captcha/turnstile.ts +38 -0
  294. package/src/captcha/verify.ts +71 -0
  295. package/src/cli/args.ts +32 -0
  296. package/src/cli/command-meta.ts +5 -0
  297. package/src/cli/commands/audit.ts +4 -1
  298. package/src/cli/commands/doctor.ts +25 -17
  299. package/src/cli/commands/eval.ts +19 -11
  300. package/src/cli/commands/skill.ts +96 -0
  301. package/src/cli/commands/translate.ts +9 -12
  302. package/src/cli/commands/validate.ts +3 -1
  303. package/src/cli/dev-lock.ts +157 -37
  304. package/src/cli/index.ts +5 -0
  305. package/src/cli/report-format.ts +11 -6
  306. package/src/cli/required-secrets.ts +71 -29
  307. package/src/components/BlumePage.astro +2 -0
  308. package/src/components/colors.ts +19 -0
  309. package/src/components/content/ApiEndpoint.astro +18 -0
  310. package/src/components/content/ApiField.astro +79 -0
  311. package/src/components/content/ApiPlayground.astro +62 -0
  312. package/src/components/content/ApiRail.astro +19 -0
  313. package/src/components/content/Badge.astro +5 -3
  314. package/src/components/content/Color.astro +1 -0
  315. package/src/components/content/Component.astro +2 -2
  316. package/src/components/content/FileTree.astro +1 -0
  317. package/src/components/content/GithubInfo.astro +1 -0
  318. package/src/components/content/Panel.astro +1 -0
  319. package/src/components/content/ParamField.astro +25 -0
  320. package/src/components/content/RequestExample.astro +17 -0
  321. package/src/components/content/ResponseExample.astro +17 -0
  322. package/src/components/content/ResponseField.astro +32 -0
  323. package/src/components/content/Step.astro +1 -0
  324. package/src/components/content/Tab.astro +0 -1
  325. package/src/components/content/Tabs.astro +4 -0
  326. package/src/components/content/Tile.astro +1 -0
  327. package/src/components/content/Tooltip.astro +1 -0
  328. package/src/components/content/Tree.astro +1 -0
  329. package/src/components/content/TypeTable.astro +4 -1
  330. package/src/components/content/View.astro +21 -0
  331. package/src/components/content/ViewSwitcher.astro +168 -0
  332. package/src/components/content/api-field.ts +100 -0
  333. package/src/components/content/api-page.ts +224 -0
  334. package/src/components/content/badge-color.ts +5 -2
  335. package/src/components/content/base-href.ts +13 -36
  336. package/src/components/content/view-switcher.ts +71 -0
  337. package/src/components/index.ts +2 -0
  338. package/src/components/islands/Assistant.astro +10 -1
  339. package/src/components/islands/assistant.tsx +125 -18
  340. package/src/components/islands/base-path.ts +47 -10
  341. package/src/components/islands/code-question.ts +62 -0
  342. package/src/components/islands/hooks.ts +145 -40
  343. package/src/components/islands/support-link.ts +68 -0
  344. package/src/components/islands/webmcp.ts +4 -2
  345. package/src/components/layout/Analytics.astro +11 -2
  346. package/src/components/layout/Banner.astro +14 -10
  347. package/src/components/layout/Breadcrumbs.astro +11 -4
  348. package/src/components/layout/ConsentBanner.astro +62 -0
  349. package/src/components/layout/ConsentHead.astro +38 -0
  350. package/src/components/layout/Directory.astro +94 -0
  351. package/src/components/layout/DiscoveryLinks.astro +9 -5
  352. package/src/components/layout/DrawerSwitchers.astro +51 -0
  353. package/src/components/layout/Empty.astro +2 -2
  354. package/src/components/layout/Header.astro +124 -82
  355. package/src/components/layout/LanguageSwitcher.astro +50 -17
  356. package/src/components/layout/NarrationPlayer.astro +1085 -0
  357. package/src/components/layout/NavSelector.astro +35 -9
  358. package/src/components/layout/NavTabMenu.astro +3 -3
  359. package/src/components/layout/NavTree.astro +16 -6
  360. package/src/components/layout/PageActions.astro +4 -3
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +78 -9
  363. package/src/components/layout/Pagination.astro +3 -3
  364. package/src/components/layout/ReferenceLayout.astro +17 -6
  365. package/src/components/layout/RelatedPages.astro +42 -0
  366. package/src/components/layout/RootLayout.astro +313 -50
  367. package/src/components/layout/Search.astro +64 -30
  368. package/src/components/layout/SiteFooter.astro +124 -0
  369. package/src/components/layout/SiteMetatags.astro +16 -0
  370. package/src/components/layout/VersionBanner.astro +2 -2
  371. package/src/components/layout/analytics-client.ts +12 -0
  372. package/src/components/layout/consent/native.ts +79 -0
  373. package/src/components/layout/consent/types.ts +37 -0
  374. package/src/components/layout/footer-links.ts +53 -0
  375. package/src/components/layout/footer-socials.ts +87 -0
  376. package/src/components/layout/locale-redirect.ts +107 -0
  377. package/src/components/layout/nav-utils.ts +83 -0
  378. package/src/components/layout/page-feedback.ts +96 -0
  379. package/src/components/layout/search/analytics.ts +98 -0
  380. package/src/components/layout/search/flexsearch.ts +17 -5
  381. package/src/components/layout/search/orama-cloud.ts +26 -14
  382. package/src/components/layout/search/types.ts +20 -0
  383. package/src/components/layout/search/typesense.ts +5 -1
  384. package/src/components/layout/toc-active.ts +41 -0
  385. package/src/components/layout/toc-element.ts +8 -14
  386. package/src/components/openapi/ApiTagOperations.astro +2 -2
  387. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  388. package/src/components/openapi/GraphqlChip.astro +2 -2
  389. package/src/components/openapi/Operation.astro +163 -13
  390. package/src/components/openapi/Playground.astro +4 -4
  391. package/src/components/openapi/RequestBody.astro +6 -4
  392. package/src/components/openapi/RequestPanel.astro +63 -8
  393. package/src/components/openapi/Responses.astro +6 -4
  394. package/src/components/openapi/SchemaTable.astro +7 -0
  395. package/src/components/openapi/async-snippets.ts +4 -4
  396. package/src/components/openapi/async.ts +38 -6
  397. package/src/components/openapi/code-samples.ts +97 -0
  398. package/src/components/openapi/helpers.ts +19 -9
  399. package/src/components/openapi/message-composer.ts +6 -1
  400. package/src/components/openapi/message.ts +17 -2
  401. package/src/components/openapi/operation-model.ts +81 -11
  402. package/src/components/openapi/panel.ts +4 -2
  403. package/src/components/openapi/param-style.ts +181 -0
  404. package/src/components/openapi/playground-client.ts +73 -16
  405. package/src/components/openapi/request.ts +190 -26
  406. package/src/components/openapi/schema-tree.ts +30 -24
  407. package/src/components/openapi/security.ts +6 -3
  408. package/src/components/openapi/snippets.ts +953 -13
  409. package/src/components/openapi/ws-client.ts +18 -2
  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/base-path.ts +62 -16
  421. package/src/core/builtin-tags.ts +5 -0
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/config-input.ts +203 -27
  424. package/src/core/data.ts +44 -4
  425. package/src/core/diagnostics.ts +5 -2
  426. package/src/core/directive-diagnostics.ts +99 -0
  427. package/src/core/footer.ts +26 -0
  428. package/src/core/frontmatter.ts +21 -18
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/heading-markers.ts +5 -18
  431. package/src/core/i18n-ui.ts +82 -2
  432. package/src/core/i18n.ts +31 -3
  433. package/src/core/includes.ts +20 -3
  434. package/src/core/last-modified.ts +25 -3
  435. package/src/core/links.ts +16 -2
  436. package/src/core/locale-links.ts +5 -1
  437. package/src/core/localizable.ts +26 -0
  438. package/src/core/meta.ts +33 -19
  439. package/src/core/navigation.ts +71 -20
  440. package/src/core/page-modes.ts +56 -0
  441. package/src/core/project-graph.ts +138 -10
  442. package/src/core/redirect-patterns.ts +491 -0
  443. package/src/core/related.ts +131 -0
  444. package/src/core/schema.ts +342 -68
  445. package/src/core/sources/contentful-rich-text.ts +27 -20
  446. package/src/core/sources/filesystem.ts +20 -2
  447. package/src/core/sources/github-releases.ts +25 -6
  448. package/src/core/sources/lexical.ts +23 -18
  449. package/src/core/sources/lower.ts +201 -34
  450. package/src/core/sources/mdx-remote.ts +51 -16
  451. package/src/core/sources/normalize.ts +327 -91
  452. package/src/core/sources/notion.ts +52 -25
  453. package/src/core/sources/obsidian.ts +23 -6
  454. package/src/core/sources/portable-text.ts +38 -21
  455. package/src/core/sources/read.ts +8 -3
  456. package/src/core/sources/strapi-blocks.ts +20 -16
  457. package/src/core/sources/watch.ts +20 -7
  458. package/src/core/standard-schema.ts +10 -6
  459. package/src/core/types.ts +1 -1
  460. package/src/core/ui-packs/ar.ts +43 -0
  461. package/src/core/ui-packs/bg.ts +45 -0
  462. package/src/core/ui-packs/bn.ts +43 -0
  463. package/src/core/ui-packs/ca.ts +46 -0
  464. package/src/core/ui-packs/cs.ts +44 -0
  465. package/src/core/ui-packs/da.ts +45 -0
  466. package/src/core/ui-packs/de.ts +46 -0
  467. package/src/core/ui-packs/el.ts +45 -0
  468. package/src/core/ui-packs/es.ts +46 -0
  469. package/src/core/ui-packs/fa.ts +45 -0
  470. package/src/core/ui-packs/fi.ts +45 -0
  471. package/src/core/ui-packs/fr.ts +46 -0
  472. package/src/core/ui-packs/he.ts +43 -0
  473. package/src/core/ui-packs/hi.ts +44 -0
  474. package/src/core/ui-packs/hr.ts +45 -0
  475. package/src/core/ui-packs/hu.ts +44 -0
  476. package/src/core/ui-packs/id.ts +45 -0
  477. package/src/core/ui-packs/it.ts +45 -0
  478. package/src/core/ui-packs/ja.ts +45 -0
  479. package/src/core/ui-packs/ko.ts +44 -0
  480. package/src/core/ui-packs/nl.ts +46 -0
  481. package/src/core/ui-packs/no.ts +44 -0
  482. package/src/core/ui-packs/pl.ts +45 -0
  483. package/src/core/ui-packs/pt-br.ts +46 -0
  484. package/src/core/ui-packs/pt.ts +45 -0
  485. package/src/core/ui-packs/ro.ts +45 -0
  486. package/src/core/ui-packs/ru.ts +45 -0
  487. package/src/core/ui-packs/sk.ts +44 -0
  488. package/src/core/ui-packs/sr.ts +45 -0
  489. package/src/core/ui-packs/sv.ts +44 -0
  490. package/src/core/ui-packs/th.ts +43 -0
  491. package/src/core/ui-packs/tr.ts +45 -0
  492. package/src/core/ui-packs/uk.ts +45 -0
  493. package/src/core/ui-packs/vi.ts +45 -0
  494. package/src/core/ui-packs/zh-tw.ts +43 -0
  495. package/src/core/ui-packs/zh.ts +43 -0
  496. package/src/core/variables.ts +103 -0
  497. package/src/core/version-cut.ts +52 -15
  498. package/src/deploy/artifacts.ts +104 -14
  499. package/src/deploy/cloudflare-negotiation.ts +39 -17
  500. package/src/deploy/headers.ts +10 -5
  501. package/src/deploy/node-headers.ts +64 -10
  502. package/src/deploy/platforms/cloudflare.ts +44 -3
  503. package/src/deploy/platforms/netlify.ts +34 -1
  504. package/src/deploy/platforms/node.ts +1 -0
  505. package/src/deploy/platforms/static.ts +1 -0
  506. package/src/deploy/platforms/types.ts +8 -0
  507. package/src/deploy/platforms/vercel.ts +38 -2
  508. package/src/deploy/redirects.ts +139 -36
  509. package/src/deploy/robots.ts +2 -2
  510. package/src/deploy/rss.ts +4 -3
  511. package/src/deploy/sitemap.ts +7 -5
  512. package/src/deploy/vercel-negotiation.ts +36 -0
  513. package/src/markdown/api-rail.ts +259 -0
  514. package/src/markdown/base-links.ts +34 -36
  515. package/src/markdown/code-title.ts +35 -9
  516. package/src/markdown/directives.ts +242 -36
  517. package/src/markdown/features.ts +20 -2
  518. package/src/markdown/fence-meta.ts +2 -0
  519. package/src/markdown/include.ts +11 -1
  520. package/src/markdown/index.ts +35 -11
  521. package/src/markdown/math.ts +4 -4
  522. package/src/markdown/mdast.ts +5 -2
  523. package/src/markdown/relative-links.ts +12 -4
  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 +136 -11
  531. package/src/og/derive.ts +41 -31
  532. package/src/og/index.ts +1 -0
  533. package/src/openapi/asyncapi.ts +4 -2
  534. package/src/openapi/model.ts +77 -36
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +55 -2
  537. package/src/openapi/proxy.ts +63 -10
  538. package/src/openapi/references.ts +21 -5
  539. package/src/openapi/render-mdx.ts +46 -3
  540. package/src/openapi/scalar.ts +40 -6
  541. package/src/openapi/source.ts +1 -1
  542. package/src/ratelimit/cloudflare.ts +54 -0
  543. package/src/ratelimit/index.ts +30 -0
  544. package/src/ratelimit/memory.ts +45 -0
  545. package/src/ratelimit/runtime.ts +229 -0
  546. package/src/ratelimit/schema.ts +28 -0
  547. package/src/ratelimit/upstash.ts +30 -0
  548. package/src/ratelimit/wrangler.ts +70 -0
  549. package/src/reference/asyncapi.ts +2 -2
  550. package/src/reference/graphql.ts +5 -2
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +51 -7
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +201 -28
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/version-scope.ts +30 -0
  557. package/src/search/documents.ts +93 -28
  558. package/src/search/orama-index.ts +28 -3
  559. package/src/search/pagefind-ranking.ts +39 -0
  560. package/src/search/popular.ts +2 -1
  561. package/src/search/sync/algolia.ts +15 -4
  562. package/src/search/sync/orama-cloud.ts +3 -0
  563. package/src/search/sync/typesense.ts +5 -0
  564. package/src/seo/jsonld.ts +7 -3
  565. package/src/seo/metatags.ts +67 -0
  566. package/src/theme/chrome-icons.ts +1 -0
  567. package/src/theme/color.ts +500 -0
  568. package/src/theme/entry.ts +56 -3
  569. package/src/theme/palette.ts +121 -45
  570. package/src/translate/meta.ts +68 -24
  571. package/src/translate/prompts.ts +1 -0
  572. package/src/translate/run.ts +3 -3
  573. package/src/translate/validate.ts +12 -3
  574. package/src/translate/work-list.ts +56 -17
  575. package/src/upgrade/upgrade.ts +4 -171
  576. package/dist/cli/chunk-3yce002v.js.map +0 -14
  577. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  578. package/dist/cli/chunk-6dtt0zfn.js.map +0 -10
  579. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  580. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  581. package/dist/cli/chunk-esh98wmb.js +0 -23
  582. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  583. package/dist/cli/chunk-hqp2ajnh.js.map +0 -35
  584. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  585. package/dist/cli/chunk-j85scx15.js.map +0 -10
  586. package/dist/cli/chunk-mqc662a6.js +0 -75
  587. package/dist/cli/chunk-mqc662a6.js.map +0 -11
  588. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  589. package/dist/cli/chunk-qkb5a8sa.js.map +0 -10
  590. package/dist/cli/chunk-v6ya5kcb.js.map +0 -189
  591. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  592. package/dist/cli/chunk-xh43dwgw.js.map +0 -36
  593. package/docs/content/sources.mdx +0 -338
  594. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-1n228xqb.js.map} +0 -0
  595. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-5ffywsmb.js.map} +0 -0
  596. /package/dist/cli/{chunk-g698a744.js.map → chunk-5wj2qwn2.js.map} +0 -0
  597. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-amrb6hgh.js.map} +0 -0
  598. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-gp84za0h.js.map} +0 -0
  599. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-p804f3pe.js.map} +0 -0
  600. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-ta27feev.js.map} +0 -0
  601. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-y4ykbark.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
+ };
@@ -0,0 +1,98 @@
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
+ import type { TrackProps } from "../analytics-client.ts";
14
+
15
+ /** The longest query sent: past this it's rarely a search term. */
16
+ export const MAX_QUERY_CHARS = 100;
17
+
18
+ /** How long a query sits untouched before it counts as settled. */
19
+ export const SETTLE_MS = 1000;
20
+
21
+ /** Sends one analytics event. */
22
+ export type SendEvent = (event: string, props: TrackProps) => void;
23
+
24
+ /** The timer a tracker waits on; injectable for tests. */
25
+ export interface SettleTimer {
26
+ cancel: (handle: number) => void;
27
+ start: (callback: () => void, ms: number) => number;
28
+ }
29
+
30
+ /** What the dialog tells the tracker. */
31
+ export interface SearchTracker {
32
+ /** The dialog closed; record the query it was showing. */
33
+ closed: () => void;
34
+ /** The reader picked result `position` (1-based) for `query`. */
35
+ selected: (query: string, position: number, url: string) => void;
36
+ /** Results for `query` are on screen. */
37
+ settled: (query: string, results: number) => void;
38
+ }
39
+
40
+ /** A query and how many results it found. */
41
+ interface PendingSearch {
42
+ query: string;
43
+ results: number;
44
+ }
45
+
46
+ const browserTimer: SettleTimer = {
47
+ cancel: (handle) => clearTimeout(handle),
48
+ start: (callback, ms) => Number(setTimeout(callback, ms)),
49
+ };
50
+
51
+ /** A tracker for one search dialog. */
52
+ export const createSearchTracker = (
53
+ send: SendEvent,
54
+ timer: SettleTimer = browserTimer
55
+ ): SearchTracker => {
56
+ let pending: PendingSearch | undefined;
57
+ let handle: number | undefined;
58
+ let last: string | undefined;
59
+
60
+ const flush = (): void => {
61
+ if (handle !== undefined) {
62
+ timer.cancel(handle);
63
+ handle = undefined;
64
+ }
65
+ if (pending && pending.query !== last) {
66
+ send("search", {
67
+ path: location.pathname,
68
+ query: pending.query,
69
+ results: pending.results,
70
+ });
71
+ last = pending.query;
72
+ }
73
+ pending = undefined;
74
+ };
75
+
76
+ return {
77
+ closed: () => {
78
+ flush();
79
+ last = undefined;
80
+ },
81
+ selected: (query, position, url) => {
82
+ flush();
83
+ send("search_select", {
84
+ path: location.pathname,
85
+ position,
86
+ query: query.slice(0, MAX_QUERY_CHARS),
87
+ url,
88
+ });
89
+ },
90
+ settled: (query, results) => {
91
+ if (handle !== undefined) {
92
+ timer.cancel(handle);
93
+ }
94
+ pending = { query: query.slice(0, MAX_QUERY_CHARS), results };
95
+ handle = timer.start(flush, SETTLE_MS);
96
+ },
97
+ };
98
+ };
@@ -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
  };
@@ -53,8 +53,28 @@ export interface IndexedDocument {
53
53
  section?: string;
54
54
  locale?: string;
55
55
  version?: string;
56
+ /** The page's relevance multiplier (`search.boost`). */
57
+ boost?: number;
58
+ /** Extra terms the page is found by (`search.keywords`). */
59
+ keywords?: string[];
56
60
  }
57
61
 
62
+ /** Something ranked with a relevance score, and the page it belongs to. */
63
+ export interface ScoredMatch<Match> {
64
+ boost?: number;
65
+ match: Match;
66
+ score: number;
67
+ }
68
+
69
+ /**
70
+ * Re-rank matches by score times their page's `search.boost`. The sort is
71
+ * stable, so equal scores keep the provider's order.
72
+ */
73
+ export const byBoost = <Match>(scored: ScoredMatch<Match>[]): Match[] =>
74
+ scored
75
+ .toSorted((a, b) => b.score * (b.boost ?? 1) - a.score * (a.boost ?? 1))
76
+ .map(({ match }) => match);
77
+
58
78
  /** Max results surfaced in the dialog. */
59
79
  export const SEARCH_LIMIT = 12;
60
80
 
@@ -34,7 +34,11 @@ export const createSearch = (opts: TypesenseOptions): SearchFn => {
34
34
  {
35
35
  per_page: SEARCH_LIMIT,
36
36
  q: query,
37
- query_by: "title,description,content",
37
+ query_by: "title,keywords,description,content",
38
+ // Relevance first, in ten bands, then each page's `search.boost`
39
+ // within a band: a boost lifts a page past similar matches without
40
+ // floating it over clearly better ones.
41
+ sort_by: "_text_match(buckets: 10):desc,boost:desc",
38
42
  // The sync marks `locale` and `version` as facets so hosted results
39
43
  // scope to the active language and the viewed docs version (the
40
44
  // current docs upload as "current").
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The scrollspy's choice of the active "On this page" entry, kept apart from
3
+ * the `<blume-toc>` element (toc-element.ts) so it runs without a DOM.
4
+ */
5
+
6
+ /** The two measurements the scrollspy takes of a heading. */
7
+ export interface MeasuredHeading {
8
+ getBoundingClientRect: () => { top: number };
9
+ getClientRects: () => { length: number };
10
+ }
11
+
12
+ /**
13
+ * The entry for the section being read: the last heading at or above the
14
+ * trigger line (the first heading before any has reached it), or the last
15
+ * heading once the page is scrolled to the bottom.
16
+ *
17
+ * Only rendered headings compete. A heading inside a hidden tab panel
18
+ * (`display: none`) has no boxes and measures `top: 0`, so it would always
19
+ * read as scrolled past and hold the highlight from the top of the page.
20
+ */
21
+ export const activeTocEntry = <Entry extends { heading: MeasuredHeading }>(
22
+ entries: readonly Entry[],
23
+ triggerOffset: number,
24
+ scrolledToBottom: boolean
25
+ ): Entry | null => {
26
+ const rendered = entries.filter(
27
+ ({ heading }) => heading.getClientRects().length > 0
28
+ );
29
+ if (scrolledToBottom) {
30
+ return rendered.at(-1) ?? null;
31
+ }
32
+ // Headings are in document order, so the last one whose top has reached the
33
+ // trigger line is the section currently being read; default to the first.
34
+ let active = rendered[0] ?? null;
35
+ for (const entry of rendered) {
36
+ if (entry.heading.getBoundingClientRect().top <= triggerOffset) {
37
+ active = entry;
38
+ }
39
+ }
40
+ return active;
41
+ };
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * The active heading is chosen by rect (the last heading at or above a trigger
8
8
  * line just below the sticky header), so it stays correct for sections taller
9
- * than the viewport and at the bottom of the page. An IntersectionObserver is the
9
+ * than the viewport and at the bottom of the page; headings that aren't rendered
10
+ * (inside a hidden tab panel) are skipped (see toc-active.ts). An IntersectionObserver is the
10
11
  * cheap primary trigger — it only fires as headings cross the band near the top —
11
12
  * and a passive, rAF-throttled scroll listener covers the one case it can't: a
12
13
  * final section too short to push its heading past the trigger line.
@@ -14,6 +15,8 @@
14
15
  * Imported for its side effect (registers the element) from RootLayout's script.
15
16
  */
16
17
 
18
+ import { activeTocEntry } from "./toc-active.ts";
19
+
17
20
  interface TocEntry {
18
21
  heading: HTMLElement;
19
22
  link: HTMLAnchorElement;
@@ -93,19 +96,10 @@ class BlumeToc extends HTMLElement {
93
96
  const scrolledToBottom =
94
97
  window.innerHeight + window.scrollY >=
95
98
  document.documentElement.scrollHeight - 2;
96
- if (scrolledToBottom) {
97
- return this.#entries.at(-1)?.link ?? null;
98
- }
99
-
100
- // Headings are in document order, so the last one whose top has reached the
101
- // trigger line is the section currently being read; default to the first.
102
- let active = this.#entries[0]?.link ?? null;
103
- for (const { heading, link } of this.#entries) {
104
- if (heading.getBoundingClientRect().top <= TRIGGER_OFFSET) {
105
- active = link;
106
- }
107
- }
108
- return active;
99
+ return (
100
+ activeTocEntry(this.#entries, TRIGGER_OFFSET, scrolledToBottom)?.link ??
101
+ null
102
+ );
109
103
  }
110
104
  }
111
105
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import specs from "blume:openapi";
3
3
  import { specOf } from "../../openapi/model.ts";
4
- import { withBase } from "../islands/base-path.ts";
4
+ import { withMountedBase } from "../islands/base-path.ts";
5
5
  import MethodBadge from "./MethodBadge.astro";
6
6
 
7
7
  // The operation-link list for one tag section of an API overview page. The
@@ -27,7 +27,7 @@ const operations = Object.values(specOf(specs, source)?.operations ?? {}).filter
27
27
  <li>
28
28
  <a
29
29
  class="flex items-start gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
30
- href={withBase(operation.route)}
30
+ href={withMountedBase(operation.route)}
31
31
  >
32
32
  <MethodBadge class="mt-0.5 shrink-0" method={operation.method} />
33
33
  {/* Title over path, stacked, so a long summary and a long route each