blume 2.0.1 → 2.0.3

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 (492) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/cli/{chunk-qwsrynx5.js → chunk-1d7ve1dm.js} +38 -19
  3. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-1d7ve1dm.js.map} +4 -4
  4. package/dist/cli/{chunk-s6jhgk0q.js → chunk-2eytanqx.js} +2 -2
  5. package/dist/cli/{chunk-kdp5q7ke.js → chunk-2hsdwb9n.js} +18 -19
  6. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-2hsdwb9n.js.map} +2 -2
  7. package/dist/cli/{chunk-hdpx1tax.js → chunk-2z928egk.js} +5 -5
  8. package/dist/cli/{chunk-jwyddg7y.js → chunk-35d4wj9f.js} +30 -24
  9. package/dist/cli/chunk-35d4wj9f.js.map +15 -0
  10. package/dist/cli/{chunk-ah61y8py.js → chunk-364znk6q.js} +2 -2
  11. package/dist/cli/{chunk-jts8mvcz.js → chunk-3em5wd2y.js} +25 -6
  12. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-3em5wd2y.js.map} +3 -3
  13. package/dist/cli/{chunk-s1p84fyh.js → chunk-5f86nr5m.js} +63 -22
  14. package/dist/cli/chunk-5f86nr5m.js.map +11 -0
  15. package/dist/cli/{chunk-2hn4b8z7.js → chunk-5m5nmvyq.js} +170 -76
  16. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  17. package/dist/cli/{chunk-epjnccmv.js → chunk-5xvm6tfj.js} +18 -15
  18. package/dist/cli/chunk-5xvm6tfj.js.map +10 -0
  19. package/dist/cli/{chunk-fz5wtpmh.js → chunk-6dsbexzp.js} +18 -15
  20. package/dist/cli/chunk-6dsbexzp.js.map +10 -0
  21. package/dist/cli/{chunk-vtk4a6dg.js → chunk-8ktnccpt.js} +1 -1
  22. package/dist/cli/{chunk-6hsn950k.js → chunk-9t7a85s3.js} +178 -48
  23. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  24. package/dist/cli/{chunk-fxypxtvm.js → chunk-a58773jm.js} +2 -2
  25. package/dist/cli/{chunk-mb2919y2.js → chunk-acanzt5p.js} +17 -6
  26. package/dist/cli/chunk-acanzt5p.js.map +10 -0
  27. package/dist/cli/{chunk-27g6wdth.js → chunk-akbpwfxc.js} +90 -26
  28. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  29. package/dist/cli/{chunk-wm7js3j9.js → chunk-b07cmahc.js} +2 -2
  30. package/dist/cli/{chunk-wgm7m9qk.js → chunk-c8chx29p.js} +179 -51
  31. package/dist/cli/chunk-c8chx29p.js.map +36 -0
  32. package/dist/cli/{chunk-qs4q5p4e.js → chunk-crgn1q09.js} +32 -14
  33. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  34. package/dist/cli/chunk-e04dxsz1.js +39 -0
  35. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  36. package/dist/cli/{chunk-yt5n7ppj.js → chunk-ey84smr6.js} +17 -7
  37. package/dist/cli/chunk-ey84smr6.js.map +10 -0
  38. package/dist/cli/{chunk-f2z5v128.js → chunk-g4hq16wv.js} +14 -15
  39. package/dist/cli/{chunk-f2z5v128.js.map → chunk-g4hq16wv.js.map} +2 -2
  40. package/dist/cli/{chunk-fa25z98p.js → chunk-ga0pf4aj.js} +9 -8
  41. package/dist/cli/chunk-ga0pf4aj.js.map +11 -0
  42. package/dist/cli/{chunk-kpf8rrjc.js → chunk-hm3vjy5s.js} +109 -58
  43. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  44. package/dist/cli/{chunk-fs23ddbb.js → chunk-j85vccga.js} +625 -750
  45. package/dist/cli/chunk-j85vccga.js.map +36 -0
  46. package/dist/cli/{chunk-ch6g3ar0.js → chunk-p3v96n38.js} +6 -6
  47. package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-p3v96n38.js.map} +3 -3
  48. package/dist/cli/{chunk-q5163e60.js → chunk-p73c0m7w.js} +21 -19
  49. package/dist/cli/chunk-p73c0m7w.js.map +11 -0
  50. package/dist/cli/{chunk-dh8cwk36.js → chunk-pehfxfta.js} +24 -9
  51. package/dist/cli/chunk-pehfxfta.js.map +10 -0
  52. package/dist/cli/{chunk-qkqwkpte.js → chunk-pv29h0wf.js} +3688 -1313
  53. package/dist/cli/chunk-pv29h0wf.js.map +190 -0
  54. package/dist/cli/{chunk-6vm74dry.js → chunk-q58y5e6a.js} +10 -10
  55. package/dist/cli/{chunk-6vm74dry.js.map → chunk-q58y5e6a.js.map} +3 -3
  56. package/dist/cli/{chunk-zxcczpyx.js → chunk-r20tn01b.js} +1 -1
  57. package/dist/cli/{chunk-m3vmjgmq.js → chunk-r9rcc4w7.js} +16 -8
  58. package/dist/cli/chunk-r9rcc4w7.js.map +10 -0
  59. package/dist/cli/{chunk-5shv93fd.js → chunk-tkacnehg.js} +2 -2
  60. package/dist/cli/{chunk-zxh4d9vy.js → chunk-tzmab476.js} +4 -4
  61. package/dist/cli/{chunk-yw7dm696.js → chunk-vg9r4eb9.js} +24 -11
  62. package/dist/cli/chunk-vg9r4eb9.js.map +14 -0
  63. package/dist/cli/{chunk-79jhk4py.js → chunk-wrr3j9w9.js} +260 -132
  64. package/dist/cli/chunk-wrr3j9w9.js.map +35 -0
  65. package/dist/cli/{chunk-6crbhc3x.js → chunk-yfyb25rh.js} +54 -27
  66. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  67. package/dist/cli/index.js +162 -35
  68. package/dist/cli/index.js.map +4 -4
  69. package/dist/types/ai/agent-surface.d.ts +32 -0
  70. package/dist/types/ai/api-catalog.d.ts +7 -1
  71. package/dist/types/ai/ask-context.d.ts +7 -0
  72. package/dist/types/ai/component-markdown.d.ts +4 -4
  73. package/dist/types/ai/link-headers.d.ts +8 -1
  74. package/dist/types/ai/openapi-components.d.ts +5 -2
  75. package/dist/types/ai/relative-links.d.ts +11 -4
  76. package/dist/types/ai/skills.d.ts +4 -1
  77. package/dist/types/ai/static-expression.d.ts +28 -0
  78. package/dist/types/ai/tar.d.ts +1 -3
  79. package/dist/types/analytics/databuddy.d.ts +43 -0
  80. package/dist/types/analytics/index.d.ts +4 -0
  81. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  82. package/dist/types/analytics/schema.d.ts +28 -0
  83. package/dist/types/astro/integration.d.ts +3 -2
  84. package/dist/types/cli/env.d.ts +5 -0
  85. package/dist/types/cli/init/scaffold.d.ts +19 -3
  86. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  87. package/dist/types/core/base-path.d.ts +24 -1
  88. package/dist/types/core/config-input.d.ts +13 -11
  89. package/dist/types/core/config.d.ts +2 -2
  90. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  91. package/dist/types/core/graph.d.ts +2 -0
  92. package/dist/types/core/heading-markers.d.ts +5 -7
  93. package/dist/types/core/i18n-ui.d.ts +31 -0
  94. package/dist/types/core/i18n.d.ts +9 -1
  95. package/dist/types/core/last-modified.d.ts +10 -0
  96. package/dist/types/core/links.d.ts +3 -1
  97. package/dist/types/core/load-module.d.ts +10 -0
  98. package/dist/types/core/locale-links.d.ts +12 -2
  99. package/dist/types/core/meta.d.ts +13 -1
  100. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  101. package/dist/types/core/navigation.d.ts +38 -0
  102. package/dist/types/core/ordering-prefix.d.ts +4 -0
  103. package/dist/types/core/safe-links.d.ts +3 -1
  104. package/dist/types/core/schema.d.ts +59 -13
  105. package/dist/types/core/sources/github-releases.d.ts +5 -0
  106. package/dist/types/core/sources/lower.d.ts +38 -13
  107. package/dist/types/core/sources/normalize.d.ts +23 -2
  108. package/dist/types/core/sources/remote.d.ts +11 -1
  109. package/dist/types/core/sources/resolve.d.ts +12 -0
  110. package/dist/types/core/sources/types.d.ts +31 -0
  111. package/dist/types/core/sources/watch.d.ts +12 -5
  112. package/dist/types/core/standard-schema.d.ts +5 -0
  113. package/dist/types/core/types.d.ts +9 -0
  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/artifacts.d.ts +6 -4
  117. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  118. package/dist/types/deploy/headers.d.ts +36 -7
  119. package/dist/types/deploy/node-headers.d.ts +43 -8
  120. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  121. package/dist/types/deploy/platforms/node.d.ts +6 -5
  122. package/dist/types/deploy/platforms/types.d.ts +15 -0
  123. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  124. package/dist/types/deploy/redirects.d.ts +31 -15
  125. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  126. package/dist/types/markdown/directives.d.ts +62 -0
  127. package/dist/types/markdown/features.d.ts +21 -0
  128. package/dist/types/markdown/mdast.d.ts +63 -0
  129. package/dist/types/openapi/asyncapi.d.ts +4 -2
  130. package/dist/types/openapi/model.d.ts +20 -6
  131. package/dist/types/search/sync/algolia.d.ts +3 -1
  132. package/docs/01-quickstart.mdx +3 -2
  133. package/docs/02-deployment.mdx +20 -9
  134. package/docs/08-faq.mdx +10 -3
  135. package/docs/advanced/changelog.mdx +1 -1
  136. package/docs/advanced/custom-pages.mdx +7 -5
  137. package/docs/cli/audit.mdx +19 -3
  138. package/docs/cli/doctor.mdx +2 -2
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/index.mdx +4 -1
  141. package/docs/cli/translate.mdx +4 -4
  142. package/docs/cli/version.mdx +1 -1
  143. package/docs/configuration/analytics.mdx +42 -2
  144. package/docs/configuration/assistant.mdx +1 -1
  145. package/docs/configuration/customization.mdx +6 -3
  146. package/docs/configuration/index.mdx +5 -3
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +2 -2
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -1
  151. package/docs/content/index.mdx +8 -4
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +7 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -16
  156. package/docs/content/syntax.mdx +14 -0
  157. package/docs/content/versioning.mdx +2 -1
  158. package/docs/discoverability/agent-discovery.mdx +22 -8
  159. package/docs/discoverability/index.mdx +4 -3
  160. package/docs/discoverability/llms-txt.mdx +2 -5
  161. package/docs/discoverability/markdown.mdx +6 -4
  162. package/docs/discoverability/mcp.mdx +4 -0
  163. package/docs/discoverability/metadata.mdx +3 -2
  164. package/docs/discoverability/open-graph.mdx +8 -4
  165. package/docs/discoverability/rss.mdx +4 -2
  166. package/docs/references/asyncapi.mdx +2 -2
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +6 -4
  169. package/package.json +1 -1
  170. package/skills/blume-migrate/SKILL.md +6 -6
  171. package/skills/blume-migrate/references/docusaurus.md +6 -6
  172. package/skills/blume-migrate/references/fumadocs.md +1 -1
  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 +2 -2
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +9 -4
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +2 -2
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/ai-catalog.ts +6 -2
  182. package/src/ai/api/handlers.ts +2 -2
  183. package/src/ai/api/spec.ts +17 -4
  184. package/src/ai/api-catalog.ts +13 -3
  185. package/src/ai/ask-context.ts +14 -2
  186. package/src/ai/ask-data.ts +26 -12
  187. package/src/ai/changelog-markdown.ts +2 -2
  188. package/src/ai/component-markdown.ts +43 -37
  189. package/src/ai/link-headers.ts +22 -8
  190. package/src/ai/llms.ts +22 -10
  191. package/src/ai/markdown.ts +23 -11
  192. package/src/ai/mcp/discovery.ts +2 -2
  193. package/src/ai/mcp/query.ts +2 -2
  194. package/src/ai/mcp/server.ts +80 -9
  195. package/src/ai/openapi-components.ts +22 -6
  196. package/src/ai/relative-links.ts +99 -15
  197. package/src/ai/serializers.ts +2 -1
  198. package/src/ai/skills.ts +14 -1
  199. package/src/ai/static-expression.ts +416 -0
  200. package/src/ai/tar.ts +139 -12
  201. package/src/ai/visibility.ts +45 -14
  202. package/src/analytics/databuddy.ts +67 -0
  203. package/src/analytics/head.ts +8 -0
  204. package/src/analytics/index.ts +7 -0
  205. package/src/analytics/one-dollar-stats.ts +86 -0
  206. package/src/analytics/posthog.ts +21 -3
  207. package/src/analytics/schema.ts +4 -0
  208. package/src/astro/generate.ts +72 -28
  209. package/src/astro/include-hmr.ts +31 -12
  210. package/src/astro/include-refresh.ts +19 -8
  211. package/src/astro/integration.ts +108 -14
  212. package/src/astro/runtime-modules.ts +28 -13
  213. package/src/astro/templates.ts +139 -57
  214. package/src/audit/catalog.ts +2 -2
  215. package/src/audit/checks/assets.ts +25 -5
  216. package/src/audit/checks/content.ts +20 -2
  217. package/src/audit/checks/i18n.ts +46 -8
  218. package/src/audit/checks/indexability.ts +39 -19
  219. package/src/audit/checks/links.ts +22 -1
  220. package/src/audit/checks/llms.ts +6 -3
  221. package/src/audit/checks/network.ts +3 -1
  222. package/src/audit/checks/og-image.ts +10 -0
  223. package/src/audit/checks/robots.ts +6 -1
  224. package/src/audit/checks/sitemap.ts +56 -31
  225. package/src/audit/checks/social.ts +21 -2
  226. package/src/audit/crawl.ts +88 -15
  227. package/src/audit/graph.ts +3 -1
  228. package/src/audit/report.ts +54 -17
  229. package/src/audit/run.ts +13 -9
  230. package/src/audit/snapshot.ts +5 -0
  231. package/src/audit/types.ts +18 -0
  232. package/src/audit/url.ts +37 -6
  233. package/src/cli/args.ts +32 -0
  234. package/src/cli/build-failure.ts +50 -0
  235. package/src/cli/commands/audit.ts +11 -1
  236. package/src/cli/commands/build.ts +19 -5
  237. package/src/cli/commands/check.ts +2 -0
  238. package/src/cli/commands/doctor.ts +3 -1
  239. package/src/cli/commands/eval.ts +20 -18
  240. package/src/cli/commands/init.ts +15 -5
  241. package/src/cli/commands/preview.ts +15 -0
  242. package/src/cli/commands/sync.ts +2 -0
  243. package/src/cli/commands/translate.ts +18 -18
  244. package/src/cli/commands/upgrade.ts +11 -0
  245. package/src/cli/commands/validate.ts +3 -1
  246. package/src/cli/dev-lock.ts +157 -37
  247. package/src/cli/eject-scripts.ts +32 -7
  248. package/src/cli/env.ts +12 -1
  249. package/src/cli/init/scaffold.ts +117 -11
  250. package/src/cli/init/starter-spec.ts +235 -0
  251. package/src/cli/report-format.ts +11 -6
  252. package/src/components/colors.ts +19 -0
  253. package/src/components/content/AccordionItem.astro +26 -22
  254. package/src/components/content/Badge.astro +7 -12
  255. package/src/components/content/Card.astro +2 -2
  256. package/src/components/content/Component.astro +11 -5
  257. package/src/components/content/Expandable.astro +5 -1
  258. package/src/components/content/Frame.astro +2 -7
  259. package/src/components/content/GithubInfo.astro +12 -2
  260. package/src/components/content/Prompt.astro +2 -7
  261. package/src/components/content/Tab.astro +0 -1
  262. package/src/components/content/Tabs.astro +58 -5
  263. package/src/components/content/Tile.astro +1 -1
  264. package/src/components/content/Tooltip.astro +69 -7
  265. package/src/components/content/Tree.astro +7 -2
  266. package/src/components/content/TypeTable.astro +10 -5
  267. package/src/components/content/Update.astro +8 -2
  268. package/src/components/content/auto-type-table.ts +4 -1
  269. package/src/components/content/badge-color.ts +20 -0
  270. package/src/components/content/base-href.ts +14 -22
  271. package/src/components/content/inline-markdown.ts +27 -7
  272. package/src/components/copy-feedback.ts +35 -8
  273. package/src/components/islands/assistant.tsx +37 -6
  274. package/src/components/islands/base-path.ts +47 -10
  275. package/src/components/islands/hooks.ts +42 -21
  276. package/src/components/islands/webmcp.ts +16 -10
  277. package/src/components/layout/Banner.astro +23 -4
  278. package/src/components/layout/Breadcrumbs.astro +5 -2
  279. package/src/components/layout/DiscoveryLinks.astro +9 -5
  280. package/src/components/layout/Header.astro +23 -10
  281. package/src/components/layout/LanguageSwitcher.astro +2 -2
  282. package/src/components/layout/Logo.astro +5 -0
  283. package/src/components/layout/NavSelector.astro +8 -3
  284. package/src/components/layout/NavTabMenu.astro +133 -0
  285. package/src/components/layout/NavTree.astro +31 -12
  286. package/src/components/layout/NavTreeCache.astro +5 -2
  287. package/src/components/layout/NavTreeScript.astro +45 -5
  288. package/src/components/layout/PageActions.astro +4 -3
  289. package/src/components/layout/PageLayout.astro +28 -13
  290. package/src/components/layout/Pagination.astro +3 -3
  291. package/src/components/layout/ReferenceLayout.astro +4 -1
  292. package/src/components/layout/RootLayout.astro +25 -12
  293. package/src/components/layout/Search.astro +38 -14
  294. package/src/components/layout/VersionBanner.astro +2 -2
  295. package/src/components/layout/analytics-client.ts +14 -0
  296. package/src/components/layout/toc-active.ts +41 -0
  297. package/src/components/layout/toc-element.ts +8 -14
  298. package/src/components/openapi/ApiTagOperations.astro +2 -2
  299. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  300. package/src/components/openapi/GraphqlChip.astro +2 -2
  301. package/src/components/openapi/GraphqlType.astro +11 -3
  302. package/src/components/openapi/MessageComposer.astro +1 -1
  303. package/src/components/openapi/Operation.astro +23 -5
  304. package/src/components/openapi/PanelTabs.astro +4 -1
  305. package/src/components/openapi/Playground.astro +8 -2
  306. package/src/components/openapi/RequestPanel.astro +13 -6
  307. package/src/components/openapi/SchemaProperty.astro +11 -33
  308. package/src/components/openapi/SchemaTable.astro +34 -52
  309. package/src/components/openapi/async.ts +38 -6
  310. package/src/components/openapi/helpers.ts +100 -12
  311. package/src/components/openapi/message-composer.ts +14 -1
  312. package/src/components/openapi/message-model.ts +12 -2
  313. package/src/components/openapi/message.ts +21 -3
  314. package/src/components/openapi/operation-model.ts +119 -17
  315. package/src/components/openapi/panel.ts +31 -5
  316. package/src/components/openapi/param-style.ts +181 -0
  317. package/src/components/openapi/playground-client.ts +110 -23
  318. package/src/components/openapi/playground-schema.ts +25 -6
  319. package/src/components/openapi/request.ts +192 -26
  320. package/src/components/openapi/schema-tree.ts +209 -0
  321. package/src/components/openapi/snippets.ts +113 -19
  322. package/src/components/openapi/validate-json.ts +1 -1
  323. package/src/components/openapi/ws-client.ts +18 -2
  324. package/src/core/base-path.ts +69 -8
  325. package/src/core/config-input.ts +13 -11
  326. package/src/core/config.ts +18 -4
  327. package/src/core/diagnostics.ts +219 -30
  328. package/src/core/directive-diagnostics.ts +99 -0
  329. package/src/core/frontmatter.ts +21 -18
  330. package/src/core/graph.ts +120 -11
  331. package/src/core/heading-markers.ts +5 -18
  332. package/src/core/i18n-ui.ts +39 -2
  333. package/src/core/i18n.ts +44 -5
  334. package/src/core/last-modified.ts +25 -3
  335. package/src/core/links.ts +7 -4
  336. package/src/core/load-module.ts +20 -0
  337. package/src/core/locale-links.ts +22 -18
  338. package/src/core/manifest.ts +3 -2
  339. package/src/core/meta.ts +89 -12
  340. package/src/core/nav-diagnostics.ts +56 -1
  341. package/src/core/navigation.ts +272 -83
  342. package/src/core/ordering-prefix.ts +27 -0
  343. package/src/core/project-graph.ts +50 -6
  344. package/src/core/safe-href.ts +53 -1
  345. package/src/core/safe-links.ts +11 -2
  346. package/src/core/schema.ts +94 -18
  347. package/src/core/sources/assets.ts +83 -41
  348. package/src/core/sources/contentful-rich-text.ts +28 -20
  349. package/src/core/sources/contentful.ts +25 -12
  350. package/src/core/sources/filesystem.ts +25 -3
  351. package/src/core/sources/github-releases.ts +131 -11
  352. package/src/core/sources/lexical.ts +30 -20
  353. package/src/core/sources/lower.ts +276 -56
  354. package/src/core/sources/mdx-remote.ts +52 -16
  355. package/src/core/sources/normalize.ts +456 -119
  356. package/src/core/sources/notion.ts +77 -34
  357. package/src/core/sources/obsidian.ts +40 -9
  358. package/src/core/sources/payload.ts +1 -0
  359. package/src/core/sources/portable-text.ts +85 -49
  360. package/src/core/sources/remote.ts +18 -2
  361. package/src/core/sources/resolve.ts +52 -33
  362. package/src/core/sources/sanity.ts +1 -0
  363. package/src/core/sources/strapi-blocks.ts +28 -19
  364. package/src/core/sources/strapi.ts +1 -0
  365. package/src/core/sources/types.ts +31 -0
  366. package/src/core/sources/watch.ts +20 -7
  367. package/src/core/standard-schema.ts +10 -6
  368. package/src/core/types.ts +9 -0
  369. package/src/core/ui-packs/ar.ts +13 -0
  370. package/src/core/ui-packs/bg.ts +13 -0
  371. package/src/core/ui-packs/bn.ts +13 -0
  372. package/src/core/ui-packs/ca.ts +13 -0
  373. package/src/core/ui-packs/cs.ts +13 -0
  374. package/src/core/ui-packs/da.ts +13 -0
  375. package/src/core/ui-packs/de.ts +13 -0
  376. package/src/core/ui-packs/el.ts +13 -0
  377. package/src/core/ui-packs/es.ts +13 -0
  378. package/src/core/ui-packs/fa.ts +13 -0
  379. package/src/core/ui-packs/fi.ts +13 -0
  380. package/src/core/ui-packs/fr.ts +13 -0
  381. package/src/core/ui-packs/he.ts +13 -0
  382. package/src/core/ui-packs/hi.ts +13 -0
  383. package/src/core/ui-packs/hr.ts +13 -0
  384. package/src/core/ui-packs/hu.ts +13 -0
  385. package/src/core/ui-packs/id.ts +13 -0
  386. package/src/core/ui-packs/it.ts +13 -0
  387. package/src/core/ui-packs/ja.ts +13 -0
  388. package/src/core/ui-packs/ko.ts +13 -0
  389. package/src/core/ui-packs/nl.ts +13 -0
  390. package/src/core/ui-packs/no.ts +13 -0
  391. package/src/core/ui-packs/pl.ts +13 -0
  392. package/src/core/ui-packs/pt-br.ts +13 -0
  393. package/src/core/ui-packs/pt.ts +13 -0
  394. package/src/core/ui-packs/ro.ts +13 -0
  395. package/src/core/ui-packs/ru.ts +13 -0
  396. package/src/core/ui-packs/sk.ts +13 -0
  397. package/src/core/ui-packs/sr.ts +13 -0
  398. package/src/core/ui-packs/sv.ts +13 -0
  399. package/src/core/ui-packs/th.ts +13 -0
  400. package/src/core/ui-packs/tr.ts +13 -0
  401. package/src/core/ui-packs/uk.ts +13 -0
  402. package/src/core/ui-packs/vi.ts +13 -0
  403. package/src/core/ui-packs/zh-tw.ts +13 -0
  404. package/src/core/ui-packs/zh.ts +13 -0
  405. package/src/core/version-cut.ts +69 -17
  406. package/src/core/versions.ts +4 -1
  407. package/src/deploy/adapters/node.ts +5 -2
  408. package/src/deploy/adapters/registry.ts +2 -1
  409. package/src/deploy/adapters/types.ts +13 -1
  410. package/src/deploy/artifacts.ts +52 -11
  411. package/src/deploy/cloudflare-negotiation.ts +27 -16
  412. package/src/deploy/headers.ts +75 -55
  413. package/src/deploy/node-headers.ts +148 -27
  414. package/src/deploy/platforms/cloudflare.ts +13 -3
  415. package/src/deploy/platforms/netlify.ts +83 -5
  416. package/src/deploy/platforms/node.ts +8 -5
  417. package/src/deploy/platforms/static.ts +2 -0
  418. package/src/deploy/platforms/types.ts +15 -0
  419. package/src/deploy/platforms/vercel.ts +10 -3
  420. package/src/deploy/redirects.ts +68 -21
  421. package/src/deploy/robots.ts +2 -2
  422. package/src/deploy/rss.ts +4 -3
  423. package/src/deploy/sitemap.ts +7 -5
  424. package/src/deploy/vercel-negotiation.ts +35 -3
  425. package/src/eval/agents.ts +10 -2
  426. package/src/eval/run.ts +25 -0
  427. package/src/markdown/base-links.ts +74 -29
  428. package/src/markdown/directives.ts +242 -36
  429. package/src/markdown/features.ts +17 -0
  430. package/src/markdown/index.ts +13 -9
  431. package/src/markdown/mdast.ts +5 -2
  432. package/src/markdown/relative-links.ts +15 -27
  433. package/src/markdown/route-snapshot.ts +37 -0
  434. package/src/og/card.ts +149 -9
  435. package/src/og/derive.ts +156 -5
  436. package/src/og/index.ts +1 -0
  437. package/src/openapi/asyncapi.ts +4 -2
  438. package/src/openapi/graphql-build.ts +28 -2
  439. package/src/openapi/model.ts +99 -27
  440. package/src/openapi/proxy.ts +63 -10
  441. package/src/openapi/render-mdx.ts +51 -3
  442. package/src/registry/eject.ts +297 -43
  443. package/src/search/adapters/version-scope.ts +30 -0
  444. package/src/search/documents.ts +89 -27
  445. package/src/search/popular.ts +2 -1
  446. package/src/search/sync/algolia.ts +36 -2
  447. package/src/seo/jsonld.ts +7 -3
  448. package/src/sources/registry.ts +5 -0
  449. package/src/theme/entry.ts +11 -4
  450. package/src/translate/agents.ts +6 -1
  451. package/src/translate/ledger.ts +26 -3
  452. package/src/translate/meta.ts +76 -24
  453. package/src/translate/run.ts +14 -8
  454. package/src/translate/validate.ts +14 -2
  455. package/src/translate/work-list.ts +91 -19
  456. package/src/upgrade/upgrade.ts +36 -4
  457. package/dist/cli/chunk-27g6wdth.js.map +0 -10
  458. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  459. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  460. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  461. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  462. package/dist/cli/chunk-82bbrxdn.js +0 -51
  463. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  464. package/dist/cli/chunk-abh8yjkn.js +0 -31
  465. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  466. package/dist/cli/chunk-ce574jw2.js +0 -23
  467. package/dist/cli/chunk-ce574jw2.js.map +0 -10
  468. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  469. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  470. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  471. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  472. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  473. package/dist/cli/chunk-jwyddg7y.js.map +0 -15
  474. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  475. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  476. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  477. package/dist/cli/chunk-q5163e60.js.map +0 -11
  478. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  479. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  480. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  481. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  482. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  483. package/dist/cli/chunk-yw7dm696.js.map +0 -14
  484. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-2eytanqx.js.map} +0 -0
  485. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-2z928egk.js.map} +0 -0
  486. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-364znk6q.js.map} +0 -0
  487. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-8ktnccpt.js.map} +0 -0
  488. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-a58773jm.js.map} +0 -0
  489. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-b07cmahc.js.map} +0 -0
  490. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-r20tn01b.js.map} +0 -0
  491. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-tkacnehg.js.map} +0 -0
  492. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-tzmab476.js.map} +0 -0
