blume 1.7.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (673) hide show
  1. package/AGENTS.md +19 -0
  2. package/CHANGELOG.md +217 -0
  3. package/README.md +34 -21
  4. package/dist/cli/chunk-11j0384y.js +148 -0
  5. package/dist/cli/chunk-11j0384y.js.map +10 -0
  6. package/dist/cli/{chunk-rqy0s5wh.js → chunk-1w8dp3qb.js} +17 -16
  7. package/dist/cli/{chunk-rqy0s5wh.js.map → chunk-1w8dp3qb.js.map} +3 -3
  8. package/dist/cli/chunk-2q1dwty4.js +75 -0
  9. package/dist/cli/chunk-2q1dwty4.js.map +11 -0
  10. package/dist/cli/chunk-41za066z.js +122 -0
  11. package/dist/cli/chunk-41za066z.js.map +11 -0
  12. package/dist/cli/chunk-5a2z0198.js +133 -0
  13. package/dist/cli/chunk-5a2z0198.js.map +10 -0
  14. package/dist/cli/{chunk-5g0w1e2c.js → chunk-6k8vp3ta.js} +25 -13
  15. package/dist/cli/{chunk-5g0w1e2c.js.map → chunk-6k8vp3ta.js.map} +4 -4
  16. package/dist/cli/{chunk-5qk08vmp.js → chunk-79njf86q.js} +133 -54
  17. package/dist/cli/chunk-79njf86q.js.map +11 -0
  18. package/dist/cli/{chunk-e7f42gdj.js → chunk-7ez8ny0t.js} +2 -2
  19. package/dist/cli/{chunk-nn13znc2.js → chunk-88cpgt6h.js} +1 -1
  20. package/dist/cli/chunk-a9kptbw5.js +361 -0
  21. package/dist/cli/chunk-a9kptbw5.js.map +14 -0
  22. package/dist/cli/chunk-abh8yjkn.js +31 -0
  23. package/dist/cli/chunk-abh8yjkn.js.map +10 -0
  24. package/dist/cli/chunk-b5aj94ah.js +91 -0
  25. package/dist/cli/chunk-b5aj94ah.js.map +10 -0
  26. package/dist/cli/{chunk-b27xqwn9.js → chunk-bctazmbk.js} +9 -5
  27. package/dist/cli/chunk-bctazmbk.js.map +10 -0
  28. package/dist/cli/chunk-beat36xx.js +279 -0
  29. package/dist/cli/chunk-beat36xx.js.map +10 -0
  30. package/dist/cli/chunk-bnbmcwfb.js +145 -0
  31. package/dist/cli/chunk-bnbmcwfb.js.map +11 -0
  32. package/dist/cli/{chunk-0xjyb285.js → chunk-bw22s759.js} +15 -5
  33. package/dist/cli/{chunk-0xjyb285.js.map → chunk-bw22s759.js.map} +4 -4
  34. package/dist/cli/chunk-by2290sx.js +39 -0
  35. package/dist/cli/chunk-by2290sx.js.map +10 -0
  36. package/dist/cli/chunk-d1tadaw7.js +79 -0
  37. package/dist/cli/chunk-d1tadaw7.js.map +10 -0
  38. package/dist/cli/{chunk-ahnw3kxw.js → chunk-d80hr03s.js} +24 -19
  39. package/dist/cli/chunk-d80hr03s.js.map +15 -0
  40. package/dist/cli/chunk-ernrthtr.js +97 -0
  41. package/dist/cli/chunk-ernrthtr.js.map +10 -0
  42. package/dist/cli/chunk-f2972sbt.js +374 -0
  43. package/dist/cli/chunk-f2972sbt.js.map +10 -0
  44. package/dist/cli/{chunk-vacwm2hv.js → chunk-f7t03s3g.js} +2 -2
  45. package/dist/cli/{chunk-2z47ypj8.js → chunk-fa25z98p.js} +16 -3
  46. package/dist/cli/chunk-fa25z98p.js.map +11 -0
  47. package/dist/cli/{chunk-cjtn640a.js → chunk-fh5hj5jt.js} +43 -20
  48. package/dist/cli/chunk-fh5hj5jt.js.map +10 -0
  49. package/dist/cli/{chunk-yg63d42r.js → chunk-j8mw0za6.js} +86 -57
  50. package/dist/cli/chunk-j8mw0za6.js.map +35 -0
  51. package/dist/cli/{chunk-ct47dqpx.js → chunk-jts8mvcz.js} +67 -7
  52. package/dist/cli/{chunk-2mzebbbz.js.map → chunk-jts8mvcz.js.map} +6 -4
  53. package/dist/cli/{chunk-ps4m1xh4.js → chunk-mnqj32sj.js} +505 -542
  54. package/dist/cli/chunk-mnqj32sj.js.map +12 -0
  55. package/dist/cli/{chunk-8cjtbafj.js → chunk-mwt1k8n7.js} +100 -374
  56. package/dist/cli/chunk-mwt1k8n7.js.map +10 -0
  57. package/dist/cli/chunk-nk3ts2xk.js +51 -0
  58. package/dist/cli/chunk-nk3ts2xk.js.map +10 -0
  59. package/dist/cli/{chunk-3r45185y.js → chunk-pat2zzwc.js} +9 -11
  60. package/dist/cli/{chunk-3r45185y.js.map → chunk-pat2zzwc.js.map} +2 -2
  61. package/dist/cli/chunk-pnnvybbk.js +176 -0
  62. package/dist/cli/chunk-pnnvybbk.js.map +11 -0
  63. package/dist/cli/{chunk-4x36ddpw.js → chunk-sqn5t4q0.js} +81 -87
  64. package/dist/cli/chunk-sqn5t4q0.js.map +10 -0
  65. package/dist/cli/{chunk-bvwwhd84.js → chunk-tzne8qfq.js} +15 -15
  66. package/dist/cli/{chunk-bvwwhd84.js.map → chunk-tzne8qfq.js.map} +1 -1
  67. package/dist/cli/chunk-xaz13gwg.js +12449 -0
  68. package/dist/cli/chunk-xaz13gwg.js.map +182 -0
  69. package/dist/cli/chunk-y3e45rc8.js +102 -0
  70. package/dist/cli/chunk-y3e45rc8.js.map +10 -0
  71. package/dist/cli/chunk-z01ze5c1.js +261 -0
  72. package/dist/cli/chunk-z01ze5c1.js.map +10 -0
  73. package/dist/cli/{chunk-k79xp7av.js → chunk-z1f5arsg.js} +219 -689
  74. package/dist/cli/chunk-z1f5arsg.js.map +36 -0
  75. package/dist/cli/{chunk-1jefwnfs.js → chunk-zg2gtj10.js} +1076 -2592
  76. package/dist/cli/chunk-zg2gtj10.js.map +35 -0
  77. package/dist/cli/{chunk-dwgcp5sm.js → chunk-zxccj738.js} +1 -1
  78. package/dist/cli/index.js +214 -57
  79. package/dist/cli/index.js.map +8 -7
  80. package/dist/types/ai/agent-readability.d.ts +52 -0
  81. package/dist/types/ai/ai-catalog.d.ts +42 -0
  82. package/dist/types/ai/api/paths.d.ts +17 -0
  83. package/dist/types/ai/api-catalog.d.ts +18 -0
  84. package/dist/types/ai/ask.d.ts +368 -0
  85. package/dist/types/ai/changelog-markdown.d.ts +2 -0
  86. package/dist/types/ai/component-markdown.d.ts +2 -2
  87. package/dist/types/ai/index.d.ts +24 -0
  88. package/dist/types/ai/link-headers.d.ts +24 -0
  89. package/dist/types/ai/llms.d.ts +25 -0
  90. package/dist/types/ai/markdown.d.ts +45 -0
  91. package/dist/types/ai/mcp/discovery.d.ts +68 -0
  92. package/dist/types/ai/mcp/tools.d.ts +16 -0
  93. package/dist/types/ai/openapi-components.d.ts +43 -0
  94. package/dist/types/ai/relative-links.d.ts +26 -0
  95. package/dist/types/ai/serializers.d.ts +15 -0
  96. package/dist/types/ai/skills.d.ts +42 -0
  97. package/dist/types/ai/tar.d.ts +25 -0
  98. package/dist/types/ai/visibility.d.ts +17 -0
  99. package/dist/types/ai/web-bot-auth.d.ts +16 -0
  100. package/dist/types/analytics/adobe.d.ts +35 -0
  101. package/dist/types/analytics/amplitude.d.ts +50 -0
  102. package/dist/types/analytics/clarity.d.ts +31 -0
  103. package/dist/types/analytics/clearbit.d.ts +29 -0
  104. package/dist/types/analytics/cloudflare.d.ts +41 -0
  105. package/dist/types/analytics/fathom.d.ts +41 -0
  106. package/dist/types/analytics/google-analytics.d.ts +44 -0
  107. package/dist/types/analytics/google-tag-manager.d.ts +40 -0
  108. package/dist/types/analytics/head.d.ts +30 -0
  109. package/dist/types/analytics/heap.d.ts +40 -0
  110. package/dist/types/analytics/hightouch.d.ts +44 -0
  111. package/dist/types/analytics/hotjar.d.ts +33 -0
  112. package/dist/types/analytics/index.d.ts +61 -0
  113. package/dist/types/analytics/inline.d.ts +17 -0
  114. package/dist/types/analytics/logrocket.d.ts +44 -0
  115. package/dist/types/analytics/mixpanel.d.ts +67 -0
  116. package/dist/types/analytics/pirsch.d.ts +42 -0
  117. package/dist/types/analytics/plausible.d.ts +56 -0
  118. package/dist/types/analytics/posthog.d.ts +45 -0
  119. package/dist/types/analytics/schema.d.ts +320 -0
  120. package/dist/types/analytics/script.d.ts +46 -0
  121. package/dist/types/analytics/segment.d.ts +50 -0
  122. package/dist/types/analytics/vercel.d.ts +48 -0
  123. package/dist/types/astro/integration.d.ts +76 -0
  124. package/dist/types/astro/markdown-negotiation.d.ts +23 -0
  125. package/dist/types/astro/module-types.d.ts +14 -0
  126. package/dist/types/astro/pages.d.ts +44 -0
  127. package/dist/types/cli/env.d.ts +12 -0
  128. package/dist/types/cli/init/scaffold.d.ts +154 -0
  129. package/dist/types/components/layout/nav-utils.d.ts +11 -0
  130. package/dist/types/core/adapter.d.ts +47 -0
  131. package/dist/types/core/api-name.d.ts +7 -0
  132. package/dist/types/core/changelog-index.d.ts +13 -0
  133. package/dist/types/core/config-input.d.ts +186 -515
  134. package/dist/types/core/config.d.ts +60 -34
  135. package/dist/types/core/content-assets.d.ts +76 -0
  136. package/dist/types/core/custom-pages.d.ts +33 -0
  137. package/dist/types/core/data.d.ts +27 -12
  138. package/dist/types/core/define-components.d.ts +12 -9
  139. package/dist/types/core/deployment-env.d.ts +6 -11
  140. package/dist/types/core/frontmatter.d.ts +10 -0
  141. package/dist/types/core/graph.d.ts +18 -0
  142. package/dist/types/core/heading-markers.d.ts +54 -0
  143. package/dist/types/core/i18n-ui.d.ts +6 -2
  144. package/dist/types/core/i18n.d.ts +87 -0
  145. package/dist/types/core/includes.d.ts +138 -0
  146. package/dist/types/core/last-modified.d.ts +47 -0
  147. package/dist/types/core/links.d.ts +95 -0
  148. package/dist/types/core/locale-links.d.ts +60 -0
  149. package/dist/types/core/manifest.d.ts +17 -0
  150. package/dist/types/core/meta.d.ts +38 -0
  151. package/dist/types/core/nav-diagnostics.d.ts +26 -0
  152. package/dist/types/core/navigation.d.ts +6 -0
  153. package/dist/types/core/node-require.d.ts +19 -0
  154. package/dist/types/core/package-json.d.ts +13 -0
  155. package/dist/types/core/probe.d.ts +42 -0
  156. package/dist/types/core/project-graph.d.ts +51 -0
  157. package/dist/types/core/project.d.ts +2 -0
  158. package/dist/types/core/safe-href.d.ts +2 -0
  159. package/dist/types/core/safe-links.d.ts +26 -0
  160. package/dist/types/core/schema.d.ts +2282 -452
  161. package/dist/types/core/site-url.d.ts +16 -0
  162. package/dist/types/core/sources/assets.d.ts +36 -0
  163. package/dist/types/core/sources/cache.d.ts +37 -0
  164. package/dist/types/core/sources/collection.d.ts +28 -0
  165. package/dist/types/core/sources/contentful-rich-text.d.ts +31 -0
  166. package/dist/types/core/sources/contentful.d.ts +40 -0
  167. package/dist/types/core/sources/filesystem.d.ts +24 -0
  168. package/dist/types/core/sources/github-releases.d.ts +31 -0
  169. package/dist/types/core/sources/json.d.ts +30 -0
  170. package/dist/types/core/sources/lexical.d.ts +19 -0
  171. package/dist/types/core/sources/lower.d.ts +75 -0
  172. package/dist/types/core/sources/mdx-remote.d.ts +28 -0
  173. package/dist/types/core/sources/normalize.d.ts +147 -0
  174. package/dist/types/core/sources/notion.d.ts +131 -0
  175. package/dist/types/core/sources/obsidian.d.ts +46 -0
  176. package/dist/types/core/sources/payload.d.ts +39 -0
  177. package/dist/types/core/sources/portable-text.d.ts +42 -0
  178. package/dist/types/core/sources/read.d.ts +23 -0
  179. package/dist/types/core/sources/remote.d.ts +74 -0
  180. package/dist/types/core/sources/resolve.d.ts +19 -0
  181. package/dist/types/core/sources/sanity.d.ts +43 -0
  182. package/dist/types/core/sources/strapi-blocks.d.ts +12 -0
  183. package/dist/types/core/sources/strapi.d.ts +33 -0
  184. package/dist/types/core/sources/types.d.ts +7 -0
  185. package/dist/types/core/sources/watch.d.ts +45 -0
  186. package/dist/types/core/text-width.d.ts +11 -0
  187. package/dist/types/core/types.d.ts +15 -1
  188. package/dist/types/core/unrecognized-keys.d.ts +7 -0
  189. package/dist/types/core/versions.d.ts +72 -0
  190. package/dist/types/core/yaml.d.ts +9 -0
  191. package/dist/types/deploy/adapter-output.d.ts +45 -0
  192. package/dist/types/deploy/adapters/cloudflare.d.ts +40 -0
  193. package/dist/types/deploy/adapters/index.d.ts +29 -0
  194. package/dist/types/deploy/adapters/netlify.d.ts +37 -0
  195. package/dist/types/deploy/adapters/node.d.ts +37 -0
  196. package/dist/types/deploy/adapters/registry.d.ts +133 -0
  197. package/dist/types/deploy/adapters/types.d.ts +71 -0
  198. package/dist/types/deploy/adapters/vercel.d.ts +38 -0
  199. package/dist/types/deploy/artifacts.d.ts +65 -0
  200. package/dist/types/deploy/cloudflare-negotiation.d.ts +196 -0
  201. package/dist/types/deploy/function-bundle.d.ts +80 -0
  202. package/dist/types/deploy/headers.d.ts +50 -0
  203. package/dist/types/deploy/node-headers.d.ts +42 -0
  204. package/dist/types/deploy/platforms/cloudflare.d.ts +40 -0
  205. package/dist/types/deploy/platforms/index.d.ts +18 -0
  206. package/dist/types/deploy/platforms/netlify.d.ts +13 -0
  207. package/dist/types/deploy/platforms/node.d.ts +11 -0
  208. package/dist/types/deploy/platforms/paths.d.ts +27 -0
  209. package/dist/types/deploy/platforms/static.d.ts +10 -0
  210. package/dist/types/deploy/platforms/types.d.ts +104 -0
  211. package/dist/types/deploy/platforms/vercel.d.ts +32 -0
  212. package/dist/types/deploy/redirects.d.ts +53 -0
  213. package/dist/types/deploy/robots.d.ts +8 -0
  214. package/dist/types/deploy/rss.d.ts +31 -0
  215. package/dist/types/deploy/sitemap.d.ts +21 -0
  216. package/dist/types/deploy/vercel-negotiation.d.ts +109 -0
  217. package/dist/types/markdown/code-title.d.ts +32 -0
  218. package/dist/types/markdown/fence-meta.d.ts +23 -0
  219. package/dist/types/markdown/themes.d.ts +3 -3
  220. package/dist/types/openapi/asyncapi.d.ts +129 -0
  221. package/dist/types/openapi/graphql-build.d.ts +8 -0
  222. package/dist/types/openapi/graphql.d.ts +122 -0
  223. package/dist/types/openapi/model.d.ts +158 -0
  224. package/dist/types/openapi/parse.d.ts +57 -0
  225. package/dist/types/openapi/references.d.ts +37 -25
  226. package/dist/types/openapi/render-mdx.d.ts +33 -0
  227. package/dist/types/openapi/sentence.d.ts +7 -0
  228. package/dist/types/openapi/signature.d.ts +10 -0
  229. package/dist/types/openapi/source.d.ts +22 -0
  230. package/dist/types/openapi/spec-dependency-error.d.ts +10 -0
  231. package/dist/types/reference/asyncapi.d.ts +166 -0
  232. package/dist/types/reference/graphql.d.ts +181 -0
  233. package/dist/types/reference/index.d.ts +32 -0
  234. package/dist/types/reference/openapi.d.ts +165 -0
  235. package/dist/types/reference/options.d.ts +157 -0
  236. package/dist/types/reference/scalar.d.ts +136 -0
  237. package/dist/types/reference/schema.d.ts +630 -0
  238. package/dist/types/search/adapters/algolia.d.ts +32 -0
  239. package/dist/types/search/adapters/flexsearch.d.ts +12 -0
  240. package/dist/types/search/adapters/index.d.ts +31 -0
  241. package/dist/types/search/adapters/mixedbread.d.ts +22 -0
  242. package/dist/types/search/adapters/orama-cloud.d.ts +33 -0
  243. package/dist/types/search/adapters/orama.d.ts +13 -0
  244. package/dist/types/search/adapters/pagefind.d.ts +12 -0
  245. package/dist/types/search/adapters/registry.d.ts +228 -0
  246. package/dist/types/search/adapters/types.d.ts +34 -0
  247. package/dist/types/search/adapters/typesense.d.ts +42 -0
  248. package/dist/types/search/build.d.ts +23 -0
  249. package/dist/types/search/documents.d.ts +90 -0
  250. package/dist/types/search/facets.d.ts +3 -0
  251. package/dist/types/search/sync/algolia.d.ts +14 -0
  252. package/dist/types/search/sync/index.d.ts +14 -0
  253. package/dist/types/search/sync/orama-cloud.d.ts +10 -0
  254. package/dist/types/search/sync/typesense.d.ts +14 -0
  255. package/dist/types/sources/contentful.d.ts +68 -0
  256. package/dist/types/sources/custom.d.ts +20 -0
  257. package/dist/types/sources/filesystem.d.ts +42 -0
  258. package/dist/types/sources/github-releases.d.ts +46 -0
  259. package/dist/types/sources/index.d.ts +48 -0
  260. package/dist/types/sources/mdx-remote.d.ts +63 -0
  261. package/dist/types/sources/notion.d.ts +65 -0
  262. package/dist/types/sources/obsidian.d.ts +34 -0
  263. package/dist/types/sources/payload.d.ts +68 -0
  264. package/dist/types/sources/registry.d.ts +1414 -0
  265. package/dist/types/sources/sanity.d.ts +69 -0
  266. package/dist/types/sources/shared.d.ts +46 -0
  267. package/dist/types/sources/strapi.d.ts +66 -0
  268. package/dist/types/theme/icon-kind.d.ts +11 -0
  269. package/dist/types/theme/icons.d.ts +20 -0
  270. package/docs/01-quickstart.mdx +18 -18
  271. package/docs/02-deployment.mdx +50 -26
  272. package/docs/03-upgrading.mdx +351 -0
  273. package/docs/04-migrating.mdx +58 -0
  274. package/docs/08-faq.mdx +3 -3
  275. package/docs/advanced/blog.mdx +13 -6
  276. package/docs/advanced/changelog.mdx +23 -35
  277. package/docs/advanced/custom-pages.mdx +20 -8
  278. package/docs/advanced/meta.ts +1 -8
  279. package/docs/advanced/skills.mdx +8 -0
  280. package/docs/cli/audit.mdx +646 -0
  281. package/docs/cli/doctor.mdx +29 -0
  282. package/docs/{reference/eval.mdx → cli/evals.mdx} +9 -8
  283. package/docs/cli/index.mdx +105 -0
  284. package/docs/cli/meta.ts +7 -0
  285. package/docs/{reference → cli}/translate.mdx +1 -1
  286. package/docs/cli/validate.mdx +41 -0
  287. package/docs/cli/version.mdx +40 -0
  288. package/docs/configuration/analytics.mdx +350 -59
  289. package/docs/configuration/ask-ai.mdx +162 -61
  290. package/docs/configuration/customization.mdx +15 -9
  291. package/docs/configuration/index.mdx +50 -31
  292. package/docs/configuration/search.mdx +75 -54
  293. package/docs/configuration/theming.mdx +24 -15
  294. package/docs/content/components.mdx +21 -5
  295. package/docs/{reference → content}/frontmatter.mdx +35 -1
  296. package/docs/content/i18n.mdx +8 -6
  297. package/docs/content/includes.mdx +2 -4
  298. package/docs/content/index.mdx +1 -1
  299. package/docs/content/islands.mdx +10 -5
  300. package/docs/content/meta.mdx +1 -1
  301. package/docs/content/meta.ts +1 -0
  302. package/docs/content/navigation.mdx +9 -7
  303. package/docs/content/sources.mdx +145 -50
  304. package/docs/content/syntax.mdx +16 -12
  305. package/docs/content/versioning.mdx +3 -3
  306. package/docs/discoverability/agent-discovery.mdx +11 -11
  307. package/docs/discoverability/index.mdx +4 -4
  308. package/docs/discoverability/json-api.mdx +4 -4
  309. package/docs/discoverability/llms-txt.mdx +6 -6
  310. package/docs/discoverability/markdown.mdx +4 -4
  311. package/docs/discoverability/mcp.mdx +11 -11
  312. package/docs/discoverability/sitemap-and-robots.mdx +3 -3
  313. package/docs/index.mdx +2 -2
  314. package/docs/references/asyncapi.mdx +59 -0
  315. package/docs/{advanced → references}/graphql.mdx +45 -32
  316. package/docs/{reference → references}/meta.ts +2 -2
  317. package/docs/references/openapi.mdx +171 -0
  318. package/docs/references/scalar.mdx +64 -0
  319. package/package.json +42 -8
  320. package/skills/blume/SKILL.md +21 -8
  321. package/skills/blume-migrate/SKILL.md +22 -21
  322. package/skills/blume-migrate/assets/oxfmt@0.67.0.patch +49 -0
  323. package/skills/blume-migrate/references/docusaurus.md +5 -4
  324. package/skills/blume-migrate/references/fumadocs.md +4 -4
  325. package/skills/blume-migrate/references/mintlify.md +23 -8
  326. package/skills/blume-migrate/references/monorepo.md +6 -6
  327. package/skills/blume-migrate/references/starlight.md +4 -4
  328. package/src/ai/agent-readability.ts +21 -22
  329. package/src/ai/ai-catalog.ts +50 -32
  330. package/src/ai/api/handlers.ts +46 -11
  331. package/src/ai/api-catalog.ts +8 -8
  332. package/src/ai/ask-data.ts +1 -1
  333. package/src/ai/ask.ts +624 -100
  334. package/src/ai/changelog-markdown.ts +91 -0
  335. package/src/ai/component-markdown.ts +328 -12
  336. package/src/ai/index.ts +45 -0
  337. package/src/ai/link-headers.ts +7 -6
  338. package/src/ai/llms.ts +20 -15
  339. package/src/ai/markdown.ts +35 -5
  340. package/src/ai/mcp/data.ts +9 -5
  341. package/src/ai/openapi-components.ts +4 -1
  342. package/src/ai/relative-links.ts +170 -0
  343. package/src/ai/serializers.ts +2 -2
  344. package/src/ai/skills.ts +1 -1
  345. package/src/ai/web-bot-auth.ts +2 -2
  346. package/src/analytics/adobe.ts +46 -0
  347. package/src/analytics/amplitude.ts +79 -0
  348. package/src/analytics/clarity.ts +46 -0
  349. package/src/analytics/clearbit.ts +47 -0
  350. package/src/analytics/cloudflare.ts +67 -0
  351. package/src/analytics/fathom.ts +64 -0
  352. package/src/analytics/google-analytics.ts +84 -0
  353. package/src/analytics/google-tag-manager.ts +61 -0
  354. package/src/analytics/head.ts +130 -0
  355. package/src/analytics/heap.ts +65 -0
  356. package/src/analytics/hightouch.ts +77 -0
  357. package/src/analytics/hotjar.ts +47 -0
  358. package/src/analytics/index.ts +71 -0
  359. package/src/analytics/inline.ts +21 -0
  360. package/src/analytics/logrocket.ts +71 -0
  361. package/src/analytics/mixpanel.ts +92 -0
  362. package/src/analytics/pirsch.ts +66 -0
  363. package/src/analytics/plausible.ts +83 -0
  364. package/src/analytics/posthog.ts +78 -0
  365. package/src/analytics/schema.ts +60 -0
  366. package/src/analytics/script.ts +60 -0
  367. package/src/analytics/segment.ts +85 -0
  368. package/src/analytics/vercel.ts +50 -0
  369. package/src/astro/adapter-root.ts +7 -9
  370. package/src/astro/component-slots.ts +131 -91
  371. package/src/astro/generate.ts +112 -345
  372. package/src/astro/integration.ts +2 -6
  373. package/src/astro/pages.ts +13 -101
  374. package/src/astro/render-deps.ts +379 -0
  375. package/src/astro/runtime-deps.ts +201 -0
  376. package/src/astro/templates.ts +554 -541
  377. package/src/audit/agent.ts +24 -0
  378. package/src/audit/catalog.ts +2 -2
  379. package/src/audit/checks/assets.ts +2 -2
  380. package/src/audit/checks/dns-aid.ts +1 -1
  381. package/src/audit/checks/duplicates.ts +3 -1
  382. package/src/audit/checks/i18n.ts +1 -1
  383. package/src/audit/checks/indexability.ts +7 -7
  384. package/src/audit/checks/links.ts +2 -2
  385. package/src/audit/checks/llms.ts +6 -6
  386. package/src/audit/checks/network.ts +3 -3
  387. package/src/audit/checks/og-image.ts +2 -2
  388. package/src/audit/checks/robots.ts +1 -1
  389. package/src/audit/checks/sitemap.ts +2 -2
  390. package/src/audit/checks/social.ts +1 -1
  391. package/src/audit/run.ts +4 -3
  392. package/src/audit/terms.ts +31 -0
  393. package/src/audit/url.ts +13 -13
  394. package/src/cli/command-meta.ts +10 -0
  395. package/src/cli/commands/audit.ts +39 -20
  396. package/src/cli/commands/build.ts +71 -316
  397. package/src/cli/commands/check.ts +1 -0
  398. package/src/cli/commands/dev.ts +40 -1
  399. package/src/cli/commands/doctor.ts +90 -14
  400. package/src/cli/commands/eject.ts +44 -7
  401. package/src/cli/commands/eval.ts +2 -2
  402. package/src/cli/commands/init.ts +161 -41
  403. package/src/cli/commands/migrate.ts +121 -0
  404. package/src/cli/commands/preview.ts +7 -1
  405. package/src/cli/commands/translate.ts +2 -2
  406. package/src/cli/commands/upgrade.ts +141 -0
  407. package/src/cli/commands/version.ts +57 -44
  408. package/src/cli/eject-scripts.ts +121 -6
  409. package/src/cli/index.ts +11 -1
  410. package/src/cli/init/install.ts +70 -0
  411. package/src/cli/init/questions.ts +7 -0
  412. package/src/cli/init/scaffold.ts +459 -75
  413. package/src/cli/lazy-command.ts +37 -1
  414. package/src/cli/prepare.ts +40 -6
  415. package/src/cli/required-secrets.ts +38 -11
  416. package/src/cli/unknown-flags.ts +266 -0
  417. package/src/cli/yarn-pnp.ts +52 -0
  418. package/src/components/content/AccordionItem.astro +7 -1
  419. package/src/components/content/Card.astro +4 -3
  420. package/src/components/content/ColorItem.astro +22 -4
  421. package/src/components/content/GithubInfo.astro +2 -2
  422. package/src/components/content/Prompt.astro +25 -25
  423. package/src/components/content/Tabs.astro +3 -1
  424. package/src/components/content/Tile.astro +2 -3
  425. package/src/components/content/Tooltip.astro +57 -9
  426. package/src/components/content/content-strings.ts +34 -0
  427. package/src/components/content/diff.ts +1 -1
  428. package/src/components/content/mermaid-element.ts +13 -2
  429. package/src/components/content/prompt-markdown.ts +292 -0
  430. package/src/components/content/tooltip-id.ts +41 -0
  431. package/src/components/islands/hooks.ts +25 -2
  432. package/src/components/layout/Analytics.astro +21 -79
  433. package/src/components/layout/Banner.astro +3 -1
  434. package/src/components/layout/DiscoveryLinks.astro +69 -0
  435. package/src/components/layout/Header.astro +83 -20
  436. package/src/components/layout/LanguageSwitcher.astro +4 -1
  437. package/src/components/layout/Logo.astro +23 -2
  438. package/src/components/layout/NavSelector.astro +13 -2
  439. package/src/components/layout/NavTree.astro +12 -3
  440. package/src/components/layout/NavTreeScript.astro +17 -3
  441. package/src/components/layout/PageActions.astro +3 -3
  442. package/src/components/layout/PageFeedback.astro +11 -1
  443. package/src/components/layout/PageLayout.astro +52 -5
  444. package/src/components/layout/ReferenceLayout.astro +21 -12
  445. package/src/components/layout/RootLayout.astro +69 -66
  446. package/src/components/layout/Search.astro +30 -6
  447. package/src/components/layout/WebMcp.astro +1 -1
  448. package/src/components/layout/analytics-client.ts +72 -15
  449. package/src/components/layout/drawer-inert.ts +113 -15
  450. package/src/components/layout/dropdown-clamp.ts +105 -0
  451. package/src/components/layout/head-scripts.ts +15 -6
  452. package/src/components/layout/nav-utils.ts +17 -0
  453. package/src/components/layout/search/algolia.ts +8 -8
  454. package/src/components/layout/search/orama-cloud.ts +9 -7
  455. package/src/components/layout/search/typesense.ts +11 -17
  456. package/src/components/openapi/MessageComposer.astro +2 -2
  457. package/src/components/openapi/Playground.astro +2 -2
  458. package/src/components/openapi/playground-client.ts +79 -10
  459. package/src/core/changelog-index.ts +24 -0
  460. package/src/core/component-overrides.ts +399 -154
  461. package/src/core/config-input.ts +188 -553
  462. package/src/core/config.ts +130 -41
  463. package/src/core/custom-pages.ts +105 -0
  464. package/src/core/data.ts +31 -12
  465. package/src/core/define-components.ts +12 -9
  466. package/src/core/deployment-env.ts +18 -74
  467. package/src/core/diagnostics.ts +14 -7
  468. package/src/core/graph.ts +69 -25
  469. package/src/core/i18n-ui.ts +6 -2
  470. package/src/core/i18n.ts +17 -0
  471. package/src/core/includes.ts +156 -38
  472. package/src/core/last-modified.ts +6 -11
  473. package/src/core/links.ts +166 -2
  474. package/src/core/manifest.ts +10 -3
  475. package/src/core/navigation.ts +115 -16
  476. package/src/core/new-tab.ts +35 -0
  477. package/src/core/node-require.ts +21 -0
  478. package/src/core/project-graph.ts +42 -26
  479. package/src/core/project.ts +9 -4
  480. package/src/core/request-body.ts +61 -0
  481. package/src/core/safe-href.ts +28 -0
  482. package/src/core/safe-links.ts +68 -0
  483. package/src/core/schema.ts +517 -739
  484. package/src/core/server-features.ts +8 -9
  485. package/src/core/sources/assets.ts +47 -11
  486. package/src/core/sources/collection.ts +67 -0
  487. package/src/core/sources/contentful-rich-text.ts +285 -0
  488. package/src/core/sources/contentful.ts +173 -0
  489. package/src/core/sources/github-releases.ts +51 -1
  490. package/src/core/sources/json.ts +71 -0
  491. package/src/core/sources/lexical.ts +195 -0
  492. package/src/core/sources/lower.ts +226 -0
  493. package/src/core/sources/normalize.ts +52 -2
  494. package/src/core/sources/notion.ts +39 -28
  495. package/src/core/sources/payload.ts +135 -0
  496. package/src/core/sources/portable-text.ts +11 -16
  497. package/src/core/sources/remote.ts +226 -0
  498. package/src/core/sources/resolve.ts +104 -166
  499. package/src/core/sources/sanity.ts +16 -49
  500. package/src/core/sources/strapi-blocks.ts +124 -0
  501. package/src/core/sources/strapi.ts +191 -0
  502. package/src/core/sources/types.ts +12 -1
  503. package/src/core/types.ts +15 -1
  504. package/src/core/ui-packs/ar.ts +6 -2
  505. package/src/core/ui-packs/bg.ts +6 -2
  506. package/src/core/ui-packs/bn.ts +6 -2
  507. package/src/core/ui-packs/ca.ts +3 -1
  508. package/src/core/ui-packs/cs.ts +6 -2
  509. package/src/core/ui-packs/da.ts +6 -2
  510. package/src/core/ui-packs/de.ts +6 -2
  511. package/src/core/ui-packs/el.ts +3 -1
  512. package/src/core/ui-packs/es.ts +3 -1
  513. package/src/core/ui-packs/fa.ts +6 -2
  514. package/src/core/ui-packs/fi.ts +6 -2
  515. package/src/core/ui-packs/fr.ts +3 -1
  516. package/src/core/ui-packs/he.ts +6 -2
  517. package/src/core/ui-packs/hi.ts +6 -2
  518. package/src/core/ui-packs/hr.ts +6 -2
  519. package/src/core/ui-packs/hu.ts +6 -2
  520. package/src/core/ui-packs/id.ts +6 -2
  521. package/src/core/ui-packs/it.ts +3 -1
  522. package/src/core/ui-packs/ja.ts +3 -1
  523. package/src/core/ui-packs/ko.ts +3 -1
  524. package/src/core/ui-packs/nl.ts +6 -2
  525. package/src/core/ui-packs/no.ts +6 -2
  526. package/src/core/ui-packs/pl.ts +6 -2
  527. package/src/core/ui-packs/pt-br.ts +3 -1
  528. package/src/core/ui-packs/pt.ts +3 -1
  529. package/src/core/ui-packs/ro.ts +6 -2
  530. package/src/core/ui-packs/ru.ts +6 -2
  531. package/src/core/ui-packs/sk.ts +6 -2
  532. package/src/core/ui-packs/sr.ts +6 -2
  533. package/src/core/ui-packs/sv.ts +6 -2
  534. package/src/core/ui-packs/th.ts +3 -1
  535. package/src/core/ui-packs/tr.ts +6 -2
  536. package/src/core/ui-packs/uk.ts +6 -2
  537. package/src/core/ui-packs/vi.ts +3 -1
  538. package/src/core/ui-packs/zh-tw.ts +3 -1
  539. package/src/core/ui-packs/zh.ts +3 -1
  540. package/src/core/unrecognized-keys.ts +10 -0
  541. package/src/core/version-cut.ts +116 -8
  542. package/src/deploy/adapter-output.ts +57 -97
  543. package/src/deploy/adapters/cloudflare.ts +39 -0
  544. package/src/deploy/adapters/index.ts +41 -0
  545. package/src/deploy/adapters/netlify.ts +34 -0
  546. package/src/deploy/adapters/node.ts +34 -0
  547. package/src/deploy/adapters/registry.ts +127 -0
  548. package/src/deploy/adapters/types.ts +92 -0
  549. package/src/deploy/adapters/vercel.ts +35 -0
  550. package/src/deploy/artifacts.ts +55 -27
  551. package/src/deploy/cloudflare-negotiation.ts +179 -100
  552. package/src/deploy/function-bundle.ts +18 -3
  553. package/src/deploy/headers.ts +124 -23
  554. package/src/deploy/node-headers.ts +198 -0
  555. package/src/deploy/platforms/cloudflare.ts +312 -0
  556. package/src/deploy/platforms/index.ts +67 -0
  557. package/src/deploy/platforms/netlify.ts +52 -0
  558. package/src/deploy/platforms/node.ts +40 -0
  559. package/src/deploy/platforms/paths.ts +42 -0
  560. package/src/deploy/platforms/static.ts +29 -0
  561. package/src/deploy/platforms/types.ts +112 -0
  562. package/src/deploy/platforms/vercel.ts +193 -0
  563. package/src/deploy/redirects.ts +30 -18
  564. package/src/deploy/robots.ts +3 -3
  565. package/src/deploy/rss.ts +2 -2
  566. package/src/deploy/sitemap.ts +2 -2
  567. package/src/deploy/vercel-negotiation.ts +2 -2
  568. package/src/eval/agents.ts +32 -1
  569. package/src/eval/findings.ts +19 -11
  570. package/src/eval/report.ts +10 -2
  571. package/src/markdown/external-links.ts +65 -0
  572. package/src/markdown/include.ts +45 -27
  573. package/src/markdown/index.ts +34 -9
  574. package/src/markdown/inline-code.ts +12 -6
  575. package/src/markdown/relative-links.ts +324 -0
  576. package/src/markdown/themes.ts +3 -3
  577. package/src/migrate/migrate.ts +149 -0
  578. package/src/openapi/parse.ts +64 -16
  579. package/src/openapi/proxy.ts +62 -10
  580. package/src/openapi/references.ts +147 -147
  581. package/src/openapi/render-mdx.ts +32 -10
  582. package/src/openapi/scalar.ts +15 -13
  583. package/src/openapi/sentence.ts +14 -0
  584. package/src/openapi/source.ts +37 -21
  585. package/src/openapi/spec-dependency-error.ts +15 -0
  586. package/src/reference/asyncapi.ts +83 -0
  587. package/src/reference/graphql.ts +89 -0
  588. package/src/reference/index.ts +40 -0
  589. package/src/reference/openapi.ts +83 -0
  590. package/src/reference/options.ts +201 -0
  591. package/src/reference/scalar.ts +136 -0
  592. package/src/reference/schema.ts +88 -0
  593. package/src/registry/eject.ts +97 -42
  594. package/src/search/adapters/algolia.ts +46 -0
  595. package/src/search/adapters/flexsearch.ts +29 -0
  596. package/src/search/adapters/index.ts +39 -0
  597. package/src/search/adapters/mixedbread.ts +39 -0
  598. package/src/search/adapters/orama-cloud.ts +51 -0
  599. package/src/search/adapters/orama.ts +24 -0
  600. package/src/search/adapters/pagefind.ts +27 -0
  601. package/src/search/adapters/registry.ts +131 -0
  602. package/src/search/adapters/types.ts +46 -0
  603. package/src/search/adapters/typesense.ts +57 -0
  604. package/src/search/build.ts +8 -4
  605. package/src/search/documents.ts +6 -0
  606. package/src/search/sync/algolia.ts +12 -11
  607. package/src/search/sync/index.ts +35 -20
  608. package/src/search/sync/orama-cloud.ts +12 -9
  609. package/src/search/sync/typesense.ts +15 -13
  610. package/src/sources/contentful.ts +63 -0
  611. package/src/sources/custom.ts +38 -0
  612. package/src/sources/filesystem.ts +51 -0
  613. package/src/sources/github-releases.ts +52 -0
  614. package/src/sources/index.ts +60 -0
  615. package/src/sources/mdx-remote.ts +76 -0
  616. package/src/sources/notion.ts +63 -0
  617. package/src/sources/obsidian.ts +38 -0
  618. package/src/sources/payload.ts +60 -0
  619. package/src/sources/registry.ts +182 -0
  620. package/src/sources/sanity.ts +66 -0
  621. package/src/sources/shared.ts +52 -0
  622. package/src/sources/strapi.ts +58 -0
  623. package/src/theme/entry.ts +24 -2
  624. package/src/translate/report.ts +40 -7
  625. package/src/upgrade/upgrade.ts +499 -0
  626. package/dist/cli/chunk-0qymqwzz.js +0 -164
  627. package/dist/cli/chunk-0qymqwzz.js.map +0 -15
  628. package/dist/cli/chunk-1jefwnfs.js.map +0 -48
  629. package/dist/cli/chunk-2mzebbbz.js +0 -69
  630. package/dist/cli/chunk-2z47ypj8.js.map +0 -11
  631. package/dist/cli/chunk-4x36ddpw.js.map +0 -11
  632. package/dist/cli/chunk-5093q3n7.js +0 -68
  633. package/dist/cli/chunk-5093q3n7.js.map +0 -10
  634. package/dist/cli/chunk-5qk08vmp.js.map +0 -11
  635. package/dist/cli/chunk-7s8hm3b6.js +0 -5347
  636. package/dist/cli/chunk-7s8hm3b6.js.map +0 -58
  637. package/dist/cli/chunk-8cjtbafj.js.map +0 -13
  638. package/dist/cli/chunk-97r59kpr.js +0 -381
  639. package/dist/cli/chunk-97r59kpr.js.map +0 -12
  640. package/dist/cli/chunk-ahnw3kxw.js.map +0 -15
  641. package/dist/cli/chunk-b27xqwn9.js.map +0 -10
  642. package/dist/cli/chunk-bf6bt1xt.js +0 -185
  643. package/dist/cli/chunk-bf6bt1xt.js.map +0 -11
  644. package/dist/cli/chunk-cjtn640a.js.map +0 -10
  645. package/dist/cli/chunk-ct47dqpx.js.map +0 -11
  646. package/dist/cli/chunk-esphfr8p.js +0 -107
  647. package/dist/cli/chunk-esphfr8p.js.map +0 -11
  648. package/dist/cli/chunk-ex56aa81.js +0 -1016
  649. package/dist/cli/chunk-ex56aa81.js.map +0 -13
  650. package/dist/cli/chunk-garjf5z9.js +0 -30
  651. package/dist/cli/chunk-garjf5z9.js.map +0 -10
  652. package/dist/cli/chunk-js7saxwm.js +0 -1045
  653. package/dist/cli/chunk-js7saxwm.js.map +0 -22
  654. package/dist/cli/chunk-k79xp7av.js.map +0 -39
  655. package/dist/cli/chunk-ps4m1xh4.js.map +0 -15
  656. package/dist/cli/chunk-q4rae3bg.js +0 -60
  657. package/dist/cli/chunk-q4rae3bg.js.map +0 -10
  658. package/dist/cli/chunk-rz9jmfhz.js +0 -108
  659. package/dist/cli/chunk-rz9jmfhz.js.map +0 -10
  660. package/dist/cli/chunk-vh9w1sgp.js +0 -73
  661. package/dist/cli/chunk-vh9w1sgp.js.map +0 -10
  662. package/dist/cli/chunk-vrfp10qk.js +0 -81
  663. package/dist/cli/chunk-vrfp10qk.js.map +0 -10
  664. package/dist/cli/chunk-yg63d42r.js.map +0 -34
  665. package/docs/advanced/api-reference.mdx +0 -240
  666. package/docs/reference/cli.mdx +0 -197
  667. package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +0 -20
  668. package/src/components/content/changelog-element.ts +0 -69
  669. package/src/search/providers.ts +0 -91
  670. /package/dist/cli/{chunk-e7f42gdj.js.map → chunk-7ez8ny0t.js.map} +0 -0
  671. /package/dist/cli/{chunk-nn13znc2.js.map → chunk-88cpgt6h.js.map} +0 -0
  672. /package/dist/cli/{chunk-vacwm2hv.js.map → chunk-f7t03s3g.js.map} +0 -0
  673. /package/dist/cli/{chunk-dwgcp5sm.js.map → chunk-zxccj738.js.map} +0 -0
