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
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
 
3
+ import { basename } from "pathe";
3
4
  import { z } from "zod";
4
5
 
5
6
  import type { BlumeConfig } from "./config-input.ts";
@@ -9,7 +10,7 @@ import {
9
10
  diagnosticsFromIssues,
10
11
  diagnosticsFromZod,
11
12
  } from "./diagnostics.ts";
12
- import { createModuleLoader } from "./load-module.ts";
13
+ import { createDefaultExportLoader } from "./load-module.ts";
13
14
  import { findConfigFile } from "./project.ts";
14
15
  import { blumeConfigSchema } from "./schema.ts";
15
16
  import type { ResolvedConfig } from "./schema.ts";
@@ -68,8 +69,8 @@ import type { Diagnostic } from "./types.ts";
68
69
  * - `theme` — `accent` color, `fonts` (curated slugs, any provider family, or
69
70
  * local font files), `radius`,
70
71
  * `mode` (`system`/`light`/`dark`), and `background`.
71
- * - `markdown` — `code` (language icons, inline highlighting, line wrap),
72
- * `headingAnchors`, `imageZoom`, and opt-in KaTeX `math`.
72
+ * - `markdown` — `code` (language icons, light/dark Shiki `theme`, line
73
+ * `wrap`), `externalLinks`, `headingAnchors`, and `imageZoom`.
73
74
  * - `toc` — on-page table of contents; `true`/`false` or a heading-level range.
74
75
  * - `lastModified` — "Last updated" stamps from `git` history or frontmatter.
75
76
  * - `feedback` — the per-page "Was this helpful?" widget (on by default).
@@ -199,7 +200,7 @@ export interface ConfigLoadResult {
199
200
  themeFontsConfigured: boolean;
200
201
  }
201
202
 
202
- const importConfigModule = createModuleLoader();
203
+ const importConfigModule = createDefaultExportLoader();
203
204
 
204
205
  /**
205
206
  * The slice of a user config module probed before schema defaults apply:
@@ -290,6 +291,19 @@ export const loadConfig = async (
290
291
  severity: "error",
291
292
  });
292
293
  }
294
+ // Validating whatever else the module holds would build a silent default
295
+ // site (a bare `defineConfig({…})` call) or reject a named `config` export
296
+ // as an unknown key, neither of which names the actual mistake.
297
+ if (raw === undefined) {
298
+ throw new BlumeError({
299
+ code: "BLUME_CONFIG_INVALID",
300
+ file: configFile,
301
+ message: `${basename(configFile)} has no default export, so Blume can't read its config.`,
302
+ severity: "error",
303
+ suggestion:
304
+ "Export the config as the file's default: `export default defineConfig({ … })`.",
305
+ });
306
+ }
293
307
  }
294
308
 
295
309
  // Read before parsing: schema defaults erase the set-vs-defaulted distinction.
@@ -98,50 +98,237 @@ const isKeySegment = (segment: string | number): segment is string =>
98
98
  typeof segment === "string";
99
99
 
100
100
  /**
101
- * Best-effort source position for a Zod issue path (e.g. `["seo", "title"]`) in
102
- * the raw config / frontmatter text. Narrows key-by-key — finding each string
103
- * segment as a `key:`/`key =` at or after the previous match — so a nested key
104
- * lands under its parent. Array indices are skipped. Returns 1-based line/column,
105
- * or undefined when nothing matches.
101
+ * Where a located value's children are searched. `object` and `array` are the
102
+ * inside of a `{…}`/`[…]` literal, searched entry by entry so a key or an index
103
+ * only matches a direct child. `block` has no brackets — the whole file at the
104
+ * top, or a YAML key's indented lines — and is searched at any depth.
106
105
  */