@@ -3,7 +3,8 @@ import type { Diagnostic } from "../../core/types.ts";
3
3
  import { finding } from "../catalog.ts";
4
4
  import { pageSite } from "../locate.ts";
5
5
  import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
6
- import { normalizePath } from "../url.ts";
6
+ import { decodePath, normalizePath } from "../url.ts";
7
+ import { isArchivedLatestCanonical } from "./indexability.ts";
7
8
 
8
9
  /** The normalized `deployment.base` — hreflang hrefs carry it, page URLs don't. */
9
10
  const deployBaseOf = (context: AuditContext): string =>
@@ -21,15 +22,52 @@ const isValidBcp47 = (tag: string): boolean => {
21
22
  }
22
23
  };
23
24
 
24
- /** The site path an hreflang `href` points at, or null when it isn't parseable. */
25
+ /**
26
+ * The site path an hreflang `href` points at, or null when it isn't parseable.
27
+ * Percent-decoded like every other URL the audit compares, since page URLs
28
+ * come from raw on-disk names (`/ガイド`) and `URL#pathname` is encoded.
29
+ */
25
30
  const alternatePath = (href: string, deployBase: string): string | null => {
26
31
  try {
27
- return normalizePath(stripBasePath(deployBase, new URL(href).pathname));
32
+ return normalizePath(
33
+ stripBasePath(deployBase, decodePath(new URL(href).pathname))
34
+ );
28
35
  } catch {
29
36
  return null;
30
37
  }
31
38
  };