@@ -4,24 +4,28 @@ 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 { askBackendRuntimeDep } from "../ai/ask.ts";
8
7
  import type { AskBackend } from "../ai/ask.ts";
9
8
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
10
9
  import { normalizeBasePath } from "../core/base-path.ts";
11
10
  import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
12
- import type { AskReasoning, ResolvedConfig } from "../core/schema.ts";
11
+ import type { ResolvedConfig } from "../core/schema.ts";
12
+ import { resolveDocsCollection } from "../core/sources/collection.ts";
13
13
  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
+ import { deployPassthrough } from "../deploy/adapters/types.ts";
17
+ import { SVG_ASSET_POLICY } from "../deploy/headers.ts";
18
+ import { deployPlatform } from "../deploy/platforms/index.ts";
19
+ import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
16
20
  import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
17
21
  import type { OgCache } from "../og/cache.ts";
18
22
  import type { OgFont, OgFontFamilies } from "../og/card.ts";
19
- import { hasScalarReferences } from "../openapi/references.ts";
20
- import { searchProviderMeta } from "../search/providers.ts";
23
+ import type { MixedbreadOptions } from "../search/adapters/mixedbread.ts";
24
+ import type { ResolvedSearchAdapter } from "../search/adapters/registry.ts";
21
25
  import { buildFontEntries, fontLocaleCodes } from "../theme/fonts.ts";
