SEO para apps React SPA: ¿prerenderizado, SSR o ambos?
Por qué una SPA en React posiciona mal, cómo procesa Google el JavaScript y cuándo conviene prerenderizar, renderizar en el servidor o mejorar los metadatos.
Read in English →
Una app de React de una sola página (SPA) puede posicionar en Google, pero solo si cada página pública devuelve HTML real con su propio título, descripción y contenido, ya sea prerenderizando esas páginas al compilar o renderizándolas en el servidor. Google sí ejecuta JavaScript, pero lo hace en una segunda pasada, y muchos otros rastreadores y herramientas de IA no lo ejecutan nunca. Si tu app manda el mismo <div id="root"> vacío para todas las URL, estás dejando todo al azar.
En esta guía verás qué ve realmente un rastreador en una SPA típica hecha con Vite, Lovable, Bolt o herramientas parecidas, cómo elegir entre prerenderizado y renderizado en servidor, y cómo comprobar el resultado.
¿Por qué mi app de React no aparece bien en Google?
Abre tu sitio publicado, haz clic derecho y elige "Ver código fuente de la página" (no el panel de Elementos, que muestra la página después de ejecutar JavaScript). En una app de React renderizada en el cliente casi siempre vas a encontrar:
- Un solo
<title>, igual en todas las páginas, muchas veces con el nombre del proyecto. - Ninguna meta descripción, o la misma en todas partes.
- Un elemento raíz vacío y una etiqueta de script. Sin encabezados, sin texto, sin enlaces.
La documentación de Google describe tres fases para las páginas con JavaScript: rastreo, renderizado e indexación. Las páginas que responden con estado 200 entran a una cola de renderizado, que puede tardar segundos o bastante más. Mientras tanto, Google solo tiene ese cascarón vacío. Las vistas previas al compartir un enlace en WhatsApp, Slack o LinkedIn suelen leer solo el HTML original, así que muestran el mismo título genérico en todas las páginas. Lo mismo pasa con los rastreadores de muchos buscadores con IA.
Otras causas frecuentes en apps hechas con IA:
- Rutas con almohadilla como
/#/precios. Google dice que no puede resolver bien las URL con fragmento; usa rutas reales con la History API (elBrowserRouterde React Router lo hace). - Navegación que no es un enlace. Google sigue elementos
<a href="...">. UndivcononClickque llama anavigate()es invisible para el rastreador. - Errores 404 suaves. Una SPA responde 200 a cualquier URL, incluso a las que no existen, y Google puede indexar pantallas de "Página no encontrada" como si fueran reales.
- Un
noindexolvidado del entorno de pruebas en el HTML base. Google puede saltarse el renderizado de una página que empieza connoindex, así que quitarlo luego con un script no sirve.
¿Qué opciones tengo?
| Enfoque | Qué significa | Sirve para | Esfuerzo |
|---|---|---|---|
| Solo metadatos por página | Cada ruta define título y descripción en el cliente | Apps donde solo importa la portada | Bajo, pero sigue dependiendo del renderizado |
| Prerenderizado (generación estática) | Se crea un HTML real por cada ruta pública al compilar | Páginas de marketing, blog, documentación, precios | Bajo a medio |
| Renderizado en servidor (SSR) | El servidor arma el HTML en cada petición | Muchas URL con datos (listados, perfiles públicos) | Medio a alto, necesita servidor |
| Renderizado dinámico | Se sirve una copia prerenderizada solo a los bots | Montajes heredados | Google lo llama una solución provisional, no a largo plazo |
Para la mayoría, la respuesta honesta es: prerenderiza las páginas públicas y deja la parte con sesión iniciada como una SPA normal. El panel detrás del login no necesita aparecer en Google.
¿Cómo pongo títulos y descripciones distintos en cada página?
Aunque prerenderices, cada ruta necesita sus propios metadatos. Desde React 19 puedes renderizar <title> y <meta> dentro de cualquier componente y React los mueve al <head>:
export default function PaginaPrecios() {
return (
<>
<title>Precios | Acme</title>
<meta name="description" content="Planes, límites y qué incluye cada uno." />
<h1>Precios</h1>
</>
);
}
Según la documentación de React, debe haber un solo <title> a la vez y su contenido debe ser una única cadena de texto. En versiones anteriores, react-helmet-async cumple la misma función.
Agrega también una etiqueta canonical que apunte a la propia página, y ponla en el HTML en lugar de cambiarla luego con JavaScript. Google indica que una canonical puesta por script debería coincidir con la original.
¿Cómo prerenderizo una app de Vite o de React Router?
Si usas React Router en modo framework
React Router tiene la opción prerender en react-router.config.ts. Puedes mantener la app como SPA estática (ssr: false) y aun así generar HTML real para las rutas que elijas:
import type { Config } from "@react-router/dev/config";
export default {
ssr: false,
prerender: ["/", "/precios", "/nosotros", "/blog"],
} satisfies Config;
La compilación escribe un archivo HTML por cada ruta de la lista. Las demás URL caen en el cascarón de la SPA, que tu hosting debe servir para las rutas desconocidas.
Si tienes una app de Vite con BrowserRouter
Tienes tres caminos realistas:
- Agregar un paso de prerenderizado que abra cada ruta pública en un navegador sin interfaz al compilar y guarde el HTML resultante. Hay varios plugins de Vite que lo hacen. Es el cambio más chico; ojo con los componentes que leen
windowal renderizar, porque rompen la compilación. - Llevar las páginas de marketing a un sitio estático (Astro, o un proyecto aparte de Next.js) en el dominio principal, y servir la app desde
/appo un subdominio. Suele ser más limpio que forzar el SEO en una app que no se pensó para eso. - Migrar a un framework con renderizado en servidor, como Next.js o React Router en modo framework. Es el cambio más grande; conviene si tienes muchas URL públicas con datos.
¿Y los hostings que reescriben todo a index.html?
Casi todos los hostings de SPA mandan cualquier ruta a index.html. Asegúrate de que los archivos prerenderizados se sirvan primero. Devuelve además un 404 real para rutas desconocidas o, como mínimo, agrega <meta name="robots" content="noindex"> en tu pantalla de no encontrado.
¿Cómo compruebo que funcionó?
- Mira el código fuente de tres o cuatro URL públicas. Deberías ver en el HTML original el título, la descripción, el
<h1>y el texto propios de cada página. - Ejecuta
curl -s https://tudominio.com/precios | grep -i "<title>". Si el resultado cambia en cada página, los rastreadores que no ejecutan JavaScript también lo verán. - Usa la herramienta de inspección de URLs de Google Search Console y revisa la página rastreada y su HTML renderizado.
- Pega un enlace en una app de mensajería y confirma que la vista previa muestra el título de esa página.
- Revisa el sitemap. Debe listar cada URL pública con su ruta real, y nada de lo que está detrás del login.
Si además quieres resultados enriquecidos, lee nuestra guía de datos estructurados para tu SaaS. La velocidad también cuenta: un paquete de JavaScript de 2 MB retrasa todo, y eso lo tratamos en Core Web Vitals en tu app. Si tu app está hecha en Lovable, mira por qué Google no indexa tu app de Lovable.
Respuestas rápidas
¿Google indexa apps de React sin SSR? Muchas veces sí, porque Googlebot renderiza JavaScript. Pero ocurre después, puede fallar si hay errores, y otros rastreadores y las vistas previas de enlaces normalmente no ejecutan JavaScript.
¿Prerenderizar es hacer cloaking? No. El prerenderizado sirve el mismo HTML a todos. El cloaking es mostrar a los rastreadores un contenido distinto del que ven las personas, y va contra las políticas de Google.
¿Necesito SSR para el panel con sesión iniciada? No. Las páginas detrás del login no deberían indexarse. Déjalas renderizadas en el cliente y márcalas con noindex o bloquéalas en robots.txt.
¿Un sitemap arregla el SEO de una SPA? Ayuda a que Google encuentre las URL, pero no les da contenido. No reemplaza al prerenderizado.
¿Te damos una mano?
Prerenderizar una app existente implica cambios pequeños en muchos lugares. Si prefieres no hacerlo solo, nuestro equipo de desarrollo de software a medida puede configurarlo y el mantenimiento de software lo mantiene funcionando a medida que la app cambia. La revisión gratuita de tu app muestra lo que se ve desde afuera hoy, o escríbenos con tu URL.
La explicación oficial de Google está en los conceptos básicos de SEO con JavaScript.


