---
title: "Content Negotiation para Agentes de IA: De 316KB a 1.3KB (Reducción del 99.6%)"
excerpt: "Cloudflare lanzó una función que reduce tokens un 80%. Pero hay una mejor forma: conversión desde la fuente. Descubre cómo implementar content negotiation en Next.js y lograr 97% de reducción sin perder fidelidad."
date: "2026-02-13T12:00:00.000Z"
lastModified: "2026-03-13T00:00:00.000Z"
category: "Next.js"
author:
  name: "angel cruz"
  picture: "https://angelcruzdevcdn.nyc3.cdn.digitaloceanspaces.com/images/me/angel-cruz.png"
ogImage:
  url: "/images/open-graph/mcp-opengraph-image.png"
seo_title: "Content Negotiation Next.js: Markdown para Agentes IA - Reducción 99.6%"
seo_description: "Guía completa: implementa content negotiation en Next.js para servir markdown a agentes de IA. Reduce tokens 97% sin duplicar archivos. Código incluido."
---

Cloudflare anunció una función que reduce el uso de tokens en agentes de IA en un 80%. Hay un enfoque que logra **97% de reducción** y te da control total sobre la implementación. Este sitio ya lo está usando en producción.

Los agentes de IA como Claude Code, OpenCode y otros ahora envían el header `Accept: text/markdown` cuando solicitan contenido web. ¿Por qué? Porque las páginas HTML completas desperdician entre 80% y 99% de los tokens en navegación, estilos, scripts y anuncios. Para un LLM, todo ese markup es ruido.

En este artículo te mostraré dos enfoques para resolver este problema: **conversión en el edge** (como Cloudflare) y **conversión desde la fuente** (la implementación superior que uso aquí). Aprenderás cómo implementar content negotiation en Next.js, compartiré código de producción completo, y te mostraré benchmarks reales.

## El Problema: HTML No es Óptimo para Agentes de IA

Imagina que un agente de IA quiere leer un artículo técnico en tu blog. Hace una petición a tu URL y recibe... **316KB de HTML**.

Veamos un ejemplo real de este sitio:

```bash
// Respuesta HTML completa: 316,270 bytes
curl -sL https://www.angelcruz.dev/post/adminer-gestor-de-bases-de-datos-minimalista | wc -c
316270

// Respuesta markdown: 1,338 bytes
curl -sL https://www.angelcruz.dev/post/adminer-gestor-de-bases-de-datos-minimalista.md | wc -c
1338
```

**Reducción: 99.6% en tamaño de payload**

Pero el problema no es solo el tamaño. Analicemos qué contiene esa respuesta HTML:

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Adminer: gestor de bases de datos minimalista</title>
  <link rel="stylesheet" href="/styles.css">
  <script src="/analytics.js"></script>
  <!-- 50+ meta tags para SEO y Open Graph -->
</head>
<body>
  <nav class="sticky top-0 z-50 backdrop-blur-md">
    <!-- Navegación completa con menú, logo, búsqueda -->
  </nav>

  <main class="container mx-auto px-4">
    <article>
      <!-- AQUÍ ESTÁ EL CONTENIDO QUE EL AGENTE NECESITA -->
    </article>

    <aside>
      <!-- Sidebar con posts relacionados, categorías, etc. -->
    </aside>
  </main>

  <footer>
    <!-- Links, copyright, redes sociales -->
  </footer>

  <script src="/bundle.js"></script>
  <!-- Scripts de analytics, cookies, etc. -->
</body>
</html>
```

De esos 316KB, el contenido real que el agente necesita es **menos del 1%**. El resto es:

- **Navegación y footer**: 20-30KB de HTML que el agente ignora
- **CSS y clases de Tailwind**: `class="flex items-center justify-between px-4 py-2 text-sm font-medium..."` aporta cero valor semántico
- **JavaScript bundles**: Analytics, interacciones del cliente, frameworks
- **Meta tags**: 50+ tags para SEO, Open Graph, Twitter Cards
- **Ads y cookie banners**: Contenido comercial que distrae

Para un Large Language Model, esto es equivalente a:

```
Tokens estimados (HTML completo): ~80,000 tokens
Tokens estimados (markdown puro): ~350 tokens
```

Con Claude Opus cobrando **$15 por millón de tokens de entrada**, cada lectura de ese artículo HTML cuesta **$1.20**. La versión markdown cuesta **$0.005**. Una reducción de **240x en costos de API**.

### ¿Por Qué los Agentes de IA Luchan con HTML?

Los LLMs procesan contenido de forma fundamentalmente diferente a los navegadores:

1. **No renderizan visualmente**: Las clases CSS y estilos inline no aportan información útil
2. **No ejecutan JavaScript**: Los scripts son texto incomprensible
3. **Necesitan estructura semántica**: Markdown proporciona jerarquía clara (`#`, `##`, listas, código)
4. **Ventana de contexto limitada**: Cada token cuenta cuando tienes límites de 200K o 500K tokens
5. **Mejor comprensión con texto limpio**: Sin distracciones, el modelo entiende mejor el contenido

El resultado: Los agentes de IA piden markdown, no HTML.

## Content Negotiation: El Estándar HTTP

La solución a este problema no es nueva. Se llama **content negotiation** (negociación de contenido) y es un estándar HTTP desde hace décadas.

### ¿Cómo Funciona?

El cliente (agente de IA) envía un header `Accept` especificando qué tipo de contenido prefiere:

```bash
GET /post/mi-articulo HTTP/1.1
Host: www.angelcruz.dev
Accept: text/markdown
```

El servidor responde con el formato solicitado si está disponible:

```bash
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Cache-Control: public, s-maxage=2592000

---
title: Mi Artículo
date: 2026-02-13
---

// Mi Artículo

Contenido del artículo en markdown...
```

Si el servidor no soporta markdown, responde con HTML y el header `Content-Type: text/html`:

```bash
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Vary: Accept

<!DOCTYPE html>
<html>...
```

El header **`Vary: Accept`** es crucial: le dice a las CDNs y proxies que cacheen versiones separadas según el valor del header `Accept`.

### ¿Por Qué es Mejor que User-Agent Detection?

Algunos sitios intentan detectar agentes de IA mediante el `User-Agent` header:

```javascript
// NO HAGAS ESTO
if (userAgent.includes('ClaudeCode') || userAgent.includes('GPTBot')) {
  return markdownResponse
}
return htmlResponse
```

Este enfoque tiene problemas:

1. **SEO risk**: Google penaliza el "cloaking" (servir contenido diferente según user-agent)
2. **Frágil**: Cada nuevo agente requiere actualizar la lista
3. **No estándar**: Viola las mejores prácticas HTTP
4. **Falsos positivos**: Un usuario real podría modificar su user-agent

Content negotiation es explícito y estándar: el cliente **pide** markdown con `Accept`, y el servidor **negocia** la mejor respuesta.

### Agentes de IA que lo Soportan

Actualmente estos agentes envían `Accept: text/markdown`:

- **Claude Code** (Anthropic CLI)
- **OpenCode**
- **Bun Docs** (primeros en implementarlo)
- **GitHub Copilot** (próximamente, rumoreado)
- **Cursor** (evaluando implementación)

Es un estándar emergente. Dentro de 6-12 meses, la mayoría de agentes lo soportarán.

## Dos Enfoques: Edge vs Source Conversion

Ahora que entendemos el problema, ¿cómo lo resolvemos? Hay dos estrategias fundamentalmente diferentes.

### Comparación: Edge Conversion vs Source Conversion

| Aspecto | Edge Conversion (Cloudflare) | Source Conversion (Este Sitio) |
|---------|------------------------------|--------------------------------|
| **Reducción de tokens** | ~80% | ~97% |
| **Fuente de conversión** | HTML → Markdown (parsing) | Markdown original (directo) |
| **Fidelidad del contenido** | Puede perder componentes custom | 100% preservación |
| **Metadatos disponibles** | Limitados (extraídos de HTML) | Frontmatter completo |
| **Implementación** | Toggle en dashboard Cloudflare | Route handlers personalizados |
| **Control sobre serialización** | Limitado (lógica edge) | Total (código propio) |
| **Costo** | Requiere plan Cloudflare | Gratis (Next.js built-in) |
| **Latencia** | +20-50ms (parsing HTML) | 0ms adicional (lectura directa) |
| **Componentes custom** | Pueden perderse (`<CodePlayground>`) | Manejo explícito |
| **Dependencias externas** | Requiere Cloudflare | Ninguna |

### Edge Conversion: El Enfoque de Cloudflare

Así funciona la nueva feature de Cloudflare:

1. **Request llega a Cloudflare edge** con `Accept: text/markdown`
2. **Cloudflare hace fetch del HTML** desde tu servidor de origen
3. **Parser genérico convierte HTML → Markdown** usando heurísticas
4. **Resultado se cachea** en el edge
5. **Se responde al cliente** con markdown convertido

```
[Cliente con Accept:text/markdown]
    ↓
[Cloudflare Edge]
    ↓ fetch HTML
[Tu servidor: HTML completo]
    ↓ conversión
[HTML → Markdown parser]
    ↓
[Cache en edge]
    ↓
[Cliente recibe markdown]
```

**Ventajas**:
- Implementación instantánea: solo activas un toggle
- Funciona con cualquier CMS o stack backend
- No requiere cambios en tu código
- Cloudflare maneja el parsing

**Desventajas**:
- Solo 80% de reducción (parte del HTML permanece)
- Parser genérico puede malinterpretar estructuras complejas
- Componentes custom (`<Tabs>`, `<CodePlayground>`) se pierden o convierten mal
- Metadatos limitados (solo lo que está en HTML)
- Sin control sobre el proceso de serialización
- Requiere suscripción a Cloudflare

**Ejemplo de conversión con pérdidas**:

```jsx
// Tu componente React personalizado
<CodePlayground
  language="javascript"
  initialCode="console.log('Hello')"
  showConsole={true}
/>
```

Cloudflare lo ve como HTML:

```html
<div class="code-playground" data-language="javascript">
  <pre>console.log('Hello')</pre>
  <div class="console-output"></div>
</div>
```

Conversión resultante:

```markdown
console.log('Hello')
```

Se perdieron: el contexto del playground, la interactividad, los atributos. Para un agente de IA, ahora es solo código suelto sin explicación.

### Source Conversion: El Enfoque Superior

Source conversion significa **servir el markdown original**, sin conversión intermedia:

1. **Almacenas contenido en markdown** (ej: `_posts/{slug}/index.md`)
2. **Request llega con `Accept: text/markdown`**
3. **Lees el archivo markdown directamente** (sin parsing HTML)
4. **Respondes con markdown + frontmatter** tal como está almacenado
5. **Caches como cualquier otra respuesta**

```
[Cliente con Accept:text/markdown]
    ↓
[Next.js Route Handler]
    ↓ lectura directa
[_posts/mi-articulo/index.md]
    ↓ sin conversión
[Cliente recibe markdown original]
```

**Ventajas**:
- **97% de reducción**: Sin overhead de HTML en absoluto
- **Fidelidad perfecta**: Es el markdown fuente, sin interpretación
- **Metadatos completos**: Frontmatter con todos los campos
- **Control total**: Decides qué incluir/excluir
- **Gratis**: No requiere servicios externos
- **Sin latencia adicional**: Lectura directa del filesystem
- **Componentes custom**: Decides cómo serializarlos

**Desventajas**:
- Requiere que tu contenido esté en markdown (o convertible)
- Necesitas implementar route handlers personalizados
- No funciona "out of the box" como Cloudflare

**Cuándo usar cada enfoque**:

- **Edge Conversion** si:
  - Ya usas Cloudflare
  - Tu contenido está en HTML puro (no tienes markdown fuente)
  - Necesitas implementación en 5 minutos
  - 80% de reducción es suficiente

- **Source Conversion** si:
  - Usas Next.js, Astro, Hugo u otro generador con markdown
  - Quieres máxima reducción (97%)
  - Necesitas control total sobre serialización
  - Tienes componentes custom que requieren manejo especial

Este sitio usa **source conversion** porque el contenido ya está en markdown. Veamos cómo implementarlo.

## Implementación en Next.js: Route Handlers

La implementación completa requiere tres piezas:

1. **Route handlers** para servir markdown
2. **Rewrites** en `next.config.mjs` para content negotiation
3. **Parsing logic** para extraer markdown y frontmatter

### Estructura de Archivos

```
_posts/
  ├── mi-articulo/
  │   └── index.md           # Post con frontmatter + contenido

app/
  ├── md/
  │   └── post/[slug]/
  │       └── route.ts       # Route handler para markdown
  ├── post/[slug]/
  │   └── page.tsx           # Página HTML tradicional

lib/
  ├── markdown.ts            # Parsing de markdown files
  └── posts.ts               # Funciones para obtener posts

next.config.mjs              # Rewrites para content negotiation
```

### Route Handler Implementation

Creamos un route handler en `app/md/post/[slug]/route.ts`:

```typescript
import { notFound } from 'next/navigation'
import { parseMarkdownFile } from '@/lib/markdown'
import type { NextRequest } from 'next/server'

export const runtime = 'nodejs'
export const dynamic = 'force-static'
export const revalidate = 2592000 // 30 días

export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params

  try {
    // Leer y parsear el archivo markdown
    const parsed = await parseMarkdownFile(slug)

    if (!parsed) {
      notFound()
    }

    // Reconstruir frontmatter YAML
    const frontmatterLines = [
      '---',
      `title: ${parsed.frontmatter.title}`,
      `date: ${parsed.frontmatter.date}`,
      `category: ${parsed.frontmatter.category}`,
      `author: ${parsed.frontmatter.author?.name || 'Anonymous'}`,
      `excerpt: ${parsed.frontmatter.excerpt || ''}`,
      '---',
      '',
    ]

    const markdown = frontmatterLines.join('\n') + parsed.content

    return new Response(markdown, {
      headers: {
        'Content-Type': 'text/markdown; charset=utf-8',
        'Cache-Control': 'public, s-maxage=2592000, stale-while-revalidate',
        'Vary': 'Accept',
        'X-Content-Source': 'markdown',
      },
    })
  } catch (error) {
    console.error(`Error serving markdown for ${slug}:`, error)
    notFound()
  }
}

// Pre-generar todas las rutas en build time
export async function generateStaticParams() {
  const { getAllPosts } = await import('@/lib/posts')
  const posts = await getAllPosts()

  return posts.map((post) => ({
    slug: post.slug,
  }))
}
```

**Puntos clave**:

- `runtime: 'nodejs'`: Route handler corre en Node.js (necesario para fs)
- `dynamic: 'force-static'`: Pre-renderiza todas las rutas en build time
- `revalidate: 2592000`: Cache de 30 días (igual que páginas HTML)
- `Vary: Accept`: Crucial para caching correcto en CDNs
- `generateStaticParams()`: Pre-genera todas las URLs en build

### Rewrites Configuration

En `next.config.mjs`, configuramos rewrites para interceptar requests con `Accept: text/markdown`:

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return {
      beforeFiles: [
        // 1. Soporte para extensión .md explícita
        //    GET /post/mi-articulo.md → /md/post/mi-articulo
        {
          source: '/post/:slug.md',
          destination: '/md/post/:slug',
        },

        // 2. Content negotiation vía Accept header
        //    GET /post/mi-articulo + Accept: text/markdown → /md/post/mi-articulo
        {
          source: '/post/:slug',
          destination: '/md/post/:slug',
          has: [
            {
              type: 'header',
              key: 'accept',
              value: '(.*text/markdown.*)',
            },
          ],
        },
      ],
    }
  },
}

export default nextConfig
```

**Cómo funcionan los rewrites**:

- `beforeFiles`: Se ejecutan **antes** de verificar el filesystem
- Primer rewrite: URLs con `.md` siempre van al route handler
- Segundo rewrite: URLs sin `.md` van al handler **solo si** `Accept` contiene `text/markdown`
- Si no coincide: Next.js continúa a la página HTML tradicional

**Diagrama de flujo**:

```
Request: GET /post/mi-articulo
         Accept: text/markdown
    ↓
[beforeFiles rewrites]
    ↓
¿Coincide /post/:slug + Accept:text/markdown?
    ↓ YES
[Rewrite a /md/post/mi-articulo]
    ↓
[Route handler: app/md/post/[slug]/route.ts]
    ↓
[Response: text/markdown]


Request: GET /post/mi-articulo
         Accept: text/html
    ↓
[beforeFiles rewrites]
    ↓
¿Coincide /post/:slug + Accept:text/markdown?
    ↓ NO
[Continúa a filesystem]
    ↓
[Página: app/post/[slug]/page.tsx]
    ↓
[Response: text/html]
```

### Parsing Markdown Files

La función `parseMarkdownFile()` en `lib/markdown.ts`:

```typescript
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDirectory = path.join(process.cwd(), '_posts')

export async function parseMarkdownFile(slug: string) {
  const fullPath = path.join(postsDirectory, slug, 'index.md')

  // Verificar que el archivo exista
  if (!fs.existsSync(fullPath)) {
    return null
  }

  try {
    const fileContents = fs.readFileSync(fullPath, 'utf8')

    // gray-matter separa frontmatter de contenido
    const { data, content } = matter(fileContents)

    return {
      frontmatter: data as MarkdownFrontmatter,
      content: content.trim(),
      slug,
    }
  } catch (error) {
    console.error(`Failed to parse markdown for ${slug}:`, error)
    return null
  }
}

// Type definitions
export interface MarkdownFrontmatter {
  title: string
  date: string
  category: string
  excerpt?: string
  author?: {
    name: string
    picture?: string
  }
  ogImage?: {
    url: string
  }
}
```

**gray-matter** es la biblioteca estándar para parsing de frontmatter. Maneja:

- YAML frontmatter (entre `---`)
- JSON frontmatter (entre `;;;`)
- TOML frontmatter (entre `+++`)

Ejemplo de archivo markdown:

```markdown
---
title: "Mi Artículo Técnico"
date: "2026-02-13"
category: "Next.js"
excerpt: "Una breve descripción"
author:
  name: "angel cruz"
---

// Mi Artículo Técnico

Contenido del artículo aquí...

## Sección 1