26
+ import { importSpecifier, wrapperPropsType } from "./component-slots.ts";
22
27
  import type { ExampleSpec } from "./examples.ts";
23
28
  import type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
24
- import type { IslandSpec } from "./islands.ts";
25
29
  import type { OgCustomRoute } from "./pages.ts";
26
30
  import { RUNTIME_MODULE_FILES } from "./runtime-modules.ts";
27
31
 
@@ -77,67 +81,54 @@ const findWorkspaceRoot = (start: string): string => {
77
81
  }
78
82
  };
79
83
 
80
- type DeploymentAdapter = NonNullable<ResolvedConfig["deployment"]["adapter"]>;
81
-
82
- const ADAPTER_IMPORTS = {
83
- cloudflare: "@astrojs/cloudflare",
84
- netlify: "@astrojs/netlify",
85
- node: "@astrojs/node",
86
- vercel: "@astrojs/vercel",
87
- } satisfies Record<DeploymentAdapter, string>;
88
-
89
- /** Adapter constructor arguments, for the adapters that need any. */
90
- const ADAPTER_OPTIONS = new Map<DeploymentAdapter, string>([
91
- ["node", '{ mode: "standalone" }'],
92
- ]);
93
-
94
- const WRANGLER_CONFIG_FILES = [
95
- "wrangler.jsonc",
96
- "wrangler.json",
97
- "wrangler.toml",
98
- ];
84
+ /**
85
+ * The `adapter:` entry of the generated config plus the import that backs it,
86
+ * for a server build. The platform (see `deploy/platforms/*`) names the
87
+ * `@astrojs/*` package and Blume's own constructor options; the descriptor's
88
+ * passthrough options are spread over those, verbatim, so anything the
89
+ * adapter was given reaches the real adapter. A platform that resolves its
90
+ * output against Astro's root is handed the project root instead of the
91
+ * hidden runtime (`withAdapterRoot`).
92
+ */
93
+ interface AstroAdapterRender {
94
+ /** Extra top-level `defineConfig` entries the adapter needs. */
95
+ configEntries: string;
96
+ importLine: string;
97
+ /** The `adapter:` entry, or empty for a static build. */
98
+ option: string;
99
+ }
99
100
 
