---
title: "Markdown Conversion · Nuxt AI Ready · Nuxt SEO"
canonical_url: "https://nuxtseo.com/docs/ai-ready/guides/markdown"
last_updated: "2026-08-16T09:50:32.577Z"
meta:
  description: "How HTML pages are converted to markdown for AI-friendly content delivery."
  "og:description": "How HTML pages are converted to markdown for AI-friendly content delivery."
  "og:title": "Markdown Conversion · Nuxt AI Ready · Nuxt SEO"
---

Nuxt SEO on GitHub

Switch to AI ReadySwitch to Nuxt SEOSwitch to RobotsSwitch to SitemapSwitch to OG ImageSwitch to Schema.orgSwitch to Link CheckerSwitch to SEO UtilsSwitch to Site ConfigSwitch to Skew Protection

**Core Concepts**

# **Markdown Conversion**

[**~~mdream~~**](https://github.com/harlan-zw/mdream) converts HTML pages to markdown during prerendering and runtime.

## Build-Time

During `**nuxi generate**`:

1. Nuxt plugin queues `**.md**` route for each rendered page ( `**/about/**` → `**/about/index.md**`)
2. Prerender middleware fetches HTML, converts to markdown, extracts metadata
3. `**ai-ready:page:markdown**` hook fires per page
4. Content appended to `**page-data.jsonl**` and streamed to `**llms-full.txt**`
5. Route serves raw markdown

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

## Runtime

Markdown served when:

- Path ends in `**.md**` (explicit), OR
- `**Accept**` includes `**text/markdown**` AND NOT `**text/html**` AND `**sec-fetch-dest**` ≠ `**document**`

Targets API clients (Claude Code, curl, [**~~Bun~~**](https://bun.sh)) while excluding browsers.

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

When the `**Accept**` header negotiates markdown on a non-`**.md**` URL, the middleware issues a `**307**` redirect to the `**.md**` twin so HTML and markdown variants live under separate cache keys.

## 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**`, `**Sec-Fetch-Dest**`, and `**User-Agent**`

This covers `**cache**` and `**swr**` route rules. A cache with `**headersOnly: true**` 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:

nuxt.config.ts

```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**`:

nuxt.config.ts

```ts
export default defineNuxtConfig({
  aiReady: {
    contentNegotiation: false,
  },
})
```

`**/about**` then stays HTML for every `**Accept**` and `**User-Agent**` header. `**/about.md**` and the Markdown alternate link remain available.

Negotiated redirects include `**Cache-Control: private, no-store**` and a complete `**Vary**` header. This prevents a compliant shared cache from storing the redirect under the HTML URL. A cache that bypasses the origin before the middleware runs 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. 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**. Every prerendered HTML page includes `**<link rel="alternate" type="text/markdown" href="/foo.md">**` in the `**<head>**`, so HTML-parsing crawlers pick it up.
2. **Request `**.md**` directly**. The build step writes `**/foo.md**` for every prerendered route and serves it as a static asset.

## Nuxt Content Integration

With [`**@nuxt/content**`](https://content.nuxt.com) v3 in your modules, the middleware serves source markdown directly for any route backed by a page collection, skipping the HTML→mdream round-trip.

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

Define a page collection as you normally would:

content.config.ts

```ts
import { defineCollection, defineContentConfig } from '@nuxt/content'

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

Now `**/<route>.md**` returns the source markdown, with `**canonical_url**` and `**last_updated**` merged into the frontmatter for [**~~agent-readability~~**](https://github.com/vercel-labs/agent-readability) compliance:

```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.

**Why source markdown?** `**@nuxt/content**` parses files into a structural AST ([**~~minimark~~**](https://github.com/farnabaz/minimark)). The lookup serializes that AST back to markdown via `**minimark/stringify**`, the same approach `**@nuxt/content**`'s built-in `**/raw/<slug>.md**` route uses. Compared to HTML→markdown conversion, the AST round-trip preserves semantic structure (lists, code fences, MDC components) without HTML parsing artifacts.

## Configuration

nuxt.config.ts

```ts
export default defineNuxtConfig({
  aiReady: {
    mdreamOptions: {
      minimal: true,
    },
    markdownCacheHeaders: {
      maxAge: 3600,
      swr: true,
    },
  },
})
```

## Hooks

### `**'ai-ready:mdreamConfig'**`

Modify mdream options before conversion:

server/plugins/mdream-config.ts

```ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ai-ready:mdreamConfig', (options) => {
    if (options.origin?.includes('/blog/'))
      options.ignoreElements = [...(options.ignoreElements || []), '.author-bio']
  })
})
```

### `**'ai-ready:page:markdown'**`

Modify markdown output at runtime:

server/plugins/markdown-footer.ts

```ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ai-ready:page:markdown', (ctx) => {
    ctx.markdown = `---\ntitle: ${ctx.title}\n---\n\n${ctx.markdown}`
  })
})
```

See [**~~Nitro Hooks~~**](https://nuxtseo.com/docs/ai-ready/nitro-api/nitro-hooks) for context types.

**Was this page helpful?**

### **Related **

[**Nitro Hooks**](https://nuxtseo.com/docs/ai-ready/nitro-api/nitro-hooks)

[**Configuration**](https://nuxtseo.com/docs/ai-ready/api/config)

[**AI-Optimized Content**](https://nuxtseo.com/learn-seo/nuxt/launch-and-listen/ai-optimized-content)

[**AI Content Signals** Control how AI systems interact with your content through robots.txt directives.](https://nuxtseo.com/docs/ai-ready/guides/content-signals) [**API Catalog** Publish an RFC 9727 catalog so agents can discover your public APIs.](https://nuxtseo.com/docs/ai-ready/guides/api-catalog)