blume 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +110 -0
  3. package/README.md +2 -2
  4. package/dist/cli/{chunk-f2972sbt.js → chunk-00gs3wqs.js} +1 -1
  5. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  6. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  7. package/dist/cli/{chunk-pat2zzwc.js → chunk-3yce002v.js} +21 -8
  8. package/dist/cli/{chunk-pat2zzwc.js.map → chunk-3yce002v.js.map} +4 -4
  9. package/dist/cli/{chunk-d1tadaw7.js → chunk-4e9b9ra6.js} +17 -7
  10. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  11. package/dist/cli/{chunk-zg2gtj10.js → chunk-5r8g91qn.js} +352 -676
  12. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  13. package/dist/cli/{chunk-11j0384y.js → chunk-6dtt0zfn.js} +14 -14
  14. package/dist/cli/{chunk-11j0384y.js.map → chunk-6dtt0zfn.js.map} +3 -3
  15. package/dist/cli/{chunk-bw22s759.js → chunk-6k4ftwze.js} +38 -19
  16. package/dist/cli/{chunk-bw22s759.js.map → chunk-6k4ftwze.js.map} +4 -4
  17. package/dist/cli/{chunk-sqn5t4q0.js → chunk-7mbqtmgb.js} +16 -7
  18. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  19. package/dist/cli/{chunk-79njf86q.js → chunk-7vtckvaw.js} +21 -19
  20. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  21. package/dist/cli/{chunk-fh5hj5jt.js → chunk-8g8ytmgx.js} +17 -18
  22. package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-8g8ytmgx.js.map} +2 -2
  23. package/dist/cli/{chunk-1w8dp3qb.js → chunk-91ws1n6j.js} +18 -15
  24. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  25. package/dist/cli/{chunk-7ez8ny0t.js → chunk-bbnwccaz.js} +2 -2
  26. package/dist/cli/{chunk-5a2z0198.js → chunk-bfwp9vp6.js} +18 -10
  27. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  28. package/dist/cli/{chunk-ernrthtr.js → chunk-d1v5rhy0.js} +14 -15
  29. package/dist/cli/{chunk-ernrthtr.js.map → chunk-d1v5rhy0.js.map} +2 -2
  30. package/dist/cli/{chunk-a9kptbw5.js → chunk-ddndchfr.js} +21 -6
  31. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  32. package/dist/cli/{chunk-zxccj738.js → chunk-esh98wmb.js} +1 -1
  33. package/dist/cli/{chunk-88cpgt6h.js → chunk-fsmrqk8a.js} +1 -1
  34. package/dist/cli/{chunk-b5aj94ah.js → chunk-g698a744.js} +5 -5
  35. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  36. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  37. package/dist/cli/{chunk-41za066z.js → chunk-h2ez8dzb.js} +4 -4
  38. package/dist/cli/{chunk-by2290sx.js → chunk-h7k3nq3v.js} +2 -2
  39. package/dist/cli/{chunk-j8mw0za6.js → chunk-hqp2ajnh.js} +252 -123
  40. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  41. package/dist/cli/{chunk-mnqj32sj.js → chunk-hr8ne106.js} +119 -50
  42. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  43. package/dist/cli/{chunk-mwt1k8n7.js → chunk-j85scx15.js} +74 -30
  44. package/dist/cli/chunk-j85scx15.js.map +10 -0
  45. package/dist/cli/{chunk-z01ze5c1.js → chunk-k7pj68a8.js} +63 -22
  46. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  47. package/dist/cli/{chunk-2q1dwty4.js → chunk-mqc662a6.js} +8 -8
  48. package/dist/cli/{chunk-2q1dwty4.js.map → chunk-mqc662a6.js.map} +4 -4
  49. package/dist/cli/{chunk-y3e45rc8.js → chunk-n1yg3tj3.js} +4 -4
  50. package/dist/cli/{chunk-bctazmbk.js → chunk-nfcyttvj.js} +17 -6
  51. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  52. package/dist/cli/{chunk-6k8vp3ta.js → chunk-pbg5a4s3.js} +60 -28
  53. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  54. package/dist/cli/{chunk-d80hr03s.js → chunk-ppzjqwx2.js} +24 -17
  55. package/dist/cli/{chunk-d80hr03s.js.map → chunk-ppzjqwx2.js.map} +4 -4
  56. package/dist/cli/{chunk-beat36xx.js → chunk-qkb5a8sa.js} +25 -10
  57. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  58. package/dist/cli/{chunk-bnbmcwfb.js → chunk-sqw4ekg1.js} +5 -5
  59. package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-sqw4ekg1.js.map} +3 -3
  60. package/dist/cli/{chunk-xaz13gwg.js → chunk-v6ya5kcb.js} +3268 -1150
  61. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  62. package/dist/cli/{chunk-tzne8qfq.js → chunk-w4bxdvsa.js} +18 -15
  63. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  64. package/dist/cli/{chunk-f7t03s3g.js → chunk-wdrt2k2v.js} +2 -2
  65. package/dist/cli/{chunk-z1f5arsg.js → chunk-xh43dwgw.js} +170 -55
  66. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  67. package/dist/cli/{chunk-pnnvybbk.js → chunk-zp79m0ts.js} +5 -5
  68. package/dist/cli/{chunk-pnnvybbk.js.map → chunk-zp79m0ts.js.map} +2 -2
  69. package/dist/cli/index.js +160 -35
  70. package/dist/cli/index.js.map +4 -4
  71. package/dist/types/ai/agent-readability.d.ts +1 -1
  72. package/dist/types/ai/agent-surface.d.ts +32 -0
  73. package/dist/types/ai/api/paths.d.ts +1 -1
  74. package/dist/types/ai/api-catalog.d.ts +7 -1
  75. package/dist/types/ai/ask-context.d.ts +14 -7
  76. package/dist/types/ai/ask.d.ts +43 -43
  77. package/dist/types/ai/component-markdown.d.ts +4 -4
  78. package/dist/types/ai/index.d.ts +3 -3
  79. package/dist/types/ai/openapi-components.d.ts +1 -1
  80. package/dist/types/ai/relative-links.d.ts +9 -4
  81. package/dist/types/ai/serializers.d.ts +1 -1
  82. package/dist/types/ai/static-expression.d.ts +28 -0
  83. package/dist/types/ai/visibility.d.ts +1 -1
  84. package/dist/types/analytics/databuddy.d.ts +43 -0
  85. package/dist/types/analytics/index.d.ts +2 -0
  86. package/dist/types/analytics/schema.d.ts +14 -0
  87. package/dist/types/cli/env.d.ts +5 -0
  88. package/dist/types/cli/init/scaffold.d.ts +19 -3
  89. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  90. package/dist/types/core/base-path.d.ts +9 -0
  91. package/dist/types/core/config-input.d.ts +21 -21
  92. package/dist/types/core/config.d.ts +5 -5
  93. package/dist/types/core/data.d.ts +3 -3
  94. package/dist/types/core/graph.d.ts +2 -0
  95. package/dist/types/core/i18n-ui.d.ts +37 -8
  96. package/dist/types/core/i18n.d.ts +7 -1
  97. package/dist/types/core/links.d.ts +3 -1
  98. package/dist/types/core/load-module.d.ts +10 -0
  99. package/dist/types/core/locale-links.d.ts +12 -2
  100. package/dist/types/core/meta.d.ts +5 -1
  101. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  102. package/dist/types/core/navigation.d.ts +38 -0
  103. package/dist/types/core/ordering-prefix.d.ts +4 -0
  104. package/dist/types/core/safe-links.d.ts +3 -1
  105. package/dist/types/core/schema.d.ts +57 -18
  106. package/dist/types/core/sources/github-releases.d.ts +5 -0
  107. package/dist/types/core/sources/lower.d.ts +14 -2
  108. package/dist/types/core/sources/normalize.d.ts +10 -1
  109. package/dist/types/core/sources/remote.d.ts +11 -1
  110. package/dist/types/core/sources/resolve.d.ts +12 -0
  111. package/dist/types/core/sources/types.d.ts +31 -0
  112. package/dist/types/core/types.d.ts +9 -0
  113. package/dist/types/core/unrecognized-keys.d.ts +1 -1
  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/cloudflare-negotiation.d.ts +3 -2
  117. package/dist/types/deploy/headers.d.ts +31 -7
  118. package/dist/types/deploy/node-headers.d.ts +43 -8
  119. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  120. package/dist/types/deploy/platforms/node.d.ts +6 -5
  121. package/dist/types/deploy/platforms/types.d.ts +7 -0
  122. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  123. package/dist/types/deploy/redirects.d.ts +7 -2
  124. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  125. package/dist/types/openapi/model.d.ts +20 -6
  126. package/dist/types/search/documents.d.ts +1 -1
  127. package/dist/types/search/orama-index.d.ts +1 -1
  128. package/dist/types/search/sync/algolia.d.ts +3 -1
  129. package/docs/01-quickstart.mdx +3 -2
  130. package/docs/02-deployment.mdx +21 -12
  131. package/docs/03-upgrading.mdx +22 -9
  132. package/docs/04-migrating.mdx +4 -4
  133. package/docs/08-faq.mdx +12 -5
  134. package/docs/advanced/changelog.mdx +1 -1
  135. package/docs/advanced/custom-pages.mdx +5 -3
  136. package/docs/advanced/skills.mdx +1 -1
  137. package/docs/cli/audit.mdx +5 -5
  138. package/docs/cli/doctor.mdx +4 -4
  139. package/docs/cli/evals.mdx +9 -9
  140. package/docs/cli/index.mdx +6 -3
  141. package/docs/cli/translate.mdx +8 -8
  142. package/docs/configuration/analytics.mdx +23 -2
  143. package/docs/configuration/{ask-ai.mdx → assistant.mdx} +20 -20
  144. package/docs/configuration/customization.mdx +5 -4
  145. package/docs/configuration/index.mdx +7 -5
  146. package/docs/configuration/meta.ts +1 -1
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +1 -1
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -3
  151. package/docs/content/index.mdx +4 -2
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +5 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -12
  156. package/docs/content/versioning.mdx +1 -0
  157. package/docs/discoverability/agent-discovery.mdx +22 -8
  158. package/docs/discoverability/index.mdx +3 -3
  159. package/docs/discoverability/llms-txt.mdx +2 -5
  160. package/docs/discoverability/markdown.mdx +5 -3
  161. package/docs/discoverability/mcp.mdx +4 -0
  162. package/docs/discoverability/metadata.mdx +3 -2
  163. package/docs/discoverability/open-graph.mdx +6 -4
  164. package/docs/discoverability/rss.mdx +3 -1
  165. package/docs/index.mdx +2 -2
  166. package/docs/references/asyncapi.mdx +1 -1
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +3 -3
  169. package/package.json +1 -1
  170. package/skills/blume/SKILL.md +5 -5
  171. package/skills/blume-migrate/SKILL.md +6 -6
  172. package/skills/blume-migrate/references/docusaurus.md +6 -6
  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 +1 -1
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +4 -4
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/api/paths.ts +1 -1
  182. package/src/ai/api/spec.ts +15 -2
  183. package/src/ai/api-catalog.ts +7 -1
  184. package/src/ai/ask-context.ts +21 -9
  185. package/src/ai/ask-data.ts +28 -14
  186. package/src/ai/ask.ts +84 -71
  187. package/src/ai/component-markdown.ts +43 -37
  188. package/src/ai/cors.ts +3 -3
  189. package/src/ai/index.ts +16 -16
  190. package/src/ai/link-headers.ts +8 -3
  191. package/src/ai/llms.ts +4 -3
  192. package/src/ai/markdown.ts +23 -11
  193. package/src/ai/mcp/server.ts +78 -7
  194. package/src/ai/openapi-components.ts +1 -1
  195. package/src/ai/relative-links.ts +78 -10
  196. package/src/ai/serializers.ts +1 -1
  197. package/src/ai/static-expression.ts +416 -0
  198. package/src/ai/visibility.ts +46 -15
  199. package/src/analytics/databuddy.ts +67 -0
  200. package/src/analytics/head.ts +4 -0
  201. package/src/analytics/index.ts +2 -0
  202. package/src/analytics/posthog.ts +21 -3
  203. package/src/analytics/schema.ts +2 -0
  204. package/src/astro/generate.ts +90 -45
  205. package/src/astro/module-types.ts +1 -1
  206. package/src/astro/runtime-deps.ts +6 -6
  207. package/src/astro/templates.ts +87 -49
  208. package/src/audit/catalog.ts +2 -2
  209. package/src/audit/checks/assets.ts +25 -5
  210. package/src/audit/checks/content.ts +20 -2
  211. package/src/audit/checks/i18n.ts +46 -8
  212. package/src/audit/checks/indexability.ts +39 -19
  213. package/src/audit/checks/links.ts +14 -0
  214. package/src/audit/checks/llms.ts +6 -3
  215. package/src/audit/checks/network.ts +3 -1
  216. package/src/audit/checks/og-image.ts +10 -0
  217. package/src/audit/checks/robots.ts +6 -1
  218. package/src/audit/checks/sitemap.ts +56 -31
  219. package/src/audit/checks/social.ts +21 -2
  220. package/src/audit/crawl.ts +88 -15
  221. package/src/audit/report.ts +54 -17
  222. package/src/audit/run.ts +1 -0
  223. package/src/audit/types.ts +18 -0
  224. package/src/audit/url.ts +37 -6
  225. package/src/blume-modules.d.ts +2 -2
  226. package/src/cli/build-failure.ts +50 -0
  227. package/src/cli/commands/audit.ts +8 -1
  228. package/src/cli/commands/build.ts +19 -5
  229. package/src/cli/commands/check.ts +2 -0
  230. package/src/cli/commands/doctor.ts +7 -5
  231. package/src/cli/commands/eval.ts +4 -10
  232. package/src/cli/commands/init.ts +15 -5
  233. package/src/cli/commands/migrate.ts +2 -2
  234. package/src/cli/commands/preview.ts +15 -0
  235. package/src/cli/commands/sync.ts +2 -0
  236. package/src/cli/commands/translate.ts +12 -9
  237. package/src/cli/commands/upgrade.ts +13 -2
  238. package/src/cli/eject-scripts.ts +32 -7
  239. package/src/cli/env.ts +12 -1
  240. package/src/cli/init/scaffold.ts +117 -11
  241. package/src/cli/init/starter-spec.ts +235 -0
  242. package/src/cli/required-secrets.ts +3 -3
  243. package/src/components/content/AccordionItem.astro +26 -22
  244. package/src/components/content/Badge.astro +2 -9
  245. package/src/components/content/Card.astro +2 -2
  246. package/src/components/content/Component.astro +9 -3
  247. package/src/components/content/Expandable.astro +5 -1
  248. package/src/components/content/Frame.astro +2 -7
  249. package/src/components/content/GithubInfo.astro +12 -2
  250. package/src/components/content/Prompt.astro +2 -7
  251. package/src/components/content/Tabs.astro +54 -5
  252. package/src/components/content/Tile.astro +1 -1
  253. package/src/components/content/Tooltip.astro +69 -7
  254. package/src/components/content/Tree.astro +7 -2
  255. package/src/components/content/TypeTable.astro +10 -5
  256. package/src/components/content/Update.astro +8 -2
  257. package/src/components/content/auto-type-table.ts +4 -1
  258. package/src/components/content/badge-color.ts +17 -0
  259. package/src/components/content/base-href.ts +18 -3
  260. package/src/components/content/inline-markdown.ts +27 -7
  261. package/src/components/copy-feedback.ts +36 -9
  262. package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
  263. package/src/components/islands/{ask-ai.tsx → assistant.tsx} +39 -25
  264. package/src/components/islands/hooks.ts +21 -14
  265. package/src/components/islands/webmcp.ts +12 -8
  266. package/src/components/layout/Banner.astro +23 -4
  267. package/src/components/layout/Header.astro +32 -19
  268. package/src/components/layout/Logo.astro +5 -0
  269. package/src/components/layout/NavSelector.astro +6 -1
  270. package/src/components/layout/NavTabMenu.astro +133 -0
  271. package/src/components/layout/NavTree.astro +15 -6
  272. package/src/components/layout/NavTreeCache.astro +5 -2
  273. package/src/components/layout/NavTreeScript.astro +45 -5
  274. package/src/components/layout/PageLayout.astro +28 -14
  275. package/src/components/layout/Pagination.astro +7 -7
  276. package/src/components/layout/ReferenceLayout.astro +5 -2
  277. package/src/components/layout/RootLayout.astro +27 -14
  278. package/src/components/layout/Search.astro +18 -14
  279. package/src/components/layout/analytics-client.ts +3 -1
  280. package/src/components/layout/drawer-inert.ts +1 -1
  281. package/src/components/openapi/GraphqlType.astro +11 -3
  282. package/src/components/openapi/MessageComposer.astro +1 -1
  283. package/src/components/openapi/Operation.astro +21 -5
  284. package/src/components/openapi/PanelTabs.astro +4 -1
  285. package/src/components/openapi/Playground.astro +8 -2
  286. package/src/components/openapi/RequestPanel.astro +9 -5
  287. package/src/components/openapi/SchemaProperty.astro +11 -33
  288. package/src/components/openapi/SchemaTable.astro +27 -52
  289. package/src/components/openapi/description.ts +2 -2
  290. package/src/components/openapi/helpers.ts +90 -12
  291. package/src/components/openapi/message-composer.ts +8 -0
  292. package/src/components/openapi/message-model.ts +12 -2
  293. package/src/components/openapi/message.ts +4 -1
  294. package/src/components/openapi/operation-model.ts +40 -8
  295. package/src/components/openapi/panel.ts +29 -5
  296. package/src/components/openapi/playground-client.ts +37 -7
  297. package/src/components/openapi/playground-schema.ts +25 -6
  298. package/src/components/openapi/request.ts +2 -0
  299. package/src/components/openapi/schema-tree.ts +203 -0
  300. package/src/components/openapi/snippets.ts +30 -15
  301. package/src/components/openapi/validate-json.ts +1 -1
  302. package/src/core/base-path.ts +15 -0
  303. package/src/core/code-fences.ts +1 -1
  304. package/src/core/config-input.ts +23 -23
  305. package/src/core/config.ts +21 -7
  306. package/src/core/data.ts +3 -3
  307. package/src/core/diagnostics.ts +217 -30
  308. package/src/core/graph.ts +120 -11
  309. package/src/core/i18n-ui.ts +74 -11
  310. package/src/core/i18n.ts +13 -2
  311. package/src/core/links.ts +7 -4
  312. package/src/core/load-module.ts +20 -0
  313. package/src/core/locale-links.ts +17 -17
  314. package/src/core/manifest.ts +3 -2
  315. package/src/core/meta.ts +69 -6
  316. package/src/core/nav-diagnostics.ts +56 -1
  317. package/src/core/navigation.ts +245 -76
  318. package/src/core/ordering-prefix.ts +27 -0
  319. package/src/core/project-graph.ts +23 -1
  320. package/src/core/request-body.ts +1 -1
  321. package/src/core/safe-href.ts +53 -1
  322. package/src/core/safe-links.ts +11 -2
  323. package/src/core/schema.ts +95 -34
  324. package/src/core/server-features.ts +2 -2
  325. package/src/core/sources/assets.ts +83 -41
  326. package/src/core/sources/contentful-rich-text.ts +3 -2
  327. package/src/core/sources/contentful.ts +25 -12
  328. package/src/core/sources/filesystem.ts +5 -1
  329. package/src/core/sources/github-releases.ts +106 -5
  330. package/src/core/sources/lexical.ts +9 -4
  331. package/src/core/sources/lower.ts +79 -26
  332. package/src/core/sources/mdx-remote.ts +1 -0
  333. package/src/core/sources/normalize.ts +132 -30
  334. package/src/core/sources/notion.ts +26 -10
  335. package/src/core/sources/obsidian.ts +17 -3
  336. package/src/core/sources/payload.ts +1 -0
  337. package/src/core/sources/portable-text.ts +63 -44
  338. package/src/core/sources/remote.ts +18 -2
  339. package/src/core/sources/resolve.ts +52 -33
  340. package/src/core/sources/sanity.ts +1 -0
  341. package/src/core/sources/strapi-blocks.ts +9 -4
  342. package/src/core/sources/strapi.ts +1 -0
  343. package/src/core/sources/types.ts +31 -0
  344. package/src/core/types.ts +9 -0
  345. package/src/core/ui-packs/ar.ts +17 -5
  346. package/src/core/ui-packs/bg.ts +17 -5
  347. package/src/core/ui-packs/bn.ts +17 -5
  348. package/src/core/ui-packs/ca.ts +17 -5
  349. package/src/core/ui-packs/cs.ts +17 -5
  350. package/src/core/ui-packs/da.ts +17 -5
  351. package/src/core/ui-packs/de.ts +17 -5
  352. package/src/core/ui-packs/el.ts +17 -5
  353. package/src/core/ui-packs/es.ts +17 -5
  354. package/src/core/ui-packs/fa.ts +17 -5
  355. package/src/core/ui-packs/fi.ts +17 -5
  356. package/src/core/ui-packs/fr.ts +17 -5
  357. package/src/core/ui-packs/he.ts +17 -5
  358. package/src/core/ui-packs/hi.ts +17 -5
  359. package/src/core/ui-packs/hr.ts +17 -5
  360. package/src/core/ui-packs/hu.ts +17 -5
  361. package/src/core/ui-packs/id.ts +17 -5
  362. package/src/core/ui-packs/it.ts +17 -5
  363. package/src/core/ui-packs/ja.ts +17 -5
  364. package/src/core/ui-packs/ko.ts +17 -5
  365. package/src/core/ui-packs/nl.ts +17 -5
  366. package/src/core/ui-packs/no.ts +17 -5
  367. package/src/core/ui-packs/pl.ts +17 -5
  368. package/src/core/ui-packs/pt-br.ts +17 -5
  369. package/src/core/ui-packs/pt.ts +17 -5
  370. package/src/core/ui-packs/ro.ts +17 -5
  371. package/src/core/ui-packs/ru.ts +17 -5
  372. package/src/core/ui-packs/sk.ts +17 -5
  373. package/src/core/ui-packs/sr.ts +17 -5
  374. package/src/core/ui-packs/sv.ts +17 -5
  375. package/src/core/ui-packs/th.ts +17 -5
  376. package/src/core/ui-packs/tr.ts +17 -5
  377. package/src/core/ui-packs/uk.ts +17 -5
  378. package/src/core/ui-packs/vi.ts +17 -5
  379. package/src/core/ui-packs/zh-tw.ts +17 -5
  380. package/src/core/ui-packs/zh.ts +17 -5
  381. package/src/core/unrecognized-keys.ts +1 -1
  382. package/src/core/version-cut.ts +17 -2
  383. package/src/core/versions.ts +4 -1
  384. package/src/deploy/adapters/node.ts +5 -2
  385. package/src/deploy/adapters/registry.ts +2 -1
  386. package/src/deploy/adapters/types.ts +13 -1
  387. package/src/deploy/artifacts.ts +25 -5
  388. package/src/deploy/cloudflare-negotiation.ts +23 -4
  389. package/src/deploy/headers.ts +67 -51
  390. package/src/deploy/node-headers.ts +148 -27
  391. package/src/deploy/platforms/cloudflare.ts +1 -0
  392. package/src/deploy/platforms/netlify.ts +82 -5
  393. package/src/deploy/platforms/node.ts +7 -5
  394. package/src/deploy/platforms/static.ts +1 -0
  395. package/src/deploy/platforms/types.ts +7 -0
  396. package/src/deploy/platforms/vercel.ts +9 -3
  397. package/src/deploy/redirects.ts +14 -3
  398. package/src/deploy/vercel-negotiation.ts +35 -3
  399. package/src/eval/agents.ts +10 -2
  400. package/src/eval/run.ts +25 -0
  401. package/src/markdown/base-links.ts +55 -8
  402. package/src/markdown/index.ts +6 -3
  403. package/src/markdown/relative-links.ts +3 -23
  404. package/src/markdown/route-snapshot.ts +37 -0
  405. package/src/og/card.ts +20 -4
  406. package/src/og/derive.ts +145 -4
  407. package/src/openapi/graphql-build.ts +28 -2
  408. package/src/openapi/model.ts +77 -13
  409. package/src/openapi/render-mdx.ts +10 -1
  410. package/src/registry/eject.ts +135 -37
  411. package/src/search/documents.ts +24 -3
  412. package/src/search/orama-index.ts +1 -1
  413. package/src/search/sync/algolia.ts +36 -2
  414. package/src/sources/registry.ts +5 -0
  415. package/src/theme/entry.ts +11 -4
  416. package/src/translate/agents.ts +6 -1
  417. package/src/translate/ledger.ts +26 -3
  418. package/src/translate/meta.ts +11 -3
  419. package/src/translate/report.ts +1 -1
  420. package/src/translate/run.ts +11 -5
  421. package/src/translate/validate.ts +10 -1
  422. package/src/translate/work-list.ts +36 -3
  423. package/src/upgrade/upgrade.ts +37 -5
  424. package/dist/cli/chunk-1w8dp3qb.js.map +0 -10
  425. package/dist/cli/chunk-5a2z0198.js.map +0 -10
  426. package/dist/cli/chunk-6k8vp3ta.js.map +0 -19
  427. package/dist/cli/chunk-79njf86q.js.map +0 -11
  428. package/dist/cli/chunk-a9kptbw5.js.map +0 -14
  429. package/dist/cli/chunk-abh8yjkn.js +0 -31
  430. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  431. package/dist/cli/chunk-bctazmbk.js.map +0 -10
  432. package/dist/cli/chunk-beat36xx.js.map +0 -10
  433. package/dist/cli/chunk-d1tadaw7.js.map +0 -10
  434. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  435. package/dist/cli/chunk-j8mw0za6.js.map +0 -35
  436. package/dist/cli/chunk-mnqj32sj.js.map +0 -12
  437. package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
  438. package/dist/cli/chunk-nk3ts2xk.js +0 -51
  439. package/dist/cli/chunk-nk3ts2xk.js.map +0 -10
  440. package/dist/cli/chunk-sqn5t4q0.js.map +0 -10
  441. package/dist/cli/chunk-tzne8qfq.js.map +0 -10
  442. package/dist/cli/chunk-xaz13gwg.js.map +0 -182
  443. package/dist/cli/chunk-z01ze5c1.js.map +0 -10
  444. package/dist/cli/chunk-z1f5arsg.js.map +0 -36
  445. package/dist/cli/chunk-zg2gtj10.js.map +0 -35
  446. /package/dist/cli/{chunk-f2972sbt.js.map → chunk-00gs3wqs.js.map} +0 -0
  447. /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-bbnwccaz.js.map} +0 -0
  448. /package/dist/cli/{chunk-zxccj738.js.map → chunk-esh98wmb.js.map} +0 -0
  449. /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-fsmrqk8a.js.map} +0 -0
  450. /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-g698a744.js.map} +0 -0
  451. /package/dist/cli/{chunk-41za066z.js.map → chunk-h2ez8dzb.js.map} +0 -0
  452. /package/dist/cli/{chunk-by2290sx.js.map → chunk-h7k3nq3v.js.map} +0 -0
  453. /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-n1yg3tj3.js.map} +0 -0
  454. /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-wdrt2k2v.js.map} +0 -0
