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,9 +1,13 @@
1
1
  ---
2
+ import data from "blume:data";
3
+
2
4
  import { EN_UI } from "../../core/i18n-ui.ts";
3
5
  import type { UIStrings } from "../../core/i18n-ui.ts";
4
6
  import type { Navigation } from "../../core/types.ts";
7
+ import { scopesVersions } from "../../search/adapters/version-scope.ts";
5
8
  import { resolvePopularIconMarkup } from "../../search/popular-icon.ts";
6
9
  import Icon from "../Icon.astro";
10
+ import { withBase, withMountedBase } from "../islands/base-path.ts";
7
11
  import { flattenPages } from "./nav-utils.ts";
8
12
 
9
13
  // The provider-specific loader is generated per project into `blume:search-client`
@@ -21,7 +25,8 @@ interface Props {
21
25
  locale?: string;
22
26
  /**
23
27
  * Docs version to filter results to (`""` = the current docs — a meaningful
24
- * value, so `null`/omitted is what disables version filtering).
28
+ * value, so `null`/omitted is what disables version filtering). Ignored when
29
+ * the configured provider can't scope by version.
25
30
  */
26
31
  version?: string | null;
27
32
  }
@@ -32,27 +37,41 @@ const {
32
37
  popularPages,
33
38
  strings,
34
39
  locale,
35
- version = null,
40
+ version: viewedVersion = null,
36
41
  } = Astro.props;
42
+ // Pagefind, Orama Cloud, and Mixedbread search every version whatever the
43
+ // dialog asks, so on those an "All versions" toggle would change nothing:
44
+ // treat the site as unversioned for search instead.
45
+ const version = scopesVersions(data.config.search.provider)
46
+ ? viewedVersion
47
+ : null;
37
48
  // Merge over the English baseline per key (rather than `strings ?? …`) so a
38
49
  // partial — or empty `{}` — strings object still resolves every label to a
39
50
  // default, matching the pattern PageActions uses for its own dictionary.
40
51
  const s = { ...EN_UI.search, ...strings };
41
52
 
42
53
  // Resolve popular icons to markup here (icon set is server-only). Unknown
43
- // names fall through to the island's file glyph.
54
+ // names fall through to the island's file glyph. Each entry's served href is
55
+ // resolved here too: a configured entry is authored, so a base written by hand
56
+ // is kept (`withBase`); the sidebar fallback links generated routes, which
57
+ // always go under the base (`withMountedBase`).
44
58
  const popular =
45
59
  popularPages && popularPages.length > 0
46
60
  ? popularPages.map((page) => ({
61
+ href: withBase(page.route),
47
62
  icon: resolvePopularIconMarkup(
48
63
  page.icon,
49
64
  import.meta.env.BASE_URL ?? "/"
50
65
  ),
51
66
  label: page.label,
52
- route: page.route,
53
67
  }))
54
68
  : navigation
55
- ? flattenPages(navigation.sidebar).slice(0, 6)
69
+ ? flattenPages(navigation.sidebar)
70
+ .slice(0, 6)
71
+ .map((page) => ({
72
+ href: withMountedBase(page.route),
73
+ label: page.label,
74
+ }))
56
75
  : [];
57
76
 
58
77
  const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
@@ -220,7 +239,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
220
239
  <script>
221
240
  import { navigate } from "astro:transitions/client";
222
241
  import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
223
- import { prefixBase } from "../islands/base-path.ts";
242
+ import { mountBase } from "../islands/base-path.ts";
224
243
  import { escape as escapeHtml } from "html-escaper";
225
244
 
226
245
  import { highlight, matchSnippet } from "./search/types.ts";
@@ -234,10 +253,11 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
234
253
  }
235
254
 
236
255
  interface PopularPage {
256
+ /** The served URL, base included (resolved server-side). */
257
+ href: string;
237
258
  /** Pre-resolved inline SVG from the server; absent falls back to `file`. */
238
259
  icon?: string;
239
260
  label: string;
240
- route: string;
241
261
  }