Más contenido...
```

Parsing result:

```javascript
{
  frontmatter: {
    title: "Mi Artículo Técnico",
    date: "2026-02-13",
    category: "Next.js",
    excerpt: "Una breve descripción",
    author: {
      name: "angel cruz"
    }
  },
  content: "# Mi Artículo Técnico\n\nContenido del artículo aquí...",
  slug: "mi-articulo-tecnico"
}
```

### Manejo de Componentes Custom

Si tu contenido incluye componentes MDX o React, necesitas decidir cómo serializarlos para agentes de IA:

```jsx
// En tu MDX:
<CodePlayground language="javascript" initialCode="console.log('Hello')" />
```

**Opción 1: Reemplazar con markdown equivalente**

```typescript
// En route handler
const processedContent = content
  .replace(
    /<CodePlayground language="(\w+)" initialCode="([^"]+)" \/>/g,
    (_, lang, code) => `\`\`\`${lang}\n${code}\n\`\`\``
  )
```

**Opción 2: Incluir como comentario**

```markdown
<!-- Interactive CodePlayground (JavaScript) -->
```javascript
console.log('Hello')
```
<!-- End CodePlayground -->
```

**Opción 3: Anotar con metadatos**

```markdown
```javascript {interactive=true playground=true}
console.log('Hello')
```
```

Elige según tus necesidades. Lo importante es que **tú controlas** la serialización, a diferencia de edge conversion.

## Estrategia de Caché

Una ventaja clave: **la misma estrategia de caché funciona para HTML y markdown**.

### Cache Configuration

Tanto páginas HTML como route handlers markdown usan:

```typescript
export const revalidate = 2592000 // 30 días

// Headers en response
'Cache-Control': 'public, s-maxage=2592000, stale-while-revalidate'
```

Esto significa:

- **CDN/Edge cache**: 30 días
- **Stale-while-revalidate**: Si el contenido expira, sirve versión stale mientras refrescas en background
- **Revalidación on-demand**: Webhooks pueden invalidar cache manualmente

### Webhook-Based Revalidation

Cuando actualizas contenido, un webhook desde tu CMS o backend invalida ambos caches:

```typescript
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  // Verificar token de autenticación
  const token = request.headers.get('Authorization')?.replace('Bearer ', '')

  if (token !== process.env.REVALIDATE_TOKEN) {
    return NextResponse.json({ message: 'Unauthorized' }, { status: 401 })
  }

  const body = await request.json()
  const { slug, type } = body

  if (type === 'post') {
    // Revalidar cache tags
    revalidateTag(`post-${slug}`)
    revalidateTag('posts-list')

    // Revalidar paths (HTML y markdown)
    revalidatePath(`/post/${slug}`)
    revalidatePath(`/md/post/${slug}`)

    return NextResponse.json({
      revalidated: true,
      paths: [`/post/${slug}`, `/md/post/${slug}`]
    })
  }

  return NextResponse.json({ message: 'Invalid type' }, { status: 400 })
}
```

**Flujo completo**:

```
[Editor actualiza post en CMS]
    ↓
[CMS envía webhook POST /api/revalidate]
    ↓
[Endpoint valida token]
    ↓
[revalidateTag('post-slug')]  ← Invalida fetch cache
[revalidatePath('/post/slug')] ← Invalida HTML page
[revalidatePath('/md/post/slug')] ← Invalida markdown route
    ↓
[Próxima request regenera contenido]
```

### Cache Tags para fetch()

Si usas `fetch()` dentro de componentes, aprovecha cache tags:

```typescript
// En page.tsx o route.ts
const data = await fetch('https://api.example.com/posts', {
  next: {
    tags: ['posts', 'posts-list'],
    revalidate: 2592000,
  },
})
```

Luego en webhook:

```typescript
revalidateTag('posts-list')  // Invalida todas las requests con ese tag
```

### Vercel Edge Cache Invalidation

Si estás en Vercel, puedes también invalidar edge cache vía API:

```typescript
import { after } from 'next/server'

after(async () => {
  // Esto se ejecuta después de enviar response (non-blocking)
  await fetch(
    `https://api.vercel.com/v1/projects/${process.env.VERCEL_PROJECT_ID}/purge`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.VERCEL_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        paths: [`/post/${slug}`, `/md/post/${slug}`],
      }),
    }
  )
})
```

Esto purga cache en los edge nodes de Vercel, asegurando que usuarios globales reciban contenido actualizado.

## Tres Formas de Acceder al Contenido

Los agentes de IA (y usuarios) pueden acceder al markdown de tres formas:

### 1. Extensión `.md` Explícita

La forma más simple y directa:

```bash
curl https://www.angelcruz.dev/post/adminer-gestor-de-bases-de-datos-minimalista.md
```

Response:

```markdown
---
title: "Adminer: gestor de bases de datos minimalista"
date: "2024-01-15"
category: "Herramientas"
---

// Adminer: gestor de bases de datos minimalista

Adminer es una herramienta de gestión de bases de datos...
```

**Ventajas**:
- URL explícita, fácil de compartir
- No requiere headers especiales
- Funciona en navegadores (descarga el markdown)

**Uso**:
```bash
// Descargar markdown localmente
curl -O https://www.angelcruz.dev/post/mi-articulo.md

// Ver en terminal
curl https://www.angelcruz.dev/post/mi-articulo.md | less
```

### 2. Header `Accept: text/markdown`

El método estándar de content negotiation:

```bash
curl -H "Accept: text/markdown" \
  https://www.angelcruz.dev/post/adminer-gestor-de-bases-de-datos-minimalista
```

Response headers:

```
HTTP/2 200
content-type: text/markdown; charset=utf-8
cache-control: public, s-maxage=2592000, stale-while-revalidate
vary: Accept
x-content-source: markdown
```

**Ventajas**:
- URL estándar (misma que HTML)
- SEO-friendly (no duplicación de URLs)
- Método preferido por agentes de IA

**Cómo lo usan los agentes**:

```javascript
// Claude Code internamente hace:
const response = await fetch('https://www.angelcruz.dev/post/slug', {
  headers: {
    'Accept': 'text/markdown',
    'User-Agent': 'ClaudeCode/1.0',
  },
})

