blume 2.0.0 → 2.0.2

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 (454) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +110 -0
  3. package/README.md +2 -2
  4. package/dist/cli/{chunk-f2972sbt.js → chunk-00gs3wqs.js} +1 -1
  5. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  6. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  7. package/dist/cli/{chunk-pat2zzwc.js → chunk-3yce002v.js} +21 -8
  8. package/dist/cli/{chunk-pat2zzwc.js.map → chunk-3yce002v.js.map} +4 -4
  9. package/dist/cli/{chunk-d1tadaw7.js → chunk-4e9b9ra6.js} +17 -7
  10. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  11. package/dist/cli/{chunk-zg2gtj10.js → chunk-5r8g91qn.js} +352 -676
  12. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  13. package/dist/cli/{chunk-11j0384y.js → chunk-6dtt0zfn.js} +14 -14
  14. package/dist/cli/{chunk-11j0384y.js.map → chunk-6dtt0zfn.js.map} +3 -3
  15. package/dist/cli/{chunk-bw22s759.js → chunk-6k4ftwze.js} +38 -19
  16. package/dist/cli/{chunk-bw22s759.js.map → chunk-6k4ftwze.js.map} +4 -4
  17. package/dist/cli/{chunk-sqn5t4q0.js → chunk-7mbqtmgb.js} +16 -7
  18. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  19. package/dist/cli/{chunk-79njf86q.js → chunk-7vtckvaw.js} +21 -19
  20. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  21. package/dist/cli/{chunk-fh5hj5jt.js → chunk-8g8ytmgx.js} +17 -18
  22. package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-8g8ytmgx.js.map} +2 -2
  23. package/dist/cli/{chunk-1w8dp3qb.js → chunk-91ws1n6j.js} +18 -15
  24. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  25. package/dist/cli/{chunk-7ez8ny0t.js → chunk-bbnwccaz.js} +2 -2
  26. package/dist/cli/{chunk-5a2z0198.js → chunk-bfwp9vp6.js} +18 -10
  27. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  28. package/dist/cli/{chunk-ernrthtr.js → chunk-d1v5rhy0.js} +14 -15
  29. package/dist/cli/{chunk-ernrthtr.js.map → chunk-d1v5rhy0.js.map} +2 -2
  30. package/dist/cli/{chunk-a9kptbw5.js → chunk-ddndchfr.js} +21 -6
  31. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  32. package/dist/cli/{chunk-zxccj738.js → chunk-esh98wmb.js} +1 -1
  33. package/dist/cli/{chunk-88cpgt6h.js → chunk-fsmrqk8a.js} +1 -1
  34. package/dist/cli/{chunk-b5aj94ah.js → chunk-g698a744.js} +5 -5
  35. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  36. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  37. package/dist/cli/{chunk-41za066z.js → chunk-h2ez8dzb.js} +4 -4
  38. package/dist/cli/{chunk-by2290sx.js → chunk-h7k3nq3v.js} +2 -2
  39. package/dist/cli/{chunk-j8mw0za6.js → chunk-hqp2ajnh.js} +252 -123
  40. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  41. package/dist/cli/{chunk-mnqj32sj.js → chunk-hr8ne106.js} +119 -50
  42. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  43. package/dist/cli/{chunk-mwt1k8n7.js → chunk-j85scx15.js} +74 -30
  44. package/dist/cli/chunk-j85scx15.js.map +10 -0
  45. package/dist/cli/{chunk-z01ze5c1.js → chunk-k7pj68a8.js} +63 -22
  46. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  47. package/dist/cli/{chunk-2q1dwty4.js → chunk-mqc662a6.js} +8 -8
  48. package/dist/cli/{chunk-2q1dwty4.js.map → chunk-mqc662a6.js.map} +4 -4
  49. package/dist/cli/{chunk-y3e45rc8.js → chunk-n1yg3tj3.js} +4 -4
  50. package/dist/cli/{chunk-bctazmbk.js → chunk-nfcyttvj.js} +17 -6
  51. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  52. package/dist/cli/{chunk-6k8vp3ta.js → chunk-pbg5a4s3.js} +60 -28
  53. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  54. package/dist/cli/{chunk-d80hr03s.js → chunk-ppzjqwx2.js} +24 -17
  55. package/dist/cli/{chunk-d80hr03s.js.map → chunk-ppzjqwx2.js.map} +4 -4
  56. package/dist/cli/{chunk-beat36xx.js → chunk-qkb5a8sa.js} +25 -10
  57. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  58. package/dist/cli/{chunk-bnbmcwfb.js → chunk-sqw4ekg1.js} +5 -5
  59. package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-sqw4ekg1.js.map} +3 -3
  60. package/dist/cli/{chunk-xaz13gwg.js → chunk-v6ya5kcb.js} +3268 -1150
  61. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  62. package/dist/cli/{chunk-tzne8qfq.js → chunk-w4bxdvsa.js} +18 -15
  63. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  64. package/dist/cli/{chunk-f7t03s3g.js → chunk-wdrt2k2v.js} +2 -2
  65. package/dist/cli/{chunk-z1f5arsg.js → chunk-xh43dwgw.js} +170 -55
  66. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  67. package/dist/cli/{chunk-pnnvybbk.js → chunk-zp79m0ts.js} +5 -5
  68. package/dist/cli/{chunk-pnnvybbk.js.map → chunk-zp79m0ts.js.map} +2 -2
  69. package/dist/cli/index.js +160 -35
  70. package/dist/cli/index.js.map +4 -4
  71. package/dist/types/ai/agent-readability.d.ts +1 -1
  72. package/dist/types/ai/agent-surface.d.ts +32 -0
  73. package/dist/types/ai/api/paths.d.ts +1 -1
  74. package/dist/types/ai/api-catalog.d.ts +7 -1
  75. package/dist/types/ai/ask-context.d.ts +14 -7
  76. package/dist/types/ai/ask.d.ts +43 -43
  77. package/dist/types/ai/component-markdown.d.ts +4 -4
  78. package/dist/types/ai/index.d.ts +3 -3
  79. package/dist/types/ai/openapi-components.d.ts +1 -1
  80. package/dist/types/ai/relative-links.d.ts +9 -4
  81. package/dist/types/ai/serializers.d.ts +1 -1
  82. package/dist/types/ai/static-expression.d.ts +28 -0
  83. package/dist/types/ai/visibility.d.ts +1 -1
  84. package/dist/types/analytics/databuddy.d.ts +43 -0
  85. package/dist/types/analytics/index.d.ts +2 -0
  86. package/dist/types/analytics/schema.d.ts +14 -0
  87. package/dist/types/cli/env.d.ts +5 -0
  88. package/dist/types/cli/init/scaffold.d.ts +19 -3
  89. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  90. package/dist/types/core/base-path.d.ts +9 -0
  91. package/dist/types/core/config-input.d.ts +21 -21
  92. package/dist/types/core/config.d.ts +5 -5
  93. package/dist/types/core/data.d.ts +3 -3
  94. package/dist/types/core/graph.d.ts +2 -0
  95. package/dist/types/core/i18n-ui.d.ts +37 -8
  96. package/dist/types/core/i18n.d.ts +7 -1
  97. package/dist/types/core/links.d.ts +3 -1
  98. package/dist/types/core/load-module.d.ts +10 -0
  99. package/dist/types/core/locale-links.d.ts +12 -2
  100. package/dist/types/core/meta.d.ts +5 -1
  101. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  102. package/dist/types/core/navigation.d.ts +38 -0
  103. package/dist/types/core/ordering-prefix.d.ts +4 -0
  104. package/dist/types/core/safe-links.d.ts +3 -1
  105. package/dist/types/core/schema.d.ts +57 -18
  106. package/dist/types/core/sources/github-releases.d.ts +5 -0
  107. package/dist/types/core/sources/lower.d.ts +14 -2
  108. package/dist/types/core/sources/normalize.d.ts +10 -1
  109. package/dist/types/core/sources/remote.d.ts +11 -1
  110. package/dist/types/core/sources/resolve.d.ts +12 -0
  111. package/dist/types/core/sources/types.d.ts +31 -0
  112. package/dist/types/core/types.d.ts +9 -0
  113. package/dist/types/core/unrecognized-keys.d.ts +1 -1
  114. package/dist/types/deploy/adapters/node.d.ts +5 -2
  115. package/dist/types/deploy/adapters/types.d.ts +7 -0
  116. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  117. package/dist/types/deploy/headers.d.ts +31 -7
  118. package/dist/types/deploy/node-headers.d.ts +43 -8
  119. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  120. package/dist/types/deploy/platforms/node.d.ts +6 -5
  121. package/dist/types/deploy/platforms/types.d.ts +7 -0
  122. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  123. package/dist/types/deploy/redirects.d.ts +7 -2
  124. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  125. package/dist/types/openapi/model.d.ts +20 -6
  126. package/dist/types/search/documents.d.ts +1 -1
  127. package/dist/types/search/orama-index.d.ts +1 -1
  128. package/dist/types/search/sync/algolia.d.ts +3 -1
  129. package/docs/01-quickstart.mdx +3 -2
  130. package/docs/02-deployment.mdx +21 -12
  131. package/docs/03-upgrading.mdx +22 -9
  132. package/docs/04-migrating.mdx +4 -4
  133. package/docs/08-faq.mdx +12 -5
  134. package/docs/advanced/changelog.mdx +1 -1
  135. package/docs/advanced/custom-pages.mdx +5 -3
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +5 -5
  138. package/docs/cli/doctor.mdx +4 -4
  139. package/docs/cli/evals.mdx +9 -9
  140. package/docs/cli/index.mdx +6 -3
  141. package/docs/cli/translate.mdx +8 -8
  142. package/docs/configuration/analytics.mdx +23 -2
  143. package/docs/configuration/{ask-ai.mdx → assistant.mdx} +20 -20
  144. package/docs/configuration/customization.mdx +5 -4
  145. package/docs/configuration/index.mdx +7 -5
  146. package/docs/configuration/meta.ts +1 -1
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +1 -1
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -3
  151. package/docs/content/index.mdx +4 -2
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +5 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -12
  156. package/docs/content/versioning.mdx +1 -0
  157. package/docs/discoverability/agent-discovery.mdx +22 -8
  158. package/docs/discoverability/index.mdx +3 -3
  159. package/docs/discoverability/llms-txt.mdx +2 -5
  160. package/docs/discoverability/markdown.mdx +5 -3
  161. package/docs/discoverability/mcp.mdx +4 -0
  162. package/docs/discoverability/metadata.mdx +3 -2
  163. package/docs/discoverability/open-graph.mdx +6 -4
  164. package/docs/discoverability/rss.mdx +3 -1
  165. package/docs/index.mdx +2 -2
  166. package/docs/references/asyncapi.mdx +1 -1
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +3 -3
  169. package/package.json +1 -1
  170. package/skills/blume/SKILL.md +5 -5
  171. package/skills/blume-migrate/SKILL.md +6 -6
  172. package/skills/blume-migrate/references/docusaurus.md +6 -6
  173. package/skills/blume-migrate/references/mintlify.md +3 -3
  174. package/skills/blume-migrate/references/monorepo.md +1 -1
  175. package/skills/blume-migrate/references/nextra.md +1 -1
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +4 -4
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/api/paths.ts +1 -1
  182. package/src/ai/api/spec.ts +15 -2
  183. package/src/ai/api-catalog.ts +7 -1
  184. package/src/ai/ask-context.ts +21 -9
  185. package/src/ai/ask-data.ts +28 -14
  186. package/src/ai/ask.ts +84 -71
  187. package/src/ai/component-markdown.ts +43 -37
  188. package/src/ai/cors.ts +3 -3
  189. package/src/ai/index.ts +16 -16
  190. package/src/ai/link-headers.ts +8 -3
  191. package/src/ai/llms.ts +4 -3
  192. package/src/ai/markdown.ts +23 -11
  193. package/src/ai/mcp/server.ts +78 -7
  194. package/src/ai/openapi-components.ts +1 -1
  195. package/src/ai/relative-links.ts +78 -10
  196. package/src/ai/serializers.ts +1 -1
  197. package/src/ai/static-expression.ts +416 -0
  198. package/src/ai/visibility.ts +46 -15
  199. package/src/analytics/databuddy.ts +67 -0
  200. package/src/analytics/head.ts +4 -0
  201. package/src/analytics/index.ts +2 -0
  202. package/src/analytics/posthog.ts +21 -3
  203. package/src/analytics/schema.ts +2 -0
  204. package/src/astro/generate.ts +90 -45
  205. package/src/astro/module-types.ts +1 -1
  206. package/src/astro/runtime-deps.ts +6 -6
  207. package/src/astro/templates.ts +87 -49
  208. package/src/audit/catalog.ts +2 -2
  209. package/src/audit/checks/assets.ts +25 -5
  210. package/src/audit/checks/content.ts +20 -2
  211. package/src/audit/checks/i18n.ts +46 -8
  212. package/src/audit/checks/indexability.ts +39 -19
  213. package/src/audit/checks/links.ts +14 -0
  214. package/src/audit/checks/llms.ts +6 -3
  215. package/src/audit/checks/network.ts +3 -1
  216. package/src/audit/checks/og-image.ts +10 -0
  217. package/src/audit/checks/robots.ts +6 -1
  218. package/src/audit/checks/sitemap.ts +56 -31
  219. package/src/audit/checks/social.ts +21 -2
  220. package/src/audit/crawl.ts +88 -15
  221. package/src/audit/report.ts +54 -17
  222. package/src/audit/run.ts +1 -0
  223. package/src/audit/types.ts +18 -0
  224. package/src/audit/url.ts +37 -6
  225. package/src/blume-modules.d.ts +2 -2
  226. package/src/cli/build-failure.ts +50 -0
  227. package/src/cli/commands/audit.ts +8 -1
  228. package/src/cli/commands/build.ts +19 -5
  229. package/src/cli/commands/check.ts +2 -0
  230. package/src/cli/commands/doctor.ts +7 -5
  231. package/src/cli/commands/eval.ts +4 -10
  232. package/src/cli/commands/init.ts +15 -5
  233. package/src/cli/commands/migrate.ts +2 -2
  234. package/src/cli/commands/preview.ts +15 -0
  235. package/src/cli/commands/sync.ts +2 -0
  236. package/src/cli/commands/translate.ts +12 -9
  237. package/src/cli/commands/upgrade.ts +13 -2
  238. package/src/cli/eject-scripts.ts +32 -7
  239. package/src/cli/env.ts +12 -1
  240. package/src/cli/init/scaffold.ts +117 -11
  241. package/src/cli/init/starter-spec.ts +235 -0
  242. package/src/cli/required-secrets.ts +3 -3
  243. package/src/components/content/AccordionItem.astro +26 -22
  244. package/src/components/content/Badge.astro +2 -9
  245. package/src/components/content/Card.astro +2 -2
  246. package/src/components/content/Component.astro +9 -3
  247. package/src/components/content/Expandable.astro +5 -1
  248. package/src/components/content/Frame.astro +2 -7
  249. package/src/components/content/GithubInfo.astro +12 -2
  250. package/src/components/content/Prompt.astro +2 -7
  251. package/src/components/content/Tabs.astro +54 -5
  252. package/src/components/content/Tile.astro +1 -1
  253. package/src/components/content/Tooltip.astro +69 -7
  254. package/src/components/content/Tree.astro +7 -2
  255. package/src/components/content/TypeTable.astro +10 -5
  256. package/src/components/content/Update.astro +8 -2
  257. package/src/components/content/auto-type-table.ts +4 -1
  258. package/src/components/content/badge-color.ts +17 -0
  259. package/src/components/content/base-href.ts +18 -3
  260. package/src/components/content/inline-markdown.ts +27 -7
  261. package/src/components/copy-feedback.ts +36 -9
  262. package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
  263. package/src/components/islands/{ask-ai.tsx → assistant.tsx} +39 -25
  264. package/src/components/islands/hooks.ts +21 -14
  265. package/src/components/islands/webmcp.ts +12 -8
  266. package/src/components/layout/Banner.astro +23 -4
  267. package/src/components/layout/Header.astro +32 -19
  268. package/src/components/layout/Logo.astro +5 -0
  269. package/src/components/layout/NavSelector.astro +6 -1
  270. package/src/components/layout/NavTabMenu.astro +133 -0
  271. package/src/components/layout/NavTree.astro +15 -6
  272. package/src/components/layout/NavTreeCache.astro +5 -2
  273. package/src/components/layout/NavTreeScript.astro +45 -5
  274. package/src/components/layout/PageLayout.astro +28 -14
  275. package/src/components/layout/Pagination.astro +7 -7
  276. package/src/components/layout/ReferenceLayout.astro +5 -2
  277. package/src/components/layout/RootLayout.astro +27 -14
  278. package/src/components/layout/Search.astro +18 -14
  279. package/src/components/layout/analytics-client.ts +3 -1
  280. package/src/components/layout/drawer-inert.ts +1 -1
  281. package/src/components/openapi/GraphqlType.astro +11 -3
  282. package/src/components/openapi/MessageComposer.astro +1 -1
  283. package/src/components/openapi/Operation.astro +21 -5
  284. package/src/components/openapi/PanelTabs.astro +4 -1
  285. package/src/components/openapi/Playground.astro +8 -2
  286. package/src/components/openapi/RequestPanel.astro +9 -5
  287. package/src/components/openapi/SchemaProperty.astro +11 -33
  288. package/src/components/openapi/SchemaTable.astro +27 -52
  289. package/src/components/openapi/description.ts +2 -2
  290. package/src/components/openapi/helpers.ts +90 -12
  291. package/src/components/openapi/message-composer.ts +8 -0
  292. package/src/components/openapi/message-model.ts +12 -2
  293. package/src/components/openapi/message.ts +4 -1
  294. package/src/components/openapi/operation-model.ts +40 -8
  295. package/src/components/openapi/panel.ts +29 -5
  296. package/src/components/openapi/playground-client.ts +37 -7
  297. package/src/components/openapi/playground-schema.ts +25 -6
  298. package/src/components/openapi/request.ts +2 -0
  299. package/src/components/openapi/schema-tree.ts +203 -0
  300. package/src/components/openapi/snippets.ts +30 -15
  301. package/src/components/openapi/validate-json.ts +1 -1
  302. package/src/core/base-path.ts +15 -0
  303. package/src/core/code-fences.ts +1 -1
  304. package/src/core/config-input.ts +23 -23
  305. package/src/core/config.ts +21 -7
  306. package/src/core/data.ts +3 -3
  307. package/src/core/diagnostics.ts +217 -30
  308. package/src/core/graph.ts +120 -11
  309. package/src/core/i18n-ui.ts +74 -11
  310. package/src/core/i18n.ts +13 -2
  311. package/src/core/links.ts +7 -4
  312. package/src/core/load-module.ts +20 -0
  313. package/src/core/locale-links.ts +17 -17
  314. package/src/core/manifest.ts +3 -2
  315. package/src/core/meta.ts +69 -6
  316. package/src/core/nav-diagnostics.ts +56 -1
  317. package/src/core/navigation.ts +245 -76
  318. package/src/core/ordering-prefix.ts +27 -0
  319. package/src/core/project-graph.ts +23 -1
  320. package/src/core/request-body.ts +1 -1
  321. package/src/core/safe-href.ts +53 -1
  322. package/src/core/safe-links.ts +11 -2
  323. package/src/core/schema.ts +95 -34
  324. package/src/core/server-features.ts +2 -2
  325. package/src/core/sources/assets.ts +83 -41
  326. package/src/core/sources/contentful-rich-text.ts +3 -2
  327. package/src/core/sources/contentful.ts +25 -12
  328. package/src/core/sources/filesystem.ts +5 -1
  329. package/src/core/sources/github-releases.ts +106 -5
  330. package/src/core/sources/lexical.ts +9 -4
  331. package/src/core/sources/lower.ts +79 -26
  332. package/src/core/sources/mdx-remote.ts +1 -0
  333. package/src/core/sources/normalize.ts +132 -30
  334. package/src/core/sources/notion.ts +26 -10
  335. package/src/core/sources/obsidian.ts +17 -3
  336. package/src/core/sources/payload.ts +1 -0
  337. package/src/core/sources/portable-text.ts +63 -44
  338. package/src/core/sources/remote.ts +18 -2
  339. package/src/core/sources/resolve.ts +52 -33
  340. package/src/core/sources/sanity.ts +1 -0
  341. package/src/core/sources/strapi-blocks.ts +9 -4
  342. package/src/core/sources/strapi.ts +1 -0
  343. package/src/core/sources/types.ts +31 -0
  344. package/src/core/types.ts +9 -0
  345. package/src/core/ui-packs/ar.ts +17 -5
  346. package/src/core/ui-packs/bg.ts +17 -5
  347. package/src/core/ui-packs/bn.ts +17 -5
  348. package/src/core/ui-packs/ca.ts +17 -5
  349. package/src/core/ui-packs/cs.ts +17 -5
  350. package/src/core/ui-packs/da.ts +17 -5
  351. package/src/core/ui-packs/de.ts +17 -5
  352. package/src/core/ui-packs/el.ts +17 -5
  353. package/src/core/ui-packs/es.ts +17 -5
  354. package/src/core/ui-packs/fa.ts +17 -5
  355. package/src/core/ui-packs/fi.ts +17 -5
  356. package/src/core/ui-packs/fr.ts +17 -5
  357. package/src/core/ui-packs/he.ts +17 -5
  358. package/src/core/ui-packs/hi.ts +17 -5
  359. package/src/core/ui-packs/hr.ts +17 -5
  360. package/src/core/ui-packs/hu.ts +17 -5
  361. package/src/core/ui-packs/id.ts +17 -5
  362. package/src/core/ui-packs/it.ts +17 -5
  363. package/src/core/ui-packs/ja.ts +17 -5
  364. package/src/core/ui-packs/ko.ts +17 -5
  365. package/src/core/ui-packs/nl.ts +17 -5
  366. package/src/core/ui-packs/no.ts +17 -5
  367. package/src/core/ui-packs/pl.ts +17 -5
  368. package/src/core/ui-packs/pt-br.ts +17 -5
  369. package/src/core/ui-packs/pt.ts +17 -5
  370. package/src/core/ui-packs/ro.ts +17 -5
  371. package/src/core/ui-packs/ru.ts +17 -5
  372. package/src/core/ui-packs/sk.ts +17 -5
  373. package/src/core/ui-packs/sr.ts +17 -5
  374. package/src/core/ui-packs/sv.ts +17 -5
  375. package/src/core/ui-packs/th.ts +17 -5
  376. package/src/core/ui-packs/tr.ts +17 -5
  377. package/src/core/ui-packs/uk.ts +17 -5
  378. package/src/core/ui-packs/vi.ts +17 -5
  379. package/src/core/ui-packs/zh-tw.ts +17 -5
  380. package/src/core/ui-packs/zh.ts +17 -5
  381. package/src/core/unrecognized-keys.ts +1 -1
  382. package/src/core/version-cut.ts +17 -2
  383. package/src/core/versions.ts +4 -1
  384. package/src/deploy/adapters/node.ts +5 -2
  385. package/src/deploy/adapters/registry.ts +2 -1
  386. package/src/deploy/adapters/types.ts +13 -1
  387. package/src/deploy/artifacts.ts +25 -5
  388. package/src/deploy/cloudflare-negotiation.ts +23 -4
  389. package/src/deploy/headers.ts +67 -51
  390. package/src/deploy/node-headers.ts +148 -27
  391. package/src/deploy/platforms/cloudflare.ts +1 -0
  392. package/src/deploy/platforms/netlify.ts +82 -5
  393. package/src/deploy/platforms/node.ts +7 -5
  394. package/src/deploy/platforms/static.ts +1 -0
  395. package/src/deploy/platforms/types.ts +7 -0
  396. package/src/deploy/platforms/vercel.ts +9 -3
  397. package/src/deploy/redirects.ts +14 -3
  398. package/src/deploy/vercel-negotiation.ts +35 -3
  399. package/src/eval/agents.ts +10 -2
  400. package/src/eval/run.ts +25 -0
  401. package/src/markdown/base-links.ts +55 -8
  402. package/src/markdown/index.ts +6 -3
  403. package/src/markdown/relative-links.ts +3 -23
  404. package/src/markdown/route-snapshot.ts +37 -0
  405. package/src/og/card.ts +20 -4
  406. package/src/og/derive.ts +145 -4
  407. package/src/openapi/graphql-build.ts +28 -2
  408. package/src/openapi/model.ts +77 -13
  409. package/src/openapi/render-mdx.ts +10 -1
  410. package/src/registry/eject.ts +135 -37
  411. package/src/search/documents.ts +24 -3
  412. package/src/search/orama-index.ts +1 -1
  413. package/src/search/sync/algolia.ts +36 -2
  414. package/src/sources/registry.ts +5 -0
  415. package/src/theme/entry.ts +11 -4
  416. package/src/translate/agents.ts +6 -1
  417. package/src/translate/ledger.ts +26 -3
  418. package/src/translate/meta.ts +11 -3
  419. package/src/translate/report.ts +1 -1
  420. package/src/translate/run.ts +11 -5
  421. package/src/translate/validate.ts +10 -1
  422. package/src/translate/work-list.ts +36 -3
  423. package/src/upgrade/upgrade.ts +37 -5
  424. package/dist/cli/chunk-1w8dp3qb.js.map +0 -10
  425. package/dist/cli/chunk-5a2z0198.js.map +0 -10
  426. package/dist/cli/chunk-6k8vp3ta.js.map +0 -19
  427. package/dist/cli/chunk-79njf86q.js.map +0 -11
  428. package/dist/cli/chunk-a9kptbw5.js.map +0 -14
  429. package/dist/cli/chunk-abh8yjkn.js +0 -31
  430. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  431. package/dist/cli/chunk-bctazmbk.js.map +0 -10
  432. package/dist/cli/chunk-beat36xx.js.map +0 -10
  433. package/dist/cli/chunk-d1tadaw7.js.map +0 -10
  434. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  435. package/dist/cli/chunk-j8mw0za6.js.map +0 -35
  436. package/dist/cli/chunk-mnqj32sj.js.map +0 -12
  437. package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
  438. package/dist/cli/chunk-nk3ts2xk.js +0 -51
  439. package/dist/cli/chunk-nk3ts2xk.js.map +0 -10
  440. package/dist/cli/chunk-sqn5t4q0.js.map +0 -10
  441. package/dist/cli/chunk-tzne8qfq.js.map +0 -10
  442. package/dist/cli/chunk-xaz13gwg.js.map +0 -182
  443. package/dist/cli/chunk-z01ze5c1.js.map +0 -10
  444. package/dist/cli/chunk-z1f5arsg.js.map +0 -36
  445. package/dist/cli/chunk-zg2gtj10.js.map +0 -35
  446. /package/dist/cli/{chunk-f2972sbt.js.map → chunk-00gs3wqs.js.map} +0 -0
  447. /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-bbnwccaz.js.map} +0 -0
  448. /package/dist/cli/{chunk-zxccj738.js.map → chunk-esh98wmb.js.map} +0 -0
  449. /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-fsmrqk8a.js.map} +0 -0
  450. /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-g698a744.js.map} +0 -0
  451. /package/dist/cli/{chunk-41za066z.js.map → chunk-h2ez8dzb.js.map} +0 -0
  452. /package/dist/cli/{chunk-by2290sx.js.map → chunk-h7k3nq3v.js.map} +0 -0
  453. /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-n1yg3tj3.js.map} +0 -0
  454. /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-wdrt2k2v.js.map} +0 -0
