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
@@ -2,7 +2,11 @@ import type { AstroIntegration } from "astro";
2
2
  import type { z } from "zod";
3
3
 
4
4
  import type { AskRetrievalOptions } from "../ai/ask-context.ts";
5
- import type { AssistantAdapter, AssistantGatewayAdapter } from "../ai/ask.ts";
5
+ import type {
6
+ AssistantAdapter,
7
+ AssistantGatewayAdapter,
8
+ AssistantOpenAIAdapter,
9
+ } from "../ai/ask.ts";
6
10
  import type { ComponentMarkdown } from "../ai/component-markdown.ts";
7
11
  import type { AnalyticsAdapter } from "../analytics/schema.ts";
8
12
  import type { CaptchaAdapter } from "../captcha/schema.ts";
@@ -155,6 +159,19 @@ export interface FooterConfig {
155
159
  socials?: Partial<Record<FooterSocial, string>>;
156
160
  }
157
161
 
162
+ /**
163
+ * The generated `/changelog` index's heading and description. Each is a
164
+ * string, or a map of locale code to text; the index renders in the default
165
+ * locale. Unset, they're the `changelog` UI strings (`Changelog` in English),
166
+ * which `i18n.ui` translates.
167
+ */
168
+ export interface ChangelogConfig {
169
+ /** The index's description, below its heading and in its meta description. */
170
+ description?: LocalizableLabel;
171
+ /** The index's heading, page title, and OG card title. */
172
+ title?: LocalizableLabel;
173
+ }
174
+
158
175
  /**
159
176
  * Where content lives and how it's discovered. `root`/`include`/`exclude` are
160
177
  * zero-config shorthand for a single `filesystem()` source; set `sources` to
@@ -506,6 +523,16 @@ export type SearchProviderConfig = AnySearchAdapter | false;
506
523
 
507
524
  /** The object form of `search`: the adapter plus adapter-independent settings. */
