blume 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +110 -0
  3. package/README.md +2 -2
  4. package/dist/cli/{chunk-f2972sbt.js → chunk-00gs3wqs.js} +1 -1
  5. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  6. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  7. package/dist/cli/{chunk-pat2zzwc.js → chunk-3yce002v.js} +21 -8
  8. package/dist/cli/{chunk-pat2zzwc.js.map → chunk-3yce002v.js.map} +4 -4
  9. package/dist/cli/{chunk-d1tadaw7.js → chunk-4e9b9ra6.js} +17 -7
  10. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  11. package/dist/cli/{chunk-zg2gtj10.js → chunk-5r8g91qn.js} +352 -676
  12. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  13. package/dist/cli/{chunk-11j0384y.js → chunk-6dtt0zfn.js} +14 -14
  14. package/dist/cli/{chunk-11j0384y.js.map → chunk-6dtt0zfn.js.map} +3 -3
  15. package/dist/cli/{chunk-bw22s759.js → chunk-6k4ftwze.js} +38 -19
  16. package/dist/cli/{chunk-bw22s759.js.map → chunk-6k4ftwze.js.map} +4 -4
  17. package/dist/cli/{chunk-sqn5t4q0.js → chunk-7mbqtmgb.js} +16 -7
  18. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  19. package/dist/cli/{chunk-79njf86q.js → chunk-7vtckvaw.js} +21 -19
  20. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  21. package/dist/cli/{chunk-fh5hj5jt.js → chunk-8g8ytmgx.js} +17 -18
  22. package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-8g8ytmgx.js.map} +2 -2
  23. package/dist/cli/{chunk-1w8dp3qb.js → chunk-91ws1n6j.js} +18 -15
  24. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  25. package/dist/cli/{chunk-7ez8ny0t.js → chunk-bbnwccaz.js} +2 -2
  26. package/dist/cli/{chunk-5a2z0198.js → chunk-bfwp9vp6.js} +18 -10
  27. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  28. package/dist/cli/{chunk-ernrthtr.js → chunk-d1v5rhy0.js} +14 -15
  29. package/dist/cli/{chunk-ernrthtr.js.map → chunk-d1v5rhy0.js.map} +2 -2
  30. package/dist/cli/{chunk-a9kptbw5.js → chunk-ddndchfr.js} +21 -6
  31. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  32. package/dist/cli/{chunk-zxccj738.js → chunk-esh98wmb.js} +1 -1
  33. package/dist/cli/{chunk-88cpgt6h.js → chunk-fsmrqk8a.js} +1 -1
  34. package/dist/cli/{chunk-b5aj94ah.js → chunk-g698a744.js} +5 -5
  35. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  36. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  37. package/dist/cli/{chunk-41za066z.js → chunk-h2ez8dzb.js} +4 -4
  38. package/dist/cli/{chunk-by2290sx.js → chunk-h7k3nq3v.js} +2 -2
  39. package/dist/cli/{chunk-j8mw0za6.js → chunk-hqp2ajnh.js} +252 -123
  40. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  41. package/dist/cli/{chunk-mnqj32sj.js → chunk-hr8ne106.js} +119 -50
  42. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  43. package/dist/cli/{chunk-mwt1k8n7.js → chunk-j85scx15.js} +74 -30
  44. package/dist/cli/chunk-j85scx15.js.map +10 -0
  45. package/dist/cli/{chunk-z01ze5c1.js → chunk-k7pj68a8.js} +63 -22
  46. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  47. package/dist/cli/{chunk-2q1dwty4.js → chunk-mqc662a6.js} +8 -8
  48. package/dist/cli/{chunk-2q1dwty4.js.map → chunk-mqc662a6.js.map} +4 -4
  49. package/dist/cli/{chunk-y3e45rc8.js → chunk-n1yg3tj3.js} +4 -4
  50. package/dist/cli/{chunk-bctazmbk.js → chunk-nfcyttvj.js} +17 -6
  51. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  52. package/dist/cli/{chunk-6k8vp3ta.js → chunk-pbg5a4s3.js} +60 -28
  53. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  54. package/dist/cli/{chunk-d80hr03s.js → chunk-ppzjqwx2.js} +24 -17
  55. package/dist/cli/{chunk-d80hr03s.js.map → chunk-ppzjqwx2.js.map} +4 -4
  56. package/dist/cli/{chunk-beat36xx.js → chunk-qkb5a8sa.js} +25 -10
  57. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  58. package/dist/cli/{chunk-bnbmcwfb.js → chunk-sqw4ekg1.js} +5 -5
  59. package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-sqw4ekg1.js.map} +3 -3
  60. package/dist/cli/{chunk-xaz13gwg.js → chunk-v6ya5kcb.js} +3268 -1150
  61. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  62. package/dist/cli/{chunk-tzne8qfq.js → chunk-w4bxdvsa.js} +18 -15
  63. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  64. package/dist/cli/{chunk-f7t03s3g.js → chunk-wdrt2k2v.js} +2 -2
  65. package/dist/cli/{chunk-z1f5arsg.js → chunk-xh43dwgw.js} +170 -55
  66. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  67. package/dist/cli/{chunk-pnnvybbk.js → chunk-zp79m0ts.js} +5 -5
  68. package/dist/cli/{chunk-pnnvybbk.js.map → chunk-zp79m0ts.js.map} +2 -2
  69. package/dist/cli/index.js +160 -35
  70. package/dist/cli/index.js.map +4 -4
  71. package/dist/types/ai/agent-readability.d.ts +1 -1
  72. package/dist/types/ai/agent-surface.d.ts +32 -0
  73. package/dist/types/ai/api/paths.d.ts +1 -1
  74. package/dist/types/ai/api-catalog.d.ts +7 -1
  75. package/dist/types/ai/ask-context.d.ts +14 -7
  76. package/dist/types/ai/ask.d.ts +43 -43
  77. package/dist/types/ai/component-markdown.d.ts +4 -4
  78. package/dist/types/ai/index.d.ts +3 -3
  79. package/dist/types/ai/openapi-components.d.ts +1 -1
  80. package/dist/types/ai/relative-links.d.ts +9 -4
  81. package/dist/types/ai/serializers.d.ts +1 -1
  82. package/dist/types/ai/static-expression.d.ts +28 -0
  83. package/dist/types/ai/visibility.d.ts +1 -1
  84. package/dist/types/analytics/databuddy.d.ts +43 -0
  85. package/dist/types/analytics/index.d.ts +2 -0
  86. package/dist/types/analytics/schema.d.ts +14 -0
  87. package/dist/types/cli/env.d.ts +5 -0
  88. package/dist/types/cli/init/scaffold.d.ts +19 -3
  89. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  90. package/dist/types/core/base-path.d.ts +9 -0
  91. package/dist/types/core/config-input.d.ts +21 -21
  92. package/dist/types/core/config.d.ts +5 -5
  93. package/dist/types/core/data.d.ts +3 -3
  94. package/dist/types/core/graph.d.ts +2 -0
  95. package/dist/types/core/i18n-ui.d.ts +37 -8
  96. package/dist/types/core/i18n.d.ts +7 -1
  97. package/dist/types/core/links.d.ts +3 -1
  98. package/dist/types/core/load-module.d.ts +10 -0
  99. package/dist/types/core/locale-links.d.ts +12 -2
  100. package/dist/types/core/meta.d.ts +5 -1
  101. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  102. package/dist/types/core/navigation.d.ts +38 -0
  103. package/dist/types/core/ordering-prefix.d.ts +4 -0
  104. package/dist/types/core/safe-links.d.ts +3 -1
  105. package/dist/types/core/schema.d.ts +57 -18
  106. package/dist/types/core/sources/github-releases.d.ts +5 -0
  107. package/dist/types/core/sources/lower.d.ts +14 -2
  108. package/dist/types/core/sources/normalize.d.ts +10 -1
  109. package/dist/types/core/sources/remote.d.ts +11 -1
  110. package/dist/types/core/sources/resolve.d.ts +12 -0
  111. package/dist/types/core/sources/types.d.ts +31 -0
  112. package/dist/types/core/types.d.ts +9 -0
  113. package/dist/types/core/unrecognized-keys.d.ts +1 -1
  114. package/dist/types/deploy/adapters/node.d.ts +5 -2
  115. package/dist/types/deploy/adapters/types.d.ts +7 -0
  116. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  117. package/dist/types/deploy/headers.d.ts +31 -7
  118. package/dist/types/deploy/node-headers.d.ts +43 -8
  119. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  120. package/dist/types/deploy/platforms/node.d.ts +6 -5
  121. package/dist/types/deploy/platforms/types.d.ts +7 -0
  122. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  123. package/dist/types/deploy/redirects.d.ts +7 -2
  124. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  125. package/dist/types/openapi/model.d.ts +20 -6
  126. package/dist/types/search/documents.d.ts +1 -1
  127. package/dist/types/search/orama-index.d.ts +1 -1
  128. package/dist/types/search/sync/algolia.d.ts +3 -1
  129. package/docs/01-quickstart.mdx +3 -2
  130. package/docs/02-deployment.mdx +21 -12
  131. package/docs/03-upgrading.mdx +22 -9
  132. package/docs/04-migrating.mdx +4 -4
  133. package/docs/08-faq.mdx +12 -5
  134. package/docs/advanced/changelog.mdx +1 -1
  135. package/docs/advanced/custom-pages.mdx +5 -3
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +5 -5
  138. package/docs/cli/doctor.mdx +4 -4
  139. package/docs/cli/evals.mdx +9 -9
  140. package/docs/cli/index.mdx +6 -3
  141. package/docs/cli/translate.mdx +8 -8
  142. package/docs/configuration/analytics.mdx +23 -2
  143. package/docs/configuration/{ask-ai.mdx → assistant.mdx} +20 -20
  144. package/docs/configuration/customization.mdx +5 -4
  145. package/docs/configuration/index.mdx +7 -5
  146. package/docs/configuration/meta.ts +1 -1
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +1 -1
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -3
  151. package/docs/content/index.mdx +4 -2
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +5 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -12
  156. package/docs/content/versioning.mdx +1 -0
  157. package/docs/discoverability/agent-discovery.mdx +22 -8
  158. package/docs/discoverability/index.mdx +3 -3
  159. package/docs/discoverability/llms-txt.mdx +2 -5
  160. package/docs/discoverability/markdown.mdx +5 -3
  161. package/docs/discoverability/mcp.mdx +4 -0
  162. package/docs/discoverability/metadata.mdx +3 -2
  163. package/docs/discoverability/open-graph.mdx +6 -4
  164. package/docs/discoverability/rss.mdx +3 -1
  165. package/docs/index.mdx +2 -2
  166. package/docs/references/asyncapi.mdx +1 -1
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +3 -3
  169. package/package.json +1 -1
  170. package/skills/blume/SKILL.md +5 -5
  171. package/skills/blume-migrate/SKILL.md +6 -6
  172. package/skills/blume-migrate/references/docusaurus.md +6 -6
  173. package/skills/blume-migrate/references/mintlify.md +3 -3
  174. package/skills/blume-migrate/references/monorepo.md +1 -1
  175. package/skills/blume-migrate/references/nextra.md +1 -1
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +4 -4
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/api/paths.ts +1 -1
  182. package/src/ai/api/spec.ts +15 -2
  183. package/src/ai/api-catalog.ts +7 -1
  184. package/src/ai/ask-context.ts +21 -9
  185. package/src/ai/ask-data.ts +28 -14
  186. package/src/ai/ask.ts +84 -71
  187. package/src/ai/component-markdown.ts +43 -37
  188. package/src/ai/cors.ts +3 -3
  189. package/src/ai/index.ts +16 -16
  190. package/src/ai/link-headers.ts +8 -3
  191. package/src/ai/llms.ts +4 -3
  192. package/src/ai/markdown.ts +23 -11
  193. package/src/ai/mcp/server.ts +78 -7
  194. package/src/ai/openapi-components.ts +1 -1
  195. package/src/ai/relative-links.ts +78 -10
  196. package/src/ai/serializers.ts +1 -1
  197. package/src/ai/static-expression.ts +416 -0
  198. package/src/ai/visibility.ts +46 -15
  199. package/src/analytics/databuddy.ts +67 -0
  200. package/src/analytics/head.ts +4 -0
  201. package/src/analytics/index.ts +2 -0
  202. package/src/analytics/posthog.ts +21 -3
  203. package/src/analytics/schema.ts +2 -0
  204. package/src/astro/generate.ts +90 -45
  205. package/src/astro/module-types.ts +1 -1
  206. package/src/astro/runtime-deps.ts +6 -6
  207. package/src/astro/templates.ts +87 -49
  208. package/src/audit/catalog.ts +2 -2
  209. package/src/audit/checks/assets.ts +25 -5
  210. package/src/audit/checks/content.ts +20 -2
  211. package/src/audit/checks/i18n.ts +46 -8
  212. package/src/audit/checks/indexability.ts +39 -19
  213. package/src/audit/checks/links.ts +14 -0
  214. package/src/audit/checks/llms.ts +6 -3
  215. package/src/audit/checks/network.ts +3 -1
  216. package/src/audit/checks/og-image.ts +10 -0
  217. package/src/audit/checks/robots.ts +6 -1
  218. package/src/audit/checks/sitemap.ts +56 -31
  219. package/src/audit/checks/social.ts +21 -2
  220. package/src/audit/crawl.ts +88 -15
  221. package/src/audit/report.ts +54 -17
  222. package/src/audit/run.ts +1 -0
  223. package/src/audit/types.ts +18 -0
  224. package/src/audit/url.ts +37 -6
  225. package/src/blume-modules.d.ts +2 -2
  226. package/src/cli/build-failure.ts +50 -0
  227. package/src/cli/commands/audit.ts +8 -1
  228. package/src/cli/commands/build.ts +19 -5
  229. package/src/cli/commands/check.ts +2 -0
  230. package/src/cli/commands/doctor.ts +7 -5
  231. package/src/cli/commands/eval.ts +4 -10
  232. package/src/cli/commands/init.ts +15 -5
  233. package/src/cli/commands/migrate.ts +2 -2
  234. package/src/cli/commands/preview.ts +15 -0
  235. package/src/cli/commands/sync.ts +2 -0
  236. package/src/cli/commands/translate.ts +12 -9
  237. package/src/cli/commands/upgrade.ts +13 -2
  238. package/src/cli/eject-scripts.ts +32 -7
  239. package/src/cli/env.ts +12 -1
  240. package/src/cli/init/scaffold.ts +117 -11
  241. package/src/cli/init/starter-spec.ts +235 -0
  242. package/src/cli/required-secrets.ts +3 -3
  243. package/src/components/content/AccordionItem.astro +26 -22
  244. package/src/components/content/Badge.astro +2 -9
  245. package/src/components/content/Card.astro +2 -2
  246. package/src/components/content/Component.astro +9 -3
  247. package/src/components/content/Expandable.astro +5 -1
  248. package/src/components/content/Frame.astro +2 -7
  249. package/src/components/content/GithubInfo.astro +12 -2
  250. package/src/components/content/Prompt.astro +2 -7
  251. package/src/components/content/Tabs.astro +54 -5
  252. package/src/components/content/Tile.astro +1 -1
  253. package/src/components/content/Tooltip.astro +69 -7
  254. package/src/components/content/Tree.astro +7 -2
  255. package/src/components/content/TypeTable.astro +10 -5
  256. package/src/components/content/Update.astro +8 -2
  257. package/src/components/content/auto-type-table.ts +4 -1
  258. package/src/components/content/badge-color.ts +17 -0
  259. package/src/components/content/base-href.ts +18 -3
  260. package/src/components/content/inline-markdown.ts +27 -7
  261. package/src/components/copy-feedback.ts +36 -9
  262. package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
  263. package/src/components/islands/{ask-ai.tsx → assistant.tsx} +39 -25
  264. package/src/components/islands/hooks.ts +21 -14
  265. package/src/components/islands/webmcp.ts +12 -8
  266. package/src/components/layout/Banner.astro +23 -4
  267. package/src/components/layout/Header.astro +32 -19
  268. package/src/components/layout/Logo.astro +5 -0
  269. package/src/components/layout/NavSelector.astro +6 -1
  270. package/src/components/layout/NavTabMenu.astro +133 -0
  271. package/src/components/layout/NavTree.astro +15 -6
  272. package/src/components/layout/NavTreeCache.astro +5 -2
  273. package/src/components/layout/NavTreeScript.astro +45 -5
  274. package/src/components/layout/PageLayout.astro +28 -14
  275. package/src/components/layout/Pagination.astro +7 -7
  276. package/src/components/layout/ReferenceLayout.astro +5 -2
  277. package/src/components/layout/RootLayout.astro +27 -14
  278. package/src/components/layout/Search.astro +18 -14
  279. package/src/components/layout/analytics-client.ts +3 -1
  280. package/src/components/layout/drawer-inert.ts +1 -1
  281. package/src/components/openapi/GraphqlType.astro +11 -3
  282. package/src/components/openapi/MessageComposer.astro +1 -1
  283. package/src/components/openapi/Operation.astro +21 -5
  284. package/src/components/openapi/PanelTabs.astro +4 -1
  285. package/src/components/openapi/Playground.astro +8 -2
  286. package/src/components/openapi/RequestPanel.astro +9 -5
  287. package/src/components/openapi/SchemaProperty.astro +11 -33
  288. package/src/components/openapi/SchemaTable.astro +27 -52
  289. package/src/components/openapi/description.ts +2 -2
  290. package/src/components/openapi/helpers.ts +90 -12
  291. package/src/components/openapi/message-composer.ts +8 -0
  292. package/src/components/openapi/message-model.ts +12 -2
  293. package/src/components/openapi/message.ts +4 -1
  294. package/src/components/openapi/operation-model.ts +40 -8
  295. package/src/components/openapi/panel.ts +29 -5
  296. package/src/components/openapi/playground-client.ts +37 -7
  297. package/src/components/openapi/playground-schema.ts +25 -6
  298. package/src/components/openapi/request.ts +2 -0
  299. package/src/components/openapi/schema-tree.ts +203 -0
  300. package/src/components/openapi/snippets.ts +30 -15
  301. package/src/components/openapi/validate-json.ts +1 -1
  302. package/src/core/base-path.ts +15 -0
  303. package/src/core/code-fences.ts +1 -1
  304. package/src/core/config-input.ts +23 -23
  305. package/src/core/config.ts +21 -7
  306. package/src/core/data.ts +3 -3
  307. package/src/core/diagnostics.ts +217 -30
  308. package/src/core/graph.ts +120 -11
  309. package/src/core/i18n-ui.ts +74 -11
  310. package/src/core/i18n.ts +13 -2
  311. package/src/core/links.ts +7 -4
  312. package/src/core/load-module.ts +20 -0
  313. package/src/core/locale-links.ts +17 -17
  314. package/src/core/manifest.ts +3 -2
  315. package/src/core/meta.ts +69 -6
  316. package/src/core/nav-diagnostics.ts +56 -1
  317. package/src/core/navigation.ts +245 -76
  318. package/src/core/ordering-prefix.ts +27 -0
  319. package/src/core/project-graph.ts +23 -1
  320. package/src/core/request-body.ts +1 -1
  321. package/src/core/safe-href.ts +53 -1
  322. package/src/core/safe-links.ts +11 -2
  323. package/src/core/schema.ts +95 -34
  324. package/src/core/server-features.ts +2 -2
  325. package/src/core/sources/assets.ts +83 -41
  326. package/src/core/sources/contentful-rich-text.ts +3 -2
  327. package/src/core/sources/contentful.ts +25 -12
  328. package/src/core/sources/filesystem.ts +5 -1
  329. package/src/core/sources/github-releases.ts +106 -5
  330. package/src/core/sources/lexical.ts +9 -4
  331. package/src/core/sources/lower.ts +79 -26
  332. package/src/core/sources/mdx-remote.ts +1 -0
  333. package/src/core/sources/normalize.ts +132 -30
  334. package/src/core/sources/notion.ts +26 -10
  335. package/src/core/sources/obsidian.ts +17 -3
  336. package/src/core/sources/payload.ts +1 -0
  337. package/src/core/sources/portable-text.ts +63 -44
  338. package/src/core/sources/remote.ts +18 -2
  339. package/src/core/sources/resolve.ts +52 -33
  340. package/src/core/sources/sanity.ts +1 -0
  341. package/src/core/sources/strapi-blocks.ts +9 -4
  342. package/src/core/sources/strapi.ts +1 -0
  343. package/src/core/sources/types.ts +31 -0
  344. package/src/core/types.ts +9 -0
  345. package/src/core/ui-packs/ar.ts +17 -5
  346. package/src/core/ui-packs/bg.ts +17 -5
  347. package/src/core/ui-packs/bn.ts +17 -5
  348. package/src/core/ui-packs/ca.ts +17 -5
  349. package/src/core/ui-packs/cs.ts +17 -5
  350. package/src/core/ui-packs/da.ts +17 -5
  351. package/src/core/ui-packs/de.ts +17 -5
  352. package/src/core/ui-packs/el.ts +17 -5
  353. package/src/core/ui-packs/es.ts +17 -5
  354. package/src/core/ui-packs/fa.ts +17 -5
  355. package/src/core/ui-packs/fi.ts +17 -5
  356. package/src/core/ui-packs/fr.ts +17 -5
  357. package/src/core/ui-packs/he.ts +17 -5
  358. package/src/core/ui-packs/hi.ts +17 -5
  359. package/src/core/ui-packs/hr.ts +17 -5
  360. package/src/core/ui-packs/hu.ts +17 -5
  361. package/src/core/ui-packs/id.ts +17 -5
  362. package/src/core/ui-packs/it.ts +17 -5
  363. package/src/core/ui-packs/ja.ts +17 -5
  364. package/src/core/ui-packs/ko.ts +17 -5
  365. package/src/core/ui-packs/nl.ts +17 -5
  366. package/src/core/ui-packs/no.ts +17 -5
  367. package/src/core/ui-packs/pl.ts +17 -5
  368. package/src/core/ui-packs/pt-br.ts +17 -5
  369. package/src/core/ui-packs/pt.ts +17 -5
  370. package/src/core/ui-packs/ro.ts +17 -5
  371. package/src/core/ui-packs/ru.ts +17 -5
  372. package/src/core/ui-packs/sk.ts +17 -5
  373. package/src/core/ui-packs/sr.ts +17 -5
  374. package/src/core/ui-packs/sv.ts +17 -5
  375. package/src/core/ui-packs/th.ts +17 -5
  376. package/src/core/ui-packs/tr.ts +17 -5
  377. package/src/core/ui-packs/uk.ts +17 -5
  378. package/src/core/ui-packs/vi.ts +17 -5
  379. package/src/core/ui-packs/zh-tw.ts +17 -5
  380. package/src/core/ui-packs/zh.ts +17 -5
  381. package/src/core/unrecognized-keys.ts +1 -1
  382. package/src/core/version-cut.ts +17 -2
  383. package/src/core/versions.ts +4 -1
  384. package/src/deploy/adapters/node.ts +5 -2
  385. package/src/deploy/adapters/registry.ts +2 -1
  386. package/src/deploy/adapters/types.ts +13 -1
  387. package/src/deploy/artifacts.ts +25 -5
  388. package/src/deploy/cloudflare-negotiation.ts +23 -4
  389. package/src/deploy/headers.ts +67 -51
  390. package/src/deploy/node-headers.ts +148 -27
  391. package/src/deploy/platforms/cloudflare.ts +1 -0
  392. package/src/deploy/platforms/netlify.ts +82 -5
  393. package/src/deploy/platforms/node.ts +7 -5
  394. package/src/deploy/platforms/static.ts +1 -0
  395. package/src/deploy/platforms/types.ts +7 -0
  396. package/src/deploy/platforms/vercel.ts +9 -3
  397. package/src/deploy/redirects.ts +14 -3
  398. package/src/deploy/vercel-negotiation.ts +35 -3
  399. package/src/eval/agents.ts +10 -2
  400. package/src/eval/run.ts +25 -0
  401. package/src/markdown/base-links.ts +55 -8
  402. package/src/markdown/index.ts +6 -3
  403. package/src/markdown/relative-links.ts +3 -23
  404. package/src/markdown/route-snapshot.ts +37 -0
  405. package/src/og/card.ts +20 -4
  406. package/src/og/derive.ts +145 -4
  407. package/src/openapi/graphql-build.ts +28 -2
  408. package/src/openapi/model.ts +77 -13
  409. package/src/openapi/render-mdx.ts +10 -1
  410. package/src/registry/eject.ts +135 -37
  411. package/src/search/documents.ts +24 -3
  412. package/src/search/orama-index.ts +1 -1
  413. package/src/search/sync/algolia.ts +36 -2
  414. package/src/sources/registry.ts +5 -0
  415. package/src/theme/entry.ts +11 -4
  416. package/src/translate/agents.ts +6 -1
  417. package/src/translate/ledger.ts +26 -3
  418. package/src/translate/meta.ts +11 -3
  419. package/src/translate/report.ts +1 -1
  420. package/src/translate/run.ts +11 -5
  421. package/src/translate/validate.ts +10 -1
  422. package/src/translate/work-list.ts +36 -3
  423. package/src/upgrade/upgrade.ts +37 -5
  424. package/dist/cli/chunk-1w8dp3qb.js.map +0 -10
  425. package/dist/cli/chunk-5a2z0198.js.map +0 -10
  426. package/dist/cli/chunk-6k8vp3ta.js.map +0 -19
  427. package/dist/cli/chunk-79njf86q.js.map +0 -11
  428. package/dist/cli/chunk-a9kptbw5.js.map +0 -14
  429. package/dist/cli/chunk-abh8yjkn.js +0 -31
  430. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  431. package/dist/cli/chunk-bctazmbk.js.map +0 -10
  432. package/dist/cli/chunk-beat36xx.js.map +0 -10
  433. package/dist/cli/chunk-d1tadaw7.js.map +0 -10
  434. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  435. package/dist/cli/chunk-j8mw0za6.js.map +0 -35
  436. package/dist/cli/chunk-mnqj32sj.js.map +0 -12
  437. package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
  438. package/dist/cli/chunk-nk3ts2xk.js +0 -51
  439. package/dist/cli/chunk-nk3ts2xk.js.map +0 -10
  440. package/dist/cli/chunk-sqn5t4q0.js.map +0 -10
  441. package/dist/cli/chunk-tzne8qfq.js.map +0 -10
  442. package/dist/cli/chunk-xaz13gwg.js.map +0 -182
  443. package/dist/cli/chunk-z01ze5c1.js.map +0 -10
  444. package/dist/cli/chunk-z1f5arsg.js.map +0 -36
  445. package/dist/cli/chunk-zg2gtj10.js.map +0 -35
  446. /package/dist/cli/{chunk-f2972sbt.js.map → chunk-00gs3wqs.js.map} +0 -0
  447. /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-bbnwccaz.js.map} +0 -0
  448. /package/dist/cli/{chunk-zxccj738.js.map → chunk-esh98wmb.js.map} +0 -0
  449. /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-fsmrqk8a.js.map} +0 -0
  450. /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-g698a744.js.map} +0 -0
  451. /package/dist/cli/{chunk-41za066z.js.map → chunk-h2ez8dzb.js.map} +0 -0
  452. /package/dist/cli/{chunk-by2290sx.js.map → chunk-h7k3nq3v.js.map} +0 -0
  453. /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-n1yg3tj3.js.map} +0 -0
  454. /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-wdrt2k2v.js.map} +0 -0
