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
@@ -16,13 +16,19 @@ import { applyAudienceVisibility } from "../ai/visibility.ts";
16
16
  import type { VisibilityAudience } from "../ai/visibility.ts";
17
17
  import matter from "../core/frontmatter.ts";
18
18
  import { parseHeadingMarkers } from "../core/heading-markers.ts";
19
+ import { EN_UI, resolveUIStrings } from "../core/i18n-ui.ts";
19
20
  import { contentIndexable } from "../core/manifest.ts";
20
21
  import type { BlumeProject } from "../core/project-graph.ts";
21
22
  import { HTML_COMMENT } from "../core/sources/normalize.ts";
22
23
  import { readExpandedEntryText } from "../core/sources/read.ts";
23
24
  import type { NavNode, PageRecord } from "../core/types.ts";
24
25
  import { parseCodeTitle } from "../markdown/code-title.ts";
25
- import { MARKDOWN_FEATURES, MDX_FEATURES } from "../markdown/features.ts";
26
+ import { directiveSource } from "../markdown/directives.ts";
27
+ import type { DirectiveNode } from "../markdown/directives.ts";
28
+ import {
29
+ MARKDOWN_BODY_FEATURES,
30
+ MDX_BODY_FEATURES,
31
+ } from "../markdown/features.ts";
26
32
  import { pageFacets } from "./facets.ts";
27
33
 
28
34
  /** A document indexed by the client-side search providers (Orama, FlexSearch). */
@@ -93,7 +99,6 @@ const INLINE_PARENTS = new Set([
93
99
  "strong",
94
100
  "subscript",
95
101
  "superscript",
96
- "textDirective",
97
102
  ]);
98
103
 
99
104
  // Nodes that never render as prose: image alt text was never indexed, MDX