508
525
  export interface SearchOptions {
526
+ /** What search reports to the configured analytics providers. */
527
+ analytics?: {
528
+ /**
529
+ * Send each query's text with the `search` and `search_select` events.
530
+ * Defaults to `true`. `false` sends the query's length (`queryChars`)
531
+ * instead, and the text rides only the `blume:track` DOM event, for a
532
+ * site to forward on its own terms.
533
+ */
534
+ queries?: boolean;
535
+ };
509
536
  /** Indexing behavior. */
510
537
  indexing?: {
511
538
  /**
@@ -529,7 +556,7 @@ export interface SearchOptions {
529
556
  /**
530
557
  * Search configuration. Pass an adapter directly (`search: algolia({…})`, or
531
558
  * `false`) as shorthand for the object form, which also carries `popular`
532
- * links and `indexing` settings.
559
+ * links and `indexing` and `analytics` settings.
533
560
  */
534
561
  export type SearchConfig = SearchProviderConfig | SearchOptions;
535
562
 
@@ -1308,8 +1335,8 @@ export interface MarkdownConfig {
1308
1335
  export interface ReactConfig {
1309
1336
  /**
1310
1337
  * Auto-memoize React components/hooks with the React Compiler
1311
- * (`babel-plugin-react-compiler`). On by default whenever React is enabled;
1312
- * set to `false` to skip the compiler's babel pass. Defaults to `true`.
1338
+ * (`oxc-transform-react`). On by default whenever React is enabled;
1339
+ * set to `false` to skip the compiler pass. Defaults to `true`.
1313
1340
  */
1314
1341
  compiler?: boolean;
1315
1342
  }
@@ -1376,9 +1403,11 @@ export type NarrationConfig =
1376
1403
  /**
1377
1404
  * Generate audio at build with a speech model:
1378
1405
  * `gateway({ model: "openai/tts-1-hd", voice: "alloy" })` from
1379
- * `blume/ai`. Without it, pages are read with browser voices.
1406
+ * `blume/ai`, or `openai({ model, voice })` for OpenAI or, with a
1407
+ * `baseUrl`, any OpenAI-compatible speech endpoint. Without it, pages
1408
+ * are read with browser voices.
1380
1409
  */
1381
- provider?: AssistantGatewayAdapter;
1410
+ provider?: AssistantGatewayAdapter | AssistantOpenAIAdapter;
1382
1411
  };
1383
1412
 
1384
1413
  /**
@@ -1495,6 +1524,11 @@ export interface BlumeConfig {
1495
1524
  * compose: with both set, a page lands at `{deployment.base}/{basePath}/page`.
1496
1525
  */
1497
1526
  basePath?: string;
1527
+ /**
1528
+ * The generated `/changelog` index's title and description, each a string
1529
+ * or a per-locale map. Unset, they're the `changelog` UI strings.
1530
+ */
1531
+ changelog?: ChangelogConfig;
1498
1532
  /**
1499
1533
  * Ask readers before analytics runs, with an adapter from `blume/consent`:
1500
1534
  * `native()` for Blume's own banner, or a hosted consent manager
@@ -1557,6 +1591,8 @@ export interface BlumeConfig {
1557
1591
  narration?: NarrationConfig;
1558
1592
  /** Header, sidebar, tabs, and switchers. */
1559
1593
  navigation?: NavigationConfig;
1594
+ /** Send `X-Powered-By: Blume` with responses. Defaults to `true`. */
1595
+ poweredBy?: boolean;
1560
1596
  /**
1561
1597
  * Limit how often one reader (by IP address) can call the server routes —
1562
1598
  * the assistant, the API playground proxy, server-side search — with an
package/src/core/data.ts CHANGED
@@ -248,6 +248,8 @@ export interface BlumeDataConfig {
248
248
  /** Repository URL for header/edit links, or `null`. */
249
249
  repoUrl: string | null;
250
250
  search: {
251
+ /** `search.analytics`: whether the dialog's events carry each query's text. */
252
+ analytics: { queries: boolean };
251
253
  enabled: boolean;
252
254
  /** Resolved empty-state links; empty when unset (Search falls back to sidebar). */
253
255
  popular: { icon?: string; label: string; route: string }[];
@@ -45,6 +45,7 @@ const DOCS_PATHS = new Map(
45
45
  BLUME_DEPENDENCY_MISSING: "/docs/cli/doctor",
46
46
  BLUME_DUPLICATE_ROUTE: DOCS_CONTENT_NAVIGATION,
47
47
  BLUME_DUPLICATE_SIDEBAR_ORDER: DOCS_CONTENT_NAVIGATION,
48
+ BLUME_EXAMPLE_NOT_FOUND: "/docs/content/components#component",
48
49
  BLUME_FRONTMATTER_INVALID: "/docs/content/frontmatter",
49
50
  BLUME_INCLUDE_CYCLE: DOCS_CONTENT_INCLUDES,
50
51
  BLUME_INCLUDE_MALFORMED: DOCS_CONTENT_INCLUDES,
@@ -60,13 +61,17 @@ const DOCS_PATHS = new Map(
60
61
  BLUME_NAV_INDEX_TITLE_MISMATCH: DOCS_CONTENT_NAVIGATION,
61
62
  BLUME_NAV_MISSING_PAGE: DOCS_CONTENT_NAVIGATION,
62
63
  BLUME_NODE_VERSION: "/docs/quickstart",
64
+ BLUME_SEARCH_SYNC_FAILED: "/docs/configuration/search",
63
65
  BLUME_SERVER_FEATURE_REQUIRED: DOCS_DEPLOYMENT,
64
66
  BLUME_SIDEBAR_DISPLAY_IGNORED: DOCS_CONTENT_NAVIGATION,
65
67
  BLUME_SOURCE_FETCH_FAILED: DOCS_CONTENT_SOURCES,
66
68
  BLUME_SOURCE_MISCONFIGURED: DOCS_CONTENT_SOURCES,
67
69
  BLUME_SOURCE_OFFLINE: DOCS_CONTENT_SOURCES,
70
+ BLUME_SOURCE_PATH_MISSING: "/docs/content/sources/remote-mdx",
68
71
  BLUME_SOURCE_SDK_MISSING: DOCS_CONTENT_SOURCES,
69
72
  BLUME_SOURCE_UNAVAILABLE: DOCS_CONTENT_SOURCES,
73
+ BLUME_SOURCE_UNRESOLVED_LINK: "/docs/content/sources/contentful",
74
+ BLUME_SOURCE_UNSUPPORTED_NODE: "/docs/content/sources/payload",
70
75
  BLUME_TRANSLATE_MISSING: "/docs/cli/translate",
71
76
  BLUME_TRANSLATE_STALE: "/docs/cli/translate",
72
77
  BLUME_UNKNOWN_COMPONENT: "/docs/configuration/customization",
@@ -90,6 +90,8 @@ const uiStringsObject = z.object({
90
90
  .default(
91
91
  "Product updates, new features, and fixes from every release."
92
92
  ),
93
+ // What the index says when it has no entries to list.
94
+ empty: z.string().default("No changelog entries yet."),
93
95
  title: z.string().default("Changelog"),
94
96
  })
95
97
  .prefault({}),
@@ -242,7 +242,7 @@ export const malformedIncludeDiagnostic = (
242
242
  "This <include> isn't a statement Blume can read, so the page shows it as written.",
243
243
  severity: "warning",
244
244
  suggestion:
245
- "Write <include>./path.mdx</include> on a line of its own, or wrap it with the path on its own line and no blank lines; only lang and meta attributes are read.",
245
+ 'Write <include>./path.mdx</include> on a line of its own, or wrap it with the path on its own line and no blank lines. Attributes take lowercase names and quoted values (plan="Pro"): lang and meta shape a code include, and any other is a prop the included file reads as {{name}}.',
246
246
  });
247
247
 
248
248
  /**
package/src/core/links.ts CHANGED
@@ -312,10 +312,13 @@ const checkPathLink = (
312
312
  ): LinkResult => {
313
313
  // Page routes carry the site-wide base; an absolute author path is written
314
314
  // as if mounted at root, so base it for the route lookup (idempotent — a
315
- // relative link already resolved against the based `page.route`). A real
316
- // route always wins over the asset-extension heuristic, so a dotted route
317
- // (e.g. `/releases/v1.0`) isn't misread as a missing asset.
318
- const authoredRoute = toRoute(withBasePath(ctx.basePath, resolved));
315
+ // relative link already resolved against the based `page.route`). A raw
316
+ // `<a href>` ships unbased, so it's looked up as written. A real route
317
+ // always wins over the asset-extension heuristic, so a dotted route (e.g.
318
+ // `/releases/v1.0`) isn't misread as a missing asset.
319
+ const authoredRoute = toRoute(
320
+ link.raw ? resolved : withBasePath(ctx.basePath, resolved)
321
+ );
319
322
  const route = servedRoute(authoredRoute, page, ctx);
320
323
  if (ctx.routes.has(route)) {
321
324
  return fragment ? checkAnchor(route, fragment, site, ctx, via) : null;
@@ -436,6 +439,12 @@ export interface FileRouteIndex {
436
439
  * into the reader's locale onto the fallback copy.
437
440
  */
438
441
  byDefaultNavPath: Map<string, string>;
442
+ /**
443
+ * A staged page's entry id (`<source>/<ref>`) → its route. A remote
444
+ * source's files have no path on disk, so a file link between two of them
445
+ * (`./02-errors.mdx`) resolves by entry id, as the rendered link does.
446
+ */
447
+ byEntryId: Map<string, string>;
439
448
  }
440
449
 
441
450
  /** The linking side of a file link: where the page's source lives. */
@@ -450,12 +459,19 @@ export const buildFileRouteIndex = (
450
459
  ): FileRouteIndex => {
451
460
  const bySource = new Map<string, string>();
452
461
  const byDefaultNavPath = new Map<string, string>();
462
+ const byEntryId = new Map<string, string>();
453
463
  for (const page of pages) {
454
- const { sourcePath } = page;
455
- if (!sourcePath || page.fallback) {
464
+ const { entryId, sourcePath } = page;
465
+ if (page.fallback) {
456
466
  continue;
457
467
  }
458
468
  const isDefault = page.locale === i18n?.defaultLocale;
469
+ if (entryId && (!byEntryId.has(entryId) || isDefault)) {
470
+ byEntryId.set(entryId, page.route);
471
+ }
472
+ if (!sourcePath) {
473
+ continue;
474
+ }
459
475
  if (!bySource.has(sourcePath) || isDefault) {
460
476
  bySource.set(sourcePath, page.route);
461
477
  }
@@ -463,7 +479,7 @@ export const buildFileRouteIndex = (
463
479
  byDefaultNavPath.set(normalize(page.navPath), page.route);
464
480
  }
465
481
  }
466
- return { byDefaultNavPath, bySource };
482
+ return { byDefaultNavPath, byEntryId, bySource };
467
483
  };
468
484
 
469
485
  /**
@@ -492,11 +508,15 @@ const relativeTarget = (
492
508
  ctx: LinkContext
493
509
  ): string => {
494
510
  const base = { isIndex: isIndexPage(page), route: page.route };
495
- const { navPath, sourcePath } = page;
496
- const resolveFile = sourcePath
497
- ? (path: string) =>
498
- routeOfLinkedFile(ctx.fileRoutes, { navPath, sourcePath }, path)
499
- : undefined;
511
+ const { entryId, navPath, sourcePath } = page;
512
+ let resolveFile: ((path: string) => string | undefined) | undefined;
513
+ if (sourcePath) {
514
+ resolveFile = (path) =>
515
+ routeOfLinkedFile(ctx.fileRoutes, { navPath, sourcePath }, path);
516
+ } else if (entryId) {
517
+ resolveFile = (path) =>
518
+ ctx.fileRoutes.byEntryId.get(normalize(join(dirname(entryId), path)));
519
+ }
500
520
  return (
501
521
  resolveRelativeHref(rawPath, base, resolveFile, (route) =>
502
522
  ctx.routes.has(route)
@@ -555,9 +575,14 @@ const classifyLink = (
555
575
  return fragment ? checkAnchor(page.route, fragment, site, ctx, via) : null;
556
576
  }
557
577
 
558
- const resolved = rawPath.startsWith("/")
559
- ? rawPath
560
- : relativeTarget(page, rawPath, ctx);
578
+ let resolved = rawPath;
579
+ if (!rawPath.startsWith("/")) {
580
+ // A raw `<a href>` isn't rewritten, so the browser resolves it against
581
+ // the page's slashless URL: its parent directory, even on an index page.
582
+ resolved = link.raw
583
+ ? resolveRelative(page.route, rawPath, false)
584
+ : relativeTarget(page, rawPath, ctx);
585
+ }
561
586
  return checkPathLink(resolved, fragment, page, link, site, ctx, via);
562
587
  };
563
588
 
@@ -582,6 +607,8 @@ export const validateLinks = async (
582
607
  i18n?: LocaleRouting | null;
583
608
  publicDir: string | null;
584
609
  checkExternal?: boolean;
610
+ /** External URLs not to request (`--ignore`). Internal links are always checked. */
611
+ ignore?: (url: string) => boolean;
585
612
  /** Configured redirects; their `from` paths count as valid link targets. */
586
613
  redirects?: { from: string }[];
587
614
  }
@@ -629,8 +656,11 @@ export const validateLinks = async (
629
656
  });
630
657
  }
631
658
 
632
- if (options.checkExternal && external.length > 0) {
633
- diagnostics.push(...(await checkExternalLinks(external)));
659
+ // `--ignore` names URLs that can't be checked from where this runs: a
660
+ // placeholder host, a local server, a site that turns bots away.
661
+ const probed = external.filter((ref) => !options.ignore?.(ref.url));
662
+ if (options.checkExternal && probed.length > 0) {
663
+ diagnostics.push(...(await checkExternalLinks(probed)));
634
664
  }
635
665
 
636
666
  // A partial spliced into several pages (or every locale of one) yields the
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The header that names Blume as the framework behind a response, sent unless
3
+ * `poweredBy` is `false`. The dev server and Blume's runtime middleware
4
+ * (`components/powered-by-middleware.ts`) send it, and so does every host
5
+ * for the built site: static hosts through `deploy/headers.ts`'s rules,
6
+ * server builds through each platform's own mechanism (`deploy/platforms`).
7
+ * It lives apart from those modules so the runtime middleware can import it
8
+ * without pulling in the deploy code.
9
+ */
10
+ export const POWERED_BY_HEADERS = {
11
+ "X-Powered-By": "Blume",
12
+ } satisfies Record<string, string>;
package/src/core/probe.ts CHANGED
@@ -16,6 +16,8 @@ export interface ProbeResult {
16
16
  status?: number;
17
17
  timedOut?: boolean;
18
18
  error?: string;
19
+ /** The system error code behind a request that got no response (`ENOTFOUND`). */
20
+ code?: string;
19
21
  /** Whether the request was redirected before landing on `status`. */
20
22
  redirected?: boolean;
21
23
  /** The URL finally landed on, after following redirects. */
@@ -28,6 +30,18 @@ export interface ProbeResult {
28
30
  robotsTag?: string | null;
29
31
  }
30
32
 
33
+ /**
34
+ * The reason a request got no response. Node's fetch rejects with a bare
35
+ * "fetch failed" and puts the system error (`getaddrinfo ENOTFOUND …`, with
36
+ * its `code`) on `cause`; Bun's rejects with the system error itself.
37
+ */
38
+ const failure = (error: Error): Pick<ProbeResult, "code" | "error"> => {
39
+ const reason = error.cause instanceof Error ? error.cause : error;
40
+ return "code" in reason
41
+ ? { code: String(reason.code), error: reason.message }
42
+ : { error: reason.message };
43
+ };
44
+
31
45
  /** Probe a URL with the given method, normalizing failures to a result. */
32
46
  const request = async (
33
47
  url: string,
@@ -56,10 +70,9 @@ const request = async (
56
70
  if (error instanceof Error && error.name === "AbortError") {
57
71
  return { ok: false, timedOut: true };
58
72
  }
59
- return {
60
- error: error instanceof Error ? error.message : String(error),
61
- ok: false,
62
- };
73
+ const reason =
74
+ error instanceof Error ? failure(error) : { error: String(error) };
75
+ return { ...reason, ok: false };
63
76
  } finally {
64
77
  clearTimeout(timer);
65
78
  }
@@ -95,10 +108,17 @@ export const gradeExternal = (
95
108
  if (result.timedOut) {
96
109
  return { detail: "request timed out", severity: "warning" };
97
110
  }
111
+ // No response at all. A host name that doesn't resolve is as dead as a 404;
112
+ // a refused, reset, or failed connection is more often someone else's outage
113
+ // or the runner's own network, so it's graded like a timeout.
98
114
  if (result.status === undefined) {
99
- return { detail: result.error ?? "unreachable", severity: "error" };
115
+ return {
116
+ detail: result.error ?? "unreachable",
117
+ severity: result.code === "ENOTFOUND" ? "error" : "warning",
118
+ };
100
119
  }
101
- // A 404/410 is definitively dead. A 403/429/5xx may well be rate limiting or a
120
+ // A 404/410 is definitively dead. Any other status — a 401/403 from a site
121
+ // that turns bots away, a 429, a 5xx — may well be rate limiting or a
102
122
  // transient blip, which is not the author's bug to fix.
103
123
  if (result.status === STATUS_NOT_FOUND || result.status === STATUS_GONE) {
104
124
  return { detail: `HTTP ${result.status}`, severity: "error" };
@@ -1,6 +1,6 @@
1
1
  import { isAbsolute, relative } from "pathe";
2
2
 
3
- import { withBasePath } from "./base-path.ts";
3
+ import { normalizePath, withBasePath } from "./base-path.ts";
4
4
  import { CHANGELOG_INDEX_ROUTE, hasChangelogIndex } from "./changelog-index.ts";
5
5
  import { loadConfig } from "./config.ts";
6
6
  import { customStaticRoutes, discoverPages } from "./custom-pages.ts";
@@ -19,10 +19,11 @@ import { buildManifest } from "./manifest.ts";
19
19
  import { discoverFolderMeta } from "./meta.ts";
20
20
  import type { FolderMetaSource } from "./meta.ts";
21
21
  import { resolveProjectContext } from "./project.ts";
22
- import { pathsUnderPattern } from "./redirect-patterns.ts";
22
+ import { isPatternPath, pathsUnderPattern } from "./redirect-patterns.ts";
23
23
  import type { ResolvedConfig } from "./schema.ts";
24
24
  import { normalizeEntry, strippedLineOffset } from "./sources/normalize.ts";
25
25
  import { resolveDocsCollection, resolveSources } from "./sources/resolve.ts";
26
+ import type { SourceRuntime } from "./sources/resolve.ts";
26
27
  import type {
27
28
  ContentSource,
28
29
  SourceEntry,
@@ -309,17 +310,36 @@ const substituteLoadedVariables = (
309
310
  * and the Node server serve the page), so the pattern is rejected rather than
310
311
  * let the answer depend on the host. `pages` are the served page paths, which
311
312
  * carry `basePath`, as the based `from` does.
313
+ *
314
+ * An exact redirect from a page's own URL answers the same way everywhere:
315
+ * Astro ranks its route above the page's catch-all, in dev and build alike,
316
+ * and writes the redirect page where the page would go, so the page never
317
+ * publishes. That's a warning rather than an error, since the outcome doesn't
318
+ * depend on the host.
312
319
  */
313
320
  const redirectPageDiagnostics = (
314
321
  config: ResolvedConfig,
315
322
  pages: readonly string[],
316
323
  configFile: string | null
317
324
  ): Diagnostic[] =>
318
- config.redirects.flatMap((redirect) => {
319
- const matched = pathsUnderPattern(
320
- withBasePath(config.basePath, redirect.from),
321
- pages
322
- );
325
+ config.redirects.flatMap((redirect): Diagnostic[] => {
326
+ const from = withBasePath(config.basePath, redirect.from);
327
+ if (!isPatternPath(redirect.from)) {
328
+ const page = normalizePath(from);
329
+ return pages.includes(page)
330
+ ? [
331
+ {
332
+ code: "BLUME_REDIRECT_MATCHES_PAGE",
333
+ file: configFile ?? undefined,
334
+ message: `The redirect from ${redirect.from} is also the page ${page}, which never publishes: its URL redirects to ${redirect.to} instead.`,
335
+ severity: "warning",
336
+ suggestion:
337
+ "If the page moved, delete it or give it a new slug; otherwise remove the redirect.",
338
+ } satisfies Diagnostic,
339
+ ]
340
+ : [];
341
+ }
342
+ const matched = pathsUnderPattern(from, pages);
323
343
  const [first] = matched;
324
344
  if (first === undefined) {
325
345
  return [];
@@ -345,6 +365,26 @@ const bannerLinkHref = (
345
365
  ? undefined
346
366
  : banner.link?.href;
347
367
 
368
+ /**
369
+ * The project's sources, before any of them loads: `beforeSources` sees the
370
+ * config first, then each source validates itself (e.g. the filesystem
371
+ * source checks its root exists), replacing the single hard `contentRoot`
372
+ * check.
373
+ */
374
+ const validatedSources = (
375
+ config: ResolvedConfig,
376
+ context: ProjectContext,
377
+ runtime: SourceRuntime,
378
+ beforeSources?: (config: ResolvedConfig) => void
379
+ ): ContentSource[] => {
380
+ beforeSources?.(config);
381
+ const sources = resolveSources(config, context, runtime);
382
+ for (const source of sources) {
383
+ source.validate?.();
384
+ }
385
+ return sources;
386
+ };
387
+
348
388
  export const scanProject = async (
349
389
  root: string,
350
390
  options: {
@@ -356,6 +396,12 @@ export const scanProject = async (
356
396
  overrides?: ConfigOverrides;
357
397
  /** Relocate the generated runtime (e.g. `.blume-verify` for isolation). */
358
398
  runtimeDir?: string;
399
+ /**
400
+ * Called with the resolved config before any source loads, so a command
401
+ * can report an unset secret ahead of a fetch that fails without it: a
402
+ * source that can't load fails the scan, and nothing after it runs.
403
+ */
404
+ beforeSources?: (config: ResolvedConfig) => void;
359
405
  } = {}
360
406
  ): Promise<BlumeProject> => {
361
407
  const mode = options.mode ?? "dev";
@@ -367,17 +413,12 @@ export const scanProject = async (
367
413
  const context = resolveProjectContext(root, config, {
368
414
  runtimeDir: options.runtimeDir,
369
415
  });
370
-
371
- // Each source validates itself (e.g. the filesystem source checks its root
372
- // exists), replacing the single hard `contentRoot` check.
373
- const sources = resolveSources(config, context, {
374
- mode,
375
- preview,
376
- refresh: options.refresh,
377
- });
378
- for (const source of sources) {
379
- source.validate?.();
380
- }
416
+ const sources = validatedSources(
417
+ config,
418
+ context,
419
+ { mode, preview, refresh: options.refresh },
420
+ options.beforeSources
421
+ );
381
422
 
382
423
  // Folder meta is discovered per filesystem source, under each source's own
383
424
  // root and keyed by its route prefix, so a prefixed/root-differing source's
@@ -20,6 +20,8 @@
20
20
  * `/v2` rather than `/v2/` on every host.
21
21
  */
22
22
 
23
+ import { normalizePath } from "./base-path.ts";
24
+
23
25
  /** The name `*` captures under, which `_redirects` reads as `:splat`. */
24
26
  export const SPLAT = "splat";
25
27
 
@@ -401,19 +403,46 @@ export type CompiledRedirect = [
401
403
  status: number,
402
404
  ];
403
405
 
406
+ /** One rule, compiled (see {@link compileRedirects}). */
407
+ const compileRule = (rule: RedirectRule): CompiledRedirect => {
408
+ const { location, source } = regexRedirect(rule);
409
+ return [source, encodeURI(location), rule.status];
410
+ };
411
+
404
412
  /**
405
413
  * The pattern redirects among `redirects`, compiled for the server wrappers
406
- * and the dev server, in the order to try them. The `Location` template is
407
- * percent-encoded as the header needs; a capture is encoded when it's filled
408
- * (see {@link fillLocation}).
414
+ * and Vercel's routing config, in the order to try them. The `Location`
415
+ * template is percent-encoded as the header needs; a capture is encoded when
416
+ * it's filled (see {@link fillLocation}).
409
417
  */
410
418
  export const compileRedirects = (
411
419
  redirects: readonly RedirectEnds[]
420
+ ): CompiledRedirect[] => redirects.flatMap(expandRedirect).map(compileRule);
421
+
422
+ /**
423
+ * Every redirect among `redirects` compiled, exact paths first: a pattern as
424
+ * {@link compileRedirects} compiles it, an exact path as a rule matching
425
+ * just that path, a trailing slash optional. What the dev server answers
426
+ * with, where Astro's own handler would send an exact redirect to a page
427
+ * served by `[...slug]` as a `301` whatever its status.
428
+ */
429
+ export const compileEveryRedirect = (
430
+ redirects: readonly RedirectEnds[]
412
431
  ): CompiledRedirect[] =>
413
- redirects.flatMap(expandRedirect).map((rule) => {
414
- const { location, source } = regexRedirect(rule);
415
- return [source, encodeURI(location), rule.status];
416
- });
432
+ exactFirst(redirects)
433
+ .flatMap((redirect) => {
434
+ const rules = expandRedirect(redirect);
435
+ return rules.length > 0
436
+ ? rules
437
+ : [
438
+ {
439
+ parts: [{ kind: "text", text: normalizePath(redirect.from) }],
440
+ status: redirect.status,
441
+ to: redirect.to,
442
+ } satisfies RedirectRule,
443
+ ];
444
+ })
445
+ .map(compileRule);
417
446
 
418
447
  /** A captured value, encoded for a `Location` header that must not grow a query or fragment. */
419
448
  export const encodeCapture = (value: string): string =>
@@ -485,6 +485,16 @@ const footerConfigSchema = z.strictObject({
485
485
  socials: z.partialRecord(z.enum(FOOTER_SOCIALS), z.string()).default({}),
486
486
  });
487
487
 
488
+ /**
489
+ * The generated `/changelog` index. Its title and description may be
490
+ * per-locale maps; unset, they're the `changelog` UI strings `i18n.ui`
491
+ * translates.
492
+ */
493
+ const changelogConfigSchema = z.strictObject({
494
+ description: localizableLabelSchema.optional(),
495
+ title: localizableLabelSchema.optional(),
496
+ });
497
+
488
498
  /** A validated `content.sources` entry: an adapter descriptor from `blume/sources`. */
489
499
  export type { ContentSourceAdapter } from "../sources/registry.ts";
490
500
 
@@ -778,9 +788,17 @@ const searchIndexingSchema = z
778
788
  })
779
789
  .prefault({});
780
790
 
791
+ /** What the search dialog's analytics events carry. */
792
+ const searchAnalyticsSchema = z
793
+ .strictObject({
794
+ queries: z.boolean().default(true),
795
+ })
796
+ .prefault({});
797
+
781
798
  /** The object form of `search`: the adapter plus its adapter-independent settings. */
782
799
  const searchOptionsSchema = z.strictObject(
783
800
  {
801
+ analytics: searchAnalyticsSchema,
784
802
  indexing: searchIndexingSchema,
785
803
  /** Curated links for the Cmd+K empty state; defaults to the first sidebar pages. */
786
804
  popular: z.array(searchPopularLinkSchema).default([]),
@@ -1188,10 +1206,10 @@ const feedbackConfigSchema = z
1188
1206
 
1189
1207
  // "Listen to this page". Off by default. `true` reads pages aloud with the
1190
1208
  // reader's browser voices, which needs no key and works on any host; an object
1191
- // with a `provider` (`gateway()` from `blume/ai`) generates neural audio at
1192
- // build instead, one cached clip per sentence, and falls back to the browser's
1193
- // voices where no clips exist (`blume dev`, a build without the key). Both
1194
- // normalize to `{ enabled, provider }`.
1209
+ // with a `provider` (`gateway()` or `openai()` from `blume/ai`) generates
1210
+ // neural audio at build instead, one cached clip per sentence, and falls back
1211
+ // to the browser's voices where no clips exist (`blume dev`, a build without
1212
+ // the key). Both normalize to `{ enabled, provider }`.
1195
1213
  const narrationConfigSchema = z
1196
1214
  .union([
1197
1215
  z.boolean(),
@@ -2022,9 +2040,9 @@ const markdownConfigSchema = z.strictObject(
2022
2040
  const reactConfigSchema = z.strictObject({
2023
2041
  /**
2024
2042
  * Auto-memoize React components/hooks with the React Compiler
2025
- * (`babel-plugin-react-compiler`). On by default whenever React is enabled
2043
+ * (`oxc-transform-react`). On by default whenever React is enabled
2026
2044
  * (a project `.tsx`/`.jsx`, a React island/example/override, or the assistant); set
2027
- * to `false` to skip the compiler's babel pass.
2045
+ * to `false` to skip the compiler pass.
2028
2046
  */
2029
2047
  compiler: z.boolean().default(true),
2030
2048
  });
@@ -2102,6 +2120,7 @@ export const blumeConfigSchema = z
2102
2120
  })
2103
2121
  .optional()
2104
2122
  .transform((value) => normalizeBasePath(value)),
2123
+ changelog: changelogConfigSchema.optional(),
2105
2124
  // An adapter from `blume/consent`; analytics waits for the reader.
2106
2125
  consent: consentConfigSchema,
2107
2126
  content: contentConfigSchema.prefault({}),
@@ -2140,6 +2159,7 @@ export const blumeConfigSchema = z
2140
2159
  markdown: markdownConfigSchema.prefault({}),
2141
2160
  narration: narrationConfigSchema.prefault(false),
2142
2161
  navigation: navigationConfigSchema.prefault({}),
2162
+ poweredBy: z.boolean().default(true),
2143
2163
  // An adapter from `blume/ratelimit`, `memory()` by default; `false` is off.
2144
2164
  rateLimit: rateLimitConfigSchema,
2145
2165
  react: reactConfigSchema.prefault({}),
@@ -109,6 +109,9 @@ export const snapshotCache = (cacheDir: string): SnapshotCache => {
109
109
  * snapshot exists, serve it without fetching (cache-first dev). On fetch failure,
110
110
  * serve the last-known-good snapshot with a warning so a CMS/network outage
111
111
  * doesn't fail the build; if there is no snapshot either, surface a hard error.
112
+ * A `BlumeError` from the source (a missing SDK or token, checked before any
113
+ * request, or a misconfiguration) already names its cause, so without a
114
+ * snapshot it surfaces as itself rather than as a fetch failure.
112
115
  */
113
116
  export const loadWithCache = async (
114
117
  name: string,
@@ -138,6 +141,9 @@ export const loadWithCache = async (
138
141
  };
139
142
  return { diagnostics: [diagnostic], entries: fallback };
140
143
  }
144
+ if (error instanceof BlumeError) {
145
+ throw error;
146
+ }
141
147
  // SAFETY: same invariant as above — `fetchEntries` failures are Errors.
142
148
  throw new BlumeError({
143
149
  code: "BLUME_SOURCE_FETCH_FAILED",