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
@@ -1,3 +1,4 @@
1
+ import { BlumeError } from "../diagnostics.ts";
1
2
  import {
2
3
  assetFromEntry,
3
4
  contentfulRichTextToMarkdown,
@@ -111,19 +112,29 @@ export const contentfulSource = (
111
112
  );
112
113
  };
113
114
 
114
- const fetchEntries = async (): Promise<SourceEntry[]> => {
115
- const preview = ctx?.preview ?? false;
116
- const deliveryToken = options.token ?? process.env.CONTENTFUL_ACCESS_TOKEN;
117
- const previewToken =
118
- options.previewToken ?? process.env.CONTENTFUL_PREVIEW_TOKEN;
119
- // The Preview API rejects delivery tokens, so falling back to one would
120
- // only trade a clear config error for a 401.
121
- if (preview && !previewToken) {
122
- throw new Error(
123
- "Contentful --preview needs a Preview API token: set previewToken or CONTENTFUL_PREVIEW_TOKEN."
124
- );
115
+ const preview = ctx?.preview ?? false;
116
+ const previewToken = (): string | undefined =>
117
+ options.previewToken ?? process.env.CONTENTFUL_PREVIEW_TOKEN;
118
+
119
+ // The Preview API rejects delivery tokens, so falling back to one would
120
+ // only trade a clear config error for a 401. Checked before the cache, so
121
+ // a warm snapshot can't stand in for the drafts `--preview` asked for.
122
+ const assertConfigured = (): void => {
123
+ if (preview && !previewToken()) {
124
+ throw new BlumeError({
125
+ code: "BLUME_SOURCE_MISCONFIGURED",
126
+ message: `Source "${options.name}": Contentful --preview needs a Preview API token.`,
127
+ severity: "error",
128
+ suggestion:
129
+ "Set previewToken on contentfulSource, or the CONTENTFUL_PREVIEW_TOKEN environment variable.",
130
+ });
125
131
  }
126
- const token = preview ? previewToken : deliveryToken;
132
+ };
133
+
134
+ const fetchEntries = async (): Promise<SourceEntry[]> => {
135
+ const token = preview
136
+ ? previewToken()
137
+ : (options.token ?? process.env.CONTENTFUL_ACCESS_TOKEN);
127
138
  const client: RestClient = {
128
139
  fetchImpl: options.fetchImpl,
129
140
  headers: token ? { authorization: `Bearer ${token}` } : {},
@@ -163,10 +174,12 @@ export const contentfulSource = (
163
174
 
164
175
  return remoteSource(
165
176
  {
177
+ assertConfigured,
166
178
  fetchEntries,
167
179
  name: options.name,
168
180
  pollInterval: options.pollInterval,
169
181
  prefix: options.prefix,
182
+ withContext: (next) => contentfulSource(options, next),
170
183
  },
171
184
  ctx
172
185
  );
@@ -83,7 +83,9 @@ export const filesystemSource = (
83
83
  file: contentRoot,
84
84
  message: `Content root not found: ${options.root}`,
85
85
  severity: "error",
86
- suggestion: `Create a "${options.root}" folder with at least one .md or .mdx file, or set content.root in blume.config.ts.`,
86
+ // `content.root` is rejected beside `content.sources`, so the hint
87
+ // names both places a root is set.
88
+ suggestion: `Create a "${options.root}" folder with at least one .md or .mdx file, or point Blume at your docs folder: content.root in blume.config.ts, or the filesystem() source's root when you list content.sources.`,
87
89
  });
88
90
  }
89
91
  };
@@ -113,6 +115,8 @@ export const filesystemSource = (
113
115
 
114
116
  return {
115
117
  contentRoot,
118
+ exclude: options.exclude,
119
+ include: options.include,
116
120
  load,
117
121
  name: options.name,
118
122
  prefix: options.prefix,
@@ -1,4 +1,4 @@
1
- import type { Heading, Nodes } from "mdast";
1
+ import type { Definition, Heading, Link, Nodes } from "mdast";
2
2
  import { fromMarkdown } from "mdast-util-from-markdown";
3
3
  import { gfmFromMarkdown } from "mdast-util-gfm";
4
4
  import { toString as mdastToString } from "mdast-util-to-string";
@@ -15,6 +15,7 @@ import {
15
15
  pollingWatch,
16
16
  snapshotCache,
17
17
  } from "./cache.ts";
18
+ import { destination, escapeMarkdownText } from "./lower.ts";
18
19
  import type {
19
20
  ContentSource,
20
21
  SourceContext,
@@ -43,6 +44,11 @@ export interface GithubReleasesSourceOptions {
43
44
  prereleases?: boolean;
44
45
  /** Repository name. */
45
46
  repo: string;
47
+ /**
48
+ * The site's own URL (`deployment.site`). A link in the notes back to it
49
+ * becomes root-relative, the way a page links to another page.
50
+ */
51
+ site?: string;
46
52
  }
47
53
 
48
54
  /** The subset of the GitHub release payload the adapter reads. */
@@ -164,6 +170,92 @@ const liftHeadings = (body: string): string => {
164
170
  return lifted;
165
171
  };
166
172
 
173
+ const WEB_PROTOCOL = /^https?:$/u;
174
+ const TITLE_SPECIALS = /["\\]/gu;
175
+
176
+ /** The origin of an http(s) site URL, or null when there is none. */
177
+ const webOrigin = (site?: string): string | null => {
178
+ const parsed = site ? URL.parse(site) : null;
179
+ return parsed && WEB_PROTOCOL.test(parsed.protocol) ? parsed.origin : null;
180
+ };
181
+
182
+ /** A link title as Markdown carries it, or nothing when there is none. */
183
+ const titleSuffix = (title?: string | null): string =>
184
+ title ? ` "${title.replaceAll(TITLE_SPECIALS, String.raw`\$&`)}"` : "";
185
+
186
+ /**
187
+ * The Markdown a link or definition to one of the site's own pages becomes,
188
+ * pointing at `path` instead. An inline link keeps its label as written, and
189
+ * an autolink keeps showing the URL it was written as.
190
+ */
191
+ const rewrittenLink = (
192
+ markdown: string,
193
+ node: Definition | Link,
194
+ path: string
195
+ ): string => {
196
+ const target = `${destination(path)}${titleSuffix(node.title)}`;
197
+ if (node.type === "definition") {
198
+ return `[${node.label ?? node.identifier}]: ${target}`;
199
+ }
200
+ const start = node.position?.start.offset ?? 0;
201
+ if (markdown[start] !== "[") {
202
+ return `[${escapeMarkdownText(mdastToString(node))}](${target})`;
203
+ }
204
+ const first = node.children.at(0)?.position?.start.offset ?? start + 1;
205
+ const last = node.children.at(-1)?.position?.end.offset ?? first;
206
+ return `[${markdown.slice(first, last)}](${target})`;
207
+ };
208
+
209
+ /**
210
+ * Point the notes' links to the site itself (`https://acme.dev/docs/x` when
211
+ * `deployment.site` is `https://acme.dev`) at the root-relative path, as a
212
+ * page on the site links to another: an absolute link hardcodes production,
213
+ * so it leaves a preview deploy and trips `blume audit`'s own-origin check.
214
+ */
215
+ const relativizeOwnLinks = (markdown: string, site?: string): string => {
216
+ const origin = webOrigin(site);
217
+ if (!origin) {
218
+ return markdown;
219
+ }
220
+ const spans: { end: number; start: number; text: string }[] = [];
221
+ const visit = (node: Nodes): void => {
222
+ if (node.type === "link" || node.type === "definition") {
223
+ const parsed = URL.parse(node.url);
224
+ if (parsed?.origin === origin) {
225
+ spans.push({
226
+ // fromMarkdown stamps every node's position.
227
+ end: node.position?.end.offset ?? 0,
228
+ start: node.position?.start.offset ?? 0,
229
+ text: rewrittenLink(
230
+ markdown,
231
+ node,
232
+ `${parsed.pathname}${parsed.search}${parsed.hash}`
233
+ ),
234
+ });
235
+ return;
236
+ }
237
+ }
238
+ if ("children" in node) {
239
+ for (const child of node.children) {
240
+ visit(child);
241
+ }
242
+ }
243
+ };
244
+ visit(
245
+ fromMarkdown(markdown, {
246
+ extensions: [gfm()],
247
+ mdastExtensions: [gfmFromMarkdown()],
248
+ })
249
+ );
250
+ let out = "";
251
+ let cursor = 0;
252
+ for (const span of spans) {
253
+ out += markdown.slice(cursor, span.start) + span.text;
254
+ cursor = span.end;
255
+ }
256
+ return out + markdown.slice(cursor);
257
+ };
258
+
167
259
  /** Slugify a tag into a stable, URL-safe source ref (`v1.2.0` -> `v1-2-0`). */
168
260
  const slugifyTag = (tag: string): string =>
169
261
  tag.toLowerCase().replaceAll(NON_SLUG, "-").replaceAll(EDGE_DASHES, "");
@@ -197,16 +289,22 @@ interface ChangelogFrontmatter {
197
289
  * generated `/changelog` timeline and RSS feed, and a summary derived from the
198
290
  * notes becomes the release page's meta description.
199
291
  */
200
- const releaseToEntry = (release: GithubRelease): SourceEntry => {
292
+ const releaseToEntry = (release: GithubRelease, site?: string): SourceEntry => {
201
293
  const version = release.tag_name.replace(LEADING_V, "");
202
294
  const title = release.name?.trim() || release.tag_name;
203
295
  const date = release.published_at ?? release.created_at;
204
296
  const category = release.prerelease ? "Prerelease" : "Release";
205
297
  // Release notes are the repository's content, not the site author's, so a
206
298
  // link whose destination isn't a web, mail, or relative address
207
- // (`javascript:`, `data:`) keeps only its label (see `safe-links.ts`).
299
+ // (`javascript:`, `data:`) keeps only its label (see `safe-links.ts`), and
300
+ // a link back to the site becomes root-relative.
208
301
  const body = liftHeadings(
209
- neutralizeUnsafeLinks((release.body ?? "").replaceAll("\r\n", "\n")).trim()
302
+ relativizeOwnLinks(
303
+ neutralizeUnsafeLinks(
304
+ (release.body ?? "").replaceAll("\r\n", "\n")
305
+ ).trim(),
306
+ site
307
+ )
210
308
  );
211
309
  // A summary in `seo.description` gives each release page a unique meta
212
310
  // description (instead of the site-wide fallback) without also rendering the
@@ -295,7 +393,9 @@ export const githubReleasesSource = (
295
393
  cache,
296
394
  async () => {
297
395
  const releases = await fetchReleases();
298
- return releases.map(releaseToEntry);
396
+ return releases.map((release) =>
397
+ releaseToEntry(release, options.site)
398
+ );
299
399
  },
300
400
  refresh
301
401
  );
@@ -347,5 +447,6 @@ export const githubReleasesSource = (
347
447
  () => load()
348
448
  )
349
449
  : undefined,
450
+ withContext: (next) => githubReleasesSource(options, next),
350
451
  };
351
452
  };
@@ -11,6 +11,7 @@ import type { InlineMarks } from "./lower.ts";
11
11
  import {
12
12
  absoluteUrl,
13
13
  blockquote,
14
+ guardBlockStart,
14
15
  headingPrefix,
15
16
  image,
16
17
  indent,
@@ -105,6 +106,10 @@ const renderInlineNode = (
105
106
  }
106
107
  };
107
108
 
109
+ /** A text block's inline children, joined and guarded as one block. */
110
+ const blockText = (node: JsonObject, options: LexicalOptions): string =>
111
+ guardBlockStart(renderInlines(children(node), options));
112
+
108
113
  const renderUpload = (node: JsonObject, options: LexicalOptions): string => {
109
114
  const value = asObject(node.value);
110
115
  const url = value ? asString(value.url) : undefined;
@@ -140,7 +145,7 @@ const renderList = (node: JsonObject, options: LexicalOptions): string => {
140
145
  if (listType === "check") {
141
146
  check = item.checked === true ? "[x] " : "[ ] ";
142
147
  }
143
- const line = `${check}${renderInlines(inline, options)}`;
148
+ const line = `${check}${guardBlockStart(renderInlines(inline, options))}`;
144
149
  items.push(listItem(lastMarker, sub ? `${line}\n${sub}` : line));
145
150
  index += 1;
146
151
  }
@@ -150,14 +155,14 @@ const renderList = (node: JsonObject, options: LexicalOptions): string => {
150
155
  const renderBlock = (node: JsonObject, options: LexicalOptions): string => {
151
156
  switch (typeOf(node)) {
152
157
  case "paragraph": {
153
- return renderInlines(children(node), options);
158
+ return blockText(node, options);
154
159
  }
155
160
  case "heading": {
156
161
  const level = Number((asString(node.tag) ?? "h1").slice(1));
157
- return `${headingPrefix(level)}${renderInlines(children(node), options)}`;
162
+ return `${headingPrefix(level)}${blockText(node, options)}`;
158
163
  }
159
164
  case "quote": {
160
- return blockquote(renderInlines(children(node), options));
165
+ return blockquote(blockText(node, options));
161
166
  }
162
167
  case "list": {
163
168
  return renderList(node, options);
@@ -22,29 +22,37 @@ const MARKDOWN_SPECIALS = /[\\`*_{}[\]~<]/gu;
22
22
  // it names rather than as written. A bare `&` is left alone.
23
23
  const CHARACTER_REFERENCE = /&(?=#?[a-z0-9]+;)/giu;
24
24
 
25
- // Block syntax is only syntax at the start of a line: `# `, `- `, `+ ` and
26
- // `1. `/`1) ` need the space (or line end) to become a heading or list item,
27
- // while `>` opens a quote on its own, and a line of only `=` or `-` turns the
28
- // line above it into a heading (or, alone, into a rule). A CMS paragraph that
29
- // begins with one of these — a soft break inside it counts as a line start
30
- // too — must stay prose.
25
+ // Block syntax is only syntax at the start of a line: `# ` (through
26
+ // `###### `), `- `, `+ ` and `1. `/`1) ` need the space (or line end) to
27
+ // become a heading or list item, while `>` opens a quote on its own, and a
28
+ // line of only `=` or `-` turns the line above it into a heading (or, alone,
29
+ // into a rule). Indentation of up to three spaces still counts. A CMS
30
+ // paragraph that begins with one of these — a soft break inside it counts as
31
+ // a line start too — must stay prose.
31
32
  const BLOCK_START =
32
- /^(?<marker>[#+-]|\d+[.)])(?=[ \t]|$)|^(?<quote>>)|^(?<rule>=+|-{2,})(?=[ \t]*$)/gmu;
33
+ /^(?<indent>[ \t]*)(?:(?<marker>#+|[+-]|\d+[.)])(?=[ \t]|$)|(?<quote>>)|(?<rule>=+|-{2,})(?=[ \t]*$))/gmu;
33
34
 
34
- // MDX reads a top-level paragraph that opens with `import ` or `export ` as an
35
- // ESM statement, and prose like "import the CSV first" then fails the whole
36
- // page. The keyword's first letter as a character reference renders the same
37
- // in `.md` and `.mdx`, and MDX no longer sees the keyword. Only a paragraph's
38
- // first line counts; a blank line inside a run starts a new paragraph.
39
- const ESM_START = /(?<=^|\n[ \t]*\n)(?<keyword>import|export)(?= )/gu;
35
+ // MDX reads a top-level line that opens with `import` or `export` followed by
36
+ // a space, a tab, `{`, or `*` as an ESM statement, and prose like "import the
37
+ // CSV first" then fails the whole page. The keyword's first letter as a
38
+ // character reference renders the same in `.md` and `.mdx`, and MDX no longer
39
+ // sees the keyword. The keyword is escaped wherever a word ends on it, which
40
+ // covers every character that opens a statement. Only a block's first line
41
+ // counts; a blank line inside the text starts a new block.
42
+ const ESM_START = /(?<=^|\n[ \t]*\n)(?<keyword>import|export)(?![\w$])/gu;
43
+
44
+ // Whitespace opening a block: four columns of it (or a tab) make an indented
45
+ // code block, and a paragraph drops it when it renders anyway.
46
+ const BLOCK_INDENT = /(?<=^|\n[ \t]*\n)[ \t]+/gu;
40
47
 
41
48
  const escapeBlockStart = (text: string): string =>
42
- text.replaceAll(BLOCK_START, (marker: string) => {
49
+ text.replaceAll(BLOCK_START, (match: string, indent: string) => {
43
50
  // A numbered marker escapes its punctuation (`1\.`), the rest themselves.
51
+ const marker = match.slice(indent.length);
44
52
  const index = marker.search(/[.)]/u);
45
53
  return index === -1
46
- ? `\\${marker}`
47
- : `${marker.slice(0, index)}\\${marker.slice(index)}`;
54
+ ? `${indent}\\${marker}`
55
+ : `${indent}${marker.slice(0, index)}\\${marker.slice(index)}`;
48
56
  });
49
57
 
50
58
  const escapeEsmStart = (text: string): string =>
@@ -92,6 +100,17 @@ export const codeSpan = (code: string): string => {
92
100
  return `${fence}${pad}${code}${pad}${fence}`;
93
101
  };
94
102
 
103
+ /**
104
+ * A block's inline text, guarded where only the whole block can tell. Runs
105
+ * are escaped one at a time, but "import" ending one run and a space or code
106
+ * span opening the next form an MDX `import` statement only once they are
107
+ * joined, and whitespace the first runs put in front of a block turns it into
108
+ * an indented code block. Apply it to the joined text of a paragraph,
109
+ * heading, quote, or list item — never to code.
110
+ */
111
+ export const guardBlockStart = (text: string): string =>
112
+ escapeEsmStart(text.replaceAll(BLOCK_INDENT, ""));
113
+
95
114
  const EDGE_SPACE = /^(?<lead>\s*)(?<body>[\s\S]*?)(?<trail>\s*)$/u;
96
115
 
97
116
  /**
@@ -122,13 +141,30 @@ export const renderInline = (text: string, marks: InlineMarks): string => {
122
141
  return `${lead}${out}${trail}`;
123
142
  };
124
143
 
125
- // A destination with whitespace or parentheses ends early in `[label](…)`;
126
- // CommonMark's pointy-bracket form carries it intact.
127
- const UNSAFE_DESTINATION = /[\s()]/u;
144
+ // A destination with whitespace, a control character, or parentheses ends
145
+ // early in `[label](…)`; CommonMark's pointy-bracket form carries it intact.
146
+ // oxlint-disable-next-line no-control-regex -- control characters are exactly what the bare form refuses.
147
+ const UNSAFE_DESTINATION = /[\s()\u0000-\u001F\u007F]/u;
148
+
149
+ // Markdown decodes a destination before the browser sees it: a backslash
150
+ // escape or a character reference becomes its character, so
151
+ // `java&#115;cript:` would reach the `href` as `javascript:` after the safety
152
+ // check passed it. Escaping both — and `<`/`>`, which would close the
153
+ // pointy form early and let the rest of the URL open a second link — keeps
154
+ // the `href` exactly the URL the CMS holds.
155
+ const DESTINATION_SPECIALS = /[\\<>]|&(?=#?[a-z0-9]+;)/giu;
156
+
157
+ // A destination can't hold a line break, and the URL parser drops tabs and
158
+ // line breaks anyway, so removing them leaves the same URL.
159
+ const DESTINATION_IGNORED = /[\t\n\r]/gu;
128
160
 
129
- /** A link or image destination as Markdown can carry it verbatim. */
130
- export const destination = (url: string): string =>
131
- UNSAFE_DESTINATION.test(url) ? `<${url}>` : url;
161
+ /** A link or image destination as Markdown carries it verbatim. */
162
+ export const destination = (url: string): string => {
163
+ const escaped = url
164
+ .replaceAll(DESTINATION_IGNORED, "")
165
+ .replaceAll(DESTINATION_SPECIALS, String.raw`\$&`);
166
+ return UNSAFE_DESTINATION.test(escaped) ? `<${escaped}>` : escaped;
167
+ };
132
168
 
133
169
  /**
134
170
  * A Markdown link, or the label alone when the target is missing — or unsafe:
@@ -172,10 +208,20 @@ export const indent = (text: string, width: number): string => {
172
208
  .join("\n");
173
209
  };
174
210
 
175
- /** A fenced code block whose fence outruns any backtick run in the code. */
211
+ // A fence's language is one word of its info string. A backtick in it keeps
212
+ // the fence from opening at all, so the code renders as Markdown and HTML,
213
+ // and whitespace would start the meta Blume reads code titles from; a
214
+ // language that isn't a single such word is dropped.
215
+ const FENCE_LANGUAGE = /^[^\s`]+$/u;
216
+
217
+ /**
218
+ * A fenced code block whose fence outruns any backtick run in the code,
219
+ * labeled with its language when that is one word a fence can carry.
220
+ */
176
221
  export const codeFence = (code: string, language = ""): string => {
177
222
  const fence = "`".repeat(Math.max(3, longestBacktickRun(code) + 1));
178
- return `${fence}${language}\n${code}\n${fence}`;
223
+ const label = FENCE_LANGUAGE.test(language) ? language : "";
224
+ return `${fence}${label}\n${code}\n${fence}`;
179
225
  };
180
226
 
181
227
  /** A Markdown image; the alt is escaped so a `]` in a caption can't close it. */
@@ -193,12 +239,19 @@ export const writesMdx = <Node>(
193
239
  serializers?: Record<string, (node: Node) => string>
194
240
  ): boolean => serializers !== undefined && Object.keys(serializers).length > 0;
195
241
 
242
+ // A node's type is the CMS's data too: a `*/` (or `--`) in it would close the
243
+ // comment early and let the rest run as MDX (or render as HTML). A space
244
+ // between the two characters keeps it inside.
245
+ const COMMENT_END = /\*\/|--/gu;
246
+
196
247
  /**
197
248
  * A comment marking a node the lowerer has no Markdown for — an MDX comment
198
249
  * when the output is MDX, which rejects `<!-- -->`.
199
250
  */
200
- export const unsupported = (what: string, mdx = false): string =>
201
- mdx ? `{/* unsupported ${what} */}` : `<!-- unsupported ${what} -->`;
251
+ export const unsupported = (what: string, mdx = false): string => {
252
+ const label = what.replaceAll(COMMENT_END, (end) => [...end].join(" "));
253
+ return mdx ? `{/* unsupported ${label} */}` : `<!-- unsupported ${label} -->`;
254
+ };
202
255
 
203
256
  /** Blocks separated by blank lines; empty blocks are dropped. */
204
257
  export const joinBlocks = (blocks: string[]): string =>
@@ -253,5 +253,6 @@ export const mdxRemoteSource = (
253
253
  () => load()
254
254
  )
255
255
  : undefined,
256
+ withContext: (next) => mdxRemoteSource(options, next),
256
257
  };
257
258
  };