SEO en React: qué ve Google en una SPA y cómo arreglarlo

SEO en React: qué ve Google en una SPA y cómo arreglarlo

Sí, una single-page application hecha con React puede posicionar en Google, pero no lo hace por accidente. Un proyecto creado con Vite o con Create React App entrega, en el primer byte, un documento HTML casi vacío: un div con id "root" y una etiqueta de script. Todo lo que un lector -y un rastreador- necesita ver se construye después, en el navegador, ejecutando JavaScript. Eso funciona perfectamente para las personas; para un motor de búsqueda añade una capa de trabajo extra, y ahí es donde la mayoría de las SPA pierden visibilidad sin que nadie del equipo lo note hasta que revisan Search Console.

Este artículo no asume que vayas a migrar a Next.js ni a ningún otro framework con renderizado en servidor integrado. Parte de que tu stack ya es React con Vite o Create React App y de que quieres seguir así: qué hace realmente Google con una SPA, dónde se rompe el SEO -las URLs del router y las etiquetas por página- y qué opciones existen para arreglarlo sin reescribir la aplicación desde cero.

¿Puede una SPA hecha con React posicionar en Google?

Google indexa JavaScript desde hace años mediante un proceso conocido como indexación en dos oleadas. En la primera, Googlebot descarga el HTML inicial y extrae lo que puede: enlaces, texto ya presente, etiquetas meta. Si tu SPA no renderiza nada del lado del servidor, esa primera oleada ve básicamente un documento vacío. La segunda llega después -minutos o días- cuando el contenido pasa por el Web Rendering Service de Google, un renderizador basado en Chromium que ejecuta tu JavaScript como lo haría un navegador real.

El problema no es que Google no pueda ejecutar React. Puede. El problema es que ese renderizado es costoso de escalar, así que Google lo pone en cola y le asigna un presupuesto de recursos limitado por sitio. Si tu aplicación tarda en hidratarse, encadena llamadas a una API antes de mostrar contenido, o depende de una interacción del usuario para cargar texto, hay una posibilidad real de que el renderizador se rinda antes de ver lo que ves en tu propio navegador. La consecuencia no es una penalización: es que Google indexa menos de lo que crees, sin avisarte con un error claro.

  • Primera oleada: Google lee solo el HTML crudo que devuelve el servidor.
  • Segunda oleada: Google ejecuta el JavaScript y vuelve a leer el DOM resultante.
  • Entre ambas oleadas puede pasar un tiempo variable; no hay un plazo garantizado.
  • El presupuesto de renderizado es finito y se reparte entre todas las páginas del sitio.

Qué hace Googlebot cuando visita tu aplicación

Cuando Googlebot solicita la URL de tu SPA, tu servidor o tu CDN responde con el index.html que generó Vite o Create React App en el build. Ese archivo suele contener un title genérico, ninguna meta description específica, y un div vacío donde React montará todo. Si inspeccionas ese HTML con curl en lugar de un navegador, verás exactamente eso: nada de contenido real. Esa es la vista de cualquier bot que no ejecute JavaScript -bots de redes sociales, lectores de feeds, ciertos crawlers de SEO- y no todos los que importan a tu tráfico lo hacen.

Para Google, la herramienta correcta para saber qué ve es la inspección de URL en Search Console, pidiendo el HTML renderizado además de la captura de pantalla: compara el HTML que devuelve tu servidor con el que Google dice haber renderizado. Si coinciden con lo que ve un usuario real, tu SPA se indexa correctamente. Si al renderizado de Google le falta contenido, tienes un problema de presupuesto, de tiempo, o de dependencias que solo se disparan con interacción -el tipo de brecha que las siguientes secciones ayudan a cerrar.

El problema de las URLs: rutas que no existen para el rastreador

React Router, por defecto, gestiona la navegación en el cliente sin recargar la página. Eso es bueno para la experiencia de usuario, pero introduce dos fallos típicos de SEO si no configuras nada más. El primero es HashRouter: una URL con una almohadilla seguida de la ruta interna no es una ruta distinta para Google, es la misma URL con un fragmento que el servidor nunca ve, porque el navegador no envía lo que va tras la almohadilla en la petición HTTP. Con hash, cada pantalla de tu SPA es, a efectos de indexación, la misma home: todo el contenido compite por la misma URL en el índice, o no se indexa por separado.

