blume 1.5.3 → 1.6.1

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 (209) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/index.js +3949 -1403
  3. package/dist/cli/index.js.map +111 -96
  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 +33 -2
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +10 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +122 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +29 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +26 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/07-faq.mdx +9 -9
  21. package/docs/_snippets/include-demo.mdx +7 -0
  22. package/docs/advanced/api-reference.mdx +13 -4
  23. package/docs/advanced/custom-pages.mdx +4 -2
  24. package/docs/advanced/graphql.mdx +84 -0
  25. package/docs/advanced/meta.ts +8 -1
  26. package/docs/configuration/ai.mdx +25 -3
  27. package/docs/configuration/index.mdx +24 -0
  28. package/docs/configuration/search.mdx +13 -1
  29. package/docs/configuration/seo.mdx +30 -3
  30. package/docs/configuration/theming.mdx +23 -0
  31. package/docs/content/components.mdx +15 -1
  32. package/docs/content/includes.mdx +68 -0
  33. package/docs/content/meta.ts +1 -0
  34. package/docs/content/navigation.mdx +25 -0
  35. package/docs/content/sources.mdx +42 -1
  36. package/docs/content/syntax.mdx +69 -1
  37. package/docs/content/versioning.mdx +15 -9
  38. package/docs/reference/cli.mdx +2 -1
  39. package/package.json +66 -57
  40. package/skills/blume-migrate/SKILL.md +16 -7
  41. package/skills/blume-migrate/references/docusaurus.md +5 -3
  42. package/skills/blume-migrate/references/fumadocs.md +10 -2
  43. package/skills/blume-migrate/references/mintlify.md +3 -2
  44. package/skills/blume-migrate/references/nextra.md +2 -2
  45. package/skills/blume-migrate/references/starlight.md +1 -1
  46. package/src/ai/agent-readability.ts +2 -1
  47. package/src/ai/ask-data.ts +2 -1
  48. package/src/ai/component-markdown.ts +199 -36
  49. package/src/ai/llms.ts +93 -6
  50. package/src/ai/markdown.ts +2 -2
  51. package/src/ai/mcp/discovery.ts +10 -2
  52. package/src/ai/mcp/server.ts +74 -2
  53. package/src/astro/examples.ts +29 -2
  54. package/src/astro/generate.ts +282 -177
  55. package/src/astro/include-hmr.ts +81 -0
  56. package/src/astro/include-refresh.ts +0 -0
  57. package/src/astro/index.ts +10 -5
  58. package/src/astro/markdown-negotiation.ts +1 -1
  59. package/src/astro/runtime-modules.ts +196 -0
  60. package/src/astro/templates.ts +365 -113
  61. package/src/cli/commands/build.ts +91 -16
  62. package/src/cli/commands/dev.ts +6 -3
  63. package/src/cli/host-args.ts +18 -0
  64. package/src/cli/index.ts +2 -1
  65. package/src/cli/init/questions.ts +1 -0
  66. package/src/cli/init/scaffold.ts +27 -4
  67. package/src/components/colors.ts +142 -0
  68. package/src/components/content/Badge.astro +5 -12
  69. package/src/components/content/Callout.astro +19 -36
  70. package/src/components/content/Card.astro +15 -21
  71. package/src/components/content/Component.astro +10 -1
  72. package/src/components/content/GithubInfo.astro +28 -9
  73. package/src/components/content/Tabs.astro +27 -5
  74. package/src/components/content/github-info.ts +20 -5
  75. package/src/components/copy-feedback.ts +93 -9
  76. package/src/components/dropdown-dismiss.ts +122 -0
  77. package/src/components/islands/ask-ai.tsx +4 -1
  78. package/src/components/islands/hooks.ts +3 -1
  79. package/src/components/layout/Fonts.astro +15 -8
  80. package/src/components/layout/Header.astro +44 -0
  81. package/src/components/layout/LanguageSwitcher.astro +9 -1
  82. package/src/components/layout/NavSelector.astro +12 -3
  83. package/src/components/layout/NavTree.astro +6 -18
  84. package/src/components/layout/PageActions.astro +54 -22
  85. package/src/components/layout/PageLayout.astro +2 -0
  86. package/src/components/layout/ReferenceLayout.astro +6 -1
  87. package/src/components/layout/RootLayout.astro +42 -15
  88. package/src/components/layout/Search.astro +36 -4
  89. package/src/components/layout/TableOfContents.astro +8 -2
  90. package/src/components/layout/head-scripts.ts +30 -1
  91. package/src/components/openapi/ApiOverview.astro +13 -3
  92. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  93. package/src/components/openapi/GraphqlChip.astro +33 -0
  94. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  95. package/src/components/openapi/GraphqlOperation.astro +186 -0
  96. package/src/components/openapi/GraphqlType.astro +154 -0
  97. package/src/components/openapi/MethodBadge.astro +3 -14
  98. package/src/components/openapi/Operation.astro +12 -5
  99. package/src/components/openapi/OperationPanel.astro +43 -0
  100. package/src/components/openapi/RequestPanel.astro +5 -10
  101. package/src/components/openapi/Responses.astro +1 -16
  102. package/src/components/openapi/graphql-helpers.ts +466 -0
  103. package/src/components/openapi/playground-client.ts +15 -0
  104. package/src/components/openapi/sample-panels.ts +45 -0
  105. package/src/components/openapi/snippets.ts +13 -35
  106. package/src/core/base-path.ts +11 -0
  107. package/src/core/config-input.ts +209 -2
  108. package/src/core/config.ts +6 -4
  109. package/src/core/content-assets.ts +15 -4
  110. package/src/core/data.ts +28 -3
  111. package/src/core/define-components.ts +2 -0
  112. package/src/core/diagnostics.ts +8 -0
  113. package/src/core/frontmatter.ts +20 -8
  114. package/src/core/github.ts +71 -0
  115. package/src/core/graph.ts +22 -8
  116. package/src/core/heading-markers.ts +96 -0
  117. package/src/core/i18n-ui.ts +12 -0
  118. package/src/core/includes.ts +633 -0
  119. package/src/core/last-modified.ts +36 -11
  120. package/src/core/links.ts +79 -13
  121. package/src/core/manifest.ts +10 -0
  122. package/src/core/meta.ts +2 -1
  123. package/src/core/nav-diagnostics.ts +11 -2
  124. package/src/core/navigation.ts +27 -6
  125. package/src/core/project-graph.ts +61 -9
  126. package/src/core/schema.ts +235 -36
  127. package/src/core/server-features.ts +5 -9
  128. package/src/core/sources/github-releases.ts +2 -2
  129. package/src/core/sources/normalize.ts +502 -115
  130. package/src/core/sources/notion.ts +43 -8
  131. package/src/core/sources/obsidian.ts +1038 -0
  132. package/src/core/sources/read.ts +36 -1
  133. package/src/core/sources/resolve.ts +34 -1
  134. package/src/core/sources/types.ts +28 -6
  135. package/src/core/sources/watch.ts +12 -8
  136. package/src/core/tsconfig-aliases.ts +48 -35
  137. package/src/core/types.ts +31 -2
  138. package/src/core/ui-packs/ar.ts +2 -0
  139. package/src/core/ui-packs/bg.ts +3 -0
  140. package/src/core/ui-packs/bn.ts +2 -0
  141. package/src/core/ui-packs/ca.ts +3 -0
  142. package/src/core/ui-packs/cs.ts +2 -0
  143. package/src/core/ui-packs/da.ts +2 -0
  144. package/src/core/ui-packs/de.ts +3 -0
  145. package/src/core/ui-packs/el.ts +3 -0
  146. package/src/core/ui-packs/es.ts +3 -0
  147. package/src/core/ui-packs/fa.ts +2 -0
  148. package/src/core/ui-packs/fi.ts +2 -0
  149. package/src/core/ui-packs/fr.ts +3 -0
  150. package/src/core/ui-packs/he.ts +2 -0
  151. package/src/core/ui-packs/hi.ts +2 -0
  152. package/src/core/ui-packs/hr.ts +3 -0
  153. package/src/core/ui-packs/hu.ts +3 -0
  154. package/src/core/ui-packs/id.ts +3 -0
  155. package/src/core/ui-packs/it.ts +2 -0
  156. package/src/core/ui-packs/ja.ts +3 -0
  157. package/src/core/ui-packs/ko.ts +3 -0
  158. package/src/core/ui-packs/nl.ts +3 -0
  159. package/src/core/ui-packs/no.ts +3 -0
  160. package/src/core/ui-packs/pl.ts +3 -0
  161. package/src/core/ui-packs/pt-br.ts +3 -0
  162. package/src/core/ui-packs/pt.ts +3 -0
  163. package/src/core/ui-packs/ro.ts +3 -0
  164. package/src/core/ui-packs/ru.ts +3 -0
  165. package/src/core/ui-packs/sk.ts +2 -0
  166. package/src/core/ui-packs/sr.ts +2 -0
  167. package/src/core/ui-packs/sv.ts +3 -0
  168. package/src/core/ui-packs/th.ts +2 -0
  169. package/src/core/ui-packs/tr.ts +3 -0
  170. package/src/core/ui-packs/uk.ts +3 -0
  171. package/src/core/ui-packs/vi.ts +2 -0
  172. package/src/core/ui-packs/zh-tw.ts +2 -0
  173. package/src/core/ui-packs/zh.ts +2 -0
  174. package/src/core/version-cut.ts +26 -6
  175. package/src/core/yaml.ts +26 -0
  176. package/src/deploy/function-bundle.ts +251 -0
  177. package/src/deploy/vercel-negotiation.ts +49 -6
  178. package/src/eval/schema.ts +3 -1
  179. package/src/markdown/code-title.ts +22 -16
  180. package/src/markdown/features.ts +21 -0
  181. package/src/markdown/fence-meta.ts +50 -0
  182. package/src/markdown/heading-anchors.ts +198 -37
  183. package/src/markdown/include.ts +247 -0
  184. package/src/markdown/index.ts +43 -34
  185. package/src/markdown/language-icon.ts +2 -2
  186. package/src/markdown/mdast.ts +7 -3
  187. package/src/markdown/ts2js.ts +264 -0
  188. package/src/og/card.ts +1 -1
  189. package/src/openapi/asyncapi.ts +4 -1
  190. package/src/openapi/graphql-build.ts +293 -0
  191. package/src/openapi/graphql.ts +212 -0
  192. package/src/openapi/model.ts +38 -5
  193. package/src/openapi/parse.ts +34 -0
  194. package/src/openapi/proxy.ts +30 -5
  195. package/src/openapi/references.ts +97 -13
  196. package/src/openapi/render-mdx.ts +66 -12
  197. package/src/openapi/scalar.ts +5 -16
  198. package/src/openapi/source.ts +91 -23
  199. package/src/registry/eject.ts +47 -17
  200. package/src/search/documents.ts +229 -37
  201. package/src/search/orama-index.ts +9 -5
  202. package/src/seo/jsonld.ts +293 -51
  203. package/src/theme/code-block-padding.ts +16 -0
  204. package/src/theme/entry.ts +67 -13
  205. package/src/theme/fonts.ts +189 -16
  206. package/src/theme/sources.ts +49 -0
  207. package/src/translate/prompts.ts +2 -0
  208. package/src/translate/run.ts +7 -0
  209. 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,18 +900,14 @@ 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) =>