@@ -119,9 +124,9 @@ interface Walk {
119
124
 
120
125
  // Front matter is already off (core/frontmatter.ts) by the time a body gets
121
126
  // here, so a body that opens with a `---` divider must read as a thematic
122
- // break rather than a second front matter block.
123
- const MD_PARSE = { features: { ...MARKDOWN_FEATURES, frontmatter: false } };
124
- const MDX_PARSE = { features: { ...MDX_FEATURES, frontmatter: false } };
127
+ // break rather than a second front matter block — as the renderer reads it.
128
+ const MD_PARSE = { features: MARKDOWN_BODY_FEATURES };
129
+ const MDX_PARSE = { features: MDX_BODY_FEATURES };
125
130
 
126
131
  const parseMdx = (markdown: string): Nodes | undefined => {
127
132
  try {
@@ -169,6 +174,25 @@ const collectCode = (
169
174
  out.push(node.value, " ");
170
175
  };
171
176
 
177
+ /**
178
+ * A text or leaf directive as the page shows it. Blume renders neither (see
179
+ * `markdown/directives.ts`), and prose is full of text that parses as one —
180
+ * `16:9`, `og:image` — so the page shows the source the author wrote, and the
181
+ * index reads the same text rather than dropping the `:name` part.
182
+ */
183
+ const directiveText = (
184
+ node: Extract<Nodes, { type: "leafDirective" | "textDirective" }>,
185
+ walk: Walk
186
+ ): string =>
187
+ directiveSource(
188
+ // SAFETY: DirectiveNode is a structural subset of Satteri's directive
189
+ // nodes — a `name`, optional attributes and children, and a position.
190
+ node as DirectiveNode,
191
+ node.type === "textDirective" ? ":" : "::",
192
+ // The walked tree was parsed from this text, so its offsets index into it.
193
+ walk.downlevel.source
194
+ );
195
+
172
196
  // Block boundaries separate words; so does an empty inline element (`<br />`,
173
197
  // a self-closing icon), which otherwise fuses its neighbors.
174
198
  const separatesWords = (node: Extract<Nodes, { children: unknown }>): boolean =>
@@ -222,28 +246,17 @@ const collectText = (node: Nodes, out: string[], walk: Walk): void => {
222
246
  out.push(" ");
223
247
  return;
224
248
  }
225
- // Trailing heading markers (`[#custom-id]`, `{#custom-id}`, `[!toc]`,
226
- // `[toc]`) are anchor
227
-
228
- // metadata, not prose — strip them so they never pollute the index. Only
229
- // a marker that ends the heading's final plain-text child counts,
230
- // mirroring the renderer: a heading ending in inline code or an image
231
- // keeps its bracketed text on the page (so it stays searchable), and a
232
- // heading that is nothing but markers renders them literally.
249
+ case "textDirective": {
250
+ out.push(directiveText(node, walk));
251
+ return;
252
+ }
253
+ case "leafDirective": {
254
+ out.push(directiveText(node, walk), " ");
255
+ return;
256
+ }
233
257
  case "heading": {
234
- const last = node.children.at(-1);
235
- const trailing = last?.type === "text" ? last : undefined;
236
- const inner: string[] = [];
237
- const kept = trailing ? node.children.slice(0, -1) : node.children;
238
- for (const child of kept) {
239
- collectText(child, inner, walk);
240
- }
241
- if (trailing) {
242
- const stripped = parseHeadingMarkers(trailing.value).text;
243
- const literal = stripped === "" && node.children.length === 1;
244
- inner.push(literal ? trailing.value : stripped);
245
- }
246
- out.push(inner.join("").trimEnd(), " ");
258
+ // oxlint-disable-next-line no-use-before-define -- mutual recursion: a heading's children are walked like any others
259
+ collectHeading(node, out, walk);
247
260
  return;
248
261
  }
249
262
  default: {
@@ -264,6 +277,35 @@ const collectText = (node: Nodes, out: string[], walk: Walk): void => {
264
277
  }
265
278
  };
266
279
 
280
+ /**
281
+ * Fold a heading into the accumulator. Trailing heading markers
282
+ * (`[#custom-id]`, `{#custom-id}`, `[!toc]`, `[toc]`) are anchor metadata,
283
+ * not prose — strip them so they never pollute the index. Only a marker that
284
+ * ends the heading's final plain-text child counts, mirroring the renderer: a
285
+ * heading ending in inline code or an image keeps its bracketed text on the
286
+ * page (so it stays searchable), and a heading that is nothing but markers
287
+ * renders them literally.
288
+ */
289
+ const collectHeading = (
290
+ node: Extract<Nodes, { type: "heading" }>,
291
+ out: string[],
292
+ walk: Walk
293
+ ): void => {
294
+ const last = node.children.at(-1);
295
+ const trailing = last?.type === "text" ? last : undefined;
296
+ const inner: string[] = [];
297
+ const kept = trailing ? node.children.slice(0, -1) : node.children;
298
+ for (const child of kept) {
299
+ collectText(child, inner, walk);
300
+ }
301
+ if (trailing) {
302
+ const stripped = parseHeadingMarkers(trailing.value).text;
303
+ const literal = stripped === "" && node.children.length === 1;
304
+ inner.push(literal ? trailing.value : stripped);
305
+ }
306
+ out.push(inner.join("").trimEnd(), " ");
307
+ };
308
+
267
309
  /**
268
310
  * Reduce Markdown/MDX to plain, searchable text: parse (GFM included) and walk
269
311
  * the tree instead of regex-stripping the source, so reference-style links,
@@ -447,6 +489,26 @@ export const buildSearchDocuments = async (
447
489
  // otherwise.
448
490
  const components = projectComponentSerializers(project);
449
491
 
492
+ // A page in no sidebar group falls under the "Docs" section, in its own
493
+ // language: the label is a UI string, resolved once per locale.
494
+ const { i18n } = project.config;
495
+ const docsSections = new Map<string, string>();
496
+ const docsSection = (locale: string): string => {
497
+ let label = docsSections.get(locale);
498
+ if (label === undefined) {
499
+ label = (
500
+ i18n
501
+ ? resolveUIStrings(locale, {
502
+ defaultLocale: i18n.defaultLocale,
503
+ overrides: i18n.ui,
504
+ })
505
+ : EN_UI
506
+ ).search.docs;
507
+ docsSections.set(locale, label);
508
+ }
509
+ return label;
510
+ };
511
+
450
512
  return await Promise.all(
451
513
  indexable.map(async (route) => {
452
514
  const page = pageById.get(route.id);
@@ -461,7 +523,7 @@ export const buildSearchDocuments = async (
461
523
  description: page?.description ?? "",
462
524
  locale: route.locale,
463
525
  route: route.path,
464
- section: crumb?.section || "Docs",
526
+ section: crumb?.section || docsSection(route.locale),
465
527
  tags: tags && tags.length > 0 ? tags : undefined,
466
528
  title: route.title,
467
529
  version: route.version,
@@ -17,7 +17,8 @@ export interface SearchPopularPage {
17
17
  * `page.route`). `withBasePath` is idempotent and skips external URLs.
18
18
  *
19
19
  * `deployment.base` is deliberately *not* applied: routes stay deploy-base-less
20
- * so `createLinkRow` can prefix it at click time, same as sidebar-derived pages.
20
+ * so `Search.astro` can add it when it renders the entry, same as
21
+ * sidebar-derived pages.
21
22
  *
22
23
  * `icon` stays a name here (like `navigation.featured`): the icon set is a
23
24
  * server-only module, so `Search.astro` resolves it to inline SVG at render.
@@ -7,6 +7,19 @@ import type { SearchRecord } from "../documents.ts";
7
7
  /** The adapter options the sync reads (the public `apiKey` is never used). */
8
8
  export type AlgoliaSyncConfig = Pick<AlgoliaOptions, "appId" | "indexName">;
9
9
 
10
+ /**
11
+ * The record attributes the search dialog filters on (`facetFilters` on
12
+ * `locale:<code>` and `version:<id>`). In Algolia a facet filter on an
13
+ * attribute the index doesn't declare for faceting matches nothing — an i18n
14
+ * or versioned site would get no hits — so the sync declares them,
15
+ * filter-only.
16
+ */
17
+ const FILTER_ATTRIBUTES = ["locale", "version"] as const;
18
+
19
+ /** A faceting declaration's attribute: `filterOnly(locale)` → `locale`. */
20
+ const facetAttribute = (declaration: string): string =>
21
+ declaration.replace(/^\w+\((?<name>.*)\)$/u, "$<name>");
22
+
10
23
  /**
11
24
  * Upload the search records to Algolia. Uses the admin key from
12
25
  * `ALGOLIA_ADMIN_API_KEY` (never the adapter options, which hold only the
@@ -14,7 +27,9 @@ export type AlgoliaSyncConfig = Pick<AlgoliaOptions, "appId" | "indexName">;
14
27
  *
15
28
  * Uses `replaceAllObjects`, which atomically replaces the index contents, so
16
29
  * pages deleted or renamed since the last sync don't linger as stale search
17
- * hits that 404 when clicked.
30
+ * hits that 404 when clicked. Then adds `filterOnly(locale)` and
31
+ * `filterOnly(version)` to the index's `attributesForFaceting`, keeping any
32
+ * the site declared itself, so the dialog's locale and version filters match.
18
33
  */
19
34
  export const syncAlgolia = async (
20
35
  records: SearchRecord[],
@@ -28,8 +43,27 @@ export const syncAlgolia = async (
28
43
  // `core/node-require.ts`).
29
44
  const { algoliasearch }: typeof AlgoliaSdk = nodeRequire("algoliasearch");
30
45
  const client = algoliasearch(config.appId, adminKey);
46
+ const { indexName } = config;
31
47
  await client.replaceAllObjects({
32
- indexName: config.indexName,
48
+ indexName,
33
49
  objects: records.map((record) => ({ ...record, objectID: record._id })),
34
50
  });
51
+ const { attributesForFaceting = [] } = await client.getSettings({
52
+ indexName,
53
+ });
54
+ const declared = new Set(attributesForFaceting.map(facetAttribute));
55
+ const missing = FILTER_ATTRIBUTES.filter((name) => !declared.has(name));
56
+ if (missing.length === 0) {
57
+ return;
58
+ }
59
+ const { taskID } = await client.setSettings({
60
+ indexName,
61
+ indexSettings: {
62
+ attributesForFaceting: [
63
+ ...attributesForFaceting,
64
+ ...missing.map((name) => `filterOnly(${name})`),
65
+ ],
66
+ },
67
+ });
68
+ await client.waitForTask({ indexName, taskID });
35
69
  };
package/src/seo/jsonld.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Crumb } from "../components/layout/nav-utils.ts";
2
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
2
+ import {
3
+ mountBasePath,
4
+ normalizeBasePath,
5
+ withBasePath,
6
+ } from "../core/base-path.ts";
3
7
 
4
8
  /** A date-ish value carried through frontmatter (string, YAML Date, or unset). */
5
9
  type DateInput = string | Date | null;
@@ -301,7 +305,7 @@ const breadcrumbNode = (
301
305
  "@type": "BreadcrumbList",
302
306
  itemListElement: linked.map((crumb, index) => ({
303
307
  "@type": "ListItem",
304
- item: absolute(base, withBasePath(deployBase, crumb.route)),
308
+ item: absolute(base, mountBasePath(deployBase, crumb.route)),
305
309
  name: crumb.label,
306
310
  position: index + 1,
307
311
  })),
@@ -323,7 +327,7 @@ export const buildStructuredData = (
323
327
  // Routes carry `basePath`; a `deployment.base` subdirectory is layered on top
324
328
  // so JSON-LD URLs match the served location.
325
329
  const deployBase = normalizeBasePath(input.base);
326
- const pageUrl = absolute(base, withBasePath(deployBase, input.route));
330
+ const pageUrl = absolute(base, mountBasePath(deployBase, input.route));
327
331
  const rootUrl = absolute(base, deployBase);
328
332
  const graph: JsonLdNode[] = [];
329
333
 
@@ -165,6 +165,11 @@ const sourceEntrySchema = z
165
165
  .custom<SourceAdapterInput>(
166
166
  (value) => isObjectLike(value) && "kind" in value,
167
167
  {
168
+ // `z.custom` marks its failure as aborting by default, which makes Zod
169
+ // skip even the `content` refinement that opts in to running after a
170
+ // failed field (the shorthand beside `sources`). The pipe below stops
171
+ // at this entry either way.
172
+ abort: false,
168
173
  error: (issue) => {
169
174
  const legacy = legacyEntrySchema.safeParse(issue.input);
170
175
  return entryHint(legacy.success ? legacy.data.type : undefined);
@@ -268,7 +268,7 @@ ${THEME_MAPPING}
268
268
  @apply block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground;
269
269
  }
270
270
  @utility blume-nav-drill {
271
- @apply flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted;
271
+ @apply flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-start font-medium text-foreground text-sm transition-colors hover:bg-muted;
272
272
  }
273
273
  @utility blume-nav-summary {
274
274
  @apply flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden;
@@ -884,11 +884,18 @@ pre:has(.line.focused):hover .line:not(.focused) {
884
884
  }
885
885
 
886
886
  /* Print / "Save as PDF" (the page-actions Export → PDF runs window.print()).
887
- Strip the surrounding chrome so the printout is just the article. */
887
+ Strip the surrounding chrome so the printout is just the article. Scoped to
888
+ the chrome itself, never bare \`header\`/\`aside\`: content renders those too
889
+ (every Callout and Panel is an <aside>, every changelog Update has a
890
+ <header>), and they belong in the printout. \`body > header\` keeps a
891
+ \`layout.Header\` override out as well. */
888
892
  @media print {
889
893
  [data-blume-banner],
890
- header,
891
- aside,
894
+ [data-blume-header],
895
+ body > header,
896
+ [data-blume-nav-drawer],
897
+ [data-blume-toc],
898
+ [data-blume-assistant-panel],
892
899
  [data-blume-page-actions],
893
900
  #blume-content > nav,
894
901
  #blume-content > details {
@@ -1,5 +1,5 @@
1
1
  import type { AgentKind } from "../audit/agent.ts";
2
- import { DISALLOWED_TOOLS } from "../eval/agents.ts";
2
+ import { CODEX_LOCKDOWN, DISALLOWED_TOOLS } from "../eval/agents.ts";
3
3
 
4
4
  /**
5
5
  * argv builders for the translator role. The subprocess machinery
@@ -28,6 +28,10 @@ const claudeArgs = (): string[] => [
28
28
  "1",
29
29
  ];
30
30
 
31
+ // `--sandbox read-only` alone still lets codex's shell read any file the user
32
+ // can, and the source page being translated is untrusted input (a remote
33
+ // contributor's prose), so the translator gets the same lockdown as an eval
34
+ // run: no shell, exec, or local-image tool, and no inherited environment.
31
35
  const codexArgs = (lastMessagePath: string): string[] => [
32
36
  "exec",
33
37
  "--skip-git-repo-check",
@@ -35,6 +39,7 @@ const codexArgs = (lastMessagePath: string): string[] => [
35
39
  "--ephemeral",
36
40
  "--sandbox",
37
41
  "read-only",
42
+ ...CODEX_LOCKDOWN,
38
43
  "--output-last-message",
39
44
  lastMessagePath,
40
45
  "-",
@@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
4
4
  import { join } from "pathe";
5
5
  import { z } from "zod";
6
6
 
7
+ import { BlumeError } from "../core/diagnostics.ts";
7
8
  import { writeTextAtomic } from "../core/fs-atomic.ts";
8
9
 
9
10
  /**
@@ -39,21 +40,34 @@ export const emptyLedger = (): TranslationLedger => ({
39
40
  * every locale's stamp. sha256-16 like the audit snapshot's content hash.
40
41
  * Kept as its own function rather than sharing core's `hashText`: ledger
41
42
  * stamps persist in user repos, so this hash must never change shape when an
42
- * ephemeral cache hash does.
43
+ * ephemeral cache hash does. CRLF line endings are normalized to LF first, so
44
+ * a Windows checkout (`core.autocrlf`) hashes the same as everyone else's —
45
+ * LF text hashes exactly as it always has.
43
46
  */
44
47
  export const hashSource = (text: string): string =>
45
- createHash("sha256").update(text).digest("hex").slice(0, 16);
48
+ createHash("sha256")
49
+ .update(text.replaceAll("\r\n", "\n"))
50
+ .digest("hex")
51
+ .slice(0, 16);
52
+
53
+ /** A git merge-conflict marker line: `<<<<<<< ours`, `=======`, `>>>>>>> theirs`. */
54
+ const CONFLICT_MARKER = /^(?:<{7}|={7}|>{7})(?:[ \r]|$)/mu;
46
55
 
47
56
  /**
48
57
  * Read the ledger at `root`, tolerantly: a missing file, unparseable JSON, or
49
58
  * an unknown shape/version all resolve to an empty ledger rather than an error
50
59
  * (same posture as the dev lock's `parseLock`) — the worst outcome of a
51
60
  * corrupt ledger is retranslating files that were already up to date.
61
+ *
62
+ * Unresolved merge-conflict markers are the exception, and an error: read as
63
+ * empty, every existing translation would be adopted as current, so
64
+ * `--check` would pass over translations that are out of date.
52
65
  */
53
66
  export const readLedger = async (root: string): Promise<TranslationLedger> => {
67
+ const path = join(root, LEDGER_FILE);
54
68
  let raw: string;
55
69
  try {
56
- raw = await readFile(join(root, LEDGER_FILE), "utf-8");
70
+ raw = await readFile(path, "utf-8");
57
71
  } catch {
58
72
  return emptyLedger();
59
73
  }
@@ -61,6 +75,15 @@ export const readLedger = async (root: string): Promise<TranslationLedger> => {
61
75
  try {
62
76
  data = JSON.parse(raw);
63
77
  } catch {
78
+ if (CONFLICT_MARKER.test(raw)) {
79
+ throw new BlumeError({
80
+ code: "BLUME_TRANSLATE_LEDGER_CONFLICT",
81
+ file: path,
82
+ message: `${LEDGER_FILE} has unresolved merge-conflict markers, so it can't say which translations are current.`,
83
+ severity: "error",
84
+ suggestion: `Resolve the merge conflict in ${LEDGER_FILE}, then rerun.`,
85
+ });
86
+ }
64
87
  return emptyLedger();
65
88
  }
66
89
  const parsed = ledgerSchema.safeParse(data);
@@ -1,12 +1,14 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { readFile } from "node:fs/promises";
2
3
 
3
4
  import { dirname, join, relative } from "pathe";
4
- import { glob } from "tinyglobby";
5
5
 
6
+ import { localeTargetPath } from "../core/i18n.ts";
6
7
  import { createModuleLoader } from "../core/load-module.ts";
8
+ import { findFolderMetaFiles } from "../core/meta.ts";
7
9
  import type { BlumeProject } from "../core/project-graph.ts";
8
10
  import { folderMetaSchema } from "../core/schema.ts";
9
- import type { FolderMeta } from "../core/schema.ts";
11
+ import type { FolderMeta, ResolvedI18nConfig } from "../core/schema.ts";
10
12
  import type { Diagnostic } from "../core/types.ts";
11
13
 
12
14
  /**
@@ -37,7 +39,10 @@ export interface TranslatableMeta {
37
39
  title: string;
38
40
  }
39
41
 
40
- const META_FILES = ["**/meta.ts", "**/meta.js", "**/meta.mjs"];
42
+ /** A folder's meta module names, in the order a lookup prefers them. */
43
+ const META_NAMES = ["meta.ts", "meta.js", "meta.mjs"];
44
+
45
+ const META_FILES = META_NAMES.map((name) => `**/${name}`);
41
46
 
42
47
  /**
43
48
  * Whether a loaded meta module default-exports a factory function. Generic so
@@ -48,19 +53,44 @@ const isFactoryModule = <T>(
48
53
  ): value is T & ((...args: never[]) => FolderMeta) =>
49
54
  typeof value === "function";
50
55
 
51
- /** Where a locale's generated meta module lives (always written as `meta.ts`). */
56
+ /**
57
+ * Where a locale's meta module lives. The locale folder is resolved the way
58
+ * pages resolve it (`localeTargetPath`): `folders` are the content root's
59
+ * existing top-level folders, and one naming the locale in another casing
60
+ * (`pt-br/` for a configured `pt-BR`) is the locale's folder already. In that
61
+ * folder, a `meta.js`/`meta.mjs` that already exists is the target — the
62
+ * generated module is plain JavaScript, and writing a `meta.ts` beside it
63
+ * would leave the folder with two meta files. Otherwise it's a new `meta.ts`.
64
+ */
52
65
  export const metaTargetPath = (
53
66
  meta: TranslatableMeta,
54
- locale: string
55
- ): string => join(meta.contentRoot, locale, meta.dir, "meta.ts");
67
+ locale: string,
68
+ i18n: ResolvedI18nConfig,
69
+ folders: readonly string[]
70
+ ): string => {
71
+ const dir = join(
72
+ meta.contentRoot,
73
+ dirname(
74
+ localeTargetPath(join(meta.dir, "meta.ts"), ".ts", locale, i18n, folders)
75
+ )
76
+ );
77
+ const existing = META_NAMES.find((name) => existsSync(join(dir, name)));
78
+ return join(dir, existing ?? "meta.ts");
79
+ };
56
80
 
57
81
  /**
58
82
  * Discover the default-locale `meta.{ts,js,mjs}` files whose titles a
59
83
  * translation run covers. Skips (in order): non-`dir` i18n projects entirely,
60
- * files inside a configured locale directory (those ARE translations),
61
- * factory-form modules (a warning — the generator can't re-emit a function),
62
- * modules that fail meta validation (the scan already errors on those), and
63
- * modules with no `title` (nothing to translate).
84
+ * files the scan's own folder-meta discovery never reads either — those
85
+ * matching the source's `exclude` globs (application code) or in folders its
86
+ * `include` globs don't reach (see `findFolderMetaFiles`) — files inside a
87
+ * configured locale directory (those ARE translations), files inside an
88
+ * archived-version snapshot (frozen, with their own translations — the same
89
+ * folders the navigation's meta lookup hoists as versions), modules that fail
90
+ * to load (the scan already errors on those), factory-form modules (a warning
91
+ * — the generator can't re-emit a function), modules that fail meta
92
+ * validation (the scan already errors on those too), and modules with no
93
+ * `title` (nothing to translate).
64
94
  */
65
95
  export const discoverTranslatableMeta = async (
66
96
  project: BlumeProject
@@ -74,33 +104,55 @@ export const discoverTranslatableMeta = async (
74
104
  locale.code === i18n.defaultLocale ? [] : [locale.code.toLowerCase()]
75
105
  )
76
106
  );
107
+ const versionDirs = new Set(
108
+ project.config.versions?.archived.map((version) => version.id)
109
+ );
77
110
 
78
111
  const load = createModuleLoader();
79
112
  const metas: TranslatableMeta[] = [];
80
113
  const diagnostics: Diagnostic[] = [];
81
114
 
82
115
  const roots = project.sources.flatMap((source) =>
83
- source.staged || !source.contentRoot ? [] : [source.contentRoot]
116
+ source.staged || !source.contentRoot
117
+ ? []
118
+ : [
119
+ {
120
+ contentRoot: source.contentRoot,
121
+ exclude: source.exclude,
122
+ include: source.include,
123
+ },
124
+ ]
84
125
  );
85
- for (const contentRoot of roots) {
126
+ for (const { contentRoot, exclude, include } of roots) {
127
+ // The scan's own file set (`discoverFolderMeta`): a `meta.ts` under an
128
+ // excluded folder (`src/lib/meta.ts` beside `exclude: ["src/**"]`) is
129
+ // application code, and one in a folder no `include` glob reaches
130
+ // configures no group the build renders.
86
131
  // oxlint-disable-next-line no-await-in-loop -- a project has O(1) sources
87
- const files = await glob(META_FILES, {
88
- absolute: true,
89
- cwd: contentRoot,
90
- ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
91
- onlyFiles: true,
92
- });
132
+ const files = await findFolderMetaFiles(
133
+ { exclude, include, root: contentRoot },
134
+ META_FILES
135
+ );
93
136
  for (const file of files.toSorted()) {
94
137
  const dir = relative(contentRoot, dirname(file));
95
- const first = dir.split("/")[0]?.toLowerCase();
96
- if (first && localeDirs.has(first)) {
138
+ const [head] = dir.split("/");
139
+ if (
140
+ head &&
141
+ (localeDirs.has(head.toLowerCase()) || versionDirs.has(head))
142
+ ) {
143
+ continue;
144
+ }
145
+ let mod: Awaited<ReturnType<typeof load>>;
146
+ try {
147
+ // oxlint-disable-next-line no-await-in-loop -- sequential, ordered discovery
148
+ mod = await load(file);
149
+ } catch {
150
+ // Folder meta that fails to load is the scan's BLUME_META_LOAD_FAILED
151
+ // error, not a crash here; either way there's no title to read.
97
152
  continue;
98
153
  }
99
154
  // oxlint-disable-next-line no-await-in-loop -- sequential, ordered discovery
100
- const [raw, mod] = await Promise.all([
101
- readFile(file, "utf-8"),
102
- load(file),
103
- ]);
155
+ const raw = await readFile(file, "utf-8");
104
156
  if (isFactoryModule(mod)) {
105
157
  diagnostics.push({
106
158
  code: "BLUME_TRANSLATE_META_FACTORY",
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
 
5
5
  import pLimit from "p-limit";
6
6
  import pMap from "p-map";
7
- import { join } from "pathe";
7
+ import { dirname, join } from "pathe";
8
8
 
9
9
  import { AGENTS } from "../audit/agent.ts";
10
10
  import type { AgentKind } from "../audit/agent.ts";
@@ -18,6 +18,7 @@ import { DEFAULT_TRANSLATE_TIMEOUT_MS, translateAgentArgs } from "./agents.ts";
18
18
  import { hashSource, stampLedger } from "./ledger.ts";
19
19
  import type { TranslationLedger } from "./ledger.ts";
20
20
  import { generateMetaModule } from "./meta.ts";
21
+ import type { TranslatableMeta } from "./meta.ts";
21
22
  import { metaPrompt, pagePrompt } from "./prompts.ts";
22
23
  import { parseMetaTitles, validateTranslation } from "./validate.ts";
23
24
  import type {
@@ -88,8 +89,13 @@ interface RunContext {
88
89
  timeoutMs: number;
89
90
  }
90
91
 
91
- /** The root directory's key in a meta-titles prompt (an empty key is opaque). */
92
- const metaDirKey = (dir: string): string => (dir === "" ? "." : dir);
92
+ /**
93
+ * A meta file's key in a meta-titles prompt: its directory relative to the
94
+ * project root. The content-root-relative `dir` isn't unique — two
95
+ * filesystem sources can both have a `guides/` folder — and colliding keys
96
+ * would hand both folders one translation.
97
+ */
98
+ const metaKey = (meta: TranslatableMeta): string => dirname(meta.sourceRel);
93
99
 
94
100
  /**
95
101
  * One headless agent call. A missing executable rejects with ENOENT on every
@@ -140,9 +146,9 @@ const runPageItem = async (
140
146
  // SAFETY: `targets` maps every configured locale, and work items only carry
141
147
  // configured locale codes.
142
148
  const target = context.targets.get(item.locale) as LocaleConfig;
143
- // A hand-authored translation can live at a non-canonical name (see
144
- // WorkStatus); the disk probe finds only canonical targets, and a miss just
145
- // means the prompt goes out without a style precedent.
149
+ // A stale item's target is the existing translation's own file, even at a
150
+ // non-canonical name (see `PageWorkItem.targetPath`), so it's the style
151
+ // precedent and the file rewritten; a missing item has nothing to read.
146
152
  const previousTranslation = existsSync(item.targetPath)
147
153
  ? await readFile(item.targetPath, "utf-8")
148
154
  : undefined;
@@ -183,7 +189,7 @@ const runMetaItem = async (
183
189
  });
184
190
 
185
191
  const titles = Object.fromEntries(
186
- item.entries.map((entry) => [metaDirKey(entry.meta.dir), entry.meta.title])
192
+ item.entries.map((entry) => [metaKey(entry.meta), entry.meta.title])
187
193
  );
188
194
  // SAFETY: `targets` maps every configured locale, and work items only carry
189
195
  // configured locale codes.
@@ -199,7 +205,7 @@ const runMetaItem = async (
199
205
 
200
206
  const parsed = parseMetaTitles(output.text, Object.keys(titles));
201
207
  for (const entry of item.entries) {
202
- const translated = parsed.titles[metaDirKey(entry.meta.dir)];
208
+ const translated = parsed.titles[metaKey(entry.meta)];
203
209
  if (translated === undefined) {
204
210
  continue;
205
211
  }
@@ -32,7 +32,10 @@ interface FrontmatterData {
32
32
  [key: string]: FrontmatterValue;
33
33
  }
34
34
 
35
- const FRONTMATTER_OPEN = /^---\r?\n/u;
35
+ // gray-matter strips a leading byte order mark before it looks for the fence,
36
+ // so a BOM-prefixed file still has frontmatter — this test must agree, or the
37
+ // reconstruction below writes the body alone and the metadata is lost.
38
+ const FRONTMATTER_OPEN = /^?---\r?\n/u;
36
39
  const FENCE_LINE = /^\s*(?:```|~~~)/u;
37
40
 
38
41
  /**
@@ -102,7 +105,16 @@ export const validateTranslation = (
102
105
  sourceText: string,
103
106
  agentText: string
104
107
  ): ValidationResult => {
105
- const candidate = stripOuterFence(agentText);
108
+ // Only a fence the agent added is stripped. A source that is itself one
109
+ // code block (a frontmatter-less partial holding a single bash block)
110
+ // starts and ends with its own fence, which the translation must keep — so
111
+ // for such a source the reply loses its outer fence only when it has more
112
+ // fence lines than the source.
113
+ const sourceIsFenced = stripOuterFence(sourceText) !== sourceText.trim();
114
+ const candidate =
115
+ sourceIsFenced && countFenceLines(agentText) <= countFenceLines(sourceText)
116
+ ? agentText.trim()
117
+ : stripOuterFence(agentText);
106
118
  if (candidate === "") {
107
119
  return { ok: false, reason: "agent returned empty output" };
108
120
  }