SEO para SPA
Cómo hacer que las aplicaciones de página única (React Router, Vue Router, Angular Router) sean rastreables e indexables: los problemas del app shell y del error soft 404, History API frente al enrutamiento por fragmentos, HTML propio por ruta mediante SSR o prerenderizado, señales de canonización y títulos por ruta, y generación de sitemaps para rutas gestionadas en el cliente.
Idiomas
Una aplicación de página única carga un único documento y cambia de vista con JavaScript en lugar de pedir una página nueva al servidor. Muchas SPA lo implementan con un "app shell": el servidor devuelve el HTML real de una sola URL, un contenedor casi vacío, hasta que JavaScript renderiza cada ruta. Pero eso es una decisión de implementación habitual, no una regla que siga toda SPA; compruebe qué devuelve realmente una petición directa a cada ruta antes de darlo por supuesto. La solución, cuando el problema es una configuración de app shell sin más, tiene dos mitades independientes: direccionabilidad (History API, no fragmentos hash/#!, para que cada vista tenga una URL real; una navegación suave del navegador mediante la History API cambia la URL y la interfaz, pero no crea por sí misma una nueva respuesta del servidor) y disponibilidad del contenido (SSR, prerenderizado o un metaframework, para que cada una de esas URL pueda devolver HTML único cuando se solicita). Además, verifique el título, la señal de canonización y el estado de robots de cada ruta tanto en la entrada directa como después del renderizado, gestione las vistas inexistentes (los routers tienden a mantener un estado 200 para «no encontrado»: redirija a una URL que devuelva un estado de error real o añada un noindex renderizado, aunque un noindex inicial puede provocar que se omita el renderizado) y asegúrese de que cada ruta del sitemap se resuelve en HTML real e indexable: no existe ningún formato de sitemap específico para SPA.
TL;DR — Una aplicación de página única (SPA) carga una página desde el servidor y después usa JavaScript para cambiar de “página” sin una recarga completa. El inconveniente: muchas SPA envían la misma página inicial casi vacía sin importar qué URL pida un motor de búsqueda; ese es un riesgo habitual de cómo se suelen construir las SPA, no algo que ocurra necesariamente en todas. Para solucionarlo, asegúrese de que cada ruta tenga su propia URL real y su propio HTML real, normalmente renderizando las páginas en el servidor o generándolas de antemano.
Qué es una SPA
Una aplicación de página única normalmente actualiza vistas y rutas en el cliente sin una navegación completa del documento. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Google puede renderizar SPA basadas en JavaScript, pero siguen siendo necesarias URL rastreables, enlaces, gestión de códigos de estado y contenido renderizado. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
Una aplicación de página única es un sitio web construido como una sola página HTML. Cuando se navega por él (de la página de productos a la página de información) JavaScript sustituye lo que hay en pantalla en lugar de pedir al servidor una página completamente nueva. React (con React Router), Vue (con Vue Router) y Angular funcionan así de forma predeterminada. Resulta rápido y parecido a una aplicación, y por eso es popular.
El riesgo está en lo que envía el servidor. Muchas SPA usan una implementación de “app shell”: la primera vez que alguien (incluido Google) carga la aplicación, el servidor devuelve un contenedor casi vacío, y el contenido real se construye después en el navegador. Esa es una forma habitual de construir una SPA, no su definición, ni algo que haga toda SPA. Pero cuando un sitio sí entrega un app shell vacío, el modo de fallo es real: si Google pide al servidor /about y /products por separado, puede recibir exactamente el mismo contenedor en blanco para ambas rutas, porque nada relativo a la ruta cambió lo que envió el servidor. La manera de saber si su aplicación está en esa situación es comprobar qué devuelve realmente una petición directa a cada ruta, no suponerlo por el hecho de que se trate de una SPA.
Por qué eso perjudica al SEO
Los motores de búsqueda necesitan ver su contenido para posicionarlo. Con una SPA sin más, tienden a fallar tres cosas:
- Todas las URL son iguales para el servidor. Los enlaces profundos, las veces que se comparte y los rastreadores acaban todos en el mismo contenedor.
- Las páginas 404 siguen respondiendo “200 OK”. Un router de JavaScript puede mostrar una pantalla de “no encontrado” mientras la página informa técnicamente de éxito, de modo que Google puede indexar páginas vacías.
- URL equivocadas. Las SPA más antiguas usaban direcciones como
example.com/#/products. Google no puede indexarlas de forma fiable.
Cómo solucionarlo (versión breve)
- Dé a cada vista una URL real usando la History API del navegador (rutas limpias como
/products), no direcciones basadas en#. - Envíe HTML real para cada URL. Renderice las páginas en el servidor (SSR) o genérelas de antemano (prerenderizado). Si empieza de cero, un framework que lo haga por usted (Next.js, Nuxt, SvelteKit, el SSR de Angular) le ahorra el trabajo.
- Dé a cada página su propio título y descripción que cambien cuando cambie la ruta.
- Haga que los enlaces sean enlaces reales (
<a href>), no<div>en los que se pueda hacer clic.
¿Quiere conocer la mecánica: por qué falla el enrutamiento por fragmentos, la trampa de la “directiva más restrictiva” con las etiquetas canónicas y cómo generar un sitemap para rutas gestionadas en el cliente? Cambie a la pestaña Avanzado.
TL;DR — El riesgo principal para el SEO de una SPA no es “JavaScript” en abstracto: es que el enrutamiento en el cliente cambia lo que ve el usuario sin cambiar lo que devolvería el servidor. La solución tiene dos mitades independientes que constantemente se confunden: direccionabilidad (History API, URL únicas por ruta, sin fragmentos
#!) y disponibilidad del contenido (SSR, prerenderizado/SSG o un metaframework). Si solo hace lo primero, obtendrá un sitemap impecable de URL que renderizan todas el mismo contenedor. Además de ambas cosas, cada ruta necesita su propia etiqueta canónica, título y descripción en el DOM renderizado, hay que gestionar los errores soft 404 que mantienen un200, y cada ruta del sitemap tiene que resolverse de forma independiente en HTML real.
Qué es en realidad el SPA SEO
Una SPA es una arquitectura de aplicación, no un fallo de SEO garantizado. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application El renderizado en el servidor, el prerenderizado o un renderizado en el cliente cuidadosamente implementado pueden exponer el contenido, pero ninguno garantiza la indexación. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
Este es el análisis en profundidad de las aplicaciones de página única en concreto: enrutamiento en el cliente con React Router, Vue Router o Angular Router, donde después de la primera carga el servidor nunca vuelve a enviar una página completa. Complementa la guía más amplia de SEO en JavaScript (que cubre la paridad, la carga diferida, el desplazamiento infinito y el renderizado de JS en general) y los artículos por framework sobre React, Next.js, Nuxt, Angular, Vue, Svelte y Astro. Aquí solo me interesa la capa de enrutamiento y lo que hace al rastreo y a la indexación.
Por qué las SPA con enrutamiento en el cliente sin más fallan en SEO
Una URL, una respuesta HTML: el problema del app shell
Google describe el modo de fallo con precisión: “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (traducción) «Algunos sitios con JavaScript pueden usar el modelo de app shell, en el que el HTML inicial no contiene el contenido real y Google necesita ejecutar JavaScript para poder ver el contenido real de la página». En una SPA sin más, el app shell es todo lo que el servidor llega a devolver. Solicite /products en frío y obtendrá el mismo documento casi vacío que obtendría para /about. El contenido solo diverge después de que el navegador ejecute su JavaScript y su router decida qué mostrar.
Ese es todo el problema en una frase: el enrutamiento en el cliente cambia lo que ve el usuario sin cambiar lo que devolvería el servidor. Todo lo que viene después (errores soft 404, contenido duplicado, títulos ausentes) es un síntoma de eso.
El servidor nunca ve qué “página” se solicitó (por qué falla el enrutamiento por fragmentos)
Las SPA más antiguas cargan vistas a partir de fragmentos de URL: example.com/#/products. Las palabras exactas de Google: “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (traducción) «Una SPA puede usar fragmentos de URL para cargar vistas diferentes». Esto falla por un motivo concreto y mecánico: los navegadores nunca envían el fragmento (todo lo que va después de #) al servidor en la petición HTTP. El servidor literalmente no puede saber qué “página” se solicitó, así que no puede devolver contenido distinto ni un código de estado distinto para ella. Un rastreador que solicite la URL una vez obtiene HTML idéntico independientemente del fragmento.
Ese es también el motivo por el que Google dejó formalmente obsoleto su esquema de rastreo AJAX de 2009 en octubre de 2015: “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (traducción) «En resumen: ya no recomendamos la propuesta de rastreo AJAX que presentamos en 2009». La antigua solución alternativa con _escaped_fragment_ permitía a los servidores prerenderizar las rutas por fragmento bajo demanda, pero parcheaba el problema de enrutamiento en lugar de resolverlo. La recomendación de Google que la sustituye es la History API, que se trata más abajo.
Errores soft 404: los routers en el cliente mantienen un 200 para todo
Este problema es casi inevitable en una SPA sin más. Los routers en el cliente, por diseño, mantienen el estado 200 de la página original en cada navegación virtual, incluidos los estados de «no encontrado». Google lo señala explícitamente: “In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” (traducción) «En una aplicación de página única puede resultar especialmente difícil. Para evitar que se indexen páginas de error, puede usar una de las estrategias siguientes o ambas». Y explica el motivo: “When a SPA is using client-side JavaScript to handle errors they often report a 200 HTTP status code instead of the appropriate status code.” (traducción) «Cuando una SPA usa JavaScript del cliente para gestionar errores, a menudo comunica un código de estado HTTP 200 en lugar del código adecuado».
El resultado son vistas vacías o de error que se indexan como páginas 200 de poco valor. Google documenta exactamente dos estrategias aquí, con precisión: redirigir (o hacer una petición completa) a una URL cuyo servidor devuelva un estado real de 404/error, o añadir una etiqueta noindex a la vista de error con JavaScript. Cuidado con la segunda: si ese noindex está presente desde el primer pintado en lugar de añadirse después de que la aplicación determine que la ruta no es válida, puede provocar que Google omita por completo el renderizado de la página; así que verifique qué devuelve una petición directa antes de que se ejecute cualquier JS, y no solo lo que aparece después en el DOM renderizado.
Duplicados por contenedor compartido: una posible causa, no un diagnóstico automático
Hay un segundo modo de fallo, más sutil: rutas distintas se indexan como duplicados entre sí porque se renderizan hasta quedar en la misma plantilla compartida de cabecera, navegación y pie. Gary Illyes describió una de las formas en que esto ocurre: “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out (my most likely explanation) and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (traducción) «Tengo varios correos en los que el problema es que el contenido principal tardó demasiado en cargarse, por lo que el renderizado agotó el tiempo de espera —mi explicación más probable— y quedaron páginas que solo contenían la plantilla. Al tener solo la plantilla, esas páginas son duplicadas». Su solución: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (traducción) «Intente reorganizar las llamadas de JavaScript para que el contenido —incluida la plantilla secundaria— se cargue primero». (Transmitido a través de una publicación de LinkedIn, que se resiste a la verificación automatizada; considérese fuente secundaria de alta confianza.)
Fíjese en que Illyes lo plantea como “my most likely explanation” (traducción) «mi explicación más probable», no como un diagnóstico confirmado: conviene tomárselo literalmente. El tiempo de espera de renderizado no es algo que se pueda diagnosticar solo a partir de los síntomas; se necesitan pruebas a nivel de petición (qué devuelve realmente una solicitud directa), pruebas del resultado renderizado (si el contenido principal está presente tras el renderizado) y pruebas de Search Console (señales de duplicación e indexación) antes de atribuir el problema a un tiempo de espera concreto, y no a un error, un recurso bloqueado o un contenedor genuinamente idéntico. Tampoco hay un tiempo de espera fijo y publicado con el que planificar: la documentación básica de Google dice que una página “may stay on this queue for a few seconds, but it can take longer than that,” (traducción) «puede permanecer en esta cola unos segundos, aunque también puede tardar más», sin comprometerse con una cifra. No planifique en torno a una duración supuesta de la cola; asegúrese de que el contenido principal se cargue lo antes posible, con independencia de cuánto tarde el renderizado.
La solución: conseguir HTML real por ruta
La solución completa tiene dos partes independientes que la gente confunde constantemente:
- Direccionabilidad de las URL: History API, URL únicas por ruta, sin fragmentos.
- Disponibilidad del contenido: SSR, prerenderizado estático/SSG o un metaframework.
Si solo hace el punto 1, obtendrá URL limpias y compartibles que siguen devolviendo el mismo contenedor en blanco. Para una ruta que realmente quiera ver indexada, necesita ambas.
Eso sí, no es un mandato universal para añadir SSR en todas partes. El SSR y el prerenderizado reducen la dependencia de que un rastreador ejecute correctamente su JavaScript; no se vuelven obligatorios en el instante en que un sitio es técnicamente una SPA. Antes de elegir una arquitectura para una ruta concreta, compruebe tres cosas: si esa ruta necesita posicionarse siquiera (un panel de administración interno no lo necesita), qué devuelve ya una petición directa sin JS (algunas configuraciones ya envían HTML significativo) y qué rastreadores necesita satisfacer realmente (Google renderiza JS con bastante fiabilidad; Bing y la mayoría de rastreadores de IA, menos, véase más abajo). Un CSR que ya supera esas comprobaciones no tiene por qué convertirse en SSR solo porque el sitio sea una SPA.
Renderizado en el servidor (SSR)
El servidor ejecuta su aplicación en cada petición y devuelve HTML completamente formado para esa ruta; después el cliente lo “hidrata” hasta convertirlo en una SPA activa. Es la opción más robusta porque el rastreador obtiene el contenido completo en la primera petición, sin necesidad de ejecutar JS.
Prerenderizado estático / SSG
En lugar de renderizar en cada petición, se genera de antemano el HTML de cada ruta en el despliegue. Perfecto para contenido que no cambia según el usuario. De lo que yo mismo he escrito sobre SEO en JavaScript: cualquier configuración de SSR, renderizado estático y prerenderizado va a funcionar bien para los motores de búsqueda; lo que hay que evitar es dejar el contenido encerrado tras un renderizado exclusivo del cliente.
O bien: no lo haga a mano, use un metaframework
Para un desarrollo nuevo, la recomendación honesta es no montar a mano un enrutamiento en el cliente basado solo en React Router o solo en Vue Router. Un metaframework (Next.js, Nuxt, Angular con su paquete de SSR, SvelteKit, Remix) le proporciona SSR/SSG y metadatos por ruta de serie, lo que evita por completo esta categoría de problemas. (Cada uno de ellos tiene su propio análisis en profundidad en este sitio). Sea sincero con la otra cara de la moneda: incorporar SSR a posteriori en una SPA existente hecha a mano es un trabajo de ingeniería real, un proyecto de migración, no un simple interruptor de configuración.
El renderizado dinámico como parche temporal, no como destino
Puede servir a los rastreadores una instantánea HTML renderizada por separado (renderizado dinámico). Tanto Google como Bing lo aceptan; Bing “recommend[s] dynamic rendering as a great alternative for websites relying heavily on JavaScript” (traducción) «recomienda el renderizado dinámico como una alternativa excelente para sitios que dependen en gran medida de JavaScript» (transmitido desde el blog de Bing de 2018; las dos citas breves siguientes sobre las capacidades de bingbot están verificadas directamente, esta más larga no se ha vuelto a comprobar de forma independiente), pero Google es claro en que se trata de una solución provisional: “Dynamic rendering was a workaround and not a long-term solution… Instead, we recommend that you use server-side rendering, static rendering, or hydration as a solution.” (traducción) «El renderizado dinámico era una solución provisional, no una solución a largo plazo. En su lugar, recomendamos usar renderizado en el servidor, renderizado estático o hidratación». (Transmitido desde la documentación de renderizado dinámico de Google; la redacción coincide con la ya citada en la guía más amplia de SEO en JavaScript de este sitio.) Es un puente, no una arquitectura.
History API frente al enrutamiento por fragmentos (#!)
Cinco estados, no dos
Probar una ruta de una SPA resulta confuso porque “si funciona” abarca en realidad cinco estados distintos y verificables por separado:
| Estado | En qué consiste |
|---|---|
| Respuesta del servidor | Los bytes que recibe realmente una petición HTTP nueva y sin JS a una URL. |
| DOM renderizado | Lo que construye un navegador (o el renderizador de Googlebot) tras ejecutar JavaScript sobre esa respuesta del servidor. |
| Procesamiento de la Búsqueda | Cómo Google rastrea por separado la respuesta del servidor, renderiza después la página e indexa a partir de ambas cosas. |
| Navegación completa de documento en el navegador | Una nueva petición HTTP real a una URL: el único estado que puede cambiar la respuesta del servidor o el código de estado. |
| Navegación suave en el navegador | Una transición de la History API (pushState/replaceState) que cambia la URL visible, el historial del navegador y la interfaz en pantalla. |
El que todo el mundo confunde: una navegación suave cambia la URL y la interfaz, pero no crea por sí misma una nueva respuesta HTTP ni un nuevo estado; eso solo ocurre en una navegación completa (o en una petición directa equivalente, como curl). Probar una ruta haciendo clic por la aplicación desde la página de inicio ejercita la navegación suave; probarla solicitando la URL directamente ejercita la respuesta del servidor. Ambas cosas importan, y pueden no coincidir.
Qué le aporta la History API
La recomendación de Google es inequívoca: “We recommend using the History API to load different content based on the URL in a SPA.” (traducción) «Recomendamos usar la History API para cargar contenido diferente según la URL de una SPA». (En la documentación en vivo, «History API» es un enlace, así que este enlace profundo apunta a la cláusula introductoria.) La History API (pushState/replaceState) permite que su router cambie la URL visible a una ruta real que se pueda guardar en marcadores (/products, no /#/products) sin una recarga completa. Eso resuelve la mitad de la direccionabilidad: ahora cada vista tiene una URL a la que un servidor podría responder de forma distinta.
Por qué las URL con fragmento o hash son invisibles para el servidor (y para Google)
Como el fragmento nunca llega al servidor (véase más arriba), la History API es la única forma de dar a cada ruta una URL que el servidor pueda servir realmente. Google establece un mínimo al respecto en su documentación básica: “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (traducción) «No use fragmentos para cargar contenido diferente. El ejemplo siguiente es una práctica incorrecta porque Googlebot no puede resolver las URL de forma fiable». Es la misma recomendación que he escrito en otros sitios: use URL de aspecto normal como /products, no URL con hash como /#/products, porque Google no puede indexar de forma fiable estas últimas.
La obsolescencia de 2015, en breve
La era del hash-bang (#!) terminó con la publicación de Google de 2015: “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers,” (traducción) «Los tiempos han cambiado. Hoy, siempre que no bloquee el rastreo de sus archivos JavaScript o CSS, por lo general podemos renderizar y entender sus páginas como los navegadores modernos», y “you can use the History API pushState() to ensure accessibility for a wider range of browsers (and our systems).” (traducción) «Puede usar pushState() de la History API para garantizar la accesibilidad para más navegadores y para nuestros sistemas». Un matiz que conviene retener: Google no desindexó al instante los antiguos sitios con hash-bang (“we’ll generally crawl, render, and index the #! URLs” (traducción) «por lo general rastrearemos, renderizaremos e indexaremos las URL con #!»), pero «todavía podemos intentarlo» no es «debería seguir haciéndolo».
Hacer que cada ruta sea indexable de forma independiente
Las URL limpias y el HTML real consiguen que le rastreen. Para que le indexen correctamente, cada ruta necesita sus propias señales.
Etiquetas canónicas por ruta, y la trampa de la “directiva más restrictiva”
Cada ruta necesita su propia declaración rel=canonical en el DOM renderizado. La trampa: si en el HTML sin procesar del contenedor se envía una señal de canonización de marcador de posición (o un noindex) y se supone que JavaScript la sobrescribirá después, puede producirse un conflicto. Google resuelve los conflictos entre la versión sin procesar y la renderizada quedándose con la señal más restrictiva; como ya he dicho antes, Google elegirá las declaraciones más restrictivas entre el HTML y la versión renderizada de una página. Un noindex perdido o una señal de canonización equivocada incrustada en el contenedor pueden suprimir en silencio toda la ruta incluso después de que su JS la “arregle”.
Títulos y metadescripciones por ruta mediante JavaScript
Definirlos con JS está bien; Google lo dice directamente: “You can use JavaScript to set or change the meta description as well as the <title> element.” (traducción) «Puede usar JavaScript para definir o cambiar la metadescripción y el elemento title». El requisito es que acaben en el DOM renderizado que Google evalúa, no que solo aparezcan un instante. Dé a cada página su propio título y descripción, que se actualicen cuando cambie la ruta.
Enlaces <a href> reales entre rutas
Haga que los enlaces entre rutas sean anclas reales con atributos href, no gestores de clic sobre <div>. Google descubre URL extrayendo los href; un <div onClick> que navega mediante el router es invisible para la extracción de enlaces del rastreador. Y no se apoye en el estado del cliente para transportar contenido entre esas navegaciones: el renderizador de Google “does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” (traducción) «No conserva el estado entre cargas: los datos de Local Storage y Session Storage, así como las cookies HTTP, se borran entre cargas».
Generación de sitemaps para rutas de SPA
No existe un formato especial de “sitemap para SPA”: es un problema de inventario
El protocolo de sitemaps no cambia para las SPA. El trabajo real consiste en asegurarse de que cada ruta que incluya se resuelva de forma independiente en HTML real, único y renderizado. Un sitemap con 500 rutas gestionadas en el cliente no vale nada si todas esas rutas devuelven el mismo contenedor. Así que el sitemap depende de su estrategia de renderizado, no la sustituye.
Cómo gestionar rutas dinámicas o parametrizadas (/product/:id)
En aplicaciones con rutas parametrizadas no se puede mantener la lista a mano. El generador del sitemap tiene que ejecutarse sobre la misma fuente de datos que usa la aplicación (un script de compilación o un endpoint de servidor que enumere cada id) para que el sitemap y la aplicación nunca se desincronicen. Esas URL enumeradas solo merece la pena incluirlas cuando estén servidas con SSR o prerenderizadas.
Mantener el sitemap sincronizado con lo que el servidor puede resolver
Regenere el sitemap como parte de su compilación o con una periodicidad ligada a su fuente de contenido. Una ruta que devuelve un 404 (o peor, un soft 404 con 200) pero figura en su sitemap es una señal de presupuesto de rastreo y de calidad que no le interesa dar.
Probar lo que Google ve realmente
No se limite a comprobar una sola URL. La esencia del problema de las SPA es que las rutas pueden diferir en el navegador pero no en la red, así que pruebe varias rutas a nivel de HTML sin procesar:
- Obtenga el HTML sin procesar de cada ruta con
curl(con un user-agent de Googlebot cuando proceda) y confirme que el contenido es único por URL, no el contenedor compartido. - URL Inspection en Search Console: compare el HTML rastreado y el renderizado de varias rutas, y confirme que el título, la descripción y la etiqueta canónica de cada ruta están presentes.
- Confirme que las rutas de error devuelven la señal correcta: una vista de “no encontrado” debería o bien redirigir a un estado de error real, o bien llevar
noindexen el DOM renderizado.
Mitos habituales sobre el SPA SEO
- “Google no puede indexar SPA en absoluto”. Obsoleto. Google ejecuta Chromium en versión permanentemente actualizada y por lo general renderiza el contenido de las SPA. Los riesgos reales son concretos: errores soft 404, enrutamiento por fragmentos, tiempos de espera de renderizado y rastreadores distintos de Google (Bing con menos fiabilidad, la mayoría de rastreadores de IA directamente no).
- “Añadir la History API arregla el SEO de una SPA”. Solo arregla la direccionabilidad. Si el servidor sigue devolviendo el mismo contenedor para cada ruta, Google todavía tiene que ejecutar JS para ver algo.
- “El enrutamiento por fragmentos sigue funcionando como alternativa”. Obsoleto desde 2015 y desaconsejado desde entonces.
- “El renderizado en el cliente es una penalización de posicionamiento”. No existe ninguna penalización directa por CSR. El daño es indirecto: renderizado fallido o tardío, errores soft 404 y agrupación de duplicados por tiempos de espera de renderizado reducen lo que se indexa.
- “Prerenderizar para los bots es cloaking”. No lo es cuando el contenido coincide con lo que los usuarios acaban viendo; solo cambia el cuándo y dónde del renderizado, no el contenido.
- “Necesita un generador de sitemaps especial para SPA”. No existe ningún formato especial; el trabajo consiste en hacer que cada ruta incluida se resuelva en HTML real.
Preguntas frecuentes
¿Puede Google indexar una aplicación de página única? Sí, si cada ruta se resuelve en HTML real y único (mediante SSR o prerenderizado) en una URL real. Las SPA sin más, exclusivamente de cliente, a menudo no lo hacen.
¿Necesito SSR, o el renderizado en el cliente resulta aceptable alguna vez? El CSR puede servir para contenido que no necesita posicionarse, pero para cualquier cosa que quiera ver indexada de forma fiable, prerenderícela o use SSR; no apueste a que el renderizador ejecute su JS a tiempo.
¿Es malo para el SEO el enrutamiento por hash (#!)? Sí: el fragmento nunca llega al servidor, así que Google no puede resolver esas URL de forma fiable. Use la History API.
¿Necesita cada ruta su propia señal de canonización? Sí, en el DOM renderizado, y asegúrese de que no se envíe nada más restrictivo (un noindex perdido o una señal de canonización equivocada) en el contenedor sin procesar.
¿Es cloaking un servicio de prerenderizado? No, siempre que el contenido servido a los bots coincida con el que ven los usuarios.
¿Debería usar un metaframework en lugar de construir el enrutamiento por mi cuenta? Para un desarrollo nuevo, normalmente sí: Next.js, Nuxt, Angular con SSR o SvelteKit le dan SSR/SSG y metadatos por ruta sin costo adicional.
¿Por qué mi SPA devuelve 200 en páginas que no existen? Porque el router en el cliente mantiene el estado 200 original en las navegaciones virtuales. Soluciónelo con una redirección por JS a un estado de error real o con un noindex renderizado.
Resumen con IA
Una versión condensada del apartado Advanced:
- El SEO para SPA se centra aquí en el enrutamiento del cliente (React Router, Vue Router y Angular Router). Una SPA se define por cargar un solo documento e intercambiar vistas con JavaScript; un app shell que envía HTML casi vacío para cada URL es una implementación habitual, no una regla que siga toda SPA. Compruebe qué devuelve realmente una petición directa en lugar de suponerlo.
- El riesgo principal: el enrutamiento en el cliente puede cambiar lo que ve el usuario sin cambiar lo que devolvería el servidor. En una SPA con app shell sin más, solicitar
/productsy/aboutdirectamente puede devolver un HTML sin procesar idéntico byte a byte. - Se confunden cinco estados: respuesta del servidor, DOM renderizado, procesamiento de la Búsqueda, navegación completa de documento en el navegador y navegación suave en el navegador. Una transición de la History API cambia la URL y la interfaz, pero no crea por sí misma una nueva respuesta ni un nuevo estado del servidor.
- Tres síntomas de un contenedor vacío: el problema del app shell, las vistas inexistentes que conservan un
200(Google documenta dos soluciones: redirigir a una URL que devuelva un estado de error real o añadir unnoindexmediante JS; unnoindexinicial puede provocar que se omita el renderizado) y los duplicados por contenedor compartido, de los que el tiempo de espera de renderizado descrito por Illyes es una posible causa, no un diagnóstico automático sin pruebas de petición, renderizado y Búsqueda. - El enrutamiento por hash falla por mecánica: el fragmento posterior a
#nunca llega al servidor, así que el servidor no puede diferenciar por ruta. Google lo dejó obsoleto en 2015; la History API aporta direccionabilidad, no una solución completa de indexabilidad. - La solución tiene dos mitades independientes cuando el problema es un contenedor vacío: direccionabilidad (History API, URL únicas por ruta) y disponibilidad del contenido (SSR, prerenderizado/SSG o un metaframework), pero el SSR no es un requisito universal: base la decisión en si la ruta necesita posicionarse, en qué devuelve ya directamente y en qué rastreadores necesitan renderizarla.
- Por ruta: señal de canonización, título y descripción propios en el DOM renderizado; ojo con la trampa de la “directiva más restrictiva”, en la que un
noindexo una señal de canonización del HTML sin procesar anulan su JS. Enlaces<a href>reales; no dependa del estado del cliente (el WRS borra el almacenamiento y las cookies entre cargas). - Sitemaps: incluir una ruta no demuestra que se resuelva; no existe un formato especial. Cada ruta incluida debe resolverse de forma independiente en HTML real, y las rutas dinámicas necesitan un generador ligado a la fuente de datos de la aplicación.
- El renderizado dinámico es un parche temporal aceptado por Google y Bing, no una arquitectura a largo plazo.
- Pruebe varias rutas a nivel de HTML sin procesar (peticiones directas), no solo navegando dentro de la aplicación.
Documentación oficial
Documentación de fuente primaria de los motores de búsqueda.
- Fundamentos del SEO para JavaScript — el modelo de app shell, la recomendación de usar la History API en lugar de fragmentos y la definición de títulos y descripciones mediante JS.
- Solucionar problemas de JavaScript relacionados con la Búsqueda — la sección sobre errores soft 404 en SPA, la recomendación de no usar fragmentos para cargar contenido distinto y el uso de la History API.
- El renderizado dinámico como solución provisional — por qué es un parche temporal y no una solución a largo plazo.
- Obsolescencia del esquema de rastreo AJAX (2015) — el final formal del rastreo con hash-bang y la recomendación de pushState.
- Crear y enviar un sitemap — el protocolo general de sitemaps (no existe un formato específico para SPA).
Bing / Microsoft
- Serie sobre bingbot: JavaScript, renderizado dinámico y cloaking — las capacidades de renderizado de JS de bingbot y su postura sobre el renderizado dinámico.
Citas de la fuente
Declaraciones oficiales de Google y Bing. Cada enlace es un enlace profundo que salta al pasaje citado en la página de origen.
Google — el problema del app shell
- “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (traducción) «En ciertos sitios con JavaScript, el HTML inicial puede ser solo un app shell sin el contenido real, por lo que Google debe ejecutar el código para verlo». Ir a la cita
Google — enrutamiento por hash o fragmento y la History API
- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (traducción) «Los fragmentos de URL pueden servir para cargar vistas distintas en una SPA». Ir a la cita
- “We recommend using the History API to load different content based on the URL in a SPA.” (traducción) «Google aconseja cargar el contenido de cada URL de la SPA mediante la History API». Ir a la cita (En la documentación en vivo, “History API” es un enlace, así que este enlace profundo apunta a la cláusula introductoria; la frase completa aparece literalmente y en orden.)
- “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (traducción) «Los fragmentos no deben cargar páginas diferentes, ya que Googlebot no resuelve esas URL con fiabilidad». Ir a la cita
Google — vistas de error en SPA
- “In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” (traducción) «Evitar que se indexen páginas de error resulta especialmente difícil en una SPA; Google propone dos estrategias que pueden combinarse». Ir a la cita
- “When a SPA is using client-side JavaScript to handle errors they often report a
200HTTP status code instead of the appropriate status code.” (traducción) «Al gestionar errores en el cliente, una SPA suele informar un código HTTP 200 en vez del estado correcto». Ir a la cita
Google — metaetiquetas mediante JavaScript y renderizado sin estado
- “You can use JavaScript to set or change the meta description as well as the
<title>element.” (traducción) «Puede usar JavaScript para definir o cambiar la metadescripción y el elemento title». Ir a la cita - “WRS does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” (traducción) «WRS no conserva el estado entre cargas: los datos de Local Storage y Session Storage, así como las cookies HTTP, se borran entre cargas». Ir a la cita
Google — obsolescencia del rastreo con hash-bang (2015)
- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (traducción) «En resumen: ya no recomendamos la propuesta de rastreo AJAX que presentamos en 2009». Ir a la cita
- “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers.” (traducción) «En la actualidad, si Googlebot puede rastrear los archivos JavaScript y CSS, sus sistemas suelen interpretar las páginas igual que un navegador moderno». Ir a la cita
Google — el renderizado dinámico es una solución provisional (transmitido; la redacción coincide con la ya citada en la guía más amplia de SEO en JavaScript de este sitio, no se ha vuelto a verificar de forma independiente en esta revisión)
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” (traducción) «El renderizado dinámico era una solución provisional, no una solución a largo plazo para los problemas del contenido generado con JavaScript en los motores de búsqueda». Ir a la cita
Bing — bingbot y JavaScript
- “bingbot is generally able to render JavaScript.” (traducción) «Por lo general, bingbot puede renderizar JavaScript». — serie sobre bingbot, blog para webmasters de Bing. Leer la publicación
- “bingbot does not necessarily support all the same JavaScript frameworks that are supported in the latest version of your favorite modern browser.” (traducción) «Bingbot no admite necesariamente todos los frameworks de JavaScript que admite la última versión de su navegador moderno favorito». — misma publicación. Leer la publicación
Gary Illyes, Google — los tiempos de espera de renderizado generan duplicados (transmitido a través de una publicación de LinkedIn, que se resiste a la verificación automatizada; considérese fuente secundaria de alta confianza)
- “the centerpiece took forever to load, so rendering timed out… and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (traducción) «Si el contenido principal tarda demasiado, el renderizado puede terminar dejando únicamente la plantilla; esas páginas terminan tratándose como duplicadas». Leer la publicación
¿Qué vía de renderizado debería seguir?
Trabaje de arriba abajo. La pregunta es siempre “¿obtiene el rastreador HTML real para esta ruta sin ejecutar mi JS?”, y empiece por confirmar que la ruta realmente necesita indexarse y por comprobar qué devuelve ya una petición directa sin JS; no todas las rutas necesitan SSR, y algunas ya devuelven HTML utilizable.
1. ¿Está empezando un desarrollo nuevo?
- Sí → Use un metaframework (Next.js, Nuxt, Angular con SSR, SvelteKit, Remix). El SSR/SSG y los metadatos por ruta vienen incorporados. Pare aquí.
- No → Continúe.
2. ¿Su contenido cambia por usuario o por petición?
- No (contenido mayoritariamente estático) → Prerenderizado / SSG en tiempo de compilación. La solución robusta más sencilla: cada ruta es HTML real en disco.
- Sí → Renderizado en el servidor (SSR) para que cada petición devuelva HTML específico de la ruta.
3. ¿No puede hacer SSR ni prerenderizado ahora mismo (SPA heredada hecha a mano)?
- Use el renderizado dinámico como puente temporal: sirva a los rastreadores una instantánea renderizada. Planifique la migración a SSR/SSG; no lo trate como el destino final.
4. Sea cual sea la vía elegida, confirme todo esto en cada ruta:
- URL real mediante la History API (sin fragmentos
#!). - Título, metadescripción y señal de canonización únicos en el DOM renderizado.
- Nada más restrictivo (
noindex, señal de canonización equivocada) incrustado en el contenedor sin procesar. - Enlaces
<a href>reales entre rutas. - Las rutas de error devuelven un estado de error real o un
noindexrenderizado (sin errores leves de página inexistente). - Cada ruta del sitemap se resuelve de forma independiente en HTML real.
Lista de comprobación de SPA SEO
Direccionabilidad
- Cada vista tiene una URL real mediante la History API (
/products), no un fragmento (/#/products). - Los enlaces entre rutas son anclas
<a href>reales, no gestores<div onClick>.
Disponibilidad del contenido
- Cada ruta devuelve HTML real y único sin que el rastreador ejecute su JS (SSR, prerenderizado/SSG o un metaframework).
- El contenido de la ruta se carga antes de que se cierre la ventana de renderizado (evite duplicados formados solo por la plantilla).
Indexabilidad por ruta
-
<title>y metadescripción únicos por ruta, presentes en el DOM renderizado. -
rel=canonicalúnico por ruta en el DOM renderizado. - Ningún
noindexperdido ni etiqueta canónica de marcador de posición en el contenedor sin procesar que una señal más restrictiva pudiera fijar.
Errores
- Las rutas de «no encontrado» redirigen a una URL cuyo servidor devuelve un estado de error real o llevan un
noindexañadido por JavaScript (sin falsas respuestas correctas con200). - Si usa la vía del
noindex, confirme que se añade después de que la aplicación determine que la ruta no es válida, y que no está presente desde el primer pintado: unnoindexinicial puede provocar que Google omita el renderizado de la página.
Sitemap
- Cada ruta incluida se resuelve de forma independiente en HTML real.
- Las rutas dinámicas (
/product/:id) se enumeran a partir de la fuente de datos de la aplicación, no se mantienen a mano.
Verificación
- HTML sin procesar comprobado en varias rutas (curl), confirmando contenido único por URL.
- URL Inspection confirma el contenido renderizado, el título, la descripción y la etiqueta canónica de cada ruta.
Los modelos mentales
1. Las dos mitades que no debe confundir. La direccionabilidad (History API, URL únicas, sin fragmentos) y la disponibilidad del contenido (SSR, prerenderizado o un metaframework) son independientes. URL limpias sin HTML por ruta = un sitemap impecable de contenedores en blanco. Necesita ambas.
2. “¿Qué devolvería el servidor en frío?”
Todo el problema de las SPA es que la vista del navegador diverge de la respuesta del servidor. Para cualquier ruta, pregúntese qué devuelve un curl sin ejecución de JS. Si es el contenedor, es posible que el rastreador vea también el contenedor.
3. El valor por defecto del código de estado es 200, incluso para los errores.
Los routers en el cliente mantienen el 200 original. Dé por hecho que cada vista de “error” es un soft 404 hasta que haya hecho deliberadamente que devuelva un estado de error real o un noindex renderizado.
4. La trampa de la directiva más restrictiva.
Google concilia lo sin procesar y lo renderizado quedándose con la señal más restrictiva. Un noindex o una etiqueta canónica equivocada en el contenedor pueden anular su “arreglo” por JS. Audite el HTML sin procesar, no solo el DOM renderizado.
5. Metaframework primero para desarrollos nuevos. Montar a mano el enrutamiento en el cliente significa asumir todos y cada uno de estos problemas. Elegir un framework que hace SSR/SSG y metadatos por ruta es elegir no tenerlos.
Chuleta de SPA SEO
Enrutamiento
| Enfoque | Ejemplo de URL | ¿Llega al servidor? | ¿Seguro para Google? |
|---|---|---|---|
| History API | /products | Sí | Sí (recomendado) |
| Enrutamiento por hash | /#/products | No (el fragmento nunca se envía) | No, obsoleto desde 2015 |
Hash-bang (#!) | /#!/products | No | No, esquema AJAX heredado, obsoleto |
Estrategias de renderizado
| Estrategia | ¿El rastreador obtiene HTML real sin ejecutar JS? | Mejor para |
|---|---|---|
| SSR | Sí | Contenido por usuario o por petición |
| Prerenderizado / SSG | Sí | Contenido mayoritariamente estático |
| Metaframework (Next/Nuxt/etc.) | Sí (incorporado) | Desarrollos nuevos |
| Renderizado dinámico | Sí, solo para bots | Puente temporal en SPA heredadas |
| CSR sin más (solo cliente) | No | Nada que quiera indexar de forma fiable |
Imprescindibles por ruta (todo en el DOM renderizado)
- URL única (History API) ·
<title>único · metadescripción única ·rel=canonicalúnico · enlaces<a href>reales · rutas de error con un estado real onoindex.
Datos rápidos
- El fragmento posterior a
#nunca se envía al servidor: por eso falla el enrutamiento por hash. - Los routers en el cliente mantienen el
200para todo, incluso ante una vista inexistente. - Google concilia lo sin procesar y lo renderizado quedándose con la señal más restrictiva.
- No existe un formato especial de sitemap para SPA: cada ruta incluida debe resolverse en HTML real.
Antipatrones del SPA SEO
Publicar una SPA con CSR sin más y enviar un sitemap completo. El sitemap enumera 500 rutas; las 500 devuelven el mismo contenedor ante una petición en frío. El sitemap no hace que el contenido exista: eso lo hace el renderizado.
Añadir la History API y darlo por hecho. Las URL limpias arreglan la direccionabilidad, no el contenido. Sin SSR ni prerenderizado, el servidor sigue devolviendo el contenedor para cada ruta.
Enrutamiento por hash o hash-bang “como alternativa”. El fragmento nunca llega al servidor, así que el servidor no puede diferenciar por ruta. Obsoleto desde 2015; no es una alternativa, es un callejón sin salida.
Navegar con <div onClick> en lugar de <a href>.
Google extrae los href para descubrir URL. La navegación por gestor de clic es invisible para la extracción de enlaces, así que esas rutas pueden no encontrarse nunca.
Un noindex o una etiqueta canónica de marcador de posición en el contenedor sin procesar “que ya sobrescribirá el JS”.
Google se queda con la más restrictiva entre la versión sin procesar y la renderizada. La directiva del contenedor puede imponerse y suprimir la ruta en silencio.
Devolver 200 para las vistas de “no encontrado”.
Las falsas respuestas correctas hacen que se indexen páginas vacías o de poco valor. Redirija a un estado de error real o añada un noindex renderizado.
Cargar el contenido de la ruta lentamente, después de la plantilla. Los tiempos de espera de renderizado dejan solo el contenedor compartido, y cada ruta se convierte en un duplicado de todas las demás (Illyes). Cargue primero el contenido principal.
Depender del estado del cliente para transportar contenido entre navegaciones. El renderizador borra el Local Storage, el Session Storage y las cookies entre cargas de página: el contenido que solo existe en el estado del cliente no estará ahí cuando Google renderice la siguiente ruta.
Problemas habituales de indexación en SPA
Todas las rutas devuelven el mismo HTML
Síntoma: /products y /about tienen vistas distintas en el navegador pero respuestas sin procesar idénticas. Causa probable: el enrutamiento con la History API aporta direccionabilidad sin SSR ni prerenderizado. Solución: genere HTML específico de cada ruta y confirme que una petición directa a cada ruta contiene su propio encabezado, su propio texto y sus propios metadatos.
Las rutas inexistentes aparecen como páginas correctas
Síntoma: una ruta inexistente muestra una vista de “no encontrado” mientras devuelve 200.
Causa probable: el router del cliente gestiona el error después de que el servidor ya haya enviado un contenedor con respuesta correcta. Solución: devuelva el estado de servidor adecuado; si eso aún no puede implementarse, redirija a una URL con un estado de error real o renderice un noindex. Confírmelo con una petición directa nueva, no con una navegación dentro de la aplicación.
Las rutas se colapsan en duplicados tras el renderizado
Síntoma: los motores de búsqueda agrupan URL distintas o solo conservan la navegación compartida. Causa probable: el contenido de la ruta se carga demasiado tarde y el renderizado captura la plantilla. Solución: priorice el contenido principal en la respuesta del servidor o en la vía de renderizado más temprana. Confirme que varias rutas exponen contenido único antes de que terminen los scripts opcionales.
Probar qué devuelve realmente el servidor en cada ruta
La trampa de las SPA es que las rutas difieren en el navegador pero no en la red. Compruebe varias rutas a nivel de HTML sin procesar, no solo una.
Obtener el HTML sin procesar de cada ruta (macOS / Linux)
# Fetch a few routes with a Googlebot UA and compare — they should NOT be identical
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
for path in / /products /about /product/123; do
echo "=== $path ==="
curl -s -A "$UA" "https://example.com$path" | wc -c # byte counts should differ
doneComparar el HTML sin procesar de dos rutas (¿son el mismo contenedor?)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
diff <(curl -s -A "$UA" https://example.com/products) \
<(curl -s -A "$UA" https://example.com/about) \
&& echo "IDENTICAL — bare shell, content is client-only" \
|| echo "Different — routes return distinct HTML (good)"Comprobar el título y la etiqueta canónica de cada ruta en la respuesta sin procesar (grep / regex)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
curl -s -A "$UA" https://example.com/products \
| grep -Eio '<title>[^<]*</title>|<link[^>]+rel=["'"'"']canonical["'"'"'][^>]*>'Comprobar el estado HTTP de una ruta inexistente (detector de soft 404)
# A missing route should NOT return 200
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-route-does-not-existEn la consola de DevTools del navegador: inspeccionar el título y la etiqueta canónica renderizados
// Run on each route after client-side navigation to confirm JS set them
console.log('title:', document.title);
console.log('description:',
document.querySelector('meta[name="description"]')?.content);
console.log('canonical:',
document.querySelector('link[rel="canonical"]')?.href);Bookmarklet: detectar “enlaces” con gestor de clic que no son anclas reales
javascript:(()=>{const bad=[...document.querySelectorAll('[onclick],div[role="link"]')]
.filter(e=>!e.closest('a[href]'));
bad.forEach(e=>e.style.outline='3px solid red');
alert(bad.length+' non-anchor clickable(s) outlined — these are invisible to link extraction');})();XPath: encontrar enlaces con fragmento o hash que un rastreador no puede resolver (péguelo en la consola de DevTools)
$x('//a[starts-with(@href, "#") or contains(@href, "/#/")]')
.map(a => a.getAttribute('href'));
// Any results are hash-routed links; migrate them to History API paths. Herramientas para auditar una SPA
- Inspección de URL (Google Search Console): vea el HTML rastreado frente al renderizado de una ruta y confirme que el título, la descripción y la señal de canonización de esa ruta llegan realmente.
- Prueba de resultados enriquecidos / «Ver página rastreada» en Inspección de URL: el propio renderizado de Google de una única URL, útil para confirmar que el contenido sobrevive al renderizado.
curlcon un user-agent de Googlebot: la forma más rápida de comparar el HTML sin procesar de varias rutas y detectar un contenedor compartido.- Screaming Frog SEO Spider (modo de renderizado de JavaScript): rastree el sitio con y sin renderizado para comparar el contenido y los títulos sin procesar frente a los renderizados en todas las rutas.
- Ahrefs Site Audit: expone problemas de indexabilidad, títulos y etiquetas canónicas ausentes o duplicados, y patrones similares a los errores soft 404 en todas las rutas.
- DebugBear: monitorización de rendimiento orientada a SPA; los tiempos de renderizado importan porque las rutas lentas pueden agotar el tiempo de espera y quedar reducidas a duplicados de la plantilla.
- Inspección de URL de Bing Webmaster Tools: bingbot renderiza JS de forma menos consistente que Googlebot, así que confirme también sus rutas en el lado de Bing.
Demostrar que una ruta de SPA es indexable de forma independiente
Probar la paridad del HTML en la entrada directa
Prueba que ejecutar: abra rutas representativas en una sesión nueva y solicite las mismas URL con curl. Resultado esperado: cada URL devuelve su propio contenido principal y sus propias señales de cabecera sin estado previo de la aplicación. Interpretación del fallo: la ruta depende de la navegación en el cliente o del estado almacenado. Ventana de seguimiento: inmediata. Criterio de reversión: una ruta solo funciona si se entra a través de la página de inicio.
Probar la gestión de códigos de estado de error
Prueba que ejecutar: solicite directamente una ruta que se sepa inválida e inspeccione tanto el estado como las directivas renderizadas. Resultado esperado: un estado de error genuino, o la alternativa documentada de un noindex renderizado o una redirección a una respuesta de error. Interpretación del fallo: la SPA presenta páginas inexistentes como respuestas correctas. Ventana de seguimiento: inmediata. Criterio de reversión: las rutas inválidas se publican como páginas 200 indexables.
Probar el aislamiento de los metadatos
Prueba que ejecutar: compare el título, la etiqueta de robots y la señal de canonización sin procesar y renderizados en al menos tres rutas. Resultado esperado: cada ruta tiene un único conjunto intencionado e internamente coherente. Interpretación del fallo: el contenedor compartido filtra metadatos entre rutas o el JS los sobrescribe demasiado tarde. Ventana de seguimiento: inmediata en local y tras el nuevo rastreo en Inspección de URL. Criterio de reversión: cualquier ruta hereda la señal de canonización de otra ruta o una directiva restrictiva del contenedor.
Ponga a prueba sus conocimientos: SPA SEO
Cinco preguntas rápidas sobre cómo hacer que las aplicaciones de página única sean rastreables e indexables. Elija una respuesta para cada una y después compruebe el resultado.
Recursos que merecen su tiempo
Mis artículos relacionados
- Problemas de SEO en JavaScript y prácticas recomendadas — mi guía completa de SEO en JavaScript, incluidas las secciones sobre no usar fragmentos en las URL y sobre el app shell y el contenido duplicado en las que se apoya este artículo.
- Guía para principiantes sobre SEO técnico — dónde encajan el renderizado y el rastreo en el panorama general.
Mis ponencias
- Cómo funciona la Búsqueda (SlideShare) — mi recorrido por el rastreo, el renderizado, la indexación y el posicionamiento, que es el proceso que una SPA tiene que superar. Se aplica mi advertencia habitual: “This is my understanding of systems… not going to be 100% complete or accurate.” (traducción) «Así entiendo estos sistemas; la explicación no pretende ser totalmente completa ni exacta».
Del sector
- Google — Fundamentos del SEO para JavaScript — el modelo de app shell y la recomendación de usar la History API en lugar de fragmentos.
- Google — Solucionar problemas de JavaScript relacionados con la Búsqueda — la sección sobre errores soft 404 en SPA y la recomendación de la History API.
- Google — Deprecating our AJAX crawling scheme (2015) — el final formal del rastreo con hash-bang.
- Bing — Serie sobre bingbot: JavaScript, renderizado dinámico y cloaking — la postura de bingbot sobre el renderizado de JS.
- Usar un servicio de prerenderizado para páginas HTML vacías de una SPA no es cloaking (Search Engine Roundtable, 2015) — cobertura de Gary Illyes sobre que prerenderizar SPA no es cloaking. (Paráfrasis de Illyes hecha por SER, fechada en 2015: fuente secundaria.)
- SEO para aplicaciones de página única (Nuxt SEO) — un recorrido por los mismos problemas desde el lado del framework.
- Cómo optimizar aplicaciones de página única para SEO (DebugBear) — el ángulo de renderizado y rendimiento en la indexabilidad de las SPA.
- SPA (glossary) (MDN) — una definición neutral del patrón de aplicación de página única.
Vídeos
- Google Search Central (YouTube) — la serie de SEO en JavaScript de Martin Splitt cubre el renderizado, la History API y los modos de fallo de las SPA que se tratan aquí. Canal
Registro de cambios
Actualizado el 20 ago 2026.
Resumen editorial y detalles registrados del cambio.Detalles del cambio
-
Los detalles del cambio están disponibles actualmente en inglés.
La comparación completa no está disponible: no se archivó una instantánea anterior para esta revisión.
Actualizado el 18 jul 2026.
Resumen editorial y detalles registrados del cambio.Detalles del cambio
-
Los detalles del cambio están disponibles actualmente en inglés.
-
Los detalles del cambio están disponibles actualmente en inglés.
-
Los detalles del cambio están disponibles actualmente en inglés.
-
Los detalles del cambio están disponibles actualmente en inglés.
-
Los detalles del cambio están disponibles actualmente en inglés.
La comparación completa no está disponible: no se archivó una instantánea anterior para esta revisión.