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
@@ -55,17 +55,12 @@ export interface HeadingMarkers {
55
55
  * the heading count (mid-text brackets are ordinary prose); when a marker kind
56
56
  * repeats, the last one written wins.
57
57
  *
58
- * `isRefDefined` reports whether a link-reference definition exists for a
59
- * bracket's label (`toc`, `!toc`, `#id`): a defined bracket is a CommonMark
60
- * shortcut link, not a marker — the renderer sees a resolved `<a>` element
61
- * there and strips nothing at or before it, so the parse stops the same way.
62
- * The render-time hast plugin never passes it (resolved links are already
63
- * elements by the time it runs); the raw-source scanner does.
58
+ * Callers pass the heading's trailing text node after a Markdown parse, so a
59
+ * bracket that a link-reference definition turns into a CommonMark shortcut
60
+ * link (`[toc]` with a `[toc]: …` definition) is already an element there and
61
+ * never reaches this parse.
64
62
  */
65
- export const parseHeadingMarkers = (
66
- text: string,
67
- isRefDefined?: (label: string) => boolean
68
- ): HeadingMarkers => {
63
+ export const parseHeadingMarkers = (text: string): HeadingMarkers => {
69
64
  let remaining = text;
70
65
  let id: string | undefined;
71
66
  let toc: HeadingMarkers["toc"];
@@ -75,14 +70,6 @@ export const parseHeadingMarkers = (
75
70
  match = MARKER.exec(remaining)
76
71
  ) {
77
72
  const explicitId = match.groups.curlyId ?? match.groups.id;
78
- const label =
79
- explicitId === undefined
80
- ? `${match.groups.hide ?? ""}toc`
81
- : `#${explicitId}`;
82
- // A `{#id}` is never a shortcut link, whatever the definitions say.
83
- if (match.groups.curlyId === undefined && isRefDefined?.(label)) {
84
- break;
85
- }
86
73
  remaining = remaining.slice(0, match.index);
87
74
  if (explicitId === undefined) {
88
75
  // Stripping runs right-to-left, so keeping the first capture of each
@@ -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,14 +212,38 @@ 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
 
222
+ /**
223
+ * The warning for a top-level folder named after the default locale
224
+ * (`docs/en/` when `defaultLocale` is `en`). Only non-default codes are
225
+ * locale folders — the default locale lives at the content root — so the
226
+ * folder is ordinary default-locale content, published one level down at
227
+ * `/en/…` (`/en/en/…` with the default prefix shown) and filled into every
228
+ * other locale at `/fr/en/…`.
229
+ */
230
+ const defaultLocaleFolderDiagnostic = (
231
+ folder: string,
232
+ i18n: ResolvedI18nConfig
233
+ ): Diagnostic => ({
234
+ code: "BLUME_I18N_DEFAULT_LOCALE_FOLDER",
235
+ message: `Folder "${folder}/" is named after the default locale, but "${i18n.defaultLocale}" pages live at the content root — its pages publish at ${localizeRoute(`/${folder}`, i18n.defaultLocale, i18n)}/… instead.`,
236
+ severity: "warning",
237
+ suggestion: `Move the contents of "${folder}/" up to the content root; only the other locales get a folder.`,
238
+ });
239
+
211
240
  /**
212
241
  * Warn about top-level content folders that look like a locale (a code Blume
213
242
  * recognizes) but aren't declared in `i18n.locales`. Without this they're
214
243
  * silently treated as default-locale content under a `/<code>/…` route, which
215
244
  * is almost never intended — usually a translation that wasn't registered.
245
+ * A folder named after the default locale gets the same treatment (see
246
+ * {@link defaultLocaleFolderDiagnostic}).
216
247
  */
217
248
  export const i18nDiagnostics = (
218
249
  pages: PageRecord[],
@@ -231,9 +262,17 @@ export const i18nDiagnostics = (
231
262
  // Inside a version snapshot the locale dir sits one level deeper
232
263
  // (`v1.0/fr/guide.md`), so a configured version segment is skipped first.
233
264
  const parts = page.source.ref.split("/");
234
- const first = (
235
- parts[0] && versionDirs.has(parts[0]) ? parts[1] : parts[0]
236
- )?.toLowerCase();
265
+ const folder = parts[0] && versionDirs.has(parts[0]) ? parts[1] : parts[0];
266
+ const first = folder?.toLowerCase();
267
+ if (
268
+ folder !== undefined &&
269
+ first === i18n.defaultLocale.toLowerCase() &&
270
+ !seen.has(first)
271
+ ) {
272
+ seen.add(first);
273
+ diagnostics.push(defaultLocaleFolderDiagnostic(folder, i18n));
274
+ continue;
275
+ }
237
276
  if (
238
277
  first &&
239
278
  !seen.has(first) &&
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
2
3
 
3
- import { relative } from "pathe";
4
+ import { normalize, relative } from "pathe";
4
5
 
5
6
  import type { Diagnostic } from "./types.ts";
6
7
 
@@ -58,6 +59,23 @@ export const parseGitLog = (output: string): Map<string, string> => {
58
59
  return times;
59
60
  };
60
61
 
62
+ /**
63
+ * `path` with every symlink resolved, the spelling git prints paths in: a
64
+ * project opened through a link (`/tmp/site` on macOS, where `/tmp` links to
65
+ * `/private/tmp`) gets `/private/tmp/site` from `rev-parse --show-toplevel`,
66
+ * so the project's own spelling would compare as outside the repository and
67
+ * every page would go undated. The native realpath also expands a Windows
68
+ * 8.3 short name (`RUNNER~1`), which git does too. A path that doesn't exist
69
+ * stays as written.
70
+ */
71
+ export const realPath = (path: string): string => {
72
+ try {
73
+ return normalize(realpathSync.native(path));
74
+ } catch {
75
+ return path;
76
+ }
77
+ };
78
+
61
79
  /**
62
80
  * The toplevel of the repository containing `root`, or null when git is
63
81
  * unavailable or the project isn't a repo. Callers use it to decide which
@@ -108,6 +126,10 @@ export const gitLastModifiedTimes = (
108
126
  if (gitRoot === null) {
109
127
  return new Map();
110
128
  }
129
+ // Git names paths by their real location, so the pathspecs and the paths
130
+ // read back against the log are compared in that spelling too (see
131
+ // `realPath`).
132
+ const top = realPath(gitRoot);
111
133
  try {
112
134
  const output = execFileSync(
113
135
  // oxlint-disable-next-line sonarjs/no-os-command-from-path -- git is a required dev-tool dependency resolved from PATH
@@ -121,14 +143,14 @@ export const gitLastModifiedTimes = (
121
143
  "--format=%x00%cI",
122
144
  "--name-only",
123
145
  "--",
124
- ...contentRoots,
146
+ ...contentRoots.map(realPath),
125
147
  ],
126
148
  { encoding: "utf-8", env: gitEnv(), maxBuffer: 256 * 1024 * 1024 }
127
149
  );
128
150
  const byRepoPath = parseGitLog(output);
129
151
  const result = new Map<string, string>();
130
152
  for (const sourcePath of sourcePaths) {
131
- const iso = byRepoPath.get(relative(gitRoot, sourcePath));
153
+ const iso = byRepoPath.get(relative(top, realPath(sourcePath)));
132
154
  if (iso) {
133
155
  result.set(sourcePath, iso);
134
156
  }
package/src/core/links.ts CHANGED
@@ -109,10 +109,11 @@ const NUMERIC_PREFIX = /^\d+[-_.]/u;
109
109
  * falsely flagged as broken. Tested against `navPath` — the locale-stripped
110
110
  * path — so a dot-parser localized index (`index.fr.mdx`) and a shared
111
111
  * locale-agnostic one (`index.$.mdx`) count too, matching how route mapping
112
- * recognizes them.
112
+ * recognizes them. Only a lowercase `index` counts, as in route mapping:
113
+ * `Index.md` publishes at `…/Index`, a page of its own.
113
114
  */
114
115
  const isIndexPage = (page: PageRecord): boolean =>
115
- /^index\.(?:md|mdx)$/iu.test(
116
+ /^index\.[Mm][Dd][Xx]?$/u.test(
116
117
  basename(page.navPath).replace(NUMERIC_PREFIX, "")
117
118
  );
118
119
 
@@ -218,14 +219,16 @@ export const resolveRelativeHref = (
218
219
  * Whether a content file is its folder's index, from its name as written on
219
220
  * disk: `index.md(x)` after an ordering prefix, optionally carrying one of
220
221
  * `localeTokens` before the extension (`index.fr.mdx` under the `dot` locale
221
- * parser, `index.$.mdx` for a file shared by every locale).
222
+ * parser, `index.$.mdx` for a file shared by every locale). The `index` itself
223
+ * is case-sensitive, as route mapping reads it: `Index.md` is a page named
224
+ * `Index`, not its folder's index.
222
225
  */
223
226
  export const isIndexFileName = (
224
227
  name: string,
225
228
  localeTokens: readonly string[] = []
226
229
  ): boolean => {
227
230
  const stem = basename(name).replace(NUMERIC_PREFIX, "");
228
- const match = /^index(?:\.(?<token>[^./]+))?\.(?:md|mdx)$/iu.exec(stem);
231
+ const match = /^index(?:\.(?<token>[^./]+))?\.[Mm][Dd][Xx]?$/u.exec(stem);
229
232
  if (!match) {
230
233
  return false;
231
234
  }
@@ -14,3 +14,23 @@ export const createModuleLoader = (): ((file: string) => Promise<unknown>) => {
14
14
  return loaded?.default ?? loaded;
15
15
  };
16
16
  };
17
+
18
+ /**
19
+ * Create a loader for a module whose default export is its whole value
20
+ * (`blume.config.ts`). Unlike {@link createModuleLoader}, it never falls back
21
+ * to the module's namespace: a module with no default export, or one that
22
+ * exports `null`/`undefined`, resolves to `undefined` so the caller can say
23
+ * so, instead of validating `{}` (a bare `defineConfig({…})` call) or
24
+ * `{ config }` (a named export). jiti's default interop is off because it
25
+ * throws on `export default null`.
26
+ */
27
+ export const createDefaultExportLoader: typeof createModuleLoader = () => {
28
+ const jiti = createJiti(import.meta.url, {
29
+ interopDefault: false,
30
+ moduleCache: false,
31
+ });
32
+ return async (file: string) => {
33
+ const loaded = await jiti.import<{ default?: unknown }>(file);
34
+ return loaded.default ?? undefined;
35
+ };
36
+ };
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  isInternalPath,
3
+ mountBasePath,
3
4
  normalizePath,
4
5
  stripBasePath,
5
6
  withBasePath,
@@ -44,13 +45,6 @@ export interface LocalizeLinkOptions {
44
45
  routes: RouteSet;
45
46
  }
46
47
 
47
- /**
48
- * Mirrors the asset heuristic in `markdown/base-links.ts`: a path whose final
49
- * segment carries a file extension is a `public/` asset (or a raw `.md` twin),
50
- * served at the site root and never localized.
51
- */
52
- const ASSET_PATH = /\.[a-z0-9]+$/iu;
53
-
54
48
  /** Decode a percent-encoded path for a route lookup; leave junk as-is. */
55
49
  const decodePercent = (value: string): string => {
56
50
  try {
@@ -60,6 +54,13 @@ const decodePercent = (value: string): string => {
60
54
  }
61
55
  };
62
56
 
57
+ /**
58
+ * Whether `routes` serves `route` (base-prefixed, fragment-less). Routes are
59
+ * stored decoded, so a browser-copied `/caf%C3%A9` still finds its page.
60
+ */
61
+ export const servesRoute = (routes: RouteSet, route: string): boolean =>
62
+ routes.has(route) || routes.has(decodePercent(route));
63
+
63
64
  /** Whether `path` (base-stripped) already sits under some locale's prefix. */
64
65
  const hasLocalePrefix = (path: string, i18n: LocaleRouting): boolean =>
65
66
  i18n.locales.some((locale) => {
@@ -85,11 +86,9 @@ export const localizeLinkPath = (
85
86
  return path;
86
87
  }
87
88
  const localized = withBasePath(basePath, localizeRoute(rest, locale, i18n));
88
- // Routes are stored decoded; a browser-copied `/caf%C3%A9` must still find
89
- // its translation, but the emitted href keeps the author's encoding.
90
- return routes.has(localized) || routes.has(decodePercent(localized))
91
- ? localized
92
- : path;
89
+ // A browser-copied `/caf%C3%A9` must still find its translation, but the
90
+ // emitted href keeps the author's encoding.
91
+ return servesRoute(routes, localized) ? localized : path;
93
92
  };
94
93
 
95
94
  export interface LocalizeHrefOptions extends LocalizeLinkOptions {
@@ -98,9 +97,14 @@ export interface LocalizeHrefOptions extends LocalizeLinkOptions {
98
97
  }
99
98
 
100
99
  /**
101
- * Localize a rendered `href`: external URLs, relative paths, bare fragments, and
102
- * asset links pass through; a root-relative page link keeps its `?query` and
100
+ * Localize a rendered `href`: external URLs, relative paths, and bare
101
+ * fragments pass through; a root-relative page link keeps its `?query` and
103
102
  * `#fragment` and its `deployment.base` layer around the localized path.
103
+ *
104
+ * Asset links (`/logo.png`, a raw `.md` twin) pass through too, because no
105
+ * page is served at their localized path — the same served-route test the
106
+ * link checker applies. A file extension alone doesn't mark an asset: a
107
+ * dotted page route (`/releases/v1.2`) is localized like any other page.
104
108
  */
105
109
  export const localizeHref = (
106
110
  href: string,
@@ -112,15 +116,15 @@ export const localizeHref = (
112
116
  const suffixAt = href.search(/[#?]/u);
113
117
  const path = suffixAt === -1 ? href : href.slice(0, suffixAt);
114
118
  const suffix = suffixAt === -1 ? "" : href.slice(suffixAt);
115
- if (ASSET_PATH.test(path)) {
116
- return href;
117
- }
118
119
  const based = stripBasePath(options.deployBase, path);
119
120
  const localized = localizeLinkPath(based, options);
120
121
  if (localized === based) {
121
122
  return href;
122
123
  }
123
- return `${withBasePath(options.deployBase, localized)}${suffix}`;
124
+ // The base came off above, so it goes back on unconditionally: a localized
125
+ // path that starts with the base's own name (`/ja/…` under base `/ja`) is
126
+ // still served under it.
127
+ return `${mountBasePath(options.deployBase, localized)}${suffix}`;
124
128
  };
125
129
 
126
130
  /** Every `<a …>` opening tag; `\s` keeps `<abbr>`/`<astro-island>` out. */
@@ -1,4 +1,4 @@
1
- import { withBasePath } from "./base-path.ts";
1
+ import { mountBasePath } from "./base-path.ts";
2
2
  import { localizeRoute, resolveFallbackLocale } from "./i18n.ts";
3
3
  import type { ResolvedConfig } from "./schema.ts";
4
4
  import type {
@@ -106,7 +106,7 @@ const buildFallbackRoutes = (
106
106
  if (present.has(key)) {
107
107
  continue;
108
108
  }
109
- const path = withBasePath(basePath, localizeRoute(key, code, i18n));
109
+ const path = mountBasePath(basePath, localizeRoute(key, code, i18n));
110
110
  // A fallback route is a real prerendered page, so it registers as a
111
111
  // version alternate too — the switcher on a sibling version's page
112
112
  // lands here instead of bouncing to the version root. Its own path is
@@ -183,6 +183,7 @@ export const buildManifest = (options: {
183
183
  }
184
184
 
185
185
  const routes: RouteManifestEntry[] = graph.pages.map((page) => ({
186
+ aiExclude: page.meta.ai.exclude,
186
187
  alternates: alternatesByKey.get(page.translationKey) ?? [],
187
188
  collection: page.collection ?? "docs",
188
189
  contentType: page.contentType,
package/src/core/meta.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { basename, dirname, relative } from "pathe";
2
- import { glob } from "tinyglobby";
2
+ import { glob, isDynamicPattern } from "tinyglobby";
3
3
 
4
4
  import { diagnosticsFromZod } from "./diagnostics.ts";
5
5
  import { createModuleLoader } from "./load-module.ts";
@@ -38,12 +38,89 @@ const resolveMeta = async (mod: MetaModuleExport) =>
38
38
 
39
39
  /** A filesystem content source to scan for folder meta: its on-disk root and
40
40
  * optional route prefix. The prefix is folded into every key so meta lines up
41
- * with the sidebar group path, which carries the same prefix. */
41
+ * with the sidebar group path, which carries the same prefix. `include` and
42
+ * `exclude` are the source's content globs, which scope the scan to the
43
+ * folders the source reads. */
42
44
  export interface FolderMetaSource {
43
45
  root: string;
44
46
  prefix?: string;
47
+ include?: readonly string[];
48
+ exclude?: readonly string[];
45
49
  }
46
50
 
51
+ /**
52
+ * The literal directory a glob starts from (`docs/**\/*.md` → `docs`, `""`
53
+ * when it matches from the root): its leading segments up to the first one
54
+ * with glob syntax, the file-name segment excluded.
55
+ */
56
+ const globBase = (pattern: string): string => {
57
+ const literal: string[] = [];
58
+ for (const segment of pattern.split("/").slice(0, -1)) {
59
+ if (isDynamicPattern(segment)) {
60
+ break;
61
+ }
62
+ if (segment !== "" && segment !== ".") {
63
+ literal.push(segment);
64
+ }
65
+ }
66
+ return literal.join("/");
67
+ };
68
+
69
+ /** Whether `dir` is `base`, or nested beneath or above it (`""` is the root). */
70
+ const onPathOf = (dir: string, base: string): boolean =>
71
+ dir === base ||
72
+ base === "" ||
73
+ dir === "" ||
74
+ dir.startsWith(`${base}/`) ||
75
+ base.startsWith(`${dir}/`);
76
+
77
+ /**
78
+ * Whether a meta file's directory can configure a sidebar group of the
79
+ * source: it lies on the path of some `include` glob's base — the folders
80
+ * that glob reads, or the ancestors whose groups hold them. Negated globs
81
+ * only narrow what a source reads, so they never qualify a folder.
82
+ */
83
+ const withinInclude = (
84
+ dir: string,
85
+ include: readonly string[] | undefined
86
+ ): boolean =>
87
+ include === undefined ||
88
+ include.some(
89
+ (pattern) => !pattern.startsWith("!") && onPathOf(dir, globBase(pattern))
90
+ );
91
+
92
+ /**
93
+ * The folder-meta files a filesystem source contributes: `patterns` (every
94
+ * meta module name, shared `meta.$.*` included, by default) under the source's
95
+ * root, minus its `exclude` globs, dependencies, and build output, and only in
96
+ * folders its `include` globs reach. `blume translate` finds the meta files it
97
+ * translates through this too, so it covers exactly the ones the scan reads.
98
+ */
99
+ export const findFolderMetaFiles = async (
100
+ source: FolderMetaSource,
101
+ patterns: readonly string[] = META_FILES
102
+ ): Promise<string[]> => {
103
+ const found = await glob([...patterns], {
104
+ absolute: true,
105
+ cwd: source.root,
106
+ // Never descend into dependencies or build output — relevant when the
107
+ // root is the project root (e.g. a `.`-rooted or all-staged project).
108
+ // The source's own `exclude` applies too: a `meta.ts` under an excluded
109
+ // folder (`src/lib/meta.ts` beside `exclude: ["src/**"]`) is application
110
+ // code, not folder meta, and importing it would fail.
111
+ ignore: [
112
+ ...(source.exclude ?? []),
113
+ "**/node_modules/**",
114
+ "**/.blume/**",
115
+ "**/dist/**",
116
+ ],
117
+ onlyFiles: true,
118
+ });
119
+ return found.filter((file) =>
120
+ withinInclude(relative(source.root, dirname(file)), source.include)
121
+ );
122
+ };
123
+
47
124
  /**
48
125
  * The folder-meta key for a directory. Mirrors the sidebar group path: the
49
126
  * source's route prefix (`docs`) followed by the directory relative to the
@@ -93,7 +170,12 @@ export const discoverFolderMeta = async (
93
170
  const list: FolderMetaSource[] = Array.isArray(sources)
94
171
  ? sources
95
172
  : [{ root: sources }];
96
- const localeDirs = new Set(options.localeDirs);
173
+ // Locale folders match case-insensitively, like content does (`pt-br/` for
174
+ // a configured `pt-BR`), and key under the configured casing, which is what
175
+ // navigation looks meta up by.
176
+ const localeDirs = new Map(
177
+ (options.localeDirs ?? []).map((code) => [code.toLowerCase(), code])
178
+ );
97
179
  const versionDirs = new Set(options.versionDirs);
98
180
 
99
181
  const load = createModuleLoader();
@@ -105,14 +187,7 @@ export const discoverFolderMeta = async (
105
187
  // `content.root` still contributes its folder meta.
106
188
  const perSource = await Promise.all(
107
189
  list.map(async (source) => {
108
- const files = await glob(META_FILES, {
109
- absolute: true,
110
- cwd: source.root,
111
- // Never descend into dependencies or build output — relevant when the
112
- // root is the project root (e.g. a `.`-rooted or all-staged project).
113
- ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
114
- onlyFiles: true,
115
- });
190
+ const files = await findFolderMetaFiles(source);
116
191
  const loaded = await Promise.all(
117
192
  files.map(
118
193
  async (
@@ -149,7 +224,9 @@ export const discoverFolderMeta = async (
149
224
  const version = head && versionDirs.has(head) ? head : "";
150
225
  const afterVersion = version ? tail : [head ?? "", ...tail];
151
226
  const [localeHead, ...localeTail] = afterVersion;
152
- const locale = localeHead && localeDirs.has(localeHead) ? localeHead : "";
227
+ const locale = localeHead
228
+ ? (localeDirs.get(localeHead.toLowerCase()) ?? "")
229
+ : "";
153
230
  const rest = (locale ? localeTail : afterVersion)
154
231
  .filter(Boolean)
155
232
  .join("/");
@@ -1,6 +1,8 @@
1
1
  import { isAssetIcon } from "../theme/icon-kind.ts";
2
2
  import { hasIcon } from "../theme/icons.ts";
3
3
  import { isInternalPath } from "./base-path.ts";
4
+ import { isPageRef, normalizeRef } from "./navigation.ts";
5
+ import type { SidebarItemConfig } from "./schema.ts";
4
6
  import type { Diagnostic, NavNode, Navigation, PageRecord } from "./types.ts";
5
7
 
6
8
  /**
@@ -137,7 +139,9 @@ export const validateNavTargets = (
137
139
  if (!isInternalPath(path) || path.startsWith("/#") || seen.has(path)) {
138
140
  continue;
139
141
  }
140
- if (!resolvesToPages(routes, path.split("#")[0] ?? path)) {
142
+ // A query or fragment (`/guides?tab=cli`) rides on the route; only the
143
+ // path names a page.
144
+ if (!resolvesToPages(routes, path.split(/[?#]/u)[0] ?? path)) {
141
145
  seen.add(path);
142
146
  diagnostics.push({
143
147
  code: "BLUME_NAV_MISSING_PAGE",
@@ -150,6 +154,57 @@ export const validateNavTargets = (
150
154
  return diagnostics;
151
155
  };
152
156
 
157
+ /**
158
+ * Warn about explicit `navigation.sidebar` items that can't render as written,
159
+ * instead of dropping them without a word: a page ref that matches no page
160
+ * (left out of the sidebar), a `root` that matches no page (its link leads
161
+ * nowhere), and an item with no page, `href`, `root`, or `items` (left out).
162
+ * `resolves` answers whether a normalized ref (`/guides`) names a page — in
163
+ * any locale — or a route served outside the content tree.
164
+ */
165
+ export const validateSidebarItems = (
166
+ items: SidebarItemConfig[],
167
+ resolves: (route: string) => boolean
168
+ ): Diagnostic[] => {
169
+ const diagnostics: Diagnostic[] = [];
170
+ const walk = (list: SidebarItemConfig[]): void => {
171
+ for (const item of list) {
172
+ if (isPageRef(item)) {
173
+ if (!resolves(normalizeRef(item))) {
174
+ diagnostics.push({
175
+ code: "BLUME_NAV_MISSING_PAGE",
176
+ message: `Sidebar entry "${item}" matches no page, so it's left out of the sidebar.`,
177
+ severity: "warning",
178
+ suggestion: "Fix the route, or add a page at it.",
179
+ });
180
+ }
181
+ continue;
182
+ }
183
+ if (item.root && !resolves(normalizeRef(item.root))) {
184
+ diagnostics.push({
185
+ code: "BLUME_NAV_MISSING_PAGE",
186
+ message: `Sidebar entry "${item.label}" has root "${item.root}", but no page matches it, so its link leads nowhere.`,
187
+ severity: "warning",
188
+ suggestion: "Fix the root, or add a page at that route.",
189
+ });
190
+ }
191
+ if (item.items) {
192
+ walk(item.items);
193
+ } else if (!(item.root || item.href)) {
194
+ diagnostics.push({
195
+ code: "BLUME_NAV_EMPTY_ITEM",
196
+ message: `Sidebar entry "${item.label}" has no page, href, root, or items, so it's left out of the sidebar.`,
197
+ severity: "warning",
198
+ suggestion:
199
+ "Give it an href or a root to link somewhere, or items to make it a group.",
200
+ });
201
+ }
202
+ }
203
+ };
204
+ walk(items);
205
+ return diagnostics;
206
+ };
207
+
153
208
  /** Warn about two nav items sharing a label at the same sidebar level. */
154
209
  const duplicateLabelDiagnostics = (navigation: Navigation): Diagnostic[] => {
155
210
  const diagnostics: Diagnostic[] = [];