blume 1.6.0 → 1.6.2

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 (97) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cli/index.js +1318 -270
  3. package/dist/cli/index.js.map +68 -61
  4. package/dist/types/core/config-input.d.ts +9 -0
  5. package/dist/types/core/data.d.ts +12 -1
  6. package/dist/types/core/i18n-ui.d.ts +4 -0
  7. package/dist/types/core/schema.d.ts +7 -0
  8. package/dist/types/core/types.d.ts +6 -0
  9. package/dist/types/openapi/references.d.ts +5 -0
  10. package/docs/07-faq.mdx +9 -9
  11. package/docs/advanced/api-reference.mdx +10 -1
  12. package/docs/advanced/custom-pages.mdx +3 -1
  13. package/docs/advanced/graphql.mdx +1 -1
  14. package/docs/configuration/ai.mdx +76 -7
  15. package/docs/configuration/seo.mdx +3 -3
  16. package/docs/configuration/theming.mdx +6 -0
  17. package/docs/content/components.mdx +8 -1
  18. package/docs/index.mdx +2 -2
  19. package/package.json +53 -53
  20. package/skills/blume/SKILL.md +2 -2
  21. package/src/ai/agent-readability.ts +60 -17
  22. package/src/ai/api/handlers.ts +273 -0
  23. package/src/ai/api/paths.ts +14 -0
  24. package/src/ai/api/problem.ts +63 -0
  25. package/src/ai/api/spec.ts +681 -0
  26. package/src/ai/api-catalog.ts +11 -1
  27. package/src/ai/link-headers.ts +12 -3
  28. package/src/ai/llms.ts +9 -2
  29. package/src/ai/mcp/query.ts +390 -0
  30. package/src/ai/mcp/server.ts +32 -352
  31. package/src/astro/examples.ts +29 -2
  32. package/src/astro/generate.ts +256 -64
  33. package/src/astro/index.ts +7 -0
  34. package/src/astro/markdown-negotiation.ts +1 -1
  35. package/src/astro/runtime-modules.ts +196 -0
  36. package/src/astro/templates.ts +398 -38
  37. package/src/cli/commands/build.ts +9 -1
  38. package/src/cli/commands/dev.ts +6 -3
  39. package/src/cli/host-args.ts +18 -0
  40. package/src/cli/index.ts +2 -1
  41. package/src/components/copy-feedback.ts +93 -9
  42. package/src/components/islands/ask-ai.tsx +4 -1
  43. package/src/components/islands/hooks.ts +3 -1
  44. package/src/components/layout/PageActions.astro +25 -14
  45. package/src/core/config-input.ts +9 -0
  46. package/src/core/data.ts +17 -2
  47. package/src/core/define-components.ts +2 -0
  48. package/src/core/i18n-ui.ts +3 -0
  49. package/src/core/includes.ts +2 -1
  50. package/src/core/manifest.ts +10 -0
  51. package/src/core/schema.ts +20 -5
  52. package/src/core/types.ts +6 -0
  53. package/src/core/ui-packs/ar.ts +1 -0
  54. package/src/core/ui-packs/bg.ts +1 -0
  55. package/src/core/ui-packs/bn.ts +1 -0
  56. package/src/core/ui-packs/ca.ts +1 -0
  57. package/src/core/ui-packs/cs.ts +1 -0
  58. package/src/core/ui-packs/da.ts +1 -0
  59. package/src/core/ui-packs/de.ts +1 -0
  60. package/src/core/ui-packs/el.ts +1 -0
  61. package/src/core/ui-packs/es.ts +1 -0
  62. package/src/core/ui-packs/fa.ts +1 -0
  63. package/src/core/ui-packs/fi.ts +1 -0
  64. package/src/core/ui-packs/fr.ts +1 -0
  65. package/src/core/ui-packs/he.ts +1 -0
  66. package/src/core/ui-packs/hi.ts +1 -0
  67. package/src/core/ui-packs/hr.ts +1 -0
  68. package/src/core/ui-packs/hu.ts +1 -0
  69. package/src/core/ui-packs/id.ts +1 -0
  70. package/src/core/ui-packs/it.ts +1 -0
  71. package/src/core/ui-packs/ja.ts +1 -0
  72. package/src/core/ui-packs/ko.ts +1 -0
  73. package/src/core/ui-packs/nl.ts +1 -0
  74. package/src/core/ui-packs/no.ts +1 -0
  75. package/src/core/ui-packs/pl.ts +1 -0
  76. package/src/core/ui-packs/pt-br.ts +1 -0
  77. package/src/core/ui-packs/pt.ts +1 -0
  78. package/src/core/ui-packs/ro.ts +1 -0
  79. package/src/core/ui-packs/ru.ts +1 -0
  80. package/src/core/ui-packs/sk.ts +1 -0
  81. package/src/core/ui-packs/sr.ts +1 -0
  82. package/src/core/ui-packs/sv.ts +1 -0
  83. package/src/core/ui-packs/th.ts +1 -0
  84. package/src/core/ui-packs/tr.ts +1 -0
  85. package/src/core/ui-packs/uk.ts +1 -0
  86. package/src/core/ui-packs/vi.ts +1 -0
  87. package/src/core/ui-packs/zh-tw.ts +1 -0
  88. package/src/core/ui-packs/zh.ts +1 -0
  89. package/src/core/version-cut.ts +5 -3
  90. package/src/deploy/vercel-negotiation.ts +97 -6
  91. package/src/og/card.ts +1 -1
  92. package/src/openapi/references.ts +8 -0
  93. package/src/openapi/render-mdx.ts +18 -4
  94. package/src/openapi/scalar.ts +0 -4
  95. package/src/registry/eject.ts +36 -17
  96. package/src/theme/entry.ts +2 -2
  97. package/src/theme/sources.ts +49 -0
