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
@@ -20,6 +20,11 @@ export interface RemoteFontConfig {
20
20
  provider?: RemoteFontProvider;
21
21
  /** Weights (or variable ranges like `"100..900"`) to load. Defaults to `[400, 500, 600, 700]`. */
22
22
  weights?: (number | string)[];
23
+ /**
24
+ * Character subsets to load (`"latin"`, `"vietnamese"`, `"cyrillic"`, …).
25
+ * Defaults to `latin` plus whatever the site's configured locales need.
26
+ */
27
+ subsets?: string[];
23
28
  /** Fallback stack category. Defaults to `"mono"` for the mono role, `"sans"` otherwise. */
24
29
  fallback?: FontCategory;
25
30
  }
@@ -52,6 +57,7 @@ export type FontEntry = {
52
57
  cssVariable: string;
53
58
  fallbacks: string[];
54
59
  name: string;
60
+ subsets: string[];
55
61
  weights: (number | string)[];
56
62
  } | {
57
63
  kind: "local";
@@ -193,19 +199,43 @@ export type FontSlug = keyof typeof GOOGLE_FONTS;
193
199
  export declare const FONT_SLUGS: string[];
194
200
  /** Type guard: is `value` a supported font slug? */
195
201
  export declare const isFontSlug: (value: string) => value is FontSlug;
202
+ /** The shape of a config's `i18n` block the font builders read. */
203
+ export interface FontLocaleSource {
204
+ locales: {
205
+ code: string;
206
+ }[];
207
+ }
208
+ /** The locale codes an optional `i18n` block declares (none when absent). */
209
+ export declare const fontLocaleCodes: (i18n: FontLocaleSource | undefined) => string[];
210
+ /**
211
+ * The subsets the site's locales need: `latin` always, plus each locale's
212
+ * script. A site with no `i18n` block (or only Latin-1 languages) gets just
213
+ * `latin` — Astro's own default — so its CSS and preloads don't change.
214
+ */
215
+ export declare const localeFontSubsets: (locales: string[]) => string[];
196
216
  /** Kebab-case a family name into a slug (`"Noto Sans JP"` -> `"noto-sans-jp"`). */
197
217
  export declare const slugifyFontName: (name: string) => string;
198
- /** The unique Astro `fonts:` entries for the configured roles (deduped). */
199
- export declare const buildFontEntries: (fonts: FontsConfig) => FontEntry[];
218
+ /**
219
+ * The unique Astro `fonts:` entries for the configured roles (deduped).
220
+ * `locales` are the site's configured locale codes; they pick the subsets a
221
+ * remote family loads unless its config pins `subsets` itself.
222
+ */
223
+ export declare const buildFontEntries: (fonts: FontsConfig, locales?: string[]) => FontEntry[];
200
224
  /**
201
225
  * The config-token CSS that points each role's `--blume-font-<role>-src` at the
202
226
  * Astro-populated family variable. Concatenated into the generated entry's
203
227
  * config tokens; empty when no fonts are set so defaults stay the system stacks.
204
228
  */
205
229
  export declare const buildFontsCss: (fonts: FontsConfig) => string;
206
- /** One `<Font>` render in the head: its CSS variable + weights to preload. */
230
+ /**
231
+ * One `<Font>` render in the head: its CSS variable + the weights (and, for
232
+ * providers that split faces by subset, the subsets) to preload. Local and
233
+ * Fontshare faces carry no subset, so those entries leave `preloadSubsets`
234
+ * unset and preload by weight alone.
235
+ */
207
236
  export interface FontHead {
208
237
  cssVariable: string;
238
+ preloadSubsets?: string[];
209
239
  preloadWeights: number[];
210
240
  }
211
241
  /**
@@ -213,4 +243,4 @@ export interface FontHead {
213
243
  * by CSS variable with preload weights unioned across the roles that share a
214
244
  * family (so `display` and `body` both set to Inter preload 400/500/600 once).
215
245
  */
216
- export declare const configuredFonts: (fonts: FontsConfig) => FontHead[];
246
+ export declare const configuredFonts: (fonts: FontsConfig, locales?: string[]) => FontHead[];
package/docs/07-faq.mdx CHANGED
@@ -87,14 +87,14 @@ We reported it upstream in [oxc-project/oxc#24096](https://github.com/oxc-projec
87
87
 
88
88
  Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships the same fix in its own repo, and you can apply it in any project.
89
89
 
90
- 1. Save the patch as `patches/oxfmt@0.61.0.patch`:
91
-
92
- ```diff patches/oxfmt@0.61.0.patch
93
- diff --git a/dist/markdown-ZuiQU4Xe.js b/dist/markdown-ZuiQU4Xe.js
94
- index 566b9e6d27f36061d64b93736e238e871e1ee2b2..82d0595acc010807c2939fc4a1717dde887a8555 100644
95
- --- a/dist/markdown-ZuiQU4Xe.js
96
- +++ b/dist/markdown-ZuiQU4Xe.js
97
- @@ -4875,7 +4875,43 @@ function lu(e, t, r) {
90
+ 1. Save the patch as `patches/oxfmt@0.66.0.patch`:
91
+
92
+ ```diff patches/oxfmt@0.66.0.patch
93
+ diff --git a/dist/markdown-BgZGxhM2.js b/dist/markdown-BgZGxhM2.js
94
+ index 859231f9387a50df16419bc22b5268c4e446ddbc..dcc0ffda6f71adb27a03e3918a3db6fc7a58ffe9 100644
95
+ --- a/dist/markdown-BgZGxhM2.js
96
+ +++ b/dist/markdown-BgZGxhM2.js
97
+ @@ -4872,7 +4872,43 @@ function lu(e, t, r) {
98
98
  case "sentence": return Oh(e, r);
99
99
  case "word": return t.parser !== "mdx" ? zh(e, t) : Uh(e);
100
100
  case "whitespace": {
@@ -146,7 +146,7 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
146
146
  ```json package.json
147
147
  {
148
148
  "patchedDependencies": {
149
- "oxfmt@0.61.0": "patches/oxfmt@0.61.0.patch"
149
+ "oxfmt@0.66.0": "patches/oxfmt@0.66.0.patch"
150
150
  }
151
151
  }
152
152
  ```
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Ignored by the including page
3
+ ---
4
+
5
+ :::tip
6
+ This callout lives in `_snippets/include-demo.mdx` — it renders here because the page splices it with an `<include>` statement.
7
+ :::
@@ -12,7 +12,7 @@ openapi: {
12
12
  }
13
13
  ```
14
14
 
15
- That mounts the reference at `/reference` (an overview page) with each operation at `/reference/<tag>/<operation>`. The `spec` is either an `http(s)` URL or a path to a local file in your project. Blume parses it with [Scalar's OpenAPI parser](https://github.com/scalar/scalar) — Swagger 2.0 and OpenAPI 3.0 specs are upgraded to 3.1 automatically.
15
+ That mounts the reference at `/reference` (an overview page) with each operation at `/reference/<tag>/<operation>`. The `spec` is either an `http(s)` URL or a path to a local file in your project. Blume parses it with [Scalar's OpenAPI parser](https://github.com/scalar/scalar) — Swagger 2.0 and OpenAPI 3.0 specs are upgraded to 3.1 automatically. Documenting a GraphQL API instead? See the [GraphQL reference](/docs/advanced/graphql).
16
16
 
17
17
  The reference doesn't add a header tab on its own. To surface it, point a [navigation tab](/docs/content/navigation#tabs) at its route — this also scopes the operations sidebar for the native renderer:
18
18
 
@@ -139,6 +139,15 @@ openapi: {
139
139
  - `includeInLlms: false` keeps them out of both `llms.txt` files.
140
140
  - `noindex: true` adds crawler noindex metadata and removes the pages from the sitemap.
141
141
 
142
+ Each operation page's meta description is the operation's own `description` (or `summary`), followed by a generated sentence naming the endpoint — "Reference for the `GET /pets` endpoint in the Petstore API." — so a spec of terse one-line summaries still ships a distinct, snippet-length description per page. That sentence is English. On a site whose spec prose is written in another language, set `seoDescriptionSuffix: false` on the source to drop it and describe each page with the authored prose alone; an operation with neither a `description` nor a `summary` falls back to its title (`GET /pets`), so no page ships an empty description:
143
+
144
+ ```ts blume.config.ts lineNumbers
145
+ openapi: {
146
+ enabled: true,
147
+ sources: [{ spec: "./openapi.de.json", seoDescriptionSuffix: false }],
148
+ }
149
+ ```
150
+
142
151
  With the [Scalar renderer](#the-scalar-renderer), only `noindex` applies — a Scalar-rendered reference already sits outside Blume's search and `llms.txt`, so the two `include*` settings have nothing to act on there.
143
152
 
144
153
  ## Authorization
@@ -165,7 +174,7 @@ openapi: {
165
174
  }
166
175
  ```
167
176
 
168
- A Scalar-rendered reference is a self-contained embed on its own route — it doesn't weave into Blume's sidebar, search, or `llms.txt`, and Blume's [`playground`](#try-it-playground) config doesn't apply to it. Scalar brings its own request client, which calls your **target API directly from the browser** (the `playground.proxy` route isn't available here), so the API must allow cross-origin requests from the docs site (`Access-Control-Allow-Origin`). `theme` applies to the Scalar renderer only.
177
+ A Scalar-rendered reference is a self-contained embed on its own route — it doesn't weave into Blume's sidebar, search, or `llms.txt`, and Blume's [`playground`](#try-it-playground) config doesn't apply to it. It does follow Blume's light/dark toggle: the embed is pinned to the page's theme when it mounts and switches with it, so Scalar's own theme switch is hidden (set `scalar.forceDarkModeState` or `scalar.darkMode` to hand color mode back to Scalar). Scalar brings its own request client, which calls your **target API directly from the browser** (the `playground.proxy` route isn't available here), so the API must allow cross-origin requests from the docs site (`Access-Control-Allow-Origin`). `theme` applies to the Scalar renderer only.
169
178
 
170
179
  ### Passing Scalar options
171
180
 
@@ -202,7 +211,7 @@ AsyncAPI **2.x specs are normalized to 3.x automatically** with the official Asy
202
211
 
203
212
  Code samples are **protocol-aware**, keyed off the operation's binding (or its servers' protocol): `wscat` and a browser `WebSocket` snippet for WebSockets, `kcat` for Kafka, `mosquitto_pub`/`mosquitto_sub` for MQTT. `codeSamples` filters that set, the same way it picks languages on the `openapi` block; a protocol without a supported tool renders the message payload example alone rather than a fabricated client.
204
213
 
205
- Everything documented above carries over, [`playground`](#try-it-for-events) included: `route`, `sources` with `label`/`route`, `expandSchemas`, the [per-source indexing](#per-source-indexing) flags, and search indexing by operation summary and tag.
214
+ Everything documented above carries over, [`playground`](#try-it-for-events) included: `route`, `sources` with `label`/`route`, `expandSchemas`, the [per-source indexing](#per-source-indexing) flags (`seoDescriptionSuffix` too — the generated sentence names the channel and action instead of an endpoint), and search indexing by operation summary and tag.
206
215
 
207
216
  Setting `renderer: "scalar"` opts back into the embedded Scalar SPA, where — as with OpenAPI — only `noindex` applies. Scalar has no AsyncAPI playground of its own; its embed auto-detects the document type and renders channels, operations, messages, and a Models section, so that swap trades the composer away.
208
217
 
@@ -225,7 +234,7 @@ asyncapi: {
225
234
  ```
226
235
 
227
236
  :::note
228
- `playground.proxy` is OpenAPI-only. It forwards HTTP requests, and a WebSocket connect goes straight from the browser to the server named in the URL, so there's nothing for a proxy to sit in front of.
237
+ `playground.proxy` doesn't apply to event operations. It forwards HTTP requests, and a WebSocket connect goes straight from the browser to the server named in the URL, so there's nothing for a proxy to sit in front of.
229
238
  :::
230
239
 
231
240
  The event composer collects no broker credentials. Each operation page's **Authorization** section documents what the broker expects, and a WebSocket connect carries only what's already in the URL. Nothing is persisted for event operations.
@@ -74,7 +74,7 @@ The module exposes:
74
74
  type: "BlumeDataConfig",
75
75
  required: true,
76
76
  description:
77
- "Resolved site settings: title, description, logo, favicon, appleIcon, banner, theme, site, repoUrl, search, i18n, mcp, ask, og, analytics, feedback, structuredData, toc, codeThemes, codeWrap, and imageZoom.",
77
+ "Resolved site settings: title, description, logo, favicon, appleIcon, banner, theme, site, repoUrl, github (owner, repo, host, and the REST api base — null when unset), search, i18n, mcp, ask, og, analytics, feedback, structuredData, toc, codeThemes, codeWrap, and imageZoom.",
78
78
  },
79
79
  navigation: {
80
80
  type: "Navigation",
@@ -272,7 +272,9 @@ import data from "blume:data";
272
272
 
273
273
  ## 404 page
274
274
 
275
- Blume ships a default **not found** page out of the box: a centered "404" message wrapped in the site chrome (header, search, theme), served for any unmatched URL. `blume build` writes it to `404.html`, which static hosts serve automatically, and `blume dev` shows it for unknown routes.
275
+ Blume ships a default **not found** page out of the box: a centered "404" message wrapped in the site chrome (header, search, theme), served for any unmatched URL. `blume build` writes it to `404.html`, which static hosts serve automatically, and `blume dev` shows it for unknown routes. Under the message, a **Where to look next** list links every top-level section plus the `sitemap.xml` and [`llms.txt`](/docs/configuration/ai#llmstxt) indexes when they exist, so a reader — or an agent that followed a stale URL — has a way back.
276
+
277
+ The page also has a Markdown twin at `/404.md` with the same recovery links (absolute URLs once [`deployment.site`](/docs/deployment) is set). On a [Vercel server build](/docs/deployment#server-rendering), a request for a missing page that sends [`Accept: text/markdown`](/docs/configuration/ai#content-negotiation), or asks for a `.md` URL no page backs, gets that Markdown body with the `404` status instead of the HTML shell — so an agent never has to parse a page of chrome to learn where to go next.
276
278
 
277
279
  To replace it with your own, add a `pages/404.astro`. It owns the `/404` route the same way `pages/changelog.astro` takes over the changelog — your page wins and the default is dropped. Build it like any other custom page, in `PageLayout` or `RootLayout`:
278
280
 
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: GraphQL
3
+ description: Drop in a GraphQL schema and get a native API reference — one real page per operation and per type, in your sidebar and search.
4
+ ---
5
+
6
+ Point Blume at a GraphQL schema and it generates a native API reference: one **real page per root field** — queries, mutations, and subscriptions — plus one **page per named type** (objects, input objects, enums, interfaces, unions, and custom scalars). Every page shows arguments, defaults, deprecations, and usage backlinks, alongside a generated example operation, code samples, and an interactive [Try it](#try-it-playground) panel. Because each page is a genuine Blume page, it gets its own URL, shows up in **site search** and `llms.txt`, and gets an Open Graph image — the same as any hand-written doc.
7
+
8
+ ```ts blume.config.ts lineNumbers
9
+ graphql: {
10
+ enabled: true,
11
+ spec: "./schema.graphql",
12
+ endpoint: "https://api.example.com/graphql",
13
+ }
14
+ ```
15
+
16
+ That mounts the reference at `/graphql` (an overview page), with root fields at `/graphql/queries/<field>`, `/graphql/mutations/<field>`, and `/graphql/subscriptions/<field>`, and types grouped by kind at `/graphql/objects/<type>`, `/graphql/enums/<type>`, and so on.
17
+
18
+ The `spec` is either a path to a local file in your project or an `http(s)` URL, and accepts two formats:
19
+
20
+ - **SDL text** — a `.graphql` file with type definitions.
21
+ - **An introspection result** — the JSON produced by running the standard introspection query, either the raw `{ "__schema": … }` shape or the full `{ "data": { "__schema": … } }` response envelope.
22
+
23
+ The `endpoint` is the live GraphQL API URL. A schema, unlike an OpenAPI document, names no server — so the endpoint is what the Try it panel and the generated code samples target. Leave it off and the samples render with a placeholder URL readers replace.
24
+
25
+ The reference doesn't add a header tab on its own. To surface it, point a [navigation tab](/docs/content/navigation#tabs) at its route — this also scopes the reference sidebar:
26
+
27
+ ```ts blume.config.ts
28
+ navigation: {
29
+ tabs: [{ label: "GraphQL", path: "/graphql" }],
30
+ }
31
+ ```
32
+
33
+ ## Generated examples
34
+
35
+ Every operation page carries a complete, valid example operation — one variable per argument, typed off the schema, with a bounded-depth selection set over the return type — plus matching example variables and an example response that mirrors the same selection. Code samples show the exact HTTP request (a JSON `POST` of `{ query, variables }`) in each configured language:
36
+
37
+ ```ts blume.config.ts lineNumbers
38
+ graphql: {
39
+ enabled: true,
40
+ spec: "./schema.graphql",
41
+ codeSamples: ["curl", "js"], // built in: curl, js, python
42
+ }
43
+ ```
44
+
45
+ ## Type pages
46
+
47
+ Named types get their own deep-linkable pages, grouped by kind in the sidebar: fields and input fields with their types linked, enum values, union members, interface implementations, and a **Used by** section listing the operations that return or accept the type and the other types that reference it. Spec-defined scalars (`String`, `Int`, …) don't get pages; custom scalars do, including their `specifiedBy` URL.
48
+
49
+ ## Multiple schemas
50
+
51
+ Each entry in `sources` renders one schema on its own route. A per-source `endpoint` overrides the block-level one:
52
+
53
+ ```ts blume.config.ts lineNumbers
54
+ graphql: {
55
+ enabled: true,
56
+ endpoint: "https://api.example.com/graphql",
57
+ sources: [
58
+ { label: "Public API", spec: "./schema.graphql" },
59
+ {
60
+ label: "Admin API",
61
+ route: "/graphql-admin",
62
+ spec: "./admin.graphql",
63
+ endpoint: "https://admin.example.com/graphql",
64
+ },
65
+ ],
66
+ }
67
+ ```
68
+
69
+ Each source takes the same [per-source controls](/docs/advanced/api-reference#per-source-indexing) as the OpenAPI block: `includeInSearch`, `includeInLlms`, `noindex`, and `seoDescriptionSuffix` (here the generated sentence names the query, mutation, or type — "Reference for the `pets` query in the GraphQL API.").
70
+
71
+ ## Try it playground
72
+
73
+ Query and mutation pages render an interactive panel: edit the request body (the query and variables), point it at your endpoint or a custom URL, and send — the code samples update live so what you copy is byte-for-byte what was sent. Disable it with `playground: false`. Subscription pages show the generated operation and an example event instead: subscriptions run over a stateful transport (WebSocket or SSE) that the playground's single HTTP `POST` can't speak.
74
+
75
+ If your GraphQL API doesn't allow cross-origin requests from the docs site, route sends through a CORS proxy — a URL of your own, or `true` for the built-in `/_api-proxy` endpoint (which requires `deployment.output: "server"`). The built-in proxy only forwards to origins your documented specs declare — each configured GraphQL `endpoint`, plus any absolute `servers[].url` from a documented [OpenAPI spec](/docs/advanced/api-reference) — so a public docs deployment can't be aimed at other hosts. That makes `endpoint` required for a working proxy: without one, the proxy has no origin to allow for this reference and refuses every send (the build warns about this).
76
+
77
+ ```ts blume.config.ts lineNumbers
78
+ graphql: {
79
+ enabled: true,
80
+ spec: "./schema.graphql",
81
+ endpoint: "https://api.example.com/graphql",
82
+ playground: { proxy: true },
83
+ }
84
+ ```
@@ -2,6 +2,13 @@ import { defineMeta } from "blume";
2
2
 
3
3
  export default defineMeta({
4
4
  order: 5,
5
- pages: ["skills", "custom-pages", "changelog", "blog", "api-reference"],
5
+ pages: [
6
+ "skills",
7
+ "custom-pages",
8
+ "changelog",
9
+ "blog",
10
+ "api-reference",
11
+ "graphql",
12
+ ],
6
13
  title: "Advanced",
7
14
  });
@@ -33,6 +33,22 @@ ai: {
33
33
  }
34
34
  ```
35
35
 
36
+ The object form also takes `details`: Markdown placed right after the title and summary in `llms.txt`, before the page sections — the [llms.txt spec](https://llmstxt.org)'s free-form "details" block. It's the place to tell agents _when_ to reach for your product and how to call it, which readiness scanners look for explicitly; an install command and the package name belong here too:
37
+
38
+ ```ts blume.config.ts lineNumbers
39
+ ai: {
40
+ llmsTxt: {
41
+ details: [
42
+ "## When to use Acme",
43
+ "",
44
+ "Reach for Acme when a project needs hosted feature flags. Install the CLI with `npm install -g acme`; the API reference below covers every endpoint.",
45
+ ].join("\n"),
46
+ },
47
+ }
48
+ ```
49
+
50
+ `llms.txt` closes with two generated sections that need no configuration. **Agent skills** lists each skill published through [`ai.skills`](#skills-discovery) with its description (where a skill says when to use it). **Agent resources** links every machine-readable artifact the build emits — `llms-full.txt`, the per-page [raw Markdown](#raw-markdown) mirror, the [MCP server](#mcp-server) and its discovery document, the skills index, the [API catalog](#api-catalog), [`agent-readability.json`](#agent-readability), and the [sitemap](/docs/configuration/seo#sitemap) — each only when it exists, so an agent that reads nothing but `llms.txt` still finds the whole surface.
51
+
36
52
  To keep an individual page out of both files, set `ai.exclude` in its frontmatter:
37
53
 
38
54
  ```mdx
@@ -59,12 +75,14 @@ Append `.md` or `.mdx` to any page's URL to fetch its raw Markdown source — pe
59
75
 
60
76
  Nested routes work the same way (`/content/syntax.md`), and the home page is served at `/index.md`.
61
77
 
62
- The `.md` variant _downlevels_ components to plain Markdown for consumers that can't interpret JSX: `<TypeTable>` becomes a Markdown table, `<Callout>` a labeled blockquote, `<Steps>` an ordered list, `<Tabs>` bold-labeled sections, and `<YouTube>` a link. Props are evaluated with the page's `frontmatter` in scope, so a prop like `title={frontmatter.status}` resolves to the same value the rendered page shows. Anything that can't be converted faithfully — a custom component, or a prop computed from an import — is left as-is, and component markup inside fenced code blocks is never touched. The same conversion applies to `llms-full.txt` and the MCP server's `get_page` tool, so every agent-facing surface reads clean Markdown. When you want the untransformed source, use the `.mdx` variant.
78
+ The `.md` variant _downlevels_ components to plain Markdown for consumers that can't interpret JSX: `<TypeTable>` becomes a Markdown table, `<Callout>` a labeled blockquote, `<Steps>` an ordered list, `<Tabs>` bold-labeled sections, `<Card>` its title as a link over its body (and `<CardGroup>` the cards it holds), and `<YouTube>` a link. Props are evaluated with the page's `frontmatter` in scope, so a prop like `title={frontmatter.status}` resolves to the same value the rendered page shows. Anything that can't be converted faithfully — a custom component, or a prop computed from an import — is left as-is, and component markup inside fenced code blocks is never touched. The same conversion applies to `llms-full.txt` and the MCP server's `get_page` tool, so every agent-facing surface reads clean Markdown. When you want the untransformed source, use the `.mdx` variant.
63
79
 
64
80
  ### Content negotiation
65
81
 
66
82
  Agents don't need to know the `.md` convention: requesting a page's own URL with an [`Accept: text/markdown`](https://acceptmarkdown.com) header serves the Markdown variant at the same address, with `Vary: Accept` so caches keep the two apart. The dev server honors the header out of the box, and a [Vercel or Cloudflare server build](/docs/deployment#server-rendering) wires the same negotiation into the deploy automatically — routing rules on Vercel, a generated Worker on Cloudflare — no configuration needed. The homepage always negotiates, even when it's a custom landing page rather than a content page: its Markdown mirror falls back to the [`llms.txt`](#llmstxt) index, so an agent asking the site root for Markdown gets the machine-readable map of the site. Markdown responses also carry an `x-markdown-tokens` header — an estimated token count (~4 characters per token), following the convention of [Cloudflare's Markdown for Agents](https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/) — on every surface where Blume controls response headers: the dev server, server-rendered responses, and the negotiated homepage on Vercel and Cloudflare. Other deploy targets serve prerendered pages from a static layer with no request-time hook, so agents there fetch the `.md` URL directly; the [agent readability manifest](#agent-readability) advertises `contentNegotiation` only on deployments that honor the header.
67
83
 
84
+ Missing pages negotiate too. Every build emits a Markdown [404 page](/docs/advanced/custom-pages#404-page) at `/404.md` — the not-found message followed by recovery links to every top-level section, the sitemap, and `llms.txt` — and on Vercel a request for a nonexistent URL that prefers Markdown, or any `.md` URL with no page behind it, gets that body with a real `404` status rather than the HTML shell.
85
+
68
86
  ### Custom component serializers
69
87
 
70
88
  Give your own components a Markdown form with `ai.markdownComponents` — a map of JSX name to serializer. Each serializer receives the component's `props` (statically evaluated from the MDX attributes, with the page's `frontmatter` in scope), its `children` (already downleveled to Markdown), and the page's `frontmatter` data, and returns the replacement — or `null` to leave the JSX as-is:
@@ -85,7 +103,7 @@ export default defineConfig({
85
103
  });
86
104
  ```
87
105
 
88
- For container components, `childComponents("Name")` extracts direct children by tag — the same way the built-in `<Steps>` serializer collects its `<Step>` items. A same-name entry replaces a built-in serializer, so you can restyle how `<Callout>` downlevels — or return `null` to opt one out entirely.
106
+ For container components, `childComponents("Name")` extracts direct children by tag — the same way the built-in `<Steps>` serializer collects its `<Step>` items — and `childBlocks()` returns every direct child in order, components and prose alike, each already downleveled to a block of Markdown (the built-in `<CardGroup>` serializer is just those blocks joined by blank lines). A same-name entry replaces a built-in serializer, so you can restyle how `<Callout>` downlevels — or return `null` to opt one out entirely.
89
107
 
90
108
  Serializers live in `blume.config.ts`, not `components.tsx`: the config file is executed at build time, while the components file is only statically analyzed (it may import `.astro` files, which can't run outside the site build). Your components themselves stay registered in `components.tsx` exactly as before — `markdownComponents` only adds their agent-facing Markdown form.
91
109
 
@@ -93,6 +111,8 @@ Serializers live in `blume.config.ts`, not `components.tsx`: the config file is
93
111
 
94
112
  Every page carries a **Copy as Markdown** action — in the [page actions](/docs/content/navigation#page-actions) beneath the table of contents — that copies the page's raw Markdown to the clipboard. It's the same source served at the [`.md` URL](#raw-markdown) above, ready to paste into an LLM, an issue, or your notes. It's available on every page, in dev and production, with no configuration.
95
113
 
114
+ Where the Clipboard API is unavailable or the browser denies it — in-app browsers, WebViews, insecure origins — the action falls back to the legacy copy command, and if nothing lands on the clipboard the button reports **Copy failed** (localized via `actions.copyFailed`) rather than staying silent. The same fallback backs every copy button Blume renders.
115
+
96
116
  ## Open in chat
97
117
 
98
118
  The **Open in chat** action opens the current page in an AI assistant — v0, ChatGPT, Claude, T3 Chat, Scira, or Cursor — pre-filled with a prompt that points it at the page's raw Markdown so it can answer questions about what you're reading:
@@ -101,6 +121,8 @@ The **Open in chat** action opens the current page in an AI assistant — v0, Ch
101
121
 
102
122
  Like Copy as Markdown, it needs no setup. The assistant fetches the page over its public URL, so it works as soon as the page is deployed.
103
123
 
124
+ The prompt is part of the [UI dictionary](/docs/content/i18n#translated-ui) (`actions.openInChatPrompt`), so localized sites send it in their language, and `i18n.ui` can override the wording — keep the `{url}` placeholder, which is replaced with the page's raw-Markdown URL.
125
+
104
126
  To tailor the action, set `ai.openInChat`. `false` hides it entirely, and an array of provider keys — `"v0"`, `"chatgpt"`, `"claude"`, `"t3"`, `"scira"`, `"cursor"` — shows just those providers, in the order you list them:
105
127
 
106
128
  ```ts blume.config.ts lineNumbers
@@ -306,7 +328,7 @@ ai: {
306
328
  | `name` | title | Server name shown to clients (defaults to title). |
307
329
  | `instructions` | — | Optional system hint passed to connecting agents. |
308
330
 
309
- The server exposes read-only tools — `search_docs`, `get_page`, `list_pages`, and `get_navigation` — and publishes discovery documents at `/.well-known/mcp.json` and `/.well-known/mcp/server-card.json`. The server card follows the [SEP-2127](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127) Server Card extension schema (reverse-DNS `name`, `remotes` transport endpoints), with initialize-shaped compat fields (`serverInfo`, `capabilities`, `transports`) for scanners built against the proposal's earlier revision. Each page's **Connect to MCP** menu offers copy-and-go install for Claude Code, Cursor, VS Code, and Codex (shown once [`deployment.site`](/docs/deployment) is set).
331
+ The server exposes read-only tools — `search_docs`, `get_page`, `list_pages`, and `get_navigation` — and every page as an MCP resource (`resources/list` enumerates the pages at their served URLs with a `text/markdown` type; `resources/read` returns the page's agent Markdown, the same output as `get_page`), so clients that attach context by URI can browse the docs without calling a tool. It publishes discovery documents at `/.well-known/mcp.json` and `/.well-known/mcp/server-card.json`. The server card follows the [SEP-2127](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127) Server Card extension schema (reverse-DNS `name`, `remotes` transport endpoints), with initialize-shaped compat fields (`serverInfo`, `capabilities`, `transports`) for scanners built against the proposal's earlier revision. Each page's **Connect to MCP** menu offers copy-and-go install for Claude Code, Cursor, VS Code, and Codex (shown once [`deployment.site`](/docs/deployment) is set).
310
332
 
311
333
  `search_docs` runs its own full-text index, so it works regardless of your [search](/docs/configuration/search) provider — and even when search is set to `none`. The MCP server is a separate feature from on-page search.
312
334
 
@@ -285,6 +285,30 @@ github: {
285
285
  | `repo` | — | Repository name. |
286
286
  | `branch` | `"main"` | Branch that edit links point at. |
287
287
  | `dir` | — | Path from the repo root to the project root (for monorepos). |
288
+ | `host` | `"https://github.com"` | Origin of the GitHub instance, for Enterprise installations. Must be HTTP(S); normalized to its origin. |
289
+ | `api` | derived from `host` | REST API base, for `<GithubInfo>` against an Enterprise instance. Must be HTTP(S); normalized to an origin and path. |
290
+
291
+ ### GitHub Enterprise
292
+
293
+ Docs whose repository lives on a GitHub Enterprise instance set `host`, and every repo-derived link — the header mark, edit links, the agent manifest — points at that instance instead of the public site:
294
+
295
+ ```ts blume.config.ts lineNumbers
296
+ github: {
297
+ host: "https://github.acme.com",
298
+ owner: "acme",
299
+ repo: "docs",
300
+ }
301
+ ```
302
+
303
+ The REST API base that [`<GithubInfo>`](/docs/content/components) queries is derived from `host`: an Enterprise Cloud tenant with data residency (`acme.ghe.com`) is served from its `api.` subdomain, and any other host is treated as Enterprise Server (`/api/v3`). Set `api` explicitly when your instance sits somewhere else.
304
+
305
+ :::warning
306
+ An instance reachable only over plain HTTP still renders its counts, but `GITHUB_TOKEN` is withheld from the request rather than sent in cleartext — so a private repo's card comes back without them.
307
+ :::
308
+
309
+ :::note
310
+ `host` covers links Blume derives from `github`. To point only the header mark somewhere else — an organization, say, when the docs repo itself is private — use [`navigation.repo`](/docs/content/navigation#repository-link) with an absolute URL.
311
+ :::
288
312
 
289
313
  ## Last modified
290
314
 
@@ -40,7 +40,19 @@ Write `href` as if the site were mounted at the root — a `basePath` is applied
40
40
 
41
41
  ## What's indexed
42
42
 
43
- For every indexable page, Blume indexes its title, description, and body reduced to plain text — code blocks, images, and markup are stripped, so results stay relevant. The index is built from your source files, so it's identical in dev and production.
43
+ For Orama, FlexSearch, Algolia, Orama Cloud, and Typesense — and the MCP server's `search_docs` tool — Blume indexes each page's title, description, and body reduced to plain text: code blocks, images, and markup are stripped, so results stay relevant. These indexes are built from your source files, so they're identical in dev and production. Pagefind indexes the built HTML instead, and Mixedbread syncs your raw Markdown, so both always search code.
44
+
45
+ If your docs rely on code examples for searchable terms like options, methods, or error names, opt fenced code into the source-built indexes:
46
+
47
+ ```ts blume.config.ts lineNumbers
48
+ search: {
49
+ indexing: {
50
+ includeCodeBlocks: true,
51
+ },
52
+ },
53
+ ```
54
+
55
+ Each fence's body and title (`blume.config.ts` above) become searchable; the language and fence markers don't. On `.mdx` pages the index reads components as the text they show — a Card's title, a Tab's label, a TypeTable's descriptions — using the same serializers as the [agent surfaces](/docs/configuration/ai), so an `ai.markdownComponents` entry covers your own components too. The option has no effect on Pagefind or Mixedbread. Expect the index to grow with your fenced content — the client index ships to every reader, hosted providers cap record size (Algolia rejects the sync batch when one page's record exceeds its plan's limit, leaving the previous index live), and a hit inside a fence shows flattened code in the result excerpt.
44
56
 
45
57
  On a [versioned](/docs/content/versioning) site, results default to the version being viewed, with an "All versions" toggle in the dialog footer (remembered per reader). Cross-version hits name their version on the row. Orama, FlexSearch, Algolia, and Typesense honor the scoping — hosted records carry a `version` facet, with the current docs uploaded as `"current"` — while Pagefind stays unscoped, matching its locale behavior.
46
58
 
@@ -125,7 +125,7 @@ seo: {
125
125
  }
126
126
  ```
127
127
 
128
- By default, each card is derived from your content and theme — the **page title** as the headline, your **site title** as the eyebrow, and your theme **accent** for the mark. Images are served at `/og/<slug>.png`, mirroring each route, and are prerendered as static files even in server mode:
128
+ By default, each card is derived from your content and theme — the **page title** as the headline, the **page description** as the subtitle (the same text as its `og:description`, so `seo.description` wins over `description`), your **site title** as the eyebrow, and your theme **accent** for the mark. Images are served at `/og/<slug>.png`, mirroring each route, and are prerendered as static files even in server mode:
129
129
 
130
130
  | Page route | Image URL |
131
131
  | ------------------- | -------------------------- |
@@ -151,13 +151,13 @@ Emoji in a page title or site title render as [Twemoji](https://github.com/jdeck
151
151
 
152
152
  ### Show, hide, or override card layers
153
153
 
154
- Beyond the headline, the card carries three optional layers: the **brand mark** in the top-left (your logo, or an accent tile with the site title's initial), the **subtitle** under the headline (your site `description`), and a **footer** with your repo slug (from `github`) and the site's URL — the deployment site's host plus [`deployment.base`](/docs/deployment#subpath-deploys), so a GitHub Pages project site reads `user.github.io/repo`. Override any of them with a string of your own, or hide one with `false`:
154
+ Beyond the headline, the card carries three optional layers: the **brand mark** in the top-left (your logo, or an accent tile with the site title's initial), the **subtitle** under the headline (the page `description`, or your site `description` for pages without one), and a **footer** with your repo slug (from `github`) and the site's URL — the deployment site's host plus [`deployment.base`](/docs/deployment#subpath-deploys), so a GitHub Pages project site reads `user.github.io/repo`. Override any of them with a string of your own, or hide one with `false`:
155
155
 
156
156
  ```ts blume.config.ts lineNumbers
157
157
  seo: {
158
158
  og: {
159
159
  site: "docs.acme.com", // footer URL text, or false to hide it
160
- description: false, // hide the subtitle; a string overrides it
160
+ description: false, // hide the subtitle on every card; a string replaces the site fallback
161
161
  logo: false, // no brand mark at all — not even the initial tile
162
162
  },
163
163
  }
@@ -247,6 +247,33 @@ Each page includes:
247
247
 
248
248
  URLs are absolute when `deployment.site` is set. Pages marked `seo.noindex` are skipped.
249
249
 
250
+ ### Site identity
251
+
252
+ The WebSite node says a site exists; it doesn't say _what_ it is or _who_ runs it — which is what AI agents read JSON-LD for before they recommend or cite you. Two optional blocks fill that in, both needing `deployment.site` (the nodes carry absolute identifiers):
253
+
254
+ ```ts blume.config.ts lineNumbers
255
+ seo: {
256
+ organization: {
257
+ name: "Acme", // defaults to the site title
258
+ email: "hello@acme.com",
259
+ telephone: "+1 555 0100",
260
+ address: { addressLocality: "Sydney", addressCountry: "AU" },
261
+ logo: "/logo.svg",
262
+ sameAs: ["https://github.com/acme", "https://x.com/acme"],
263
+ },
264
+ software: {
265
+ license: "MIT",
266
+ operatingSystem: "Node.js 22+",
267
+ price: 0, // emitted as an Offer; 0 marks it free
268
+ sameAs: ["https://www.npmjs.com/package/acme"],
269
+ },
270
+ }
271
+ ```
272
+
273
+ `organization` adds an **Organization** node to every page — the WebSite and article nodes cite it as `publisher` — with the email and telephone as a `ContactPoint` (`contactType` defaults to `"customer support"`) and the address as a `PostalAddress`, the two fields business-verification checks look for. `name` and `url` default to the site's; a root-relative `logo` is absolutized like any page URL.
274
+
275
+ `software` adds a **SoftwareApplication** node to the homepage: the product's name and description (defaulting to the site's), `applicationCategory` (default `"DeveloperApplication"`), operating system, license, an `Offer` when `price` is set, and the registry or repository URLs in `sameAs`. Pass `software: true` to take every default. The organization, when configured, is cited as its `publisher`.
276
+
250
277
  ## Sitemap
251
278
 
252
279
  Blume writes a `sitemap.xml` of every indexable page at build time. It needs an absolute [`deployment.site`](/docs/deployment) and lists every page except drafts, hidden, and `noindex` pages. On a [versioned](/docs/content/versioning) site, archived pages whose canonical points at their live equivalent are left out too — the live page is the one to index. On by default:
@@ -105,8 +105,25 @@ theme: {
105
105
  - **`name`** — the family name exactly as the provider lists it.
106
106
  - **`provider`** — where the family comes from: `google` (default), `fontsource`, `bunny`, or `fontshare`.
107
107
  - **`weights`** — the weights to load, as numbers or a variable range like `"100..900"`. Defaults to `[400, 500, 600, 700]`.
108
+ - **`subsets`** — the character subsets to load, by the provider's names (`latin`, `latin-ext`, `vietnamese`, `cyrillic`, `greek`, …). Defaults to `latin` plus whatever your configured locales need — see below.
108
109
  - **`fallback`** — the system stack shown while the font loads and for missing glyphs: `sans`, `serif`, or `mono`. Defaults to `mono` for the mono role and `sans` otherwise.
109
110
 
111
+ #### Subsets and locales
112
+
113
+ Google, Bunny, and Fontsource split each family into per-script subsets, and only the subsets you load get a `@font-face`. Blume derives the list from your [`i18n.locales`](/docs/content/i18n): a site with Vietnamese, Polish, Russian, or Greek locales loads `vietnamese`, `latin-ext`, `cyrillic`, or `greek` alongside `latin`, so diacritics and non-Latin letters render in your chosen font instead of the system fallback. Sites without an `i18n` block, or with only Latin-1 languages, load `latin` alone. Browsers download a subset only when a page uses its characters, and preloads follow the same list.
114
+
115
+ Set `subsets` on a family to override the derived list — say, for a single-locale site whose content still needs a script its locale doesn't imply:
116
+
117
+ ```ts blume.config.ts lineNumbers
118
+ theme: {
119
+ fonts: {
120
+ body: { name: "Be Vietnam Pro", subsets: ["latin", "vietnamese"] },
121
+ },
122
+ }
123
+ ```
124
+
125
+ Curated slugs like `inter` follow the locale-derived list; use the object form to pin subsets for those families too.
126
+
110
127
  #### Local font files
111
128
 
112
129
  For a font you own (or one no provider serves), point a role at font files in your project. Each variant becomes one `@font-face`:
@@ -189,6 +206,12 @@ Drop a `theme.css` in your project root to override any design token. It's the l
189
206
 
190
207
  Set a token under `:root` for light mode and under `:root[data-theme="dark"]` for dark mode. Color tokens have distinct built-in dark values declared at the dark selector's higher specificity, so a `:root`-only override of `--blume-accent`, `--blume-background`, and friends applies to light mode only — declare the dark block too when both modes should change.
191
208
 
209
+ `theme.css` is inlined into the site's Tailwind entry, so Tailwind directives work in it too. The one to know for a monorepo is `@source`: Blume scans your project for utility classes, and a page that imports components from a sibling workspace package needs that package scanned as well. Point at it relative to `theme.css` — the standard Tailwind rule — and Blume carries the path into the generated sheet:
210
+
211
+ ```css theme.css lineNumbers
212
+ @source "../../packages/ui/src";
213
+ ```
214
+
192
215
  ### Design tokens
193
216
 
194
217
  | Token | Controls |
@@ -71,6 +71,8 @@ Switch between equivalent content in place — language variants, OS-specific co
71
71
 
72
72
  Add `inline` to render borderless — a tab strip on a full-width rule with the content flowing beneath as prose — instead of the bordered box. Add `param` to sync the active tab to a URL query param instead of the hash, which makes the selection shareable: a link ending in `?install=windows` opens on the Windows tab. Each group syncs to its own `param`, so you can use several independent, deep-linkable groups on one page.
73
73
 
74
+ Groups with same-titled tabs switch together — pick "macOS" in one and every group with a macOS tab follows. Add `syncKey` to scope that syncing: only groups sharing the same key switch together, so unrelated groups that happen to share a tab title stay independent.
75
+
74
76
  <Tabs inline param="install">
75
77
  <Tab title="macOS">Use Homebrew to install the toolchain.</Tab>
76
78
  <Tab title="Windows">Use winget to install the toolchain.</Tab>
@@ -546,6 +548,8 @@ export interface ButtonProps {
546
548
 
547
549
  A card linking to a GitHub repository with its live star and fork counts. Counts are fetched at build time — no client JavaScript — and the card still renders if the API is unreachable. Pass `owner` and `repo`, or omit them to use the repository from your `blume.config`. Set a `GITHUB_TOKEN` environment variable to lift the API rate limit.
548
550
 
551
+ The card reads the instance from [`github.host`](/docs/configuration#github-enterprise), so on an Enterprise-hosted site explicit `owner`/`repo` address that instance too. Pass `host` to point one card somewhere else — a public project from an Enterprise site, say; the REST base is derived from it the same way it is from `github.host`.
552
+
549
553
  <GithubInfo owner="haydenbleasel" repo="blume" />
550
554
 
551
555
  ```astro lineNumbers
@@ -554,13 +558,16 @@ A card linking to a GitHub repository with its live star and fork counts. Counts
554
558
 
555
559
  <!-- Or point it at any repository -->
556
560
  <GithubInfo owner="haydenbleasel" repo="blume" />
561
+
562
+ <!-- Or at a repository on another instance -->
563
+ <GithubInfo host="https://github.com" owner="haydenbleasel" repo="blume" />
557
564
  ```
558
565
 
559
566
  ## Component
560
567
 
561
568
  `Component` renders an example file from your project's `examples/` directory as a live preview alongside its highlighted source, in tabs. Point it at a file with `path` — its location under `examples/`, without the extension (so `examples/counter.tsx` is `path="counter"`). React, Vue, Svelte, and Astro examples are all supported; framework examples hydrate, Astro ones render statically. It keeps the preview and the code in sync from a single file.
562
569
 
563
- The preview renders in an isolated frame that the docs styles never reach — no prose margins, typography, or theme chrome bleed into your component. The frame gets Tailwind (preflight + utilities scanned from your example files and anything they import), Blume's design tokens so classes like `bg-background` follow the site palette by default, and it follows the site's light/dark toggle live. The pane sizes itself to the rendered example — and keeps tracking it if the example grows or shrinks after load — with the Preview and Code tabs sharing one height so toggling them never shifts the page.
570
+ The preview renders in an isolated frame that the docs styles never reach — no prose margins, typography, or theme chrome bleed into your component. The frame gets Tailwind (preflight + utilities scanned from your project and your examples directory), Blume's design tokens so classes like `bg-background` follow the site palette by default, and it follows the site's light/dark toggle live. The pane sizes itself to the rendered example — and keeps tracking it if the example grows or shrinks after load — with the Preview and Code tabs sharing one height so toggling them never shifts the page.
564
571
 
565
572
  To style previews with your own design system — say, shadcn variables — point `examples.css` at a stylesheet. It's injected into every preview frame after Blume's defaults, so your tokens win. Don't `@import "tailwindcss"` in it; the frame already provides Tailwind. Both `.dark` and `[data-theme="dark"]` work for dark-mode overrides:
566
573
 
@@ -616,6 +623,13 @@ export default defineConfig({
616
623
  <Component path="file-list/examples/basic" />
617
624
  ```
618
625
 
626
+ In a monorepo, the components your examples import usually live in a sibling workspace package. Tailwind's `@source` scans files, not imports: Blume scans your project and the `examples` directory (wherever `source` points, even outside the project), so a class used only inside that sibling package isn't generated until you add the package to the scan. Do that with an `@source` directive in `examples.css` (for the preview frames) or `theme.css` (for the site), written relative to the file it sits in — the standard Tailwind rule — and Blume carries it into the generated sheet:
627
+
628
+ ```css
629
+ /* examples/theme.css */
630
+ @source "../../../packages/ui/src";
631
+ ```
632
+
619
633
  <Component path="counter" />
620
634
 
621
635
  ```astro