blume 2.0.3 → 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 (607) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +142 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-5f86nr5m.js → chunk-073rdvgb.js} +21 -48
  5. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-073rdvgb.js.map} +4 -5
  6. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  7. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  8. package/dist/cli/chunk-150jk7km.js +52 -0
  9. package/dist/cli/chunk-150jk7km.js.map +10 -0
  10. package/dist/cli/{chunk-9t7a85s3.js → chunk-313vnn40.js} +30 -19
  11. package/dist/cli/chunk-313vnn40.js.map +10 -0
  12. package/dist/cli/{chunk-tkacnehg.js → chunk-3v3ffbrj.js} +2 -2
  13. package/dist/cli/{chunk-q58y5e6a.js → chunk-3x6x3pph.js} +46 -24
  14. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  15. package/dist/cli/{chunk-1d7ve1dm.js → chunk-42t4jxdx.js} +37 -5
  16. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  17. package/dist/cli/{chunk-8ktnccpt.js → chunk-54ztw1nj.js} +21 -2
  18. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  19. package/dist/cli/{chunk-hm3vjy5s.js → chunk-5vjtgs6e.js} +28 -10
  20. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-5vjtgs6e.js.map} +4 -4
  21. package/dist/cli/chunk-66w9j7p6.js +322 -0
  22. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  23. package/dist/cli/{chunk-2hsdwb9n.js → chunk-7ppf1rva.js} +34 -18
  24. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  25. package/dist/cli/{chunk-r9rcc4w7.js → chunk-8y2kmjv8.js} +7 -7
  26. package/dist/cli/{chunk-r20tn01b.js → chunk-c1y538kd.js} +8 -2
  27. package/dist/cli/{chunk-r20tn01b.js.map → chunk-c1y538kd.js.map} +3 -3
  28. package/dist/cli/{chunk-2eytanqx.js → chunk-c887fj0s.js} +2 -2
  29. package/dist/cli/{chunk-p3v96n38.js → chunk-cfyg47zg.js} +16 -6
  30. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  31. package/dist/cli/chunk-cm89scdv.js +443 -0
  32. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  33. package/dist/cli/{chunk-akbpwfxc.js → chunk-dzwehpy7.js} +2 -2
  34. package/dist/cli/{chunk-wrr3j9w9.js → chunk-e6atmn4h.js} +299 -72
  35. package/dist/cli/chunk-e6atmn4h.js.map +36 -0
  36. package/dist/cli/{chunk-tzmab476.js → chunk-e8xw9qkh.js} +8 -6
  37. package/dist/cli/{chunk-tzmab476.js.map → chunk-e8xw9qkh.js.map} +3 -3
  38. package/dist/cli/{chunk-6dsbexzp.js → chunk-e9q7a2ak.js} +17 -15
  39. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-e9q7a2ak.js.map} +2 -2
  40. package/dist/cli/{chunk-pv29h0wf.js → chunk-ehweg4fr.js} +5155 -1249
  41. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  42. package/dist/cli/{chunk-c8chx29p.js → chunk-ep6nd0rn.js} +390 -318
  43. package/dist/cli/chunk-ep6nd0rn.js.map +36 -0
  44. package/dist/cli/{chunk-p73c0m7w.js → chunk-fdrkhpjy.js} +18 -15
  45. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-fdrkhpjy.js.map} +3 -3
  46. package/dist/cli/{chunk-2z928egk.js → chunk-ft9vfys6.js} +5 -5
  47. package/dist/cli/{chunk-5m5nmvyq.js → chunk-gb420vvz.js} +316 -79
  48. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  49. package/dist/cli/{chunk-vg9r4eb9.js → chunk-hebn0tpx.js} +86 -13
  50. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  51. package/dist/cli/{chunk-35d4wj9f.js → chunk-htrwgabr.js} +41 -23
  52. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  53. package/dist/cli/{chunk-g4hq16wv.js → chunk-hx3xqnk9.js} +16 -14
  54. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-hx3xqnk9.js.map} +2 -2
  55. package/dist/cli/chunk-j134vrp2.js +65 -0
  56. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  57. package/dist/cli/{chunk-a58773jm.js → chunk-kma9f6sf.js} +2 -2
  58. package/dist/cli/chunk-kwrg63tm.js +31 -0
  59. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  60. package/dist/cli/{chunk-ey84smr6.js → chunk-mrcccgnh.js} +6 -6
  61. package/dist/cli/{chunk-ey84smr6.js.map → chunk-mrcccgnh.js.map} +1 -1
  62. package/dist/cli/{chunk-yfyb25rh.js → chunk-n0v7m8ry.js} +24 -8
  63. package/dist/cli/chunk-n0v7m8ry.js.map +14 -0
  64. package/dist/cli/{chunk-j85vccga.js → chunk-ncj7w596.js} +576 -367
  65. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  66. package/dist/cli/{chunk-5xvm6tfj.js → chunk-nq1d20w2.js} +17 -15
  67. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-nq1d20w2.js.map} +2 -2
  68. package/dist/cli/{chunk-pehfxfta.js → chunk-nrns10mj.js} +6 -98
  69. package/dist/cli/chunk-nrns10mj.js.map +10 -0
  70. package/dist/cli/{chunk-crgn1q09.js → chunk-sds923fa.js} +4 -4
  71. package/dist/cli/chunk-t2m5j4pr.js +104 -0
  72. package/dist/cli/chunk-t2m5j4pr.js.map +11 -0
  73. package/dist/cli/{chunk-acanzt5p.js → chunk-tdqhyxfz.js} +6 -6
  74. package/dist/cli/chunk-tphac74f.js +126 -0
  75. package/dist/cli/chunk-tphac74f.js.map +11 -0
  76. package/dist/cli/chunk-w1r4vase.js +18 -0
  77. package/dist/cli/chunk-w1r4vase.js.map +10 -0
  78. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  79. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  80. package/dist/cli/index.js +31 -20
  81. package/dist/cli/index.js.map +5 -5
  82. package/dist/types/ai/agent-surface.d.ts +8 -7
  83. package/dist/types/ai/ai-catalog.d.ts +7 -0
  84. package/dist/types/ai/ask-context.d.ts +26 -0
  85. package/dist/types/ai/ask.d.ts +271 -58
  86. package/dist/types/ai/component-markdown.d.ts +2 -0
  87. package/dist/types/ai/index.d.ts +2 -2
  88. package/dist/types/ai/openapi-components.d.ts +4 -1
  89. package/dist/types/ai/site-skill.d.ts +26 -0
  90. package/dist/types/ai/skills.d.ts +16 -0
  91. package/dist/types/astro/integration.d.ts +9 -0
  92. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  93. package/dist/types/astro/pages.d.ts +10 -0
  94. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  95. package/dist/types/captcha/index.d.ts +28 -0
  96. package/dist/types/captcha/schema.d.ts +29 -0
  97. package/dist/types/captcha/turnstile.d.ts +28 -0
  98. package/dist/types/cli/init/scaffold.d.ts +1 -1
  99. package/dist/types/components/content/api-field.d.ts +62 -0
  100. package/dist/types/components/content/api-page.d.ts +100 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  103. package/dist/types/components/openapi/helpers.d.ts +158 -0
  104. package/dist/types/components/openapi/security.d.ts +95 -0
  105. package/dist/types/components/openapi/snippets.d.ts +60 -0
  106. package/dist/types/consent/ethyca.d.ts +48 -0
  107. package/dist/types/consent/index.d.ts +37 -0
  108. package/dist/types/consent/native.d.ts +28 -0
  109. package/dist/types/consent/osano.d.ts +42 -0
  110. package/dist/types/consent/schema.d.ts +110 -0
  111. package/dist/types/core/changelog-index.d.ts +8 -0
  112. package/dist/types/core/config-input.d.ts +217 -23
  113. package/dist/types/core/data.d.ts +53 -4
  114. package/dist/types/core/footer.d.ts +8 -0
  115. package/dist/types/core/i18n-ui.d.ts +88 -0
  116. package/dist/types/core/includes.d.ts +8 -2
  117. package/dist/types/core/links.d.ts +8 -0
  118. package/dist/types/core/localizable.d.ts +7 -0
  119. package/dist/types/core/page-modes.d.ts +29 -0
  120. package/dist/types/core/probe.d.ts +2 -0
  121. package/dist/types/core/project-graph.d.ts +6 -0
  122. package/dist/types/core/redirect-patterns.d.ts +152 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +670 -56
  125. package/dist/types/core/sources/cache.d.ts +3 -0
  126. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  127. package/dist/types/core/sources/contentful.d.ts +17 -1
  128. package/dist/types/core/sources/lexical.d.ts +5 -0
  129. package/dist/types/core/sources/normalize.d.ts +20 -1
  130. package/dist/types/core/sources/read.d.ts +5 -0
  131. package/dist/types/core/sources/remote.d.ts +37 -10
  132. package/dist/types/core/sources/sanity.d.ts +1 -1
  133. package/dist/types/core/types.d.ts +18 -1
  134. package/dist/types/core/variables.d.ts +42 -0
  135. package/dist/types/deploy/adapter-output.d.ts +2 -1
  136. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  137. package/dist/types/deploy/adapters/node.d.ts +58 -23
  138. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  139. package/dist/types/deploy/adapters/types.d.ts +3 -2
  140. package/dist/types/deploy/node-headers.d.ts +13 -2
  141. package/dist/types/deploy/platforms/cloudflare.d.ts +15 -7
  142. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  143. package/dist/types/deploy/platforms/node.d.ts +3 -1
  144. package/dist/types/deploy/platforms/types.d.ts +18 -7
  145. package/dist/types/deploy/platforms/vercel.d.ts +26 -1
  146. package/dist/types/deploy/redirects.d.ts +55 -11
  147. package/dist/types/deploy/sitemap.d.ts +2 -1
  148. package/dist/types/deploy/vercel-negotiation.d.ts +51 -21
  149. package/dist/types/markdown/code-title.d.ts +17 -0
  150. package/dist/types/markdown/features.d.ts +3 -2
  151. package/dist/types/narration/build.d.ts +70 -0
  152. package/dist/types/narration/provider.d.ts +26 -0
  153. package/dist/types/narration/script.d.ts +154 -0
  154. package/dist/types/openapi/checks.d.ts +27 -0
  155. package/dist/types/openapi/graphql.d.ts +6 -0
  156. package/dist/types/openapi/model.d.ts +27 -10
  157. package/dist/types/openapi/overlay.d.ts +54 -0
  158. package/dist/types/openapi/parse.d.ts +16 -0
  159. package/dist/types/openapi/references.d.ts +30 -4
  160. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  161. package/dist/types/ratelimit/index.d.ts +27 -0
  162. package/dist/types/ratelimit/memory.d.ts +36 -0
  163. package/dist/types/ratelimit/schema.d.ts +119 -0
  164. package/dist/types/ratelimit/upstash.d.ts +46 -0
  165. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  166. package/dist/types/reference/asyncapi.d.ts +10 -10
  167. package/dist/types/reference/graphql.d.ts +89 -11
  168. package/dist/types/reference/index.d.ts +1 -1
  169. package/dist/types/reference/openapi.d.ts +33 -12
  170. package/dist/types/reference/options.d.ts +82 -6
  171. package/dist/types/reference/scalar.d.ts +13 -0
  172. package/dist/types/reference/schema.d.ts +144 -36
  173. package/dist/types/search/adapters/registry.d.ts +3 -0
  174. package/dist/types/search/adapters/typesense.d.ts +14 -4
  175. package/dist/types/search/build.d.ts +3 -1
  176. package/dist/types/search/documents.d.ts +12 -1
  177. package/dist/types/search/orama-index.d.ts +13 -9
  178. package/dist/types/search/sync/algolia.d.ts +5 -2
  179. package/dist/types/search/sync/index.d.ts +3 -1
  180. package/dist/types/search/sync/typesense.d.ts +11 -7
  181. package/dist/types/seo/metatags.d.ts +20 -0
  182. package/dist/types/sources/contentful.d.ts +16 -0
  183. package/dist/types/sources/github-releases.d.ts +8 -1
  184. package/dist/types/sources/payload.d.ts +2 -0
  185. package/dist/types/sources/registry.d.ts +54 -0
  186. package/dist/types/sources/shared.d.ts +3 -0
  187. package/dist/types/sources/strapi.d.ts +2 -0
  188. package/docs/01-quickstart.mdx +4 -2
  189. package/docs/02-deployment.mdx +71 -17
  190. package/docs/03-upgrading.mdx +4 -4
  191. package/docs/04-migrating.mdx +1 -1
  192. package/docs/advanced/changelog.mdx +13 -2
  193. package/docs/advanced/skills.mdx +15 -1
  194. package/docs/cli/audit.mdx +51 -8
  195. package/docs/cli/doctor.mdx +5 -1
  196. package/docs/cli/evals.mdx +4 -4
  197. package/docs/cli/index.mdx +1 -0
  198. package/docs/cli/translate.mdx +1 -0
  199. package/docs/cli/validate.mdx +4 -3
  200. package/docs/configuration/analytics.mdx +3 -1
  201. package/docs/configuration/assistant.mdx +194 -38
  202. package/docs/configuration/consent.mdx +100 -0
  203. package/docs/configuration/customization.mdx +8 -6
  204. package/docs/configuration/export.mdx +5 -3
  205. package/docs/configuration/index.mdx +64 -5
  206. package/docs/configuration/meta.ts +3 -0
  207. package/docs/configuration/narration.mdx +119 -0
  208. package/docs/configuration/rate-limiting.mdx +90 -0
  209. package/docs/configuration/search.mdx +63 -12
  210. package/docs/configuration/theming.mdx +3 -1
  211. package/docs/content/components.mdx +108 -2
  212. package/docs/content/frontmatter.mdx +80 -2
  213. package/docs/content/i18n.mdx +39 -4
  214. package/docs/content/includes.mdx +16 -0
  215. package/docs/content/meta.mdx +1 -0
  216. package/docs/content/meta.ts +1 -0
  217. package/docs/content/navigation.mdx +32 -10
  218. package/docs/content/sources/contentful.mdx +35 -0
  219. package/docs/content/sources/custom.mdx +43 -0
  220. package/docs/content/sources/github-releases.mdx +39 -0
  221. package/docs/content/sources/index.mdx +91 -0
  222. package/docs/content/sources/meta.ts +16 -0
  223. package/docs/content/sources/notion.mdx +32 -0
  224. package/docs/content/sources/obsidian.mdx +37 -0
  225. package/docs/content/sources/payload.mdx +31 -0
  226. package/docs/content/sources/remote-mdx.mdx +44 -0
  227. package/docs/content/sources/sanity.mdx +29 -0
  228. package/docs/content/sources/strapi.mdx +32 -0
  229. package/docs/content/syntax.mdx +69 -4
  230. package/docs/content/variables.mdx +41 -0
  231. package/docs/discoverability/agent-discovery.mdx +29 -2
  232. package/docs/discoverability/index.mdx +2 -2
  233. package/docs/discoverability/json-api.mdx +2 -0
  234. package/docs/discoverability/llms-txt.mdx +2 -2
  235. package/docs/discoverability/markdown.mdx +3 -1
  236. package/docs/discoverability/mcp.mdx +4 -0
  237. package/docs/discoverability/metadata.mdx +17 -0
  238. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  239. package/docs/references/api-pages.mdx +74 -0
  240. package/docs/references/asyncapi.mdx +4 -2
  241. package/docs/references/graphql.mdx +14 -2
  242. package/docs/references/meta.ts +1 -1
  243. package/docs/references/openapi.mdx +148 -7
  244. package/docs/references/scalar.mdx +3 -1
  245. package/package.json +43 -8
  246. package/skills/blume/SKILL.md +3 -0
  247. package/skills/blume-migrate/SKILL.md +6 -6
  248. package/skills/blume-migrate/references/docusaurus.md +7 -7
  249. package/skills/blume-migrate/references/fumadocs.md +17 -13
  250. package/skills/blume-migrate/references/mintlify.md +37 -19
  251. package/skills/blume-migrate/references/nextra.md +10 -7
  252. package/skills/blume-migrate/references/starlight.md +11 -8
  253. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  254. package/skills/blume-write-skill/SKILL.md +82 -0
  255. package/src/ai/agent-readability.ts +2 -1
  256. package/src/ai/agent-surface.ts +9 -7
  257. package/src/ai/ai-catalog.ts +6 -3
  258. package/src/ai/ask-context.ts +103 -20
  259. package/src/ai/ask-data.ts +6 -0
  260. package/src/ai/ask-limits.ts +13 -0
  261. package/src/ai/ask-tools.ts +142 -0
  262. package/src/ai/ask.ts +354 -76
  263. package/src/ai/changelog-markdown.ts +11 -6
  264. package/src/ai/component-markdown.ts +76 -1
  265. package/src/ai/cors.ts +14 -3
  266. package/src/ai/index.ts +12 -0
  267. package/src/ai/llms.ts +6 -2
  268. package/src/ai/markdown.ts +2 -1
  269. package/src/ai/mcp/data.ts +6 -0
  270. package/src/ai/mcp/query.ts +14 -6
  271. package/src/ai/openapi-components.ts +36 -6
  272. package/src/ai/relative-links.ts +4 -38
  273. package/src/ai/site-skill.ts +274 -0
  274. package/src/ai/skills.ts +35 -2
  275. package/src/ai/write-skill.ts +97 -0
  276. package/src/astro/generate.ts +210 -66
  277. package/src/astro/index.ts +1 -0
  278. package/src/astro/integration.ts +42 -4
  279. package/src/astro/markdown-negotiation.ts +3 -0
  280. package/src/astro/module-types.ts +9 -1
  281. package/src/astro/pages.ts +23 -0
  282. package/src/astro/templates.ts +406 -108
  283. package/src/astro/variables.ts +31 -0
  284. package/src/audit/catalog.ts +38 -3
  285. package/src/audit/checks/contrast.ts +183 -0
  286. package/src/audit/checks/i18n.ts +2 -3
  287. package/src/audit/checks/indexability.ts +2 -3
  288. package/src/audit/checks/links.ts +19 -13
  289. package/src/audit/checks/network.ts +110 -6
  290. package/src/audit/checks/redirects.ts +6 -4
  291. package/src/audit/checks/robots.ts +58 -2
  292. package/src/audit/checks/sitemap.ts +2 -3
  293. package/src/audit/redirects.ts +46 -15
  294. package/src/audit/run.ts +5 -0
  295. package/src/audit/types.ts +4 -1
  296. package/src/audit/url.ts +14 -5
  297. package/src/blume-modules.d.ts +12 -1
  298. package/src/captcha/client.ts +152 -0
  299. package/src/captcha/hcaptcha.ts +36 -0
  300. package/src/captcha/index.ts +28 -0
  301. package/src/captcha/schema.ts +32 -0
  302. package/src/captcha/turnstile.ts +38 -0
  303. package/src/captcha/verify.ts +71 -0
  304. package/src/cli/args.ts +51 -0
  305. package/src/cli/build-failure.ts +23 -0
  306. package/src/cli/command-meta.ts +5 -0
  307. package/src/cli/commands/audit.ts +10 -1
  308. package/src/cli/commands/dev.ts +18 -2
  309. package/src/cli/commands/doctor.ts +67 -28
  310. package/src/cli/commands/eval.ts +36 -4
  311. package/src/cli/commands/init.ts +6 -4
  312. package/src/cli/commands/skill.ts +96 -0
  313. package/src/cli/commands/validate.ts +10 -1
  314. package/src/cli/index.ts +5 -0
  315. package/src/cli/init/scaffold.ts +3 -3
  316. package/src/cli/prepare.ts +6 -3
  317. package/src/cli/required-secrets.ts +72 -30
  318. package/src/components/BlumePage.astro +2 -0
  319. package/src/components/content/ApiEndpoint.astro +18 -0
  320. package/src/components/content/ApiField.astro +79 -0
  321. package/src/components/content/ApiPlayground.astro +66 -0
  322. package/src/components/content/ApiRail.astro +19 -0
  323. package/src/components/content/Color.astro +1 -0
  324. package/src/components/content/FileTree.astro +1 -0
  325. package/src/components/content/GithubInfo.astro +1 -0
  326. package/src/components/content/Panel.astro +1 -0
  327. package/src/components/content/ParamField.astro +25 -0
  328. package/src/components/content/RequestExample.astro +17 -0
  329. package/src/components/content/ResponseExample.astro +17 -0
  330. package/src/components/content/ResponseField.astro +32 -0
  331. package/src/components/content/Step.astro +1 -0
  332. package/src/components/content/Tile.astro +1 -0
  333. package/src/components/content/Tooltip.astro +1 -0
  334. package/src/components/content/Tree.astro +1 -0
  335. package/src/components/content/TypeTable.astro +4 -1
  336. package/src/components/content/View.astro +21 -0
  337. package/src/components/content/ViewSwitcher.astro +168 -0
  338. package/src/components/content/api-field.ts +103 -0
  339. package/src/components/content/api-page.ts +248 -0
  340. package/src/components/content/mermaid-element.ts +15 -1
  341. package/src/components/content/view-switcher.ts +71 -0
  342. package/src/components/index.ts +2 -0
  343. package/src/components/islands/Assistant.astro +10 -1
  344. package/src/components/islands/assistant.tsx +104 -14
  345. package/src/components/islands/code-question.ts +62 -0
  346. package/src/components/islands/hooks.ts +136 -21
  347. package/src/components/islands/support-link.ts +68 -0
  348. package/src/components/layout/Analytics.astro +11 -2
  349. package/src/components/layout/Banner.astro +14 -10
  350. package/src/components/layout/Breadcrumbs.astro +6 -2
  351. package/src/components/layout/ConsentBanner.astro +62 -0
  352. package/src/components/layout/ConsentHead.astro +38 -0
  353. package/src/components/layout/Directory.astro +94 -0
  354. package/src/components/layout/DrawerSwitchers.astro +51 -0
  355. package/src/components/layout/Empty.astro +2 -2
  356. package/src/components/layout/Header.astro +122 -80
  357. package/src/components/layout/LanguageSwitcher.astro +48 -15
  358. package/src/components/layout/NarrationPlayer.astro +1085 -0
  359. package/src/components/layout/NavSelector.astro +33 -7
  360. package/src/components/layout/PageActions.astro +139 -24
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +71 -3
  363. package/src/components/layout/ReferenceLayout.astro +17 -6
  364. package/src/components/layout/RelatedPages.astro +42 -0
  365. package/src/components/layout/RootLayout.astro +318 -47
  366. package/src/components/layout/Search.astro +47 -19
  367. package/src/components/layout/SiteFooter.astro +124 -0
  368. package/src/components/layout/SiteMetatags.astro +16 -0
  369. package/src/components/layout/TableOfContents.astro +24 -17
  370. package/src/components/layout/consent/native.ts +79 -0
  371. package/src/components/layout/consent/types.ts +37 -0
  372. package/src/components/layout/footer-links.ts +53 -0
  373. package/src/components/layout/footer-socials.ts +87 -0
  374. package/src/components/layout/head-scripts.ts +6 -1
  375. package/src/components/layout/locale-redirect.ts +107 -0
  376. package/src/components/layout/nav-utils.ts +83 -0
  377. package/src/components/layout/page-feedback.ts +96 -0
  378. package/src/components/layout/search/algolia.ts +10 -1
  379. package/src/components/layout/search/analytics.ts +126 -0
  380. package/src/components/layout/search/endpoint.ts +48 -5
  381. package/src/components/layout/search/flexsearch.ts +17 -5
  382. package/src/components/layout/search/orama-cloud.ts +26 -14
  383. package/src/components/layout/search/pagefind.ts +114 -5
  384. package/src/components/layout/search/types.ts +32 -0
  385. package/src/components/layout/search/typesense.ts +19 -3
  386. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  387. package/src/components/openapi/Authorization.astro +10 -0
  388. package/src/components/openapi/GraphqlOperation.astro +5 -0
  389. package/src/components/openapi/Operation.astro +184 -13
  390. package/src/components/openapi/ParametersTable.astro +8 -4
  391. package/src/components/openapi/Playground.astro +7 -1
  392. package/src/components/openapi/RequestBody.astro +24 -5
  393. package/src/components/openapi/RequestPanel.astro +59 -7
  394. package/src/components/openapi/Responses.astro +7 -4
  395. package/src/components/openapi/SchemaTable.astro +15 -3
  396. package/src/components/openapi/async-snippets.ts +65 -9
  397. package/src/components/openapi/async.ts +123 -29
  398. package/src/components/openapi/code-samples.ts +97 -0
  399. package/src/components/openapi/graphql-helpers.ts +35 -12
  400. package/src/components/openapi/helpers.ts +36 -2
  401. package/src/components/openapi/message-model.ts +54 -11
  402. package/src/components/openapi/message.ts +14 -4
  403. package/src/components/openapi/operation-model.ts +16 -7
  404. package/src/components/openapi/playground-client.ts +24 -5
  405. package/src/components/openapi/playground-schema.ts +1 -1
  406. package/src/components/openapi/schema-tree.ts +44 -14
  407. package/src/components/openapi/security.ts +16 -3
  408. package/src/components/openapi/snippets.ts +873 -10
  409. package/src/components/openapi/validate-json.ts +4 -3
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/builtin-tags.ts +5 -0
  421. package/src/core/changelog-index.ts +31 -1
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/component-diagnostics.ts +53 -0
  424. package/src/core/component-overrides.ts +32 -0
  425. package/src/core/config-input.ts +225 -23
  426. package/src/core/data.ts +46 -4
  427. package/src/core/diagnostics.ts +8 -2
  428. package/src/core/footer.ts +26 -0
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/i18n-ui.ts +84 -2
  431. package/src/core/includes.ts +21 -4
  432. package/src/core/links.ts +63 -19
  433. package/src/core/localizable.ts +26 -0
  434. package/src/core/navigation.ts +43 -12
  435. package/src/core/page-modes.ts +56 -0
  436. package/src/core/probe.ts +26 -6
  437. package/src/core/project-graph.ts +163 -16
  438. package/src/core/redirect-patterns.ts +520 -0
  439. package/src/core/related.ts +131 -0
  440. package/src/core/schema.ts +339 -66
  441. package/src/core/sources/cache.ts +6 -0
  442. package/src/core/sources/contentful-rich-text.ts +29 -6
  443. package/src/core/sources/contentful.ts +135 -25
  444. package/src/core/sources/filesystem.ts +26 -4
  445. package/src/core/sources/github-releases.ts +12 -1
  446. package/src/core/sources/lexical.ts +15 -10
  447. package/src/core/sources/mdx-remote.ts +62 -10
  448. package/src/core/sources/normalize.ts +228 -41
  449. package/src/core/sources/notion.ts +7 -1
  450. package/src/core/sources/obsidian.ts +16 -3
  451. package/src/core/sources/payload.ts +38 -15
  452. package/src/core/sources/read.ts +8 -3
  453. package/src/core/sources/remote.ts +72 -15
  454. package/src/core/sources/resolve.ts +11 -1
  455. package/src/core/sources/sanity.ts +43 -25
  456. package/src/core/sources/strapi.ts +2 -2
  457. package/src/core/types.ts +19 -1
  458. package/src/core/ui-packs/ar.ts +44 -0
  459. package/src/core/ui-packs/bg.ts +46 -0
  460. package/src/core/ui-packs/bn.ts +44 -0
  461. package/src/core/ui-packs/ca.ts +47 -0
  462. package/src/core/ui-packs/cs.ts +45 -0
  463. package/src/core/ui-packs/da.ts +46 -0
  464. package/src/core/ui-packs/de.ts +47 -0
  465. package/src/core/ui-packs/el.ts +46 -0
  466. package/src/core/ui-packs/es.ts +47 -0
  467. package/src/core/ui-packs/fa.ts +46 -0
  468. package/src/core/ui-packs/fi.ts +46 -0
  469. package/src/core/ui-packs/fr.ts +47 -0
  470. package/src/core/ui-packs/he.ts +44 -0
  471. package/src/core/ui-packs/hi.ts +45 -0
  472. package/src/core/ui-packs/hr.ts +46 -0
  473. package/src/core/ui-packs/hu.ts +45 -0
  474. package/src/core/ui-packs/id.ts +46 -0
  475. package/src/core/ui-packs/it.ts +46 -0
  476. package/src/core/ui-packs/ja.ts +46 -0
  477. package/src/core/ui-packs/ko.ts +45 -0
  478. package/src/core/ui-packs/nl.ts +47 -0
  479. package/src/core/ui-packs/no.ts +45 -0
  480. package/src/core/ui-packs/pl.ts +46 -0
  481. package/src/core/ui-packs/pt-br.ts +47 -0
  482. package/src/core/ui-packs/pt.ts +46 -0
  483. package/src/core/ui-packs/ro.ts +46 -0
  484. package/src/core/ui-packs/ru.ts +46 -0
  485. package/src/core/ui-packs/sk.ts +45 -0
  486. package/src/core/ui-packs/sr.ts +46 -0
  487. package/src/core/ui-packs/sv.ts +45 -0
  488. package/src/core/ui-packs/th.ts +44 -0
  489. package/src/core/ui-packs/tr.ts +46 -0
  490. package/src/core/ui-packs/uk.ts +46 -0
  491. package/src/core/ui-packs/vi.ts +46 -0
  492. package/src/core/ui-packs/zh-tw.ts +44 -0
  493. package/src/core/ui-packs/zh.ts +44 -0
  494. package/src/core/variables.ts +103 -0
  495. package/src/deploy/adapter-output.ts +7 -2
  496. package/src/deploy/adapters/cloudflare.ts +7 -5
  497. package/src/deploy/adapters/node.ts +47 -9
  498. package/src/deploy/adapters/types.ts +7 -3
  499. package/src/deploy/artifacts.ts +79 -9
  500. package/src/deploy/cloudflare-negotiation.ts +35 -5
  501. package/src/deploy/headers.ts +2 -1
  502. package/src/deploy/node-headers.ts +63 -9
  503. package/src/deploy/platforms/cloudflare.ts +45 -9
  504. package/src/deploy/platforms/netlify.ts +35 -2
  505. package/src/deploy/platforms/node.ts +12 -2
  506. package/src/deploy/platforms/types.ts +21 -7
  507. package/src/deploy/platforms/vercel.ts +149 -17
  508. package/src/deploy/redirects.ts +151 -24
  509. package/src/deploy/rss.ts +3 -1
  510. package/src/deploy/sitemap.ts +39 -5
  511. package/src/deploy/vercel-negotiation.ts +326 -102
  512. package/src/eval/agents.ts +18 -1
  513. package/src/eval/prompts.ts +6 -3
  514. package/src/eval/report.ts +34 -13
  515. package/src/eval/run.ts +5 -0
  516. package/src/markdown/api-rail.ts +259 -0
  517. package/src/markdown/code-title.ts +35 -9
  518. package/src/markdown/features.ts +3 -2
  519. package/src/markdown/fence-meta.ts +2 -0
  520. package/src/markdown/include.ts +11 -1
  521. package/src/markdown/index.ts +28 -5
  522. package/src/markdown/math.ts +4 -4
  523. package/src/markdown/relative-links.ts +3 -2
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +7 -6
  531. package/src/openapi/checks.ts +273 -0
  532. package/src/openapi/graphql-build.ts +21 -0
  533. package/src/openapi/graphql.ts +11 -0
  534. package/src/openapi/model.ts +72 -27
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +182 -4
  537. package/src/openapi/references.ts +64 -8
  538. package/src/openapi/render-mdx.ts +5 -1
  539. package/src/openapi/scalar.ts +40 -6
  540. package/src/openapi/source.ts +40 -15
  541. package/src/ratelimit/cloudflare.ts +54 -0
  542. package/src/ratelimit/index.ts +30 -0
  543. package/src/ratelimit/memory.ts +45 -0
  544. package/src/ratelimit/runtime.ts +247 -0
  545. package/src/ratelimit/schema.ts +28 -0
  546. package/src/ratelimit/upstash.ts +60 -0
  547. package/src/ratelimit/wrangler.ts +70 -0
  548. package/src/reference/asyncapi.ts +2 -2
  549. package/src/reference/graphql.ts +20 -3
  550. package/src/reference/index.ts +1 -0
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +80 -8
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +23 -5
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/locale-scope.ts +30 -0
  557. package/src/search/adapters/typesense.ts +14 -4
  558. package/src/search/build.ts +21 -3
  559. package/src/search/documents.ts +26 -2
  560. package/src/search/orama-index.ts +191 -34
  561. package/src/search/pagefind-ranking.ts +39 -0
  562. package/src/search/source-pages.ts +29 -0
  563. package/src/search/sync/algolia.ts +111 -10
  564. package/src/search/sync/index.ts +52 -13
  565. package/src/search/sync/orama-cloud.ts +3 -0
  566. package/src/search/sync/typesense.ts +76 -26
  567. package/src/seo/metatags.ts +67 -0
  568. package/src/sources/contentful.ts +12 -0
  569. package/src/sources/github-releases.ts +7 -1
  570. package/src/sources/shared.ts +3 -0
  571. package/src/theme/chrome-icons.ts +1 -0
  572. package/src/theme/color.ts +500 -0
  573. package/src/theme/entry.ts +85 -6
  574. package/src/theme/palette.ts +121 -45
  575. package/src/translate/prompts.ts +1 -0
  576. package/src/translate/validate.ts +31 -3
  577. package/src/upgrade/upgrade.ts +4 -171
  578. package/dist/cli/chunk-1d7ve1dm.js.map +0 -12
  579. package/dist/cli/chunk-2hsdwb9n.js.map +0 -10
  580. package/dist/cli/chunk-35d4wj9f.js.map +0 -15
  581. package/dist/cli/chunk-364znk6q.js +0 -75
  582. package/dist/cli/chunk-364znk6q.js.map +0 -11
  583. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  584. package/dist/cli/chunk-8ktnccpt.js.map +0 -10
  585. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  586. package/dist/cli/chunk-b07cmahc.js +0 -145
  587. package/dist/cli/chunk-b07cmahc.js.map +0 -11
  588. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  589. package/dist/cli/chunk-e04dxsz1.js +0 -39
  590. package/dist/cli/chunk-e04dxsz1.js.map +0 -10
  591. package/dist/cli/chunk-j85vccga.js.map +0 -36
  592. package/dist/cli/chunk-p3v96n38.js.map +0 -10
  593. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  594. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  595. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  596. package/dist/cli/chunk-vg9r4eb9.js.map +0 -14
  597. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  598. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  599. package/docs/content/sources.mdx +0 -334
  600. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-3v3ffbrj.js.map} +0 -0
  601. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-8y2kmjv8.js.map} +0 -0
  602. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-c887fj0s.js.map} +0 -0
  603. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-dzwehpy7.js.map} +0 -0
  604. /package/dist/cli/{chunk-2z928egk.js.map → chunk-ft9vfys6.js.map} +0 -0
  605. /package/dist/cli/{chunk-a58773jm.js.map → chunk-kma9f6sf.js.map} +0 -0
  606. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-sds923fa.js.map} +0 -0
  607. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-tdqhyxfz.js.map} +0 -0