107
- const stepSegment = (
106
+ interface Scope {
107
+ end: number;
108
+ /** A literal's direct entries (keys or elements), in source order. */
109
+ entries: number[];
110
+ kind: "array" | "block" | "object";
111
+ start: number;
112
+ }
113
+
114
+ /** A located key or element: where it starts, and where its value starts. */
115
+ interface Step {
116
+ index: number;
117
+ next: number;
118
+ }
119
+
120
+ const QUOTES = new Set(['"', "'", "`"]);
121
+ const OPENERS = new Set(["(", "[", "{"]);
122
+ const CLOSERS = new Set([")", "]", "}"]);
123
+ const WHITESPACE = /\s/u;
124
+
125
+ /**
126
+ * The index just past a comment starting at `index`, or `index` when none does.
127
+ * A `//` after a `:` is a URL scheme in YAML (`href: https://…`), not a comment.
128
+ */
129
+ const skipComment = (source: string, index: number): number => {
130
+ if (source.startsWith("//", index) && source.charAt(index - 1) !== ":") {
131
+ const end = source.indexOf("\n", index);
132
+ return end === -1 ? source.length : end;
133
+ }
134
+ if (source.startsWith("/*", index)) {
135
+ const end = source.indexOf("*/", index + 2);
136
+ return end === -1 ? source.length : end + 2;
137
+ }
138
+ return index;
139
+ };
140
+
141
+ /**
142
+ * The index just past a string literal starting at `index`, or `index` when
143
+ * none does. A `'`/`"` that doesn't close on its own line is an apostrophe in
144
+ * plain YAML text, not a string.
145
+ */
146
+ const skipString = (source: string, index: number): number => {
147
+ const quote = source.charAt(index);
148
+ if (!QUOTES.has(quote)) {
149
+ return index;
150
+ }
151
+ let cursor = index + 1;
152
+ while (cursor < source.length) {
153
+ const char = source.charAt(cursor);
154
+ if (char === quote) {
155
+ return cursor + 1;
156
+ }
157
+ if (char === "\n" && quote !== "`") {
158
+ return index;
159
+ }
160
+ cursor += char === "\\" ? 2 : 1;
161
+ }
162
+ return index;
163
+ };
164
+
165
+ /** Where a literal closes, and where each of its direct entries starts. */
166
+ interface LiteralScan {
167
+ end: number;
168
+ entries: number[];
169
+ }
170
+
171
+ /**
172
+ * Walk the `{…}`/`[…]` literal opened at `open`: where each direct entry starts
173
+ * (a key, or an array element) and where the literal closes. Nested brackets,
174
+ * strings, and comments are stepped over, so only the literal's own commas
175
+ * separate its entries.
176
+ */
177
+ const scanLiteral = (source: string, open: number): LiteralScan => {
178
+ const entries: number[] = [];
179
+ let depth = 0;
180
+ let pending = true;
181
+ let index = open + 1;
182
+ while (index < source.length) {
183
+ const afterComment = skipComment(source, index);
184
+ const char = source.charAt(index);
185
+ if (afterComment !== index || WHITESPACE.test(char)) {
186
+ index = Math.max(afterComment, index + 1);
187
+ continue;
188
+ }
189
+ if (depth === 0 && CLOSERS.has(char)) {
190
+ return { end: index, entries };
191
+ }
192
+ if (depth === 0 && pending) {
193
+ entries.push(index);
194
+ pending = false;
195
+ }
196
+ const afterString = skipString(source, index);
197
+ if (afterString !== index) {
198
+ index = afterString;
199
+ continue;
200
+ }
201
+ if (OPENERS.has(char)) {
202
+ depth += 1;
203
+ } else if (CLOSERS.has(char)) {
204
+ depth -= 1;
205
+ } else if (char === "," && depth === 0) {
206
+ pending = true;
207
+ }
208
+ index += 1;
209
+ }
210
+ return { end: source.length, entries };
211
+ };
212
+
213
+ /**
214
+ * The lines indented deeper than the key at `anchor` — a YAML mapping's
215
+ * children — up to the first line that isn't, or `limit`.
216
+ */
217
+ const indentedBlock = (
108
218
  source: string,
109
- segment: string | number,
110
- cursor: number
111
- ) => {
112
- // A non-string path segment (array index) is skipped without moving on.
113
- if (!isKeySegment(segment)) {
114
- return { index: -1, next: cursor, stop: false };
219
+ anchor: number,
220
+ limit: number
221
+ ): Scope => {
222
+ const column = anchor - (source.lastIndexOf("\n", anchor - 1) + 1);
223
+ const lineEnd = source.indexOf("\n", anchor);
224
+ const start = lineEnd === -1 ? limit : Math.min(lineEnd + 1, limit);
225
+ let end = start;
226
+ while (end < limit) {
227
+ const next = source.indexOf("\n", end);
228
+ const after = next === -1 || next >= limit ? limit : next + 1;
229
+ const line = source.slice(end, after);
230
+ if (line.trim() !== "" && line.length - line.trimStart().length <= column) {
231
+ break;
232
+ }
233
+ end = after;
115
234
  }
116
- // The negative lookbehind keeps a segment like `title` from matching the
117
- // tail of an unrelated key such as `subtitle:`.
118
- const matcher = new RegExp(
119
- `(?<![\\w$])${escapeRegExp(segment)}\\s*[:=]`,
120
- "gu"
121
- );
122
- matcher.lastIndex = cursor;
123
- const match = matcher.exec(source);
124
- if (!match) {
125
- return { index: -1, next: cursor, stop: true };
235
+ return { end, entries: [], kind: "block", start };
236
+ };
237
+
238
+ /** A value's lead-in before its literal: spaces, or an adapter call's `vercel(`. */
239
+ const VALUE_LEAD = /[\s\w$.(]*/uy;
240
+
241
+ /**
242
+ * The scope holding the children of the key or element at `anchor`, whose
243
+ * value starts at `from`. A `{…}`/`[…]` literal — written directly, or as the
244
+ * first argument of a call like `vercel({…})` — is walked entry by entry;
245
+ * anything else falls back to the lines indented under `anchor` (YAML's
246
+ * nesting), within the enclosing scope's `limit`.
247
+ */
248
+ const valueScope = (source: string, step: Step, limit: number): Scope => {
249
+ VALUE_LEAD.lastIndex = step.next;
250
+ VALUE_LEAD.exec(source);
251
+ const open = VALUE_LEAD.lastIndex;
252
+ const char = source.charAt(open);
253
+ if (open < limit && (char === "{" || char === "[")) {
254
+ const literal = scanLiteral(source, open);
255
+ return {
256
+ end: literal.end,
257
+ entries: literal.entries,
258
+ kind: char === "{" ? "object" : "array",
259
+ start: open,
260
+ };
126
261
  }
127
- return { index: match.index, next: matcher.lastIndex, stop: false };
262
+ return indentedBlock(source, step.index, limit);
128
263
  };
129
264
 
265
+ /**
266
+ * Find `segment` as a `key:`/`key =` (bare or quoted) in `scope`: among an
267
+ * object literal's own entries, or anywhere in a block. The negative lookbehind
268
+ * keeps `title` from matching the tail of an unrelated key such as `subtitle:`.
269
+ */
270
+ const findKey = (
271
+ source: string,
272
+ scope: Scope,
273
+ segment: string
274
+ ): Step | undefined => {
275
+ const key = escapeRegExp(segment);
276
+ const pattern = `(?<![\\w$])(?:${key}|"${key}"|'${key}')\\s*[:=]`;
277
+ if (scope.kind === "block") {
278
+ const matcher = new RegExp(pattern, "gu");
279
+ matcher.lastIndex = scope.start;
280
+ const match = matcher.exec(source);
281
+ return match && match.index < scope.end
282
+ ? { index: match.index, next: matcher.lastIndex }
283
+ : undefined;
284
+ }
285
+ // Sticky: the key must start exactly where the entry does.
286
+ const matcher = new RegExp(pattern, "uy");
287
+ for (const entry of scope.kind === "object" ? scope.entries : []) {
288
+ matcher.lastIndex = entry;
289
+ if (matcher.test(source)) {
290
+ return { index: entry, next: matcher.lastIndex };
291
+ }
292
+ }
293
+ return undefined;
294
+ };
295
+
296
+ /** The element at `position` of an array literal; a block can't be indexed. */
297
+ const findElement = (scope: Scope, position: number): Step | undefined => {
298
+ const index = scope.kind === "array" ? scope.entries[position] : undefined;
299
+ return index === undefined ? undefined : { index, next: index };
300
+ };
301
+
302
+ /**
303
+ * Best-effort source position for a Zod issue path (e.g. `["seo", "title"]`,
304
+ * `["redirects", 2, "status"]`) in the raw config / frontmatter text. Narrows
305
+ * segment by segment: a key is found among its parent's own entries (or, where
306
+ * there are no brackets, in the parent's indented lines), an index picks that
307
+ * element of an array literal. A segment that isn't there — a missing key —
308
+ * stops the walk at its parent, so the position never escapes into a sibling.
309
+ * Returns 1-based line/column, or undefined when not even the first segment
310
+ * matches.
311
+ */
130
312
  const locatePath = (
131
313
  source: string,
132
314
  path: readonly (string | number)[]
133
315
  ): { column: number; line: number } | undefined => {
134
- let cursor = 0;
316
+ let scope: Scope = {
317
+ end: source.length,
318
+ entries: [],
319
+ kind: "block",
320
+ start: 0,
321
+ };
135
322
  let found = -1;
136
323
  for (const segment of path) {
137
- const step = stepSegment(source, segment, cursor);
138
- if (step.stop) {
324
+ const step = isKeySegment(segment)
325
+ ? findKey(source, scope, segment)
326
+ : findElement(scope, segment);
327
+ if (!step) {
139
328
  break;
140
329
  }
141
- cursor = step.next;
142
- if (step.index >= 0) {
143
- found = step.index;
144
- }
330
+ found = step.index;
331
+ scope = valueScope(source, step, scope.end);
145
332
  }
146
333
  if (found < 0) {
147
334
  return;
package/src/core/graph.ts CHANGED
@@ -1,17 +1,22 @@
1
- import { withBasePath } from "./base-path.ts";
1
+ import { mountBasePath, withBasePath } from "./base-path.ts";
2
2
  import {
3
3
  localizeInternalPath,
4
4
  localizeRoute,
5
5
  resolveFallbackLocale,
6
6
  } from "./i18n.ts";
7
- import { validateNavIcons, validateNavStructure } from "./nav-diagnostics.ts";
8
- import { buildNavigation } from "./navigation.ts";
7
+ import {
8
+ validateNavIcons,
9
+ validateNavStructure,
10
+ validateSidebarItems,
11
+ } from "./nav-diagnostics.ts";
12
+ import { buildNavigation, isPageRef, pagesByRef } from "./navigation.ts";
9
13
  import type {
10
14
  FolderMeta,
11
15
  LocalizableLabel,
12
16
  ResolvedConfig,
13
17
  ResolvedI18nConfig,
14
18
  ResolvedVersionsConfig,
19
+ SidebarItemConfig,
15
20
  } from "./schema.ts";
16
21
  import type {
17
22
  ContentGraph,
@@ -23,6 +28,8 @@ import type {
23
28
  import { versionizeRoute } from "./versions.ts";
24
29
 
25
30
  interface BuildContentGraphOptions {
31
+ /** The banner's link target as configured (`banner.link.href`), if any. */
32
+ bannerHref?: string;
26
33
  /** Site-wide route mount point (`""` or `/seg`); invisible to the nav tree. */
27
34
  basePath?: string;
28
35
  /** The header brand link as configured (`logo.href`, default `/`). */
@@ -83,7 +90,7 @@ const localePagesFor = (
83
90
  ...source,
84
91
  fallback: true,
85
92
  locale: code,
86
- route: withBasePath(basePath, localizeRoute(key, code, i18n)),
93
+ route: mountBasePath(basePath, localizeRoute(key, code, i18n)),
87
94
  });
88
95
  }
89
96
  }
@@ -115,6 +122,28 @@ const resolveLabel = (
115
122
  );
116
123
  };
117
124
 
125
+ /**
126
+ * An explicit sidebar with every item's `href` passed through `localize` (page
127
+ * refs and `root`s resolve against each locale's own pages already).
128
+ */
129
+ const localizeSidebarHrefs = (
130
+ items: SidebarItemConfig[],
131
+ localize: (href: string) => string
132
+ ): SidebarItemConfig[] =>
133
+ items.map((item) => {
134
+ if (isPageRef(item)) {
135
+ return item;
136
+ }
137
+ const localized = { ...item };
138
+ if (item.href !== undefined) {
139
+ localized.href = localize(item.href);
140
+ }
141
+ if (item.items) {
142
+ localized.items = localizeSidebarHrefs(item.items, localize);
143
+ }
144
+ return localized;
145
+ });
146
+
118
147
  /** Resolve every localizable label in the configured tabs for one locale. */
119
148
  const resolveTabLabels = (
120
149
  tabs: BuildContentGraphOptions["navigation"]["tabs"],
@@ -130,6 +159,18 @@ const resolveTabLabels = (
130
159
  label: resolveLabel(tab.label, locale, defaultLocale),
131
160
  }));
132
161
 
162
+ /**
163
+ * Mount a configured link authored as if the site were served at root (the
164
+ * banner's): under `basePath`, except a route served outside the content tree
165
+ * — a custom page, the generated changelog index — which answers only at its
166
+ * own path.
167
+ */
168
+ const mountConfiguredLink = (
169
+ path: string,
170
+ basePath: string,
171
+ extraRoutes: ReadonlySet<string>
172
+ ): string => (extraRoutes.has(path) ? path : withBasePath(basePath, path));
173
+
133
174
  /**
134
175
  * Build one locale's navigation tree from its own pages and folder meta.
135
176
  * `version` is the archived version id when building a snapshot's tree
@@ -217,8 +258,15 @@ const buildLocaleNavigation = (
217
258
  // next to the originals and `guides/meta.ts` applies to every locale —
218
259
  // prefixing would look up keys that can never exist. Inside a snapshot the
219
260
  // version dir is hoisted in front (`v1.0/fr`), matching `discoverFolderMeta`.
220
- const localeDir =
221
- i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "";
261
+ const localeDirOf = (locale: string): string =>
262
+ i18n.parser === "dir" && locale !== i18n.defaultLocale ? locale : "";
263
+ const localeDir = localeDirOf(code);
264
+ // A locale padded from the fallback locale mirrors that locale's folder meta
265
+ // for any folder it has no meta of its own for.
266
+ const fallbackMetaPrefix =
267
+ fallback && code !== fallback
268
+ ? [version, localeDirOf(fallback)].filter(Boolean).join("/")
269
+ : undefined;
222
270
  // Internal featured and header hrefs are localized like tab paths — a pinned
223
271
  // `/changelog` link rendered on `/fr/…` pages must stay inside the reader's
224
272
  // locale, not kick them back to the default one.
@@ -227,6 +275,7 @@ const buildLocaleNavigation = (
227
275
  href: localizeServed(item.href, false),
228
276
  });
229
277
  const { actions, cta, featured } = options.navigation;
278
+ const sidebarItems = options.navigation.sidebar.items;
230
279
 
231
280
  const navigation = buildNavigation(localePages, {
232
281
  actions: actions?.map(localizeHref),
@@ -235,6 +284,7 @@ const buildLocaleNavigation = (
235
284
  diagnostics,
236
285
  display: options.navigation.sidebar.display,
237
286
  extraRoutes: options.extraRoutes,
287
+ fallbackMetaPrefix,
238
288
  featured: featured?.map(localizeHref),
239
289
  folderMeta: options.folderMeta,
240
290
  // The localized tree root ("/" for the hidden default, "/fr" otherwise;
@@ -250,13 +300,43 @@ const buildLocaleNavigation = (
250
300
  sharedMetaPrefix: version,
251
301
  // A configured explicit sidebar describes the current docs; a frozen
252
302
  // snapshot's structure comes from the snapshot itself, so archived trees
253
- // always build from the filesystem.
254
- sidebar: version ? undefined : options.navigation.sidebar.items,
303
+ // always build from the filesystem. Its `href` links are localized like
304
+ // featured links.
305
+ sidebar:
306
+ version || !sidebarItems
307
+ ? undefined
308
+ : localizeSidebarHrefs(sidebarItems, (href) =>
309
+ localizeServed(href, false)
310
+ ),
311
+ // Tab paths are written against the current docs' root, which a
312
+ // snapshot's versionized `localizedRoot` no longer is.
313
+ tabRoot: localizeRoute("/", code, i18n),
255
314
  tabs,
256
315
  });
257
- return options.brandHref === undefined
258
- ? navigation
259
- : { ...navigation, brandHref: localizeServed(options.brandHref, false) };
316
+ // `serves` checked the localized brand link against based routes, and
317
+ // unlike tab and header links it isn't rebased later, so a localized one
318
+ // takes the base here (`/fr` is served at `/docs/fr`).
319
+ const localizedBrand = (href: string): string => {
320
+ const localized = localizeServed(href, false);
321
+ return localized === href ? localized : withBasePath(basePath, localized);
322
+ };
323
+ const branded =
324
+ options.brandHref === undefined
325
+ ? navigation
326
+ : { ...navigation, brandHref: localizedBrand(options.brandHref) };
327
+ // The banner link is authored like a featured href — in the default
328
+ // locale's path space, as if mounted at root — so it is localized the same
329
+ // way before it is mounted.
330
+ return options.bannerHref === undefined
331
+ ? branded
332
+ : {
333
+ ...branded,
334
+ bannerHref: mountConfiguredLink(
335
+ localizeServed(options.bannerHref, false),
336
+ basePath,
337
+ extraRoutes
338
+ ),
339
+ };
260
340
  };
261
341
 
262
342
  /**
@@ -348,6 +428,8 @@ const buildVersionNavigation = (
348
428
  selectors: options.navigation.selectors,
349
429
  sharedFolderMeta: options.sharedFolderMeta,
350
430
  sharedMetaPrefix: id,
431
+ // Tab paths are written against the current docs' root.
432
+ tabRoot: "/",
351
433
  // A configured explicit sidebar describes the current docs; a snapshot's
352
434
  // structure comes from the snapshot itself.
353
435
  tabs: resolveTabLabels(options.navigation.tabs, ""),
@@ -398,6 +480,16 @@ export const buildContentGraph = (
398
480
  // entry on a single-locale site.
399
481
  tabs: resolveTabLabels(options.navigation.tabs, ""),
400
482
  });
483
+ if (options.bannerHref !== undefined) {
484
+ navigation = {
485
+ ...navigation,
486
+ bannerHref: mountConfiguredLink(
487
+ options.bannerHref,
488
+ options.basePath ?? "",
489
+ options.extraRoutes ?? new Set<string>()
490
+ ),
491
+ };
492
+ }
401
493
  }
402
494
 
403
495
  const navigationByVersion: Record<string, Record<string, Navigation>> = {};
@@ -417,6 +509,23 @@ export const buildContentGraph = (
417
509
  ...validateNavIcons(navigation),
418
510
  ...validateNavStructure(navigation, currentPages)
419
511
  );
512
+ // Checked once against every locale's pages, not per locale tree: a page
513
+ // missing from one locale's sidebar is an untranslated page, not a typo.
514
+ const sidebarItems = options.navigation.sidebar.items;
515
+ if (sidebarItems) {
516
+ const byRef = pagesByRef(
517
+ currentPages,
518
+ options.basePath ?? "",
519
+ Boolean(i18n)
520
+ );
521
+ const extraRoutes = options.extraRoutes ?? new Set<string>();
522
+ diagnostics.push(
523
+ ...validateSidebarItems(
524
+ sidebarItems,
525
+ (route) => byRef.has(route) || extraRoutes.has(route)
526
+ )
527
+ );
528
+ }
420
529
 
421
530
  return {
422
531
  diagnostics,
@@ -72,11 +72,32 @@ const uiStringsObject = z.object({
72
72
  .prefault({}),
73
73
  content: z
74
74
  .object({
75
+ // `<Component>`'s source tab.
76
+ code: z.string().default("Code"),
75
77
  // A `<Color.Item>` swatch's accessible name; `{name}` and `{value}` are
76
78
  // replaced with the color's name and the value it shows.
77
79
  copyColor: z.string().default("Copy {name} color {value}"),
78
80
  copyPrompt: z.string().default("Copy prompt"),
81
+ // `<TypeTable>`: a row's default-value label.
82
+ default: z.string().default("Default"),
79
83
  diagramError: z.string().default("Could not render this diagram."),
84
+ // `<GithubInfo>`: the screen-reader labels of the fork and star counts.
85
+ forks: z.string().default("Forks"),
86
+ // `<Component>`'s live-preview tab, also its frame's title.
87
+ preview: z.string().default("Preview"),
88
+ // `<TypeTable>`: the property-name column.
89
+ prop: z.string().default("Prop"),
90
+ // `<Tabs dropdown>`: the accessible name of the tab picker.
91
+ selectTab: z.string().default("Select tab"),
92
+ // `<Expandable>`'s toggle when it sets no `title`.
93
+ showMore: z.string().default("Show more"),
94
+ stars: z.string().default("Stars"),
95
+ // An untitled tab's label; `{n}` is replaced with its position.
96
+ tab: z.string().default("Tab {n}"),
97
+ // `<TypeTable>`: the type column and a row's type label.
98
+ type: z.string().default("Type"),
99
+ // `<Update>`'s heading when it sets no `label` or `title`.
100
+ update: z.string().default("Update"),
80
101
  })
81
102
  .prefault({}),
82
103
  feedback: z
@@ -147,8 +168,13 @@ const uiStringsObject = z.object({
147
168
  devOnly: z
148
169
  .string()
149
170
  .default("Search is available in the production build."),
171
+ // The section a hit falls under when its page sits in no sidebar group.
172
+ docs: z.string().default("Docs"),
150
173
  error: z.string().default("Something went wrong. Please try again."),
151
174
  label: z.string().default("Search docs"),
175
+ // The version tag on a result from the current docs in an all-versions
176
+ // search (a result from an archived version shows its version id).
177
+ latest: z.string().default("latest"),
152
178
  navigate: z.string().default("navigate"),
153
179
  noResults: z.string().default("No results found."),
154
180
  open: z.string().default("open"),
@@ -281,11 +307,20 @@ const packFor = (code: string): UIStringsOverride | undefined => {
281
307
  );
282
308
  };
283
309
 
310
+ /** Whether a locale code is English (`en`, `en-GB`, `en_US`), in any case. */
311
+ const isEnglish = (code: string): boolean =>
312
+ code.toLowerCase().split(/[-_]/u)[0] === "en";
313
+
284
314
  /**
285
315
  * Resolve the active dictionary for a locale. Layers, in order:
286
316
  * English baseline ← default-locale pack ← default-locale override ←
287
317
  * locale pack ← locale override. So a missing key falls back to the default
288
318
  * locale's translation, then to English.
319
+ *
320
+ * English has no pack because the baseline is its complete translation, so an
321
+ * English locale (`en`, `en-GB`) skips the default-locale layers: on a site
322
+ * whose default is German, English pages keep English chrome instead of
323
+ * inheriting the German strings.
289
324
  */
290
325
  export const resolveUIStrings = (
291
326
  locale: string,
@@ -296,8 +331,10 @@ export const resolveUIStrings = (
296
331
  ): UIStrings => {
297
332
  const { defaultLocale, overrides } = options;
298
333
  let dict = EN_UI;
299
- dict = mergeUI(dict, packFor(defaultLocale));
300
- dict = mergeUI(dict, overrides?.[defaultLocale]);
334
+ if (!isEnglish(locale) || isEnglish(defaultLocale)) {
335
+ dict = mergeUI(dict, packFor(defaultLocale));
336
+ dict = mergeUI(dict, overrides?.[defaultLocale]);
337
+ }
301
338
  if (locale !== defaultLocale) {
302
339
  dict = mergeUI(dict, packFor(locale));
303
340
  dict = mergeUI(dict, overrides?.[locale]);
package/src/core/i18n.ts CHANGED
@@ -187,12 +187,19 @@ export const localePlacement = (
187
187
  * (`x.en.mdx`) swaps it for the target's (`x.fr.mdx`) — same case-insensitive
188
188
  * last-dot-inside-filename logic as `localePlacement`, so both spellings of a
189
189
  * default-locale file resolve to one canonical target.
190
+ *
191
+ * Under `dir`, `folders` are the content root's existing top-level folders: one
192
+ * that names the locale in another casing (`pt-br/` for a configured `pt-BR`)
193
+ * is the locale's folder already, since {@link detectLocale} matches it case-
194
+ * insensitively. Writing beside it into a second `pt-BR/` would, on a case-
195
+ * sensitive filesystem, publish the page twice.
190
196
  */
191
197
  export const localeTargetPath = (
192
198
  rel: string,
193
199
  ext: string,
194
200
  locale: string,
195
- i18n: ResolvedI18nConfig
201
+ i18n: ResolvedI18nConfig,
202
+ folders: readonly string[] = []
196
203
  ): string => {
197
204
  if (i18n.parser === "dot") {
198
205
  let base = rel.slice(0, rel.length - ext.length);
@@ -205,7 +212,11 @@ export const localeTargetPath = (
205
212
  }
206
213
  return `${base}.${locale}${ext}`;
207
214
  }
208
- return `${locale}/${rel}`;
215
+ const folder = folders.includes(locale)
216
+ ? locale
217
+ : (folders.find((name) => name.toLowerCase() === locale.toLowerCase()) ??
218
+ locale);
219
+ return `${folder}/${rel}`;
209
220
  };
210
221
 
211
222
  /**