blume 2.1.0 → 2.1.2

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 (450) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  3. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  4. package/dist/cli/{chunk-43zcf4gz.js → chunk-0pmb4tr4.js} +541 -368
  5. package/dist/cli/chunk-0pmb4tr4.js.map +35 -0
  6. package/dist/cli/{chunk-9geymqns.js → chunk-0w6hs479.js} +15 -15
  7. package/dist/cli/{chunk-xkz5xbtv.js → chunk-124fvmzq.js} +2802 -1422
  8. package/dist/cli/chunk-124fvmzq.js.map +220 -0
  9. package/dist/cli/chunk-150jk7km.js +52 -0
  10. package/dist/cli/chunk-150jk7km.js.map +10 -0
  11. package/dist/cli/{chunk-y4ykbark.js → chunk-1f882900.js} +2 -2
  12. package/dist/cli/{chunk-5ffywsmb.js → chunk-297ajdcv.js} +8 -6
  13. package/dist/cli/{chunk-5ffywsmb.js.map → chunk-297ajdcv.js.map} +3 -3
  14. package/dist/cli/{chunk-h2a48yhd.js → chunk-32khwcb6.js} +120 -16
  15. package/dist/cli/{chunk-h2a48yhd.js.map → chunk-32khwcb6.js.map} +10 -10
  16. package/dist/cli/{chunk-5cg2rzgf.js → chunk-36bxs9c7.js} +20 -49
  17. package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-36bxs9c7.js.map} +4 -5
  18. package/dist/cli/{chunk-py62fewh.js → chunk-53jqw8z0.js} +3 -3
  19. package/dist/cli/{chunk-5wj2qwn2.js → chunk-58bt3hag.js} +4 -4
  20. package/dist/cli/{chunk-e7vvk5ya.js → chunk-5cmts6dv.js} +3 -3
  21. package/dist/cli/{chunk-y5h4r2y0.js → chunk-60hz265r.js} +15 -5
  22. package/dist/cli/chunk-60hz265r.js.map +10 -0
  23. package/dist/cli/chunk-74wfhv79.js +65 -0
  24. package/dist/cli/chunk-74wfhv79.js.map +10 -0
  25. package/dist/cli/{chunk-g2z9x01v.js → chunk-9qzgg6mj.js} +3 -3
  26. package/dist/cli/{chunk-q3wxxq39.js → chunk-a57yh0h9.js} +5 -5
  27. package/dist/cli/{chunk-713mngdq.js → chunk-ave1wcbr.js} +76 -10
  28. package/dist/cli/chunk-ave1wcbr.js.map +14 -0
  29. package/dist/cli/{chunk-2n4ps1b3.js → chunk-bqxaqn5p.js} +40 -22
  30. package/dist/cli/chunk-bqxaqn5p.js.map +15 -0
  31. package/dist/cli/{chunk-jnwj5kjj.js → chunk-bxwhtvw8.js} +20 -7
  32. package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-bxwhtvw8.js.map} +3 -3
  33. package/dist/cli/chunk-ckrcm08h.js +324 -0
  34. package/dist/cli/chunk-ckrcm08h.js.map +14 -0
  35. package/dist/cli/{chunk-2f3ndkpw.js → chunk-desvm72z.js} +3 -3
  36. package/dist/cli/{chunk-gp84za0h.js → chunk-dhwtqnnd.js} +2 -2
  37. package/dist/cli/{chunk-4h8z9vv5.js → chunk-dpy8qdjf.js} +93 -47
  38. package/dist/cli/chunk-dpy8qdjf.js.map +17 -0
  39. package/dist/cli/{chunk-2f7n4qzt.js → chunk-dqhg54mr.js} +1 -1
  40. package/dist/cli/{chunk-0gz36akr.js → chunk-dv2z0rzf.js} +21 -5
  41. package/dist/cli/chunk-dv2z0rzf.js.map +12 -0
  42. package/dist/cli/{chunk-cm89scdv.js → chunk-es8h0v3j.js} +9 -11
  43. package/dist/cli/{chunk-cm89scdv.js.map → chunk-es8h0v3j.js.map} +3 -3
  44. package/dist/cli/{chunk-zhvd6adm.js → chunk-gfc5fqvf.js} +14 -14
  45. package/dist/cli/{chunk-nd1821sk.js → chunk-gxtg6ed2.js} +33 -19
  46. package/dist/cli/chunk-gxtg6ed2.js.map +10 -0
  47. package/dist/cli/{chunk-p804f3pe.js → chunk-hvcxng5f.js} +6 -6
  48. package/dist/cli/{chunk-ta27feev.js → chunk-j1x92mx9.js} +21 -2
  49. package/dist/cli/chunk-j1x92mx9.js.map +10 -0
  50. package/dist/cli/{chunk-rekqz4qg.js → chunk-k3gr1nm9.js} +19 -16
  51. package/dist/cli/chunk-k3gr1nm9.js.map +10 -0
  52. package/dist/cli/{chunk-1n228xqb.js → chunk-kdpzw38a.js} +2 -2
  53. package/dist/cli/{chunk-cy5bm0j0.js → chunk-ms9z66nz.js} +2 -2
  54. package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-ms9z66nz.js.map} +2 -2
  55. package/dist/cli/{chunk-zf748avv.js → chunk-nbyvgnnm.js} +193 -286
  56. package/dist/cli/chunk-nbyvgnnm.js.map +36 -0
  57. package/dist/cli/{chunk-6bm0xpgd.js → chunk-s0bdnc7v.js} +2 -2
  58. package/dist/cli/{chunk-yxmk4pec.js → chunk-sc93apfs.js} +17 -16
  59. package/dist/cli/{chunk-yxmk4pec.js.map → chunk-sc93apfs.js.map} +3 -3
  60. package/dist/cli/{chunk-cmtcnh9f.js → chunk-tfycddbt.js} +2 -2
  61. package/dist/cli/{chunk-9xr4dgee.js → chunk-twy7psdz.js} +29 -16
  62. package/dist/cli/chunk-twy7psdz.js.map +10 -0
  63. package/dist/cli/{chunk-ntv4qrt7.js → chunk-y4msrwm9.js} +15 -15
  64. package/dist/cli/{chunk-yj17vs39.js → chunk-ydp3065p.js} +5 -5
  65. package/dist/cli/index.js +24 -19
  66. package/dist/cli/index.js.map +3 -3
  67. package/dist/types/ai/ask-context.d.ts +6 -2
  68. package/dist/types/ai/ask.d.ts +24 -5
  69. package/dist/types/ai/component-markdown.d.ts +2 -0
  70. package/dist/types/ai/openapi-components.d.ts +4 -1
  71. package/dist/types/analytics/clics.d.ts +37 -0
  72. package/dist/types/analytics/index.d.ts +2 -0
  73. package/dist/types/analytics/schema.d.ts +18 -0
  74. package/dist/types/astro/integration.d.ts +11 -4
  75. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  76. package/dist/types/astro/pages.d.ts +10 -0
  77. package/dist/types/cli/init/scaffold.d.ts +1 -1
  78. package/dist/types/components/content/api-field.d.ts +4 -1
  79. package/dist/types/components/content/api-page.d.ts +6 -1
  80. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  81. package/dist/types/components/openapi/helpers.d.ts +22 -1
  82. package/dist/types/components/openapi/security.d.ts +10 -0
  83. package/dist/types/components/openapi/snippets.d.ts +60 -0
  84. package/dist/types/core/changelog-index.d.ts +8 -0
  85. package/dist/types/core/config-input.d.ts +37 -6
  86. package/dist/types/core/data.d.ts +4 -0
  87. package/dist/types/core/i18n-ui.d.ts +2 -0
  88. package/dist/types/core/links.d.ts +8 -0
  89. package/dist/types/core/powered-by.d.ts +12 -0
  90. package/dist/types/core/probe.d.ts +2 -0
  91. package/dist/types/core/project-graph.d.ts +6 -0
  92. package/dist/types/core/redirect-patterns.d.ts +11 -3
  93. package/dist/types/core/schema.d.ts +228 -3
  94. package/dist/types/core/sources/cache.d.ts +3 -0
  95. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  96. package/dist/types/core/sources/contentful.d.ts +17 -1
  97. package/dist/types/core/sources/lexical.d.ts +5 -0
  98. package/dist/types/core/sources/lower.d.ts +10 -2
  99. package/dist/types/core/sources/normalize.d.ts +20 -1
  100. package/dist/types/core/sources/notion.d.ts +8 -0
  101. package/dist/types/core/sources/remote.d.ts +37 -10
  102. package/dist/types/core/sources/sanity.d.ts +1 -1
  103. package/dist/types/core/types.d.ts +17 -0
  104. package/dist/types/deploy/adapter-output.d.ts +2 -1
  105. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  106. package/dist/types/deploy/adapters/node.d.ts +58 -23
  107. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  108. package/dist/types/deploy/adapters/types.d.ts +3 -2
  109. package/dist/types/deploy/cloudflare-negotiation.d.ts +5 -2
  110. package/dist/types/deploy/node-headers.d.ts +12 -4
  111. package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
  112. package/dist/types/deploy/platforms/node.d.ts +3 -1
  113. package/dist/types/deploy/platforms/types.d.ts +18 -7
  114. package/dist/types/deploy/platforms/vercel.d.ts +21 -2
  115. package/dist/types/deploy/redirects.d.ts +17 -1
  116. package/dist/types/deploy/sitemap.d.ts +2 -1
  117. package/dist/types/deploy/vercel-negotiation.d.ts +44 -21
  118. package/dist/types/narration/build.d.ts +34 -12
  119. package/dist/types/narration/provider.d.ts +96 -8
  120. package/dist/types/openapi/checks.d.ts +27 -0
  121. package/dist/types/openapi/graphql.d.ts +6 -0
  122. package/dist/types/openapi/model.d.ts +15 -4
  123. package/dist/types/openapi/parse.d.ts +3 -0
  124. package/dist/types/openapi/references.d.ts +16 -0
  125. package/dist/types/openapi/sentence.d.ts +11 -1
  126. package/dist/types/ratelimit/index.d.ts +1 -1
  127. package/dist/types/ratelimit/schema.d.ts +8 -0
  128. package/dist/types/ratelimit/upstash.d.ts +39 -14
  129. package/dist/types/reference/graphql.d.ts +76 -1
  130. package/dist/types/reference/index.d.ts +1 -1
  131. package/dist/types/reference/openapi.d.ts +2 -2
  132. package/dist/types/reference/options.d.ts +37 -1
  133. package/dist/types/reference/schema.d.ts +84 -0
  134. package/dist/types/search/adapters/registry.d.ts +3 -0
  135. package/dist/types/search/adapters/typesense.d.ts +14 -4
  136. package/dist/types/search/build.d.ts +3 -1
  137. package/dist/types/search/orama-index.d.ts +9 -9
  138. package/dist/types/search/sync/algolia.d.ts +5 -2
  139. package/dist/types/search/sync/index.d.ts +3 -1
  140. package/dist/types/search/sync/typesense.d.ts +11 -7
  141. package/dist/types/seo/jsonld.d.ts +11 -6
  142. package/dist/types/sources/contentful.d.ts +16 -0
  143. package/dist/types/sources/github-releases.d.ts +8 -1
  144. package/dist/types/sources/notion.d.ts +8 -0
  145. package/dist/types/sources/payload.d.ts +2 -0
  146. package/dist/types/sources/registry.d.ts +63 -0
  147. package/dist/types/sources/shared.d.ts +3 -0
  148. package/dist/types/sources/strapi.d.ts +2 -0
  149. package/docs/01-quickstart.mdx +4 -2
  150. package/docs/02-deployment.mdx +59 -13
  151. package/docs/03-upgrading.mdx +2 -1
  152. package/docs/04-migrating.mdx +9 -9
  153. package/docs/08-faq.mdx +12 -12
  154. package/docs/advanced/changelog.mdx +30 -2
  155. package/docs/advanced/custom-pages.mdx +1 -1
  156. package/docs/advanced/skills.mdx +1 -1
  157. package/docs/cli/audit.mdx +30 -7
  158. package/docs/cli/doctor.mdx +6 -2
  159. package/docs/cli/evals.mdx +4 -4
  160. package/docs/cli/translate.mdx +1 -0
  161. package/docs/cli/validate.mdx +4 -3
  162. package/docs/configuration/analytics.mdx +19 -1
  163. package/docs/configuration/assistant.mdx +6 -2
  164. package/docs/configuration/customization.mdx +4 -2
  165. package/docs/configuration/export.mdx +5 -3
  166. package/docs/configuration/index.mdx +4 -2
  167. package/docs/configuration/narration.mdx +43 -6
  168. package/docs/configuration/rate-limiting.mdx +23 -2
  169. package/docs/configuration/search.mdx +37 -14
  170. package/docs/content/components.mdx +2 -2
  171. package/docs/content/frontmatter.mdx +5 -3
  172. package/docs/content/i18n.mdx +2 -2
  173. package/docs/content/includes.mdx +1 -1
  174. package/docs/content/navigation.mdx +8 -6
  175. package/docs/content/sources/contentful.mdx +9 -4
  176. package/docs/content/sources/custom.mdx +7 -3
  177. package/docs/content/sources/github-releases.mdx +12 -2
  178. package/docs/content/sources/index.mdx +3 -3
  179. package/docs/content/sources/notion.mdx +17 -2
  180. package/docs/content/sources/obsidian.mdx +3 -1
  181. package/docs/content/sources/payload.mdx +5 -2
  182. package/docs/content/sources/remote-mdx.mdx +6 -0
  183. package/docs/content/sources/sanity.mdx +4 -2
  184. package/docs/content/sources/strapi.mdx +5 -2
  185. package/docs/content/syntax.mdx +2 -2
  186. package/docs/discoverability/agent-discovery.mdx +1 -1
  187. package/docs/discoverability/index.mdx +1 -1
  188. package/docs/discoverability/json-api.mdx +2 -0
  189. package/docs/discoverability/llms-txt.mdx +1 -1
  190. package/docs/discoverability/markdown.mdx +3 -1
  191. package/docs/discoverability/mcp.mdx +6 -0
  192. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  193. package/docs/discoverability/structured-data.mdx +8 -6
  194. package/docs/references/api-pages.mdx +1 -1
  195. package/docs/references/asyncapi.mdx +5 -1
  196. package/docs/references/graphql.mdx +15 -1
  197. package/docs/references/openapi.mdx +64 -8
  198. package/package.json +9 -9
  199. package/skills/blume/SKILL.md +1 -1
  200. package/skills/blume-migrate/SKILL.md +2 -2
  201. package/skills/blume-migrate/assets/{oxfmt@0.67.0.patch → oxfmt@0.71.0.patch} +7 -7
  202. package/skills/blume-migrate/references/docusaurus.md +4 -4
  203. package/skills/blume-migrate/references/fumadocs.md +17 -13
  204. package/skills/blume-migrate/references/monorepo.md +5 -5
  205. package/skills/blume-migrate/references/nextra.md +8 -5
  206. package/skills/blume-migrate/references/starlight.md +9 -7
  207. package/src/ai/ask-context.ts +33 -3
  208. package/src/ai/ask-limits.ts +13 -0
  209. package/src/ai/ask.ts +24 -6
  210. package/src/ai/changelog-markdown.ts +11 -6
  211. package/src/ai/component-markdown.ts +1 -1
  212. package/src/ai/cors.ts +12 -3
  213. package/src/ai/mcp/query.ts +14 -6
  214. package/src/ai/openapi-components.ts +28 -4
  215. package/src/ai/relative-links.ts +4 -38
  216. package/src/analytics/clics.ts +58 -0
  217. package/src/analytics/head.ts +5 -0
  218. package/src/analytics/index.ts +2 -0
  219. package/src/analytics/schema.ts +2 -0
  220. package/src/astro/generate.ts +91 -56
  221. package/src/astro/integration.ts +72 -22
  222. package/src/astro/markdown-negotiation.ts +3 -0
  223. package/src/astro/module-types.ts +3 -1
  224. package/src/astro/pages.ts +23 -0
  225. package/src/astro/render-deps.ts +10 -5
  226. package/src/astro/runtime-deps.ts +23 -2
  227. package/src/astro/templates.ts +129 -81
  228. package/src/audit/catalog.ts +19 -3
  229. package/src/audit/checks/links.ts +14 -1
  230. package/src/audit/checks/network.ts +110 -6
  231. package/src/audit/checks/redirects.ts +6 -4
  232. package/src/audit/checks/robots.ts +58 -2
  233. package/src/audit/run.ts +3 -0
  234. package/src/audit/types.ts +2 -0
  235. package/src/audit/url.ts +14 -5
  236. package/src/blume-modules.d.ts +5 -1
  237. package/src/cli/args.ts +51 -0
  238. package/src/cli/build-failure.ts +23 -0
  239. package/src/cli/commands/audit.ts +10 -1
  240. package/src/cli/commands/dev.ts +18 -2
  241. package/src/cli/commands/doctor.ts +45 -12
  242. package/src/cli/commands/eval.ts +36 -4
  243. package/src/cli/commands/init.ts +6 -4
  244. package/src/cli/commands/validate.ts +10 -1
  245. package/src/cli/init/scaffold.ts +9 -6
  246. package/src/cli/prepare.ts +6 -3
  247. package/src/cli/required-secrets.ts +3 -2
  248. package/src/components/content/ApiPlayground.astro +6 -2
  249. package/src/components/content/api-field.ts +4 -1
  250. package/src/components/content/api-page.ts +31 -7
  251. package/src/components/content/mermaid-element.ts +15 -1
  252. package/src/components/content/view-switcher.ts +7 -4
  253. package/src/components/islands/hooks.ts +27 -1
  254. package/src/components/layout/NavTree.astro +49 -5
  255. package/src/components/layout/NavTreeScript.astro +18 -0
  256. package/src/components/layout/PageActions.astro +139 -24
  257. package/src/components/layout/PageLayout.astro +13 -3
  258. package/src/components/layout/RootLayout.astro +27 -14
  259. package/src/components/layout/Search.astro +17 -3
  260. package/src/components/layout/TableOfContents.astro +24 -17
  261. package/src/components/layout/analytics-client.ts +8 -8
  262. package/src/components/layout/head-scripts.ts +6 -1
  263. package/src/components/layout/search/algolia.ts +10 -1
  264. package/src/components/layout/search/analytics.ts +42 -14
  265. package/src/components/layout/search/endpoint.ts +48 -5
  266. package/src/components/layout/search/pagefind.ts +114 -5
  267. package/src/components/layout/search/types.ts +12 -0
  268. package/src/components/layout/search/typesense.ts +18 -6
  269. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  270. package/src/components/openapi/Authorization.astro +10 -0
  271. package/src/components/openapi/GraphqlOperation.astro +5 -0
  272. package/src/components/openapi/Operation.astro +24 -1
  273. package/src/components/openapi/ParametersTable.astro +8 -4
  274. package/src/components/openapi/Playground.astro +7 -1
  275. package/src/components/openapi/RequestBody.astro +19 -2
  276. package/src/components/openapi/Responses.astro +1 -0
  277. package/src/components/openapi/SchemaTable.astro +15 -3
  278. package/src/components/openapi/async-snippets.ts +61 -5
  279. package/src/components/openapi/async.ts +123 -29
  280. package/src/components/openapi/graphql-helpers.ts +35 -12
  281. package/src/components/openapi/helpers.ts +36 -2
  282. package/src/components/openapi/message-model.ts +54 -11
  283. package/src/components/openapi/message.ts +14 -4
  284. package/src/components/openapi/operation-model.ts +16 -7
  285. package/src/components/openapi/playground-client.ts +77 -58
  286. package/src/components/openapi/playground-schema.ts +1 -1
  287. package/src/components/openapi/schema-tree.ts +44 -14
  288. package/src/components/openapi/security.ts +10 -0
  289. package/src/components/openapi/snippets.ts +5 -3
  290. package/src/components/openapi/validate-json.ts +4 -3
  291. package/src/components/powered-by-middleware.ts +33 -0
  292. package/src/core/changelog-index.ts +31 -1
  293. package/src/core/component-diagnostics.ts +53 -0
  294. package/src/core/component-overrides.ts +32 -0
  295. package/src/core/config-input.ts +42 -6
  296. package/src/core/data.ts +2 -0
  297. package/src/core/diagnostics.ts +5 -0
  298. package/src/core/i18n-ui.ts +2 -0
  299. package/src/core/includes.ts +1 -1
  300. package/src/core/links.ts +47 -17
  301. package/src/core/powered-by.ts +12 -0
  302. package/src/core/probe.ts +26 -6
  303. package/src/core/project-graph.ts +59 -18
  304. package/src/core/redirect-patterns.ts +36 -7
  305. package/src/core/schema.ts +26 -6
  306. package/src/core/sources/cache.ts +6 -0
  307. package/src/core/sources/contentful-rich-text.ts +29 -6
  308. package/src/core/sources/contentful.ts +135 -25
  309. package/src/core/sources/filesystem.ts +26 -4
  310. package/src/core/sources/github-releases.ts +12 -1
  311. package/src/core/sources/lexical.ts +15 -10
  312. package/src/core/sources/lower.ts +19 -3
  313. package/src/core/sources/mdx-remote.ts +62 -10
  314. package/src/core/sources/normalize.ts +227 -41
  315. package/src/core/sources/notion.ts +34 -10
  316. package/src/core/sources/obsidian.ts +16 -3
  317. package/src/core/sources/payload.ts +38 -15
  318. package/src/core/sources/remote.ts +72 -15
  319. package/src/core/sources/resolve.ts +11 -1
  320. package/src/core/sources/sanity.ts +43 -25
  321. package/src/core/sources/strapi.ts +2 -2
  322. package/src/core/types.ts +18 -0
  323. package/src/core/ui-packs/ar.ts +1 -0
  324. package/src/core/ui-packs/bg.ts +1 -0
  325. package/src/core/ui-packs/bn.ts +1 -0
  326. package/src/core/ui-packs/ca.ts +1 -0
  327. package/src/core/ui-packs/cs.ts +1 -0
  328. package/src/core/ui-packs/da.ts +1 -0
  329. package/src/core/ui-packs/de.ts +1 -0
  330. package/src/core/ui-packs/el.ts +1 -0
  331. package/src/core/ui-packs/es.ts +1 -0
  332. package/src/core/ui-packs/fa.ts +1 -0
  333. package/src/core/ui-packs/fi.ts +1 -0
  334. package/src/core/ui-packs/fr.ts +1 -0
  335. package/src/core/ui-packs/he.ts +1 -0
  336. package/src/core/ui-packs/hi.ts +1 -0
  337. package/src/core/ui-packs/hr.ts +1 -0
  338. package/src/core/ui-packs/hu.ts +1 -0
  339. package/src/core/ui-packs/id.ts +1 -0
  340. package/src/core/ui-packs/it.ts +1 -0
  341. package/src/core/ui-packs/ja.ts +1 -0
  342. package/src/core/ui-packs/ko.ts +1 -0
  343. package/src/core/ui-packs/nl.ts +1 -0
  344. package/src/core/ui-packs/no.ts +1 -0
  345. package/src/core/ui-packs/pl.ts +1 -0
  346. package/src/core/ui-packs/pt-br.ts +1 -0
  347. package/src/core/ui-packs/pt.ts +1 -0
  348. package/src/core/ui-packs/ro.ts +1 -0
  349. package/src/core/ui-packs/ru.ts +1 -0
  350. package/src/core/ui-packs/sk.ts +1 -0
  351. package/src/core/ui-packs/sr.ts +1 -0
  352. package/src/core/ui-packs/sv.ts +1 -0
  353. package/src/core/ui-packs/th.ts +1 -0
  354. package/src/core/ui-packs/tr.ts +1 -0
  355. package/src/core/ui-packs/uk.ts +1 -0
  356. package/src/core/ui-packs/vi.ts +1 -0
  357. package/src/core/ui-packs/zh-tw.ts +1 -0
  358. package/src/core/ui-packs/zh.ts +1 -0
  359. package/src/deploy/adapter-output.ts +7 -2
  360. package/src/deploy/adapters/cloudflare.ts +7 -5
  361. package/src/deploy/adapters/node.ts +47 -9
  362. package/src/deploy/adapters/types.ts +7 -3
  363. package/src/deploy/artifacts.ts +2 -1
  364. package/src/deploy/cloudflare-negotiation.ts +36 -7
  365. package/src/deploy/headers.ts +15 -1
  366. package/src/deploy/node-headers.ts +32 -20
  367. package/src/deploy/platforms/cloudflare.ts +14 -9
  368. package/src/deploy/platforms/netlify.ts +2 -1
  369. package/src/deploy/platforms/node.ts +12 -2
  370. package/src/deploy/platforms/types.ts +21 -7
  371. package/src/deploy/platforms/vercel.ts +115 -17
  372. package/src/deploy/redirects.ts +66 -6
  373. package/src/deploy/rss.ts +3 -1
  374. package/src/deploy/sitemap.ts +39 -5
  375. package/src/deploy/vercel-negotiation.ts +325 -103
  376. package/src/eval/agents.ts +18 -1
  377. package/src/eval/prompts.ts +6 -3
  378. package/src/eval/report.ts +34 -13
  379. package/src/eval/run.ts +5 -0
  380. package/src/markdown/base-links.ts +15 -14
  381. package/src/markdown/package-commands.ts +9 -8
  382. package/src/markdown/relative-links.ts +3 -2
  383. package/src/narration/build.ts +218 -40
  384. package/src/narration/provider.ts +89 -9
  385. package/src/openapi/checks.ts +273 -0
  386. package/src/openapi/graphql-build.ts +21 -0
  387. package/src/openapi/graphql.ts +11 -0
  388. package/src/openapi/model.ts +15 -3
  389. package/src/openapi/parse.ts +127 -2
  390. package/src/openapi/references.ts +43 -3
  391. package/src/openapi/render-mdx.ts +68 -8
  392. package/src/openapi/sentence.ts +29 -2
  393. package/src/openapi/source.ts +39 -14
  394. package/src/ratelimit/index.ts +1 -1
  395. package/src/ratelimit/runtime.ts +37 -19
  396. package/src/ratelimit/upstash.ts +40 -10
  397. package/src/reference/graphql.ts +15 -1
  398. package/src/reference/index.ts +1 -0
  399. package/src/reference/openapi.ts +2 -2
  400. package/src/reference/options.ts +29 -1
  401. package/src/registry/eject.ts +8 -2
  402. package/src/search/adapters/locale-scope.ts +30 -0
  403. package/src/search/adapters/typesense.ts +14 -4
  404. package/src/search/build.ts +21 -3
  405. package/src/search/orama-index.ts +163 -31
  406. package/src/search/source-pages.ts +29 -0
  407. package/src/search/sync/algolia.ts +99 -9
  408. package/src/search/sync/index.ts +52 -13
  409. package/src/search/sync/typesense.ts +72 -27
  410. package/src/seo/jsonld.ts +38 -20
  411. package/src/sources/contentful.ts +12 -0
  412. package/src/sources/github-releases.ts +7 -1
  413. package/src/sources/notion.ts +7 -0
  414. package/src/sources/shared.ts +3 -0
  415. package/src/theme/color.ts +9 -10
  416. package/src/theme/entry.ts +43 -6
  417. package/src/translate/validate.ts +23 -1
  418. package/dist/cli/chunk-0gz36akr.js.map +0 -12
  419. package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
  420. package/dist/cli/chunk-43zcf4gz.js.map +0 -36
  421. package/dist/cli/chunk-4ejxbzee.js +0 -39
  422. package/dist/cli/chunk-4ejxbzee.js.map +0 -10
  423. package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
  424. package/dist/cli/chunk-713mngdq.js.map +0 -14
  425. package/dist/cli/chunk-9xr4dgee.js.map +0 -10
  426. package/dist/cli/chunk-amrb6hgh.js +0 -145
  427. package/dist/cli/chunk-amrb6hgh.js.map +0 -11
  428. package/dist/cli/chunk-nd1821sk.js.map +0 -10
  429. package/dist/cli/chunk-rekqz4qg.js.map +0 -10
  430. package/dist/cli/chunk-ta27feev.js.map +0 -10
  431. package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
  432. package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
  433. package/dist/cli/chunk-zf748avv.js.map +0 -37
  434. /package/dist/cli/{chunk-9geymqns.js.map → chunk-0w6hs479.js.map} +0 -0
  435. /package/dist/cli/{chunk-y4ykbark.js.map → chunk-1f882900.js.map} +0 -0
  436. /package/dist/cli/{chunk-py62fewh.js.map → chunk-53jqw8z0.js.map} +0 -0
  437. /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-58bt3hag.js.map} +0 -0
  438. /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-5cmts6dv.js.map} +0 -0
  439. /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-9qzgg6mj.js.map} +0 -0
  440. /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-a57yh0h9.js.map} +0 -0
  441. /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-desvm72z.js.map} +0 -0
  442. /package/dist/cli/{chunk-gp84za0h.js.map → chunk-dhwtqnnd.js.map} +0 -0
  443. /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-dqhg54mr.js.map} +0 -0
  444. /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-gfc5fqvf.js.map} +0 -0
  445. /package/dist/cli/{chunk-p804f3pe.js.map → chunk-hvcxng5f.js.map} +0 -0
  446. /package/dist/cli/{chunk-1n228xqb.js.map → chunk-kdpzw38a.js.map} +0 -0
  447. /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-s0bdnc7v.js.map} +0 -0
  448. /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-tfycddbt.js.map} +0 -0
  449. /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-y4msrwm9.js.map} +0 -0
  450. /package/dist/cli/{chunk-yj17vs39.js.map → chunk-ydp3065p.js.map} +0 -0
