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
@@ -1,3 +1,14 @@
1
+ import {
2
+ defaultExplode,
3
+ defaultStyle,
4
+ isJsonObject,
5
+ memberText,
6
+ parseStyledValue,
7
+ queryPairs,
8
+ styledValue,
9
+ templateValue,
10
+ } from "./param-style.ts";
11
+ import type { JsonValue } from "./param-style.ts";
1
12
  import type { RequestSample } from "./snippets.ts";
2
13
 
3
14
  /**
@@ -10,7 +21,18 @@ import type { RequestSample } from "./snippets.ts";
10
21
  * byte-for-byte what gets sent.
11
22
  */
12
23
 
13
- export interface PlaygroundParam {
24
+ /**
25
+ * How a parameter or form-body field serializes, as far as the spec says:
26
+ * the request builder applies the OpenAPI defaults for what it leaves out.
27
+ */
28
+ export interface ParamSerialization {
29
+ /** The spec's `style`; else the location's default. */
30
+ style?: string;
31
+ /** The spec's `explode`; else the style's default. */
32
+ explode?: boolean;
33
+ }
34
+
35
+ export interface PlaygroundParam extends ParamSerialization {
14
36
  name: string;
15
37
  in: "path" | "query" | "header";
16
38
  required: boolean;
@@ -49,9 +71,14 @@ export interface PlaygroundBody {
49
71
  contentType: string;
50
72
  /** Present when the schema is a flat object of primitives -> typed fields UI. */
51
73
  fields?: PlaygroundBodyField[];
52
- /** Pretty-printed example JSON prefill for the raw editor; "" when none. */
74
+ /**
75
+ * Prefill for the raw editor: pretty-printed example JSON, or a raw media
76
+ * type's (`text/plain`, XML) string example verbatim; "" when none.
77
+ */
53
78
  example: string;
54
79
  schema?: ValidationSchema;
80
+ /** A form-urlencoded body's per-field `encoding` (`style`/`explode`). */
81
+ encoding?: Record<string, ParamSerialization>;
55
82
  }
56
83
 
57
84
  export type AuthKind = "bearer" | "basic" | "apiKey" | "oauth2";
@@ -103,6 +130,38 @@ export interface RequestValues {
103
130
  export const paramKey = (p: { in: string; name: string }): string =>
104
131
  `${p.in}:${p.name}`;
105
132
 
133
+ /**
134
+ * The request header a proxied send names the headers the playground set in,
135
+ * comma-separated. Blume's built-in proxy forwards those and nothing else, so
136
+ * credentials the browser attaches on its own — HTTP Basic auth for a docs
137
+ * site behind a password, platform headers added in front of the docs server
138
+ * — never reach the documented API.
139
+ */
140
+ export const PROXY_HEADERS_HEADER = "X-Blume-Proxy-Headers";
141
+
142
+ /**
143
+ * How a body's media type is put on the wire: JSON as the editor text,
144
+ * `application/x-www-form-urlencoded` as `name=value` pairs, and
145
+ * `multipart/form-data` as form parts — both from the editor's JSON object —
146
+ * and any other type (`text/plain`, XML) as the raw text.
147
+ */
148
+ export type BodyEncoding = "json" | "form" | "multipart" | "raw";
149
+
150
+ /** A `+json` suffix or a `json` subtype: `application/json`, `application/vnd.api+json`. */
151
+ const JSON_MEDIA_TYPE = /[+/]json$/u;
152
+
153
+ /** The {@link BodyEncoding} a body's media type (parameters ignored) takes. */
154
+ export const bodyEncoding = (contentType: string): BodyEncoding => {
155
+ const type = (contentType.split(";")[0] ?? "").trim().toLowerCase();
156
+ if (type === "application/x-www-form-urlencoded") {
157
+ return "form";
158
+ }
159
+ if (type === "multipart/form-data") {
160
+ return "multipart";
161
+ }
162
+ return JSON_MEDIA_TYPE.test(type) ? "json" : "raw";
163
+ };
164
+
106
165
  /** Values pre-filled from the model's precomputed examples; auth entries empty. */
107
166
  export const defaultValues = (model: PlaygroundModel): RequestValues => ({
108
167
  auth: Object.fromEntries(
@@ -197,48 +256,119 @@ const applyAuth = (
197
256
  }
198
257
  };
199
258
 
200
- /** The raw editor text to send, with a parsed mirror when it is valid JSON. */
259
+ /**
260
+ * A form-urlencoded body's `name=value` pairs: each field serialized like a
261
+ * query parameter, under the `encoding` the spec declares for it (`form`,
262
+ * exploded, by default — so a list repeats its name).
263
+ */
264
+ const urlencodedBody = (
265
+ value: { [key: string]: JsonValue },
266
+ encoding: PlaygroundBody["encoding"]
267
+ ): string =>
268
+ Object.entries(value)
269
+ .flatMap(([name, member]) => {
270
+ const style = encoding?.[name]?.style ?? "form";
271
+ return queryPairs(
272
+ name,
273
+ styledValue(member),
274
+ style,
275
+ encoding?.[name]?.explode ?? defaultExplode(style)
276
+ );
277
+ })
278
+ .join("&");
279
+
280
+ /** Multipart form parts in order: a list contributes one part per item. */
281
+ const multipartParts = (value: {
282
+ [key: string]: JsonValue;
283
+ }): [string, string][] =>
284
+ Object.entries(value).flatMap(([name, member]) =>
285
+ Array.isArray(member)
286
+ ? member.map((item): [string, string] => [name, memberText(item)])
287
+ : [[name, memberText(member)]]
288
+ );
289
+
290
+ /**
291
+ * The editor text serialized for the body's media type, with a parsed mirror
292
+ * when it is valid JSON. A form body is built from the editor's JSON object;
293
+ * text that isn't one (mid-edit, or a raw media type) is sent as written.
294
+ */
201
295
  const bodyFor = (
296
+ body: PlaygroundBody,
202
297
  text: string | undefined
203
- ): Pick<RequestSample, "body" | "bodyValue"> => {
298
+ ): Pick<RequestSample, "body" | "bodyValue" | "formData"> => {
204
299
  if (text === undefined || text === "") {
205
300
  return {};
206
301
  }
302
+ const encoding = bodyEncoding(body.contentType);
303
+ if (encoding === "raw") {
304
+ return { body: text };
305
+ }
306
+ let value: JsonValue;
207
307
  try {
208
- return { body: text, bodyValue: JSON.parse(text) };
308
+ value = JSON.parse(text);
209
309
  } catch {
210
- // Not valid JSON (mid-edit or intentionally raw): still send it — the
211
- // structured mirror is only a nicety for consumers of `bodyValue`.
310
+ // Not valid JSON (mid-edit): still send it — the structured mirror is
311
+ // only a nicety for consumers of `bodyValue`.
212
312
  return { body: text };
213
313
  }
314
+ if (encoding === "json" || !isJsonObject(value)) {
315
+ return { body: text, bodyValue: value };
316
+ }
317
+ return encoding === "form"
318
+ ? { body: urlencodedBody(value, body.encoding), bodyValue: value }
319
+ : { bodyValue: value, formData: multipartParts(value) };
214
320
  };
215
321
 
216
- /** THE one request builder: samples, copy buttons, and fetch all consume its output. */
217
- export const buildRequest = (
218
- model: PlaygroundModel,
219
- values: RequestValues
220
- ): RequestSample => {
221
- const base = values.server.replace(TRAILING_SLASH, "");
322
+ /**
323
+ * A path or header parameter's value under its `style`: an array or object
324
+ * parameter's JSON text becomes `dog,cat` (simple), `.dog.cat` (label), or
325
+ * `;tags=dog;tags=cat` (matrix) rather than its JSON.
326
+ */
327
+ const styledParam = (
328
+ param: PlaygroundParam,
329
+ value: string,
330
+ encode: (text: string) => string
331
+ ): string => {
332
+ const style = param.style ?? defaultStyle(param.in);
333
+ return templateValue(
334
+ param.name,
335
+ parseStyledValue(value, param.type),
336
+ style,
337
+ param.explode ?? defaultExplode(style),
338
+ encode
339
+ );
340
+ };
222
341
 
223
- // Path params: an empty value substitutes the raw param name, so a blank
224
- // form still renders a readable templated URL instead of `//`.
225
- let resolvedPath = model.path;
342
+ /**
343
+ * The model's path with each `{param}` filled in. An empty value substitutes
344
+ * the raw param name, so a blank form still renders a readable templated URL
345
+ * instead of `//`.
346
+ */
347
+ const resolvePath = (model: PlaygroundModel, values: RequestValues): string => {
348
+ let resolved = model.path;
226
349
  for (const param of model.params) {
227
350
  if (param.in !== "path") {
228
351
  continue;
229
352
  }
230
353
  const value = values.params[paramKey(param)] ?? "";
231
- resolvedPath = resolvedPath.replace(
354
+ resolved = resolved.replace(
232
355
  `{${param.name}}`,
233
- value === "" ? param.name : encodeURIComponent(value)
356
+ value === "" ? param.name : styledParam(param, value, encodeURIComponent)
234
357
  );
235
358
  }
359
+ return resolved;
360
+ };
236
361
 
237
- // Query params with a value contribute in model order; a query-borne auth
238
- // credential appends after — unless the spec also declares that name as an
239
- // explicit query parameter, whose (better) example wins.
362
+ /**
363
+ * The query parameters' `name=value` pairs in model order, each name added to
364
+ * `seen` so a query-borne credential can defer to it.
365
+ */
366
+ const queryParams = (
367
+ model: PlaygroundModel,
368
+ values: RequestValues,
369
+ seen: Set<string>
370
+ ): string[] => {
240
371
  const query: string[] = [];
241
- const seen = new Set<string>();
242
372
  for (const param of model.params) {
243
373
  if (param.in !== "query") {
244
374
  continue;
@@ -251,10 +381,37 @@ export const buildRequest = (
251
381
  continue;
252
382
  }
253
383
  seen.add(param.name);
384
+ const style = param.style ?? defaultStyle(param.in);
254
385
  query.push(
255
- `${encodeURIComponent(param.name)}=${encodeURIComponent(value)}`
386
+ ...queryPairs(
387
+ param.name,
388
+ parseStyledValue(value, param.type),
389
+ style,
390
+ param.explode ?? defaultExplode(style)
391
+ )
256
392
  );
257
393
  }
394
+ return query;
395
+ };
396
+
397
+ /** THE one request builder: samples, copy buttons, and fetch all consume its output. */
398
+ export const buildRequest = (
399
+ model: PlaygroundModel,
400
+ values: RequestValues
401
+ ): RequestSample => {
402
+ // The server's trailing slash only goes when a path is joined onto it: a
403
+ // GraphQL endpoint (no path) is sent exactly as configured, and
404
+ // `https://host/graphql/` is not always the same route as `…/graphql`.
405
+ const base =
406
+ model.path === ""
407
+ ? values.server
408
+ : values.server.replace(TRAILING_SLASH, "");
409
+
410
+ // Query params with a value contribute in model order; a query-borne auth
411
+ // credential appends after — unless the spec also declares that name as an
412
+ // explicit query parameter, whose (better) example wins.
413
+ const seen = new Set<string>();
414
+ const query = queryParams(model, values, seen);
258
415
 
259
416
  // Auth first, so a spec that also declares the credential as an explicit
260
417
  // header parameter overrides it below with its own (better) example.
@@ -268,12 +425,18 @@ export const buildRequest = (
268
425
  // Required headers emit even when blank, mirroring the query rule above —
269
426
  // except a blank one never clobbers a credential auth already placed
270
427
  // under the same name.
271
- if (value !== "" || (param.required && headers[param.name] === undefined)) {
428
+ if (value !== "") {
429
+ headers[param.name] = styledParam(param, value, (text) => text);
430
+ } else if (param.required && headers[param.name] === undefined) {
272
431
  headers[param.name] = value;
273
432
  }
274
433
  }
275
434
 
276
- const { body, bodyValue } = model.body ? bodyFor(values.body) : {};
435
+ const { body, bodyValue, formData } = model.body
436
+ ? bodyFor(model.body, values.body)
437
+ : {};
438
+ // Multipart parts carry no Content-Type here: the client (curl, fetch,
439
+ // requests) writes it along with the boundary that separates the parts.
277
440
  if (body !== undefined && model.body) {
278
441
  headers["Content-Type"] = model.body.contentType;
279
442
  }
@@ -282,8 +445,9 @@ export const buildRequest = (
282
445
  return {
283
446
  body,
284
447
  bodyValue,
448
+ formData,
285
449
  headers,
286
450
  method: model.method.toUpperCase(),
287
- url: `${base}${resolvedPath}${search}`,
451
+ url: `${base}${resolvePath(model, values)}${search}`,
288
452
  };
289
453
  };
@@ -55,6 +55,11 @@ export type SchemaNode =
55
55
  kind: "branches";
56
56
  label: "One of" | "Any of";
57
57
  branches: { label: string; slot: SchemaSlot }[];
58
+ /**
59
+ * Properties the schema declares beside its `oneOf`/`anyOf`, which
60
+ * every variant shares; absent when it declares none.
61
+ */
62
+ rows?: SchemaRow[];
58
63
  }
59
64
  | { kind: "properties"; rows: SchemaRow[] }
60
65
  | { kind: "type"; label: string };
@@ -125,19 +130,6 @@ export const schemaTree = (
125
130
  : { itemsLabel: typeLabel({}), kind: "array" },
126
131
  };
127
132
  }
128
- const branches = schema.oneOf ?? schema.anyOf;
129
- if (branches) {
130
- return {
131
- node: {
132
- branches: branches.map((branch, index) => ({
133
- label: typeLabel(branch) || `Option ${index + 1}`,
134
- slot: table(branch, ancestors),
135
- })),
136
- kind: "branches",
137
- label: schema.oneOf ? "One of" : "Any of",
138
- },
139
- };
140
- }
141
133
  // A row discloses nested structure — never a model inside itself.
142
134
  const rowChildren = (property: SchemaLike): SchemaSlot | undefined => {
143
135
  if (
@@ -155,18 +147,32 @@ export const schemaTree = (
155
147
  : undefined;
156
148
  };
157
149
  const { properties, required } = objectProperties(schema, schemas);
158
- if (properties.length > 0) {
159
- return {
160
- node: {
161
- kind: "properties",
162
- rows: properties.map(([name, property]) => ({
163
- children: rowChildren(property),
164
- name,
165
- required: required.has(name),
166
- schema: property,
167
- })),
168
- },
150
+ const rows: SchemaRow[] = properties.map(([name, property]) => ({
151
+ children: rowChildren(property),
152
+ name,
153
+ required: required.has(name),
154
+ schema: property,
155
+ }));
156
+ // Shared properties beside a `oneOf`/`anyOf` (`amount` and `currency`
157
+ // next to `Card | BankAccount`) render above the variants, planned first
158
+ // as they read first.
159
+ const branches = schema.oneOf ?? schema.anyOf;
160
+ if (branches) {
161
+ const node: Extract<SchemaNode, { kind: "branches" }> = {
162
+ branches: branches.map((branch, index) => ({
163
+ label: typeLabel(branch) || `Option ${index + 1}`,
164
+ slot: table(branch, ancestors),
165
+ })),
166
+ kind: "branches",
167
+ label: schema.oneOf ? "One of" : "Any of",
169
168
  };
169
+ if (rows.length > 0) {
170
+ node.rows = rows;
171
+ }
172
+ return { node };
173
+ }
174
+ if (rows.length > 0) {
175
+ return { node: { kind: "properties", rows } };
170
176
  }
171
177
  return { node: { kind: "type", label: typeLabel(schema) } };
172
178
  };
@@ -11,9 +11,18 @@ export interface RequestSample {
11
11
  method: string;
12
12
  url: string;
13
13
  headers: Record<string, string>;
14
- /** JSON-stringified request body, when the operation takes one. */
14
+ /**
15
+ * The request body as sent, when the operation takes one: JSON text,
16
+ * form-urlencoded pairs, or a raw media type's text.
17
+ */
15
18
  body?: string;
16
19
  bodyValue?: unknown;
20
+ /**
21
+ * A `multipart/form-data` body's parts as `[name, value]` pairs, set
22
+ * instead of `body`. The request carries no Content-Type of its own for
23
+ * them: the client writes one with the parts' boundary.
24
+ */
25
+ formData?: [string, string][];
17
26
  }
18
27
 
19
28
  const headerLines = (
@@ -40,7 +49,11 @@ const stringLiteral = (value: string): string => JSON.stringify(value);
40
49
 
41
50
  const curlSnippet = (sample: RequestSample): string => {
42
51
  const lines = [
43
- `curl -X ${sample.method} ${shellQuote(sample.url)}`,
52
+ // `-X HEAD` sends a HEAD but still waits for the body the response
53
+ // announces, so the command hangs; `--head` knows there is none.
54
+ sample.method === "HEAD"
55
+ ? `curl --head ${shellQuote(sample.url)}`
56
+ : `curl -X ${sample.method} ${shellQuote(sample.url)}`,
44
57
  ...headerLines(
45
58
  sample.headers,
46
59
  (key, value) => ` -H ${shellQuote(`${key}: ${value}`)}`
@@ -49,10 +62,37 @@ const curlSnippet = (sample: RequestSample): string => {
49
62
  if (sample.body) {
50
63
  lines.push(` -d ${shellQuote(sample.body)}`);
51
64
  }
65
+ // `--form-string`, not `-F`: `-F` reads a value starting with `@` or `<` as
66
+ // a file to upload.
67
+ for (const [name, value] of sample.formData ?? []) {
68
+ lines.push(` --form-string ${shellQuote(`${name}=${value}`)}`);
69
+ }
52
70
  return lines.join(" \\\n");
53
71
  };
54
72
 
73
+ /**
74
+ * Whether fetch refuses to send `method` (an upper-case HTTP method). TRACE is
75
+ * one of the Fetch standard's forbidden methods, and the only one an OpenAPI
76
+ * spec can declare: browsers and Node's fetch alike throw a TypeError before
77
+ * any request goes out.
78
+ */
79
+ export const fetchRefusesMethod = (method: string): boolean =>
80
+ method === "TRACE";
81
+
82
+ /**
83
+ * The JavaScript sample for a method fetch refuses: a note in place of a call
84
+ * that could only throw.
85
+ */
86
+ const FETCH_REFUSED_NOTE = [
87
+ "// fetch() refuses the TRACE method: browsers and Node both throw before",
88
+ "// sending, so there's no fetch sample for this operation. Send it with",
89
+ "// curl or another HTTP client instead.",
90
+ ].join("\n");
91
+
55
92
  const fetchSnippet = (sample: RequestSample): string => {
93
+ if (fetchRefusesMethod(sample.method)) {
94
+ return FETCH_REFUSED_NOTE;
95
+ }
56
96
  const options = [` method: ${stringLiteral(sample.method)}`];
57
97
  if (Object.keys(sample.headers).length > 0) {
58
98
  const headers = headerLines(
@@ -71,11 +111,35 @@ const fetchSnippet = (sample: RequestSample): string => {
71
111
  // text that isn't JSON yet.
72
112
  options.push(` body: ${stringLiteral(sample.body)}`);
73
113
  }
74
- return `const response = await fetch(${stringLiteral(sample.url)}, {\n${options.join(
114
+ // A FormData body: fetch writes the multipart Content-Type and boundary.
115
+ let form = "";
116
+ if (sample.formData) {
117
+ form = [
118
+ "const form = new FormData();\n",
119
+ ...sample.formData.map(
120
+ ([name, value]) =>
121
+ `form.append(${stringLiteral(name)}, ${stringLiteral(value)});\n`
122
+ ),
123
+ "\n",
124
+ ].join("");
125
+ options.push(" body: form");
126
+ }
127
+ return `${form}const response = await fetch(${stringLiteral(sample.url)}, {\n${options.join(
75
128
  ",\n"
76
129
  )}\n});`;
77
130
  };
78
131
 
132
+ /** The methods `requests` has a module-level helper for. */
133
+ const PYTHON_METHODS = new Set([
134
+ "delete",
135
+ "get",
136
+ "head",
137
+ "options",
138
+ "patch",
139
+ "post",
140
+ "put",
141
+ ]);
142
+
79
143
  const pythonSnippet = (sample: RequestSample): string => {
80
144
  const args = [` ${stringLiteral(sample.url)}`];
81
145
  if (Object.keys(sample.headers).length > 0) {
@@ -91,9 +155,24 @@ const pythonSnippet = (sample: RequestSample): string => {
91
155
  // `1e400` as `Infinity` and would otherwise diverge from the live send.
92
156
  args.push(` data=${stringLiteral(sample.body)}`);
93
157
  }
94
- return `import requests\n\nresponse = requests.${sample.method.toLowerCase()}(\n${args.join(
95
- ",\n"
96
- )},\n)`;
158
+ if (sample.formData) {
159
+ // `files=` is what makes requests encode multipart; a `(None, value)`
160
+ // tuple is a plain field rather than an upload.
161
+ const parts = sample.formData.map(
162
+ ([name, value]) =>
163
+ ` (${stringLiteral(name)}, (None, ${stringLiteral(value)})),\n`
164
+ );
165
+ args.push(` files=[\n${parts.join("")} ]`);
166
+ }
167
+ // requests has a helper per common method; anything else (TRACE) goes
168
+ // through `requests.request` with the method named.
169
+ const method = sample.method.toLowerCase();
170
+ let call = `requests.${method}`;
171
+ if (!PYTHON_METHODS.has(method)) {
172
+ call = "requests.request";
173
+ args.unshift(` ${stringLiteral(sample.method)}`);
174
+ }
175
+ return `import requests\n\nresponse = ${call}(\n${args.join(",\n")},\n)`;
97
176
  };
98
177
 
99
178
  /** A code-sample language: config id -> label, Shiki lang, and builder. */
@@ -57,7 +57,10 @@ export interface WsClientOptions {
57
57
  /** Clock for frame timestamps; defaults to `Date.now`. */
58
58
  now?: () => number;
59
59
  onFrame: (frame: WsFrame) => void;
60
- /** State transitions, with an optional human detail (close reason/code). */
60
+ /**
61
+ * State transitions, with an optional human detail: the close reason/code,
62
+ * or why a URL couldn't be dialed at all.
63
+ */
61
64
  onState: (state: WsState, detail?: string) => void;
62
65
  }
63
66
 
@@ -125,7 +128,20 @@ export const createWsClient = (options: WsClientOptions): WsClient => {
125
128
  return;
126
129
  }
127
130
  transition("connecting");
128
- const next = create(url);
131
+ let next: SocketLike;
132
+ try {
133
+ next = create(url);
134
+ } catch (error) {
135
+ // `new WebSocket` throws synchronously on a URL it can't dial (a bad
136
+ // scheme, a malformed host), before any event could settle the state:
137
+ // left alone, the panel would say "Connecting…" forever with nothing to
138
+ // disconnect.
139
+ transition(
140
+ "error",
141
+ error instanceof Error ? error.message : String(error)
142
+ );
143
+ return;
144
+ }
129
145
  socket = next;
130
146
  // Every listener below is scoped to the socket it was wired for: a browser
131
147
  // can deliver a discarded socket's error/close/message after `disconnect`
@@ -73,28 +73,46 @@ export const isInternalPath = (target: string): boolean =>
73
73
  export const isExternalUrl = (target: string): boolean =>
74
74
  /^https?:\/\//iu.test(target) || target.startsWith("//");
75
75
 
76
+ /**
77
+ * The path part of a root-relative target, before any `?query` or `#fragment`
78
+ * (`/docs#install` -> `/docs`).
79
+ */
80
+ const pathPart = (target: string): string => {
81
+ const at = target.search(/[#?]/u);
82
+ return at === -1 ? target : target.slice(0, at);
83
+ };
84
+
85
+ /**
86
+ * Whether `route`'s path already equals or sits under `base`. Only the path
87
+ * part is compared, so a fragment or query (`/docs#install`) doesn't hide a
88
+ * base written by hand.
89
+ */
90
+ const isUnderBase = (base: string, route: string): boolean => {
91
+ const path = pathPart(route);
92
+ return path === base || path.startsWith(`${base}/`);
93
+ };
94
+
76
95
  /**
77
96
  * Idempotently prepend `basePath` to a root-relative route. A route already
78
97
  * equal to or nested under the base is returned unchanged, so authors who write
79
- * the base by hand (`/docs/x`) aren't double-prefixed to `/docs/docs/x`.
98
+ * the base by hand (`/docs/x`, `/docs#install`) aren't double-prefixed to
99
+ * `/docs/docs/x`.
80
100
  */
81
101
  export const withBasePath = (basePath: string, route: string): string => {
82
- if (!basePath || !isInternalPath(route)) {
83
- return route;
84
- }
85
- if (route === basePath || route.startsWith(`${basePath}/`)) {
102
+ if (!basePath || !isInternalPath(route) || isUnderBase(basePath, route)) {
86
103
  return route;
87
104
  }
88
105
  return route === "/" ? basePath : `${basePath}${route}`;
89
106
  };
90
107
 
91
108
  /**
92
- * Mount a route Blume generates from content under `basePath`,
93
- * unconditionally. {@link withBasePath} leaves a route that already starts
94
- * with the base alone, which is right for a link an author based by hand but
95
- * wrong for a content route: with `basePath: "/docs"`, `docs/guide.md` routes
96
- * to `/docs/guide` before the base and publishes at `/docs/docs/guide`, not on
97
- * top of the root `guide.md`.
109
+ * Mount a route Blume generates under a base, unconditionally: `basePath` for
110
+ * a content route, `deployment.base` for any generated URL (a sitemap entry,
111
+ * a feed item). {@link withBasePath} leaves a route that already starts with
112
+ * the base alone, which is right for a link an author based by hand but wrong
113
+ * for a generated route: with `basePath: "/docs"`, `docs/guide.md` routes to
114
+ * `/docs/guide` before the base and publishes at `/docs/docs/guide`, not on
115
+ * top of the root `guide.md` — and likewise under `deployment.base: "/docs"`.
98
116
  */
99
117
  export const mountBasePath = (basePath: string, route: string): string => {
100
118
  if (!basePath) {
@@ -117,16 +135,44 @@ export const withComposedBasePath = (
117
135
  route: string
118
136
  ): string => {
119
137
  const composed = `${deployBase}${basePath}`;
120
- if (
121
- composed &&
122
- isInternalPath(route) &&
123
- (route === composed || route.startsWith(`${composed}/`))
124
- ) {
138
+ if (composed && isInternalPath(route) && isUnderBase(composed, route)) {
125
139
  return route;
126
140
  }
127
141
  return withBasePath(deployBase, withBasePath(basePath, route));
128
142
  };
129
143
 
144
+ /**
145
+ * A path whose final segment carries a file extension (`/spec.pdf`,
146
+ * `/logo.svg`) — a public asset, unless a page is served there.
147
+ */
148
+ const DOTTED_PATH = /\.[a-z0-9]+$/iu;
149
+
150
+ /**
151
+ * Base a root-relative link an author wrote as if mounted at root — a content
152
+ * link (`markdown/base-links.ts`, `components/content/base-href.ts`) or a
153
+ * redirect target (`deploy/redirects.ts`) — by what it names. A page gains the
154
+ * composed stack ({@link withComposedBasePath}). A public asset — a path whose
155
+ * final segment carries a file extension, with no page served there (a dotted
156
+ * route like `/releases/v1.2` is still a page) — gains the deployment base
157
+ * alone: Astro serves `public/` under `deployment.base`, but never under
158
+ * `basePath`. `servesPage` answers whether a page is served at a
159
+ * `basePath`-prefixed, fragment-less path. Other targets pass through.
160
+ */
161
+ export const withAuthoredBasePath = (
162
+ deployBase: string,
163
+ basePath: string,
164
+ target: string,
165
+ servesPage: (route: string) => boolean
166
+ ): string => {
167
+ if (!isInternalPath(target)) {
168
+ return target;
169
+ }
170
+ const path = pathPart(target);
171
+ return DOTTED_PATH.test(path) && !servesPage(withBasePath(basePath, path))
172
+ ? withBasePath(deployBase, target)
173
+ : withComposedBasePath(deployBase, basePath, target);
174
+ };
175
+
130
176
  /**
131
177
  * Remove `basePath` from the front of a route (`/docs/guide` -> `/guide`,
132
178
  * `/docs` -> `/`). A route not under the base is returned unchanged. Inverse of
@@ -958,13 +958,15 @@ export interface OgConfig {
958
958
  */
959
959
  enabled?: boolean;
960
960
  /**
961
- * Fonts for the generated card, extending Takumi's Latin-only default so
962
- * non-Latin titles (CJK, and so on) render instead of tofu. A bare string is
963
- * a Google Fonts family fetched at build; the name-only object form pins
964
- * weights (`700`, `[400, 700]`, or a `"100..900"` variable range) and
965
- * styles; the `src` form reads a local font file from the project instead.
966
- * When omitted and `theme.fonts` is explicitly configured, the theme's
967
- * display and body fonts are used automatically — pass `[]` to opt out.
961
+ * Fonts for the generated card, replacing the default stack: Takumi's
962
+ * Latin-only built-in font plus a Noto fallback per script, which a card
963
+ * fetches only when its text needs one. A bare string is a Google Fonts
964
+ * family fetched at build; the name-only object form pins weights (`700`,
965
+ * `[400, 700]`, or a `"100..900"` variable range) and styles; the `src`
966
+ * form reads a local font file from the project instead. When omitted and
967
+ * `theme.fonts` is explicitly configured, the theme's display and body
968
+ * fonts are used automatically, ahead of the fallbacks — pass `[]` to opt
969
+ * out of both.
968
970
  */
969
971
  fonts?: (
970
972
  | string