blume 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli/{chunk-vtk4a6dg.js → chunk-00gs3wqs.js} +1 -1
  3. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  4. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  5. package/dist/cli/{chunk-yw7dm696.js → chunk-3yce002v.js} +21 -8
  6. package/dist/cli/{chunk-yw7dm696.js.map → chunk-3yce002v.js.map} +3 -3
  7. package/dist/cli/{chunk-yt5n7ppj.js → chunk-4e9b9ra6.js} +17 -7
  8. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  9. package/dist/cli/{chunk-fs23ddbb.js → chunk-5r8g91qn.js} +352 -676
  10. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  11. package/dist/cli/{chunk-6vm74dry.js → chunk-6dtt0zfn.js} +8 -8
  12. package/dist/cli/{chunk-qwsrynx5.js → chunk-6k4ftwze.js} +38 -19
  13. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-6k4ftwze.js.map} +4 -4
  14. package/dist/cli/{chunk-qs4q5p4e.js → chunk-7mbqtmgb.js} +16 -7
  15. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  16. package/dist/cli/{chunk-q5163e60.js → chunk-7vtckvaw.js} +21 -19
  17. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  18. package/dist/cli/{chunk-kdp5q7ke.js → chunk-8g8ytmgx.js} +17 -18
  19. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-8g8ytmgx.js.map} +2 -2
  20. package/dist/cli/{chunk-epjnccmv.js → chunk-91ws1n6j.js} +18 -15
  21. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  22. package/dist/cli/{chunk-fxypxtvm.js → chunk-bbnwccaz.js} +2 -2
  23. package/dist/cli/{chunk-m3vmjgmq.js → chunk-bfwp9vp6.js} +16 -8
  24. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  25. package/dist/cli/{chunk-f2z5v128.js → chunk-d1v5rhy0.js} +14 -15
  26. package/dist/cli/{chunk-f2z5v128.js.map → chunk-d1v5rhy0.js.map} +2 -2
  27. package/dist/cli/{chunk-6crbhc3x.js → chunk-ddndchfr.js} +21 -6
  28. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  29. package/dist/cli/{chunk-ce574jw2.js → chunk-esh98wmb.js} +1 -1
  30. package/dist/cli/{chunk-zxcczpyx.js → chunk-fsmrqk8a.js} +1 -1
  31. package/dist/cli/{chunk-hdpx1tax.js → chunk-g698a744.js} +5 -5
  32. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  33. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  34. package/dist/cli/{chunk-zxh4d9vy.js → chunk-h2ez8dzb.js} +4 -4
  35. package/dist/cli/{chunk-5shv93fd.js → chunk-h7k3nq3v.js} +2 -2
  36. package/dist/cli/{chunk-79jhk4py.js → chunk-hqp2ajnh.js} +251 -122
  37. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  38. package/dist/cli/{chunk-2hn4b8z7.js → chunk-hr8ne106.js} +109 -40
  39. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  40. package/dist/cli/{chunk-6hsn950k.js → chunk-j85scx15.js} +62 -18
  41. package/dist/cli/chunk-j85scx15.js.map +10 -0
  42. package/dist/cli/{chunk-s1p84fyh.js → chunk-k7pj68a8.js} +63 -22
  43. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  44. package/dist/cli/{chunk-ah61y8py.js → chunk-mqc662a6.js} +2 -2
  45. package/dist/cli/{chunk-ch6g3ar0.js → chunk-n1yg3tj3.js} +4 -4
  46. package/dist/cli/{chunk-mb2919y2.js → chunk-nfcyttvj.js} +17 -6
  47. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  48. package/dist/cli/{chunk-kpf8rrjc.js → chunk-pbg5a4s3.js} +56 -24
  49. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  50. package/dist/cli/{chunk-jwyddg7y.js → chunk-ppzjqwx2.js} +21 -14
  51. package/dist/cli/{chunk-jwyddg7y.js.map → chunk-ppzjqwx2.js.map} +4 -4
  52. package/dist/cli/{chunk-dh8cwk36.js → chunk-qkb5a8sa.js} +24 -9
  53. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  54. package/dist/cli/{chunk-wm7js3j9.js → chunk-sqw4ekg1.js} +2 -2
  55. package/dist/cli/{chunk-qkqwkpte.js → chunk-v6ya5kcb.js} +3083 -953
  56. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  57. package/dist/cli/{chunk-fz5wtpmh.js → chunk-w4bxdvsa.js} +18 -15
  58. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  59. package/dist/cli/{chunk-27g6wdth.js → chunk-wdrt2k2v.js} +2 -2
  60. package/dist/cli/{chunk-wgm7m9qk.js → chunk-xh43dwgw.js} +158 -43
  61. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  62. package/dist/cli/{chunk-s6jhgk0q.js → chunk-zp79m0ts.js} +2 -2
  63. package/dist/cli/index.js +160 -35
  64. package/dist/cli/index.js.map +4 -4
  65. package/dist/types/ai/agent-surface.d.ts +32 -0
  66. package/dist/types/ai/api-catalog.d.ts +7 -1
  67. package/dist/types/ai/ask-context.d.ts +7 -0
  68. package/dist/types/ai/component-markdown.d.ts +4 -4
  69. package/dist/types/ai/relative-links.d.ts +9 -4
  70. package/dist/types/ai/static-expression.d.ts +28 -0
  71. package/dist/types/analytics/databuddy.d.ts +43 -0
  72. package/dist/types/analytics/index.d.ts +2 -0
  73. package/dist/types/analytics/schema.d.ts +14 -0
  74. package/dist/types/cli/env.d.ts +5 -0
  75. package/dist/types/cli/init/scaffold.d.ts +19 -3
  76. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  77. package/dist/types/core/base-path.d.ts +9 -0
  78. package/dist/types/core/config-input.d.ts +4 -4
  79. package/dist/types/core/config.d.ts +2 -2
  80. package/dist/types/core/graph.d.ts +2 -0
  81. package/dist/types/core/i18n-ui.d.ts +31 -0
  82. package/dist/types/core/i18n.d.ts +7 -1
  83. package/dist/types/core/links.d.ts +3 -1
  84. package/dist/types/core/load-module.d.ts +10 -0
  85. package/dist/types/core/locale-links.d.ts +12 -2
  86. package/dist/types/core/meta.d.ts +5 -1
  87. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  88. package/dist/types/core/navigation.d.ts +38 -0
  89. package/dist/types/core/ordering-prefix.d.ts +4 -0
  90. package/dist/types/core/safe-links.d.ts +3 -1
  91. package/dist/types/core/schema.d.ts +52 -13
  92. package/dist/types/core/sources/github-releases.d.ts +5 -0
  93. package/dist/types/core/sources/lower.d.ts +14 -2
  94. package/dist/types/core/sources/normalize.d.ts +10 -1
  95. package/dist/types/core/sources/remote.d.ts +11 -1
  96. package/dist/types/core/sources/resolve.d.ts +12 -0
  97. package/dist/types/core/sources/types.d.ts +31 -0
  98. package/dist/types/core/types.d.ts +9 -0
  99. package/dist/types/deploy/adapters/node.d.ts +5 -2
  100. package/dist/types/deploy/adapters/types.d.ts +7 -0
  101. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  102. package/dist/types/deploy/headers.d.ts +31 -7
  103. package/dist/types/deploy/node-headers.d.ts +43 -8
  104. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  105. package/dist/types/deploy/platforms/node.d.ts +6 -5
  106. package/dist/types/deploy/platforms/types.d.ts +7 -0
  107. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  108. package/dist/types/deploy/redirects.d.ts +7 -2
  109. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  110. package/dist/types/openapi/model.d.ts +20 -6
  111. package/dist/types/search/sync/algolia.d.ts +3 -1
  112. package/docs/01-quickstart.mdx +3 -2
  113. package/docs/02-deployment.mdx +17 -8
  114. package/docs/08-faq.mdx +9 -2
  115. package/docs/advanced/changelog.mdx +1 -1
  116. package/docs/advanced/custom-pages.mdx +4 -2
  117. package/docs/cli/audit.mdx +2 -2
  118. package/docs/cli/doctor.mdx +2 -2
  119. package/docs/cli/evals.mdx +2 -2
  120. package/docs/cli/index.mdx +4 -1
  121. package/docs/cli/translate.mdx +1 -1
  122. package/docs/configuration/analytics.mdx +23 -2
  123. package/docs/configuration/assistant.mdx +1 -1
  124. package/docs/configuration/customization.mdx +4 -3
  125. package/docs/configuration/index.mdx +5 -3
  126. package/docs/configuration/search.mdx +1 -1
  127. package/docs/content/components.mdx +1 -1
  128. package/docs/content/frontmatter.mdx +5 -1
  129. package/docs/content/i18n.mdx +1 -1
  130. package/docs/content/index.mdx +4 -2
  131. package/docs/content/meta.mdx +5 -3
  132. package/docs/content/navigation.mdx +29 -4
  133. package/docs/content/sources.mdx +18 -12
  134. package/docs/content/versioning.mdx +1 -0
  135. package/docs/discoverability/agent-discovery.mdx +21 -7
  136. package/docs/discoverability/index.mdx +2 -2
  137. package/docs/discoverability/llms-txt.mdx +2 -5
  138. package/docs/discoverability/markdown.mdx +5 -3
  139. package/docs/discoverability/mcp.mdx +4 -0
  140. package/docs/discoverability/metadata.mdx +3 -2
  141. package/docs/discoverability/open-graph.mdx +6 -4
  142. package/docs/discoverability/rss.mdx +3 -1
  143. package/docs/references/asyncapi.mdx +1 -1
  144. package/docs/references/graphql.mdx +2 -2
  145. package/docs/references/openapi.mdx +3 -3
  146. package/package.json +1 -1
  147. package/skills/blume-migrate/SKILL.md +6 -6
  148. package/skills/blume-migrate/references/docusaurus.md +6 -6
  149. package/skills/blume-migrate/references/mintlify.md +3 -3
  150. package/skills/blume-migrate/references/monorepo.md +1 -1
  151. package/skills/blume-migrate/references/nextra.md +1 -1
  152. package/skills/blume-migrate/references/starlight.md +5 -5
  153. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  154. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  155. package/src/ai/agent-surface.ts +56 -0
  156. package/src/ai/api/spec.ts +15 -2
  157. package/src/ai/api-catalog.ts +7 -1
  158. package/src/ai/ask-context.ts +14 -2
  159. package/src/ai/ask-data.ts +26 -12
  160. package/src/ai/component-markdown.ts +43 -37
  161. package/src/ai/link-headers.ts +8 -3
  162. package/src/ai/llms.ts +4 -3
  163. package/src/ai/markdown.ts +23 -11
  164. package/src/ai/mcp/server.ts +78 -7
  165. package/src/ai/relative-links.ts +78 -10
  166. package/src/ai/static-expression.ts +416 -0
  167. package/src/ai/visibility.ts +45 -14
  168. package/src/analytics/databuddy.ts +67 -0
  169. package/src/analytics/head.ts +4 -0
  170. package/src/analytics/index.ts +2 -0
  171. package/src/analytics/posthog.ts +21 -3
  172. package/src/analytics/schema.ts +2 -0
  173. package/src/astro/generate.ts +71 -27
  174. package/src/astro/templates.ts +61 -23
  175. package/src/audit/catalog.ts +2 -2
  176. package/src/audit/checks/assets.ts +25 -5
  177. package/src/audit/checks/content.ts +20 -2
  178. package/src/audit/checks/i18n.ts +46 -8
  179. package/src/audit/checks/indexability.ts +39 -19
  180. package/src/audit/checks/links.ts +14 -0
  181. package/src/audit/checks/llms.ts +6 -3
  182. package/src/audit/checks/network.ts +3 -1
  183. package/src/audit/checks/og-image.ts +10 -0
  184. package/src/audit/checks/robots.ts +6 -1
  185. package/src/audit/checks/sitemap.ts +56 -31
  186. package/src/audit/checks/social.ts +21 -2
  187. package/src/audit/crawl.ts +88 -15
  188. package/src/audit/report.ts +54 -17
  189. package/src/audit/run.ts +1 -0
  190. package/src/audit/types.ts +18 -0
  191. package/src/audit/url.ts +37 -6
  192. package/src/cli/build-failure.ts +50 -0
  193. package/src/cli/commands/audit.ts +7 -0
  194. package/src/cli/commands/build.ts +19 -5
  195. package/src/cli/commands/check.ts +2 -0
  196. package/src/cli/commands/eval.ts +1 -7
  197. package/src/cli/commands/init.ts +15 -5
  198. package/src/cli/commands/preview.ts +15 -0
  199. package/src/cli/commands/sync.ts +2 -0
  200. package/src/cli/commands/translate.ts +9 -6
  201. package/src/cli/commands/upgrade.ts +11 -0
  202. package/src/cli/eject-scripts.ts +32 -7
  203. package/src/cli/env.ts +12 -1
  204. package/src/cli/init/scaffold.ts +117 -11
  205. package/src/cli/init/starter-spec.ts +235 -0
  206. package/src/components/content/AccordionItem.astro +26 -22
  207. package/src/components/content/Badge.astro +2 -9
  208. package/src/components/content/Card.astro +2 -2
  209. package/src/components/content/Component.astro +9 -3
  210. package/src/components/content/Expandable.astro +5 -1
  211. package/src/components/content/Frame.astro +2 -7
  212. package/src/components/content/GithubInfo.astro +12 -2
  213. package/src/components/content/Prompt.astro +2 -7
  214. package/src/components/content/Tabs.astro +54 -5
  215. package/src/components/content/Tile.astro +1 -1
  216. package/src/components/content/Tooltip.astro +69 -7
  217. package/src/components/content/Tree.astro +7 -2
  218. package/src/components/content/TypeTable.astro +10 -5
  219. package/src/components/content/Update.astro +8 -2
  220. package/src/components/content/auto-type-table.ts +4 -1
  221. package/src/components/content/badge-color.ts +17 -0
  222. package/src/components/content/base-href.ts +18 -3
  223. package/src/components/content/inline-markdown.ts +27 -7
  224. package/src/components/copy-feedback.ts +35 -8
  225. package/src/components/islands/assistant.tsx +16 -2
  226. package/src/components/islands/hooks.ts +7 -2
  227. package/src/components/islands/webmcp.ts +12 -8
  228. package/src/components/layout/Banner.astro +23 -4
  229. package/src/components/layout/Header.astro +22 -9
  230. package/src/components/layout/Logo.astro +5 -0
  231. package/src/components/layout/NavSelector.astro +6 -1
  232. package/src/components/layout/NavTabMenu.astro +133 -0
  233. package/src/components/layout/NavTree.astro +15 -6
  234. package/src/components/layout/NavTreeCache.astro +5 -2
  235. package/src/components/layout/NavTreeScript.astro +45 -5
  236. package/src/components/layout/PageLayout.astro +22 -8
  237. package/src/components/layout/ReferenceLayout.astro +4 -1
  238. package/src/components/layout/RootLayout.astro +21 -8
  239. package/src/components/layout/Search.astro +5 -1
  240. package/src/components/layout/analytics-client.ts +2 -0
  241. package/src/components/openapi/GraphqlType.astro +11 -3
  242. package/src/components/openapi/MessageComposer.astro +1 -1
  243. package/src/components/openapi/Operation.astro +21 -5
  244. package/src/components/openapi/PanelTabs.astro +4 -1
  245. package/src/components/openapi/Playground.astro +8 -2
  246. package/src/components/openapi/RequestPanel.astro +9 -5
  247. package/src/components/openapi/SchemaProperty.astro +11 -33
  248. package/src/components/openapi/SchemaTable.astro +27 -52
  249. package/src/components/openapi/helpers.ts +90 -12
  250. package/src/components/openapi/message-composer.ts +8 -0
  251. package/src/components/openapi/message-model.ts +12 -2
  252. package/src/components/openapi/message.ts +4 -1
  253. package/src/components/openapi/operation-model.ts +40 -8
  254. package/src/components/openapi/panel.ts +29 -5
  255. package/src/components/openapi/playground-client.ts +37 -7
  256. package/src/components/openapi/playground-schema.ts +25 -6
  257. package/src/components/openapi/request.ts +2 -0
  258. package/src/components/openapi/schema-tree.ts +203 -0
  259. package/src/components/openapi/snippets.ts +30 -15
  260. package/src/components/openapi/validate-json.ts +1 -1
  261. package/src/core/base-path.ts +15 -0
  262. package/src/core/config-input.ts +4 -4
  263. package/src/core/config.ts +18 -4
  264. package/src/core/diagnostics.ts +217 -30
  265. package/src/core/graph.ts +120 -11
  266. package/src/core/i18n-ui.ts +39 -2
  267. package/src/core/i18n.ts +13 -2
  268. package/src/core/links.ts +7 -4
  269. package/src/core/load-module.ts +20 -0
  270. package/src/core/locale-links.ts +17 -17
  271. package/src/core/manifest.ts +3 -2
  272. package/src/core/meta.ts +69 -6
  273. package/src/core/nav-diagnostics.ts +56 -1
  274. package/src/core/navigation.ts +245 -76
  275. package/src/core/ordering-prefix.ts +27 -0
  276. package/src/core/project-graph.ts +23 -1
  277. package/src/core/safe-href.ts +53 -1
  278. package/src/core/safe-links.ts +11 -2
  279. package/src/core/schema.ts +70 -14
  280. package/src/core/sources/assets.ts +83 -41
  281. package/src/core/sources/contentful-rich-text.ts +3 -2
  282. package/src/core/sources/contentful.ts +25 -12
  283. package/src/core/sources/filesystem.ts +5 -1
  284. package/src/core/sources/github-releases.ts +106 -5
  285. package/src/core/sources/lexical.ts +9 -4
  286. package/src/core/sources/lower.ts +79 -26
  287. package/src/core/sources/mdx-remote.ts +1 -0
  288. package/src/core/sources/normalize.ts +132 -30
  289. package/src/core/sources/notion.ts +26 -10
  290. package/src/core/sources/obsidian.ts +17 -3
  291. package/src/core/sources/payload.ts +1 -0
  292. package/src/core/sources/portable-text.ts +63 -44
  293. package/src/core/sources/remote.ts +18 -2
  294. package/src/core/sources/resolve.ts +52 -33
  295. package/src/core/sources/sanity.ts +1 -0
  296. package/src/core/sources/strapi-blocks.ts +9 -4
  297. package/src/core/sources/strapi.ts +1 -0
  298. package/src/core/sources/types.ts +31 -0
  299. package/src/core/types.ts +9 -0
  300. package/src/core/ui-packs/ar.ts +13 -0
  301. package/src/core/ui-packs/bg.ts +13 -0
  302. package/src/core/ui-packs/bn.ts +13 -0
  303. package/src/core/ui-packs/ca.ts +13 -0
  304. package/src/core/ui-packs/cs.ts +13 -0
  305. package/src/core/ui-packs/da.ts +13 -0
  306. package/src/core/ui-packs/de.ts +13 -0
  307. package/src/core/ui-packs/el.ts +13 -0
  308. package/src/core/ui-packs/es.ts +13 -0
  309. package/src/core/ui-packs/fa.ts +13 -0
  310. package/src/core/ui-packs/fi.ts +13 -0
  311. package/src/core/ui-packs/fr.ts +13 -0
  312. package/src/core/ui-packs/he.ts +13 -0
  313. package/src/core/ui-packs/hi.ts +13 -0
  314. package/src/core/ui-packs/hr.ts +13 -0
  315. package/src/core/ui-packs/hu.ts +13 -0
  316. package/src/core/ui-packs/id.ts +13 -0
  317. package/src/core/ui-packs/it.ts +13 -0
  318. package/src/core/ui-packs/ja.ts +13 -0
  319. package/src/core/ui-packs/ko.ts +13 -0
  320. package/src/core/ui-packs/nl.ts +13 -0
  321. package/src/core/ui-packs/no.ts +13 -0
  322. package/src/core/ui-packs/pl.ts +13 -0
  323. package/src/core/ui-packs/pt-br.ts +13 -0
  324. package/src/core/ui-packs/pt.ts +13 -0
  325. package/src/core/ui-packs/ro.ts +13 -0
  326. package/src/core/ui-packs/ru.ts +13 -0
  327. package/src/core/ui-packs/sk.ts +13 -0
  328. package/src/core/ui-packs/sr.ts +13 -0
  329. package/src/core/ui-packs/sv.ts +13 -0
  330. package/src/core/ui-packs/th.ts +13 -0
  331. package/src/core/ui-packs/tr.ts +13 -0
  332. package/src/core/ui-packs/uk.ts +13 -0
  333. package/src/core/ui-packs/vi.ts +13 -0
  334. package/src/core/ui-packs/zh-tw.ts +13 -0
  335. package/src/core/ui-packs/zh.ts +13 -0
  336. package/src/core/version-cut.ts +17 -2
  337. package/src/core/versions.ts +4 -1
  338. package/src/deploy/adapters/node.ts +5 -2
  339. package/src/deploy/adapters/registry.ts +2 -1
  340. package/src/deploy/adapters/types.ts +13 -1
  341. package/src/deploy/artifacts.ts +25 -5
  342. package/src/deploy/cloudflare-negotiation.ts +23 -4
  343. package/src/deploy/headers.ts +67 -51
  344. package/src/deploy/node-headers.ts +148 -27
  345. package/src/deploy/platforms/cloudflare.ts +1 -0
  346. package/src/deploy/platforms/netlify.ts +82 -5
  347. package/src/deploy/platforms/node.ts +7 -5
  348. package/src/deploy/platforms/static.ts +1 -0
  349. package/src/deploy/platforms/types.ts +7 -0
  350. package/src/deploy/platforms/vercel.ts +9 -3
  351. package/src/deploy/redirects.ts +14 -3
  352. package/src/deploy/vercel-negotiation.ts +35 -3
  353. package/src/eval/agents.ts +10 -2
  354. package/src/eval/run.ts +25 -0
  355. package/src/markdown/base-links.ts +55 -8
  356. package/src/markdown/index.ts +6 -3
  357. package/src/markdown/relative-links.ts +3 -23
  358. package/src/markdown/route-snapshot.ts +37 -0
  359. package/src/og/card.ts +20 -4
  360. package/src/og/derive.ts +145 -4
  361. package/src/openapi/graphql-build.ts +28 -2
  362. package/src/openapi/model.ts +77 -13
  363. package/src/openapi/render-mdx.ts +10 -1
  364. package/src/registry/eject.ts +119 -22
  365. package/src/search/documents.ts +22 -1
  366. package/src/search/sync/algolia.ts +36 -2
  367. package/src/sources/registry.ts +5 -0
  368. package/src/theme/entry.ts +11 -4
  369. package/src/translate/agents.ts +6 -1
  370. package/src/translate/ledger.ts +26 -3
  371. package/src/translate/meta.ts +11 -3
  372. package/src/translate/run.ts +11 -5
  373. package/src/translate/validate.ts +10 -1
  374. package/src/translate/work-list.ts +36 -3
  375. package/src/upgrade/upgrade.ts +36 -4
  376. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  377. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  378. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  379. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  380. package/dist/cli/chunk-82bbrxdn.js +0 -51
  381. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  382. package/dist/cli/chunk-abh8yjkn.js +0 -31
  383. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  384. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  385. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  386. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  387. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  388. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  389. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  390. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  391. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  392. package/dist/cli/chunk-q5163e60.js.map +0 -11
  393. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  394. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  395. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  396. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  397. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  398. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-00gs3wqs.js.map} +0 -0
  399. /package/dist/cli/{chunk-6vm74dry.js.map → chunk-6dtt0zfn.js.map} +0 -0
  400. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-bbnwccaz.js.map} +0 -0
  401. /package/dist/cli/{chunk-ce574jw2.js.map → chunk-esh98wmb.js.map} +0 -0
  402. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-fsmrqk8a.js.map} +0 -0
  403. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-g698a744.js.map} +0 -0
  404. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-h2ez8dzb.js.map} +0 -0
  405. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-h7k3nq3v.js.map} +0 -0
  406. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-mqc662a6.js.map} +0 -0
  407. /package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-n1yg3tj3.js.map} +0 -0
  408. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-sqw4ekg1.js.map} +0 -0
  409. /package/dist/cli/{chunk-27g6wdth.js.map → chunk-wdrt2k2v.js.map} +0 -0
  410. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-zp79m0ts.js.map} +0 -0
