blume 2.0.2 → 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 (293) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
  3. package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
  4. package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
  5. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
  6. package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
  7. package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
  8. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
  9. package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
  10. package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
  11. package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
  12. package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
  13. package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
  14. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  15. package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
  16. package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
  17. package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
  18. package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
  19. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  20. package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
  21. package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
  22. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
  23. package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
  24. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  25. package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
  26. package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
  27. package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
  28. package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
  29. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  30. package/dist/cli/chunk-e04dxsz1.js +39 -0
  31. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  32. package/dist/cli/{chunk-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
  33. package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
  34. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  35. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  36. package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
  37. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  38. package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
  39. package/dist/cli/chunk-j85vccga.js.map +36 -0
  40. package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
  41. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
  42. package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
  43. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
  44. package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
  45. package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
  46. package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
  47. package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
  48. package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
  49. package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
  50. package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
  51. package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
  52. package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
  53. package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
  54. package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
  55. package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
  56. package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
  57. package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
  58. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  59. package/dist/cli/index.js +20 -18
  60. package/dist/cli/index.js.map +3 -3
  61. package/dist/types/ai/link-headers.d.ts +8 -1
  62. package/dist/types/ai/openapi-components.d.ts +5 -2
  63. package/dist/types/ai/relative-links.d.ts +9 -7
  64. package/dist/types/ai/skills.d.ts +4 -1
  65. package/dist/types/ai/tar.d.ts +1 -3
  66. package/dist/types/analytics/index.d.ts +2 -0
  67. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  68. package/dist/types/analytics/schema.d.ts +14 -0
  69. package/dist/types/astro/integration.d.ts +3 -2
  70. package/dist/types/core/base-path.d.ts +21 -7
  71. package/dist/types/core/config-input.d.ts +9 -7
  72. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  73. package/dist/types/core/heading-markers.d.ts +5 -7
  74. package/dist/types/core/i18n.d.ts +2 -0
  75. package/dist/types/core/last-modified.d.ts +10 -0
  76. package/dist/types/core/meta.d.ts +8 -0
  77. package/dist/types/core/schema.d.ts +7 -0
  78. package/dist/types/core/sources/lower.d.ts +29 -16
  79. package/dist/types/core/sources/normalize.d.ts +13 -1
  80. package/dist/types/core/sources/watch.d.ts +12 -5
  81. package/dist/types/core/standard-schema.d.ts +5 -0
  82. package/dist/types/deploy/artifacts.d.ts +6 -4
  83. package/dist/types/deploy/headers.d.ts +5 -0
  84. package/dist/types/deploy/platforms/types.d.ts +8 -0
  85. package/dist/types/deploy/redirects.d.ts +24 -13
  86. package/dist/types/markdown/directives.d.ts +62 -0
  87. package/dist/types/markdown/features.d.ts +21 -0
  88. package/dist/types/markdown/mdast.d.ts +63 -0
  89. package/dist/types/openapi/asyncapi.d.ts +4 -2
  90. package/docs/02-deployment.mdx +4 -2
  91. package/docs/08-faq.mdx +1 -1
  92. package/docs/advanced/custom-pages.mdx +3 -3
  93. package/docs/cli/audit.mdx +17 -1
  94. package/docs/cli/evals.mdx +3 -3
  95. package/docs/cli/translate.mdx +3 -3
  96. package/docs/cli/version.mdx +1 -1
  97. package/docs/configuration/analytics.mdx +20 -1
  98. package/docs/configuration/customization.mdx +2 -0
  99. package/docs/configuration/search.mdx +1 -1
  100. package/docs/content/components.mdx +1 -1
  101. package/docs/content/frontmatter.mdx +1 -1
  102. package/docs/content/i18n.mdx +2 -0
  103. package/docs/content/index.mdx +4 -2
  104. package/docs/content/islands.mdx +1 -1
  105. package/docs/content/meta.mdx +3 -1
  106. package/docs/content/sources.mdx +1 -5
  107. package/docs/content/syntax.mdx +14 -0
  108. package/docs/content/versioning.mdx +1 -1
  109. package/docs/discoverability/agent-discovery.mdx +1 -1
  110. package/docs/discoverability/index.mdx +2 -1
  111. package/docs/discoverability/markdown.mdx +2 -2
  112. package/docs/discoverability/open-graph.mdx +5 -3
  113. package/docs/discoverability/rss.mdx +1 -1
  114. package/docs/references/asyncapi.mdx +1 -1
  115. package/docs/references/graphql.mdx +1 -1
  116. package/docs/references/openapi.mdx +5 -3
  117. package/package.json +1 -1
  118. package/skills/blume-migrate/references/fumadocs.md +1 -1
  119. package/skills/blume-migrate/references/nextra.md +1 -1
  120. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
  121. package/src/ai/agent-readability.ts +2 -2
  122. package/src/ai/ai-catalog.ts +6 -2
  123. package/src/ai/api/handlers.ts +2 -2
  124. package/src/ai/api/spec.ts +2 -2
  125. package/src/ai/api-catalog.ts +6 -2
  126. package/src/ai/changelog-markdown.ts +2 -2
  127. package/src/ai/link-headers.ts +14 -5
  128. package/src/ai/llms.ts +18 -7
  129. package/src/ai/mcp/discovery.ts +2 -2
  130. package/src/ai/mcp/query.ts +2 -2
  131. package/src/ai/mcp/server.ts +2 -2
  132. package/src/ai/openapi-components.ts +22 -6
  133. package/src/ai/relative-links.ts +42 -26
  134. package/src/ai/serializers.ts +2 -1
  135. package/src/ai/skills.ts +14 -1
  136. package/src/ai/tar.ts +139 -12
  137. package/src/analytics/head.ts +4 -0
  138. package/src/analytics/index.ts +5 -0
  139. package/src/analytics/one-dollar-stats.ts +86 -0
  140. package/src/analytics/schema.ts +2 -0
  141. package/src/astro/generate.ts +1 -1
  142. package/src/astro/include-hmr.ts +31 -12
  143. package/src/astro/include-refresh.ts +19 -8
  144. package/src/astro/integration.ts +108 -14
  145. package/src/astro/runtime-modules.ts +28 -13
  146. package/src/astro/templates.ts +81 -37
  147. package/src/audit/checks/links.ts +8 -1
  148. package/src/audit/graph.ts +3 -1
  149. package/src/audit/run.ts +12 -9
  150. package/src/audit/snapshot.ts +5 -0
  151. package/src/cli/args.ts +32 -0
  152. package/src/cli/commands/audit.ts +4 -1
  153. package/src/cli/commands/doctor.ts +3 -1
  154. package/src/cli/commands/eval.ts +19 -11
  155. package/src/cli/commands/translate.ts +9 -12
  156. package/src/cli/commands/validate.ts +3 -1
  157. package/src/cli/dev-lock.ts +157 -37
  158. package/src/cli/report-format.ts +11 -6
  159. package/src/components/colors.ts +19 -0
  160. package/src/components/content/Badge.astro +5 -3
  161. package/src/components/content/Component.astro +2 -2
  162. package/src/components/content/Tab.astro +0 -1
  163. package/src/components/content/Tabs.astro +4 -0
  164. package/src/components/content/badge-color.ts +5 -2
  165. package/src/components/content/base-href.ts +13 -36
  166. package/src/components/islands/assistant.tsx +21 -4
  167. package/src/components/islands/base-path.ts +47 -10
  168. package/src/components/islands/hooks.ts +35 -19
  169. package/src/components/islands/webmcp.ts +4 -2
  170. package/src/components/layout/Breadcrumbs.astro +5 -2
  171. package/src/components/layout/DiscoveryLinks.astro +9 -5
  172. package/src/components/layout/Header.astro +2 -2
  173. package/src/components/layout/LanguageSwitcher.astro +2 -2
  174. package/src/components/layout/NavSelector.astro +2 -2
  175. package/src/components/layout/NavTabMenu.astro +3 -3
  176. package/src/components/layout/NavTree.astro +16 -6
  177. package/src/components/layout/PageActions.astro +4 -3
  178. package/src/components/layout/PageLayout.astro +7 -6
  179. package/src/components/layout/Pagination.astro +3 -3
  180. package/src/components/layout/RootLayout.astro +5 -5
  181. package/src/components/layout/Search.astro +33 -13
  182. package/src/components/layout/VersionBanner.astro +2 -2
  183. package/src/components/layout/analytics-client.ts +12 -0
  184. package/src/components/layout/toc-active.ts +41 -0
  185. package/src/components/layout/toc-element.ts +8 -14
  186. package/src/components/openapi/ApiTagOperations.astro +2 -2
  187. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  188. package/src/components/openapi/GraphqlChip.astro +2 -2
  189. package/src/components/openapi/Operation.astro +2 -0
  190. package/src/components/openapi/Playground.astro +4 -4
  191. package/src/components/openapi/RequestPanel.astro +5 -2
  192. package/src/components/openapi/SchemaTable.astro +7 -0
  193. package/src/components/openapi/async.ts +38 -6
  194. package/src/components/openapi/helpers.ts +19 -9
  195. package/src/components/openapi/message-composer.ts +6 -1
  196. package/src/components/openapi/message.ts +17 -2
  197. package/src/components/openapi/operation-model.ts +81 -11
  198. package/src/components/openapi/panel.ts +4 -2
  199. package/src/components/openapi/param-style.ts +181 -0
  200. package/src/components/openapi/playground-client.ts +73 -16
  201. package/src/components/openapi/request.ts +190 -26
  202. package/src/components/openapi/schema-tree.ts +30 -24
  203. package/src/components/openapi/snippets.ts +85 -6
  204. package/src/components/openapi/ws-client.ts +18 -2
  205. package/src/core/base-path.ts +62 -16
  206. package/src/core/config-input.ts +9 -7
  207. package/src/core/diagnostics.ts +2 -0
  208. package/src/core/directive-diagnostics.ts +99 -0
  209. package/src/core/frontmatter.ts +21 -18
  210. package/src/core/heading-markers.ts +5 -18
  211. package/src/core/i18n.ts +31 -3
  212. package/src/core/last-modified.ts +25 -3
  213. package/src/core/locale-links.ts +5 -1
  214. package/src/core/meta.ts +33 -19
  215. package/src/core/navigation.ts +28 -8
  216. package/src/core/project-graph.ts +27 -5
  217. package/src/core/schema.ts +24 -4
  218. package/src/core/sources/contentful-rich-text.ts +27 -20
  219. package/src/core/sources/filesystem.ts +20 -2
  220. package/src/core/sources/github-releases.ts +25 -6
  221. package/src/core/sources/lexical.ts +23 -18
  222. package/src/core/sources/lower.ts +201 -34
  223. package/src/core/sources/mdx-remote.ts +51 -16
  224. package/src/core/sources/normalize.ts +325 -90
  225. package/src/core/sources/notion.ts +52 -25
  226. package/src/core/sources/obsidian.ts +23 -6
  227. package/src/core/sources/portable-text.ts +38 -21
  228. package/src/core/sources/strapi-blocks.ts +20 -16
  229. package/src/core/sources/watch.ts +20 -7
  230. package/src/core/standard-schema.ts +10 -6
  231. package/src/core/version-cut.ts +52 -15
  232. package/src/deploy/artifacts.ts +27 -6
  233. package/src/deploy/cloudflare-negotiation.ts +4 -12
  234. package/src/deploy/headers.ts +8 -4
  235. package/src/deploy/node-headers.ts +1 -1
  236. package/src/deploy/platforms/cloudflare.ts +12 -3
  237. package/src/deploy/platforms/netlify.ts +1 -0
  238. package/src/deploy/platforms/node.ts +1 -0
  239. package/src/deploy/platforms/static.ts +1 -0
  240. package/src/deploy/platforms/types.ts +8 -0
  241. package/src/deploy/platforms/vercel.ts +1 -0
  242. package/src/deploy/redirects.ts +54 -18
  243. package/src/deploy/robots.ts +2 -2
  244. package/src/deploy/rss.ts +4 -3
  245. package/src/deploy/sitemap.ts +7 -5
  246. package/src/markdown/base-links.ts +34 -36
  247. package/src/markdown/directives.ts +242 -36
  248. package/src/markdown/features.ts +17 -0
  249. package/src/markdown/index.ts +7 -6
  250. package/src/markdown/mdast.ts +5 -2
  251. package/src/markdown/relative-links.ts +12 -4
  252. package/src/og/card.ts +129 -5
  253. package/src/og/derive.ts +41 -31
  254. package/src/og/index.ts +1 -0
  255. package/src/openapi/asyncapi.ts +4 -2
  256. package/src/openapi/model.ts +22 -14
  257. package/src/openapi/proxy.ts +63 -10
  258. package/src/openapi/render-mdx.ts +41 -2
  259. package/src/registry/eject.ts +181 -24
  260. package/src/search/adapters/version-scope.ts +30 -0
  261. package/src/search/documents.ts +67 -26
  262. package/src/search/popular.ts +2 -1
  263. package/src/seo/jsonld.ts +7 -3
  264. package/src/translate/meta.ts +68 -24
  265. package/src/translate/run.ts +3 -3
  266. package/src/translate/validate.ts +4 -1
  267. package/src/translate/work-list.ts +56 -17
  268. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  269. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  270. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  271. package/dist/cli/chunk-esh98wmb.js +0 -23
  272. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  273. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  274. package/dist/cli/chunk-j85scx15.js.map +0 -10
  275. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  276. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  277. /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
  278. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
  279. /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
  280. /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
  281. /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
  282. /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
  283. /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
  284. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
  285. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
  286. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
  287. /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
  288. /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
  289. /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
  290. /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
  291. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
  292. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
  293. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
@@ -3,10 +3,13 @@ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
 
4
4
  import { join, relative } from "pathe";
5
5
 
6
+ import { OPENAPI_PATH } from "../ai/api/paths.ts";
7
+ import { buildApiSpec } from "../ai/api/spec.ts";
6
8
  import { buildAskData } from "../ai/ask-data.ts";
7
9
  import { resolveAskBackend } from "../ai/ask.ts";
8
10
  import { buildRawMarkdown } from "../ai/markdown.ts";
9
11
  import { buildMcpData } from "../ai/mcp/data.ts";
12
+ import type { McpData } from "../ai/mcp/data.ts";
10
13
  import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
11
14
  import { planComponentSlots } from "../astro/component-slots.ts";
12
15
  import {
@@ -31,6 +34,11 @@ import {
31
34
  import { discoverIslands } from "../astro/islands.ts";
32
35
  import { customOgRoutes, discoverPages, routeIsTaken } from "../astro/pages.ts";
33
36
  import {
37
+ apiNavigationTemplate,
38
+ apiNotFoundTemplate,
39
+ apiPagesIndexTemplate,
40
+ apiPageTemplate,
41
+ apiSearchTemplate,
34
42
  askComponentTemplate,
35
43
  askEndpointTemplate,
36
44
  astroConfigTemplate,
@@ -46,6 +54,8 @@ import {
46
54
  mcpEndpointTemplate,
47
55
  mcpPageFile,
48
56
  mixedbreadSearchEndpointTemplate,
57
+ notFoundJsonTemplate,
58
+ notFoundMarkdownTemplate,
49
59
  notFoundPageTemplate,
50
60
  ogEndpointTemplate,
51
61
  playgroundProxyTemplate,
@@ -68,6 +78,7 @@ import {
68
78
  } from "../core/sources/collection.ts";
69
79
  import type { ProjectContext } from "../core/types.ts";
70
80
  import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
81
+ import { resolveOgFonts } from "../og/derive.ts";
71
82
  import type { OpenApiData } from "../openapi/model.ts";
72
83
  import {
73
84
  hasScalarReferences,
@@ -80,6 +91,7 @@ import {
80
91
  examplesEntryTemplate,
81
92
  tailwindEntryTemplate,
82
93
  } from "../theme/entry.ts";
94
+ import { fontLocaleCodes } from "../theme/fonts.ts";
83
95
  import { buildThemeCss } from "../theme/palette.ts";
84
96
  import { rebaseSourceDirectives } from "../theme/sources.ts";
85
97
  import { twoslashCss } from "../theme/twoslash.ts";
@@ -238,21 +250,33 @@ const playgroundProxyFiles = (
238
250
  : [];
239
251
 
240
252
  /**
241
- * The MCP data snapshot, server endpoint, and `.well-known` discovery
242
- * documents, mirroring `writeMcpFiles` in generate.ts. Empty when the server
243
- * is disabled or its route is already owned by a page.
253
+ * The agent data snapshot (`blume:mcp-data`) behind the MCP server and the
254
+ * JSON docs API, mirroring `publishAgentData` in generate.ts: built when
255
+ * either is on, null when neither needs it.
244
256
  */
245
- const mcpFiles = async (
257
+ const ejectAgentData = async (
258
+ project: BlumeProject,
259
+ userPages: { pattern: string }[]
260
+ ): Promise<McpData | null> =>
261
+ hostsMcp(project, userPages) || project.config.agents.api
262
+ ? await buildMcpData(project)
263
+ : null;
264
+
265
+ /**
266
+ * The MCP server endpoint and `.well-known` discovery documents, mirroring
267
+ * `writeMcpFiles` in generate.ts. Empty when the server is disabled or its
268
+ * route is already owned by a page.
269
+ */
270
+ const mcpFiles = (
246
271
  project: BlumeProject,
247
272
  userPages: { pattern: string }[],
248
273
  srcDir: string,
249
- genDir: string
250
- ): Promise<{ content: string; path: string }[]> => {
251
- if (!hostsMcp(project, userPages)) {
274
+ data: McpData | null
275
+ ): { content: string; path: string }[] => {
276
+ if (!(data && hostsMcp(project, userPages))) {
252
277
  return [];
253
278
  }
254
279
  const { route } = project.config.agents.mcp;
255
- const data = await buildMcpData(project);
256
280
  const discoveryInput = {
257
281
  base: data.base,
258
282
  name: data.name,
@@ -261,10 +285,6 @@ const mcpFiles = async (
261
285
  version: data.version,
262
286
  };
263
287
  return [
264
- {
265
- content: `${JSON.stringify(data)}\n`,
266
- path: join(genDir, "mcp-data.json"),
267
- },
268
288
  {
269
289
  content: mcpEndpointTemplate(),
270
290
  path: join(srcDir, "pages", mcpPageFile(route)),
@@ -280,6 +300,87 @@ const mcpFiles = async (
280
300
  ];
281
301
  };
282
302
 
303
+ /**
304
+ * The JSON docs API, mirroring `planApi` and `writeApiFiles` in generate.ts:
305
+ * the prerendered page index, per-page documents, and navigation; on server
306
+ * output the search endpoint and, unless a user page or a content page already
307
+ * lives under `/api/`, the JSON 404 catch-all; and `/openapi.json`, unless a
308
+ * `public/openapi.json` or a page owns that route. The ejected `llms.txt`,
309
+ * homepage `Link` header, and 404 page advertise these routes, so the ejected
310
+ * app serves them. Empty when `agents.api` is off.
311
+ */
312
+ const apiFiles = (
313
+ project: BlumeProject,
314
+ userPages: { pattern: string }[],
315
+ root: string,
316
+ srcDir: string,
317
+ data: McpData | null
318
+ ): { content: string; path: string }[] => {
319
+ const { config } = project;
320
+ if (!(data && config.agents.api)) {
321
+ return [];
322
+ }
323
+ const server = config.deployment.options.output === "server";
324
+ const apiDir = join(srcDir, "pages", "api");
325
+ const files = [
326
+ {
327
+ content: apiPagesIndexTemplate(),
328
+ path: join(apiDir, "docs", "pages.json.ts"),
329
+ },
330
+ {
331
+ content: apiPageTemplate(),
332
+ path: join(apiDir, "docs", "pages", "[...route].json.ts"),
333
+ },
334
+ {
335
+ content: apiNavigationTemplate(),
336
+ path: join(apiDir, "docs", "navigation.json.ts"),
337
+ },
338
+ ];
339
+ if (server) {
340
+ files.push({
341
+ content: apiSearchTemplate(),
342
+ path: join(apiDir, "docs", "search.ts"),
343
+ });
344
+ }
345
+ const apiOwned =
346
+ userPages.some((page) => page.pattern.startsWith("/api/[")) ||
347
+ project.graph.pages.some(
348
+ (page) => page.route === "/api" || page.route.startsWith("/api/")
349
+ );
350
+ if (server && !apiOwned) {
351
+ files.push({
352
+ content: apiNotFoundTemplate({ base: data.base, site: data.site }),
353
+ path: join(apiDir, "[...path].ts"),
354
+ });
355
+ }
356
+ if (
357
+ !(
358
+ routeIsTaken(userPages, project.graph.pages, OPENAPI_PATH) ||
359
+ existsSync(join(root, "public", "openapi.json"))
360
+ )
361
+ ) {
362
+ files.push({
363
+ content: staticJsonEndpointTemplate(
364
+ buildApiSpec({
365
+ agentReadability: config.agents.agentReadability,
366
+ base: data.base,
367
+ description: config.description,
368
+ llmsTxt: config.agents.llmsTxt.enabled,
369
+ mcpRoute: hostsMcp(project, userPages)
370
+ ? config.agents.mcp.route
371
+ : null,
372
+ name: config.title,
373
+ search: server,
374
+ site: data.site,
375
+ version: data.version,
376
+ })
377
+ ),
378
+ path: join(srcDir, "pages", "openapi.json.ts"),
379
+ });
380
+ }
381
+ return files;
382
+ };
383
+
283
384
  /**
284
385
  * The `/changelog` index page, mirroring `shouldGenerateChangelog` in
285
386
  * generate.ts: emitted when `type: changelog` entries or a release-backed
@@ -346,6 +447,25 @@ const contentAssetFiles = async (
346
447
  ];
347
448
  };
348
449
 
450
+ /**
451
+ * The partial → including-pages map with every path relative to the project
452
+ * root, like every other path in the ejected app, so it holds in any checkout.
453
+ * Its readers (`includeHmrPlugin`, `withIncludeRefresh`) resolve each path
454
+ * against the app's root, which after eject is the project itself.
455
+ */
456
+ const portableIncludeGraph = (
457
+ project: BlumeProject,
458
+ root: string
459
+ ): Record<string, string[]> =>
460
+ Object.fromEntries(
461
+ Object.entries(buildIncludeGraph(project.graph.pages)).map(
462
+ ([partial, includers]) => [
463
+ toPosix(relative(root, partial)),
464
+ includers.map((page) => toPosix(relative(root, page))),
465
+ ]
466
+ )
467
+ );
468
+
349
469
  /** Contents of the configured `examples.css`, or `""` when unset/absent. */
350
470
  /**
351
471
  * Read a user stylesheet that eject inlines into a generated entry under
@@ -655,7 +775,7 @@ export const eject = async (
655
775
  // hot update's read would silently no-op and partial edits would serve
656
776
  // stale pages. A snapshot like the rest of `src/generated`: the ejected
657
777
  // app owns (and may regenerate or prune) it.
658
- content: `${JSON.stringify(buildIncludeGraph(project.graph.pages))}\n`,
778
+ content: `${JSON.stringify(portableIncludeGraph(project, root))}\n`,
659
779
  path: join(genDir, "includes.json"),
660
780
  },
661
781
  {
@@ -684,32 +804,69 @@ export const eject = async (
684
804
  });
685
805
 
686
806
  if (config.seo.og.enabled) {
807
+ // The default script fallbacks ride along (Google family names, nothing
808
+ // machine-specific), so a non-Latin card still renders after eject. An
809
+ // explicit seo.og.fonts replaces them, and isn't carried over.
810
+ const { fallbacks, families } = resolveOgFonts(
811
+ {
812
+ locales: fontLocaleCodes(config.i18n),
813
+ ogFonts: config.seo.og.fonts,
814
+ themeFonts: config.theme.fonts,
815
+ themeFontsConfigured: false,
816
+ },
817
+ root
818
+ );
687
819
  files.push({
688
820
  content: ogEndpointTemplate(
689
821
  customOgRoutes(pages, config.title, config.seo.og.titles),
690
- { pageDescriptions: config.seo.og.description !== false },
822
+ {
823
+ fallbacks,
824
+ families,
825
+ pageDescriptions: config.seo.og.description !== false,
826
+ },
691
827
  changelog.length > 0
692
828
  ),
693
829
  path: join(srcDir, "pages", "og", "[...slug].png.ts"),
694
830
  });
695
831
  }
696
832
 
697
- // The hosted MCP server and the playground's built-in proxy, mirrored from
698
- // the generated runtime (`[]` when each is off).
833
+ // The hosted MCP server, the JSON docs API, and the playground's built-in
834
+ // proxy, mirrored from the generated runtime (`[]` when each is off). The
835
+ // server and the API share one agent data snapshot.
836
+ const agentData = await ejectAgentData(project, pages);
837
+ if (agentData) {
838
+ files.push({
839
+ content: `${JSON.stringify(agentData)}\n`,
840
+ path: join(genDir, "mcp-data.json"),
841
+ });
842
+ }
699
843
  files.push(
700
- ...(await mcpFiles(project, pages, srcDir, genDir)),
844
+ ...mcpFiles(project, pages, srcDir, agentData),
845
+ ...apiFiles(project, pages, root, srcDir, agentData),
701
846
  ...changelog,
702
847
  ...playgroundProxyFiles(config, openApiData, srcDir)
703
848
  );
704
849
 
705
- // Default 404 page, unless the project already owns `/404` (a custom
706
- // `pages/404.astro` or a `404.md` content page). The ejected project owns the
707
- // file afterwards and can edit or remove it.
850
+ // Default 404 page and its Markdown (`/404.md`) and JSON (`/404.json`)
851
+ // twins, mirroring `writeNotFoundPage` in generate.ts: all three are skipped
852
+ // when the project already owns `/404` (a custom `pages/404.astro` or a
853
+ // `404.md` content page). The ejected project owns the files afterwards and
854
+ // can edit or remove them.
708
855
  if (!routeIsTaken(pages, project.graph.pages, "/404")) {
709
- files.push({
710
- content: notFoundPageTemplate(),
711
- path: join(srcDir, "pages", "404.astro"),
712
- });
856
+ files.push(
857
+ {
858
+ content: notFoundPageTemplate(),
859
+ path: join(srcDir, "pages", "404.astro"),
860
+ },
861
+ {
862
+ content: notFoundMarkdownTemplate(),
863
+ path: join(srcDir, "pages", "404.md.ts"),
864
+ },
865
+ {
866
+ content: notFoundJsonTemplate(),
867
+ path: join(srcDir, "pages", "404.json.ts"),
868
+ }
869
+ );
713
870
  }
714
871
 
715
872
  // The client-feature loaders behind the `blume:features` alias, and the
@@ -0,0 +1,30 @@
1
+ import type { SearchAdapterKind } from "./registry.ts";
2
+
3
+ /**
4
+ * Whether each adapter's client can filter results to one docs version. The
5
+ * source-built indexes (Orama, FlexSearch) store every document's `version`,
6
+ * and the Algolia and Typesense syncs upload it as a facet their clients
7
+ * filter on. Pagefind indexes the built HTML with no version metadata, the
8
+ * Orama Cloud sync uploads no `version`, and the Mixedbread endpoint takes no
9
+ * filter, so those always search every version. Keyed by every kind so a new
10
+ * adapter has to decide.
11
+ */
12
+ const SCOPES_VERSIONS = {
13
+ algolia: true,
14
+ flexsearch: true,
15
+ mixedbread: false,
16
+ none: false,
17
+ orama: true,
18
+ "orama-cloud": false,
19
+ pagefind: false,
20
+ typesense: true,
21
+ } satisfies Record<SearchAdapterKind, boolean>;
22
+
23
+ /**
24
+ * Whether the configured search adapter scopes results to the viewed docs
25
+ * version. The search dialog offers its "All versions" toggle only when it
26
+ * does: on the others the toggle would change nothing, and every query
27
+ * already spans all versions.
28
+ */
29
+ export const scopesVersions = (kind: SearchAdapterKind): boolean =>
30
+ SCOPES_VERSIONS[kind];
@@ -23,7 +23,12 @@ import { HTML_COMMENT } from "../core/sources/normalize.ts";
23
23
  import { readExpandedEntryText } from "../core/sources/read.ts";
24
24
  import type { NavNode, PageRecord } from "../core/types.ts";
25
25
  import { parseCodeTitle } from "../markdown/code-title.ts";
26
- import { MARKDOWN_FEATURES, MDX_FEATURES } from "../markdown/features.ts";
26
+ import { directiveSource } from "../markdown/directives.ts";
27
+ import type { DirectiveNode } from "../markdown/directives.ts";
28
+ import {
29
+ MARKDOWN_BODY_FEATURES,
30
+ MDX_BODY_FEATURES,
31
+ } from "../markdown/features.ts";
27
32
  import { pageFacets } from "./facets.ts";
28
33
 
29
34
  /** A document indexed by the client-side search providers (Orama, FlexSearch). */
@@ -94,7 +99,6 @@ const INLINE_PARENTS = new Set([
94
99
  "strong",
95
100
  "subscript",
96
101
  "superscript",
97
- "textDirective",
98
102
  ]);
99
103
 
100
104
  // Nodes that never render as prose: image alt text was never indexed, MDX
@@ -120,9 +124,9 @@ interface Walk {
120
124
 
121
125
  // Front matter is already off (core/frontmatter.ts) by the time a body gets
122
126
  // here, so a body that opens with a `---` divider must read as a thematic
123
- // break rather than a second front matter block.
124
- const MD_PARSE = { features: { ...MARKDOWN_FEATURES, frontmatter: false } };
125
- const MDX_PARSE = { features: { ...MDX_FEATURES, frontmatter: false } };
127
+ // break rather than a second front matter block — as the renderer reads it.
128
+ const MD_PARSE = { features: MARKDOWN_BODY_FEATURES };
129
+ const MDX_PARSE = { features: MDX_BODY_FEATURES };
126
130
 
127
131
  const parseMdx = (markdown: string): Nodes | undefined => {
128
132
  try {
@@ -170,6 +174,25 @@ const collectCode = (
170
174
  out.push(node.value, " ");
171
175
  };
172
176
 
177
+ /**
178
+ * A text or leaf directive as the page shows it. Blume renders neither (see
179
+ * `markdown/directives.ts`), and prose is full of text that parses as one —
180
+ * `16:9`, `og:image` — so the page shows the source the author wrote, and the
181
+ * index reads the same text rather than dropping the `:name` part.
182
+ */
183
+ const directiveText = (
184
+ node: Extract<Nodes, { type: "leafDirective" | "textDirective" }>,
185
+ walk: Walk
186
+ ): string =>
187
+ directiveSource(
188
+ // SAFETY: DirectiveNode is a structural subset of Satteri's directive
189
+ // nodes — a `name`, optional attributes and children, and a position.
190
+ node as DirectiveNode,
191
+ node.type === "textDirective" ? ":" : "::",
192
+ // The walked tree was parsed from this text, so its offsets index into it.
193
+ walk.downlevel.source
194
+ );
195
+
173
196
  // Block boundaries separate words; so does an empty inline element (`<br />`,
174
197
  // a self-closing icon), which otherwise fuses its neighbors.
175
198
  const separatesWords = (node: Extract<Nodes, { children: unknown }>): boolean =>
@@ -223,28 +246,17 @@ const collectText = (node: Nodes, out: string[], walk: Walk): void => {
223
246
  out.push(" ");
224
247
  return;
225
248
  }
226
- // Trailing heading markers (`[#custom-id]`, `{#custom-id}`, `[!toc]`,
227
- // `[toc]`) are anchor
228
-
229
- // metadata, not prose — strip them so they never pollute the index. Only
230
- // a marker that ends the heading's final plain-text child counts,
231
- // mirroring the renderer: a heading ending in inline code or an image
232
- // keeps its bracketed text on the page (so it stays searchable), and a
233
- // heading that is nothing but markers renders them literally.
249
+ case "textDirective": {
250
+ out.push(directiveText(node, walk));
251
+ return;
252
+ }
253
+ case "leafDirective": {
254
+ out.push(directiveText(node, walk), " ");
255
+ return;
256
+ }
234
257
  case "heading": {
235
- const last = node.children.at(-1);
236
- const trailing = last?.type === "text" ? last : undefined;
237
- const inner: string[] = [];
238
- const kept = trailing ? node.children.slice(0, -1) : node.children;
239
- for (const child of kept) {
240
- collectText(child, inner, walk);
241
- }
242
- if (trailing) {
243
- const stripped = parseHeadingMarkers(trailing.value).text;
244
- const literal = stripped === "" && node.children.length === 1;
245
- inner.push(literal ? trailing.value : stripped);
246
- }
247
- out.push(inner.join("").trimEnd(), " ");
258
+ // oxlint-disable-next-line no-use-before-define -- mutual recursion: a heading's children are walked like any others
259
+ collectHeading(node, out, walk);
248
260
  return;
249
261
  }
250
262
  default: {
@@ -265,6 +277,35 @@ const collectText = (node: Nodes, out: string[], walk: Walk): void => {
265
277
  }
266
278
  };
267
279
 
280
+ /**
281
+ * Fold a heading into the accumulator. Trailing heading markers
282
+ * (`[#custom-id]`, `{#custom-id}`, `[!toc]`, `[toc]`) are anchor metadata,
283
+ * not prose — strip them so they never pollute the index. Only a marker that
284
+ * ends the heading's final plain-text child counts, mirroring the renderer: a
285
+ * heading ending in inline code or an image keeps its bracketed text on the
286
+ * page (so it stays searchable), and a heading that is nothing but markers
287
+ * renders them literally.
288
+ */
289
+ const collectHeading = (
290
+ node: Extract<Nodes, { type: "heading" }>,
291
+ out: string[],
292
+ walk: Walk
293
+ ): void => {
294
+ const last = node.children.at(-1);
295
+ const trailing = last?.type === "text" ? last : undefined;
296
+ const inner: string[] = [];
297
+ const kept = trailing ? node.children.slice(0, -1) : node.children;
298
+ for (const child of kept) {
299
+ collectText(child, inner, walk);
300
+ }
301
+ if (trailing) {
302
+ const stripped = parseHeadingMarkers(trailing.value).text;
303
+ const literal = stripped === "" && node.children.length === 1;
304
+ inner.push(literal ? trailing.value : stripped);
305
+ }
306
+ out.push(inner.join("").trimEnd(), " ");
307
+ };
308
+
268
309
  /**
269
310
  * Reduce Markdown/MDX to plain, searchable text: parse (GFM included) and walk
270
311
  * the tree instead of regex-stripping the source, so reference-style links,
@@ -17,7 +17,8 @@ export interface SearchPopularPage {
17
17
  * `page.route`). `withBasePath` is idempotent and skips external URLs.
18
18
  *
19
19
  * `deployment.base` is deliberately *not* applied: routes stay deploy-base-less
20
- * so `createLinkRow` can prefix it at click time, same as sidebar-derived pages.
20
+ * so `Search.astro` can add it when it renders the entry, same as
21
+ * sidebar-derived pages.
21
22
  *
22
23
  * `icon` stays a name here (like `navigation.featured`): the icon set is a
23
24
  * server-only module, so `Search.astro` resolves it to inline SVG at render.
package/src/seo/jsonld.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Crumb } from "../components/layout/nav-utils.ts";
2
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
2
+ import {
3
+ mountBasePath,
4
+ normalizeBasePath,
5
+ withBasePath,
6
+ } from "../core/base-path.ts";
3
7
 
4
8
  /** A date-ish value carried through frontmatter (string, YAML Date, or unset). */
5
9
  type DateInput = string | Date | null;
@@ -301,7 +305,7 @@ const breadcrumbNode = (
301
305
  "@type": "BreadcrumbList",
302
306
  itemListElement: linked.map((crumb, index) => ({
303
307
  "@type": "ListItem",
304
- item: absolute(base, withBasePath(deployBase, crumb.route)),
308
+ item: absolute(base, mountBasePath(deployBase, crumb.route)),
305
309
  name: crumb.label,
306
310
  position: index + 1,
307
311
  })),
@@ -323,7 +327,7 @@ export const buildStructuredData = (
323
327
  // Routes carry `basePath`; a `deployment.base` subdirectory is layered on top
324
328
  // so JSON-LD URLs match the served location.
325
329
  const deployBase = normalizeBasePath(input.base);
326
- const pageUrl = absolute(base, withBasePath(deployBase, input.route));
330
+ const pageUrl = absolute(base, mountBasePath(deployBase, input.route));
327
331
  const rootUrl = absolute(base, deployBase);
328
332
  const graph: JsonLdNode[] = [];
329
333
 
@@ -1,12 +1,14 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { readFile } from "node:fs/promises";
2
3
 
3
4
  import { dirname, join, relative } from "pathe";
4
- import { glob } from "tinyglobby";
5
5
 
6
+ import { localeTargetPath } from "../core/i18n.ts";
6
7
  import { createModuleLoader } from "../core/load-module.ts";
8
+ import { findFolderMetaFiles } from "../core/meta.ts";
7
9
  import type { BlumeProject } from "../core/project-graph.ts";
8
10
  import { folderMetaSchema } from "../core/schema.ts";
9
- import type { FolderMeta } from "../core/schema.ts";
11
+ import type { FolderMeta, ResolvedI18nConfig } from "../core/schema.ts";
10
12
  import type { Diagnostic } from "../core/types.ts";
11
13
 
12
14
  /**
@@ -37,7 +39,10 @@ export interface TranslatableMeta {
37
39
  title: string;
38
40
  }
39
41
 
40
- const META_FILES = ["**/meta.ts", "**/meta.js", "**/meta.mjs"];
42
+ /** A folder's meta module names, in the order a lookup prefers them. */
43
+ const META_NAMES = ["meta.ts", "meta.js", "meta.mjs"];
44
+
45
+ const META_FILES = META_NAMES.map((name) => `**/${name}`);
41
46
 
42
47
  /**
43
48
  * Whether a loaded meta module default-exports a factory function. Generic so
@@ -48,21 +53,44 @@ const isFactoryModule = <T>(
48
53
  ): value is T & ((...args: never[]) => FolderMeta) =>
49
54
  typeof value === "function";
50
55
 
51
- /** Where a locale's generated meta module lives (always written as `meta.ts`). */
56
+ /**
57
+ * Where a locale's meta module lives. The locale folder is resolved the way
58
+ * pages resolve it (`localeTargetPath`): `folders` are the content root's
59
+ * existing top-level folders, and one naming the locale in another casing
60
+ * (`pt-br/` for a configured `pt-BR`) is the locale's folder already. In that
61
+ * folder, a `meta.js`/`meta.mjs` that already exists is the target — the
62
+ * generated module is plain JavaScript, and writing a `meta.ts` beside it
63
+ * would leave the folder with two meta files. Otherwise it's a new `meta.ts`.
64
+ */
52
65
  export const metaTargetPath = (
53
66
  meta: TranslatableMeta,
54
- locale: string
55
- ): string => join(meta.contentRoot, locale, meta.dir, "meta.ts");
67
+ locale: string,
68
+ i18n: ResolvedI18nConfig,
69
+ folders: readonly string[]
70
+ ): string => {
71
+ const dir = join(
72
+ meta.contentRoot,
73
+ dirname(
74
+ localeTargetPath(join(meta.dir, "meta.ts"), ".ts", locale, i18n, folders)
75
+ )
76
+ );
77
+ const existing = META_NAMES.find((name) => existsSync(join(dir, name)));
78
+ return join(dir, existing ?? "meta.ts");
79
+ };
56
80
 
57
81
  /**
58
82
  * Discover the default-locale `meta.{ts,js,mjs}` files whose titles a
59
83
  * translation run covers. Skips (in order): non-`dir` i18n projects entirely,
60
- * files inside a configured locale directory (those ARE translations), files
61
- * inside an archived-version snapshot (frozen, with their own translations —
62
- * the same folders the navigation's meta lookup hoists as versions),
63
- * factory-form modules (a warning — the generator can't re-emit a function),
64
- * modules that fail meta validation (the scan already errors on those), and
65
- * modules with no `title` (nothing to translate).
84
+ * files the scan's own folder-meta discovery never reads either — those
85
+ * matching the source's `exclude` globs (application code) or in folders its
86
+ * `include` globs don't reach (see `findFolderMetaFiles`) — files inside a
87
+ * configured locale directory (those ARE translations), files inside an
88
+ * archived-version snapshot (frozen, with their own translations — the same
89
+ * folders the navigation's meta lookup hoists as versions), modules that fail
90
+ * to load (the scan already errors on those), factory-form modules (a warning
91
+ * — the generator can't re-emit a function), modules that fail meta
92
+ * validation (the scan already errors on those too), and modules with no
93
+ * `title` (nothing to translate).
66
94
  */
67
95
  export const discoverTranslatableMeta = async (
68
96
  project: BlumeProject
@@ -85,16 +113,26 @@ export const discoverTranslatableMeta = async (
85
113
  const diagnostics: Diagnostic[] = [];
86
114
 
87
115
  const roots = project.sources.flatMap((source) =>
88
- source.staged || !source.contentRoot ? [] : [source.contentRoot]
116
+ source.staged || !source.contentRoot
117
+ ? []
118
+ : [
119
+ {
120
+ contentRoot: source.contentRoot,
121
+ exclude: source.exclude,
122
+ include: source.include,
123
+ },
124
+ ]
89
125
  );
90
- for (const contentRoot of roots) {
126
+ for (const { contentRoot, exclude, include } of roots) {
127
+ // The scan's own file set (`discoverFolderMeta`): a `meta.ts` under an
128
+ // excluded folder (`src/lib/meta.ts` beside `exclude: ["src/**"]`) is
129
+ // application code, and one in a folder no `include` glob reaches
130
+ // configures no group the build renders.
91
131
  // oxlint-disable-next-line no-await-in-loop -- a project has O(1) sources
92
- const files = await glob(META_FILES, {
93
- absolute: true,
94
- cwd: contentRoot,
95
- ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
96
- onlyFiles: true,
97
- });
132
+ const files = await findFolderMetaFiles(
133
+ { exclude, include, root: contentRoot },
134
+ META_FILES
135
+ );
98
136
  for (const file of files.toSorted()) {
99
137
  const dir = relative(contentRoot, dirname(file));
100
138
  const [head] = dir.split("/");
@@ -104,11 +142,17 @@ export const discoverTranslatableMeta = async (
104
142
  ) {
105
143
  continue;
106
144
  }
145
+ let mod: Awaited<ReturnType<typeof load>>;
146
+ try {
147
+ // oxlint-disable-next-line no-await-in-loop -- sequential, ordered discovery
148
+ mod = await load(file);
149
+ } catch {
150
+ // Folder meta that fails to load is the scan's BLUME_META_LOAD_FAILED
151
+ // error, not a crash here; either way there's no title to read.
152
+ continue;
153
+ }
107
154
  // oxlint-disable-next-line no-await-in-loop -- sequential, ordered discovery
108
- const [raw, mod] = await Promise.all([
109
- readFile(file, "utf-8"),
110
- load(file),
111
- ]);
155
+ const raw = await readFile(file, "utf-8");
112
156
  if (isFactoryModule(mod)) {
113
157
  diagnostics.push({
114
158
  code: "BLUME_TRANSLATE_META_FACTORY",
@@ -146,9 +146,9 @@ const runPageItem = async (
146
146
  // SAFETY: `targets` maps every configured locale, and work items only carry
147
147
  // configured locale codes.
148
148
  const target = context.targets.get(item.locale) as LocaleConfig;
149
- // A hand-authored translation can live at a non-canonical name (see
150
- // WorkStatus); the disk probe finds only canonical targets, and a miss just
151
- // means the prompt goes out without a style precedent.
149
+ // A stale item's target is the existing translation's own file, even at a
150
+ // non-canonical name (see `PageWorkItem.targetPath`), so it's the style
151
+ // precedent and the file rewritten; a missing item has nothing to read.
152
152
  const previousTranslation = existsSync(item.targetPath)
153
153
  ? await readFile(item.targetPath, "utf-8")
154
154
  : undefined;