El segundo fallo aparece incluso con BrowserRouter y rutas limpias: si tu servidor no devuelve index.html para cualquier ruta que no coincide con un archivo físico, una recarga directa en esa URL devuelve un 404 real. Googlebot hace peticiones directas a cada URL que descubre, no clics dentro de la SPA, así que una ruta que funciona perfecto navegando desde la home puede devolver 404 y ser tratada como inexistente.

  • Con HashRouter, cambia a BrowserRouter (History API) para tener URLs reales y distintas por ruta.
  • Configura un fallback de servidor -try_files en Nginx, rewrite en Apache, la reescritura para SPA en Vercel o Netlify- para que cualquier ruta desconocida sirva index.html con 200, no 404.
  • Verifica cada ruta importante con una recarga dura, no solo navegando con clics desde la home.
  • Usa etiquetas de ancla reales, o el componente Link de React Router, nunca un onClick sobre un div: Google sigue enlaces, no manejadores de eventos.

Cada ruta necesita su propio title y meta description

Un build típico de Vite o Create React App genera un único index.html, con un único title y una única meta description en el head. Como todas las rutas cargan ese mismo archivo, sin trabajo adicional cada URL del sitio comparte el mismo título y la misma descripción en los resultados de búsqueda, sin importar si el usuario está en la home o en la ficha de un producto concreto. Eso desperdicia una de las señales más directas para diferenciar cada página ante quien decide en qué resultado hacer clic.

La solución no requiere servidor: se resuelve manipulando el head desde el propio React, por ruta. Una librería como react-helmet-async -mantenida, a diferencia del react-helmet original- deja declarar el title, la meta description y las etiquetas Open Graph dentro del componente de cada página, y las inyecta en el head real al montarse. Sin dependencias nuevas, el mismo resultado se logra con un hook useEffect que actualiza document.title y el atributo content de la meta description en cada cambio de ruta. Lo que importa es el resultado medible: si el HTML renderizado de dos rutas distintas muestra el mismo title, esto no está resuelto todavía.

Prerenderizado: generar HTML estático sin cambiar de framework

El prerenderizado ataca el problema desde otro ángulo: en lugar de esperar a que Google ejecute tu JavaScript, generas tú mismo el HTML final -con contenido, title y meta ya resueltos- en el build, y lo guardas como archivo estático por ruta. El primer byte que recibe cualquier bot ya contiene el contenido real, sin depender de una segunda oleada de renderizado. No requiere un servidor Node en producción: el resultado son archivos HTML planos que sirve cualquier CDN o hosting estático, el mismo despliegue que ya usas para una SPA.

Para un proyecto Vite, un plugin como vite-plugin-ssg, o un script que recorre tus rutas con Puppeteer o Playwright tras el build, son los caminos habituales: visita cada URL en un navegador headless, espera a que React termine de renderizar, y guarda el DOM como HTML estático. Para Create React App, la herramienta histórica es react-snap, con el mismo enfoque, aunque su mantenimiento es irregular. El límite del prerenderizado es que solo sirve para contenido que existe en el momento del build: si tus páginas muestran datos por usuario, lo prerenderizado es una foto fija, útil para SEO pero no sustituto de la versión interactiva tras la hidratación.

  • El prerenderizado no reemplaza tu SPA: genera una versión estática que Google indexa mientras React sigue hidratando para el usuario real.
  • Funciona mejor cuanto más estable es el contenido entre builds: páginas de producto, artículos, landing pages.
  • Cada ruta nueva debe añadirse a la lista de rutas a prerenderizar; es fácil olvidarla y dejar una página sin versión estática.
  • Revisa el HTML generado, no solo que el proceso termine sin error: un timeout silencioso puede dejar una página prerenderizada vacía.

SSR con Vite sin migrar a Next.js

Si el contenido cambia demasiado para que el prerenderizado baste, o quieres que cada visita reciba siempre HTML actualizado, la alternativa es el SSR real, y no requiere adoptar Next.js. Vite tiene soporte de SSR de primera clase en su propio núcleo: su guía oficial describe cómo compilar un paquete de cliente y uno de servidor desde el mismo código React, y montar un servidor Express o Node mínimo que ejecuta renderToString -o renderToPipeableStream para streaming- en cada petición.