@@ -1,10 +1,44 @@
1
- // Mirrors the fence masking in `core/sources/assets.ts`: a fenced code sample
2
- // that *shows* `<Visibility>` markup must keep showing what the author wrote.
3
- const CODE_FENCE_BLOCK =
4
- /^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
1
+ import { nextFenceState } from "../core/code-fences.ts";
2
+ import type { FenceState } from "../core/code-fences.ts";
3
+
4
+ /** An inline code span on one line, as the other source scanners read it. */
5
+ const INLINE_CODE = /`[^`\n]*`/gu;
5
6
  // NUL delimiters cannot appear in authored markdown, so tokens never collide.
6
7
  // oxlint-disable-next-line no-control-regex -- the NUL is the collision guard.
7
- const FENCE_TOKEN = /\u0000blume-fence-(?<index>\d+)\u0000/gu;
8
+ const CODE_TOKEN = /\u0000blume-code-(?<index>\d+)\u0000/gu;
9
+
10
+ /**
11
+ * Swap every code sample for a one-line token — fenced blocks at any indent
12
+ * (a fence inside a `<Tab>` or `<Step>` is indented under it) and inline code
13
+ * spans — so markup that *shows* `<Visibility>` keeps showing what the author
14
+ * wrote. An unclosed fence runs to the end, as CommonMark reads it.
15
+ */
16
+ const maskCode = (markdown: string, stash: string[]): string => {
17
+ const token = (code: string): string => {
18
+ stash.push(code);
19
+ return `\u0000blume-code-${stash.length - 1}\u0000`;
20
+ };
21
+ const lines: string[] = [];
22
+ let fence: FenceState = null;
23
+ let block: string[] = [];
24
+ for (const line of markdown.split("\n")) {
25
+ const next = nextFenceState(line, fence);
26
+ if (fence === null && next === null) {
27
+ lines.push(line.replaceAll(INLINE_CODE, token));
28
+ } else {
29
+ block.push(line);
30
+ if (next === null) {
31
+ lines.push(token(block.join("\n")));
32
+ block = [];
33
+ }
34
+ }
35
+ fence = next;
36
+ }
37
+ if (block.length > 0) {
38
+ lines.push(token(block.join("\n")));
39
+ }
40
+ return lines.join("\n");
41
+ };
8
42
 
9
43
  /** The audience an output surface serves — the component's two `for` values. */
10
44
  export type VisibilityAudience = "agents" | "web";
@@ -36,12 +70,9 @@ export const applyAudienceVisibility = (
36
70
  markdown: string,
37
71
  audience: VisibilityAudience
38
72
  ): string => {
39
- // Mask fenced code blocks so documentation *about* Visibility survives.
40
- const fences: string[] = [];
41
- const masked = markdown.replace(CODE_FENCE_BLOCK, (block) => {
42
- fences.push(block);
43
- return `\u0000blume-fence-${fences.length - 1}\u0000`;
44
- });
73
+ // Mask code so documentation *about* Visibility survives.
74
+ const code: string[] = [];
75
+ const masked = maskCode(markdown, code);
45
76
 
46
77
  let touched = false;
47
78
  const filtered = masked
@@ -56,12 +87,12 @@ export const applyAudienceVisibility = (
56
87
 
57
88
  // Removing/unwrapping block-level tags leaves runs of blank lines behind;
58
89
  // collapse them only when something matched so untouched files round-trip
59
- // exactly. Fences are masked as single-line tokens, so they are unaffected.
90
+ // exactly. Code is masked as single-line tokens, so it is unaffected.
60
91
  const tidied = touched ? filtered.replaceAll(/\n{3,}/gu, "\n\n") : filtered;
61
92
 
62
93
  return tidied.replaceAll(
63
- FENCE_TOKEN,
64
- (token, index) => fences[Number(index)] ?? token
94
+ CODE_TOKEN,
95
+ (token, index) => code[Number(index)] ?? token
65
96
  );
66
97
  };
67
98
 
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+
3
+ import type { AdapterDescriptor } from "../core/adapter.ts";
4
+ import { adapterDescriptorSchema } from "../core/adapter.ts";
5
+ import type { HeadScript } from "./head.ts";
6
+
7
+ /** The script Databuddy's install snippet loads. */
8
+ export const DATABUDDY_SCRIPT_SRC = "https://cdn.databuddy.cc/databuddy.js";
9
+
10
+ /** The options {@link databuddy} maps itself. */
11
+ export interface DatabuddyNamedOptions {
12
+ /** Client ID, from the website's settings in the Databuddy dashboard. */
13
+ clientId: string;
14
+ }
15
+
16
+ /**
17
+ * Options for {@link databuddy}: the client ID plus any other tracker setting,
18
+ * forwarded as a `data-` attribute on the tag (`"track-web-vitals"` →
19
+ * `data-track-web-vitals`, `"track-errors"`, `"track-outgoing-links"`,
20
+ * `"api-url"`, …).
21
+ */
22
+ export type DatabuddyOptions = DatabuddyNamedOptions & {
23
+ [setting: string]: string;
24
+ };
25
+
26
+ export const databuddyOptionsSchema = z
27
+ .object({
28
+ clientId: z.string().min(1),
29
+ })
30
+ .catchall(z.string());
31
+
32
+ export type DatabuddyAdapter = AdapterDescriptor<"databuddy", DatabuddyOptions>;
33
+
34
+ export const databuddyAdapterSchema = adapterDescriptorSchema(
35
+ "databuddy",
36
+ databuddyOptionsSchema
37
+ );
38
+
39
+ /**
40
+ * Databuddy. The client ID is public — it's in the tag on every page.
41
+ */
42
+ export const databuddy = (options: DatabuddyOptions): DatabuddyAdapter => ({
43
+ kind: "databuddy",
44
+ options,
45
+ requiredSecrets: [],
46
+ runtimeDeps: [],
47
+ });
48
+
49
+ /**
50
+ * The async tag from the install instructions, with `clientId` as
51
+ * `data-client-id` and every other option as its own `data-` attribute. The
52
+ * tracker skips setup when `window.databuddy` already exists, so there is no
53
+ * queue stub. Databuddy's script tracks history changes on its own, so
54
+ * client-router navigations need no extra hook.
55
+ */
56
+ export const databuddyHead = (options: DatabuddyOptions): HeadScript[] => {
57
+ const { clientId, ...settings } = options;
58
+ const attributes: HeadScript["attributes"] = {};
59
+ for (const [setting, value] of Object.entries(settings)) {
60
+ attributes[`data-${setting}`] = value;
61
+ }
62
+ attributes["data-client-id"] = clientId;
63
+ attributes.async = true;
64
+ attributes.crossorigin = "anonymous";
65
+ attributes.src = DATABUDDY_SCRIPT_SRC;
66
+ return [{ attributes, content: null }];
67
+ };
@@ -3,6 +3,7 @@ import { amplitudeHead } from "./amplitude.ts";
3
3
  import { clarityHead } from "./clarity.ts";
4
4
  import { clearbitHead } from "./clearbit.ts";
5
5
  import { cloudflareHead } from "./cloudflare.ts";
6
+ import { databuddyHead } from "./databuddy.ts";
6
7
  import { fathomHead } from "./fathom.ts";
7
8
  import { googleAnalyticsHead } from "./google-analytics.ts";
8
9
  import { googleTagManagerHead } from "./google-tag-manager.ts";
@@ -64,6 +65,9 @@ const adapterScripts = (
64
65
  case "cloudflare": {
65
66
  return cloudflareHead(adapter.options);
66
67
  }
68
+ case "databuddy": {
69
+ return databuddyHead(adapter.options);
70
+ }
67
71
  case "fathom": {
68
72
  return fathomHead(adapter.options);
69
73
  }
@@ -30,6 +30,8 @@ export { clearbit } from "./clearbit.ts";
30
30
  export type { ClearbitAdapter, ClearbitOptions } from "./clearbit.ts";
31
31
  export { cloudflare } from "./cloudflare.ts";
32
32
  export type { CloudflareAdapter, CloudflareOptions } from "./cloudflare.ts";
33
+ export { databuddy } from "./databuddy.ts";
34
+ export type { DatabuddyAdapter, DatabuddyOptions } from "./databuddy.ts";
33
35
  export { fathom } from "./fathom.ts";
34
36
  export type { FathomAdapter, FathomOptions } from "./fathom.ts";
35
37
  export { googleAnalytics } from "./google-analytics.ts";
@@ -54,13 +54,30 @@ export const posthog = (options: PosthogOptions): PosthogAdapter => ({
54
54
  const POSTHOG_LOADER =
55
55
  '!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);';
56
56
 
57
- // PostHog's loader only captures a pageview per real page load, so the client
58
- // router's in-place swaps are captured here.
57
+ // With PostHog's classic pageview setting its loader captures one pageview
58
+ // per real page load, so the client router's in-place swaps are captured here.
59
59
  const POSTHOG_SPA_PAGEVIEWS = spaPageviews(
60
60
  "__blumePosthogPath",
61
61
  'posthog.capture("$pageview");'
62
62
  );
63
63
 
64
+ /**
65
+ * Whether Blume should capture client-router navigations itself, given the
66
+ * `posthog.init` options. Only when PostHog captures load-time pageviews and
67
+ * nothing more: `capture_pageview: false` turns pageviews off, and
68
+ * `capture_pageview: "history_change"` — the default under any `defaults`
69
+ * snapshot (`defaults: "2025-05-24"` in PostHog's current snippet), unless
70
+ * `capture_pageview` says otherwise — already captures every navigation, so
71
+ * sending another would count each one twice.
72
+ */
73
+ const capturesNavigations = (init: Record<string, JsonValue>): boolean => {
74
+ const setting = init.capture_pageview;
75
+ if (setting === undefined) {
76
+ return init.defaults === undefined || init.defaults === "unset";
77
+ }
78
+ return setting !== false && setting !== "history_change";
79
+ };
80
+
64
81
  /**
65
82
  * The inline loader-plus-init snippet. `key` and `host` are the options Blume
66
83
  * maps (`host` becomes `api_host`); everything else lands in `posthog.init`'s
@@ -69,10 +86,11 @@ const POSTHOG_SPA_PAGEVIEWS = spaPageviews(
69
86
  export const posthogHead = (options: PosthogOptions): HeadScript[] => {
70
87
  const { host, key, ...init } = options;
71
88
  const initOptions = { api_host: host ?? POSTHOG_DEFAULT_HOST, ...init };
89
+ const pageviews = capturesNavigations(init) ? POSTHOG_SPA_PAGEVIEWS : "";
72
90
  return [
73
91
  {
74
92
  attributes: {},
75
- content: `${POSTHOG_LOADER}posthog.init(${inlineJson(key)},${inlineJson(initOptions)});${POSTHOG_SPA_PAGEVIEWS}`,
93
+ content: `${POSTHOG_LOADER}posthog.init(${inlineJson(key)},${inlineJson(initOptions)});${pageviews}`,
76
94
  },
77
95
  ];
78
96
  };
@@ -5,6 +5,7 @@ import { amplitudeAdapterSchema } from "./amplitude.ts";
5
5
  import { clarityAdapterSchema } from "./clarity.ts";
6
6
  import { clearbitAdapterSchema } from "./clearbit.ts";
7
7
  import { cloudflareAdapterSchema } from "./cloudflare.ts";
8
+ import { databuddyAdapterSchema } from "./databuddy.ts";
8
9
  import { fathomAdapterSchema } from "./fathom.ts";
9
10
  import { googleAnalyticsAdapterSchema } from "./google-analytics.ts";
10
11
  import { googleTagManagerAdapterSchema } from "./google-tag-manager.ts";
@@ -27,6 +28,7 @@ export const analyticsAdapterSchema = z.discriminatedUnion("kind", [
27
28
  clarityAdapterSchema,
28
29
  clearbitAdapterSchema,
29
30
  cloudflareAdapterSchema,
31
+ databuddyAdapterSchema,
30
32
  fathomAdapterSchema,
31
33
  googleAnalyticsAdapterSchema,
32
34
  googleTagManagerAdapterSchema,
@@ -31,7 +31,7 @@ import {
31
31
  hasDeferrableGroups,
32
32
  navVariants,
33
33
  } from "../components/layout/nav-utils.ts";
34
- import { normalizeBasePath } from "../core/base-path.ts";
34
+ import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
35
35
  import { validateUsedComponents } from "../core/component-diagnostics.ts";
36
36
  import {
37
37
  analyzeComponentOverrides,
@@ -81,7 +81,9 @@ import { ogCacheDir } from "../og/cache.ts";
81
81
  import { missingFontFiles, resolveOgFonts } from "../og/derive.ts";
82
82
  import type { DerivedOgFonts } from "../og/derive.ts";
83
83
  import { resolveOgLogo } from "../og/logo.ts";
84
- import type { ApiSpecData, OpenApiData } from "../openapi/model.ts";
84
+ import type { AsyncApiSpecValue } from "../openapi/asyncapi.ts";
85
+ import type { ApiSpecData, HttpMethod, OpenApiData } from "../openapi/model.ts";
86
+ import { HTTP_METHODS, withServerDefaults } from "../openapi/model.ts";
85
87
  import {
86
88
  builtinProxyReferences,
87
89
  hasScalarReferences,
@@ -954,6 +956,8 @@ const resolveBanner = (config: ResolvedConfig): BlumeBanner | null => {
954
956
  if (isStringShorthand(banner)) {
955
957
  return { content: banner, dismissible: false, key: banner };
956
958
  }
959
+ // `link.href` stays as authored; the link the banner renders is the
960
+ // navigation's `bannerHref`, localized and mounted under `basePath`.
957
961
  return {
958
962
  content: banner.content,
959
963
  dismissible: banner.dismissible,
@@ -1489,28 +1493,41 @@ const writeApiFiles = async (
1489
1493
  await Promise.all(writes);
1490
1494
  };
1491
1495
 
1496
+ /** The proxy endpoint's file under the Astro `src/` dir (eject writes it too). */
1497
+ export const PLAYGROUND_PROXY_ENTRY = join("blume-openapi", "api-proxy.ts");
1498
+
1499
+ /** The URL the built-in proxy is injected at (see {@link planPlaygroundProxy}). */
1500
+ export const playgroundProxyPattern = (config: ResolvedConfig): string =>
1501
+ withBasePath(config.basePath, "/_api-proxy");
1502
+
1492
1503
  /**
1493
1504
  * Decide whether to generate the playground's built-in CORS proxy endpoint.
1494
1505
  * Only the Blume renderer's playground with `proxy: true` needs it — a proxy
1495
1506
  * URL string points at an external service, and `false` sends requests
1496
1507
  * directly. Injected at `/_api-proxy` (rather than written under `pages/`)
1497
1508
  * because Astro treats `_`-prefixed page files as private; the endpoint's own
1498
- * `prerender = false` export wins over the injection default.
1509
+ * `prerender = false` export wins over the injection default. Mounted under
1510
+ * the site `basePath`, at the URL the playground sends to (see
1511
+ * openapi/source.ts); Astro layers `deployment.base` onto both.
1499
1512
  */
1500
1513
  const planPlaygroundProxy = (config: ResolvedConfig, srcDir: string) => ({
1501
1514
  enabled: needsPlaygroundProxy(config),
1502
- entrypoint: join(srcDir, "blume-openapi", "api-proxy.ts"),
1503
- pattern: "/_api-proxy",
1515
+ entrypoint: join(srcDir, PLAYGROUND_PROXY_ENTRY),
1516
+ pattern: playgroundProxyPattern(config),
1504
1517
  });
1505
1518
 
1506
1519
  /**
1507
1520
  * The origins the built-in proxy is allowed to reach: one per absolute
1508
- * `servers[].url` across the parsed specs. This is the endpoint's whole trust
1509
- * boundary — the client sends the target as a query parameter, and a reader's
1510
- * custom base URL is not a documented server — so it is derived here, at build
1511
- * time, from the same documents the operation pages render.
1521
+ * `servers[].url` across the parsed specs — the document's, each path item's,
1522
+ * and each operation's, since an operation's playground sends to the most
1523
+ * specific of those. This is the endpoint's whole trust boundary — the client
1524
+ * sends the target as a query parameter, and a reader's custom base URL is not
1525
+ * a documented server — so it is derived here, at build time, from the same
1526
+ * documents the operation pages render.
1512
1527
  *
1513
- * Relative (`/v1`) and templated (`{env}.api.example.com`) server URLs carry no
1528
+ * A templated URL (`https://{env}.api.example.com`) is allowed as the
1529
+ * playground sends it, with each variable at its declared default. Relative
1530
+ * (`/v1`) URLs, and templates with a variable left undefaulted, carry no
1514
1531
  * origin to allow and are skipped; AsyncAPI documents declare `servers` as a
