blume 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (601) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +113 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-6k4ftwze.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-6k4ftwze.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-zp79m0ts.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-ddndchfr.js → chunk-2f3ndkpw.js} +57 -29
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-fsmrqk8a.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-ppzjqwx2.js → chunk-2n4ps1b3.js} +17 -18
  12. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-2n4ps1b3.js.map} +3 -3
  13. package/dist/cli/{chunk-5r8g91qn.js → chunk-43zcf4gz.js} +398 -159
  14. package/dist/cli/chunk-43zcf4gz.js.map +36 -0
  15. package/dist/cli/chunk-4ejxbzee.js +39 -0
  16. package/dist/cli/chunk-4ejxbzee.js.map +10 -0
  17. package/dist/cli/{chunk-hr8ne106.js → chunk-4h8z9vv5.js} +296 -77
  18. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  19. package/dist/cli/{chunk-k7pj68a8.js → chunk-5cg2rzgf.js} +18 -16
  20. package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5cg2rzgf.js.map} +2 -2
  21. package/dist/cli/{chunk-h2ez8dzb.js → chunk-5ffywsmb.js} +4 -4
  22. package/dist/cli/{chunk-g698a744.js → chunk-5wj2qwn2.js} +5 -5
  23. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  24. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  25. package/dist/cli/{chunk-3yce002v.js → chunk-713mngdq.js} +15 -8
  26. package/dist/cli/chunk-713mngdq.js.map +14 -0
  27. package/dist/cli/{chunk-91ws1n6j.js → chunk-9geymqns.js} +17 -15
  28. package/dist/cli/{chunk-91ws1n6j.js.map → chunk-9geymqns.js.map} +2 -2
  29. package/dist/cli/{chunk-6dtt0zfn.js → chunk-9xr4dgee.js} +27 -18
  30. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  31. package/dist/cli/{chunk-sqw4ekg1.js → chunk-amrb6hgh.js} +2 -2
  32. package/dist/cli/chunk-cm89scdv.js +443 -0
  33. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  34. package/dist/cli/{chunk-wdrt2k2v.js → chunk-cmtcnh9f.js} +90 -26
  35. package/dist/cli/chunk-cmtcnh9f.js.map +10 -0
  36. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  37. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  38. package/dist/cli/{chunk-7mbqtmgb.js → chunk-e7vvk5ya.js} +21 -12
  39. package/dist/cli/chunk-e7vvk5ya.js.map +10 -0
  40. package/dist/cli/{chunk-qkb5a8sa.js → chunk-g2z9x01v.js} +6 -98
  41. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  42. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  43. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  44. package/dist/cli/{chunk-h7k3nq3v.js → chunk-gp84za0h.js} +2 -2
  45. package/dist/cli/{chunk-hqp2ajnh.js → chunk-h2a48yhd.js} +195 -73
  46. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  47. package/dist/cli/{chunk-pbg5a4s3.js → chunk-jnwj5kjj.js} +71 -47
  48. package/dist/cli/chunk-jnwj5kjj.js.map +19 -0
  49. package/dist/cli/chunk-kwrg63tm.js +31 -0
  50. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  51. package/dist/cli/{chunk-8g8ytmgx.js → chunk-nd1821sk.js} +21 -19
  52. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-nd1821sk.js.map} +2 -2
  53. package/dist/cli/{chunk-w4bxdvsa.js → chunk-ntv4qrt7.js} +17 -15
  54. package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-ntv4qrt7.js.map} +2 -2
  55. package/dist/cli/{chunk-bfwp9vp6.js → chunk-p804f3pe.js} +7 -7
  56. package/dist/cli/{chunk-4e9b9ra6.js → chunk-py62fewh.js} +6 -6
  57. package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-py62fewh.js.map} +1 -1
  58. package/dist/cli/chunk-q3wxxq39.js +126 -0
  59. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  60. package/dist/cli/{chunk-j85scx15.js → chunk-rekqz4qg.js} +142 -46
  61. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  62. package/dist/cli/{chunk-00gs3wqs.js → chunk-ta27feev.js} +1 -1
  63. package/dist/cli/{chunk-v6ya5kcb.js → chunk-xkz5xbtv.js} +4031 -1132
  64. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  65. package/dist/cli/{chunk-bbnwccaz.js → chunk-y4ykbark.js} +2 -2
  66. package/dist/cli/{chunk-n1yg3tj3.js → chunk-y5h4r2y0.js} +6 -6
  67. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-y5h4r2y0.js.map} +3 -3
  68. package/dist/cli/{chunk-nfcyttvj.js → chunk-yj17vs39.js} +9 -9
  69. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-yj17vs39.js.map} +1 -1
  70. package/dist/cli/{chunk-7vtckvaw.js → chunk-yxmk4pec.js} +16 -14
  71. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-yxmk4pec.js.map} +2 -2
  72. package/dist/cli/{chunk-gs7r695n.js → chunk-zccbjcj0.js} +22 -8
  73. package/dist/cli/{chunk-gs7r695n.js.map → chunk-zccbjcj0.js.map} +3 -3
  74. package/dist/cli/{chunk-xh43dwgw.js → chunk-zf748avv.js} +415 -225
  75. package/dist/cli/chunk-zf748avv.js.map +37 -0
  76. package/dist/cli/{chunk-d1v5rhy0.js → chunk-zhvd6adm.js} +16 -14
  77. package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-zhvd6adm.js.map} +2 -2
  78. package/dist/cli/index.js +28 -20
  79. package/dist/cli/index.js.map +5 -5
  80. package/dist/types/ai/agent-surface.d.ts +8 -7
  81. package/dist/types/ai/ai-catalog.d.ts +7 -0
  82. package/dist/types/ai/ask-context.d.ts +22 -0
  83. package/dist/types/ai/ask.d.ts +271 -58
  84. package/dist/types/ai/index.d.ts +2 -2
  85. package/dist/types/ai/link-headers.d.ts +8 -1
  86. package/dist/types/ai/openapi-components.d.ts +5 -2
  87. package/dist/types/ai/relative-links.d.ts +9 -7
  88. package/dist/types/ai/site-skill.d.ts +26 -0
  89. package/dist/types/ai/skills.d.ts +20 -1
  90. package/dist/types/ai/tar.d.ts +1 -3
  91. package/dist/types/analytics/index.d.ts +2 -0
  92. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  93. package/dist/types/analytics/schema.d.ts +14 -0
  94. package/dist/types/astro/integration.d.ts +11 -2
  95. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  96. package/dist/types/captcha/index.d.ts +28 -0
  97. package/dist/types/captcha/schema.d.ts +29 -0
  98. package/dist/types/captcha/turnstile.d.ts +28 -0
  99. package/dist/types/components/content/api-field.d.ts +59 -0
  100. package/dist/types/components/content/api-page.d.ts +95 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/helpers.d.ts +137 -0
  103. package/dist/types/components/openapi/security.d.ts +85 -0
  104. package/dist/types/consent/ethyca.d.ts +48 -0
  105. package/dist/types/consent/index.d.ts +37 -0
  106. package/dist/types/consent/native.d.ts +28 -0
  107. package/dist/types/consent/osano.d.ts +42 -0
  108. package/dist/types/consent/schema.d.ts +110 -0
  109. package/dist/types/core/base-path.d.ts +21 -7
  110. package/dist/types/core/config-input.d.ts +196 -27
  111. package/dist/types/core/data.d.ts +49 -4
  112. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  113. package/dist/types/core/footer.d.ts +8 -0
  114. package/dist/types/core/heading-markers.d.ts +5 -7
  115. package/dist/types/core/i18n-ui.d.ts +86 -0
  116. package/dist/types/core/i18n.d.ts +2 -0
  117. package/dist/types/core/includes.d.ts +8 -2
  118. package/dist/types/core/last-modified.d.ts +10 -0
  119. package/dist/types/core/localizable.d.ts +7 -0
  120. package/dist/types/core/meta.d.ts +8 -0
  121. package/dist/types/core/page-modes.d.ts +29 -0
  122. package/dist/types/core/redirect-patterns.d.ts +144 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +558 -56
  125. package/dist/types/core/sources/lower.d.ts +29 -16
  126. package/dist/types/core/sources/normalize.d.ts +13 -1
  127. package/dist/types/core/sources/read.d.ts +5 -0
  128. package/dist/types/core/sources/watch.d.ts +12 -5
  129. package/dist/types/core/standard-schema.d.ts +5 -0
  130. package/dist/types/core/types.d.ts +1 -1
  131. package/dist/types/core/variables.d.ts +42 -0
  132. package/dist/types/deploy/artifacts.d.ts +6 -4
  133. package/dist/types/deploy/headers.d.ts +5 -0
  134. package/dist/types/deploy/node-headers.d.ts +13 -2
  135. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  136. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  137. package/dist/types/deploy/platforms/types.d.ts +8 -0
  138. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  139. package/dist/types/deploy/redirects.d.ts +61 -22
  140. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  141. package/dist/types/markdown/code-title.d.ts +17 -0
  142. package/dist/types/markdown/directives.d.ts +62 -0
  143. package/dist/types/markdown/features.d.ts +24 -2
  144. package/dist/types/markdown/mdast.d.ts +63 -0
  145. package/dist/types/narration/build.d.ts +70 -0
  146. package/dist/types/narration/provider.d.ts +26 -0
  147. package/dist/types/narration/script.d.ts +154 -0
  148. package/dist/types/openapi/asyncapi.d.ts +4 -2
  149. package/dist/types/openapi/model.d.ts +12 -6
  150. package/dist/types/openapi/overlay.d.ts +54 -0
  151. package/dist/types/openapi/parse.d.ts +13 -0
  152. package/dist/types/openapi/references.d.ts +14 -4
  153. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  154. package/dist/types/ratelimit/index.d.ts +27 -0
  155. package/dist/types/ratelimit/memory.d.ts +36 -0
  156. package/dist/types/ratelimit/schema.d.ts +111 -0
  157. package/dist/types/ratelimit/upstash.d.ts +21 -0
  158. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  159. package/dist/types/reference/asyncapi.d.ts +10 -10
  160. package/dist/types/reference/graphql.d.ts +13 -10
  161. package/dist/types/reference/openapi.d.ts +33 -12
  162. package/dist/types/reference/options.d.ts +45 -5
  163. package/dist/types/reference/scalar.d.ts +13 -0
  164. package/dist/types/reference/schema.d.ts +60 -36
  165. package/dist/types/search/documents.d.ts +12 -1
  166. package/dist/types/search/orama-index.d.ts +4 -0
  167. package/dist/types/seo/metatags.d.ts +20 -0
  168. package/docs/02-deployment.mdx +44 -6
  169. package/docs/03-upgrading.mdx +4 -4
  170. package/docs/04-migrating.mdx +1 -1
  171. package/docs/08-faq.mdx +1 -1
  172. package/docs/advanced/changelog.mdx +2 -2
  173. package/docs/advanced/custom-pages.mdx +3 -3
  174. package/docs/advanced/skills.mdx +14 -0
  175. package/docs/cli/audit.mdx +38 -2
  176. package/docs/cli/evals.mdx +3 -3
  177. package/docs/cli/index.mdx +1 -0
  178. package/docs/cli/translate.mdx +3 -3
  179. package/docs/cli/version.mdx +1 -1
  180. package/docs/configuration/analytics.mdx +22 -1
  181. package/docs/configuration/assistant.mdx +188 -36
  182. package/docs/configuration/consent.mdx +100 -0
  183. package/docs/configuration/customization.mdx +6 -4
  184. package/docs/configuration/index.mdx +63 -5
  185. package/docs/configuration/meta.ts +3 -0
  186. package/docs/configuration/narration.mdx +119 -0
  187. package/docs/configuration/rate-limiting.mdx +69 -0
  188. package/docs/configuration/search.mdx +30 -2
  189. package/docs/configuration/theming.mdx +3 -1
  190. package/docs/content/components.mdx +107 -1
  191. package/docs/content/frontmatter.mdx +77 -1
  192. package/docs/content/i18n.mdx +40 -3
  193. package/docs/content/includes.mdx +16 -0
  194. package/docs/content/index.mdx +4 -2
  195. package/docs/content/islands.mdx +1 -1
  196. package/docs/content/meta.mdx +4 -1
  197. package/docs/content/meta.ts +1 -0
  198. package/docs/content/navigation.mdx +28 -6
  199. package/docs/content/sources/contentful.mdx +32 -0
  200. package/docs/content/sources/custom.mdx +39 -0
  201. package/docs/content/sources/github-releases.mdx +31 -0
  202. package/docs/content/sources/index.mdx +91 -0
  203. package/docs/content/sources/meta.ts +16 -0
  204. package/docs/content/sources/notion.mdx +32 -0
  205. package/docs/content/sources/obsidian.mdx +37 -0
  206. package/docs/content/sources/payload.mdx +30 -0
  207. package/docs/content/sources/remote-mdx.mdx +40 -0
  208. package/docs/content/sources/sanity.mdx +29 -0
  209. package/docs/content/sources/strapi.mdx +31 -0
  210. package/docs/content/syntax.mdx +81 -2
  211. package/docs/content/variables.mdx +41 -0
  212. package/docs/content/versioning.mdx +1 -1
  213. package/docs/discoverability/agent-discovery.mdx +30 -3
  214. package/docs/discoverability/index.mdx +3 -2
  215. package/docs/discoverability/llms-txt.mdx +1 -1
  216. package/docs/discoverability/markdown.mdx +2 -2
  217. package/docs/discoverability/metadata.mdx +17 -0
  218. package/docs/discoverability/open-graph.mdx +5 -3
  219. package/docs/discoverability/rss.mdx +1 -1
  220. package/docs/references/api-pages.mdx +74 -0
  221. package/docs/references/asyncapi.mdx +2 -2
  222. package/docs/references/graphql.mdx +3 -3
  223. package/docs/references/meta.ts +1 -1
  224. package/docs/references/openapi.mdx +106 -5
  225. package/docs/references/scalar.mdx +3 -1
  226. package/package.json +36 -1
  227. package/skills/blume/SKILL.md +3 -0
  228. package/skills/blume-migrate/SKILL.md +5 -5
  229. package/skills/blume-migrate/references/docusaurus.md +3 -3
  230. package/skills/blume-migrate/references/fumadocs.md +1 -1
  231. package/skills/blume-migrate/references/mintlify.md +37 -19
  232. package/skills/blume-migrate/references/nextra.md +3 -3
  233. package/skills/blume-migrate/references/starlight.md +2 -1
  234. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +67 -16
  235. package/skills/blume-write-skill/SKILL.md +82 -0
  236. package/src/ai/agent-readability.ts +4 -3
  237. package/src/ai/agent-surface.ts +9 -7
  238. package/src/ai/ai-catalog.ts +12 -5
  239. package/src/ai/api/handlers.ts +2 -2
  240. package/src/ai/api/spec.ts +2 -2
  241. package/src/ai/api-catalog.ts +6 -2
  242. package/src/ai/ask-context.ts +73 -20
  243. package/src/ai/ask-data.ts +6 -0
  244. package/src/ai/ask-tools.ts +142 -0
  245. package/src/ai/ask.ts +354 -76
  246. package/src/ai/changelog-markdown.ts +2 -2
  247. package/src/ai/component-markdown.ts +75 -0
  248. package/src/ai/cors.ts +2 -0
  249. package/src/ai/index.ts +12 -0
  250. package/src/ai/link-headers.ts +14 -5
  251. package/src/ai/llms.ts +24 -9
  252. package/src/ai/markdown.ts +2 -1
  253. package/src/ai/mcp/data.ts +6 -0
  254. package/src/ai/mcp/discovery.ts +2 -2
  255. package/src/ai/mcp/query.ts +2 -2
  256. package/src/ai/mcp/server.ts +2 -2
  257. package/src/ai/openapi-components.ts +31 -9
  258. package/src/ai/relative-links.ts +42 -26
  259. package/src/ai/serializers.ts +2 -1
  260. package/src/ai/site-skill.ts +274 -0
  261. package/src/ai/skills.ts +49 -3
  262. package/src/ai/tar.ts +139 -12
  263. package/src/ai/write-skill.ts +97 -0
  264. package/src/analytics/head.ts +4 -0
  265. package/src/analytics/index.ts +5 -0
  266. package/src/analytics/one-dollar-stats.ts +86 -0
  267. package/src/analytics/schema.ts +2 -0
  268. package/src/astro/generate.ts +133 -19
  269. package/src/astro/include-hmr.ts +31 -12
  270. package/src/astro/include-refresh.ts +19 -8
  271. package/src/astro/index.ts +1 -0
  272. package/src/astro/integration.ts +143 -14
  273. package/src/astro/module-types.ts +6 -0
  274. package/src/astro/runtime-modules.ts +28 -13
  275. package/src/astro/templates.ts +364 -67
  276. package/src/astro/variables.ts +31 -0
  277. package/src/audit/catalog.ts +19 -0
  278. package/src/audit/checks/contrast.ts +183 -0
  279. package/src/audit/checks/i18n.ts +2 -3
  280. package/src/audit/checks/indexability.ts +2 -3
  281. package/src/audit/checks/links.ts +13 -13
  282. package/src/audit/checks/sitemap.ts +2 -3
  283. package/src/audit/graph.ts +3 -1
  284. package/src/audit/redirects.ts +46 -15
  285. package/src/audit/run.ts +14 -9
  286. package/src/audit/snapshot.ts +5 -0
  287. package/src/audit/types.ts +2 -1
  288. package/src/blume-modules.d.ts +7 -0
  289. package/src/captcha/client.ts +152 -0
  290. package/src/captcha/hcaptcha.ts +36 -0
  291. package/src/captcha/index.ts +28 -0
  292. package/src/captcha/schema.ts +32 -0
  293. package/src/captcha/turnstile.ts +38 -0
  294. package/src/captcha/verify.ts +71 -0
  295. package/src/cli/args.ts +32 -0
  296. package/src/cli/command-meta.ts +5 -0
  297. package/src/cli/commands/audit.ts +4 -1
  298. package/src/cli/commands/doctor.ts +25 -17
  299. package/src/cli/commands/eval.ts +19 -11
  300. package/src/cli/commands/skill.ts +96 -0
  301. package/src/cli/commands/translate.ts +9 -12
  302. package/src/cli/commands/validate.ts +3 -1
  303. package/src/cli/dev-lock.ts +157 -37
  304. package/src/cli/index.ts +5 -0
  305. package/src/cli/report-format.ts +11 -6
  306. package/src/cli/required-secrets.ts +71 -29
  307. package/src/components/BlumePage.astro +2 -0
  308. package/src/components/colors.ts +19 -0
  309. package/src/components/content/ApiEndpoint.astro +18 -0
  310. package/src/components/content/ApiField.astro +79 -0
  311. package/src/components/content/ApiPlayground.astro +62 -0
  312. package/src/components/content/ApiRail.astro +19 -0
  313. package/src/components/content/Badge.astro +5 -3
  314. package/src/components/content/Color.astro +1 -0
  315. package/src/components/content/Component.astro +2 -2
  316. package/src/components/content/FileTree.astro +1 -0
  317. package/src/components/content/GithubInfo.astro +1 -0
  318. package/src/components/content/Panel.astro +1 -0
  319. package/src/components/content/ParamField.astro +25 -0
  320. package/src/components/content/RequestExample.astro +17 -0
  321. package/src/components/content/ResponseExample.astro +17 -0
  322. package/src/components/content/ResponseField.astro +32 -0
  323. package/src/components/content/Step.astro +1 -0
  324. package/src/components/content/Tab.astro +0 -1
  325. package/src/components/content/Tabs.astro +4 -0
  326. package/src/components/content/Tile.astro +1 -0
  327. package/src/components/content/Tooltip.astro +1 -0
  328. package/src/components/content/Tree.astro +1 -0
  329. package/src/components/content/TypeTable.astro +4 -1
  330. package/src/components/content/View.astro +21 -0
  331. package/src/components/content/ViewSwitcher.astro +168 -0
  332. package/src/components/content/api-field.ts +100 -0
  333. package/src/components/content/api-page.ts +224 -0
  334. package/src/components/content/badge-color.ts +5 -2
  335. package/src/components/content/base-href.ts +13 -36
  336. package/src/components/content/view-switcher.ts +71 -0
  337. package/src/components/index.ts +2 -0
  338. package/src/components/islands/Assistant.astro +10 -1
  339. package/src/components/islands/assistant.tsx +125 -18
  340. package/src/components/islands/base-path.ts +47 -10
  341. package/src/components/islands/code-question.ts +62 -0
  342. package/src/components/islands/hooks.ts +145 -40
  343. package/src/components/islands/support-link.ts +68 -0
  344. package/src/components/islands/webmcp.ts +4 -2
  345. package/src/components/layout/Analytics.astro +11 -2
  346. package/src/components/layout/Banner.astro +14 -10
  347. package/src/components/layout/Breadcrumbs.astro +11 -4
  348. package/src/components/layout/ConsentBanner.astro +62 -0
  349. package/src/components/layout/ConsentHead.astro +38 -0
  350. package/src/components/layout/Directory.astro +94 -0
  351. package/src/components/layout/DiscoveryLinks.astro +9 -5
  352. package/src/components/layout/DrawerSwitchers.astro +51 -0
  353. package/src/components/layout/Empty.astro +2 -2
  354. package/src/components/layout/Header.astro +124 -82
  355. package/src/components/layout/LanguageSwitcher.astro +50 -17
  356. package/src/components/layout/NarrationPlayer.astro +1085 -0
  357. package/src/components/layout/NavSelector.astro +35 -9
  358. package/src/components/layout/NavTabMenu.astro +3 -3
  359. package/src/components/layout/NavTree.astro +16 -6
  360. package/src/components/layout/PageActions.astro +4 -3
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +78 -9
  363. package/src/components/layout/Pagination.astro +3 -3
  364. package/src/components/layout/ReferenceLayout.astro +17 -6
  365. package/src/components/layout/RelatedPages.astro +42 -0
  366. package/src/components/layout/RootLayout.astro +313 -50
  367. package/src/components/layout/Search.astro +64 -30
  368. package/src/components/layout/SiteFooter.astro +124 -0
  369. package/src/components/layout/SiteMetatags.astro +16 -0
  370. package/src/components/layout/VersionBanner.astro +2 -2
  371. package/src/components/layout/analytics-client.ts +12 -0
  372. package/src/components/layout/consent/native.ts +79 -0
  373. package/src/components/layout/consent/types.ts +37 -0
  374. package/src/components/layout/footer-links.ts +53 -0
  375. package/src/components/layout/footer-socials.ts +87 -0
  376. package/src/components/layout/locale-redirect.ts +107 -0
  377. package/src/components/layout/nav-utils.ts +83 -0
  378. package/src/components/layout/page-feedback.ts +96 -0
  379. package/src/components/layout/search/analytics.ts +98 -0
  380. package/src/components/layout/search/flexsearch.ts +17 -5
  381. package/src/components/layout/search/orama-cloud.ts +26 -14
  382. package/src/components/layout/search/types.ts +20 -0
  383. package/src/components/layout/search/typesense.ts +5 -1
  384. package/src/components/layout/toc-active.ts +41 -0
  385. package/src/components/layout/toc-element.ts +8 -14
  386. package/src/components/openapi/ApiTagOperations.astro +2 -2
  387. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  388. package/src/components/openapi/GraphqlChip.astro +2 -2
  389. package/src/components/openapi/Operation.astro +163 -13
  390. package/src/components/openapi/Playground.astro +4 -4
  391. package/src/components/openapi/RequestBody.astro +6 -4
  392. package/src/components/openapi/RequestPanel.astro +63 -8
  393. package/src/components/openapi/Responses.astro +6 -4
  394. package/src/components/openapi/SchemaTable.astro +7 -0
  395. package/src/components/openapi/async-snippets.ts +4 -4
  396. package/src/components/openapi/async.ts +38 -6
  397. package/src/components/openapi/code-samples.ts +97 -0
  398. package/src/components/openapi/helpers.ts +19 -9
  399. package/src/components/openapi/message-composer.ts +6 -1
  400. package/src/components/openapi/message.ts +17 -2
  401. package/src/components/openapi/operation-model.ts +81 -11
  402. package/src/components/openapi/panel.ts +4 -2
  403. package/src/components/openapi/param-style.ts +181 -0
  404. package/src/components/openapi/playground-client.ts +73 -16
  405. package/src/components/openapi/request.ts +190 -26
  406. package/src/components/openapi/schema-tree.ts +30 -24
  407. package/src/components/openapi/security.ts +6 -3
  408. package/src/components/openapi/snippets.ts +953 -13
  409. package/src/components/openapi/ws-client.ts +18 -2
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/base-path.ts +62 -16
  421. package/src/core/builtin-tags.ts +5 -0
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/config-input.ts +203 -27
  424. package/src/core/data.ts +44 -4
  425. package/src/core/diagnostics.ts +5 -2
  426. package/src/core/directive-diagnostics.ts +99 -0
  427. package/src/core/footer.ts +26 -0
  428. package/src/core/frontmatter.ts +21 -18
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/heading-markers.ts +5 -18
  431. package/src/core/i18n-ui.ts +82 -2
  432. package/src/core/i18n.ts +31 -3
  433. package/src/core/includes.ts +20 -3
  434. package/src/core/last-modified.ts +25 -3
  435. package/src/core/links.ts +16 -2
  436. package/src/core/locale-links.ts +5 -1
  437. package/src/core/localizable.ts +26 -0
  438. package/src/core/meta.ts +33 -19
  439. package/src/core/navigation.ts +71 -20
  440. package/src/core/page-modes.ts +56 -0
  441. package/src/core/project-graph.ts +138 -10
  442. package/src/core/redirect-patterns.ts +491 -0
  443. package/src/core/related.ts +131 -0
  444. package/src/core/schema.ts +342 -68
  445. package/src/core/sources/contentful-rich-text.ts +27 -20
  446. package/src/core/sources/filesystem.ts +20 -2
  447. package/src/core/sources/github-releases.ts +25 -6
  448. package/src/core/sources/lexical.ts +23 -18
  449. package/src/core/sources/lower.ts +201 -34
  450. package/src/core/sources/mdx-remote.ts +51 -16
  451. package/src/core/sources/normalize.ts +327 -91
  452. package/src/core/sources/notion.ts +52 -25
  453. package/src/core/sources/obsidian.ts +23 -6
  454. package/src/core/sources/portable-text.ts +38 -21
  455. package/src/core/sources/read.ts +8 -3
  456. package/src/core/sources/strapi-blocks.ts +20 -16
  457. package/src/core/sources/watch.ts +20 -7
  458. package/src/core/standard-schema.ts +10 -6
  459. package/src/core/types.ts +1 -1
  460. package/src/core/ui-packs/ar.ts +43 -0
  461. package/src/core/ui-packs/bg.ts +45 -0
  462. package/src/core/ui-packs/bn.ts +43 -0
  463. package/src/core/ui-packs/ca.ts +46 -0
  464. package/src/core/ui-packs/cs.ts +44 -0
  465. package/src/core/ui-packs/da.ts +45 -0
  466. package/src/core/ui-packs/de.ts +46 -0
  467. package/src/core/ui-packs/el.ts +45 -0
  468. package/src/core/ui-packs/es.ts +46 -0
  469. package/src/core/ui-packs/fa.ts +45 -0
  470. package/src/core/ui-packs/fi.ts +45 -0
  471. package/src/core/ui-packs/fr.ts +46 -0
  472. package/src/core/ui-packs/he.ts +43 -0
  473. package/src/core/ui-packs/hi.ts +44 -0
  474. package/src/core/ui-packs/hr.ts +45 -0
  475. package/src/core/ui-packs/hu.ts +44 -0
  476. package/src/core/ui-packs/id.ts +45 -0
  477. package/src/core/ui-packs/it.ts +45 -0
  478. package/src/core/ui-packs/ja.ts +45 -0
  479. package/src/core/ui-packs/ko.ts +44 -0
  480. package/src/core/ui-packs/nl.ts +46 -0
  481. package/src/core/ui-packs/no.ts +44 -0
  482. package/src/core/ui-packs/pl.ts +45 -0
  483. package/src/core/ui-packs/pt-br.ts +46 -0
  484. package/src/core/ui-packs/pt.ts +45 -0
  485. package/src/core/ui-packs/ro.ts +45 -0
  486. package/src/core/ui-packs/ru.ts +45 -0
  487. package/src/core/ui-packs/sk.ts +44 -0
  488. package/src/core/ui-packs/sr.ts +45 -0
  489. package/src/core/ui-packs/sv.ts +44 -0
  490. package/src/core/ui-packs/th.ts +43 -0
  491. package/src/core/ui-packs/tr.ts +45 -0
  492. package/src/core/ui-packs/uk.ts +45 -0
  493. package/src/core/ui-packs/vi.ts +45 -0
  494. package/src/core/ui-packs/zh-tw.ts +43 -0
  495. package/src/core/ui-packs/zh.ts +43 -0
  496. package/src/core/variables.ts +103 -0
  497. package/src/core/version-cut.ts +52 -15
  498. package/src/deploy/artifacts.ts +104 -14
  499. package/src/deploy/cloudflare-negotiation.ts +39 -17
  500. package/src/deploy/headers.ts +10 -5
  501. package/src/deploy/node-headers.ts +64 -10
  502. package/src/deploy/platforms/cloudflare.ts +44 -3
  503. package/src/deploy/platforms/netlify.ts +34 -1
  504. package/src/deploy/platforms/node.ts +1 -0
  505. package/src/deploy/platforms/static.ts +1 -0
  506. package/src/deploy/platforms/types.ts +8 -0
  507. package/src/deploy/platforms/vercel.ts +38 -2
  508. package/src/deploy/redirects.ts +139 -36
  509. package/src/deploy/robots.ts +2 -2
  510. package/src/deploy/rss.ts +4 -3
  511. package/src/deploy/sitemap.ts +7 -5
  512. package/src/deploy/vercel-negotiation.ts +36 -0
  513. package/src/markdown/api-rail.ts +259 -0
  514. package/src/markdown/base-links.ts +34 -36
  515. package/src/markdown/code-title.ts +35 -9
  516. package/src/markdown/directives.ts +242 -36
  517. package/src/markdown/features.ts +20 -2
  518. package/src/markdown/fence-meta.ts +2 -0
  519. package/src/markdown/include.ts +11 -1
  520. package/src/markdown/index.ts +35 -11
  521. package/src/markdown/math.ts +4 -4
  522. package/src/markdown/mdast.ts +5 -2
  523. package/src/markdown/relative-links.ts +12 -4
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +136 -11
  531. package/src/og/derive.ts +41 -31
  532. package/src/og/index.ts +1 -0
  533. package/src/openapi/asyncapi.ts +4 -2
  534. package/src/openapi/model.ts +77 -36
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +55 -2
  537. package/src/openapi/proxy.ts +63 -10
  538. package/src/openapi/references.ts +21 -5
  539. package/src/openapi/render-mdx.ts +46 -3
  540. package/src/openapi/scalar.ts +40 -6
  541. package/src/openapi/source.ts +1 -1
  542. package/src/ratelimit/cloudflare.ts +54 -0
  543. package/src/ratelimit/index.ts +30 -0
  544. package/src/ratelimit/memory.ts +45 -0
  545. package/src/ratelimit/runtime.ts +229 -0
  546. package/src/ratelimit/schema.ts +28 -0
  547. package/src/ratelimit/upstash.ts +30 -0
  548. package/src/ratelimit/wrangler.ts +70 -0
  549. package/src/reference/asyncapi.ts +2 -2
  550. package/src/reference/graphql.ts +5 -2
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +51 -7
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +201 -28
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/version-scope.ts +30 -0
  557. package/src/search/documents.ts +93 -28
  558. package/src/search/orama-index.ts +28 -3
  559. package/src/search/pagefind-ranking.ts +39 -0
  560. package/src/search/popular.ts +2 -1
  561. package/src/search/sync/algolia.ts +15 -4
  562. package/src/search/sync/orama-cloud.ts +3 -0
  563. package/src/search/sync/typesense.ts +5 -0
  564. package/src/seo/jsonld.ts +7 -3
  565. package/src/seo/metatags.ts +67 -0
  566. package/src/theme/chrome-icons.ts +1 -0
  567. package/src/theme/color.ts +500 -0
  568. package/src/theme/entry.ts +56 -3
  569. package/src/theme/palette.ts +121 -45
  570. package/src/translate/meta.ts +68 -24
  571. package/src/translate/prompts.ts +1 -0
  572. package/src/translate/run.ts +3 -3
  573. package/src/translate/validate.ts +12 -3
  574. package/src/translate/work-list.ts +56 -17
  575. package/src/upgrade/upgrade.ts +4 -171
  576. package/dist/cli/chunk-3yce002v.js.map +0 -14
  577. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  578. package/dist/cli/chunk-6dtt0zfn.js.map +0 -10
  579. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  580. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  581. package/dist/cli/chunk-esh98wmb.js +0 -23
  582. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  583. package/dist/cli/chunk-hqp2ajnh.js.map +0 -35
  584. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  585. package/dist/cli/chunk-j85scx15.js.map +0 -10
  586. package/dist/cli/chunk-mqc662a6.js +0 -75
  587. package/dist/cli/chunk-mqc662a6.js.map +0 -11
  588. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  589. package/dist/cli/chunk-qkb5a8sa.js.map +0 -10
  590. package/dist/cli/chunk-v6ya5kcb.js.map +0 -189
  591. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  592. package/dist/cli/chunk-xh43dwgw.js.map +0 -36
  593. package/docs/content/sources.mdx +0 -338
  594. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-1n228xqb.js.map} +0 -0
  595. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-5ffywsmb.js.map} +0 -0
  596. /package/dist/cli/{chunk-g698a744.js.map → chunk-5wj2qwn2.js.map} +0 -0
  597. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-amrb6hgh.js.map} +0 -0
  598. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-gp84za0h.js.map} +0 -0
  599. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-p804f3pe.js.map} +0 -0
  600. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-ta27feev.js.map} +0 -0
  601. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-y4ykbark.js.map} +0 -0