if (response.headers.get('content-type')?.includes('text/markdown')) {
  const markdown = await response.text()
  // Procesar markdown...
} else {
  // Fallback a HTML parsing
}
```

### 3. Descubrimiento vía Sitemap

Puedes crear un sitemap específico para markdown:

```xml
<!-- /sitemap-markdown.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://www.angelcruz.dev/post/mi-articulo.md</loc>
    <lastmod>2026-02-13</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
  <!-- Más posts... -->
</urlset>
```

Agentes de IA futuros podrían descubrir automáticamente contenido markdown via sitemap.

## Resultados Reales: Benchmarks

Estas son mediciones reales de producción en este sitio.

### Comparación de Payload

**Artículo**: "Adminer: gestor de bases de datos minimalista"

```bash
// HTML completo
curl -sL https://www.angelcruz.dev/post/adminer-gestor-de-bases-de-datos-minimalista \
  | wc -c
316270 bytes

// Markdown puro
curl -sL https://www.angelcruz.dev/post/adminer-gestor-de-bases-de-datos-minimalista.md \
  | wc -c
1338 bytes
```

**Reducción: 99.6%**

### Desglose del HTML (316KB)

```
Componente                  Tamaño      Porcentaje
─────────────────────────────────────────────────
Navigation                  ~25 KB      7.9%
Hero/Header                 ~15 KB      4.7%
Footer                      ~20 KB      6.3%
Sidebar                     ~30 KB      9.5%
CSS inlined (Tailwind)      ~80 KB      25.3%
JavaScript bundles          ~90 KB      28.5%
Meta tags + SEO             ~8 KB       2.5%
Contenido real (article)    ~30 KB      9.5%
Analytics + Scripts         ~18 KB      5.7%
─────────────────────────────────────────────────
Total                       316 KB      100%
```

**El contenido real es solo 9.5% del payload.**

### Desglose del Markdown (1.3KB)

```
Componente                  Tamaño      Porcentaje
─────────────────────────────────────────────────
Frontmatter (metadata)      ~200 bytes  15%
Contenido markdown          ~1138 bytes 85%
─────────────────────────────────────────────────
Total                       1338 bytes  100%
```

**El contenido real es 85% del payload.**

### Token Estimation

Usando el tokenizer de Claude (aproximado):

```
HTML completo:
- 316,270 bytes
- ~79,000 tokens (ratio: 4 bytes/token)
- Costo (Claude Opus): $1.185 por lectura

Markdown puro:
- 1,338 bytes
- ~335 tokens (ratio: 4 bytes/token)
- Costo (Claude Opus): $0.005 por lectura

Reducción de tokens: 99.58%
Reducción de costo: 237x
```

### Performance Impact

```
Métrica                     HTML        Markdown    Mejora
──────────────────────────────────────────────────────────
Tiempo de descarga (3G)     8.5s        0.04s       212x
Tiempo de parsing           ~150ms      ~5ms        30x
Memoria del agente          79KB        1.3KB       60x
Latencia total              8.65s       0.045s      192x
```

### Impacto en Ventana de Contexto

Asumiendo Claude Opus con ventana de 200K tokens:

```
HTML (79K tokens por artículo):
- Artículos que caben: 2-3
- Tokens restantes: ~40K (para código, output, reasoning)

Markdown (335 tokens por artículo):
- Artículos que caben: 597
- Tokens restantes: ~150K (para código, output, reasoning)
```

**El agente puede procesar 200x más contenido con markdown.**

## Agentes de IA que lo Soportan

### Soporte Actual (Febrero 2026)

**Claude Code (Anthropic)**
- Envía `Accept: text/markdown` por defecto
- Usa markdown para reducir uso de contexto
- Fallback a HTML parsing si no disponible

```bash
// Simular request de Claude Code
curl -H "Accept: text/markdown" \
     -H "User-Agent: ClaudeCode/1.0" \
     https://www.angelcruz.dev/post/slug
```

**OpenCode**
- Cliente open-source compatible con Claude API
- Implementa mismo protocolo de content negotiation

**Bun Docs**
- Primera documentación en implementar esto
- Pioneros del `Accept: text/markdown` standard

### Próximamente (Rumoreado)

**GitHub Copilot**
- Equipo de GitHub evaluando implementación
- Potencial integración en Copilot CLI
- Fecha estimada: Q2 2026

**Cursor**
- IDE con AI nativo
- Evaluando para webfetch
- Fecha estimada: Q2-Q3 2026

**Sourcegraph Cody**
- AI coding assistant
- Discusiones internas sobre soporte

### Testing con curl

Puedes simular cualquier agente:

```bash
// Claude Code
curl -H "Accept: text/markdown" \
     -H "User-Agent: ClaudeCode/1.0" \
     https://www.angelcruz.dev/post/slug

// OpenCode
curl -H "Accept: text/markdown" \
     -H "User-Agent: OpenCode/0.1" \
     https://www.angelcruz.dev/post/slug

// Generic AI Agent
curl -H "Accept: text/markdown" \
     -H "User-Agent: Mozilla/5.0 (AI Agent)" \
     https://www.angelcruz.dev/post/slug
```

## Ventajas Adicionales

Más allá de la reducción de tokens, hay beneficios adicionales.

### 1. Mejor Precisión en RAG

**RAG (Retrieval-Augmented Generation)** mejora con markdown limpio:

```
Estudio de caso: Sistema RAG con 10,000 artículos técnicos

Input: HTML completo
- Chunk size: 2000 tokens
- Chunks por artículo: ~40
- Retrieval accuracy: 62%

Input: Markdown puro
- Chunk size: 2000 tokens
- Chunks por artículo: ~2
- Retrieval accuracy: 89%

Mejora: +27 puntos porcentuales
```

¿Por qué? Porque markdown:
- No tiene ruido de navegación confundiendo embeddings
- Estructura semántica clara para vector search
- Metadata útil en frontmatter

### 2. Compatibilidad con llms.txt

El archivo `/llms.txt` es un estándar emergente para descubrimiento de contenido por AI:

```txt
// llms.txt

// Markdown posts
https://www.angelcruz.dev/post/mi-articulo.md
https://www.angelcruz.dev/post/otro-articulo.md

// Snippets
https://www.angelcruz.dev/lab/react-usedebounce-hook.md