1515
1532
  * map and contribute nothing (the proxy is OpenAPI-only).
1516
1533
  */
@@ -1526,27 +1543,53 @@ const specOriginsOf = (spec: ApiSpecData): string[] => {
1526
1543
  }
1527
1544
  }
1528
1545
  // SAFETY: `document` is arbitrary parsed JSON; the assertion only names
1529
- // the optional `servers` shape, and every access below re-checks it —
1530
- // `Array.isArray(servers)` guards the list and `server.url ?? ""` the url.
1531
- const { servers } = spec.document as { servers?: { url?: string }[] };
1532
- for (const server of Array.isArray(servers) ? servers : []) {
1533
- const url = server.url ?? "";
1534
- // `new URL("https://{region}.api.example.com")` parses — the braces land
1535
- // in the hostname — so templated URLs need an explicit check or their
1536
- // junk literal becomes an allowlist entry no real request can match.
1537
- if (url.includes("{")) {
1538
- continue;
1546
+ // the optional `servers` and `paths` shapes, and every access below
1547
+ // re-checks them — `Array.isArray` guards each list, optional chaining each
1548
+ // path item and operation, and `server.url ?? ""` the url.
1549
+ const document = spec.document as {
1550
+ paths?: Record<string, DocumentPathItem | null | undefined>;
1551
+ servers?: DocumentServer[];
1552
+ };
1553
+ const lists = [document.servers];
1554
+ for (const item of Object.values(document.paths ?? {})) {
1555
+ lists.push(item?.servers);
1556
+ for (const method of HTTP_METHODS) {
1557
+ lists.push(item?.[method]?.servers);
1539
1558
  }
1540
- try {
1541
- origins.add(new URL(url).origin);
1542
- } catch {
1543
- // Not an absolute URL: nothing to allow.
1559
+ }
1560
+ for (const servers of lists) {
1561
+ for (const server of Array.isArray(servers) ? servers : []) {
1562
+ const url = withServerDefaults(server.url ?? "", server.variables);
1563
+ // `new URL("https://{region}.api.example.com")` parses — the braces land
1564
+ // in the hostname — so a variable with no default needs an explicit
1565
+ // check or its junk literal becomes an allowlist entry no real request
1566
+ // can match.
1567
+ if (url.includes("{")) {
1568
+ continue;
1569
+ }
1570
+ try {
1571
+ origins.add(new URL(url).origin);
1572
+ } catch {
1573
+ // Not an absolute URL: nothing to allow.
1574
+ }
1544
1575
  }
1545
1576
  }
1546
1577
  return [...origins];
1547
1578
  };
1548
1579
 
1549
- const specOrigins = (data: OpenApiData): string[] =>
1580
+ /** A `servers[]` entry as a parsed spec document declares it. */
1581
+ interface DocumentServer {
1582
+ url?: string;
1583
+ variables?: AsyncApiSpecValue;
1584
+ }
1585
+
1586
+ /** An OpenAPI path item: its own `servers` plus one per operation. */
1587
+ type DocumentPathItem = { servers?: DocumentServer[] } & Partial<
1588
+ Record<HttpMethod, { servers?: DocumentServer[] } | null>
1589
+ >;
1590
+
1591
+ /** The built-in proxy's allowlist: every spec's origins, deduped and sorted. */
1592
+ export const specOrigins = (data: OpenApiData): string[] =>
1550
1593
  [...new Set(Object.values(data).flatMap(specOriginsOf))].toSorted();
1551
1594
 
1552
1595
  /**
@@ -1558,7 +1601,7 @@ const specOrigins = (data: OpenApiData): string[] =>
1558
1601
  * whose own origins came out empty (no absolute `servers[].url`, no absolute
1559
1602
  * GraphQL `endpoint`) gets a warning naming it.
1560
1603
  */
1561
- const proxyAllowlistWarnings = (
1604
+ export const proxyAllowlistWarnings = (
1562
1605
  config: ResolvedConfig,
1563
1606
  data: OpenApiData
1564
1607
  ): string[] => {
@@ -1573,7 +1616,7 @@ const proxyAllowlistWarnings = (
1573
1616
  warnings.push(
1574
1617
  spec.kind === "graphql"
1575
1618
  ? `The "${spec.label}" GraphQL reference (${spec.route}) has playground.proxy: true, but no absolute endpoint is configured for it, so the built-in proxy has no origin to allow and will refuse every request its playground sends. Set \`endpoint\` on the graphql() adapter (or the source) to the live GraphQL URL, or point playground.proxy at an external proxy URL.`
1576
- : `The "${spec.label}" reference (${spec.route}) has playground.proxy: true, but its spec declares no absolute servers[].url (relative and templated URLs carry no origin), so the built-in proxy has no origin to allow and will refuse every request its playground sends. Add an absolute server URL to the spec, or point playground.proxy at an external proxy URL.`
1619
+ : `The "${spec.label}" reference (${spec.route}) has playground.proxy: true, but its spec declares no absolute servers[].url (relative URLs, and templated ones with a variable missing its default, carry no origin), so the built-in proxy has no origin to allow and will refuse every request its playground sends. Add an absolute server URL to the spec, or point playground.proxy at an external proxy URL.`
1577
1620
  );
1578
1621
  }
1579
1622
  return warnings;
@@ -1699,6 +1742,7 @@ export const analyzeComponentsFile = async (
1699
1742
  const projectOgFonts = (project: BlumeProject): DerivedOgFonts =>
1700
1743
  resolveOgFonts(
1701
1744
  {
1745
+ locales: fontLocaleCodes(project.config.i18n),
1702
1746
  ogFonts: project.config.seo.og.fonts,
1703
1747
  themeFonts: project.config.theme.fonts,
1704
1748
  themeFontsConfigured: project.themeFontsConfigured,
@@ -14,7 +14,7 @@ import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
14
14
  import { trimChar } from "../core/trim.ts";
15
15
  import type { ProjectContext } from "../core/types.ts";
16
16
  import { deployPassthrough } from "../deploy/adapters/types.ts";
17
- import { SVG_ASSET_POLICY } from "../deploy/headers.ts";
17
+ import { SVG_ASSET_HEADERS } from "../deploy/headers.ts";
18
18
  import { deployPlatform } from "../deploy/platforms/index.ts";
19
19
  import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
20
20
  import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
@@ -1377,7 +1377,10 @@ export const createSearch = () => create(${JSON.stringify(provider.options)});
1377
1377
  * site uses the feature and `null` otherwise, so an unused library is absent
1378
1378
  * from the module graph (never bundled, never pre-bundled in dev) rather than
1379
1379
  * merely lazy. Regenerated on every pass: a page that gains a mermaid fence
1380
- * turns the loader on.
1380
+ * turns the loader on. The EPUB module is named — even in a type — only when
1381
+ * `export.epub` is on: an ejected app depends on `epub-gen-memory` only then,
1382
+ * and a type import of a package it can't resolve fails its type check under
1383
+ * a strict linker such as pnpm.
1381
1384
  */
1382
1385
  export const featuresTemplate = (features: ClientFeatures): string =>
1383
1386
  `// Generated by Blume. Do not edit.
@@ -1394,9 +1397,13 @@ export const loadMermaid: (() => Promise<unknown>) | null = ${
1394
1397
  };
1395
1398
 
1396
1399
  /** The EPUB generator's browser bundle; null when export.epub is off. */
1397
- export const loadEpub:
1400
+ ${
1401
+ features.epub
1402
+ ? `export const loadEpub:
1398
1403
  | (() => Promise<typeof import("epub-gen-memory/bundle")>)
1399
- | null = ${features.epub ? '() => import("epub-gen-memory/bundle")' : "null"};
1404
+ | null = () => import("epub-gen-memory/bundle");`
1405
+ : "export const loadEpub: (() => Promise<unknown>) | null = null;"
1406
+ }
1400
1407
  `;
1401
1408
 
1402
1409
  /**
@@ -1561,12 +1568,13 @@ export function GET({ props }: { props: { route: string } }) {
1561
1568
  * materializes under `.blume/public/blume-assets` (see
1562
1569
  * `core/sources/assets.ts`). That directory is NOT Astro's `publicDir`
1563
1570
  * (which points at the user project's `public/`), so without this endpoint
1564
- * those references 404.
1571
+ * those references 404. `null` for an ejected app, which copies those
1572
+ * files into its own `public/blume-assets` instead.
1565
1573
  * Prerendered: every asset becomes a static file in the build output; the dev
1566
1574
  * server renders on demand, so new images appear without a restart.
1567
1575
  */
1568
1576
  export const contentAssetsEndpointTemplate = (
1569
- stagedAssetsDir: string
1577
+ stagedAssetsDir: string | null
1570
1578
  ): string =>
1571
1579
  `// Generated by Blume. Do not edit.
1572
1580
  import { existsSync } from "node:fs";
@@ -1588,18 +1596,29 @@ const TYPES: Record<string, string> = {
1588
1596
  ".ico": "image/x-icon",
1589
1597
  ".jpeg": "image/jpeg",
1590
1598
  ".jpg": "image/jpeg",
1599
+ ".mov": "video/quicktime",
1600
+ ".mp4": "video/mp4",
1601
+ ".ogv": "video/ogg",
1591
1602
  ".png": "image/png",
1592
1603
  ".svg": "image/svg+xml",
1593
1604
  ".tiff": "image/tiff",
1605
+ ".webm": "video/webm",
1594
1606
  ".webp": "image/webp",
1595
1607
  };
1596
1608
 
1609
+ const extensionOf = (path: string): string =>
1610
+ path.slice(path.lastIndexOf(".")).toLowerCase();
1611
+
1597
1612
  const contentType = (path: string): string =>
1598
- TYPES[path.slice(path.lastIndexOf(".")).toLowerCase()] ??
1599
- "application/octet-stream";
1613
+ TYPES[extensionOf(path)] ?? "application/octet-stream";
1614
+
1615
+ // A source downloads only images and videos; anything else in the staged
1616
+ // directory (a page an older build saved under its URL's extension) is never
1617
+ // published from the docs origin.
1618
+ const isMedia = (path: string): boolean => Object.hasOwn(TYPES, extensionOf(path));
1600
1619
 
1601
1620
  const stagedParams = async (): Promise<string[]> => {
1602
- if (!existsSync(STAGED_DIR)) {
1621
+ if (STAGED_DIR === null || !existsSync(STAGED_DIR)) {
1603
1622
  return [];
1604
1623
  }
1605
1624
  const entries = await readdir(STAGED_DIR, {
@@ -1607,7 +1626,7 @@ const stagedParams = async (): Promise<string[]> => {
1607
1626
  withFileTypes: true,
1608
1627
  });
1609
1628
  return entries
1610
- .filter((entry) => entry.isFile())
1629
+ .filter((entry) => entry.isFile() && isMedia(entry.name))
1611
1630
  .map((entry) =>
1612
1631
  relative(STAGED_DIR, join(entry.parentPath, entry.name)).replaceAll(
1613
1632
  "\\\\",
@@ -1628,6 +1647,9 @@ const resolveAsset = (asset: string): string | null => {
1628
1647
  if (asset.startsWith("content/")) {
1629
1648
  return files[asset.slice("content/".length)] ?? null;
1630
1649
  }
1650
+ if (STAGED_DIR === null) {
1651
+ return null;
1652
+ }
1631
1653
  const abs = resolve(STAGED_DIR, asset);
1632
1654
  // Traversal guard: the dev server renders on demand, so the param is
1633
1655
  // attacker-controlled there — never step outside the staged directory.
@@ -1637,7 +1659,7 @@ const resolveAsset = (asset: string): string | null => {
1637
1659
  // asset — and a bare prefix also admits a sibling directory that merely
1638
1660
  // shares the name.
1639
1661
  const rel = relative(STAGED_DIR, abs);
1640
- if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
1662
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel) || !isMedia(rel)) {
1641
1663
  return null;
1642
1664
  }
1643
1665
  return existsSync(abs) ? abs : null;
@@ -1651,11 +1673,12 @@ export const GET: APIRoute = async ({ params }) => {
1651
1673
  const body = await readFile(path);
1652
1674
  const type = contentType(path);
1653
1675
  // An SVG opened directly is a document that can run script; the sandbox
1654
- // keeps an uploaded one inert on the docs origin. Static hosts get the same
1655
- // header from the build's header rules.
1676
+ // keeps an uploaded one inert on the docs origin. This endpoint
1677
+ // prerenders, so these headers reach dev only: a build serves the files
1678
+ // with the same headers from its host's rules (see deploy/headers.ts).
1656
1679
  const headers: Record<string, string> =
1657
1680
  type === "image/svg+xml"
1658
- ? { "Content-Security-Policy": ${JSON.stringify(SVG_ASSET_POLICY)}, "Content-Type": type }
1681
+ ? { ...${JSON.stringify(SVG_ASSET_HEADERS)}, "Content-Type": type }
1659
1682
  : { "Content-Type": type };
1660
1683
  return new Response(new Uint8Array(body), { headers });
1661
1684
  };
@@ -2758,6 +2781,7 @@ export const notFoundPageTemplate = (): string => `---
2758
2781
  import Icon from "blume/components/Icon.astro";
2759
2782
  import PageLayout from "blume/components/layout/PageLayout.astro";
2760
2783
  import { withBase } from "blume/components/islands/base-path.ts";
2784
+ import { withBasePath } from "blume/core/base-path.ts";
2761
2785
  import data from "blume:data";
2762
2786
 
2763
2787
  export const prerender = true;
@@ -2795,8 +2819,13 @@ const suggestions = [
2795
2819
  // Every host serves this one page for any missing URL, so a reader who
2796
2820
  // mistyped \`/ar/…\` would get the default locale's message. The other locales'
2797
2821
  // copy rides along, and the script below swaps it in when the URL's first
2798
- // segment names one of them — the message, its language and direction, the
2799
- // tab title, and the home link (to that locale's root).
2822
+ // segment past the site's base names one of them — the message, its language
2823
+ // and direction, the tab title, and the home link (to that locale's root).
2824
+ // Localized routes sit under both bases (\`{deployment.base}/{basePath}/ar/…\`),
2825
+ // so the script strips the two together, trailing slash included.
2826
+ const { basePath } = data.config;
2827
+ const siteRoot = withBase(withBasePath(basePath, "/"));
2828
+ const localeBase = siteRoot.endsWith("/") ? siteRoot : siteRoot + "/";
2800
2829
  const localized = Object.fromEntries(
2801
2830
  (i18n?.locales ?? [])
2802
2831
  .filter((l) => l.code !== i18n?.defaultLocale && data.uiByLocale[l.code])
@@ -2808,7 +2837,7 @@ const localized = Object.fromEntries(
2808
2837
  description: strings.description,
2809
2838
  dir: l.dir ?? "ltr",
2810
2839
  home: strings.home,
2811
- homeHref: withBase("/" + l.code),
2840
+ homeHref: withBase(withBasePath(basePath, "/" + l.code)),
2812
2841
  suggestions: strings.suggestions,
2813
2842
  title: strings.title,
2814
2843
  },
@@ -2837,7 +2866,7 @@ const localizedJson = JSON.stringify(localized).replaceAll("<", "\\\\u003c");
2837
2866
  >
2838
2867
  <div
2839
2868
  class="mx-auto grid w-full max-w-5xl gap-12 px-6 py-20 sm:py-28 md:grid-cols-[3fr_2fr] md:gap-16 lg:gap-24 lg:py-36"
2840
- data-base={withBase("/")}
2869
+ data-base={localeBase}
2841
2870
  data-blume-not-found
2842
2871
  >
2843
2872
  <div class="flex flex-col items-start">
@@ -3300,11 +3329,20 @@ const Example = entry.Component;
3300
3329
  root.dataset.theme = theme;
3301
3330
  root.classList.toggle("dark", theme === "dark");
3302
3331
  };
3303
- const stored = () =>
3304
- localStorage.getItem("blume-theme") ??
3305
- (matchMedia("(prefers-color-scheme: dark)").matches
3306
- ? "dark"
3307
- : "light");
3332
+ // Blocked storage (a sandboxed or privacy-locked frame) throws on
3333
+ // access; it falls through to the OS setting like an unset preference.
3334
+ const stored = () => {
3335
+ let theme = null;
3336
+ try {
3337
+ theme = localStorage.getItem("blume-theme");
3338
+ } catch {}
3339
+ return (
3340
+ theme ??
3341
+ (matchMedia("(prefers-color-scheme: dark)").matches
3342
+ ? "dark"
3343
+ : "light")
3344
+ );
3345
+ };
3308
3346
  try {
3309
3347
  const host = window.parent.document.documentElement;
3310
3348
  apply(host.dataset.theme ?? stored());
@@ -583,7 +583,7 @@ export const CHECKS = [
583
583
  },
584
584
  {
585
585
  category: "sitemap",
586
- fix: "Sitemaps must be valid XML in the sitemaps.org urlset format.",
586
+ fix: "Sitemaps must be valid XML in the sitemaps.org urlset or sitemap index format.",
587
587
  id: "BLUME_AUDIT_SITEMAP_INVALID",
588
588
  severity: "error",
589
589
  tier: "static",
@@ -684,7 +684,7 @@ export const CHECKS = [
684
684
  },
685
685
  {
686
686
  category: "ai",
687
- fix: "Rebuild so llms.txt matches the site; if the page is deliberately excluded, mark it `seo.noindex` or `sidebar.hidden`.",
687
+ fix: "Rebuild so llms.txt matches the site; if the page is deliberately excluded, set `ai.exclude: true` in its front matter.",
688
688
  id: "BLUME_AUDIT_LLMS_TXT_PAGE_MISSING",
689
689
  severity: "warning",
690
690
  tier: "static",