blume 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/{chunk-5cg2rzgf.js → chunk-073rdvgb.js} +19 -48
  3. package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-073rdvgb.js.map} +4 -5
  4. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  5. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  6. package/dist/cli/chunk-150jk7km.js +52 -0
  7. package/dist/cli/chunk-150jk7km.js.map +10 -0
  8. package/dist/cli/{chunk-rekqz4qg.js → chunk-313vnn40.js} +16 -15
  9. package/dist/cli/chunk-313vnn40.js.map +10 -0
  10. package/dist/cli/{chunk-gp84za0h.js → chunk-3v3ffbrj.js} +2 -2
  11. package/dist/cli/{chunk-9xr4dgee.js → chunk-3x6x3pph.js} +29 -16
  12. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  13. package/dist/cli/{chunk-0gz36akr.js → chunk-42t4jxdx.js} +21 -5
  14. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  15. package/dist/cli/{chunk-ta27feev.js → chunk-54ztw1nj.js} +21 -2
  16. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  17. package/dist/cli/{chunk-jnwj5kjj.js → chunk-5vjtgs6e.js} +20 -7
  18. package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-5vjtgs6e.js.map} +3 -3
  19. package/dist/cli/chunk-66w9j7p6.js +322 -0
  20. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  21. package/dist/cli/{chunk-nd1821sk.js → chunk-7ppf1rva.js} +32 -18
  22. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  23. package/dist/cli/{chunk-p804f3pe.js → chunk-8y2kmjv8.js} +6 -6
  24. package/dist/cli/{chunk-2f7n4qzt.js → chunk-c1y538kd.js} +1 -1
  25. package/dist/cli/{chunk-1n228xqb.js → chunk-c887fj0s.js} +2 -2
  26. package/dist/cli/{chunk-y5h4r2y0.js → chunk-cfyg47zg.js} +15 -5
  27. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  28. package/dist/cli/{chunk-cmtcnh9f.js → chunk-dzwehpy7.js} +2 -2
  29. package/dist/cli/{chunk-h2a48yhd.js → chunk-e6atmn4h.js} +119 -15
  30. package/dist/cli/{chunk-h2a48yhd.js.map → chunk-e6atmn4h.js.map} +10 -10
  31. package/dist/cli/{chunk-5ffywsmb.js → chunk-e8xw9qkh.js} +8 -6
  32. package/dist/cli/{chunk-5ffywsmb.js.map → chunk-e8xw9qkh.js.map} +3 -3
  33. package/dist/cli/{chunk-ntv4qrt7.js → chunk-e9q7a2ak.js} +14 -14
  34. package/dist/cli/{chunk-xkz5xbtv.js → chunk-ehweg4fr.js} +2366 -1114
  35. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  36. package/dist/cli/{chunk-zf748avv.js → chunk-ep6nd0rn.js} +176 -281
  37. package/dist/cli/{chunk-zf748avv.js.map → chunk-ep6nd0rn.js.map} +11 -12
  38. package/dist/cli/{chunk-yxmk4pec.js → chunk-fdrkhpjy.js} +16 -15
  39. package/dist/cli/{chunk-yxmk4pec.js.map → chunk-fdrkhpjy.js.map} +3 -3
  40. package/dist/cli/{chunk-5wj2qwn2.js → chunk-ft9vfys6.js} +4 -4
  41. package/dist/cli/{chunk-4h8z9vv5.js → chunk-gb420vvz.js} +89 -46
  42. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  43. package/dist/cli/{chunk-713mngdq.js → chunk-hebn0tpx.js} +76 -10
  44. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  45. package/dist/cli/{chunk-2n4ps1b3.js → chunk-htrwgabr.js} +40 -22
  46. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  47. package/dist/cli/{chunk-zhvd6adm.js → chunk-hx3xqnk9.js} +13 -13
  48. package/dist/cli/chunk-j134vrp2.js +65 -0
  49. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  50. package/dist/cli/{chunk-y4ykbark.js → chunk-kma9f6sf.js} +2 -2
  51. package/dist/cli/{chunk-py62fewh.js → chunk-mrcccgnh.js} +3 -3
  52. package/dist/cli/{chunk-2f3ndkpw.js → chunk-n0v7m8ry.js} +3 -3
  53. package/dist/cli/{chunk-43zcf4gz.js → chunk-ncj7w596.js} +522 -353
  54. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  55. package/dist/cli/{chunk-9geymqns.js → chunk-nq1d20w2.js} +14 -14
  56. package/dist/cli/{chunk-g2z9x01v.js → chunk-nrns10mj.js} +3 -3
  57. package/dist/cli/{chunk-e7vvk5ya.js → chunk-sds923fa.js} +3 -3
  58. package/dist/cli/{chunk-cy5bm0j0.js → chunk-t2m5j4pr.js} +2 -2
  59. package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-t2m5j4pr.js.map} +1 -1
  60. package/dist/cli/{chunk-yj17vs39.js → chunk-tdqhyxfz.js} +5 -5
  61. package/dist/cli/{chunk-q3wxxq39.js → chunk-tphac74f.js} +5 -5
  62. package/dist/cli/{chunk-6bm0xpgd.js → chunk-w1r4vase.js} +2 -2
  63. package/dist/cli/index.js +24 -19
  64. package/dist/cli/index.js.map +3 -3
  65. package/dist/types/ai/ask-context.d.ts +6 -2
  66. package/dist/types/ai/component-markdown.d.ts +2 -0
  67. package/dist/types/ai/openapi-components.d.ts +4 -1
  68. package/dist/types/astro/integration.d.ts +5 -4
  69. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  70. package/dist/types/astro/pages.d.ts +10 -0
  71. package/dist/types/cli/init/scaffold.d.ts +1 -1
  72. package/dist/types/components/content/api-field.d.ts +4 -1
  73. package/dist/types/components/content/api-page.d.ts +6 -1
  74. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  75. package/dist/types/components/openapi/helpers.d.ts +22 -1
  76. package/dist/types/components/openapi/security.d.ts +10 -0
  77. package/dist/types/components/openapi/snippets.d.ts +60 -0
  78. package/dist/types/core/changelog-index.d.ts +8 -0
  79. package/dist/types/core/config-input.d.ts +30 -3
  80. package/dist/types/core/data.d.ts +4 -0
  81. package/dist/types/core/i18n-ui.d.ts +2 -0
  82. package/dist/types/core/links.d.ts +8 -0
  83. package/dist/types/core/probe.d.ts +2 -0
  84. package/dist/types/core/project-graph.d.ts +6 -0
  85. package/dist/types/core/redirect-patterns.d.ts +11 -3
  86. package/dist/types/core/schema.d.ts +119 -0
  87. package/dist/types/core/sources/cache.d.ts +3 -0
  88. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  89. package/dist/types/core/sources/contentful.d.ts +17 -1
  90. package/dist/types/core/sources/lexical.d.ts +5 -0
  91. package/dist/types/core/sources/normalize.d.ts +20 -1
  92. package/dist/types/core/sources/remote.d.ts +37 -10
  93. package/dist/types/core/sources/sanity.d.ts +1 -1
  94. package/dist/types/core/types.d.ts +17 -0
  95. package/dist/types/deploy/adapter-output.d.ts +2 -1
  96. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  97. package/dist/types/deploy/adapters/node.d.ts +58 -23
  98. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  99. package/dist/types/deploy/adapters/types.d.ts +3 -2
  100. package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
  101. package/dist/types/deploy/platforms/node.d.ts +3 -1
  102. package/dist/types/deploy/platforms/types.d.ts +18 -7
  103. package/dist/types/deploy/platforms/vercel.d.ts +20 -1
  104. package/dist/types/deploy/redirects.d.ts +17 -1
  105. package/dist/types/deploy/sitemap.d.ts +2 -1
  106. package/dist/types/deploy/vercel-negotiation.d.ts +42 -21
  107. package/dist/types/openapi/checks.d.ts +27 -0
  108. package/dist/types/openapi/graphql.d.ts +6 -0
  109. package/dist/types/openapi/model.d.ts +15 -4
  110. package/dist/types/openapi/parse.d.ts +3 -0
  111. package/dist/types/openapi/references.d.ts +16 -0
  112. package/dist/types/ratelimit/index.d.ts +1 -1
  113. package/dist/types/ratelimit/schema.d.ts +8 -0
  114. package/dist/types/ratelimit/upstash.d.ts +39 -14
  115. package/dist/types/reference/graphql.d.ts +76 -1
  116. package/dist/types/reference/index.d.ts +1 -1
  117. package/dist/types/reference/openapi.d.ts +2 -2
  118. package/dist/types/reference/options.d.ts +37 -1
  119. package/dist/types/reference/schema.d.ts +84 -0
  120. package/dist/types/search/adapters/registry.d.ts +3 -0
  121. package/dist/types/search/adapters/typesense.d.ts +14 -4
  122. package/dist/types/search/build.d.ts +3 -1
  123. package/dist/types/search/orama-index.d.ts +9 -9
  124. package/dist/types/search/sync/algolia.d.ts +5 -2
  125. package/dist/types/search/sync/index.d.ts +3 -1
  126. package/dist/types/search/sync/typesense.d.ts +11 -7
  127. package/dist/types/sources/contentful.d.ts +16 -0
  128. package/dist/types/sources/github-releases.d.ts +8 -1
  129. package/dist/types/sources/payload.d.ts +2 -0
  130. package/dist/types/sources/registry.d.ts +54 -0
  131. package/dist/types/sources/shared.d.ts +3 -0
  132. package/dist/types/sources/strapi.d.ts +2 -0
  133. package/docs/01-quickstart.mdx +4 -2
  134. package/docs/02-deployment.mdx +31 -13
  135. package/docs/advanced/changelog.mdx +11 -0
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +30 -7
  138. package/docs/cli/doctor.mdx +5 -1
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/translate.mdx +1 -0
  141. package/docs/cli/validate.mdx +4 -3
  142. package/docs/configuration/analytics.mdx +1 -1
  143. package/docs/configuration/assistant.mdx +6 -2
  144. package/docs/configuration/customization.mdx +4 -2
  145. package/docs/configuration/export.mdx +5 -3
  146. package/docs/configuration/index.mdx +3 -2
  147. package/docs/configuration/rate-limiting.mdx +23 -2
  148. package/docs/configuration/search.mdx +37 -14
  149. package/docs/content/components.mdx +2 -2
  150. package/docs/content/frontmatter.mdx +5 -3
  151. package/docs/content/i18n.mdx +2 -2
  152. package/docs/content/includes.mdx +1 -1
  153. package/docs/content/navigation.mdx +4 -4
  154. package/docs/content/sources/contentful.mdx +7 -4
  155. package/docs/content/sources/custom.mdx +7 -3
  156. package/docs/content/sources/github-releases.mdx +10 -2
  157. package/docs/content/sources/index.mdx +3 -3
  158. package/docs/content/sources/notion.mdx +1 -1
  159. package/docs/content/sources/obsidian.mdx +1 -1
  160. package/docs/content/sources/payload.mdx +3 -2
  161. package/docs/content/sources/remote-mdx.mdx +4 -0
  162. package/docs/content/sources/sanity.mdx +2 -2
  163. package/docs/content/sources/strapi.mdx +3 -2
  164. package/docs/content/syntax.mdx +2 -2
  165. package/docs/discoverability/agent-discovery.mdx +1 -1
  166. package/docs/discoverability/index.mdx +1 -1
  167. package/docs/discoverability/json-api.mdx +2 -0
  168. package/docs/discoverability/llms-txt.mdx +1 -1
  169. package/docs/discoverability/markdown.mdx +3 -1
  170. package/docs/discoverability/mcp.mdx +4 -0
  171. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  172. package/docs/references/api-pages.mdx +1 -1
  173. package/docs/references/asyncapi.mdx +3 -1
  174. package/docs/references/graphql.mdx +13 -1
  175. package/docs/references/openapi.mdx +50 -8
  176. package/package.json +9 -9
  177. package/skills/blume-migrate/SKILL.md +1 -1
  178. package/skills/blume-migrate/references/docusaurus.md +4 -4
  179. package/skills/blume-migrate/references/fumadocs.md +17 -13
  180. package/skills/blume-migrate/references/nextra.md +8 -5
  181. package/skills/blume-migrate/references/starlight.md +9 -7
  182. package/src/ai/ask-context.ts +33 -3
  183. package/src/ai/ask-limits.ts +13 -0
  184. package/src/ai/changelog-markdown.ts +11 -6
  185. package/src/ai/component-markdown.ts +1 -1
  186. package/src/ai/cors.ts +12 -3
  187. package/src/ai/mcp/query.ts +14 -6
  188. package/src/ai/openapi-components.ts +28 -4
  189. package/src/ai/relative-links.ts +4 -38
  190. package/src/astro/generate.ts +78 -48
  191. package/src/astro/integration.ts +16 -13
  192. package/src/astro/markdown-negotiation.ts +3 -0
  193. package/src/astro/module-types.ts +3 -1
  194. package/src/astro/pages.ts +23 -0
  195. package/src/astro/templates.ts +125 -80
  196. package/src/audit/catalog.ts +19 -3
  197. package/src/audit/checks/links.ts +14 -1
  198. package/src/audit/checks/network.ts +110 -6
  199. package/src/audit/checks/redirects.ts +6 -4
  200. package/src/audit/checks/robots.ts +58 -2
  201. package/src/audit/run.ts +3 -0
  202. package/src/audit/types.ts +2 -0
  203. package/src/audit/url.ts +14 -5
  204. package/src/blume-modules.d.ts +5 -1
  205. package/src/cli/args.ts +51 -0
  206. package/src/cli/build-failure.ts +23 -0
  207. package/src/cli/commands/audit.ts +10 -1
  208. package/src/cli/commands/dev.ts +18 -2
  209. package/src/cli/commands/doctor.ts +45 -12
  210. package/src/cli/commands/eval.ts +36 -4
  211. package/src/cli/commands/init.ts +6 -4
  212. package/src/cli/commands/validate.ts +10 -1
  213. package/src/cli/init/scaffold.ts +3 -3
  214. package/src/cli/prepare.ts +6 -3
  215. package/src/cli/required-secrets.ts +1 -1
  216. package/src/components/content/ApiPlayground.astro +6 -2
  217. package/src/components/content/api-field.ts +4 -1
  218. package/src/components/content/api-page.ts +31 -7
  219. package/src/components/content/mermaid-element.ts +15 -1
  220. package/src/components/islands/hooks.ts +27 -1
  221. package/src/components/layout/PageActions.astro +139 -24
  222. package/src/components/layout/RootLayout.astro +10 -2
  223. package/src/components/layout/Search.astro +17 -3
  224. package/src/components/layout/TableOfContents.astro +24 -17
  225. package/src/components/layout/head-scripts.ts +6 -1
  226. package/src/components/layout/search/algolia.ts +10 -1
  227. package/src/components/layout/search/analytics.ts +42 -14
  228. package/src/components/layout/search/endpoint.ts +48 -5
  229. package/src/components/layout/search/pagefind.ts +114 -5
  230. package/src/components/layout/search/types.ts +12 -0
  231. package/src/components/layout/search/typesense.ts +18 -6
  232. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  233. package/src/components/openapi/Authorization.astro +10 -0
  234. package/src/components/openapi/GraphqlOperation.astro +5 -0
  235. package/src/components/openapi/Operation.astro +24 -1
  236. package/src/components/openapi/ParametersTable.astro +8 -4
  237. package/src/components/openapi/Playground.astro +7 -1
  238. package/src/components/openapi/RequestBody.astro +19 -2
  239. package/src/components/openapi/Responses.astro +1 -0
  240. package/src/components/openapi/SchemaTable.astro +15 -3
  241. package/src/components/openapi/async-snippets.ts +61 -5
  242. package/src/components/openapi/async.ts +123 -29
  243. package/src/components/openapi/graphql-helpers.ts +35 -12
  244. package/src/components/openapi/helpers.ts +36 -2
  245. package/src/components/openapi/message-model.ts +54 -11
  246. package/src/components/openapi/message.ts +14 -4
  247. package/src/components/openapi/operation-model.ts +16 -7
  248. package/src/components/openapi/playground-client.ts +24 -5
  249. package/src/components/openapi/playground-schema.ts +1 -1
  250. package/src/components/openapi/schema-tree.ts +44 -14
  251. package/src/components/openapi/security.ts +10 -0
  252. package/src/components/openapi/snippets.ts +5 -3
  253. package/src/components/openapi/validate-json.ts +4 -3
  254. package/src/core/changelog-index.ts +31 -1
  255. package/src/core/component-diagnostics.ts +53 -0
  256. package/src/core/component-overrides.ts +32 -0
  257. package/src/core/config-input.ts +31 -3
  258. package/src/core/data.ts +2 -0
  259. package/src/core/diagnostics.ts +5 -0
  260. package/src/core/i18n-ui.ts +2 -0
  261. package/src/core/includes.ts +1 -1
  262. package/src/core/links.ts +47 -17
  263. package/src/core/probe.ts +26 -6
  264. package/src/core/project-graph.ts +59 -18
  265. package/src/core/redirect-patterns.ts +36 -7
  266. package/src/core/schema.ts +21 -2
  267. package/src/core/sources/cache.ts +6 -0
  268. package/src/core/sources/contentful-rich-text.ts +29 -6
  269. package/src/core/sources/contentful.ts +135 -25
  270. package/src/core/sources/filesystem.ts +26 -4
  271. package/src/core/sources/github-releases.ts +12 -1
  272. package/src/core/sources/lexical.ts +15 -10
  273. package/src/core/sources/mdx-remote.ts +62 -10
  274. package/src/core/sources/normalize.ts +227 -41
  275. package/src/core/sources/notion.ts +7 -1
  276. package/src/core/sources/obsidian.ts +16 -3
  277. package/src/core/sources/payload.ts +38 -15
  278. package/src/core/sources/remote.ts +72 -15
  279. package/src/core/sources/resolve.ts +11 -1
  280. package/src/core/sources/sanity.ts +43 -25
  281. package/src/core/sources/strapi.ts +2 -2
  282. package/src/core/types.ts +18 -0
  283. package/src/core/ui-packs/ar.ts +1 -0
  284. package/src/core/ui-packs/bg.ts +1 -0
  285. package/src/core/ui-packs/bn.ts +1 -0
  286. package/src/core/ui-packs/ca.ts +1 -0
  287. package/src/core/ui-packs/cs.ts +1 -0
  288. package/src/core/ui-packs/da.ts +1 -0
  289. package/src/core/ui-packs/de.ts +1 -0
  290. package/src/core/ui-packs/el.ts +1 -0
  291. package/src/core/ui-packs/es.ts +1 -0
  292. package/src/core/ui-packs/fa.ts +1 -0
  293. package/src/core/ui-packs/fi.ts +1 -0
  294. package/src/core/ui-packs/fr.ts +1 -0
  295. package/src/core/ui-packs/he.ts +1 -0
  296. package/src/core/ui-packs/hi.ts +1 -0
  297. package/src/core/ui-packs/hr.ts +1 -0
  298. package/src/core/ui-packs/hu.ts +1 -0
  299. package/src/core/ui-packs/id.ts +1 -0
  300. package/src/core/ui-packs/it.ts +1 -0
  301. package/src/core/ui-packs/ja.ts +1 -0
  302. package/src/core/ui-packs/ko.ts +1 -0
  303. package/src/core/ui-packs/nl.ts +1 -0
  304. package/src/core/ui-packs/no.ts +1 -0
  305. package/src/core/ui-packs/pl.ts +1 -0
  306. package/src/core/ui-packs/pt-br.ts +1 -0
  307. package/src/core/ui-packs/pt.ts +1 -0
  308. package/src/core/ui-packs/ro.ts +1 -0
  309. package/src/core/ui-packs/ru.ts +1 -0
  310. package/src/core/ui-packs/sk.ts +1 -0
  311. package/src/core/ui-packs/sr.ts +1 -0
  312. package/src/core/ui-packs/sv.ts +1 -0
  313. package/src/core/ui-packs/th.ts +1 -0
  314. package/src/core/ui-packs/tr.ts +1 -0
  315. package/src/core/ui-packs/uk.ts +1 -0
  316. package/src/core/ui-packs/vi.ts +1 -0
  317. package/src/core/ui-packs/zh-tw.ts +1 -0
  318. package/src/core/ui-packs/zh.ts +1 -0
  319. package/src/deploy/adapter-output.ts +7 -2
  320. package/src/deploy/adapters/cloudflare.ts +7 -5
  321. package/src/deploy/adapters/node.ts +47 -9
  322. package/src/deploy/adapters/types.ts +7 -3
  323. package/src/deploy/artifacts.ts +2 -1
  324. package/src/deploy/platforms/cloudflare.ts +13 -9
  325. package/src/deploy/platforms/netlify.ts +2 -1
  326. package/src/deploy/platforms/node.ts +12 -2
  327. package/src/deploy/platforms/types.ts +21 -7
  328. package/src/deploy/platforms/vercel.ts +113 -16
  329. package/src/deploy/redirects.ts +66 -6
  330. package/src/deploy/rss.ts +3 -1
  331. package/src/deploy/sitemap.ts +39 -5
  332. package/src/deploy/vercel-negotiation.ts +290 -102
  333. package/src/eval/agents.ts +18 -1
  334. package/src/eval/prompts.ts +6 -3
  335. package/src/eval/report.ts +34 -13
  336. package/src/eval/run.ts +5 -0
  337. package/src/markdown/relative-links.ts +3 -2
  338. package/src/openapi/checks.ts +273 -0
  339. package/src/openapi/graphql-build.ts +21 -0
  340. package/src/openapi/graphql.ts +11 -0
  341. package/src/openapi/model.ts +15 -3
  342. package/src/openapi/parse.ts +127 -2
  343. package/src/openapi/references.ts +43 -3
  344. package/src/openapi/source.ts +39 -14
  345. package/src/ratelimit/index.ts +1 -1
  346. package/src/ratelimit/runtime.ts +37 -19
  347. package/src/ratelimit/upstash.ts +40 -10
  348. package/src/reference/graphql.ts +15 -1
  349. package/src/reference/index.ts +1 -0
  350. package/src/reference/openapi.ts +2 -2
  351. package/src/reference/options.ts +29 -1
  352. package/src/registry/eject.ts +3 -1
  353. package/src/search/adapters/locale-scope.ts +30 -0
  354. package/src/search/adapters/typesense.ts +14 -4
  355. package/src/search/build.ts +21 -3
  356. package/src/search/orama-index.ts +163 -31
  357. package/src/search/source-pages.ts +29 -0
  358. package/src/search/sync/algolia.ts +99 -9
  359. package/src/search/sync/index.ts +52 -13
  360. package/src/search/sync/typesense.ts +72 -27
  361. package/src/sources/contentful.ts +12 -0
  362. package/src/sources/github-releases.ts +7 -1
  363. package/src/sources/shared.ts +3 -0
  364. package/src/theme/entry.ts +29 -3
  365. package/src/translate/validate.ts +23 -1
  366. package/dist/cli/chunk-0gz36akr.js.map +0 -12
  367. package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
  368. package/dist/cli/chunk-43zcf4gz.js.map +0 -36
  369. package/dist/cli/chunk-4ejxbzee.js +0 -39
  370. package/dist/cli/chunk-4ejxbzee.js.map +0 -10
  371. package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
  372. package/dist/cli/chunk-713mngdq.js.map +0 -14
  373. package/dist/cli/chunk-9xr4dgee.js.map +0 -10
  374. package/dist/cli/chunk-amrb6hgh.js +0 -145
  375. package/dist/cli/chunk-amrb6hgh.js.map +0 -11
  376. package/dist/cli/chunk-nd1821sk.js.map +0 -10
  377. package/dist/cli/chunk-rekqz4qg.js.map +0 -10
  378. package/dist/cli/chunk-ta27feev.js.map +0 -10
  379. package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
  380. package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
  381. /package/dist/cli/{chunk-gp84za0h.js.map → chunk-3v3ffbrj.js.map} +0 -0
  382. /package/dist/cli/{chunk-p804f3pe.js.map → chunk-8y2kmjv8.js.map} +0 -0
  383. /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-c1y538kd.js.map} +0 -0
  384. /package/dist/cli/{chunk-1n228xqb.js.map → chunk-c887fj0s.js.map} +0 -0
  385. /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-dzwehpy7.js.map} +0 -0
  386. /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-e9q7a2ak.js.map} +0 -0
  387. /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-ft9vfys6.js.map} +0 -0
  388. /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-hx3xqnk9.js.map} +0 -0
  389. /package/dist/cli/{chunk-y4ykbark.js.map → chunk-kma9f6sf.js.map} +0 -0
  390. /package/dist/cli/{chunk-py62fewh.js.map → chunk-mrcccgnh.js.map} +0 -0
  391. /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-n0v7m8ry.js.map} +0 -0
  392. /package/dist/cli/{chunk-9geymqns.js.map → chunk-nq1d20w2.js.map} +0 -0
  393. /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-nrns10mj.js.map} +0 -0
  394. /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-sds923fa.js.map} +0 -0
  395. /package/dist/cli/{chunk-yj17vs39.js.map → chunk-tdqhyxfz.js.map} +0 -0
  396. /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-tphac74f.js.map} +0 -0
  397. /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-w1r4vase.js.map} +0 -0