910
+ .transform((value): LlmsTxtResolved =>
872
911
  isBoolean(value) ? { enabled: value, openapi: true } : value
873
912
  ),
874
913
  // Serializers for the agent-facing Markdown downlevel (the `.md` mirror,
@@ -954,11 +993,37 @@ const featuredLinkSchema = z.strictObject({
954
993
  label: z.string(),
955
994
  });
956
995
 
996
+ /**
997
+ * A header link: a plain one (`Log in`, `Status`), or the single call to
998
+ * action. Same shape as a featured link minus the icon — the header row is
999
+ * text, not iconography.
1000
+ */
1001
+ const headerActionSchema = z.strictObject({
1002
+ href: z.string(),
1003
+ label: z.string(),
1004
+ });
1005
+
957
1006
  const navigationConfigSchema = z.strictObject({
1007
+ /** Plain links in the header, left of the icon buttons. */
1008
+ actions: z.array(headerActionSchema).default([]),
1009
+ /**
1010
+ * The one primary call to action in the header, as a filled button.
1011
+ *
1012
+ * Singular on purpose: a docs header has room for exactly one thing a
1013
+ * reader is being asked to do, a row of buttons asks for nothing, and
1014
+ * `featured` already takes the secondary links.
1015
+ */
1016
+ cta: headerActionSchema.optional(),
958
1017
  /** Pinned links shown above the generated sidebar sections. */
959
1018
  featured: z.array(featuredLinkSchema).default([]),
960
- /** Show a GitHub repo link in the header (requires `github` configured). */
961
- repo: z.boolean().default(true),
1019
+ /**
1020
+ * The GitHub link in the header. `true` derives it from `github`, `false`
1021
+ * hides it, and an absolute URL points it anywhere on GitHub — an
1022
+ * organization, say, when the docs repo itself is private and `github` has
1023
+ * to stay unset. The mark stays the GitHub one, so a URL elsewhere belongs in
1024
+ * `actions`.
1025
+ */
1026
+ repo: z.union([z.boolean(), httpUrlSchema]).default(true),
962
1027
  selectors: z.array(navSelectorSchema).default([]),
963
1028
  /**
964
1029
  * Sidebar behavior. `display` sets how every group renders (a group in an
@@ -1266,8 +1331,9 @@ const ogFontSchema = z.union([
1266
1331
 
1267
1332
  const ogConfigSchema = z.strictObject({
1268
1333
  /**
1269
- * Card subtitle. Defaults to the site description; a string overrides it,
1270
- * `false` renders the card without one.
1334
+ * Site-wide card subtitle, shown on pages without their own description.
1335
+ * Defaults to the site description; a string overrides it, `false` renders
1336
+ * every card without a subtitle (page descriptions included).
1271
1337
  */
1272
1338
  description: z.union([z.string(), z.literal(false)]).optional(),
1273
1339
  /**
@@ -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
  });
@@ -1560,6 +1708,14 @@ const openapiSourceSchema = z.strictObject({
1560
1708
  noindex: z.boolean().default(false),
1561
1709
  /** Per-source route; defaults to the block's `route` (or a derived path). */
1562
1710
  route: z.string().optional(),
1711
+ /**
1712
+ * Append the English "Reference for the … endpoint in the … API." sentence
1713
+ * to every generated operation page's meta description. On by default, so
1714
+ * terse specs still ship distinct, snippet-length descriptions; set to
1715
+ * `false` on a non-English site to describe pages with the spec's own prose
1716
+ * alone (falling back to the page title when an operation has none).
1717
+ */
1718
+ seoDescriptionSuffix: z.boolean().default(true),
1563
1719
  /** Local path or `http(s)` URL to the spec. */
1564
1720
  spec: z.string(),
1565
1721
  });
