blume 2.1.2 → 2.2.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 (377) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +133 -0
  3. package/README.md +4 -4
  4. package/dist/cli/{chunk-74wfhv79.js → chunk-05f21r8h.js} +1 -1
  5. package/dist/cli/{chunk-9qzgg6mj.js → chunk-1cm0h819.js} +3 -3
  6. package/dist/cli/{chunk-nbyvgnnm.js → chunk-1rkxzg3n.js} +104 -47
  7. package/dist/cli/chunk-1rkxzg3n.js.map +36 -0
  8. package/dist/cli/{chunk-desvm72z.js → chunk-2nr29y95.js} +22 -26
  9. package/dist/cli/chunk-2nr29y95.js.map +13 -0
  10. package/dist/cli/{chunk-32khwcb6.js → chunk-49r370mb.js} +42 -69
  11. package/dist/cli/{chunk-32khwcb6.js.map → chunk-49r370mb.js.map} +9 -10
  12. package/dist/cli/{chunk-ms9z66nz.js → chunk-4rftmttv.js} +2 -2
  13. package/dist/cli/{chunk-s0bdnc7v.js → chunk-79bzzkx4.js} +5 -4
  14. package/dist/cli/chunk-79bzzkx4.js.map +10 -0
  15. package/dist/cli/{chunk-hvcxng5f.js → chunk-7tekknev.js} +6 -6
  16. package/dist/cli/{chunk-dv2z0rzf.js → chunk-8b489v1b.js} +38 -13
  17. package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-8b489v1b.js.map} +5 -4
  18. package/dist/cli/{chunk-0pmb4tr4.js → chunk-8f8xw4x2.js} +1195 -222
  19. package/dist/cli/chunk-8f8xw4x2.js.map +44 -0
  20. package/dist/cli/chunk-96snx8yy.js +329 -0
  21. package/dist/cli/chunk-96snx8yy.js.map +13 -0
  22. package/dist/cli/{chunk-ckrcm08h.js → chunk-a3ydrqny.js} +48 -62
  23. package/dist/cli/{chunk-ckrcm08h.js.map → chunk-a3ydrqny.js.map} +4 -5
  24. package/dist/cli/{chunk-bxwhtvw8.js → chunk-ahc5gc9h.js} +5 -5
  25. package/dist/cli/{chunk-dpy8qdjf.js → chunk-as09pafp.js} +73 -182
  26. package/dist/cli/chunk-as09pafp.js.map +18 -0
  27. package/dist/cli/{chunk-tfycddbt.js → chunk-bjk964fp.js} +2 -2
  28. package/dist/cli/{chunk-gfc5fqvf.js → chunk-br0n8n3e.js} +18 -18
  29. package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-br0n8n3e.js.map} +1 -1
  30. package/dist/cli/{chunk-297ajdcv.js → chunk-c69tw29j.js} +6 -6
  31. package/dist/cli/{chunk-297ajdcv.js.map → chunk-c69tw29j.js.map} +3 -3
  32. package/dist/cli/chunk-cn3dy3hz.js +146 -0
  33. package/dist/cli/chunk-cn3dy3hz.js.map +11 -0
  34. package/dist/cli/{chunk-124fvmzq.js → chunk-dm4mg9bj.js} +11064 -8742
  35. package/dist/cli/{chunk-124fvmzq.js.map → chunk-dm4mg9bj.js.map} +78 -64
  36. package/dist/cli/{chunk-dqhg54mr.js → chunk-dmx6cezx.js} +1 -1
  37. package/dist/cli/{chunk-60hz265r.js → chunk-dpcahcrb.js} +24 -11
  38. package/dist/cli/chunk-dpcahcrb.js.map +10 -0
  39. package/dist/cli/{chunk-a57yh0h9.js → chunk-dyk3r7jw.js} +7 -7
  40. package/dist/cli/{chunk-a57yh0h9.js.map → chunk-dyk3r7jw.js.map} +1 -1
  41. package/dist/cli/{chunk-twy7psdz.js → chunk-enk76qd5.js} +14 -10
  42. package/dist/cli/chunk-enk76qd5.js.map +10 -0
  43. package/dist/cli/{chunk-k3gr1nm9.js → chunk-fqy4n3q2.js} +13 -16
  44. package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-fqy4n3q2.js.map} +3 -3
  45. package/dist/cli/{chunk-sc93apfs.js → chunk-frk2wdq1.js} +17 -17
  46. package/dist/cli/{chunk-sc93apfs.js.map → chunk-frk2wdq1.js.map} +2 -2
  47. package/dist/cli/{chunk-kdpzw38a.js → chunk-gprwej7h.js} +95 -7
  48. package/dist/cli/chunk-gprwej7h.js.map +11 -0
  49. package/dist/cli/chunk-kkrteb15.js +157 -0
  50. package/dist/cli/chunk-kkrteb15.js.map +10 -0
  51. package/dist/cli/{chunk-ave1wcbr.js → chunk-m9bp2hc1.js} +1 -1
  52. package/dist/cli/{chunk-58bt3hag.js → chunk-mrvh4c88.js} +4 -4
  53. package/dist/cli/{chunk-5cmts6dv.js → chunk-p8zg3a2h.js} +7 -7
  54. package/dist/cli/{chunk-5cmts6dv.js.map → chunk-p8zg3a2h.js.map} +1 -1
  55. package/dist/cli/{chunk-0w6hs479.js → chunk-rkqae7kh.js} +30 -18
  56. package/dist/cli/chunk-rkqae7kh.js.map +10 -0
  57. package/dist/cli/{chunk-bqxaqn5p.js → chunk-t5xx74rd.js} +8 -8
  58. package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-t5xx74rd.js.map} +1 -1
  59. package/dist/cli/{chunk-1f882900.js → chunk-th0pgaj5.js} +2 -2
  60. package/dist/cli/{chunk-dhwtqnnd.js → chunk-vnbvhgmd.js} +2 -2
  61. package/dist/cli/{chunk-gxtg6ed2.js → chunk-vzy9rh6y.js} +21 -21
  62. package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-vzy9rh6y.js.map} +3 -3
  63. package/dist/cli/{chunk-j1x92mx9.js → chunk-w6e455pz.js} +1 -1
  64. package/dist/cli/chunk-wehv26ry.js +155 -0
  65. package/dist/cli/chunk-wehv26ry.js.map +11 -0
  66. package/dist/cli/{chunk-y4msrwm9.js → chunk-xgq4eytj.js} +15 -15
  67. package/dist/cli/{chunk-36bxs9c7.js → chunk-xt0qvz1t.js} +27 -23
  68. package/dist/cli/chunk-xt0qvz1t.js.map +10 -0
  69. package/dist/cli/index.js +78 -42
  70. package/dist/cli/index.js.map +7 -6
  71. package/dist/types/ai/changelog-components.d.ts +18 -0
  72. package/dist/types/ai/changelog-markdown.d.ts +12 -0
  73. package/dist/types/ai/openapi-components.d.ts +7 -7
  74. package/dist/types/ai/operation-details.d.ts +39 -0
  75. package/dist/types/ai/relative-links.d.ts +3 -1
  76. package/dist/types/ai/serializers.d.ts +6 -4
  77. package/dist/types/analytics/head.d.ts +4 -2
  78. package/dist/types/analytics/script.d.ts +8 -2
  79. package/dist/types/components/content/youtube.d.ts +20 -0
  80. package/dist/types/components/layout/nav-utils.d.ts +8 -0
  81. package/dist/types/components/openapi/example-panels.d.ts +33 -0
  82. package/dist/types/components/openapi/helpers.d.ts +53 -11
  83. package/dist/types/components/openapi/snippets.d.ts +7 -1
  84. package/dist/types/core/alert-diagnostics.d.ts +10 -0
  85. package/dist/types/core/builtin-tags.d.ts +16 -0
  86. package/dist/types/core/changelog-list.d.ts +48 -0
  87. package/dist/types/core/closest-match.d.ts +6 -0
  88. package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
  89. package/dist/types/core/code-fences.d.ts +8 -1
  90. package/dist/types/core/config-input.d.ts +1 -1
  91. package/dist/types/core/content-assets.d.ts +16 -18
  92. package/dist/types/core/diagnostics.d.ts +14 -0
  93. package/dist/types/core/directive-diagnostics.d.ts +22 -8
  94. package/dist/types/core/former-routes.d.ts +14 -0
  95. package/dist/types/core/github.d.ts +5 -4
  96. package/dist/types/core/heading-markers.d.ts +65 -2
  97. package/dist/types/core/hidden-pages.d.ts +3 -0
  98. package/dist/types/core/includes.d.ts +23 -0
  99. package/dist/types/core/last-modified.d.ts +18 -6
  100. package/dist/types/core/links.d.ts +42 -2
  101. package/dist/types/core/load-module.d.ts +19 -4
  102. package/dist/types/core/manifest.d.ts +14 -1
  103. package/dist/types/core/markdown-mirrors.d.ts +22 -0
  104. package/dist/types/core/mdx-elements.d.ts +32 -0
  105. package/dist/types/core/mdx-syntax.d.ts +100 -0
  106. package/dist/types/core/meta-diagnostics.d.ts +7 -0
  107. package/dist/types/core/meta.d.ts +32 -5
  108. package/dist/types/core/nav-diagnostics.d.ts +9 -2
  109. package/dist/types/core/navigation.d.ts +13 -0
  110. package/dist/types/core/ordering-prefix.d.ts +6 -0
  111. package/dist/types/core/pinned-ids.d.ts +23 -0
  112. package/dist/types/core/project-graph.d.ts +9 -1
  113. package/dist/types/core/redirect-patterns.d.ts +18 -0
  114. package/dist/types/core/relative-files.d.ts +31 -0
  115. package/dist/types/core/schema.d.ts +267 -7
  116. package/dist/types/core/sources/normalize.d.ts +101 -9
  117. package/dist/types/core/sources/types.d.ts +16 -0
  118. package/dist/types/core/static-files.d.ts +10 -0
  119. package/dist/types/core/static-host.d.ts +42 -0
  120. package/dist/types/core/svg-images.d.ts +26 -0
  121. package/dist/types/core/syntax-diagnostics.d.ts +14 -0
  122. package/dist/types/core/types.d.ts +42 -1
  123. package/dist/types/deploy/generated-files.d.ts +41 -0
  124. package/dist/types/deploy/redirects.d.ts +21 -7
  125. package/dist/types/markdown/code-title.d.ts +9 -5
  126. package/dist/types/markdown/directives.d.ts +8 -0
  127. package/dist/types/markdown/fence-language.d.ts +51 -0
  128. package/dist/types/markdown/fence-meta.d.ts +5 -0
  129. package/dist/types/markdown/github-alerts.d.ts +35 -0
  130. package/dist/types/openapi/asyncapi.d.ts +6 -5
  131. package/dist/types/openapi/checks.d.ts +10 -5
  132. package/dist/types/openapi/model.d.ts +55 -8
  133. package/dist/types/openapi/parse.d.ts +4 -1
  134. package/dist/types/sources/filesystem.d.ts +14 -4
  135. package/dist/types/sources/mdx-remote.d.ts +6 -3
  136. package/dist/types/sources/registry.d.ts +3 -3
  137. package/dist/types/theme/color.d.ts +33 -0
  138. package/dist/types/theme/palette.d.ts +122 -0
  139. package/docs/01-quickstart.mdx +1 -1
  140. package/docs/02-deployment.mdx +23 -8
  141. package/docs/04-migrating.mdx +16 -4
  142. package/docs/advanced/changelog.mdx +27 -1
  143. package/docs/advanced/skills.mdx +1 -1
  144. package/docs/cli/audit.mdx +1 -1
  145. package/docs/cli/doctor.mdx +2 -2
  146. package/docs/cli/evals.mdx +1 -1
  147. package/docs/cli/index.mdx +4 -2
  148. package/docs/cli/validate.mdx +10 -7
  149. package/docs/configuration/analytics.mdx +2 -0
  150. package/docs/configuration/customization.mdx +3 -1
  151. package/docs/configuration/index.mdx +2 -2
  152. package/docs/configuration/theming.mdx +5 -1
  153. package/docs/content/components.mdx +26 -6
  154. package/docs/content/frontmatter.mdx +1 -1
  155. package/docs/content/includes.mdx +6 -2
  156. package/docs/content/index.mdx +11 -1
  157. package/docs/content/islands.mdx +2 -0
  158. package/docs/content/meta.mdx +9 -3
  159. package/docs/content/navigation.mdx +14 -6
  160. package/docs/content/sources/obsidian.mdx +1 -1
  161. package/docs/content/sources/remote-mdx.mdx +3 -1
  162. package/docs/content/syntax.mdx +76 -8
  163. package/docs/content/variables.mdx +3 -1
  164. package/docs/discoverability/agent-discovery.mdx +1 -1
  165. package/docs/discoverability/json-api.mdx +1 -1
  166. package/docs/discoverability/llms-txt.mdx +1 -1
  167. package/docs/discoverability/markdown.mdx +2 -2
  168. package/docs/discoverability/open-graph.mdx +1 -1
  169. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  170. package/docs/references/graphql.mdx +2 -0
  171. package/docs/references/openapi.mdx +45 -7
  172. package/package.json +10 -9
  173. package/skills/blume/SKILL.md +2 -2
  174. package/skills/blume-migrate/SKILL.md +49 -27
  175. package/skills/blume-migrate/references/docsify.md +262 -0
  176. package/skills/blume-migrate/references/docus.md +298 -0
  177. package/skills/blume-migrate/references/docusaurus.md +3 -3
  178. package/skills/blume-migrate/references/fern.md +327 -0
  179. package/skills/blume-migrate/references/fumadocs.md +1 -1
  180. package/skills/blume-migrate/references/gitbook.md +267 -0
  181. package/skills/blume-migrate/references/github-wiki.md +259 -0
  182. package/skills/blume-migrate/references/jekyll.md +253 -0
  183. package/skills/blume-migrate/references/mdbook.md +256 -0
  184. package/skills/blume-migrate/references/mintlify.md +5 -5
  185. package/skills/blume-migrate/references/mkdocs.md +236 -0
  186. package/skills/blume-migrate/references/monorepo.md +20 -8
  187. package/skills/blume-migrate/references/nextra.md +2 -2
  188. package/skills/blume-migrate/references/readme.md +257 -0
  189. package/skills/blume-migrate/references/redocly.md +198 -0
  190. package/skills/blume-migrate/references/starlight.md +3 -3
  191. package/skills/blume-migrate/references/vitepress.md +204 -0
  192. package/skills/blume-migrate/references/vuepress.md +271 -0
  193. package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
  194. package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
  195. package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
  196. package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
  197. package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
  198. package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
  199. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
  200. package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
  201. package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
  202. package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
  203. package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
  204. package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
  205. package/src/ai/changelog-components.ts +24 -0
  206. package/src/ai/changelog-markdown.ts +45 -41
  207. package/src/ai/component-markdown.ts +22 -9
  208. package/src/ai/llms.ts +36 -5
  209. package/src/ai/markdown.ts +2 -2
  210. package/src/ai/openapi-components.ts +25 -9
  211. package/src/ai/operation-details.ts +208 -0
  212. package/src/ai/relative-links.ts +48 -8
  213. package/src/ai/serializers.ts +7 -6
  214. package/src/ai/site-skill.ts +6 -2
  215. package/src/analytics/head.ts +11 -5
  216. package/src/analytics/script.ts +13 -3
  217. package/src/astro/generate.ts +32 -6
  218. package/src/astro/index.ts +1 -0
  219. package/src/astro/integration.ts +27 -1
  220. package/src/astro/mdx-source-errors.ts +46 -0
  221. package/src/astro/templates.ts +89 -185
  222. package/src/audit/checks/redirects.ts +5 -1
  223. package/src/audit/redirects.ts +36 -19
  224. package/src/audit/report.ts +22 -9
  225. package/src/audit/run.ts +12 -28
  226. package/src/audit/terms.ts +23 -0
  227. package/src/audit/url.ts +2 -2
  228. package/src/cli/build-failure.ts +256 -7
  229. package/src/cli/command-meta.ts +1 -1
  230. package/src/cli/commands/audit.ts +1 -1
  231. package/src/cli/commands/build.ts +12 -3
  232. package/src/cli/commands/check.ts +13 -2
  233. package/src/cli/commands/dev.ts +1 -1
  234. package/src/cli/commands/doctor.ts +4 -2
  235. package/src/cli/commands/init.ts +3 -3
  236. package/src/cli/commands/preview.ts +17 -1
  237. package/src/cli/commands/validate.ts +22 -10
  238. package/src/cli/prepare.ts +1 -1
  239. package/src/cli/preview-pages.ts +161 -0
  240. package/src/cli/unknown-flags.ts +1 -42
  241. package/src/components/code-copy.ts +51 -0
  242. package/src/components/code-language.ts +75 -0
  243. package/src/components/content/AccordionItem.astro +16 -1
  244. package/src/components/content/Callout.astro +11 -1
  245. package/src/components/content/Changelog.astro +189 -0
  246. package/src/components/content/CodeBlock.astro +8 -11
  247. package/src/components/content/Tabs.astro +16 -2
  248. package/src/components/content/YouTube.astro +6 -4
  249. package/src/components/content/code-block.ts +33 -0
  250. package/src/components/content/content-strings.ts +5 -1
  251. package/src/components/content/inline-markdown.ts +35 -8
  252. package/src/components/content/youtube.ts +49 -7
  253. package/src/components/layout/Analytics.astro +5 -1
  254. package/src/components/layout/NavTree.astro +8 -3
  255. package/src/components/layout/RootLayout.astro +18 -31
  256. package/src/components/layout/nav-utils.ts +14 -0
  257. package/src/components/layout/search/pagefind.ts +2 -1
  258. package/src/components/openapi/Operation.astro +4 -0
  259. package/src/components/openapi/PanelTabs.astro +9 -3
  260. package/src/components/openapi/RequestBody.astro +42 -8
  261. package/src/components/openapi/RequestPanel.astro +34 -53
  262. package/src/components/openapi/example-panels.ts +110 -0
  263. package/src/components/openapi/helpers.ts +195 -20
  264. package/src/components/openapi/operation-model.ts +63 -26
  265. package/src/components/openapi/request.ts +13 -0
  266. package/src/components/openapi/snippets.ts +14 -2
  267. package/src/core/alert-diagnostics.ts +89 -0
  268. package/src/core/builtin-tags.ts +76 -0
  269. package/src/core/changelog-list.ts +87 -0
  270. package/src/core/closest-match.ts +41 -0
  271. package/src/core/code-fence-diagnostics.ts +267 -0
  272. package/src/core/code-fences.ts +43 -7
  273. package/src/core/component-diagnostics.ts +65 -12
  274. package/src/core/config-input.ts +1 -1
  275. package/src/core/config.ts +46 -1
  276. package/src/core/content-assets.ts +64 -144
  277. package/src/core/diagnostics.ts +36 -3
  278. package/src/core/directive-diagnostics.ts +265 -47
  279. package/src/core/former-routes.ts +46 -0
  280. package/src/core/git-ignored.ts +53 -0
  281. package/src/core/github.ts +5 -4
  282. package/src/core/heading-markers.ts +156 -2
  283. package/src/core/hidden-pages.ts +64 -0
  284. package/src/core/includes.ts +181 -41
  285. package/src/core/last-modified.ts +47 -10
  286. package/src/core/links.ts +438 -83
  287. package/src/core/load-module.ts +98 -9
  288. package/src/core/manifest.ts +12 -9
  289. package/src/core/markdown-mirrors.ts +52 -0
  290. package/src/core/mdx-elements.ts +153 -0
  291. package/src/core/mdx-syntax.ts +377 -0
  292. package/src/core/meta-diagnostics.ts +134 -0
  293. package/src/core/meta.ts +154 -55
  294. package/src/core/nav-diagnostics.ts +16 -4
  295. package/src/core/navigation.ts +70 -10
  296. package/src/core/ordering-prefix.ts +22 -2
  297. package/src/core/pinned-ids.ts +141 -0
  298. package/src/core/project-graph.ts +221 -29
  299. package/src/core/redirect-patterns.ts +41 -0
  300. package/src/core/relative-files.ts +145 -0
  301. package/src/core/schema.ts +17 -3
  302. package/src/core/sources/mdx-remote.ts +75 -14
  303. package/src/core/sources/normalize.ts +885 -179
  304. package/src/core/sources/notion.ts +5 -4
  305. package/src/core/sources/obsidian.ts +2 -5
  306. package/src/core/sources/types.ts +17 -0
  307. package/src/core/static-files.ts +40 -0
  308. package/src/core/static-host.ts +125 -0
  309. package/src/core/svg-images.ts +98 -0
  310. package/src/core/syntax-diagnostics.ts +491 -0
  311. package/src/core/types.ts +43 -1
  312. package/src/deploy/artifacts.ts +2 -28
  313. package/src/deploy/generated-files.ts +128 -0
  314. package/src/deploy/platforms/vercel.ts +25 -1
  315. package/src/deploy/redirects.ts +43 -16
  316. package/src/deploy/sitemap.ts +2 -1
  317. package/src/markdown/base-links.ts +87 -6
  318. package/src/markdown/code-title.ts +29 -13
  319. package/src/markdown/content-assets.ts +210 -0
  320. package/src/markdown/directives.ts +69 -5
  321. package/src/markdown/fence-language.ts +106 -0
  322. package/src/markdown/fence-meta.ts +42 -0
  323. package/src/markdown/github-alerts.ts +172 -0
  324. package/src/markdown/heading-anchors.ts +265 -56
  325. package/src/markdown/include.ts +30 -3
  326. package/src/markdown/index.ts +145 -40
  327. package/src/markdown/prompt-text.ts +95 -0
  328. package/src/markdown/relative-links.ts +37 -2
  329. package/src/markdown/route-snapshot.ts +22 -12
  330. package/src/markdown/svg-fallback.ts +51 -0
  331. package/src/markdown/title-heading.ts +134 -0
  332. package/src/migrate/migrate.ts +127 -2
  333. package/src/og/card.ts +14 -4
  334. package/src/openapi/asyncapi.ts +14 -6
  335. package/src/openapi/checks.ts +160 -15
  336. package/src/openapi/model.ts +172 -33
  337. package/src/openapi/parse.ts +13 -4
  338. package/src/openapi/render-mdx.ts +55 -9
  339. package/src/openapi/source.ts +87 -6
  340. package/src/registry/eject.ts +0 -1
  341. package/src/search/documents.ts +10 -2
  342. package/src/sources/filesystem.ts +28 -3
  343. package/src/sources/mdx-remote.ts +7 -15
  344. package/src/theme/color.ts +14 -0
  345. package/src/theme/entry.ts +22 -1
  346. package/src/theme/palette.ts +74 -1
  347. package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
  348. package/dist/cli/chunk-0w6hs479.js.map +0 -10
  349. package/dist/cli/chunk-150jk7km.js +0 -52
  350. package/dist/cli/chunk-150jk7km.js.map +0 -10
  351. package/dist/cli/chunk-36bxs9c7.js.map +0 -10
  352. package/dist/cli/chunk-53jqw8z0.js +0 -89
  353. package/dist/cli/chunk-53jqw8z0.js.map +0 -10
  354. package/dist/cli/chunk-60hz265r.js.map +0 -10
  355. package/dist/cli/chunk-desvm72z.js.map +0 -14
  356. package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
  357. package/dist/cli/chunk-es8h0v3j.js +0 -441
  358. package/dist/cli/chunk-es8h0v3j.js.map +0 -11
  359. package/dist/cli/chunk-kdpzw38a.js.map +0 -11
  360. package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
  361. package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
  362. package/dist/cli/chunk-twy7psdz.js.map +0 -10
  363. package/dist/cli/chunk-ydp3065p.js +0 -64
  364. package/dist/cli/chunk-ydp3065p.js.map +0 -10
  365. /package/dist/cli/{chunk-74wfhv79.js.map → chunk-05f21r8h.js.map} +0 -0
  366. /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-1cm0h819.js.map} +0 -0
  367. /package/dist/cli/{chunk-ms9z66nz.js.map → chunk-4rftmttv.js.map} +0 -0
  368. /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-7tekknev.js.map} +0 -0
  369. /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-ahc5gc9h.js.map} +0 -0
  370. /package/dist/cli/{chunk-tfycddbt.js.map → chunk-bjk964fp.js.map} +0 -0
  371. /package/dist/cli/{chunk-dqhg54mr.js.map → chunk-dmx6cezx.js.map} +0 -0
  372. /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-m9bp2hc1.js.map} +0 -0
  373. /package/dist/cli/{chunk-58bt3hag.js.map → chunk-mrvh4c88.js.map} +0 -0
  374. /package/dist/cli/{chunk-1f882900.js.map → chunk-th0pgaj5.js.map} +0 -0
  375. /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-vnbvhgmd.js.map} +0 -0
  376. /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-w6e455pz.js.map} +0 -0
  377. /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-xgq4eytj.js.map} +0 -0