@@ -1,5 +1,11 @@
1
- import { isInternalPath, withComposedBasePath } from "../core/base-path.ts";
1
+ import {
2
+ isInternalPath,
3
+ withBasePath,
4
+ withComposedBasePath,
5
+ } from "../core/base-path.ts";
6
+ import { servesRoute } from "../core/locale-links.ts";
2
7
  import type { MdastNode } from "./mdast.ts";
8
+ import { routeSnapshotReader } from "./route-snapshot.ts";
3
9
 
4
10
  interface UrlNode extends MdastNode {
5
11
  url?: string | null;
@@ -16,12 +22,12 @@ interface MdastUrlContext {
16
22
 
17
23
  /**
18
24
  * A path whose final segment carries a file extension (`/spec.pdf`, `/logo.svg`)
19
- * — treated as a public asset, which Blume serves from `public/` at the site
20
- * root and does *not* move under `basePath`. Bare page links (`/guide`) have no
21
- * extension. The rare dotted route (`/releases/v1.0`) is left un-based here; the
22
- * build-time link checker still resolves it against the route set.
25
+ * — a public asset, which Blume serves from `public/` at the site root and
26
+ * does *not* move under `basePath`, unless a page is served there: a dotted
27
+ * route (`/releases/v1.2`) is based like any other page link, the same
28
+ * served-route test the link checker and the locale rewrite apply.
23
29
  */
24
- const ASSET_PATH = /\.[a-z0-9]+$/iu;
30
+ const DOTTED_PATH = /\.[a-z0-9]+$/iu;
25
31
 
26
32
  /** Strip any `#fragment`/`?query` so only the path is extension-tested. */
27
33
  const pathOf = (url: string): string => url.replace(/[#?].*$/u, "");
@@ -30,6 +36,14 @@ const pathOf = (url: string): string => url.replace(/[#?].*$/u, "");
30
36
  const isUrl = (url: string | null | undefined): url is string =>
31
37
  typeof url === "string";
32
38
 
39
+ export interface BaseLinksPluginOptions {
40
+ /**
41
+ * The `blume:data` JSON file, for an ejected app: with no CLI in the process
42
+ * to publish the snapshot, the plugin reads the file eject writes instead.
43
+ */
44
+ dataFile?: string;
45
+ }
46
+
33
47
  /**
34
48
  * Satteri MDAST plugin that prepends the served-URL base — `deployment.base`
35
49
  * layered over the site-wide `basePath` — to root-relative internal page links
@@ -39,10 +53,43 @@ const isUrl = (url: string | null | undefined): url is string =>
39
53
  * relative paths, images, and asset links. Only constructed when a base is set
40
54
  * (see `markdown/index.ts`).
41
55
  */
42
- export const baseLinksPlugin = (deployBase: string, basePath: string) => {
56
+ export const baseLinksPlugin = (
57
+ deployBase: string,
58
+ basePath: string,
59
+ options: BaseLinksPluginOptions = {}
60
+ ) => {
61
+ const readSnapshot = routeSnapshotReader(options.dataFile);
62
+ // Parsed once per published snapshot, like the relative-links index.
63
+ let cached: { routes: Set<string>; text: string } | undefined;
64
+
65
+ /** Every route the site serves (base-prefixed), from the route snapshot. */
66
+ const servedRoutes = (): ReadonlySet<string> => {
67
+ const text = readSnapshot();
68
+ if (text === undefined) {
69
+ return new Set();
70
+ }
71
+ if (cached?.text !== text) {
72
+ const data: { routes: { path: string }[] } = JSON.parse(text);
73
+ cached = {
74
+ routes: new Set(data.routes.map((route) => route.path)),
75
+ text,
76
+ };
77
+ }
78
+ return cached.routes;
79
+ };
80
+
81
+ /** Whether an internal `url` links a page rather than a public asset. */
82
+ const isPageLink = (url: string): boolean => {
83
+ const path = pathOf(url);
84
+ return (
85
+ !DOTTED_PATH.test(path) ||
86
+ servesRoute(servedRoutes(), withBasePath(basePath, path))
87
+ );
88
+ };
89
+
43
90
  const rebase = (node: UrlNode, ctx: MdastUrlContext): void => {
44
91
  const { url } = node;
45
- if (isUrl(url) && isInternalPath(url) && !ASSET_PATH.test(pathOf(url))) {
92
+ if (isUrl(url) && isInternalPath(url) && isPageLink(url)) {
46
93
  const next = withComposedBasePath(deployBase, basePath, url);
47
94
  if (next !== url) {
48
95
  ctx.setProperty(node, "url", next);
@@ -286,8 +286,9 @@ export interface BlumeMarkdownOptions {
286
286
  contentRoot?: string;
287
287
  /**
288
288
  * The `blume:data` snapshot file an ejected app aliases. Relative page links
289
- * resolve through the snapshot the CLI publishes; with no CLI in the
290
- * process, they read this file instead.
289
+ * (and the base-path rewrite of dotted page routes) resolve through the
290
+ * snapshot the CLI publishes; with no CLI in the process, they read this
291
+ * file instead.
291
292
  */
292
293
  dataFile?: string;
293
294
  }
@@ -310,7 +311,9 @@ const blumeSharedMdastPlugins = (
310
311
  ...(options.basePath || options.deployBase
311
312
  ? [
312
313
  asMdastPlugin(
313
- baseLinksPlugin(options.deployBase ?? "", options.basePath ?? "")
314
+ baseLinksPlugin(options.deployBase ?? "", options.basePath ?? "", {
315
+ dataFile: options.dataFile,
316
+ })
314
317
  ),
315
318
  ]
316
319
  : []),
@@ -1,12 +1,11 @@
1
- import { readFileSync, statSync } from "node:fs";
2
1
  import { fileURLToPath } from "node:url";
3
2
 
4
3
  import { dirname, normalize, relative, resolve } from "pathe";
5
4
 
6
- import { readRuntimeModule } from "../astro/runtime-modules.ts";
7
5
  import { isIndexFileName, resolveRelativeHref } from "../core/links.ts";
8
6
  import type { RelativeLinkBase } from "../core/links.ts";
9
7
  import type { MdastNode, MdastValue } from "./mdast.ts";
8
+ import { routeSnapshotReader } from "./route-snapshot.ts";
10
9
 
11
10
  interface UrlNode extends MdastNode {
12
11
  url?: string | null;
@@ -156,34 +155,15 @@ export const relativeLinksPlugin = (
156
155
  const contentRoot = options.contentRoot
157
156
  ? resolve(options.contentRoot)
158
157
  : undefined;
159
- const dataFile = options.dataFile ? resolve(options.dataFile) : undefined;
158
+ const readSnapshot = routeSnapshotReader(options.dataFile);
160
159
 
161
160
  // Parsed once per published snapshot: the CLI republishes on regeneration,
162
161
  // and an unchanged snapshot is the same string, so every page compiled
163
162
  // between regenerations reuses one index.
164
163
  let cached: { index: RouteIndex; text: string } | undefined;
165
- let fileStamp: number | undefined;
166
- let fileText: string | undefined;
167
-
168
- /** The ejected snapshot file's text, re-read only when it changes. */
169
- const readDataFile = (path: string): string | undefined => {
170
- let stamp: number;
171
- try {
172
- stamp = statSync(path).mtimeMs;
173
- } catch {
174
- return undefined;
175
- }
176
- if (stamp !== fileStamp) {
177
- fileStamp = stamp;
178
- fileText = readFileSync(path, "utf-8");
179
- }
180
- return fileText;
181
- };
182
164
 
183
165
  const routeIndex = (): RouteIndex | undefined => {
184
- const text =
185
- readRuntimeModule("blume:data") ??
186
- (dataFile ? readDataFile(dataFile) : undefined);
166
+ const text = readSnapshot();
187
167
  if (text === undefined) {
188
168
  return undefined;
189
169
  }
@@ -0,0 +1,37 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+
3
+ import { resolve } from "pathe";
4
+
5
+ import { readRuntimeModule } from "../astro/runtime-modules.ts";
6
+
7
+ /**
8
+ * A reader for the `blume:data` snapshot the Markdown link plugins consult for
9
+ * the site's routes: the text the CLI publishes, or — in an ejected app, with
10
+ * no CLI in the process — the snapshot file eject writes (`dataFile`), re-read
11
+ * only when it changes. Resolves to `undefined` when neither is available.
12
+ */
13
+ export const routeSnapshotReader = (
14
+ dataFile?: string
15
+ ): (() => string | undefined) => {
16
+ const file = dataFile ? resolve(dataFile) : undefined;
17
+ let fileStamp: number | undefined;
18
+ let fileText: string | undefined;
19
+
20
+ /** The ejected snapshot file's text, re-read only when it changes. */
21
+ const readDataFile = (path: string): string | undefined => {
22
+ let stamp: number;
23
+ try {
24
+ stamp = statSync(path).mtimeMs;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ if (stamp !== fileStamp) {
29
+ fileStamp = stamp;
30
+ fileText = readFileSync(path, "utf-8");
31
+ }
32
+ return fileText;
33
+ };
34
+
35
+ return () =>
36
+ readRuntimeModule("blume:data") ?? (file ? readDataFile(file) : undefined);
37
+ };
package/src/og/card.ts CHANGED
@@ -261,11 +261,23 @@ const logoMark = (svg: string, foreground: string): Node => {
261
261
  };
262
262
 
263
263
  // Fallback mark when no SVG logo is configured: an accent tile with the brand's
264
- // initial, matching the docs favicon aesthetic.
265
- const initialMark = (accent: string, initial: string): Node =>
264
+ // initial, matching the docs favicon aesthetic. The initial takes the headline's
265
+ // family, so it matches the title rather than whichever loaded font comes first.
266
+ const initialMark = (
267
+ accent: string,
268
+ initial: string,
269
+ family: { fontFamily?: string }
270
+ ): Node =>
266
271
  container({
267
272
  children: initial
268
- ? [text(initial, { color: "#ffffff", fontSize: 32, fontWeight: 600 })]
273
+ ? [
274
+ text(initial, {
275
+ color: "#ffffff",
276
+ fontSize: 32,
277
+ fontWeight: 600,
278
+ ...family,
279
+ }),
280
+ ]
269
281
  : [],
270
282
  style: {
271
283
  alignItems: "center",
@@ -319,7 +331,11 @@ export const renderOgImage = async (
319
331
  if (logo === false) {
320
332
  return [];
321
333
  }
322
- return [logo ? logoMark(logo, foreground) : initialMark(accent, initial)];
334
+ return [
335
+ logo
336
+ ? logoMark(logo, foreground)
337
+ : initialMark(accent, initial, titleFamily),
338
+ ];
323
339
  };
324
340
  const header = container({
325
341
  children: mark(),
package/src/og/derive.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  * Bridges `theme.fonts` into the OG card renderer. A site that explicitly
3
3
  * picks its typefaces gets matching cards (and non-Latin coverage) without
4
4
  * configuring `seo.og.fonts`; untouched defaults derive nothing, so plain
5
- * sites keep Takumi's built-in font and gain no build-time font fetch.
5
+ * sites keep Takumi's built-in font and gain no build-time font fetch. Locales
6
+ * in scripts that font can't draw (Japanese, Hindi, Russian, …) add a Noto
7
+ * fallback for their script either way.
6
8
  */
7
9
 
8
10
  import { existsSync } from "node:fs";
@@ -14,7 +16,7 @@ import type {
14
16
  FontValue,
15
17
  LocalFontConfig,
16
18
  } from "../theme/fonts.ts";
17
- import { GOOGLE_FONTS, isFontSlug } from "../theme/fonts.ts";
19
+ import { GOOGLE_FONTS, isFontSlug, localeFontSubsets } from "../theme/fonts.ts";
18
20
  import type { OgFont, OgFontFamilies, OgLocalFont } from "./card.ts";
19
21
 
20
22
  /** Fonts plus per-role families for the generated OG endpoint. */
@@ -160,6 +162,140 @@ export const deriveOgFonts = (
160
162
  return result;
161
163
  };
162
164
 
165
+ /**
166
+ * The family Takumi renders a card in when no font is loaded: its embedded
167
+ * Geist, which covers Latin only. Naming it keeps a card's Latin text in that
168
+ * face once locale fallbacks are loaded, since Takumi otherwise tries loaded
169
+ * fonts first; a glyph Geist lacks still falls back to them. Were Takumi to
170
+ * rename it, the name would resolve to nothing and cards would fall back to
171
+ * the loaded fonts, never to tofu.
172
+ */
173
+ const BUILT_IN_FAMILY = "Geist";
174
+
175
+ /**
176
+ * The Google Noto family that draws each language's script, keyed by BCP 47
177
+ * language subtag, for scripts beyond the Latin, Cyrillic, Greek, and
178
+ * Vietnamese that `Noto Sans` covers (see {@link localeOgFonts}). Keep keys
179
+ * alphabetical.
180
+ */
181
+ const SCRIPT_FAMILIES = {
182
+ am: "Noto Sans Ethiopic",
183
+ ar: "Noto Sans Arabic",
184
+ as: "Noto Sans Bengali",
185
+ bn: "Noto Sans Bengali",
186
+ ckb: "Noto Sans Arabic",
187
+ fa: "Noto Sans Arabic",
188
+ gu: "Noto Sans Gujarati",
189
+ he: "Noto Sans Hebrew",
190
+ hi: "Noto Sans Devanagari",
191
+ hy: "Noto Sans Armenian",
192
+ ja: "Noto Sans JP",
193
+ ka: "Noto Sans Georgian",
194
+ km: "Noto Sans Khmer",
195
+ kn: "Noto Sans Kannada",
196
+ ko: "Noto Sans KR",
197
+ lo: "Noto Sans Lao",
198
+ ml: "Noto Sans Malayalam",
199
+ mr: "Noto Sans Devanagari",
200
+ my: "Noto Sans Myanmar",
201
+ ne: "Noto Sans Devanagari",
202
+ or: "Noto Sans Oriya",
203
+ pa: "Noto Sans Gurmukhi",
204
+ ps: "Noto Sans Arabic",
205
+ sa: "Noto Sans Devanagari",
206
+ si: "Noto Sans Sinhala",
207
+ ta: "Noto Sans Tamil",
208
+ te: "Noto Sans Telugu",
209
+ th: "Noto Sans Thai",
210
+ ti: "Noto Sans Ethiopic",
211
+ ur: "Noto Sans Arabic",
212
+ yi: "Noto Sans Hebrew",
213
+ zh: "Noto Sans SC",
214
+ } satisfies Record<string, string>;
215
+
216
+ /** Type guard: does `language` have an entry in the script table? */
217
+ const isScriptLanguage = (
218
+ language: string
219
+ ): language is keyof typeof SCRIPT_FAMILIES =>
220
+ Object.hasOwn(SCRIPT_FAMILIES, language);
221
+
222
+ /** A locale fallback: a Google family at the weights the card renders. */
223
+ export interface LocaleOgFont {
224
+ name: string;
225
+ weight: number[];
226
+ }
227
+
228
+ /** Subtags that write Chinese in traditional characters (`zh-Hant`, `zh-TW`). */
229
+ const TRADITIONAL_CHINESE = new Set(["hant", "hk", "mo", "tw"]);
230
+
231
+ /** The Noto family for one locale's script, or null when Geist covers it. */
232
+ const localeFamily = (locale: string): string | null => {
233
+ const [language = "", ...subtags] = locale.toLowerCase().split("-");
234
+ if (
235
+ language === "zh" &&
236
+ subtags.some((tag) => TRADITIONAL_CHINESE.has(tag))
237
+ ) {
238
+ return "Noto Sans TC";
239
+ }
240
+ if (isScriptLanguage(language)) {
241
+ return SCRIPT_FAMILIES[language];
242
+ }
243
+ // Anything past plain `latin` in the theme's subset table (Polish, Russian,
244
+ // Greek, Vietnamese, …) needs glyphs Geist lacks and `Noto Sans` has.
245
+ return localeFontSubsets([locale]).length > 1 ? "Noto Sans" : null;
246
+ };
247
+
248
+ /**
249
+ * The fallback card fonts the configured locales' scripts need, at the card's
250
+ * weights. Takumi's built-in font covers only basic Latin, so without these a
251
+ * Japanese or Hindi page's card renders every glyph as tofu. The renderer
252
+ * fetches only the glyph subsets a card's text uses, so an English card on
253
+ * the same site pulls nothing extra.
254
+ */
255
+ export const localeOgFonts = (locales: string[]): LocaleOgFont[] => {
256
+ const names = new Set<string>();
257
+ for (const locale of locales) {
258
+ const family = localeFamily(locale);
259
+ if (family) {
260
+ names.add(family);
261
+ }
262
+ }
263
+ return [...names].map((name) => ({ name, weight: CARD_WEIGHTS }));
264
+ };
265
+
266
+ /** A bare Google family name entry (as opposed to the object forms). */
267
+ const isGoogleFamilyName = (font: OgFont): font is string =>
268
+ typeof font === "string";
269
+
270
+ /** The family an OG font entry registers under. */
271
+ const ogFontName = (font: OgFont): string =>
272
+ isGoogleFamilyName(font) ? font : font.name;
273
+
274
+ /**
275
+ * `derived` plus the locale fallbacks it doesn't already load. A card that
276
+ * names no family is pinned to {@link BUILT_IN_FAMILY}, so its Latin text
277
+ * keeps the face it had before any fallback was loaded.
278
+ */
279
+ const withLocaleFonts = (
280
+ derived: DerivedOgFonts,
281
+ locales: string[]
282
+ ): DerivedOgFonts => {
283
+ const loaded = new Set(derived.fonts.map(ogFontName));
284
+ const fallbacks = localeOgFonts(locales).filter(
285
+ (font) => !loaded.has(ogFontName(font))
286
+ );
287
+ if (fallbacks.length === 0) {
288
+ return derived;
289
+ }
290
+ return {
291
+ families: derived.families ?? {
292
+ body: BUILT_IN_FAMILY,
293
+ title: BUILT_IN_FAMILY,
294
+ },
295
+ fonts: [...derived.fonts, ...fallbacks],
296
+ };
297
+ };
298
+
163
299
  /** Explicit `seo.og.fonts` with local `src` paths resolved to absolute. */
164
300
  export const resolveOgFontSources = (fonts: OgFont[], root: string): OgFont[] =>
165
301
  fonts.map((font) =>
@@ -170,10 +306,14 @@ export const resolveOgFontSources = (fonts: OgFont[], root: string): OgFont[] =>
170
306
  * The fonts baked into the generated OG endpoint. An explicit `seo.og.fonts`
171
307
  * always wins (including `[]` to opt out, keeping the card's role styling
172
308
  * untouched); otherwise a site that explicitly set `theme.fonts` gets its
173
- * display/body fonts derived so cards match the site without extra config.
309
+ * display/body fonts derived so cards match the site without extra config,
310
+ * and either way the configured locales add a fallback for each script the
311
+ * built-in font can't draw.
174
312
  */
175
313
  export const resolveOgFonts = (
176
314
  options: {
315
+ /** The site's configured locale codes (none without `i18n`). */
316
+ locales?: string[];
177
317
  /** Explicit `seo.og.fonts`, or undefined when unset. */
178
318
  ogFonts: OgFont[] | undefined;
179
319
  themeFonts: FontsConfig;
@@ -185,9 +325,10 @@ export const resolveOgFonts = (
185
325
  if (options.ogFonts) {
186
326
  return { fonts: resolveOgFontSources(options.ogFonts, root) };
187
327
  }
188
- return options.themeFontsConfigured
328
+ const derived = options.themeFontsConfigured
189
329
  ? deriveOgFonts(options.themeFonts, root)
190
330
  : { fonts: [] };
331
+ return withLocaleFonts(derived, options.locales ?? []);
191
332
  };
192
333
 
193
334
  /**
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ DocumentNode,
2
3
  GraphQLArgument,
3
4
  GraphQLEnumType,
4
5
  GraphQLField,
@@ -12,8 +13,8 @@ import type {
12
13
  GraphQLUnionType,
13
14
  } from "graphql";
14
15
  import {
16
+ buildASTSchema,
15
17
  buildClientSchema,
16
- buildSchema,
17
18
  getNamedType,
18
19
  isEnumType,
19
20
  isInputObjectType,
@@ -21,7 +22,11 @@ import {
21
22
  isObjectType,
22
23
  isSpecifiedScalarType,
23
24
  isUnionType,
25
+ Kind,
26
+ parse,
24
27
  print,
28
+ specifiedDirectives,
29
+ visit,
25
30
  } from "graphql";
26
31
 
27
32
  import type {
@@ -259,6 +264,27 @@ const introspectionOf = <Value>(parsed: Value): object | undefined => {
259
264
  : undefined;
260
265
  };
261
266
 
267
+ /**
268
+ * SDL with every use of an undeclared directive removed. Subgraph and platform
269
+ * schemas lean on directives their server defines out of band — Apollo
270
+ * Federation's `@key` and `@link`, AppSync's `@aws_*` — and strict SDL
271
+ * validation rejects each one as `Unknown directive`. The reference renders
272
+ * none of them, so they are dropped rather than declared; every other check
273
+ * (syntax, unknown types, duplicates) still runs.
274
+ */
275
+ const withoutUnknownDirectives = (document: DocumentNode): DocumentNode => {
276
+ const known = new Set(specifiedDirectives.map((directive) => directive.name));
277
+ for (const definition of document.definitions) {
278
+ if (definition.kind === Kind.DIRECTIVE_DEFINITION) {
279
+ known.add(definition.name.value);
280
+ }
281
+ }
282
+ return visit(document, {
283
+ // `null` deletes the node; `undefined` leaves it in place.
284
+ Directive: (node) => (known.has(node.name.value) ? undefined : null),
285
+ });
286
+ };
287
+
262
288
  /**
263
289
  * Build the document from schema text: an introspection JSON result (the raw
264
290
  * `{ __schema }` shape or a `{ data: { __schema } }` response envelope) or
@@ -289,5 +315,5 @@ export const buildGraphqlDocument = (text: string): GraphqlDocument => {
289
315
  )
290
316
  );
291
317
  }
292
- return documentOf(buildSchema(text));
318
+ return documentOf(buildASTSchema(withoutUnknownDirectives(parse(text))));
293
319
  };
@@ -1,6 +1,10 @@
1
1
  import type { Document, OperationObject } from "@scalar/openapi-types/3.1";
2
2
 
3
- import type { AsyncApiAction, AsyncApiDocument } from "./asyncapi.ts";
3
+ import type {
4
+ AsyncApiAction,
5
+ AsyncApiDocument,
6
+ AsyncApiSpecValue,
7
+ } from "./asyncapi.ts";
4
8
  // Type-only, so the import can't cycle at runtime (graphql.ts imports the
5
9
  // collector from here).
6
10
  import type { GraphqlDocument, GraphqlMember } from "./graphql.ts";
@@ -99,7 +103,10 @@ export interface ApiOperationRef {
99
103
  channelId?: string;
100
104
  }
101
105
 
102
- /** A tag/section, in first-seen order. */
106
+ /**
107
+ * A tag/section: the spec's declared tags in their declared order, then any
108
+ * undeclared tag an operation uses, in first-seen order.
109
+ */
103
110
  export interface ApiTagRef {
104
111
  slug: string;
105
112
  name: string;
@@ -212,6 +219,40 @@ export const specAddresses = (spec: ApiSpecData): SpecAddresses => {
212
219
  return { addresses, label: "Base URL" };
213
220
  };
214
221
 
222
+ const SERVER_VARIABLE = /\{(?<name>[^{}]+)\}/gu;
223
+
224
+ const isDocumentObject = (
225
+ value: AsyncApiSpecValue
226
+ ): value is Record<string, AsyncApiSpecValue> =>
227
+ typeof value === "object" && value !== null && !Array.isArray(value);
228
+
229
+ // YAML reads an unquoted `default: 8443` as a number; it is still the port.
230
+ const isVariableDefault = (
231
+ value: AsyncApiSpecValue
232
+ ): value is string | number =>
233
+ typeof value === "string" || typeof value === "number";
234
+
235
+ /**
236
+ * A server URL template — an OpenAPI `servers[].url`, an AsyncAPI server's
237
+ * `host` or `pathname` — with each `{variable}` replaced by the `default` its
238
+ * `variables` map declares. Code samples, the playground's Send, and the
239
+ * proxy allowlist all need a real address, and
240
+ * `https://{region}.api.example.com` is not one. A variable the map doesn't
241
+ * define with a default (invalid per both specs) stays templated.
242
+ */
243
+ export const withServerDefaults = (
244
+ template: string,
245
+ variables: AsyncApiSpecValue
246
+ ): string =>
247
+ template.replaceAll(SERVER_VARIABLE, (match, name: string) => {
248
+ const variable =
249
+ isDocumentObject(variables) && Object.hasOwn(variables, name)
250
+ ? variables[name]
251
+ : undefined;
252
+ const value = isDocumentObject(variable) ? variable.default : undefined;
253
+ return isVariableDefault(value) ? String(value) : match;
254
+ });
255
+
215
256
  // The runtime object check stands guard because the document was parsed from
216
257
  // arbitrary YAML/JSON: a spec can put a scalar where the type promises an
217
258
  // operation object.
@@ -276,9 +317,10 @@ export interface ExtractedOperations extends CollectedOperations {
276
317
 
277
318
  /**
278
319
  * The collector behind both extractors (OpenAPI here, AsyncAPI in
279
- * `asyncapi.ts`): first-seen tag ordering, key de-duplication (a repeated key
280
- * gains its method/action as a suffix), and the shared route template — so
281
- * URL shape and slug rules can never drift between the two spec kinds.
320
+ * `asyncapi.ts`): declared-then-first-seen tag ordering, key de-duplication
321
+ * (a repeated key gains its method/action as a suffix), and the shared route
322
+ * template — so URL shape and slug rules can never drift between the two spec
323
+ * kinds.
282
324
  */
283
325
  export const operationCollector = (
284
326
  baseRoute: string,
@@ -310,15 +352,27 @@ export const operationCollector = (
310
352
  });
311
353
  };
312
354
 
355
+ // Declared tags in the order the spec's `tags` list gives them, then any
356
+ // tag an operation uses without declaring it, in first-use order. The
357
+ // declared order is the author's section order, not an accident of which
358
+ // path happens to come first.
359
+ const declared = [...tagMeta.keys()];
360
+ const rank = (name: string): number => {
361
+ const index = declared.indexOf(name);
362
+ return index === -1 ? declared.length : index;
363
+ };
364
+
313
365
  const finish = (): CollectedOperations => ({
314
366
  operations,
315
- tags: tagOrder.map((name) => ({
316
- description: tagMeta.get(name) ?? "",
317
- name,
318
- // The same slugger instance, so every tag resolves to the slug its
319
- // operations were routed under.
320
- slug: slugForTag(name),
321
- })),
367
+ tags: tagOrder
368
+ .toSorted((a, b) => rank(a) - rank(b))
369
+ .map((name) => ({
370
+ description: tagMeta.get(name) ?? "",
371
+ name,
372
+ // The same slugger instance, so every tag resolves to the slug its
373
+ // operations were routed under.
374
+ slug: slugForTag(name),
375
+ })),
322
376
  });
323
377
 
324
378
  return { add, finish };
@@ -328,7 +382,8 @@ export const operationCollector = (
328
382
  * Flatten a 3.1 document into a route-mapped operation list and its ordered
329
383
  * tags. Operations inherit the first tag they declare; keys are de-duplicated so
330
384
  * a repeated `operationId` still yields distinct routes. `warnings` reports
331
- * anything skipped (a `$ref` path item), so missing operations aren't silent.
385
+ * anything skipped (a `$ref` path item, webhooks), so missing operations
386
+ * aren't silent.
332
387
  */
333
388
  export const extractOperations = (
334
389
  document: ApiDocument,
@@ -371,6 +426,15 @@ export const extractOperations = (
371
426
  }
372
427
  }
373
428
 
429
+ // OpenAPI 3.1 webhooks (requests the API sends, not ones it serves) have no
430
+ // page renderer; say so rather than dropping them silently.
431
+ const webhooks = Object.keys(document.webhooks ?? {});
432
+ if (webhooks.length > 0) {
433
+ warnings.push(
434
+ `The spec declares ${webhooks.length === 1 ? "a webhook" : `${webhooks.length} webhooks`} under "webhooks" (${webhooks.join(", ")}); webhooks aren't rendered, so they are missing from the reference.`
435
+ );
436
+ }
437
+
374
438
  return { ...collector.finish(), warnings };
375
439
  };
376
440
 
@@ -90,6 +90,9 @@ const codeSpans = (text: string, tree: Root): [number, number][] => {
90
90
  return spans;
91
91
  };
92
92
 
93
+ /** Stands in for `<` when locating code spans (see `mdxSafe`). */
94
+ const HTML_MASK = "";
95
+
93
96
  /**
94
97
  * Escape MDX-special syntax in prose while leaving code verbatim. A spec is
95
98
  * someone else's content, so a link whose destination isn't a web, mail, or
@@ -101,8 +104,14 @@ const mdxSafe = (text: string): string => {
101
104
  const unsafe = unsafeLinkSpans(tree);
102
105
  const insideUnsafe = (offset: number): boolean =>
103
106
  unsafe.some((span) => offset >= span.start && offset < span.end);
107
+ // Code spans come from a parse with every `<` masked. The emitted MDX
108
+ // escapes each `<` outside code to `&lt;`, so no line of it is an HTML
109
+ // block — but CommonMark reads `<div>` as one and never looks for the code
110
+ // spans inside it, which would leave their braces to be escaped and shown
111
+ // as a literal `&#123;`. The mask is one code unit, so offsets line up.
112
+ const masked = text.replaceAll("<", HTML_MASK);
104
113
  const segments = [
105
- ...codeSpans(text, tree)
114
+ ...codeSpans(masked, fromMarkdown(masked))
106
115
  .filter(([start]) => !insideUnsafe(start))
107
116
  .map(([start, end]) => ({ end, start, text: text.slice(start, end) })),
108
117
  ...unsafe,