@@ -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>
@@ -126,10 +126,17 @@ interface Props {
126
126
  canonical?: string | null;
127
127
  /**
128
128
  * Emit schema.org JSON-LD for this page (`data.config.structuredData`) — a
129
- * WebSite node, plus an article node on non-root routes. Defaults to on,
130
- * matching RootLayout.
129
+ * WebSite node, plus a WebPage node on the homepage and an article node on
130
+ * other routes. Defaults to on, matching RootLayout.
131
131
  */
132
132
  structuredDataEnabled?: boolean;
133
+ /**
134
+ * When the page last changed, emitted as the JSON-LD `dateModified`. A
135
+ * custom page has no content file to date, so pass it by hand; without a
136
+ * machine-readable date, search engines take whatever other date the page
137
+ * shows.
138
+ */
139
+ lastModified?: string | Date | null;
133
140
  /**
134
141
  * Which agent-discovery resources exist, advertised as `describedby` head
135
142
  * links plus the AI Catalog/ARD pair — the same `DiscoveryLinks` partial
@@ -184,6 +191,7 @@ const {
184
191
  ogImageSize,
185
192
  canonical,
186
193
  structuredDataEnabled,
194
+ lastModified,
187
195
  discovery = data.config.discovery,
188
196
  x,
189
197
  noindex,
@@ -279,7 +287,8 @@ const xSite = normalizeXHandle(x?.handle);
279
287
  const xCreator = normalizeXHandle(x?.creator);
280
288
 
281
289
  // JSON-LD, mirroring RootLayout: skipped when disabled or noindexed. A custom
282
- // page has no breadcrumb trail; the home route yields the WebSite node alone.
290
+ // page has no breadcrumb trail; the home route is a WebPage, dated by
291
+ // `lastModified` when the page passes one.
283
292
  const structuredData =
284
293
  structuredDataEnabled === false || noindex
285
294
  ? null
@@ -289,6 +298,7 @@ const structuredData =
289
298
  description,
290
299
  identity: data.config.identity,
291
300
  locale,
301
+ modified: lastModified,
292
302
  route,
293
303
  siteName: site.title,
294
304
  siteUrl: siteUrl ?? null,
@@ -432,10 +432,13 @@ const description = page.description ?? site.description;
432
432
  // pages (and the changelog index, which has no `type`) stay `website`.
433
433
  const ogType =
434
434
  pageType === "blog" || pageType === "changelog" ? "article" : "website";
435
+ // The last-modified date as ISO 8601, parsed once for every surface that
436
+ // carries it; undefined when absent or unparseable.
437
+ const lastModifiedIso = toIso(lastModified);
435
438
  // `article:*` timestamps only belong on an article; a docs page carrying them
436
439
  // would be declaring properties its `og:type` doesn't define.
437
440
  const articlePublished = ogType === "article" ? toIso(published) : undefined;
438
- const articleModified = ogType === "article" ? toIso(lastModified) : undefined;
441
+ const articleModified = ogType === "article" ? lastModifiedIso : undefined;
439
442
  // X sizes the card from `twitter:card`: a large image needs the wide variant,
440
443
  // and a page with no image still gets a card (the compact `summary`) rather
441
444
  // than rendering as a bare link.
@@ -448,14 +451,12 @@ const xCreator = normalizeXHandle(x?.creator);
448
451
 
449
452
  // "Last updated on <date>" — the configured `dateFormat`, in UTC (unless the
450
453
  // config names a zone) so it matches the changelog timeline.
451
- const lastModifiedDate = lastModified ? new Date(lastModified) : null;
452
- const formattedLastModified =
453
- lastModifiedDate && !Number.isNaN(lastModifiedDate.getTime())
454
- ? new Intl.DateTimeFormat(
455
- locale || "en",
456
- resolveDateFormatOptions(dateFormat)
457
- ).format(lastModifiedDate)
458
- : null;
454
+ const formattedLastModified = lastModifiedIso
455
+ ? new Intl.DateTimeFormat(
456
+ locale || "en",
457
+ resolveDateFormatOptions(dateFormat)
458
+ ).format(new Date(lastModifiedIso))
459
+ : null;
459
460
 
460
461
  // This page's raw-Markdown mirror, advertised as a `text/markdown` alternate
461
462
  // in the head. Content routes always have one (see `markdownRoutePaths`, which
@@ -521,6 +522,7 @@ const structuredData =
521
522
  base: import.meta.env.BASE_URL,
522
523
  breadcrumbs: crumbs,
523
524
  description,
525
+ homeRoute: navigation.root,
524
526
  identity: data.config.identity,
525
527
  locale,
526
528
  modified: lastModified,
@@ -868,13 +870,20 @@ const localeRedirect = localeTargets
868
870
  />
869
871
  )
870
872
  }
873
+ {/* Below `xl`, where the rail is hidden, the page actions ride in
874
+ the "On this page" dropdown, so it renders wherever the rail
875
+ would, with or without headings. They render once, in the rail:
876
+ the dropdown gets an empty mount the actions script moves them
877
+ into (see PageActions.astro). */}
871
878
  {
872
- hasToc && (
879
+ showToc && (
873
880
  <TableOfContentsSlot
874
881
  headings={tocHeadings}
875
882
  title={strings.toc.title}
876
883
  variant="mobile"
877
- />
884
+ >
885
+ <div data-blume-page-actions-mount />
886
+ </TableOfContentsSlot>
878
887
  )
879
888
  }
880
889
  <PageHeaderSlot headings={tocHeadings} page={page} route={page.route} />
@@ -911,6 +920,7 @@ const localeRedirect = localeTargets
911
920
  (apiRail || modeLayout.width === "full") && "[&>*]:max-w-none",
912
921
  modeLayout.width === "center" && "[&>*]:max-w-[54rem]",
913
922
  ]}
