Next.js

Content Negotiation para Agentes de IA: De 316KB a 1.3KB (Reducción del 99.6%)

Autorangel cruz
Publicado
Lectura17 min de lectura
Actualizado
Content Negotiation para Agentes de IA: De 316KB a 1.3KB (Reducción del 99.6%)

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:

// 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:

<!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:

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

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

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:

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:

// 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:

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

Cloudflare lo ve como HTML:

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

Conversión resultante:

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:

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:

/** @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:

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:

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

{
  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:

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

Opción 1: Reemplazar con markdown equivalente

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

Opción 2: Incluir como comentario

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

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

// 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:

// 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:

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:

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:

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

Response:

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

// 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:

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:

// 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:

<!-- /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"

// 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
// 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:

// 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:

// 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:

// 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:

// 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:

<!-- 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:

// 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

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

Contenido de route.ts:

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:

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

// 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:

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

Paso 6: (Opcional) Crear Sitemap Markdown

// 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

// 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

// 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:

![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?

// 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:

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?

// 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:

// 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:

// 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 - Anuncio oficial de la feature de Cloudflare
  2. Vercel: Agent-Friendly Pages - Guía de Vercel sobre content negotiation
  3. Next.js: Route Handlers - Documentación oficial de route handlers
  4. Next.js: Rewrites - Documentación de rewrites en Next.js
  5. Next.js: Caching - Sistema de caché en App Router

HTTP Standards & Specs

  1. RFC 9110: HTTP Semantics - Content Negotiation - Especificación oficial de HTTP
  2. MDN: HTTP Content Negotiation - Guía de MDN sobre content negotiation
  3. MDN: Accept Header - Documentación del header Accept
  4. MDN: Vary Header - Documentación del header Vary
  5. IANA Media Types - Registro oficial de text/markdown

Technical Implementations

  1. Bun: Markdown Content Negotiation - Primera implementación de Bun
  2. Sanity.io: Portable Text to Markdown - Conversión de contenido estructurado
  3. gray-matter GitHub - Parsing de frontmatter YAML
  4. unified GitHub - Pipeline de procesamiento de markdown
  5. Shiki Documentation - Syntax highlighter usado en este sitio

Research & Analysis

  1. Anthropic: Claude Code CLI - Documentación de Claude Code
  2. Token Reduction Benchmarks - Mediciones de Cloudflare
  3. LLM Token Economics 2026 - Precios actuales de APIs
  4. RAG with Clean Text - RAG best practices
  5. Web Scraping vs Source Conversion - Comparación de enfoques

Tools & Libraries

  1. Firecrawl API - Servicio de HTML to Markdown
  2. Crawl4AI GitHub - Self-hosted scraping
  3. Turndown GitHub - HTML to Markdown converter
  4. remark GitHub - Markdown processor
  5. rehype GitHub - HTML processor

Community & Discussions

  1. Reddit: r/nextjs - Content Negotiation - Discusiones de la comunidad
  2. Hacker News: Cloudflare Markdown Announcement - Reacciones y debates
  3. GitHub: Claude Code Issues - Discusiones técnicas
  4. Next.js Discord: #help Channel - Soporte de la comunidad

SEO & Standards

  1. Google: Content Negotiation Best Practices - Guía de Google sobre URLs duplicadas
  2. llms.txt Spec - Estándar emergente para descubrimiento AI
  3. Schema.org: Article - Structured data para artículos
Bio
Angel Cruz

Desarrollador web full-stack enfocado en React, buenas prácticas y código abierto. Apasionado por construir productos útiles y compartir lo aprendido en el camino.