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
@@ -1,11 +1,16 @@
1
- import { normalizeBasePath } from "../../core/base-path.ts";
1
+ import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
2
2
  import { gradeExternal, probeAll } from "../../core/probe.ts";
3
3
  import type { ProbeResult } from "../../core/probe.ts";
4
4
  import type { Diagnostic } from "../../core/types.ts";
5
5
  import { finding } from "../catalog.ts";
6
6
  import { pageSite } from "../locate.ts";
7
- import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
8
- import { resolveHref, siteOrigin } from "../url.ts";
7
+ import type {
8
+ AuditContext,
9
+ CheckModule,
10
+ PageSnapshot,
11
+ RedirectResolution,
12
+ } from "../types.ts";
13
+ import { decodePath, normalizePath, resolveHref, siteOrigin } from "../url.ts";
9
14
 
10
15
  const CLIENT_ERROR = 400;
11
16
  const SERVER_ERROR = 500;
@@ -135,6 +140,79 @@ export const servedPageChecks = (
135
140
  return found;
136
141
  };
137
142
 
143
+ /**
144
+ * The configured redirects the live site should answer: only those the static
145
+ * checks passed. A pattern (`/beta/:slug*`) names no single URL to request, a
146
+ * loop never lands, and a redirect whose source is also a page never fires.
147
+ */
148
+ const liveRedirects = (context: AuditContext): RedirectResolution[] =>
149
+ context.redirects.filter(
150
+ (redirect) =>
151
+ (redirect.outcome === "ok" || redirect.outcome === "chain") &&
152
+ !context.byUrl.has(normalizePath(redirect.from))
153
+ );
154
+
155
+ /**
156
+ * What requesting a redirect's old URL on the live site got wrong, or null
157
+ * when it redirected to the configured destination.
158
+ *
159
+ * The build writes the host's redirect file, but a host that doesn't read it
160
+ * answers with the meta-refresh page Astro writes at the old URL (a 200:
161
+ * readers still get there, so it's a warning), and a missing redirect 404s (an
162
+ * error). An external destination isn't compared: its site may redirect again.
163
+ */
164
+ export const liveRedirectCheck = (
165
+ context: AuditContext,
166
+ redirect: RedirectResolution,
167
+ result: ProbeResult,
168
+ deployBase: string
169
+ ): Diagnostic | null => {
170
+ const from = normalizePath(redirect.from);
171
+ const site = {
172
+ file: context.project.context.configFile ?? undefined,
173
+ url: from,
174
+ };
175
+ const landed =
176
+ result.redirected && result.finalUrl
177
+ ? normalizePath(
178
+ stripBasePath(
179
+ deployBase,
180
+ decodePath(new URL(result.finalUrl).pathname)
181
+ )
182
+ )
183
+ : from;
184
+
185
+ // A host that only adds a trailing slash (`/old` → `/old/`) still didn't
186
+ // redirect to the destination.
187
+ if (landed === from) {
188
+ let answer = `could not be reached (${result.error ?? "no response"})`;
189
+ if (result.timedOut) {
190
+ answer = "did not respond in time";
191
+ } else if (result.status !== undefined) {
192
+ answer = `answered it with HTTP ${result.status} instead`;
193
+ }
194
+ const found = finding(
195
+ "BLUME_AUDIT_REDIRECT_NOT_SERVED",
196
+ site,
197
+ `${from} should redirect to ${redirect.to}, but the live site ${answer}.`
198
+ );
199
+ return result.ok ? { ...found, severity: "warning" } : found;
200
+ }
201
+
202
+ const destination = redirect.chain.at(-1) ?? from;
203
+ if (/^https?:\/\//iu.test(destination) || landed === destination) {
204
+ return null;
205
+ }
206
+ return {
207
+ ...finding(
208
+ "BLUME_AUDIT_REDIRECT_NOT_SERVED",
209
+ site,
210
+ `${from} redirects to ${landed} on the live site, not to ${destination}.`
211
+ ),
212
+ severity: "warning",
213
+ };
214
+ };
215
+
138
216
  /**
139
217
  * The built site, checked against a live deployment.
140
218
  *
@@ -164,8 +242,23 @@ export const networkChecks: CheckModule = {
164
242
  // build output, which the host serves under the deployment base.
165
243
  const robotsUrl = new URL(`${deployBase}/robots.txt`, origin).toString();
166
244
  const sitemapUrl = new URL(`${deployBase}/sitemap.xml`, origin).toString();
245
+ // Each configured redirect is requested at its old URL, the way a reader
246
+ // following an old link arrives. Its `from` carries `basePath`; the host
247
+ // serves it under the deployment base too.
248
+ const redirects = liveRedirects(context).map(
249
+ (redirect) =>
250
+ [
251
+ redirect,
252
+ new URL(`${deployBase}${redirect.from}`, origin).toString(),
253
+ ] as const
254
+ );
167
255
 
168
- const results = await probeAll([...targets, robotsUrl, sitemapUrl]);
256
+ const results = await probeAll([
257
+ ...targets,
258
+ robotsUrl,
259
+ sitemapUrl,
260
+ ...redirects.map(([, url]) => url),
261
+ ]);
169
262
 
170
263
  for (const page of context.pages) {
171
264
  const result = results.get(liveUrl(origin, page, deployBase));
@@ -180,6 +273,16 @@ export const networkChecks: CheckModule = {
180
273
  found.push(...servedPageChecks(context, page, result, origin));
181
274
  }
182
275
 
276
+ for (const [redirect, url] of redirects) {
277
+ // SAFETY: every redirect URL was in the probed list, and `probeAll`
278
+ // returns a result for each URL it was given.
279
+ const result = results.get(url) as ProbeResult;
280
+ const wrong = liveRedirectCheck(context, redirect, result, deployBase);
281
+ if (wrong) {
282
+ found.push(wrong);
283
+ }
284
+ }
285
+
183
286
  // With `seo.robots: false` Blume writes no robots.txt, so its absence on
184
287
  // the live site is the configured outcome, not a defect.
185
288
  const robots = results.get(robotsUrl);
@@ -210,7 +313,8 @@ export const networkChecks: CheckModule = {
210
313
  };
211
314
 
212
315
  /**
213
- * Outbound links, probed over the network (`--external`).
316
+ * Outbound links, probed over the network (`--external`), except those an
317
+ * `--ignore` glob matches.
214
318
  *
215
319
  * Severity is graded rather than flat: a 404 is the author's bug, but a 403 or a
216
320
  * 5xx is usually rate limiting or someone else's outage, and failing a build on
@@ -229,7 +333,7 @@ export const externalChecks: CheckModule = {
229
333
  for (const page of context.pages) {
230
334
  for (const link of page.links) {
231
335
  const resolved = resolveHref(page.url, link.href, origin, deployBase);
232
- if (resolved.kind !== "external") {
336
+ if (resolved.kind !== "external" || context.ignore(resolved.url)) {
233
337
  continue;
234
338
  }
235
339
  const pages = linkers.get(resolved.url);
@@ -23,15 +23,17 @@ export const redirectChecks: CheckModule = {
23
23
  for (const redirect of context.redirects) {
24
24
  const from = normalizePath(redirect.from);
25
25
 
26
- // A redirect whose source is also a built page never fires — the page
27
- // wins. Ahrefs can't see this: it only observes the served response, which
28
- // looks perfectly healthy.
26
+ // A redirect whose source is also a built page: one of the two never
27
+ // serves, and which depends on the host (a static layer that answers
28
+ // files first keeps the page; a router that runs redirects first keeps
29
+ // the redirect). Ahrefs can't see this: it only observes the served
30
+ // response, which looks perfectly healthy.
29
31
  if (context.byUrl.has(from)) {
30
32
  found.push(
31
33
  finding(
32
34
  "BLUME_AUDIT_REDIRECT_SOURCE_IS_PAGE",
33
35
  { ...site, url: from },
34
- `A redirect is configured from ${from}, but ${from} is also a real page — the redirect never fires.`
36
+ `A redirect is configured from ${from}, but ${from} is also a real page — readers get one or the other, depending on the host.`
35
37
  )
36
38
  );
37
39
  continue;
@@ -12,9 +12,30 @@ import { normalizePath } from "../url.ts";
12
12
  */
