blume 1.5.2 → 1.6.0

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 (194) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. package/src/translate/work-list.ts +0 -0
@@ -7,11 +7,21 @@ import { normalizeRoute } from "../openapi/references.ts";
7
7
  import { normalizeXHandle } from "../seo/x-handle.ts";
8
8
  import { FONT_SLUGS, isFontSlug } from "../theme/fonts.ts";
9
9
  import { normalizeBasePath } from "./base-path.ts";
10
+ import { PUBLIC_HOST_URL } from "./github.ts";
10
11
  import { uiLocaleOverridesSchema } from "./i18n-ui.ts";
11
12
  import { openInChatProviders } from "./open-in-chat.ts";
12
13
  import type { ContentSource } from "./sources/types.ts";
13
14
  import { isStandardSchema } from "./standard-schema.ts";
14
15
  import type { StandardSchema } from "./standard-schema.ts";
16
+ import { trimEnd } from "./trim.ts";
17
+
18
+ /**
19
+ * An absolute HTTP(S) URL, for any field that lands verbatim in an `href` —
20
+ * the header repo mark, the GitHub instance origin. Zod's bare `z.url()` also
21
+ * admits `javascript:` and `data:`, which would render as a script link on
22
+ * every page.
23
+ */
24
+ const httpUrlSchema = z.url({ protocol: /^https?$/u });
15
25
 
16
26
  /**
17
27
  * Public Blume schemas.
@@ -347,6 +357,21 @@ const notionSourceSchema = z.object({
347
357
  type: z.literal("notion"),
348
358
  });
349
359
 
360
+ /**
361
+ * An Obsidian vault, read in place. Wikilinks become route links and
362
+ * `%%comments%%` are stripped at load time, so the vault stays the source of
363
+ * truth — no export step and no generated notes in the repo.
364
+ */
365
+ const obsidianSourceSchema = z.strictObject({
366
+ /** Vault folder names to skip at any depth, in addition to dot-folders. */
367
+ exclude: z.array(z.string()).optional(),
368
+ /** Namespaces the source's routes under `/<prefix>/`; e.g. `vault`. */
369
+ prefix: z.string().optional(),
370
+ type: z.literal("obsidian"),
371
+ /** Vault directory, absolute or relative to the project root. */
372
+ vault: z.string().min(1),
373
+ });
374
+
350
375
  /**
351
376
  * A repo's GitHub Releases, materialized as `type: changelog` entries — release
352
377
  * notes become the changelog with no files to maintain. A private repo reads a
@@ -396,6 +421,7 @@ const contentSourceSchema = z.discriminatedUnion("type", [
396
421
  githubReleasesSourceSchema,
397
422
  sanitySourceSchema,
398
423
  notionSourceSchema,
424
+ obsidianSourceSchema,
399
425
  customSourceSchema,
400
426
  ]);
401
427
 
@@ -547,6 +573,8 @@ const remoteFontSchema = z.strictObject({
547
573
  provider: z
548
574
  .enum(["google", "fontsource", "bunny", "fontshare"])
549
575
  .default("google"),
576
+ /** Character subsets to load; defaults to `latin` plus the locales' scripts. */
577
+ subsets: z.array(z.string().min(1)).nonempty().optional(),
550
578
  weights: z
551
579
  .array(
552
580
  z.union([z.number().int().positive(), z.string().regex(/^\d+\.\.\d+$/u)])
@@ -693,6 +721,7 @@ const searchConfigSchema = z
693
721
  algolia: algoliaSearchSchema.optional(),
694
722
  indexing: z
695
723
  .strictObject({
724
+ includeCodeBlocks: z.boolean().default(false),
696
725
  includeHiddenPages: z.boolean().default(false),
697
726
  })
698
727
  .prefault({}),
@@ -794,6 +823,20 @@ const askEndpointSchema = z
794
823
  }
795
824
  );
796
825
 