@@ -0,0 +1,31 @@
1
+ import { substituteVariables } from "../core/variables.ts";
2
+ import type { ContentVariables } from "../core/variables.ts";
3
+
4
+ /**
5
+ * The MDX half of content variables (see `core/variables.ts`): a Vite plugin
6
+ * that replaces `{{name}}` in an `.mdx` module's source before the MDX
7
+ * compiler reads it. It has to run first: the compiler parses `{{version}}`
8
+ * as a JavaScript expression (a `ReferenceError` at render) and rejects
9
+ * `{{api-url}}` outright, so no plugin after parsing could see either.
10
+ */
11
+ /** The Vite plugin slice this needs (structurally typed, like Blume's other plugins). */
12
+ export interface VariablesVitePlugin {
13
+ enforce: "pre";
14
+ name: string;
15
+ transform: (code: string, id: string) => { code: string; map: null } | null;
16
+ }
17
+
18
+ export const variablesVitePlugin = (
19
+ variables: ContentVariables
20
+ ): VariablesVitePlugin => ({
21
+ enforce: "pre",
22
+ name: "blume:variables",
23
+ transform(code, id) {
24
+ const [path = ""] = id.split("?");
25
+ if (!path.endsWith(".mdx")) {
26
+ return null;
27
+ }
28
+ const next = substituteVariables(code, variables);
29
+ return next === code ? null : { code: next, map: null };
30
+ },
31
+ });
@@ -161,7 +161,7 @@ export const CHECKS = [
161
161
  },
