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
package/src/og/derive.ts CHANGED
@@ -2,9 +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. Locales
6
- * in scripts that font can't draw (Japanese, Hindi, Russian, …) add a Noto
7
- * fallback for their script either way.
5
+ * sites keep Takumi's built-in font. Either way every card gets a Noto
6
+ * fallback stack for the scripts that font can't draw (Japanese, Hindi,
7
+ * Russian, …), which it fetches only when its text needs one.
8
8
  */
9
9
 
10
10
  import { existsSync } from "node:fs";
@@ -19,9 +19,10 @@ import type {
19
19
  import { GOOGLE_FONTS, isFontSlug, localeFontSubsets } from "../theme/fonts.ts";
20
20
  import type { OgFont, OgFontFamilies, OgLocalFont } from "./card.ts";
21
21
 
22
- /** Fonts plus per-role families for the generated OG endpoint. */
22
+ /** Fonts, script fallbacks, and per-role families for the OG endpoint. */
23
23
  export interface DerivedOgFonts {
24
24
  families?: OgFontFamilies;
25
+ fallbacks?: OgFallbackFont[];
25
26
  fonts: OgFont[];
26
27
  }
27
28
 
@@ -165,17 +166,17 @@ export const deriveOgFonts = (
165
166
  /**
166
167
  * The family Takumi renders a card in when no font is loaded: its embedded
167
168
  * 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.
169
+ * face once fallbacks are loaded, since Takumi otherwise tries loaded fonts
170
+ * first; a glyph Geist lacks still falls back to them. Were Takumi to rename
171
+ * it, the name would resolve to nothing and cards would fall back to the
172
+ * loaded fonts, never to tofu.
172
173
  */
173
174
  const BUILT_IN_FAMILY = "Geist";
174
175
 
175
176
  /**
176
177
  * The Google Noto family that draws each language's script, keyed by BCP 47
177
178
  * language subtag, for scripts beyond the Latin, Cyrillic, Greek, and
178
- * Vietnamese that `Noto Sans` covers (see {@link localeOgFonts}). Keep keys
179
+ * Vietnamese that `Noto Sans` covers (see {@link localeFamily}). Keep keys
179
180
  * alphabetical.
180
181
  */
181
182
  const SCRIPT_FAMILIES = {
@@ -219,8 +220,8 @@ const isScriptLanguage = (
219
220
  ): language is keyof typeof SCRIPT_FAMILIES =>
220
221
  Object.hasOwn(SCRIPT_FAMILIES, language);
221
222
 
222
- /** A locale fallback: a Google family at the weights the card renders. */
223
- export interface LocaleOgFont {
223
+ /** A script fallback: a Google family at the weights the card renders. */
224
+ export interface OgFallbackFont {
224
225
  name: string;
225
226
  weight: number[];
226
227
  }
@@ -246,16 +247,28 @@ const localeFamily = (locale: string): string | null => {
246
247
  };
247
248
 
248
249
  /**
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.
250
+ * Every script's fallback, in the order a card tries them: `Noto Sans` for
251
+ * extended Latin, Cyrillic, Greek, and Vietnamese, then a family per script.
252
+ * The CJK families all draw Han ideographs, so the first one wins them —
253
+ * Japanese forms, unless a configured locale moves its own family ahead.
254
254
  */
255
- export const localeOgFonts = (locales: string[]): LocaleOgFont[] => {
255
+ const DEFAULT_FALLBACKS = [
256
+ "Noto Sans",
257
+ ...new Set(Object.values(SCRIPT_FAMILIES)),
258
+ "Noto Sans TC",
259
+ ];
260
+
261
+ /**
262
+ * The card's script fallbacks, at the card's weights: each configured
263
+ * locale's family first, so a Chinese site draws Han in Chinese forms, then
264
+ * every other script's, so a page in a language the site never configured
265
+ * still renders instead of tofu. Takumi's built-in font covers only basic
266
+ * Latin; the card fetches these only when its text needs a glyph beyond it,
267
+ * and then only the families and subsets that glyph needs.
268
+ */
269
+ export const ogFallbackFonts = (locales: string[]): OgFallbackFont[] => {
256
270
  const names = new Set<string>();
257
- for (const locale of locales) {
258
- const family = localeFamily(locale);
271
+ for (const family of [...locales.map(localeFamily), ...DEFAULT_FALLBACKS]) {
259
272
  if (family) {
260
273
  names.add(family);
261
274
  }
@@ -272,27 +285,24 @@ const ogFontName = (font: OgFont): string =>
272
285
  isGoogleFamilyName(font) ? font : font.name;
273
286
 
274
287
  /**
275
- * `derived` plus the locale fallbacks it doesn't already load. A card that
288
+ * `derived` plus the script fallbacks it doesn't already load. A card that
276
289
  * names no family is pinned to {@link BUILT_IN_FAMILY}, so its Latin text
277
290
  * keeps the face it had before any fallback was loaded.
278
291
  */
279
- const withLocaleFonts = (
292
+ const withFallbacks = (
280
293
  derived: DerivedOgFonts,
281
294
  locales: string[]
282
295
  ): DerivedOgFonts => {
283
296
  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
297
  return {
298
+ fallbacks: ogFallbackFonts(locales).filter(
299
+ (font) => !loaded.has(font.name)
300
+ ),
291
301
  families: derived.families ?? {
292
302
  body: BUILT_IN_FAMILY,
293
303
  title: BUILT_IN_FAMILY,
294
304
  },
295
- fonts: [...derived.fonts, ...fallbacks],
305
+ fonts: derived.fonts,
296
306
  };
297
307
  };
298
308
 
@@ -307,8 +317,8 @@ export const resolveOgFontSources = (fonts: OgFont[], root: string): OgFont[] =>
307
317
  * always wins (including `[]` to opt out, keeping the card's role styling
308
318
  * untouched); otherwise a site that explicitly set `theme.fonts` gets its
309
319
  * 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.
320
+ * and either way every card gets the script fallbacks, the configured
321
+ * locales' first.
312
322
  */
313
323
  export const resolveOgFonts = (
314
324
  options: {
@@ -328,7 +338,7 @@ export const resolveOgFonts = (
328
338
  const derived = options.themeFontsConfigured
329
339
  ? deriveOgFonts(options.themeFonts, root)
330
340
  : { fonts: [] };
331
- return withLocaleFonts(derived, options.locales ?? []);
341
+ return withFallbacks(derived, options.locales ?? []);
332
342
  };
333
343
 
334
344
  /**
package/src/og/index.ts CHANGED
@@ -11,4 +11,5 @@ export type {
11
11
  OgCardPalette,
12
12
  OgFont,
13
13
  OgFontFamilies,
14
+ OgGoogleFont,
14
15
  } from "./card.ts";
@@ -46,7 +46,8 @@ export interface AsyncApiChannelObject {
46
46
  messages?: Record<string, AsyncApiRefLike>;
47
47
  parameters?: Record<string, AsyncApiRefLike>;
48
48
  servers?: AsyncApiRefLike[];
49
- bindings?: Record<string, Record<string, AsyncApiSpecValue>>;
49
+ /** Protocol-keyed binding objects, or a `$ref` to a components entry. */
50
+ bindings?: Record<string, AsyncApiSpecValue>;
50
51
  [key: string]: AsyncApiSpecValue;
51
52
  }
52
53
 
@@ -61,7 +62,8 @@ export interface AsyncApiOperationObject {
61
62
  tags?: { name?: string; description?: string }[];
62
63
  security?: AsyncApiRefLike[];
63
64
  messages?: AsyncApiRefLike[];
64
- bindings?: Record<string, Record<string, AsyncApiSpecValue>>;
65
+ /** Protocol-keyed binding objects, or a `$ref` to a components entry. */
66
+ bindings?: Record<string, AsyncApiSpecValue>;
65
67
  [key: string]: AsyncApiSpecValue;
66
68
  }
67
69
 
@@ -260,9 +260,18 @@ const isOperation = (
260
260
  value: OperationObject | undefined
261
261
  ): value is OperationObject => typeof value === "object" && value !== null;
262
262
 
263
- /** A declared tag whose `name` really is a string at runtime, type aside. */
264
- const hasTagName = (tag: SpecTag): tag is SpecTag =>
265
- typeof tag.name === "string";
263
+ /** Whether a spec value is text YAML may have read as a number. */
264
+ const isText = <Value>(value: Value): value is Value & (string | number) =>
265
+ typeof value === "string" || typeof value === "number";
266
+
267
+ /**
268
+ * A spec field that names something, as a string. YAML reads an unquoted
269
+ * `operationId: 404` or `tags: [2024]` as a number, which still names the
270
+ * operation or tag; anything else a hand-written spec puts there counts as
271
+ * absent.
272
+ */
273
+ const specText = <Value>(value: Value): string | undefined =>
274
+ isText(value) ? String(value) : undefined;
266
275
 
267
276
  /**
268
277
  * Assign each distinct tag name a unique slug. `slugify` can collapse
@@ -295,9 +304,6 @@ export const tagSlugger = (): ((name: string) => string) => {
295
304
  /** An operation before the collector assigns its unique key and route. */
296
305
  type CollectedOperation = Omit<ApiOperationRef, "route" | "tagSlug">;
297
306
 
298
- /** A document's declared tag entry (`tags[n]`). */
299
- type SpecTag = NonNullable<ApiDocument["tags"]>[number];
300
-
301
307
  /** The flattened output both extractors produce. */
302
308
  export interface CollectedOperations {
303
309
  operations: ApiOperationRef[];
@@ -391,9 +397,10 @@ export const extractOperations = (
391
397
  ): ExtractedOperations => {
392
398
  const warnings: string[] = [];
393
399
  const tagMeta = new Map(
394
- (document.tags ?? [])
395
- .filter(hasTagName)
396
- .map((tag) => [tag.name, tag.description ?? ""])
400
+ (document.tags ?? []).flatMap((tag): [string, string][] => {
401
+ const name = specText(tag.name);
402
+ return name === undefined ? [] : [[name, tag.description ?? ""]];
403
+ })
397
404
  );
398
405
  const collector = operationCollector(baseRoute, tagMeta);
399
406
 
@@ -413,15 +420,16 @@ export const extractOperations = (
413
420
  if (!isOperation(operation)) {
414
421
  continue;
415
422
  }
423
+ const operationId = specText(operation.operationId);
416
424
  collector.add({
417
425
  deprecated: operation.deprecated ?? false,
418
- description: operation.description ?? "",
419
- key: operationKey(method, path, operation.operationId),
426
+ description: specText(operation.description) ?? "",
427
+ key: operationKey(method, path, operationId),
420
428
  method,
421
- operationId: operation.operationId,
429
+ operationId,
422
430
  path,
423
- summary: operation.summary ?? "",
424
- tag: operation.tags?.[0] ?? UNTAGGED,
431
+ summary: specText(operation.summary) ?? "",
432
+ tag: specText(operation.tags?.[0]) ?? UNTAGGED,
425
433
  });
426
434
  }
427
435
  }
@@ -12,25 +12,52 @@
12
12
  * network and stays safe to bundle into the generated endpoint file.
13
13
  */
14
14
 
15
+ import { PROXY_HEADERS_HEADER } from "../components/openapi/request.ts";
15
16
  import { readCappedBody } from "../core/request-body.ts";
16
17
 
17
18
  /**
18
- * Request headers never forwarded upstream: hop-by-hop headers describe this
19
- * connection (not the upstream one), `host`/`origin`/`referer` would leak or
20
- * misattribute the docs site, and `cookie` would forward reader credentials
21
- * to an arbitrary target. `accept-encoding`/`content-length` are recomputed
22
- * by the runtime's own fetch.
19
+ * Request headers never forwarded upstream, even when the playground names
20
+ * them: hop-by-hop headers describe this connection (not the upstream one),
21
+ * `host`/`origin`/`referer` would leak or misattribute the docs site, `cookie`
22
+ * would forward reader credentials to an arbitrary target, and the rest are
23
+ * set or rewritten by the platform in front of the docs server — the
24
+ * reader's address, the edge's own identity — whatever the browser sent.
25
+ * `accept-encoding`/`content-length` are recomputed by the runtime's own
26
+ * fetch.
23
27
  */
24
28
  const REQUEST_DROP = {
25
29
  "accept-encoding": true,
30
+ "cdn-loop": true,
26
31
  connection: true,
27
32
  "content-length": true,
28
33
  cookie: true,
34
+ forwarded: true,
29
35
  host: true,
36
+ "keep-alive": true,
30
37
  origin: true,
38
+ "proxy-authorization": true,
31
39
  referer: true,
40
+ te: true,
41
+ trailer: true,
42
+ "transfer-encoding": true,
43
+ "true-client-ip": true,
44
+ upgrade: true,
45
+ via: true,
46
+ "x-client-ip": true,
47
+ "x-real-ip": true,
32
48
  } satisfies Record<string, true>;
33
49
 
50
+ /**
51
+ * Platform header families, dropped like {@link REQUEST_DROP}: Cloudflare's
52
+ * (`cf-connecting-ip`, the Access `cf-access-jwt-assertion`), the
53
+ * `x-forwarded-*` set, Vercel's (`x-vercel-oidc-token`), Netlify's, Fly's,
54
+ * and AWS load balancers' (`x-amzn-oidc-data`).
55
+ */
56
+ const PLATFORM_HEADER = /^(?:cf-|x-forwarded-|x-vercel-|x-nf-|fly-|x-amzn-)/u;
57
+
58
+ /** The {@link PROXY_HEADERS_HEADER} name as `Headers` iterates it. */
59
+ const DECLARED = PROXY_HEADERS_HEADER.toLowerCase();
60
+
34
61
  /**
35
62
  * Upstream response headers never returned to the browser: the runtime's
36
63
  * fetch already decoded the body (so `content-encoding`/`content-length` no
@@ -99,20 +126,45 @@ const DOCUMENT_TYPE =
99
126
  const badRequest = (error: string): Response =>
100
127
  Response.json({ error }, { status: 400 });
101
128
 
102
- /** Copy headers, skipping the given denylist (names are already lowercase). */
129
+ /**
130
+ * Copy headers, skipping the given denylist and any name `keep` rejects
131
+ * (names are already lowercase).
132
+ */
103
133
  const filterHeaders = (
104
134
  source: Headers,
105
- drop: Record<string, true>
135
+ drop: Record<string, true>,
136
+ keep: (name: string) => boolean = () => true
106
137
  ): Headers => {
107
138
  const headers = new Headers();
108
139
  for (const [name, value] of source) {
109
- if (!drop[name]) {
140
+ if (!drop[name] && keep(name)) {
110
141
  headers.set(name, value);
111
142
  }
112
143
  }
113
144
  return headers;
114
145
  };
115
146
 
147
+ /**
148
+ * The headers to send upstream: only those the playground set itself, which
149
+ * it names in {@link PROXY_HEADERS_HEADER}. Forwarding everything else minus
150
+ * a denylist would hand the documented API whatever the browser and the
151
+ * platform attach on their own — the HTTP Basic credentials of a docs site
152
+ * behind a password (a same-origin fetch with no `Authorization` of its own
153
+ * carries them), a Cloudflare Access assertion, a Vercel OIDC token.
154
+ */
155
+ const forwardedHeaders = (source: Headers): Headers => {
156
+ const named = new Set(
157
+ (source.get(DECLARED) ?? "")
158
+ .split(",")
159
+ .map((name) => name.trim().toLowerCase())
160
+ );
161
+ return filterHeaders(
162
+ source,
163
+ REQUEST_DROP,
164
+ (name) => named.has(name) && !PLATFORM_HEADER.test(name)
165
+ );
166
+ };
167
+
116
168
  /** A 403 for a target no configured spec declares as one of its servers. */
117
169
  const forbidden = (origin: string): Response =>
118
170
  Response.json(
@@ -186,7 +238,8 @@ const followUpstream = async (args: {
186
238
  /**
187
239
  * Build the `/_api-proxy` fetch handler. The client sends its REAL method,
188
240
  * headers, and body to `?url=<encodeURIComponent(target)>`; the handler
189
- * forwards them (minus {@link REQUEST_DROP}) and mirrors the upstream response
241
+ * forwards the method, the body, and the headers the client names (see
242
+ * {@link forwardedHeaders}), and mirrors the upstream response
190
243
  * (minus {@link RESPONSE_DROP}) with an `x-blume-proxy` marker. An unreachable
191
244
  * upstream is a 502 with a JSON `error`.
192
245
  *
@@ -252,7 +305,7 @@ export const createPlaygroundProxyHandler = (
252
305
  allowed,
253
306
  body,
254
307
  fetchImpl,
255
- headers: filterHeaders(request.headers, REQUEST_DROP),
308
+ headers: forwardedHeaders(request.headers),
256
309
  hop: 0,
257
310
  method: request.method,
258
311
  signal: AbortSignal.timeout(timeoutMs),
@@ -126,6 +126,45 @@ const mdxSafe = (text: string): string => {
126
126
  return out + escapeProse(text.slice(cursor));
127
127
  };
128
128
 
129
+ // A fence opener: up to three columns of indentation, then a run of three or
130
+ // more backticks or tildes.
131
+ const FENCE_OPEN = /^ {0,3}(?<fence>`{3,}|~{3,})/u;
132
+
133
+ /**
134
+ * Close a fenced code block the text leaves open. An unclosed fence runs to
135
+ * the end of the document, so the component appended after a description
136
+ * would render as code, and the page would lose its parameters, responses,
137
+ * and playground. Only the last top-level block can run that far — a fence in
138
+ * a quote or list closes with its container — and it's read from the same
139
+ * `<`-masked parse as `mdxSafe`, since the emitted MDX has no HTML blocks to
140
+ * hide a fence in.
141
+ */
142
+ const closeOpenFence = (text: string): string => {
143
+ const last = fromMarkdown(text.replaceAll("<", HTML_MASK)).children.at(-1);
144
+ if (last?.type !== "code") {
145
+ return text;
146
+ }
147
+ // fromMarkdown always stamps positions; 0 is an unreachable guard.
148
+ const block = text.slice(last.position?.start.offset ?? 0);
149
+ const fence = FENCE_OPEN.exec(block)?.groups?.fence;
150
+ // No fence: indented code, which MDX reads as a paragraph.
151
+ if (!fence) {
152
+ return text;
153
+ }
154
+ const lines = block.split("\n");
155
+ const closer = new RegExp(
156
+ `^ {0,3}${fence[0]}{${fence.length},}[ \\t\\r]*$`,
157
+ "u"
158
+ );
159
+ return lines.length > 1 && closer.test(lines.at(-1) ?? "")
160
+ ? text
161
+ : `${text}\n${fence}`;
162
+ };
163
+
164
+ /** Spec prose as MDX that is safe to follow with a component. */
165
+ const descriptionMdx = (text: string): string =>
166
+ mdxSafe(closeOpenFence(text.trim()));
167
+
129
168
  /**
130
169
  * Frontmatter emitted for one operation or overview page. Boolean flags are
131
170
  * assigned only when set, so absent keys stay absent in the staged MDX.
@@ -249,7 +288,7 @@ const operationDescription = (
249
288
  /** Prepend a markdown description (if any) above a component invocation. */
250
289
  const withDescription = (description: string, component: string): string =>
251
290
  description.trim()
252
- ? `${mdxSafe(description.trim())}\n\n${component}`
291
+ ? `${descriptionMdx(description)}\n\n${component}`
253
292
  : component;
254
293
 
255
294
  export const operationMdx = (
@@ -365,7 +404,7 @@ export const overviewMdx = (
365
404
  continue;
366
405
  }
367
406
  const description = tag.description.trim()
368
- ? [mdxSafe(tag.description.trim())]
407
+ ? [descriptionMdx(tag.description)]
369
408
  : [];
370
409
  tagSections.push(
371
410
  [