32
39
 
40
+ /**
41
+ * An i18n fallback copy renders another locale's page at this locale's URL.
42
+ * Blume gives it hreflang links to the real translations, which by design
43
+ * never link back to it (it isn't one), and leaves it out of the sitemap —
44
+ * so the hreflang cluster checks don't apply to it.
45
+ */
46
+ const isFallbackCopy = (page: PageSnapshot): boolean =>
47
+ page.route?.fallback === true;
48
+
49
+ /**
50
+ * Whether `target` names a URL other than its own as canonical — unless that
51
+ * is Blume's archived-version default pointing at the latest equivalent,
52
+ * which the canonical checks already accept.
53
+ */
54
+ const canonicalizesElsewhere = (
55
+ context: AuditContext,
56
+ target: PageSnapshot,
57
+ deployBase: string
58
+ ): boolean => {
59
+ if (!target.canonical) {
60
+ return false;
61
+ }
62
+ const canonical = alternatePath(target.canonical, deployBase);
63
+ if (canonical === normalizePath(target.url)) {
64
+ return false;
65
+ }
66
+ return !(
67
+ canonical !== null && isArchivedLatestCanonical(context, target, canonical)
68
+ );
69
+ };
70
+
33
71
  const langChecks = (
34
72
  context: AuditContext,
35
73
  page: PageSnapshot
@@ -115,10 +153,7 @@ const hreflangChecks = (
115
153
  `hreflang="${alternate.lang}" points at ${path}, which the build does not serve.`
116
154
  )
117
155
  );