@@ -11,6 +11,58 @@ const SAFE_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
11
11
 
12
12
  const SCHEME = /^(?<scheme>[a-z][\d+.a-z-]*):/iu;
13
13
 
14
+ // Markdown decodes a destination before the browser sees it: a backslash
15
+ // escape (`javascript\:`) and a character reference (`java&#115;cript:`,
16
+ // `javascript&colon;`) both reach the `href` as the plain character, so the
17
+ // check reads the destination decoded. Of HTML's named references, these are
18
+ // the only ones that decode to a scheme character, a colon, or a character
19
+ // the URL parser strips; every other one leaves the scheme a browser reads
20
+ // unchanged.
21
+ const MARKDOWN_DECODED =
22
+ /\\(?<escaped>[!-/:-@[-`{-~])|&(?:#[Xx](?<hex>[\dA-Fa-f]{1,6})|#(?<decimal>\d{1,7})|(?<named>colon|fjlig|NewLine|period|plus|Tab));/gu;
23
+
24
+ const NAMED_REFERENCES = new Map([
25
+ ["colon", ":"],
26
+ ["fjlig", "fj"],
27
+ ["NewLine", "\n"],
28
+ ["period", "."],
29
+ ["plus", "+"],
30
+ ["Tab", "\t"],
31
+ ]);
32
+
33
+ const MAX_CODE_POINT = 0x10_ff_ff;
34
+ const SURROGATES = { end: 0xdf_ff, start: 0xd8_00 };
35
+
36
+ /** A numeric reference's character; U+FFFD for NUL, surrogates, and overflow. */
37
+ const fromReference = (code: number): string =>
38
+ code === 0 ||
39
+ code > MAX_CODE_POINT ||
40
+ (code >= SURROGATES.start && code <= SURROGATES.end)
41
+ ? "�"
42
+ : String.fromCodePoint(code);
43
+
44
+ const decodeMarkdown = (href: string): string =>
45
+ href.replaceAll(
46
+ MARKDOWN_DECODED,
47
+ (
48
+ _match: string,
49
+ escaped?: string,
50
+ hex?: string,
51
+ decimal?: string,
52
+ named?: string
53
+ ) => {
54
+ if (escaped !== undefined) {
55
+ return escaped;
56
+ }
57
+ if (named !== undefined) {
58
+ return NAMED_REFERENCES.get(named) ?? "";
59
+ }
60
+ const code =
61
+ hex === undefined ? Number(decimal) : Number.parseInt(hex, 16);
62
+ return fromReference(code);
63
+ }
64
+ );
65
+
14
66
  // The URL parser removes ASCII tabs and newlines anywhere, and strips leading
15
67
  // C0 controls and spaces, before it reads a scheme — so `java\tscript:` is a
16
68
  // `javascript:` URL to a browser, and the check must see it the same way.
@@ -18,7 +70,7 @@ const URL_IGNORED = /[\t\n\r]/gu;
18
70
 
19
71
  /** Whether a link destination from imported content is safe to keep. */
20
72
  export const isSafeHref = (href: string): boolean => {
21
- const cleaned = href.replaceAll(URL_IGNORED, "");
73
+ const cleaned = decodeMarkdown(href).replaceAll(URL_IGNORED, "");
22
74
  let start = 0;
23
75
  while (start < cleaned.length && (cleaned.codePointAt(start) ?? 0) <= 0x20) {
24
76
  start += 1;
@@ -1,6 +1,8 @@
1
1
  import type { Nodes, Root } from "mdast";
2
2
  import { fromMarkdown } from "mdast-util-from-markdown";
3
+ import { gfmFromMarkdown } from "mdast-util-gfm";
3
4
  import { toString } from "mdast-util-to-string";
5
+ import { gfm } from "micromark-extension-gfm";
4
6
 
5
7
  import { isSafeHref } from "./safe-href.ts";
6
8
  import { escapeMarkdownText } from "./sources/lower.ts";
@@ -54,10 +56,17 @@ export const unsafeLinkSpans = (tree: Root): UnsafeLinkSpan[] => {
54
56
  /**
55
57
  * Markdown with every unsafe link reduced to its label text and every unsafe
56
58
  * link definition removed. Everything else — safe links, raw HTML, code —
57
- * passes through as written.
59
+ * passes through as written. The Markdown is read as GitHub-flavored, the way
60
+ * the site renders it: a link inside a footnote definition or a table cell is
61
+ * only a link to a GFM parser.
58
62
  */
59
63
  export const neutralizeUnsafeLinks = (markdown: string): string => {
60
- const spans = unsafeLinkSpans(fromMarkdown(markdown));
64
+ const spans = unsafeLinkSpans(
65
+ fromMarkdown(markdown, {
66
+ extensions: [gfm()],
67
+ mdastExtensions: [gfmFromMarkdown()],
68
+ })
69
+ );
61
70
  let out = "";
62
71
  let cursor = 0;
63
72
  for (const span of spans) {
@@ -2,9 +2,9 @@ import type { AstroIntegration } from "astro";
2
2
  import { z } from "zod";
3
3
 
4
4
  import {
5
- askAdapterSchema,
6
- askMovedFieldsHint,
7
- DEFAULT_ASK_PROVIDER,
5
+ assistantAdapterSchema,
6
+ assistantMovedFieldsHint,
7
+ DEFAULT_ASSISTANT_PROVIDER,
8
8
  } from "../ai/ask.ts";
9
9
  import type { ComponentMarkdown } from "../ai/component-markdown.ts";
10
10
  import { analyticsConfigSchema } from "../analytics/schema.ts";
@@ -397,20 +397,27 @@ const contentConfigSchema = z
397
397
  */
398
398
  types: z.record(z.string(), contentTypeConfigSchema).default({}),
399
399
  })
400
- .superRefine((value, ctx) => {
401
- if (!value.sources) {
402
- return;
403
- }
404
- for (const key of FILESYSTEM_SHORTHAND_KEYS) {
405
- if (value[key] !== undefined) {
406
- ctx.addIssue({
407
- code: z.ZodIssueCode.custom,
408
- message: `content.${key} is shorthand for a single filesystem() source and can't be combined with content.sources — move it into a filesystem({ ${key} }) entry in content.sources.`,
409
- path: [key],
410
- });
400
+ .superRefine(
401
+ (value, ctx) => {
402
+ if (!value.sources) {
403
+ return;
411
404
  }
412
- }
413
- })
405
+ for (const key of FILESYSTEM_SHORTHAND_KEYS) {
406
+ if (value[key] !== undefined) {
407
+ ctx.addIssue({
408
+ code: z.ZodIssueCode.custom,
409
+ message: `content.${key} is shorthand for a single filesystem() source and can't be combined with content.sources — move it into a filesystem({ ${key} }) entry in content.sources.`,
410
+ path: [key],
411
+ });
412
+ }
413
+ }
414
+ },
415
+ // Zod skips a refinement once a field has failed, so without this a
416
+ // Blume 1 config (`{ type: "filesystem" }` sources beside `content.root`)
417
+ // would only learn about the shorthand after fixing every source entry.
418
+ // Any object gets the check; a non-object already failed on its own.
419
+ { when: (payload) => isObjectLike(payload.value) }
420
+ )
414
421
  .transform(({ exclude, include, root, sources, ...rest }) => ({
415
422
  ...rest,
416
423
  sources: sources ?? [
@@ -456,7 +463,11 @@ const navTabSchema = z.strictObject({
456
463
  )
457
464
  .optional(),
458
465
  label: localizableLabelSchema,
459
- path: z.string(),
466
+ // Canonical like the routes it's matched against — one leading slash, no
467
+ // trailing one — so `/guides/` still marks its pages' tab and scopes their
468
+ // sidebar, and `api` links to `/api` rather than resolving relative to the
469
+ // current page.
470
+ path: z.string().transform(normalizeRoute),
460
471
  });
461
472
 
462
473
  const navSelectorItemSchema = z.strictObject({
@@ -744,7 +755,7 @@ const mcpConfigSchema = z.strictObject({
744
755
  route: z.string().default("/mcp").transform(normalizeRoute),
745
756
  });
746
757
 
747
- const askEndpointSchema = z
758
+ const assistantEndpointSchema = z
748
759
  .string()
749
760
  .trim()
750
761
  .min(1)
@@ -762,7 +773,7 @@ const askEndpointSchema = z
762
773
  },
763
774
  {
764
775
  message:
765
- "ai.ask.endpoint must be an HTTP(S) URL or a root-relative path.",
776
+ "ai.assistant.endpoint must be an HTTP(S) URL or a root-relative path.",
766
777
  }
767
778
  );
768
779
 
@@ -799,7 +810,7 @@ type AiCatalogResolved = z.output<typeof aiCatalogObjectSchema>;
799
810
 
800
811
  /**
801
812
  * The keys that moved from `ai` to `agents`: `ai` now holds only what faces a
802
- * model at read time (Ask AI, Open in chat), and the machine-readable surface
813
+ * model at read time (the assistant, Open in chat), and the machine-readable surface
803
814
  * agents consume lives under `agents`.
804
815
  */
805
816
  const MOVED_TO_AGENTS = [
@@ -821,9 +832,9 @@ const movedToAgentsHints = (from: string): Record<string, string> =>
821
832
  ])
822
833
  );
823
834
 
824
- /** Model-facing config: the Ask AI assistant and the "Open in chat" action. */
835
+ /** Model-facing config: the assistant and the "Open in chat" action. */
825
836
  const aiConfigFields = {
826
- ask: z
837
+ assistant: z
827
838
  .strictObject(
828
839
  {
829
840
  // Origins allowed to call the generated `/api/ask` from another site (a
@@ -843,9 +854,9 @@ const aiConfigFields = {
843
854
  .optional(),
844
855
  enabled: z.boolean().default(false),
845
856
  // Optional external endpoint for projects that keep their docs static
846
- // and host Ask AI in an existing backend. Absolute URLs and root-relative
857
+ // and host the assistant in an existing backend. Absolute URLs and root-relative
847
858
  // paths are both valid; the built-in request/stream contract is unchanged.
848
- endpoint: askEndpointSchema.optional(),
859
+ endpoint: assistantEndpointSchema.optional(),
849
860
  // Extra system-prompt text (identity, language, tone) appended to the
850
861
  // built-in instructions, so the grounding contract — answer from the
851
862
  // retrieved excerpts, cite pages as Markdown links — stays intact.
@@ -853,9 +864,9 @@ const aiConfigFields = {
853
864
  // The adapter descriptor a `gateway()`/`openrouter()`/... factory
854
865
  // returns; each adapter validates its own options (model, key env var,
855
866
  // reasoning mapping, `providerOptions` passthrough) in `ai/ask.ts`.
856
- // Unset means the gateway with its default model, so zero-config Ask AI
867
+ // Unset means the gateway with its default model, so the zero-config assistant
857
868
  // is unchanged.
858
- provider: askAdapterSchema.prefault(DEFAULT_ASK_PROVIDER),
869
+ provider: assistantAdapterSchema.prefault(DEFAULT_ASSISTANT_PROVIDER),
859
870
  // How much documentation each question carries. Injected characters are
860
871
  // the dominant term in time-to-first-token on a self-hosted backend, so
861
872
  // these trade recall for latency. No zod defaults here: only what the
@@ -880,7 +891,7 @@ const aiConfigFields = {
880
891
  )
881
892
  .default([]),
882
893
  },
883
- askMovedFieldsHint
894
+ assistantMovedFieldsHint
884
895
  )
885
896
  .superRefine((value, ctx) => {
886
897
  // `cors` configures the generated route, which an external `endpoint`
@@ -889,7 +900,7 @@ const aiConfigFields = {
889
900
  ctx.addIssue({
890
901
  code: z.ZodIssueCode.custom,
891
902
  message:
892
- "ai.ask.cors only applies to the generated route; with ai.ask.endpoint, CORS is that backend's job.",
903
+ "ai.assistant.cors only applies to the generated route; with ai.assistant.endpoint, CORS is that backend's job.",
893
904
  path: ["cors"],
894
905
  });
895
906
  }
@@ -921,7 +932,10 @@ const aiConfigFields = {
921
932
 
922
933
  const aiConfigSchema = z.strictObject(
923
934
  aiConfigFields,
924
- removedKeysHint(movedToAgentsHints("ai"))
935
+ removedKeysHint({
936
+ ...movedToAgentsHints("ai"),
937
+ ask: "ai.ask was renamed to ai.assistant.",
938
+ })
925
939
  );
926
940
 
927
941
  /**
@@ -988,9 +1002,11 @@ const navigationConfigSchema = z.strictObject({
988
1002
  tabs: z.array(navTabSchema).default([]),
989
1003
  });
990
1004
 
991
- export { askReasoningLevels } from "../ai/ask.ts";
992
- export type { AskReasoning } from "../ai/ask.ts";
993
- export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
1005
+ export { assistantReasoningLevels } from "../ai/ask.ts";
1006
+ export type { AssistantReasoning } from "../ai/ask.ts";
1007
+ export type AssistantConfig = NonNullable<
1008
+ z.infer<typeof aiConfigSchema>["assistant"]
1009
+ >;
994
1010
  export { openInChatProviders } from "./open-in-chat.ts";
995
1011
  export type { OpenInChatProvider } from "./open-in-chat.ts";
996
1012
 
@@ -1149,9 +1165,30 @@ const versionsConfigSchema = z
1149
1165
  const REDIRECT_PATTERN = /(?:^|\/):[A-Za-z_]|\*/u;
1150
1166
  const URL_ORIGIN = /^[a-z][\d+.a-z-]*:\/\/[^/]*/iu;
1151
1167
 
1168
+ /**
1169
+ * The shape each end must start with: `from` is always a path on this site,
1170
+ * `to` may also leave it. A path without its leading slash would never gain
1171
+ * `basePath` or the deployment base, and hosts read it relative to nothing.
1172
+ */
1173
+ const REDIRECT_START = {
1174
+ from: {
1175
+ allows: (path: string) => path.startsWith("/"),
1176
+ message:
1177
+ "redirects take root-relative paths: `from` must start with `/` (`/old-page`, not `old-page`).",
1178
+ },
1179
+ to: {
1180
+ allows: (path: string) => path.startsWith("/") || URL_ORIGIN.test(path),
1181
+ message:
1182
+ "redirects take root-relative paths or full URLs: `to` must start with `/` (`/new-page`, not `new-page`) or be an absolute URL (`https://…`).",
1183
+ },
1184
+ };
1185
+
1152
1186
  const exactRedirectPath = (end: "from" | "to") =>
1153
1187
  z
1154
1188
  .string()
1189
+ .refine(REDIRECT_START[end].allows, {
1190
+ message: REDIRECT_START[end].message,
1191
+ })
1155
1192
  .refine((path) => !REDIRECT_PATTERN.test(path.replace(URL_ORIGIN, "")), {
1156
1193
  message: `redirects take exact paths: \`${end}\` can't hold a \`:param\` segment or a \`*\` wildcard. Add one redirect per path, or put pattern rules in your host's redirect config (vercel.json, _redirects).`,
1157
1194
  });
@@ -1415,7 +1452,7 @@ const seoConfigSchema = z.strictObject(
1415
1452
  /**
1416
1453
  * The machine-readable surface agents consume: the JSON API, `llms.txt`, the
1417
1454
  * MCP server, published skills, discovery manifests, and the robots.txt
1418
- * usage policy. Everything reader-facing that talks to a model (Ask AI, Open
1455
+ * usage policy. Everything reader-facing that talks to a model (the assistant, Open
1419
1456
  * in chat) stays under `ai`.
1420
1457
  */
1421
1458
  const agentsConfigSchema = z.strictObject({
@@ -1640,6 +1677,16 @@ const lastModifiedConfigSchema = z.union(
1640
1677
  * build machine's zone. `dateStyle` is a preset that can't be combined with the
1641
1678
  * individual component fields (`year`, `month`, …), matching `Intl`'s own rule.
1642
1679
  */
1680
+ /** The `dateFormat` fields that choose the date's shape (length, components). */
1681
+ const DATE_FORM_FIELDS = [
1682
+ "dateStyle",
1683
+ "weekday",
1684
+ "era",
1685
+ "year",
1686
+ "month",
1687
+ "day",
1688
+ ] as const;
1689
+
1643
1690
  const dateFormatConfigSchema = z
1644
1691
  .strictObject({
1645
1692
  /** Calendar system (e.g. `japanese`, `buddhist`). */
@@ -1673,6 +1720,14 @@ const dateFormatConfigSchema = z
1673
1720
  message:
1674
1721
  "dateFormat.dateStyle can't be combined with weekday/era/year/month/day; use one or the other.",
1675
1722
  }
1723
+ )
1724
+ // `timeZone`, `calendar`, and `numberingSystem` don't pick a shape, so a
1725
+ // format that sets only those keeps the long default instead of falling to
1726
+ // Intl's bare numeric date (`7/21/2026`).
1727
+ .transform((value) =>
1728
+ DATE_FORM_FIELDS.some((field) => value[field] !== undefined)
1729
+ ? value
1730
+ : { ...value, dateStyle: "long" as const }
1676
1731
  );
1677
1732
 
1678
1733
  /** Code rendering options (`markdown.code`). */
@@ -1731,7 +1786,7 @@ const reactConfigSchema = z.strictObject({
1731
1786
  /**
1732
1787
  * Auto-memoize React components/hooks with the React Compiler
1733
1788
  * (`babel-plugin-react-compiler`). On by default whenever React is enabled
1734
- * (a project `.tsx`/`.jsx`, a React island/example/override, or Ask AI); set
1789
+ * (a project `.tsx`/`.jsx`, a React island/example/override, or the assistant); set
1735
1790
  * to `false` to skip the compiler's babel pass.
1736
1791
  */
1737
1792
  compiler: z.boolean().default(true),
@@ -1801,6 +1856,12 @@ export const blumeConfigSchema = z
1801
1856
  */
1802
1857
  basePath: z
1803
1858
  .string()
1859
+ // Normalizing would turn a URL into `/https:/docs.example.com/docs`
1860
+ // and keep a query or fragment inside every route.
1861
+ .refine((value) => !URL_ORIGIN.test(value) && !/[#?]/u.test(value), {
1862
+ message:
1863
+ 'basePath takes a path like "/docs", not a URL: no scheme, host, query, or fragment. The site\'s origin goes in deployment.site.',
1864
+ })
1804
1865
  .optional()
1805
1866
  .transform((value) => normalizeBasePath(value)),
1806
1867
  content: contentConfigSchema.prefault({}),
@@ -7,8 +7,8 @@ import type { ResolvedConfig } from "./schema.ts";
7
7
  */
8
8
  export const serverFeatures = (config: ResolvedConfig): string[] => {
9
9
  const features: string[] = [];
10
- if (config.ai.ask?.enabled && !config.ai.ask.endpoint) {
11
- features.push("Ask AI");
10
+ if (config.ai.assistant?.enabled && !config.ai.assistant.endpoint) {
11
+ features.push("Assistant");
12
12
  }
13
13
  // The hosted MCP server is a live JSON-RPC endpoint, so it needs a runtime.
14
14
  if (config.agents.mcp.enabled) {
@@ -13,7 +13,14 @@ import { extname, join } from "pathe";
13
13
  import type { Diagnostic } from "../types.ts";
14
14
  import { hashText } from "./cache.ts";
15
15
 
16
- const MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
16
+ // A Markdown image as the lowerers write it (`image()` in `lower.ts`): the
17
+ // alt text can hold backslash escapes (`\]`), and a destination holding
18
+ // spaces or parentheses sits in angle brackets, both with its own escapes.
19
+ const MD_IMAGE =
20
+ /!\[(?<alt>(?:\\.|[^\\\]])*)\]\((?:<(?<pointy>(?:\\.|[^\\<>\n])*)>|(?<bare>(?:\\.|[^\\\s)])+))\)/gu;
21
+ // A backslash escape inside a destination, which Markdown reads as the
22
+ // character it escapes.
23
+ const DESTINATION_ESCAPE = /\\(?<char>[!-/:-@[-`{-~])/gu;
17
24
  // A `<video>` tag's `src`, split so the rewrite can swap the URL and keep the
18
25
  // surrounding attributes untouched. Notion's uploaded videos arrive as signed,
19
26
  // expiring URLs exactly like its images, so they rot the same way. `[^>]`
@@ -21,35 +28,47 @@ const MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
21
28
  const HTML_VIDEO_SRC =
22
29
  /(?<open><video\b[^>]*?\ssrc=")(?<url>[^"]+)(?<close>")/gu;
23
30
  const REMOTE = /^https?:\/\//u;
24
- const SAFE_EXT = /^\.[a-z0-9]+$/iu;
25
31
  const CODE_FENCE_BLOCK =
26
32
  /^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
27
33
  // NUL delimiters cannot appear in authored markdown, so tokens never collide.
28
34
  // oxlint-disable-next-line no-control-regex -- the NUL is the collision guard.
29
35
  const FENCE_TOKEN = /\u0000blume-fence-(?<index>\d+)\u0000/gu;
30
36
 
31
- // The extension for a URL whose path carries none, keyed by the media type the
32
- // server reports. A static host serves by extension, so a `.png` holding JPEG
33
- // bytes is mislabeled and a `.png` holding a video is refused by strict
34
- // players — the response is the source of truth, not the reference kind.
37
+ // The extension an asset is saved under, keyed by the media type the server
38
+ // reports. Only images and videos are ever written: the file is served from
39
+ // the docs origin by its extension, so an HTML page (a Vimeo/Loom watch page,
40
+ // or a hostile "image" URL) saved as `.html` would render as a page on the
41
+ // site. A static host serves by extension, so a `.png` holding JPEG bytes is
42
+ // mislabeled and a `.png` holding a video is refused by strict players — the
43
+ // response is the source of truth, not the reference kind.
35
44
  const EXT_BY_MIME = new Map([
45
+ ["image/apng", ".apng"],
36
46
  ["image/avif", ".avif"],
47
+ ["image/bmp", ".bmp"],
37
48
  ["image/gif", ".gif"],
38
49
  ["image/jpeg", ".jpg"],
50
+ ["image/jpg", ".jpg"],
39
51
  ["image/png", ".png"],
40
52
  ["image/svg+xml", ".svg"],
53
+ ["image/tiff", ".tiff"],
54
+ ["image/vnd.microsoft.icon", ".ico"],
41
55
  ["image/webp", ".webp"],
56
+ ["image/x-icon", ".ico"],
42
57
  ["video/mp4", ".mp4"],
43
58
  ["video/ogg", ".ogv"],
44
59
  ["video/quicktime", ".mov"],
45
60
  ["video/webm", ".webm"],
46
61
  ]);
47
- const UNKNOWN_EXT = ".bin";
48
- // Response types that are never a media file. A pasted Vimeo/Loom/Wistia link
49
- // is a video block in Notion whose URL is a watch page (200 text/html), and an
50
- // API endpoint answers JSON or XML; `image/svg+xml` is media and stays.
51
- const NON_MEDIA_TYPE =
52
- /^(?:text\/|application\/(?:[\w.-]+\+)?(?:json|xml|javascript))/u;
62
+ // The same images and videos as a URL's path names them, for a response
63
+ // whose type says nothing about the bytes.
64
+ const MEDIA_EXTS = new Set([...EXT_BY_MIME.values(), ".jpeg"]);
65
+ // Response types that don't say what the bytes are; object stores answer
66
+ // uploads this way, so the URL's extension decides.
67
+ const OPAQUE_TYPES = new Set([
68
+ "",
69
+ "application/octet-stream",
70
+ "binary/octet-stream",
71
+ ]);
53
72
  const PART_SUFFIX = ".part";
54
73
  // Generous enough for a multi-hundred-megabyte recording on an ordinary
55
74
  // connection; its job is to fail a stalled download rather than hang the build.
@@ -78,13 +97,17 @@ export interface AssetContext {
78
97
  timeoutMs?: number;
79
98
  }
80
99
 
81
- /** The extension in a URL's path, or null when it carries none we'd trust. */
100
+ /** The image or video extension in a URL's path, or null when it names none. */
82
101
  const extFromUrl = (url: string): string | null => {
83
102
  const clean = url.split("?")[0] ?? url;
84
- const ext = extname(clean);
85
- return SAFE_EXT.test(ext) ? ext.toLowerCase() : null;
103
+ const ext = extname(clean).toLowerCase();
104
+ return MEDIA_EXTS.has(ext) ? ext : null;
86
105
  };
87
106
 
107
+ /** An image's destination as the URL it names, its escapes decoded. */
108
+ const imageUrl = (pointy?: string, bare?: string): string =>
109
+ (pointy ?? bare ?? "").replaceAll(DESTINATION_ESCAPE, "$<char>");
110
+
88
111
  /** The response's media type, lowercased and stripped of parameters. */
89
112
  const mediaType = (res: Response): string =>
90
113
  res.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
@@ -100,9 +123,10 @@ const exists = async (path: string): Promise<boolean> => {
100
123
 
101
124
  /**
102
125
  * The completed file for a stem whose extension came from an earlier response
103
- * rather than the URL, so an extension-less asset is not fetched on every
104
- * poll. Only a lone candidate counts: query-less URLs can collide, and picking
105
- * between two files would be a guess.
126
+ * rather than the URL, so the asset is not fetched on every poll. Only a lone
127
+ * image or video counts: query-less URLs can collide, picking between two
128
+ * files would be a guess, and a partial download (or any other file an older
129
+ * version saved) is never reused.
106
130
  */
107
131
  const completedFor = async (
108
132
  dir: string,
@@ -115,11 +139,28 @@ const completedFor = async (
115
139
  return null;
116
140
  }
117
141
  const candidates = names.filter(
118
- (name) => name.startsWith(`${stem}.`) && !name.endsWith(PART_SUFFIX)
142
+ (name) => name.startsWith(stem) && MEDIA_EXTS.has(name.slice(stem.length))
119
143
  );
120
144
  return candidates.length === 1 ? (candidates[0] ?? null) : null;
121
145
  };
122
146
 
147
+ /**
148
+ * The extension to save a response under: the image or video type it
149
+ * reports, or the URL's image or video extension when the type says nothing.
150
+ * Anything else is refused rather than written where the site would serve it.
151
+ */
152
+ const extFor = (type: string, urlExt: string | null): string => {
153
+ const ext = EXT_BY_MIME.get(type) ?? (OPAQUE_TYPES.has(type) ? urlExt : null);
154
+ if (ext) {
155
+ return ext;
156
+ }
157
+ throw new Error(
158
+ OPAQUE_TYPES.has(type)
159
+ ? `responded with ${type || "no content type"} and its URL names no image or video file`
160
+ : `responded with ${type}, not an image or video`
161
+ );
162
+ };
163
+
123
164
  /**
124
165
  * Download remote media referenced in a Markdown body into the asset dir and
125
166
  * rewrite the reference to the local public path. Markdown images and the
@@ -148,26 +189,26 @@ export const materializeAssets = async (
148
189
  return `\u0000blume-fence-${fences.length - 1}\u0000`;
149
190
  });
150
191
 
151
- const urls = new Set<string>();
152
- for (const pattern of [MD_IMAGE, HTML_VIDEO_SRC]) {
153
- for (const match of masked.matchAll(pattern)) {
154
- const url = match.groups?.url;
155
- if (url && REMOTE.test(url)) {
156
- urls.add(url);
157
- }
158
- }
159
- }
192
+ const referenced = [
193
+ ...[...masked.matchAll(MD_IMAGE)].map((match) =>
194
+ imageUrl(match.groups?.pointy, match.groups?.bare)
195
+ ),
196
+ ...[...masked.matchAll(HTML_VIDEO_SRC)].map(
197
+ (match) => match.groups?.url ?? ""
198
+ ),
199
+ ];
200
+ const urls = new Set(referenced.filter((url) => REMOTE.test(url)));
160
201
 
161
202
  /** Fetch one asset into the asset dir and return its file name. */
162
203
  const download = async (url: string, stem: string): Promise<string> => {
163
204
  const urlExt = extFromUrl(url);
164
- // The name is known up front whenever the path has an extension (every
165
- // Notion upload does), so a file from an earlier run is reused as is;
205
+ // The name is usually known up front from the path's extension (every
206
+ // Notion upload has one), so a file from an earlier run is reused as is;
166
207
  // otherwise the extension came from the last response, so look for it.
167
208
  if (urlExt && (await exists(join(ctx.assetsDir, `${stem}${urlExt}`)))) {
168
209
  return `${stem}${urlExt}`;
169
210
  }
170
- const completed = urlExt ? null : await completedFor(ctx.assetsDir, stem);
211
+ const completed = await completedFor(ctx.assetsDir, stem);
171
212
  if (completed) {
172
213
  return completed;
173
214
  }
@@ -176,15 +217,13 @@ export const materializeAssets = async (
176
217
  throw new Error(`${res.status}`);
177
218
  }
178
219
  // Writing a watch page or an API response out as `.mp4` gives a player
179
- // that can't play and a green build, so refuse what is never media.
180
- const type = mediaType(res);
181
- if (NON_MEDIA_TYPE.test(type)) {
182
- throw new Error(`responded with ${type}, not a media file`);
183
- }
220
+ // that can't play and a green build, and one saved as `.html` would be
221
+ // served as a page on the docs origin: only images and videos are kept.
222
+ const ext = extFor(mediaType(res), urlExt);
184
223
  if (!res.body) {
185
224
  throw new Error("empty response body");
186
225
  }
187
- const file = `${stem}${urlExt ?? EXT_BY_MIME.get(type) ?? UNKNOWN_EXT}`;
226
+ const file = `${stem}${ext}`;
188
227
  const target = join(ctx.assetsDir, file);
189
228
  await mkdir(ctx.assetsDir, { recursive: true });
190
229
  // Stream the body to disk rather than buffering it: a video is hundreds of
@@ -258,10 +297,13 @@ export const materializeAssets = async (
258
297
  );
259
298
 
260
299
  const rewritten = masked
261
- .replaceAll(MD_IMAGE, (match, alt, url) => {
262
- const local = rewrites.get(url);
263
- return local ? `![${alt}](${local})` : match;
264
- })
300
+ .replaceAll(
301
+ MD_IMAGE,
302
+ (match: string, alt: string, pointy?: string, bare?: string) => {
303
+ const local = rewrites.get(imageUrl(pointy, bare));
304
+ return local ? `![${alt}](${local})` : match;
305
+ }
306
+ )
265
307
  .replaceAll(HTML_VIDEO_SRC, (match, open, url, close) => {
266
308
  const local = rewrites.get(url);
267
309
  return local ? `${open}${local}${close}` : match;
@@ -13,6 +13,7 @@ import type { InlineMarks } from "./lower.ts";
13
13
  import {
14
14
  absoluteUrl,
15
15
  blockquote,
16
+ guardBlockStart,
16
17
  headingPrefix,
17
18
  image,
18
19
  joinBlocks,
@@ -234,11 +235,11 @@ const renderBlock = (
234
235
  const type = asString(node.nodeType) ?? "";
235
236
  const heading = HEADING.exec(type)?.groups?.level;
236
237
  if (heading) {
237
- return `${headingPrefix(Number(heading))}${renderInlines(children(node), options)}`;
238
+ return `${headingPrefix(Number(heading))}${guardBlockStart(renderInlines(children(node), options))}`;
238
239
  }
239
240
  switch (type) {
240
241
  case "paragraph": {
241
- return renderInlines(children(node), options);
242
+ return guardBlockStart(renderInlines(children(node), options));
242
243
  }
243
244
  case "blockquote": {
244
245
  return blockquote(