blume 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli/{chunk-vtk4a6dg.js → chunk-00gs3wqs.js} +1 -1
  3. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  4. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  5. package/dist/cli/{chunk-yw7dm696.js → chunk-3yce002v.js} +21 -8
  6. package/dist/cli/{chunk-yw7dm696.js.map → chunk-3yce002v.js.map} +3 -3
  7. package/dist/cli/{chunk-yt5n7ppj.js → chunk-4e9b9ra6.js} +17 -7
  8. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  9. package/dist/cli/{chunk-fs23ddbb.js → chunk-5r8g91qn.js} +352 -676
  10. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  11. package/dist/cli/{chunk-6vm74dry.js → chunk-6dtt0zfn.js} +8 -8
  12. package/dist/cli/{chunk-qwsrynx5.js → chunk-6k4ftwze.js} +38 -19
  13. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-6k4ftwze.js.map} +4 -4
  14. package/dist/cli/{chunk-qs4q5p4e.js → chunk-7mbqtmgb.js} +16 -7
  15. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  16. package/dist/cli/{chunk-q5163e60.js → chunk-7vtckvaw.js} +21 -19
  17. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  18. package/dist/cli/{chunk-kdp5q7ke.js → chunk-8g8ytmgx.js} +17 -18
  19. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-8g8ytmgx.js.map} +2 -2
  20. package/dist/cli/{chunk-epjnccmv.js → chunk-91ws1n6j.js} +18 -15
  21. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  22. package/dist/cli/{chunk-fxypxtvm.js → chunk-bbnwccaz.js} +2 -2
  23. package/dist/cli/{chunk-m3vmjgmq.js → chunk-bfwp9vp6.js} +16 -8
  24. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  25. package/dist/cli/{chunk-f2z5v128.js → chunk-d1v5rhy0.js} +14 -15
  26. package/dist/cli/{chunk-f2z5v128.js.map → chunk-d1v5rhy0.js.map} +2 -2
  27. package/dist/cli/{chunk-6crbhc3x.js → chunk-ddndchfr.js} +21 -6
  28. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  29. package/dist/cli/{chunk-ce574jw2.js → chunk-esh98wmb.js} +1 -1
  30. package/dist/cli/{chunk-zxcczpyx.js → chunk-fsmrqk8a.js} +1 -1
  31. package/dist/cli/{chunk-hdpx1tax.js → chunk-g698a744.js} +5 -5
  32. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  33. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  34. package/dist/cli/{chunk-zxh4d9vy.js → chunk-h2ez8dzb.js} +4 -4
  35. package/dist/cli/{chunk-5shv93fd.js → chunk-h7k3nq3v.js} +2 -2
  36. package/dist/cli/{chunk-79jhk4py.js → chunk-hqp2ajnh.js} +251 -122
  37. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  38. package/dist/cli/{chunk-2hn4b8z7.js → chunk-hr8ne106.js} +109 -40
  39. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  40. package/dist/cli/{chunk-6hsn950k.js → chunk-j85scx15.js} +62 -18
  41. package/dist/cli/chunk-j85scx15.js.map +10 -0
  42. package/dist/cli/{chunk-s1p84fyh.js → chunk-k7pj68a8.js} +63 -22
  43. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  44. package/dist/cli/{chunk-ah61y8py.js → chunk-mqc662a6.js} +2 -2
  45. package/dist/cli/{chunk-ch6g3ar0.js → chunk-n1yg3tj3.js} +4 -4
  46. package/dist/cli/{chunk-mb2919y2.js → chunk-nfcyttvj.js} +17 -6
  47. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  48. package/dist/cli/{chunk-kpf8rrjc.js → chunk-pbg5a4s3.js} +56 -24
  49. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  50. package/dist/cli/{chunk-jwyddg7y.js → chunk-ppzjqwx2.js} +21 -14
  51. package/dist/cli/{chunk-jwyddg7y.js.map → chunk-ppzjqwx2.js.map} +4 -4
  52. package/dist/cli/{chunk-dh8cwk36.js → chunk-qkb5a8sa.js} +24 -9
  53. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  54. package/dist/cli/{chunk-wm7js3j9.js → chunk-sqw4ekg1.js} +2 -2
  55. package/dist/cli/{chunk-qkqwkpte.js → chunk-v6ya5kcb.js} +3083 -953
  56. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  57. package/dist/cli/{chunk-fz5wtpmh.js → chunk-w4bxdvsa.js} +18 -15
  58. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  59. package/dist/cli/{chunk-27g6wdth.js → chunk-wdrt2k2v.js} +2 -2
  60. package/dist/cli/{chunk-wgm7m9qk.js → chunk-xh43dwgw.js} +158 -43
  61. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  62. package/dist/cli/{chunk-s6jhgk0q.js → chunk-zp79m0ts.js} +2 -2
  63. package/dist/cli/index.js +160 -35
  64. package/dist/cli/index.js.map +4 -4
  65. package/dist/types/ai/agent-surface.d.ts +32 -0
  66. package/dist/types/ai/api-catalog.d.ts +7 -1
  67. package/dist/types/ai/ask-context.d.ts +7 -0
  68. package/dist/types/ai/component-markdown.d.ts +4 -4
  69. package/dist/types/ai/relative-links.d.ts +9 -4
  70. package/dist/types/ai/static-expression.d.ts +28 -0
  71. package/dist/types/analytics/databuddy.d.ts +43 -0
  72. package/dist/types/analytics/index.d.ts +2 -0
  73. package/dist/types/analytics/schema.d.ts +14 -0
  74. package/dist/types/cli/env.d.ts +5 -0
  75. package/dist/types/cli/init/scaffold.d.ts +19 -3
  76. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  77. package/dist/types/core/base-path.d.ts +9 -0
  78. package/dist/types/core/config-input.d.ts +4 -4
  79. package/dist/types/core/config.d.ts +2 -2
  80. package/dist/types/core/graph.d.ts +2 -0
  81. package/dist/types/core/i18n-ui.d.ts +31 -0
  82. package/dist/types/core/i18n.d.ts +7 -1
  83. package/dist/types/core/links.d.ts +3 -1
  84. package/dist/types/core/load-module.d.ts +10 -0
  85. package/dist/types/core/locale-links.d.ts +12 -2
  86. package/dist/types/core/meta.d.ts +5 -1
  87. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  88. package/dist/types/core/navigation.d.ts +38 -0
  89. package/dist/types/core/ordering-prefix.d.ts +4 -0
  90. package/dist/types/core/safe-links.d.ts +3 -1
  91. package/dist/types/core/schema.d.ts +52 -13
  92. package/dist/types/core/sources/github-releases.d.ts +5 -0
  93. package/dist/types/core/sources/lower.d.ts +14 -2
  94. package/dist/types/core/sources/normalize.d.ts +10 -1
  95. package/dist/types/core/sources/remote.d.ts +11 -1
  96. package/dist/types/core/sources/resolve.d.ts +12 -0
  97. package/dist/types/core/sources/types.d.ts +31 -0
  98. package/dist/types/core/types.d.ts +9 -0
  99. package/dist/types/deploy/adapters/node.d.ts +5 -2
  100. package/dist/types/deploy/adapters/types.d.ts +7 -0
  101. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  102. package/dist/types/deploy/headers.d.ts +31 -7
  103. package/dist/types/deploy/node-headers.d.ts +43 -8
  104. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  105. package/dist/types/deploy/platforms/node.d.ts +6 -5
  106. package/dist/types/deploy/platforms/types.d.ts +7 -0
  107. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  108. package/dist/types/deploy/redirects.d.ts +7 -2
  109. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  110. package/dist/types/openapi/model.d.ts +20 -6
  111. package/dist/types/search/sync/algolia.d.ts +3 -1
  112. package/docs/01-quickstart.mdx +3 -2
  113. package/docs/02-deployment.mdx +17 -8
  114. package/docs/08-faq.mdx +9 -2
  115. package/docs/advanced/changelog.mdx +1 -1
  116. package/docs/advanced/custom-pages.mdx +4 -2
  117. package/docs/cli/audit.mdx +2 -2
  118. package/docs/cli/doctor.mdx +2 -2
  119. package/docs/cli/evals.mdx +2 -2
  120. package/docs/cli/index.mdx +4 -1
  121. package/docs/cli/translate.mdx +1 -1
  122. package/docs/configuration/analytics.mdx +23 -2
  123. package/docs/configuration/assistant.mdx +1 -1
  124. package/docs/configuration/customization.mdx +4 -3
  125. package/docs/configuration/index.mdx +5 -3
  126. package/docs/configuration/search.mdx +1 -1
  127. package/docs/content/components.mdx +1 -1
  128. package/docs/content/frontmatter.mdx +5 -1
  129. package/docs/content/i18n.mdx +1 -1
  130. package/docs/content/index.mdx +4 -2
  131. package/docs/content/meta.mdx +5 -3
  132. package/docs/content/navigation.mdx +29 -4
  133. package/docs/content/sources.mdx +18 -12
  134. package/docs/content/versioning.mdx +1 -0
  135. package/docs/discoverability/agent-discovery.mdx +21 -7
  136. package/docs/discoverability/index.mdx +2 -2
  137. package/docs/discoverability/llms-txt.mdx +2 -5
  138. package/docs/discoverability/markdown.mdx +5 -3
  139. package/docs/discoverability/mcp.mdx +4 -0
  140. package/docs/discoverability/metadata.mdx +3 -2
  141. package/docs/discoverability/open-graph.mdx +6 -4
  142. package/docs/discoverability/rss.mdx +3 -1
  143. package/docs/references/asyncapi.mdx +1 -1
  144. package/docs/references/graphql.mdx +2 -2
  145. package/docs/references/openapi.mdx +3 -3
  146. package/package.json +1 -1
  147. package/skills/blume-migrate/SKILL.md +6 -6
  148. package/skills/blume-migrate/references/docusaurus.md +6 -6
  149. package/skills/blume-migrate/references/mintlify.md +3 -3
  150. package/skills/blume-migrate/references/monorepo.md +1 -1
  151. package/skills/blume-migrate/references/nextra.md +1 -1
  152. package/skills/blume-migrate/references/starlight.md +5 -5
  153. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  154. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  155. package/src/ai/agent-surface.ts +56 -0
  156. package/src/ai/api/spec.ts +15 -2
  157. package/src/ai/api-catalog.ts +7 -1
  158. package/src/ai/ask-context.ts +14 -2
  159. package/src/ai/ask-data.ts +26 -12
  160. package/src/ai/component-markdown.ts +43 -37
  161. package/src/ai/link-headers.ts +8 -3
  162. package/src/ai/llms.ts +4 -3
  163. package/src/ai/markdown.ts +23 -11
  164. package/src/ai/mcp/server.ts +78 -7
  165. package/src/ai/relative-links.ts +78 -10
  166. package/src/ai/static-expression.ts +416 -0
  167. package/src/ai/visibility.ts +45 -14
  168. package/src/analytics/databuddy.ts +67 -0
  169. package/src/analytics/head.ts +4 -0
  170. package/src/analytics/index.ts +2 -0
  171. package/src/analytics/posthog.ts +21 -3
  172. package/src/analytics/schema.ts +2 -0
  173. package/src/astro/generate.ts +71 -27
  174. package/src/astro/templates.ts +61 -23
  175. package/src/audit/catalog.ts +2 -2
  176. package/src/audit/checks/assets.ts +25 -5
  177. package/src/audit/checks/content.ts +20 -2
  178. package/src/audit/checks/i18n.ts +46 -8
  179. package/src/audit/checks/indexability.ts +39 -19
  180. package/src/audit/checks/links.ts +14 -0
  181. package/src/audit/checks/llms.ts +6 -3
  182. package/src/audit/checks/network.ts +3 -1
  183. package/src/audit/checks/og-image.ts +10 -0
  184. package/src/audit/checks/robots.ts +6 -1
  185. package/src/audit/checks/sitemap.ts +56 -31
  186. package/src/audit/checks/social.ts +21 -2
  187. package/src/audit/crawl.ts +88 -15
  188. package/src/audit/report.ts +54 -17
  189. package/src/audit/run.ts +1 -0
  190. package/src/audit/types.ts +18 -0
  191. package/src/audit/url.ts +37 -6
  192. package/src/cli/build-failure.ts +50 -0
  193. package/src/cli/commands/audit.ts +7 -0
  194. package/src/cli/commands/build.ts +19 -5
  195. package/src/cli/commands/check.ts +2 -0
  196. package/src/cli/commands/eval.ts +1 -7
  197. package/src/cli/commands/init.ts +15 -5
  198. package/src/cli/commands/preview.ts +15 -0
  199. package/src/cli/commands/sync.ts +2 -0
  200. package/src/cli/commands/translate.ts +9 -6
  201. package/src/cli/commands/upgrade.ts +11 -0
  202. package/src/cli/eject-scripts.ts +32 -7
  203. package/src/cli/env.ts +12 -1
  204. package/src/cli/init/scaffold.ts +117 -11
  205. package/src/cli/init/starter-spec.ts +235 -0
  206. package/src/components/content/AccordionItem.astro +26 -22
  207. package/src/components/content/Badge.astro +2 -9
  208. package/src/components/content/Card.astro +2 -2
  209. package/src/components/content/Component.astro +9 -3
  210. package/src/components/content/Expandable.astro +5 -1
  211. package/src/components/content/Frame.astro +2 -7
  212. package/src/components/content/GithubInfo.astro +12 -2
  213. package/src/components/content/Prompt.astro +2 -7
  214. package/src/components/content/Tabs.astro +54 -5
  215. package/src/components/content/Tile.astro +1 -1
  216. package/src/components/content/Tooltip.astro +69 -7
  217. package/src/components/content/Tree.astro +7 -2
  218. package/src/components/content/TypeTable.astro +10 -5
  219. package/src/components/content/Update.astro +8 -2
  220. package/src/components/content/auto-type-table.ts +4 -1
  221. package/src/components/content/badge-color.ts +17 -0
  222. package/src/components/content/base-href.ts +18 -3
  223. package/src/components/content/inline-markdown.ts +27 -7
  224. package/src/components/copy-feedback.ts +35 -8
  225. package/src/components/islands/assistant.tsx +16 -2
  226. package/src/components/islands/hooks.ts +7 -2
  227. package/src/components/islands/webmcp.ts +12 -8
  228. package/src/components/layout/Banner.astro +23 -4
  229. package/src/components/layout/Header.astro +22 -9
  230. package/src/components/layout/Logo.astro +5 -0
  231. package/src/components/layout/NavSelector.astro +6 -1
  232. package/src/components/layout/NavTabMenu.astro +133 -0
  233. package/src/components/layout/NavTree.astro +15 -6
  234. package/src/components/layout/NavTreeCache.astro +5 -2
  235. package/src/components/layout/NavTreeScript.astro +45 -5
  236. package/src/components/layout/PageLayout.astro +22 -8
  237. package/src/components/layout/ReferenceLayout.astro +4 -1
  238. package/src/components/layout/RootLayout.astro +21 -8
  239. package/src/components/layout/Search.astro +5 -1
  240. package/src/components/layout/analytics-client.ts +2 -0
  241. package/src/components/openapi/GraphqlType.astro +11 -3
  242. package/src/components/openapi/MessageComposer.astro +1 -1
  243. package/src/components/openapi/Operation.astro +21 -5
  244. package/src/components/openapi/PanelTabs.astro +4 -1
  245. package/src/components/openapi/Playground.astro +8 -2
  246. package/src/components/openapi/RequestPanel.astro +9 -5
  247. package/src/components/openapi/SchemaProperty.astro +11 -33
  248. package/src/components/openapi/SchemaTable.astro +27 -52
  249. package/src/components/openapi/helpers.ts +90 -12
  250. package/src/components/openapi/message-composer.ts +8 -0
  251. package/src/components/openapi/message-model.ts +12 -2
  252. package/src/components/openapi/message.ts +4 -1
  253. package/src/components/openapi/operation-model.ts +40 -8
  254. package/src/components/openapi/panel.ts +29 -5
  255. package/src/components/openapi/playground-client.ts +37 -7
  256. package/src/components/openapi/playground-schema.ts +25 -6
  257. package/src/components/openapi/request.ts +2 -0
  258. package/src/components/openapi/schema-tree.ts +203 -0
  259. package/src/components/openapi/snippets.ts +30 -15
  260. package/src/components/openapi/validate-json.ts +1 -1
  261. package/src/core/base-path.ts +15 -0
  262. package/src/core/config-input.ts +4 -4
  263. package/src/core/config.ts +18 -4
  264. package/src/core/diagnostics.ts +217 -30
  265. package/src/core/graph.ts +120 -11
  266. package/src/core/i18n-ui.ts +39 -2
  267. package/src/core/i18n.ts +13 -2
  268. package/src/core/links.ts +7 -4
  269. package/src/core/load-module.ts +20 -0
  270. package/src/core/locale-links.ts +17 -17
  271. package/src/core/manifest.ts +3 -2
  272. package/src/core/meta.ts +69 -6
  273. package/src/core/nav-diagnostics.ts +56 -1
  274. package/src/core/navigation.ts +245 -76
  275. package/src/core/ordering-prefix.ts +27 -0
  276. package/src/core/project-graph.ts +23 -1
  277. package/src/core/safe-href.ts +53 -1
  278. package/src/core/safe-links.ts +11 -2
  279. package/src/core/schema.ts +70 -14
  280. package/src/core/sources/assets.ts +83 -41
  281. package/src/core/sources/contentful-rich-text.ts +3 -2
  282. package/src/core/sources/contentful.ts +25 -12
  283. package/src/core/sources/filesystem.ts +5 -1
  284. package/src/core/sources/github-releases.ts +106 -5
  285. package/src/core/sources/lexical.ts +9 -4
  286. package/src/core/sources/lower.ts +79 -26
  287. package/src/core/sources/mdx-remote.ts +1 -0
  288. package/src/core/sources/normalize.ts +132 -30
  289. package/src/core/sources/notion.ts +26 -10
  290. package/src/core/sources/obsidian.ts +17 -3
  291. package/src/core/sources/payload.ts +1 -0
  292. package/src/core/sources/portable-text.ts +63 -44
  293. package/src/core/sources/remote.ts +18 -2
  294. package/src/core/sources/resolve.ts +52 -33
  295. package/src/core/sources/sanity.ts +1 -0
  296. package/src/core/sources/strapi-blocks.ts +9 -4
  297. package/src/core/sources/strapi.ts +1 -0
  298. package/src/core/sources/types.ts +31 -0
  299. package/src/core/types.ts +9 -0
  300. package/src/core/ui-packs/ar.ts +13 -0
  301. package/src/core/ui-packs/bg.ts +13 -0
  302. package/src/core/ui-packs/bn.ts +13 -0
  303. package/src/core/ui-packs/ca.ts +13 -0
  304. package/src/core/ui-packs/cs.ts +13 -0
  305. package/src/core/ui-packs/da.ts +13 -0
  306. package/src/core/ui-packs/de.ts +13 -0
  307. package/src/core/ui-packs/el.ts +13 -0
  308. package/src/core/ui-packs/es.ts +13 -0
  309. package/src/core/ui-packs/fa.ts +13 -0
  310. package/src/core/ui-packs/fi.ts +13 -0
  311. package/src/core/ui-packs/fr.ts +13 -0
  312. package/src/core/ui-packs/he.ts +13 -0
  313. package/src/core/ui-packs/hi.ts +13 -0
  314. package/src/core/ui-packs/hr.ts +13 -0
  315. package/src/core/ui-packs/hu.ts +13 -0
  316. package/src/core/ui-packs/id.ts +13 -0
  317. package/src/core/ui-packs/it.ts +13 -0
  318. package/src/core/ui-packs/ja.ts +13 -0
  319. package/src/core/ui-packs/ko.ts +13 -0
  320. package/src/core/ui-packs/nl.ts +13 -0
  321. package/src/core/ui-packs/no.ts +13 -0
  322. package/src/core/ui-packs/pl.ts +13 -0
  323. package/src/core/ui-packs/pt-br.ts +13 -0
  324. package/src/core/ui-packs/pt.ts +13 -0
  325. package/src/core/ui-packs/ro.ts +13 -0
  326. package/src/core/ui-packs/ru.ts +13 -0
  327. package/src/core/ui-packs/sk.ts +13 -0
  328. package/src/core/ui-packs/sr.ts +13 -0
  329. package/src/core/ui-packs/sv.ts +13 -0
  330. package/src/core/ui-packs/th.ts +13 -0
  331. package/src/core/ui-packs/tr.ts +13 -0
  332. package/src/core/ui-packs/uk.ts +13 -0
  333. package/src/core/ui-packs/vi.ts +13 -0
  334. package/src/core/ui-packs/zh-tw.ts +13 -0
  335. package/src/core/ui-packs/zh.ts +13 -0
  336. package/src/core/version-cut.ts +17 -2
  337. package/src/core/versions.ts +4 -1
  338. package/src/deploy/adapters/node.ts +5 -2
  339. package/src/deploy/adapters/registry.ts +2 -1
  340. package/src/deploy/adapters/types.ts +13 -1
  341. package/src/deploy/artifacts.ts +25 -5
  342. package/src/deploy/cloudflare-negotiation.ts +23 -4
  343. package/src/deploy/headers.ts +67 -51
  344. package/src/deploy/node-headers.ts +148 -27
  345. package/src/deploy/platforms/cloudflare.ts +1 -0
  346. package/src/deploy/platforms/netlify.ts +82 -5
  347. package/src/deploy/platforms/node.ts +7 -5
  348. package/src/deploy/platforms/static.ts +1 -0
  349. package/src/deploy/platforms/types.ts +7 -0
  350. package/src/deploy/platforms/vercel.ts +9 -3
  351. package/src/deploy/redirects.ts +14 -3
  352. package/src/deploy/vercel-negotiation.ts +35 -3
  353. package/src/eval/agents.ts +10 -2
  354. package/src/eval/run.ts +25 -0
  355. package/src/markdown/base-links.ts +55 -8
  356. package/src/markdown/index.ts +6 -3
  357. package/src/markdown/relative-links.ts +3 -23
  358. package/src/markdown/route-snapshot.ts +37 -0
  359. package/src/og/card.ts +20 -4
  360. package/src/og/derive.ts +145 -4
  361. package/src/openapi/graphql-build.ts +28 -2
  362. package/src/openapi/model.ts +77 -13
  363. package/src/openapi/render-mdx.ts +10 -1
  364. package/src/registry/eject.ts +119 -22
  365. package/src/search/documents.ts +22 -1
  366. package/src/search/sync/algolia.ts +36 -2
  367. package/src/sources/registry.ts +5 -0
  368. package/src/theme/entry.ts +11 -4
  369. package/src/translate/agents.ts +6 -1
  370. package/src/translate/ledger.ts +26 -3
  371. package/src/translate/meta.ts +11 -3
  372. package/src/translate/run.ts +11 -5
  373. package/src/translate/validate.ts +10 -1
  374. package/src/translate/work-list.ts +36 -3
  375. package/src/upgrade/upgrade.ts +36 -4
  376. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  377. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  378. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  379. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  380. package/dist/cli/chunk-82bbrxdn.js +0 -51
  381. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  382. package/dist/cli/chunk-abh8yjkn.js +0 -31
  383. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  384. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  385. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  386. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  387. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  388. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  389. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  390. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  391. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  392. package/dist/cli/chunk-q5163e60.js.map +0 -11
  393. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  394. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  395. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  396. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  397. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  398. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-00gs3wqs.js.map} +0 -0
  399. /package/dist/cli/{chunk-6vm74dry.js.map → chunk-6dtt0zfn.js.map} +0 -0
  400. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-bbnwccaz.js.map} +0 -0
  401. /package/dist/cli/{chunk-ce574jw2.js.map → chunk-esh98wmb.js.map} +0 -0
  402. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-fsmrqk8a.js.map} +0 -0
  403. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-g698a744.js.map} +0 -0
  404. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-h2ez8dzb.js.map} +0 -0
  405. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-h7k3nq3v.js.map} +0 -0
  406. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-mqc662a6.js.map} +0 -0
  407. /package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-n1yg3tj3.js.map} +0 -0
  408. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-sqw4ekg1.js.map} +0 -0
  409. /package/dist/cli/{chunk-27g6wdth.js.map → chunk-wdrt2k2v.js.map} +0 -0
  410. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-zp79m0ts.js.map} +0 -0
