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
@@ -14,7 +14,7 @@ import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
14
14
  import { trimChar } from "../core/trim.ts";
15
15
  import type { ProjectContext } from "../core/types.ts";
16
16
  import { deployPassthrough } from "../deploy/adapters/types.ts";
17
- import { SVG_ASSET_POLICY } from "../deploy/headers.ts";
17
+ import { SVG_ASSET_HEADERS } from "../deploy/headers.ts";
18
18
  import { deployPlatform } from "../deploy/platforms/index.ts";
19
19
  import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
20
20
  import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
@@ -196,10 +196,10 @@ export const runtimeDependencies = (options: {
196
196
  }
197
197
  }
198
198
  }
199
- // Ask AI's provider SDK, as its adapter declares it (the gateway needs
199
+ // The assistant's provider SDK, as its adapter declares it (the gateway needs
200
200
  // nothing beyond core `ai`, so it declares none).
201
- if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
202
- deps.push(...config.ai.ask.provider.runtimeDeps);
201
+ if (config.ai.assistant?.enabled && !config.ai.assistant.endpoint) {
202
+ deps.push(...config.ai.assistant.provider.runtimeDeps);
203
203
  }
204
204
  // The deployment adapter's `@astrojs/*` package, for a server build; the
205
205
  // descriptor declares it (and nothing for a static build).
@@ -727,7 +727,7 @@ export const astroConfigTemplate = (options: {
727
727
  hasFonts: fontEntries.length > 0,
728
728
  });
729
729
 
730
- // Framework renderers are only wired in when an island (or Ask AI, for React)
730
+ // Framework renderers are only wired in when an island (or the assistant, for React)
731
731
  // needs them. The core theme is Astro-first and ships no client JS.
732
732
  const reactImport = needsReact ? `import react from "@astrojs/react";\n` : "";
733
733
  const vueImport = needsVue ? `import vue from "@astrojs/vue";\n` : "";
@@ -1064,17 +1064,17 @@ export const collections = { docs${options.staged ? ", staged" : ""} };
1064
1064
  const ASK_FALLBACK_PROMPT =
1065
1065
  "You are a helpful documentation assistant. Answer using the project's documentation.";
1066
1066
 
1067
- /** The `ai.ask` values the generated endpoint has to carry with it. */
1067
+ /** The `ai.assistant` values the generated endpoint has to carry with it. */
1068
1068
  export interface AskEndpointOptions {
1069
- /** `ai.ask.cors` — origins allowed to call the route from another site. */
1069
+ /** `ai.assistant.cors` — origins allowed to call the route from another site. */
1070
1070
  cors?: string[];
1071
- /** `ai.ask.instructions` — extra system-prompt text. */
1071
+ /** `ai.assistant.instructions` — extra system-prompt text. */
1072
1072
  instructions?: string;
1073
- /** `ai.ask.retrieval` — how much documentation each question carries. */
1073
+ /** `ai.assistant.retrieval` — how much documentation each question carries. */
1074
1074
  retrieval?: AskRetrievalOptions;
1075
1075
  }
1076
1076
 