13
13
  const MATCH_ORIGIN = "https://robots-audit.invalid";
14
14
 
15
+ const USER_AGENT_LINE = /^\s*user-agent\s*:\s*(?<agent>[^#]*)/iu;
16
+
17
+ /**
18
+ * The crawlers robots.txt names in a `User-agent` line, other than `*`, as
19
+ * written (first spelling wins). robots-parser matches a group by the token
20
+ * before any `/version`, case-insensitively, so that's what dedupes them.
21
+ */
22
+ const namedAgents = (raw: string): string[] => {
23
+ const agents = new Map<string, string>();
24
+ for (const line of raw.split(/\r?\n/u)) {
25
+ const agent = USER_AGENT_LINE.exec(line)?.groups?.agent?.trim() ?? "";
26
+ const key = agent.toLowerCase().split("/")[0]?.trim() ?? "";
27
+ if (key && key !== "*" && !agents.has(key)) {
28
+ agents.set(key, agent);
29
+ }
30
+ }
31
+ return [...agents.values()];
32
+ };
33
+
15
34
  /**
16
35
  * robots.txt: is it there, is it well-formed, does it point at the sitemap, and
17
36
  * — the one that matters — does it block a page the sitemap is advertising?
37
+ * Blocking every crawler (`User-agent: *`) is an error; a group aimed at one
38
+ * crawler is a warning, since shutting out an AI crawler is often on purpose.
18
39
  *
19
40
  * Ahrefs also tracks "robots.txt has too many redirects". A static host serves
20
41
  * the file directly, so that is effectively unreachable here and isn't checked.
@@ -70,6 +91,14 @@ export const robotsChecks: CheckModule = {
70
91
  // consecutive User-agent lines form one group as the spec requires.
71
92
  const parser = robotsParser(`${MATCH_ORIGIN}/robots.txt`, robots.raw);
72
93
  const lines = robots.raw.split(/\r?\n/u);
94
+ const ruleAt = (url: string, agent: string) => {
95
+ const line = parser.getMatchingLineNumber(url, agent);
96
+ return { line, rule: line > 0 ? lines[line - 1]?.trim() : undefined };
97
+ };
98
+ // A crawler named in its own group follows only that group, so a rule
99
+ // there can block it from a page every other crawler may read.
100
+ const agents = namedAgents(robots.raw);
101
+ const blockedFor = new Map<string, string[]>();
73
102
  for (const loc of context.sitemap?.urls ?? []) {
74
103
  let pathname: string;
75
104
  try {
@@ -82,8 +111,7 @@ export const robotsChecks: CheckModule = {
82
111
  const path = normalizePath(pathname);
83
112
  const url = `${MATCH_ORIGIN}${pathname}`;
84
113
  if (parser.isDisallowed(url, "*")) {
85
- const line = parser.getMatchingLineNumber(url, "*");
86
- const rule = line > 0 ? lines[line - 1]?.trim() : undefined;
114
+ const { rule } = ruleAt(url, "*");
87
115
  found.push(
88
116
  finding(
89
117
  "BLUME_AUDIT_ROBOTS_DISALLOWS_INDEXABLE",
@@ -91,7 +119,35 @@ export const robotsChecks: CheckModule = {
91
119
  `robots.txt "${rule ?? "Disallow"}" blocks ${path}, which sitemap.xml advertises.`
92
120
  )
93
121
  );
122
+ continue;
94
123
  }
124
+ for (const agent of agents) {
125
+ if (!parser.isDisallowed(url, agent)) {
126
+ continue;
127
+ }
128
+ const blocked = blockedFor.get(agent);
129
+ if (blocked) {
130
+ blocked.push(url);
131
+ } else {
132
+ blockedFor.set(agent, [url]);
133
+ }
134
+ }
135
+ }
136
+
137
+ // One finding per crawler, at the rule that blocks its first page: a
138
+ // `Disallow: /` aimed at one bot would otherwise list every page.
139
+ for (const [agent, urls] of blockedFor) {
140
+ const [first = ""] = urls;
141
+ const { line, rule } = ruleAt(first, agent);
142
+ const path = normalizePath(new URL(first).pathname);
143
+ const more = urls.length > 1 ? ` and ${urls.length - 1} more` : "";
144
+ found.push(
145
+ finding(
146
+ "BLUME_AUDIT_ROBOTS_BLOCKS_CRAWLER",
147
+ { file: robots.file, line, url: "/robots.txt" },
148
+ `robots.txt "${rule ?? "Disallow"}" blocks ${agent} from ${path}${more}, which sitemap.xml advertises.`
149
+ )
150
+ );
95
151
  }
96
152
 
97
153
  return found;
package/src/audit/run.ts CHANGED
@@ -69,6 +69,8 @@ export interface AuditOptions {
69
69
  origin?: string;
70
70
  /** Probe outbound links (`--external`). */
71
71
  external?: boolean;
72
+ /** Outbound URLs not to probe (`--ignore`). */
73
+ ignore?: (url: string) => boolean;
72
74
  /** Only report these check ids or categories. */
73
75
  only?: string[];
74
76
  /** Suppress these check ids or categories. */
@@ -159,6 +161,7 @@ export const runAudit = async (options: AuditOptions): Promise<AuditResult> => {
159
161
  siteOrigin(project.config.deployment.options.site),
160
162
  normalizeBasePath(project.config.deployment.options.base)
161
163
  ),
164
+ ignore: options.ignore ?? (() => false),
162
165
  llms: crawl.llms,
163
166
  origin,
164
167
  pages: crawl.pages,
@@ -249,6 +249,8 @@ export interface AuditContext {
249
249
  robots: RobotsDoc | null;
250
250
  llms: LlmsDoc | null;
251
251
  thresholds: AuditThresholds;
252
+ /** Outbound URLs the external tier doesn't probe (`--ignore`). */
253
+ ignore: (url: string) => boolean;
252
254
  }
253
255
 
254
256
  /** One category's checks. Modules, not per-check closures — see catalog.ts. */
package/src/audit/url.ts CHANGED
@@ -45,10 +45,11 @@ export type ResolvedHref =
45
45
  | { kind: "self-origin"; path: string; hash: string }
46
46
  /**
47
47
  * A path on this origin that lacks `deployment.base`. The host serves the
48
- * whole build under the base, so nothing answers here — `path` is the path
49
- * as requested, base missing.
48
+ * whole build under the base, so nothing in the build answers here — `path`
49
+ * is the path as requested, base missing. `absolute` when the href spelled
50
+ * out the origin: a full URL can name another app on the same host.
50
51
  */
51
- | { kind: "outside-base"; path: string }
52
+ | { kind: "outside-base"; path: string; absolute: boolean }
52
53
  /** An absolute URL on another origin. */
53
54
  | { kind: "external"; url: string }
54
55
  /** In-page anchor, `mailto:`, `tel:`, `javascript:`, data URI — not a page link. */
@@ -139,7 +140,11 @@ export const resolveHref = (
139
140
  if (origin && parsed.origin === origin) {
140
141
  const served = decodePath(parsed.pathname);
141
142
  if (!underBase(deployBase, served)) {
142
- return { kind: "outside-base", path: normalizePath(served) };
143
+ return {
144
+ absolute: true,
145
+ kind: "outside-base",
146
+ path: normalizePath(served),
147
+ };
143
148
  }
144
149
  return {
145
150
  hash: parsed.hash.slice(1),
@@ -171,7 +176,11 @@ export const resolveHref = (
171
176
  // (`./auth`) was resolved against the base-less page URL above, so it
172
177
  // inherits the page's base by construction.
173
178
  if (target.startsWith("/") && !underBase(deployBase, served)) {
174
- return { kind: "outside-base", path: normalizePath(served) };
179
+ return {
180
+ absolute: false,
181
+ kind: "outside-base",
182
+ path: normalizePath(served),
183
+ };
175
184
  }
176
185
  return {
177
186
  hash: resolved.hash.slice(1),
@@ -12,7 +12,11 @@ declare module "blume:search-client" {
12
12
  // biome-ignore lint/style/useImportType: ambient module must stay a global script
13
13
  // oxlint-disable-next-line typescript/consistent-type-imports
14
14
  type Fn = import("./components/layout/search/types.ts").SearchFn;
15
- export const createSearch: () => Fn | Promise<Fn>;
15
+ type Options =
16
+ // biome-ignore lint/style/useImportType: ambient module must stay a global script
17
+ // oxlint-disable-next-line typescript/consistent-type-imports
18
+ import("./components/layout/search/types.ts").SearchClientOptions;
19
+ export const createSearch: (options?: Options) => Fn | Promise<Fn>;
16
20
  }
17
21
 
18
22
  declare module "blume:consent-client" {
package/src/cli/args.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import picomatch from "picomatch";
2
+
1
3
  import { logger } from "./log.ts";
2
4
 
3
5
  const MAX_PORT = 65_535;
@@ -22,6 +24,55 @@ export const parsePort = (value?: string): number | undefined => {
22
24
  return port;
23
25
  };
24
26
 
27
+ /**
28
+ * Every value a repeatable string flag was given (`--ignore a --ignore b`), in
29
+ * order. citty 0.2 parses with `node:util.parseArgs` without its `multiple`
30
+ * option, so the parsed `args` keep only the last value; this reads the raw
31
+ * arguments the way that parser does instead: `--name=value`, or `--name`
32
+ * followed by the next token whatever it looks like (`""` when there is
33
+ * none), and nothing after a bare `--`.
34
+ */
35
+ export const repeatedFlag = (
36
+ rawArgs: readonly string[],
37
+ name: string
38
+ ): string[] => {
39
+ const flag = `--${name}`;
40
+ const values: string[] = [];
41
+ for (let index = 0; index < rawArgs.length; index += 1) {
42
+ const arg = rawArgs[index];
43
+ if (arg === "--") {
44
+ break;
45
+ }
46
+ if (arg === flag) {
47
+ values.push(rawArgs[index + 1] ?? "");
48
+ index += 1;
49
+ } else if (arg?.startsWith(`${flag}=`)) {
50
+ values.push(arg.slice(flag.length + 1));
51
+ }
52
+ }
53
+ return values;
54
+ };
55
+
56
+ /**
57
+ * The `--ignore` globs of `blume validate` and `blume audit`, as a test for
58
+ * the external URLs they skip: any glob matching the full URL skips it, with
59
+ * `dot` set so `**` also crosses a segment like `/.well-known/`. No globs skip
60
+ * nothing. An empty one (`--ignore` with nothing after it) exits with an
61
+ * error: it names no URL, and picomatch throws on it.
62
+ */
63
+ export const parseIgnoreFlag = (
64
+ rawArgs: readonly string[]
65
+ ): ((url: string) => boolean) => {
66
+ const globs = repeatedFlag(rawArgs, "ignore");
67
+ if (globs.includes("")) {
68
+ logger.error(
69
+ 'Invalid --ignore "": pass a glob for the URLs to skip (e.g. --ignore "https://example.com/**").'
70
+ );
71
+ process.exit(1);
72
+ }
73
+ return picomatch(globs, { dot: true });
74
+ };
75
+
25
76
  /**
26
77
  * The longest `--timeout` a timer can hold, in seconds. Node clamps a
27
78
  * `setTimeout` delay above 2^31−1 ms (about 24.8 days) to 1 ms, so a larger
@@ -20,8 +20,31 @@ const buildErrorSchema = z.looseObject({
20
20
  .optional(),
21
21
  });
22
22
 
23
+ /**
24
+ * The diagnostic a `BlumeError` thrown inside the build carries, such as a
25
+ * failed hosted search sync in `astro:build:done`. The hook runs Blume's
26
+ * source rather than the CLI bundle, so its error is another copy of the class
27
+ * that the command's `instanceof BlumeError` doesn't recognize.
28
+ */
29
+ const blumeErrorSchema = z.looseObject({
30
+ diagnostic: z.object({
31
+ code: z.string(),
32
+ column: z.number().optional(),
33
+ docsUrl: z.string().optional(),
34
+ file: z.string().optional(),
35
+ line: z.number().optional(),
36
+ message: z.string(),
37
+ severity: z.enum(["error", "info", "warning"]),
38
+ suggestion: z.string().optional(),
39
+ }),
40
+ });
41
+
23
42
  /** One failure as a diagnostic at the file (and position) it names. */
24
43
  const failureDiagnostic = (error: Error): Diagnostic => {
44
+ const raised = blumeErrorSchema.safeParse(error);
45
+ if (raised.success) {
46
+ return raised.data.diagnostic;
47
+ }
25
48
  const parsed = buildErrorSchema.safeParse(error);
26
49
  const { id, loc } = parsed.success ? parsed.data : {};
27
50
  // A named error (`MDXError`, `CompilerError`) says which step failed.
@@ -15,6 +15,7 @@ import { isHttpUrl } from "../../audit/url.ts";
15
15
  import { BlumeError } from "../../core/diagnostics.ts";
16
16
  import { scanProject } from "../../core/project-graph.ts";
17
17
  import type { DiagnosticSeverity } from "../../core/types.ts";
18
+ import { parseIgnoreFlag } from "../args.ts";
18
19
  import { commandMeta } from "../command-meta.ts";
19
20
  import { reportInternalError } from "../internal-error.ts";
20
21
  import { flushStdout, logger, reportDiagnostics } from "../log.ts";
@@ -103,6 +104,12 @@ export const auditCommand = defineCommand({
103
104
  "Exit non-zero at this severity or above: error | warning | info. Defaults to error.",
104
105
  type: "string",
105
106
  },
107
+ ignore: {
108
+ description:
109
+ 'With --external, skip outbound links whose URL matches this glob (e.g. "https://example.com/**"). Repeat it for more; internal links are always checked.',
110
+ type: "string",
111
+ valueHint: "glob",
112
+ },
106
113
  json: {
107
114
  description: "Emit the report as JSON on stdout (for CI/editors).",
108
115
  type: "boolean",
@@ -134,7 +141,7 @@ export const auditCommand = defineCommand({
134
141
  },
135
142
  },
136
143
  meta: commandMeta.audit,
137
- async run({ args }) {
144
+ async run({ args, rawArgs }) {
138
145
  if (args["list-checks"]) {
139
146
  process.stdout.write(formatCatalog());
140
147
  return;
@@ -172,6 +179,7 @@ export const auditCommand = defineCommand({
172
179
  const only = splitTerms(args.only);
173
180
  const skip = splitTerms(args.skip);
174
181
  rejectUnknownTerms([...only, ...skip]);
182
+ const ignore = parseIgnoreFlag(rawArgs);
175
183
  let result: AuditResult;
176
184
  try {
177
185
  // `scanProject`, not `prepareProject`: the audit reads the *existing*
@@ -180,6 +188,7 @@ export const auditCommand = defineCommand({
180
188
  const project = await scanProject(root, { mode: "build" });
181
189
  result = await runAudit({
182
190
  external: args.external,
191
+ ignore,
183
192
  only,
184
193
  origin: args.url,
185
194
  project,
@@ -11,7 +11,9 @@ import {
11
11
  import { BlumeError } from "../../core/diagnostics.ts";
12
12
  import { scanProject } from "../../core/project-graph.ts";
13
13
  import { resolveRuntimeDir } from "../../core/project.ts";
14
+ import { referenceSpecFiles } from "../../openapi/references.ts";
14
15
  import { parsePort } from "../args.ts";
16
+ import { astroBuildDiagnostics } from "../build-failure.ts";
15
17
  import { commandMeta } from "../command-meta.ts";
16
18
  import {
17
19
  acquireDevLock,
@@ -140,7 +142,19 @@ export const devCommand = defineCommand({
140
142
  server: { host: normalizeHost(args.host), open, port: listenPort },
141
143
  });
142
144
 
143
- let server = await createServer(explicitPort, args.open ?? false);
145
+ let server: Awaited<ReturnType<typeof createServer>>;
146
+ try {
147
+ server = await createServer(explicitPort, args.open ?? false);
148
+ } catch (error) {
149
+ // Astro's first content sync rejects on a page it can't load (front
150
+ // matter that isn't valid YAML): report it at the file it names, as
151
+ // `blume build` does, not as an internal error blaming Blume.
152
+ if (error instanceof BlumeError || !(error instanceof Error)) {
153
+ throw error;
154
+ }
155
+ reportDiagnostics(astroBuildDiagnostics(error), root);
156
+ process.exit(1);
157
+ }
144
158
 
145
159
  // Vite bumps to the next free port when the default is taken, so record
146
160
  // the port the server actually bound — the lock's URL is what a refused
@@ -229,7 +243,8 @@ export const devCommand = defineCommand({
229
243
 
230
244
  // Content is watched per source (filesystem uses fs.watch; remote sources
231
245
  // are frozen for the session). The remaining project inputs — user pages,
232
- // config, theme, and component overrides — are watched directly.
246
+ // config, theme, component overrides, and the local specs and overlays API
247
+ // references read — are watched directly.
233
248
  const dirTargets = [project.context.pagesRoot].filter(
234
249
  (target) => target !== null
235
250
  );
@@ -237,6 +252,7 @@ export const devCommand = defineCommand({
237
252
  project.context.configFile,
238
253
  project.context.themeFile,
239
254
  project.context.componentsFile,
255
+ ...referenceSpecFiles(project.config, root),
240
256
  ].filter((target) => target !== null);
241
257
 
242
258
  // chokidar handles what raw fs.watch made us hand-roll: recursive
@@ -5,13 +5,17 @@ import { defineCommand } from "citty";
5
5
  import { join } from "pathe";
6
6
  import { satisfies } from "semver";
7
7
 
8
+ import { discoverExamples } from "../../astro/examples.ts";
8
9
  import { discoverIslands } from "../../astro/islands.ts";
10
+ import { discoverPages, navTargetRoutes } from "../../astro/pages.ts";
9
11
  import { missingDependencyDiagnostic } from "../../astro/runtime-deps.ts";
12
+ import { missingExampleDiagnostics } from "../../core/component-diagnostics.ts";
10
13
  import {
11
14
  analyzeComponentOverrides,
12
15
  ComponentOverridesError,
13
16
  } from "../../core/component-overrides.ts";
14
17
  import { BlumeError } from "../../core/diagnostics.ts";
18
+ import { validateNavTargets } from "../../core/nav-diagnostics.ts";
15
19
  import { packageRoot } from "../../core/package-root.ts";
16
20
  import { scanProject } from "../../core/project-graph.ts";
17
21
  import type { ResolvedConfig } from "../../core/schema.ts";
@@ -35,23 +39,24 @@ const FALLBACK_NODE_RANGE = ">=22.12.0";
35
39
  * Plan `components.ts` the way `blume dev`/`build` do, reporting each override
36
40
  * that can't be planned — a non-static form, or an import of a file that
37
41
  * doesn't exist — with its own line. The scan alone never reads the file, so
38
- * without this doctor would pass a project whose build fails.
42
+ * without this doctor would pass a project whose build fails. Also returns
43
+ * the MDX tags the file overrides.
39
44
  */
40
45
  const componentsDiagnostics = async (
41
46
  componentsFile: string | null
42
- ): Promise<Diagnostic[]> => {
47
+ ): Promise<{ issues: Diagnostic[]; mdx: string[] }> => {
43
48
  if (!componentsFile) {
44
- return [];
49
+ return { issues: [], mdx: [] };
45
50
  }
46
51
  try {
47
- analyzeComponentOverrides(
52
+ const { mdx } = analyzeComponentOverrides(
48
53
  await readFile(componentsFile, "utf-8"),
49
54
  componentsFile
50
55
  );
51
- return [];
56
+ return { issues: [], mdx: mdx.map((entry) => entry.key) };
52
57
  } catch (error) {
53
58
  if (error instanceof ComponentOverridesError) {
54
- return error.issues;
59
+ return { issues: error.issues, mdx: [] };
55
60
  }
56
61
  throw error;
57
62
  }
@@ -117,15 +122,33 @@ export const doctorCommand = defineCommand({
117
122
  loadEnvFiles(root);
118
123
 
119
124
  try {
120
- const project = await scanProject(root, { mode: "build" });
125
+ const project = await scanProject(root, {
126
+ // A missing secret fails at the first request. Checked before the
127
+ // sources fetch, so it's still reported when one of them fails the
128
+ // scan for want of it.
129
+ beforeSources: (config) => {
130
+ diagnostics.push(...checkRequiredSecrets(config));
131
+ },
132
+ mode: "build",
133
+ });
134
+ // Tabs, selector items, and featured links can point at custom pages
135
+ // and generated routes too, so they're checked against every route
136
+ // the site serves, as `blume dev`/`build` check them.
137
+ const userPages = project.context.pagesRoot
138
+ ? await discoverPages(project.context.pagesRoot)
139
+ : [];
121
140
  diagnostics.push(
122
141
  ...project.diagnostics,
123
- ...unregisteredSnapshotDiagnostics(project)
142
+ ...unregisteredSnapshotDiagnostics(project),
143
+ ...validateNavTargets(
144
+ project.graph.navigation,
145
+ navTargetRoutes(project, userPages)
146
+ )
124
147
  );
125
148
 
126
149
  const { config } = project;
127
- // The packages and secrets a build would need: an adapter's missing SDK
128
- // fails the build, and a missing secret fails at the first request.
150
+ // The packages a build would need: an adapter's missing SDK fails the
151
+ // build.
129
152
  const { islands } = await discoverIslands(root);
130
153
  const dependencies = await missingDependencyDiagnostic(
131
154
  config,
@@ -136,9 +159,19 @@ export const doctorCommand = defineCommand({
136
159
  if (dependencies) {
137
160
  diagnostics.push(dependencies);
138
161
  }
162
+ const overrides = await componentsDiagnostics(
163
+ project.context.componentsFile
164
+ );
139
165
  diagnostics.push(
140
- ...(await componentsDiagnostics(project.context.componentsFile)),
141
- ...checkRequiredSecrets(config)
166
+ ...overrides.issues,
167
+ // A `<Component path>` naming no example renders a "No example
168
+ // found" box, which dev and build warn about too.
169
+ ...missingExampleDiagnostics(
170
+ project.graph.pages,
171
+ await discoverExamples(root, config.examples.source),
172
+ root,
173
+ new Set([...islands.map((island) => island.name), ...overrides.mdx])
174
+ )
142
175
  );
143
176
  const features = serverFeatures(config);
144
177
  if (