@@ -3,12 +3,14 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import GithubSlugger from "github-slugger";
4
4
  import { extname } from "pathe";
5
5
 
6
- import { withBasePath } from "../base-path.ts";
6
+ import { mountBasePath } from "../base-path.ts";
7
7
  import { nextFenceState } from "../code-fences.ts";
8
8
  import type { FenceState } from "../code-fences.ts";
9
9
  import { diagnosticsFromIssues, diagnosticsFromZod } from "../diagnostics.ts";
10
10
  import { occupySlug, parseHeadingMarkers } from "../heading-markers.ts";
11
11
  import { localePlacement, localizeRoute } from "../i18n.ts";
12
+ import { titleWord } from "../navigation.ts";
13
+ import { stripOrderingPrefix } from "../ordering-prefix.ts";
12
14
  import { pageMetaSchema } from "../schema.ts";
13
15
  import type {
14
16
  FrontmatterExtend,
@@ -20,13 +22,12 @@ import type { Diagnostic, Heading, PageLink, PageRecord } from "../types.ts";
20
22
  import { detectVersionRef, versionizeRoute } from "../versions.ts";
21
23
  import type { NormalizeContext, SourceEntry } from "./types.ts";
22
24
 
23
- const NUMERIC_PREFIX = /^\d+[-_.]/u;
24
25
  const GROUP_FOLDER = /^\((?<label>.+)\)$/u;
25
26
  const WORD_SPLIT = /[-_]/u;
26
27
 
27
28
  /** Strip a leading numeric ordering prefix (`01-intro` -> `intro`). */
28
29
  const stripNumericPrefix = (segment: string): string =>
29
- segment.replace(NUMERIC_PREFIX, "");
30
+ stripOrderingPrefix(segment);
30
31
 
31
32
  /** Detect a group folder `(name)` and return its label, else null. */
32
33
  const groupLabel = (segment: string): string | null =>
@@ -63,13 +64,9 @@ export const slugify = (text: string): string =>
63
64
  export const slugifyPath = (text: string): string =>
64
65
  text.split("/").map(slugify).filter(Boolean).join("/");
65
66
 
66
- /** Title-case a slug segment for display. */
67
+ /** Title-case a slug segment for display, acronyms as the sidebar spells them. */
67
68
  const titleCase = (value: string): string =>
68
- value
69
- .split(WORD_SPLIT)
70
- .filter(Boolean)
71
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
72
- .join(" ");
69
+ value.split(WORD_SPLIT).filter(Boolean).map(titleWord).join(" ");
73
70
 
74
71
  /**
75
72
  * Strip characters that cannot survive the route → URL → output-file round
@@ -83,11 +80,15 @@ const titleCase = (value: string): string =>
83
80
  const sanitizeSegment = (segment: string): string =>
84
81
  segment.replaceAll(/[:\p{Cc}]/gu, "");
85
82
 
86
- /** Fold one raw path part into the accumulating route segments/groups. */
83
+ /**
84
+ * Fold one raw path part into the accumulating route segments/groups.
85
+ * `ordered` parts are file or folder names, whose ordering prefix is dropped.
86
+ */
87
87
  const addRouteSegment = (
88
88
  part: string,
89
89
  segments: string[],
90
- groups: string[]
90
+ groups: string[],
91
+ ordered: boolean
91
92
  ): void => {
92
93
  // A leading/trailing/double slash yields an empty part; keeping it would
93
94
  // produce a malformed route (`//foo`, `/foo/`) that nothing can link to.
@@ -99,7 +100,7 @@ const addRouteSegment = (
99
100
  groups.push(group);
100
101
  return;
101
102
  }
102
- const clean = stripNumericPrefix(part);
103
+ const clean = ordered ? stripNumericPrefix(part) : part;
103
104
  if (clean === "index") {
104
105
  return;
105
106
  }
@@ -119,22 +120,31 @@ interface MappedRoute {
119
120
  }
120
121
 
121
122
  /**
122
- * Convert a content-root-relative path into URL + nav metadata. Not exported:
123
- * a source that needs to predict a route goes through
123
+ * Convert a source's route prefix and a content-root-relative path into URL +
124
+ * nav metadata. Only the path's parts lose an ordering prefix, and only when
125
+ * `ordered` says they are file and folder names: the route prefix, a slug,
126
+ * a release tag, or a CMS slug is a route spelled out, kept as written. Not
127
+ * exported: a source that needs to predict a route goes through
124
128
  * {@link resolveEntryRoute}, so there is exactly one derivation.
125
129
  */
126
- const mapRoute = (relativePath: string): MappedRoute => {
130
+ const mapRoute = (
131
+ prefix: string | undefined,
132
+ relativePath: string,
133
+ ordered: boolean
134
+ ): MappedRoute => {
127
135
  const withoutExt = relativePath.slice(
128
136
  0,
129
137
  relativePath.length - extname(relativePath).length
130
138
  );
131
- const rawParts = withoutExt.split("/");
132
139
 
133
140
  const segments: string[] = [];
134
141
  const groups: string[] = [];
135
142
 
136
- for (const part of rawParts) {
137
- addRouteSegment(part, segments, groups);
143
+ for (const part of prefix ? prefix.split("/") : []) {
144
+ addRouteSegment(part, segments, groups, false);
145
+ }
146
+ for (const part of withoutExt.split("/")) {
147
+ addRouteSegment(part, segments, groups, ordered);
138
148
  }
139
149
 
140
150
  const route = segments.length === 0 ? "/" : `/${segments.join("/")}`;
@@ -241,6 +251,8 @@ interface HeadingScanState {
241
251
  * end so a tag wrapped over several lines still matches.
242
252
  */
243
253
  anchorLines: string[];
254
+ /** The multi-line comment the scan is inside, if any — see `scanCommentLine`. */
255
+ comment: "html" | "jsx" | null;
244
256
  curlyMarkers: CurlyMarker[];
245
257
  fence: FenceState;
246
258
  /** 1-based body line of the line being scanned. */
@@ -473,6 +485,48 @@ const scanContentLine = (
473
485
  state.paragraph.push(line.trim());
474
486
  };
475
487
 
488
+ // A comment that opens a line hides everything up to its close from the
489
+ // reader: an HTML comment (`<!--` … `-->`, a CommonMark HTML block, so up to
490
+ // 3 leading spaces) or an MDX JSX one (`{/*` … `*/}`, any indentation — MDX
491
+ // has no indented code).
492
+ const HTML_COMMENT_OPEN = /^ {0,3}<!--/u;
493
+ const JSX_COMMENT_OPEN = /^\s*\{\/\*/u;
494
+ const JSX_COMMENT_CLOSE = /\*\/\s*\}/u;
495
+
496
+ /**
497
+ * Advance the multi-line comment state over one line, returning whether the
498
+ * line belongs to a comment. A commented-out `# Old title` renders nothing, so
499
+ * it must not become a heading — the page title, a TOC entry, or an anchor.
500
+ * A comment that closes on its opening line is left to the regular scan,
501
+ * which never reads a line starting with `<` or `{` as a heading. The
502
+ * CommonMark end condition is any `-->` on the line, the opening one
503
+ * included.
504
+ */
505
+ const scanCommentLine = (line: string, state: HeadingScanState): boolean => {
506
+ if (state.comment === "html") {
507
+ if (line.includes("-->")) {
508
+ state.comment = null;
509
+ }
510
+ return true;
511
+ }
512
+ if (state.comment === "jsx") {
513
+ if (JSX_COMMENT_CLOSE.test(line)) {
514
+ state.comment = null;
515
+ }
516
+ return true;
517
+ }
518
+ if (HTML_COMMENT_OPEN.test(line) && !line.includes("-->")) {
519
+ state.comment = "html";
520
+ return true;
521
+ }
522
+ const jsx = JSX_COMMENT_OPEN.exec(line);
523
+ if (jsx && !JSX_COMMENT_CLOSE.test(line.slice(jsx[0].length))) {
524
+ state.comment = "jsx";
525
+ return true;
526
+ }
527
+ return false;
528
+ };
529
+
476
530
  /** Scan one line for a heading, advancing the fence/paragraph state. */
477
531
  const scanHeadingLine = (
478
532
  line: string,
@@ -481,6 +535,18 @@ const scanHeadingLine = (
481
535
  headings: Heading[],
482
536
  isRefDefined: (label: string) => boolean
483
537
  ): void => {
538
+ // Comments hide fences too, and fences and prompts hide comments. A comment
539
+ // line still goes to the anchor pass, which strips HTML comments itself.
540
+ if (
541
+ state.fence === null &&
542
+ state.promptDepth === 0 &&
543
+ !state.promptTag &&
544
+ scanCommentLine(line, state)
545
+ ) {
546
+ state.anchorLines.push(line.replaceAll(INLINE_CODE, ""));
547
+ state.paragraph = [];
548
+ return;
549
+ }
484
550
  const next = nextFenceState(line, state.fence);
485
551
  // Skip fence delimiter lines themselves and anything inside a fence. A fence
486
552
  // also ends any open paragraph, so no underline can reach across it.
@@ -548,6 +614,7 @@ export const scanBody = (body: string): BodyScan => {
548
614
  const slugger = new GithubSlugger();
549
615
  const state: HeadingScanState = {
550
616
  anchorLines: [],
617
+ comment: null,
551
618
  curlyMarkers: [],
552
619
  fence: null,
553
620
  line: 0,
@@ -792,6 +859,14 @@ export const strippedLineOffset = (
792
859
  ): number =>
793
860
  raw ? Math.max(0, raw.split("\n").length - body.split("\n").length) : 0;
794
861
 
862
+ /**
863
+ * How many lines of the entry's source file sit above its body: what the
864
+ * source reported (`bodyLineOffset`, when `raw` is a rewrite of the file), or
865
+ * else the height of `raw`'s stripped front matter.
866
+ */
867
+ const entryLineOffset = (entry: SourceEntry): number =>
868
+ entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
869
+
795
870
  /**
796
871
  * Map links extracted from include-expanded text back to the file and raw
797
872
  * line each expanded line came from, so a broken link inside a partial is
@@ -834,10 +909,7 @@ const entryLinks = (entry: SourceEntry): PageLink[] =>
834
909
  entry.expanded.origins,
835
910
  entry.sourcePath
836
911
  )
837
- : extractLinks(
838
- entry.body.text,
839
- strippedLineOffset(entry.raw, entry.body.text)
840
- );
912
+ : extractLinks(entry.body.text, entryLineOffset(entry));
841
913
 
842
914
  /**
843
915
  * Diagnostics for `{#id}` heading markers in an `.mdx` page. The MDX parser
@@ -860,8 +932,7 @@ const curlyMarkerDiagnostics = (
860
932
  return {
861
933
  code: "BLUME_MDX_CURLY_ANCHOR",
862
934
  file: origin?.file ?? page,
863
- line:
864
- origin?.line ?? line + strippedLineOffset(entry.raw, entry.body.text),
935
+ line: origin?.line ?? line + entryLineOffset(entry),
865
936
  message: inPartial
866
937
  ? `\`{#${id}}\` is a JSX expression once this partial is included in ${page} (.mdx), so that page fails to compile.`
867
938
  : `\`{#${id}}\` is a JSX expression in .mdx, so this page fails to compile.`,
@@ -882,8 +953,17 @@ const deriveTitle = (
882
953
  if (firstHeading) {
883
954
  return firstHeading.text;
884
955
  }
885
- const base = id.split("/").pop() ?? id;
886
- return titleCase(stripNumericPrefix(base.replace(extname(base), "")));
956
+ const parts = id.split("/");
957
+ const base = parts.at(-1) ?? id;
958
+ const stem = stripNumericPrefix(base.replace(extname(base), ""));
959
+ // An index page stands for its folder, so an untitled one takes the label
960
+ // the sidebar gives that folder (`guides/index.md` is "Guides"). The
961
+ // content root's own index has no folder to borrow from.
962
+ const folder = stem === "index" ? parts.at(-2) : undefined;
963
+ if (folder !== undefined) {
964
+ return titleCase(stripNumericPrefix(groupLabel(folder) ?? folder));
965
+ }
966
+ return titleCase(stem);
887
967
  };
888
968
 
889
969
  /** Strip habitual leading/trailing slashes (`/getting-started`, `guides/`). */
@@ -902,6 +982,13 @@ const withPrefix = (prefix: string | undefined, path: string): string => {
902
982
 
903
983
  /** What a route resolution needs from the owning source and the config. */
904
984
  export type RouteContext = Pick<NormalizeContext, "i18n" | "versions"> & {
985
+ /**
986
+ * Whether the entry's ref is a path of file and folder names whose ordering
987
+ * prefixes (`01-intro`) sort the sidebar and drop from the route: true for
988
+ * filesystem sources. A staged source's ref is a slug, a release tag, or a
989
+ * note name, and keeps its leading numbers.
990
+ */
991
+ orderingPrefixes?: boolean;
905
992
  /** The source's route prefix (`NormalizeContext["source"]["prefix"]`). */
906
993
  prefix?: string;
907
994
  };
@@ -970,7 +1057,9 @@ export interface EntryRoute extends Pick<
970
1057
  * A frontmatter `slug` wins, then the adapter-supplied `entry.slug` (the typed
971
1058
  * SPI's "logical route input; defaults to ref if omitted"), then the ref. The
972
1059
  * extension is re-appended so `mapRoute`'s extname strip can't eat a dotted
973
- * slug segment (`v1.2`). A slug that trims to nothing falls back. The version
1060
+ * slug segment (`v1.2`). A slug that trims to nothing falls back. Only a
1061
+ * filesystem ref (`ctx.orderingPrefixes`) loses its ordering prefixes; a slug
1062
+ * is a route spelled out, so `2024-year-in-review` stays whole. The version
974
1063
  * prefixes the mapped route *after* `mapRoute` runs: the mapped route is the
975
1064
  * version-agnostic key, the config id is prepended verbatim (never
976
1065
  * numeric-prefix-stripped), a frontmatter `slug` gets versionized so snapshots
@@ -987,8 +1076,18 @@ export const resolveEntryRoute = (
987
1076
  const { locales, navPath, version } = placeEntryRef(entry.ref, ext, ctx);
988
1077
  const slugInput = frontmatterSlug ?? entry.slug;
989
1078
  const slug = slugInput ? trimSlashes(slugInput) : "";
990
- const routeInput = withPrefix(ctx.prefix, slug ? `${slug}${ext}` : navPath);
991
- const { segments, groups, route: versionKey } = mapRoute(routeInput);
1079
+ // A frontmatter slug is a route spelled out; an adapter's `entry.slug` is
1080
+ // one too unless the source's names are ordered file names (a vault note's
1081
+ // path), which lose their prefixes like the ref does.
1082
+ const ordered =
1083
+ ctx.orderingPrefixes === true && frontmatterSlug === undefined;
1084
+ const {
1085
+ segments,
1086
+ groups,
1087
+ route: versionKey,
1088
+ } = slug
1089
+ ? mapRoute(ctx.prefix, `${slug}${ext}`, ordered)
1090
+ : mapRoute(ctx.prefix, navPath, ctx.orderingPrefixes === true);
992
1091
  return {
993
1092
  groups,
994
1093
  locales,
@@ -1192,6 +1291,7 @@ export const normalizeEntry = (
1192
1291
  versionKey,
1193
1292
  } = resolveEntryRoute(entry, ext, meta.slug, {
1194
1293
  i18n: ctx.i18n,
1294
+ orderingPrefixes: !ctx.source.staged || ctx.source.orderedNames === true,
1195
1295
  prefix: ctx.source.prefix,
1196
1296
  versions: ctx.versions,
1197
1297
  });
@@ -1237,10 +1337,12 @@ export const normalizeEntry = (
1237
1337
  // `basePath` is applied outermost — after locale prefixing — so the route
1238
1338
  // reads `{basePath}/{locale?}/{prefix?}/…`; `navPath` and `translationKey`
1239
1339
  // stay base-less so the nav tree and translation matching are unaffected.
1340
+ // The base is mounted unconditionally: a `docs/` folder under a `/docs`
1341
+ // base is a real `/docs/docs/…` route, not an already-based one.
1240
1342
  const pages = locales.map((locale) => ({
1241
1343
  ...base,
1242
1344
  locale,
1243
- route: withBasePath(
1345
+ route: mountBasePath(
1244
1346
  ctx.basePath ?? "",
1245
1347
  localizedRoute(logicalRoute, locale, ctx.i18n)
1246
1348
  ),
@@ -15,7 +15,13 @@ import {
15
15
  pollingWatch,
16
16
  snapshotCache,
17
17
  } from "./cache.ts";
18
- import { codeFence, image, renderInline, renderLink } from "./lower.ts";
18
+ import {
19
+ codeFence,
20
+ guardBlockStart,
21
+ image,
22
+ renderInline,
23
+ renderLink,
24
+ } from "./lower.ts";
19
25
  import { slugifyPath } from "./normalize.ts";
20
26
  import type {
21
27
  ContentSource,
@@ -217,12 +223,12 @@ const withNotionRetry = async <T>(
217
223
  if (status !== RATE_LIMITED || attempt === MAX_RETRIES) {
218
224
  throw error;
219
225
  }
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
- );
226
+ // SAFETY: same APIResponseError shape — `headers` is the failed
227
+ // response's fetch `Headers`, read with `get()`; a missing header (or
228
+ // headers without `get`) yields no positive wait and falls back to
229
+ // exponential backoff.
230
+ const { headers } = error as { headers?: Headers };
231
+ const retryAfter = Number(headers?.get?.("retry-after"));
226
232
  const wait =
227
233
  retryAfter > 0
228
234
  ? retryAfter * SECOND_MS
@@ -266,6 +272,12 @@ const richToPlain = (rich: NotionRichText[] = []): string =>
266
272
  */
267
273
  const jsxString = (value: string): string => `{${JSON.stringify(value)}}`;
268
274
 
275
+ // `<Frame>` renders its caption as Markdown after escaping `<` and `>` itself
276
+ // (so a caption can't inject HTML), which turns the lowering's own `\<` into
277
+ // a visible `&lt;`. Its caption drops those backslashes and leaves the
278
+ // escaping to Frame; every other escape still applies.
279
+ const FRAME_ESCAPED = /\\(?=[<>])/gu;
280
+
269
281
  const YOUTUBE_HOST = /(?:^|\.)(?:youtube(?:-nocookie)?\.com|youtu\.be)$/u;
270
282
 
271
283
  // `parseYouTubeId` is host-agnostic on purpose (the component accepts bare
@@ -300,14 +312,14 @@ const renderVideo = (data: NotionBlockPayload): string => {
300
312
  ? `<YouTube${title} url=${JSON.stringify(url)} />`
301
313
  : `<video controls src=${JSON.stringify(url)} />`;
302
314
  return caption
303
- ? `<Frame caption=${jsxString(caption)}>\n${media}\n</Frame>`
315
+ ? `<Frame caption=${jsxString(caption.replaceAll(FRAME_ESCAPED, ""))}>\n${media}\n</Frame>`
304
316
  : media;
305
317
  };
306
318
 
307
319
  /** Render a leaf (non-container) block to Markdown, or null for containers. */
308
320
  const renderLeaf = (block: NotionBlock): string | null => {
309
321
  const data = payloadOf(block) ?? {};
310
- const text = richToMarkdown(blockField(block));
322
+ const text = guardBlockStart(richToMarkdown(blockField(block)));
311
323
  switch (block.type) {
312
324
  case "paragraph": {
313
325
  return text;
@@ -440,7 +452,10 @@ export const notionSource = (
440
452
  };
441
453
 
442
454
  if (block.type === "callout") {
443
- const body = [richToMarkdown(blockField(block)), await children(block)]
455
+ const body = [
456
+ guardBlockStart(richToMarkdown(blockField(block))),
457
+ await children(block),
458
+ ]
444
459
  .filter(Boolean)
445
460
  .join("\n\n");
446
461
  return `<Callout>\n${body}\n</Callout>`;
@@ -680,5 +695,6 @@ export const notionSource = (
680
695
  () => load()
681
696
  )
682
697
  : undefined,
698
+ withContext: (next) => notionSource(options, next),
683
699
  };
684
700
  };
@@ -20,6 +20,7 @@ import {
20
20
  placeEntryRef,
21
21
  resolveEntryRoute,
22
22
  slugifyPath,
23
+ strippedLineOffset,
23
24
  } from "./normalize.ts";
24
25
  import type {
25
26
  ContentSource,
@@ -84,6 +85,8 @@ interface ParsedNote {
84
85
  /** The note body, frontmatter stripped and otherwise untouched. */
85
86
  content: string;
86
87
  data: SourceEntry["data"];
88
+ /** How many lines of the file the frontmatter block (if any) spans. */
89
+ frontmatterLines: number;
87
90
  placement: EntryPlacement;
88
91
  /** Vault-relative path, e.g. `guides/Getting Started.md`. */
89
92
  rel: string;
@@ -638,7 +641,7 @@ const buildLinkIndex = (
638
641
  { ref: note.rel, slug: entrySlugFor(note.placement.navPath) },
639
642
  ".md",
640
643
  isStringValue(note.data.slug) ? note.data.slug : undefined,
641
- options
644
+ { ...options, orderingPrefixes: true }
642
645
  );
643
646
  const built: IndexedNote = {
644
647
  anchors: new Map(),
@@ -756,10 +759,17 @@ const noteToEntry = (
756
759
  )
757
760
  );
758
761
  const merged = title === undefined ? data : { ...data, title };
759
- const text = transformBody(note.content.trim(), index, pair.self, unresolved);
762
+ const body = note.content.trim();
763
+ const text = transformBody(body, index, pair.self, unresolved);
760
764
  const raw = matter.stringify(`${text}\n`, merged);
765
+ // `raw` re-serializes the frontmatter without the dropped properties, so
766
+ // its height is not the file's: count the file's own frontmatter and the
767
+ // blank lines the trim dropped, so a link diagnostic names the line the
768
+ // author sees in the note.
769
+ const leading = note.content.slice(0, note.content.indexOf(body));
761
770
  return {
762
771
  body: { format: "md", text },
772
+ bodyLineOffset: note.frontmatterLines + leading.split("\n").length - 1,
763
773
  data: merged,
764
774
  hash: hashText(raw),
765
775
  raw,
@@ -782,11 +792,13 @@ const readNote = async (
782
792
  ): Promise<ParsedNote | null> => {
783
793
  const absPath = join(vaultDir, rel);
784
794
  try {
785
- const { content, data } = matter(await readFile(absPath, "utf-8"));
795
+ const file = await readFile(absPath, "utf-8");
796
+ const { content, data } = matter(file);
786
797
  return {
787
798
  absPath,
788
799
  content,
789
800
  data,
801
+ frontmatterLines: strippedLineOffset(file, content),
790
802
  // Version outermost, then locale, the way `normalizeEntry` reads the
791
803
  // ref — the slug is built from what remains, so the pipeline's own
792
804
  // re-prefixing does not stack a second `fr/` or `v1.0/` onto the route.
@@ -1030,6 +1042,8 @@ export const obsidianSource = (
1030
1042
  contentRoot: vaultDir,
1031
1043
  load,
1032
1044
  name: options.name,
1045
+ // Notes are named like files: `01 Intro` sorts first and routes `/intro`.
1046
+ orderedNames: true,
1033
1047
  prefix: options.prefix,
1034
1048
  read,
1035
1049
  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,14 @@
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 {
8
+ guardBlockStart,
9
+ image,
10
+ renderInline,
11
+ renderLink,
12
+ unsupported,
13
+ writesMdx,
14
+ } from "./lower.ts";
8
15
 
9
16
  /**
10
17
  * A field value on a Portable Text node: the arbitrary JSON the CMS query
@@ -66,47 +73,29 @@ const HEADING_STYLES = new Map([
66
73
  ["h6", "###### "],
67
74
  ]);
68
75
 
69
- /** Wrap a span's text in Markdown for its marks (decorators + link defs). */
76
+ /**
77
+ * A span as Markdown for its marks, through the shared lowering every CMS
78
+ * uses: prose is escaped, a code span outruns the backticks inside it, edge
79
+ * whitespace stays outside emphasis, and a link def wraps last (so its label
80
+ * keeps emphasis) only when its destination is safe to click.
81
+ */
70
82
  const renderSpan = (
71
83
  span: PortableTextSpan,
72
84
  defs: Map<string, PortableTextMarkDef>
73
85
  ): 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;
86
+ const marks = span.marks ?? [];
87
+ const link = marks
88
+ .map((mark) => defs.get(mark))
89
+ .findLast((def) => def?._type === "link");
90
+ return renderLink(
91
+ renderInline(span.text ?? "", {
92
+ bold: marks.includes("strong"),
93
+ code: marks.includes("code"),
94
+ italic: marks.includes("em"),
95
+ strike: marks.includes("strike-through"),
96
+ }),
97
+ link?.href
98
+ );
110
99
  };
111
100
 
112
101
  /** Render the inline children of a block to a single Markdown string. */
@@ -114,16 +103,45 @@ const renderChildren = (block: PortableTextBlock): string => {
114
103
  const defs = new Map(
115
104
  (block.markDefs ?? []).map((def) => [def._key, def] as const)
116
105
  );
117
- return (block.children ?? []).map((span) => renderSpan(span, defs)).join("");
106
+ return guardBlockStart(
107
+ (block.children ?? []).map((span) => renderSpan(span, defs)).join("")
108
+ );
118
109
  };
119
110
 
120
111
  /** Whether an image block's `alt` field is usable alt text (CMS JSON may hold anything). */
121
112
  const isAltText = (value: PortableTextValue): value is string =>
122
113
  typeof value === "string";
123
114
 
115
+ /** The marker a list block opens with. */
116
+ const listMarker = (block: PortableTextBlock): string =>
117
+ block.listItem === "number" ? "1." : "-";
118
+
119
+ /**
120
+ * How far each block is indented. Portable Text lists are flat blocks with a
121
+ * `level`, while Markdown nests an item by indenting it to its parent item's
122
+ * content column: two past a `-` item's indent, three past a `1.` item's. The
123
+ * column comes from the item actually above, so a skipped level nests under
124
+ * the deepest open item instead of indenting into a code block.
125
+ */
126
+ const listIndents = (blocks: PortableTextBlock[]): number[] => {
127
+ const columns: number[] = [];
128
+ return blocks.map((block) => {
129
+ if (!block.listItem) {
130
+ columns.length = 0;
131
+ return 0;
132
+ }
133
+ const depth = Math.min(Math.max(0, (block.level ?? 1) - 1), columns.length);
134
+ const indent = columns[depth - 1] ?? 0;
135
+ columns.length = depth;
136
+ columns.push(indent + listMarker(block).length + 1);
137
+ return indent;
138
+ });
139
+ };
140
+
124
141
  const renderBlock = (
125
142
  block: PortableTextBlock,
126
- options: PortableTextOptions
143
+ options: PortableTextOptions,
144
+ indent: number
127
145
  ): string => {
128
146
  const custom = options.serializers?.[block._type];
129
147
  if (custom) {
@@ -143,9 +161,7 @@ const renderBlock = (
143
161
 
144
162
  const inline = renderChildren(block);
145
163
  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}`;
164
+ return `${" ".repeat(indent)}${listMarker(block)} ${inline}`;
149
165
  }
150
166
  if (block.style === "blockquote") {
151
167
  return `> ${inline}`;
@@ -158,7 +174,10 @@ export const portableTextToMarkdown = (
158
174
  blocks: PortableTextBlock[],
159
175
  options: PortableTextOptions = {}
160
176
  ): string => {
161
- const lines = blocks.map((block) => renderBlock(block, options));
177
+ const indents = listIndents(blocks);
178
+ const lines = blocks.map((block, i) =>
179
+ renderBlock(block, options, indents[i] ?? 0)
180
+ );
162
181
  // List items are single-newline separated; everything else gets a blank line.
163
182
  const out: string[] = [];
164
183
  for (let i = 0; i < lines.length; i += 1) {