blume 1.6.2 → 1.6.4

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 (79) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli/index.js +307 -68
  3. package/dist/cli/index.js.map +28 -23
  4. package/dist/types/ai/component-markdown.d.ts +14 -0
  5. package/docs/01-quickstart.mdx +2 -2
  6. package/docs/02-deployment.mdx +5 -5
  7. package/docs/{07-faq.mdx → 08-faq.mdx} +7 -7
  8. package/docs/advanced/blog.mdx +3 -3
  9. package/docs/advanced/changelog.mdx +2 -2
  10. package/docs/advanced/custom-pages.mdx +4 -4
  11. package/docs/advanced/meta.ts +1 -1
  12. package/docs/configuration/ask-ai.mdx +179 -0
  13. package/docs/configuration/index.mdx +8 -7
  14. package/docs/configuration/meta.ts +1 -2
  15. package/docs/configuration/search.mdx +1 -1
  16. package/docs/configuration/theming.mdx +1 -1
  17. package/docs/content/components.mdx +1 -1
  18. package/docs/content/i18n.mdx +7 -1
  19. package/docs/content/index.mdx +1 -1
  20. package/docs/content/navigation.mdx +2 -2
  21. package/docs/content/syntax.mdx +1 -1
  22. package/docs/discoverability/agent-discovery.mdx +196 -0
  23. package/docs/discoverability/index.mdx +48 -0
  24. package/docs/discoverability/json-api.mdx +58 -0
  25. package/docs/discoverability/llms-txt.mdx +68 -0
  26. package/docs/discoverability/markdown.mdx +76 -0
  27. package/docs/discoverability/mcp.mdx +64 -0
  28. package/docs/discoverability/meta.ts +18 -0
  29. package/docs/discoverability/metadata.mdx +82 -0
  30. package/docs/discoverability/open-graph.mdx +113 -0
  31. package/docs/discoverability/rss.mdx +24 -0
  32. package/docs/discoverability/sitemap-and-robots.mdx +95 -0
  33. package/docs/discoverability/structured-data.mdx +51 -0
  34. package/docs/index.mdx +5 -5
  35. package/docs/reference/eval.mdx +1 -1
  36. package/docs/reference/meta.ts +1 -1
  37. package/docs/reference/translate.mdx +1 -0
  38. package/package.json +18 -18
  39. package/src/ai/component-markdown.ts +17 -2
  40. package/src/ai/llms.ts +3 -10
  41. package/src/ai/markdown.ts +3 -10
  42. package/src/ai/openapi-components.ts +123 -0
  43. package/src/ai/serializers.ts +24 -0
  44. package/src/astro/templates.ts +42 -12
  45. package/src/audit/checks/links.ts +1 -8
  46. package/src/audit/checks/llms.ts +5 -4
  47. package/src/audit/redirects.ts +4 -3
  48. package/src/audit/run.ts +6 -8
  49. package/src/audit/url.ts +33 -0
  50. package/src/cli/commands/validate.ts +1 -0
  51. package/src/components/content/Component.astro +65 -68
  52. package/src/components/content/Tabs.astro +24 -9
  53. package/src/components/content/example-pane.ts +6 -0
  54. package/src/components/layout/LocaleLinks.astro +42 -0
  55. package/src/components/layout/PageLayout.astro +5 -3
  56. package/src/components/layout/ReferenceLayout.astro +5 -0
  57. package/src/components/layout/RootLayout.astro +110 -39
  58. package/src/components/layout/search-locale.ts +13 -0
  59. package/src/components/openapi/ApiOverview.astro +7 -39
  60. package/src/components/openapi/ApiTagOperations.astro +2 -1
  61. package/src/components/openapi/AsyncApiOperation.astro +3 -2
  62. package/src/components/openapi/GraphqlOperation.astro +3 -2
  63. package/src/components/openapi/Operation.astro +3 -2
  64. package/src/core/i18n.ts +13 -2
  65. package/src/core/links.ts +33 -1
  66. package/src/core/locale-links.ts +163 -0
  67. package/src/core/sources/normalize.ts +57 -7
  68. package/src/markdown/package-commands.ts +27 -3
  69. package/src/openapi/graphql.ts +29 -0
  70. package/src/openapi/model.ts +69 -0
  71. package/src/openapi/render-mdx.ts +3 -2
  72. package/src/openapi/signature.ts +18 -0
  73. package/src/search/documents.ts +4 -9
  74. package/src/theme/code-block-padding.ts +0 -8
  75. package/src/theme/entry.ts +33 -27
  76. package/src/translate/anchors.ts +91 -0
  77. package/src/translate/validate.ts +8 -3
  78. package/docs/configuration/ai.mdx +0 -613
  79. package/docs/configuration/seo.mdx +0 -364