Esto es más trabajo que instalar un plugin: implica mantener un proceso Node en producción y resolver la hidratación para que el HTML del servidor coincida con lo que React monta en el cliente -un desajuste de hidratación es un error clásico de SSR. Esa complejidad se justifica cuando el prerenderizado por build ya no alcanza: contenido generado por usuarios, o catálogos que cambian varias veces al día. Migrar el renderizado dentro de tu propio stack de Vite suele costar menos que reescribir la aplicación en otro framework, porque el código de los componentes React no cambia, solo quién y cuándo lo ejecuta.

Cómo comprobar qué indexa Google de tu SPA

Antes de invertir tiempo en prerenderizado o SSR, confirma qué está pasando hoy. La inspección de URL en Search Console, sobre una URL en vivo, da el HTML renderizado y una captura: compáralos con lo que ves al abrir esa URL en incógnito. Si falta contenido, suele ser uno de tres motivos: algo se carga solo tras una interacción, como un scroll infinito; una llamada a una API tarda más de lo que el renderizador espera; o hay un error de JavaScript exclusivo del Web Rendering Service.

Complementa esa inspección con el informe de indexación de páginas, filtrando por detectada sin indexar y rastreada sin indexar: en rutas que quieres posicionar, ambos apuntan casi siempre a un problema de renderizado, no a un bloqueo obvio como un robots.txt mal configurado. Un sitemap XML con las rutas reales de la SPA ayuda a que Google descubra rutas sin depender de un enlace interno, pero no sustituye a resolver el renderizado: dice que una URL existe, no arregla lo que Google ve al visitarla.

Errores frecuentes que hunden el SEO en una aplicación React

Más allá del router y del renderizado, hay un puñado de patrones comunes en aplicaciones React que sabotean el SEO sin que nadie los diseñara con esa intención.

Ninguno de ellos es exclusivo de React: son consecuencia de tratar el renderizado en el cliente como un detalle invisible para el SEO. No lo es, y los seis se detectan con la misma herramienta -la inspección de URL renderizada en Search Console, comparada con la versión que ve un usuario real.

  • Contenido cargado solo al hacer scroll o al pulsar "cargar más", sin enlaces reales a páginas siguientes para quien no interactúa.
  • Redirecciones hechas con la función navigate del cliente para lo que debería ser un 301 del servidor: Google ve primero la URL vieja devolviendo 200 antes de que el JavaScript redirija.
  • Etiquetas canonical inyectadas por JavaScript que nunca llegan a estar presentes si el renderizador se agota antes de ejecutar ese código.
  • Rutas protegidas por autenticación que muestran una pantalla vacía a un visitante no autenticado, y Googlebot rastrea sin sesión: ve exactamente eso.
  • Title y meta description que cambian por ruta en el navegador pero nunca llegan a estar en el DOM cuando Google captura el renderizado, por una condición de carrera con la carga de datos.
  • Bloquear en robots.txt la carpeta de assets o de la API que React necesita para hidratarse, lo que impide que el renderizador complete la página aunque el HTML inicial esté bien.

Preguntas frecuentes

¿Puede Google indexar el contenido de una SPA de React?

Sí. Google ejecuta el JavaScript de tu aplicación en una segunda fase de renderizado, después de leer el HTML inicial. El problema no es que no pueda, sino que ese proceso tiene un presupuesto limitado y puede no llegar a ver contenido que depende de interacciones o de llamadas lentas a una API.

¿Es obligatorio migrar a Next.js para que una app de React posicione bien?

No. Puedes mantener Vite o Create React App y resolver los mismos problemas con rutas reales en el servidor, meta tags por ruta gestionadas desde React, y prerenderizado o SSR añadido a tu stack actual, sin cambiar de framework.

¿Qué diferencia hay entre prerenderizado y SSR para una SPA?

El prerenderizado genera el HTML de cada ruta una vez, en el momento del build, y sirve ese archivo estático a cualquier visita. El SSR genera el HTML en cada petición, en tiempo real, lo que cuesta más mantener pero sirve siempre contenido actualizado.

¿Por qué mi SPA con React Router devuelve 404 al recargar una página?

Porque el servidor no está configurado para servir index.html en rutas que no corresponden a un archivo físico. React Router gestiona esas rutas en el cliente, pero una recarga directa o una visita de Googlebot llega primero al servidor, que necesita su propia regla de fallback.

¿Cómo compruebo qué ve Google de mi aplicación React hoy?

Usa la inspección de URL en Search Console sobre una URL en vivo y pide el HTML renderizado. Compáralo con lo que ves al abrir esa misma URL en una ventana de incógnito; cualquier contenido ausente en la versión de Google señala el problema a resolver.

Todos los artículos