@@ -104,12 +104,37 @@ const parseFlags = (args: EvalFlags) => {
104
104
  return { agent: args.agent, threshold, timeoutS };
105
105
  };
106
106
 
107
+ /** A shell word for the rerun command, single-quoted only when it has to be. */
108
+ const shellWord = (value: string): string =>
109
+ /^[\w./:@%+=,-]+$/u.test(value)
110
+ ? value
111
+ : `'${value.replaceAll("'", String.raw`'\''`)}'`;
112
+
113
+ /**
114
+ * The command a `--fix` agent reruns to verify its edits. It carries the run's
115
+ * own flags: a bare `blume eval` would fall back to Codex and `evals.yaml`,
116
+ * grading a different file with a different agent than the run it's fixing.
117
+ */
118
+ const rerunCommand = (
119
+ args: EvalFlags,
120
+ { agent, threshold, timeoutS }: ReturnType<typeof parseFlags>
121
+ ): string =>
122
+ [
123
+ `blume eval --agent ${agent}`,
124
+ args.file === DEFAULT_FILE ? "" : `--file ${shellWord(args.file)}`,
125
+ args.threshold === undefined ? "" : `--threshold ${threshold}`,
126
+ args.timeout === undefined ? "" : `--timeout ${timeoutS}`,
127
+ ]
128
+ .filter((part) => part !== "")
129
+ .join(" ");
130
+
107
131
  /** `blume eval --fix`: hand the failing report to the interactive agent. */
