blume 2.1.0 → 2.1.1

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 (397) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/{chunk-5cg2rzgf.js → chunk-073rdvgb.js} +19 -48
  3. package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-073rdvgb.js.map} +4 -5
  4. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  5. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  6. package/dist/cli/chunk-150jk7km.js +52 -0
  7. package/dist/cli/chunk-150jk7km.js.map +10 -0
  8. package/dist/cli/{chunk-rekqz4qg.js → chunk-313vnn40.js} +16 -15
  9. package/dist/cli/chunk-313vnn40.js.map +10 -0
  10. package/dist/cli/{chunk-gp84za0h.js → chunk-3v3ffbrj.js} +2 -2
  11. package/dist/cli/{chunk-9xr4dgee.js → chunk-3x6x3pph.js} +29 -16
  12. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  13. package/dist/cli/{chunk-0gz36akr.js → chunk-42t4jxdx.js} +21 -5
  14. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  15. package/dist/cli/{chunk-ta27feev.js → chunk-54ztw1nj.js} +21 -2
  16. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  17. package/dist/cli/{chunk-jnwj5kjj.js → chunk-5vjtgs6e.js} +20 -7
  18. package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-5vjtgs6e.js.map} +3 -3
  19. package/dist/cli/chunk-66w9j7p6.js +322 -0
  20. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  21. package/dist/cli/{chunk-nd1821sk.js → chunk-7ppf1rva.js} +32 -18
  22. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  23. package/dist/cli/{chunk-p804f3pe.js → chunk-8y2kmjv8.js} +6 -6
  24. package/dist/cli/{chunk-2f7n4qzt.js → chunk-c1y538kd.js} +1 -1
  25. package/dist/cli/{chunk-1n228xqb.js → chunk-c887fj0s.js} +2 -2
  26. package/dist/cli/{chunk-y5h4r2y0.js → chunk-cfyg47zg.js} +15 -5
  27. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  28. package/dist/cli/{chunk-cmtcnh9f.js → chunk-dzwehpy7.js} +2 -2
  29. package/dist/cli/{chunk-h2a48yhd.js → chunk-e6atmn4h.js} +119 -15
  30. package/dist/cli/{chunk-h2a48yhd.js.map → chunk-e6atmn4h.js.map} +10 -10
  31. package/dist/cli/{chunk-5ffywsmb.js → chunk-e8xw9qkh.js} +8 -6
  32. package/dist/cli/{chunk-5ffywsmb.js.map → chunk-e8xw9qkh.js.map} +3 -3
  33. package/dist/cli/{chunk-ntv4qrt7.js → chunk-e9q7a2ak.js} +14 -14
  34. package/dist/cli/{chunk-xkz5xbtv.js → chunk-ehweg4fr.js} +2366 -1114
  35. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  36. package/dist/cli/{chunk-zf748avv.js → chunk-ep6nd0rn.js} +176 -281
  37. package/dist/cli/{chunk-zf748avv.js.map → chunk-ep6nd0rn.js.map} +11 -12
  38. package/dist/cli/{chunk-yxmk4pec.js → chunk-fdrkhpjy.js} +16 -15
  39. package/dist/cli/{chunk-yxmk4pec.js.map → chunk-fdrkhpjy.js.map} +3 -3
  40. package/dist/cli/{chunk-5wj2qwn2.js → chunk-ft9vfys6.js} +4 -4
  41. package/dist/cli/{chunk-4h8z9vv5.js → chunk-gb420vvz.js} +89 -46
  42. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  43. package/dist/cli/{chunk-713mngdq.js → chunk-hebn0tpx.js} +76 -10
  44. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  45. package/dist/cli/{chunk-2n4ps1b3.js → chunk-htrwgabr.js} +40 -22
  46. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  47. package/dist/cli/{chunk-zhvd6adm.js → chunk-hx3xqnk9.js} +13 -13
  48. package/dist/cli/chunk-j134vrp2.js +65 -0
  49. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  50. package/dist/cli/{chunk-y4ykbark.js → chunk-kma9f6sf.js} +2 -2
  51. package/dist/cli/{chunk-py62fewh.js → chunk-mrcccgnh.js} +3 -3
  52. package/dist/cli/{chunk-2f3ndkpw.js → chunk-n0v7m8ry.js} +3 -3
  53. package/dist/cli/{chunk-43zcf4gz.js → chunk-ncj7w596.js} +522 -353
  54. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  55. package/dist/cli/{chunk-9geymqns.js → chunk-nq1d20w2.js} +14 -14
  56. package/dist/cli/{chunk-g2z9x01v.js → chunk-nrns10mj.js} +3 -3
  57. package/dist/cli/{chunk-e7vvk5ya.js → chunk-sds923fa.js} +3 -3
  58. package/dist/cli/{chunk-cy5bm0j0.js → chunk-t2m5j4pr.js} +2 -2
  59. package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-t2m5j4pr.js.map} +1 -1
  60. package/dist/cli/{chunk-yj17vs39.js → chunk-tdqhyxfz.js} +5 -5
  61. package/dist/cli/{chunk-q3wxxq39.js → chunk-tphac74f.js} +5 -5
  62. package/dist/cli/{chunk-6bm0xpgd.js → chunk-w1r4vase.js} +2 -2
  63. package/dist/cli/index.js +24 -19
  64. package/dist/cli/index.js.map +3 -3
  65. package/dist/types/ai/ask-context.d.ts +6 -2
  66. package/dist/types/ai/component-markdown.d.ts +2 -0
  67. package/dist/types/ai/openapi-components.d.ts +4 -1
  68. package/dist/types/astro/integration.d.ts +5 -4
  69. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  70. package/dist/types/astro/pages.d.ts +10 -0
  71. package/dist/types/cli/init/scaffold.d.ts +1 -1
  72. package/dist/types/components/content/api-field.d.ts +4 -1
  73. package/dist/types/components/content/api-page.d.ts +6 -1
  74. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  75. package/dist/types/components/openapi/helpers.d.ts +22 -1
  76. package/dist/types/components/openapi/security.d.ts +10 -0
  77. package/dist/types/components/openapi/snippets.d.ts +60 -0
  78. package/dist/types/core/changelog-index.d.ts +8 -0
  79. package/dist/types/core/config-input.d.ts +30 -3
  80. package/dist/types/core/data.d.ts +4 -0
  81. package/dist/types/core/i18n-ui.d.ts +2 -0
  82. package/dist/types/core/links.d.ts +8 -0
  83. package/dist/types/core/probe.d.ts +2 -0
  84. package/dist/types/core/project-graph.d.ts +6 -0
  85. package/dist/types/core/redirect-patterns.d.ts +11 -3
  86. package/dist/types/core/schema.d.ts +119 -0
  87. package/dist/types/core/sources/cache.d.ts +3 -0
  88. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  89. package/dist/types/core/sources/contentful.d.ts +17 -1
  90. package/dist/types/core/sources/lexical.d.ts +5 -0
  91. package/dist/types/core/sources/normalize.d.ts +20 -1
  92. package/dist/types/core/sources/remote.d.ts +37 -10
  93. package/dist/types/core/sources/sanity.d.ts +1 -1
  94. package/dist/types/core/types.d.ts +17 -0
  95. package/dist/types/deploy/adapter-output.d.ts +2 -1
  96. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  97. package/dist/types/deploy/adapters/node.d.ts +58 -23
  98. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  99. package/dist/types/deploy/adapters/types.d.ts +3 -2
  100. package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
  101. package/dist/types/deploy/platforms/node.d.ts +3 -1
  102. package/dist/types/deploy/platforms/types.d.ts +18 -7
  103. package/dist/types/deploy/platforms/vercel.d.ts +20 -1
  104. package/dist/types/deploy/redirects.d.ts +17 -1
  105. package/dist/types/deploy/sitemap.d.ts +2 -1
  106. package/dist/types/deploy/vercel-negotiation.d.ts +42 -21
  107. package/dist/types/openapi/checks.d.ts +27 -0
  108. package/dist/types/openapi/graphql.d.ts +6 -0
  109. package/dist/types/openapi/model.d.ts +15 -4
  110. package/dist/types/openapi/parse.d.ts +3 -0
  111. package/dist/types/openapi/references.d.ts +16 -0
  112. package/dist/types/ratelimit/index.d.ts +1 -1
  113. package/dist/types/ratelimit/schema.d.ts +8 -0
  114. package/dist/types/ratelimit/upstash.d.ts +39 -14
  115. package/dist/types/reference/graphql.d.ts +76 -1
  116. package/dist/types/reference/index.d.ts +1 -1
  117. package/dist/types/reference/openapi.d.ts +2 -2
  118. package/dist/types/reference/options.d.ts +37 -1
  119. package/dist/types/reference/schema.d.ts +84 -0
  120. package/dist/types/search/adapters/registry.d.ts +3 -0
  121. package/dist/types/search/adapters/typesense.d.ts +14 -4
  122. package/dist/types/search/build.d.ts +3 -1
  123. package/dist/types/search/orama-index.d.ts +9 -9
  124. package/dist/types/search/sync/algolia.d.ts +5 -2
  125. package/dist/types/search/sync/index.d.ts +3 -1
  126. package/dist/types/search/sync/typesense.d.ts +11 -7
  127. package/dist/types/sources/contentful.d.ts +16 -0
  128. package/dist/types/sources/github-releases.d.ts +8 -1
  129. package/dist/types/sources/payload.d.ts +2 -0
  130. package/dist/types/sources/registry.d.ts +54 -0
  131. package/dist/types/sources/shared.d.ts +3 -0
  132. package/dist/types/sources/strapi.d.ts +2 -0
  133. package/docs/01-quickstart.mdx +4 -2
  134. package/docs/02-deployment.mdx +31 -13
  135. package/docs/advanced/changelog.mdx +11 -0
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +30 -7
  138. package/docs/cli/doctor.mdx +5 -1
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/translate.mdx +1 -0
  141. package/docs/cli/validate.mdx +4 -3
  142. package/docs/configuration/analytics.mdx +1 -1
  143. package/docs/configuration/assistant.mdx +6 -2
  144. package/docs/configuration/customization.mdx +4 -2
  145. package/docs/configuration/export.mdx +5 -3
  146. package/docs/configuration/index.mdx +3 -2
  147. package/docs/configuration/rate-limiting.mdx +23 -2
  148. package/docs/configuration/search.mdx +37 -14
  149. package/docs/content/components.mdx +2 -2
  150. package/docs/content/frontmatter.mdx +5 -3
  151. package/docs/content/i18n.mdx +2 -2
  152. package/docs/content/includes.mdx +1 -1
  153. package/docs/content/navigation.mdx +4 -4
  154. package/docs/content/sources/contentful.mdx +7 -4
  155. package/docs/content/sources/custom.mdx +7 -3
  156. package/docs/content/sources/github-releases.mdx +10 -2
  157. package/docs/content/sources/index.mdx +3 -3
  158. package/docs/content/sources/notion.mdx +1 -1
  159. package/docs/content/sources/obsidian.mdx +1 -1
  160. package/docs/content/sources/payload.mdx +3 -2
  161. package/docs/content/sources/remote-mdx.mdx +4 -0
  162. package/docs/content/sources/sanity.mdx +2 -2
  163. package/docs/content/sources/strapi.mdx +3 -2
  164. package/docs/content/syntax.mdx +2 -2
  165. package/docs/discoverability/agent-discovery.mdx +1 -1
  166. package/docs/discoverability/index.mdx +1 -1
  167. package/docs/discoverability/json-api.mdx +2 -0
  168. package/docs/discoverability/llms-txt.mdx +1 -1
  169. package/docs/discoverability/markdown.mdx +3 -1
  170. package/docs/discoverability/mcp.mdx +4 -0
  171. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  172. package/docs/references/api-pages.mdx +1 -1
  173. package/docs/references/asyncapi.mdx +3 -1
  174. package/docs/references/graphql.mdx +13 -1
  175. package/docs/references/openapi.mdx +50 -8
  176. package/package.json +9 -9
  177. package/skills/blume-migrate/SKILL.md +1 -1
  178. package/skills/blume-migrate/references/docusaurus.md +4 -4
  179. package/skills/blume-migrate/references/fumadocs.md +17 -13
  180. package/skills/blume-migrate/references/nextra.md +8 -5
  181. package/skills/blume-migrate/references/starlight.md +9 -7
  182. package/src/ai/ask-context.ts +33 -3
  183. package/src/ai/ask-limits.ts +13 -0
  184. package/src/ai/changelog-markdown.ts +11 -6
  185. package/src/ai/component-markdown.ts +1 -1
  186. package/src/ai/cors.ts +12 -3
  187. package/src/ai/mcp/query.ts +14 -6
  188. package/src/ai/openapi-components.ts +28 -4
  189. package/src/ai/relative-links.ts +4 -38
  190. package/src/astro/generate.ts +78 -48
  191. package/src/astro/integration.ts +16 -13
  192. package/src/astro/markdown-negotiation.ts +3 -0
  193. package/src/astro/module-types.ts +3 -1
  194. package/src/astro/pages.ts +23 -0
  195. package/src/astro/templates.ts +125 -80
  196. package/src/audit/catalog.ts +19 -3
  197. package/src/audit/checks/links.ts +14 -1
  198. package/src/audit/checks/network.ts +110 -6
  199. package/src/audit/checks/redirects.ts +6 -4
  200. package/src/audit/checks/robots.ts +58 -2
  201. package/src/audit/run.ts +3 -0
  202. package/src/audit/types.ts +2 -0
  203. package/src/audit/url.ts +14 -5
  204. package/src/blume-modules.d.ts +5 -1
  205. package/src/cli/args.ts +51 -0
  206. package/src/cli/build-failure.ts +23 -0
  207. package/src/cli/commands/audit.ts +10 -1
  208. package/src/cli/commands/dev.ts +18 -2
  209. package/src/cli/commands/doctor.ts +45 -12
  210. package/src/cli/commands/eval.ts +36 -4
  211. package/src/cli/commands/init.ts +6 -4
  212. package/src/cli/commands/validate.ts +10 -1
  213. package/src/cli/init/scaffold.ts +3 -3
  214. package/src/cli/prepare.ts +6 -3
  215. package/src/cli/required-secrets.ts +1 -1
  216. package/src/components/content/ApiPlayground.astro +6 -2
  217. package/src/components/content/api-field.ts +4 -1
  218. package/src/components/content/api-page.ts +31 -7
  219. package/src/components/content/mermaid-element.ts +15 -1
  220. package/src/components/islands/hooks.ts +27 -1
  221. package/src/components/layout/PageActions.astro +139 -24
  222. package/src/components/layout/RootLayout.astro +10 -2
  223. package/src/components/layout/Search.astro +17 -3
  224. package/src/components/layout/TableOfContents.astro +24 -17
  225. package/src/components/layout/head-scripts.ts +6 -1
  226. package/src/components/layout/search/algolia.ts +10 -1
  227. package/src/components/layout/search/analytics.ts +42 -14
  228. package/src/components/layout/search/endpoint.ts +48 -5
  229. package/src/components/layout/search/pagefind.ts +114 -5
  230. package/src/components/layout/search/types.ts +12 -0
  231. package/src/components/layout/search/typesense.ts +18 -6
  232. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  233. package/src/components/openapi/Authorization.astro +10 -0
  234. package/src/components/openapi/GraphqlOperation.astro +5 -0
  235. package/src/components/openapi/Operation.astro +24 -1
  236. package/src/components/openapi/ParametersTable.astro +8 -4
  237. package/src/components/openapi/Playground.astro +7 -1
  238. package/src/components/openapi/RequestBody.astro +19 -2
  239. package/src/components/openapi/Responses.astro +1 -0
  240. package/src/components/openapi/SchemaTable.astro +15 -3
  241. package/src/components/openapi/async-snippets.ts +61 -5
  242. package/src/components/openapi/async.ts +123 -29
  243. package/src/components/openapi/graphql-helpers.ts +35 -12
  244. package/src/components/openapi/helpers.ts +36 -2
  245. package/src/components/openapi/message-model.ts +54 -11
  246. package/src/components/openapi/message.ts +14 -4
  247. package/src/components/openapi/operation-model.ts +16 -7
  248. package/src/components/openapi/playground-client.ts +24 -5
  249. package/src/components/openapi/playground-schema.ts +1 -1
  250. package/src/components/openapi/schema-tree.ts +44 -14
  251. package/src/components/openapi/security.ts +10 -0
  252. package/src/components/openapi/snippets.ts +5 -3
  253. package/src/components/openapi/validate-json.ts +4 -3
  254. package/src/core/changelog-index.ts +31 -1
  255. package/src/core/component-diagnostics.ts +53 -0
  256. package/src/core/component-overrides.ts +32 -0
  257. package/src/core/config-input.ts +31 -3
  258. package/src/core/data.ts +2 -0
  259. package/src/core/diagnostics.ts +5 -0
  260. package/src/core/i18n-ui.ts +2 -0
  261. package/src/core/includes.ts +1 -1
  262. package/src/core/links.ts +47 -17
  263. package/src/core/probe.ts +26 -6
  264. package/src/core/project-graph.ts +59 -18
  265. package/src/core/redirect-patterns.ts +36 -7
  266. package/src/core/schema.ts +21 -2
  267. package/src/core/sources/cache.ts +6 -0
  268. package/src/core/sources/contentful-rich-text.ts +29 -6
  269. package/src/core/sources/contentful.ts +135 -25
  270. package/src/core/sources/filesystem.ts +26 -4
  271. package/src/core/sources/github-releases.ts +12 -1
  272. package/src/core/sources/lexical.ts +15 -10
  273. package/src/core/sources/mdx-remote.ts +62 -10
  274. package/src/core/sources/normalize.ts +227 -41
  275. package/src/core/sources/notion.ts +7 -1
  276. package/src/core/sources/obsidian.ts +16 -3
  277. package/src/core/sources/payload.ts +38 -15
  278. package/src/core/sources/remote.ts +72 -15
  279. package/src/core/sources/resolve.ts +11 -1
  280. package/src/core/sources/sanity.ts +43 -25
  281. package/src/core/sources/strapi.ts +2 -2
  282. package/src/core/types.ts +18 -0
  283. package/src/core/ui-packs/ar.ts +1 -0
  284. package/src/core/ui-packs/bg.ts +1 -0
  285. package/src/core/ui-packs/bn.ts +1 -0
  286. package/src/core/ui-packs/ca.ts +1 -0
  287. package/src/core/ui-packs/cs.ts +1 -0
  288. package/src/core/ui-packs/da.ts +1 -0
  289. package/src/core/ui-packs/de.ts +1 -0
  290. package/src/core/ui-packs/el.ts +1 -0
  291. package/src/core/ui-packs/es.ts +1 -0
  292. package/src/core/ui-packs/fa.ts +1 -0
  293. package/src/core/ui-packs/fi.ts +1 -0
  294. package/src/core/ui-packs/fr.ts +1 -0
  295. package/src/core/ui-packs/he.ts +1 -0
  296. package/src/core/ui-packs/hi.ts +1 -0
  297. package/src/core/ui-packs/hr.ts +1 -0
  298. package/src/core/ui-packs/hu.ts +1 -0
  299. package/src/core/ui-packs/id.ts +1 -0
  300. package/src/core/ui-packs/it.ts +1 -0
  301. package/src/core/ui-packs/ja.ts +1 -0
  302. package/src/core/ui-packs/ko.ts +1 -0
  303. package/src/core/ui-packs/nl.ts +1 -0
  304. package/src/core/ui-packs/no.ts +1 -0
  305. package/src/core/ui-packs/pl.ts +1 -0
  306. package/src/core/ui-packs/pt-br.ts +1 -0
  307. package/src/core/ui-packs/pt.ts +1 -0
  308. package/src/core/ui-packs/ro.ts +1 -0
  309. package/src/core/ui-packs/ru.ts +1 -0
  310. package/src/core/ui-packs/sk.ts +1 -0
  311. package/src/core/ui-packs/sr.ts +1 -0
  312. package/src/core/ui-packs/sv.ts +1 -0
  313. package/src/core/ui-packs/th.ts +1 -0
  314. package/src/core/ui-packs/tr.ts +1 -0
  315. package/src/core/ui-packs/uk.ts +1 -0
  316. package/src/core/ui-packs/vi.ts +1 -0
  317. package/src/core/ui-packs/zh-tw.ts +1 -0
  318. package/src/core/ui-packs/zh.ts +1 -0
  319. package/src/deploy/adapter-output.ts +7 -2
  320. package/src/deploy/adapters/cloudflare.ts +7 -5
  321. package/src/deploy/adapters/node.ts +47 -9
  322. package/src/deploy/adapters/types.ts +7 -3
  323. package/src/deploy/artifacts.ts +2 -1
  324. package/src/deploy/platforms/cloudflare.ts +13 -9
  325. package/src/deploy/platforms/netlify.ts +2 -1
  326. package/src/deploy/platforms/node.ts +12 -2
  327. package/src/deploy/platforms/types.ts +21 -7
  328. package/src/deploy/platforms/vercel.ts +113 -16
  329. package/src/deploy/redirects.ts +66 -6
  330. package/src/deploy/rss.ts +3 -1
  331. package/src/deploy/sitemap.ts +39 -5
  332. package/src/deploy/vercel-negotiation.ts +290 -102
  333. package/src/eval/agents.ts +18 -1
  334. package/src/eval/prompts.ts +6 -3
  335. package/src/eval/report.ts +34 -13
  336. package/src/eval/run.ts +5 -0
  337. package/src/markdown/relative-links.ts +3 -2
  338. package/src/openapi/checks.ts +273 -0
  339. package/src/openapi/graphql-build.ts +21 -0
  340. package/src/openapi/graphql.ts +11 -0
  341. package/src/openapi/model.ts +15 -3
  342. package/src/openapi/parse.ts +127 -2
  343. package/src/openapi/references.ts +43 -3
  344. package/src/openapi/source.ts +39 -14
  345. package/src/ratelimit/index.ts +1 -1
  346. package/src/ratelimit/runtime.ts +37 -19
  347. package/src/ratelimit/upstash.ts +40 -10
  348. package/src/reference/graphql.ts +15 -1
  349. package/src/reference/index.ts +1 -0
  350. package/src/reference/openapi.ts +2 -2
  351. package/src/reference/options.ts +29 -1
  352. package/src/registry/eject.ts +3 -1
  353. package/src/search/adapters/locale-scope.ts +30 -0
  354. package/src/search/adapters/typesense.ts +14 -4
  355. package/src/search/build.ts +21 -3
  356. package/src/search/orama-index.ts +163 -31
  357. package/src/search/source-pages.ts +29 -0
  358. package/src/search/sync/algolia.ts +99 -9
  359. package/src/search/sync/index.ts +52 -13
  360. package/src/search/sync/typesense.ts +72 -27
  361. package/src/sources/contentful.ts +12 -0
  362. package/src/sources/github-releases.ts +7 -1
  363. package/src/sources/shared.ts +3 -0
  364. package/src/theme/entry.ts +29 -3
  365. package/src/translate/validate.ts +23 -1
  366. package/dist/cli/chunk-0gz36akr.js.map +0 -12
  367. package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
  368. package/dist/cli/chunk-43zcf4gz.js.map +0 -36
  369. package/dist/cli/chunk-4ejxbzee.js +0 -39
  370. package/dist/cli/chunk-4ejxbzee.js.map +0 -10
  371. package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
  372. package/dist/cli/chunk-713mngdq.js.map +0 -14
  373. package/dist/cli/chunk-9xr4dgee.js.map +0 -10
  374. package/dist/cli/chunk-amrb6hgh.js +0 -145
  375. package/dist/cli/chunk-amrb6hgh.js.map +0 -11
  376. package/dist/cli/chunk-nd1821sk.js.map +0 -10
  377. package/dist/cli/chunk-rekqz4qg.js.map +0 -10
  378. package/dist/cli/chunk-ta27feev.js.map +0 -10
  379. package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
  380. package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
  381. /package/dist/cli/{chunk-gp84za0h.js.map → chunk-3v3ffbrj.js.map} +0 -0
  382. /package/dist/cli/{chunk-p804f3pe.js.map → chunk-8y2kmjv8.js.map} +0 -0
  383. /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-c1y538kd.js.map} +0 -0
  384. /package/dist/cli/{chunk-1n228xqb.js.map → chunk-c887fj0s.js.map} +0 -0
  385. /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-dzwehpy7.js.map} +0 -0
  386. /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-e9q7a2ak.js.map} +0 -0
  387. /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-ft9vfys6.js.map} +0 -0
  388. /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-hx3xqnk9.js.map} +0 -0
  389. /package/dist/cli/{chunk-y4ykbark.js.map → chunk-kma9f6sf.js.map} +0 -0
  390. /package/dist/cli/{chunk-py62fewh.js.map → chunk-mrcccgnh.js.map} +0 -0
  391. /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-n0v7m8ry.js.map} +0 -0
  392. /package/dist/cli/{chunk-9geymqns.js.map → chunk-nq1d20w2.js.map} +0 -0
  393. /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-nrns10mj.js.map} +0 -0
  394. /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-sds923fa.js.map} +0 -0
  395. /package/dist/cli/{chunk-yj17vs39.js.map → chunk-tdqhyxfz.js.map} +0 -0
  396. /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-tphac74f.js.map} +0 -0
  397. /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-w1r4vase.js.map} +0 -0
