---
title: Markdown for agents
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.
---

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.

## Raw Markdown

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.

| URL               | Returns                                   |
| ----------------- | ----------------------------------------- |
| `/quickstart`     | The rendered page                         |
| `/quickstart.md`  | Plain Markdown, with components converted |
| `/quickstart.mdx` | The raw MDX source, exactly as written    |

Nested routes work the same way (`/content/syntax.md`), and the home page is served at `/index.md`.

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.

### Content negotiation

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.

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.

### Custom component serializers

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:

```ts blume.config.ts lineNumbers
import { defineConfig } from "blume";
import type { ComponentMarkdown } from "blume";

const chart: ComponentMarkdown = ({ props }) =>
  `![${props.title}](/charts/${props.slug}.png)`;

export default defineConfig({
  ai: {
    markdownComponents: {
      Chart: chart,
    },
  },
});
```

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.

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.

## Copy as Markdown

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.

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.

## Open in chat

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:

> Read `https://your-site/this-page.md` so I can ask you questions about this page.

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.

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.

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:

```ts blume.config.ts lineNumbers
ai: {
  openInChat: ["claude", "chatgpt", "cursor"],
}
```

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.
