# Serving your site as Markdown to LLMs and agents

Every page in Turbo Start Sanity is also available as clean Markdown. Here is exactly how the content-negotiation layer works, from proxy to serializer.

![Neon yellow and orange light streaks crossing a dark dotted grid](https://cdn.sanity.io/images/s6kuy1ts/production/57efbed4ccc2c41cd2bb2821848afe69b12edd57-4096x2596.png?w=1600&fm=webp&q=80&auto=format)

Agents do not want your HTML. They want the words. When an LLM crawls a normal page it burns tokens parsing navigation, wrappers and script tags to recover a few paragraphs of meaning. Turbo Start Sanity sidesteps that entirely: every page is also served as clean Markdown, from the same content, with no separate export step.

## Two ways to ask for it

There are two triggers. Append `.md` to any URL — `/about.md`, `/blog/some-post.md`, `/index.md` — or send an `Accept: text/markdown` header. Either one gets you Markdown; everything else gets the normal page.

## The proxy decides

A proxy inspects each GET or HEAD request. If the path ends in `.md` or the `Accept` header prefers Markdown, it strips the suffix, normalises the path, and rewrites the request to an internal route handler — forwarding the resolved content path as a request header, because a rewrite's query params are not reliably visible downstream:

```ts
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-markdown-path", contentPath);

const url = request.nextUrl.clone();
url.pathname = "/api/markdown";
url.searchParams.set("path", contentPath);
return NextResponse.rewrite(url, { request: { headers: requestHeaders } });
```

There is a deliberate guard: a header-negotiated request whose last path segment contains a dot — an asset like a `.png` sent with a broad `Accept` — passes straight through, so you never try to Markdown-render an image.

## The route builds the Markdown

The route handler reads the path, splits it into segments, and fetches the matching Sanity data — home, blog index, blog post or generic page — each wrapped in `"use cache"` so the underlying `sanityFetch` participates in tag-based revalidation. It always fetches the published perspective with stega disabled, because this output is for machines.

The serialization is the interesting part. It never touches React. A thin dispatcher walks the page-builder array and maps each block's `_type` to a co-located Markdown serializer:

```ts
function blockToMarkdown(block, options) {
  switch (block?._type) {
    case "hero": return heroToMarkdown(block, options);
    case "cta": return ctaToMarkdown(block, options);
    case "faqAccordion": return faqAccordionToMarkdown(block, options);
    // ...one case per block
    default: return "";
  }
}
```

Because it serializes structured data rather than rendering components, a block can never leak as a raw `<Component/>` tag. An unknown block type simply returns an empty string and is filtered out. That is also why adding a new page-builder block includes adding its Markdown serializer — without one, the block renders blank in `.md` output.

## Headers that keep caches honest

The Markdown response is careful about caching and crawlers. It sets `Vary: Accept` so a shared cache never serves Markdown to a browser, `content-location` pointing at the canonical HTML page, and `x-robots-tag: noindex, nofollow` to keep the Markdown twin out of search results. A short `s-maxage` with `stale-while-revalidate` bounds CDN drift, since the rendered response itself is not tag-purged.

There is even graceful failure: an upstream fetch error returns a 503, not a 404, so crawlers do not treat a transient blip as a page that is gone. Redirects modelled in Sanity are honoured too, mapped to their `.md` form and restricted to same-origin destinations.

## Why bother

It is a small amount of infrastructure for an outsized payoff. Your content becomes cheap for agents to read, your structured data stays the single source of truth, and you never maintain a parallel Markdown copy that drifts out of date.