@@ -0,0 +1,196 @@
1
+ ---
2
+ title: Agent discovery
3
+ description: How agents find your machine-readable surface without guessing — the agent-readability manifest, Link headers, the RFC 9727 API catalog, WebMCP, published skills, DNS-based discovery, and Web Bot Auth keys.
4
+ ---
5
+
6
+ Publishing `llms.txt`, Markdown mirrors, a JSON API, and an MCP server is only half the job — an agent still has to find them. Blume advertises the whole surface through the conventions agents actually probe: a manifest at the site root, `Link` headers and `<link>` tags, well-known files, and the browser's own model context. Everything here is on by default and derived from what you've already enabled.
7
+
8
+ ## Agent readability
9
+
10
+ Blume writes an **`/agent-readability.json`** manifest at your site root that indexes the agent-facing surface described across this section — 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:
11
+
12
+ ```ts blume.config.ts lineNumbers
13
+ seo: {
14
+ agentReadability: true,
15
+ }
16
+ ```
17
+
18
+ The manifest lists only what you've enabled — the [raw Markdown](/docs/discoverability/markdown) mirror pattern, the [JSON API](/docs/discoverability/json-api) and its OpenAPI description, [`llms.txt`](/docs/discoverability/llms-txt) and `llms-full.txt`, the [MCP server](/docs/discoverability/mcp) and its discovery document, the [Ask AI](/docs/configuration/ask-ai) endpoint, the [sitemap](/docs/discoverability/sitemap-and-robots#sitemap), and [RSS feeds](/docs/discoverability/rss) — alongside your site name, description, source repository, and the [content-signal](/docs/discoverability/sitemap-and-robots#content-signals) usage policy. URLs are absolute when [`deployment.site`](/docs/deployment) is set and root-relative otherwise:
19
+
20
+ ```json agent-readability.json
21
+ {
22
+ "artifacts": {
23
+ "markdown": {
24
+ "contentNegotiation": "text/markdown",
25
+ "pattern": "https://docs.example.com/{route}.md"
26
+ },
27
+ "api": {
28
+ "openapi": "https://docs.example.com/openapi.json",
29
+ "pages": "https://docs.example.com/api/docs/pages.json",
30
+ "search": "https://docs.example.com/api/docs/search"
31
+ },
32
+ "llmsFullTxt": "https://docs.example.com/llms-full.txt",
33
+ "llmsTxt": "https://docs.example.com/llms.txt",
34
+ "mcp": {
35
+ "discovery": "https://docs.example.com/.well-known/mcp.json",
36
+ "url": "https://docs.example.com/mcp"
37
+ }
38
+ },
39
+ "description": "Docs for the Acme API.",
40
+ "generator": "blume@1.0.0",
41
+ "name": "Acme Docs",
42
+ "site": "https://docs.example.com",
43
+ "contentUsage": { "search": true, "ai-input": true, "ai-train": true },
44
+ "repository": "https://github.com/acme/docs"
45
+ }
46
+ ```
47
+
48
+ The `contentNegotiation` field appears only when the deployed site actually honors the `Accept: text/markdown` header — see [content negotiation](/docs/discoverability/markdown#content-negotiation); on every other deployment the manifest advertises just the `.md` mirror pattern.
49
+
50
+ Set `seo.agentReadability` to `false` to skip it, or ship your own `public/agent-readability.json` to take over — Blume never overwrites a file you place in `public/`.
51
+
52
+ ## Discovery Link header
53
+
54
+ 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:
55
+
56
+ ```http
57
+ Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json",
58
+ </openapi.json>; rel="service-desc"; type="application/json",
59
+ </agent-readability.json>; rel="describedby"; type="application/json",
60
+ </llms.txt>; rel="describedby"; type="text/plain",
61
+ </index.md>; rel="alternate"; type="text/markdown"
62
+ ```
63
+
64
+ 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](/docs/discoverability/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](/docs/discoverability/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.
65
+
66
+ 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:
67
+
68
+ ```html
69
+ <link
70
+ rel="describedby"
71
+ href="/agent-readability.json"
72
+ type="application/json"
73
+ />
74
+ <link rel="describedby" href="/llms.txt" type="text/plain" />
75
+ <link rel="alternate" href="/docs/example.md" type="text/markdown" />
76
+ ```
77
+
78
+ Here the `alternate` link points at _that page's own_ [raw-Markdown mirror](/docs/discoverability/markdown), so an agent can jump straight from the HTML it landed on to the token-efficient version. Because the head links travel with the prerendered HTML, they also work on hosts that ignore `_headers` and can't send custom response headers at all (GitHub Pages, S3) — no matter which page the agent enters on.
79
+
80
+ ## API catalog
81
+
82
+ 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](/docs/discoverability/json-api) becomes an entry described by its `/openapi.json`; and the [MCP server](/docs/discoverability/mcp) becomes an entry with its discovery document as the service description:
83
+
84
+ ```json .well-known/api-catalog
85
+ {
86
+ "linkset": [
87
+ {
88
+ "anchor": "https://docs.example.com/reference",
89
+ "service-doc": [
90
+ { "href": "https://docs.example.com/reference", "type": "text/html" }
91
+ ],
92
+ "service-desc": [{ "href": "https://api.example.com/openapi.json" }]
93
+ },
94
+ {
95
+ "anchor": "https://docs.example.com/api/docs",
96
+ "service-desc": [
97
+ {
98
+ "href": "https://docs.example.com/openapi.json",
99
+ "type": "application/json"
100
+ }
101
+ ],
102
+ "service-doc": [
103
+ { "href": "https://docs.example.com/", "type": "text/html" }
104
+ ]
105
+ },
106
+ {
107
+ "anchor": "https://docs.example.com/mcp",
108
+ "service-desc": [
109
+ {
110
+ "href": "https://docs.example.com/.well-known/mcp.json",
111
+ "type": "application/json"
112
+ }
113
+ ],
114
+ "service-doc": [
115
+ { "href": "https://docs.example.com/", "type": "text/html" }
116
+ ]
117
+ }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ A site with no API references, no MCP server, and the [JSON API](/docs/discoverability/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.
123
+
124
+ ## WebMCP
125
+
126
+ [WebMCP](https://webmachinelearning.github.io/webmcp/) is an emerging browser API that lets a page register tools directly with an agentic browser — no separate server connection needed. Every Blume page registers the docs' read-only surface on the page's model context: `search_docs` (site search), `get_page` (a page's [raw Markdown](/docs/discoverability/markdown)), and `list_pages` (the [`llms.txt`](/docs/discoverability/llms-txt) index). The script is tiny, loads no search machinery until a tool is actually called, and silently no-ops in every browser without the API — which today is all of them outside [Chrome's early preview](https://developer.chrome.com/blog/webmcp-epp). It registers on whichever surface the in-flux spec exposes (`navigator.modelContext` or `document.modelContext`), via `provideContext` or per-tool `registerTool`.
127
+
128
+ It's on by default; set `webmcp: false` to opt out:
129
+
130
+ ```ts blume.config.ts lineNumbers
131
+ ai: {
132
+ webmcp: false,
133
+ }
134
+ ```
135
+
136
+ ## Skills discovery
137
+
138
+ If your project ships [agent skills](https://agentskills.io) — the [Blume repo itself does](/docs/advanced/skills) — point `ai.skills` at the directory that holds them, and the build publishes them for discovery per the [Agent Skills Discovery RFC](https://github.com/cloudflare/agent-skills-discovery-rfc):
139
+
140
+ ```ts blume.config.ts lineNumbers
141
+ ai: {
142
+ skills: "./skills",
143
+ }
144
+ ```
145
+
146
+ The path resolves against your project root, and each subdirectory with a `SKILL.md` becomes a published skill. A skill that's a lone `SKILL.md` is copied verbatim to `/.well-known/agent-skills/<name>/SKILL.md` (`type: "skill-md"`); a skill with supporting resources (`scripts/`, `references/`, `assets/`) is bundled into a deterministic `.tar.gz` (`type: "archive"`) so its relative references resolve after unpacking, with script execute bits preserved. The discovery index at `/.well-known/agent-skills/index.json` carries the v0.2.0 `$schema` and, per skill, its name, type, description (from the `SKILL.md` frontmatter), artifact URL, and the SHA-256 digest clients verify downloads against.
147
+
148
+ Skills with a missing or spec-invalid `name`/`description` are skipped with a build warning rather than published broken, and a `public/.well-known/agent-skills/index.json` you ship yourself takes over the whole surface. Published skills are also listed in [`llms.txt`](/docs/discoverability/llms-txt#generated-sections).
149
+
150
+ ## DNS-based discovery (DNS-AID)
151
+
152
+ [DNS for AI Discovery](https://datatracker.ietf.org/doc/draft-mozleywilliams-dnsop-dnsaid/) is an emerging IETF draft that lets agents discover a site's AI surface before making a single HTTP request, by querying ServiceMode [SVCB/HTTPS records](https://www.rfc-editor.org/rfc/rfc9460) at a well-known DNS entrypoint. DNS records live in your zone, not in the build, so this is the one discovery surface Blume can't publish for you — instead, add a record with your DNS provider:
153
+
154
+ ```txt
155
+ _index._agents.docs.example.com. 3600 IN HTTPS 1 docs.example.com. alpn=h2
156
+ ```
157
+
158
+ Use the `HTTPS` record type if your provider offers it (Vercel DNS does; it doesn't support the plain `SVCB` type), or a ServiceMode `SVCB` record with `alpn` and `port` parameters otherwise. The draft also recommends signing the zone with DNSSEC so validating resolvers return authenticated answers — providers like Cloudflare enable it in one click, while some (including Vercel DNS) don't support it at all.
159
+
160
+ `blume audit --url <origin>` checks this for you: when [`deployment.site`](/docs/deployment) is set, the network tier queries the entrypoint over DNS-over-HTTPS and reports the exact record to publish if none exists, plus whether the answers are DNSSEC-authenticated. Set `BLUME_DOH_URL` to point the lookup at your own resolver if your network blocks the public ones (Google, Cloudflare).
161
+
162
+ ## Web Bot Auth
163
+
164
+ [Web Bot Auth](https://datatracker.ietf.org/wg/webbotauth/about/) works in the other direction: it's not about agents reading your docs, but about **your organization's agents identifying themselves** when they make requests elsewhere. Your agents sign their requests with [HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421), and receiving sites verify them against a public-key directory published on your domain. If your org runs agents and your Blume site lives at the domain they identify as, publish their public keys:
165
+
166
+ ```ts blume.config.ts lineNumbers
167
+ ai: {
168
+ webBotAuth: {
169
+ keys: [{ kty: "OKP", crv: "Ed25519", x: "JrQLj5P_89iXES9-vFgrIy29c…" }],
170
+ },
171
+ }
172
+ ```
173
+
174
+ Blume then serves the JWKS at `/.well-known/http-message-signatures-directory` with its registered media type on every build surface. The directory is public by definition, so the config only admits public keys — a JWK containing private material (`d`, `p`, `q`, …) fails validation with an error rather than shipping a leaked credential. Generate an Ed25519 pair with:
175
+
176
+ ```bash
177
+ node -e 'const { generateKeyPairSync } = require("node:crypto"); const { publicKey, privateKey } = generateKeyPairSync("ed25519"); console.log("public: ", JSON.stringify(publicKey.export({ format: "jwk" }))); console.log("private:", JSON.stringify(privateKey.export({ format: "jwk" })))'
178
+ ```
179
+
180
+ The public JWK goes in the config above; the private one goes wherever your signing agent runs (a secret manager, never the repo). If your organization doesn't operate agents, skip this — an empty directory advertises nothing worth verifying.
181
+
182
+ Since `blume.config.ts` is executed at build time, the key doesn't have to be hardcoded — load it from a build-time environment variable to keep the config free of key blobs and rotate without a commit:
183
+
184
+ ```ts blume.config.ts lineNumbers
185
+ const webBotAuthKey = process.env.WEB_BOT_AUTH_PUBLIC_JWK;
186
+
187
+ export default defineConfig({
188
+ ai: {
189
+ webBotAuth: {
190
+ keys: webBotAuthKey ? [JSON.parse(webBotAuthKey)] : [],
191
+ },
192
+ },
193
+ });
194
+ ```
195
+
196
+ Environments without the variable publish no directory, and a key loaded this way is validated exactly like an inline one — including the private-material check. (The public key isn't a secret, so committing it inline is equally fine; the env var is an ergonomic choice, not a security one.)
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: SEO and AEO
3
+ description: How a Blume site gets found — by search engines and social platforms on one side, coding agents and AI assistants on the other — and where each surface is configured.
4
+ ---
5
+
6
+ Search engines and AI agents want the same thing from your docs: a clean, machine-readable account of what each page says, how the pages relate, and who publishes them. Blume treats the two as one discoverability layer. Metadata, social cards, feeds, and structured data serve the crawlers that rank you; `llms.txt`, raw Markdown, a JSON API, an MCP server, and discovery manifests serve the agents that answer questions about you — and the files in between (`robots.txt`, the sitemap, JSON-LD) are read by both.
7
+
8
+ Almost all of it is on by default and needs no configuration. The knobs live under two keys in `blume.config.ts`: `seo` for what search engines and social platforms see, and `ai` for what agents see.
9
+
10
+ ```ts blume.config.ts lineNumbers
11
+ seo: {
12
+ og: { enabled: true },
13
+ rss: { enabled: true, types: ["blog", "changelog"] },
14
+ sitemap: true,
15
+ robots: true,
16
+ structuredData: true,
17
+ x: { handle: "@acme" },
18
+ },
19
+ ai: {
20
+ llmsTxt: true,
21
+ mcp: { enabled: false },
22
+ },
23
+ ```
24
+
25
+ Most of this is sharper with an absolute site URL — set [`deployment.site`](/docs/deployment) so feeds, OG images, canonicals, the sitemap, JSON-LD, and every agent manifest can emit full URLs. A few surfaces (Open Graph images, the sitemap) stay off until it's set.
26
+
27
+ ## What the build emits
28
+
29
+ | Surface | Where | Default | Guide |
30
+ | --- | --- | --- | --- |
31
+ | `<head>` metadata, canonicals, X cards | every page | on | [Metadata](/docs/discoverability/metadata) |
32
+ | Social share images | `/og/<route>.png` | on with a site URL | [Open Graph images](/docs/discoverability/open-graph) |
33
+ | schema.org JSON-LD | every page | on | [Structured data](/docs/discoverability/structured-data) |
34
+ | RSS feeds | `/<type>/rss.xml` | on | [RSS feeds](/docs/discoverability/rss) |
35
+ | `sitemap.xml`, `robots.txt`, content signals | site root | on | [Sitemap and robots](/docs/discoverability/sitemap-and-robots) |
36
+ | `llms.txt`, `llms-full.txt` | site root | on | [llms.txt](/docs/discoverability/llms-txt) |
37
+ | Raw Markdown mirrors, content negotiation, Copy as Markdown, Open in chat | `/<route>.md` | on | [Markdown for agents](/docs/discoverability/markdown) |
38
+ | JSON API and its OpenAPI description | `/api/docs/…`, `/openapi.json` | on | [JSON API](/docs/discoverability/json-api) |
39
+ | MCP server | `/mcp` | opt-in, server output | [MCP server](/docs/discoverability/mcp) |
40
+ | `agent-readability.json`, `Link` headers, API catalog, WebMCP, skills, Web Bot Auth | site root, `/.well-known/…` | on | [Agent discovery](/docs/discoverability/agent-discovery) |
41
+
42
+ Every generated file yields to one you ship yourself: drop a `robots.txt`, `sitemap.xml`, `llms.txt`, `openapi.json`, or `agent-readability.json` in `public/` and Blume serves yours in its place.
43
+
44
+ The in-page **Ask AI** assistant is the one AI feature documented elsewhere: it's a reader-facing product feature rather than a discovery surface, so it lives under [Configuration](/docs/configuration/ask-ai).
45
+
46
+ ## Checking your work
47
+
48
+ [`blume audit`](/docs/reference/cli#auditing-the-built-site) crawls the built site and reports on titles, descriptions, canonicals, Open Graph and X cards, hreflang, the sitemap, `robots.txt`, and structured data. Point it at a deployment with `--url` to also check response headers and [DNS-based agent discovery](/docs/discoverability/agent-discovery#dns-based-discovery-dns-aid).
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: JSON API
3
+ description: The read-only JSON API every Blume site serves — page index, per-page documents, navigation, search — and the OpenAPI 3.1 description that lets function-calling frameworks build tools from it.
4
+ ---
5
+
6
+ Every Blume site also serves its docs as a small read-only **JSON API** — the REST twin of the [MCP server](/docs/discoverability/mcp)'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:
7
+
8
+ | Endpoint | Returns |
9
+ | --- | --- |
10
+ | `/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. |
11
+ | `/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. |
12
+ | `/api/docs/navigation.json` | The navigation tree — header tabs and the sidebar hierarchy. |
13
+ | `/api/docs/search?q=` | Full-text search, with the same `limit`, `contentTypes`, `locale`, `version`, and `filters[key]` scoping as `search_docs`. Server output only. |
14
+ | `/openapi.json` | The OpenAPI 3.1 description of the whole machine-readable surface. |
15
+
16
+ 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.
17
+
18
+ ## Errors
19
+
20
+ 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:
21
+
22
+ ```json
23
+ {
24
+ "code": "API_ROUTE_NOT_FOUND",
25
+ "detail": "No API route exists at /api/nope.",
26
+ "instance": "/api/nope",
27
+ "links": [
28
+ {
29
+ "href": "https://docs.example.com/openapi.json",
30
+ "label": "OpenAPI description"
31
+ },
32
+ {
33
+ "href": "https://docs.example.com/api/docs/pages.json",
34
+ "label": "Page index"
35
+ }
36
+ ],
37
+ "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.",
38
+ "status": 404,
39
+ "title": "API route not found",
40
+ "type": "about:blank"
41
+ }
42
+ ```
43
+
44
+ ## OpenAPI description
45
+
46
+ 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](/docs/discoverability/markdown), [`llms.txt`](/docs/discoverability/llms-txt) and `llms-full.txt`, [`agent-readability.json`](/docs/discoverability/agent-discovery#agent-readability) — and the [MCP endpoint](/docs/discoverability/mcp) 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](/docs/discoverability/agent-discovery#api-catalog), the [readability manifest](/docs/discoverability/agent-discovery#agent-readability), the homepage `Link` header as `rel="service-desc"`, and `llms.txt`.
47
+
48
+ 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.
49
+
50
+ ## Turning it off
51
+
52
+ Set `ai.api` to `false` to publish none of it:
53
+
54
+ ```ts blume.config.ts lineNumbers
55
+ ai: {
56
+ api: false,
57
+ }
58
+ ```
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: llms.txt
3
+ description: The llms.txt index and llms-full.txt corpus Blume generates for coding agents and chat assistants, the details block that tells them when to use your product, and how to exclude a page.
4
+ ---
5
+
6
+ Blume emits machine-readable versions of your docs that coding agents and chat assistants can consume. This is on by default; set `llmsTxt: false` to turn it off:
7
+
8
+ ```ts blume.config.ts lineNumbers
9
+ ai: {
10
+ llmsTxt: false,
11
+ }
12
+ ```
13
+
14
+ While enabled, `blume build` writes two files to the root of your site:
15
+
16
+ - **`/llms.txt`** — a compact index: your site title and description, then a linked list of every page with its summary, organized into sections that mirror your sidebar — folders and groups become headings, so an agent sees the docs' structure, not one flat blob.
17
+ - **`/llms-full.txt`** — the entire corpus: each page's full Markdown body, with its source URL, in one file.
18
+
19
+ Draft pages are excluded. Set [`deployment.site`](/docs/deployment) so the links and source URLs resolve to absolute addresses.
20
+
21
+ ## Options
22
+
23
+ `llmsTxt` also takes an object form with knobs for what the files include. If your [API reference](/docs/advanced/api-reference) documents a placeholder or example spec, set `openapi: false` to keep its generated pages out of both files:
24
+
25
+ ```ts blume.config.ts lineNumbers
26
+ ai: {
27
+ llmsTxt: {
28
+ enabled: true, // default
29
+ openapi: false, // exclude generated API reference pages
30
+ },
31
+ }
32
+ ```
33
+
34
+ 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:
35
+
36
+ ```ts blume.config.ts lineNumbers
37
+ ai: {
38
+ llmsTxt: {
39
+ details: [
40
+ "## When to use Acme",
41
+ "",
42
+ "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.",
43
+ ].join("\n"),
44
+ },
45
+ }
46
+ ```
47
+
48
+ ## Generated sections
49
+
50
+ `llms.txt` closes with two generated sections that need no configuration. **Agent skills** lists each skill published through [`ai.skills`](/docs/discoverability/agent-discovery#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](/docs/discoverability/markdown) mirror, the [MCP server](/docs/discoverability/mcp) and its discovery document, the skills index, the [API catalog](/docs/discoverability/agent-discovery#api-catalog), [`agent-readability.json`](/docs/discoverability/agent-discovery#agent-readability), and the [sitemap](/docs/discoverability/sitemap-and-robots#sitemap) — each only when it exists, so an agent that reads nothing but `llms.txt` still finds the whole surface.
51
+
52
+ ## Excluding a page
53
+
54
+ To keep an individual page out of both files, set `ai.exclude` in its frontmatter:
55
+
56
+ ```mdx
57
+ ---
58
+ title: Internal notes
59
+ ai:
60
+ exclude: true
61
+ ---
62
+ ```
63
+
64
+ The page still renders, stays in search, and keeps its place in the sitemap — only the `llms.txt` files skip it.
65
+
66
+ ## Bringing your own
67
+
68
+ To take full control of either file, add your own `llms.txt` or `llms-full.txt` to your `public/` folder. Like a custom favicon, it's picked up automatically and ships in place of the generated file — override one and Blume still generates the other.
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: Markdown for agents
3
+ description: Every page's raw Markdown at a .md URL or via Accept content negotiation, custom serializers for your own components, and the Copy as Markdown and Open in chat actions readers get for free.
4
+ ---
5
+
6
+ HTML is for browsers. Agents and LLMs do better with the Markdown your pages are written in — fewer tokens, no chrome, and components rendered in a form a model can read. Blume serves that Markdown for every page, in dev and production, with no configuration.
7
+
8
+ ## Raw Markdown
9
+
10
+ Append `.md` or `.mdx` to any page's URL to fetch its raw Markdown source — perfect for LLMs, coding agents, and "copy as Markdown" workflows.
11
+
12
+ | URL | Returns |
13
+ | ----------------- | ----------------------------------------- |
14
+ | `/quickstart` | The rendered page |
15
+ | `/quickstart.md` | Plain Markdown, with components converted |
16
+ | `/quickstart.mdx` | The raw MDX source, exactly as written |
17
+
18
+ Nested routes work the same way (`/content/syntax.md`), and the home page is served at `/index.md`.
19
+
20
+ 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. The components a generated [API reference](/docs/advanced/api-reference) page is made of downlevel too: `<Operation>` becomes the endpoint in its spec's own notation (`GET /pets/{id}`, `SEND user/signup`, `query pets`) with a deprecation marker, `<ApiTagOperations>` a list of those endpoints linked to their pages with their summaries, and `<ApiOverview>` the API's version and base URLs — so an agent reading a reference page knows what to call, and site search matches an endpoint's path. 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`](/docs/discoverability/llms-txt) and the [MCP server](/docs/discoverability/mcp)'s `get_page` tool, so every agent-facing surface reads clean Markdown. When you want the untransformed source, use the `.mdx` variant.
21
+
22
+ ### Content negotiation
23
+
24
+ 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`](/docs/discoverability/llms-txt) 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](/docs/discoverability/agent-discovery#agent-readability) advertises `contentNegotiation` only on deployments that honor the header.
25
+
26
+ 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.
27
+
28
+ ### Custom component serializers
29
+
30
+ 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:
31
+
32
+ ```ts blume.config.ts lineNumbers
33
+ import { defineConfig } from "blume";
34
+ import type { ComponentMarkdown } from "blume";
35
+
36
+ const chart: ComponentMarkdown = ({ props }) =>
37
+ `![${props.title}](/charts/${props.slug}.png)`;
38
+
39
+ export default defineConfig({
40
+ ai: {
41
+ markdownComponents: {
42
+ Chart: chart,
43
+ },
44
+ },
45
+ });
46
+ ```
47
+
48
+ 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.
49
+
50
+ 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.
51
+
52
+ ## Copy as Markdown
53
+
54
+ 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.
55
+
56
+ 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.
57
+
58
+ ## Open in chat
59
+
60
+ 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:
61
+
62
+ > Read `https://your-site/this-page.md` so I can ask you questions about this page.
63
+
64
+ 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.
65
+
66
+ 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.
67
+
68
+ 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:
69
+
70
+ ```ts blume.config.ts lineNumbers
71
+ ai: {
72
+ openInChat: ["claude", "chatgpt", "cursor"],
73
+ }
74
+ ```
75
+
76
+ To embed a ready-to-copy prompt inline in your content — rather than a whole-page action — use the [Prompt component](/docs/content/components#prompt), which renders a labeled row with a **Copy prompt** button and an optional open-in-Cursor link.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: MCP server
3
+ description: Host a Model Context Protocol server so coding agents can search and read your docs directly — tools, resources, discovery documents, content-type and facet filters, and the server output it needs.
4
+ ---
5
+
6
+ Host a [Model Context Protocol](https://modelcontextprotocol.io) server so coding agents (Claude Code, Cursor, VS Code, claude.ai connectors) can search and read your docs directly — no scraping. It's opt-in:
7
+
8
+ ```ts blume.config.ts lineNumbers
9
+ ai: {
10
+ mcp: {
11
+ enabled: true,
12
+ route: "/mcp", // where the server is mounted
13
+ },
14
+ }
15
+ ```
16
+
17
+ | Option | Default | Description |
18
+ | -------------- | ------- | ------------------------------------------------- |
19
+ | `enabled` | `false` | Generate and host the MCP server. |
20
+ | `route` | `/mcp` | Path the Streamable-HTTP endpoint is mounted on. |
21
+ | `name` | title | Server name shown to clients (defaults to title). |
22
+ | `instructions` | — | Optional system hint passed to connecting agents. |
23
+
24
+ ## Tools and resources
25
+
26
+ 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](/docs/discoverability/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).
27
+
28
+ `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.
29
+
30
+ The same tools are available over plain HTTP as the [JSON API](/docs/discoverability/json-api), for frameworks that don't speak MCP.
31
+
32
+ ## Scoping by content type and facets
33
+
34
+ `search_docs` and `list_pages` both accept an optional `contentTypes` filter, narrowing results to pages of the given frontmatter [`type`s](/docs/reference/frontmatter) — `["rfc"]`, `["blog", "changelog"]` — so an agent working against a site that mixes docs with RFCs, runbooks, or policies can scope retrieval to the kind of page it needs. Every result names its content type, and `list_pages` output shows the types in use.
35
+
36
+ Both tools also accept a `filters` object matching against the facets a site declares per content type ([`content.types.<type>.facets`](/docs/configuration#frontmatter)) — custom frontmatter keys whose values become filterable metadata:
37
+
38
+ ```json
39
+ {
40
+ "query": "OpenAPI request schemas",
41
+ "contentTypes": ["rfc"],
42
+ "filters": { "domain": "architecture", "status": "enforced" }
43
+ }
44
+ ```
45
+
46
+ Every `filters` entry must match (results carry their facet values, and `list_pages` shows each page's), so a knowledge base can drive progressive-disclosure agent workflows — enumerate the enforced standards, search only within them — without any server of its own.
47
+
48
+ ## Server output required
49
+
50
+ The MCP server is a live endpoint (`/mcp`), so it can't run on a static build. Switch to server output and pick an adapter:
51
+
52
+ ```ts blume.config.ts lineNumbers
53
+ deployment: {
54
+ output: "server",
55
+ adapter: "node", // or "vercel" | "netlify" | "cloudflare"
56
+ site: "https://docs.example.com",
57
+ }
58
+ ```
59
+
60
+ A static build with `ai.mcp.enabled` fails fast with a message telling you to set `deployment.output` to `server`. See [Deployment](/docs/deployment) for the adapters. Once deployed, connect from Claude Code with:
61
+
62
+ ```bash
63
+ claude mcp add --transport http my-docs https://docs.example.com/mcp
64
+ ```
@@ -0,0 +1,18 @@
1
+ import { defineMeta } from "blume";
2
+
3
+ export default defineMeta({
4
+ order: 5,
5
+ pages: [
6
+ "metadata",
7
+ "open-graph",
8
+ "structured-data",
9
+ "rss",
10
+ "sitemap-and-robots",
11
+ "llms-txt",
12
+ "markdown",
13
+ "json-api",
14
+ "mcp",
15
+ "agent-discovery",
16
+ ],
17
+ title: "Discoverability",
18
+ });
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: Metadata
3
+ description: The head tags every page renders — title, description, canonical, Open Graph and X card tags — and how to override them per page from frontmatter.
4
+ ---
5
+
6
+ Every page renders the standard `<head>` tags from your config and frontmatter — no configuration needed. Where a value can be tuned it lives under the `seo` key in `blume.config.ts` or under `seo` in a page's frontmatter.
7
+
8
+ - `<title>` — the page title plus your site `title`.
9
+ - `<meta name="description">` and `og:description` — the page `description`, falling back to the site `description`.
10
+ - `og:title` and `og:site_name` — the page title and your site `title`.
11
+ - `<link rel="canonical">` and `og:url` — the page's absolute URL (when [`deployment.site`](/docs/deployment) is set).
12
+ - `og:type` — `article` on blog posts and changelog entries, `website` elsewhere. Article pages also emit `article:published_time` and `article:modified_time` from the page's `date` and last-modified timestamp.
13
+ - `og:image` — the [OG image](/docs/discoverability/open-graph) for the page. A generated card also declares its `og:image:width`, `og:image:height`, `og:image:type`, and `og:image:alt`, so a crawler can lay the card out without fetching it first; an `seo.image` you supply yourself declares none of these, since its size and format are unknown.
14
+ - `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image` — the X card. Pages with an image get the wide `summary_large_image` variant; pages without one still get the compact `summary` card rather than rendering as a bare link.
15
+
16
+ ## X attribution
17
+
18
+ X reads everything else on the card from the `og:*` tags, so the only values it can't infer are the accounts to credit. Set them under `seo.x` and Blume emits `twitter:site` (your site's account) and `twitter:creator` (the author's). The `@` is optional — `acme` and `@acme` both work.
19
+
20
+ ```ts blume.config.ts lineNumbers
21
+ seo: {
22
+ x: { handle: "@acme", creator: "@jane" },
23
+ }
24
+ ```
25
+
26
+ A page can claim its own author, which is what you want for a guest post:
27
+
28
+ ```yaml lineNumbers
29
+ ---
30
+ title: How we shipped it
31
+ seo:
32
+ x:
33
+ creator: "@guestauthor"
34
+ ---
35
+ ```
36
+
37
+ ## Per-page overrides
38
+
39
+ Override any of the other tags per page with `seo` frontmatter:
40
+
41
+ ```yaml lineNumbers
42
+ ---
43
+ title: Pricing
44
+ description: Plans and pricing for every team size.
45
+ seo:
46
+ title: Pricing — Acme
47
+ canonical: https://acme.com/pricing
48
+ noindex: false
49
+ ---
50
+ ```
51
+
52
+ <TypeTable
53
+ type={{
54
+ "seo.title": {
55
+ type: "string",
56
+ description: "Override the <title> and og:title for this page.",
57
+ },
58
+ "seo.description": {
59
+ type: "string",
60
+ description: "Override the meta + og:description.",
61
+ },
62
+ "seo.image": {
63
+ type: "string",
64
+ description: "Custom social image (see Open Graph images).",
65
+ },
66
+ "seo.canonical": {
67
+ type: "string",
68
+ description: "Override the canonical URL.",
69
+ },
70
+ "seo.noindex": {
71
+ type: "boolean",
72
+ description: "Emit robots noindex and skip structured data.",
73
+ },
74
+ "seo.x.creator": {
75
+ type: "string",
76
+ description:
77
+ "Credit this page to an X account (twitter:creator), overriding seo.x.creator from your config.",
78
+ },
79
+ }}
80
+ />
81
+
82
+ A `noindex` page also drops out of the [sitemap](/docs/discoverability/sitemap-and-robots#sitemap) and skips [structured data](/docs/discoverability/structured-data).