100
- const resolveCloudflareAdapterArgs = (context: ProjectContext): string => {
101
- // Every Blume HTML route prerenders (the only server routes are API
102
- // endpoints), so images are optimized at build time with sharp. The
103
- // adapter's default (`cloudflare-binding`) would instead declare a runtime
104
- // `IMAGES` binding in the generated wrangler config that nothing uses.
105
- const args: string[] = [
106
- 'prerenderEnvironment: "node"',
107
- 'imageService: "compile"',
108
- ];
109
- const wranglerPath = WRANGLER_CONFIG_FILES.map((file) =>
110
- join(context.root, file)
111
- ).find((file) => existsSync(file));
112
- if (wranglerPath) {
113
- let configPath = relative(context.outDir, wranglerPath);
114
- // The wrangler config always lives at the project root, above the `.blume`
115
- // runtime, so `relative` yields a `../…` path; normalize the theoretical
116
- // sibling case to an explicit `./` so it reads as a relative import.
117
- if (!configPath.startsWith(".") && !configPath.startsWith("/")) {
118
- configPath = `./${configPath}`;
119
- }
120
- args.push(`configPath: ${JSON.stringify(configPath)}`);
101
+ const renderAstroAdapter = (
102
+ deployment: ResolvedConfig["deployment"],
103
+ context: ProjectContext,
104
+ ejected: boolean
105
+ ): AstroAdapterRender => {
106
+ const platform = deployPlatform(deployment);
107
+ if (deployment.options.output !== "server" || !platform.astro) {
108
+ return { configEntries: "", importLine: "", option: "" };
121
109
  }
122
- return `{ ${args.join(", ")} }`;
110
+ const { astro } = platform;
111
+ const args = {
112
+ ...astro.options(context),
113
+ ...deployPassthrough(deployment.options),
114
+ };
115
+ const argsLiteral = Object.keys(args).length > 0 ? JSON.stringify(args) : "";
116
+ const construct = `adapter(${argsLiteral})`;
117
+ // An ejected app's Astro root is the project root already, so its adapter
118
+ // needs no redirect (and the config no machine-specific path).
119
+ const expression =
120
+ platform.hiddenRuntime.showProjectRoot && !ejected
121
+ ? `withAdapterRoot(${construct}, ${JSON.stringify(adapterRoot(context))})`
122
+ : construct;
123
+ return {
124
+ configEntries: Object.entries(astro.config)
125
+ .map(([key, value]) => `\n ${key}: ${JSON.stringify(value)},`)
126
+ .join(""),
127
+ importLine: `import adapter from "${astro.package}";\n`,
128
+ option: `\n adapter: ${expression},`,
129
+ };
123
130
  };
124
131
 
125
- /**
126
- * Without a configured driver, `@astrojs/cloudflare` force-enables KV-backed
127
- * sessions and declares a `SESSION` kv_namespaces entry in the generated
128
- * wrangler config — which `wrangler deploy` then requires a real KV namespace
129
- * for, even though Blume never reads `Astro.session`. Astro's `session: false`
130
- * opts the project out, and the adapter checks for it before adding the
131
- * binding. (An in-memory driver used to stand in before the opt-out existed.)
132
- */
133
- const resolveSessionOption = (deployment: {
134
- adapter: string | null;
135
- output: string;
136
- }): string =>
137
- deployment.output === "server" && deployment.adapter === "cloudflare"
138
- ? "\n session: false,"
139
- : "";
140
-
141
132
  /**
142
133
  * A font weight as Astro's Fonts API spells it: a variable range is
143
134
  * `"100 900"` there, where Blume's config (and Takumi's Google Fonts helper,
@@ -183,29 +174,36 @@ export const runtimeDependencies = (options: {
183
174
  if (needsSvelte) {
184
175
  deps.push("@astrojs/svelte");
185
176
  }
186
- // The Scalar integration is only declared for a Scalar-rendered reference
187
- // (the `renderer: "scalar"` opt-out on either block). Blume-rendered
188
- // references parse at generate time and need no runtime Scalar dependency.
189
- if (hasScalarReferences(config)) {
190
- deps.push("@scalar/astro");
191
- }
192
- // Only the configured search provider's SDK is declared, so a project pulls in
193
- // (and the user installs) exactly the backend it uses — nothing more.
194
- deps.push(...searchProviderMeta(config.search.provider).runtimeDeps);
195
- // Ask AI's provider SDK, when its backend needs one (gateway uses core `ai`).
196
- if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
197
- const askDep = askBackendRuntimeDep(config.ai.ask);
198
- if (askDep) {
199
- deps.push(askDep);
177
+ // Each reference adapter declares what it needs: Blume's own renderer
178
+ // parses at generate time and needs nothing, while `scalar()` declares
179
+ // `@scalar/astro` so the framework crawl bundles the embed (two Scalar
180
+ // adapters declare it twice, hence the set). Only the configured
181
+ // search adapter's SDK is declared, so a project pulls in (and the user
182
+ // installs) exactly the backend it uses — nothing more. Each analytics
183
+ // adapter declares what it needs the same way; the built-ins need nothing
184
+ // beyond Blume's own deps, so their share is usually empty.
185
+ deps.push(
186
+ ...new Set(config.reference.flatMap((adapter) => adapter.runtimeDeps)),
187
+ ...config.search.provider.runtimeDeps,
188
+ ...config.analytics.flatMap((adapter) => adapter.runtimeDeps)
189
+ );
190
+ // Each content source adapter declares the SDK its fetch imports (Notion,
191
+ // Sanity); the descriptor is the one place that knows.
192
+ for (const source of config.content.sources) {
193
+ for (const dep of source.runtimeDeps) {
194
+ if (!deps.includes(dep)) {
195
+ deps.push(dep);
196
+ }
200
197
  }
201
198
  }
202
- const { deployment } = config;
203
- if (deployment.output === "server" && deployment.adapter) {
204
- const adapter = ADAPTER_IMPORTS[deployment.adapter];
205
- if (adapter) {
206
- deps.push(adapter);
207
- }
199
+ // Ask AI's provider SDK, as its adapter declares it (the gateway needs
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);
208
203
  }
204
+ // The deployment adapter's `@astrojs/*` package, for a server build; the
205
+ // descriptor declares it (and nothing for a static build).
206
+ deps.push(...config.deployment.runtimeDeps);
209
207
  return deps;
210
208
  };
211
209
 
@@ -271,25 +269,6 @@ const renderUserAliases = (
271
269
  )
272
270
  .join("");
273
271
 
274
- /** Astro's build output dir: the runtime's own `distDir`, else `<root>/dist`. */
275
- const astroOutDir = (context: ProjectContext): string =>
276
- context.distDir ?? `${context.root}/dist`;
277
-
278
- /**
279
- * The root a deploy adapter is shown, in place of the `.blume` runtime Astro
280
- * actually roots at. Adapters assume `outDir` is `<root>/dist` and resolve their
281
- * own output (and Vercel's dependency trace) against `root`, so the root implied
282
- * by Blume's `outDir` is the one that keeps that assumption true. See
283
- * {@link withAdapterRoot}.
284
- *
285
- * For a normal build that is the project root (`<project>/dist` -> `<project>`).
286
- * For a relocated runtime (`blume build --isolated`) it is the runtime dir
287
- * itself (`<runtime>/dist` -> `<runtime>`), keeping a verify build's adapter
288
- * output self-contained instead of overwriting the real `.vercel/output`.
289
- */
290
- const adapterRoot = (context: ProjectContext): string =>
291
- dirname(astroOutDir(context));
292
-
293
272
  /**
294
273
  * Excludes Vite's pre-bundled dep cache from @vitejs/plugin-react. Astro's
295
274
  * react() replaces the plugin's default `/node_modules/` exclude with just
@@ -451,6 +430,59 @@ const resolveOptimizeDeps = (options: {
451
430
  return { optimizeDepsEntries, optimizeDepsInclude };
452
431
  };
453
432
 
433
+ /**
434
+ * A path the generated config hands Vite or a plugin. The ejected config
435
+ * resolves it against the config file itself when it loads, so the project
436
+ * builds from any checkout and working directory, and Vite gets the absolute
437
+ * alias target it expects (it warns on every relative one). The hidden
438
+ * runtime's paths are absolute already.
439
+ */
440
+ const configPath = (path: string, ejected: boolean): string =>
441
+ ejected
442
+ ? `fileURLToPath(new URL(${JSON.stringify(path)}, import.meta.url))`
443
+ : JSON.stringify(path);
444
+
445
+ /**
446
+ * The first line of an ejected `astro.config.mjs`, which also marks the
447
+ * project as ejected for the CLI (`blume dev`/`build` step aside, and a second
448
+ * `blume eject` refuses to overwrite the app).
449
+ */
450
+ export const EJECTED_CONFIG_HEADER =
451
+ "// Written by `blume eject`. This is your Astro config now: edit it freely.";
452
+
453
+ /**
454
+ * The generated config's first line. The hidden runtime's config is rewritten
455
+ * on every run; an ejected one belongs to the project from then on.
456
+ */
457
+ const astroConfigHeader = (ejected: boolean): string =>
458
+ ejected
459
+ ? EJECTED_CONFIG_HEADER
460
+ : "// Generated by Blume. Do not edit; this file is recreated on each run.";
461
+
462
+ /**
463
+ * The client build's chunk-size warning threshold (kB) on a site with Mermaid
464
+ * diagrams. Mermaid's ELK layout engine (about 1.5 MB) and its core (about
465
+ * 650 kB) land in their own chunks, which the `blume:features` loader fetches
466
+ * only on a page with a diagram — so Vite's default 500 kB warning fired on
467
+ * every such build without anything to fix. A chunk past this still warns.
468
+ */
469
+ const MERMAID_CHUNK_LIMIT = "\n chunkSizeWarningLimit: 2048,";
470
+
471
+ /**
472
+ * Rolldown's log hook for the build. Astro's content-assets plugin opens each
473
+ * page's `?astroPropagatedAssets` module with a `"use astro:head-inject"`
474
+ * directive that nothing reads once bundled (head propagation rides on the
475
+ * module's `__astroPropagation` export), and Rolldown 1.2.10+ warns about it
476
+ * in nine lines per page, on every build. That one warning is dropped; every
477
+ * other log goes to Vite's default handler.
478
+ */
479
+ const ROLLDOWN_ON_LOG = `onLog(level, log, handler) {
480
+ if (log.code === "MODULE_LEVEL_DIRECTIVE" && String(log.message).includes("use astro:head-inject")) {
481
+ return;
482
+ }
483
+ handler(level, log);
484
+ }`;
485
+
454
486
  /**
455
487
  * How the generated config reaches the runtime data modules (`blume:data`,
456
488
  * the search index, …): served from memory by `runtimeModulesPlugin` in the
@@ -479,7 +511,7 @@ const renderRuntimeModuleWiring = (
479
511
  const aliasLines = [...RUNTIME_MODULE_FILES]
480
512
  .map(
481
513
  ([id, file]) =>
482
- `\n ${JSON.stringify(id)}: ${JSON.stringify(`${generatedModulesDir}/${file}`)},`
514
+ `\n ${JSON.stringify(id)}: ${configPath(`${generatedModulesDir}/${file}`, true)},`
483
515
  )
484
516
  .join("");
485
517
  return { aliasLines, imports: [], pluginEntry: "" };
@@ -572,6 +604,7 @@ export const astroConfigTemplate = (options: {
572
604
  needsVue,
573
605
  searchClientPath,
574
606
  } = options;
607
+ const ejected = generatedModulesDir !== undefined;
575
608
  const {
576
609
  aliasLines: runtimeModuleAliasLines,
577
610
  imports: runtimeModuleImports,
@@ -579,7 +612,6 @@ export const astroConfigTemplate = (options: {
579
612
  } = renderRuntimeModuleWiring(generatedModulesDir);
580
613
  const { deployment } = config;
581
614
  const userAliasLines = renderUserAliases(options.aliases);
582
- const server = deployment.output === "server";
583
615
 
584
616
  // The project root plus the workspace root, so hoisted dependencies (e.g.
585
617
  // KaTeX fonts under a monorepo's root node_modules) stay servable in dev.
@@ -593,38 +625,17 @@ export const astroConfigTemplate = (options: {
593
625
  reactCompilerPath: options.reactCompilerPath,
594
626
  });
595
627
 
596
- const adapterImport =
597
- server && deployment.adapter
598
- ? `import adapter from "${ADAPTER_IMPORTS[deployment.adapter]}";\n`
599
- : "";
600
- const adapterArgs = (() => {
601
- if (!server || !deployment.adapter) {
602
- return "";
603
- }
604
- if (deployment.adapter === "cloudflare") {
605
- return resolveCloudflareAdapterArgs(context);
606
- }
607
- return ADAPTER_OPTIONS.get(deployment.adapter) ?? "";
608
- })();
609
- // Vercel resolves its Build Output tree and its `@vercel/nft` dependency
610
- // trace against the Astro root, which for Blume is the hidden `.blume`
611
- // runtime — leaving the traced function without its chunks or node_modules.
612
- // The other adapters emit into `outDir` (cloudflare, node) or are surfaced
613
- // afterwards (netlify), so none of them read `root` this way.
614
- const adapterExpr =
615
- deployment.adapter === "vercel"
616
- ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})`
617
- : `adapter(${adapterArgs})`;
618
- const adapterOption =
619
- server && deployment.adapter ? `\n adapter: ${adapterExpr},` : "";
620
-
621
- const sessionOption = resolveSessionOption(deployment);
622
-
623
- const siteOption = deployment.site
624
- ? `\n site: ${JSON.stringify(deployment.site)},`
628
+ const {
629
+ configEntries: adapterConfigEntries,
630
+ importLine: adapterImport,
631
+ option: adapterOption,
632
+ } = renderAstroAdapter(deployment, context, ejected);
633
+
634
+ const siteOption = deployment.options.site
635
+ ? `\n site: ${JSON.stringify(deployment.options.site)},`
625
636
  : "";
626
- const baseOption = deployment.base
627
- ? `\n base: ${JSON.stringify(deployment.base)},`
637
+ const baseOption = deployment.options.base
638
+ ? `\n base: ${JSON.stringify(deployment.options.base)},`
628
639
  : "";
629
640
  const imageOption = renderImageOption(config);
630
641
 
@@ -649,7 +660,7 @@ export const astroConfigTemplate = (options: {
649
660
  const basedRedirects = applyBaseToAstroRedirects(
650
661
  config.redirects,
651
662
  config.basePath,
652
- deployment.base ?? ""
663
+ deployment.options.base ?? ""
653
664
  );
654
665
  const redirectsOption =
655
666
  basedRedirects.length > 0
@@ -746,16 +757,26 @@ export const astroConfigTemplate = (options: {
746
757
  // hand-written `basePath` link (`/docs/x`) isn't double-prefixed (see
747
758
  // `withComposedBasePath`). The link checker validates the base-less authored
748
759
  // path against `basePath` routes separately.
749
- const deployBase = normalizeBasePath(deployment.base);
760
+ const deployBase = normalizeBasePath(deployment.options.base);
761
+ // Both processors take the same options. An ejected app has no CLI to
762
+ // publish the `blume:data` snapshot that relative page links resolve
763
+ // through, so its processors read the snapshot file instead, resolved
764
+ // against the config file like the module aliases.
765
+ const processorLiteral = JSON.stringify({
766
+ basePath: config.basePath,
767
+ codeThemes: config.markdown.code.theme,
768
+ contentRoot: options.contentRoot,
769
+ deployBase,
770
+ externalLinks: config.markdown.externalLinks,
771
+ headingAnchors: config.markdown.headingAnchors,
772
+ });
773
+ const processorOptions =
774
+ options.generatedModulesDir === undefined
775
+ ? processorLiteral
776
+ : `{ ...${processorLiteral}, dataFile: ${configPath(`${options.generatedModulesDir}/data.json`, true)} }`;
750
777
 
751
778
  const integrations = [
752
- `mdx({ processor: blumeMdxProcessor(${JSON.stringify({
753
- basePath: config.basePath,
754
- codeThemes: config.markdown.codeBlocks.theme,
755
- contentRoot: options.contentRoot,
756
- deployBase,
757
- headingAnchors: config.markdown.headingAnchors,
758
- })}) })`,
779
+ `mdx({ processor: blumeMdxProcessor(${processorOptions}) })`,
759
780
  ];
760
781
  if (needsReact) {
761
782
  integrations.push(reactIntegration(options.reactCompilerPath));
@@ -774,7 +795,7 @@ export const astroConfigTemplate = (options: {
774
795
  blumeIntegrationOptions({
775
796
  config,
776
797
  contentRoutes,
777
- ejected: generatedModulesDir !== undefined,
798
+ ejected,
778
799
  pages,
779
800
  })
780
801
  )})`
@@ -787,9 +808,13 @@ export const astroConfigTemplate = (options: {
787
808
  userIntegrationSpread,
788
809
  } = renderIntegrationBridge(options.integrationBridge);
789
810
 
790
- return `// Generated by Blume. Do not edit; this file is recreated on each run.
811
+ const fileUrlImport = ejected
812
+ ? `import { fileURLToPath } from "node:url";\n`
813
+ : "";
814
+
815
+ return `${astroConfigHeader(ejected)}
791
816
  ${configSourceMarker}${userConfigImports}import { availableParallelism } from "node:os";
792
- ${defineConfigImport}
817
+ ${fileUrlImport}${defineConfigImport}
793
818
  import mdx from "@astrojs/mdx";
794
819
  import tailwindcss from "@tailwindcss/vite";
795
820
  import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
@@ -797,22 +822,16 @@ ${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
797
822
  ${userConfigSetup}export default defineConfig({
798
823
  root: ${JSON.stringify(context.outDir)},
799
824
  srcDir: ${JSON.stringify(`${context.outDir}/src`)},
800
- outDir: ${JSON.stringify(astroOutDir(context))},
825
+ outDir: ${JSON.stringify(distDir(context))},
801
826
  publicDir: ${JSON.stringify(`${context.root}/public`)},${cacheOptions}
802
- output: ${JSON.stringify(deployment.output)},${adapterOption}${sessionOption}${siteOption}${baseOption}${imageOption}${redirectsOption}${i18nOption}${fontsOption}
827
+ output: ${JSON.stringify(deployment.options.output)},${adapterOption}${adapterConfigEntries}${siteOption}${baseOption}${imageOption}${redirectsOption}${i18nOption}${fontsOption}
803
828
  integrations: [${integrations.join(", ")}${userIntegrationSpread}],
804
829
  markdown: {
805
- processor: blumeMarkdownProcessor(${JSON.stringify({
806
- basePath: config.basePath,
807
- codeThemes: config.markdown.codeBlocks.theme,
808
- contentRoot: options.contentRoot,
809
- deployBase,
810
- headingAnchors: config.markdown.headingAnchors,
811
- })}),
830
+ processor: blumeMarkdownProcessor(${processorOptions}),
812
831
  shikiConfig: {
813
832
  themes: {
814
- light: ${JSON.stringify(config.markdown.codeBlocks.theme.light)},
815
- dark: ${JSON.stringify(config.markdown.codeBlocks.theme.dark)},
833
+ light: ${JSON.stringify(config.markdown.code.theme.light)},
834
+ dark: ${JSON.stringify(config.markdown.code.theme.dark)},
816
835
  },
817
836
  defaultColor: false,
818
837
  transformers: [${twoslashTransformer}...blumeShikiTransformers(${JSON.stringify(
@@ -841,9 +860,15 @@ ${userConfigSetup}export default defineConfig({
841
860
  // the overlap only adds memory.
842
861
  build: { concurrency: Math.min(8, availableParallelism()) },
843
862
  vite: {${viteCacheOption}
844
- plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${JSON.stringify(
845
- `${context.outDir}/src/generated/includes.json`
863
+ plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
864
+ `${context.outDir}/src/generated/includes.json`,
865
+ ejected
846
866
  )}), prerenderDepsPlugin()],
867
+ build: {${features.mermaid ? MERMAID_CHUNK_LIMIT : ""}
868
+ rolldownOptions: {
869
+ ${ROLLDOWN_ON_LOG},
870
+ },
871
+ },
847
872
  // Everything hydration can reach must be part of the dev dep optimizer's
848
873
  // FIRST run. The Vite root is the generated runtime, so user pages,
849
874
  // islands, and aliased components live outside it and are only crawled
@@ -889,12 +914,12 @@ ${userConfigSetup}export default defineConfig({
889
914
  },
890
915
  resolve: {
891
916
  alias: {
892
- "blume:ask": ${JSON.stringify(askPath)},
893
- "blume:examples": ${JSON.stringify(examplesPath)},
894
- "blume:examples-theme": ${JSON.stringify(examplesThemePath)},
895
- "blume:features": ${JSON.stringify(featuresPath)},
896
- "blume:search-client": ${JSON.stringify(searchClientPath)},
897
- "blume:theme": ${JSON.stringify(themePath)},${runtimeModuleAliasLines}${userAliasLines}
917
+ "blume:ask": ${configPath(askPath, ejected)},
918
+ "blume:examples": ${configPath(examplesPath, ejected)},
919
+ "blume:examples-theme": ${configPath(examplesThemePath, ejected)},
920
+ "blume:features": ${configPath(featuresPath, ejected)},
921
+ "blume:search-client": ${configPath(searchClientPath, ejected)},
922
+ "blume:theme": ${configPath(themePath, ejected)},${runtimeModuleAliasLines}${userAliasLines}
898
923
  },
899
924
  },
900
925
  server: {
@@ -943,10 +968,9 @@ export const contentConfigTemplate = (options: {
943
968
  /** Base dir for the staged collection; defaults to `<outDir>/content`. */
944
969
  stagedBase?: string;
945
970
  /**
946
- * The `docs` collection's base + include/exclude globs. Defaults to
947
- * `content.root` and the top-level content globs; a single filesystem source
948
- * roots the collection at *its* root so entry ids resolve (see
949
- * `resolveDocsCollection`).
971
+ * The `docs` collection's base + include/exclude globs. Defaults to what
972
+ * `resolveDocsCollection` derives from the filesystem sources: the collection
973
+ * roots at the first one so entry ids resolve.
950
974
  */
951
975
  collection?: { base: string; include: string[]; exclude: string[] };
952
976
  /**
@@ -958,9 +982,11 @@ export const contentConfigTemplate = (options: {
958
982
  }): string => {
959
983
  const { context, config } = options;
960
984
  const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
961
- const collectionBase = options.collection?.base ?? context.contentRoot;
962
- const includeGlobs = options.collection?.include ?? config.content.include;
963
- const excludeGlobs = options.collection?.exclude ?? config.content.exclude;
985
+ const collection =
986
+ options.collection ?? resolveDocsCollection(config, context.root);
987
+ const collectionBase = collection.base;
988
+ const includeGlobs = collection.include;
989
+ const excludeGlobs = collection.exclude;
964
990
 
965
991
  // Fold the content excludes into the glob as negative patterns so the `docs`
966
992
  // collection doesn't ingest ignored trees (`node_modules`, `snippets`, the
@@ -1044,11 +1070,6 @@ export interface AskEndpointOptions {
1044
1070
  cors?: string[];
1045
1071
  /** `ai.ask.instructions` — extra system-prompt text. */
1046
1072
  instructions?: string;
1047
- /**
1048
- * `ai.ask.reasoning` — how much the model reasons before answering, sent
1049
- * as the backend's own reasoning-effort control.
1050
- */
1051
- reasoning?: AskReasoning;
1052
1073
  /** `ai.ask.retrieval` — how much documentation each question carries. */
1053
1074
  retrieval?: AskRetrievalOptions;
1054
1075
  }
@@ -1092,31 +1113,38 @@ export const OPTIONS: APIRoute = ({ request }) =>
1092
1113
  }