@@ -342,10 +342,11 @@ export interface BlumeIntegrationOptions {
342
342
  */
343
343
  buildArtifactsRoot?: string;
344
344
  /**
345
- * The pattern redirects (`/beta/:slug*`), compiled against base-less
346
- * request paths (see `core/redirect-patterns.ts`). Astro's own `redirects`
347
- * carry only the exact ones, since it can't prerender a pattern's pages, so
348
- * the dev server answers these itself.
345
+ * Every configured redirect, compiled against base-less request paths
346
+ * (see `compileEveryRedirect`), for the dev server to answer itself.
347
+ * Astro's own `redirects` carry only the exact ones, since it can't
348
+ * prerender a pattern's pages, and its dev handler answers one whose
349
+ * destination `[...slug]` serves with a `301` whatever its status.
349
350
  */
350
351
  redirects?: CompiledRedirect[];
351
352
  }
@@ -398,10 +399,12 @@ const negotiateMarkdown =
398
399
  if (homeLinkHeader && isHomeUrl(req.url)) {
399
400
  res.setHeader("Link", homeLinkHeader);
400
401
  }
401
- if (prefersMarkdown(req.headers.accept)) {
402
- const variant = markdownVariantUrl(req.url, contentRoutes);
403
- if (variant) {
404
- res.setHeader("Vary", "Accept");
402
+ const variant = markdownVariantUrl(req.url, contentRoutes);
403
+ if (variant) {
404
+ // Both answers at a negotiated URL depend on `Accept`, the HTML one
405
+ // too, so a cache never hands one client's variant to the other.
406
+ res.setHeader("Vary", "Accept");
407
+ if (prefersMarkdown(req.headers.accept)) {
405
408
  req.url = variant;
406
409
  }
407
410
  }
@@ -410,11 +413,11 @@ const negotiateMarkdown =
410
413
  };
411
414
 
412
415
  /**
413
- * Answer a pattern redirect in dev with its configured status, the way the
414
- * server wrappers do in a build. Request URLs arrive base-less (see
415
- * {@link negotiateMarkdown}), and the patterns were based to match.
416
+ * Answer a configured redirect in dev with its status, the way the server
417
+ * wrappers do in a build. Request URLs arrive base-less (see
418
+ * {@link negotiateMarkdown}), and the redirects were based to match.
416
419
  */
417
- const answerPatternRedirects =
420
+ const answerRedirects =
418
421
  (redirects: readonly CompiledRedirect[]) =>
419
422
  (req: IncomingMessage, res: ServerResponse, next: () => void): void => {
420
423
  const answer = matchCompiledRedirect(redirects, requestPath(req.url ?? ""));
@@ -533,7 +536,7 @@ export const blumeIntegration = (
533
536
  });
534
537
  if (options.redirects) {
535
538
  server.middlewares.stack.unshift({
536
- handle: answerPatternRedirects(options.redirects),
539
+ handle: answerRedirects(options.redirects),
537
540
  route: "",
538
541
  });
539
542
  }
@@ -8,6 +8,9 @@
8
8
  * helpers (`deploy/cloudflare-negotiation.ts` — its deploy bundle is uploaded
9
9
  * unbundled, so it cannot import this module); when editing here, mirror the
10
10
  * change there. Parity is enforced by `test/cloudflare-negotiation.test.ts`.
11
+ * Vercel's routing conditions (`deploy/vercel-negotiation.ts`) spell the same
12
+ * rule as regexes, checked against {@link prefersMarkdown} by
13
+ * `test/vercel-negotiation.test.ts`.
11
14
  */
12
15
 
13
16
  interface AcceptEntry {
@@ -82,7 +82,9 @@ declare module "blume:consent-client" {
82
82
  }
83
83
 
84
84
  declare module "blume:search-client" {
85
- export const createSearch: () =>
85
+ export const createSearch: (
86
+ options?: import("blume/components/layout/search/types.ts").SearchClientOptions
87
+ ) =>
86
88
  | import("blume/components/layout/search/types.ts").SearchFn
87
89
  | Promise<import("blume/components/layout/search/types.ts").SearchFn>;
88
90
  }
@@ -4,6 +4,7 @@ import {
4
4
  } from "../core/changelog-index.ts";
5
5
  import { staticSegments } from "../core/custom-pages.ts";
6
6
  import type { BlumeProject } from "../core/project-graph.ts";
7
+ import { referenceRoutes } from "../openapi/references.ts";
7
8
  import type { BlumePageRoute } from "./integration.ts";
8
9
 
9
10
  export {
@@ -48,6 +49,28 @@ export const hasGeneratedChangelog = (
48
49
  hasChangelogIndex(project.graph.pages, project.config) &&
49
50
  !routeIsTaken(userPages, project.graph.pages, CHANGELOG_INDEX_ROUTE);
50
51
 
52
+ /**
53
+ * Every route a navigation entry (a tab, selector item, featured link, header
54
+ * action, or CTA) may point at: content routes, custom `.astro` pages, the
55
+ * generated changelog, and reference routes (so a tab an author points at one
56
+ * still validates). `generateRuntime` and `blume doctor` both check
57
+ * `validateNavTargets` against it, so they flag the same entries.
58
+ */
59
+ export const navTargetRoutes = (
60
+ project: BlumeProject,
61
+ userPages: { pattern: string }[]
62
+ ): Set<string> => {
63
+ const routes = new Set<string>([
64
+ ...project.graph.routes.keys(),
65
+ ...userPages.map((page) => page.pattern),
66
+ ...referenceRoutes(project.config),
67
+ ]);
68
+ if (hasGeneratedChangelog(project, userPages)) {
69
+ routes.add(CHANGELOG_INDEX_ROUTE);
70
+ }
71
+ return routes;
72
+ };
73
+
51
74
  const humanizeSegment = (segment: string): string =>
52
75
  segment
53
76
  .split(/[-_]/u)
@@ -4,13 +4,17 @@ import { pathToFileURL } from "node:url";
4
4
  import { dirname, isAbsolute, join, relative } from "pathe";
5
5
 
6
6
  import type { AskRetrievalOptions } from "../ai/ask-context.ts";
7
+ import { ASK_MAX_MESSAGES, ASK_MAX_MESSAGES_CHARS } from "../ai/ask-limits.ts";
7
8
  import type { AskBackend } from "../ai/ask.ts";
8
9
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
9
10
  import type { CaptchaAdapter } from "../captcha/schema.ts";
10
11
  import { CONSENT_CLIENT_MODULES } from "../consent/clients.ts";
11
12
  import { normalizeBasePath } from "../core/base-path.ts";
12
13
  import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
13
- import { compileRedirects, isPatternPath } from "../core/redirect-patterns.ts";
14
+ import {
15
+ compileEveryRedirect,
16
+ isPatternPath,
17
+ } from "../core/redirect-patterns.ts";
14
18
  import type { CompiledRedirect } from "../core/redirect-patterns.ts";
15
19
  import type { ResolvedConfig } from "../core/schema.ts";
16
20
  import { resolveDocsCollection } from "../core/sources/collection.ts";
@@ -22,7 +26,10 @@ import { deployPassthrough } from "../deploy/adapters/types.ts";
22
26
  import { SVG_ASSET_HEADERS } from "../deploy/headers.ts";
23
27
  import { deployPlatform } from "../deploy/platforms/index.ts";
24
28
  import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
25
- import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
29
+ import {
30
+ applyBaseToAstroRedirects,
31
+ withMirrorRedirects,
32
+ } from "../deploy/redirects.ts";
26
33
  import { API_RAIL_KEY } from "../markdown/api-rail.ts";
27
34
  import { VIEWS_KEY } from "../markdown/views.ts";
28
35
  import type { OgCache } from "../og/cache.ts";
@@ -34,6 +41,7 @@ import type {
34
41
  ResolvedSearchAdapter,
35
42
  SearchAdapterKind,
36
43
  } from "../search/adapters/registry.ts";
44
+ import type { SourcePage } from "../search/source-pages.ts";
37
45
  import { buildFontEntries, fontLocaleCodes } from "../theme/fonts.ts";
38
46
  import { importSpecifier, wrapperPropsType } from "./component-slots.ts";
39
47
  import type { ExampleSpec } from "./examples.ts";
@@ -122,7 +130,7 @@ const renderAstroAdapter = (
122
130
  const { astro } = platform;
123
131
  const args = {
124
132
  ...astro.options(context),
125
- ...deployPassthrough(deployment.options),
133
+ ...deployPassthrough(deployment.options, astro.configOptions),
126
134
  };
127
135
  const argsLiteral = Object.keys(args).length > 0 ? JSON.stringify(args) : "";
128
136
  const construct = `adapter(${argsLiteral})`;
@@ -133,7 +141,7 @@ const renderAstroAdapter = (
133
141
  ? `withAdapterRoot(${construct}, ${JSON.stringify(adapterRoot(context))})`
134
142
  : construct;
135
143
  return {
136
- configEntries: Object.entries(astro.config)
144
+ configEntries: Object.entries(astro.config(deployment.options))
137
145
  .map(([key, value]) => `\n ${key}: ${JSON.stringify(value)},`)
138
146
  .join(""),
139
147
  importLine: `import adapter from "${astro.package}";\n`,
@@ -283,18 +291,14 @@ const renderUserAliases = (
283
291
  .join("");
284
292
 
285
293
  /**
286
- * Excludes Vite's pre-bundled dep cache from @vitejs/plugin-react. Astro's
287
- * react() replaces the plugin's default `/node_modules/` exclude with just
288
- * `/\.astro$/`, so without this Babel re-parses every optimized dep chunk
289
- * served from `.vite/deps` — a 500KB+ vendor bundle per chunk, re-done on each
290
- * re-optimization. A blanket `/node_modules/` exclude would instead switch the
291
- * React Compiler off for Blume's own components in published installs (they
292
- * resolve under `node_modules/blume/src`, and exclude beats include in the
293
- * plugin's filter), so only the pre-bundle cache is excluded. The hidden runtime
294
- * relocates that cache to `<runtime>/.cache/vite` (see `cacheOptions`), so both
295
- * the default and the relocated path are excluded.
294
+ * Excludes the hidden runtime's pre-bundled dep cache from @vitejs/plugin-react.
295
+ * Astro's react() already excludes `/node_modules/`, which covers Vite's
296
+ * default `node_modules/.vite` cache, but the hidden runtime relocates that
297
+ * cache to `<runtime>/.cache/vite` (see `cacheOptions`). Without this the React
298
+ * Compiler re-transforms every optimized dep chunk served from there — a 500KB+
299
+ * vendor bundle per chunk, re-done on each re-optimization.
296
300
  */
297
- const REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//, /\/\.cache\/vite\//]`;
301
+ const REACT_EXCLUDE = String.raw`exclude: [/\/\.cache\/vite\//]`;
298
302
 
299
303
  /**
300
304
  * The `cacheDir` entries for the generated config's top level and its `vite`
@@ -324,17 +328,14 @@ const runtimeCacheOptions = (
324
328
  };
325
329
 
326
330
  /**
327
- * The `react()` integration call. When `compilerPath` is set (the resolved
328
- * absolute path to `babel-plugin-react-compiler`), react() carries the compiler
329
- * as the first babel plugin — an absolute path, because @vitejs/plugin-react
330
- * resolves babel plugins from the *project* root, not `.blume/`, so a bare
331
- * specifier wouldn't resolve in a user project. `target: "19"` matches Blume's
332
- * React pin. `null`/`undefined` (compiler off or unresolvable) omits the babel
333
- * block. Both variants carry the pre-bundle exclude above.
331
+ * The `react()` integration call. `compiler` turns on @astrojs/react's React
332
+ * Compiler, which runs on `oxc-transform-react` and targets the installed
333
+ * React major; false/absent (compiler off or unresolvable) leaves it out. Both
334
+ * variants carry the pre-bundle exclude above.
334
335
  */
335
- const reactIntegration = (compilerPath: string | null | undefined): string =>
336
- compilerPath
337
- ? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })`
336
+ const reactIntegration = (compiler: boolean | undefined): string =>
337
+ compiler
338
+ ? `react({ compiler: true, ${REACT_EXCLUDE} })`
338
339
  : `react({ ${REACT_EXCLUDE} })`;
339
340
 
340
341
  interface IntegrationBridgeOptions {
@@ -388,11 +389,10 @@ interface OptimizeDepsConfig {
388
389
  * the Vite root is the generated runtime, so user pages, convention islands,
389
390
  * and alias-reachable components all live outside it and are otherwise only
390
391
  * crawled when first requested. The compiler runtime rides the include list
391
- * because it is Babel-injected and no source scan can see it. @vitejs/plugin-react
392
- * would add it itself, but only when the babel plugin is passed by its bare
393
- * name (`getReactCompilerPlugin` is an exact string match) — Blume passes an
394
- * absolute path (see `reactIntegration`), which that check never matches. See
395
- * the optimizeDeps comment in the generated config for the failure this prevents.
392
+ * because the compiler injects its import and no source scan can see it. The
393
+ * compiler plugin in `@vitejs/plugin-react` lists it too; Blume keeps it
394
+ * explicit so the guard doesn't hinge on that plugin's internals. See the
395
+ * optimizeDeps comment in the generated config for the failure this prevents.
396
396
  */
397
397
  /**
398
398
  * The client-side libraries a site needs, decided at generation time. A
@@ -432,7 +432,7 @@ const resolveOptimizeDeps = (options: {
432
432
  context: ProjectContext;
433
433
  features: ClientFeatures;
434
434
  needsReact: boolean;
435
- reactCompilerPath: string | null | undefined;
435
+ reactCompiler: boolean | undefined;
436
436
  searchKind: SearchAdapterKind;
437
437
  }): OptimizeDepsConfig => {
438
438
  const { context, features } = options;
@@ -456,7 +456,7 @@ const resolveOptimizeDeps = (options: {
456
456
  // (__PREFETCH_PREFETCH_ALL__ and friends) that a pre-bundled copy loses,
457
457
  // throwing ReferenceError on every page. Astro manages their optimization
458
458
  // itself, without a mid-session reload.
459
- ...(options.needsReact && options.reactCompilerPath
459
+ ...(options.needsReact && options.reactCompiler
460
460
  ? ["react/compiler-runtime"]
461
461
  : []),
462
462
  ];
@@ -612,11 +612,10 @@ export const astroConfigTemplate = (options: {
612
612
  */
613
613
  generatedModulesDir?: string;
614
614
  /**
615
- * Absolute path to `babel-plugin-react-compiler` when the React Compiler is
616
- * enabled (resolved from Blume's package root by the caller); null/absent
617
- * disables the compiler and emits a bare `react()`.
615
+ * Turn on the React Compiler (the caller checked that `oxc-transform-react`
616
+ * resolves); false/absent emits `react()` without it.
618
617
  */
619
- reactCompilerPath?: string | null;
618
+ reactCompiler?: boolean;
620
619
  /** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
621
620
  aliases?: Record<string, string>;
622
621
  /**
@@ -664,7 +663,7 @@ export const astroConfigTemplate = (options: {
664
663
  context,
665
664
  features,
666
665
  needsReact,
667
- reactCompilerPath: options.reactCompilerPath,
666
+ reactCompiler: options.reactCompiler,
668
667
  searchKind: config.search.provider.kind,
669
668
  });
670
669
 
@@ -700,11 +699,17 @@ export const astroConfigTemplate = (options: {
700
699
  // Base the redirect paths the same way routes are based, so a redirect lands
701
700
  // under `basePath` too. Astro layers its own `base` (deployment.base) onto
702
701
  // `from` when matching, but never onto `to` — see applyBaseToAstroRedirects.
703
- const basedRedirects = applyBaseToAstroRedirects(
704
- config.redirects,
705
- config.basePath,
706
- deployment.options.base ?? "",
707
- new Set(contentRoutes)
702
+ // A moved page's Markdown copies move with it (see withMirrorRedirects).
703
+ const redirectPages = new Set(contentRoutes);
704
+ const basedRedirects = withMirrorRedirects(
705
+ applyBaseToAstroRedirects(
706
+ config.redirects,
707
+ config.basePath,
708
+ deployment.options.base ?? "",
709
+ redirectPages
710
+ ),
711
+ redirectPages,
712
+ { from: "", to: normalizeBasePath(deployment.options.base) }
708
713
  );
709
714
  // Only exact redirects: Astro can't prerender a pattern's redirect pages
710
715
  // (it would need every path the pattern covers), so a pattern reaches the
@@ -837,7 +842,7 @@ export const astroConfigTemplate = (options: {
837
842
  `mdx({ processor: blumeMdxProcessor(${processorOptions}) })`,
838
843
  ];
839
844
  if (needsReact) {
840
- integrations.push(reactIntegration(options.reactCompilerPath));
845
+ integrations.push(reactIntegration(options.reactCompiler));
841
846
  }
842
847
  if (needsVue) {
843
848
  integrations.push("vue()");
@@ -855,7 +860,7 @@ export const astroConfigTemplate = (options: {
855
860
  contentRoutes,
856
861
  ejected,
857
862
  pages,
858
- redirects: compileRedirects(basedRedirects),
863
+ redirects: compileEveryRedirect(basedRedirects),
859
864
  })
860
865
  )})`
861
866
  );
@@ -931,8 +936,8 @@ ${userConfigSetup}export default defineConfig({
931
936
  // Everything hydration can reach must be part of the dev dep optimizer's
932
937
  // FIRST run. The Vite root is the generated runtime, so user pages,
933
938
  // islands, and aliased components live outside it and are only crawled
934
- // when first requested — and \`react/compiler-runtime\` is Babel-injected,
935
- // so no source scan can ever see it. A dependency discovered after
939
+ // when first requested — and \`react/compiler-runtime\` is injected by the
940
+ // React Compiler, so no source scan can ever see it. A dependency discovered after
936
941
  // hydration begins triggers a mid-session re-optimization whose new
937
942
  // generation imports React through new \`?v=\` URLs; the browser then
938
943
  // evaluates a second React copy and every island tears down with
@@ -1270,8 +1275,8 @@ const askCaptchaTemplate = (adapter?: CaptchaAdapter): RateLimitTemplate => {
1270
1275
 
1271
1276
  /**
1272
1277
  * Largest request body the assistant route reads: 64 KB, well above the
1273
- * 24,000-character message budget it validates next, so a real conversation
1274
- * never meets it.
1278
+ * message budget it validates next (`ai/ask-limits.ts`), so a real
1279
+ * conversation never meets it.
1275
1280
  */
1276
1281
  const ASK_BODY_LIMIT_BYTES = 65_536;
1277
1282
 
@@ -1380,7 +1385,7 @@ ${limit.check} const text = await readCappedText(request, ${ASK_BODY_LIMIT_BYTE
1380
1385
  const valid =
1381
1386
  Array.isArray(raw) &&
1382
1387
  raw.length > 0 &&
1383
- raw.length <= 40 &&
1388
+ raw.length <= ${ASK_MAX_MESSAGES} &&
1384
1389
  raw.every(
1385
1390
  (m: unknown) =>
1386
1391
  typeof m === "object" &&
@@ -1388,10 +1393,10 @@ ${limit.check} const text = await readCappedText(request, ${ASK_BODY_LIMIT_BYTE
1388
1393
  ("role" in m && (m.role === "user" || m.role === "assistant")) &&
1389
1394
  ("content" in m && typeof m.content === "string")
1390
1395
  ) &&
1391
- JSON.stringify(raw).length <= 24_000;
1396
+ JSON.stringify(raw).length <= ${ASK_MAX_MESSAGES_CHARS};
1392
1397
  if (!valid) {
1393
1398
  return new Response(
1394
- "Invalid request: send 1-40 user/assistant messages with string content.",
1399
+ "Invalid request: send 1-${ASK_MAX_MESSAGES} user/assistant messages with string content.",
1395
1400
  { status: 400 }
1396
1401
  );
1397
1402
  }
@@ -1649,10 +1654,12 @@ export const searchClientTemplate = (config: ResolvedConfig): string => {
1649
1654
  return hostedSearchClient(provider);
1650
1655
  }
1651
1656
  case "server": {
1652
- return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}
1657
+ // The dialog passes `typing`, which paces its queries (see endpoint.ts).
1658
+ return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}import type { SearchClientOptions } from "blume/components/layout/search/types.ts";
1653
1659
  const api = joinBase(import.meta.env.BASE_URL, "api/search");
1654
1660
 
1655
- export const createSearch = () => create({ api });
1661
+ export const createSearch = (options: SearchClientOptions = {}) =>
1662
+ create({ ...options, api });
1656
1663
  `;
1657
1664
  }
1658
1665
  case "pagefind": {
@@ -1674,14 +1681,17 @@ export const createSearch = () => create({ url });
1674
1681
  /**
1675
1682
  * Generate the Mixedbread search endpoint (`/api/search`). It holds the secret
1676
1683
  * key server-side and proxies semantic queries to the configured store. The
1677
- * adapter's options are inlined as a literal so the route never imports the
1678
- * config. The result mapping is best-effort and may need tuning to how your
1679
- * content was synced (see the Mixedbread sync step / \`mxbai vs sync\`).
1684
+ * adapter's options are inlined as literals so the route never imports the
1685
+ * config. `pages` (see `sourcePages`) maps each source file to its page: a
1686
+ * chunk links to the page whose file `mxbai store sync` uploaded it from,
1687
+ * found by the longest trailing run of the path the CLI recorded.
1680
1688
  */
1681
1689
  export const mixedbreadSearchEndpointTemplate = (
1682
1690
  options: MixedbreadOptions,
1691
+ pages: [string, SourcePage][],
1683
1692
  rateLimit?: RateLimitAdapter | null
1684
1693
  ): string => {
1694
+ const { search_options: tuning, storeId, ...searchOptions } = options;
1685
1695
  const limit = rateLimitTemplate(rateLimit, "search");
1686
1696
  const imports = [
1687
1697
  'import type { APIRoute } from "astro";',
@@ -1696,9 +1706,26 @@ ${imports.join("\n")}
1696
1706
  export const prerender = false;
1697
1707
 
1698
1708
  const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" });
1699
- const OPTIONS = ${JSON.stringify(options)};
1700
- // Every option besides the store reaches the search call verbatim.
1701
- const { storeId: STORE_ID, ...SEARCH_OPTIONS } = OPTIONS;
1709
+ const STORE_ID = ${JSON.stringify(storeId)};
1710
+ // Every other option reaches the search call verbatim.
1711
+ const SEARCH_OPTIONS = ${JSON.stringify(searchOptions)};
1712
+ const SEARCH_TUNING = ${JSON.stringify(tuning ?? {})};
1713
+ // Each page's source file, relative to the project root, and the page it renders.
1714
+ const PAGES = new Map<string, { title: string; url: string }>(${JSON.stringify(pages)});
1715
+
1716
+ // The page a synced file renders. \`mxbai store sync\` records the path
1717
+ // relative to where it ran (the project root, or a monorepo root above it), so
1718
+ // the longest trailing run of that path that names a page's file wins.
1719
+ const pageFor = (path: string) => {
1720
+ const segments = path.replaceAll("\\\\", "/").split("/");
1721
+ for (let start = 0; start < segments.length; start += 1) {
1722
+ const page = PAGES.get(segments.slice(start).join("/"));
1723
+ if (page) {
1724
+ return page;
1725
+ }
1726
+ }
1727
+ return undefined;
1728
+ };
1702
1729
  ${limit.setup}
1703
1730
  export const POST: APIRoute = async (context) => {
1704
1731
  const { request } = context;
@@ -1724,23 +1751,32 @@ ${limit.check} // A search body is one short query: read it under a 16 KB cap,
1724
1751
  });
1725
1752
  }
1726
1753
  // \`top_k\` defaults to 8 unless an option sets it; the query and store
1727
- // always come from the request and \`storeId\`.
1754
+ // always come from the request and \`storeId\`. File metadata is always
1755
+ // returned: it holds the path \`mxbai store sync\` recorded for the file.
1728
1756
  const response = await client.stores.search({
1729
1757
  top_k: 8,
1730
1758
  ...SEARCH_OPTIONS,
1731
1759
  query,
1760
+ search_options: { ...SEARCH_TUNING, return_metadata: true },
1732
1761
  store_identifiers: [STORE_ID],
1733
1762
  });
1734
- const hits = (response.data ?? []).map((chunk) => {
1763
+ const seen = new Set<string>();
1764
+ const hits = (response.data ?? []).flatMap((chunk) => {
1765
+ const { file_path: path } = (chunk.metadata ?? {}) as { file_path?: unknown };
1766
+ const page = typeof path === "string" ? pageFor(path) : undefined;
1767
+ // A file no page renders has nowhere to link, and a page's later chunks
1768
+ // would repeat it: one hit per page, at its best chunk.
1769
+ if (!page || seen.has(page.url)) {
1770
+ return [];
1771
+ }
1772
+ seen.add(page.url);
1735
1773
  const meta = chunk.generated_metadata ?? {};
1736
1774
  // Only a text chunk carries \`text\`; image, audio, and video chunks fall
1737
1775
  // back to the excerpt the store generated for them.
1738
1776
  const text = "text" in chunk ? chunk.text : undefined;
1739
- return {
1740
- excerpt: text ?? meta.excerpt ?? "",
1741
- title: meta.title ?? chunk.filename ?? "",
1742
- url: meta.url ?? "",
1743
- };
1777
+ return [
1778
+ { excerpt: text ?? meta.excerpt ?? "", title: page.title, url: page.url },
1779
+ ];
1744
1780
  });
1745
1781
  return new Response(JSON.stringify(hits), {
1746
1782
  headers: { "Content-Type": "application/json" },
@@ -2417,6 +2453,7 @@ import type { CollectionKey } from "astro:content";
2417
2453
  import RootLayout from "blume/components/layout/RootLayout.astro";
2418
2454
  import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
2419
2455
  import { mountBasePath, stripBasePath } from "blume/core/base-path.ts";
2456
+ import { routeSetFor, servesRoute } from "blume/core/locale-links.ts";
2420
2457
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2421
2458
  ${componentImports}
2422
2459
  import data from "blume:data";
@@ -2620,18 +2657,24 @@ const logicalRoute = i18n
2620
2657
  ? stripLocale(stripBasePath(data.config.basePath, route), locale)
2621
2658
  : route;
2622
2659
  // A page from a one-language source (GitHub Releases) gets no switcher: every
2623
- // other locale would only repeat the same text.
2660
+ // other locale would only repeat the same text. A locale with no real
2661
+ // translation links the page's fallback copy, which exists only while
2662
+ // fallbacks are on; where nothing is served at that URL (\`fallbackLocale:
2663
+ // null\`), the locale is left out rather than linked to a 404.
2624
2664
  const localeSwitch = i18n && !monolingual
2625
- ? i18n.locales.map((l) => {
2665
+ ? i18n.locales.flatMap((l) => {
2626
2666
  const alt = (alternates ?? []).find((x) => x.locale === l.code);
2627
- return {
2628
- code: l.code,
2629
- current: l.code === locale,
2630
- dir: l.dir,
2631
- href: alt ? alt.path : mountLocalized(logicalRoute, l.code),
2632
- label: l.label,
2633
- untranslated: !alt,
2634
- };
2667
+ const href = alt ? alt.path : mountLocalized(logicalRoute, l.code);
2668
+ return alt || servesRoute(routeSetFor(data.routes), href)
2669
+ ? [{
2670
+ code: l.code,
2671
+ current: l.code === locale,
2672
+ dir: l.dir,
2673
+ href,
2674
+ label: l.label,
2675
+ untranslated: !alt,
2676
+ }]
2677
+ : [];
2635
2678
  })
2636
2679
  : [];
2637
2680
 
@@ -2958,13 +3001,15 @@ const canonical = base ? base + basedRoute : null;
2958
3001
  const ogPath = data.config.og.enabled ? withMountedBase("/og/changelog.png") : null;
2959
3002
  const ogImage = ogPath && base ? base + ogPath : ogPath;
2960
3003
 
2961
- // The page chrome (h1, title, description) comes from the translatable
2962
- // \`changelog\` group; optional chaining tolerates a not-yet-regenerated data
2963
- // snapshot from before these keys existed.
3004
+ // The page chrome (h1, title, description, empty state) comes from the
3005
+ // translatable \`changelog\` group, which carries the \`changelog\` config's title and
3006
+ // description when it sets them; optional chaining tolerates a
3007
+ // not-yet-regenerated data snapshot from before these keys existed.
2964
3008
  const changelogTitle = data.ui.changelog?.title ?? "Changelog";
2965
3009
  const changelogDescription =
2966
3010
  data.ui.changelog?.description ??
2967
3011
  "Product updates, new features, and fixes from every release.";
3012
+ const changelogEmpty = data.ui.changelog?.empty ?? "No changelog entries yet.";
2968
3013
  // The layout suffixes "- {site title}" itself, so the page title is just the
2969
3014
  // changelog's own name — prefixing the site title too would double it
2970
3015
  // ("Acme Changelog - Acme").
@@ -3017,7 +3062,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
3017
3062
  <p class="text-lg text-muted-foreground">{changelogDescription}</p>
3018
3063
  {
3019
3064
  items.length === 0 ? (
3020
- <p>No changelog entries yet.</p>
3065
+ <p>{changelogEmpty}</p>
3021
3066
  ) : (
3022
3067
  <div class="not-prose mt-10 divide-y divide-border border-border border-y">
3023
3068
  {groups.map((group) => (
@@ -161,7 +161,7 @@ export const CHECKS = [
161
161
  },
162
162
  {
163
163
  category: "duplicates",
164
- fix: "Merge the pages, or set `seo.canonical` on all but one.",
164
+ fix: "Merge the pages, or point `seo.canonical` on all but one at the page to index.",
165
165
  id: "BLUME_AUDIT_DUPLICATE_CONTENT",
166
166
  severity: "warning",
167
167
  tier: "static",
@@ -359,7 +359,7 @@ export const CHECKS = [
359
359
  },
360
360
  {
361
361
  category: "redirects",
362
- fix: "Remove the redirect, or delete the page it shadows — the page wins and the redirect never fires.",
362
+ fix: "Remove the redirect, or delete the page it shadows — a path can't be both, and which one readers get depends on the host.",
363
363
  id: "BLUME_AUDIT_REDIRECT_SOURCE_IS_PAGE",
364
364
  severity: "error",
365
365
  tier: "static",
@@ -373,6 +373,14 @@ export const CHECKS = [
373
373
  tier: "network",
374
374
  title: "HTTPS to HTTP redirect",
375
375
  },
376
+ {
377
+ category: "redirects",
378
+ fix: "Deploy the redirect file your host reads (see Redirects in the deployment docs), or name the host in `deployment`.",
379
+ id: "BLUME_AUDIT_REDIRECT_NOT_SERVED",
380
+ severity: "error",
381
+ tier: "network",
382
+ title: "Live site doesn't redirect as configured",
383
+ },
376
384
 
377
385
  // Social
378
386
  {
@@ -570,7 +578,7 @@ export const CHECKS = [
570
578
  // Sitemap
571
579
  {
572
580
  category: "sitemap",
573
- fix: "Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed.",
581
+ fix: "Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed. A `public/sitemap.xml` replaces the generated sitemap, so add the page there or delete that file.",
574
582
  id: "BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP",
575
583
  severity: "warning",
576
584
  tier: "static",
@@ -666,6 +674,14 @@ export const CHECKS = [
666
674
  tier: "static",
667
675
  title: "robots.txt disallows a page that is in the sitemap",
668
676
  },
677
+ {
678
+ category: "robots",
679
+ fix: "Narrow or remove the rule in that crawler's `User-agent` group if it should read these pages. Blocking an AI crawler on purpose is fine; skip this check.",
680
+ id: "BLUME_AUDIT_ROBOTS_BLOCKS_CRAWLER",
681
+ severity: "warning",
682
+ tier: "static",
683
+ title: "robots.txt blocks one crawler from pages in the sitemap",
684
+ },
669
685
  {
670
686
  category: "robots",
671
687
  fix: "Set `deployment.site` so robots.txt can reference the sitemap.",
@@ -101,6 +101,12 @@ export const linkChecks: CheckModule = {
101
101
  return;
102
102
  }
103
103
  if (resolved.kind === "outside-base") {
104
+ // A full URL on this host outside the base names another app there
105
+ // (the navigation docs say to link one that way), not a page of this
106
+ // build that lost its base.
107
+ if (resolved.absolute) {
108
+ return;
109
+ }
104
110
  if (link.content) {
105
111
  found.push(
106
112
  finding(
@@ -215,11 +221,18 @@ export const linkChecks: CheckModule = {
215
221
 
216
222
  const homeUrl = normalizeBasePath(context.project.config.basePath) || "/";
217
223
  for (const page of orphanPages(context.pages, context.graph, homeUrl)) {
224
+ // A `sidebar.hidden` page is in no other page's navigation either, so
225
+ // it isn't reachable from the sidebar at all.
226
+ const fromNavigation = [
227
+ ...(context.graph.chromeIn.get(page.url) ?? []),
228
+ ].some((from) => from !== page.url);
218
229
  found.push(
219
230
  finding(
220
231
  "BLUME_AUDIT_ORPHAN_PAGE",
221
232
  pageSite(context, page),
222
- "No other page's body links here — it is reachable only from the sidebar."
233
+ fromNavigation
234
+ ? "No other page's body links here — it is reachable only from the sidebar."
235
+ : "No page links here, from its body or from navigation, so readers and crawlers can't reach it."
223
236
  )
224
237
  );
225
238
  }