blume 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/{chunk-5cg2rzgf.js → chunk-073rdvgb.js} +19 -48
  3. package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-073rdvgb.js.map} +4 -5
  4. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  5. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  6. package/dist/cli/chunk-150jk7km.js +52 -0
  7. package/dist/cli/chunk-150jk7km.js.map +10 -0
  8. package/dist/cli/{chunk-rekqz4qg.js → chunk-313vnn40.js} +16 -15
  9. package/dist/cli/chunk-313vnn40.js.map +10 -0
  10. package/dist/cli/{chunk-gp84za0h.js → chunk-3v3ffbrj.js} +2 -2
  11. package/dist/cli/{chunk-9xr4dgee.js → chunk-3x6x3pph.js} +29 -16
  12. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  13. package/dist/cli/{chunk-0gz36akr.js → chunk-42t4jxdx.js} +21 -5
  14. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  15. package/dist/cli/{chunk-ta27feev.js → chunk-54ztw1nj.js} +21 -2
  16. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  17. package/dist/cli/{chunk-jnwj5kjj.js → chunk-5vjtgs6e.js} +20 -7
  18. package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-5vjtgs6e.js.map} +3 -3
  19. package/dist/cli/chunk-66w9j7p6.js +322 -0
  20. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  21. package/dist/cli/{chunk-nd1821sk.js → chunk-7ppf1rva.js} +32 -18
  22. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  23. package/dist/cli/{chunk-p804f3pe.js → chunk-8y2kmjv8.js} +6 -6
  24. package/dist/cli/{chunk-2f7n4qzt.js → chunk-c1y538kd.js} +1 -1
  25. package/dist/cli/{chunk-1n228xqb.js → chunk-c887fj0s.js} +2 -2
  26. package/dist/cli/{chunk-y5h4r2y0.js → chunk-cfyg47zg.js} +15 -5
  27. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  28. package/dist/cli/{chunk-cmtcnh9f.js → chunk-dzwehpy7.js} +2 -2
  29. package/dist/cli/{chunk-h2a48yhd.js → chunk-e6atmn4h.js} +119 -15
  30. package/dist/cli/{chunk-h2a48yhd.js.map → chunk-e6atmn4h.js.map} +10 -10
  31. package/dist/cli/{chunk-5ffywsmb.js → chunk-e8xw9qkh.js} +8 -6
  32. package/dist/cli/{chunk-5ffywsmb.js.map → chunk-e8xw9qkh.js.map} +3 -3
  33. package/dist/cli/{chunk-ntv4qrt7.js → chunk-e9q7a2ak.js} +14 -14
  34. package/dist/cli/{chunk-xkz5xbtv.js → chunk-ehweg4fr.js} +2366 -1114
  35. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  36. package/dist/cli/{chunk-zf748avv.js → chunk-ep6nd0rn.js} +176 -281
  37. package/dist/cli/{chunk-zf748avv.js.map → chunk-ep6nd0rn.js.map} +11 -12
  38. package/dist/cli/{chunk-yxmk4pec.js → chunk-fdrkhpjy.js} +16 -15
  39. package/dist/cli/{chunk-yxmk4pec.js.map → chunk-fdrkhpjy.js.map} +3 -3
  40. package/dist/cli/{chunk-5wj2qwn2.js → chunk-ft9vfys6.js} +4 -4
  41. package/dist/cli/{chunk-4h8z9vv5.js → chunk-gb420vvz.js} +89 -46
  42. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  43. package/dist/cli/{chunk-713mngdq.js → chunk-hebn0tpx.js} +76 -10
  44. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  45. package/dist/cli/{chunk-2n4ps1b3.js → chunk-htrwgabr.js} +40 -22
  46. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  47. package/dist/cli/{chunk-zhvd6adm.js → chunk-hx3xqnk9.js} +13 -13
  48. package/dist/cli/chunk-j134vrp2.js +65 -0
  49. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  50. package/dist/cli/{chunk-y4ykbark.js → chunk-kma9f6sf.js} +2 -2
  51. package/dist/cli/{chunk-py62fewh.js → chunk-mrcccgnh.js} +3 -3
  52. package/dist/cli/{chunk-2f3ndkpw.js → chunk-n0v7m8ry.js} +3 -3
  53. package/dist/cli/{chunk-43zcf4gz.js → chunk-ncj7w596.js} +522 -353
  54. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  55. package/dist/cli/{chunk-9geymqns.js → chunk-nq1d20w2.js} +14 -14
  56. package/dist/cli/{chunk-g2z9x01v.js → chunk-nrns10mj.js} +3 -3
  57. package/dist/cli/{chunk-e7vvk5ya.js → chunk-sds923fa.js} +3 -3
  58. package/dist/cli/{chunk-cy5bm0j0.js → chunk-t2m5j4pr.js} +2 -2
  59. package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-t2m5j4pr.js.map} +1 -1
  60. package/dist/cli/{chunk-yj17vs39.js → chunk-tdqhyxfz.js} +5 -5
  61. package/dist/cli/{chunk-q3wxxq39.js → chunk-tphac74f.js} +5 -5
  62. package/dist/cli/{chunk-6bm0xpgd.js → chunk-w1r4vase.js} +2 -2
  63. package/dist/cli/index.js +24 -19
  64. package/dist/cli/index.js.map +3 -3
  65. package/dist/types/ai/ask-context.d.ts +6 -2
  66. package/dist/types/ai/component-markdown.d.ts +2 -0
  67. package/dist/types/ai/openapi-components.d.ts +4 -1
  68. package/dist/types/astro/integration.d.ts +5 -4
  69. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  70. package/dist/types/astro/pages.d.ts +10 -0
  71. package/dist/types/cli/init/scaffold.d.ts +1 -1
  72. package/dist/types/components/content/api-field.d.ts +4 -1
  73. package/dist/types/components/content/api-page.d.ts +6 -1
  74. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  75. package/dist/types/components/openapi/helpers.d.ts +22 -1
  76. package/dist/types/components/openapi/security.d.ts +10 -0
  77. package/dist/types/components/openapi/snippets.d.ts +60 -0
  78. package/dist/types/core/changelog-index.d.ts +8 -0
  79. package/dist/types/core/config-input.d.ts +30 -3
  80. package/dist/types/core/data.d.ts +4 -0
  81. package/dist/types/core/i18n-ui.d.ts +2 -0
  82. package/dist/types/core/links.d.ts +8 -0
  83. package/dist/types/core/probe.d.ts +2 -0
  84. package/dist/types/core/project-graph.d.ts +6 -0
  85. package/dist/types/core/redirect-patterns.d.ts +11 -3
  86. package/dist/types/core/schema.d.ts +119 -0
  87. package/dist/types/core/sources/cache.d.ts +3 -0
  88. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  89. package/dist/types/core/sources/contentful.d.ts +17 -1
  90. package/dist/types/core/sources/lexical.d.ts +5 -0
  91. package/dist/types/core/sources/normalize.d.ts +20 -1
  92. package/dist/types/core/sources/remote.d.ts +37 -10
  93. package/dist/types/core/sources/sanity.d.ts +1 -1
  94. package/dist/types/core/types.d.ts +17 -0
  95. package/dist/types/deploy/adapter-output.d.ts +2 -1
  96. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  97. package/dist/types/deploy/adapters/node.d.ts +58 -23
  98. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  99. package/dist/types/deploy/adapters/types.d.ts +3 -2
  100. package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
  101. package/dist/types/deploy/platforms/node.d.ts +3 -1
  102. package/dist/types/deploy/platforms/types.d.ts +18 -7
  103. package/dist/types/deploy/platforms/vercel.d.ts +20 -1
  104. package/dist/types/deploy/redirects.d.ts +17 -1
  105. package/dist/types/deploy/sitemap.d.ts +2 -1
  106. package/dist/types/deploy/vercel-negotiation.d.ts +42 -21
  107. package/dist/types/openapi/checks.d.ts +27 -0
  108. package/dist/types/openapi/graphql.d.ts +6 -0
  109. package/dist/types/openapi/model.d.ts +15 -4
  110. package/dist/types/openapi/parse.d.ts +3 -0
  111. package/dist/types/openapi/references.d.ts +16 -0
  112. package/dist/types/ratelimit/index.d.ts +1 -1
  113. package/dist/types/ratelimit/schema.d.ts +8 -0
  114. package/dist/types/ratelimit/upstash.d.ts +39 -14
  115. package/dist/types/reference/graphql.d.ts +76 -1
  116. package/dist/types/reference/index.d.ts +1 -1
  117. package/dist/types/reference/openapi.d.ts +2 -2
  118. package/dist/types/reference/options.d.ts +37 -1
  119. package/dist/types/reference/schema.d.ts +84 -0
  120. package/dist/types/search/adapters/registry.d.ts +3 -0
  121. package/dist/types/search/adapters/typesense.d.ts +14 -4
  122. package/dist/types/search/build.d.ts +3 -1
  123. package/dist/types/search/orama-index.d.ts +9 -9
  124. package/dist/types/search/sync/algolia.d.ts +5 -2
  125. package/dist/types/search/sync/index.d.ts +3 -1
  126. package/dist/types/search/sync/typesense.d.ts +11 -7
  127. package/dist/types/sources/contentful.d.ts +16 -0
  128. package/dist/types/sources/github-releases.d.ts +8 -1
  129. package/dist/types/sources/payload.d.ts +2 -0
  130. package/dist/types/sources/registry.d.ts +54 -0
  131. package/dist/types/sources/shared.d.ts +3 -0
  132. package/dist/types/sources/strapi.d.ts +2 -0
  133. package/docs/01-quickstart.mdx +4 -2
  134. package/docs/02-deployment.mdx +31 -13
  135. package/docs/advanced/changelog.mdx +11 -0
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +30 -7
  138. package/docs/cli/doctor.mdx +5 -1
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/translate.mdx +1 -0
  141. package/docs/cli/validate.mdx +4 -3
  142. package/docs/configuration/analytics.mdx +1 -1
  143. package/docs/configuration/assistant.mdx +6 -2
  144. package/docs/configuration/customization.mdx +4 -2
  145. package/docs/configuration/export.mdx +5 -3
  146. package/docs/configuration/index.mdx +3 -2
  147. package/docs/configuration/rate-limiting.mdx +23 -2
  148. package/docs/configuration/search.mdx +37 -14
  149. package/docs/content/components.mdx +2 -2
  150. package/docs/content/frontmatter.mdx +5 -3
  151. package/docs/content/i18n.mdx +2 -2
  152. package/docs/content/includes.mdx +1 -1
  153. package/docs/content/navigation.mdx +4 -4
  154. package/docs/content/sources/contentful.mdx +7 -4
  155. package/docs/content/sources/custom.mdx +7 -3
  156. package/docs/content/sources/github-releases.mdx +10 -2
  157. package/docs/content/sources/index.mdx +3 -3
  158. package/docs/content/sources/notion.mdx +1 -1
  159. package/docs/content/sources/obsidian.mdx +1 -1
  160. package/docs/content/sources/payload.mdx +3 -2
  161. package/docs/content/sources/remote-mdx.mdx +4 -0
  162. package/docs/content/sources/sanity.mdx +2 -2
  163. package/docs/content/sources/strapi.mdx +3 -2
  164. package/docs/content/syntax.mdx +2 -2
  165. package/docs/discoverability/agent-discovery.mdx +1 -1
  166. package/docs/discoverability/index.mdx +1 -1
  167. package/docs/discoverability/json-api.mdx +2 -0
  168. package/docs/discoverability/llms-txt.mdx +1 -1
  169. package/docs/discoverability/markdown.mdx +3 -1
  170. package/docs/discoverability/mcp.mdx +4 -0
  171. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  172. package/docs/references/api-pages.mdx +1 -1
  173. package/docs/references/asyncapi.mdx +3 -1
  174. package/docs/references/graphql.mdx +13 -1
  175. package/docs/references/openapi.mdx +50 -8
  176. package/package.json +9 -9
  177. package/skills/blume-migrate/SKILL.md +1 -1
  178. package/skills/blume-migrate/references/docusaurus.md +4 -4
  179. package/skills/blume-migrate/references/fumadocs.md +17 -13
  180. package/skills/blume-migrate/references/nextra.md +8 -5
  181. package/skills/blume-migrate/references/starlight.md +9 -7
  182. package/src/ai/ask-context.ts +33 -3
  183. package/src/ai/ask-limits.ts +13 -0
  184. package/src/ai/changelog-markdown.ts +11 -6
  185. package/src/ai/component-markdown.ts +1 -1
  186. package/src/ai/cors.ts +12 -3
  187. package/src/ai/mcp/query.ts +14 -6
  188. package/src/ai/openapi-components.ts +28 -4
  189. package/src/ai/relative-links.ts +4 -38
  190. package/src/astro/generate.ts +78 -48
  191. package/src/astro/integration.ts +16 -13
  192. package/src/astro/markdown-negotiation.ts +3 -0
  193. package/src/astro/module-types.ts +3 -1
  194. package/src/astro/pages.ts +23 -0
  195. package/src/astro/templates.ts +125 -80
  196. package/src/audit/catalog.ts +19 -3
  197. package/src/audit/checks/links.ts +14 -1
  198. package/src/audit/checks/network.ts +110 -6
  199. package/src/audit/checks/redirects.ts +6 -4
  200. package/src/audit/checks/robots.ts +58 -2
  201. package/src/audit/run.ts +3 -0
  202. package/src/audit/types.ts +2 -0
  203. package/src/audit/url.ts +14 -5
  204. package/src/blume-modules.d.ts +5 -1
  205. package/src/cli/args.ts +51 -0
  206. package/src/cli/build-failure.ts +23 -0
  207. package/src/cli/commands/audit.ts +10 -1
  208. package/src/cli/commands/dev.ts +18 -2
  209. package/src/cli/commands/doctor.ts +45 -12
  210. package/src/cli/commands/eval.ts +36 -4
  211. package/src/cli/commands/init.ts +6 -4
  212. package/src/cli/commands/validate.ts +10 -1
  213. package/src/cli/init/scaffold.ts +3 -3
  214. package/src/cli/prepare.ts +6 -3
  215. package/src/cli/required-secrets.ts +1 -1
  216. package/src/components/content/ApiPlayground.astro +6 -2
  217. package/src/components/content/api-field.ts +4 -1
  218. package/src/components/content/api-page.ts +31 -7
  219. package/src/components/content/mermaid-element.ts +15 -1
  220. package/src/components/islands/hooks.ts +27 -1
  221. package/src/components/layout/PageActions.astro +139 -24
  222. package/src/components/layout/RootLayout.astro +10 -2
  223. package/src/components/layout/Search.astro +17 -3
  224. package/src/components/layout/TableOfContents.astro +24 -17
  225. package/src/components/layout/head-scripts.ts +6 -1
  226. package/src/components/layout/search/algolia.ts +10 -1
  227. package/src/components/layout/search/analytics.ts +42 -14
  228. package/src/components/layout/search/endpoint.ts +48 -5
  229. package/src/components/layout/search/pagefind.ts +114 -5
  230. package/src/components/layout/search/types.ts +12 -0
  231. package/src/components/layout/search/typesense.ts +18 -6
  232. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  233. package/src/components/openapi/Authorization.astro +10 -0
  234. package/src/components/openapi/GraphqlOperation.astro +5 -0
  235. package/src/components/openapi/Operation.astro +24 -1
  236. package/src/components/openapi/ParametersTable.astro +8 -4
  237. package/src/components/openapi/Playground.astro +7 -1
  238. package/src/components/openapi/RequestBody.astro +19 -2
  239. package/src/components/openapi/Responses.astro +1 -0
  240. package/src/components/openapi/SchemaTable.astro +15 -3
  241. package/src/components/openapi/async-snippets.ts +61 -5
  242. package/src/components/openapi/async.ts +123 -29
  243. package/src/components/openapi/graphql-helpers.ts +35 -12
  244. package/src/components/openapi/helpers.ts +36 -2
  245. package/src/components/openapi/message-model.ts +54 -11
  246. package/src/components/openapi/message.ts +14 -4
  247. package/src/components/openapi/operation-model.ts +16 -7
  248. package/src/components/openapi/playground-client.ts +24 -5
  249. package/src/components/openapi/playground-schema.ts +1 -1
  250. package/src/components/openapi/schema-tree.ts +44 -14
  251. package/src/components/openapi/security.ts +10 -0
  252. package/src/components/openapi/snippets.ts +5 -3
  253. package/src/components/openapi/validate-json.ts +4 -3
  254. package/src/core/changelog-index.ts +31 -1
  255. package/src/core/component-diagnostics.ts +53 -0
  256. package/src/core/component-overrides.ts +32 -0
  257. package/src/core/config-input.ts +31 -3
  258. package/src/core/data.ts +2 -0
  259. package/src/core/diagnostics.ts +5 -0
  260. package/src/core/i18n-ui.ts +2 -0
  261. package/src/core/includes.ts +1 -1
  262. package/src/core/links.ts +47 -17
  263. package/src/core/probe.ts +26 -6
  264. package/src/core/project-graph.ts +59 -18
  265. package/src/core/redirect-patterns.ts +36 -7
  266. package/src/core/schema.ts +21 -2
  267. package/src/core/sources/cache.ts +6 -0
  268. package/src/core/sources/contentful-rich-text.ts +29 -6
  269. package/src/core/sources/contentful.ts +135 -25
  270. package/src/core/sources/filesystem.ts +26 -4
  271. package/src/core/sources/github-releases.ts +12 -1
  272. package/src/core/sources/lexical.ts +15 -10
  273. package/src/core/sources/mdx-remote.ts +62 -10
  274. package/src/core/sources/normalize.ts +227 -41
  275. package/src/core/sources/notion.ts +7 -1
  276. package/src/core/sources/obsidian.ts +16 -3
  277. package/src/core/sources/payload.ts +38 -15
  278. package/src/core/sources/remote.ts +72 -15
  279. package/src/core/sources/resolve.ts +11 -1
  280. package/src/core/sources/sanity.ts +43 -25
  281. package/src/core/sources/strapi.ts +2 -2
  282. package/src/core/types.ts +18 -0
  283. package/src/core/ui-packs/ar.ts +1 -0
  284. package/src/core/ui-packs/bg.ts +1 -0
  285. package/src/core/ui-packs/bn.ts +1 -0
  286. package/src/core/ui-packs/ca.ts +1 -0
  287. package/src/core/ui-packs/cs.ts +1 -0
  288. package/src/core/ui-packs/da.ts +1 -0
  289. package/src/core/ui-packs/de.ts +1 -0
  290. package/src/core/ui-packs/el.ts +1 -0
  291. package/src/core/ui-packs/es.ts +1 -0
  292. package/src/core/ui-packs/fa.ts +1 -0
  293. package/src/core/ui-packs/fi.ts +1 -0
  294. package/src/core/ui-packs/fr.ts +1 -0
  295. package/src/core/ui-packs/he.ts +1 -0
  296. package/src/core/ui-packs/hi.ts +1 -0
  297. package/src/core/ui-packs/hr.ts +1 -0
  298. package/src/core/ui-packs/hu.ts +1 -0
  299. package/src/core/ui-packs/id.ts +1 -0
  300. package/src/core/ui-packs/it.ts +1 -0
  301. package/src/core/ui-packs/ja.ts +1 -0
  302. package/src/core/ui-packs/ko.ts +1 -0
  303. package/src/core/ui-packs/nl.ts +1 -0
  304. package/src/core/ui-packs/no.ts +1 -0
  305. package/src/core/ui-packs/pl.ts +1 -0
  306. package/src/core/ui-packs/pt-br.ts +1 -0
  307. package/src/core/ui-packs/pt.ts +1 -0
  308. package/src/core/ui-packs/ro.ts +1 -0
  309. package/src/core/ui-packs/ru.ts +1 -0
  310. package/src/core/ui-packs/sk.ts +1 -0
  311. package/src/core/ui-packs/sr.ts +1 -0
  312. package/src/core/ui-packs/sv.ts +1 -0
  313. package/src/core/ui-packs/th.ts +1 -0
  314. package/src/core/ui-packs/tr.ts +1 -0
  315. package/src/core/ui-packs/uk.ts +1 -0
  316. package/src/core/ui-packs/vi.ts +1 -0
  317. package/src/core/ui-packs/zh-tw.ts +1 -0
  318. package/src/core/ui-packs/zh.ts +1 -0
  319. package/src/deploy/adapter-output.ts +7 -2
  320. package/src/deploy/adapters/cloudflare.ts +7 -5
  321. package/src/deploy/adapters/node.ts +47 -9
  322. package/src/deploy/adapters/types.ts +7 -3
  323. package/src/deploy/artifacts.ts +2 -1
  324. package/src/deploy/platforms/cloudflare.ts +13 -9
  325. package/src/deploy/platforms/netlify.ts +2 -1
  326. package/src/deploy/platforms/node.ts +12 -2
  327. package/src/deploy/platforms/types.ts +21 -7
  328. package/src/deploy/platforms/vercel.ts +113 -16
  329. package/src/deploy/redirects.ts +66 -6
  330. package/src/deploy/rss.ts +3 -1
  331. package/src/deploy/sitemap.ts +39 -5
  332. package/src/deploy/vercel-negotiation.ts +290 -102
  333. package/src/eval/agents.ts +18 -1
  334. package/src/eval/prompts.ts +6 -3
  335. package/src/eval/report.ts +34 -13
  336. package/src/eval/run.ts +5 -0
  337. package/src/markdown/relative-links.ts +3 -2
  338. package/src/openapi/checks.ts +273 -0
  339. package/src/openapi/graphql-build.ts +21 -0
  340. package/src/openapi/graphql.ts +11 -0
  341. package/src/openapi/model.ts +15 -3
  342. package/src/openapi/parse.ts +127 -2
  343. package/src/openapi/references.ts +43 -3
  344. package/src/openapi/source.ts +39 -14
  345. package/src/ratelimit/index.ts +1 -1
  346. package/src/ratelimit/runtime.ts +37 -19
  347. package/src/ratelimit/upstash.ts +40 -10
  348. package/src/reference/graphql.ts +15 -1
  349. package/src/reference/index.ts +1 -0
  350. package/src/reference/openapi.ts +2 -2
  351. package/src/reference/options.ts +29 -1
  352. package/src/registry/eject.ts +3 -1
  353. package/src/search/adapters/locale-scope.ts +30 -0
  354. package/src/search/adapters/typesense.ts +14 -4
  355. package/src/search/build.ts +21 -3
  356. package/src/search/orama-index.ts +163 -31
  357. package/src/search/source-pages.ts +29 -0
  358. package/src/search/sync/algolia.ts +99 -9
  359. package/src/search/sync/index.ts +52 -13
  360. package/src/search/sync/typesense.ts +72 -27
  361. package/src/sources/contentful.ts +12 -0
  362. package/src/sources/github-releases.ts +7 -1
  363. package/src/sources/shared.ts +3 -0
  364. package/src/theme/entry.ts +29 -3
  365. package/src/translate/validate.ts +23 -1
  366. package/dist/cli/chunk-0gz36akr.js.map +0 -12
  367. package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
  368. package/dist/cli/chunk-43zcf4gz.js.map +0 -36
  369. package/dist/cli/chunk-4ejxbzee.js +0 -39
  370. package/dist/cli/chunk-4ejxbzee.js.map +0 -10
  371. package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
  372. package/dist/cli/chunk-713mngdq.js.map +0 -14
  373. package/dist/cli/chunk-9xr4dgee.js.map +0 -10
  374. package/dist/cli/chunk-amrb6hgh.js +0 -145
  375. package/dist/cli/chunk-amrb6hgh.js.map +0 -11
  376. package/dist/cli/chunk-nd1821sk.js.map +0 -10
  377. package/dist/cli/chunk-rekqz4qg.js.map +0 -10
  378. package/dist/cli/chunk-ta27feev.js.map +0 -10
  379. package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
  380. package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
  381. /package/dist/cli/{chunk-gp84za0h.js.map → chunk-3v3ffbrj.js.map} +0 -0
  382. /package/dist/cli/{chunk-p804f3pe.js.map → chunk-8y2kmjv8.js.map} +0 -0
  383. /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-c1y538kd.js.map} +0 -0
  384. /package/dist/cli/{chunk-1n228xqb.js.map → chunk-c887fj0s.js.map} +0 -0
  385. /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-dzwehpy7.js.map} +0 -0
  386. /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-e9q7a2ak.js.map} +0 -0
  387. /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-ft9vfys6.js.map} +0 -0
  388. /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-hx3xqnk9.js.map} +0 -0
  389. /package/dist/cli/{chunk-y4ykbark.js.map → chunk-kma9f6sf.js.map} +0 -0
  390. /package/dist/cli/{chunk-py62fewh.js.map → chunk-mrcccgnh.js.map} +0 -0
  391. /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-n0v7m8ry.js.map} +0 -0
  392. /package/dist/cli/{chunk-9geymqns.js.map → chunk-nq1d20w2.js.map} +0 -0
  393. /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-nrns10mj.js.map} +0 -0
  394. /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-sds923fa.js.map} +0 -0
  395. /package/dist/cli/{chunk-yj17vs39.js.map → chunk-tdqhyxfz.js.map} +0 -0
  396. /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-tphac74f.js.map} +0 -0
  397. /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-w1r4vase.js.map} +0 -0
