blume 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (601) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +113 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-6k4ftwze.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-6k4ftwze.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-zp79m0ts.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-ddndchfr.js → chunk-2f3ndkpw.js} +57 -29
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-fsmrqk8a.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-ppzjqwx2.js → chunk-2n4ps1b3.js} +17 -18
  12. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-2n4ps1b3.js.map} +3 -3
  13. package/dist/cli/{chunk-5r8g91qn.js → chunk-43zcf4gz.js} +398 -159
  14. package/dist/cli/chunk-43zcf4gz.js.map +36 -0
  15. package/dist/cli/chunk-4ejxbzee.js +39 -0
  16. package/dist/cli/chunk-4ejxbzee.js.map +10 -0
  17. package/dist/cli/{chunk-hr8ne106.js → chunk-4h8z9vv5.js} +296 -77
  18. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  19. package/dist/cli/{chunk-k7pj68a8.js → chunk-5cg2rzgf.js} +18 -16
  20. package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5cg2rzgf.js.map} +2 -2
  21. package/dist/cli/{chunk-h2ez8dzb.js → chunk-5ffywsmb.js} +4 -4
  22. package/dist/cli/{chunk-g698a744.js → chunk-5wj2qwn2.js} +5 -5
  23. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  24. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  25. package/dist/cli/{chunk-3yce002v.js → chunk-713mngdq.js} +15 -8
  26. package/dist/cli/chunk-713mngdq.js.map +14 -0
  27. package/dist/cli/{chunk-91ws1n6j.js → chunk-9geymqns.js} +17 -15
  28. package/dist/cli/{chunk-91ws1n6j.js.map → chunk-9geymqns.js.map} +2 -2
  29. package/dist/cli/{chunk-6dtt0zfn.js → chunk-9xr4dgee.js} +27 -18
  30. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  31. package/dist/cli/{chunk-sqw4ekg1.js → chunk-amrb6hgh.js} +2 -2
  32. package/dist/cli/chunk-cm89scdv.js +443 -0
  33. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  34. package/dist/cli/{chunk-wdrt2k2v.js → chunk-cmtcnh9f.js} +90 -26
  35. package/dist/cli/chunk-cmtcnh9f.js.map +10 -0
  36. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  37. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  38. package/dist/cli/{chunk-7mbqtmgb.js → chunk-e7vvk5ya.js} +21 -12
  39. package/dist/cli/chunk-e7vvk5ya.js.map +10 -0
  40. package/dist/cli/{chunk-qkb5a8sa.js → chunk-g2z9x01v.js} +6 -98
  41. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  42. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  43. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  44. package/dist/cli/{chunk-h7k3nq3v.js → chunk-gp84za0h.js} +2 -2
  45. package/dist/cli/{chunk-hqp2ajnh.js → chunk-h2a48yhd.js} +195 -73
  46. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  47. package/dist/cli/{chunk-pbg5a4s3.js → chunk-jnwj5kjj.js} +71 -47
  48. package/dist/cli/chunk-jnwj5kjj.js.map +19 -0
  49. package/dist/cli/chunk-kwrg63tm.js +31 -0
  50. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  51. package/dist/cli/{chunk-8g8ytmgx.js → chunk-nd1821sk.js} +21 -19
  52. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-nd1821sk.js.map} +2 -2
  53. package/dist/cli/{chunk-w4bxdvsa.js → chunk-ntv4qrt7.js} +17 -15
  54. package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-ntv4qrt7.js.map} +2 -2
  55. package/dist/cli/{chunk-bfwp9vp6.js → chunk-p804f3pe.js} +7 -7
  56. package/dist/cli/{chunk-4e9b9ra6.js → chunk-py62fewh.js} +6 -6
  57. package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-py62fewh.js.map} +1 -1
  58. package/dist/cli/chunk-q3wxxq39.js +126 -0
  59. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  60. package/dist/cli/{chunk-j85scx15.js → chunk-rekqz4qg.js} +142 -46
  61. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  62. package/dist/cli/{chunk-00gs3wqs.js → chunk-ta27feev.js} +1 -1
  63. package/dist/cli/{chunk-v6ya5kcb.js → chunk-xkz5xbtv.js} +4031 -1132
  64. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  65. package/dist/cli/{chunk-bbnwccaz.js → chunk-y4ykbark.js} +2 -2
  66. package/dist/cli/{chunk-n1yg3tj3.js → chunk-y5h4r2y0.js} +6 -6
  67. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-y5h4r2y0.js.map} +3 -3
  68. package/dist/cli/{chunk-nfcyttvj.js → chunk-yj17vs39.js} +9 -9
  69. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-yj17vs39.js.map} +1 -1
  70. package/dist/cli/{chunk-7vtckvaw.js → chunk-yxmk4pec.js} +16 -14
  71. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-yxmk4pec.js.map} +2 -2
  72. package/dist/cli/{chunk-gs7r695n.js → chunk-zccbjcj0.js} +22 -8
  73. package/dist/cli/{chunk-gs7r695n.js.map → chunk-zccbjcj0.js.map} +3 -3
  74. package/dist/cli/{chunk-xh43dwgw.js → chunk-zf748avv.js} +415 -225
  75. package/dist/cli/chunk-zf748avv.js.map +37 -0
  76. package/dist/cli/{chunk-d1v5rhy0.js → chunk-zhvd6adm.js} +16 -14
  77. package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-zhvd6adm.js.map} +2 -2
  78. package/dist/cli/index.js +28 -20
  79. package/dist/cli/index.js.map +5 -5
  80. package/dist/types/ai/agent-surface.d.ts +8 -7
  81. package/dist/types/ai/ai-catalog.d.ts +7 -0
  82. package/dist/types/ai/ask-context.d.ts +22 -0
  83. package/dist/types/ai/ask.d.ts +271 -58
  84. package/dist/types/ai/index.d.ts +2 -2
  85. package/dist/types/ai/link-headers.d.ts +8 -1
  86. package/dist/types/ai/openapi-components.d.ts +5 -2
  87. package/dist/types/ai/relative-links.d.ts +9 -7
  88. package/dist/types/ai/site-skill.d.ts +26 -0
  89. package/dist/types/ai/skills.d.ts +20 -1
  90. package/dist/types/ai/tar.d.ts +1 -3
  91. package/dist/types/analytics/index.d.ts +2 -0
  92. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  93. package/dist/types/analytics/schema.d.ts +14 -0
  94. package/dist/types/astro/integration.d.ts +11 -2
  95. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  96. package/dist/types/captcha/index.d.ts +28 -0
  97. package/dist/types/captcha/schema.d.ts +29 -0
  98. package/dist/types/captcha/turnstile.d.ts +28 -0
  99. package/dist/types/components/content/api-field.d.ts +59 -0
  100. package/dist/types/components/content/api-page.d.ts +95 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/helpers.d.ts +137 -0
  103. package/dist/types/components/openapi/security.d.ts +85 -0
  104. package/dist/types/consent/ethyca.d.ts +48 -0
  105. package/dist/types/consent/index.d.ts +37 -0
  106. package/dist/types/consent/native.d.ts +28 -0
  107. package/dist/types/consent/osano.d.ts +42 -0
  108. package/dist/types/consent/schema.d.ts +110 -0
  109. package/dist/types/core/base-path.d.ts +21 -7
  110. package/dist/types/core/config-input.d.ts +196 -27
  111. package/dist/types/core/data.d.ts +49 -4
  112. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  113. package/dist/types/core/footer.d.ts +8 -0
  114. package/dist/types/core/heading-markers.d.ts +5 -7
  115. package/dist/types/core/i18n-ui.d.ts +86 -0
  116. package/dist/types/core/i18n.d.ts +2 -0
  117. package/dist/types/core/includes.d.ts +8 -2
  118. package/dist/types/core/last-modified.d.ts +10 -0
  119. package/dist/types/core/localizable.d.ts +7 -0
  120. package/dist/types/core/meta.d.ts +8 -0
  121. package/dist/types/core/page-modes.d.ts +29 -0
  122. package/dist/types/core/redirect-patterns.d.ts +144 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +558 -56
  125. package/dist/types/core/sources/lower.d.ts +29 -16
  126. package/dist/types/core/sources/normalize.d.ts +13 -1
  127. package/dist/types/core/sources/read.d.ts +5 -0
  128. package/dist/types/core/sources/watch.d.ts +12 -5
  129. package/dist/types/core/standard-schema.d.ts +5 -0
  130. package/dist/types/core/types.d.ts +1 -1
  131. package/dist/types/core/variables.d.ts +42 -0
  132. package/dist/types/deploy/artifacts.d.ts +6 -4
  133. package/dist/types/deploy/headers.d.ts +5 -0
  134. package/dist/types/deploy/node-headers.d.ts +13 -2
  135. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  136. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  137. package/dist/types/deploy/platforms/types.d.ts +8 -0
  138. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  139. package/dist/types/deploy/redirects.d.ts +61 -22
  140. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  141. package/dist/types/markdown/code-title.d.ts +17 -0
  142. package/dist/types/markdown/directives.d.ts +62 -0
  143. package/dist/types/markdown/features.d.ts +24 -2
  144. package/dist/types/markdown/mdast.d.ts +63 -0
  145. package/dist/types/narration/build.d.ts +70 -0
  146. package/dist/types/narration/provider.d.ts +26 -0
  147. package/dist/types/narration/script.d.ts +154 -0
  148. package/dist/types/openapi/asyncapi.d.ts +4 -2
  149. package/dist/types/openapi/model.d.ts +12 -6
  150. package/dist/types/openapi/overlay.d.ts +54 -0
  151. package/dist/types/openapi/parse.d.ts +13 -0
  152. package/dist/types/openapi/references.d.ts +14 -4
  153. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  154. package/dist/types/ratelimit/index.d.ts +27 -0
  155. package/dist/types/ratelimit/memory.d.ts +36 -0
  156. package/dist/types/ratelimit/schema.d.ts +111 -0
  157. package/dist/types/ratelimit/upstash.d.ts +21 -0
  158. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  159. package/dist/types/reference/asyncapi.d.ts +10 -10
  160. package/dist/types/reference/graphql.d.ts +13 -10
  161. package/dist/types/reference/openapi.d.ts +33 -12
  162. package/dist/types/reference/options.d.ts +45 -5
  163. package/dist/types/reference/scalar.d.ts +13 -0
  164. package/dist/types/reference/schema.d.ts +60 -36
  165. package/dist/types/search/documents.d.ts +12 -1
  166. package/dist/types/search/orama-index.d.ts +4 -0
  167. package/dist/types/seo/metatags.d.ts +20 -0
  168. package/docs/02-deployment.mdx +44 -6
  169. package/docs/03-upgrading.mdx +4 -4
  170. package/docs/04-migrating.mdx +1 -1
  171. package/docs/08-faq.mdx +1 -1
  172. package/docs/advanced/changelog.mdx +2 -2
  173. package/docs/advanced/custom-pages.mdx +3 -3
  174. package/docs/advanced/skills.mdx +14 -0
  175. package/docs/cli/audit.mdx +38 -2
  176. package/docs/cli/evals.mdx +3 -3
  177. package/docs/cli/index.mdx +1 -0
  178. package/docs/cli/translate.mdx +3 -3
  179. package/docs/cli/version.mdx +1 -1
  180. package/docs/configuration/analytics.mdx +22 -1
  181. package/docs/configuration/assistant.mdx +188 -36
  182. package/docs/configuration/consent.mdx +100 -0
  183. package/docs/configuration/customization.mdx +6 -4
  184. package/docs/configuration/index.mdx +63 -5
  185. package/docs/configuration/meta.ts +3 -0
  186. package/docs/configuration/narration.mdx +119 -0
  187. package/docs/configuration/rate-limiting.mdx +69 -0
  188. package/docs/configuration/search.mdx +30 -2
  189. package/docs/configuration/theming.mdx +3 -1
  190. package/docs/content/components.mdx +107 -1
  191. package/docs/content/frontmatter.mdx +77 -1
  192. package/docs/content/i18n.mdx +40 -3
  193. package/docs/content/includes.mdx +16 -0
  194. package/docs/content/index.mdx +4 -2
  195. package/docs/content/islands.mdx +1 -1
  196. package/docs/content/meta.mdx +4 -1
  197. package/docs/content/meta.ts +1 -0
  198. package/docs/content/navigation.mdx +28 -6
  199. package/docs/content/sources/contentful.mdx +32 -0
  200. package/docs/content/sources/custom.mdx +39 -0
  201. package/docs/content/sources/github-releases.mdx +31 -0
  202. package/docs/content/sources/index.mdx +91 -0
  203. package/docs/content/sources/meta.ts +16 -0
  204. package/docs/content/sources/notion.mdx +32 -0
  205. package/docs/content/sources/obsidian.mdx +37 -0
  206. package/docs/content/sources/payload.mdx +30 -0
  207. package/docs/content/sources/remote-mdx.mdx +40 -0
  208. package/docs/content/sources/sanity.mdx +29 -0
  209. package/docs/content/sources/strapi.mdx +31 -0
  210. package/docs/content/syntax.mdx +81 -2
  211. package/docs/content/variables.mdx +41 -0
  212. package/docs/content/versioning.mdx +1 -1
  213. package/docs/discoverability/agent-discovery.mdx +30 -3
  214. package/docs/discoverability/index.mdx +3 -2
  215. package/docs/discoverability/llms-txt.mdx +1 -1
  216. package/docs/discoverability/markdown.mdx +2 -2
  217. package/docs/discoverability/metadata.mdx +17 -0
  218. package/docs/discoverability/open-graph.mdx +5 -3
  219. package/docs/discoverability/rss.mdx +1 -1
  220. package/docs/references/api-pages.mdx +74 -0
  221. package/docs/references/asyncapi.mdx +2 -2
  222. package/docs/references/graphql.mdx +3 -3
  223. package/docs/references/meta.ts +1 -1
  224. package/docs/references/openapi.mdx +106 -5
  225. package/docs/references/scalar.mdx +3 -1
  226. package/package.json +36 -1
  227. package/skills/blume/SKILL.md +3 -0
  228. package/skills/blume-migrate/SKILL.md +5 -5
  229. package/skills/blume-migrate/references/docusaurus.md +3 -3
  230. package/skills/blume-migrate/references/fumadocs.md +1 -1
  231. package/skills/blume-migrate/references/mintlify.md +37 -19
  232. package/skills/blume-migrate/references/nextra.md +3 -3
  233. package/skills/blume-migrate/references/starlight.md +2 -1
  234. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +67 -16
  235. package/skills/blume-write-skill/SKILL.md +82 -0
  236. package/src/ai/agent-readability.ts +4 -3
  237. package/src/ai/agent-surface.ts +9 -7
  238. package/src/ai/ai-catalog.ts +12 -5
  239. package/src/ai/api/handlers.ts +2 -2
  240. package/src/ai/api/spec.ts +2 -2
  241. package/src/ai/api-catalog.ts +6 -2
  242. package/src/ai/ask-context.ts +73 -20
  243. package/src/ai/ask-data.ts +6 -0
  244. package/src/ai/ask-tools.ts +142 -0
  245. package/src/ai/ask.ts +354 -76
  246. package/src/ai/changelog-markdown.ts +2 -2
  247. package/src/ai/component-markdown.ts +75 -0
  248. package/src/ai/cors.ts +2 -0
  249. package/src/ai/index.ts +12 -0
  250. package/src/ai/link-headers.ts +14 -5
  251. package/src/ai/llms.ts +24 -9
  252. package/src/ai/markdown.ts +2 -1
  253. package/src/ai/mcp/data.ts +6 -0
  254. package/src/ai/mcp/discovery.ts +2 -2
  255. package/src/ai/mcp/query.ts +2 -2
  256. package/src/ai/mcp/server.ts +2 -2
  257. package/src/ai/openapi-components.ts +31 -9
  258. package/src/ai/relative-links.ts +42 -26
  259. package/src/ai/serializers.ts +2 -1
  260. package/src/ai/site-skill.ts +274 -0
  261. package/src/ai/skills.ts +49 -3
  262. package/src/ai/tar.ts +139 -12
  263. package/src/ai/write-skill.ts +97 -0
  264. package/src/analytics/head.ts +4 -0
  265. package/src/analytics/index.ts +5 -0
  266. package/src/analytics/one-dollar-stats.ts +86 -0
  267. package/src/analytics/schema.ts +2 -0
  268. package/src/astro/generate.ts +133 -19
  269. package/src/astro/include-hmr.ts +31 -12
  270. package/src/astro/include-refresh.ts +19 -8
  271. package/src/astro/index.ts +1 -0
  272. package/src/astro/integration.ts +143 -14
  273. package/src/astro/module-types.ts +6 -0
  274. package/src/astro/runtime-modules.ts +28 -13
  275. package/src/astro/templates.ts +364 -67
  276. package/src/astro/variables.ts +31 -0
  277. package/src/audit/catalog.ts +19 -0
  278. package/src/audit/checks/contrast.ts +183 -0
  279. package/src/audit/checks/i18n.ts +2 -3
  280. package/src/audit/checks/indexability.ts +2 -3
  281. package/src/audit/checks/links.ts +13 -13
  282. package/src/audit/checks/sitemap.ts +2 -3
  283. package/src/audit/graph.ts +3 -1
  284. package/src/audit/redirects.ts +46 -15
  285. package/src/audit/run.ts +14 -9
  286. package/src/audit/snapshot.ts +5 -0
  287. package/src/audit/types.ts +2 -1
  288. package/src/blume-modules.d.ts +7 -0
  289. package/src/captcha/client.ts +152 -0
  290. package/src/captcha/hcaptcha.ts +36 -0
  291. package/src/captcha/index.ts +28 -0
  292. package/src/captcha/schema.ts +32 -0
  293. package/src/captcha/turnstile.ts +38 -0
  294. package/src/captcha/verify.ts +71 -0
  295. package/src/cli/args.ts +32 -0
  296. package/src/cli/command-meta.ts +5 -0
  297. package/src/cli/commands/audit.ts +4 -1
  298. package/src/cli/commands/doctor.ts +25 -17
  299. package/src/cli/commands/eval.ts +19 -11
  300. package/src/cli/commands/skill.ts +96 -0
  301. package/src/cli/commands/translate.ts +9 -12
  302. package/src/cli/commands/validate.ts +3 -1
  303. package/src/cli/dev-lock.ts +157 -37
  304. package/src/cli/index.ts +5 -0
  305. package/src/cli/report-format.ts +11 -6
  306. package/src/cli/required-secrets.ts +71 -29
  307. package/src/components/BlumePage.astro +2 -0
  308. package/src/components/colors.ts +19 -0
  309. package/src/components/content/ApiEndpoint.astro +18 -0
  310. package/src/components/content/ApiField.astro +79 -0
  311. package/src/components/content/ApiPlayground.astro +62 -0
  312. package/src/components/content/ApiRail.astro +19 -0
  313. package/src/components/content/Badge.astro +5 -3
  314. package/src/components/content/Color.astro +1 -0
  315. package/src/components/content/Component.astro +2 -2
  316. package/src/components/content/FileTree.astro +1 -0
  317. package/src/components/content/GithubInfo.astro +1 -0
  318. package/src/components/content/Panel.astro +1 -0
  319. package/src/components/content/ParamField.astro +25 -0
  320. package/src/components/content/RequestExample.astro +17 -0
  321. package/src/components/content/ResponseExample.astro +17 -0
  322. package/src/components/content/ResponseField.astro +32 -0
  323. package/src/components/content/Step.astro +1 -0
  324. package/src/components/content/Tab.astro +0 -1
  325. package/src/components/content/Tabs.astro +4 -0
  326. package/src/components/content/Tile.astro +1 -0
  327. package/src/components/content/Tooltip.astro +1 -0
  328. package/src/components/content/Tree.astro +1 -0
  329. package/src/components/content/TypeTable.astro +4 -1
  330. package/src/components/content/View.astro +21 -0
  331. package/src/components/content/ViewSwitcher.astro +168 -0
  332. package/src/components/content/api-field.ts +100 -0
  333. package/src/components/content/api-page.ts +224 -0
  334. package/src/components/content/badge-color.ts +5 -2
  335. package/src/components/content/base-href.ts +13 -36
  336. package/src/components/content/view-switcher.ts +71 -0
  337. package/src/components/index.ts +2 -0
  338. package/src/components/islands/Assistant.astro +10 -1
  339. package/src/components/islands/assistant.tsx +125 -18
  340. package/src/components/islands/base-path.ts +47 -10
  341. package/src/components/islands/code-question.ts +62 -0
  342. package/src/components/islands/hooks.ts +145 -40
  343. package/src/components/islands/support-link.ts +68 -0
  344. package/src/components/islands/webmcp.ts +4 -2
  345. package/src/components/layout/Analytics.astro +11 -2
  346. package/src/components/layout/Banner.astro +14 -10
  347. package/src/components/layout/Breadcrumbs.astro +11 -4
  348. package/src/components/layout/ConsentBanner.astro +62 -0
  349. package/src/components/layout/ConsentHead.astro +38 -0
  350. package/src/components/layout/Directory.astro +94 -0
  351. package/src/components/layout/DiscoveryLinks.astro +9 -5
  352. package/src/components/layout/DrawerSwitchers.astro +51 -0
  353. package/src/components/layout/Empty.astro +2 -2
  354. package/src/components/layout/Header.astro +124 -82
  355. package/src/components/layout/LanguageSwitcher.astro +50 -17
  356. package/src/components/layout/NarrationPlayer.astro +1085 -0
  357. package/src/components/layout/NavSelector.astro +35 -9
  358. package/src/components/layout/NavTabMenu.astro +3 -3
  359. package/src/components/layout/NavTree.astro +16 -6
  360. package/src/components/layout/PageActions.astro +4 -3
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +78 -9
  363. package/src/components/layout/Pagination.astro +3 -3
  364. package/src/components/layout/ReferenceLayout.astro +17 -6
  365. package/src/components/layout/RelatedPages.astro +42 -0
  366. package/src/components/layout/RootLayout.astro +313 -50
  367. package/src/components/layout/Search.astro +64 -30
  368. package/src/components/layout/SiteFooter.astro +124 -0
  369. package/src/components/layout/SiteMetatags.astro +16 -0
  370. package/src/components/layout/VersionBanner.astro +2 -2
  371. package/src/components/layout/analytics-client.ts +12 -0
  372. package/src/components/layout/consent/native.ts +79 -0
  373. package/src/components/layout/consent/types.ts +37 -0
  374. package/src/components/layout/footer-links.ts +53 -0
  375. package/src/components/layout/footer-socials.ts +87 -0
  376. package/src/components/layout/locale-redirect.ts +107 -0
  377. package/src/components/layout/nav-utils.ts +83 -0
  378. package/src/components/layout/page-feedback.ts +96 -0
  379. package/src/components/layout/search/analytics.ts +98 -0
  380. package/src/components/layout/search/flexsearch.ts +17 -5
  381. package/src/components/layout/search/orama-cloud.ts +26 -14
  382. package/src/components/layout/search/types.ts +20 -0
  383. package/src/components/layout/search/typesense.ts +5 -1
  384. package/src/components/layout/toc-active.ts +41 -0
  385. package/src/components/layout/toc-element.ts +8 -14
  386. package/src/components/openapi/ApiTagOperations.astro +2 -2
  387. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  388. package/src/components/openapi/GraphqlChip.astro +2 -2
  389. package/src/components/openapi/Operation.astro +163 -13
  390. package/src/components/openapi/Playground.astro +4 -4
  391. package/src/components/openapi/RequestBody.astro +6 -4
  392. package/src/components/openapi/RequestPanel.astro +63 -8
  393. package/src/components/openapi/Responses.astro +6 -4
  394. package/src/components/openapi/SchemaTable.astro +7 -0
  395. package/src/components/openapi/async-snippets.ts +4 -4
  396. package/src/components/openapi/async.ts +38 -6
  397. package/src/components/openapi/code-samples.ts +97 -0
  398. package/src/components/openapi/helpers.ts +19 -9
  399. package/src/components/openapi/message-composer.ts +6 -1
  400. package/src/components/openapi/message.ts +17 -2
  401. package/src/components/openapi/operation-model.ts +81 -11
  402. package/src/components/openapi/panel.ts +4 -2
  403. package/src/components/openapi/param-style.ts +181 -0
  404. package/src/components/openapi/playground-client.ts +73 -16
  405. package/src/components/openapi/request.ts +190 -26
  406. package/src/components/openapi/schema-tree.ts +30 -24
  407. package/src/components/openapi/security.ts +6 -3
  408. package/src/components/openapi/snippets.ts +953 -13
  409. package/src/components/openapi/ws-client.ts +18 -2
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/base-path.ts +62 -16
  421. package/src/core/builtin-tags.ts +5 -0
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/config-input.ts +203 -27
  424. package/src/core/data.ts +44 -4
  425. package/src/core/diagnostics.ts +5 -2
  426. package/src/core/directive-diagnostics.ts +99 -0
  427. package/src/core/footer.ts +26 -0
  428. package/src/core/frontmatter.ts +21 -18
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/heading-markers.ts +5 -18
  431. package/src/core/i18n-ui.ts +82 -2
  432. package/src/core/i18n.ts +31 -3
  433. package/src/core/includes.ts +20 -3
  434. package/src/core/last-modified.ts +25 -3
  435. package/src/core/links.ts +16 -2
  436. package/src/core/locale-links.ts +5 -1
  437. package/src/core/localizable.ts +26 -0
  438. package/src/core/meta.ts +33 -19
  439. package/src/core/navigation.ts +71 -20
  440. package/src/core/page-modes.ts +56 -0
  441. package/src/core/project-graph.ts +138 -10
  442. package/src/core/redirect-patterns.ts +491 -0
  443. package/src/core/related.ts +131 -0
  444. package/src/core/schema.ts +342 -68
  445. package/src/core/sources/contentful-rich-text.ts +27 -20
  446. package/src/core/sources/filesystem.ts +20 -2
  447. package/src/core/sources/github-releases.ts +25 -6
  448. package/src/core/sources/lexical.ts +23 -18
  449. package/src/core/sources/lower.ts +201 -34
  450. package/src/core/sources/mdx-remote.ts +51 -16
  451. package/src/core/sources/normalize.ts +327 -91
  452. package/src/core/sources/notion.ts +52 -25
  453. package/src/core/sources/obsidian.ts +23 -6
  454. package/src/core/sources/portable-text.ts +38 -21
  455. package/src/core/sources/read.ts +8 -3
  456. package/src/core/sources/strapi-blocks.ts +20 -16
  457. package/src/core/sources/watch.ts +20 -7
  458. package/src/core/standard-schema.ts +10 -6
  459. package/src/core/types.ts +1 -1
  460. package/src/core/ui-packs/ar.ts +43 -0
  461. package/src/core/ui-packs/bg.ts +45 -0
  462. package/src/core/ui-packs/bn.ts +43 -0
  463. package/src/core/ui-packs/ca.ts +46 -0
  464. package/src/core/ui-packs/cs.ts +44 -0
  465. package/src/core/ui-packs/da.ts +45 -0
  466. package/src/core/ui-packs/de.ts +46 -0
  467. package/src/core/ui-packs/el.ts +45 -0
  468. package/src/core/ui-packs/es.ts +46 -0
  469. package/src/core/ui-packs/fa.ts +45 -0
  470. package/src/core/ui-packs/fi.ts +45 -0
  471. package/src/core/ui-packs/fr.ts +46 -0
  472. package/src/core/ui-packs/he.ts +43 -0
  473. package/src/core/ui-packs/hi.ts +44 -0
  474. package/src/core/ui-packs/hr.ts +45 -0
  475. package/src/core/ui-packs/hu.ts +44 -0
  476. package/src/core/ui-packs/id.ts +45 -0
  477. package/src/core/ui-packs/it.ts +45 -0
  478. package/src/core/ui-packs/ja.ts +45 -0
  479. package/src/core/ui-packs/ko.ts +44 -0
  480. package/src/core/ui-packs/nl.ts +46 -0
  481. package/src/core/ui-packs/no.ts +44 -0
  482. package/src/core/ui-packs/pl.ts +45 -0
  483. package/src/core/ui-packs/pt-br.ts +46 -0
  484. package/src/core/ui-packs/pt.ts +45 -0
  485. package/src/core/ui-packs/ro.ts +45 -0
  486. package/src/core/ui-packs/ru.ts +45 -0
  487. package/src/core/ui-packs/sk.ts +44 -0
  488. package/src/core/ui-packs/sr.ts +45 -0
  489. package/src/core/ui-packs/sv.ts +44 -0
  490. package/src/core/ui-packs/th.ts +43 -0
  491. package/src/core/ui-packs/tr.ts +45 -0
  492. package/src/core/ui-packs/uk.ts +45 -0
  493. package/src/core/ui-packs/vi.ts +45 -0
  494. package/src/core/ui-packs/zh-tw.ts +43 -0
  495. package/src/core/ui-packs/zh.ts +43 -0
  496. package/src/core/variables.ts +103 -0
  497. package/src/core/version-cut.ts +52 -15
  498. package/src/deploy/artifacts.ts +104 -14
  499. package/src/deploy/cloudflare-negotiation.ts +39 -17
  500. package/src/deploy/headers.ts +10 -5
  501. package/src/deploy/node-headers.ts +64 -10
  502. package/src/deploy/platforms/cloudflare.ts +44 -3
  503. package/src/deploy/platforms/netlify.ts +34 -1
  504. package/src/deploy/platforms/node.ts +1 -0
  505. package/src/deploy/platforms/static.ts +1 -0
  506. package/src/deploy/platforms/types.ts +8 -0
  507. package/src/deploy/platforms/vercel.ts +38 -2
  508. package/src/deploy/redirects.ts +139 -36
  509. package/src/deploy/robots.ts +2 -2
  510. package/src/deploy/rss.ts +4 -3
  511. package/src/deploy/sitemap.ts +7 -5
  512. package/src/deploy/vercel-negotiation.ts +36 -0
  513. package/src/markdown/api-rail.ts +259 -0
  514. package/src/markdown/base-links.ts +34 -36
  515. package/src/markdown/code-title.ts +35 -9
  516. package/src/markdown/directives.ts +242 -36
  517. package/src/markdown/features.ts +20 -2
  518. package/src/markdown/fence-meta.ts +2 -0
  519. package/src/markdown/include.ts +11 -1
  520. package/src/markdown/index.ts +35 -11
  521. package/src/markdown/math.ts +4 -4
  522. package/src/markdown/mdast.ts +5 -2
  523. package/src/markdown/relative-links.ts +12 -4
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +136 -11
  531. package/src/og/derive.ts +41 -31
  532. package/src/og/index.ts +1 -0
  533. package/src/openapi/asyncapi.ts +4 -2
  534. package/src/openapi/model.ts +77 -36
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +55 -2
  537. package/src/openapi/proxy.ts +63 -10
  538. package/src/openapi/references.ts +21 -5
  539. package/src/openapi/render-mdx.ts +46 -3
  540. package/src/openapi/scalar.ts +40 -6
  541. package/src/openapi/source.ts +1 -1
  542. package/src/ratelimit/cloudflare.ts +54 -0
  543. package/src/ratelimit/index.ts +30 -0
  544. package/src/ratelimit/memory.ts +45 -0
  545. package/src/ratelimit/runtime.ts +229 -0
  546. package/src/ratelimit/schema.ts +28 -0
  547. package/src/ratelimit/upstash.ts +30 -0
  548. package/src/ratelimit/wrangler.ts +70 -0
  549. package/src/reference/asyncapi.ts +2 -2
  550. package/src/reference/graphql.ts +5 -2
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +51 -7
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +201 -28
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/version-scope.ts +30 -0
  557. package/src/search/documents.ts +93 -28
  558. package/src/search/orama-index.ts +28 -3
  559. package/src/search/pagefind-ranking.ts +39 -0
  560. package/src/search/popular.ts +2 -1
  561. package/src/search/sync/algolia.ts +15 -4
  562. package/src/search/sync/orama-cloud.ts +3 -0
  563. package/src/search/sync/typesense.ts +5 -0
  564. package/src/seo/jsonld.ts +7 -3
  565. package/src/seo/metatags.ts +67 -0
  566. package/src/theme/chrome-icons.ts +1 -0
  567. package/src/theme/color.ts +500 -0
  568. package/src/theme/entry.ts +56 -3
  569. package/src/theme/palette.ts +121 -45
  570. package/src/translate/meta.ts +68 -24
  571. package/src/translate/prompts.ts +1 -0
  572. package/src/translate/run.ts +3 -3
  573. package/src/translate/validate.ts +12 -3
  574. package/src/translate/work-list.ts +56 -17
  575. package/src/upgrade/upgrade.ts +4 -171
  576. package/dist/cli/chunk-3yce002v.js.map +0 -14
  577. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  578. package/dist/cli/chunk-6dtt0zfn.js.map +0 -10
  579. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  580. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  581. package/dist/cli/chunk-esh98wmb.js +0 -23
  582. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  583. package/dist/cli/chunk-hqp2ajnh.js.map +0 -35
  584. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  585. package/dist/cli/chunk-j85scx15.js.map +0 -10
  586. package/dist/cli/chunk-mqc662a6.js +0 -75
  587. package/dist/cli/chunk-mqc662a6.js.map +0 -11
  588. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  589. package/dist/cli/chunk-qkb5a8sa.js.map +0 -10
  590. package/dist/cli/chunk-v6ya5kcb.js.map +0 -189
  591. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  592. package/dist/cli/chunk-xh43dwgw.js.map +0 -36
  593. package/docs/content/sources.mdx +0 -338
  594. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-1n228xqb.js.map} +0 -0
  595. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-5ffywsmb.js.map} +0 -0
  596. /package/dist/cli/{chunk-g698a744.js.map → chunk-5wj2qwn2.js.map} +0 -0
  597. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-amrb6hgh.js.map} +0 -0
  598. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-gp84za0h.js.map} +0 -0
  599. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-p804f3pe.js.map} +0 -0
  600. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-ta27feev.js.map} +0 -0
  601. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-y4ykbark.js.map} +0 -0