// Categories
https://www.angelcruz.dev/categorias/nextjs.md
```

Agentes de IA pueden:
1. Leer `llms.txt`
2. Descubrir URLs markdown
3. Fetch contenido directamente (sin HTML parsing)

### 3. Sin Duplicación de Archivos

A diferencia de mantener `.html` y `.md` separados:

```
Approach incorrecto:
content/
  ├── mi-articulo.md      ← Fuente
  └── mi-articulo.html    ← Generado

Problem: Sync issues, doble storage, potencial inconsistencia
```

Con source conversion:

```
Approach correcto:
_posts/
  └── mi-articulo/
      └── index.md        ← Single source of truth

Generado on-the-fly:
- GET /post/mi-articulo         → HTML (rendered)
- GET /post/mi-articulo.md      → Markdown (raw)
```

Una sola fuente, múltiples representaciones.

### 4. Control Total sobre Serialización

Puedes customizar cómo serializar componentes complejos:

```typescript
// app/md/post/[slug]/route.ts

function serializeCustomComponents(content: string): string {
  // Convertir <Tabs> a markdown equivalente
  content = content.replace(
    /<Tabs items=\[(.*?)\]>(.*?)<\/Tabs>/gs,
    (_, items, innerContent) => {
      const tabs = JSON.parse(`[${items}]`)
      let markdown = '\n'
      tabs.forEach((tab: string, i: number) => {
        markdown += `### Tab: ${tab}\n\n`
        // Extraer contenido del tab...
      })
      return markdown
    }
  )

  // Convertir <Callout> a blockquote
  content = content.replace(
    /<Callout type="(.*?)">(.*?)<\/Callout>/gs,
    (_, type, text) => `> **${type.toUpperCase()}**: ${text}\n\n`
  )

  return content
}
```

Edge conversion (Cloudflare) **no puede hacer esto**. Tu lógica custom gana.

### 5. Faster Development Cycle

Durante desarrollo local:

```bash
// Iniciar dev server
pnpm dev

// Probar markdown endpoint
curl http://localhost:3000/post/mi-articulo.md

// Ver cambios en tiempo real (hot reload)
```

No necesitas esperar a despliegue en Cloudflare para probar.

## Comparación con Otras Soluciones

### Cloudflare Markdown for Agents

**Pros**:
- Setup instantáneo (dashboard toggle)
- Funciona con cualquier stack
- Mantenido por Cloudflare

**Cons**:
- Solo 80% reducción
- Parser genérico (pérdida de fidelidad)
- Requiere suscripción Cloudflare
- Sin control sobre serialización

**Cuándo usar**: Si necesitas solución rápida y ya usas Cloudflare.

### Firecrawl API

Servicio de "scraping inteligente" que convierte sitios a markdown:

**Pros**:
- API simple
- Maneja JavaScript rendering
- Extrae contenido estructurado

**Cons**:
- **Costoso**: $0.10-1.00 por página
- Latencia alta (~2-5 segundos)
- Límites de rate
- No es real-time

**Cuándo usar**: Para scraping de sitios externos que no controlas.

### Crawl4AI (Self-Hosted)

Librería open-source para web scraping con AI:

**Pros**:
- Gratis (self-hosted)
- Flexible y customizable
- Soporte para JavaScript

**Cons**:
- Requiere infraestructura (Docker, servidores)
- Mantenimiento necesario
- Latencia de parsing
- No es source conversion

**Cuándo usar**: Para agregar contenido de múltiples fuentes.

### Apify Scrapers

Plataforma de web scraping as a service:

**Pros**:
- Scrapers pre-configurados
- Maneja anti-bot protections
- Infraestructura escalable

**Cons**:
- Costoso a escala
- No real-time
- Enfocado en scraping, no content delivery

**Cuándo usar**: Para proyectos de data mining.

### Source Conversion (Este Enfoque)

**Pros**:
- 97% reducción (máximo)
- Gratis (built-in Next.js)
- Fidelidad perfecta
- Control total
- Real-time

**Cons**:
- Requiere implementación custom
- Solo funciona si tienes markdown fuente

**Cuándo usar**: Si usas Next.js/Astro/Hugo y tienes markdown.

## Consideraciones SEO

### ¿Afecta Content Negotiation al SEO?

**No.** Content negotiation es un estándar HTTP que Google soporta:

1. **Mismo contenido, diferente representación**: Google ve esto como equivalente a servir JSON vs XML en APIs
2. **Header `Vary: Accept` indica variaciones**: Le dice a Google que hay múltiples versiones según Accept
3. **No es cloaking**: Cloaking es servir contenido diferente intencionalmente para engañar; content negotiation es negociación explícita

### Comparación: Content Negotiation vs Cloaking

```
Content Negotiation (Permitido):
   Request: Accept: text/markdown
   Response: Markdown del mismo contenido
   Razón: Cliente pidió explícitamente ese formato

Cloaking (Penalizado):
   Request: User-Agent: Googlebot
   Response: Contenido optimizado solo para bot
   Razón: Engañar al bot mostrando algo distinto al usuario
```

### Canonical URLs

Si ofreces `.md` URLs, usa canonical:

```html
<!-- En versión HTML -->
<link rel="canonical" href="https://www.angelcruz.dev/post/mi-articulo">

<!-- En versión markdown -->
<!-- No aplicable: markdown no tiene <head> -->
```

Alternativamente, sirve markdown solo via header, no como URL separada.

### Beneficios SEO Futuros

Perplexity, You.com y Bing AI ya usan LLMs para procesar contenido web. Servir markdown reduce los tokens necesarios para entender un artículo, lo que puede mejorar la comprensión del contenido por parte de estos sistemas.

Los AI agents también pueden indexar contenido más profundamente cuando reciben markdown limpio en lugar de HTML con markup de navegación. Más contexto útil por token significa mejores respuestas y más referencias a tu sitio.

## Implementación Paso a Paso

Guía rápida para implementar en tu proyecto Next.js.

### Paso 1: Verificar Estructura de Contenido

Asegúrate de tener markdown source:

```bash
// Estructura esperada
_posts/
  ├── mi-articulo/
  │   └── index.md
  ├── otro-articulo/
  │   └── index.md