242
262
 
243
263
  const GRID_COLS = "md:grid-cols-[22rem_minmax(0,1fr)]";
@@ -671,7 +691,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
671
691
  if (this.popular.length > 0) {
672
692
  const group = this.addGroup(this.popularMsg);
673
693
  for (const page of this.popular) {
674
- const item = this.createLinkRow(page.route, page.label, page.icon);
694
+ const item = this.createLinkRow(page.href, page.label, page.icon);
675
695
  group.appendChild(item.el);
676
696
  this.selectables.push(item);
677
697
  }
@@ -755,9 +775,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
755
775
 
756
776
  createHitRow(hit: SearchHit, query: string): Selectable {
757
777
  const el = document.createElement("a");
758
- // Index URLs are base-less logical routes; prefix the deployment base so
759
- // clicking a result lands on the page's real served URL.
760
- const href = prefixBase(import.meta.env.BASE_URL, hit.url);
778
+ // Index URLs are base-less logical routes; mount them under the
779
+ // deployment base so clicking a result lands on the page's real served
780
+ // URL, even when the route starts with the base's own name.
781
+ const href = mountBase(import.meta.env.BASE_URL, hit.url);
761
782
  el.href = href;
762
783
  el.className = `${ROW_CLASS} ${MARK}`;
763
784
  // `line-clamp-2` already sets `display`, so no `block` here (it would
@@ -782,9 +803,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
782
803
  return item;
783
804
  }
784
805
 
785
- createLinkRow(url: string, label: string, icon?: string): Selectable {
806
+ createLinkRow(href: string, label: string, icon?: string): Selectable {
786
807
  const el = document.createElement("a");
787
- const href = prefixBase(import.meta.env.BASE_URL, url);
788
808
  el.href = href;
789
809
  el.className = ROW_CLASS;
790
810
  // `icon` is server-resolved markup (built-in Lucide, `<img>`, or
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { withBase } from "../islands/base-path.ts";
2
+ import { withMountedBase } from "../islands/base-path.ts";
3
3
  import Icon from "../Icon.astro";
4
4
 
5
5
  // The "you're viewing an old version" notice, shared by RootLayout and
@@ -29,7 +29,7 @@ const { notice } = Astro.props;
29
29
  <span>{notice.message}</span>
30
30
  <a
31
31
  class="inline-flex items-center gap-1 font-medium underline underline-offset-2"
32
- href={withBase(notice.latestHref)}
32
+ href={withMountedBase(notice.latestHref)}
33
33
  >
34
34
  {notice.latestLabel}
35
35
  <Icon name="arrow-right" size={14} />
@@ -40,6 +40,9 @@ interface AnalyticsWindow {
40
40
  pirsch?: (event: string, options?: { meta?: TrackProps }) => void;
41
41
  plausible?: (event: string, options?: { props?: TrackProps }) => void;
42
42
  posthog?: { capture?: (event: string, props?: TrackProps) => void };
43
+ stonks?: {
44
+ event?: (event: string, props?: Record<string, string>) => void;
45
+ };
43
46
  }
44
47
 
45
48
  /** Run one provider call; its failure must not reach the others or the caller. */
@@ -85,6 +88,15 @@ const trackGlobals = (
85
88
  attempt(() => w.plausible?.(event, { props }));
86
89
  attempt(() => w.databuddy?.track?.(event, props));
87
90
  attempt(() => w.fathom?.trackEvent?.(event));
91
+ // OneDollarStats only takes string property values.
92
+ attempt(() =>
93
+ w.stonks?.event?.(
94
+ event,
95
+ Object.fromEntries(
96
+ Object.entries(props).map(([key, value]) => [key, String(value)])
97
+ )
98
+ )
99
+ );
88
100
  // Pirsch stringifies `meta` values in place, so it gets its own copy and
89
101
  // the `blume:track` listeners still see the original types.
90
102
  attempt(() => w.pirsch?.(event, { meta: { ...props } }));
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The scrollspy's choice of the active "On this page" entry, kept apart from
3
+ * the `<blume-toc>` element (toc-element.ts) so it runs without a DOM.
4
+ */
5
+
6
+ /** The two measurements the scrollspy takes of a heading. */
7
+ export interface MeasuredHeading {
8
+ getBoundingClientRect: () => { top: number };
9
+ getClientRects: () => { length: number };
10
+ }
11
+
12
+ /**
13
+ * The entry for the section being read: the last heading at or above the
14
+ * trigger line (the first heading before any has reached it), or the last
15
+ * heading once the page is scrolled to the bottom.
16
+ *
17
+ * Only rendered headings compete. A heading inside a hidden tab panel
18
+ * (`display: none`) has no boxes and measures `top: 0`, so it would always
19
+ * read as scrolled past and hold the highlight from the top of the page.
20
+ */
21
+ export const activeTocEntry = <Entry extends { heading: MeasuredHeading }>(
22
+ entries: readonly Entry[],
23
+ triggerOffset: number,
24
+ scrolledToBottom: boolean
25
+ ): Entry | null => {
26
+ const rendered = entries.filter(
27
+ ({ heading }) => heading.getClientRects().length > 0
28
+ );
29
+ if (scrolledToBottom) {
30
+ return rendered.at(-1) ?? null;
31
+ }
32
+ // Headings are in document order, so the last one whose top has reached the
33
+ // trigger line is the section currently being read; default to the first.
34
+ let active = rendered[0] ?? null;
35
+ for (const entry of rendered) {
36
+ if (entry.heading.getBoundingClientRect().top <= triggerOffset) {
37
+ active = entry;
38
+ }
39
+ }
40
+ return active;
41
+ };
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * The active heading is chosen by rect (the last heading at or above a trigger
8
8
  * line just below the sticky header), so it stays correct for sections taller
9
- * than the viewport and at the bottom of the page. An IntersectionObserver is the
9
+ * than the viewport and at the bottom of the page; headings that aren't rendered
10
+ * (inside a hidden tab panel) are skipped (see toc-active.ts). An IntersectionObserver is the
10
11
  * cheap primary trigger — it only fires as headings cross the band near the top —
11
12
  * and a passive, rAF-throttled scroll listener covers the one case it can't: a
12
13
  * final section too short to push its heading past the trigger line.
@@ -14,6 +15,8 @@
14
15
  * Imported for its side effect (registers the element) from RootLayout's script.
15
16
  */
16
17
 
18
+ import { activeTocEntry } from "./toc-active.ts";
19
+
17
20
  interface TocEntry {
18
21
  heading: HTMLElement;
19
22
  link: HTMLAnchorElement;
@@ -93,19 +96,10 @@ class BlumeToc extends HTMLElement {
93
96
  const scrolledToBottom =
94
97
  window.innerHeight + window.scrollY >=
95
98
  document.documentElement.scrollHeight - 2;
96
- if (scrolledToBottom) {
97
- return this.#entries.at(-1)?.link ?? null;
98
- }
99
-
100
- // Headings are in document order, so the last one whose top has reached the
101
- // trigger line is the section currently being read; default to the first.
102
- let active = this.#entries[0]?.link ?? null;
103
- for (const { heading, link } of this.#entries) {
104
- if (heading.getBoundingClientRect().top <= TRIGGER_OFFSET) {
105
- active = link;
106
- }
107
- }
108
- return active;
99
+ return (
100
+ activeTocEntry(this.#entries, TRIGGER_OFFSET, scrolledToBottom)?.link ??
101
+ null
102
+ );
109
103
  }
110
104
  }
111
105
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import specs from "blume:openapi";
3
3
  import { specOf } from "../../openapi/model.ts";
4
- import { withBase } from "../islands/base-path.ts";
4
+ import { withMountedBase } from "../islands/base-path.ts";
5
5
  import MethodBadge from "./MethodBadge.astro";
6
6
 
7
7
  // The operation-link list for one tag section of an API overview page. The
@@ -27,7 +27,7 @@ const operations = Object.values(specOf(specs, source)?.operations ?? {}).filter
27
27
  <li>
28
28
  <a
29
29
  class="flex items-start gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
30
- href={withBase(operation.route)}
30
+ href={withMountedBase(operation.route)}
31
31
  >
32
32
  <MethodBadge class="mt-0.5 shrink-0" method={operation.method} />
33
33
  {/* Title over path, stacked, so a long summary and a long route each
@@ -70,7 +70,7 @@ const security = resolveAsyncApiSecurity(
70
70
  );
71
71
  const parameters = channelParameters(channel, document);
72
72
  const messages = operation ? operationMessages(operation, channel, document) : [];
73
- const protocol = protocolOf(operation, channel, servers);
73
+ const protocol = protocolOf(operation, channel, servers, document.components);
74
74
 
75
75
  /** Declared example first, else a sampled value from the payload schema. */
76
76
  const exampleOf = (message: (typeof messages)[number]["message"]): unknown =>
@@ -115,8 +115,8 @@ const samplePanels = sample
115
115
  ? await languageSamplePanels(languages, sample, data.config.codeThemes)
116
116
  : [];
117
117
 
118
- const operationBindings = bindingGroups(operation?.bindings);
119
- const channelBindings = bindingGroups(channel?.bindings);
118
+ const operationBindings = bindingGroups(operation?.bindings, document.components);
119
+ const channelBindings = bindingGroups(channel?.bindings, document.components);
120
120
  ---
121
121
 
122
122
  {
@@ -144,7 +144,10 @@ const channelBindings = bindingGroups(channel?.bindings);
144
144
  // Headers may be multi-format too; undefined with `headers`
145
145
  // present means an inline-unrenderable schema format.
146
146
  const headers = schemaOf(named.message.headers);
147
- const messageBindings = bindingGroups(named.message.bindings);
147
+ const messageBindings = bindingGroups(
148
+ named.message.bindings,
149
+ document.components
150
+ );
148
151
  return (
149
152
  <section class="mt-6">
150
153
  <div
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { withBase } from "../islands/base-path.ts";
2
+ import { withMountedBase } from "../islands/base-path.ts";
3
3
 
4
4
  /**
5
5
  * A schema name (or full type display like `[Pet!]!`) rendered as a link when
@@ -21,7 +21,7 @@ const { name, route, class: className } = Astro.props;
21
21
  route ? (
22
22
  <a
23
23
  class:list={["font-mono text-accent hover:underline", className]}
24
- href={withBase(route)}
24
+ href={withMountedBase(route)}
25
25
  >
26
26
  {name}
27
27
  </a>
@@ -127,6 +127,7 @@ const security = resolveSecurity(
127
127
  const model =
128
128
  ref && operation
129
129
  ? operationModel({
130
+ components,
130
131
  method: ref.method,
131
132
  parameters: params,
132
133
  path: ref.path,
@@ -195,6 +196,7 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
195
196
  />
196
197
  )}
197
198
  <RequestPanel
199
+ components={components}
198
200
  languages={languages}
199
201
  responses={responses}
200
202
  sample={sample}
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import { stableId } from "../content/tooltip-id.ts";
3
- import { withBase } from "../islands/base-path.ts";
3
+ import { withMountedBase } from "../islands/base-path.ts";
4
4
 
5
5
  import { paramKey, type PlaygroundModel } from "./request.ts";
6
6
 
@@ -25,9 +25,9 @@ interface Props {
25
25
  const { model, proxy, slug, operation } = Astro.props;
26
26
 
27
27
  // The built-in proxy route carries the site `basePath` but, like every
28
- // internal URL in the spec data, not `deployment.base`; `withBase` adds it
29
- // and passes an external proxy URL through untouched.
30
- const proxyUrl = proxy ? withBase(proxy) : undefined;
28
+ // internal URL in the spec data, not `deployment.base`; `withMountedBase`
29
+ // adds it and passes an external proxy URL through untouched.
30
+ const proxyUrl = proxy ? withMountedBase(proxy) : undefined;
31
31
 
32
32
  // `</script>`-proof embedding: `<` only ever occurs inside JSON strings,
33
33
  // where the \u escape is equivalent, so a spec description can never close
@@ -3,6 +3,7 @@ import data from "blume:data";
3
3
 
4
4
  import { highlightCode } from "../../markdown/index.ts";
5
5
  import {
6
+ type ComponentsLike,
6
7
  responseExample,
7
8
  type SchemaLike,
8
9
  type SpecValue,
@@ -24,13 +25,15 @@ interface ResponseLike {
24
25
  }
25
26
 
26
27
  interface Props {
28
+ /** The document's `components`, which `$ref`'d examples resolve against. */
29
+ components?: ComponentsLike;
27
30
  sample: RequestSample;
28
31
  languages: SampleLanguage[];
29
32
  responses: Record<string, ResponseLike>;
30
33
  schemas: Record<string, SchemaLike>;
31
34
  }
32
35
 
33
- const { sample, languages, responses, schemas } = Astro.props;
36
+ const { components, sample, languages, responses, schemas } = Astro.props;
34
37
 
35
38
  const requestPanels = await languageSamplePanels(
36
39
  languages,
@@ -44,7 +47,7 @@ const responsePanels = await Promise.all(
44
47
  Object.entries(response.content ?? {}).find(([type]) =>
45
48
  type.includes("json")
46
49
  )?.[1] ?? Object.values(response.content ?? {})[0];
47
- const example = media ? responseExample(media, schemas) : undefined;
50
+ const example = media ? responseExample(media, schemas, components) : undefined;
48
51
  const html =
49
52
  example === undefined || example === null
50
53
  ? null
@@ -35,6 +35,13 @@ const node = planned ?? schemaTree(schema, schemas);
35
35
  </div>
36
36
  ) : node.kind === "branches" ? (
37
37
  <div class="flex flex-col gap-3">
38
+ {node.rows && (
39
+ <div class="not-prose">
40
+ {node.rows.map((row) => (
41
+ <SchemaProperty row={row} schemas={schemas} expandAll={expandAll} />
42
+ ))}
43
+ </div>
44
+ )}
38
45
  <div class="text-muted-foreground text-xs">{node.label}:</div>
39
46
  {node.branches.map((branch) => (
40
47
  <div class="rounded-blume border border-border p-3">
@@ -265,6 +265,32 @@ export const channelServers = (
265
265
  return servers;
266
266
  };
267
267
 
268
+ /** A protocol-keyed `bindings` map, whatever object it sits on. */
269
+ type BindingMap = Record<string, AsyncApiSpecValue>;
270
+
271
+ const BINDINGS_REF =
272
+ /^#\/components\/(?<section>(?:server|channel|operation|message)Bindings)\/(?<name>[^/]+)$/u;
273
+
274
+ /**
275
+ * A `bindings` map as declared: inline, or — which AsyncAPI allows in its
276
+ * place — a `$ref` to a `#/components/*Bindings` entry, resolved here. Read
277
+ * as-is, a referenced map's only key is `$ref`, which would pass for the
278
+ * protocol. `undefined` when a ref resolves to nothing.
279
+ */
280
+ export const resolveBindings = (
281
+ bindings: BindingMap | undefined,
282
+ components?: Components
283
+ ): BindingMap | undefined => {
284
+ const ref = bindings?.$ref;
285
+ if (!isString(ref)) {
286
+ return bindings;
287
+ }
288
+ const groups = BINDINGS_REF.exec(ref)?.groups;
289
+ const target =
290
+ components?.[groups?.section ?? ""]?.[unescapePointer(groups?.name ?? "")];
291
+ return isObject(target) ? target : undefined;
292
+ };
293
+
268
294
  /** Normalize protocol spellings onto the binding key they document. */
269
295
  const PROTOCOL_ALIASES = {
270
296
  "kafka-secure": "kafka",
@@ -277,15 +303,17 @@ const PROTOCOL_ALIASES = {
277
303
  /**
278
304
  * The protocol an operation speaks, for binding-aware code samples: the first
279
305
  * operation/channel binding key, else the first relevant server's `protocol`.
306
+ * `components` resolves bindings given as a `$ref`.
280
307
  */
281
308
  export const protocolOf = (
282
309
  operation: AsyncApiOperationObject | undefined,
283
310
  channel: AsyncApiChannelObject | undefined,
284
- servers: AsyncApiServerObject[]
311
+ servers: AsyncApiServerObject[],
312
+ components?: Components
285
313
  ): string | undefined => {
286
314
  const declared =
287
- Object.keys(operation?.bindings ?? {})[0] ??
288
- Object.keys(channel?.bindings ?? {})[0] ??
315
+ Object.keys(resolveBindings(operation?.bindings, components) ?? {})[0] ??
316
+ Object.keys(resolveBindings(channel?.bindings, components) ?? {})[0] ??
289
317
  servers.find((server) => isString(server.protocol))?.protocol;
290
318
  if (!isString(declared) || declared === "") {
291
319
  return undefined;
@@ -338,13 +366,17 @@ export interface BindingGroup {
338
366
  /**
339
367
  * Binding maps flattened for display, `bindingVersion` (metadata, not
340
368
  * behavior) dropped. Values stay unformatted — the component renders schema-ish
341
- * objects as nested schema tables and everything else as code.
369
+ * objects as nested schema tables and everything else as code. `components`
370
+ * resolves a map given as a `$ref`.
342
371
  */
343
372
  export const bindingGroups = (
344
- bindings?: AsyncApiChannelObject["bindings"]
373
+ bindings?: BindingMap,
374
+ components?: Components
345
375
  ): BindingGroup[] => {
346
376
  const groups: BindingGroup[] = [];
347
- for (const [protocol, fields] of Object.entries(bindings ?? {})) {
377
+ for (const [protocol, fields] of Object.entries(
378
+ resolveBindings(bindings, components) ?? {}
379
+ )) {
348
380
  if (!isObject(fields)) {
349
381
  continue;
350
382
  }
@@ -335,12 +335,17 @@ const isRecord = (value: SpecValue): value is Record<string, SpecValue> =>
335
335
 
336
336
  /**
337
337
  * The example a parameter or media type declares: its `example`, else the
338
- * first inline `value` in its `examples` map. Scalar's upgrade moves every
339
- * OpenAPI 3.0 and Swagger 2.0 `example` into `examples.default.value`, and
340
- * 3.1 authors write that map directly, so reading `example` alone would drop
341
- * them all. `undefined` when nothing is declared.
338
+ * first `value` in its `examples` map — inline, or behind a `$ref` to
339
+ * `#/components/examples`, resolved against `components`. Scalar's upgrade
340
+ * moves every OpenAPI 3.0 and Swagger 2.0 `example` into
341
+ * `examples.default.value`, and 3.1 authors write that map directly, so
342
+ * reading `example` alone would drop them all. `undefined` when nothing is
343
+ * declared.
342
344
  */
343
- export const declaredExample = (carrier: ExampleCarrier): SpecValue => {
345
+ export const declaredExample = (
346
+ carrier: ExampleCarrier,
347
+ components?: ComponentsLike
348
+ ): SpecValue => {
344
349
  if (carrier.example !== undefined) {
345
350
  return carrier.example;
346
351
  }
@@ -349,8 +354,11 @@ export const declaredExample = (carrier: ExampleCarrier): SpecValue => {
349
354
  return undefined;
350
355
  }
351
356
  for (const entry of Object.values(examples)) {
352
- if (isRecord(entry) && entry.value !== undefined) {
353
- return entry.value;
357
+ const example = isRecord(entry)
358
+ ? resolveComponentRef(entry, components, "examples")
359
+ : undefined;
360
+ if (example?.value !== undefined) {
361
+ return example.value;
354
362
  }
355
363
  }
356
364
  return undefined;
@@ -363,9 +371,11 @@ export const declaredExample = (carrier: ExampleCarrier): SpecValue => {
363
371
  */
364
372
  export const responseExample = (
365
373
  media: ExampleCarrier & { schema?: SchemaLike },
366
- schemas: Record<string, SchemaLike>
374
+ schemas: Record<string, SchemaLike>,
375
+ components?: ComponentsLike
367
376
  ): SpecValue =>
368
- declaredExample(media) ?? exampleValue(media.schema, schemas, "response");
377
+ declaredExample(media, components) ??
378
+ exampleValue(media.schema, schemas, "response");
369
379
 
370
380
  /** Pretty-print a JSON value for an example/code block. */
371
381
  export const toJson = <T>(value: T): string => JSON.stringify(value, null, 2);
@@ -171,7 +171,12 @@ export const initComposer = (root: HTMLElement): void => {
171
171
  return;
172
172
  }
173
173
  if (state === "error") {
174
- setStatus(ERROR_MESSAGE, ERROR_TEXT);
174
+ // A detail is the browser refusing the URL before dialing; a failed
175
+ // handshake carries none.
176
+ setStatus(
177
+ detail ? `Couldn't connect: ${detail}` : ERROR_MESSAGE,
178
+ ERROR_TEXT
179
+ );
175
180
  return;
176
181
  }
177
182
  setStatus(
@@ -87,6 +87,17 @@ export const defaultMessageValues = (model: MessageModel): MessageValues => ({
87
87
 
88
88
  const PARAM_TEMPLATE = /\{(?<name>[^{}]+)\}/gu;
89
89
 
90
+ /**
91
+ * Protocols whose channel address is a URL path, where a parameter value must
92
+ * be percent-encoded. Everywhere else — a Kafka or MQTT topic, an AMQP routing
93
+ * key — the address is a name the value goes into as written: `sensor 1`,
94
+ * not `sensor%201`.
95
+ */
96
+ const URL_PROTOCOLS = { http: true, https: true, ws: true } satisfies Record<
97
+ string,
98
+ true
99
+ >;
100
+
90
101
  /**
91
102
  * A free-text base URL lowered onto the server shape the snippet builders and
92
103
  * the connect URL read. A full URL contributes its scheme, host, and path.
@@ -114,7 +125,8 @@ const customServer = (url: string): AsyncApiServerObject => {
114
125
  /**
115
126
  * THE one message builder: the composer's samples, its copy buttons, and the
116
127
  * live WebSocket frame all consume its output. Parameter values are
117
- * URL-encoded into the address; a parameter left blank keeps its `{name}`
128
+ * URL-encoded into the address when the protocol addresses channels by URL
129
+ * (see {@link URL_PROTOCOLS}); a parameter left blank keeps its `{name}`
118
130
  * template so the sample still reads as a template rather than a broken
119
131
  * address. Payload text that doesn't parse yields no payload — the editor is
120
132
  * showing the reader a validation error at that moment, and neither a sample
@@ -126,6 +138,9 @@ export const buildMessage = (
126
138
  ): MessageSample => {
127
139
  const custom = values.customUrl.trim();
128
140
  const picked = model.servers[values.server] ?? model.servers[0];
141
+ const encode = Object.hasOwn(URL_PROTOCOLS, model.protocol ?? "")
142
+ ? encodeURIComponent
143
+ : (value: string) => value;
129
144
  let payload: unknown;
130
145
  try {
131
146
  // SAFETY: JSON.parse returns `any`; widening it to `unknown` claims
@@ -138,7 +153,7 @@ export const buildMessage = (
138
153
  action: model.action,
139
154
  address: model.address.replaceAll(PARAM_TEMPLATE, (template, name) => {
140
155
  const value = values.params[String(name)] ?? "";
141
- return value === "" ? template : encodeURIComponent(value);
156
+ return value === "" ? template : encode(value);
142
157
  }),
143
158
  payload,
144
159
  server: custom === "" ? picked?.server : customServer(custom),