1093
1114
  : { close: "};", imports: [], open: "async ({ request }) => {", setup: "" };
1094
1115
 
1116
+ /**
1117
+ * Largest request body the Ask AI route reads: 64 KB, well above the
1118
+ * 24,000-character message budget it validates next, so a real conversation
1119
+ * never meets it.
1120
+ */
1121
+ const ASK_BODY_LIMIT_BYTES = 65_536;
1122
+
1095
1123
  /**
1096
1124
  * Generate the Ask AI server endpoint (`.blume/src/pages/api/ask.ts`).
1097
1125
  *
1126
+ * The provider-specific pieces — imports, the provider factory call, the model
1127
+ * expression, the credential guard, and the adapter's reasoning mapping and
1128
+ * `providerOptions` — come from the resolved `backend` descriptor, inlined as
1129
+ * literals so the route imports the provider SDK by bare name and never
1130
+ * `blume.config.ts`. `backend.grounded` decides whether answers are grounded
1131
+ * in the retrieved docs (every adapter but Inkeep, which retrieves itself).
1132
+ *
1098
1133
  * `options.instructions` (the `ai.ask.instructions` config) is appended to the
1099
1134
  * built-in prompt on every path: the grounded prompt via `createAskContext`,
1100
1135
  * and the plain fallback here. `options.retrieval` (the `ai.ask.retrieval`
1101
1136
  * config) is forwarded to `createAskContext` on the grounded path, where it
1102
- * sizes retrieval. `options.reasoning` (the `ai.ask.reasoning` config)
1103
- * reaches the model call on both paths. `options.cors` (the `ai.ask.cors`
1104
- * config) adds a preflight handler and wraps the `POST` so every response
1105
- * names a listed origin. All four travel in one options object so a new call
1106
- * site can't silently drop one of them.
1137
+ * sizes retrieval. `options.cors` (the `ai.ask.cors` config) adds a preflight
1138
+ * handler and wraps the `POST` so every response names a listed origin. All
1139
+ * three travel in one options object so a new call site can't silently drop
1140
+ * one of them.
1107
1141
  */
