---
title: "Markdown Conversion"
description: "Serve Markdown versions of your pages and customize HTML conversion."
canonical_url: "https://nuxtseo.com/docs/ai-ready/guides/markdown"
last_updated: "2026-09-26T09:28:49.582Z"
---

Request an eligible page with a `.md` suffix to read it as Markdown:

```bash
curl https://example.com/about.md
```

[mdream](https://github.com/harlan-zw/mdream) converts rendered HTML during prerendering and at runtime.
Nuxt Content pages can use their content source instead.

## Build-Time

During `nuxi generate`{lang="bash"}:

1. The module queues a Markdown route for each eligible rendered page, such as `/about/` → `/about/index.md`.
2. The middleware reads content source or converts rendered HTML.
3. The Nuxt `ai-ready:page:markdown` hook can change the result.
4. The module stores page data in [SQLite](https://sqlite.org) and appends content to `llms-full.txt`.
5. Nuxt writes the Markdown route as a static file.

Metadata extracted: title, description, headings, updatedAt (from `article:modified_time` etc).

## Runtime

Explicit `.md` requests select Markdown directly.
For other eligible page URLs, the module checks `Accept`, `Sec-Fetch-Dest`, and AI bot classification.
It uses the negotiated media preference rather than requiring an exact header string.

To request Markdown through content negotiation, follow the redirect:

```bash
curl -L -H "Accept: text/markdown" https://example.com/about
```

When the `Accept` header negotiates markdown on a non-`.md` URL, the module
issues a `307`{lang="http"} redirect to the `.md` twin so HTML and markdown
variants live under separate cache keys.

Negotiation runs ahead of the Nitro static asset handler. A prerendered route
served by your own Nitro server, such as the `node-server`{lang="ts"} preset,
negotiates the same way a server-rendered route does.

## Cache Safety

The module resolves automatic negotiation from the effective route rule on each request. It turns negotiation off for:

- ISR routes
- Nitro response caches that do not vary by `Accept`{lang="http"}, `Sec-Fetch-Dest`{lang="http"}, and `User-Agent`{lang="http"}

This covers `cache`{lang="ts"} and `swr`{lang="ts"} route rules. A cache with `headersOnly: true`{lang="ts"} does not store the response body, so negotiation remains enabled.

To use negotiation with Nitro response caching, include every negotiation input in the cache key:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  routeRules: {
    '/docs/**': {
      cache: {
        maxAge: 3600,
        varies: ['accept', 'sec-fetch-dest', 'user-agent'],
      },
    },
  },
})
```

CDN and reverse proxy rules configured outside Nuxt are not visible at request time. Disable negotiation when one of those caches responses by URL and cannot honor `Vary`{lang="http"}:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  aiReady: {
    contentNegotiation: false,
  },
})
```

`/about` then stays HTML for every `Accept`{lang="http"} and `User-Agent`{lang="http"} header. `/about.md` and the Markdown alternate link remain available.

Negotiated redirects include `Cache-Control: private, no-store`{lang="http"} and a complete `Vary`{lang="http"} header. This prevents a compliant shared cache from storing the redirect under the HTML URL. A cache that bypasses the origin can still return cached HTML to an AI client.

**Prerendered routes behind a CDN.** Request-time negotiation cannot run when a CDN serves the prerendered HTML asset directly, because the request never reaches Nitro. Configure the CDN to vary on all three headers, or use the discovery options below.

Two ways agents can still find the markdown URL:

1. **Follow the alternate link**. Eligible prerendered HTML pages include
`<link rel="alternate" type="text/markdown" href="/foo.md">`{lang="html"} in
the `<head>`{lang="html"}, so HTML-parsing crawlers pick it up.
2. **Request `.md` directly**. The build step writes a Markdown file for each
eligible prerendered page and serves it as a static asset.

## Nuxt Content Integration

With [`@nuxt/content`](https://content.nuxt.com) v3 installed, page collections can supply Markdown without rendering HTML.
The module serializes the stored content tree back to Markdown.

```bash
npx nuxi@latest module add @nuxt/content
```

Define a page collection as you normally would:

```ts [content.config.ts]
import { defineCollection, defineContentConfig } from '@nuxt/content'

export default defineContentConfig({
  collections: {
    blog: defineCollection({
      type: 'page',
      source: 'blog/**/*.md',
    }),
  },
})
```

Now `/<route>.md` returns serialized content with `canonical_url` in its frontmatter, plus `last_updated` when the file has a date.
For example:

```bash
curl https://example.com/blog/hello-world.md
```

```md
---
title: "Hello World"
description: "A post served from @nuxt/content source markdown."
canonical_url: "https://example.com/blog/hello-world"
last_updated: "2026-04-25T03:43:31.143Z"
---

# Hello from Nuxt Content
...
```

Routes not backed by a content collection fall through to HTML→mdream conversion. The module auto-detects `@nuxt/content` at build time, no configuration required.

The content tree preserves Markdown structure, including code fences and MDC components.
Serialization can change formatting, so the response is not a byte-for-byte copy of the source file.

## Configuration

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  aiReady: {
    mdreamOptions: {
      minimal: true,
    },
    markdownCacheHeaders: {
      maxAge: 3600,
      swr: true,
    },
  },
})
```

## Hooks

These Nitro conversion hooks run for runtime HTML-to-Markdown requests.
They do not run for content-source responses or manual indexing conversion.

### `'ai-ready:mdreamConfig'`{lang="ts"}

Filter an element from HTML conversion with the supported `filter.exclude` option:

```ts [server/plugins/mdream-config.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ai-ready:mdreamConfig', (options) => {
    options.filter = {
      ...options.filter,
      exclude: [...(options.filter?.exclude || []), '.author-bio'],
    }
  })
})
```

### `'ai-ready:page:markdown'`{lang="ts"}

Append a source route to the converted Markdown:

```ts [server/plugins/markdown-footer.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ai-ready:page:markdown', (ctx) => {
    ctx.markdown += `\n\nSource: ${ctx.route}`
  })
})
```

See [Nitro Hooks](/docs/ai-ready/nitro-api/nitro-hooks) for context types.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
