blume 2.0.1 → 2.0.3

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 (492) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/cli/{chunk-qwsrynx5.js → chunk-1d7ve1dm.js} +38 -19
  3. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-1d7ve1dm.js.map} +4 -4
  4. package/dist/cli/{chunk-s6jhgk0q.js → chunk-2eytanqx.js} +2 -2
  5. package/dist/cli/{chunk-kdp5q7ke.js → chunk-2hsdwb9n.js} +18 -19
  6. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-2hsdwb9n.js.map} +2 -2
  7. package/dist/cli/{chunk-hdpx1tax.js → chunk-2z928egk.js} +5 -5
  8. package/dist/cli/{chunk-jwyddg7y.js → chunk-35d4wj9f.js} +30 -24
  9. package/dist/cli/chunk-35d4wj9f.js.map +15 -0
  10. package/dist/cli/{chunk-ah61y8py.js → chunk-364znk6q.js} +2 -2
  11. package/dist/cli/{chunk-jts8mvcz.js → chunk-3em5wd2y.js} +25 -6
  12. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-3em5wd2y.js.map} +3 -3
  13. package/dist/cli/{chunk-s1p84fyh.js → chunk-5f86nr5m.js} +63 -22
  14. package/dist/cli/chunk-5f86nr5m.js.map +11 -0
  15. package/dist/cli/{chunk-2hn4b8z7.js → chunk-5m5nmvyq.js} +170 -76
  16. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  17. package/dist/cli/{chunk-epjnccmv.js → chunk-5xvm6tfj.js} +18 -15
  18. package/dist/cli/chunk-5xvm6tfj.js.map +10 -0
  19. package/dist/cli/{chunk-fz5wtpmh.js → chunk-6dsbexzp.js} +18 -15
  20. package/dist/cli/chunk-6dsbexzp.js.map +10 -0
  21. package/dist/cli/{chunk-vtk4a6dg.js → chunk-8ktnccpt.js} +1 -1
  22. package/dist/cli/{chunk-6hsn950k.js → chunk-9t7a85s3.js} +178 -48
  23. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  24. package/dist/cli/{chunk-fxypxtvm.js → chunk-a58773jm.js} +2 -2
  25. package/dist/cli/{chunk-mb2919y2.js → chunk-acanzt5p.js} +17 -6
  26. package/dist/cli/chunk-acanzt5p.js.map +10 -0
  27. package/dist/cli/{chunk-27g6wdth.js → chunk-akbpwfxc.js} +90 -26
  28. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  29. package/dist/cli/{chunk-wm7js3j9.js → chunk-b07cmahc.js} +2 -2
  30. package/dist/cli/{chunk-wgm7m9qk.js → chunk-c8chx29p.js} +179 -51
  31. package/dist/cli/chunk-c8chx29p.js.map +36 -0
  32. package/dist/cli/{chunk-qs4q5p4e.js → chunk-crgn1q09.js} +32 -14
  33. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  34. package/dist/cli/chunk-e04dxsz1.js +39 -0
  35. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  36. package/dist/cli/{chunk-yt5n7ppj.js → chunk-ey84smr6.js} +17 -7
  37. package/dist/cli/chunk-ey84smr6.js.map +10 -0
  38. package/dist/cli/{chunk-f2z5v128.js → chunk-g4hq16wv.js} +14 -15
  39. package/dist/cli/{chunk-f2z5v128.js.map → chunk-g4hq16wv.js.map} +2 -2
  40. package/dist/cli/{chunk-fa25z98p.js → chunk-ga0pf4aj.js} +9 -8
  41. package/dist/cli/chunk-ga0pf4aj.js.map +11 -0
  42. package/dist/cli/{chunk-kpf8rrjc.js → chunk-hm3vjy5s.js} +109 -58
  43. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  44. package/dist/cli/{chunk-fs23ddbb.js → chunk-j85vccga.js} +625 -750
  45. package/dist/cli/chunk-j85vccga.js.map +36 -0
  46. package/dist/cli/{chunk-ch6g3ar0.js → chunk-p3v96n38.js} +6 -6
  47. package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-p3v96n38.js.map} +3 -3
  48. package/dist/cli/{chunk-q5163e60.js → chunk-p73c0m7w.js} +21 -19
  49. package/dist/cli/chunk-p73c0m7w.js.map +11 -0
  50. package/dist/cli/{chunk-dh8cwk36.js → chunk-pehfxfta.js} +24 -9
  51. package/dist/cli/chunk-pehfxfta.js.map +10 -0
  52. package/dist/cli/{chunk-qkqwkpte.js → chunk-pv29h0wf.js} +3688 -1313
  53. package/dist/cli/chunk-pv29h0wf.js.map +190 -0
  54. package/dist/cli/{chunk-6vm74dry.js → chunk-q58y5e6a.js} +10 -10
  55. package/dist/cli/{chunk-6vm74dry.js.map → chunk-q58y5e6a.js.map} +3 -3
  56. package/dist/cli/{chunk-zxcczpyx.js → chunk-r20tn01b.js} +1 -1
  57. package/dist/cli/{chunk-m3vmjgmq.js → chunk-r9rcc4w7.js} +16 -8
  58. package/dist/cli/chunk-r9rcc4w7.js.map +10 -0
  59. package/dist/cli/{chunk-5shv93fd.js → chunk-tkacnehg.js} +2 -2
  60. package/dist/cli/{chunk-zxh4d9vy.js → chunk-tzmab476.js} +4 -4
  61. package/dist/cli/{chunk-yw7dm696.js → chunk-vg9r4eb9.js} +24 -11
  62. package/dist/cli/chunk-vg9r4eb9.js.map +14 -0
  63. package/dist/cli/{chunk-79jhk4py.js → chunk-wrr3j9w9.js} +260 -132
  64. package/dist/cli/chunk-wrr3j9w9.js.map +35 -0
  65. package/dist/cli/{chunk-6crbhc3x.js → chunk-yfyb25rh.js} +54 -27
  66. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  67. package/dist/cli/index.js +162 -35
  68. package/dist/cli/index.js.map +4 -4
  69. package/dist/types/ai/agent-surface.d.ts +32 -0
  70. package/dist/types/ai/api-catalog.d.ts +7 -1
  71. package/dist/types/ai/ask-context.d.ts +7 -0
  72. package/dist/types/ai/component-markdown.d.ts +4 -4
  73. package/dist/types/ai/link-headers.d.ts +8 -1
  74. package/dist/types/ai/openapi-components.d.ts +5 -2
  75. package/dist/types/ai/relative-links.d.ts +11 -4
  76. package/dist/types/ai/skills.d.ts +4 -1
  77. package/dist/types/ai/static-expression.d.ts +28 -0
  78. package/dist/types/ai/tar.d.ts +1 -3
  79. package/dist/types/analytics/databuddy.d.ts +43 -0
  80. package/dist/types/analytics/index.d.ts +4 -0
  81. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  82. package/dist/types/analytics/schema.d.ts +28 -0
  83. package/dist/types/astro/integration.d.ts +3 -2
  84. package/dist/types/cli/env.d.ts +5 -0
  85. package/dist/types/cli/init/scaffold.d.ts +19 -3
  86. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  87. package/dist/types/core/base-path.d.ts +24 -1
  88. package/dist/types/core/config-input.d.ts +13 -11
  89. package/dist/types/core/config.d.ts +2 -2
  90. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  91. package/dist/types/core/graph.d.ts +2 -0
  92. package/dist/types/core/heading-markers.d.ts +5 -7
  93. package/dist/types/core/i18n-ui.d.ts +31 -0
  94. package/dist/types/core/i18n.d.ts +9 -1
  95. package/dist/types/core/last-modified.d.ts +10 -0
  96. package/dist/types/core/links.d.ts +3 -1
  97. package/dist/types/core/load-module.d.ts +10 -0
  98. package/dist/types/core/locale-links.d.ts +12 -2
  99. package/dist/types/core/meta.d.ts +13 -1
  100. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  101. package/dist/types/core/navigation.d.ts +38 -0
  102. package/dist/types/core/ordering-prefix.d.ts +4 -0
  103. package/dist/types/core/safe-links.d.ts +3 -1
  104. package/dist/types/core/schema.d.ts +59 -13
  105. package/dist/types/core/sources/github-releases.d.ts +5 -0
  106. package/dist/types/core/sources/lower.d.ts +38 -13
  107. package/dist/types/core/sources/normalize.d.ts +23 -2
  108. package/dist/types/core/sources/remote.d.ts +11 -1
  109. package/dist/types/core/sources/resolve.d.ts +12 -0
  110. package/dist/types/core/sources/types.d.ts +31 -0
  111. package/dist/types/core/sources/watch.d.ts +12 -5
  112. package/dist/types/core/standard-schema.d.ts +5 -0
  113. package/dist/types/core/types.d.ts +9 -0
  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/artifacts.d.ts +6 -4
  117. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  118. package/dist/types/deploy/headers.d.ts +36 -7
  119. package/dist/types/deploy/node-headers.d.ts +43 -8
  120. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  121. package/dist/types/deploy/platforms/node.d.ts +6 -5
  122. package/dist/types/deploy/platforms/types.d.ts +15 -0
  123. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  124. package/dist/types/deploy/redirects.d.ts +31 -15
  125. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  126. package/dist/types/markdown/directives.d.ts +62 -0
  127. package/dist/types/markdown/features.d.ts +21 -0
  128. package/dist/types/markdown/mdast.d.ts +63 -0
  129. package/dist/types/openapi/asyncapi.d.ts +4 -2
  130. package/dist/types/openapi/model.d.ts +20 -6
  131. package/dist/types/search/sync/algolia.d.ts +3 -1
  132. package/docs/01-quickstart.mdx +3 -2
  133. package/docs/02-deployment.mdx +20 -9
  134. package/docs/08-faq.mdx +10 -3
  135. package/docs/advanced/changelog.mdx +1 -1
  136. package/docs/advanced/custom-pages.mdx +7 -5
  137. package/docs/cli/audit.mdx +19 -3
  138. package/docs/cli/doctor.mdx +2 -2
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/index.mdx +4 -1
  141. package/docs/cli/translate.mdx +4 -4
  142. package/docs/cli/version.mdx +1 -1
  143. package/docs/configuration/analytics.mdx +42 -2
  144. package/docs/configuration/assistant.mdx +1 -1
  145. package/docs/configuration/customization.mdx +6 -3
  146. package/docs/configuration/index.mdx +5 -3
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +2 -2
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -1
  151. package/docs/content/index.mdx +8 -4
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +7 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -16
  156. package/docs/content/syntax.mdx +14 -0
  157. package/docs/content/versioning.mdx +2 -1
  158. package/docs/discoverability/agent-discovery.mdx +22 -8
  159. package/docs/discoverability/index.mdx +4 -3
  160. package/docs/discoverability/llms-txt.mdx +2 -5
  161. package/docs/discoverability/markdown.mdx +6 -4
  162. package/docs/discoverability/mcp.mdx +4 -0
  163. package/docs/discoverability/metadata.mdx +3 -2
  164. package/docs/discoverability/open-graph.mdx +8 -4
  165. package/docs/discoverability/rss.mdx +4 -2
  166. package/docs/references/asyncapi.mdx +2 -2
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +6 -4
  169. package/package.json +1 -1
  170. package/skills/blume-migrate/SKILL.md +6 -6
  171. package/skills/blume-migrate/references/docusaurus.md +6 -6
  172. package/skills/blume-migrate/references/fumadocs.md +1 -1
  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 +2 -2
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +9 -4
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +2 -2
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/ai-catalog.ts +6 -2
  182. package/src/ai/api/handlers.ts +2 -2
  183. package/src/ai/api/spec.ts +17 -4
  184. package/src/ai/api-catalog.ts +13 -3
  185. package/src/ai/ask-context.ts +14 -2
  186. package/src/ai/ask-data.ts +26 -12
  187. package/src/ai/changelog-markdown.ts +2 -2
  188. package/src/ai/component-markdown.ts +43 -37
  189. package/src/ai/link-headers.ts +22 -8
  190. package/src/ai/llms.ts +22 -10
  191. package/src/ai/markdown.ts +23 -11
  192. package/src/ai/mcp/discovery.ts +2 -2
  193. package/src/ai/mcp/query.ts +2 -2
  194. package/src/ai/mcp/server.ts +80 -9
  195. package/src/ai/openapi-components.ts +22 -6
  196. package/src/ai/relative-links.ts +99 -15
  197. package/src/ai/serializers.ts +2 -1
  198. package/src/ai/skills.ts +14 -1
  199. package/src/ai/static-expression.ts +416 -0
  200. package/src/ai/tar.ts +139 -12
  201. package/src/ai/visibility.ts +45 -14
  202. package/src/analytics/databuddy.ts +67 -0
  203. package/src/analytics/head.ts +8 -0
  204. package/src/analytics/index.ts +7 -0
  205. package/src/analytics/one-dollar-stats.ts +86 -0
  206. package/src/analytics/posthog.ts +21 -3
  207. package/src/analytics/schema.ts +4 -0
  208. package/src/astro/generate.ts +72 -28
  209. package/src/astro/include-hmr.ts +31 -12
  210. package/src/astro/include-refresh.ts +19 -8
  211. package/src/astro/integration.ts +108 -14
  212. package/src/astro/runtime-modules.ts +28 -13
  213. package/src/astro/templates.ts +139 -57
  214. package/src/audit/catalog.ts +2 -2
  215. package/src/audit/checks/assets.ts +25 -5
  216. package/src/audit/checks/content.ts +20 -2
  217. package/src/audit/checks/i18n.ts +46 -8
  218. package/src/audit/checks/indexability.ts +39 -19
  219. package/src/audit/checks/links.ts +22 -1
  220. package/src/audit/checks/llms.ts +6 -3
  221. package/src/audit/checks/network.ts +3 -1
  222. package/src/audit/checks/og-image.ts +10 -0
  223. package/src/audit/checks/robots.ts +6 -1
  224. package/src/audit/checks/sitemap.ts +56 -31
  225. package/src/audit/checks/social.ts +21 -2
  226. package/src/audit/crawl.ts +88 -15
  227. package/src/audit/graph.ts +3 -1
  228. package/src/audit/report.ts +54 -17
  229. package/src/audit/run.ts +13 -9
  230. package/src/audit/snapshot.ts +5 -0
  231. package/src/audit/types.ts +18 -0
  232. package/src/audit/url.ts +37 -6
  233. package/src/cli/args.ts +32 -0
  234. package/src/cli/build-failure.ts +50 -0
  235. package/src/cli/commands/audit.ts +11 -1
  236. package/src/cli/commands/build.ts +19 -5
  237. package/src/cli/commands/check.ts +2 -0
  238. package/src/cli/commands/doctor.ts +3 -1
  239. package/src/cli/commands/eval.ts +20 -18
  240. package/src/cli/commands/init.ts +15 -5
  241. package/src/cli/commands/preview.ts +15 -0
  242. package/src/cli/commands/sync.ts +2 -0
  243. package/src/cli/commands/translate.ts +18 -18
  244. package/src/cli/commands/upgrade.ts +11 -0
  245. package/src/cli/commands/validate.ts +3 -1
  246. package/src/cli/dev-lock.ts +157 -37
  247. package/src/cli/eject-scripts.ts +32 -7
  248. package/src/cli/env.ts +12 -1
  249. package/src/cli/init/scaffold.ts +117 -11
  250. package/src/cli/init/starter-spec.ts +235 -0
  251. package/src/cli/report-format.ts +11 -6
  252. package/src/components/colors.ts +19 -0
  253. package/src/components/content/AccordionItem.astro +26 -22
  254. package/src/components/content/Badge.astro +7 -12
  255. package/src/components/content/Card.astro +2 -2
  256. package/src/components/content/Component.astro +11 -5
  257. package/src/components/content/Expandable.astro +5 -1
  258. package/src/components/content/Frame.astro +2 -7
  259. package/src/components/content/GithubInfo.astro +12 -2
  260. package/src/components/content/Prompt.astro +2 -7
  261. package/src/components/content/Tab.astro +0 -1
  262. package/src/components/content/Tabs.astro +58 -5
  263. package/src/components/content/Tile.astro +1 -1
  264. package/src/components/content/Tooltip.astro +69 -7
  265. package/src/components/content/Tree.astro +7 -2
  266. package/src/components/content/TypeTable.astro +10 -5
  267. package/src/components/content/Update.astro +8 -2
  268. package/src/components/content/auto-type-table.ts +4 -1
  269. package/src/components/content/badge-color.ts +20 -0
  270. package/src/components/content/base-href.ts +14 -22
  271. package/src/components/content/inline-markdown.ts +27 -7
  272. package/src/components/copy-feedback.ts +35 -8
  273. package/src/components/islands/assistant.tsx +37 -6
  274. package/src/components/islands/base-path.ts +47 -10
  275. package/src/components/islands/hooks.ts +42 -21
  276. package/src/components/islands/webmcp.ts +16 -10
  277. package/src/components/layout/Banner.astro +23 -4
  278. package/src/components/layout/Breadcrumbs.astro +5 -2
  279. package/src/components/layout/DiscoveryLinks.astro +9 -5
  280. package/src/components/layout/Header.astro +23 -10
  281. package/src/components/layout/LanguageSwitcher.astro +2 -2
  282. package/src/components/layout/Logo.astro +5 -0
  283. package/src/components/layout/NavSelector.astro +8 -3
  284. package/src/components/layout/NavTabMenu.astro +133 -0
  285. package/src/components/layout/NavTree.astro +31 -12
  286. package/src/components/layout/NavTreeCache.astro +5 -2
  287. package/src/components/layout/NavTreeScript.astro +45 -5
  288. package/src/components/layout/PageActions.astro +4 -3
  289. package/src/components/layout/PageLayout.astro +28 -13
  290. package/src/components/layout/Pagination.astro +3 -3
  291. package/src/components/layout/ReferenceLayout.astro +4 -1
  292. package/src/components/layout/RootLayout.astro +25 -12
  293. package/src/components/layout/Search.astro +38 -14
  294. package/src/components/layout/VersionBanner.astro +2 -2
  295. package/src/components/layout/analytics-client.ts +14 -0
  296. package/src/components/layout/toc-active.ts +41 -0
  297. package/src/components/layout/toc-element.ts +8 -14
  298. package/src/components/openapi/ApiTagOperations.astro +2 -2
  299. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  300. package/src/components/openapi/GraphqlChip.astro +2 -2
  301. package/src/components/openapi/GraphqlType.astro +11 -3
  302. package/src/components/openapi/MessageComposer.astro +1 -1
  303. package/src/components/openapi/Operation.astro +23 -5
  304. package/src/components/openapi/PanelTabs.astro +4 -1
  305. package/src/components/openapi/Playground.astro +8 -2
  306. package/src/components/openapi/RequestPanel.astro +13 -6
  307. package/src/components/openapi/SchemaProperty.astro +11 -33
  308. package/src/components/openapi/SchemaTable.astro +34 -52
  309. package/src/components/openapi/async.ts +38 -6
  310. package/src/components/openapi/helpers.ts +100 -12
  311. package/src/components/openapi/message-composer.ts +14 -1
  312. package/src/components/openapi/message-model.ts +12 -2
  313. package/src/components/openapi/message.ts +21 -3
  314. package/src/components/openapi/operation-model.ts +119 -17
  315. package/src/components/openapi/panel.ts +31 -5
  316. package/src/components/openapi/param-style.ts +181 -0
  317. package/src/components/openapi/playground-client.ts +110 -23
  318. package/src/components/openapi/playground-schema.ts +25 -6
  319. package/src/components/openapi/request.ts +192 -26
  320. package/src/components/openapi/schema-tree.ts +209 -0
  321. package/src/components/openapi/snippets.ts +113 -19
  322. package/src/components/openapi/validate-json.ts +1 -1
  323. package/src/components/openapi/ws-client.ts +18 -2
  324. package/src/core/base-path.ts +69 -8
  325. package/src/core/config-input.ts +13 -11
  326. package/src/core/config.ts +18 -4
  327. package/src/core/diagnostics.ts +219 -30
  328. package/src/core/directive-diagnostics.ts +99 -0
  329. package/src/core/frontmatter.ts +21 -18
  330. package/src/core/graph.ts +120 -11
  331. package/src/core/heading-markers.ts +5 -18
  332. package/src/core/i18n-ui.ts +39 -2
  333. package/src/core/i18n.ts +44 -5
  334. package/src/core/last-modified.ts +25 -3
  335. package/src/core/links.ts +7 -4
  336. package/src/core/load-module.ts +20 -0
  337. package/src/core/locale-links.ts +22 -18
  338. package/src/core/manifest.ts +3 -2
  339. package/src/core/meta.ts +89 -12
  340. package/src/core/nav-diagnostics.ts +56 -1
  341. package/src/core/navigation.ts +272 -83
  342. package/src/core/ordering-prefix.ts +27 -0
  343. package/src/core/project-graph.ts +50 -6
  344. package/src/core/safe-href.ts +53 -1
  345. package/src/core/safe-links.ts +11 -2
  346. package/src/core/schema.ts +94 -18
  347. package/src/core/sources/assets.ts +83 -41
  348. package/src/core/sources/contentful-rich-text.ts +28 -20
  349. package/src/core/sources/contentful.ts +25 -12
  350. package/src/core/sources/filesystem.ts +25 -3
  351. package/src/core/sources/github-releases.ts +131 -11
  352. package/src/core/sources/lexical.ts +30 -20
  353. package/src/core/sources/lower.ts +276 -56
  354. package/src/core/sources/mdx-remote.ts +52 -16
  355. package/src/core/sources/normalize.ts +456 -119
  356. package/src/core/sources/notion.ts +77 -34
  357. package/src/core/sources/obsidian.ts +40 -9
  358. package/src/core/sources/payload.ts +1 -0
  359. package/src/core/sources/portable-text.ts +85 -49
  360. package/src/core/sources/remote.ts +18 -2
  361. package/src/core/sources/resolve.ts +52 -33
  362. package/src/core/sources/sanity.ts +1 -0
  363. package/src/core/sources/strapi-blocks.ts +28 -19
  364. package/src/core/sources/strapi.ts +1 -0
  365. package/src/core/sources/types.ts +31 -0
  366. package/src/core/sources/watch.ts +20 -7
  367. package/src/core/standard-schema.ts +10 -6
  368. package/src/core/types.ts +9 -0
  369. package/src/core/ui-packs/ar.ts +13 -0
  370. package/src/core/ui-packs/bg.ts +13 -0
  371. package/src/core/ui-packs/bn.ts +13 -0
  372. package/src/core/ui-packs/ca.ts +13 -0
  373. package/src/core/ui-packs/cs.ts +13 -0
  374. package/src/core/ui-packs/da.ts +13 -0
  375. package/src/core/ui-packs/de.ts +13 -0
  376. package/src/core/ui-packs/el.ts +13 -0
  377. package/src/core/ui-packs/es.ts +13 -0
  378. package/src/core/ui-packs/fa.ts +13 -0
  379. package/src/core/ui-packs/fi.ts +13 -0
  380. package/src/core/ui-packs/fr.ts +13 -0
  381. package/src/core/ui-packs/he.ts +13 -0
  382. package/src/core/ui-packs/hi.ts +13 -0
  383. package/src/core/ui-packs/hr.ts +13 -0
  384. package/src/core/ui-packs/hu.ts +13 -0
  385. package/src/core/ui-packs/id.ts +13 -0
  386. package/src/core/ui-packs/it.ts +13 -0
  387. package/src/core/ui-packs/ja.ts +13 -0
  388. package/src/core/ui-packs/ko.ts +13 -0
  389. package/src/core/ui-packs/nl.ts +13 -0
  390. package/src/core/ui-packs/no.ts +13 -0
  391. package/src/core/ui-packs/pl.ts +13 -0
  392. package/src/core/ui-packs/pt-br.ts +13 -0
  393. package/src/core/ui-packs/pt.ts +13 -0
  394. package/src/core/ui-packs/ro.ts +13 -0
  395. package/src/core/ui-packs/ru.ts +13 -0
  396. package/src/core/ui-packs/sk.ts +13 -0
  397. package/src/core/ui-packs/sr.ts +13 -0
  398. package/src/core/ui-packs/sv.ts +13 -0
  399. package/src/core/ui-packs/th.ts +13 -0
  400. package/src/core/ui-packs/tr.ts +13 -0
  401. package/src/core/ui-packs/uk.ts +13 -0
  402. package/src/core/ui-packs/vi.ts +13 -0
  403. package/src/core/ui-packs/zh-tw.ts +13 -0
  404. package/src/core/ui-packs/zh.ts +13 -0
  405. package/src/core/version-cut.ts +69 -17
  406. package/src/core/versions.ts +4 -1
  407. package/src/deploy/adapters/node.ts +5 -2
  408. package/src/deploy/adapters/registry.ts +2 -1
  409. package/src/deploy/adapters/types.ts +13 -1
  410. package/src/deploy/artifacts.ts +52 -11
  411. package/src/deploy/cloudflare-negotiation.ts +27 -16
  412. package/src/deploy/headers.ts +75 -55
  413. package/src/deploy/node-headers.ts +148 -27
  414. package/src/deploy/platforms/cloudflare.ts +13 -3
  415. package/src/deploy/platforms/netlify.ts +83 -5
  416. package/src/deploy/platforms/node.ts +8 -5
  417. package/src/deploy/platforms/static.ts +2 -0
  418. package/src/deploy/platforms/types.ts +15 -0
  419. package/src/deploy/platforms/vercel.ts +10 -3
  420. package/src/deploy/redirects.ts +68 -21
  421. package/src/deploy/robots.ts +2 -2
  422. package/src/deploy/rss.ts +4 -3
  423. package/src/deploy/sitemap.ts +7 -5
  424. package/src/deploy/vercel-negotiation.ts +35 -3
  425. package/src/eval/agents.ts +10 -2
  426. package/src/eval/run.ts +25 -0
  427. package/src/markdown/base-links.ts +74 -29
  428. package/src/markdown/directives.ts +242 -36
  429. package/src/markdown/features.ts +17 -0
  430. package/src/markdown/index.ts +13 -9
  431. package/src/markdown/mdast.ts +5 -2
  432. package/src/markdown/relative-links.ts +15 -27
  433. package/src/markdown/route-snapshot.ts +37 -0
  434. package/src/og/card.ts +149 -9
  435. package/src/og/derive.ts +156 -5
  436. package/src/og/index.ts +1 -0
  437. package/src/openapi/asyncapi.ts +4 -2
  438. package/src/openapi/graphql-build.ts +28 -2
  439. package/src/openapi/model.ts +99 -27
  440. package/src/openapi/proxy.ts +63 -10
  441. package/src/openapi/render-mdx.ts +51 -3
  442. package/src/registry/eject.ts +297 -43
  443. package/src/search/adapters/version-scope.ts +30 -0
  444. package/src/search/documents.ts +89 -27
  445. package/src/search/popular.ts +2 -1
  446. package/src/search/sync/algolia.ts +36 -2
  447. package/src/seo/jsonld.ts +7 -3
  448. package/src/sources/registry.ts +5 -0
  449. package/src/theme/entry.ts +11 -4
  450. package/src/translate/agents.ts +6 -1
  451. package/src/translate/ledger.ts +26 -3
  452. package/src/translate/meta.ts +76 -24
  453. package/src/translate/run.ts +14 -8
  454. package/src/translate/validate.ts +14 -2
  455. package/src/translate/work-list.ts +91 -19
  456. package/src/upgrade/upgrade.ts +36 -4
  457. package/dist/cli/chunk-27g6wdth.js.map +0 -10
  458. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  459. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  460. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  461. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  462. package/dist/cli/chunk-82bbrxdn.js +0 -51
  463. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  464. package/dist/cli/chunk-abh8yjkn.js +0 -31
  465. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  466. package/dist/cli/chunk-ce574jw2.js +0 -23
  467. package/dist/cli/chunk-ce574jw2.js.map +0 -10
  468. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  469. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  470. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  471. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  472. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  473. package/dist/cli/chunk-jwyddg7y.js.map +0 -15
  474. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  475. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  476. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  477. package/dist/cli/chunk-q5163e60.js.map +0 -11
  478. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  479. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  480. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  481. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  482. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  483. package/dist/cli/chunk-yw7dm696.js.map +0 -14
  484. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-2eytanqx.js.map} +0 -0
  485. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-2z928egk.js.map} +0 -0
  486. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-364znk6q.js.map} +0 -0
  487. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-8ktnccpt.js.map} +0 -0
  488. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-a58773jm.js.map} +0 -0
  489. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-b07cmahc.js.map} +0 -0
  490. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-r20tn01b.js.map} +0 -0
  491. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-tkacnehg.js.map} +0 -0
  492. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-tzmab476.js.map} +0 -0