1108
1142
  export const askEndpointTemplate = (
1109
1143
  backend: AskBackend,
1110
- grounded: boolean,
1111
1144
  options?: AskEndpointOptions
1112
1145
  ): string => {
1113
- const { instructions, reasoning, retrieval } = options ?? {};
1114
- // `ai.ask.reasoning`. The gateway and OpenAI-compatible providers take it
1115
- // from `streamText`'s top-level `reasoning` (the gateway maps it to the
1116
- // model's own control, the OpenAI-compatible provider sends it as
1117
- // `reasoning_effort`). OpenRouter's provider ignores that call option and
1118
- // only reads its own model setting, so there the level rides on the model
1119
- // as `reasoning.effort`. Omitted keeps the provider default on every path.
1146
+ const { instructions, retrieval } = options ?? {};
1147
+ const { grounded } = backend;
1120
1148
  const fallbackPrompt = instructions
1121
1149
  ? `${ASK_FALLBACK_PROMPT}\n\n${instructions}`
1122
1150
  : ASK_FALLBACK_PROMPT;
@@ -1126,47 +1154,10 @@ export const askEndpointTemplate = (
1126
1154
  const imports = [
1127
1155
  'import type { APIRoute } from "astro";',
1128
1156
  'import { getSecret } from "astro:env/server";',
1129
- // The gateway provider reads the key (or Vercel's OIDC token) from the
1130
- // environment itself; passing the key explicitly lets a binding-backed
1131
- // secret store reach it too.
1132
- backend.kind === "gateway"
1133
- ? 'import { createGateway, streamText } from "ai";'
1134
- : 'import { streamText } from "ai";',
1157
+ 'import { readCappedText } from "blume/core/request-body.ts";',
1158
+ ...backend.template.imports,
1135
1159
  ];
1136
- let setup = "";
1137
- let modelExpr = JSON.stringify(backend.model);
1138
- // `ai.ask.headers`, inlined as literals: every provider factory below takes
1139
- // the same `headers` option, so one line serves all three.
1140
- const headersField = backend.headers
1141
- ? `\n headers: ${JSON.stringify(backend.headers)},`
1142
- : "";
1143
- if (backend.kind === "gateway") {
1144
- setup = `\nconst gateway = createGateway({
1145
- apiKey: getSecret("AI_GATEWAY_API_KEY"),${headersField}
1146
- });\n`;
1147
- modelExpr = `gateway(${JSON.stringify(backend.model)})`;
1148
- } else if (backend.kind === "openrouter") {
1149
- imports.push(
1150
- 'import { createOpenRouter } from "@openrouter/ai-sdk-provider";'
1151
- );
1152
- setup = `\nconst openrouter = createOpenRouter({
1153
- apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),${headersField}
1154
- });\n`;
1155
- const settings = reasoning
1156
- ? `, { reasoning: { effort: ${JSON.stringify(reasoning)} } }`
1157
- : "";
1158
- modelExpr = `openrouter(${JSON.stringify(backend.model)}${settings})`;
1159
- } else if (backend.kind === "openai-compatible") {
1160
- imports.push(
1161
- 'import { createOpenAICompatible } from "@ai-sdk/openai-compatible";'
1162
- );
1163
- setup = `\nconst provider = createOpenAICompatible({
1164
- apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),
1165
- baseURL: ${JSON.stringify(backend.baseUrl)},${headersField}
1166
- name: ${JSON.stringify(backend.name)},
1167
- });\n`;
1168
- modelExpr = `provider(${JSON.stringify(backend.model)})`;
1169
- }
1160
+ let { setup } = backend.template;
1170
1161
  // Ground the answer in retrieved docs, except for RAG-native backends (Inkeep),
1171
1162
  // which run their own retrieval and would conflict with injected context.
1172
1163
  if (grounded) {
@@ -1192,8 +1183,21 @@ export const askEndpointTemplate = (
1192
1183
  // can spend against the model per request, and restricting roles to
1193
1184
  // user/assistant keeps callers from injecting their own system prompt and
1194
1185
  // repurposing the endpoint as a general LLM proxy; front it with a rate
1195
- // limiter (or your provider's limits) for stronger protection.
1196
- const validate = ` const body = await request.json().catch(() => null);
1186
+ // limiter (or your provider's limits) for stronger protection. The body is
1187
+ // read under a 64 KB cap before any parsing, since a self-hosted Node server
1188
+ // would otherwise buffer an arbitrarily large POST in memory first.
1189
+ const validate = ` const text = await readCappedText(request, ${ASK_BODY_LIMIT_BYTES});
1190
+ if (text === undefined) {
1191
+ return new Response("Request too large: the body must be at most 64 KB.", {
1192
+ status: 413,
1193
+ });
1194
+ }
1195
+ let body;
1196
+ try {
1197
+ body = JSON.parse(text);
1198
+ } catch {
1199
+ body = null;
1200
+ }
1197
1201
  const raw = body?.messages;
1198
1202
  const valid =
1199
1203
  Array.isArray(raw) &&
@@ -1221,23 +1225,10 @@ export const askEndpointTemplate = (
1221
1225
  // `streamText` returns synchronously and defers provider/auth/network errors
1222
1226
  // to stream consumption, so the handler's try/catch never sees them: without
1223
1227
  // these the client gets a 200 whose stream aborts mid-flight and nothing is
1224
- // logged server-side. A missing credential is rejected up front as a real
1225
- // 500; everything else is at least logged via `onError`.
1226
- const keyCheck =
1227
- backend.kind === "gateway"
1228
- ? ` // The AI Gateway authenticates with an API key or Vercel's OIDC token.
1229
- if (!(getSecret("AI_GATEWAY_API_KEY") || getSecret("VERCEL_OIDC_TOKEN"))) {
1230
- return new Response(
1231
- "Ask AI is not configured: set AI_GATEWAY_API_KEY (or deploy on Vercel with OIDC).",
1232
- { status: 500 }
1233
- );
1234
- }`
1235
- : ` if (!getSecret(${JSON.stringify(backend.apiKeyEnv)})) {
1236
- return new Response(
1237
- ${JSON.stringify(`Ask AI is not configured: set ${backend.apiKeyEnv}.`)},
1238
- { status: 500 }
1239
- );
1240
- }`;
1228
+ // logged server-side. A missing credential is rejected up front with a 503
1229
+ // (the adapter's `keyCheck`); everything else is at least logged via
1230
+ // `onError`.
1231
+ const { keyCheck } = backend.template;
1241
1232
  // Provider errors surface mid-stream, after the 200 is committed; this is
1242
1233
  // the only place they can be observed server-side.
1243
1234
  const onError = ` onError({ error }) {
@@ -1245,16 +1236,16 @@ export const askEndpointTemplate = (
1245
1236
  },`;
1246
1237
  // The `streamText` argument list, built once so the grounded and plain
1247
1238
  // paths can't drift: they differ only in where the instructions come from.
1239
+ // The adapter appends its own call-level fields (its reasoning mapping when
1240
+ // that is a call option, and the verbatim `providerOptions`).
1248
1241
  const streamFields = [
1249
- `model: ${modelExpr}`,
1242
+ `model: ${backend.template.model}`,
1250
1243
  grounded
1251
1244
  ? "instructions"
1252
1245
  : `instructions:\n ${JSON.stringify(fallbackPrompt)}`,
1253
1246
  "messages",
1247
+ ...backend.template.fields,
1254
1248
  ];
1255
- if (reasoning && backend.kind !== "openrouter") {
1256
- streamFields.push(`reasoning: ${JSON.stringify(reasoning)}`);
1257
- }
1258
1249
  const call = ` const result = streamText({
1259
1250
  ${streamFields.join(",\n ")},
1260
1251
  ${onError}
@@ -1270,7 +1261,9 @@ ${validate}
1270
1261
  ${keyCheck}
1271
1262
  try {
1272
1263
  ${stream}
1273
- return result.toTextStreamResponse();
1264
+ return createTextStreamResponse({
1265
+ stream: toTextStream({ stream: result.stream }),
1266
+ });
1274
1267
  } catch {
1275
1268
  return new Response("Failed to generate a response.", { status: 500 });
1276
1269
  }
@@ -1366,47 +1359,18 @@ export const createSearch = () => create({ indexUrl${
1366
1359
  `;
1367
1360
 
1368
1361
  /**
1369
- * Public credential fields baked into a hosted provider's generated client
1370
- * (Algolia/Orama Cloud/Typesense config values from `search.*`, all plain
1371
- * strings or numbers; `JSON.stringify` drops the absent ones).
1362
+ * A client that passes the adapter's options — public credentials, by
1363
+ * contract — straight to the provider SDK. The options are inlined verbatim
1364
+ * as a literal: Blume maps only the fields it names, and any extra option
1365
+ * the adapter was given rides along untouched.
1372
1366
  */
1373
- type HostedSearchCredentials = Record<string, string | number | undefined>;
1374
-
1375
- /** A client that passes public credentials straight to the provider SDK. */
1376
1367
  const hostedSearchClient = (
1377
- module: string,
1378
- options: HostedSearchCredentials
1368
+ provider: Extract<ResolvedSearchAdapter, { mode: "hosted" }>
1379
1369
  ): string =>
1380
- `${SEARCH_CLIENT_HEADER}${searchClientImport(module)}
1381
- export const createSearch = () => create(${JSON.stringify(options)});
1370
+ `${SEARCH_CLIENT_HEADER}${searchClientImport(provider.kind)}
1371
+ export const createSearch = () => create(${JSON.stringify(provider.options)});
1382
1372
  `;
1383
1373
 
1384
- /** Build the per-provider config object the hosted client is created with. */
1385
- const hostedSearchOptions = (
1386
- search: ResolvedConfig["search"]
1387
- ): { module: string; options: HostedSearchCredentials } | null => {
1388
- switch (search.provider) {
1389
- case "algolia": {
1390
- return { module: "algolia", options: { ...search.algolia } };
1391
- }
1392
- case "orama-cloud": {
1393
- return {
1394
- module: "orama-cloud",
1395
- options: {
1396
- apiKey: search.oramaCloud?.apiKey,
1397
- endpoint: search.oramaCloud?.endpoint,
1398
- },
1399
- };
1400
- }
1401
- case "typesense": {
1402
- return { module: "typesense", options: { ...search.typesense } };
1403
- }
1404
- default: {
1405
- return null;
1406
- }
1407
- }
1408
- };
1409
-
1410
1374
  /**
1411
1375
  * Generate `.blume/src/generated/features.ts` — the client-feature loaders
1412
1376
  * behind the `blume:features` alias. Each loader is a dynamic import when the
@@ -1443,78 +1407,102 @@ export const loadEpub:
1443
1407
  * baked in here; secret keys never reach the client.
1444
1408
  */
1445
1409
  export const searchClientTemplate = (config: ResolvedConfig): string => {
1446
- const { search } = config;
1447
-
1448
- if (search.provider === "orama" || search.provider === "flexsearch") {
1449
- // Only Orama derives a tokenizer from the locale; FlexSearch has no
1450
- // equivalent hook, so its client keeps the bare index URL.
1451
- const locale =
1452
- search.provider === "orama" ? config.i18n?.defaultLocale : undefined;
1453
- return staticSearchClient(search.provider, locale);
1454
- }
1455
-
1456
- const hosted = hostedSearchOptions(search);
1457
- if (hosted) {
1458
- return hostedSearchClient(hosted.module, hosted.options);
1459
- }
1460
-
1461
- if (search.provider === "mixedbread") {
1462
- return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}
1410
+ const { provider } = config.search;
1411
+
1412
+ switch (provider.mode) {
1413
+ case "static": {
1414
+ // Only Orama derives a tokenizer from the locale; FlexSearch has no
1415
+ // equivalent hook, so its client keeps the bare index URL.
1416
+ const locale =
1417
+ provider.kind === "orama" ? config.i18n?.defaultLocale : undefined;
1418
+ return staticSearchClient(provider.kind, locale);
1419
+ }
1420
+ case "hosted": {
1421
+ return hostedSearchClient(provider);
1422
+ }
1423
+ case "server": {
1424
+ return `${SEARCH_CLIENT_HEADER}${searchClientImport("endpoint")}${SEARCH_BASE_IMPORT}
1463
1425
  const api = joinBase(import.meta.env.BASE_URL, "api/search");
1464
1426
 
1465
1427
  export const createSearch = () => create({ api });
1466
1428
  `;
1467
- }
1468
-
1469
- if (search.provider === "pagefind") {
1470
- return `${SEARCH_CLIENT_HEADER}${searchClientImport("pagefind")}${SEARCH_BASE_IMPORT}
1429
+ }
1430
+ case "pagefind": {
1431
+ return `${SEARCH_CLIENT_HEADER}${searchClientImport("pagefind")}${SEARCH_BASE_IMPORT}
1471
1432
  const url = joinBase(import.meta.env.BASE_URL, "pagefind/pagefind.js");
1472
1433
 
1473
1434
  export const createSearch = () => create({ url });
1474
1435
  `;
1475
- }
1476
-
1477
- // Search disabled: a no-op client so the alias always resolves.
1478
- return `${SEARCH_CLIENT_HEADER}export const createSearch = () => () =>
1436
+ }
1437
+ default: {
1438
+ // Search disabled: a no-op client so the alias always resolves.
1439
+ return `${SEARCH_CLIENT_HEADER}export const createSearch = () => () =>
1479
1440
  Promise.resolve({ hits: [], sections: [] });
1480
1441
  `;
1442
+ }
1443
+ }
1481
1444
  };
1482
1445
 
1483
1446
  /**
1484
1447
  * Generate the Mixedbread search endpoint (`/api/search`). It holds the secret
1485
1448
  * key server-side and proxies semantic queries to the configured store. The
1486
- * result mapping is best-effort and may need tuning to how your content was
1487
- * synced (see the Mixedbread sync step / \`mxbai vs sync\`).
1449
+ * adapter's options are inlined as a literal so the route never imports the
1450
+ * config. The result mapping is best-effort and may need tuning to how your
1451
+ * content was synced (see the Mixedbread sync step / \`mxbai vs sync\`).
1488
1452
  */
1489
- export const mixedbreadSearchEndpointTemplate = (storeId: string): string =>
1453
+ export const mixedbreadSearchEndpointTemplate = (
1454
+ options: MixedbreadOptions
1455
+ ): string =>
1490
1456
  `// Generated by Blume. Do not edit.
1491
1457
  import type { APIRoute } from "astro";
1492
1458
  import { getSecret } from "astro:env/server";
1493
1459
  import Mixedbread from "@mixedbread/sdk";
1460
+ import { readCappedText } from "blume/core/request-body.ts";
1494
1461
 
1495
1462
  export const prerender = false;
1496
1463
 
1497
1464
  const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" });
1498
- const STORE_ID = ${JSON.stringify(storeId)};
1465
+ const OPTIONS = ${JSON.stringify(options)};
1466
+ // Every option besides the store reaches the search call verbatim.
1467
+ const { storeId: STORE_ID, ...SEARCH_OPTIONS } = OPTIONS;
1499
1468
 
1500
1469
  export const POST: APIRoute = async ({ request }) => {
1470
+ // A search body is one short query: read it under a 16 KB cap, so a
1471
+ // self-hosted server never buffers an arbitrarily large POST first.
1472
+ const text = await readCappedText(request, 16_384);
1473
+ if (text === undefined) {
1474
+ return new Response("Request too large: the body must be at most 16 KB.", {
1475
+ status: 413,
1476
+ });
1477
+ }
1501
1478
  // The endpoint is public: a malformed body must 200-empty, not 500.
1502
- const body = await request.json().catch(() => null);
1479
+ let body = null;
1480
+ try {
1481
+ body = JSON.parse(text);
1482
+ } catch {
1483
+ body = null;
1484
+ }
1503
1485
  const query = body?.query;
1504
1486
  if (!query || typeof query !== "string") {
1505
1487
  return new Response("[]", {
1506
1488
  headers: { "Content-Type": "application/json" },
1507
1489
  });
1508
1490
  }
1491
+ // \`top_k\` defaults to 8 unless an option sets it; the query and store
1492
+ // always come from the request and \`storeId\`.
1509
1493
  const response = await client.stores.search({
1494
+ top_k: 8,
1495
+ ...SEARCH_OPTIONS,
1510
1496
  query,
1511
1497
  store_identifiers: [STORE_ID],
1512
- top_k: 8,
1513
1498
  });
1514
1499
  const hits = (response.data ?? []).map((chunk) => {
1515
1500
  const meta = chunk.generated_metadata ?? {};
1501
+ // Only a text chunk carries \`text\`; image, audio, and video chunks fall
1502
+ // back to the excerpt the store generated for them.
1503
+ const text = "text" in chunk ? chunk.text : undefined;
1516
1504
  return {
1517
- excerpt: chunk.text ?? meta.excerpt ?? "",
1505
+ excerpt: text ?? meta.excerpt ?? "",
1518
1506
  title: meta.title ?? chunk.filename ?? "",
1519
1507
  url: meta.url ?? "",
1520
1508
  };
@@ -1661,9 +1649,15 @@ export const GET: APIRoute = async ({ params }) => {
1661
1649
  return new Response(null, { status: 404 });
1662
1650
  }
1663
1651
  const body = await readFile(path);
1664
- return new Response(new Uint8Array(body), {
1665
- headers: { "Content-Type": contentType(path) },
1666
- });
1652
+ const type = contentType(path);
1653
+ // 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.
1656
+ const headers: Record<string, string> =
1657
+ type === "image/svg+xml"
1658
+ ? { "Content-Security-Policy": ${JSON.stringify(SVG_ASSET_POLICY)}, "Content-Type": type }
1659
+ : { "Content-Type": type };
1660
+ return new Response(new Uint8Array(body), { headers });
1667
1661
  };
1668
1662
  `;
1669
1663
 
@@ -1693,7 +1687,8 @@ export const ALL: APIRoute = ({ request }) => handler(request);
1693
1687
  /**
1694
1688
  * Generate the playground's CORS proxy endpoint
1695
1689
  * (`.blume/src/blume-openapi/api-proxy.ts`), behind
1696
- * `openapi.playground.proxy: true`. A thin server-rendered wrapper around the
1690
+ * `playground: { proxy: true }` on an `openapi()` reference. A thin
1691
+ * server-rendered wrapper around the
1697
1692
  * shipped `createPlaygroundProxyHandler`; injected at `/_api-proxy` rather
1698
1693
  * than written under `pages/` because Astro treats `_`-prefixed page files as
1699
1694
  * private.
@@ -1793,7 +1788,9 @@ export function getStaticPaths() {
1793
1788
  return navVariants(data, hiddenDefaultLocale(data.config.i18n)).flatMap(
1794
1789
  ({ locale, navigation, version }) =>
1795
1790
  [...navGroupIds(navigation.sidebar)]
1796
- .filter(([node]) => (node.display ?? "flat") !== "flat")
1791
+ .filter(
1792
+ ([node]) => node.kind === "group" && (node.display ?? "flat") !== "flat"
1793
+ )
1797
1794
  .map(([, id]) => ({ params: { id, locale, version } }))
1798
1795
  );
1799
1796
  }
@@ -2067,8 +2064,7 @@ import LocaleLinks from "blume/components/layout/LocaleLinks.astro";
2067
2064
  import ApiOverview from "blume/components/openapi/ApiOverview.astro";
2068
2065
  import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
2069
2066
  import Operation from "blume/components/openapi/Operation.astro";
2070
- ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
2071
- import { islandComponents } from "../generated/islands.ts";`,
2067
+ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";`,
2072
2068
  map: `{
2073
2069
  Accordion,
2074
2070
  AccordionItem,
@@ -2104,8 +2100,7 @@ import { islandComponents } from "../generated/islands.ts";`,
2104
2100
  TypeTable,
2105
2101
  Visibility,
2106
2102
  YouTube,
2107
- ${mathEntry}...islandComponents,
2108
- ...userMdx,
2103
+ ${mathEntry}...userMdx,
2109
2104
  }`,
2110
2105
  };
2111
2106
  };
@@ -2164,6 +2159,7 @@ export function getStaticPaths() {
2164
2159
  indexable: route.indexable,
2165
2160
  lastModified: route.lastModified,
2166
2161
  locale: route.locale,
2162
+ monolingual: route.monolingual,
2167
2163
  route: route.path,
2168
2164
  title: route.title,
2169
2165
  version: route.version,
@@ -2172,7 +2168,7 @@ export function getStaticPaths() {
2172
2168
  }));
2173
2169
  }
2174
2170
 
2175
- const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback, version, versionAlternates } = Astro.props;
2171
+ const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback, monolingual, version, versionAlternates } = Astro.props;
2176
2172
  const entry = await getEntry(collection as CollectionKey, entryId);
2177
2173
  if (!entry) {
2178
2174
  return new Response(null, { status: 404 });
@@ -2264,13 +2260,32 @@ const contentDir = i18n
2264
2260
  // hreflang URLs byte-match the sitemap's <loc> for the home page.
2265
2261
  const absolute = (path: string) => base + withBase(path);
2266
2262
 
2263
+ // A fallback page renders the fallback locale's page at this locale's URL, so
2264
+ // its canonical names the page it copies, and search engines never rank the
2265
+ // copy against the original.
2266
+ const fallbackSource =
2267
+ fallback && i18n?.fallbackLocale
2268
+ ? (alternates ?? []).find((alt) => alt.locale === i18n.fallbackLocale)
2269
+ : undefined;
2270
+ // The page it copies, when that page is archived too, points on to the latest
2271
+ // docs; the copy names that final page directly, the fallback locale's
2272
+ // version of the latest one, so the canonical is never a chain.
2273
+ const fallbackLatest =
2274
+ fallbackSource && archived && archived.canonical === "latest" && latestVersionAlt
2275
+ ? (data.routes.find((entry) => entry.path === latestVersionAlt.path)?.alternates ?? []).find(
2276
+ (alt) => alt.locale === i18n?.fallbackLocale
2277
+ )
2278
+ : undefined;
2279
+ const fallbackTarget = fallbackLatest ?? fallbackSource;
2267
2280
  // An archived page defaults its canonical to the same page in the latest docs
2268
2281
  // when that page still exists — search engines treat the live page as
2269
2282
  // authoritative without deindexing version-only content. A page's own
2270
2283
  // \`seo.canonical\` always wins, and \`canonical: "self"\` keeps the default.
2271
2284
  const canonical =
2272
2285
  seo.canonical ??
2273
- (archived && archived.canonical === "latest" && latestVersionAlt && base
2286
+ (fallbackTarget && base
2287
+ ? absolute(fallbackTarget.path)
2288
+ : archived && archived.canonical === "latest" && latestVersionAlt && base
2274
2289
  ? absolute(latestVersionAlt.path)
2275
2290
  : base
2276
2291
  ? \`\${base}\${basedRoute === "/" ? "/" : encodeURI(basedRoute)}\`
@@ -2300,7 +2315,9 @@ const mountLocalized = (logical: string, codeArg: string) =>
2300
2315
  const logicalRoute = i18n
2301
2316
  ? stripLocale(stripBasePath(data.config.basePath, route), locale)
2302
2317
  : route;
2303
- const localeSwitch = i18n
2318
+ // A page from a one-language source (GitHub Releases) gets no switcher: every
2319
+ // other locale would only repeat the same text.
2320
+ const localeSwitch = i18n && !monolingual
2304
2321
  ? i18n.locales.map((l) => {
2305
2322
  const alt = (alternates ?? []).find((x) => x.locale === l.code);
2306
2323
  return {
@@ -2397,6 +2414,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2397
2414
  locale={htmlLang}
2398
2415
  dir={dir}
2399
2416
  contentDir={contentDir}
2417
+ contentLocale={contentLocale}
2400
2418
  ui={ui}
2401
2419
  localeAlternates={localeAlternates}
2402
2420
  xDefault={xDefault}
@@ -2446,14 +2464,17 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2446
2464
 
2447
2465
  /**
2448
2466
  * Generate `.blume/src/pages/changelog.astro` — the changelog index. Collects
2449
- * every `type: changelog` entry, sorts newest-first, and renders each through
2450
- * the `Update` timeline layout (date/version rail + entry content). Only written
2451
- * by {@link generateAstroProject} when changelog entries exist.
2467
+ * every `type: changelog` entry, sorts newest-first, and lists them grouped by
2468
+ * year: one row per release with its title (linked to the entry's own page),
2469
+ * its `category` tag, and its date. Bodies are deliberately not rendered — a
2470
+ * long-lived project's index otherwise grows past what an agent can read in
2471
+ * one context window (and what a reader will scroll), while every entry
2472
+ * already has a page of its own. Only written by
2473
+ * {@link generateAstroProject} when changelog entries exist.
2452
2474
  */
2453
2475
  export const changelogIndexTemplate = (options: {
2454
2476
  exportEpub: boolean;
2455
2477
  exportPdf: boolean;
2456
- mathEnabled: boolean;
2457
2478
  /** Serialize the island-hooks snapshot; only needed when React is enabled. */
2458
2479
  needsReact: boolean;
2459
2480
  /** Whether a `staged` collection exists (non-filesystem changelog sources). */
@@ -2467,40 +2488,29 @@ export const changelogIndexTemplate = (options: {
2467
2488
  const stagedSpread = options.staged
2468
2489
  ? '\n ...(await getCollection("staged")),'
2469
2490
  : "";
2470
- const { imports: componentImports, map: componentMap } =
2471
- contentComponentsSource(options.mathEnabled);
2472
2491
 
2473
2492
  return `---
2474
2493
  // Generated by Blume. Do not edit.
2475
- import { getCollection, render } from "astro:content";
2494
+ import { getCollection } from "astro:content";
2476
2495
  import RootLayout from "blume/components/layout/RootLayout.astro";
2477
- import Update from "blume/components/content/Update.astro";
2478
2496
  import { withBase } from "blume/components/islands/base-path.ts";
2479
2497
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2480
2498
  import { resolveDateFormatOptions } from "blume/core/date-format.ts";
2481
- ${componentImports}
2499
+ import { layoutOverrides } from "../generated/components.ts";
2482
2500
  import data from "blume:data";
2483
2501
 
2484
- const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
2485
- const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
2486
-
2487
2502
  export const prerender = true;
2488
2503
 
2489
- // Entry bodies are MDX rendered outside the catch-all, so they need the same
2490
- // component map: a \`:::\` callout or \`<Steps>\` in a release note otherwise
2491
- // throws "Expected component ... to be defined" at build time.
2492
- const components = ${componentMap};
2493
-
2494
2504
  const entryDate = (entry: {
2495
2505
  data: { date?: string | null; changelog?: { date?: string | null } | null };
2496
2506
  }) => entry.data.date ?? entry.data.changelog?.date ?? null;
2497
2507
 
2498
- const toTime = (value: string | null | undefined) => {
2508
+ const parseDate = (value: string | null | undefined) => {
2499
2509
  if (!value) {
2500
- return 0;
2510
+ return null;
2501
2511
  }
2502
2512
  const date = new Date(value);
2503
- return Number.isNaN(date.getTime()) ? 0 : date.getTime();
2513
+ return Number.isNaN(date.getTime()) ? null : date;
2504
2514
  };
2505
2515
 
2506
2516
  // The changelog is an unlocalized route, so its chrome renders in the default
@@ -2515,34 +2525,45 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
2515
2525
 
2516
2526
  // Formatted in the same locale as the chrome, and with the configured
2517
2527
  // \`dateFormat\` (UTC by default), to match the per-page "last updated" stamp.
2528
+ // The year heads each group, so a row shows the rest of the date: a preset
2529
+ // style keeps its month wording, a component format simply drops the year.
2518
2530
  const dateFormatOptions = resolveDateFormatOptions(data.config.dateFormat);
2519
- const formatDate = (value: string | null | undefined) => {
2520
- if (!value) {
2521
- return;
2522
- }
2523
- const date = new Date(value);
2524
- return Number.isNaN(date.getTime())
2525
- ? undefined
2526
- : new Intl.DateTimeFormat(htmlLang, dateFormatOptions).format(date);
2527
- };
2531
+ const { dateStyle, year: _year, ...dateComponents } = dateFormatOptions;
2532
+ const rowDateFormat: Intl.DateTimeFormatOptions = dateStyle
2533
+ ? {
2534
+ ...dateComponents,
2535
+ day: "numeric",
2536
+ month: dateStyle === "medium" ? "short" : "long",
2537
+ }
2538
+ : dateComponents;
2539
+ const formatRowDate = (date: Date) =>
2540
+ new Intl.DateTimeFormat(htmlLang, rowDateFormat).format(date);
2541
+ const formatYear = (date: Date) =>
2542
+ new Intl.DateTimeFormat(htmlLang, {
2543
+ timeZone: dateFormatOptions.timeZone,
2544
+ year: "numeric",
2545
+ }).format(date);
2528
2546
 
2529
2547
  const slugify = (text: string) =>
2530
2548
  text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
2531
2549
  "update";
2532
2550
 
2533
- // The major of a version's embedded semver (\`1.2.3\` -> 1, \`pkg@2.0.0\` -> 2), or
2534
- // null when there is no full major.minor.patch to key on. Drives the changelog's
2535
- // group-by-major pagination, so it tolerates the scoped tags monorepos publish.
2536
- const majorVersion = (version: string | null | undefined) => {
2537
- const match = /(\\d+)\\.\\d+\\.\\d+/.exec(String(version ?? ""));
2538
- return match ? Number(match[1]) : null;
2539
- };
2540
-
2541
- // Map each entry to its own generated page so the timeline heading can deep-link
2542
- // to it. The collection entry id matches the route manifest's \`entryId\`.
2543
- const routeByEntry = new Map(
2544
- data.routes.map((route) => [route.entryId, route.path])
2545
- );
2551
+ // Map each entry to its own generated page so the row can link to it. The
2552
+ // collection entry id matches the route manifest's \`entryId\` — and under
2553
+ // i18n every locale serves a page for it (its translations plus fallback
2554
+ // copies of an untranslated one), all sharing that id, so a map over every
2555
+ // route would keep whichever locale came last: a Portuguese permalink on an
2556
+ // unlocalized index. Only the default locale's routes are kept, and an entry
2557
+ // is listed only when it has one, so a translated changelog file (a distinct
2558
+ // entry that lives under its own locale) does not add a second row for the
2559
+ // same release.
2560
+ const defaultLocale = i18n ? i18n.defaultLocale : null;
2561
+ const routeByEntry = new Map<string, string>();
2562
+ for (const route of data.routes) {
2563
+ if (defaultLocale === null || route.locale === defaultLocale) {
2564
+ routeByEntry.set(route.entryId, route.path);
2565
+ }
2566
+ }
2546
2567
 
2547
2568
  const changelogEntries = [
2548
2569
  ...(await getCollection("docs")),${stagedSpread}
@@ -2551,35 +2572,37 @@ const changelogEntries = [
2551
2572
  (entry) =>
2552
2573
  entry.data.type === "changelog" &&
2553
2574
  !entry.data.draft &&
2554
- !entry.data.sidebar?.hidden
2575
+ !entry.data.sidebar?.hidden &&
2576
+ (defaultLocale === null || routeByEntry.has(entry.id))
2555
2577
  )
2556
- .toSorted((a, b) => toTime(entryDate(b)) - toTime(entryDate(a)));
2557
-
2558
- const items = await Promise.all(
2559
- changelogEntries.map(async (entry) => {
2560
- const label =
2561
- entry.data.title ??
2562
- (entry.data.changelog?.version
2563
- ? "v" + entry.data.changelog.version
2564
- : "Update");
2565
- return {
2566
- Content: (await render(entry)).Content,
2567
- date: formatDate(entryDate(entry)),
2568
- href: routeByEntry.get(entry.id) ?? undefined,
2569
- id: slugify(label),
2570
- label,
2571
- major: majorVersion(entry.data.changelog?.version),
2572
- tags: entry.data.changelog?.category
2573
- ? [entry.data.changelog.category]
2574
- : [],
2575
- };
2576
- })
2577
- );
2578
+ .toSorted(
2579
+ (a, b) =>
2580
+ (parseDate(entryDate(b))?.getTime() ?? 0) -
2581
+ (parseDate(entryDate(a))?.getTime() ?? 0)
2582
+ );
2583
+
2584
+ const items = changelogEntries.map((entry) => {
2585
+ const label =
2586
+ entry.data.title ??
2587
+ (entry.data.changelog?.version
2588
+ ? "v" + entry.data.changelog.version
2589
+ : "Update");
2590
+ const date = parseDate(entryDate(entry));
2591
+ const route = routeByEntry.get(entry.id);
2592
+ return {
2593
+ date: date ? formatRowDate(date) : null,
2594
+ dateTime: date ? date.toISOString().slice(0, 10) : null,
2595
+ href: route ? withBase(route) : null,
2596
+ id: slugify(label),
2597
+ label,
2598
+ tag: entry.data.changelog?.category ?? null,
2599
+ year: date ? formatYear(date) : null,
2600
+ };
2601
+ });
2578
2602
 
2579
2603
  // Repeated labels slug to the same id (e.g. two entries with neither a title
2580
2604
  // nor a version both falling back to "update"); suffix the later ones -2, -3,
2581
- // ... so every heading deep-links to its own entry. The first keeps the plain
2582
- // slug, and the rendered ids stay in lockstep with the \`headings\` list below.
2605
+ // ... so every row keeps its own hash anchor. The first keeps the plain slug.
2583
2606
  const seenIds = new Set();
2584
2607
  for (const item of items) {
2585
2608
  let uniqueId = item.id;
@@ -2590,26 +2613,18 @@ for (const item of items) {
2590
2613
  item.id = uniqueId;
2591
2614
  }
2592
2615
 
2593
- // A changelog is semver-paginated only when every visible release parses as
2594
- // semver and they span more than one major line. Older majors then collapse
2595
- // into groups the reader reveals one at a time; otherwise the timeline is flat.
2596
- const majors = items.every((item) => item.major !== null)
2597
- ? [...new Set(items.map((item) => item.major))]
2598
- .filter((major): major is number => major !== null)
2599
- .toSorted((a, b) => b - a)
2600
- : [];
2601
- const paginate = majors.length > 1;
2602
- const majorGroups = majors.map((major) => ({
2603
- items: items.filter((item) => item.major === major),
2604
- label: major + ".x",
2605
- major,
2606
- }));
2607
-
2608
- const headings = items.map((item) => ({
2609
- depth: 2,
2610
- slug: item.id,
2611
- text: item.label,
2612
- }));
2616
+ // Consecutive releases from the same year share a group, so the list reads as
2617
+ // a year rail beside the rows; undated entries (sorted last) form a final
2618
+ // group with no year label.
2619
+ const groups: { items: typeof items; year: string | null }[] = [];
2620
+ for (const item of items) {
2621
+ const last = groups[groups.length - 1];
2622
+ if (last && last.year === item.year) {
2623
+ last.items.push(item);
2624
+ } else {
2625
+ groups.push({ items: [item], year: item.year });
2626
+ }
2627
+ }
2613
2628
 
2614
2629
  const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
2615
2630
  // The canonical URL carries the deployment base (the page is served under it),
@@ -2623,9 +2638,9 @@ const canonical = base ? base + basedRoute : null;
2623
2638
  const ogPath = data.config.og.enabled ? withBase("/og/changelog.png") : null;
2624
2639
  const ogImage = ogPath && base ? base + ogPath : ogPath;
2625
2640
 
2626
- // The page chrome (h1, title, description) comes from the same translatable
2627
- // \`changelog\` group as the reveal button; optional chaining tolerates a
2628
- // not-yet-regenerated data snapshot from before these keys existed.
2641
+ // The page chrome (h1, title, description) comes from the translatable
2642
+ // \`changelog\` group; optional chaining tolerates a not-yet-regenerated data
2643
+ // snapshot from before these keys existed.
2629
2644
  const changelogTitle = data.ui.changelog?.title ?? "Changelog";
2630
2645
  const changelogDescription =
2631
2646
  data.ui.changelog?.description ??
@@ -2658,7 +2673,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2658
2673
  description: changelogDescription,
2659
2674
  route: "/changelog",
2660
2675
  }}
2661
- headings={headings}
2676
+ headings={[]}
2662
2677
  toc={data.config.toc}
2663
2678
  contentLayout="bare"
2664
2679
  themeMode={data.config.theme.mode}
@@ -2679,56 +2694,52 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2679
2694
  structuredDataEnabled={data.config.structuredData}
2680
2695
  >
2681
2696
  <h1>{changelogTitle}</h1>
2697
+ <p class="text-lg text-muted-foreground">{changelogDescription}</p>
2682
2698
  {
2683
2699
  items.length === 0 ? (
2684
2700
  <p>No changelog entries yet.</p>
2685
- ) : paginate ? (
2686
- <blume-changelog
2687
- class="not-prose mt-8 block"
2688
- data-i18n-more={data.ui.changelog?.showReleases}
2689
- >
2690
- {majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
2691
- <Update description={date} href={href} id={id} label={label} tags={tags}>
2692
- <Content components={components} />
2693
- </Update>
2694
- ))}
2695
- {majorGroups.slice(1).map((group) => (
2701
+ ) : (
2702
+ <div class="not-prose mt-10 divide-y divide-border border-border border-y">
2703
+ {groups.map((group) => (
2696
2704
  <section
2697
- aria-label={group.label + " releases"}
2698
- data-changelog-label={group.label}
2699
- data-changelog-major={group.major}
2705
+ aria-label={group.year ?? undefined}
2706
+ class="grid md:grid-cols-[6rem_minmax(0,1fr)] md:gap-x-8"
2700
2707
  >
2701
- {group.items.map(({ Content, href, id, label, date, tags }) => (
2702
- <Update description={date} href={href} id={id} label={label} tags={tags}>
2703
- <Content components={components} />
2704
- </Update>
2705
- ))}
2708
+ <h2 class="mt-0! pt-3! font-medium! text-muted-foreground text-sm! leading-5! tabular-nums max-md:pb-1">
2709
+ {group.year}
2710
+ </h2>
2711
+ <ul class="m-0! list-none divide-y divide-border p-0!">
2712
+ {group.items.map((item) => (
2713
+ <li class="m-0! p-0!" id={item.id}>
2714
+ <a
2715
+ class="group/release flex items-baseline gap-4 py-3 no-underline! hover:no-underline!"
2716
+ href={item.href ?? "#" + item.id}
2717
+ >
2718
+ <span class="min-w-0 flex-1 font-medium text-foreground text-sm transition-colors group-hover/release:text-accent">
2719
+ {item.label}
2720
+ </span>
2721
+ {item.tag && (
2722
+ <span class="shrink-0 rounded-full bg-muted px-2 py-0.5 font-medium text-[0.65rem] text-muted-foreground">
2723
+ {item.tag}
2724
+ </span>
2725
+ )}
2726
+ {item.date && (
2727
+ <time
2728
+ class="shrink-0 font-mono text-muted-foreground text-xs tabular-nums"
2729
+ datetime={item.dateTime}
2730
+ >
2731
+ {item.date}
2732
+ </time>
2733
+ )}
2734
+ </a>
2735
+ </li>
2736
+ ))}
2737
+ </ul>
2706
2738
  </section>
2707
2739
  ))}
2708
- <div class="mt-10 flex justify-center">
2709
- <button
2710
- class="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground"
2711
- data-changelog-more
2712
- hidden
2713
- type="button"
2714
- >
2715
- Show older releases
2716
- </button>
2717
- </div>
2718
- </blume-changelog>
2719
- ) : (
2720
- <div class="not-prose mt-8">
2721
- {items.map(({ Content, href, id, label, date, tags }) => (
2722
- <Update description={date} href={href} id={id} label={label} tags={tags}>
2723
- <Content components={components} />
2724
- </Update>
2725
- ))}
2726
2740
  </div>
2727
2741
  )
2728
2742
  }
2729
- <script>
2730
- import "blume/components/content/changelog-element.ts";
2731
- </script>
2732
2743
  </LayoutComponent>
2733
2744
  `;
2734
2745
  };
@@ -2766,7 +2777,7 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
2766
2777
  // Recovery links, so a reader — or an agent that followed a stale URL — can
2767
2778
  // get back on track without guessing: every top-level section, then the
2768
2779
  // machine-readable indexes the build emits (the sitemap only exists with a
2769
- // \`deployment.site\`; llms.txt only when \`ai.llmsTxt\` is on). Tabs link to
2780
+ // \`deployment.site\`; llms.txt only when \`agents.llmsTxt\` is on). Tabs link to
2770
2781
  // their resolved target when the section has no index page of its own.
2771
2782
  const suggestions = [
2772
2783
  ...data.navigation.tabs.map((tab) => ({
@@ -2780,6 +2791,31 @@ const suggestions = [
2780
2791
  ? [{ href: withBase("/llms.txt"), label: nf.llms }]
2781
2792
  : []),
2782
2793
  ];
2794
+
2795
+ // Every host serves this one page for any missing URL, so a reader who
2796
+ // mistyped \`/ar/…\` would get the default locale's message. The other locales'
2797
+ // 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).
2800
+ const localized = Object.fromEntries(
2801
+ (i18n?.locales ?? [])
2802
+ .filter((l) => l.code !== i18n?.defaultLocale && data.uiByLocale[l.code])
2803
+ .map((l) => {
2804
+ const strings = data.uiByLocale[l.code].notFound;
2805
+ return [
2806
+ l.code,
2807
+ {
2808
+ description: strings.description,
2809
+ dir: l.dir ?? "ltr",
2810
+ home: strings.home,
2811
+ homeHref: withBase("/" + l.code),
2812
+ suggestions: strings.suggestions,
2813
+ title: strings.title,
2814
+ },
2815
+ ];
2816
+ })
2817
+ );
2818
+ const localizedJson = JSON.stringify(localized).replaceAll("<", "\\\\u003c");
2783
2819
  ---
2784
2820
 
2785
2821
  <PageLayout
@@ -2801,6 +2837,8 @@ const suggestions = [
2801
2837
  >
2802
2838
  <div
2803
2839
  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("/")}
2841
+ data-blume-not-found
2804
2842
  >
2805
2843
  <div class="flex flex-col items-start">
2806
2844
  <p
@@ -2810,24 +2848,26 @@ const suggestions = [
2810
2848
  </p>
2811
2849
  <h1
2812
2850
  class="mt-4 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl"
2851
+ data-nf="title"
2813
2852
  >
2814
2853
  {nf.title}
2815
2854
  </h1>
2816
- <p class="mt-4 max-w-md text-pretty text-lg text-muted-foreground">
2855
+ <p class="mt-4 max-w-md text-pretty text-lg text-muted-foreground" data-nf="description">
2817
2856
  {nf.description}
2818
2857
  </p>
2819
2858
  <a
2820
2859
  class="mt-8 inline-flex items-center gap-1.5 rounded-full bg-accent py-2 pe-4 ps-3.5 text-sm font-medium text-accent-foreground transition-opacity hover:opacity-90"
2860
+ data-nf-home
2821
2861
  href={withBase("/")}
2822
2862
  >
2823
2863
  <Icon class="rtl:-scale-x-100" name="arrow-left" size={14} />
2824
- {nf.home}
2864
+ <span data-nf="home">{nf.home}</span>
2825
2865
  </a>
2826
2866
  </div>
2827
2867
  {
2828
2868
  suggestions.length > 0 && (
2829
- <nav aria-label={nf.suggestions} class="md:border-s md:border-border md:ps-12 lg:ps-16">
2830
- <h2 class="text-xs font-medium uppercase tracking-widest text-muted-foreground">
2869
+ <nav aria-label={nf.suggestions} class="md:border-s md:border-border md:ps-12 lg:ps-16" data-nf-suggestions>
2870
+ <h2 class="text-xs font-medium uppercase tracking-widest text-muted-foreground" data-nf="suggestions">
2831
2871
  {nf.suggestions}
2832
2872
  </h2>
2833
2873
  <ul class="mt-4 divide-y divide-border border-y border-border">
@@ -2851,6 +2891,37 @@ const suggestions = [
2851
2891
  )
2852
2892
  }
2853
2893
  </div>
2894
+ <script id="blume-not-found-locales" is:inline type="application/json" set:html={localizedJson} />
2895
+ <script is:inline>
2896
+ (() => {
2897
+ const apply = () => {
2898
+ const root = document.querySelector("[data-blume-not-found]");
2899
+ const source = document.getElementById("blume-not-found-locales");
2900
+ if (!root || !source) {
2901
+ return;
2902
+ }
2903
+ const base = root.getAttribute("data-base") || "/";
2904
+ const path = location.pathname.startsWith(base)
2905
+ ? location.pathname.slice(base.length)
2906
+ : location.pathname.replace(/^\\/+/, "");
2907
+ const code = path.split("/")[0];
2908
+ const entry = JSON.parse(source.textContent || "{}")[code];
2909
+ if (!entry) {
2910
+ return;
2911
+ }
2912
+ root.setAttribute("lang", code);
2913
+ root.setAttribute("dir", entry.dir);
2914
+ for (const el of root.querySelectorAll("[data-nf]")) {
2915
+ el.textContent = entry[el.getAttribute("data-nf")];
2916
+ }
2917
+ root.querySelector("[data-nf-home]")?.setAttribute("href", entry.homeHref);
2918
+ root.querySelector("[data-nf-suggestions]")?.setAttribute("aria-label", entry.suggestions);
2919
+ document.title = entry.title;
2920
+ };
2921
+ apply();
2922
+ document.addEventListener("astro:page-load", apply);
2923
+ })();
2924
+ </script>
2854
2925
  </PageLayout>
2855
2926
  `;
2856
2927
 
@@ -2859,7 +2930,9 @@ const suggestions = [
2859
2930
  * page, prerendered to `dist/404.md`. An agent that asked for a missing page
2860
2931
  * with `Accept: text/markdown` — or fetched a `.md` URL no page backs — gets
2861
2932
  * this body with the 404 status instead of the HTML shell; Vercel server
2862
- * builds wire that into the routing config (`deploy/vercel-negotiation.ts`).
2933
+ * builds wire that into the routing config (`deploy/vercel-negotiation.ts`)
2934
+ * and Cloudflare server builds into the wrapper Worker
2935
+ * (`deploy/cloudflare-negotiation.ts`).
2863
2936
  * Same recovery links as the HTML page, absolute when the site URL is known:
2864
2937
  * the body is read out of context, so a relative link would leave the reader
2865
2938
  * guessing the host. Written alongside `404.astro` and skipped under the same
@@ -2929,8 +3002,8 @@ export function GET() {
2929
3002
  * Generate `.blume/src/pages/404.json.ts`: the JSON twin of the default 404
2930
3003
  * page, prerendered to `dist/404.json` as RFC 9457 problem details. An agent
2931
3004
  * that asked for a missing page with `Accept: application/json` gets this body
2932
- * with the 404 status instead of the HTML shell (Vercel server builds wire
2933
- * that into the routing config, like the Markdown twin). Same recovery links
3005
+ * with the 404 status instead of the HTML shell (Vercel and Cloudflare server
3006
+ * builds wire that into the deploy, like the Markdown twin). Same recovery links
2934
3007
  * as the other variants, carried as `links` and spelled out in `resolution`.
2935
3008
  * Written alongside `404.astro` and skipped under the same rule.
2936
3009
  */
@@ -3079,71 +3152,6 @@ const context = ${JSON.stringify(context)};
3079
3152
  export const ALL: APIRoute = ({ request }) => apiNotFoundResponse(request, context);
3080
3153
  `;
3081
3154
 
3082
- /** The literal Astro hydration directive for an island's client mode. */
3083
- const islandDirective = (spec: IslandSpec): string =>
3084
- spec.client === "only"
3085
- ? `client:only="${spec.framework}"`
3086
- : `client:${spec.client}`;
3087
-
3088
- /**
3089
- * Frontmatter `Props` alias mirroring the wrapped component's own props, so
3090
- * `{...Astro.props}` satisfies required props under `astro check` (the spread
3091
- * of an untyped `Astro.props` contributes nothing to the JSX props type).
3092
- * `infer P extends object` rather than `Record<string, unknown>` because
3093
- * interfaces have no implicit index signature and would miss the narrower
3094
- * constraint. Non-function component types (Vue/Svelte ambient modules) fall
3095
- * back to an open record, keeping the untyped permissiveness they had.
3096
- */
3097
- const wrapperPropsType = (name: string): string =>
3098
- `type Props = typeof ${name} extends (
3099
- props: infer P extends object,
3100
- ...rest: never[]
3101
- ) => unknown
3102
- ? P
3103
- : Record<string, unknown>;`;
3104
-
3105
- /**
3106
- * Generate `.blume/src/generated/islands/<Name>.astro` — a wrapper that renders
3107
- * a convention island with its hydration directive applied. Astro client
3108
- * directives must be written statically, so one wrapper is emitted per island;
3109
- * props and the default slot (MDX children) forward through.
3110
- */
3111
- export const islandWrapperTemplate = (spec: IslandSpec): string =>
3112
- `---
3113
- // Generated by Blume. Do not edit.
3114
- import Island from ${JSON.stringify(spec.file)};
3115
- ${wrapperPropsType("Island")}
3116
- ---
3117
- <Island ${islandDirective(spec)} {...Astro.props}><slot /></Island>
3118
- `;
3119
-
3120
- /**
3121
- * Generate `.blume/src/generated/islands.ts` — the map of island names to their
3122
- * wrappers, spread into the MDX component scope by the catch-all page. Always
3123
- * written (an empty map when there are no islands) so the import resolves.
3124
- */
3125
- export const islandMapTemplate = (specs: IslandSpec[]): string => {
3126
- if (specs.length === 0) {
3127
- return `// Generated by Blume. Do not edit.
3128
- export const islandComponents = {};
3129
- `;
3130
- }
3131
- const imports = specs
3132
- .map(
3133
- (spec, index) => `import I${index} from "./islands/${spec.name}.astro";`
3134
- )
3135
- .join("\n");
3136
- const entries = specs
3137
- .map((spec, index) => ` ${spec.name}: I${index},`)
3138
- .join("\n");
3139
- return `// Generated by Blume. Do not edit.
3140
- ${imports}
3141
- export const islandComponents = {
3142
- ${entries}
3143
- };
3144
- `;
3145
- };
3146
-
3147
3155
  /** The literal Astro hydration directive for an example's framework/client. */
3148
3156
  const exampleDirective = (spec: ExampleSpec): string => {
3149
3157
  if (spec.framework === "astro" || !spec.client) {
@@ -3169,12 +3177,17 @@ export const exampleSlug = (path: string): string =>
3169
3177
  /**
3170
3178
  * Generate `.blume/src/generated/examples/<slug>.astro` — a wrapper that renders
3171
3179
  * one example live, with its hydration directive applied (none for `.astro`).
3172
- * Mirrors {@link islandWrapperTemplate}; `<Component>` resolves these by path.
3180
+ * Mirrors the component-slot wrappers (`component-slots.ts`); `<Component>`
3181
+ * resolves these by path.
3173
3182
  */
3174
- export const exampleWrapperTemplate = (spec: ExampleSpec): string =>
3183
+ export const exampleWrapperTemplate = (
3184
+ spec: ExampleSpec,
3185
+ /** The wrapper's directory, when its import must be relative (eject). */
3186
+ fromDir?: string
3187
+ ): string =>
3175
3188
  `---
3176
3189
  // Generated by Blume. Do not edit.
3177
- import Example from ${JSON.stringify(spec.file)};
3190
+ import Example from ${JSON.stringify(importSpecifier(spec.file, fromDir))};
3178
3191
  ${wrapperPropsType("Example")}
3179
3192
  ---
3180
3193
  <Example ${exampleDirective(spec)}{...Astro.props}><slot /></Example>