@@ -1577,9 +1733,33 @@ export type OpenApiSource = z.input<typeof openapiSourceSchema>;
1577
1733
  const scalarConfigSchema = z.record(z.string(), z.unknown()).optional();
1578
1734
 
1579
1735
  /**
1580
- * The shared shape of both API-reference blocks — only the mount route and
1736
+ * The interactive "Try it" panel on operation pages (Blume renderer). On by
1737
+ * default; `false` hides it. The object form keeps it on and sets `proxy`,
1738
+ * the CORS escape hatch the Send button routes requests through: a proxy URL,
1739
+ * or `true` for the built-in `/_api-proxy` endpoint (which requires
1740
+ * `deployment.output: "server"`). Booleans normalize to the object shape so
1741
+ * consumers read `{ enabled, proxy }` directly. `proxy` applies to the
1742
+ * HTTP-posting playgrounds (OpenAPI, GraphQL) — an event composer's WebSocket
1743
+ * connect is direct. One schema for every reference block, so the
1744
+ * normalization can never drift between them.
1745
+ */
1746
+ const playgroundConfigSchema = z
1747
+ .union([
1748
+ z.boolean(),
1749
+ z.strictObject({
1750
+ enabled: z.boolean().default(true),
1751
+ proxy: z.union([z.boolean(), z.string()]).default(false),
1752
+ }),
1753
+ ])
1754
+ .default(true)
1755
+ .transform((value) =>
1756
+ isBoolean(value) ? { enabled: value, proxy: false } : value
1757
+ );
1758
+
1759
+ /**
1760
+ * The shared shape of the API-reference blocks — only the mount route and
1581
1761
  * code-sample defaults differ per spec kind, so each block declares just
1582
- * those.
1762
+ * those (the GraphQL block derives from this via omit/extend below).
1583
1763
  */