@@ -690,6 +690,15 @@ export interface McpConfig {
690
690
  * hosted MCP server.
691
691
  */
692
692
  export interface AiConfig {
693
+ /**
694
+ * The JSON docs API — the REST twin of the MCP tools. Serves the page index
695
+ * (`/api/docs/pages.json`), each page as JSON (`/api/docs/pages/{route}.json`),
696
+ * and the navigation tree (`/api/docs/navigation.json`) as prerendered
697
+ * files, plus full-text search (`/api/docs/search?q=`) on server output,
698
+ * all described by an OpenAPI document at `/openapi.json`. Errors are RFC
699
+ * 9457 problem details. Defaults to `true`; set `false` to publish none of it.
700
+ */
701
+ api?: boolean;
693
702
  /** The Ask AI chat assistant. */
694
703
  ask?: AskConfig;
695
704
  /**
@@ -82,6 +82,11 @@ export interface BlumeRoute {
82
82
  alternates: RouteAlternate[];
83
83
  /** Astro collection the entry renders through (`"docs"` | `"staged"`). */
84
84
  collection: string;
85
+ /**
86
+ * Meta description as the page head renders it (`seo.description` over the
87
+ * front matter `description`), or `null` when the page declares neither.
88
+ */
89
+ description: string | null;
85
90
  draft: boolean;
86
91
  /** "Edit this page" URL, or `null` when no repo/source provides one. */
87
92
  editUrl: string | null;
@@ -138,6 +143,8 @@ export interface BlumeDataConfig {
138
143
  */
139
144
  discovery: {
140
145
  agentReadability: boolean;
146
+ /** Whether the JSON docs API and its `/openapi.json` are published. */
147
+ api: boolean;
141
148
  llmsTxt: boolean;
142
149
  sitemap: boolean;
143
150
  };
@@ -176,7 +183,11 @@ export interface BlumeDataConfig {
176
183
  * kept out of this snapshot, which pages serialize into HTML.
177
184
  */
178
185
  og: {
179
- /** Card subtitle: `seo.og.description` (`false` omits it) over the site description. */
186
+ /**
187
+ * Site-wide card subtitle: `seo.og.description` (`false` omits it) over
188
+ * the site description. A page with its own description shows that
189
+ * instead; this is the fallback for pages without one.
190
+ */
180
191
  description?: string;
181
192
  enabled: boolean;
182
193
  /** Inlined SVG brand mark; `false` renders the card without any mark. */
@@ -17,6 +17,7 @@ declare const uiStringsObject: z.ZodObject<{
17
17
  copyClaudeCode: z.ZodDefault<z.ZodString>;
18
18
  copyCode: z.ZodDefault<z.ZodString>;
19
19
  copyCodex: z.ZodDefault<z.ZodString>;
20
+ copyFailed: z.ZodDefault<z.ZodString>;
20
21
  copyMarkdown: z.ZodDefault<z.ZodString>;
21
22
  copyServerUrl: z.ZodDefault<z.ZodString>;
22
23
  edit: z.ZodDefault<z.ZodString>;
@@ -78,6 +79,7 @@ declare const uiStringsObject: z.ZodObject<{
78
79
  toggleTheme: z.ZodDefault<z.ZodString>;
79
80
  }, z.core.$strip>>;
80
81
  notFound: z.ZodPrefault<z.ZodObject<{
82
+ api: z.ZodDefault<z.ZodString>;
81
83
  description: z.ZodDefault<z.ZodString>;
82
84
  home: z.ZodDefault<z.ZodString>;
83
85
  llms: z.ZodDefault<z.ZodString>;
@@ -129,6 +131,7 @@ export declare const uiStringsSchema: z.ZodPrefault<z.ZodObject<{
129
131
  copyClaudeCode: z.ZodDefault<z.ZodString>;
130
132
  copyCode: z.ZodDefault<z.ZodString>;
131
133
  copyCodex: z.ZodDefault<z.ZodString>;
134
+ copyFailed: z.ZodDefault<z.ZodString>;
132
135
  copyMarkdown: z.ZodDefault<z.ZodString>;
133
136
  copyServerUrl: z.ZodDefault<z.ZodString>;
134
137
  edit: z.ZodDefault<z.ZodString>;
@@ -190,6 +193,7 @@ export declare const uiStringsSchema: z.ZodPrefault<z.ZodObject<{
190
193
  toggleTheme: z.ZodDefault<z.ZodString>;
191
194
  }, z.core.$strip>>;
192
195
  notFound: z.ZodPrefault<z.ZodObject<{
196
+ api: z.ZodDefault<z.ZodString>;
193
197
  description: z.ZodDefault<z.ZodString>;
194
198
  home: z.ZodDefault<z.ZodString>;
195
199
  llms: z.ZodDefault<z.ZodString>;
@@ -260,6 +260,7 @@ export declare const searchProviders: readonly ["orama", "pagefind", "flexsearch
260
260
  /** Ask AI backends. `gateway` (default) routes through the Vercel AI Gateway. */
261
261
  export declare const askAiProviders: readonly ["gateway", "openrouter", "llmgateway", "inkeep", "openai-compatible"];
262
262
  declare const aiConfigSchema: z.ZodObject<{
263
+ api: z.ZodDefault<z.ZodBoolean>;
263
264
  ask: z.ZodOptional<z.ZodObject<{
264
265
  apiKeyEnv: z.ZodOptional<z.ZodString>;
265
266
  baseUrl: z.ZodOptional<z.ZodURL>;
@@ -477,6 +478,7 @@ declare const openapiSourceSchema: z.ZodObject<{
477
478
  label: z.ZodOptional<z.ZodString>;
478
479
  noindex: z.ZodDefault<z.ZodBoolean>;
479
480
  route: z.ZodOptional<z.ZodString>;
481
+ seoDescriptionSuffix: z.ZodDefault<z.ZodBoolean>;
480
482
  spec: z.ZodString;
481
483
  }, z.core.$strict>;
482
484
  export type OpenApiSource = z.input<typeof openapiSourceSchema>;
@@ -492,12 +494,14 @@ declare const graphqlSourceSchema: z.ZodObject<{
492
494
  label: z.ZodOptional<z.ZodString>;
493
495
  noindex: z.ZodDefault<z.ZodBoolean>;
494
496
  route: z.ZodOptional<z.ZodString>;
497
+ seoDescriptionSuffix: z.ZodDefault<z.ZodBoolean>;
495
498
  spec: z.ZodString;
496
499
  endpoint: z.ZodOptional<z.ZodString>;
497
500
  }, z.core.$strict>;
498
501
  export type GraphqlSource = z.input<typeof graphqlSourceSchema>;
499
502
  export declare const blumeConfigSchema: z.ZodObject<{
500
503
  ai: z.ZodPrefault<z.ZodObject<{
504
+ api: z.ZodDefault<z.ZodBoolean>;
501
505
  ask: z.ZodOptional<z.ZodObject<{
502
506
  apiKeyEnv: z.ZodOptional<z.ZodString>;
503
507
  baseUrl: z.ZodOptional<z.ZodURL>;
@@ -598,6 +602,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
598
602
  label: z.ZodOptional<z.ZodString>;
599
603
  noindex: z.ZodDefault<z.ZodBoolean>;
600
604
  route: z.ZodOptional<z.ZodString>;
605
+ seoDescriptionSuffix: z.ZodDefault<z.ZodBoolean>;
601
606
  spec: z.ZodString;
602
607
  }, z.core.$strict>>>;
603
608
  spec: z.ZodOptional<z.ZodString>;
@@ -795,6 +800,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
795
800
  label: z.ZodOptional<z.ZodString>;
796
801
  noindex: z.ZodDefault<z.ZodBoolean>;
797
802
  route: z.ZodOptional<z.ZodString>;
803
+ seoDescriptionSuffix: z.ZodDefault<z.ZodBoolean>;
798
804
  spec: z.ZodString;
799
805
  endpoint: z.ZodOptional<z.ZodString>;
800
806
  }, z.core.$strict>>>;
@@ -942,6 +948,7 @@ export declare const blumeConfigSchema: z.ZodObject<{
942
948
  label: z.ZodOptional<z.ZodString>;
943
949
  noindex: z.ZodDefault<z.ZodBoolean>;
944
950
  route: z.ZodOptional<z.ZodString>;
951
+ seoDescriptionSuffix: z.ZodDefault<z.ZodBoolean>;
945
952
  spec: z.ZodString;
946
953
  }, z.core.$strict>>>;
947
954
  spec: z.ZodOptional<z.ZodString>;
@@ -347,6 +347,12 @@ export interface RouteManifestEntry {
347
347
  /** Adapter-supplied "edit this page" URL (non-filesystem sources). */
348
348
  editUrl?: string;
349
349
  title: string;
350
+ /**
351
+ * The page's meta description as the head renders it: `seo.description`
352
+ * over the front matter `description`. Feeds the generated OG card's
353
+ * subtitle, so a shared link's image and its `og:description` agree.
354
+ */
355
+ description?: string;
350
356
  contentType: string;
351
357
  hidden: boolean;
352
358
  draft: boolean;
@@ -47,6 +47,11 @@ export interface ReferenceSource {
47
47
  includeInSearch: boolean;
48
48
  /** Whether generated pages emit noindex metadata and stay out of the sitemap. */
49
49
  noindex: boolean;
50
+ /**
51
+ * Whether operation meta descriptions end with the generated English
52
+ * "Reference for …" sentence, or carry the spec's own prose alone.
53
+ */
54
+ seoDescriptionSuffix: boolean;
50
55
  /** Local path or `http(s)` URL, verbatim from config. */
51
56
  spec: string;
52
57
  /**
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
  ```
@@ -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
@@ -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
 
@@ -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` and a JSON twin at `/404.json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details) with the same recovery links (absolute URLs once [`deployment.site`](/docs/deployment) is set), plus the [`openapi.json`](/docs/configuration/ai#json-api) description when the JSON API is on. 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 the Markdown body with the `404` status instead of the HTML shell; one that sends `Accept: application/json`, or asks for a `.json` URL no file backs, gets the problem document — 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
 
@@ -66,7 +66,7 @@ graphql: {
66
66
  }
67
67
  ```
68
68
 
69
- Each source takes the same per-source controls as the [OpenAPI block](/docs/advanced/api-reference): `includeInSearch`, `includeInLlms`, and `noindex`.
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
70
 
71
71
  ## Try it playground
72
72
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  title: AI
3
- description: Make your docs machine-readable with llms.txt, add an optional in-page Ask AI assistant, and expose a hosted MCP server for coding agents.
3
+ description: Make your docs machine-readable with llms.txt and a JSON API described by OpenAPI, add an optional in-page Ask AI assistant, and expose a hosted MCP server for coding agents.
4
4
  ---
5
5
 
6
- Blume has a few AI features: machine-readable docs for external tools (`llms.txt`, on by default), an in-page **Ask AI** assistant, and a hosted **MCP server** for coding agents. Ask AI and MCP are opt-in, and static docs stay fully static until you turn a feature on.
6
+ Blume has a few AI features: machine-readable docs for external tools (`llms.txt` and a [JSON API](#json-api) with an OpenAPI description, both on by default), an in-page **Ask AI** assistant, and a hosted **MCP server** for coding agents. Ask AI and MCP are opt-in, and static docs stay fully static until you turn a feature on.
7
7
 
8
8
  ## llms.txt
9
9
 
@@ -81,6 +81,8 @@ The `.md` variant _downlevels_ components to plain Markdown for consumers that c
81
81
 
82
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.
83
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
+
84
86
  ### Custom component serializers
85
87
 
86
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:
@@ -109,6 +111,8 @@ Serializers live in `blume.config.ts`, not `components.tsx`: the config file is
109
111
 
110
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.
111
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
+
112
116
  ## Open in chat
113
117
 
114
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:
@@ -360,6 +364,52 @@ A static build with `ai.mcp.enabled` fails fast with a message telling you to se
360
364
  claude mcp add --transport http my-docs https://docs.example.com/mcp
361
365
  ```
362
366
 
367
+ ## JSON API
368
+
369
+ Every Blume site also serves its docs as a small read-only **JSON API** — the REST twin of the [MCP server](#mcp-server)'s tools, over the same page snapshot, for agents and function-calling frameworks that speak plain HTTP rather than MCP. It's on by default and needs no configuration:
370
+
371
+ | Endpoint | Returns |
372
+ | --- | --- |
373
+ | `/api/docs/pages.json` | Every page with its route, title, description, content type, locale, facets, and the URLs of its rendered, Markdown, and JSON forms. |
374
+ | `/api/docs/pages/{route}.json` | One page: its index entry plus the agent Markdown (the same body `get_page` returns). `{route}` is the page route without the leading slash, `index` for home. |
375
+ | `/api/docs/navigation.json` | The navigation tree — header tabs and the sidebar hierarchy. |
376
+ | `/api/docs/search?q=` | Full-text search, with the same `limit`, `contentTypes`, `locale`, `version`, and `filters[key]` scoping as `search_docs`. Server output only. |
377
+ | `/openapi.json` | The OpenAPI 3.1 description of the whole machine-readable surface. |
378
+
379
+ The page index, per-page documents, and navigation are prerendered, so a static site serves them as files from any host. Search is a live endpoint and exists only under [server output](/docs/deployment#server-rendering), where it runs the same index `search_docs` does. Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details (`application/problem+json`) with a stable `code`, a `detail`, and a `resolution` hint telling the agent where to go next — a missing page, a blank search query, or on server output any `/api/…` URL no endpoint answers:
380
+
381
+ ```json
382
+ {
383
+ "code": "API_ROUTE_NOT_FOUND",
384
+ "detail": "No API route exists at /api/nope.",
385
+ "instance": "/api/nope",
386
+ "links": [
387
+ {
388
+ "href": "https://docs.example.com/openapi.json",
389
+ "label": "OpenAPI description"
390
+ },
391
+ {
392
+ "href": "https://docs.example.com/api/docs/pages.json",
393
+ "label": "Page index"
394
+ }
395
+ ],
396
+ "resolution": "Discover the available operations through the OpenAPI description at https://docs.example.com/openapi.json, or list every page at https://docs.example.com/api/docs/pages.json.",
397
+ "status": 404,
398
+ "title": "API route not found",
399
+ "type": "about:blank"
400
+ }
401
+ ```
402
+
403
+ The **OpenAPI document** at `/openapi.json` is generated per build from your config, so it describes only what the deployed site serves: every JSON endpoint with a unique `operationId`, typed parameters, and response schemas, plus the text surfaces alongside — the [`.md` mirrors](#raw-markdown), [`llms.txt`](#llmstxt) and `llms-full.txt`, [`agent-readability.json`](#agent-readability) — and the [MCP endpoint](#mcp-server) when it's enabled. Frameworks that build tools from an OpenAPI description get the same reach an MCP client has. The document is linked from the [API catalog](#api-catalog), the [readability manifest](#agent-readability), the homepage `Link` header as `rel="service-desc"`, and `llms.txt`.
404
+
405
+ None of this touches your own [API reference](/docs/advanced/api-reference): a documented spec is rendered into pages, never served at `/openapi.json`, and the catalog lists both. A `public/openapi.json` you ship yourself takes over that route (the JSON endpoints stay). The `/api/…` catch-all steps aside when a docs section is served from the `/api` namespace (`content/api/overview.md`) or a custom page owns a rest route under `/api/`, so those pages keep winning. Set `ai.api` to `false` to publish none of it:
406
+
407
+ ```ts blume.config.ts lineNumbers
408
+ ai: {
409
+ api: false,
410
+ }
411
+ ```
412
+
363
413
  ## Agent readability
364
414
 
365
415
  Blume writes an **`/agent-readability.json`** manifest at your site root that indexes the agent-facing surface described on this page — so an agent can discover it in a single fetch instead of guessing at conventions or scraping HTML. Like `llms.txt`, it's on by default:
@@ -370,7 +420,7 @@ seo: {
370
420
  }
371
421
  ```
372
422
 
373
- The manifest lists only what you've enabled — the [raw Markdown](#raw-markdown) mirror pattern, [`llms.txt`](#llmstxt) and `llms-full.txt`, the [MCP server](#mcp-server) and its discovery document, the [Ask AI](#ask-ai) endpoint, the [sitemap](/docs/configuration/seo#sitemap), and [RSS feeds](/docs/configuration/seo#rss-feeds) — alongside your site name, description, source repository, and the [content-signal](/docs/configuration/seo#content-signals) usage policy. URLs are absolute when [`deployment.site`](/docs/deployment) is set and root-relative otherwise:
423
+ The manifest lists only what you've enabled — the [raw Markdown](#raw-markdown) mirror pattern, the [JSON API](#json-api) and its OpenAPI description, [`llms.txt`](#llmstxt) and `llms-full.txt`, the [MCP server](#mcp-server) and its discovery document, the [Ask AI](#ask-ai) endpoint, the [sitemap](/docs/configuration/seo#sitemap), and [RSS feeds](/docs/configuration/seo#rss-feeds) — alongside your site name, description, source repository, and the [content-signal](/docs/configuration/seo#content-signals) usage policy. URLs are absolute when [`deployment.site`](/docs/deployment) is set and root-relative otherwise:
374
424
 
375
425
  ```json agent-readability.json
376
426
  {
@@ -379,6 +429,11 @@ The manifest lists only what you've enabled — the [raw Markdown](#raw-markdown
379
429
  "contentNegotiation": "text/markdown",
380
430
  "pattern": "https://docs.example.com/{route}.md"
381
431
  },
432
+ "api": {
433
+ "openapi": "https://docs.example.com/openapi.json",
434
+ "pages": "https://docs.example.com/api/docs/pages.json",
435
+ "search": "https://docs.example.com/api/docs/search"
436
+ },
382
437
  "llmsFullTxt": "https://docs.example.com/llms-full.txt",
383
438
  "llmsTxt": "https://docs.example.com/llms.txt",
384
439
  "mcp": {
@@ -404,12 +459,14 @@ Set `seo.agentReadability` to `false` to skip it, or ship your own `public/agent
404
459
  Agents that probe a site don't know to look for the manifest — so Blume also advertises it in an [RFC 8288](https://www.rfc-editor.org/rfc/rfc8288) `Link` response header on the homepage, using IANA-registered relation types:
405
460
 
406
461
  ```http
407
- Link: </agent-readability.json>; rel="describedby"; type="application/json",
462
+ Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json",
463
+ </openapi.json>; rel="service-desc"; type="application/json",
464
+ </agent-readability.json>; rel="describedby"; type="application/json",
408
465
  </llms.txt>; rel="describedby"; type="text/plain",
409
466
  </index.md>; rel="alternate"; type="text/markdown"
410
467
  ```
411
468
 
412
- Each entry appears only when its feature is on. The `alternate` link points at the homepage's Markdown mirror — the page's own [raw Markdown](#raw-markdown) when the home route is a content page, or the synthesized `llms.txt` fallback when it's a landing page. Sites that publish APIs also get a `rel="api-catalog"` entry pointing at the [generated API catalog](#api-catalog). The header rides on every surface Blume controls: the dev server (check it with `curl -I localhost:4321`), static builds via the emitted `_headers` file (Netlify and Cloudflare), and Vercel server builds via the deploy's routing rules.
469
+ Each entry appears only when its feature is on. The `alternate` link points at the homepage's Markdown mirror — the page's own [raw Markdown](#raw-markdown) when the home route is a content page, or the synthesized `llms.txt` fallback when it's a landing page. The `service-desc` link ([RFC 8631](https://www.rfc-editor.org/rfc/rfc8631)) points at the [JSON API](#json-api)'s OpenAPI description, and `api-catalog` at the [generated API catalog](#api-catalog). The header rides on every surface Blume controls: the dev server (check it with `curl -I localhost:4321`), static builds via the emitted `_headers` file (Netlify and Cloudflare), and Vercel server builds via the deploy's routing rules.
413
470
 
414
471
  Not every agent enters through the root, though — one following a search result or a shared link lands on a deep page and never sees the homepage header. So every rendered page also carries the same discovery links in its HTML `<head>`, using the same IANA-registered relations:
415
472
 
@@ -427,7 +484,7 @@ Here the `alternate` link points at _that page's own_ [raw-Markdown mirror](#raw
427
484
 
428
485
  ### API catalog
429
486
 
430
- When the site publishes APIs, Blume generates an [RFC 9727](https://www.rfc-editor.org/rfc/rfc9727) API catalog at `/.well-known/api-catalog` — a [linkset](https://www.rfc-editor.org/rfc/rfc9264) that lets agents enumerate your APIs from the domain alone, served with its registered `application/linkset+json` media type on every build surface. There's nothing to configure: the catalog is derived from what's already in `blume.config.ts`. Each [OpenAPI or AsyncAPI reference](/docs/advanced/api-reference) becomes an entry anchored at its rendered docs route, with `service-doc` pointing at those docs and `service-desc` at the spec when it lives at a fetchable URL; the [MCP server](#mcp-server) becomes an entry with its discovery document as the service description:
487
+ When the site publishes APIs, Blume generates an [RFC 9727](https://www.rfc-editor.org/rfc/rfc9727) API catalog at `/.well-known/api-catalog` — a [linkset](https://www.rfc-editor.org/rfc/rfc9264) that lets agents enumerate your APIs from the domain alone, served with its registered `application/linkset+json` media type on every build surface. There's nothing to configure: the catalog is derived from what's already in `blume.config.ts`. Each [OpenAPI or AsyncAPI reference](/docs/advanced/api-reference) becomes an entry anchored at its rendered docs route, with `service-doc` pointing at those docs and `service-desc` at the spec when it lives at a fetchable URL; the site's own [JSON API](#json-api) becomes an entry described by its `/openapi.json`; and the [MCP server](#mcp-server) becomes an entry with its discovery document as the service description:
431
488
 
432
489
  ```json .well-known/api-catalog
433
490
  {
@@ -439,6 +496,18 @@ When the site publishes APIs, Blume generates an [RFC 9727](https://www.rfc-edit
439
496
  ],
440
497
  "service-desc": [{ "href": "https://api.example.com/openapi.json" }]
441
498
  },
499
+ {
500
+ "anchor": "https://docs.example.com/api/docs",
501
+ "service-desc": [
502
+ {
503
+ "href": "https://docs.example.com/openapi.json",
504
+ "type": "application/json"
505
+ }
506
+ ],
507
+ "service-doc": [
508
+ { "href": "https://docs.example.com/", "type": "text/html" }
509
+ ]
510
+ },
442
511
  {
443
512
  "anchor": "https://docs.example.com/mcp",
444
513
  "service-desc": [
@@ -455,7 +524,7 @@ When the site publishes APIs, Blume generates an [RFC 9727](https://www.rfc-edit
455
524
  }
456
525
  ```
457
526
 
458
- A site with no API references and no MCP server emits no catalog — there'd be nothing in it. As everywhere, a `public/.well-known/api-catalog` file you ship yourself wins over the generated one.
527
+ A site with no API references, no MCP server, and the [JSON API](#json-api) turned off emits no catalog — there'd be nothing in it. As everywhere, a `public/.well-known/api-catalog` file you ship yourself wins over the generated one.
459
528
 
460
529
  ### WebMCP
461
530
 
@@ -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
  }
@@ -206,6 +206,12 @@ Drop a `theme.css` in your project root to override any design token. It's the l
206
206
 
207
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.
208
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
+
209
215
  ### Design tokens
210
216
 
211
217
  | Token | Controls |
@@ -567,7 +567,7 @@ The card reads the instance from [`github.host`](/docs/configuration#github-ente
567
567
 
568
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.
569
569
 
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 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.
571
571
 
572
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:
573
573
 
@@ -623,6 +623,13 @@ export default defineConfig({
623
623
  <Component path="file-list/examples/basic" />
624
624
  ```
625
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
+
626
633
  <Component path="counter" />
627
634
 
628
635
  ```astro
package/docs/index.mdx CHANGED
@@ -31,7 +31,7 @@ Blume builds on Astro and Vite and renders static HTML by default — fast, cach
31
31
 
32
32
  ### AI-ready out of the box
33
33
 
34
- Every Blume site speaks fluent machine. It emits [`llms.txt` and `llms-full.txt`](/docs/configuration/ai), serves any page's raw Markdown by appending `.md` to its URL, and gives readers **Copy as Markdown** and **Open in chat** actions on every page. Add an optional in-page **Ask AI** assistant, or host an [**MCP server**](/docs/configuration/ai#mcp-server) so coding agents like Claude Code and Cursor can search and read your docs directly — no scraping, no hosted service. Your Markdown is the source of truth for both humans and models.
34
+ Every Blume site speaks fluent machine. It emits [`llms.txt` and `llms-full.txt`](/docs/configuration/ai), serves any page's raw Markdown by appending `.md` to its URL, exposes a [JSON API described by OpenAPI](/docs/configuration/ai#json-api), and gives readers **Copy as Markdown** and **Open in chat** actions on every page. Add an optional in-page **Ask AI** assistant, or host an [**MCP server**](/docs/configuration/ai#mcp-server) so coding agents like Claude Code and Cursor can search and read your docs directly — no scraping, no hosted service. Your Markdown is the source of truth for both humans and models.
35
35
 
36
36
  ### Zero configuration — even the template
37
37
 
@@ -45,7 +45,7 @@ Your [`blume.config.ts`](/docs/configuration) and every [`meta.ts`](/docs/conten
45
45
 
46
46
  - **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with [no imports](/docs/content/components).
47
47
  - **Local search** — Orama works in dev and production; Pagefind is one flag away for large sites. No hosted index.
48
- - **AI** — [`llms.txt`, raw Markdown URLs, Copy as Markdown, Open in chat, an Ask AI assistant, and a hosted MCP server](/docs/configuration/ai).
48
+ - **AI** — [`llms.txt`, raw Markdown URLs, a JSON API with an OpenAPI description, Copy as Markdown, Open in chat, an Ask AI assistant, and a hosted MCP server](/docs/configuration/ai).
49
49
  - **Navigation** — inferred from files, refined with `meta.ts` or config.
50
50
  - **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD, [built in](/docs/configuration/seo).
51
51
  - **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.