@@ -0,0 +1,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
+ });
@@ -498,6 +498,25 @@ export const CHECKS = [
498
498
  title: "Hreflang group has a language conflict",
499
499
  },
500
500
 
501
+ // Accessibility. Theme colors are one per site, not per page: these run
502
+ // on the config, the way the build resolves it.
503
+ {
504
+ category: "accessibility",
505
+ fix: "Pick a darker shade for light mode or a lighter one for dark mode, per mode with `theme.accent: { light, dark }`.",
506
+ id: "BLUME_AUDIT_THEME_CONTRAST_LOW",
507
+ severity: "warning",
508
+ tier: "static",
509
+ title: "Theme colors miss WCAG AA contrast",
510
+ },
511
+ {
512
+ category: "accessibility",
513
+ fix: "Write the color as hex, `rgb()`, `hsl()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, or a named color so its contrast can be checked.",
514
+ id: "BLUME_AUDIT_THEME_COLOR_UNCHECKED",
515
+ severity: "info",
516
+ tier: "static",
517
+ title: "Theme color can't be checked for contrast",
518
+ },
519
+
501
520
  // Assets
502
521
  {
503
522
  category: "assets",
@@ -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,17 +3,16 @@ 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"]);
16
12
 
13
+ /** Where a fragment's directives (text fragments) start. */
14
+ const FRAGMENT_DIRECTIVE = ":~:";
15
+
17
16
  /** Whether a fragment lands on an id of the target page. */
18
17
  const anchorResolves = (target: PageSnapshot, fragment: string): boolean => {
19
18
  if (MAGIC_FRAGMENTS.has(fragment)) {
@@ -30,9 +29,6 @@ const anchorResolves = (target: PageSnapshot, fragment: string): boolean => {
30
29
  }
31
30
  };
32
31
 
33
- const redirectFrom = (context: AuditContext, path: string) =>
34
- context.redirects.find((entry) => normalizePath(entry.from) === path);
35
-
36
32
  /**
37
33
  * Internal links, and what they land on.
38
34
  *
@@ -65,8 +61,12 @@ export const linkChecks: CheckModule = {
65
61
  page: PageSnapshot,
66
62
  link: SnapshotLink,
67
63
  target: PageSnapshot,
68
- fragment: string
64
+ hash: string
69
65
  ): void => {
66
+ // A text fragment (`#:~:text=…`) is a directive the browser strips
67
+ // before it looks for an id; only the part in front of `:~:` names an
68
+ // element, and an empty one scrolls to the text instead.
69
+ const [fragment = ""] = hash.split(FRAGMENT_DIRECTIVE);
70
70
  if (anchorResolves(target, fragment)) {
71
71
  return;
72
72
  }
@@ -143,7 +143,7 @@ export const linkChecks: CheckModule = {
143
143
  }
144
144
 
145
145
  const { path } = resolved;
146
- const redirect = redirectFrom(context, path);
146
+ const redirect = redirectAt(context.redirects, path);
147
147
  if (redirect) {
148
148
  if (link.content) {
149
149
  found.push(
@@ -193,7 +193,7 @@ export const linkChecks: CheckModule = {
193
193
  );
194
194
  }
195
195
  for (const [path, page] of redirectedChrome) {
196
- const redirect = redirectFrom(context, path);
196
+ const redirect = redirectAt(context.redirects, path);
197
197
  found.push(
198
198
  finding(
199
199
  "BLUME_AUDIT_LINK_TO_REDIRECT",
@@ -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 } from "../types.ts";
6
7
  import { decodePath, normalizePath, siteOrigin } from "../url.ts";
7
8
 
@@ -100,9 +101,7 @@ const checkListedUrl = (
100
101
  );
101
102
  const page = context.byUrl.get(path);
102
103
  if (!page) {
103
- const redirect = context.redirects.find(
104
- (entry) => normalizePath(entry.from) === path
105
- );
104
+ const redirect = redirectAt(context.redirects, path);
106
105
  return [
107
106
  finding(
108
107
  "BLUME_AUDIT_SITEMAP_BAD_URL",
@@ -58,7 +58,8 @@ export const buildGraph = (
58
58
  * Non-indexable pages are excluded — a `noindex` page is *meant* to be
59
59
  * unreachable — as is the home page, which is nobody's job to link to. Under a
60
60
  * `basePath` the home page's built URL is the base itself, so the caller names
61
- * it via `homeUrl`.
61
+ * it via `homeUrl`. An i18n fallback copy is excluded too: it stands in for a
62
+ * translation nobody has written yet, so the page to link to is the original.
62
63
  */
63
64
  export const orphanPages = (
64
65
  pages: PageSnapshot[],
@@ -68,6 +69,7 @@ export const orphanPages = (
68
69
  pages.filter(
69
70
  (page) =>
70
71
  page.indexable &&
72
+ page.route?.fallback !== true &&
71
73
  page.url !== "/" &&
72
74
  page.url !== homeUrl &&
73
75
  (graph.contentIn.get(page.url)?.size ?? 0) === 0
@@ -1,3 +1,7 @@
1
+ import {
2
+ isPatternPath,
3
+ patternDestination,
4
+ } from "../core/redirect-patterns.ts";
1
5
  import type { RedirectResolution } from "./types.ts";
2
6
  import { normalizePath } from "./url.ts";
3
7
 
@@ -7,18 +11,6 @@ interface ConfiguredRedirect {
7
11
  status: number;
8
12
  }
9
13
 
10
- /**
11
- * Follow every configured redirect through to its destination, classifying what
12
- * it lands on.
13
- *
14
- * - `loop` — the chain revisits a hop it has already been to. Never resolves.
15
- * - `broken` — the chain ends somewhere the build does not serve.
16
- * - `chain` — it resolves, but through at least one intermediate redirect.
17
- * - `ok` — one hop, straight to a real page.
18
- *
19
- * An external destination (`https://…`) is always `ok`: it's outside the site,
20
- * so there's no local page to check it against.
21
- */
22
14
  /**
23
15
  * A destination's page path: the part before any query string or fragment. A
24
16
  * redirect to `/guide#setup` or `/search?q=x` lands on the `/guide` / `/search`
@@ -30,6 +22,22 @@ const pathOnly = (value: string): string => {
30
22
  return cut === -1 ? value : value.slice(0, cut);
31
23
  };
32
24
 
25
+ /**
26
+ * Follow every configured redirect through to its destination, classifying what
27
+ * it lands on.
28
+ *
29
+ * - `loop` — the chain revisits a hop it has already been to. Never resolves.
30
+ * - `broken` — the chain ends somewhere the build does not serve.
31
+ * - `chain` — it resolves, but through at least one intermediate redirect.
32
+ * - `ok` — one hop, straight to a real page.
33
+ * - `pattern` — a pattern (`/beta/:slug*`), which covers paths rather than
34
+ * naming one, so there is no single chain to walk; {@link redirectAt}
35
+ * resolves a path it covers.
36
+ *
37
+ * An external destination (`https://…`) is always `ok`: it's outside the site,
38
+ * so there's no local page to check it against. A hop onto a path a pattern
39
+ * covers continues through that pattern.
40
+ */
33
41
  export const resolveRedirects = (
34
42
  redirects: readonly ConfiguredRedirect[],
35
43
  /** Whether the build serves a normalized path — see `isServed`. */
@@ -39,9 +47,14 @@ export const resolveRedirects = (
39
47
  for (const redirect of redirects) {
40
48
  byFrom.set(normalizePath(redirect.from), redirect);
41
49
  }
50
+ const hopFrom = (path: string): string | undefined =>
51
+ byFrom.get(path)?.to ?? patternDestination(redirects, path);
42
52
 
43
53
  return redirects.map((redirect) => {
44
54
  const from = normalizePath(redirect.from);
55
+ if (isPatternPath(redirect.from)) {
56
+ return { ...redirect, chain: [from], outcome: "pattern" as const };
57
+ }
45
58
  const chain: string[] = [from];
46
59
  const seen = new Set<string>([from]);
47
60
  let current = redirect.to;
@@ -63,11 +76,11 @@ export const resolveRedirects = (
63
76
  }
64
77
  chain.push(next);
65
78
  seen.add(next);
66
- const hop = byFrom.get(next);
67
- if (!hop) {
79
+ const hop = hopFrom(next);
80
+ if (hop === undefined) {
68
81
  break;
69
82
  }
70
- current = hop.to;
83
+ current = hop;
71
84
  }
72
85
 
73
86
  const destination = chain.at(-1) ?? from;
@@ -84,3 +97,21 @@ export const resolveRedirects = (
84
97
  };
85
98
  });
86
99
  };
100
+
101
+ /**
102
+ * The configured redirect a path takes, for the checks that report a link,
103
+ * canonical, sitemap entry, or `hreflang` pointing through one: the exact
104
+ * redirect from that path, else the pattern covering it, its `to` filled in
105
+ * for this path.
106
+ */
107
+ export const redirectAt = (
108
+ redirects: readonly RedirectResolution[],
109
+ path: string
110
+ ): { to: string } | undefined => {
111
+ const exact = redirects.find((entry) => normalizePath(entry.from) === path);
112
+ if (exact) {
113
+ return exact;
114
+ }
115
+ const to = patternDestination(redirects, path);
116
+ return to === undefined ? undefined : { to };
117
+ };
package/src/audit/run.ts CHANGED
@@ -2,14 +2,17 @@ import { readFile } from "node:fs/promises";
2
2
 
3
3
  import pMap from "p-map";
4
4
 
5
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
5
+ import { normalizeBasePath } from "../core/base-path.ts";
6
+ import { routeSetFor } from "../core/locale-links.ts";
6
7
  import type { BlumeProject } from "../core/project-graph.ts";
7
8
  import type { Diagnostic } from "../core/types.ts";
8
9
  import { deployStaticDir } from "../deploy/adapter-output.ts";
10
+ import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
9
11
  import { CHECKS } from "./catalog.ts";
10
12
  import type { CheckId } from "./catalog.ts";
11
13
  import { assetChecks } from "./checks/assets.ts";
12
14
  import { contentChecks } from "./checks/content.ts";
15
+ import { contrastChecks } from "./checks/contrast.ts";
13
16
  import { dnsAidChecks } from "./checks/dns-aid.ts";
14
17
  import { duplicateChecks } from "./checks/duplicates.ts";
15
18
  import { i18nChecks } from "./checks/i18n.ts";
@@ -49,6 +52,7 @@ const MODULES: CheckModule[] = [
49
52
  ogImageChecks,
50
53
  i18nChecks,
51
54
  assetChecks,
55
+ contrastChecks,
52
56
  sitemapChecks,
53
57
  robotsChecks,
54
58
  llmsChecks,
@@ -162,14 +166,15 @@ export const runAudit = async (options: AuditOptions): Promise<AuditResult> => {
162
166
  redirects: resolveRedirects(
163
167
  // Redirects are authored as if mounted at root; the built page URLs they
164
168
  // are checked against carry `basePath` (it's a real directory in the
165
- // build), so both sides gain it here — mirroring what
166
- // `applyBaseToAstroRedirects` does at build time. `withBasePath` is
167
- // idempotent and leaves external `to` URLs untouched.
168
- project.config.redirects.map((redirect) => ({
169
- ...redirect,
170
- from: withBasePath(basePath, redirect.from),
171
- to: withBasePath(basePath, redirect.to),
172
- })),
169
+ // build), so both sides gain it here, based exactly as the build bases
170
+ // them — a `to` naming a public file stays at the root, and external
171
+ // `to` URLs pass through.
172
+ applyBaseToAstroRedirects(
173
+ project.config.redirects,
174
+ basePath,
175
+ "",
176
+ routeSetFor(project.manifest.routes)
177
+ ),
173
178
  // Pages, static files, and server routes alike: a redirect may
174
179
  // legitimately land on a served asset (`/old-whitepaper` ->
175
180
  // `/files/whitepaper.pdf`), and the same predicate the link and llms.txt
@@ -196,8 +196,13 @@ export const buildSnapshot = (options: {
196
196
  scripts: collectAssets(document, "script[src]", "src"),
197
197
  source: route?.sourcePath,
198
198
  styles: collectAssets(document, 'link[rel="stylesheet"][href]', "href"),
199
+ // Only the document's own titles: an accessible inline SVG carries a
200
+ // `<title>` of its own (an SVG-namespace element, not the page title), and
201
+ // counting it would flag BLUME_AUDIT_TITLE_MULTIPLE — or stand in for a
202
+ // head `<title>` that is genuinely missing.
199
203
  titles: document
200
204
  .querySelectorAll("title")
205
+ .filter((title) => !title.closest("svg"))
201
206
  .map((title) => title.text.trim())
202
207
  .filter((text) => text.length > 0),
203
208
  twitter: prefixedMeta(document, "name", "twitter:"),
@@ -13,6 +13,7 @@ import type {
13
13
  export type AuditTier = "static" | "network" | "external";
14
14
 
15
15
  export type AuditCategory =
16
+ | "accessibility"
16
17
  | "content"
17
18
  | "duplicates"
18
19
  | "indexability"
@@ -110,7 +111,7 @@ export interface RedirectResolution {
110
111
  status: number;
111
112
  /** Every hop from `from` to the final target, inclusive. */
112
113
  chain: string[];
113
- outcome: "ok" | "loop" | "broken" | "chain";
114
+ outcome: "ok" | "loop" | "broken" | "chain" | "pattern";
114
115
  }
115
116
 
116
117
  /** One urlset file behind a sitemap, for the per-file size limits. */
@@ -15,6 +15,13 @@ declare module "blume:search-client" {
15
15
  export const createSearch: () => Fn | Promise<Fn>;
16
16
  }
17
17
 
18
+ declare module "blume:consent-client" {
19
+ /** Start the configured consent adapter's browser module (see `consentClientTemplate`); a no-op for one without. */
20
+ // oxlint-disable-next-line typescript/consistent-type-imports
21
+ type Consent = import("./components/layout/consent/types.ts").BlumeConsent;
22
+ export const startConsentClient: (consent: Consent) => void;
23
+ }
24
+
18
25
  declare module "blume:ask" {
19
26
  /** The generated Ask trigger (see `askComponentTemplate`); empty when Ask is off. */
20
27
  // Mirrors the generated `.blume/src/env.d.ts` declaration: the trigger's type