108
132
  const runFixHandoff = async (
109
133
  agent: AgentKind,
110
134
  result: EvalResult,
111
135
  root: string,
112
- threshold: number
136
+ threshold: number,
137
+ command: string
113
138
  ): Promise<void> => {
114
139
  const count = result.counts.fail + result.counts.error;
115
140
  if (count === 0) {
@@ -120,7 +145,7 @@ const runFixHandoff = async (
120
145
  process.stderr.write(
121
146
  ` Handing ${count} failed question${count === 1 ? "" : "s"} to ${cli.name}…\n\n`
122
147
  );
123
- const code = await launchAgentCode(agent, evalFixPrompt(report));
148
+ const code = await launchAgentCode(agent, evalFixPrompt(report, command));
124
149
  if (code !== 0) {
125
150
  process.exit(code);
126
151
  }
@@ -184,7 +209,8 @@ export const evalCommand = defineCommand({
184
209
  meta: commandMeta.eval,
185
210
  async run({ args }) {
186
211
  const root = process.cwd();
187
- const { agent, threshold, timeoutS } = parseFlags(args);
212
+ const flags = parseFlags(args);
213
+ const { agent, threshold, timeoutS } = flags;
188
214
  if (args.action === "init") {
189
215
  await runInit(agent, args.file);
190
216
  return;
@@ -259,7 +285,13 @@ export const evalCommand = defineCommand({
259
285
  if (args.fix) {
260
286
  // The gate is a CI concern; a handoff run succeeds when the agent
261
287
  // session does, not when the docs already passed.
262
- await runFixHandoff(agent, result, root, threshold);
288
+ await runFixHandoff(
289
+ agent,
290
+ result,
291
+ root,
292
+ threshold,
293
+ rerunCommand(args, flags)
294
+ );
263
295
  return;
264
296
  }
265
297
 
@@ -272,14 +272,16 @@ export const initCommand = defineCommand({
272
272
  // which `init` leaves alone; say what it still needs before installing.
273
273
  const note = workspaceNote(root, answers, env);
274
274
 
275
- // Keep installed dependencies, Blume's generated runtime (`.blume/`), and
276
- // build output (`dist/`) out of version control. Idempotent: creates
277
- // `.gitignore` when absent and skips entries already present
278
- // (trailing-slash agnostic).
275
+ // Keep installed dependencies, Blume's generated runtime (`.blume/`),
276
+ // build output (`dist/`), and local secrets (`.env.local`, where every
277
+ // missing-secret warning says to put them) out of version control.
278
+ // Idempotent: creates `.gitignore` when absent and skips entries already
279
+ // present (trailing-slash agnostic).
279
280
  const ignored = await ensureGitignore(root, [
280
281
  "node_modules/",
281
282
  ".blume/",
282
283
  "dist/",
284
+ ".env.local",
283
285
  ]);
284
286
  if (ignored.length > 0) {
285
287
  sink.success(`Added ${ignored.join(", ")} to .gitignore`);
@@ -13,6 +13,7 @@ import { validateLinks } from "../../core/links.ts";
13
13
  import { buildManifest } from "../../core/manifest.ts";
14
14
  import { scanProject } from "../../core/project-graph.ts";
15
15
  import type { Diagnostic } from "../../core/types.ts";
16
+ import { parseIgnoreFlag } from "../args.ts";
16
17
  import { commandMeta } from "../command-meta.ts";
17
18
  import { reportInternalError } from "../internal-error.ts";
18
19
  import {
@@ -28,6 +29,12 @@ export const validateCommand = defineCommand({
28
29
  description: "Check external (HTTP) links over the network.",
29
30
  type: "boolean",
30
31
  },
32
+ ignore: {
33
+ description:
34
+ 'With --external, skip external links whose URL matches this glob (e.g. "https://example.com/**"). Repeat it for more; internal links are always checked.',
35
+ type: "string",
36
+ valueHint: "glob",
37
+ },
31
38
  json: {
32
39
  description: "Emit diagnostics as JSON on stdout (for CI/editors).",
33
40
  type: "boolean",
@@ -38,8 +45,9 @@ export const validateCommand = defineCommand({
38
45
  },
39
46
  },
40
47
  meta: commandMeta.validate,
41
- async run({ args }) {
48
+ async run({ args, rawArgs }) {
42
49
  const root = process.cwd();
50
+ const ignore = parseIgnoreFlag(rawArgs);
43
51
  const diagnostics: Diagnostic[] = [];
44
52
 
45
53
  try {
@@ -83,6 +91,7 @@ export const validateCommand = defineCommand({
83
91
  checkExternal: Boolean(args.external),
84
92
  extraRoutes,
85
93
  i18n: project.config.i18n,
94
+ ignore,
86
95
  publicDir: existsSync(publicDir) ? publicDir : null,
87
96
  redirects: project.config.redirects,
88
97
  }))
@@ -679,7 +679,7 @@ const ESBUILD_APPROVAL = " allowBuilds:\n esbuild: true";
679
679
  * What the project still needs from a workspace it sits in, when `init` left
680
680
  * that workspace's config alone: a place in a pnpm workspace's `packages`
681
681
  * (pnpm skips a folder it doesn't list), pnpm's approval for esbuild's build
682
- * script (pnpm 10 and later fail the install without it), or Yarn Berry's
682
+ * script (pnpm 11 and later fail the install without it), or Yarn Berry's
683
683
  * `node_modules` linker. Undefined when the workspace already has it, or the
684
684
  * project doesn't sit in one.
685
685
  */
@@ -699,12 +699,12 @@ export const workspaceNote = (
699
699
  const entry = ` packages:\n - ${relative(workspace, root)}`;
700
700
  return approved
701
701
  ? `This folder sits inside the pnpm workspace at ${file}, which doesn't list it under packages, so pnpm won't install it. Add it there before installing:\n\n${entry}`
702
- : `This folder sits inside the pnpm workspace at ${file}, which doesn't list it under packages, so pnpm won't install it. Add it there before installing, and approve esbuild's build script, or pnpm 10 and later stop the install:\n\n${entry}\n${ESBUILD_APPROVAL}`;
702
+ : `This folder sits inside the pnpm workspace at ${file}, which doesn't list it under packages, so pnpm won't install it. Add it there before installing, and approve esbuild's build script, or pnpm 11 and later stop the install:\n\n${entry}\n${ESBUILD_APPROVAL}`;
703
703
  }
704
704
  if (approved) {
705
705
  return;
706
706
  }
707
- return `This project joins the pnpm workspace at ${file}. Approve esbuild's build script there before installing, or pnpm 10 and later stop the install:\n\n${ESBUILD_APPROVAL}`;
707
+ return `This project joins the pnpm workspace at ${file}. Approve esbuild's build script there before installing, or pnpm 11 and later stop the install:\n\n${ESBUILD_APPROVAL}`;
708
708
  }
709
709
  if (answers.packageManager !== "yarn" || yarnMajor(env.userAgent) === 1) {
710
710
  return;
@@ -59,6 +59,12 @@ export const prepareProject = async (
59
59
  let project: BlumeProject;
60
60
  try {
61
61
  project = await scanProject(options.root, {
62
+ // Fail-fast on missing secrets: warn now, not at the first request, and
63
+ // before the sources fetch, so a source that can't authenticate on a
64
+ // fresh build still gets its variable named.
65
+ beforeSources: (config) => {
66
+ reportDiagnostics(checkRequiredSecrets(config), options.root);
67
+ },
62
68
  devServerUrl: options.devServerUrl,
63
69
  mode: options.mode,
64
70
  overrides: options.overrides,
@@ -138,8 +144,5 @@ export const prepareProject = async (
138
144
  logger.warn(warning);
139
145
  }
140
146
 
141
- // Fail-fast on missing runtime secrets: warn now, not at the first request.
142
- reportDiagnostics(checkRequiredSecrets(project.config), options.root);
143
-
144
147
  return project;
145
148
  };
@@ -61,7 +61,7 @@ const adapterSecretOwners = (config: ResolvedConfig): SecretOwner[] => {
61
61
  * the failure surfaces at `blume dev`/`build` instead of at the first request in
62
62
  * production. These are runtime secrets (the endpoint reads them on the server),
63
63
  * so this warns rather than hard-fails — the value may live only in the deploy
64
- * environment. Build-time secrets (search-index sync) already warn during sync.
64
+ * environment. Build-time secrets (search-index sync) are checked by the sync.
65
65
  */
66
66
  export const checkRequiredSecrets = (config: ResolvedConfig): Diagnostic[] => {
67
67
  const diagnostics: Diagnostic[] = [];
@@ -11,7 +11,10 @@ import { operationModel } from "../openapi/operation-model.ts";
11
11
  import Playground from "../openapi/Playground.astro";
12
12
  import { buildRequest, defaultValues } from "../openapi/request.ts";
13
13
  import RequestPanel from "../openapi/RequestPanel.astro";
14
- import { sampleLanguages } from "../openapi/snippets.ts";
14
+ import {
15
+ DEFAULT_SAMPLE_LANGUAGES,
16
+ sampleLanguages,
17
+ } from "../openapi/snippets.ts";
15
18
  import {
16
19
  endpointBody,
17
20
  endpointParameters,
@@ -46,6 +49,7 @@ const tryIt = spec.playground !== "simple" && api.playground.enabled;
46
49
  model={model}
47
50
  operation={`${spec.method} ${spec.target}`}
48
51
  proxy={api.playground.proxy}
52
+ proxyConfig="api"
49
53
  slug="api-pages"
50
54
  />
51
55
  )
@@ -53,7 +57,7 @@ const tryIt = spec.playground !== "simple" && api.playground.enabled;
53
57
  {
54
58
  !spec.requestExample && (
55
59
  <RequestPanel
56
- languages={sampleLanguages([])}
60
+ languages={sampleLanguages(DEFAULT_SAMPLE_LANGUAGES)}
57
61
  responses={{}}
58
62
  sample={buildRequest(model, defaultValues(model))}
59
63
  schemas={{}}
@@ -43,7 +43,10 @@ export interface ParamFieldProps extends FieldProps {
43
43
  /** The name, for a field with no location attribute. */
44
44
  name?: string;
45
45
  path?: string;
46
- /** Placeholder text for the parameter's playground input. */
46
+ /**
47
+ * An example value: the one the playground and request samples start
48
+ * with, not an input's placeholder hint (see `fieldSchema`).
49
+ */
47
50
  placeholder?: string;
48
51
  query?: string;
49
52
  }
@@ -1,3 +1,4 @@
1
+ import { exampleValue } from "../openapi/helpers.ts";
1
2
  import type { ParameterLike, SchemaLike } from "../openapi/helpers.ts";
2
3
  import { resolveSecurity } from "../openapi/security.ts";
3
4
  import type { OperationSecurity } from "../openapi/security.ts";
@@ -168,19 +169,42 @@ export const endpointParameters = (fields: EndpointField[]): ParameterLike[] =>
168
169
  ]
169
170
  );
170
171
 
171
- /** The body fields as a JSON request body, or `undefined` when there are none. */
172
+ /** Whether a field, or one nested in it, has a `default` or `placeholder`. */
173
+ const hasValue = (field: EndpointField): boolean =>
174
+ field.default !== undefined ||
175
+ Boolean(field.placeholder) ||
176
+ (field.fields ?? []).some(hasValue);
177
+
178
+ /**
179
+ * The fields a request starts with: required ones, and ones with a value to
180
+ * send. An optional field with neither is left for the reader to fill,
181
+ * rather than sent as the sampler's `"string"`, `0`, or `true`.
182
+ */
183
+ const sentFields = (fields: EndpointField[]): EndpointField[] =>
184
+ fields
185
+ .filter((field) => field.required === true || hasValue(field))
186
+ .map((field) => ({ ...field, fields: sentFields(field.fields ?? []) }));
187
+
188
+ /**
189
+ * The body fields as a JSON request body, or `undefined` when there are none.
190
+ * Its example, which the playground and samples start from, holds only the
191
+ * {@link sentFields}.
192
+ */
172
193
  export const endpointBody = (fields: EndpointField[]) => {
173
194
  const body = fields.filter((field) => field.location === "body");
195
+ const object = (members: EndpointField[]): SchemaLike =>
196
+ fieldSchema({
197
+ fields: members,
198
+ location: "body",
199
+ name: "",
200
+ type: "object",
201
+ });
174
202
  return body.length > 0
175
203
  ? {
176
204
  content: {
177
205
  "application/json": {
178
- schema: fieldSchema({
179
- fields: body,
180
- location: "body",
181
- name: "",
182
- type: "object",
183
- }),
206
+ example: exampleValue(object(sentFields(body)), {}),
207
+ schema: object(body),
184
208
  },
185
209
  },
186
210
  }
@@ -55,6 +55,9 @@ export class BlumeMermaid extends HTMLElement {
55
55
  look: "classic",
56
56
  securityLevel: "strict",
57
57
  startOnLoad: false,
58
+ // Without this, a diagram that fails to parse leaves Mermaid's own
59
+ // "Syntax error in text" graphic appended to <body>, below the page.
60
+ suppressErrorRendering: true,
58
61
  theme: prefersDark() ? "dark" : "default",
59
62
  });
60
63
  try {
@@ -71,13 +74,24 @@ export class BlumeMermaid extends HTMLElement {
71
74
  // oxlint-disable-next-line github/no-inner-html -- Mermaid-generated SVG must be injected as HTML
72
75
  output.innerHTML = svg;
73
76
  }
74
- } catch {
77
+ } catch (error) {
75
78
  // Localized message stamped on <body> by RootLayout's markup (the
76
79
  // data-attribute channel); English fallback when the attribute is
77
80
  // missing (a stale snapshot or a custom layout).
78
81
  output.textContent =
79
82
  document.body.dataset.i18nDiagramError ||
80
83
  "Could not render this diagram.";
84
+ // Readers get only the message above; the writer needs Mermaid's
85
+ // parser error (line and expected token) to fix the source. It goes
86
+ // to the console, and `blume dev` also shows it under the message.
87
+ console.error("[blume] Mermaid could not render a diagram:", error);
88
+ if (import.meta.env.DEV) {
89
+ const detail = document.createElement("pre");
90
+ detail.className =
91
+ "mt-2 overflow-x-auto text-muted-foreground text-xs";
92
+ detail.textContent = String(error);
93
+ output.append(detail);
94
+ }
81
95
  }
82
96
  output.removeAttribute("aria-busy");
83
97
  };
@@ -1,5 +1,9 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
+ import {
4
+ ASK_MAX_MESSAGES,
5
+ ASK_MAX_MESSAGES_CHARS,
6
+ } from "../../ai/ask-limits.ts";
3
7
  import { captchaToken } from "../../captcha/client.ts";
4
8
  import type { CaptchaSettings } from "../../captcha/schema.ts";
5
9
  import type { BlumeClientData } from "../../core/data.ts";
@@ -264,6 +268,28 @@ const refusalNotice = (
264
268
  const currentPath = (): string =>
265
269
  stripBase(import.meta.env.BASE_URL, window.location.pathname);
266
270
 
271
+ /**
272
+ * The conversation as the route is sent it: all of it while it fits the
273
+ * route's limits (`ai/ask-limits.ts`), else the latest turns that do, the
274
+ * oldest dropped first, so a long chat keeps working instead of answering
275
+ * `400`. The kept turns open with a question, not an answer cut from its
276
+ * own, and the question being asked always goes, even alone over the limit.
277
+ */
278
+ const withinAskLimits = (history: AskMessage[]): AskMessage[] => {
279
+ const last = history.length - 1;
280
+ let start = Math.max(0, history.length - ASK_MAX_MESSAGES);
281
+ while (
282
+ start < last &&
283
+ JSON.stringify(history.slice(start)).length > ASK_MAX_MESSAGES_CHARS
284
+ ) {
285
+ start += 1;
286
+ }
287
+ while (start < last && history[start]?.role !== "user") {
288
+ start += 1;
289
+ }
290
+ return history.slice(start);
291
+ };
292
+
267
293
  /**
268
294
  * Stream answers from the assistant endpoint. Mirrors the built-in assistant island so
269
295
  * a custom chat UI shares the same grounded, page-aware backend.
@@ -366,7 +392,7 @@ export const useAssistant = (
366
392
  }
367
393
  const payload: AskPayload = {
368
394
  captcha: token,
369
- messages: history,
395
+ messages: withinAskLimits(history),
370
396
  page: { path },
371
397
  };
372
398
  const response = await fetch(endpoint, {
@@ -89,6 +89,10 @@ const providers = (openInChat ?? openInChatProviders).map((key) => ({
89
89
  // keep that separator only while a configured list preserves v0 first.
90
90
  const chatSeparator = providers.length > 1 && providers[0]?.key === "v0";
91
91
 
92
+ // The block renders once, for the right rail. Below `xl` the script moves it
93
+ // into the mobile "On this page" dropdown, and the global styles below the
94
+ // markup restyle it there from that container.
95
+
92
96
  // Top-level actions match the ToC links: flat, no background/padding, color
93
97
  // shift on hover only.
94
98
  const rowClass =
@@ -98,6 +102,8 @@ const rowClass =
98
102
  // background).
99
103
  const menuRowClass =
100
104
  "flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-start text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground";
105
+ const panelClass =
106
+ "absolute top-full right-2 z-50 mt-1 w-max min-w-[14rem] max-w-[22rem] rounded-blume border border-border bg-background p-1 shadow-xl";
101
107
  ---
102
108
 
103
109
  <div
@@ -130,24 +136,24 @@ const menuRowClass =
130
136
  <span data-blume-copy-label>{a.copyMarkdown}</span>
131
137
  </button>
132
138
 
139
+ {/* Each chevron turns with its own <details> only: in the mobile dropdown
140
+ these nest inside the outline's <details>, and a `group-open` variant
141
+ would match that open ancestor too. */}
133
142
  {
134
143
  (exportPdf || exportEpub) && (
135
- <details class="group relative" data-blume-dropdown>
144
+ <details class="relative" data-blume-dropdown>
136
145
  <summary
137
146
  class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
138
147
  >
139
148
  <Icon name="download" size={16} />
140
149
  {a.export}
141
150
  <Icon
142
- class="ms-auto transition-transform group-open:rotate-180"
151
+ class="ms-auto transition-transform [details[open]>summary_&]:rotate-180"
143
152
  name="chevron-down"
144
153
  size={14}
145
154
  />
146
155
  </summary>
147
- <div
148
- class="absolute top-full right-2 z-50 mt-1 w-max min-w-[14rem] max-w-[22rem] rounded-blume border border-border bg-background p-1 shadow-xl"
149
- data-blume-menu
150
- >
156
+ <div class={panelClass} data-blume-menu>
151
157
  {exportPdf && (
152
158
  <button class={menuRowClass} data-blume-export-pdf type="button">
153
159
  <Icon name="file" size={16} />
@@ -169,22 +175,19 @@ const menuRowClass =
169
175
 
170
176
  {
171
177
  providers.length > 0 && (
172
- <details class="group relative" data-blume-dropdown>
178
+ <details class="relative" data-blume-dropdown>
173
179
  <summary
174
180
  class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
175
181
  >
176
182
  <Icon name="external-link" size={16} />
177
183
  {a.openInChat}
178
184
  <Icon
179
- class="ms-auto transition-transform group-open:rotate-180"
185
+ class="ms-auto transition-transform [details[open]>summary_&]:rotate-180"
180
186
  name="chevron-down"
181
187
  size={14}
182
188
  />
183
189
  </summary>
184
- <div
185
- class="absolute top-full right-2 z-50 mt-1 w-max min-w-[14rem] max-w-[22rem] rounded-blume border border-border bg-background p-1 shadow-xl"
186
- data-blume-menu
187
- >
190
+ <div class={panelClass} data-blume-menu>
188
191
  {providers.map((provider, index) => (
189
192
  <>
190
193
  {index === 1 && chatSeparator && (
@@ -208,22 +211,19 @@ const menuRowClass =
208
211
 
209
212
  {
210
213
  mcpUrl && (
211
- <details class="group relative" data-blume-dropdown>
214
+ <details class="relative" data-blume-dropdown>
212
215
  <summary
213
216
  class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
214
217
  >
215
218
  <Icon name="plug" size={16} />
216
219
  {a.connectMcp}
217
220
  <Icon
218
- class="ms-auto transition-transform group-open:rotate-180"
221
+ class="ms-auto transition-transform [details[open]>summary_&]:rotate-180"
219
222
  name="chevron-down"
220
223
  size={14}
221
224
  />
222
225
  </summary>
223
- <div
224
- class="absolute top-full right-2 z-50 mt-1 w-max min-w-[14rem] max-w-[22rem] rounded-blume border border-border bg-background p-1 shadow-xl"
225
- data-blume-menu
226
- >
226
+ <div class={panelClass} data-blume-menu>
227
227
  <button class={menuRowClass} data-mcp-copy-url type="button">
228
228
  <Icon name="link" size={16} />
229
229
  <span class="flex-1" data-mcp-url-label>
@@ -259,6 +259,45 @@ const menuRowClass =
259
259
  }
260
260
  </div>
261
261
 
262
+ <style is:global>
263
+ /* Below `xl` the script moves the block into the mobile "On this page"
264
+ dropdown (see `placeActions`), where it reads like the outline above it:
265
+ under a rule, its rows padded like the outline's links, its groups
266
+ expanding in place under their row instead of floating, and without
267
+ Scroll to top, since that dropdown already sits at the top of the page. */
268
+ [data-blume-page-actions-mount] > [data-blume-page-actions] {
269
+ margin-top: 0;
270
+ border-top: 1px solid var(--blume-border);
271
+ padding: 0.5rem;
272
+ }
273
+ [data-blume-page-actions-mount] [data-blume-scroll-top] {
274
+ display: none;
275
+ }
276
+ [data-blume-page-actions-mount]
277
+ :is([data-blume-page-actions] > :is(a, button), summary) {
278
+ border-radius: 0.375rem;
279
+ padding-inline: 0.5rem;
280
+ }
281
+ [data-blume-page-actions-mount] [data-blume-menu] {
282
+ position: static;
283
+ width: auto;
284
+ min-width: 0;
285
+ max-width: none;
286
+ margin: 0;
287
+ margin-inline-start: 0.5rem;
288
+ border-width: 0;
289
+ border-inline-start-width: 1px;
290
+ border-radius: 0;
291
+ background-color: transparent;
292
+ padding: 0;
293
+ padding-inline-start: 0.5rem;
294
+ box-shadow: none;
295
+ }
296
+ [data-blume-page-actions-mount] [data-blume-menu] > :hover {
297
+ background-color: transparent;
298
+ }
299
+ </style>
300
+
262
301
  <script>
263
302
  import { copyDeferredText, copyText, flashLabel } from "../copy-feedback.ts";
264
303
  import { installDropdownDismiss } from "../dropdown-dismiss.ts";
@@ -301,13 +340,42 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
301
340
  // for the EPUB: drop decorative SVGs and interactive controls (which render as
302
341
  // broken boxes in a reader), flatten Shiki's variable-colored code to plain
303
342
  // monospace text, and reveal panels the tab scripts would otherwise hide.
304
- const buildEpubChapterHtml = (article: Element): string => {
343
+ // Links point at the site: a site-relative `/docs/page` means nothing inside
344
+ // the book, so every link but an in-page `#fragment` is made absolute.
345
+ const buildEpubChapterHtml = async (article: Element): Promise<string> => {
305
346
  const clone = article.cloneNode(true) as Element;
306
347
  for (const el of clone.querySelectorAll(
307
348
  "svg, button, select, script, style, link, noscript, template"
308
349
  )) {
309
350
  el.remove();
310
351
  }
352
+ for (const link of clone.querySelectorAll<HTMLAnchorElement>("a[href]")) {
353
+ if (!link.getAttribute("href")?.startsWith("#")) {
354
+ link.setAttribute("href", link.href);
355
+ }
356
+ }
357
+ // The generator fetches every image into the book, and one it can't fetch
358
+ // (a cross-origin image served without CORS headers, a 404) fails the
359
+ // whole export. Try each first, and leave out the ones that fail.
360
+ await Promise.all(
361
+ [...clone.querySelectorAll("img")].map(async (img) => {
362
+ if (!img.getAttribute("src")) {
363
+ return;
364
+ }
365
+ try {
366
+ const response = await fetch(img.src);
367
+ if (!response.ok) {
368
+ throw new Error(`HTTP ${response.status}`);
369
+ }
370
+ } catch (error) {
371
+ console.warn(
372
+ `[blume] EPUB export left out an image it couldn't fetch: ${img.src}`,
373
+ error
374
+ );
375
+ img.remove();
376
+ }
377
+ })
378
+ );
311
379
  for (const pre of clone.querySelectorAll("pre")) {
312
380
  const code = document.createElement("code");
313
381
  code.textContent = pre.textContent ?? "";
@@ -339,9 +407,11 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
339
407
  menu.classList.toggle("mb-1", flipUp);
340
408
  };
341
409
 
410
+ // Only the rail's floating dropdowns need placing; a group open in place in
411
+ // the mobile dropdown is not one (see `placeActions`).
342
412
  const openDropdown = () =>
343
413
  document.querySelector<HTMLDetailsElement>(
344
- "[data-blume-page-actions] details[open]"
414
+ "[data-blume-page-actions] details[data-blume-dropdown][open]"
345
415
  );
346
416
 
347
417
  // rAF-coalesced so a live resize drag re-reads layout once per frame, not
@@ -362,6 +432,32 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
362
432
  // dropdown behavior (also the header language switcher and nav selectors).
363
433
  installDropdownDismiss();
364
434
 
435
+ // The page renders its one actions block in the right rail, which only
436
+ // shows at `xl`. Below that the block moves into the mobile "On this page"
437
+ // dropdown, into the empty mount the layout passes it as children, and
438
+ // back at `xl`; a moved node keeps its listeners, so nothing rebinds. In
439
+ // the dropdown its groups expand in place, so they drop out of the shared
440
+ // light-dismiss and the placement here (both keyed on
441
+ // `data-blume-dropdown`) until the block is back in the rail, and the
442
+ // styles above restyle it from the mount.
443
+ const railQuery = window.matchMedia("(min-width: 80rem)");
444
+ const placeActions = () => {
445
+ const root = document.querySelector("[data-blume-page-actions]");
446
+ const rail = railQuery.matches;
447
+ const home = document.querySelector(
448
+ rail ? "[data-blume-toc]" : "[data-blume-page-actions-mount]"
449
+ );
450
+ if (!(root && home) || root.parentElement === home) {
451
+ return;
452
+ }
453
+ // A group left open would reopen as the other kind of menu.
454
+ for (const details of root.querySelectorAll("details")) {
455
+ details.open = false;
456
+ details.toggleAttribute("data-blume-dropdown", rail);
457
+ }
458
+ home.append(root);
459
+ };
460
+
365
461
  // Per-page setup, run on the initial load and again after every
366
462
  // client-router swap: the swap rebuilds the actions menu from
367
463
  // server-rendered markup, so every handler below binds to fresh elements.
@@ -384,7 +480,9 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
384
480
  other.open = false;
385
481
  }
386
482
  }
387
- placeMenu(details);
483
+ if (details.hasAttribute("data-blume-dropdown")) {
484
+ placeMenu(details);
485
+ }
388
486
  });
389
487
  }
390
488
 
@@ -537,10 +635,24 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
537
635
  const epubModule = await loadEpub();
538
636
  const epub = epubModule.default?.default ?? epubModule.default;
539
637
  // The article HTML already opens with the page's <h1>, so don't let the
540
- // generator prepend a second chapter-title heading.
638
+ // generator prepend a second chapter-title heading. The book's
639
+ // language is the article's (a fallback page's content locale, else
640
+ // the page's), and the site is its author and publisher; left out,
641
+ // the generator says English and "anonymous". Its defaults are spread
642
+ // under these options, so an unknown value is omitted, not undefined.
643
+ const site = document
644
+ .querySelector('meta[property="og:site_name"]')
645
+ ?.getAttribute("content");
646
+ const lang = article.closest("[lang]")?.getAttribute("lang");
541
647
  const result = await epub(
542
- { css: EPUB_STYLES, prependChapterTitles: false, title },
543
- [{ content: buildEpubChapterHtml(article), title }]
648
+ {
649
+ css: EPUB_STYLES,
650
+ prependChapterTitles: false,
651
+ title,
652
+ ...(site && { author: site, publisher: site }),
653
+ ...(lang && { lang }),
654
+ },
655
+ [{ content: await buildEpubChapterHtml(article), title }]
544
656
  );
545
657
  const blob =
546
658
  result instanceof Blob
@@ -572,5 +684,8 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
572
684
  };
573
685
 
574
686
  initPageActions();
687
+ placeActions();
575
688
  document.addEventListener("astro:after-swap", initPageActions);
689
+ document.addEventListener("astro:after-swap", placeActions);
690
+ railQuery.addEventListener("change", placeActions);
576
691
  </script>