923
+ data-blume-page-end
914
924
  >
915
925
  {directoryGroup && <Directory group={directoryGroup} />}
916
926
  <RelatedPages links={relatedItems} title={strings.page.related} />
@@ -921,7 +931,7 @@ const localeRedirect = localeTargets
921
931
  />
922
932
  {formattedLastModified && (
923
933
  <p class="mx-auto mt-10 max-w-content text-muted-foreground text-sm">
924
- {strings.page.lastUpdated} {formattedLastModified}
934
+ {strings.page.lastUpdated} <time datetime={lastModifiedIso}>{formattedLastModified}</time>
925
935
  </p>
926
936
  )}
927
937
  {feedback && (
@@ -1370,10 +1380,13 @@ const localeRedirect = localeTargets
1370
1380
  }
1371
1381
  @media (prefers-reduced-motion: reduce) {
1372
1382
  /* html prefix outranks the library's rules regardless of sheet
1373
- order; its transition declarations carry !important. */
1383
+ order; its transition declarations carry !important. Near-instant,
1384
+ never `none`: medium-zoom only finishes opening and closing on the
1385
+ image's `transitionend`, so with no transition a zoomed image
1386
+ ignores Escape, clicks, and scrolls forever. */
1374
1387
  html .medium-zoom-image,
1375
1388
  html .medium-zoom-overlay {
1376
- transition: none !important;
1389
+ transition-duration: 1ms !important;
1377
1390
  }
1378
1391
  }
1379
1392
  </style>
@@ -4,6 +4,7 @@ import data from "blume:data";
4
4
  import { EN_UI } from "../../core/i18n-ui.ts";
5
5
  import type { UIStrings } from "../../core/i18n-ui.ts";
6
6
  import type { Navigation } from "../../core/types.ts";
7
+ import { scopesLocales } from "../../search/adapters/locale-scope.ts";
7
8
  import { scopesVersions } from "../../search/adapters/version-scope.ts";
8
9
  import { resolvePopularIconMarkup } from "../../search/popular-icon.ts";
9
10
  import Icon from "../Icon.astro";
@@ -36,9 +37,15 @@ const {
36
37
  navigation,
37
38
  popularPages,
38
39
  strings,
39
- locale,
40
+ locale: viewedLocale,
40
41
  version: viewedVersion = null,
41
42
  } = Astro.props;
43
+ // Mixedbread searches every language whatever the dialog asks, so on it an
44
+ // "All languages" toggle would change nothing: treat the site as
45
+ // single-language for search instead.
46
+ const locale = scopesLocales(data.config.search.provider)
47
+ ? viewedLocale
48
+ : undefined;
42
49
  // Pagefind, Orama Cloud, and Mixedbread search every version whatever the
43
50
  // dialog asks, so on those an "All versions" toggle would change nothing:
44
51
  // treat the site as unversioned for search instead.
@@ -90,6 +97,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
90
97
  data-i18n-popular={s.popular}
91
98
  data-i18n-results={s.results}
92
99
  data-locale={locale || undefined}
100
+ data-redact-queries={data.config.search.analytics.queries ? undefined : ""}
93
101
  data-version={version ?? undefined}
94
102
  data-versioned={version === null ? undefined : ""}
95
103
  >
@@ -240,6 +248,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
240
248
 
241
249
  import { track } from "./analytics-client.ts";
242
250
  import { createSearchTracker } from "./search/analytics.ts";
251
+ import type { SearchTracker } from "./search/analytics.ts";
243
252
  import { highlight, matchSnippet } from "./search/types.ts";
244
253
  import type { SearchFn, SearchHit } from "./search/types.ts";
245
254
 
@@ -306,7 +315,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
306
315
  searchClient ??= (async () => {
307
316
  try {
308
317
  const { createSearch } = await import("blume:search-client");
309
- return await createSearch();
318
+ // The reader types each query, so each supersedes the last.
319
+ return await createSearch({ typing: true });
310
320
  } catch (error) {
311
321
  searchClient = undefined;
312
322
  throw error;
@@ -325,7 +335,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
325
335
  preview!: HTMLElement;
326
336
  searchFn: SearchFn | null = null;
327
337
  /** Records settled queries and picked results (search/analytics.ts). */
328
- tracker = createSearchTracker(track);
338
+ tracker!: SearchTracker;
329
339
  loaded = false;
330
340
  loadFailed = false;
331
341
  /** First-open client/index load still in flight. */
@@ -419,6 +429,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
419
429
  this.message = this.querySelector("[data-blume-search-message]")!;
420
430
  this.preview = this.querySelector("[data-blume-search-preview]")!;
421
431
  this.assistantEnabled = this.hasAttribute("data-ask");
432
+ // `search.analytics.queries: false` keeps query text off providers.
433
+ this.tracker = createSearchTracker(track, {
434
+ queries: !this.hasAttribute("data-redact-queries"),
435
+ });
422
436
 
423
437
  const popularScript = this.querySelector(
424
438
  "[data-blume-search-popular]"
@@ -15,10 +15,14 @@ interface Props {
15
15
  }
16
16
 
17
17
  const { headings, title, variant } = Astro.props;
18
+ // The mobile dropdown also holds what the layout passes as children (the
19
+ // mount the page actions move into below `xl`, where the rail that renders
20
+ // them is hidden), so a page with actions but no headings still gets it.
21
+ const hasChildren = Astro.slots.has("default");
18
22
  ---
19
23
 
20
24
  {
21
- headings.length > 0 && variant === "mobile" && (
25
+ (headings.length > 0 || hasChildren) && variant === "mobile" && (
22
26
  <details class="group mx-auto mb-6 max-w-content rounded-blume border border-border xl:hidden">
23
27
  <summary class="flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 font-medium text-foreground text-sm [&::-webkit-details-marker]:hidden">
24
28
  <span>{title}</span>
@@ -28,23 +32,26 @@ const { headings, title, variant } = Astro.props;
28
32
  size={16}
29
33
  />
30
34
  </summary>
31
- <blume-toc class="block">
32
- <ul class="m-0 list-none border-border border-t p-2">
33
- {headings.map((heading) => (
34
- <li
35
- data-depth={heading.depth}
36
- style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
37
- >
38
- <a
39
- class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
40
- href={`#${heading.slug}`}
35
+ {headings.length > 0 && (
36
+ <blume-toc class="block">
37
+ <ul class="m-0 list-none border-border border-t p-2">
38
+ {headings.map((heading) => (
39
+ <li
40
+ data-depth={heading.depth}
41
+ style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
41
42
  >
42
- {heading.text}
43
- </a>
44
- </li>
45
- ))}
46
- </ul>
47
- </blume-toc>
43
+ <a
44
+ class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
45
+ href={`#${heading.slug}`}
46
+ >
47
+ {heading.text}
48
+ </a>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ </blume-toc>
53
+ )}
54
+ <slot />
48
55
  </details>
49
56
  )
50
57
  }
@@ -2,14 +2,14 @@
2
2
  * Send a custom analytics event to every analytics platform configured in
3
3
  * `blume.config.ts`. Mirrors the adapters `Analytics.astro` wires
4
4
  * (`src/analytics/*`): each one with a custom-event API is called through the
5
- * global its snippet defines (Cloudflare and Clearbit have none to forward
6
- * to), and the `blume:track` CustomEvent fires unconditionally so a project
7
- * can bridge the event to anything else — a `script()` adapter, say. Every
8
- * call no-ops cleanly when a provider isn't present — for example during
9
- * `blume dev`, where `Analytics.astro` injects nothing — and a provider that
10
- * throws (a consent shim that stubs `gtag` with a raise, a broken snippet) is
11
- * isolated so it neither starves the providers after it nor surfaces in the
12
- * feature that reported the event.
5
+ * global its snippet defines (Cloudflare, Clearbit, and Clics have none to
6
+ * forward to), and the `blume:track` CustomEvent fires unconditionally so a
7
+ * project can bridge the event to anything else — a `script()` adapter, say.
8
+ * Every call no-ops cleanly when a provider isn't present — for example
9
+ * during `blume dev`, where `Analytics.astro` injects nothing — and a provider
10
+ * that throws (a consent shim that stubs `gtag` with a raise, a broken
11
+ * snippet) is isolated so it neither starves the providers after it nor
12
+ * surfaces in the feature that reported the event.
13
13
  */
14
14
  import { track as vercelTrack } from "@vercel/analytics";
15
15
 
@@ -44,8 +44,13 @@
44
44
  * stored preference instead of aborting before the swap listeners exist, and
45
45
  * after a swap the theme the `before-swap` stamp carried over stands, so a
46
46
  * toggle the reader made still holds for the rest of their visit.
47
+ *
48
+ * Paper is white, so a dark page prints light: `beforeprint` switches the root
49
+ * to the light theme (every token, Shiki color, and `dark:` utility follows
50
+ * it) and `afterprint` puts the dark theme back. The listeners sit on
51
+ * `window`, so the one registration outlives client-router swaps.
47
52
  */
48
- export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{const r=document.documentElement;let s=null;let blocked=false;try{s=localStorage.getItem("blume-theme");}catch{blocked=true;}const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.dataset.theme=s??(blocked&&r.dataset.theme?r.dataset.theme:m==="system"?sys:m);};apply();document.addEventListener("astro:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);})();`;
53
+ export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{const r=document.documentElement;let s=null;let blocked=false;try{s=localStorage.getItem("blume-theme");}catch{blocked=true;}const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.dataset.theme=s??(blocked&&r.dataset.theme?r.dataset.theme:m==="system"?sys:m);};apply();document.addEventListener("astro:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);let printed=null;addEventListener("beforeprint",()=>{const r=document.documentElement;if(r.dataset.theme==="dark"){printed="dark";r.dataset.theme="light";}});addEventListener("afterprint",()=>{if(printed){document.documentElement.dataset.theme=printed;printed=null;}});})();`;
49
54
 
50
55
  /**
51
56
  * Keep an embedded Scalar reference on Blume's theme. Scalar picks its own
@@ -48,7 +48,16 @@ export const createSearch = (opts: AlgoliaOptions): SearchFn => {
48
48
  // shape, so hits returned by that index carry those fields.
49
49
  const records =
50
50
  first && "hits" in first ? (first.hits as AlgoliaRecord[]) : [];
51
- const hits = records.map((record) => ({
51
+ // The sync splits a long page across records that share its `url`. The
52
+ // index's `distinct` returns one of them, unless the site set its own
53
+ // `attributeForDistinct`; either way a page is listed once.
54
+ const pages = new Set<string>();
55
+ const unique = records.filter((record) => {
56
+ const seen = pages.has(record.url);
57
+ pages.add(record.url);
58
+ return !seen;
59
+ });
60
+ const hits = unique.map((record) => ({
52
61
  content: record.content ?? "",
53
62
  excerpt: highlight(
54
63
  excerptFor(record.description ?? "", record.content ?? "", query),