1584
1764
  const referenceConfigSchema = (defaults: {
1585
1765
  codeSamples: string[];
@@ -1591,27 +1771,8 @@ const referenceConfigSchema = (defaults: {
1591
1771
  enabled: z.boolean().default(false),
1592
1772
  /** Start nested schema rows expanded rather than collapsed (Blume renderer). */
1593
1773
  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
- ),
1774
+ /** The "Try it" panel; see {@link playgroundConfigSchema}. */
1775
+ playground: playgroundConfigSchema,
1615
1776
  /** Who renders the reference: Blume's own UI, or the embedded Scalar SPA. */
1616
1777
  renderer: z.enum(["blume", "scalar"]).default("blume"),
1617
1778
  /** Where the reference mounts. */
@@ -1651,6 +1812,43 @@ const asyncapiConfigSchema = referenceConfigSchema({
1651
1812
  route: "/events",
1652
1813
  });
1653
1814
 
1815
+ /**
1816
+ * A single GraphQL schema rendered by the reference. `spec` is a local path or
1817
+ * an `http(s)` URL to SDL text or an introspection JSON result; `endpoint` is
1818
+ * the live GraphQL API URL the playground and code samples target (a schema,
1819
+ * unlike an OpenAPI document, names no server).
1820
+ */
1821
+ const graphqlSourceSchema = openapiSourceSchema.extend({
1822
+ /** URL of the live GraphQL endpoint (playground + code samples). */
1823
+ endpoint: z.string().optional(),
1824
+ });
1825
+
1826
+ export type GraphqlSource = z.input<typeof graphqlSourceSchema>;
1827
+
1828
+ /**
1829
+ * GraphQL reference. Blume lowers the schema (SDL or introspection JSON) to
1830
+ * one real page per root field — grouped as Queries/Mutations/Subscriptions —
1831
+ * plus one page per named type (Objects, Input Objects, Enums, Interfaces,
1832
+ * Unions, Scalars), all included in the sidebar, search, llms.txt, and OG.
1833
+ * Always Blume-rendered: the Scalar SPA reads OpenAPI documents only, so the
1834
+ * block declares no `renderer`/`scalar`/`theme` escape hatches.
1835
+ */
1836
+ const graphqlConfigSchema = referenceConfigSchema({
1837
+ codeSamples: ["curl", "js", "python"],
1838
+ route: "/graphql",
1839
+ })
1840
+ // No `renderer`/`scalar`/`theme` escape hatches (the Scalar SPA reads
1841
+ // OpenAPI documents only) and no `expandSchemas` (GraphQL field tables have
1842
+ // no nesting) — everything else, the playground normalization included, is
1843
+ // the shared reference shape.
1844
+ .omit({ expandSchemas: true, renderer: true, scalar: true, theme: true })
1845
+ .extend({
1846
+ /** Default live endpoint URL for every source (per-source `endpoint` wins). */
1847
+ endpoint: z.string().optional(),
1848
+ /** One or more schemas; each renders on its own route by default. */
1849
+ sources: z.array(graphqlSourceSchema).default([]),
1850
+ });
1851
+
1654
1852
  /**
1655
1853
  * Opt-in custom frontmatter keys. `extend` maps each extra key a project's
1656
1854
  * pages may carry (e.g. `owner`, `reviewedAt`) to a validation schema; the
@@ -1736,6 +1934,7 @@ export const blumeConfigSchema = z
1736
1934
  /** Opt-in custom frontmatter keys, validated by user-supplied schemas. */
1737
1935
  frontmatter: frontmatterConfigSchema.prefault({}),
1738
1936
  github: githubConfigSchema.optional(),
1937
+ graphql: graphqlConfigSchema.prefault({}),
1739
1938
  i18n: i18nConfigSchema.optional(),
1740
1939
  image: imageConfigSchema.prefault({}),
1741
1940
  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>();