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
@@ -1,9 +1,12 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
+ import { captchaToken } from "../../captcha/client.ts";
4
+ import type { CaptchaSettings } from "../../captcha/schema.ts";
3
5
  import type { BlumeClientData } from "../../core/data.ts";
4
6
  import { track } from "../layout/analytics-client.ts";
5
7
  import type { SearchFn, SearchResult } from "../layout/search/types.ts";
6
8
  import { joinBase, stripBase } from "./base-path.ts";
9
+ import { newThreadId } from "./support-link.ts";
7
10
 
8
11
  /**
9
12
  * React hooks for Blume islands.
@@ -23,27 +26,29 @@ import { joinBase, stripBase } from "./base-path.ts";
23
26
 
24
27
  export type { BlumeClientData } from "../../core/data.ts";
25
28
 
26
- let cachedData: BlumeClientData | null = null;
29
+ /**
30
+ * The last parsed snapshot, keyed by the script text it came from. Each page
31
+ * renders its own snapshot (its route and title, its locale's navigation), and
32
+ * a client-router navigation swaps in the new page's tag, so the cache is only
33
+ * reused while the tag's text is unchanged.
34
+ */
35
+ let cached: { data: BlumeClientData; text: string } | null = null;
27
36
 
28
- /** Read + parse the injected snapshot (memoized); null on the server. */
37
+ /** Read + parse the current page's injected snapshot (memoized per text). */
29
38
  const readClientData = (): BlumeClientData | null => {
30
- if (cachedData) {
31
- return cachedData;
32
- }
33
- // `document` is undeclared on the server; probing `globalThis` avoids both
34
- // the bare-reference ReferenceError and a `typeof` sniff.
35
- if (!("document" in globalThis)) {
39
+ const text = document.querySelector("#blume-client-data")?.textContent;
40
+ if (!text) {
36
41
  return null;
37
42
  }
38
- const element = document.querySelector("#blume-client-data");
39
- if (!element?.textContent) {
40
- return null;
43
+ if (cached?.text === text) {
44
+ return cached.data;
41
45
  }
42
46
  try {
43
47
  // SAFETY: the layout serialized this script tag's JSON from the same
44
48
  // `BlumeClientData` snapshot this reads back.
45
- cachedData = JSON.parse(element.textContent) as BlumeClientData;
46
- return cachedData;
49
+ const data = JSON.parse(text) as BlumeClientData;
50
+ cached = { data, text };
51
+ return data;
47
52
  } catch {
48
53
  return null;
49
54
  }
@@ -51,15 +56,29 @@ const readClientData = (): BlumeClientData | null => {
51
56
 
52
57
  /**
53
58
  * Read the injected snapshot after mount. `null` on the server and on the first
54
- * client render (so hydration matches), then the data once mounted.
59
+ * client render (so hydration matches), then the data once mounted, re-read
60
+ * after every client-router swap.
55
61
  */
56
62
  const useClientData = (): BlumeClientData | null => {
57
63
  const [data, setData] = useState<BlumeClientData | null>(null);
58
- // Intentional post-mount hydration guard: `null` on the server and first
59
- // client render so hydration matches, then the snapshot once mounted. The
60
- // extra render is required; do not seed the initial value from the DOM.
61
- // oxlint-disable-next-line react/react-compiler, react/set-state-in-effect, react-doctor/no-initialize-state -- deliberate SSR hydration guard
62
- useEffect(() => setData(readClientData()), []);
64
+ useEffect(() => {
65
+ // `document` is undeclared on the server; probing `globalThis` avoids both
66
+ // the bare-reference ReferenceError and a `typeof` sniff.
67
+ if (!("document" in globalThis)) {
68
+ return;
69
+ }
70
+ const sync = () => setData(readClientData());
71
+ // Intentional post-mount hydration guard: `null` on the server and first
72
+ // client render so hydration matches, then the snapshot once mounted. The
73
+ // extra render is required; do not seed the initial value from the DOM.
74
+ // oxlint-disable-next-line react/react-compiler, react/set-state-in-effect, react-doctor/no-initialize-state -- deliberate SSR hydration guard
75
+ sync();
76
+ // An island kept across navigations (`transition:persist`) outlives the
77
+ // page it mounted on; the swap brings the next page's snapshot, so read
78
+ // it again or the hook keeps answering with the first page's data.
79
+ document.addEventListener("astro:after-swap", sync);
80
+ return () => document.removeEventListener("astro:after-swap", sync);
81
+ }, []);
63
82
  return data;
64
83
  };
65
84
 
@@ -143,11 +162,22 @@ export interface UseAssistant {
143
162
  loading: boolean;
144
163
  messages: AskMessage[];
145
164
  reset: () => void;
165
+ /**
166
+ * The conversation's id, new after every `reset()`. The `ask` analytics
167
+ * events carry it as `thread`, and the support link passes it along.
168
+ */
169
+ thread: string;
146
170
  }
147
171
 
148
172
  const DEFAULT_ASK_ENDPOINT = joinBase(import.meta.env.BASE_URL, "api/ask");
149
173
 
150
174
  export interface UseAssistantOptions {
175
+ /**
176
+ * The site's bot check (`ai.assistant.captcha`), as `useBlume().config
177
+ * .assistant.captcha` holds it: each question gets a fresh token, sent as
178
+ * `captcha` in the request body.
179
+ */
180
+ captcha?: CaptchaSettings | null;
151
181
  /** Existing assistant endpoint; defaults to Blume's generated `/api/ask`. */
152
182
  endpoint?: string;
153
183
  /**
@@ -156,11 +186,38 @@ export interface UseAssistantOptions {
156
186
  * dictionary string.
157
187
  */
158
188
  errorMessage?: string;
189
+ /**
190
+ * Shown as the assistant's answer when the route's rate limit turns the
191
+ * question away (`429`). Defaults to an English notice; the built-in
192
+ * island passes its localized dictionary string.
193
+ */
194
+ rateLimitMessage?: string;
195
+ /**
196
+ * Shown as the assistant's answer when the bot check fails, in the browser
197
+ * or at the route (`403`). Defaults to an English notice; the built-in
198
+ * island passes its localized dictionary string.
199
+ */
200
+ verifyMessage?: string;
159
201
  }
160
202
 
161
203
  /** Shown as the assistant's answer when the request fails or throws. */
162
204
  const ASK_ERROR = "Something went wrong answering that. Please try again.";
163
205
 
206
+ /** Shown as the assistant's answer when the rate limit turns a question away. */
207
+ const ASK_RATE_LIMITED =
208
+ "You've asked a lot of questions. Try again in a few minutes.";
209
+
210
+ /** Shown as the assistant's answer when the bot check fails. */
211
+ const ASK_VERIFY_FAILED = "We couldn't check that you're human. Try again.";
212
+
213
+ /** What the assistant sends the route. */
214
+ interface AskPayload {
215
+ /** The bot check's token, when the site has one. */
216
+ captcha?: string;
217
+ messages: AskMessage[];
218
+ page: { path: string };
219
+ }
220
+
164
221
  /** How the assistant's generated route opens its missing-credential notice. */
165
222
  const NOT_CONFIGURED = /^The assistant is not configured/u;
166
223
 
@@ -183,6 +240,26 @@ const unavailableNotice = async (
183
240
  return NOT_CONFIGURED.test(text) && text.length <= 200 ? text : null;
184
241
  };
185
242
 
243
+ /** The notices the route's refusals map to. */
244
+ interface RefusalNotices {
245
+ rateLimitMessage: string;
246
+ verifyMessage: string;
247
+ }
248
+
249
+ /**
250
+ * The reader-facing notice for the route turning a question away: the rate
251
+ * limit (`429`) or the bot check (`403`), or `null` for any other status.
252
+ */
253
+ const refusalNotice = (
254
+ status: number,
255
+ notices: RefusalNotices
256
+ ): string | null => {
257
+ if (status === 429) {
258
+ return notices.rateLimitMessage;
259
+ }
260
+ return status === 403 ? notices.verifyMessage : null;
261
+ };
262
+
186
263
  /** The current route with the deployment base stripped, for page grounding. */
187
264
  const currentPath = (): string =>
188
265
  stripBase(import.meta.env.BASE_URL, window.location.pathname);
@@ -196,8 +273,12 @@ export const useAssistant = (
196
273
  ): UseAssistant => {
197
274
  const endpoint = options.endpoint ?? DEFAULT_ASK_ENDPOINT;
198
275
  const errorMessage = options.errorMessage ?? ASK_ERROR;
276
+ const rateLimitMessage = options.rateLimitMessage ?? ASK_RATE_LIMITED;
277
+ const verifyMessage = options.verifyMessage ?? ASK_VERIFY_FAILED;
278
+ const { captcha } = options;
199
279
  const [messages, setMessages] = useState<AskMessage[]>([]);
200
280
  const [loading, setLoading] = useState(false);
281
+ const [thread, setThread] = useState(newThreadId);
201
282
  // The stream writes into the conversation via state updates, so `reset()`
202
283
  // mid-answer must revoke the in-flight request's right to write — otherwise
203
284
  // its next chunk re-appends the assistant bubble onto the emptied list, and
@@ -239,7 +320,7 @@ export const useAssistant = (
239
320
  ) =>
240
321
  track(
241
322
  event,
242
- { ...props, path: pathname, questionChars: trimmed.length },
323
+ { ...props, path: pathname, questionChars: trimmed.length, thread },
243
324
  { question: trimmed }
244
325
  );
245
326
  report("ask", {});
@@ -264,12 +345,32 @@ export const useAssistant = (
264
345
  const assistant: AskMessage = { content: "", role: "assistant" };
265
346
  setMessages([...history, assistant]);
266
347
  setLoading(true);
348
+ // Answer with a notice instead: an error, or the route turning the
349
+ // question away. Silent once a reset has revoked the question.
350
+ const fail = (notice: string) => {
351
+ if (live()) {
352
+ outcome("ask_error", { status });
353
+ assistant.content = notice;
354
+ setMessages([...history, { ...assistant }]);
355
+ }
356
+ };
267
357
  try {
358
+ // A fresh bot-check token per question, when the site has a check;
359
+ // `null` when the check didn't pass in the browser.
360
+ const token = captcha
361
+ ? await captchaToken(captcha).catch(() => null)
362
+ : undefined;
363
+ if (token === null) {
364
+ fail(verifyMessage);
365
+ return;
366
+ }
367
+ const payload: AskPayload = {
368
+ captcha: token,
369
+ messages: history,
370
+ page: { path },
371
+ };
268
372
  const response = await fetch(endpoint, {
269
- body: JSON.stringify({
270
- messages: history,
271
- page: { path },
272
- }),
373
+ body: JSON.stringify(payload),
273
374
  headers: { "content-type": "application/json" },
274
375
  method: "POST",
275
376
  signal: controller.signal,
@@ -278,12 +379,12 @@ export const useAssistant = (
278
379
  if (!response.ok) {
279
380
  // An error body (JSON, HTML error page) must not stream in as the
280
381
  // assistant's answer — only the route's own not-configured notice.
281
- const notice = await unavailableNotice(response).catch(() => null);
282
- if (live()) {
283
- outcome("ask_error", { status });
284
- assistant.content = notice ?? errorMessage;
285
- setMessages([...history, { ...assistant }]);
286
- }
382
+ const notice =
383
+ refusalNotice(status, {
384
+ rateLimitMessage,
385
+ verifyMessage,
386
+ }) ?? (await unavailableNotice(response).catch(() => null));
387
+ fail(notice ?? errorMessage);
287
388
  return;
288
389
  }
289
390
  const reader = response.body?.getReader();
@@ -316,20 +417,14 @@ export const useAssistant = (
316
417
  if (assistant.content) {
317
418
  outcome("ask_answer", { chars: assistant.content.length });
318
419
  } else {
319
- outcome("ask_error", { status });
320
- assistant.content = errorMessage;
321
- setMessages([...history, { ...assistant }]);
420
+ fail(errorMessage);
322
421
  }
323
422
  }
324
423
  } catch {
325
424
  // A thrown fetch (offline, DNS failure, CORS) must not strand the
326
425
  // pre-appended empty assistant message as a stuck placeholder. A
327
426
  // reset's abort lands here too — the guard keeps it silent.
328
- if (live()) {
329
- outcome("ask_error", { status });
330
- assistant.content = errorMessage;
331
- setMessages([...history, { ...assistant }]);
332
- }
427
+ fail(errorMessage);
333
428
  // oxlint-disable-next-line react/todo -- React Compiler cannot lower try/finally; the hook stays manually memoized above
334
429
  } finally {
335
430
  if (live()) {
@@ -337,7 +432,16 @@ export const useAssistant = (
337
432
  }
338
433
  }
339
434
  },
340
- [endpoint, errorMessage, loading, messages]
435
+ [
436
+ captcha,
437
+ endpoint,
438
+ errorMessage,
439
+ loading,
440
+ messages,
441
+ rateLimitMessage,
442
+ thread,
443
+ verifyMessage,
444
+ ]
341
445
  );
342
446
 
343
447
  // Retained for the compiler-off opt-out path (`react: { compiler: false }`):
@@ -348,9 +452,10 @@ export const useAssistant = (
348
452
  abortRef.current?.abort();
349
453
  abortRef.current = null;
350
454
  setMessages([]);
455
+ setThread(newThreadId());
351
456
  // The in-flight `ask`'s finally is now stale and won't clear this.
352
457
  setLoading(false);
353
458
  }, []);
354
459
 
355
- return { ask, loading, messages, reset };
460
+ return { ask, loading, messages, reset, thread };
356
461
  };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The assistant's handoff to your support channel (`ai.assistant.support`).
3
+ * A `mailto:` link starts an email with the conversation as its body, so the
4
+ * reader doesn't have to repeat themselves; any other link gets the
5
+ * conversation's `thread` id in its query, the same id the assistant's
6
+ * analytics events carry, so a support team can find the conversation.
7
+ */
8
+
9
+ /** One turn of the conversation. */
10
+ export interface SupportMessage {
11
+ content: string;
12
+ role: "assistant" | "user";
13
+ }
14
+
15
+ /** What the link is built from. */
16
+ export interface SupportContext {
17
+ /** The label the transcript gives the assistant's turns. */
18
+ ai: string;
19
+ messages: readonly SupportMessage[];
20
+ /** The email's subject. */
21
+ subject: string;
22
+ thread: string;
23
+ /** The label the transcript gives the reader's turns. */
24
+ you: string;
25
+ }
26
+
27
+ /**
28
+ * How much conversation an email carries: mail clients cut long `mailto:`
29
+ * links, so a long conversation is trimmed from the start, keeping the
30
+ * latest turns.
31
+ */
32
+ export const MAX_TRANSCRIPT_CHARS = 1500;
33
+
34
+ const transcriptOf = (context: SupportContext): string => {
35
+ const full = context.messages
36
+ .map(
37
+ (message) =>
38
+ `${message.role === "user" ? context.you : context.ai}: ${message.content}`
39
+ )
40
+ .join("\n\n");
41
+ return full.length > MAX_TRANSCRIPT_CHARS
42
+ ? `…${full.slice(-MAX_TRANSCRIPT_CHARS)}`
43
+ : full;
44
+ };
45
+
46
+ /** The support link for this conversation. */
47
+ export const supportHref = (
48
+ support: string,
49
+ context: SupportContext
50
+ ): string => {
51
+ if (support.startsWith("mailto:")) {
52
+ // `mailto:` wants %20, not the `+` URLSearchParams writes for a space.
53
+ const body = `${transcriptOf(context)}\n\n(${context.thread})`;
54
+ const separator = support.includes("?") ? "&" : "?";
55
+ return `${support}${separator}subject=${encodeURIComponent(context.subject)}&body=${encodeURIComponent(body)}`;
56
+ }
57
+ const url = new URL(support, "https://blume.invalid");
58
+ url.searchParams.set("thread", context.thread);
59
+ return url.origin === "https://blume.invalid"
60
+ ? `${url.pathname}${url.search}${url.hash}`
61
+ : url.href;
62
+ };
63
+
64
+ /** A fresh conversation id: 16 hex characters. */
65
+ export const newThreadId = (): string =>
66
+ Array.from(crypto.getRandomValues(new Uint8Array(8)), (byte) =>
67
+ byte.toString(16).padStart(2, "0")
68
+ ).join("");
@@ -1,7 +1,7 @@
1
1
  import { unescape } from "html-escaper";
2
2
 
3
3
  import type { SearchFn } from "../layout/search/types.ts";
4
- import { joinBase, prefixBase } from "./base-path.ts";
4
+ import { joinBase, mountBase, prefixBase } from "./base-path.ts";
5
5
 
6
6
  /**
7
7
  * WebMCP (W3C Web Machine Learning CG): in-page tools an agentic browser can
@@ -144,7 +144,7 @@ export const buildWebMcpTools = (options: WebMcpToolOptions): WebMcpTool[] => {
144
144
  hits
145
145
  .map(
146
146
  (hit) =>
147
- `${plain(hit.title)} — ${prefixBase(options.base, hit.url)}\n${plain(hit.excerpt)}`
147
+ `${plain(hit.title)} — ${mountBase(options.base, hit.url)}\n${plain(hit.excerpt)}`
148
148
  )
149
149
  .join("\n\n")
150
150
  );
@@ -175,6 +175,8 @@ export const buildWebMcpTools = (options: WebMcpToolOptions): WebMcpTool[] => {
175
175
  }
176
176
  const trimmed = route.length > 1 ? route.replace(/\/+$/u, "") : route;
177
177
  const target = trimmed === "/" ? "/index" : trimmed;
178
+ // The agent may hand back a served URL `search_docs` returned, base
179
+ // included, so a path already under the base keeps it (`prefixBase`).
178
180
  const response = await fetchFn(
179
181
  `${prefixBase(options.base, target)}.md`
180
182
  ).catch(() => null);
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import VercelAnalytics from "@vercel/analytics/astro";
3
+ import data from "blume:data";
3
4
 
4
5
  import { analyticsHead } from "../../analytics/head.ts";
5
6
  import type { BlumeDataConfig } from "../../core/data.ts";
@@ -10,12 +11,20 @@ import type { BlumeDataConfig } from "../../core/data.ts";
10
11
  // component for `vercel()`, one <script> for every other adapter. Loads ONLY
11
12
  // in production builds so `blume dev` stays clean and local traffic never
12
13
  // reaches your analytics.
14
+ //
15
+ // With `consent` configured, every tag is held as inert `text/plain` until
16
+ // the reader allows analytics, when the consent runtime runs it
17
+ // (`consent/client.ts`); Vercel's component sends nothing until then instead
18
+ // (`consent/init.ts`).
13
19
  interface Props {
14
20
  analytics?: BlumeDataConfig["analytics"] | null;
15
21
  }
16
22
 
17
23
  const { analytics } = Astro.props;
18
24
  const nodes = analyticsHead(import.meta.env.PROD ? (analytics ?? []) : []);
25
+ const held = data.config.consent
26
+ ? { "data-blume-consent": "analytics", type: "text/plain" }
27
+ : {};
19
28
  ---
20
29
 
21
30
  {
@@ -24,9 +33,9 @@ const nodes = analyticsHead(import.meta.env.PROD ? (analytics ?? []) : []);
24
33
  return <VercelAnalytics {...node.props} />;
25
34
  }
26
35
  return node.content === null ? (
27
- <script is:inline {...node.attributes} />
36
+ <script is:inline {...node.attributes} {...held} />
28
37
  ) : (
29
- <script is:inline set:html={node.content} {...node.attributes} />
38
+ <script is:inline set:html={node.content} {...node.attributes} {...held} />
30
39
  );
31
40
  })
32
41
  }
@@ -1,7 +1,9 @@
1
1
  ---
2
2
  import data from "blume:data";
3
+ import type { BlumeBanner } from "../../core/data.ts";
3
4
  import { EN_UI } from "../../core/i18n-ui.ts";
4
5
  import type { UIStrings } from "../../core/i18n-ui.ts";
6
+ import { resolveLocalizable } from "../../core/localizable.ts";
5
7
  import { withBase } from "../islands/base-path.ts";
6
8
  import Icon from "../Icon.astro";
7
9
 
@@ -9,14 +11,11 @@ import Icon from "../Icon.astro";
9
11
  // every shell shows the same bar. The pre-paint hide for a dismissed banner and
10
12
  // the dismiss click handler live in the layout/Header, not here.
11
13
  interface Props {
12
- banner:
13
- | {
14
- content: string;
15
- link?: { text: string; href: string };
16
- dismissible: boolean;
17
- key: string;
18
- }
19
- | null;
14
+ /**
15
+ * The banner. Its `content` and link `text` may be per-locale maps, shown
16
+ * in `locale` (else the default locale, else the map's first entry).
17
+ */
18
+ banner: BlumeBanner | null;
20
19
  /** Localized banner labels. */
21
20
  strings?: UIStrings["banner"];
22
21
  /**
@@ -40,6 +39,11 @@ const b = { ...EN_UI.banner, ...strings };
40
39
  const navigation =
41
40
  (locale === undefined ? undefined : data.navigationByLocale[locale]) ??
42
41
  data.navigation;
42
+ const defaultLocale = data.config.i18n?.defaultLocale;
43
+ const content =
44
+ banner && resolveLocalizable(banner.content, locale, defaultLocale);
45
+ const linkText =
46
+ banner?.link && resolveLocalizable(banner.link.text, locale, defaultLocale);
43
47
  const linkHref =
44
48
  banner?.link && banner.link.href === data.config.banner?.link?.href
45
49
  ? (navigation.bannerHref ?? banner.link.href)
@@ -55,13 +59,13 @@ const linkHref =
55
59
  >
56
60
  {/* The message is authored in one language, whatever the page's
57
61
  direction: `dir="auto"` keeps its punctuation where it was typed. */}
58
- <span dir="auto">{banner.content}</span>
62
+ <span dir="auto">{content}</span>
59
63
  {banner.link && linkHref && (
60
64
  <a
61
65
  class="inline-flex items-center gap-1 font-medium underline underline-offset-2"
62
66
  href={withBase(linkHref)}
63
67
  >
64
- {banner.link.text}
68
+ {linkText}
65
69
  <Icon class="rtl:-scale-x-100" name="arrow-right" size={14} />
66
70
  </a>
67
71
  )}
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import { EN_UI } from "../../core/i18n-ui.ts";
3
3
  import type { UIStrings } from "../../core/i18n-ui.ts";
4
- import { withBase } from "../islands/base-path.ts";
4
+ import { withMountedBase } from "../islands/base-path.ts";
5
5
  import type { Crumb } from "./nav-utils.ts";
6
6
 
7
7
  interface Props {
@@ -11,9 +11,11 @@ interface Props {
11
11
  strings?: UIStrings["nav"];
12
12
  /** Left-align full width (for the wide API layout) instead of the prose measure. */
13
13
  wide?: boolean;
14
+ /** Match a `center` page's wider centered column instead of the prose measure. */
15
+ centered?: boolean;
14
16
  }
15
17
 
16
- const { crumbs, strings, wide = false } = Astro.props;
18
+ const { crumbs, strings, wide = false, centered = false } = Astro.props;
17
19
 
18
20
  // Merge over the English defaults so a label missing from a translation (or
19
21
  // from a not-yet-regenerated snapshot) still renders instead of coming out
@@ -31,11 +33,16 @@ const eyebrowCrumb = crumbs.length > 1 ? crumbs[crumbs.length - 2] : null;
31
33
  aria-label={n.breadcrumb}
32
34
  class:list={[
33
35
  "mb-2 text-muted-foreground text-sm",
34
- wide ? "max-w-none" : "mx-auto max-w-content",
36
+ wide
37
+ ? "max-w-none"
38
+ : ["mx-auto", centered ? "max-w-[54rem]" : "max-w-content"],
35
39
  ]}
36
40
  >
37
41
  {eyebrowCrumb.route ? (
38
- <a class="hover:text-foreground" href={withBase(eyebrowCrumb.route)}>
42
+ <a
43
+ class="hover:text-foreground"
44
+ href={withMountedBase(eyebrowCrumb.route)}
45
+ >
39
46
  {eyebrowCrumb.label}
40
47
  </a>
41
48
  ) : (
@@ -0,0 +1,62 @@
1
+ ---
2
+ import { EN_UI } from "../../core/i18n-ui.ts";
3
+ import type { UIStrings } from "../../core/i18n-ui.ts";
4
+ import { newTabAttrs } from "../../core/new-tab.ts";
5
+ import { withBase } from "../islands/base-path.ts";
6
+
7
+ // The `native()` consent banner: a card at the foot of the viewport asking to
8
+ // use analytics cookies. It renders hidden, and the consent runtime
9
+ // (`consent/client.ts`) shows it until the reader answers, stores the answer,
10
+ // and shows it again from the footer's Cookie settings link. Accept and
11
+ // Decline carry the same weight, so declining is as easy as accepting.
12
+ interface Props {
13
+ /** The privacy policy link: a page route or a URL. */
14
+ policy?: string;
15
+ /** Localized banner labels. */
16
+ strings?: UIStrings["consent"];
17
+ }
18
+
19
+ const { policy, strings } = Astro.props;
20
+ const c = { ...EN_UI.consent, ...strings };
21
+ const buttonClass =
22
+ "flex-1 rounded-blume border border-border px-4 py-2 text-foreground text-sm transition-colors hover:border-foreground";
23
+ ---
24
+
25
+ <div
26
+ aria-label={c.label}
27
+ class="fixed inset-x-4 bottom-4 z-50 rounded-blume border border-border bg-background p-4 shadow-lg sm:start-auto sm:end-4 sm:w-96"
28
+ data-blume-consent-banner
29
+ hidden
30
+ role="region"
31
+ >
32
+ <p class="text-foreground text-sm">
33
+ {c.message}{" "}
34
+ {
35
+ policy && (
36
+ <a
37
+ class="text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
38
+ href={withBase(policy)}
39
+ {...newTabAttrs(policy)}
40
+ >
41
+ {c.policy}
42
+ </a>
43
+ )
44
+ }
45
+ </p>
46
+ <div class="mt-3 flex gap-2">
47
+ <button
48
+ class={buttonClass}
49
+ data-blume-consent-choice="decline"
50
+ type="button"
51
+ >
52
+ {c.decline}
53
+ </button>
54
+ <button
55
+ class={buttonClass}
56
+ data-blume-consent-choice="accept"
57
+ type="button"
58
+ >
59
+ {c.accept}
60
+ </button>
61
+ </div>
62
+ </div>
@@ -0,0 +1,38 @@
1
+ ---
2
+ import { consentHead } from "../../consent/head.ts";
3
+ import { CONSENT_INIT_SCRIPT } from "../../consent/init.ts";
4
+ import type { BlumeDataConfig } from "../../core/data.ts";
5
+
6
+ // The consent layer's <head> output, rendered ahead of `Analytics.astro` by
7
+ // every layout when `consent` is configured: the inline script that creates
8
+ // `window.blumeConsent` (`consent/init.ts`), the adapter's own tags — a
9
+ // hosted consent manager's loader and the bridge that reports its answers —
10
+ // and the runtime that runs the held analytics (`consent/client.ts`), which
11
+ // starts the adapter's browser module, if it has one (`blume:consent-client`,
12
+ // see `consent/clients.ts`). Like analytics, a hosted manager loads in
13
+ // production builds only; a browser module (`native()`'s banner) works in
14
+ // `blume dev` too.
15
+ interface Props {
16
+ consent: NonNullable<BlumeDataConfig["consent"]>;
17
+ }
18
+
19
+ const { consent } = Astro.props;
20
+ const tags = import.meta.env.PROD ? consentHead(consent) : [];
21
+ ---
22
+
23
+ <script data-kind={consent.kind} is:inline set:html={CONSENT_INIT_SCRIPT} />
24
+ {
25
+ tags.map((tag) =>
26
+ tag.content === null ? (
27
+ <script is:inline {...tag.attributes} />
28
+ ) : (
29
+ <script is:inline set:html={tag.content} {...tag.attributes} />
30
+ )
31
+ )
32
+ }
33
+ <script>
34
+ import { startConsentClient } from "blume:consent-client";
35
+ import { startConsent } from "../../consent/client.ts";
36
+
37
+ startConsent(startConsentClient);
38
+ </script>