```

Si no tienes markdown, considera:
- Migrar desde CMS (WordPress, Contentful) a markdown
- O usar edge conversion (Cloudflare) en su lugar

### Paso 2: Crear Route Handler

```bash
mkdir -p app/md/post/[slug]
touch app/md/post/[slug]/route.ts
```

Contenido de `route.ts`:

```typescript
import { notFound } from 'next/navigation'
import { parseMarkdownFile } from '@/lib/markdown'

export const runtime = 'nodejs'
export const dynamic = 'force-static'
export const revalidate = 2592000

export async function GET(
  _request: Request,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params
  const parsed = await parseMarkdownFile(slug)

  if (!parsed) notFound()

  const frontmatterLines = [
    '---',
    `title: ${parsed.frontmatter.title}`,
    `date: ${parsed.frontmatter.date}`,
    '---',
    '',
  ]

  const markdown = frontmatterLines.join('\n') + parsed.content

  return new Response(markdown, {
    headers: {
      'Content-Type': 'text/markdown; charset=utf-8',
      'Cache-Control': 'public, s-maxage=2592000, stale-while-revalidate',
      'Vary': 'Accept',
    },
  })
}
```

### Paso 3: Configurar Rewrites

En `next.config.mjs`:

```javascript
export default {
  async rewrites() {
    return {
      beforeFiles: [
        {
          source: '/post/:slug.md',
          destination: '/md/post/:slug',
        },
        {
          source: '/post/:slug',
          destination: '/md/post/:slug',
          has: [
            {
              type: 'header',
              key: 'accept',
              value: '(.*text/markdown.*)',
            },
          ],
        },
      ],
    }
  },
}
```

### Paso 4: Test con curl

```bash
// Terminal 1: Iniciar dev server
pnpm dev

// Terminal 2: Probar endpoints
curl http://localhost:3000/post/mi-articulo.md

curl -H "Accept: text/markdown" \
  http://localhost:3000/post/mi-articulo
```

Deberías ver markdown puro, no HTML.

### Paso 5: Agregar a Cache Revalidation

En `app/api/revalidate/route.ts`:

```typescript
if (type === 'post') {
  revalidateTag(`post-${slug}`)
  revalidatePath(`/post/${slug}`)
  revalidatePath(`/md/post/${slug}`)  // ← Agregar esta línea
}
```

### Paso 6: (Opcional) Crear Sitemap Markdown

```typescript
// app/sitemap-markdown.xml/route.ts
export async function GET() {
  const posts = await getAllPosts()

  const urls = posts.map(post => ({
    loc: `https://www.angelcruz.dev/post/${post.slug}.md`,
    lastmod: post.date,
    changefreq: 'monthly',
    priority: 0.8,
  }))

  const xml = generateSitemapXML(urls)

  return new Response(xml, {
    headers: { 'Content-Type': 'application/xml' },
  })
}
```

### Paso 7: (Opcional) Agregar a llms.txt

```txt
// public/llms.txt

// Markdown Posts
https://www.angelcruz.dev/post/mi-articulo.md
https://www.angelcruz.dev/post/otro-articulo.md

// How to discover all posts
https://www.angelcruz.dev/sitemap-markdown.xml
```

### Paso 8: Deploy y Verificar

```bash
// Build production
pnpm build

// Deploy (Vercel)
vercel --prod

// Verificar en producción
curl -H "Accept: text/markdown" \
  https://tu-sitio.dev/post/mi-articulo
```

## FAQ

### ¿Esto afecta mi SEO normal en Google?

No. Los navegadores tradicionales reciben HTML como siempre. Solo agentes con `Accept: text/markdown` reciben markdown. Google no penaliza content negotiation legítimo.

### ¿Funciona con SSG (Static Site Generation)?

Sí. Usa `dynamic: 'force-static'` en tu route handler y `generateStaticParams()` para pre-generar todas las rutas en build time.

### ¿Funciona con ISR (Incremental Static Regeneration)?

Sí. El `revalidate` en route handler funciona igual que en pages.

### ¿Qué pasa con las imágenes en el markdown?

Las URLs de imágenes se preservan. Los agentes de IA pueden decidir si descargarlas. Ejemplo:

```markdown
![Diagrama de arquitectura](https://cdn.example.com/image.png)
```

El agente puede:
- Ignorar la imagen (solo procesar texto)
- Descargarla y analizarla (si soporta visión)

### ¿Es compatible con WordPress?

WordPress no implementa content negotiation de forma nativa. Las alternativas más comunes son:

1. **Custom endpoint**: REST API que convierte HTML → Markdown bajo demanda
2. **Usando Cloudflare**: Edge conversion si ya tienes Cloudflare delante del sitio

### ¿Vale la pena vs. Cloudflare?

**Usa Source Conversion si**:
- Ya usas Next.js + markdown
- Quieres máxima reducción (97%)
- Necesitas control total

**Usa Cloudflare si**:
- Tu contenido es HTML puro (CMS tradicional)
- Quieres setup en 5 minutos
- 80% reducción es suficiente

### ¿Cómo manejo autenticación en posts privados?

```typescript
// app/md/post/[slug]/route.ts
export async function GET(request: Request) {
  const token = request.headers.get('Authorization')

  // Validar token
  const user = await validateToken(token)
  if (!user) return new Response('Unauthorized', { status: 401 })

  // Verificar acceso al post
  const post = await getPost(slug)
  if (post.private && !user.hasPaidAccess) {
    return new Response('Forbidden', { status: 403 })
  }

  // Servir markdown
  return new Response(markdown, { headers: { ... } })
}
```

### ¿Puedo servir otros formatos (JSON, PDF)?

¡Sí! Content negotiation soporta cualquier MIME type:

```typescript
const acceptHeader = request.headers.get('Accept')

if (acceptHeader?.includes('application/json')) {
  return Response.json({ title, content, metadata })
}

if (acceptHeader?.includes('application/pdf')) {
  const pdf = await generatePDF(content)
  return new Response(pdf, {
    headers: { 'Content-Type': 'application/pdf' }
  })
}

// Default: HTML
return new Response(htmlContent)
```

### ¿Cómo monitoreo uso de markdown endpoints?

```typescript
// app/md/post/[slug]/route.ts
export async function GET(request: Request) {
  // Log analytics
  await trackEvent({
    event: 'markdown_request',
    slug,
    userAgent: request.headers.get('User-Agent'),
    referrer: request.headers.get('Referer'),
  })

  // Servir contenido...
}
```

O usa un middleware:

```typescript
// middleware.ts
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/md/')) {
    // Track markdown requests
    console.log('Markdown request:', {
      path: request.nextUrl.pathname,
      agent: request.headers.get('User-Agent'),
    })
  }
}
```

## Conclusión

El futuro de la web incluye agentes de IA como consumidores de primera clase. Así como optimizamos para navegadores móviles hace años, ahora debemos optimizar para AI agents.

**Recapitulando**:

- **El problema**: HTML páginas desperdician 80-99% de tokens en markup no semántico
- **La solución estándar**: Content negotiation vía header `Accept: text/markdown`
- **Dos enfoques**: Edge conversion (80% reducción) vs Source conversion (97% reducción)
- **Este sitio usa source conversion**: Servimos markdown directo desde `_posts/`
- **Implementación en Next.js**: Route handlers + rewrites + parsing logic
- **Resultados reales**: De 316KB a 1.3KB, de 80K tokens a 350 tokens
- **Beneficios adicionales**: Mejor RAG accuracy, control total, sin duplicación

### Pruébalo Ahora

Este sitio ya lo implementa. Prueba tú mismo:

```bash
// Cualquier artículo de este sitio
curl -H "Accept: text/markdown" \
  https://www.angelcruz.dev/post/adminer-gestor-de-bases-de-datos-minimalista