@@ -10,7 +10,7 @@ import { buildHomeLinkHeader } from "../ai/link-headers.ts";
10
10
  import type { CaptchaAdapter } from "../captcha/schema.ts";
11
11
  import { CONSENT_CLIENT_MODULES } from "../consent/clients.ts";
12
12
  import { normalizeBasePath } from "../core/base-path.ts";
13
- import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
13
+ import { TOC_HIDDEN_KEY, TOC_TEXT_KEY } from "../core/heading-markers.ts";
14
14
  import {
15
15
  compileEveryRedirect,
16
16
  isPatternPath,
@@ -22,15 +22,18 @@ import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
22
22
  import { trimChar } from "../core/trim.ts";
23
23
  import type { ProjectContext } from "../core/types.ts";
24
24
  import { hasVariables } from "../core/variables.ts";
25
+ import { getBlumeVersion } from "../core/version.ts";
25
26
  import { deployPassthrough } from "../deploy/adapters/types.ts";
26
27
  import { SVG_ASSET_HEADERS } from "../deploy/headers.ts";
27
28
  import { deployPlatform } from "../deploy/platforms/index.ts";
28
29
  import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
29
30
  import {
30
31
  applyBaseToAstroRedirects,
32
+ isPageIndexFile,
31
33
  withMirrorRedirects,
32
34
  } from "../deploy/redirects.ts";
33
35
  import { API_RAIL_KEY } from "../markdown/api-rail.ts";
36
+ import { TITLE_ID_KEY } from "../markdown/title-heading.ts";
34
37
  import { VIEWS_KEY } from "../markdown/views.ts";
35
38
  import type { OgCache } from "../og/cache.ts";
36
39
  import type { OgFont, OgFontFamilies, OgGoogleFont } from "../og/card.ts";
@@ -717,8 +720,16 @@ export const astroConfigTemplate = (options: {
717
720
  // Only exact redirects: Astro can't prerender a pattern's redirect pages
718
721
  // (it would need every path the pattern covers), so a pattern reaches the
719
722
  // dev server through the integration and each host through its own rules.
723
+ // Nor one from a page's own `index.html` in a static build, where Astro
724
+ // would write its redirect page inside the page's file (see
725
+ // `isPageIndexFile`); the host files and the dev server still carry it.
726
+ const staticOutput = deployment.options.output === "static";
720
727
  const exactRedirects = basedRedirects.filter(
721
- (redirect) => !isPatternPath(redirect.from)
728
+ (redirect) =>
729
+ !(
730
+ isPatternPath(redirect.from) ||
731
+ (staticOutput && isPageIndexFile(redirect.from, redirectPages))
732
+ )
722
733
  );
723
734
  const redirectsOption =
724
735
  exactRedirects.length > 0
@@ -801,6 +812,7 @@ export const astroConfigTemplate = (options: {
801
812
  const blumeImports = [
802
813
  "blumeIntegration",
803
814
  "includeHmrPlugin",
815
+ "mdxSourceErrorsPlugin",
804
816
  "prerenderDepsPlugin",
805
817
  ...runtimeModuleImports,
806
818
  ...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
@@ -909,10 +921,10 @@ ${userConfigSetup}export default defineConfig({
909
921
  devToolbar: { enabled: false },
910
922
  // One canonical URL per page: canonicals, the sitemap, and hreflang all use
911
923
  // the slashless form, so the slashed spelling is not a second address. Astro
912
- // applies this itself — its dev server answers a slashed URL with a 404 that
913
- // names the setting, an on-demand route redirects — and the Vercel adapter
914
- // turns it into the platform's 308 route, so the Build Output config needs
915
- // no hand-spliced redirect (see deploy/vercel-negotiation.ts). Static hosts
924
+ // applies this itself — an on-demand route redirects, and the integration
925
+ // has the dev server redirect too, ahead of Astro's own 404 — and the Vercel
926
+ // adapter turns it into the platform's 308 route, so the Build Output config
927
+ // needs no hand-spliced redirect (see deploy/vercel-negotiation.ts). Static hosts
916
928
  // serve the \`index.html\` directory layout as they always did.
917
929
  trailingSlash: "never",
918
930
  // The layouts render Astro's <ClientRouter />, and its in-place swaps read
@@ -930,7 +942,7 @@ ${userConfigSetup}export default defineConfig({
930
942
  plugins: [${runtimeModulesPluginEntry}${variablesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
931
943
  `${context.outDir}/src/generated/includes.json`,
932
944
  ejected
933
- )}), prerenderDepsPlugin()],
945
+ )}), mdxSourceErrorsPlugin(), prerenderDepsPlugin()],
934
946
  build: {${features.mermaid ? MERMAID_CHUNK_LIMIT : ""}
935
947
  rolldownOptions: {
936
948
  ${ROLLDOWN_ON_LOG},
@@ -1031,6 +1043,13 @@ const runtimeDirWithin = (base: string, outDir: string): string | null => {
1031
1043
  const astroGlobBase = (base: string): string =>
1032
1044
  isAbsolute(base) ? pathToFileURL(base).href : base;
1033
1045
 
1046
+ // The characters a glob reads as syntax; a `(group)` folder has two.
1047
+ const GLOB_SYNTAX = /[!()*+?@[\\\]{|}]/gu;
1048
+
1049
+ /** A relative path as a glob that matches only that file. */
1050
+ const escapeGlob = (path: string): string =>
1051
+ path.replaceAll(GLOB_SYNTAX, String.raw`\$&`);
1052
+
1034
1053
  /** Generate `.blume/src/content.config.ts`. */
1035
1054
  export const contentConfigTemplate = (options: {
1036
1055
  context: ProjectContext;
@@ -1051,6 +1070,13 @@ export const contentConfigTemplate = (options: {
1051
1070
  * a non-filesystem source), the collection globs nothing — see below.
1052
1071
  */
1053
1072
  filesystem?: boolean;
1073
+ /**
1074
+ * Absolute paths of `.mdx` files the build dropped because they don't
1075
+ * parse. Astro compiles every `.mdx` file in the collection, routed or
1076
+ * not, so each is left out of the glob, or a `--no-strict` build would
1077
+ * still fail on it.
1078
+ */
1079
+ unparsable?: readonly string[];
1054
1080
  }): string => {
1055
1081
  const { context, config } = options;
1056
1082
  const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
@@ -1087,6 +1113,12 @@ export const contentConfigTemplate = (options: {
1087
1113
  dir === ".blume" ? [] : [`!**/${dir}/**`]
1088
1114
  ),
1089
1115
  ...outDirIgnore,
1116
+ ...(options.unparsable ?? []).flatMap((file) => {
1117
+ const rel = relative(collectionBase, file);
1118
+ return rel.startsWith("..") || isAbsolute(rel)
1119
+ ? []
1120
+ : [`!${escapeGlob(rel)}`];
1121
+ }),
1090
1122
  ]
1091
1123
  : [];
1092
1124
 
@@ -1118,6 +1150,10 @@ import { pageCollectionSchema } from "blume/core/schema.ts";
1118
1150
  // pageCollectionSchema is the scan's page front-matter schema, so entry.data
1119
1151
  // is typed and normalized the same way; it passes custom keys through and
1120
1152
  // falls back to defaults for a page the scan already dropped as invalid.
1153
+ //
1154
+ // Astro's content store re-parses an entry only when its file or this config
1155
+ // changes, so naming the Blume version (${getBlumeVersion()}) here re-parses
1156
+ // every entry after an upgrade: none keeps what an older schema produced.
1121
1157
  const docs = defineCollection({
1122
1158
  loader: withIncludeRefresh(glob({
1123
1159
  pattern: ${JSON.stringify(docsPattern)},
@@ -1831,7 +1867,10 @@ export function GET({ props }: { props: { route: string } }) {
1831
1867
  * - `content/<project-relative path>` — colocated content images
1832
1868
  * (`![alt](./diagram.png)`), served as originals for the agent-facing
1833
1869
  * Markdown endpoints (the HTML render uses the `astro:assets`-optimized
1834
- * copies instead). The mapping comes from `generated/content-assets.json`.
1870
+ * copies instead), and the other files beside a page that its links and
1871
+ * element URLs name (`[spec](./spec.pdf)`, `<img src="./a.png">`), which
1872
+ * the HTML render points here too. The mapping comes from
1873
+ * `generated/content-assets.json`.
1835
1874
  * - `<source>/<hash>.<ext>` — remote-source images the scan pipeline
1836
1875
  * materializes under `.blume/public/blume-assets` (see
1837
1876
  * `core/sources/assets.ts`). That directory is NOT Astro's `publicDir`
@@ -1874,11 +1913,25 @@ const TYPES: Record<string, string> = {
1874
1913
  ".webp": "image/webp",
1875
1914
  };
1876
1915
 
1916
+ // The other files a page's links name beside it, typed so the dev server
1917
+ // opens them the way a static host would. Anything else downloads.
1918
+ const FILE_TYPES: Record<string, string> = {
1919
+ ".csv": "text/csv; charset=utf-8",
1920
+ ".json": "application/json",
1921
+ ".mp3": "audio/mpeg",
1922
+ ".pdf": "application/pdf",
1923
+ ".txt": "text/plain; charset=utf-8",
1924
+ ".wav": "audio/wav",
1925
+ ".zip": "application/zip",
1926
+ };
1927
+
1877
1928
  const extensionOf = (path: string): string =>
1878
1929
  path.slice(path.lastIndexOf(".")).toLowerCase();
1879
1930
 
1880
1931
  const contentType = (path: string): string =>
1881
- TYPES[extensionOf(path)] ?? "application/octet-stream";
1932
+ TYPES[extensionOf(path)] ??
1933
+ FILE_TYPES[extensionOf(path)] ??
1934
+ "application/octet-stream";
1882
1935
 
1883
1936
  // A source downloads only images and videos; anything else in the staged
1884
1937
  // directory (a page an older build saved under its URL's extension) is never
@@ -2340,6 +2393,7 @@ import Badge from "blume/components/content/Badge.astro";
2340
2393
  import Callout from "blume/components/content/Callout.astro";
2341
2394
  import Card from "blume/components/content/Card.astro";
2342
2395
  import CardGroup from "blume/components/content/CardGroup.astro";
2396
+ import Changelog from "blume/components/content/Changelog.astro";
2343
2397
  import CodeBlock from "blume/components/content/CodeBlock.astro";
2344
2398
  import CodeGroup from "blume/components/content/CodeGroup.astro";
2345
2399
  import ColorRoot from "blume/components/content/Color.astro";
@@ -2393,6 +2447,7 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
2393
2447
  Callout,
2394
2448
  Card,
2395
2449
  CardGroup,
2450
+ Changelog,
2396
2451
  CodeBlock,
2397
2452
  CodeGroup,
2398
2453
  Color,
@@ -2513,10 +2568,25 @@ const views = Array.isArray(viewsRaw)
2513
2568
  : [];
2514
2569
  const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
2515
2570
  const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
2516
- const headings =
2571
+ // A heading holding a \`<Badge>\` lists without the badge's text, which the
2572
+ // heading plugin reports by slug the same way.
2573
+ const tocTextRaw = remarkPluginFrontmatter?.${TOC_TEXT_KEY};
2574
+ const tocText = tocTextRaw && typeof tocTextRaw === "object" ? tocTextRaw : {};
2575
+ const headings = (
2517
2576
  tocHidden.size > 0
2518
2577
  ? allHeadings.filter((heading) => !tocHidden.has(heading.slug))
2519
- : allHeadings;
2578
+ : allHeadings
2579
+ ).map((heading) =>
2580
+ typeof tocText[heading.slug] === "string"
2581
+ ? { ...heading, text: tocText[heading.slug] }
2582
+ : heading
2583
+ );
2584
+ // An untitled page's opening \`# Heading\` is its title, so the title-heading
2585
+ // plugin drops it from the body and reports its id here: the title \`<h1>\`
2586
+ // below takes it, so links to the heading's anchor still land (see
2587
+ // markdown/title-heading.ts).
2588
+ const titleIdRaw = remarkPluginFrontmatter?.${TITLE_ID_KEY};
2589
+ const titleId = typeof titleIdRaw === "string" ? titleIdRaw : undefined;
2520
2590
  const frontmatter = entry.data;
2521
2591
 
2522
2592
  const seo = frontmatter.seo;
@@ -2809,7 +2879,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2809
2879
  noindex={effectiveNoindex}
2810
2880
  structuredDataEnabled={data.config.structuredData}
2811
2881
  >
2812
- {pageChrome && <h1>{title}</h1>}
2882
+ {pageChrome && <h1 id={titleId}>{title}</h1>}
2813
2883
  {pageChrome && frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
2814
2884
  {narration && (
2815
2885
  <NarrationPlayer
@@ -2843,42 +2913,22 @@ export const changelogIndexTemplate = (options: {
2843
2913
  exportPdf: boolean;
2844
2914
  /** Serialize the island-hooks snapshot; only needed when React is enabled. */
2845
2915
  needsReact: boolean;
2846
- /** Whether a `staged` collection exists (non-filesystem changelog sources). */
2847
- staged: boolean;
2848
2916
  }): string => {
2849
2917
  const clientData = options.needsReact
2850
2918
  ? '\n clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: pageTitle } }}'
2851
2919
  : "";
2852
- // Staged sources (e.g. GitHub Releases) render through a parallel collection,
2853
- // so fold them in alongside filesystem entries when one exists.
2854
- const stagedSpread = options.staged
2855
- ? '\n ...(await getCollection("staged")),'
2856
- : "";
2857
2920
 
2858
2921
  return `---
2859
2922
  // Generated by Blume. Do not edit.
2860
- import { getCollection } from "astro:content";
2861
2923
  import RootLayout from "blume/components/layout/RootLayout.astro";
2924
+ import Changelog from "blume/components/content/Changelog.astro";
2862
2925
  import { withMountedBase } from "blume/components/islands/base-path.ts";
2863
2926
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2864
- import { resolveDateFormatOptions } from "blume/core/date-format.ts";
2865
2927
  import { layoutOverrides } from "../generated/components.ts";
2866
2928
  import data from "blume:data";
2867
2929
 
2868
2930
  export const prerender = true;
2869
2931
 
2870
- const entryDate = (entry: {
2871
- data: { date?: string | null; changelog?: { date?: string | null } | null };
2872
- }) => entry.data.date ?? entry.data.changelog?.date ?? null;
2873
-
2874
- const parseDate = (value: string | null | undefined) => {
2875
- if (!value) {
2876
- return null;
2877
- }
2878
- const date = new Date(value);
2879
- return Number.isNaN(date.getTime()) ? null : date;
2880
- };
2881
-
2882
2932
  // The changelog is an unlocalized route, so its chrome renders in the default
2883
2933
  // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
2884
2934
  // dictionary), mirroring the catch-all's locale wiring.
@@ -2889,109 +2939,6 @@ const localeMeta = i18n
2889
2939
  const dir = localeMeta?.dir ?? "ltr";
2890
2940
  const htmlLang = i18n ? i18n.defaultLocale : "en";
2891
2941
 
2892
- // Formatted in the same locale as the chrome, and with the configured
2893
- // \`dateFormat\` (UTC by default), to match the per-page "last updated" stamp.
2894
- // The year heads each group, so a row shows the rest of the date: a preset
2895
- // style keeps its month wording, a component format simply drops the year.
2896
- const dateFormatOptions = resolveDateFormatOptions(data.config.dateFormat);
2897
- const { dateStyle, year: _year, ...dateComponents } = dateFormatOptions;
2898
- const rowDateFormat: Intl.DateTimeFormatOptions = dateStyle
2899
- ? {
2900
- ...dateComponents,
2901
- day: "numeric",
2902
- month: dateStyle === "medium" ? "short" : "long",
2903
- }
2904
- : dateComponents;
2905
- const formatRowDate = (date: Date) =>
2906
- new Intl.DateTimeFormat(htmlLang, rowDateFormat).format(date);
2907
- const formatYear = (date: Date) =>
2908
- new Intl.DateTimeFormat(htmlLang, {
2909
- timeZone: dateFormatOptions.timeZone,
2910
- year: "numeric",
2911
- }).format(date);
2912
-
2913
- const slugify = (text: string) =>
2914
- text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
2915
- "update";
2916
-
2917
- // Map each entry to its own generated page so the row can link to it. The
2918
- // collection entry id matches the route manifest's \`entryId\` — and under
2919
- // i18n every locale serves a page for it (its translations plus fallback
2920
- // copies of an untranslated one), all sharing that id, so a map over every
2921
- // route would keep whichever locale came last: a Portuguese permalink on an
2922
- // unlocalized index. Only the default locale's routes are kept, and an entry
2923
- // is listed only when it has one, so a translated changelog file (a distinct
2924
- // entry that lives under its own locale) does not add a second row for the
2925
- // same release.
2926
- const defaultLocale = i18n ? i18n.defaultLocale : null;
2927
- const routeByEntry = new Map<string, string>();
2928
- for (const route of data.routes) {
2929
- if (defaultLocale === null || route.locale === defaultLocale) {
2930
- routeByEntry.set(route.entryId, route.path);
2931
- }
2932
- }
2933
-
2934
- const changelogEntries = [
2935
- ...(await getCollection("docs")),${stagedSpread}
2936
- ]
2937
- .filter(
2938
- (entry) =>
2939
- entry.data.type === "changelog" &&
2940
- !entry.data.draft &&
2941
- !entry.data.sidebar?.hidden &&
2942
- (defaultLocale === null || routeByEntry.has(entry.id))
2943
- )
2944
- .toSorted(
2945
- (a, b) =>
2946
- (parseDate(entryDate(b))?.getTime() ?? 0) -
2947
- (parseDate(entryDate(a))?.getTime() ?? 0)
2948
- );
2949
-
2950
- const items = changelogEntries.map((entry) => {
2951
- const label =
2952
- entry.data.title ??
2953
- (entry.data.changelog?.version
2954
- ? "v" + entry.data.changelog.version
2955
- : "Update");
2956
- const date = parseDate(entryDate(entry));
2957
- const route = routeByEntry.get(entry.id);
2958
- return {
2959
- date: date ? formatRowDate(date) : null,
2960
- dateTime: date ? date.toISOString().slice(0, 10) : null,
2961
- href: route ? withMountedBase(route) : null,
2962
- id: slugify(label),
2963
- label,
2964
- tag: entry.data.changelog?.category ?? null,
2965
- year: date ? formatYear(date) : null,
2966
- };
2967
- });
2968
-
2969
- // Repeated labels slug to the same id (e.g. two entries with neither a title
2970
- // nor a version both falling back to "update"); suffix the later ones -2, -3,
2971
- // ... so every row keeps its own hash anchor. The first keeps the plain slug.
2972
- const seenIds = new Set();
2973
- for (const item of items) {
2974
- let uniqueId = item.id;
2975
- for (let n = 2; seenIds.has(uniqueId); n += 1) {
2976
- uniqueId = item.id + "-" + n;
2977
- }
2978
- seenIds.add(uniqueId);
2979
- item.id = uniqueId;
2980
- }
2981
-
2982
- // Consecutive releases from the same year share a group, so the list reads as
2983
- // a year rail beside the rows; undated entries (sorted last) form a final
2984
- // group with no year label.
2985
- const groups: { items: typeof items; year: string | null }[] = [];
2986
- for (const item of items) {
2987
- const last = groups[groups.length - 1];
2988
- if (last && last.year === item.year) {
2989
- last.items.push(item);
2990
- } else {
2991
- groups.push({ items: [item], year: item.year });
2992
- }
2993
- }
2994
-
2995
2942
  const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
2996
2943
  // The canonical URL carries the deployment base (the page is served under it),
2997
2944
  // matching how the catch-all canonicalizes via \`withMountedBase(route)\`.
@@ -3004,15 +2951,16 @@ const canonical = base ? base + basedRoute : null;
3004
2951
  const ogPath = data.config.og.enabled ? withMountedBase("/og/changelog.png") : null;
3005
2952
  const ogImage = ogPath && base ? base + ogPath : ogPath;
3006
2953
 
3007
- // The page chrome (h1, title, description, empty state) comes from the
3008
- // translatable \`changelog\` group, which carries the \`changelog\` config's title and
2954
+ // The page chrome (h1, title, description) comes from the translatable
2955
+ // \`changelog\` group, which carries the \`changelog\` config's title and
3009
2956
  // description when it sets them; optional chaining tolerates a
3010
- // not-yet-regenerated data snapshot from before these keys existed.
2957
+ // not-yet-regenerated data snapshot from before these keys existed. The list
2958
+ // itself is \`<Changelog />\`, the same component an authored page can place to
2959
+ // put prose above it.
3011
2960
  const changelogTitle = data.ui.changelog?.title ?? "Changelog";
3012
2961
  const changelogDescription =
3013
2962
  data.ui.changelog?.description ??
3014
2963
  "Product updates, new features, and fixes from every release.";
3015
- const changelogEmpty = data.ui.changelog?.empty ?? "No changelog entries yet.";
3016
2964
  // The layout suffixes "- {site title}" itself, so the page title is just the
3017
2965
  // changelog's own name — prefixing the site title too would double it
3018
2966
  // ("Acme Changelog - Acme").
@@ -3063,51 +3011,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
3063
3011
  >
3064
3012
  <h1>{changelogTitle}</h1>
3065
3013
  <p class="text-lg text-muted-foreground">{changelogDescription}</p>
3066
- {
3067
- items.length === 0 ? (
3068
- <p>{changelogEmpty}</p>
3069
- ) : (
3070
- <div class="not-prose mt-10 divide-y divide-border border-border border-y">
3071
- {groups.map((group) => (
3072
- <section
3073
- aria-label={group.year ?? undefined}
3074
- class="grid md:grid-cols-[6rem_minmax(0,1fr)] md:gap-x-8"
3075
- >
3076
- <h2 class="mt-0! pt-3! font-medium! text-muted-foreground text-sm! leading-5! tabular-nums max-md:pb-1">
3077
- {group.year}
3078
- </h2>
3079
- <ul class="m-0! list-none divide-y divide-border p-0!">
3080
- {group.items.map((item) => (
3081
- <li class="m-0! p-0!" id={item.id}>
3082
- <a
3083
- class="group/release flex items-baseline gap-4 py-3 no-underline! hover:no-underline!"
3084
- href={item.href ?? "#" + item.id}
3085
- >
3086
- <span class="min-w-0 flex-1 font-medium text-foreground text-sm transition-colors group-hover/release:text-accent">
3087
- {item.label}
3088
- </span>
3089
- {item.tag && (
3090
- <span class="shrink-0 rounded-full bg-muted px-2 py-0.5 font-medium text-[0.65rem] text-muted-foreground">
3091
- {item.tag}
3092
- </span>
3093
- )}
3094
- {item.date && (
3095
- <time
3096
- class="shrink-0 font-mono text-muted-foreground text-xs tabular-nums"
3097
- datetime={item.dateTime}
3098
- >
3099
- {item.date}
3100
- </time>
3101
- )}
3102
- </a>
3103
- </li>
3104
- ))}
3105
- </ul>
3106
- </section>
3107
- ))}
3108
- </div>
3109
- )
3110
- }
3014
+ <Changelog />
3111
3015
  </LayoutComponent>
3112
3016
  `;
3113
3017
  };
@@ -48,11 +48,15 @@ export const redirectChecks: CheckModule = {
48
48
  )
49
49
  );
50
50
  } else if (redirect.outcome === "broken") {
51
+ // A pattern's chain starts at the bare path it also matches.
52
+ const [start] = redirect.chain;
53
+ const what =
54
+ start === from ? from : `${from}, which also matches ${start},`;
51
55
  found.push(
52
56
  finding(
53
57
  "BLUME_AUDIT_REDIRECT_BROKEN",
54
58
  { ...site, url: from },
55
- `Redirect from ${from} lands on ${redirect.chain.at(-1)}, which the build does not serve.`
59
+ `Redirect from ${what} lands on ${redirect.chain.at(-1)}, which the build does not serve.`
56
60
  )
57
61
  );
58
62
  } else if (redirect.outcome === "chain") {
@@ -1,4 +1,5 @@
1
1
  import {
2
+ bareRedirect,
2
3
  isPatternPath,
3
4
  patternDestination,
4
5
  } from "../core/redirect-patterns.ts";
@@ -22,11 +23,22 @@ const pathOnly = (value: string): string => {
22
23
  return cut === -1 ? value : value.slice(0, cut);
23
24
  };
24
25
 
26
+ /**
27
+ * The most hops a walk follows before calling it a loop. A pattern can send a
28
+ * path somewhere it covers again, one segment longer each time (`/a/*` to
29
+ * `/a/b/:splat`), which never revisits a hop yet never resolves either.
30
+ */
31
+ const MAX_HOPS = 32;
32
+
33
+ /** Where a walk from one path ended, and how. */
34
+ type Walk = Pick<RedirectResolution, "chain" | "outcome">;
35
+
25
36
  /**
26
37
  * Follow every configured redirect through to its destination, classifying what
27
38
  * it lands on.
28
39
  *
29
- * - `loop` — the chain revisits a hop it has already been to. Never resolves.
40
+ * - `loop` — the chain revisits a hop it has already been to, or runs past
41
+ * {@link MAX_HOPS}. Never resolves.
30
42
  * - `broken` — the chain ends somewhere the build does not serve.
31
43
  * - `chain` — it resolves, but through at least one intermediate redirect.
32
44
  * - `ok` — one hop, straight to a real page.
@@ -34,6 +46,11 @@ const pathOnly = (value: string): string => {
34
46
  * naming one, so there is no single chain to walk; {@link redirectAt}
35
47
  * resolves a path it covers.
36
48
  *
49
+ * A pattern ending in a rest also matches its bare path (`/beta`, see
50
+ * `bareRedirect`), which every host gets as an exact rule. That one path is
51
+ * walked like an exact redirect: when it loops or breaks, the pattern takes
52
+ * that outcome, its chain starting at the bare path.
53
+ *
37
54
  * An external destination (`https://…`) is always `ok`: it's outside the site,
38
55
  * so there's no local page to check it against. A hop onto a path a pattern
39
56
  * covers continues through that pattern.
@@ -50,14 +67,10 @@ export const resolveRedirects = (
50
67
  const hopFrom = (path: string): string | undefined =>
51
68
  byFrom.get(path)?.to ?? patternDestination(redirects, path);
52
69
 
53
- return redirects.map((redirect) => {
54
- const from = normalizePath(redirect.from);
55
- if (isPatternPath(redirect.from)) {
56
- return { ...redirect, chain: [from], outcome: "pattern" as const };
57
- }
70
+ const walk = (from: string, to: string): Walk => {
58
71
  const chain: string[] = [from];
59
72
  const seen = new Set<string>([from]);
60
- let current = redirect.to;
73
+ let current = to;
61
74
 
62
75
  for (;;) {
63
76
  // An external hop ends the walk — we can't follow it locally.
@@ -66,13 +79,9 @@ export const resolveRedirects = (
66
79
  break;
67
80
  }
68
81
  const next = normalizePath(pathOnly(current));
69
- if (seen.has(next)) {
82
+ if (seen.has(next) || chain.length > MAX_HOPS) {
70
83
  chain.push(next);
71
- return {
72
- ...redirect,
73
- chain,
74
- outcome: "loop" as const,
75
- };
84
+ return { chain, outcome: "loop" };
76
85
  }
77
86
  chain.push(next);
78
87
  seen.add(next);
@@ -86,15 +95,23 @@ export const resolveRedirects = (
86
95
  const destination = chain.at(-1) ?? from;
87
96
  const external = /^https?:\/\//iu.test(destination);
88
97
  if (!(external || served(destination))) {
89
- return { ...redirect, chain, outcome: "broken" as const };
98
+ return { chain, outcome: "broken" };
90
99
  }
91
100
  // `chain` is [from, …hops, destination]; more than two entries means at
92
101
  // least one intermediate redirect.
93
- return {
94
- ...redirect,
95
- chain,
96
- outcome: chain.length > 2 ? ("chain" as const) : ("ok" as const),
97
- };
102
+ return { chain, outcome: chain.length > 2 ? "chain" : "ok" };
103
+ };
104
+
105
+ return redirects.map((redirect) => {
106
+ const from = normalizePath(redirect.from);
107
+ if (!isPatternPath(redirect.from)) {
108
+ return { ...redirect, ...walk(from, redirect.to) };
109
+ }
110
+ const bare = bareRedirect(redirect);
111
+ const walked = bare && walk(normalizePath(bare.from), bare.to);
112
+ return walked && (walked.outcome === "loop" || walked.outcome === "broken")
113
+ ? { ...redirect, ...walked }
114
+ : { ...redirect, chain: [from], outcome: "pattern" as const };
98
115
  });
99
116
  };
100
117
 
@@ -7,6 +7,7 @@ import type { Diagnostic, DiagnosticSeverity } from "../core/types.ts";
7
7
  import { CHECKS, checkMeta } from "./catalog.ts";
8
8
  import type { CheckId } from "./catalog.ts";
9
9
  import type { AuditResult } from "./run.ts";
10
+ import { checkSelected } from "./terms.ts";
10
11
  import type { AuditCategory, AuditTier } from "./types.ts";
11
12
 
12
13
  const SEVERITY_COLOR = {
@@ -128,15 +129,27 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
128
129
  };
129
130
 
130
131
  /** Categories that had no findings but were never run, and the flag that runs them. */
131
- const skippedTiers = (tiers: Record<AuditTier, boolean>): string[] =>
132
- TIER_FLAGS.filter(({ tier }) => !tiers[tier]).map(({ flag, tier }) => {
133
- const label = CHECKS.filter((check) => check.tier === tier).length;
134
- return ` ${colors.dim(`⊘ ${tier.padEnd(12)} skipped — pass ${flag} (${label} checks)`)}`;
132
+ const skippedTiers = (result: AuditResult): string[] =>
133
+ TIER_FLAGS.flatMap(({ flag, tier }) => {
134
+ // Only the checks `--only`/`--skip` left in would have run.
135
+ const count = CHECKS.filter(
136
+ (check) => check.tier === tier && checkSelected(check.id, result)
137
+ ).length;
138
+ return result.tiers[tier] || count === 0
139
+ ? []
140
+ : [
141
+ ` ${colors.dim(`⊘ ${tier.padEnd(12)} skipped — pass ${flag} (${count} checks)`)}`,
142
+ ];
135
143
  });
136
144
 
137
- /** How many checks actually ran, i.e. those whose tier was enabled. */
138
- const activeChecks = (tiers: Record<AuditTier, boolean>): number =>
139
- CHECKS.filter((check) => tiers[check.tier]).length;
145
+ /**
146
+ * How many checks the report covers: those whose tier was enabled, and that
147
+ * `--only`/`--skip` left in.
148
+ */
149
+ const activeChecks = (result: AuditResult): number =>
150
+ CHECKS.filter(
151
+ (check) => result.tiers[check.tier] && checkSelected(check.id, result)
152
+ ).length;
140
153
 
141
154
  /**
142
155
  * Individual checks performed: every rule that ran, against every page crawled.
@@ -144,7 +157,7 @@ const activeChecks = (tiers: Record<AuditTier, boolean>): number =>
144
157
  * rather than a bare count.
145
158
  */
146
159
  export const auditCount = (result: AuditResult): number =>
147
- activeChecks(result.tiers) * result.pages;
160
+ activeChecks(result) * result.pages;
148
161
 
149
162
  const summaryLine = (
150
163
  counts: Record<DiagnosticSeverity, number>,
@@ -244,7 +257,7 @@ export const formatReport = (
244
257
  lines.push("");
245
258
  }
246
259
 
247
- const skipped = skippedTiers(result.tiers);
260
+ const skipped = skippedTiers(result);
248
261
  if (skipped.length > 0) {
249
262
  lines.push(...skipped, "");
250
263
  }