@@ -0,0 +1,82 @@
1
+ import type { MdastNode, MdastValue } from "./mdast.ts";
2
+
3
+ /**
4
+ * A page's views: the `<View title>` blocks it's written in, as Mintlify
5
+ * writes them. Each distinct title is one option of the page's view picker,
6
+ * in the order the page first uses it, and several blocks can share a title.
7
+ * The plugin reads them off the tree and hands the list to the page template
8
+ * through the render's frontmatter, so the picker renders with the page
9
+ * instead of after it (see `ViewSwitcher.astro`).
10
+ */
11
+
12
+ /** The render-frontmatter key the page's views travel on. */
13
+ export const VIEWS_KEY = "blumeViews";
14
+
15
+ /** One option of the view picker. */
16
+ export interface PageView {
17
+ icon?: string;
18
+ title: string;
19
+ }
20
+
21
+ /** The root, as the plugin's `after` hook receives it. */
22
+ interface ViewsRoot {
23
+ children: MdastNode[];
24
+ }
25
+
26
+ /** The slice of Satteri's visitor context the plugin uses. */
27
+ export interface ViewsContext {
28
+ data?: { astro?: { frontmatter?: Record<string, MdastValue> } };
29
+ }
30
+
31
+ const isNode = (value: MdastValue): value is MdastNode =>
32
+ typeof value === "object" && value !== null && !Array.isArray(value);
33
+
34
+ const isText = (value: MdastValue): value is string =>
35
+ typeof value === "string";
36
+
37
+ /** A JSX element's literal string attribute, when it has one. */
38
+ const stringAttribute = (node: MdastNode, name: string): string | undefined => {
39
+ const attributes = Array.isArray(node.attributes) ? node.attributes : [];
40
+ const value = attributes.find(
41
+ (attribute) => isNode(attribute) && attribute.name === name
42
+ );
43
+ return isNode(value) && isText(value.value) && value.value.trim() !== ""
44
+ ? value.value.trim()
45
+ : undefined;
46
+ };
47
+
48
+ /** Every `<View>` element under `nodes`, in document order. */
49
+ const viewElements = (nodes: readonly MdastValue[]): MdastNode[] =>
50
+ nodes
51
+ .filter(isNode)
52
+ .flatMap((node) => [
53
+ ...(node.type === "mdxJsxFlowElement" && node.name === "View"
54
+ ? [node]
55
+ : []),
56
+ ...viewElements(Array.isArray(node.children) ? node.children : []),
57
+ ]);
58
+
59
+ /** The page's views, one per distinct title, in first-use order. */
60
+ export const pageViews = (nodes: readonly MdastValue[]): PageView[] => {
61
+ const views = new Map<string, PageView>();
62
+ for (const element of viewElements(nodes)) {
63
+ const title = stringAttribute(element, "title");
64
+ if (title !== undefined && !views.has(title)) {
65
+ const icon = stringAttribute(element, "icon");
66
+ views.set(title, icon === undefined ? { title } : { icon, title });
67
+ }
68
+ }
69
+ return [...views.values()];
70
+ };
71
+
72
+ export const viewsPlugin = () => ({
73
+ after(root: ViewsRoot, ctx: ViewsContext) {
74
+ const frontmatter = ctx.data?.astro?.frontmatter;
75
+ const views = pageViews(root.children);
76
+ // A page with one view has nothing to switch between.
77
+ if (frontmatter && views.length > 1) {
78
+ frontmatter[VIEWS_KEY] = views.map((view) => ({ ...view }));
79
+ }
80
+ },
81
+ name: "blume-views",
82
+ });
@@ -0,0 +1,412 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import {
4
+ copyFile,
5
+ mkdir,
6
+ readdir,
7
+ readFile,
8
+ rename,
9
+ rm,
10
+ writeFile,
11
+ } from "node:fs/promises";
12
+
13
+ import { createGateway, generateSpeech } from "ai";
14
+ import { HTMLElement, parse, TextNode } from "node-html-parser";
15
+ import type { Node } from "node-html-parser";
16
+ import { dirname, join } from "pathe";
17
+
18
+ import { normalizeBasePath } from "../core/base-path.ts";
19
+ import type { BlumeProject } from "../core/project-graph.ts";
20
+ import type { ProjectContext } from "../core/types.ts";
21
+ import type { NarrationProvider } from "./provider.ts";
22
+ import {
23
+ extractNarration,
24
+ isNarratable,
25
+ isNarrationCues,
26
+ segmentNarration,
27
+ } from "./script.ts";
28
+ import type {
29
+ NarrationCues,
30
+ NarrationManifest,
31
+ NarrationSegment,
32
+ NarrationTree,
33
+ } from "./script.ts";
34
+
35
+ /**
36
+ * Generated narration: after Astro writes the HTML, read every page that
37
+ * carries the player exactly as the browser will (same walker, same blocks),
38
+ * split it into sentences, and give each sentence a clip from the configured
39
+ * speech model. Clips are cached by what decides their sound (text, language,
40
+ * model, voice, instructions, provider options), so a rebuild only pays for
41
+ * sentences that changed, and one sentence shared by many pages is generated
42
+ * once. Each page gets a manifest listing its clips; the player plays them in
43
+ * order and falls back to browser voices wherever a manifest is missing.
44
+ */
45
+
46
+ /** How the build logs: the integration's Astro logger. */
47
+ export interface NarrationLogger {
48
+ info: (message: string) => void;
49
+ warn: (message: string) => void;
50
+ }
51
+
52
+ /** Speak `text` in `lang`, returning MP3 bytes. */
53
+ export type Speak = (text: string, lang: string) => Promise<Uint8Array>;
54
+
55
+ /** What a narration build did, for the log line and tests. */
56
+ export interface NarrationBuildResult {
57
+ /** Clips that failed to generate; their pages get no manifest. */
58
+ failed: number;
59
+ /** Clips generated by the speech model on this build. */
60
+ generated: number;
61
+ /** Pages that got a manifest. */
62
+ pages: number;
63
+ /** Clips read back from the cache. */
64
+ reused: number;
65
+ }
66
+
67
+ // Bump to regenerate every clip: part of every cache key.
68
+ const CLIP_FORMAT = 1;
69
+
70
+ // Parallel speech requests. Low enough to stay inside provider rate limits on
71
+ // a first build of a large site, where every sentence is new.
72
+ const CONCURRENCY = 4;
73
+
74
+ /**
75
+ * The clip cache directory: `node_modules/.cache/blume/narration`, next to the
76
+ * OG card cache and restored by the same host build caches (see `ogCacheDir`).
77
+ */
78
+ export const narrationCacheDir = (
79
+ context: Pick<ProjectContext, "outDir" | "root">
80
+ ): string =>
81
+ existsSync(join(context.root, "node_modules"))
82
+ ? join(context.root, "node_modules", ".cache", "blume", "narration")
83
+ : join(context.outDir, ".cache", "narration");
84
+
85
+ /** The walker's view of a node-html-parser tree. */
86
+ export const htmlTree: NarrationTree<Node> = {
87
+ attr: (node, name) =>
88
+ node instanceof HTMLElement ? (node.getAttribute(name) ?? null) : null,
89
+ children: (node) => node.childNodes,
90
+ tag: (node) =>
91
+ node instanceof HTMLElement && node.tagName
92
+ ? node.tagName.toLowerCase()
93
+ : null,
94
+ text: (node) => (node instanceof TextNode ? node.text : null),
95
+ };
96
+
97
+ /** The player's settings on a built page, and the article it reads. */
98
+ interface PlayerPage {
99
+ article: HTMLElement;
100
+ audioBase: string;
101
+ cues: NarrationCues;
102
+ lang: string;
103
+ manifest: string;
104
+ }
105
+
106
+ const parseCues = (json: string | undefined): NarrationCues | null => {
107
+ try {
108
+ const value: unknown = JSON.parse(json ?? "");
109
+ return isNarrationCues(value) ? value : null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ };
114
+
115
+ /**
116
+ * The generated player on a built page, or `null` when the page has none (or
117
+ * a player without generated audio, which reads with browser voices).
118
+ */
119
+ export const readPlayerPage = (html: string): PlayerPage | null => {
120
+ const root = parse(html);
121
+ const player = root.querySelector("[data-blume-narration-player]");
122
+ // node-html-parser has no `dataset`; `attributes` is its decoded map.
123
+ const data = player?.attributes ?? {};
124
+ const manifest = data["data-manifest"];
125
+ const audioBase = data["data-audio-base"];
126
+ const article = player?.parentNode;
127
+ const cues = parseCues(data["data-cues"]);
128
+ if (
129
+ !(player && manifest && audioBase && cues) ||
130
+ !(article instanceof HTMLElement)
131
+ ) {
132
+ return null;
133
+ }
134
+ return {
135
+ article,
136
+ audioBase,
137
+ cues,
138
+ lang: data["data-lang"] ?? "en",
139
+ manifest,
140
+ };
141
+ };
142
+
143
+ /** The language code a speech model takes (`pt-BR` -> `pt`). */
144
+ const speechLanguage = (lang: string): string =>
145
+ lang.toLowerCase().replaceAll("_", "-").split("-")[0] ?? lang;
146
+
147
+ /**
148
+ * A clip's cache key: a digest of everything that decides how it sounds. The
149
+ * Blume version is deliberately left out, unlike the OG card's: an upgrade
150
+ * shouldn't pay to regenerate audio that would sound the same.
151
+ */
152
+ export const clipKey = (
153
+ provider: NarrationProvider,
154
+ lang: string,
155
+ text: string
156
+ ): string => {
157
+ const { instructions, model, providerOptions, voice } = provider.options;
158
+ return createHash("sha256")
159
+ .update(
160
+ JSON.stringify({
161
+ format: CLIP_FORMAT,
162
+ instructions,
163
+ lang: speechLanguage(lang),
164
+ model,
165
+ providerOptions,
166
+ text,
167
+ voice,
168
+ })
169
+ )
170
+ .digest("hex")
171
+ .slice(0, 32);
172
+ };
173
+
174
+ /** Speak through the Vercel AI Gateway with the provider's model and voice. */
175
+ export const gatewaySpeaker = (provider: NarrationProvider): Speak => {
176
+ const { apiKeyEnv, headers, instructions, model, providerOptions, voice } =
177
+ provider.options;
178
+ const gateway = createGateway({ apiKey: process.env[apiKeyEnv], headers });
179
+ return async (text, lang) => {
180
+ const { audio } = await generateSpeech({
181
+ instructions,
182
+ language: speechLanguage(lang),
183
+ model: gateway.speechModel(model),
184
+ outputFormat: "mp3",
185
+ providerOptions,
186
+ text,
187
+ voice,
188
+ });
189
+ return audio.uint8Array;
190
+ };
191
+ };
192
+
193
+ /** Every `.html` file under `dir`. */
194
+ const listHtml = async (dir: string): Promise<string[]> => {
195
+ const entries = await readdir(dir, { recursive: true, withFileTypes: true });
196
+ return entries
197
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".html"))
198
+ .map((entry) => join(entry.parentPath, entry.name))
199
+ .toSorted();
200
+ };
201
+
202
+ /** Run `task` over `items`, at most `limit` at a time. */
203
+ const eachLimited = async <T>(
204
+ items: readonly T[],
205
+ limit: number,
206
+ task: (item: T) => Promise<void>
207
+ ): Promise<void> => {
208
+ let next = 0;
209
+ const worker = async (): Promise<void> => {
210
+ while (next < items.length) {
211
+ const item = items[next];
212
+ next += 1;
213
+ if (item !== undefined) {
214
+ // Workers pull one item at a time on purpose: that is the limit.
215
+ // oxlint-disable-next-line no-await-in-loop
216
+ await task(item);
217
+ }
218
+ }
219
+ };
220
+ await Promise.all(
221
+ Array.from({ length: Math.min(limit, items.length) }, worker)
222
+ );
223
+ };
224
+
225
+ // Written to a sibling temp file and renamed into place, so a crashed build
226
+ // never leaves a truncated clip for the next one to reuse.
227
+ const storeClip = async (file: string, bytes: Uint8Array): Promise<void> => {
228
+ await mkdir(dirname(file), { recursive: true });
229
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
230
+ await writeFile(tmp, bytes);
231
+ await rename(tmp, file);
232
+ };
233
+
234
+ /** Remove cached clips this build didn't use, and stray temp files. */
235
+ const pruneClips = async (dir: string, used: Set<string>): Promise<void> => {
236
+ let entries: string[];
237
+ try {
238
+ entries = await readdir(dir);
239
+ } catch {
240
+ return;
241
+ }
242
+ const stale = entries.filter(
243
+ (name) =>
244
+ name.endsWith(".tmp") ||
245
+ (name.endsWith(".mp3") && !used.has(name.slice(0, -".mp3".length)))
246
+ );
247
+ await Promise.all(stale.map((name) => rm(join(dir, name), { force: true })));
248
+ };
249
+
250
+ /** A site URL path, minus the deployment base, as a path inside `distDir`. */
251
+ const distPath = (distDir: string, base: string, url: string): string => {
252
+ const path = decodeURI(url);
253
+ const local =
254
+ base && path.startsWith(`${base}/`) ? path.slice(base.length) : path;
255
+ return join(distDir, local);
256
+ };
257
+
258
+ /** One page's plan: where its manifest goes and what it says. */
259
+ interface PagePlan {
260
+ blocks: string[];
261
+ keys: string[];
262
+ manifestFile: string;
263
+ segments: NarrationSegment[];
264
+ }
265
+
266
+ const manifestFor = (plan: PagePlan): NarrationManifest => ({
267
+ blocks: plan.blocks,
268
+ segments: plan.segments.map((segment, index) => {
269
+ const audio = `${plan.keys[index] ?? ""}.mp3`;
270
+ return segment.block === null
271
+ ? { audio, text: segment.text }
272
+ : { audio, block: segment.block, end: segment.end, start: segment.start };
273
+ }),
274
+ version: 1,
275
+ });
276
+
277
+ /**
278
+ * Generate the narration for a built site in `distDir`: a clip per sentence
279
+ * and a manifest per page. Does nothing unless `narration.provider` is set,
280
+ * and warns instead of generating when the provider's key is missing (the
281
+ * pages then read with browser voices). `speak` replaces the gateway call.
282
+ */
283
+ export const buildNarration = async (
284
+ project: BlumeProject,
285
+ distDir: string,
286
+ logger: NarrationLogger,
287
+ speak?: Speak
288
+ ): Promise<NarrationBuildResult | null> => {
289
+ const { narration } = project.config;
290
+ const { provider } = narration;
291
+ if (!(narration.enabled && provider)) {
292
+ return null;
293
+ }
294
+ const env = provider.options.apiKeyEnv;
295
+ if (!(speak || process.env[env] || process.env.VERCEL_OIDC_TOKEN)) {
296
+ logger.warn(
297
+ `Narration audio was not generated: ${env} is not set, so pages will be read with browser voices.`
298
+ );
299
+ return null;
300
+ }
301
+ const say = speak ?? gatewaySpeaker(provider);
302
+ const base = normalizeBasePath(project.config.deployment.options.base);
303
+
304
+ // Read every page first, so the log can say what generation will cost
305
+ // before any of it is spent.
306
+ const plans: PagePlan[] = [];
307
+ const texts = new Map<string, { lang: string; text: string }>();
308
+ let audioDir: string | null = null;
309
+ for (const file of await listHtml(distDir)) {
310
+ // oxlint-disable-next-line no-await-in-loop -- one page in memory at a time
311
+ const html = await readFile(file, "utf-8");
312
+ const page = html.includes("data-blume-narration-player")
313
+ ? readPlayerPage(html)
314
+ : null;
315
+ if (!page) {
316
+ continue;
317
+ }
318
+ const extract = extractNarration(page.article, htmlTree, page.cues);
319
+ if (!isNarratable(extract)) {
320
+ continue;
321
+ }
322
+ audioDir ??= distPath(distDir, base, page.audioBase);
323
+ const segments = segmentNarration(extract, page.lang);
324
+ const keys = segments.map((segment) => {
325
+ const key = clipKey(provider, page.lang, segment.text);
326
+ texts.set(key, { lang: page.lang, text: segment.text });
327
+ return key;
328
+ });
329
+ plans.push({
330
+ blocks: extract.blocks.map((block) => block.text),
331
+ keys,
332
+ manifestFile: distPath(distDir, base, page.manifest),
333
+ segments,
334
+ });
335
+ }
336
+ if (!audioDir || plans.length === 0) {
337
+ return { failed: 0, generated: 0, pages: 0, reused: 0 };
338
+ }
339
+
340
+ const cacheDir = narrationCacheDir(project.context);
341
+ const cached = (key: string) => join(cacheDir, `${key}.mp3`);
342
+ const missing = [...texts.keys()].filter((key) => !existsSync(cached(key)));
343
+ const reused = texts.size - missing.length;
344
+ if (missing.length > 0) {
345
+ const chars = missing.reduce(
346
+ (total, key) => total + (texts.get(key)?.text.length ?? 0),
347
+ 0
348
+ );
349
+ logger.info(
350
+ `Generating narration: ${missing.length} new clip(s), ${chars.toLocaleString("en-US")} characters, with ${provider.options.model}`
351
+ );
352
+ }
353
+
354
+ // The AI SDK already retries a transient failure, so one that gets through
355
+ // (a bad key, an unknown model, a spent quota) would fail every clip after
356
+ // it too: stop generating at the first, and keep what's already cached.
357
+ const failures = new Set<string>();
358
+ let firstError: string | null = null;
359
+ await eachLimited(missing, CONCURRENCY, async (key) => {
360
+ const entry = texts.get(key);
361
+ if (!entry || firstError !== null) {
362
+ failures.add(key);
363
+ return;
364
+ }
365
+ try {
366
+ await storeClip(cached(key), await say(entry.text, entry.lang));
367
+ } catch (error) {
368
+ failures.add(key);
369
+ firstError ??= error instanceof Error ? error.message : String(error);
370
+ }
371
+ });
372
+ if (firstError !== null) {
373
+ logger.warn(
374
+ `Narration stopped generating after a clip failed (${firstError}). Pages missing clips will be read with browser voices.`
375
+ );
376
+ }
377
+
378
+ const target = audioDir;
379
+ await mkdir(target, { recursive: true });
380
+ const shipped = new Set<string>();
381
+ let pages = 0;
382
+ for (const plan of plans) {
383
+ if (plan.keys.some((key) => failures.has(key))) {
384
+ continue;
385
+ }
386
+ for (const key of plan.keys) {
387
+ if (!shipped.has(key)) {
388
+ shipped.add(key);
389
+ // oxlint-disable-next-line no-await-in-loop -- bounded by unique clips
390
+ await copyFile(cached(key), join(target, `${key}.mp3`));
391
+ }
392
+ }
393
+ // oxlint-disable-next-line no-await-in-loop -- one manifest per page
394
+ await mkdir(dirname(plan.manifestFile), { recursive: true });
395
+ // oxlint-disable-next-line no-await-in-loop
396
+ await writeFile(
397
+ plan.manifestFile,
398
+ `${JSON.stringify(manifestFor(plan))}\n`,
399
+ "utf-8"
400
+ );
401
+ pages += 1;
402
+ }
403
+
404
+ // Only clips for text no longer on the site go; the current site's stay,
405
+ // whether or not this run generated all of them.
406
+ await pruneClips(cacheDir, new Set(texts.keys()));
407
+ const generated = missing.length - failures.size;
408
+ logger.info(
409
+ `Narration: ${pages} page(s), ${generated} clip(s) generated, ${reused} reused from the cache`
410
+ );
411
+ return { failed: failures.size, generated, pages, reused };
412
+ };