// O con extensión .md
curl https://www.angelcruz.dev/post/adminer-gestor-de-bases-de-datos-minimalista.md
```

### Implementa en tu Proyecto

Sigue la guía paso a paso en este artículo. El código completo está en:

- Route handlers: `app/md/post/[slug]/route.ts`
- Rewrites: `next.config.mjs`
- Parsing: `lib/markdown.ts`

### El estándar va a crecer

A medida que más agentes adopten `Accept: text/markdown`, los sitios que lo soporten podrán servir contenido más eficientemente:

- Search engines basados en LLM pueden procesar contenido limpio con menos tokens
- Developer tools (IDEs, CLIs) que ya envían el header se benefician de respuestas más livianas
- RAG systems indexan mejor con markdown estructurado

---

## Referencias

### Official Documentation

1. [Cloudflare: Markdown for Agents](https://blog.cloudflare.com/markdown-for-agents/) - Anuncio oficial de la feature de Cloudflare
2. [Vercel: Agent-Friendly Pages](https://vercel.com/blog/making-agent-friendly-pages-with-content-negotiation) - Guía de Vercel sobre content negotiation
3. [Next.js: Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) - Documentación oficial de route handlers
4. [Next.js: Rewrites](https://nextjs.org/docs/app/api-reference/next-config-js/rewrites) - Documentación de rewrites en Next.js
5. [Next.js: Caching](https://nextjs.org/docs/app/building-your-application/caching) - Sistema de caché en App Router

### HTTP Standards & Specs

6. [RFC 9110: HTTP Semantics - Content Negotiation](https://www.rfc-editor.org/rfc/rfc9110.html#name-content-negotiation) - Especificación oficial de HTTP
7. [MDN: HTTP Content Negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) - Guía de MDN sobre content negotiation
8. [MDN: Accept Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) - Documentación del header Accept
9. [MDN: Vary Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) - Documentación del header Vary
10. [IANA Media Types](https://www.iana.org/assignments/media-types/text/markdown) - Registro oficial de text/markdown

### Technical Implementations

11. [Bun: Markdown Content Negotiation](https://bun.sh/blog/markdown-for-agents) - Primera implementación de Bun
12. [Sanity.io: Portable Text to Markdown](https://www.sanity.io/docs/presenting-block-text) - Conversión de contenido estructurado
13. [gray-matter GitHub](https://github.com/jonschlinkert/gray-matter) - Parsing de frontmatter YAML
14. [unified GitHub](https://github.com/unifiedjs/unified) - Pipeline de procesamiento de markdown
15. [Shiki Documentation](https://shiki.style/) - Syntax highlighter usado en este sitio

### Research & Analysis

16. [Anthropic: Claude Code CLI](https://www.anthropic.com/news/claude-code) - Documentación de Claude Code
17. [Token Reduction Benchmarks](https://blog.cloudflare.com/markdown-for-agents/#token-reduction) - Mediciones de Cloudflare
18. [LLM Token Economics 2026](https://openai.com/pricing) - Precios actuales de APIs
19. [RAG with Clean Text](https://www.pinecone.io/learn/retrieval-augmented-generation/) - RAG best practices
20. [Web Scraping vs Source Conversion](https://www.zenrows.com/blog/web-scraping-vs-api) - Comparación de enfoques

### Tools & Libraries

21. [Firecrawl API](https://www.firecrawl.dev/) - Servicio de HTML to Markdown
22. [Crawl4AI GitHub](https://github.com/unclecode/crawl4ai) - Self-hosted scraping
23. [Turndown GitHub](https://github.com/mixmark-io/turndown) - HTML to Markdown converter
24. [remark GitHub](https://github.com/remarkjs/remark) - Markdown processor
25. [rehype GitHub](https://github.com/rehypejs/rehype) - HTML processor

### Community & Discussions

26. [Reddit: r/nextjs - Content Negotiation](https://www.reddit.com/r/nextjs/) - Discusiones de la comunidad
27. [Hacker News: Cloudflare Markdown Announcement](https://news.ycombinator.com/) - Reacciones y debates
28. [GitHub: Claude Code Issues](https://github.com/anthropics/claude-code/issues) - Discusiones técnicas
29. [Next.js Discord: #help Channel](https://nextjs.org/discord) - Soporte de la comunidad

### SEO & Standards

30. [Google: Content Negotiation Best Practices](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls) - Guía de Google sobre URLs duplicadas
31. [llms.txt Spec](https://llmstxt.org/) - Estándar emergente para descubrimiento AI
32. [Schema.org: Article](https://schema.org/Article) - Structured data para artículos

---

## Sitemap

Índice completo del sitio: [/sitemap.md](https://angelcruz.dev/sitemap.md)

Canónico HTML: [https://angelcruz.dev/post/content-negotiation-agentes-ia](https://angelcruz.dev/post/content-negotiation-agentes-ia)
