¿Qué significa "renderizado por JavaScript"?
Una página está renderizada por JavaScript cuando el HTML que envía el servidor está esencialmente vacío — el contenido real (encabezados, prosa, información del producto) solo aparece después de que el navegador descarga y ejecuta un bundle de JavaScript que monta un framework como React, Vue, Svelte o Angular en un elemento raíz vacío.
Una homepage típica renderizada por JS se ve así en la red:
<body>
<div id="root"></div>
<script type="module" src="/assets/index-D9LVtTP6.js"></script>
</body>
El contenido visible del <body> es de cero bytes. Todo lo que ves en el navegador fue construido en el cliente después de que se ejecutó el JS.
Por qué importa para los agentes
La mayoría de los rastreadores de IA no ejecutan JavaScript. Cuando ChatGPT, Claude, Perplexity, los fetches de Gemini o un framework de agentes típico acceden a tu URL, reciben el HTML crudo — y tu homepage está en blanco. La descripción de tu producto, encabezados, enlaces y prosa son invisibles para ellos.
Este es un daño real para la recuperación y resumen de información. Un LLM al que le pregunten "¿qué hace esta empresa?" mientras cita tu URL producirá una respuesta pobre o alucinará, porque no hay nada en la página que leer.
Lo que esto no rompe
Las etiquetas que se emiten en el <head> estático de tu HTML no se ven afectadas por el renderizado del lado del cliente. Incluso una SPA totalmente renderizada por JS puede pasar estas comprobaciones si la herramienta de build pobla el head correctamente:
- Datos estructurados JSON-LD (
<script type="application/ld+json">) - OpenGraph (
og:image,og:title, etc.) - Metadatos de Twitter Card
- URL canónica
- Favicons y etiquetas
<link rel="alternate">
Si tu sitio está renderizado por JS y también fallan estas comprobaciones, son brechas independientes — arréglalas añadiendo las etiquetas <head> correctas a la plantilla HTML de tu build, sin importar si arreglas el renderizado en sí.
La causa raíz compartida: bundles de SPA en hosting estático
Los sitios renderizados por JS suelen distribuirse como un bundle estático (un index.html y una carpeta de JS/CSS) desplegado en un host que sirve index.html para cualquier petición GET: Vercel estático, Netlify, Cloudflare Pages, GitHub Pages, S3 + CloudFront, Fastly Frontend, Firebase Hosting.
Ese patrón de despliegue no tiene ninguna lógica del lado del servidor por petición. La misma configuración que produce un cuerpo renderizado por JS también produce:
- El mismo HTML devuelto para cada cabecera
Accept(sin negociación de contenido) - HTML 200 para rutas desconocidas en lugar de 404 JSON estructurados
- Sin cabeceras
Vary,Cache-Controlo rate-limit afinadas para agentes - Sin forma de variar la respuesta por User-Agent
Arreglar un síntoma (p. ej. la negociación de contenido) sin abordar el modelo de despliegue suele ser imposible — necesitas lógica del lado del servidor o una edge function delante del bundle estático.
Escalera de soluciones
Elige la opción más barata que resuelva tu caso.
1. Prerenderizado en tiempo de build
Si el contenido de tu homepage es mayormente estático, prerenderízalo durante el build. El navegador sigue hidratando con React/Vue/etc., pero el HTML inicial que envía el servidor ya contiene el DOM renderizado. Los rastreadores ven contenido real.
- Vite + react-snap o vite-plugin-prerender
- Astro (
output: 'static') — los componentes renderizados por JS se renderizan a HTML en tiempo de build - Next.js con
output: 'export'ogetStaticProps - Nuxt con
nuxt generate
Es el arreglo de menor esfuerzo para páginas de marketing, blogs y documentación.
2. Renderizado del lado del servidor (SSR) completo
Si la homepage necesita datos dinámicos (estado autenticado, A/B tests, contenido fresco), usa un framework que renderice en cada petición:
- Next.js (App Router por defecto con React Server Components)
- Remix
- SvelteKit
- Nuxt en modo SSR
SSR requiere un runtime (Node, Bun, Deno o un edge runtime), lo que significa dejar el hosting puramente estático.
3. Edge function para negociación de contenido
Si migrar a SSR es demasiado disruptivo, pon una edge function delante de tu bundle estático que intercepte peticiones y sirva una representación alternativa:
// Cloudflare Worker / Vercel Edge / Netlify Edge
export default async function (request) {
const accept = request.headers.get('accept') || '';
const ua = (request.headers.get('user-agent') || '').toLowerCase();
const isAgent = /claudebot|gptbot|chatgpt-user|perplexitybot|google-extended/.test(ua);
if (isAgent || accept.includes('text/markdown')) {
return new Response(await fetchMarkdownSummary(), {
headers: { 'Content-Type': 'text/markdown', 'Vary': 'Accept, User-Agent' },
});
}
return fetch(request); // pasa al bundle estático
}
Es el arreglo adecuado cuando la app subyacente debe seguir renderizándose en el cliente (p. ej. una app interactiva pesada), pero aun así quieres que los agentes lean contenido significativo.
4. Fallback con <noscript> (mínimo viable)
Como mínimo, incluye tu contenido clave dentro de un bloque <noscript> en el HTML estático:
<noscript>
<h1>Tu Empresa</h1>
<p>Lo que haces, en dos frases. Enlaza a docs, precios, API.</p>
<a href="/about">Acerca de</a> · <a href="/docs">Docs</a> · <a href="/pricing">Precios</a>
</noscript>
No sustituye al prerenderizado, pero evita que los rastreadores vean una página completamente vacía. La mayoría de sitios renderizados por JS no tienen esto.
Cómo verificar
Solicita tu sitio sin ejecutar JavaScript y comprueba si hay contenido real en el cuerpo:
curl -s -A "ClaudeBot/1.0" https://tudominio.com/ | \
python3 -c "import sys, re; t = sys.stdin.read(); body = re.search(r'<body[^>]*>(.*?)</body>', t, re.S); print(len(re.sub(r'<[^>]+>', '', body.group(1) if body else '').strip()))"
Si el número impreso es menor de unas pocas centenas de caracteres, los agentes no están viendo tu contenido.