@@ -155,6 +155,19 @@ export interface FooterConfig {
155
155
  socials?: Partial<Record<FooterSocial, string>>;
156
156
  }
157
157
 
158
+ /**
159
+ * The generated `/changelog` index's heading and description. Each is a
160
+ * string, or a map of locale code to text; the index renders in the default
161
+ * locale. Unset, they're the `changelog` UI strings (`Changelog` in English),
162
+ * which `i18n.ui` translates.
163
+ */
164
+ export interface ChangelogConfig {
165
+ /** The index's description, below its heading and in its meta description. */
166
+ description?: LocalizableLabel;
167
+ /** The index's heading, page title, and OG card title. */
168
+ title?: LocalizableLabel;
169
+ }
170
+
158
171
  /**
159
172
  * Where content lives and how it's discovered. `root`/`include`/`exclude` are
160
173
  * zero-config shorthand for a single `filesystem()` source; set `sources` to
@@ -506,6 +519,16 @@ export type SearchProviderConfig = AnySearchAdapter | false;
506
519
 
507
520
  /** The object form of `search`: the adapter plus adapter-independent settings. */
508
521
  export interface SearchOptions {
522
+ /** What search reports to the configured analytics providers. */
523
+ analytics?: {
524
+ /**
525
+ * Send each query's text with the `search` and `search_select` events.
526
+ * Defaults to `true`. `false` sends the query's length (`queryChars`)
527
+ * instead, and the text rides only the `blume:track` DOM event, for a
528
+ * site to forward on its own terms.
529
+ */
530
+ queries?: boolean;
531
+ };
509
532
  /** Indexing behavior. */
510
533
  indexing?: {
511
534
  /**
@@ -529,7 +552,7 @@ export interface SearchOptions {
529
552
  /**
530
553
  * Search configuration. Pass an adapter directly (`search: algolia({…})`, or
531
554
  * `false`) as shorthand for the object form, which also carries `popular`
532
- * links and `indexing` settings.
555
+ * links and `indexing` and `analytics` settings.
533
556
  */
534
557
  export type SearchConfig = SearchProviderConfig | SearchOptions;
535
558
 
@@ -1308,8 +1331,8 @@ export interface MarkdownConfig {
1308
1331
  export interface ReactConfig {
1309
1332
  /**
1310
1333
  * 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`.
1334
+ * (`oxc-transform-react`). On by default whenever React is enabled;
1335
+ * set to `false` to skip the compiler pass. Defaults to `true`.
1313
1336
  */
1314
1337
  compiler?: boolean;
1315
1338
  }
@@ -1495,6 +1518,11 @@ export interface BlumeConfig {
1495
1518
  * compose: with both set, a page lands at `{deployment.base}/{basePath}/page`.
1496
1519
  */
1497
1520
  basePath?: string;
1521
+ /**
1522
+ * The generated `/changelog` index's title and description, each a string
1523
+ * or a per-locale map. Unset, they're the `changelog` UI strings.
1524
+ */
1525
+ changelog?: ChangelogConfig;
1498
1526
  /**
1499
1527
  * Ask readers before analytics runs, with an adapter from `blume/consent`:
1500
1528
  * `native()` for Blume's own banner, or a hosted consent manager
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
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([]),
@@ -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({}),
@@ -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",
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Contentful rich text → Markdown. Covers the block, inline, and mark types
3
3
  * the Rich Text editor emits; embedded entries fall through to a serializer
4
- * keyed by content type or are noted in a comment. Links to assets and
4
+ * keyed by content type or are noted in a comment, and a link to an entry
5
+ * points at its page when the source says it has one. Links to assets and
5
6
  * entries arrive unresolved from the Delivery API (`data.target` is a
6
7
  * `sys` link), so the source hands in resolvers over the response's
7
8
  * `includes`; a document the SDK already resolved (with `fields` inline)
@@ -25,14 +26,22 @@ import {
25
26
  writesMdx,
26
27
  } from "./lower.ts";
27
28
 
28
- /** An asset's file as a Markdown image needs it. */
29
+ /** An asset's file as a Markdown image or link needs it. */
29
30
  export interface ContentfulAsset {
31
+ /** The file's MIME type; an asset that isn't an image renders as a link. */
32
+ contentType?: string;
30
33
  description?: string;
34
+ fileName?: string;
31
35
  title?: string;
32
36
  url: string;
33
37
  }
34
38
 
35
39
  export interface ContentfulRichTextOptions {
40
+ /**
41
+ * The route of a linked entry's page; an entry hyperlink to an entry with
42
+ * none keeps only its text.
43
+ */
44
+ entryHref?: (entry: JsonObject) => string | undefined;
36
45
  /** Resolve an asset link (`data.target.sys.id`) to its file. */
37
46
  resolveAsset?: (id: string) => ContentfulAsset | null;
38
47
  /** Resolve an entry link to the entry (`sys` plus `fields`). */
@@ -55,7 +64,9 @@ export const assetFromEntry = (asset: JsonObject): ContentfulAsset | null => {
55
64
  return null;
56
65
  }
57
66
  return {
67
+ contentType: asString(getPath(asset, "fields.file.contentType")),
58
68
  description: asString(getPath(asset, "fields.description")),
69
+ fileName: asString(getPath(asset, "fields.file.fileName")),
59
70
  title: asString(getPath(asset, "fields.title")),
60
71
  url: absoluteUrl(url),
61
72
  };
@@ -166,11 +177,16 @@ const inlineNodeParts = (
166
177
  linkedAsset(node, options)?.url
167
178
  );
168
179
  }
180
+ case "entry-hyperlink": {
181
+ const entry = linkedTarget(node, options.resolveEntry);
182
+ const href = entry ? options.entryHref?.(entry) : undefined;
183
+ return linkParts(inlineParts(children(node), options), href);
184
+ }
169
185
  case "embedded-entry-inline": {
170
186
  return [{ markdown: embeddedEntry(node, options) }];
171
187
  }
172
188
  default: {
173
- // entry-hyperlink (no route to point at) and anything unknown: the label.
189
+ // Anything unknown: the label.
174
190
  return inlineParts(children(node), options);
175
191
  }
176
192
  }
@@ -264,11 +280,18 @@ const renderBlock = (
264
280
  }
265
281
  case "embedded-asset-block": {
266
282
  const asset = linkedAsset(node, options);
283
+ if (!asset) {
284
+ return "";
285
+ }
286
+ // Any file can be embedded: one that isn't an image (a PDF, a video)
287
+ // would render as a broken image, so it's a link to the file.
288
+ if (asset.contentType && !asset.contentType.startsWith("image/")) {
289
+ const label = asset.title ?? asset.fileName ?? asset.url;
290
+ return renderInline(linkParts([{ marks: {}, text: label }], asset.url));
291
+ }
267
292
  // Contentful's own rich-text renderer uses the description as alt text
268
293
  // and the title only as a fallback.
269
- return asset
270
- ? image(asset.description ?? asset.title ?? "", asset.url)
271
- : "";
294
+ return image(asset.description ?? asset.title ?? "", asset.url);
272
295
  }
273
296
  case "embedded-entry-block": {
274
297
  return embeddedEntry(node, options);