@@ -15,8 +15,15 @@ import {
15
15
  pollingWatch,
16
16
  snapshotCache,
17
17
  } from "./cache.ts";
18
- import { codeFence, image, renderInline, renderLink } from "./lower.ts";
19
- import { slugifyPath } from "./normalize.ts";
18
+ import type { InlineRun } from "./lower.ts";
19
+ import {
20
+ codeFence,
21
+ guardBlockStart,
22
+ image,
23
+ linkParts,
24
+ renderInline,
25
+ } from "./lower.ts";
26
+ import { slugify, slugifyPath } from "./normalize.ts";
20
27
  import type {
21
28
  ContentSource,
22
29
  SourceContext,
@@ -154,26 +161,43 @@ interface NotionFrontmatter {
154
161
  [key: string]: boolean | string | { order: number } | undefined;
155
162
  }
156
163
 
164
+ /** Neighboring rich-text runs that link to one `href` (or none), and it. */
165
+ interface LinkedRuns {
166
+ href?: string;
167
+ runs: InlineRun[];
168
+ }
169
+
157
170
  /**
158
- * Rich text as Markdown for an MDX body. Each run goes through the shared
171
+ * Rich text as Markdown for an MDX body. The runs go through the shared
159
172
  * lowering, so a literal `{`, `<`, `*`, or `[` typed in Notion renders as
160
- * itself instead of opening a JSX expression, a tag, or emphasis, and a code
161
- * run keeps its text verbatim inside a long-enough code span.
173
+ * itself instead of opening a JSX expression, a tag, or emphasis, a code run
174
+ * keeps its text verbatim inside a long-enough code span, neighbors share
175
+ * their marks' delimiters, and neighbors that link to one `href` share one
176
+ * link.
162
177
  */
163
- const richToMarkdown = (rich: NotionRichText[] = []): string =>
164
- rich
165
- .map(({ annotations = {}, href, plain_text: text }) =>
166
- renderLink(
167
- renderInline(text, {
168
- bold: annotations.bold,
169
- code: annotations.code,
170
- italic: annotations.italic,
171
- strike: annotations.strikethrough,
172
- }),
173
- href ?? undefined
174
- )
175
- )
176
- .join("");
178
+ const richToMarkdown = (rich: NotionRichText[] = []): string => {
179
+ const linked: LinkedRuns[] = [];
180
+ for (const { annotations = {}, href, plain_text: text } of rich) {
181
+ const run: InlineRun = {
182
+ marks: {
183
+ bold: annotations.bold,
184
+ code: annotations.code,
185
+ italic: annotations.italic,
186
+ strike: annotations.strikethrough,
187
+ },
188
+ text,
189
+ };
190
+ const last = linked.at(-1);
191
+ if (last && last.href === (href ?? undefined)) {
192
+ last.runs.push(run);
193
+ } else {
194
+ linked.push({ href: href ?? undefined, runs: [run] });
195
+ }
196
+ }
197
+ return renderInline(
198
+ linked.flatMap(({ href, runs }) => linkParts(runs, href))
199
+ );
200
+ };
177
201
 
178
202
  const isBlockPayload = (
179
203
  value: NotionBlockPayload | boolean | string | undefined
@@ -217,12 +241,12 @@ const withNotionRetry = async <T>(
217
241
  if (status !== RATE_LIMITED || attempt === MAX_RETRIES) {
218
242
  throw error;
219
243
  }
220
- // SAFETY: same APIResponseError shape — `headers` maps lower-cased HTTP
221
- // header names to their values; a missing header yields `NaN` and falls
222
- // back to exponential backoff.
223
- const retryAfter = Number(
224
- (error as { headers?: Record<string, string> }).headers?.["retry-after"]
225
- );
244
+ // SAFETY: same APIResponseError shape — `headers` is the failed
245
+ // response's fetch `Headers`, read with `get()`; a missing header (or
246
+ // headers without `get`) yields no positive wait and falls back to
247
+ // exponential backoff.
248
+ const { headers } = error as { headers?: Headers };
249
+ const retryAfter = Number(headers?.get?.("retry-after"));
226
250
  const wait =
227
251
  retryAfter > 0
228
252
  ? retryAfter * SECOND_MS
@@ -266,6 +290,12 @@ const richToPlain = (rich: NotionRichText[] = []): string =>
266
290
  */
267
291
  const jsxString = (value: string): string => `{${JSON.stringify(value)}}`;
268
292
 
293
+ // `<Frame>` renders its caption as Markdown after escaping `<` and `>` itself
294
+ // (so a caption can't inject HTML), which turns the lowering's own `\<` into
295
+ // a visible `&lt;`. Its caption drops those backslashes and leaves the
296
+ // escaping to Frame; every other escape still applies.
297
+ const FRAME_ESCAPED = /\\(?=[<>])/gu;
298
+
269
299
  const YOUTUBE_HOST = /(?:^|\.)(?:youtube(?:-nocookie)?\.com|youtu\.be)$/u;
270
300
 
271
301
  // `parseYouTubeId` is host-agnostic on purpose (the component accepts bare
@@ -295,19 +325,23 @@ const renderVideo = (data: NotionBlockPayload): string => {
295
325
  const title = caption ? ` title=${jsxString(richToPlain(data.caption))}` : "";
296
326
  // Everything else (a Notion upload, or a direct link to a media file) is a
297
327
  // real video file: `materializeAssets` rewrites the `src`, which matters most
298
- // for uploads, whose Notion URLs are signed and expire.
328
+ // for uploads, whose Notion URLs are signed and expire. It finds the file by
329
+ // that quoted `src`, where MDX decodes no backslash escapes — a JSON
330
+ // string's `\\` and `\"` would corrupt the URL or end the attribute — so the
331
+ // URL goes in as written, with a double quote as the `%22` it means anyway.
332
+ // The embed's `url` takes the expression form, like every string prop here.
299
333
  const media = isYouTubeUrl(url)
300
- ? `<YouTube${title} url=${JSON.stringify(url)} />`
301
- : `<video controls src=${JSON.stringify(url)} />`;
334
+ ? `<YouTube${title} url=${jsxString(url)} />`
335
+ : `<video controls src="${url.replaceAll('"', "%22")}" />`;
302
336
  return caption
303
- ? `<Frame caption=${jsxString(caption)}>\n${media}\n</Frame>`
337
+ ? `<Frame caption=${jsxString(caption.replaceAll(FRAME_ESCAPED, ""))}>\n${media}\n</Frame>`
304
338
  : media;
305
339
  };
306
340
 
307
341
  /** Render a leaf (non-container) block to Markdown, or null for containers. */
308
342
  const renderLeaf = (block: NotionBlock): string | null => {
309
343
  const data = payloadOf(block) ?? {};
310
- const text = richToMarkdown(blockField(block));
344
+ const text = guardBlockStart(richToMarkdown(blockField(block)));
311
345
  switch (block.type) {
312
346
  case "paragraph": {
313
347
  return text;
@@ -440,7 +474,10 @@ export const notionSource = (
440
474
  };
441
475
 
442
476
  if (block.type === "callout") {
443
- const body = [richToMarkdown(blockField(block)), await children(block)]
477
+ const body = [
478
+ guardBlockStart(richToMarkdown(blockField(block))),
479
+ await children(block),
480
+ ]
444
481
  .filter(Boolean)
445
482
  .join("\n\n");
446
483
  return `<Callout>\n${body}\n</Callout>`;
@@ -562,9 +599,14 @@ export const notionSource = (
562
599
  const slugProp = richToPlain(
563
600
  page.properties[props.slug ?? "Slug"]?.rich_text
564
601
  );
565
- // Path-aware: a `guides/setup` slug keeps its `/` (per-segment slugging)
566
- // instead of mashing into `guidessetup`.
567
- const slug = slugifyPath(slugProp || title) || page.id;
602
+ // A Slug property is path-aware: `guides/setup` keeps its `/` (per-segment
603
+ // slugging) instead of mashing into `guidessetup`. A title is one name, so
604
+ // its slashes become hyphens: "CI/CD Setup" routes to `ci-cd-setup`, not
605
+ // to `cd-setup` inside an invented `ci` group.
606
+ const slug =
607
+ (slugProp
608
+ ? slugifyPath(slugProp)
609
+ : slugify(title.replaceAll("/", "-"))) || page.id;
568
610
  return { data, slug };
569
611
  };
570
612
 
@@ -680,5 +722,6 @@ export const notionSource = (
680
722
  () => load()
681
723
  )
682
724
  : undefined,
725
+ withContext: (next) => notionSource(options, next),
683
726
  };
684
727
  };
@@ -20,6 +20,8 @@ import {
20
20
  placeEntryRef,
21
21
  resolveEntryRoute,
22
22
  slugifyPath,
23
+ strippedLineOffset,
24
+ unloadablePathDiagnostic,
23
25
  } from "./normalize.ts";
24
26
  import type {
25
27
  ContentSource,
@@ -84,6 +86,8 @@ interface ParsedNote {
84
86
  /** The note body, frontmatter stripped and otherwise untouched. */
85
87
  content: string;
86
88
  data: SourceEntry["data"];
89
+ /** How many lines of the file the frontmatter block (if any) spans. */
90
+ frontmatterLines: number;
87
91
  placement: EntryPlacement;
88
92
  /** Vault-relative path, e.g. `guides/Getting Started.md`. */
89
93
  rel: string;
@@ -584,10 +588,11 @@ const transformBody = (
584
588
  * `[#custom-id]` pin and the `setup-1` a later collision gets are honored
585
589
  * rather than re-slugged. When no heading holds anything the rewrite changes,
586
590
  * the raw body's headings are the staged body's; otherwise the body is lowered
587
- * once more so the headings are scanned exactly as they ship. A link inside a
588
- * heading is rewritten here before the other notes' anchors are known, so its
589
- * own anchor is left off; that only moves the manifest id of a heading that
590
- * itself holds a heading link, which the docs already flag as unaddressable.
591
+ * once more so the headings are scanned exactly as they ship. A heading link
592
+ * inside a heading is rewritten here before every note's anchors are known,
593
+ * so its href may still lack the anchor it ships with. That moves no id: a
594
+ * heading is slugged by its rendered text, where the link is its label alone,
595
+ * so the heading stays as addressable as any other.
591
596
  */
592
597
  const fillAnchors = (pair: IndexedPair, index: LinkIndex): void => {
593
598
  const raw = extractHeadings(pair.note.content);
@@ -638,7 +643,7 @@ const buildLinkIndex = (
638
643
  { ref: note.rel, slug: entrySlugFor(note.placement.navPath) },
639
644
  ".md",
640
645
  isStringValue(note.data.slug) ? note.data.slug : undefined,
641
- options
646
+ { ...options, orderingPrefixes: true }
642
647
  );
643
648
  const built: IndexedNote = {
644
649
  anchors: new Map(),
@@ -756,10 +761,17 @@ const noteToEntry = (
756
761
  )
757
762
  );
758
763
  const merged = title === undefined ? data : { ...data, title };
759
- const text = transformBody(note.content.trim(), index, pair.self, unresolved);
764
+ const body = note.content.trim();
765
+ const text = transformBody(body, index, pair.self, unresolved);
760
766
  const raw = matter.stringify(`${text}\n`, merged);
767
+ // `raw` re-serializes the frontmatter without the dropped properties, so
768
+ // its height is not the file's: count the file's own frontmatter and the
769
+ // blank lines the trim dropped, so a link diagnostic names the line the
770
+ // author sees in the note.
771
+ const leading = note.content.slice(0, note.content.indexOf(body));
761
772
  return {
762
773
  body: { format: "md", text },
774
+ bodyLineOffset: note.frontmatterLines + leading.split("\n").length - 1,
763
775
  data: merged,
764
776
  hash: hashText(raw),
765
777
  raw,
@@ -782,11 +794,13 @@ const readNote = async (
782
794
  ): Promise<ParsedNote | null> => {
783
795
  const absPath = join(vaultDir, rel);
784
796
  try {
785
- const { content, data } = matter(await readFile(absPath, "utf-8"));
797
+ const file = await readFile(absPath, "utf-8");
798
+ const { content, data } = matter(file);
786
799
  return {
787
800
  absPath,
788
801
  content,
789
802
  data,
803
+ frontmatterLines: strippedLineOffset(file, content),
790
804
  // Version outermost, then locale, the way `normalizeEntry` reads the
791
805
  // ref — the slug is built from what remains, so the pipeline's own
792
806
  // re-prefixing does not stack a second `fr/` or `v1.0/` onto the route.
@@ -946,7 +960,19 @@ export const obsidianSource = (
946
960
  const exclude = new Set([...BLUME_IGNORE_DIRS, ...(options.exclude ?? [])]);
947
961
 
948
962
  const load = async (): Promise<SourceLoadResult> => {
949
- const files = await walkVault(vaultDir, vaultDir, exclude);
963
+ // A note is staged under its vault path, where Astro's content loader
964
+ // can't read a name holding `#` or `?`; such a note is reported and left
965
+ // out, and links to it degrade like links to any missing note.
966
+ const skipped: Diagnostic[] = [];
967
+ const files: string[] = [];
968
+ for (const rel of await walkVault(vaultDir, vaultDir, exclude)) {
969
+ const unloadable = unloadablePathDiagnostic(rel, join(vaultDir, rel));
970
+ if (unloadable) {
971
+ skipped.push(unloadable);
972
+ } else {
973
+ files.push(rel);
974
+ }
975
+ }
950
976
  const read = await Promise.all(
951
977
  files.map((rel) => readNote(vaultDir, rel, options))
952
978
  );
@@ -959,7 +985,10 @@ export const obsidianSource = (
959
985
  noteToEntry(pair, index, options, unresolved)
960
986
  );
961
987
  return {
962
- diagnostics: unresolvedDiagnostics(options.name, unresolved),
988
+ diagnostics: [
989
+ ...skipped,
990
+ ...unresolvedDiagnostics(options.name, unresolved),
991
+ ],
963
992
  entries,
964
993
  };
965
994
  };
@@ -1030,6 +1059,8 @@ export const obsidianSource = (
1030
1059
  contentRoot: vaultDir,
1031
1060
  load,
1032
1061
  name: options.name,
1062
+ // Notes are named like files: `01 Intro` sorts first and routes `/intro`.
1063
+ orderedNames: true,
1033
1064
  prefix: options.prefix,
1034
1065
  read,
1035
1066
  staged: true,
@@ -129,6 +129,7 @@ export const payloadSource = (
129
129
  name: options.name,
130
130
  pollInterval: options.pollInterval,
131
131
  prefix: options.prefix,
132
+ withContext: (next) => payloadSource(options, next),
132
133
  },
133
134
  ctx
134
135
  );
@@ -4,7 +4,15 @@
4
4
  * user-supplied serializer or are skipped with a noted comment. Output is
5
5
  * Markdown text that flows through Blume's normal pipeline.
6
6
  */
7
- import { escapeMarkdownText, image, unsupported, writesMdx } from "./lower.ts";
7
+ import type { InlineRun } from "./lower.ts";
8
+ import {
9
+ guardBlockStart,
10
+ image,
11
+ linkParts,
12
+ renderInline,
13
+ unsupported,
14
+ writesMdx,
15
+ } from "./lower.ts";
8
16
 
9
17
  /**
10
18
  * A field value on a Portable Text node: the arbitrary JSON the CMS query
@@ -66,64 +74,91 @@ const HEADING_STYLES = new Map([
66
74
  ["h6", "###### "],
67
75
  ]);
68
76
 
69
- /** Wrap a span's text in Markdown for its marks (decorators + link defs). */
70
- const renderSpan = (
71
- span: PortableTextSpan,
72
- defs: Map<string, PortableTextMarkDef>
73
- ): string => {
74
- // Code spans stay verbatim: their text is literal inside the backticks,
75
- // and backslash escapes would render as backslashes.
76
- const isCode = span.marks?.includes("code") ?? false;
77
- let text = isCode ? (span.text ?? "") : escapeMarkdownText(span.text ?? "");
78
- if (!span.marks || span.marks.length === 0) {
79
- return text;
80
- }
81
- // Decorators wrap inline; a link def wraps last so its label keeps emphasis.
82
- let link: PortableTextMarkDef | undefined;
83
- for (const mark of span.marks) {
84
- switch (mark) {
85
- case "strong": {
86
- text = `**${text}**`;
87
- break;
88
- }
89
- case "em": {
90
- text = `*${text}*`;
91
- break;
92
- }
93
- case "code": {
94
- text = `\`${text}\``;
95
- break;
96
- }
97
- case "strike-through": {
98
- text = `~~${text}~~`;
99
- break;
100
- }
101
- default: {
102
- const def = defs.get(mark);
103
- if (def?._type === "link") {
104
- link = def;
105
- }
106
- }
107
- }
108
- }
109
- return link?.href ? `[${text}](${link.href})` : text;
77
+ /** A span's text and the marks Markdown has syntax for. */
78
+ const spanRun = (span: PortableTextSpan): InlineRun => {
79
+ const marks = span.marks ?? [];
80
+ return {
81
+ marks: {
82
+ bold: marks.includes("strong"),
83
+ code: marks.includes("code"),
84
+ italic: marks.includes("em"),
85
+ strike: marks.includes("strike-through"),
86
+ },
87
+ text: span.text ?? "",
88
+ };
110
89
  };
111
90
 
112
- /** Render the inline children of a block to a single Markdown string. */
91
+ /** Neighboring spans under one link def (or none), and that def. */
92
+ interface LinkedSpans {
93
+ link?: PortableTextMarkDef;
94
+ runs: InlineRun[];
95
+ }
96
+
97
+ /**
98
+ * Render the inline children of a block to a single Markdown string, through
99
+ * the shared lowering every CMS uses: prose is escaped, a code span outruns
100
+ * the backticks inside it, edge whitespace stays outside emphasis, neighbors
101
+ * share their marks' delimiters, and neighbors under one link def share one
102
+ * link — wrapped last (so its label keeps emphasis), and only when its
103
+ * destination is safe to click.
104
+ */
113
105
  const renderChildren = (block: PortableTextBlock): string => {
114
106
  const defs = new Map(
115
107
  (block.markDefs ?? []).map((def) => [def._key, def] as const)
116
108
  );
117
- return (block.children ?? []).map((span) => renderSpan(span, defs)).join("");
109
+ const linked: LinkedSpans[] = [];
110
+ for (const span of block.children ?? []) {
111
+ const link = (span.marks ?? [])
112
+ .map((mark) => defs.get(mark))
113
+ .findLast((def) => def?._type === "link");
114
+ const last = linked.at(-1);
115
+ if (last && last.link === link) {
116
+ last.runs.push(spanRun(span));
117
+ } else {
118
+ linked.push({ link, runs: [spanRun(span)] });
119
+ }
120
+ }
121
+ return guardBlockStart(
122
+ renderInline(
123
+ linked.flatMap(({ link, runs }) => linkParts(runs, link?.href))
124
+ )
125
+ );
118
126
  };
119
127
 
120
128
  /** Whether an image block's `alt` field is usable alt text (CMS JSON may hold anything). */
121
129
  const isAltText = (value: PortableTextValue): value is string =>
122
130
  typeof value === "string";
123
131
 
132
+ /** The marker a list block opens with. */
133
+ const listMarker = (block: PortableTextBlock): string =>
134
+ block.listItem === "number" ? "1." : "-";
135
+
136
+ /**
137
+ * How far each block is indented. Portable Text lists are flat blocks with a
138
+ * `level`, while Markdown nests an item by indenting it to its parent item's
139
+ * content column: two past a `-` item's indent, three past a `1.` item's. The
140
+ * column comes from the item actually above, so a skipped level nests under
141
+ * the deepest open item instead of indenting into a code block.
142
+ */
143
+ const listIndents = (blocks: PortableTextBlock[]): number[] => {
144
+ const columns: number[] = [];
145
+ return blocks.map((block) => {
146
+ if (!block.listItem) {
147
+ columns.length = 0;
148
+ return 0;
149
+ }
150
+ const depth = Math.min(Math.max(0, (block.level ?? 1) - 1), columns.length);
151
+ const indent = columns[depth - 1] ?? 0;
152
+ columns.length = depth;
153
+ columns.push(indent + listMarker(block).length + 1);
154
+ return indent;
155
+ });
156
+ };
157
+
124
158
  const renderBlock = (
125
159
  block: PortableTextBlock,
126
- options: PortableTextOptions
160
+ options: PortableTextOptions,
161
+ indent: number
127
162
  ): string => {
128
163
  const custom = options.serializers?.[block._type];
129
164
  if (custom) {
@@ -143,9 +178,7 @@ const renderBlock = (
143
178
 
144
179
  const inline = renderChildren(block);
145
180
  if (block.listItem) {
146
- const indent = " ".repeat(Math.max(0, (block.level ?? 1) - 1));
147
- const marker = block.listItem === "number" ? "1." : "-";
148
- return `${indent}${marker} ${inline}`;
181
+ return `${" ".repeat(indent)}${listMarker(block)} ${inline}`;
149
182
  }
150
183
  if (block.style === "blockquote") {
151
184
  return `> ${inline}`;
@@ -158,7 +191,10 @@ export const portableTextToMarkdown = (
158
191
  blocks: PortableTextBlock[],
159
192
  options: PortableTextOptions = {}
160
193
  ): string => {
161
- const lines = blocks.map((block) => renderBlock(block, options));
194
+ const indents = listIndents(blocks);
195
+ const lines = blocks.map((block, i) =>
196
+ renderBlock(block, options, indents[i] ?? 0)
197
+ );
162
198
  // List items are single-newline separated; everything else gets a blank line.
163
199
  const out: string[] = [];
164
200
  for (let i = 0; i < lines.length; i += 1) {
@@ -1,6 +1,7 @@
1
1
  import { join } from "pathe";
2
2
 
3
3
  import matter from "../frontmatter.ts";
4
+ import { neutralizeUnsafeLinks } from "../safe-links.ts";
4
5
  import {
5
6
  hashText,
6
7
  loadWithCache,
@@ -19,12 +20,21 @@ import type {
19
20
 
20
21
  /** What every REST-backed CMS source needs to be a staged, cached source. */
21
22
  export interface RemoteSourceOptions {
23
+ /**
24
+ * Throw a `BlumeError` when the source cannot load as configured. Runs
25
+ * before the cache is consulted: thrown from `fetchEntries` instead, a
26
+ * misconfiguration would be served over by a stale snapshot with only an
27
+ * offline warning.
28
+ */
29
+ assertConfigured?: () => void;
22
30
  /** Pull every entry from the API; called on refresh and on each poll. */
23
31
  fetchEntries: () => Promise<SourceEntry[]>;
24
32
  name: string;
25
33
  /** Opt-in dev polling interval (seconds); omit to freeze for the session. */
26
34
  pollInterval?: number;
27
35
  prefix?: string;
36
+ /** The adapter rebuilt on another context (`ContentSource.withContext`). */
37
+ withContext?: (ctx: SourceContext) => ContentSource;
28
38
  }
29
39
 
30
40
  /**
@@ -45,6 +55,7 @@ export const remoteSource = (
45
55
  const load = async (
46
56
  refresh = ctx?.refresh ?? true
47
57
  ): Promise<SourceLoadResult> => {
58
+ options.assertConfigured?.();
48
59
  const result = await loadWithCache(
49
60
  options.name,
50
61
  cache,
@@ -77,6 +88,7 @@ export const remoteSource = (
77
88
  () => load()
78
89
  )
79
90
  : undefined,
91
+ withContext: options.withContext,
80
92
  };
81
93
  };
82
94
 
@@ -182,7 +194,8 @@ export interface RemoteFieldMap {
182
194
  * when the slug field is missing or slugifies to nothing (pure punctuation),
183
195
  * so distinct documents never collapse onto one `untitled.md`; a slashed
184
196
  * slug keeps its segments. A string body is Markdown and passes through as
185
- * `.md`; any other shape goes to the CMS's lowerer, and is written as `.mdx`
197
+ * `.md`, its unsafe links reduced to their labels; any other shape goes to
198
+ * the CMS's lowerer, and is written as `.mdx`
186
199
  * when `lowersToMdx` (the source has serializers, see `writesMdx`).
187
200
  */
188
201
  export const documentEntry = (
@@ -211,7 +224,10 @@ export const documentEntry = (
211
224
  let markdown = "";
212
225
  let format: "md" | "mdx" = "md";
213
226
  if (isStringValue(body)) {
214
- markdown = `${body.trimEnd()}\n`;
227
+ // The CMS's Markdown is its editors', not the site author's: a link whose
228
+ // destination isn't a web, mail, or relative address (`javascript:`)
229
+ // keeps only its label, as release notes do (see `safe-links.ts`).
230
+ markdown = neutralizeUnsafeLinks(`${body.trimEnd()}\n`);
215
231
  } else if (body !== undefined) {
216
232
  markdown = lower(body);
217
233
  format = lowersToMdx ? "mdx" : "md";