118
- } else if (
119
- target.canonical &&
120
- alternatePath(target.canonical, deployBase) !== normalizePath(target.url)
121
- ) {
156
+ } else if (canonicalizesElsewhere(context, target, deployBase)) {
122
157
  found.push(
123
158
  finding(
124
159
  "BLUME_AUDIT_HREFLANG_BAD_TARGET",
@@ -190,6 +225,9 @@ const returnTagChecks = (context: AuditContext): Diagnostic[] => {
190
225
  const found: Diagnostic[] = [];
191
226
  const deployBase = deployBaseOf(context);
192
227
  for (const page of context.pages) {
228
+ if (isFallbackCopy(page)) {
229
+ continue;
230
+ }
193
231
  for (const alternate of page.hreflang) {
194
232
  if (alternate.lang === X_DEFAULT) {
195
233
  continue;
@@ -235,7 +273,7 @@ export const i18nChecks: CheckModule = {
235
273
  const found: Diagnostic[] = [];
236
274
  for (const page of context.pages) {
237
275
  found.push(...langChecks(context, page));
238
- if (page.hreflang.length > 0) {
276
+ if (page.hreflang.length > 0 && !isFallbackCopy(page)) {
239
277
  found.push(...hreflangChecks(context, page));
240
278
  }
241
279
  }
@@ -16,6 +16,33 @@ const parseCanonical = (canonical: string): URL | null => {
16
16
  }
17
17
  };
18
18
 
19
+ /**
20
+ * Whether `page` naming `target` as its canonical is Blume's own default for
21
+ * an archived-version page (`versions.archived[].canonical: "latest"`)
22
+ * pointing at its latest equivalent — the live page is the authoritative one
23
+ * by design, not a defect. `target` is a base-less, normalized site path.
24
+ */
25
+ export const isArchivedLatestCanonical = (
26
+ context: AuditContext,
27
+ page: PageSnapshot,
28
+ target: string
29
+ ): boolean => {
30
+ const version = page.route?.version;
31
+ const archived = version
32
+ ? context.project.config.versions?.archived.find(
33
+ (entry) => entry.id === version
34
+ )
35
+ : undefined;
36
+ const latestAlternate = page.route?.versionAlternates.find(
37
+ (alternate) => alternate.version === ""
38
+ );
39
+ return (
40
+ archived?.canonical === "latest" &&
41
+ latestAlternate !== undefined &&
42
+ normalizePath(latestAlternate.path) === target
43
+ );
44
+ };
45
+
19
46
  const canonicalChecks = (
20
47
  context: AuditContext,
21
48
  page: PageSnapshot
@@ -33,8 +60,10 @@ const canonicalChecks = (
33
60
  if (!page.canonical) {
34
61
  // Without `deployment.site` Blume has no absolute URL to canonicalize to, so
35
62
  // report the root cause once (in the sitemap/robots checks) rather than
36
- // flagging every page for a config field they can't fix individually.
37
- return site
63
+ // flagging every page for a config field they can't fix individually. A
64
+ // noindex page omits its canonical on purpose — the layouts drop it, since
65
+ // Google treats the pairing as contradictory (see CANONICAL_ON_NOINDEX).
66
+ return site && page.indexable
38
67
  ? [
39
68
  finding(
40
69
  "BLUME_AUDIT_CANONICAL_MISSING",
@@ -101,23 +130,14 @@ const canonicalChecks = (
101
130
  )
102
131
  );
103
132
  } else if (context.byUrl.has(target)) {
104
- // An archived-version page pointing at its latest equivalent is Blume's
105
- // own default (versions.archived[].canonical: "latest"), not a defect —
106
- // the live page is the authoritative one by design.
107
- const version = page.route?.version;
108
- const archived = version
109
- ? context.project.config.versions?.archived.find(
110
- (entry) => entry.id === version
111
- )
112
- : undefined;
113
- const latestAlternate = page.route?.versionAlternates.find(
114
- (alternate) => alternate.version === ""
115
- );
116
- const intentional =
117
- archived?.canonical === "latest" &&
118
- latestAlternate !== undefined &&
119
- normalizePath(latestAlternate.path) === target;
120
- if (!intentional) {
133
+ // An i18n fallback copy canonicalizes to the page it copies on purpose,
134
+ // like an archived page to its latest equivalent.
135
+ if (
136
+ !(
137
+ page.route?.fallback === true ||
138
+ isArchivedLatestCanonical(context, page, target)
139
+ )
140
+ ) {
121
141
  found.push(
122
142
  finding(
123
143
  "BLUME_AUDIT_CANONICAL_NOT_SELF",
@@ -14,6 +14,9 @@ import { isServed, normalizePath, resolveHref, siteOrigin } from "../url.ts";
14
14
  /** Browser-magic fragments that scroll without needing a matching id. */
15
15
  const MAGIC_FRAGMENTS = new Set(["", "top"]);
16
16
 
17
+ /** Where a fragment's directives (text fragments) start. */
18
+ const FRAGMENT_DIRECTIVE = ":~:";
19
+
17
20
  /** Whether a fragment lands on an id of the target page. */
18
21
  const anchorResolves = (target: PageSnapshot, fragment: string): boolean => {
19
22
  if (MAGIC_FRAGMENTS.has(fragment)) {
@@ -65,8 +68,12 @@ export const linkChecks: CheckModule = {
65
68
  page: PageSnapshot,
66
69
  link: SnapshotLink,
67
70
  target: PageSnapshot,
68
- fragment: string
71
+ hash: string
69
72
  ): void => {
73
+ // A text fragment (`#:~:text=…`) is a directive the browser strips
74
+ // before it looks for an id; only the part in front of `:~:` names an
75
+ // element, and an empty one scrolls to the text instead.
76
+ const [fragment = ""] = hash.split(FRAGMENT_DIRECTIVE);
70
77
  if (anchorResolves(target, fragment)) {
71
78
  return;
72
79
  }
@@ -100,6 +107,20 @@ export const linkChecks: CheckModule = {
100
107
  if (resolved.kind === "external" || resolved.kind === "ignored") {
101
108
  return;
102
109
  }
110
+ if (resolved.kind === "outside-base") {
111
+ if (link.content) {
112
+ found.push(
113
+ finding(
114
+ "BLUME_AUDIT_LINK_TO_BROKEN",
115
+ pageSite(context, page),
116
+ `Link to ${link.href} is missing deployment.base (${deployBase}), so the deployed site does not serve it.`
117
+ )
118
+ );
119
+ } else if (!brokenChrome.has(resolved.path)) {
120
+ brokenChrome.set(resolved.path, page);
121
+ }
122
+ return;
123
+ }
103
124
 
104
125
  const anchorTarget = resolved.hash
105
126
  ? context.byUrl.get(resolved.path)
@@ -113,15 +113,18 @@ export const llmsChecks: CheckModule = {
113
113
 
114
114
  // The reverse direction, mirroring INDEXABLE_PAGE_NOT_IN_SITEMAP: a page
115
115
  // that is built, indexable, and in the nav belongs in the index. Pages the
116
- // generator deliberately skips — hidden, drafts, error routes, API
117
- // reference when `agents.llmsTxt.openapi` is off, custom pages with no
118
- // manifest route — are skipped here for the same reasons.
116
+ // generator deliberately skips — hidden, drafts, `ai.exclude`, i18n
117
+ // fallback copies, error routes, API reference when
118
+ // `agents.llmsTxt.openapi` is off, custom pages with no manifest route —
119
+ // are skipped here for the same reasons.
119
120
  for (const page of context.pages) {
120
121
  const { route } = page;
121
122
  if (
122
123
  !route ||
123
124
  route.hidden ||
124
125
  route.draft ||
126
+ route.aiExclude ||
127
+ route.fallback ||
125
128
  !page.indexable ||
126
129
  ERROR_ROUTES.has(page.url) ||
127
130
  (!openapi && route.source.name === "openapi") ||
@@ -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
  };
@@ -58,7 +58,8 @@ export const buildGraph = (
58
58
  * Non-indexable pages are excluded — a `noindex` page is *meant* to be
59
59
  * unreachable — as is the home page, which is nobody's job to link to. Under a
60
60
  * `basePath` the home page's built URL is the base itself, so the caller names
61
- * it via `homeUrl`.
61
+ * it via `homeUrl`. An i18n fallback copy is excluded too: it stands in for a
62
+ * translation nobody has written yet, so the page to link to is the original.
62
63
  */
63
64
  export const orphanPages = (
64
65
  pages: PageSnapshot[],
@@ -68,6 +69,7 @@ export const orphanPages = (
68
69
  pages.filter(
69
70
  (page) =>
70
71
  page.indexable &&
72
+ page.route?.fallback !== true &&
71
73
  page.url !== "/" &&
72
74
  page.url !== homeUrl &&
73
75
  (graph.contentIn.get(page.url)?.size ?? 0) === 0