Cloudflare acaba de anunciar una función que reduce el uso de tokens en agentes de IA en un 80%. Suena impresionante, ¿verdad? Pero hay un enfoque que logra 97% de reducción y te da control total sobre la implementación. Y 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:
- No renderizan visualmente: Las clases CSS y estilos inline no aportan información útil
- No ejecutan JavaScript: Los scripts son texto incomprensible
- Necesitan estructura semántica: Markdown proporciona jerarquía clara (
#,##, listas, código) - Ventana de contexto limitada: Cada token cuenta cuando tienes límites de 200K o 500K tokens
- 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:
- SEO risk: Google penaliza el “cloaking” (servir contenido diferente según user-agent)
- Frágil: Cada nuevo agente requiere actualizar la lista
- No estándar: Viola las mejores prácticas HTTP
- 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:
- Request llega a Cloudflare edge con
Accept: text/markdown - Cloudflare hace fetch del HTML desde tu servidor de origen
- Parser genérico convierte HTML → Markdown usando heurísticas
- Resultado se cachea en el edge
- 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:
- Almacenas contenido en markdown (ej:
_posts/{slug}/index.md) - Request llega con
Accept: text/markdown - Lees el archivo markdown directamente (sin parsing HTML)
- Respondes con markdown + frontmatter tal como está almacenado
- 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:
- Route handlers para servir markdown
- Rewrites en
next.config.mjspara content negotiation - 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 timerevalidate: 2592000: Cache de 30 días (igual que páginas HTML)Vary: Accept: Crucial para caching correcto en CDNsgenerateStaticParams(): 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
.mdsiempre van al route handler - Segundo rewrite: URLs sin
.mdvan al handler solo siAcceptcontienetext/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: "ángel"
---
# 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: "ángel"
}
},
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/markdownpor 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/markdownstandard
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:
- Leer
llms.txt - Descubrir URLs markdown
- 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:
- Mismo contenido, diferente representación: Google ve esto como equivalente a servir JSON vs XML en APIs
- Header
Vary: Acceptindica variaciones: Le dice a Google que hay múltiples versiones según Accept - 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
LLM-based Search Engines:
- Perplexity, You.com, Bing AI ya usan LLMs
- Markdown facilita mejor comprensión del contenido
- Potencial factor de ranking: “AI-friendliness”
Content Indexing:
- AI agents pueden indexar contenido más profundamente
- Mejor contexto = mejor respuestas = más referencias a tu sitio
Future-Proofing:
- La web está evolucionando hacia consumo AI
- Sites AI-friendly tendrán ventaja competitiva
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:

El agente puede:
- Ignorar la imagen (solo procesar texto)
- Descargarla y analizarla (si soporta visión)
¿Es compatible con WordPress?
WordPress no usa markdown nativamente, pero puedes:
- Usar plugin Markdown: Convertir posts a markdown storage
- Custom endpoint: REST API que convierte HTML → Markdown
- O usar Cloudflare: Edge conversion es más fácil para WordPress
¿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 Futuro es AI-Friendly
En 12 meses, esto será estándar. Sites que no soporten Accept: text/markdown estarán en desventaja:
- Search engines basados en LLM preferirán contenido limpio
- Developer tools (IDEs, CLIs) consumirán markdown automáticamente
- RAG systems indexarán mejor con markdown estructurado
Los early adopters capturan la ventaja. Implementa content negotiation hoy.
Comparte y Contribuye
Si este artículo te resultó útil:
- Comparte en redes (X/Twitter, LinkedIn, Reddit)
- Implementa en tu sitio y comparte resultados
- Contribuye al estándar emergente
La web AI-friendly empieza con desarrolladores como tú.
Recursos Adicionales:
Próximamente: Tutorial en video implementando content negotiation desde cero en 10 minutos.
Referencias
Official Documentation
- Cloudflare: Markdown for Agents - Anuncio oficial de la feature de Cloudflare
- Vercel: Agent-Friendly Pages - Guía de Vercel sobre content negotiation
- Next.js: Route Handlers - Documentación oficial de route handlers
- Next.js: Rewrites - Documentación de rewrites en Next.js
- Next.js: Caching - Sistema de caché en App Router
HTTP Standards & Specs
- RFC 9110: HTTP Semantics - Content Negotiation - Especificación oficial de HTTP
- MDN: HTTP Content Negotiation - Guía de MDN sobre content negotiation
- MDN: Accept Header - Documentación del header Accept
- MDN: Vary Header - Documentación del header Vary
- IANA Media Types - Registro oficial de text/markdown
Technical Implementations
- Bun: Markdown Content Negotiation - Primera implementación de Bun
- Sanity.io: Portable Text to Markdown - Conversión de contenido estructurado
- gray-matter GitHub - Parsing de frontmatter YAML
- unified GitHub - Pipeline de procesamiento de markdown
- Shiki Documentation - Syntax highlighter usado en este sitio
Research & Analysis
- Anthropic: Claude Code CLI - Documentación de Claude Code
- Token Reduction Benchmarks - Mediciones de Cloudflare
- LLM Token Economics 2026 - Precios actuales de APIs
- RAG with Clean Text - RAG best practices
- Web Scraping vs Source Conversion - Comparación de enfoques
Tools & Libraries
- Firecrawl API - Servicio de HTML to Markdown
- Crawl4AI GitHub - Self-hosted scraping
- Turndown GitHub - HTML to Markdown converter
- remark GitHub - Markdown processor
- rehype GitHub - HTML processor
Community & Discussions
- Reddit: r/nextjs - Content Negotiation - Discusiones de la comunidad
- Hacker News: Cloudflare Markdown Announcement - Reacciones y debates
- GitHub: Claude Code Issues - Discusiones técnicas
- Next.js Discord: #help Channel - Soporte de la comunidad
SEO & Standards
- Google: Content Negotiation Best Practices - Guía de Google sobre URLs duplicadas
- llms.txt Spec - Estándar emergente para descubrimiento AI
- Schema.org: Article - Structured data para artículos