826
+ /** The object form of `ai.llmsTxt`; a bare boolean normalizes onto it. */
827
+ const llmsTxtObjectSchema = z.strictObject({
828
+ /**
829
+ * Markdown inserted after the title and summary, before the page sections:
830
+ * the llms.txt spec's "details" slot. The place to tell agents when to use
831
+ * the product and how to call it; trimmed, and dropped when blank.
832
+ */
833
+ details: z.string().trim().min(1).optional(),
834
+ enabled: z.boolean().default(true),
835
+ openapi: z.boolean().default(true),
836
+ });
837
+
838
+ type LlmsTxtResolved = z.output<typeof llmsTxtObjectSchema>;
839
+
797
840
  const aiConfigSchema = z.strictObject({
798
841
  ask: z
799
842
  .strictObject({
@@ -857,19 +900,16 @@ const aiConfigSchema = z.strictObject({
857
900
  /**
858
901
  * `llms.txt`/`llms-full.txt` emission. A bare boolean toggles it; the object
859
902
  * form adds `openapi: false` to keep generated API reference pages out of
860
- * both files (e.g. when the configured spec is example content).
903
+ * both files (e.g. when the configured spec is example content) and
904
+ * `details`, free-form Markdown placed after the summary — the llms.txt
905
+ * spec's details block, where a site tells agents when to reach for it.
861
906
  */
862
907
  llmsTxt: z
863
- .union([
864
- z.boolean(),
865
- z.strictObject({
866
- enabled: z.boolean().default(true),
867
- openapi: z.boolean().default(true),
868
- }),
869
- ])
908
+ .union([z.boolean(), llmsTxtObjectSchema])
870
909
  .default(true)
871
- .transform((value) =>
872
- isBoolean(value) ? { enabled: value, openapi: true } : value
910
+ .transform(
911
+ (value): LlmsTxtResolved =>
912
+ isBoolean(value) ? { enabled: value, openapi: true } : value
873
913
  ),
874
914
  // Serializers for the agent-facing Markdown downlevel (the `.md` mirror,
875
915
  // llms-full.txt, MCP get_page), keyed by JSX name. Functions live here —
@@ -954,11 +994,37 @@ const featuredLinkSchema = z.strictObject({
954
994
  label: z.string(),
955
995
  });
956
996
 
997
+ /**
998
+ * A header link: a plain one (`Log in`, `Status`), or the single call to
999
+ * action. Same shape as a featured link minus the icon — the header row is
1000
+ * text, not iconography.
1001
+ */
1002
+ const headerActionSchema = z.strictObject({
1003
+ href: z.string(),
1004
+ label: z.string(),
1005
+ });
1006
+
957
1007
  const navigationConfigSchema = z.strictObject({
1008
+ /** Plain links in the header, left of the icon buttons. */
1009
+ actions: z.array(headerActionSchema).default([]),
1010
+ /**
1011
+ * The one primary call to action in the header, as a filled button.
1012
+ *
1013
+ * Singular on purpose: a docs header has room for exactly one thing a
1014
+ * reader is being asked to do, a row of buttons asks for nothing, and
1015
+ * `featured` already takes the secondary links.
1016
+ */
1017
+ cta: headerActionSchema.optional(),
958
1018
  /** Pinned links shown above the generated sidebar sections. */
959
1019
  featured: z.array(featuredLinkSchema).default([]),
960
- /** Show a GitHub repo link in the header (requires `github` configured). */
961
- repo: z.boolean().default(true),
1020
+ /**
1021
+ * The GitHub link in the header. `true` derives it from `github`, `false`
1022
+ * hides it, and an absolute URL points it anywhere on GitHub — an
1023
+ * organization, say, when the docs repo itself is private and `github` has
1024
+ * to stay unset. The mark stays the GitHub one, so a URL elsewhere belongs in
1025
+ * `actions`.
1026
+ */
1027
+ repo: z.union([z.boolean(), httpUrlSchema]).default(true),
962
1028
  selectors: z.array(navSelectorSchema).default([]),
963
1029
  /**
964
1030
  * Sidebar behavior. `display` sets how every group renders (a group in an
@@ -1346,6 +1412,50 @@ const contentSignalsSchema = z
1346
1412
  return value;
1347
1413
  });
1348
1414
 
1415
+ /** A schema.org `PostalAddress`, any part of which may be given. */
1416
+ const postalAddressSchema = z.strictObject({
1417
+ addressCountry: z.string().optional(),
1418
+ addressLocality: z.string().optional(),
1419
+ addressRegion: z.string().optional(),
1420
+ postalCode: z.string().optional(),
1421
+ streetAddress: z.string().optional(),
1422
+ });
1423
+
1424
+ /**
1425
+ * `seo.organization`: the organization behind the site, emitted on every page
1426
+ * as a schema.org `Organization` node (see `seo/jsonld.ts`). Name and URL
1427
+ * default to the site's; contact details become a `ContactPoint`, the address
1428
+ * a `PostalAddress` — what agents check to verify a business.
1429
+ */
1430
+ const organizationConfigSchema = z.strictObject({
1431
+ address: postalAddressSchema.optional(),
1432
+ contactType: z.string().default("customer support"),
1433
+ email: z.email().optional(),
1434
+ logo: z.string().optional(),
1435
+ name: z.string().optional(),
1436
+ sameAs: z.array(z.url()).default([]),
1437
+ telephone: z.string().optional(),
1438
+ url: z.url().optional(),
1439
+ });
1440
+
1441
+ /**
1442
+ * `seo.software`: the product the site documents, emitted on the homepage as
1443
+ * a schema.org `SoftwareApplication` node. `true` takes every default (name
1444
+ * and description from the site, category `DeveloperApplication`).
1445
+ */
1446
+ const softwareConfigSchema = z.strictObject({
1447
+ applicationCategory: z.string().default("DeveloperApplication"),
1448
+ description: z.string().optional(),
1449
+ license: z.string().optional(),
1450
+ name: z.string().optional(),
1451
+ operatingSystem: z.string().optional(),
1452
+ price: z.union([z.number().nonnegative(), z.string()]).optional(),
1453
+ priceCurrency: z.string().default("USD"),
1454
+ sameAs: z.array(z.url()).default([]),
1455
+ });
1456
+
1457
+ type SoftwareResolved = z.output<typeof softwareConfigSchema>;
1458
+
1349
1459
  /** Discoverability features: OG images, feeds, sitemap, structured data. */
1350
1460
  const seoConfigSchema = z.strictObject({
1351
1461
  /**
@@ -1357,21 +1467,59 @@ const seoConfigSchema = z.strictObject({
1357
1467
  /** robots.txt `Content-Signal` usage declaration (on by default). */
1358
1468
  contentSignals: contentSignalsSchema.prefault(true),
1359
1469
  og: ogConfigSchema.default({}),
1470
+ /** The organization behind the site, as an `Organization` JSON-LD node. */
1471
+ organization: organizationConfigSchema.optional(),
1360
1472
  /** Generate robots.txt (with a Sitemap reference when available). */
1361
1473
  robots: z.boolean().default(true),
1362
1474
  rss: rssConfigSchema.prefault({}),
1363
1475
  /** Generate sitemap.xml (requires deployment.site). */
1364
1476
  sitemap: z.boolean().default(true),
1477
+ /** The documented product, as a homepage `SoftwareApplication` node. */
1478
+ software: z
1479
+ .union([z.boolean(), softwareConfigSchema])
1480
+ .optional()
1481
+ .transform((value): SoftwareResolved | undefined => {
1482
+ if (value === true) {
1483
+ return softwareConfigSchema.parse({});
1484
+ }
1485
+ return value === false ? undefined : value;
1486
+ }),
1365
1487
  /** Emit schema.org JSON-LD in each page's <head>. */
1366
1488
  structuredData: z.boolean().default(true),
1367
1489
  /** X (Twitter) account attribution for share cards. */
1368
1490
  x: xConfigSchema.default({}),
1369
1491
  });
1370
1492
 
1493
+ /**
1494
+ * The GitHub instance's origin, normalized. Repo, edit, and API URLs are all
1495
+ * built by appending to this, so it is reduced to a bare origin: a trailing
1496
+ * slash would double the separator, and a path, query, fragment, or embedded
1497
+ * credentials would land in the middle of every generated link.
1498
+ */
1499
+ const githubOriginSchema = httpUrlSchema.transform(
1500
+ (value) => new URL(value).origin
1501
+ );
1502
+
1503
+ /**
1504
+ * A REST API base: an origin plus an optional path, since Enterprise Server
1505
+ * serves the API from `/api/v3`. Anything past the path is dropped for the same
1506
+ * reason the host is reduced — `/repos/{owner}/{repo}` is appended as a string,
1507
+ * so a query would swallow the route and a fragment would strip it from the
1508
+ * request entirely, leaving a lookup that silently returns the wrong thing.
1509
+ */
1510
+ const githubApiSchema = httpUrlSchema.transform((value) => {
1511
+ const { origin, pathname } = new URL(value);
1512
+ return trimEnd(`${origin}${pathname}`, "/");
1513
+ });
1514
+
1371
1515
  const githubConfigSchema = z.strictObject({
1516
+ /** REST API base. Derived from `host` when unset. */
1517
+ api: githubApiSchema.optional(),
1372
1518
  branch: z.string().default("main"),
1373
1519
  /** Path from the repo root to the project root (for monorepos). */
1374
1520
  dir: z.string().optional(),
1521
+ /** Origin of the GitHub instance, for Enterprise installations. */
1522
+ host: githubOriginSchema.default(PUBLIC_HOST_URL),
1375
1523
  owner: z.string(),
1376
1524
  repo: z.string(),
1377
1525
  });
@@ -1577,9 +1725,33 @@ export type OpenApiSource = z.input<typeof openapiSourceSchema>;
1577
1725
  const scalarConfigSchema = z.record(z.string(), z.unknown()).optional();
1578
1726
 
1579
1727
  /**
1580
- * The shared shape of both API-reference blocks — only the mount route and
1728
+ * The interactive "Try it" panel on operation pages (Blume renderer). On by
1729
+ * default; `false` hides it. The object form keeps it on and sets `proxy`,
1730
+ * the CORS escape hatch the Send button routes requests through: a proxy URL,
1731
+ * or `true` for the built-in `/_api-proxy` endpoint (which requires
1732
+ * `deployment.output: "server"`). Booleans normalize to the object shape so
1733
+ * consumers read `{ enabled, proxy }` directly. `proxy` applies to the
1734
+ * HTTP-posting playgrounds (OpenAPI, GraphQL) — an event composer's WebSocket
1735
+ * connect is direct. One schema for every reference block, so the
1736
+ * normalization can never drift between them.
1737
+ */
1738
+ const playgroundConfigSchema = z
1739
+ .union([
1740
+ z.boolean(),
1741
+ z.strictObject({
1742
+ enabled: z.boolean().default(true),
1743
+ proxy: z.union([z.boolean(), z.string()]).default(false),
1744
+ }),
1745
+ ])
1746
+ .default(true)
1747
+ .transform((value) =>
1748
+ isBoolean(value) ? { enabled: value, proxy: false } : value
1749
+ );
1750
+
1751
+ /**
1752
+ * The shared shape of the API-reference blocks — only the mount route and
1581
1753
  * code-sample defaults differ per spec kind, so each block declares just
1582
- * those.
1754
+ * those (the GraphQL block derives from this via omit/extend below).
1583
1755
  */
1584
1756
  const referenceConfigSchema = (defaults: {
1585
1757
  codeSamples: string[];
@@ -1591,27 +1763,8 @@ const referenceConfigSchema = (defaults: {
1591
1763
  enabled: z.boolean().default(false),
1592
1764
  /** Start nested schema rows expanded rather than collapsed (Blume renderer). */
1593
1765
  expandSchemas: z.boolean().default(false),
1594
- /**
1595
- * The interactive "Try it" panel on operation pages (Blume renderer). On by
1596
- * default; `false` hides it. The object form keeps it on and sets `proxy`,
1597
- * the CORS escape hatch the OpenAPI Send button routes requests through: a
1598
- * proxy URL, or `true` for the built-in `/_api-proxy` endpoint (which
1599
- * requires `deployment.output: "server"`). Booleans normalize to the object
1600
- * shape so consumers read `{ enabled, proxy }` directly. `proxy` is
1601
- * OpenAPI-only — an event composer's WebSocket connect is direct.
1602
- */
1603
- playground: z
1604
- .union([
1605
- z.boolean(),
1606
- z.strictObject({
1607
- enabled: z.boolean().default(true),
1608
- proxy: z.union([z.boolean(), z.string()]).default(false),
1609
- }),
1610
- ])
1611
- .default(true)
1612
- .transform((value) =>
1613
- isBoolean(value) ? { enabled: value, proxy: false } : value
1614
- ),
1766
+ /** The "Try it" panel; see {@link playgroundConfigSchema}. */
1767
+ playground: playgroundConfigSchema,
1615
1768
  /** Who renders the reference: Blume's own UI, or the embedded Scalar SPA. */
1616
1769
  renderer: z.enum(["blume", "scalar"]).default("blume"),
1617
1770
  /** Where the reference mounts. */
@@ -1651,6 +1804,43 @@ const asyncapiConfigSchema = referenceConfigSchema({
1651
1804
  route: "/events",
1652
1805
  });
1653
1806
 
1807
+ /**
1808
+ * A single GraphQL schema rendered by the reference. `spec` is a local path or
1809
+ * an `http(s)` URL to SDL text or an introspection JSON result; `endpoint` is
1810
+ * the live GraphQL API URL the playground and code samples target (a schema,
1811
+ * unlike an OpenAPI document, names no server).
1812
+ */
1813
+ const graphqlSourceSchema = openapiSourceSchema.extend({
1814
+ /** URL of the live GraphQL endpoint (playground + code samples). */
1815
+ endpoint: z.string().optional(),
1816
+ });
1817
+
1818
+ export type GraphqlSource = z.input<typeof graphqlSourceSchema>;
1819
+
1820
+ /**
1821
+ * GraphQL reference. Blume lowers the schema (SDL or introspection JSON) to
1822
+ * one real page per root field — grouped as Queries/Mutations/Subscriptions —
1823
+ * plus one page per named type (Objects, Input Objects, Enums, Interfaces,
1824
+ * Unions, Scalars), all included in the sidebar, search, llms.txt, and OG.
1825
+ * Always Blume-rendered: the Scalar SPA reads OpenAPI documents only, so the
1826
+ * block declares no `renderer`/`scalar`/`theme` escape hatches.
1827
+ */
1828
+ const graphqlConfigSchema = referenceConfigSchema({
1829
+ codeSamples: ["curl", "js", "python"],
1830
+ route: "/graphql",
1831
+ })
1832
+ // No `renderer`/`scalar`/`theme` escape hatches (the Scalar SPA reads
1833
+ // OpenAPI documents only) and no `expandSchemas` (GraphQL field tables have
1834
+ // no nesting) — everything else, the playground normalization included, is
1835
+ // the shared reference shape.
1836
+ .omit({ expandSchemas: true, renderer: true, scalar: true, theme: true })
1837
+ .extend({
1838
+ /** Default live endpoint URL for every source (per-source `endpoint` wins). */
1839
+ endpoint: z.string().optional(),
1840
+ /** One or more schemas; each renders on its own route by default. */
1841
+ sources: z.array(graphqlSourceSchema).default([]),
1842
+ });
1843
+
1654
1844
  /**
1655
1845
  * Opt-in custom frontmatter keys. `extend` maps each extra key a project's
1656
1846
  * pages may carry (e.g. `owner`, `reviewedAt`) to a validation schema; the
@@ -1736,6 +1926,7 @@ export const blumeConfigSchema = z
1736
1926
  /** Opt-in custom frontmatter keys, validated by user-supplied schemas. */
1737
1927
  frontmatter: frontmatterConfigSchema.prefault({}),
1738
1928
  github: githubConfigSchema.optional(),
1929
+ graphql: graphqlConfigSchema.prefault({}),
1739
1930
  i18n: i18nConfigSchema.optional(),
1740
1931
  image: imageConfigSchema.prefault({}),
1741
1932
  integrations: z.array(z.custom<AstroIntegration>()).default([]),
@@ -1,3 +1,4 @@
1
+ import { needsPlaygroundProxy } from "../openapi/references.ts";
1
2
  import { searchProviderMeta } from "../search/providers.ts";
2
3
  import type { ResolvedConfig } from "./schema.ts";
3
4
 
@@ -14,15 +15,10 @@ export const serverFeatures = (config: ResolvedConfig): string[] => {
14
15
  if (config.ai.mcp.enabled) {
15
16
  features.push("MCP server");
16
17
  }
17
- // The built-in playground proxy (`openapi.playground.proxy: true`) is a
18
- // live fetch endpoint at `/_api-proxy`; an external proxy URL (string) or a
19
- // proxy-less playground stays fully static.
20
- if (
21
- config.openapi.enabled &&
22
- config.openapi.renderer === "blume" &&
23
- config.openapi.playground.enabled &&
24
- config.openapi.playground.proxy === true
25
- ) {
18
+ // The built-in playground proxy (`playground.proxy: true` on the OpenAPI or
19
+ // GraphQL block) is a live fetch endpoint at `/_api-proxy`; an external
20
+ // proxy URL (string) or a proxy-less playground stays fully static.
21
+ if (needsPlaygroundProxy(config)) {
26
22
  features.push("API playground proxy");
27
23
  }
28
24
  // Mixedbread (and any future provider) that proxies queries through a secret
@@ -5,6 +5,7 @@ import { gfm } from "micromark-extension-gfm";
5
5
  import stringWidth from "string-width";
6
6
 
7
7
  import matter from "../frontmatter.ts";
8
+ import { PUBLIC_API_URL } from "../github.ts";
8
9
  import { columnsPrefix } from "../text-width.ts";
9
10
  import {
10
11
  hashText,
@@ -55,7 +56,6 @@ interface GithubRelease {
55
56
  tag_name: string;
56
57
  }
57
58
 
58
- const DEFAULT_BASE_URL = "https://api.github.com";
59
59
  const DEFAULT_LIMIT = 100;
60
60
  const PER_PAGE = 100;
61
61
 
@@ -203,7 +203,7 @@ export const githubReleasesSource = (
203
203
  ctx: SourceContext
204
204
  ): ContentSource => {
205
205
  const doFetch = options.fetchImpl ?? globalThis.fetch;
206
- const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/u, "");
206
+ const base = (options.baseUrl ?? PUBLIC_API_URL).replace(/\/$/u, "");
207
207
  const max = options.limit ?? DEFAULT_LIMIT;
208
208
  const cache = snapshotCache(ctx.cacheDir);
209
209
  let snapshot = new Map<string, SourceEntry>();