1077
- /** The pieces `askEndpointTemplate` splices in for `ai.ask.cors`. */
1077
+ /** The pieces `askEndpointTemplate` splices in for `ai.assistant.cors`. */
1078
1078
  interface AskCorsTemplate {
1079
1079
  /** The route's closing token: `});` when the POST is wrapped, `};` otherwise. */
1080
1080
  close: string;
@@ -1087,7 +1087,7 @@ interface AskCorsTemplate {
1087
1087
  }
1088
1088
 
1089
1089
  /**
1090
- * `ai.ask.cors`: a browser only lets another origin read the stream when the
1090
+ * `ai.assistant.cors`: a browser only lets another origin read the stream when the
1091
1091
  * response names that origin, and a JSON POST preflights first, so the route
1092
1092
  * answers `OPTIONS` and wraps the `POST` in `withCors`, which stamps a listed
1093
1093
  * origin on every response — errors included, so a cross-origin caller can
@@ -1114,14 +1114,14 @@ export const OPTIONS: APIRoute = ({ request }) =>
1114
1114
  : { close: "};", imports: [], open: "async ({ request }) => {", setup: "" };
1115
1115
 
1116
1116
  /**
1117
- * Largest request body the Ask AI route reads: 64 KB, well above the
1117
+ * Largest request body the assistant route reads: 64 KB, well above the
1118
1118
  * 24,000-character message budget it validates next, so a real conversation
1119
1119
  * never meets it.
1120
1120
  */
1121
1121
  const ASK_BODY_LIMIT_BYTES = 65_536;
1122
1122
 
1123
1123
  /**
1124
- * Generate the Ask AI server endpoint (`.blume/src/pages/api/ask.ts`).
1124
+ * Generate the assistant server endpoint (`.blume/src/pages/api/ask.ts`).
1125
1125
  *
1126
1126
  * The provider-specific pieces — imports, the provider factory call, the model
1127
1127
  * expression, the credential guard, and the adapter's reasoning mapping and
@@ -1130,11 +1130,11 @@ const ASK_BODY_LIMIT_BYTES = 65_536;
1130
1130
  * `blume.config.ts`. `backend.grounded` decides whether answers are grounded
1131
1131
  * in the retrieved docs (every adapter but Inkeep, which retrieves itself).
1132
1132
  *
1133
- * `options.instructions` (the `ai.ask.instructions` config) is appended to the
1133
+ * `options.instructions` (the `ai.assistant.instructions` config) is appended to the
1134
1134
  * built-in prompt on every path: the grounded prompt via `createAskContext`,
1135
- * and the plain fallback here. `options.retrieval` (the `ai.ask.retrieval`
1135
+ * and the plain fallback here. `options.retrieval` (the `ai.assistant.retrieval`
1136
1136
  * config) is forwarded to `createAskContext` on the grounded path, where it
1137
- * sizes retrieval. `options.cors` (the `ai.ask.cors` config) adds a preflight
1137
+ * sizes retrieval. `options.cors` (the `ai.assistant.cors` config) adds a preflight
1138
1138
  * handler and wraps the `POST` so every response names a listed origin. All
1139
1139
  * three travel in one options object so a new call site can't silently drop
1140
1140
  * one of them.
@@ -1232,7 +1232,7 @@ export const askEndpointTemplate = (
1232
1232
  // Provider errors surface mid-stream, after the 200 is committed; this is
1233
1233
  // the only place they can be observed server-side.
1234
1234
  const onError = ` onError({ error }) {
1235
- console.error("Ask AI provider error:", error);
1235
+ console.error("Assistant provider error:", error);
1236
1236
  },`;
1237
1237
  // The `streamText` argument list, built once so the grounded and plain
1238
1238
  // paths can't drift: they differ only in where the instructions come from.
@@ -1281,9 +1281,9 @@ ${handler}
1281
1281
  * Generate `.blume/src/generated/Ask.astro` — the component behind the
1282
1282
  * `blume:ask` alias that the shared header renders in place of a per-page slot.
1283
1283
  *
1284
- * The header can't import the Ask AI island directly: it's a React component, so
1284
+ * The header can't import the assistant island directly: it's a React component, so
1285
1285
  * the import alone would drag the JSX renderer into the module graph of every
1286
- * project — including the ones that never enable Ask AI and therefore have no
1286
+ * project — including the ones that never enable the assistant and therefore have no
1287
1287
  * React integration wired into their generated Astro config (see `needsReact`).
1288
1288
  * Routing the import through a generated component keeps that dependency behind
1289
1289
  * the config switch: enabled projects get the island, disabled ones get a
@@ -1293,25 +1293,25 @@ ${handler}
1293
1293
  * state suggestions are read straight from the data snapshot, which is why no
1294
1294
  * page has to pass them.
1295
1295
  */
1296
- export const askComponentTemplate = (askEnabled: boolean): string =>
1297
- askEnabled
1296
+ export const askComponentTemplate = (assistantEnabled: boolean): string =>
1297
+ assistantEnabled
1298
1298
  ? `---
1299
1299
  // Generated by Blume. Do not edit.
1300
- import AskAI from "blume/components/islands/AskAI.astro";
1300
+ import Assistant from "blume/components/islands/Assistant.astro";
1301
1301
  import data from "blume:data";
1302
1302
 
1303
1303
  const { strings } = Astro.props;
1304
1304
  ---
1305
1305
 
1306
- <AskAI
1307
- endpoint={data.config.ask?.endpoint ?? undefined}
1308
- strings={strings ?? data.ui.ask}
1309
- suggestions={data.config.ask?.suggestions ?? []}
1306
+ <Assistant
1307
+ endpoint={data.config.assistant?.endpoint ?? undefined}
1308
+ strings={strings ?? data.ui.assistant}
1309
+ suggestions={data.config.assistant?.suggestions ?? []}
1310
1310
  />
1311
1311
  `
1312
1312
  : `---
1313
1313
  // Generated by Blume. Do not edit.
1314
- // Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
1314
+ // The assistant is off (\`ai.assistant.enabled\`), so the header's Ask trigger renders nothing.
1315
1315
  // Deliberately imports no React island, keeping the JSX renderer out of projects
1316
1316
  // that don't need it.
1317
1317
  ---
@@ -1377,7 +1377,10 @@ export const createSearch = () => create(${JSON.stringify(provider.options)});
1377
1377
  * site uses the feature and `null` otherwise, so an unused library is absent
1378
1378
  * from the module graph (never bundled, never pre-bundled in dev) rather than
1379
1379
  * merely lazy. Regenerated on every pass: a page that gains a mermaid fence
1380
- * turns the loader on.
1380
+ * turns the loader on. The EPUB module is named — even in a type — only when
1381
+ * `export.epub` is on: an ejected app depends on `epub-gen-memory` only then,
1382
+ * and a type import of a package it can't resolve fails its type check under
1383
+ * a strict linker such as pnpm.
1381
1384
  */
1382
1385
  export const featuresTemplate = (features: ClientFeatures): string =>
1383
1386
  `// Generated by Blume. Do not edit.
@@ -1394,9 +1397,13 @@ export const loadMermaid: (() => Promise<unknown>) | null = ${
1394
1397
  };
1395
1398
 
1396
1399
  /** The EPUB generator's browser bundle; null when export.epub is off. */
1397
- export const loadEpub:
1400
+ ${
1401
+ features.epub
1402
+ ? `export const loadEpub:
1398
1403
  | (() => Promise<typeof import("epub-gen-memory/bundle")>)
1399
- | null = ${features.epub ? '() => import("epub-gen-memory/bundle")' : "null"};
1404
+ | null = () => import("epub-gen-memory/bundle");`
1405
+ : "export const loadEpub: (() => Promise<unknown>) | null = null;"
1406
+ }
1400
1407
  `;
1401
1408
 
1402
1409
  /**
@@ -1561,12 +1568,13 @@ export function GET({ props }: { props: { route: string } }) {
1561
1568
  * materializes under `.blume/public/blume-assets` (see
1562
1569
  * `core/sources/assets.ts`). That directory is NOT Astro's `publicDir`
1563
1570
  * (which points at the user project's `public/`), so without this endpoint
1564
- * those references 404.
1571
+ * those references 404. `null` for an ejected app, which copies those
1572
+ * files into its own `public/blume-assets` instead.
1565
1573
  * Prerendered: every asset becomes a static file in the build output; the dev
1566
1574
  * server renders on demand, so new images appear without a restart.
1567
1575
  */
1568
1576
  export const contentAssetsEndpointTemplate = (
1569
- stagedAssetsDir: string
1577
+ stagedAssetsDir: string | null
1570
1578
  ): string =>
1571
1579
  `// Generated by Blume. Do not edit.
1572
1580
  import { existsSync } from "node:fs";
@@ -1588,18 +1596,29 @@ const TYPES: Record<string, string> = {
1588
1596
  ".ico": "image/x-icon",
1589
1597
  ".jpeg": "image/jpeg",
1590
1598
  ".jpg": "image/jpeg",
1599
+ ".mov": "video/quicktime",
1600
+ ".mp4": "video/mp4",
1601
+ ".ogv": "video/ogg",
1591
1602
  ".png": "image/png",
1592
1603
  ".svg": "image/svg+xml",
1593
1604
  ".tiff": "image/tiff",
1605
+ ".webm": "video/webm",
1594
1606
  ".webp": "image/webp",
1595
1607
  };
1596
1608
 
1609
+ const extensionOf = (path: string): string =>
1610
+ path.slice(path.lastIndexOf(".")).toLowerCase();
1611
+
1597
1612
  const contentType = (path: string): string =>
1598
- TYPES[path.slice(path.lastIndexOf(".")).toLowerCase()] ??
1599
- "application/octet-stream";
1613
+ TYPES[extensionOf(path)] ?? "application/octet-stream";
1614
+
1615
+ // A source downloads only images and videos; anything else in the staged
1616
+ // directory (a page an older build saved under its URL's extension) is never
1617
+ // published from the docs origin.
1618
+ const isMedia = (path: string): boolean => Object.hasOwn(TYPES, extensionOf(path));
1600
1619
 
1601
1620
  const stagedParams = async (): Promise<string[]> => {
1602
- if (!existsSync(STAGED_DIR)) {
1621
+ if (STAGED_DIR === null || !existsSync(STAGED_DIR)) {
1603
1622
  return [];
1604
1623
  }
1605
1624
  const entries = await readdir(STAGED_DIR, {
@@ -1607,7 +1626,7 @@ const stagedParams = async (): Promise<string[]> => {
1607
1626
  withFileTypes: true,
1608
1627
  });
1609
1628
  return entries
1610
- .filter((entry) => entry.isFile())
1629
+ .filter((entry) => entry.isFile() && isMedia(entry.name))
1611
1630
  .map((entry) =>
1612
1631
  relative(STAGED_DIR, join(entry.parentPath, entry.name)).replaceAll(
1613
1632
  "\\\\",
@@ -1628,6 +1647,9 @@ const resolveAsset = (asset: string): string | null => {
1628
1647
  if (asset.startsWith("content/")) {
1629
1648
  return files[asset.slice("content/".length)] ?? null;
1630
1649
  }
1650
+ if (STAGED_DIR === null) {
1651
+ return null;
1652
+ }
1631
1653
  const abs = resolve(STAGED_DIR, asset);
1632
1654
  // Traversal guard: the dev server renders on demand, so the param is
1633
1655
  // attacker-controlled there — never step outside the staged directory.
@@ -1637,7 +1659,7 @@ const resolveAsset = (asset: string): string | null => {
1637
1659
  // asset — and a bare prefix also admits a sibling directory that merely
1638
1660
  // shares the name.
1639
1661
  const rel = relative(STAGED_DIR, abs);
1640
- if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
1662
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel) || !isMedia(rel)) {
1641
1663
  return null;
1642
1664
  }
1643
1665
  return existsSync(abs) ? abs : null;
@@ -1651,11 +1673,12 @@ export const GET: APIRoute = async ({ params }) => {
1651
1673
  const body = await readFile(path);
1652
1674
  const type = contentType(path);
1653
1675
  // An SVG opened directly is a document that can run script; the sandbox
1654
- // keeps an uploaded one inert on the docs origin. Static hosts get the same
1655
- // header from the build's header rules.
1676
+ // keeps an uploaded one inert on the docs origin. This endpoint
1677
+ // prerenders, so these headers reach dev only: a build serves the files
1678
+ // with the same headers from its host's rules (see deploy/headers.ts).
1656
1679
  const headers: Record<string, string> =
1657
1680
  type === "image/svg+xml"
1658
- ? { "Content-Security-Policy": ${JSON.stringify(SVG_ASSET_POLICY)}, "Content-Type": type }
1681
+ ? { ...${JSON.stringify(SVG_ASSET_HEADERS)}, "Content-Type": type }
1659
1682
  : { "Content-Type": type };
1660
1683
  return new Response(new Uint8Array(body), { headers });
1661
1684
  };
@@ -2758,6 +2781,7 @@ export const notFoundPageTemplate = (): string => `---
2758
2781
  import Icon from "blume/components/Icon.astro";
2759
2782
  import PageLayout from "blume/components/layout/PageLayout.astro";
2760
2783
  import { withBase } from "blume/components/islands/base-path.ts";
2784
+ import { withBasePath } from "blume/core/base-path.ts";
2761
2785
  import data from "blume:data";
2762
2786
 
2763
2787
  export const prerender = true;
@@ -2795,8 +2819,13 @@ const suggestions = [
2795
2819
  // Every host serves this one page for any missing URL, so a reader who
2796
2820
  // mistyped \`/ar/…\` would get the default locale's message. The other locales'
2797
2821
  // copy rides along, and the script below swaps it in when the URL's first
2798
- // segment names one of them — the message, its language and direction, the
2799
- // tab title, and the home link (to that locale's root).
2822
+ // segment past the site's base names one of them — the message, its language
2823
+ // and direction, the tab title, and the home link (to that locale's root).
2824
+ // Localized routes sit under both bases (\`{deployment.base}/{basePath}/ar/…\`),
2825
+ // so the script strips the two together, trailing slash included.
2826
+ const { basePath } = data.config;
2827
+ const siteRoot = withBase(withBasePath(basePath, "/"));
2828
+ const localeBase = siteRoot.endsWith("/") ? siteRoot : siteRoot + "/";
2800
2829
  const localized = Object.fromEntries(
2801
2830
  (i18n?.locales ?? [])
2802
2831
  .filter((l) => l.code !== i18n?.defaultLocale && data.uiByLocale[l.code])
@@ -2808,7 +2837,7 @@ const localized = Object.fromEntries(
2808
2837
  description: strings.description,
2809
2838
  dir: l.dir ?? "ltr",
2810
2839
  home: strings.home,
2811
- homeHref: withBase("/" + l.code),
2840
+ homeHref: withBase(withBasePath(basePath, "/" + l.code)),
2812
2841
  suggestions: strings.suggestions,
2813
2842
  title: strings.title,
2814
2843
  },
@@ -2837,7 +2866,7 @@ const localizedJson = JSON.stringify(localized).replaceAll("<", "\\\\u003c");
2837
2866
  >
2838
2867
  <div
2839
2868
  class="mx-auto grid w-full max-w-5xl gap-12 px-6 py-20 sm:py-28 md:grid-cols-[3fr_2fr] md:gap-16 lg:gap-24 lg:py-36"
2840
- data-base={withBase("/")}
2869
+ data-base={localeBase}
2841
2870
  data-blume-not-found
2842
2871
  >
2843
2872
  <div class="flex flex-col items-start">
@@ -3300,11 +3329,20 @@ const Example = entry.Component;
3300
3329
  root.dataset.theme = theme;
3301
3330
  root.classList.toggle("dark", theme === "dark");
3302
3331
  };
3303
- const stored = () =>
3304
- localStorage.getItem("blume-theme") ??
3305
- (matchMedia("(prefers-color-scheme: dark)").matches
3306
- ? "dark"
3307
- : "light");
3332
+ // Blocked storage (a sandboxed or privacy-locked frame) throws on
3333
+ // access; it falls through to the OS setting like an unset preference.
3334
+ const stored = () => {
3335
+ let theme = null;
3336
+ try {
3337
+ theme = localStorage.getItem("blume-theme");
3338
+ } catch {}
3339
+ return (
3340
+ theme ??
3341
+ (matchMedia("(prefers-color-scheme: dark)").matches
3342
+ ? "dark"
3343
+ : "light")
3344
+ );
3345
+ };
3308
3346
  try {
3309
3347
  const host = window.parent.document.documentElement;
3310
3348
  apply(host.dataset.theme ?? stored());
@@ -583,7 +583,7 @@ export const CHECKS = [
583
583
  },
584
584
  {
585
585
  category: "sitemap",
586
- fix: "Sitemaps must be valid XML in the sitemaps.org urlset format.",
586
+ fix: "Sitemaps must be valid XML in the sitemaps.org urlset or sitemap index format.",
587
587
  id: "BLUME_AUDIT_SITEMAP_INVALID",
588
588
  severity: "error",
589
589
  tier: "static",
@@ -684,7 +684,7 @@ export const CHECKS = [
684
684
  },
685
685
  {
686
686
  category: "ai",
687
- fix: "Rebuild so llms.txt matches the site; if the page is deliberately excluded, mark it `seo.noindex` or `sidebar.hidden`.",
687
+ fix: "Rebuild so llms.txt matches the site; if the page is deliberately excluded, set `ai.exclude: true` in its front matter.",
688
688
  id: "BLUME_AUDIT_LLMS_TXT_PAGE_MISSING",
689
689
  severity: "warning",
690
690
  tier: "static",
@@ -11,6 +11,13 @@ import type {
11
11
  } from "../types.ts";
12
12
  import { resolveHref, siteOrigin } from "../url.ts";
13
13
 
14
+ /**
15
+ * Vercel's image optimization route (`/_vercel/image?url=…&w=…`), which
16
+ * `imageService` on the Vercel adapter points `<img>` at. The platform
17
+ * serves it; the build never writes a file there.
18
+ */
19
+ const VERCEL_IMAGE_ENDPOINT = "/_vercel/image";
20
+
14
21
  const formatBytes = (bytes: number): string =>
15
22
  bytes < 1024 * 1024
16
23
  ? `${Math.round(bytes / 1024)} kB`
@@ -44,17 +51,30 @@ const resolveAsset = (
44
51
  }
45
52
 
46
53
  const origin = siteOrigin(context.project.config.deployment.options.site);
47
- const resolved = resolveHref(
48
- page.url,
49
- asset.src,
50
- origin,
51
- normalizeBasePath(context.project.config.deployment.options.base)
54
+ const deployBase = normalizeBasePath(
55
+ context.project.config.deployment.options.base
52
56
  );
57
+ const resolved = resolveHref(page.url, asset.src, origin, deployBase);
53
58
  // A subresource on another origin (a CDN, an analytics script) is outside the
54
59
  // build; we can't check whether it exists without the network.
55
60
  if (resolved.kind === "external" || resolved.kind === "ignored") {
56
61
  return null;
57
62
  }
63
+ // Vercel's image optimization endpoint answers these at request time, at the
64
+ // host root whatever the base.
65
+ if (resolved.path === VERCEL_IMAGE_ENDPOINT) {
66
+ return null;
67
+ }
68
+ if (resolved.kind === "outside-base") {
69
+ found.push(
70
+ finding(
71
+ missingId,
72
+ pageSite(context, page),
73
+ `Page references ${asset.src}, which is missing deployment.base (${deployBase}), so the deployed site does not serve it.`
74
+ )
75
+ );
76
+ return null;
77
+ }
58
78
 
59
79
  const bytes = context.files.get(resolved.path);
60
80
  if (bytes === undefined) {
@@ -38,6 +38,22 @@ const frontmatterDate = (source: string): Date | null => {
38
38
  }
39
39
  };
40
40
 
41
+ /**
42
+ * API reference pages (OpenAPI, AsyncAPI, GraphQL) are generated from a spec
43
+ * and have no front matter to edit: their title comes from the operation's
44
+ * summary (the spec's own title on the overview page) and their description
45
+ * from its description or summary. A fix naming front matter would send the
46
+ * reader to a file that doesn't exist.
47
+ */
48
+ const isGeneratedReference = (page: PageSnapshot): boolean =>
49
+ page.route?.source.name === "openapi";
50
+
51
+ const GENERATED_TITLE_FIX =
52
+ "This page is generated from an API spec, which titles it with the operation's `summary` (the spec's own title on the overview page) — rewrite it there.";
53
+
54
+ const GENERATED_DESCRIPTION_FIX =
55
+ "This page is generated from an API spec, which describes it with the operation's `description` or `summary` (the spec's own description on the overview page) — rewrite it there.";
56
+
41
57
  const titleChecks = (
42
58
  context: AuditContext,
43
59
  page: PageSnapshot
@@ -74,7 +90,8 @@ const titleChecks = (
74
90
  finding(
75
91
  "BLUME_AUDIT_TITLE_LENGTH",
76
92
  pageSite(context, page, ["title"]),
77
- `Title renders ${width} columns wide — too ${direction} (aim for ${titleMin}–${titleMax}).`
93
+ `Title renders ${width} columns wide — too ${direction} (aim for ${titleMin}–${titleMax}).`,
94
+ isGeneratedReference(page) ? GENERATED_TITLE_FIX : undefined
78
95
  )
79
96
  );
80
97
  }
@@ -123,7 +140,8 @@ const descriptionChecks = (
123
140
  finding(
124
141
  "BLUME_AUDIT_DESCRIPTION_LENGTH",
125
142
  pageSite(context, page, ["description"]),
126
- `Meta description renders ${width} columns wide — too ${direction} (aim for ${descriptionMin}–${descriptionMax}).`
143
+ `Meta description renders ${width} columns wide — too ${direction} (aim for ${descriptionMin}–${descriptionMax}).`,
144
+ isGeneratedReference(page) ? GENERATED_DESCRIPTION_FIX : undefined
127
145
  )
128
146
  );
129
147
  }
@@ -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",
@@ -100,6 +100,20 @@ export const linkChecks: CheckModule = {
100
100
  if (resolved.kind === "external" || resolved.kind === "ignored") {
101
101
  return;
102
102
  }
103
+ if (resolved.kind === "outside-base") {
104
+ if (link.content) {
105
+ found.push(
106
+ finding(
107
+ "BLUME_AUDIT_LINK_TO_BROKEN",
108
+ pageSite(context, page),
109
+ `Link to ${link.href} is missing deployment.base (${deployBase}), so the deployed site does not serve it.`
110
+ )
111
+ );
112
+ } else if (!brokenChrome.has(resolved.path)) {
113
+ brokenChrome.set(resolved.path, page);
114
+ }
115
+ return;
116
+ }
103
117
 
104
118
  const anchorTarget = resolved.hash
105
119
  ? 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") ||