@@ -180,8 +180,10 @@ export const networkChecks: CheckModule = {
180
180
  found.push(...servedPageChecks(context, page, result, origin));
181
181
  }
182
182
 
183
+ // With `seo.robots: false` Blume writes no robots.txt, so its absence on
184
+ // the live site is the configured outcome, not a defect.
183
185
  const robots = results.get(robotsUrl);
184
- if (robots && !robots.ok) {
186
+ if (context.project.config.seo.robots && robots && !robots.ok) {
185
187
  found.push(
186
188
  finding(
187
189
  "BLUME_AUDIT_ROBOTS_NOT_ACCESSIBLE",
@@ -71,6 +71,16 @@ export const ogImageChecks: CheckModule = {
71
71
  if (resolved.kind === "external" || resolved.kind === "ignored") {
72
72
  continue;
73
73
  }
74
+ if (resolved.kind === "outside-base") {
75
+ found.push(
76
+ finding(
77
+ "BLUME_AUDIT_OG_IMAGE_BROKEN",
78
+ pageSite(context, page, ["seo", "image"]),
79
+ `og:image points at ${resolved.path}, which is missing deployment.base (${deployBase}), so the deployed site does not serve it.`
80
+ )
81
+ );
82
+ continue;
83
+ }
74
84
 
75
85
  const { path } = resolved;
76
86
  if (
@@ -47,7 +47,12 @@ export const robotsChecks: CheckModule = {
47
47
  )
48
48
  );
49
49
 
50
- if (site && robots.sitemaps.length === 0) {
50
+ // Blume only writes the Sitemap line when there is a sitemap to point at.
51
+ if (
52
+ site &&
53
+ context.project.config.seo.sitemap &&
54
+ robots.sitemaps.length === 0
55
+ ) {
51
56
  found.push(
52
57
  finding(
53
58
  "BLUME_AUDIT_ROBOTS_SITEMAP_MISSING",
@@ -138,6 +138,40 @@ const checkListedUrl = (
138
138
  return found;
139
139
  };
140
140
 
141
+ /**
142
+ * The other direction: a page that was built, is indexable, and should be
143
+ * findable — but never made it into the sitemap. A page that names another
144
+ * URL as its canonical (an archived-version page pointing at the latest docs)
145
+ * and an i18n fallback copy are left out of the sitemap on purpose: it lists
146
+ * canonical URLs only (see NON_CANONICAL_IN_SITEMAP).
147
+ */
148
+ const unlistedPages = (
149
+ context: AuditContext,
150
+ listed: Map<string, string>,
151
+ deployBase: string
152
+ ): Diagnostic[] =>
153
+ context.pages.flatMap((page) => {
154
+ const canonical = page.canonical
155
+ ? canonicalPath(page.canonical, deployBase)
156
+ : null;
157
+ if (
158
+ !page.indexable ||
159
+ page.route?.fallback ||
160
+ (canonical && canonical !== normalizePath(page.url)) ||
161
+ ERROR_ROUTES.has(page.url) ||
162
+ listed.has(normalizePath(page.url))
163
+ ) {
164
+ return [];
165
+ }
166
+ return [
167
+ finding(
168
+ "BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP",
169
+ pageSite(context, page),
170
+ `${page.url} is built and indexable but is not listed in sitemap.xml.`
171
+ ),
172
+ ];
173
+ });
174
+
141
175
  /**
142
176
  * The sitemap, cross-checked against what was actually built.
143
177
  *
@@ -175,23 +209,32 @@ export const sitemapChecks: CheckModule = {
175
209
  finding(
176
210
  "BLUME_AUDIT_SITEMAP_INVALID",
177
211
  { file: sitemap.file, url: "/sitemap.xml" },
178
- `sitemap.xml is not a valid urlset: ${sitemap.error}`
212
+ `sitemap.xml is not a valid urlset or sitemap index: ${sitemap.error}`
179
213
  )
180
214
  );
181
215
  return found;
182
216
  }
183
217
 
184
- if (
185
- sitemap.bytes > MAX_SITEMAP_BYTES ||
186
- sitemap.urls.length > MAX_SITEMAP_URLS
187
- ) {
188
- found.push(
189
- finding(
190
- "BLUME_AUDIT_SITEMAP_TOO_LARGE",
191
- { file: sitemap.file, url: "/sitemap.xml" },
192
- `sitemap.xml holds ${sitemap.urls.length} URLs in ${Math.round(sitemap.bytes / 1024 / 1024)} MB.`
193
- )
194
- );
218
+ // The limits are per file: a sitemap index splits a large site across
219
+ // urlsets, each of which must fit on its own.
220
+ const parts = sitemap.parts ?? [
221
+ {
222
+ bytes: sitemap.bytes,
223
+ file: sitemap.file,
224
+ url: "/sitemap.xml",
225
+ urls: sitemap.urls.length,
226
+ },
227
+ ];
228
+ for (const part of parts) {
229
+ if (part.bytes > MAX_SITEMAP_BYTES || part.urls > MAX_SITEMAP_URLS) {
230
+ found.push(
231
+ finding(
232
+ "BLUME_AUDIT_SITEMAP_TOO_LARGE",
233
+ { file: part.file, url: part.url },
234
+ `${part.url.slice(1)} holds ${part.urls} URLs in ${Math.round(part.bytes / 1024 / 1024)} MB.`
235
+ )
236
+ );
237
+ }
195
238
  }
196
239
 
197
240
  // A `<lastmod>` that lies — malformed, or claiming the future — teaches
@@ -230,25 +273,7 @@ export const sitemapChecks: CheckModule = {
230
273
  );
231
274
  }
232
275
 
233
- // The other direction: a page that was built, is indexable, and should be
234
- // findable — but never made it into the sitemap.
235
- for (const page of context.pages) {
236
- if (
237
- !page.indexable ||
238
- ERROR_ROUTES.has(page.url) ||
239
- listed.has(normalizePath(page.url))
240
- ) {
241
- continue;
242
- }
243
- found.push(
244
- finding(
245
- "BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP",
246
- pageSite(context, page),
247
- `${page.url} is built and indexable but is not listed in sitemap.xml.`
248
- )
249
- );
250
- }
251
-
276
+ found.push(...unlistedPages(context, listed, deployBase));
252
277
  return found;
253
278
  },
254
279
  tier: "static",
@@ -1,7 +1,8 @@
1
+ import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
1
2
  import type { Diagnostic } from "../../core/types.ts";
2
3
  import { finding } from "../catalog.ts";
3
4
  import { pageSite } from "../locate.ts";
4
- import type { CheckModule, PageSnapshot } from "../types.ts";
5
+ import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
5
6
  import { normalizePath } from "../url.ts";
6
7
 
7
8
  /** The Open Graph properties a share card is unusable without. */
@@ -175,6 +176,23 @@ const URL_STYLE: { name: string; test: RegExp }[] = [
175
176
  { name: "spaces", test: /%20| /u },
176
177
  ];
177
178
 
179
+ /**
180
+ * The part of a page URL its author named: the URL minus a leading i18n locale
181
+ * segment. A locale code like `pt-BR` is BCP 47 casing set once in config,
182
+ * not an untidy slug, so it must not trip the uppercase check on every page
183
+ * of that locale. The segment follows `basePath`, which page URLs carry.
184
+ */
185
+ const authoredPath = (context: AuditContext, url: string): string => {
186
+ const { basePath, i18n } = context.project.config;
187
+ if (!i18n) {
188
+ return url;
189
+ }
190
+ const base = normalizeBasePath(basePath);
191
+ const [, first = "", ...rest] = stripBasePath(base, url).split("/");
192
+ const isLocale = i18n.locales.some((locale) => locale.code === first);
193
+ return isLocale ? `${base}/${rest.join("/")}` : url;
194
+ };
195
+
178
196
  /** A protocol-relative URL names a dotted host (`//cdn.example.com/x`). */
179
197
  const DOTTED_HOST = /^\/\/[^/]*\./u;
180
198
 
@@ -220,7 +238,8 @@ export const urlChecks: CheckModule = {
220
238
  }
221
239
  }
222
240
 
223
- const untidy = URL_STYLE.filter((style) => style.test.test(page.url));
241
+ const authored = authoredPath(context, page.url);
242
+ const untidy = URL_STYLE.filter((style) => style.test.test(authored));
224
243
  if (untidy.length > 0) {
225
244
  found.push(
226
245
  finding(
@@ -125,6 +125,11 @@ type XmlValue =
125
125
  const isUrlsetElement = (value: XmlValue): value is { url?: XmlValue } =>
126
126
  typeof value === "object" && value !== null;
127
127
 
128
+ const isSitemapIndexElement = (
129
+ value: XmlValue
130
+ ): value is { sitemap?: XmlValue } =>
131
+ typeof value === "object" && value !== null;
132
+
128
133
  const isUrlEntry = (
129
134
  value: XmlValue
130
135
  ): value is { lastmod?: XmlValue; loc?: XmlValue } =>
@@ -134,7 +139,9 @@ const isText = (value: XmlValue): value is string => typeof value === "string";
134
139
 
135
140
  /**
136
141
  * Parse `sitemap.xml`. Deliberately shallow: the checks only need the `<loc>`
137
- * list, each loc's `<lastmod>`, and whether the document is a urlset at all.
142
+ * list, each loc's `<lastmod>`, and whether the document is a urlset at all —
143
+ * or, for a sitemap index, the child sitemaps it lists (`sitemaps`), which
144
+ * the crawler reads from the build.
138
145
  */
139
146
  export const parseSitemap = (
140
147
  file: string,
@@ -149,10 +156,20 @@ export const parseSitemap = (
149
156
  doc.error = "no <urlset> element";
150
157
  return doc;
151
158
  }
159
+ if (Object.hasOwn(parsed, "sitemapindex")) {
160
+ const { sitemapindex } = parsed;
161
+ const children = isSitemapIndexElement(sitemapindex)
162
+ ? [sitemapindex.sitemap].flat()
163
+ : [];
164
+ doc.sitemaps = children.flatMap((child) =>
165
+ isUrlEntry(child) && isText(child.loc) && child.loc.trim()
166
+ ? [child.loc.trim()]
167
+ : []
168
+ );
169
+ return doc;
170
+ }
152
171
  if (!Object.hasOwn(parsed, "urlset")) {
153
- doc.error = Object.hasOwn(parsed, "sitemapindex")
154
- ? "sitemap is an index, not a urlset"
155
- : "no <urlset> element";
172
+ doc.error = "no <urlset> element";
156
173
  return doc;
157
174
  }
158
175
  const { urlset } = parsed;
@@ -252,6 +269,69 @@ const readIfPresent = async (file: string): Promise<string | null> => {
252
269
  }
253
270
  };
254
271
 
272
+ /**
273
+ * Read `sitemap.xml`, following it through a sitemap index: each child
274
+ * sitemap is read from the build (its `<loc>` carries the site origin and the
275
+ * deployment base, the file tree neither) and its URLs merged into the one
276
+ * document the checks see. A child that can't be resolved, is missing, or
277
+ * isn't a urlset makes the whole document invalid.
278
+ */
279
+ const readSitemap = async (
280
+ staticDir: string,
281
+ files: Map<string, number>,
282
+ deployBase: string
283
+ ): Promise<SitemapDoc | null> => {
284
+ const file = join(staticDir, "sitemap.xml");
285
+ const xml = await readIfPresent(file);
286
+ if (xml === null) {
287
+ return null;
288
+ }
289
+ const doc = parseSitemap(
290
+ file,
291
+ xml,
292
+ files.get("/sitemap.xml") ?? Buffer.byteLength(xml, "utf-8")
293
+ );
294
+ if (!doc.sitemaps) {
295
+ return doc;
296
+ }
297
+ doc.parts = [];
298
+ for (const loc of doc.sitemaps) {
299
+ let url: string;
300
+ try {
301
+ url = stripBasePath(deployBase, decodeURI(new URL(loc).pathname));
302
+ } catch {
303
+ doc.error = `the sitemap index lists "${loc}", which is not a valid absolute URL`;
304
+ return doc;
305
+ }
306
+ const childFile = join(staticDir, url);
307
+ // oxlint-disable-next-line no-await-in-loop -- one read per child, in index order
308
+ const childXml = await readIfPresent(childFile);
309
+ const child =
310
+ childXml === null
311
+ ? null
312
+ : parseSitemap(
313
+ childFile,
314
+ childXml,
315
+ files.get(url) ?? Buffer.byteLength(childXml, "utf-8")
316
+ );
317
+ if (!child || child.error || child.sitemaps) {
318
+ doc.error = `the sitemap index lists ${loc}, which is not a urlset in the build`;
319
+ return doc;
320
+ }
321
+ doc.urls.push(...child.urls);
322
+ for (const [key, value] of child.lastmod ?? []) {
323
+ doc.lastmod?.set(key, value);
324
+ }
325
+ doc.parts.push({
326
+ bytes: child.bytes,
327
+ file: childFile,
328
+ url,
329
+ urls: child.urls.length,
330
+ });
331
+ }
332
+ return doc;
333
+ };
334
+
255
335
  /**
256
336
  * Read the built site: every HTML page reduced to a snapshot, the full file
257
337
  * index (for resolving subresource references), plus sitemap.xml and robots.txt.
@@ -260,8 +340,10 @@ export const crawlStaticDir = async (options: {
260
340
  staticDir: string;
261
341
  manifest: BlumeManifest;
262
342
  basePath: string;
343
+ /** The normalized `deployment.base`, which sitemap-index `<loc>`s carry. */
344
+ deployBase?: string;
263
345
  }): Promise<CrawlResult> => {
264
- const { staticDir, manifest, basePath } = options;
346
+ const { staticDir, manifest, basePath, deployBase = "" } = options;
265
347
  const routes = routeIndex(manifest, basePath);
266
348
 
267
349
  const htmlFiles = await glob("**/*.html", { absolute: true, cwd: staticDir });
@@ -287,8 +369,6 @@ export const crawlStaticDir = async (options: {
287
369
  );
288
370
  const pages = snapshots.filter((page) => page !== null);
289
371
 
290
- const sitemapFile = join(staticDir, "sitemap.xml");
291
- const sitemapXml = await readIfPresent(sitemapFile);
292
372
  const robotsFile = join(staticDir, "robots.txt");
293
373
  const robotsTxt = await readIfPresent(robotsFile);
294
374
  const llmsFile = join(staticDir, "llms.txt");
@@ -300,13 +380,6 @@ export const crawlStaticDir = async (options: {
300
380
  llms: llmsText === null ? null : parseLlms(llmsFile, llmsText),
301
381
  pages,
302
382
  robots: robotsTxt === null ? null : parseRobots(robotsFile, robotsTxt),
303
- sitemap:
304
- sitemapXml === null
305
- ? null
306
- : parseSitemap(
307
- sitemapFile,
308
- sitemapXml,
309
- files.get("/sitemap.xml") ?? Buffer.byteLength(sitemapXml, "utf-8")
310
- ),
383
+ sitemap: await readSitemap(staticDir, files, deployBase),
311
384
  };
312
385
  };
@@ -33,12 +33,47 @@ const TIER_FLAGS: { tier: AuditTier; flag: string }[] = [
33
33
  interface CheckRollup {
34
34
  id: CheckId;
35
35
  count: number;
36
+ /** Distinct affected URLs — one page can carry several findings. */
37
+ pages: number;
36
38
  severity: DiagnosticSeverity;
37
39
  category: AuditCategory;
38
40
  title: string;
39
41
  findings: Diagnostic[];
40
42
  }
41
43
 
44
+ const SEVERITY_ORDER = {
45
+ error: 0,
46
+ info: 2,
47
+ warning: 1,
48
+ } satisfies Record<DiagnosticSeverity, number>;
49
+
50
+ /**
51
+ * The group's severity: the worst of its findings'. A check can downgrade a
52
+ * finding at runtime (an external link answering 503 is a warning, not the
53
+ * catalog's error), and the group's glyph must agree with the summary counts.
54
+ */
55
+ const worstSeverity = (findings: Diagnostic[]): DiagnosticSeverity => {
56
+ let worst: DiagnosticSeverity = "info";
57
+ for (const finding of findings) {
58
+ if (SEVERITY_ORDER[finding.severity] < SEVERITY_ORDER[worst]) {
59
+ worst = finding.severity;
60
+ }
61
+ }
62
+ return worst;
63
+ };
64
+
65
+ /** The first finding per affected URL, in report order. */
66
+ const firstPerPage = (findings: Diagnostic[]): Diagnostic[] => {
67
+ const seen = new Set<string | undefined>();
68
+ return findings.filter((finding) => {
69
+ if (seen.has(finding.url)) {
70
+ return false;
71
+ }
72
+ seen.add(finding.url);
73
+ return true;
74
+ });
75
+ };
76
+
42
77
  /**
43
78
  * Group findings by check. This is the difference between a report people read
44
79
  * and one they close: 214 pages × 6 findings is an unreadable wall, but "Meta
@@ -55,22 +90,18 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
55
90
  }
56
91
  }
57
92
 
58
- const order = {
59
- error: 0,
60
- info: 2,
61
- warning: 1,
62
- } satisfies Record<DiagnosticSeverity, number>;
63
93
  const checks = [...groups.entries()].map(([id, findings]) => {
64
94
  // SAFETY: audit findings are only ever created through `finding()`, whose
65
95
  // codes are the check catalog's ids.
66
96
  const checkId = id as CheckId;
67
- const { category, severity, title } = checkMeta(checkId);
97
+ const { category, title } = checkMeta(checkId);
68
98
  return {
69
99
  category,
70
100
  count: findings.length,
71
101
  findings,
72
102
  id: checkId,
73
- severity,
103
+ pages: firstPerPage(findings).length,
104
+ severity: worstSeverity(findings),
74
105
  title,
75
106
  };
76
107
  });
@@ -80,7 +111,7 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
80
111
  // and print "content" three separate times.
81
112
  const worst = new Map<AuditCategory, number>();
82
113
  for (const check of checks) {
83
- const rank = order[check.severity];
114
+ const rank = SEVERITY_ORDER[check.severity];
84
115
  worst.set(
85
116
  check.category,
86
117
  Math.min(worst.get(check.category) ?? rank, rank)
@@ -91,7 +122,7 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
91
122
  (a, b) =>
92
123
  (worst.get(a.category) ?? 0) - (worst.get(b.category) ?? 0) ||
93
124
  a.category.localeCompare(b.category) ||
94
- order[a.severity] - order[b.severity] ||
125
+ SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] ||
95
126
  b.count - a.count
96
127
  );
97
128
  };
@@ -176,14 +207,16 @@ export const formatReport = (
176
207
  }
177
208
 
178
209
  const color = SEVERITY_COLOR[group.severity];
179
- const pages = `${group.count} page${group.count === 1 ? "" : "s"}`;
210
+ const pages = `${group.pages} page${group.pages === 1 ? "" : "s"}`;
180
211
  lines.push(
181
212
  ` ${color(`${GLYPH[group.severity]} ${group.title}`)} ${colors.dim(pages)}`
182
213
  );
183
214
 
215
+ // The preview lists affected pages, each once; --verbose lists every
216
+ // finding, since each one's message names a different specific.
184
217
  const shown = options.verbose
185
218
  ? group.findings
186
- : group.findings.slice(0, PREVIEW);
219
+ : firstPerPage(group.findings).slice(0, PREVIEW);
187
220
  for (const diagnostic of shown) {
188
221
  lines.push(findingLine(diagnostic, root));
189
222
  // The message names the specifics the rolled-up line can't — which target
@@ -192,16 +225,20 @@ export const formatReport = (
192
225
  lines.push(` ${colors.dim(diagnostic.message)}`);
193
226
  }
194
227
  }
195
- const hidden = group.count - shown.length;
228
+ const hidden = options.verbose ? 0 : group.pages - shown.length;
196
229
  if (hidden > 0) {
197
230
  lines.push(` ${colors.dim(`… and ${hidden} more (--verbose)`)}`);
198
231
  }
199
232
 
200
- // Every finding in a group shares the catalog's fix unless it overrode it,
201
- // so showing the first one's is showing the group's.
202
- const [first] = group.findings;
203
- const fix = first?.suggestion;
204
- if (fix) {
233
+ // Findings share the catalog's fix unless one overrode it (a navigation
234
+ // entry, a page generated from an API spec), so each distinct fix is
235
+ // printed once.
236
+ const fixes = new Set(
237
+ group.findings.flatMap((finding) =>
238
+ finding.suggestion ? [finding.suggestion] : []
239
+ )
240
+ );
241
+ for (const fix of fixes) {
205
242
  lines.push(` ${colors.cyan(`fix: ${fix}`)}`);
206
243
  }
207
244
  lines.push("");
package/src/audit/run.ts CHANGED
@@ -137,6 +137,7 @@ export const runAudit = async (options: AuditOptions): Promise<AuditResult> => {
137
137
 
138
138
  const crawl = await crawlStaticDir({
139
139
  basePath,
140
+ deployBase: normalizeBasePath(project.config.deployment.options.base),
140
141
  manifest: project.manifest,
141
142
  staticDir,
142
143
  });
@@ -113,6 +113,16 @@ export interface RedirectResolution {
113
113
  outcome: "ok" | "loop" | "broken" | "chain";
114
114
  }
115
115
 
116
+ /** One urlset file behind a sitemap, for the per-file size limits. */
117
+ export interface SitemapPart {
118
+ file: string;
119
+ /** Site path the file is served at, e.g. `/sitemap-1.xml`. */
120
+ url: string;
121
+ bytes: number;
122
+ /** How many `<url>` entries the file lists. */
123
+ urls: number;
124
+ }
125
+
116
126
  /** A parsed `sitemap.xml`. */
117
127
  export interface SitemapDoc {
118
128
  file: string;
@@ -121,6 +131,14 @@ export interface SitemapDoc {
121
131
  urls: string[];
122
132
  /** Each `<url>` block's `<lastmod>`, keyed by its `<loc>`. */
123
133
  lastmod?: Map<string, string>;
134
+ /**
135
+ * The child sitemaps' `<loc>`s, when `sitemap.xml` is a sitemap index —
136
+ * Blume writes one past 50,000 URLs. The crawler reads each child from the
137
+ * build and merges its URLs into `urls` and `lastmod`.
138
+ */
139
+ sitemaps?: string[];
140
+ /** The index's child urlset files, once the crawler has read them. */
141
+ parts?: SitemapPart[];
124
142
  /** Parse failure, when the document isn't usable. */
125
143
  error?: string;
126
144
  }
package/src/audit/url.ts CHANGED
@@ -43,11 +43,21 @@ export type ResolvedHref =
43
43
  | { kind: "internal"; path: string; hash: string }
44
44
  /** An absolute URL that resolves back to this site — should have been a path. */
45
45
  | { kind: "self-origin"; path: string; hash: string }
46
+ /**
47
+ * A path on this origin that lacks `deployment.base`. The host serves the
48
+ * whole build under the base, so nothing answers here — `path` is the path
49
+ * as requested, base missing.
50
+ */
51
+ | { kind: "outside-base"; path: string }
46
52
  /** An absolute URL on another origin. */
47
53
  | { kind: "external"; url: string }
48
54
  /** In-page anchor, `mailto:`, `tel:`, `javascript:`, data URI — not a page link. */
49
55
  | { kind: "ignored" };
50
56
 
57
+ /** Whether a served path sits under the deployment base (always, without one). */
58
+ const underBase = (deployBase: string, path: string): boolean =>
59
+ !deployBase || path === deployBase || path.startsWith(`${deployBase}/`);
60
+
51
61
  const NON_HTTP_SCHEME = /^(?!https?:)[a-z][a-z0-9+.-]*:/iu;
52
62
 
53
63
  /**
@@ -65,6 +75,20 @@ export const decodePath = (path: string): string => {
65
75
  }
66
76
  };
67
77
 
78
+ /**
79
+ * Whether a `--url` value is an absolute http(s) URL the network tier can
80
+ * probe. A bare host (`example.com`) has no scheme, and `localhost:4321`
81
+ * parses with `localhost:` as its scheme — both are rejected.
82
+ */
83
+ export const isHttpUrl = (input: string): boolean => {
84
+ try {
85
+ const { protocol } = new URL(input);
86
+ return protocol === "http:" || protocol === "https:";
87
+ } catch {
88
+ return false;
89
+ }
90
+ };
91
+
68
92
  /** The origin of `deployment.site`, or null when no site is configured. */
69
93
  export const siteOrigin = (site?: string): string | null => {
70
94
  if (!site) {
@@ -113,12 +137,14 @@ export const resolveHref = (
113
137
  return { kind: "ignored" };
114
138
  }
115
139
  if (origin && parsed.origin === origin) {
140
+ const served = decodePath(parsed.pathname);
141
+ if (!underBase(deployBase, served)) {
142
+ return { kind: "outside-base", path: normalizePath(served) };
143
+ }
116
144
  return {
117
145
  hash: parsed.hash.slice(1),
118
146
  kind: "self-origin",
119
- path: normalizePath(
120
- stripBasePath(deployBase, decodePath(parsed.pathname))
121
- ),
147
+ path: normalizePath(stripBasePath(deployBase, served)),
122
148
  };
123
149
  }
124
150
  return { kind: "external", url: parsed.toString() };
@@ -140,11 +166,16 @@ export const resolveHref = (
140
166
  } catch {
141
167
  return { kind: "ignored" };
142
168
  }
169
+ const served = decodePath(resolved.pathname);
170
+ // Only a root-relative href names the served path outright; a relative one
171
+ // (`./auth`) was resolved against the base-less page URL above, so it
172
+ // inherits the page's base by construction.
173
+ if (target.startsWith("/") && !underBase(deployBase, served)) {
174
+ return { kind: "outside-base", path: normalizePath(served) };
175
+ }
143
176
  return {
144
177
  hash: resolved.hash.slice(1),
145
178
  kind: "internal",
146
- path: normalizePath(
147
- stripBasePath(deployBase, decodePath(resolved.pathname))
148
- ),
179
+ path: normalizePath(stripBasePath(deployBase, served)),
149
180
  };
150
181
  };
@@ -18,9 +18,9 @@ declare module "blume:search-client" {
18
18
  declare module "blume:ask" {
19
19
  /** The generated Ask trigger (see `askComponentTemplate`); empty when Ask is off. */
20
20
  // Mirrors the generated `.blume/src/env.d.ts` declaration: the trigger's type
21
- // is the AskAI island the enabled template re-exports.
21
+ // is the Assistant island the enabled template re-exports.
22
22
  // oxlint-disable-next-line typescript/consistent-type-imports
23
- const Ask: typeof import("./components/islands/AskAI.astro").default;
23
+ const Ask: typeof import("./components/islands/Assistant.astro").default;
24
24
  export default Ask;
25
25
  }
26
26
 
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+
3
+ import type { Diagnostic } from "../core/types.ts";
4
+
5
+ /**
6
+ * The fields of an `astro build` rejection the report reads. Rolldown bundles
7
+ * every plugin failure (an MDX compile error, say) into one error whose
8
+ * `errors` holds each; a failure carries `loc` where the compiler pinned it,
9
+ * or only `id`, the module that failed.
10
+ */
11
+ const buildErrorSchema = z.looseObject({
12
+ errors: z.array(z.instanceof(Error)).optional(),
13
+ id: z.string().optional(),
14
+ loc: z
15
+ .looseObject({
16
+ column: z.number().optional(),
17
+ file: z.string().optional(),
18
+ line: z.number().optional(),
19
+ })
20
+ .optional(),
21
+ });
22
+
23
+ /** One failure as a diagnostic at the file (and position) it names. */
24
+ const failureDiagnostic = (error: Error): Diagnostic => {
25
+ const parsed = buildErrorSchema.safeParse(error);
26
+ const { id, loc } = parsed.success ? parsed.data : {};
27
+ // A named error (`MDXError`, `CompilerError`) says which step failed.
28
+ const name = error.name === "Error" ? "" : `${error.name}: `;
29
+ return {
30
+ code: "BLUME_BUILD_FAILED",
31
+ column: loc?.column,
32
+ file: loc?.file ?? id,
33
+ line: loc?.line,
34
+ message: `${name}${error.message}`,
35
+ severity: "error",
36
+ };
37
+ };
38
+
39
+ /**
40
+ * What `astro build` rejected with, as build diagnostics: one per failure
41
+ * Rolldown collected, else the error itself. A page Blume's own checks pass
42
+ * can still fail Astro's compile (MDX that doesn't parse) or render, and that
43
+ * is a problem in the site, reported at its file, rather than an internal
44
+ * error blaming Blume.
45
+ */
46
+ export const astroBuildDiagnostics = (error: Error): Diagnostic[] => {
47
+ const parsed = buildErrorSchema.safeParse(error);
48
+ const failures = parsed.success ? (parsed.data.errors ?? []) : [];
49
+ return (failures.length > 0 ? failures : [error]).map(failureDiagnostic);
50
+ };