PRIVACIDAD

Tus preferencias.

Rechazar no limita el acceso a la web. Cerrar sin guardar conserva tu decisión anterior.

Técnicas · siempre activas

Guardamos únicamente tu elección durante un máximo de 12 meses en este navegador.

Consultar proveedores, duración y transferencias

Renderizado

JavaScript SEO: buenas prácticas de Google y cómo depurarlas

Enlaces rastreables, History API, canonical, soft 404, noindex y recursos bloqueados: lo que dice Google sobre JavaScript y cómo lo compruebo.

¿Quieres aplicarlo a tu web?Hablemos
ResumenCómo procesa Google el JavaScript y qué no dice
Puntos clave
  • Google solo descubre enlaces que sean <a> con atributo href; un onclick o un routerLink sin href no es fiable para el rastreo.
  • Para el enrutado en cliente, usa la History API. Los fragmentos (#) no sirven para cargar contenido distinto.
  • Una SPA que responde 200 a todo genera soft 404. Redirige a una URL que devuelva 404 o añade noindex cuando el contenido no exista.
  • Si el HTML original lleva noindex, Google puede saltarse el renderizado: no cuentes con que un script lo quite.
  • Bloquear en robots.txt los JS o la API de la que depende el contenido impide que Google renderice la página como la ve un usuario.
  • Se depura comparando el código fuente con el HTML renderizado en la Inspección de URLs, no mirando solo lo que ves en el navegador.

El error más caro que veo con JavaScript no es elegir mal el framework: es un enlace sin href, una ruta que devuelve 200 cuando no existe o un script bloqueado en robots.txt. Son fallos pequeños que dejan páginas sin descubrir, sin indexar o indexadas con contenido de error, y que no aparecen si solo revisas la web en tu navegador.

Aquí no repito la comparativa entre CSR, SSR, SSG e ISR, que ya tienes en mi artículo sobre estrategias de renderizado. Me centro en las buenas prácticas que Google documenta para JavaScript, en cómo se depura y en qué hace mi propia web, que es PHP con HTML renderizado en el servidor y JavaScript solo como mejora.

Tres fases en fila: rastreo con comprobación de robots.txt, renderizado con Chromium sin interfaz e indexación del HTML renderizado.
Las tres fases con las que Google procesa una web con JavaScript. Los fallos que veremos se producen en una de ellas.

Cómo procesa Google el JavaScript y qué no dice

Google describe tres fases: rastreo, renderizado e indexación. Primero comprueba robots.txt; si la URL está bloqueada, Googlebot ni siquiera hace la petición HTTP. Si no lo está, descarga el HTML y extrae los enlaces de los atributos href. Cuando hay recursos disponibles, un Chromium sin interfaz ejecuta el JavaScript, y el HTML resultante se vuelve a analizar en busca de enlaces y se usa para indexar la página.

Ese renderizado lo hace el Web Rendering Service (WRS). Lo documentado es poco y conviene no inflarlo: la página puede esperar en cola «unos segundos», aunque «puede tardar más»; Google no publica un tiempo máximo de ejecución ni un límite numérico de recursos. Lo que sí está escrito son restricciones concretas, que resumo en esta tabla.

Límite o comportamiento del WRSQué implica para ti
No retiene estado entre cargas: ni Local Storage, ni Session Storage, ni cookiesEl contenido no puede depender de algo guardado en una visita anterior
Puede ignorar las cabeceras de cachéSi cambias un script con el mismo nombre, puede ejecutar la versión vieja: versiona los archivos en el nombre
Rechaza peticiones de permisos, como la cámaraEl contenido no debe quedar tras un permiso del navegador
Puede no tener ciertas APIs, como WebGLDetecta la función y ofrece alternativa, o renderiza en servidor
Solo usa HTTP; no admite otras conexiones, como WebSockets o WebRTCDa una vía HTTP para el contenido que cargues por esos canales
Las páginas con estado distinto de 200 pueden no renderizarseUn error con JavaScript posterior no va a «arreglarse» en el render
Dos columnas: límites del renderizado que Google documenta, como la falta de estado y el límite de 2 MB, y datos que no documenta, como el tiempo máximo de ejecución.
Lo que Google documenta sobre el WRS y lo que deja sin especificar.

Separado de esto está el tamaño: Googlebot rastrea los primeros 2 MB de un tipo de archivo compatible, medidos sin comprimir, y cada recurso referenciado (un JS, un CSS) se descarga por separado con su propio límite. Un bundle enorme no cuenta contra el HTML, pero si pasa de ese tamaño Google solo recibe la parte descargada.

Sobre la cola de renderizado, el retraso y qué crawlers de IA ejecutan JavaScript, no lo repito: está en el artículo de CSR, SSR, SSG e ISR.

Enlaces rastreables: <a href> o nada

La regla de Google es literal: solo descubre enlaces que sean elementos <a> con atributo href. Las variantes sin href (<span href>, routerLink, un <a onclick> sin destino) figuran como «no recomendadas», aunque Google pueda intentar interpretarlas. Un href="javascript:goTo('x')" tampoco cumple, porque el href debe resolver en una dirección web real que se pueda solicitar.

Lo que sí vale: insertar enlaces con JavaScript, siempre que el resultado sea un <a href> normal, y mantener un onclick junto a un href válido. Eso permite que el router de tu app intercepte el clic sin quitarle al rastreador el destino.

<!-- Rastreable: href real y JS encima -->
<a href="/productos/zapatillas/" onclick="irA('zapatillas'); return false;">Zapatillas</a>

<!-- No fiable: sin href -->
<span onclick="irA('zapatillas')">Zapatillas</span>
<a onclick="irA('zapatillas')">Zapatillas</a>

<!-- No cumple: el href no es una URL -->
<a href="javascript:irA('zapatillas')">Zapatillas</a>
Cuatro formas de escribir un enlace: a con href real y a con href más onclick marcados como válidos; span con onclick y a con href javascript marcados como no válidos.
Cuatro formas de escribir un enlace y cuáles puede descubrir Google según su documentación.

El texto ancla también cuenta con JavaScript: si lo insertas por script, comprueba en el HTML renderizado que está dentro del enlace. Y si el enlace es una imagen, Google usa su alt; un enlace sin texto y con alt vacío no dice nada.

History API frente a fragmentos con #

Una SPA cambia de «página» sin recargar, y la URL tiene que cambiar con ella. La documentación de Google lo dice sin matices: para que Googlebot pueda analizar y extraer tus URLs, no uses fragmentos para cargar contenido distinto. /#/productos y /#/contacto son para el rastreador una sola URL; con la History API, /productos/ y /contacto/ son dos.

// Fragmento: una sola URL para Google
location.hash = '/productos';

// History API: una URL por vista
history.pushState({}, '', '/productos/');

Con la History API hay una condición más: cada URL tiene que funcionar también cuando alguien la abre directamente o Googlebot la solicita, es decir, el servidor debe responder esa ruta con el contenido (o al menos con la app y un estado correcto). Una ruta que solo existe después de navegar desde la home no es una URL rastreable.

Los fragmentos siguen teniendo su sitio: un #seccion que lleva a un ancla dentro de la misma página es correcto y es lo que usa el índice de este artículo.

Título, meta description y canonical con JavaScript

JavaScript puede establecer el título y la meta description, y Google los usará porque indexa el HTML renderizado. Con el canonical, la recomendación es otra: el HTML es el método preferido, y el JavaScript no debería cambiarlo a una URL distinta de la que ya indica el HTML original. Google dice que recogerá el canonical inyectado al renderizar, pero también que canonicals múltiples o en conflicto pueden dar resultados inesperados.

Esto lleva a un fallo habitual en plantillas: el HTML inicial trae un canonical (a menudo el de la home, por una plantilla) y el script añade otro al montar la vista. Hay dos etiquetas, y no controlas cuál manda. Si usas JavaScript para la metainformación, elimina la del servidor o haz que ambas coincidan siempre. Tienes la parte de canonical sin JavaScript en el artículo sobre canonicals.

ElementoCon JavaScriptMi criterio
Title y meta descriptionSe leen del HTML renderizadoPonlos ya en el HTML del servidor; JS solo para actualizar tras navegar
CanonicalSe recoge el inyectado, pero HTML preferidoUna sola etiqueta, la misma en HTML y en el DOM
JSON-LDGoogle lo admite generado con JS; hay que probarloEn el HTML inicial si puedes; cómo montarlo, en datos estructurados con JSON-LD
Meta robotsSe lee del renderizado, con matices (siguiente sección)No dependas de JS para quitar un noindex

Códigos de estado y soft 404 en una SPA

En una app de cliente, la URL /producto-que-no-existe/ suele devolver el mismo HTML con un 200 y es JavaScript quien pinta el aviso de «no encontrado». Google lo advierte: una SPA que gestiona errores en cliente suele reportar un 200 en lugar del código adecuado. El resultado es una página de error candidata a indexarse; en Search Console aparece como soft 404, y el rastreo se gasta en URLs sin valor.

Google propone dos soluciones, que se pueden combinar: redirigir con JavaScript a una URL que devuelva un 404 real, o añadir una meta robots noindex cuando la API indica que el contenido no existe.

// Opción 1: redirigir a una URL que responde 404 en servidor
fetch('/api/producto/123').then(r => {
  if (r.status === 404) window.location.href = '/no-encontrado/';
});

// Opción 2: añadir noindex al detectar que no hay contenido
const m = document.createElement('meta');
m.name = 'robots'; m.content = 'noindex';
document.head.appendChild(m);

La opción buena es evitar el problema: si el servidor sabe que la ruta no existe, que responda 404 antes de enviar la app. Con SSR o PHP es lo normal; en una SPA pura, no. Para el resto de códigos (301, 410, 5xx) vale lo mismo: el estado lo decide el servidor, y un script no puede cambiar el código de una respuesta ya enviada.

Comparación de una SPA que responde 200 con aviso de no encontrado frente a las tres soluciones: 404 desde el servidor, redirección a una URL 404 y noindex añadido por JavaScript.
Una ruta inexistente que responde 200 se indexa como página de error. Tres formas de evitarlo, de mejor a peor.

noindex y JavaScript: la trampa del HTML original

La regla que más se pasa por alto está en la documentación: cuando Google encuentra noindex, puede saltarse el renderizado y la ejecución de JavaScript. Y la consecuencia directa: si quieres que la página se indexe, no pongas noindex en el código original, ni siquiera con la idea de quitarlo luego con un script. Puede que el script nunca se ejecute.

La dirección contraria sí está documentada: añadir noindex con JavaScript cuando no hay contenido es una de las dos vías oficiales contra los soft 404. Funciona porque Google lee el HTML renderizado. Aun así, para páginas que no quieres indexar y puedes decidirlo en el servidor, es más robusto el noindex en el HTML o en una cabecera X-Robots-Tag, algo que tratamos en el artículo sobre X-Robots-Tag.

Haz esta prueba en cada plantilla: compara la meta robots del código fuente (Ctrl+U) con la del DOM renderizado. Si difieren, tienes un problema o una dependencia de JavaScript que debes decidir a propósito.

robots.txt y recursos JavaScript bloqueados

Google no renderiza JavaScript procedente de archivos bloqueados ni en páginas bloqueadas. Si tu robots.txt tiene un Disallow: /assets/ o Disallow: /static/ por costumbre, Google puede ver la página sin los scripts que construyen el contenido, y lo que indexa se parece poco a lo que ven tus usuarios. Lo mismo ocurre con los endpoints de API de los que depende el contenido: si los bloqueas, el render queda vacío.

Google aplica la regla más específica según la longitud de la ruta, y admite los comodines * y $. Eso te permite bloquear un directorio y permitir los archivos que hacen falta para renderizar, pero lo normal y más seguro es no bloquear nada que sea CSS, JS o la API pública. Los detalles de sintaxis los he tratado en el artículo sobre robots.txt.

Hay un matiz: Google indica que Googlebot y el WRS pueden omitir recursos que no contribuyen al contenido esencial, y recomienda mirar el informe de estadísticas de rastreo para ver qué se pide. Qué recursos concretos omite en cada caso no está documentado.

Lazy-loading y contenido que depende de una interacción

Googlebot no hace scroll ni clics. Por eso Google pide que el lazy-loading cargue todo el contenido relevante cuando sea visible en el viewport, con métodos que no dependan de acciones del usuario: el atributo nativo para imágenes e iframes, la API IntersectionObserver o librerías que carguen al entrar en el viewport. Lo que se carga solo tras un clic en «Ver más» o un evento de scroll puede no verse nunca. Para los iframes, el detalle está en mi guía de lazy loading en iframes sin romper el SEO.

Patrón¿Lo ve Google?Cómo arreglarlo
loading="lazy" en imágenesSí, es un método admitidoNo lo uses en la imagen principal visible al cargar
IntersectionObserver al entrar en viewportSíComprueba que la URL de la imagen acaba en el src
Botón «Cargar más» que pide datos al hacer clicNo es fiableURLs paginadas con enlaces reales
Scroll infinito sin URLs por tramoNo es fiableCada tramo con URL propia, estable y enlazada
Pestañas o acordeones que cargan el texto al pulsarNo es fiableTexto en el HTML, oculto con CSS o details

Para el scroll infinito Google propone dar a cada tramo una URL persistente y única, con contenido estable, numeración absoluta (?page=12, no ?fecha=ayer), enlazar los tramos entre sí y actualizar la URL con la History API cuando un tramo pasa a ser el principal. No lo hagas con el contenido que el usuario ve nada más llegar: retrasarlo penaliza la carga y es perceptible. La parte de rendimiento de ese retraso la tienes en Core Web Vitals.

Cómo depuro una web con JavaScript, paso a paso

La clave es comparar tres versiones de la página: lo que envía el servidor, lo que construye tu navegador y lo que ve Google. Para cada URL con problemas sigo este orden.

  1. Código fuente (Ctrl+U o curl). Es el HTML inicial. Busca el texto principal, los enlaces, el canonical, la meta robots y el JSON-LD. Lo que no esté aquí depende del renderizado.
  2. DOM en DevTools (pestaña Elements). Es lo que ha construido tu navegador tras ejecutar el JavaScript. La diferencia con el paso 1 es el contenido que depende de JS.
  3. Inspección de URLs en Search Console. Con «Probar URL publicada» y «Ver página probada», el panel muestra el HTML renderizado, las cabeceras HTTP, la salida de la consola de JavaScript y los recursos cargados. Para la versión ya indexada se usa «Ver página rastreada».
  4. Consola y errores. Los errores de JavaScript y los recursos que no cargan aparecen en la prueba; un error que rompe el script es una causa típica de contenido ausente.
  5. Búsqueda de una frase exacta. Una frase entre comillas del texto que depende de JS, en Google, indica si ese contenido está en el índice.
Tres columnas comparables: código fuente que envía el servidor, DOM del navegador tras ejecutar JavaScript y HTML renderizado por Google en la Inspección de URLs.
Las tres versiones de una página que se comparan al depurar. La diferencia entre las dos primeras es lo que depende de JavaScript.

Un detalle de la herramienta: la captura de pantalla de la página renderizada solo está disponible en una prueba en tiempo real. Y una limitación mía: la Inspección de URLs prueba una URL cada vez; para auditar muchas a la vez uso un rastreador con renderizado de JavaScript, y compruebo después las URLs dudosas en Search Console, que es la fuente oficial. Para un repaso rápido de cómo comparar el código fuente y el HTML renderizado, tienes la sección de auditoría del artículo de renderizado.

Errores típicos con JavaScript y SEO

Seis tarjetas con errores frecuentes: enlaces sin href, rutas con almohadilla, soft 404, noindex en el HTML original, recursos bloqueados y contenido tras un clic.
Seis errores que se repiten en auditorías de webs con JavaScript.
ErrorSíntomaCorrección
Navegación con onclick y sin hrefSecciones enteras sin descubrir<a href> real
Rutas con #/Una sola URL indexadaHistory API
Todo responde 200Soft 404 en Search Console404 de servidor, redirección o noindex
noindex en el HTML y JS que lo quitaPáginas que no se indexanQuitarlo del HTML original
JS o API bloqueados en robots.txtRender vacío o distintoPermitir recursos de renderizado
Texto que aparece tras un clicContenido que no se indexaTexto en el HTML, no cargado por evento
Dos canonicals (HTML y JS)Canonical elegido por GoogleUna sola etiqueta coherente
Contenido de pago oculto con JSTexto visible en código fuenteServidor envía el texto completo solo con suscripción

El último caso conviene explicarlo: esconder un contenido con JavaScript no es un control de acceso fiable. Google lo indica para los muros de pago: el servidor debería enviar el contenido completo solo cuando se confirma la suscripción.

Renderizado dinámico: por qué ya no es la respuesta

Servir a los bots una versión prerenderizada y a los usuarios la de JavaScript fue una solución habitual. Hoy Google lo describe como un workaround y no como una solución a largo plazo ni recomendada, por la complejidad y los recursos que exige. Sus alternativas son el renderizado en servidor, el renderizado estático o la hidratación. Si tu proyecto todavía depende de un servicio de prerender, tiene sentido planificar la salida, con los criterios del artículo de estrategias de renderizado.

Cómo lo tengo yo: PHP en servidor y JavaScript solo como mejora

Mi web es PHP: cada URL devuelve el HTML completo desde el servidor, con el título, el canonical, la meta robots y el JSON-LD ya en el código fuente. No hay una app de cliente que construya las páginas. El JavaScript lo añade solo como mejora, y esto es lo que he verificado leyendo el código del sitio.

ArchivoDónde se cargaPeso en brutoPara qué sirve
site.jsTodas las páginas2,5 KBMenús y cabecera; los menús son details nativos y funcionan sin JS
cookie-consent.jsTodas las páginas8,2 KBAviso de cookies
post.jsArtículos2,1 KBCopiar enlace y resumen con sección activa
blog.jsListado del blog5,3 KBBúsqueda y filtros sin recargar
Herramientas (analizador de logs, redirecciones, schema visual, radar local)Cada una en su páginaDe 8 a 93 KBLa herramienta en sí

Un artículo como este carga, por tanto, unos 13 KB de JavaScript propio en bruto (site.js, cookie-consent.js y post.js), todo con defer. Son tamaños sin comprimir; no he medido el peso transferido. Las copias versionadas que genero para caché tienen exactamente el mismo tamaño que los originales, así que hoy no minifico ese paso.

Los puntos concretos que cumplen lo de arriba:

  • Enlaces. Los filtros y la paginación del blog son <a href> con URL real; blog.js solo intercepta el clic para evitar la recarga. Sin JavaScript, el clic navega a la misma URL y el servidor devuelve la lista.
  • History API. El mismo script actualiza la URL con history.pushState, no con fragmentos.
  • Canonical y robots desde JS, coherentes con el servidor. Tras filtrar, blog.js actualiza el canonical, la meta robots y el título. El servidor ya emite esos mismos valores para esa URL (noindex, follow con búsqueda o categoría; canonical sin filtros), así que el DOM renderizado y el HTML inicial no se contradicen.
  • Datos de la lista. blog/feed.php, que usa el script, responde JSON con X-Robots-Tag: noindex y Cache-Control: no-store. No está bloqueado en robots.txt, a propósito, porque forma parte de la carga del listado.
  • robots.txt. Bloqueo solo carpetas internas (/includes/, /tools/, /tests/…) y el procesamiento de formularios. Los CSS, JS e imágenes públicos están permitidos.
  • Terceros. En la configuración actual todos los proveedores de analítica y publicidad están desactivados, así que mis páginas no cargan scripts de terceros. Si los activo, se insertarán solo tras el consentimiento.

Lo que no he verificado: cómo trata el WRS el sondeo periódico que hace blog.js para avisar de artículos nuevos (no está documentado) ni cómo se ve cada página en la Inspección de URLs. Lo primero no afecta al contenido, que ya está en el HTML. Lo segundo lo reviso con el procedimiento de la sección anterior.

Cuándo un enfoque con mucho JavaScript sí compensa

Una aplicación con login, un panel o una herramienta no necesita posicionar sus pantallas internas, y ahí el JavaScript del lado del cliente es razonable. El problema aparece cuando una app así convive con páginas que sí quieres posicionar (la home, categorías, fichas, blog) y todas comparten el mismo enfoque. La separación que recomiendo es clara: lo que debe rankear sale con el contenido en el HTML; lo que es interacción, se carga encima. Es el principio de mejora progresiva, y lo que decida el equipo técnico sobre el framework tiene que partir de esa lista de URLs, no de la preferencia del desarrollador.

Checklist de verificación antes de publicar

ComprobaciónCómo
Cada enlace de navegación es <a href>Inspeccionar el DOM o rastrear con JS desactivado
Cada vista tiene su URL sin #Abrir la URL directamente en una ventana nueva
Una ruta inexistente no responde 200curl -I a una URL inventada
Meta robots y canonical iguales en fuente y DOMCtrl+U frente a Elements
Los scripts y la API no están bloqueadosrobots.txt y «Ver página probada»
El texto principal está en el HTML renderizadoInspección de URLs
Nada esencial depende de un clic, scroll o permisoProbar la página sin interactuar
Esquema de cristofercruz.net: el servidor PHP entrega el HTML completo y encima se cargan cuatro scripts pequeños con defer que solo mejoran la experiencia.
Cómo está montada mi web: el contenido llega en el HTML y el JavaScript se carga encima, con defer.

Si quieres que revise cómo se renderiza tu web y qué ve Google de verdad, es una parte habitual de mi auditoría SEO; si lo que necesitas es acompañamiento continuo con tu equipo técnico, lo trabajo en una consultoría SEO, y si prefieres hablarlo antes, puedes escribirme.

Auditoría SEO

Ver auditoría SEO

Preguntas frecuentes

¿Google puede rastrear e indexar JavaScript?

Sí. Renderiza las páginas con un Chromium sin interfaz y usa el HTML renderizado para indexar. Lo que no garantiza es cuándo: la página puede esperar en cola, y hay restricciones documentadas, como la ausencia de estado entre cargas o de algunas APIs.

¿Es obligatorio usar SSR para posicionar?

No es obligatorio, pero Google recomienda el renderizado en servidor, el estático o la hidratación frente al renderizado dinámico. Una web con CSR puede indexarse; lo que gana el HTML servido ya hecho es que no depende del renderizado en cola.

¿Los enlaces con onclick cuentan para SEO?

Solo si además llevan un href válido. Google descubre enlaces que sean <a> con href; un onclick sin destino aparece como no recomendado, aunque Google pueda intentar interpretarlo.

¿Puedo usar # en las URLs de una SPA?

No para cargar contenido distinto. Google indica que no se usen fragmentos para ello y que se emplee la History API. Un fragmento como ancla dentro de una página sí es correcto.

¿Puedo quitar un noindex con JavaScript?

No cuentes con ello. Google puede saltarse el renderizado cuando encuentra noindex en el HTML original, así que el script que lo retira puede no ejecutarse. Si la página debe indexarse, que el HTML original no lleve noindex.

¿Cómo evito los soft 404 en una SPA?

Con una redirección por JavaScript a una URL que devuelva un 404 real o añadiendo una meta robots noindex cuando el contenido no existe. Lo más sólido es que el servidor responda 404 directamente.

¿Cómo veo lo que Google ve de mi JavaScript?

Con la Inspección de URLs de Search Console: «Probar URL publicada» y «Ver página probada» muestran el HTML renderizado, las cabeceras, la consola de JavaScript y los recursos cargados. Compáralo con el código fuente de la página.

Referencias

  1. Understand JavaScript SEO basics Google Search Central
  2. Fix Search-related JavaScript problems Google Search Central
  3. Make your links crawlable Google Search Central
  4. Fix lazy-loaded content Google Search Central
  5. Dynamic rendering as a workaround Google Search Central
  6. Googlebot Google Search Central
  7. Cómo interpreta Google las especificaciones de robots.txt Google Search Central
  8. Herramienta de inspección de URLs Ayuda de Search Console

Sigue leyendo

Rastreo

· 13 min

robots.txt: qué controla, qué no y cómo configurarlo bien

Qué bloquea robots.txt y qué no, cómo se leen los grupos y comodines, qué hacer con los bots de IA y el robots.txt real de mi web, línea a línea.

Leer el artículo: robots.txt: qué controla, qué no y cómo configurarlo bien

Tu próximo paso empieza con una conversación.

Cuéntame qué necesitas conseguir con tu web.

Hablemos de tu proyecto