162
162
  {
163
163
  category: "duplicates",
164
- fix: "Merge the pages, or set `seo.canonical` on all but one.",
164
+ fix: "Merge the pages, or point `seo.canonical` on all but one at the page to index.",
165
165
  id: "BLUME_AUDIT_DUPLICATE_CONTENT",
166
166
  severity: "warning",
167
167
  tier: "static",
@@ -359,7 +359,7 @@ export const CHECKS = [
359
359
  },
360
360
  {
361
361
  category: "redirects",
362
- fix: "Remove the redirect, or delete the page it shadows — the page wins and the redirect never fires.",
362
+ fix: "Remove the redirect, or delete the page it shadows — a path can't be both, and which one readers get depends on the host.",
363
363
  id: "BLUME_AUDIT_REDIRECT_SOURCE_IS_PAGE",
364
364
  severity: "error",
365
365
  tier: "static",
@@ -373,6 +373,14 @@ export const CHECKS = [
373
373
  tier: "network",
374
374
  title: "HTTPS to HTTP redirect",
375
375
  },
376
+ {
377
+ category: "redirects",
378
+ fix: "Deploy the redirect file your host reads (see Redirects in the deployment docs), or name the host in `deployment`.",
379
+ id: "BLUME_AUDIT_REDIRECT_NOT_SERVED",
380
+ severity: "error",
381
+ tier: "network",
382
+ title: "Live site doesn't redirect as configured",
383
+ },
376
384
 
377
385
  // Social
378
386
  {
@@ -498,6 +506,25 @@ export const CHECKS = [
498
506
  title: "Hreflang group has a language conflict",
499
507
  },
500
508
 
509
+ // Accessibility. Theme colors are one per site, not per page: these run
510
+ // on the config, the way the build resolves it.
511
+ {
512
+ category: "accessibility",
513
+ fix: "Pick a darker shade for light mode or a lighter one for dark mode, per mode with `theme.accent: { light, dark }`.",
514
+ id: "BLUME_AUDIT_THEME_CONTRAST_LOW",
515
+ severity: "warning",
516
+ tier: "static",
517
+ title: "Theme colors miss WCAG AA contrast",
518
+ },
519
+ {
520
+ category: "accessibility",
521
+ fix: "Write the color as hex, `rgb()`, `hsl()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, or a named color so its contrast can be checked.",
522
+ id: "BLUME_AUDIT_THEME_COLOR_UNCHECKED",
523
+ severity: "info",
524
+ tier: "static",
525
+ title: "Theme color can't be checked for contrast",
526
+ },
527
+
501
528
  // Assets
502
529
  {
503
530
  category: "assets",
@@ -551,7 +578,7 @@ export const CHECKS = [
551
578
  // Sitemap
552
579
  {
553
580
  category: "sitemap",
554
- fix: "Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed.",
581
+ fix: "Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed. A `public/sitemap.xml` replaces the generated sitemap, so add the page there or delete that file.",
555
582
  id: "BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP",
556
583
  severity: "warning",
557
584
  tier: "static",
@@ -647,6 +674,14 @@ export const CHECKS = [
647
674
  tier: "static",
648
675
  title: "robots.txt disallows a page that is in the sitemap",
649
676
  },
677
+ {
678
+ category: "robots",
679
+ fix: "Narrow or remove the rule in that crawler's `User-agent` group if it should read these pages. Blocking an AI crawler on purpose is fine; skip this check.",
680
+ id: "BLUME_AUDIT_ROBOTS_BLOCKS_CRAWLER",
681
+ severity: "warning",
682
+ tier: "static",
683
+ title: "robots.txt blocks one crawler from pages in the sitemap",
684
+ },
650
685
  {
651
686
  category: "robots",
652
687
  fix: "Set `deployment.site` so robots.txt can reference the sitemap.",
@@ -0,0 +1,183 @@
1
+ import type { ResolvedConfig } from "../../core/schema.ts";
2
+ import type { Diagnostic } from "../../core/types.ts";
3
+ import { composite, contrastRatio, parseColor } from "../../theme/color.ts";
4
+ import type { Rgba } from "../../theme/color.ts";
5
+ import {
6
+ MIN_TEXT_CONTRAST,
7
+ labelOn,
8
+ resolveAccent,
9
+ resolveAction,
10
+ themeInk,
11
+ } from "../../theme/palette.ts";
12
+ import type { ColorMode } from "../../theme/palette.ts";
13
+ import { finding } from "../catalog.ts";
14
+ import type { CheckModule } from "../types.ts";
15
+
16
+ const MODES = ["light", "dark"] as const satisfies readonly ColorMode[];
17
+
18
+ /** The theme config key a color comes from. */
19
+ type ThemeKey = "theme.accent" | "theme.action" | "theme.background";
20
+
21
+ /** A color the theme draws text in, over the surface it sits on. */
22
+ interface TextPair {
23
+ fix: string;
24
+ key: ThemeKey;
25
+ /** What the pair is, for the message: "Accent text on the page". */
26
+ label: string;
27
+ surface: Rgba;
28
+ text: Rgba;
29
+ }
30
+
31
+ /** A theme color, resolved for one mode, and the config key it came from. */
32
+ interface ThemeColor {
33
+ key: ThemeKey;
34
+ value: string;
35
+ }
36
+
37
+ /** What one mode's check found: pairs to measure, colors it couldn't read. */
38
+ interface ModeColors {
39
+ pairs: TextPair[];
40
+ unread: ThemeColor[];
41
+ }
42
+
43
+ /** A fill and the label the build puts on it (`labelOn`, as `readableOn`). */
44
+ const fillPair = (
45
+ key: "theme.accent" | "theme.action",
46
+ fill: Rgba,
47
+ page: Rgba,
48
+ mode: ColorMode
49
+ ): TextPair => ({
50
+ fix: `Neither white nor dark text reaches ${MIN_TEXT_CONTRAST}:1 on this shade. Move \`${key}\`'s ${mode} shade lighter or darker.`,
51
+ key,
52
+ label:
53
+ key === "theme.action"
54
+ ? "The call-to-action label"
55
+ : "Button labels on the accent",
56
+ surface: composite(fill, page),
57
+ text: parseColor(labelOn(fill, mode)) ?? fill,
58
+ });
59
+
60
+ /** The accent's pairs: accent text on the page, and labels on accent fills. */
61
+ const accentPairs = (fill: Rgba, page: Rgba, mode: ColorMode): TextPair[] => [
62
+ {
63
+ fix: `${mode === "light" ? "Darken" : "Lighten"} \`theme.accent\`'s ${mode} shade (\`theme.accent: { light, dark }\`).`,
64
+ key: "theme.accent",
65
+ label: "Accent text on the page",
66
+ surface: page,
67
+ text: fill,
68
+ },
69
+ fillPair("theme.accent", fill, page, mode),
70
+ ];
71
+
72
+ /** The pairs a custom page background sets: body and secondary text on it. */
73
+ const backgroundPairs = (page: Rgba, mode: ColorMode): TextPair[] => {
74
+ const fix = `Pick a \`theme.background\` ${mode} color further from the text color.`;
75
+ return [
76
+ {
77
+ fix,
78
+ key: "theme.background",
79
+ label: "Body text on the page",
80
+ surface: page,
81
+ text: themeInk(mode, "foreground"),
82
+ },
83
+ {
84
+ fix,
85
+ key: "theme.background",
86
+ label: "Secondary text on the page",
87
+ surface: page,
88
+ text: themeInk(mode, "mutedForeground"),
89
+ },
90
+ ];
91
+ };
92
+
93
+ /** Everything one mode draws text in, from the theme config. */
94
+ const modeColors = (
95
+ theme: ResolvedConfig["theme"],
96
+ mode: ColorMode
97
+ ): ModeColors => {
98
+ const unread: ThemeColor[] = [];
99
+ const read = (color: ThemeColor): Rgba | null => {
100
+ const parsed = parseColor(color.value);
101
+ if (!parsed) {
102
+ unread.push(color);
103
+ }
104
+ return parsed;
105
+ };
106
+ const custom = theme.background?.[mode];
107
+ const customPage =
108
+ custom === undefined
109
+ ? null
110
+ : read({ key: "theme.background", value: custom });
111
+ // Text on a page color the audit can't read can't be measured.
112
+ if (custom !== undefined && !customPage) {
113
+ return { pairs: [], unread };
114
+ }
115
+ const page = customPage ?? themeInk(mode, "background");
116
+ const pairs = customPage ? backgroundPairs(customPage, mode) : [];
117
+ const accent = read({
118
+ key: "theme.accent",
119
+ value: resolveAccent(theme)[mode],
120
+ });
121
+ if (accent) {
122
+ pairs.push(...accentPairs(accent, page, mode));
123
+ }
124
+ const action = resolveAction(theme);
125
+ const actionFill =
126
+ action && read({ key: "theme.action", value: action[mode] });
127
+ if (actionFill) {
128
+ pairs.push(fillPair("theme.action", actionFill, page, mode));
129
+ }
130
+ return { pairs, unread };
131
+ };
132
+
133
+ /**
134
+ * Theme contrast: the text colors a configured theme draws — accent text,
135
+ * labels on accent and call-to-action fills, and body text on a custom
136
+ * background — measured against WCAG AA's 4.5:1 in both color modes, resolved
137
+ * the way the build resolves them (presets per mode, labels by `labelOn`).
138
+ * One finding per failing pair, pointed at blume.config.ts, since a theme
139
+ * color is a site-wide choice, not a page's.
140
+ */
141
+ export const contrastChecks: CheckModule = {
142
+ category: "accessibility",
143
+ run(context) {
144
+ const { theme } = context.project.config;
145
+ const site = {
146
+ file: context.project.context.configFile ?? undefined,
147
+ url: "/",
148
+ };
149
+ const found: Diagnostic[] = [];
150
+ const reported = new Set<string>();
151
+ for (const mode of MODES) {
152
+ const { pairs, unread } = modeColors(theme, mode);
153
+ for (const color of unread) {
154
+ const id = `${color.key}:${color.value}`;
155
+ if (!reported.has(id)) {
156
+ reported.add(id);
157
+ found.push(
158
+ finding(
159
+ "BLUME_AUDIT_THEME_COLOR_UNCHECKED",
160
+ site,
161
+ `\`${color.key}\` is "${color.value}", which the audit can't read as a color, so its contrast went unchecked.`
162
+ )
163
+ );
164
+ }
165
+ }
166
+ for (const pair of pairs) {
167
+ const ratio = contrastRatio(pair.text, pair.surface);
168
+ if (ratio < MIN_TEXT_CONTRAST) {
169
+ found.push(
170
+ finding(
171
+ "BLUME_AUDIT_THEME_CONTRAST_LOW",
172
+ site,
173
+ `${pair.label} in ${mode} mode is ${ratio.toFixed(2)}:1, under the ${MIN_TEXT_CONTRAST}:1 WCAG AA asks of body text (\`${pair.key}\`).`,
174
+ pair.fix
175
+ )
176
+ );
177
+ }
178
+ }
179
+ }
180
+ return found;
181
+ },
182
+ tier: "static",
183
+ };
@@ -2,6 +2,7 @@ import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
2
2
  import type { Diagnostic } from "../../core/types.ts";
3
3
  import { finding } from "../catalog.ts";
4
4
  import { pageSite } from "../locate.ts";
5
+ import { redirectAt } from "../redirects.ts";
5
6
  import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
6
7
  import { decodePath, normalizePath } from "../url.ts";
7
8
  import { isArchivedLatestCanonical } from "./indexability.ts";
@@ -134,9 +135,7 @@ const hreflangChecks = (
134
135
  }
135
136
 
136
137
  const target = context.byUrl.get(path);
137
- const redirect = context.redirects.find(
138
- (entry) => normalizePath(entry.from) === path
139
- );
138
+ const redirect = redirectAt(context.redirects, path);
140
139
  if (redirect) {
141
140
  found.push(
142
141
  finding(
@@ -3,6 +3,7 @@ import type { Diagnostic } from "../../core/types.ts";
3
3
  import { deployPlatform } from "../../deploy/platforms/index.ts";
4
4
  import { finding } from "../catalog.ts";
5
5
  import { pageSite } from "../locate.ts";
6
+ import { redirectAt } from "../redirects.ts";
6
7
  import { ERROR_ROUTES } from "../types.ts";
7
8
  import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
8
9
  import { decodePath, normalizePath, siteOrigin } from "../url.ts";
@@ -118,9 +119,7 @@ const canonicalChecks = (
118
119
 
119
120
  // The canonical points at another page on this site. It must exist, and it
120
121
  // must not itself redirect — a canonical to a redirect is a dead end.
121
- const redirect = context.redirects.find(
122
- (entry) => normalizePath(entry.from) === target
123
- );
122
+ const redirect = redirectAt(context.redirects, target);
124
123
  if (redirect) {
125
124
  found.push(
126
125
  finding(
@@ -3,13 +3,9 @@ import type { Diagnostic } from "../../core/types.ts";
3
3
  import { finding } from "../catalog.ts";
4
4
  import { orphanPages } from "../graph.ts";
5
5
  import { pageSite } from "../locate.ts";
6
- import type {
7
- AuditContext,
8
- CheckModule,
9
- PageSnapshot,
10
- SnapshotLink,
11
- } from "../types.ts";
12
- import { isServed, normalizePath, resolveHref, siteOrigin } from "../url.ts";
6
+ import { redirectAt } from "../redirects.ts";
7
+ import type { CheckModule, PageSnapshot, SnapshotLink } from "../types.ts";
8
+ import { isServed, resolveHref, siteOrigin } from "../url.ts";
13
9
 
14
10
  /** Browser-magic fragments that scroll without needing a matching id. */
15
11
  const MAGIC_FRAGMENTS = new Set(["", "top"]);
@@ -33,9 +29,6 @@ const anchorResolves = (target: PageSnapshot, fragment: string): boolean => {
33
29
  }
34
30
  };
35
31
 
36
- const redirectFrom = (context: AuditContext, path: string) =>
37
- context.redirects.find((entry) => normalizePath(entry.from) === path);
38
-
39
32
  /**
40
33
  * Internal links, and what they land on.
41
34
  *
@@ -108,6 +101,12 @@ export const linkChecks: CheckModule = {
108
101
  return;
109
102
  }
110
103
  if (resolved.kind === "outside-base") {
104
+ // A full URL on this host outside the base names another app there
105
+ // (the navigation docs say to link one that way), not a page of this
106
+ // build that lost its base.
107
+ if (resolved.absolute) {
108
+ return;
109
+ }
111
110
  if (link.content) {
112
111
  found.push(
113
112
  finding(
@@ -150,7 +149,7 @@ export const linkChecks: CheckModule = {
150
149
  }
151
150
 
152
151
  const { path } = resolved;
153
- const redirect = redirectFrom(context, path);
152
+ const redirect = redirectAt(context.redirects, path);
154
153
  if (redirect) {
155
154
  if (link.content) {
156
155
  found.push(
@@ -200,7 +199,7 @@ export const linkChecks: CheckModule = {
200
199
  );
201
200
  }
202
201
  for (const [path, page] of redirectedChrome) {
203
- const redirect = redirectFrom(context, path);
202
+ const redirect = redirectAt(context.redirects, path);
204
203
  found.push(
205
204
  finding(
206
205
  "BLUME_AUDIT_LINK_TO_REDIRECT",
@@ -222,11 +221,18 @@ export const linkChecks: CheckModule = {
222
221
 
223
222
  const homeUrl = normalizeBasePath(context.project.config.basePath) || "/";
224
223
  for (const page of orphanPages(context.pages, context.graph, homeUrl)) {
224
+ // A `sidebar.hidden` page is in no other page's navigation either, so
225
+ // it isn't reachable from the sidebar at all.
226
+ const fromNavigation = [
227
+ ...(context.graph.chromeIn.get(page.url) ?? []),
228
+ ].some((from) => from !== page.url);
225
229
  found.push(
226
230
  finding(
227
231
  "BLUME_AUDIT_ORPHAN_PAGE",
228
232
  pageSite(context, page),
229
- "No other page's body links here — it is reachable only from the sidebar."
233
+ fromNavigation
234
+ ? "No other page's body links here — it is reachable only from the sidebar."
235
+ : "No page links here, from its body or from navigation, so readers and crawlers can't reach it."
230
236
  )
231
237
  );
232
238
  }
@@ -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;