SEO para Angular

Cómo hacer que las aplicaciones Angular sean rastreables e indexables: @angular/ssr frente a prerenderizado y renderizado híbrido, hidratación, los servicios Title/Meta, el enrutamiento con History API y las pruebas de lo que realmente renderiza Googlebot.

Publicado por primera vez: 26 jun 2026 · Última actualización: 8 ago 2026 · Avanzado
Idiomas

El SEO para Angular se resume en una decisión: servir HTML real en lugar de un shell renderizado en el cliente. Angular moderno (v17+) incorpora SSR a la CLI mediante @angular/ssr: prerenderiza las rutas estáticas, renderiza las dinámicas en el servidor e hidrata para no desechar el HTML. Después aplica lo básico: títulos y descripciones únicos con los servicios Title y Meta de Angular, enrutamiento HTML5 History (nunca URL con hash), enlaces reales <a href> y verificación con URL Inspection. El renderizado dinámico es una solución provisional, no una estrategia, y AngularJS es un framework distinto.

TL;DR — El SEO para Angular es una decisión arquitectónica con muchas consecuencias: introduce HTML real en la respuesta en lugar de un shell renderizado en el cliente. Angular moderno (v17+) incorpora SSR a la CLI mediante @angular/ssr, sucesor integrado y renombrado de Angular Universal. Prerenderiza las rutas estáticas, renderiza las dinámicas en el servidor, combina ambas con renderizado híbrido e hidrata para reutilizar el HTML del servidor en vez de reconstruirlo. Después aborda lo esencial: títulos y descripciones únicos mediante los servicios Title/Meta (o el TitleStrategy del enrutador), enrutamiento HTML5 History —nunca HashLocationStrategy—, enlaces reales <a href>, JSON-LD inyectado de forma segura y verificación con URL Inspection. El renderizado dinámico es una solución provisional reconocida por Google, no una estrategia.

Evidence for this claim Modern Angular documents @angular/ssr as its server-side and hybrid rendering package. Scope: Current Angular documentation; historical Angular Universal details are not required for this claim. Confidence: high · Verified: Angular: Server-side and hybrid rendering

El problema predeterminado: el renderizado del lado del cliente

Una build estándar de Angular envía un index.html cuyo cuerpo es básicamente <app-root></app-root> más etiquetas de script. Sin ejecutar JavaScript, un rastreador no ve encabezados, texto ni enlaces: el contenido se ensambla en el navegador después de cargar el paquete. Es el mismo problema central que describo en SEO de JavaScript: la web dejó atrás el HTML plano y CSR es el extremo más arriesgado de ese espectro.

(Los detalles de implementación de esta guía —API de RenderMode, activadores de hidratación y repetición de eventos— reflejan Angular v22. Las funciones con versión que siguen están fechadas: cambio de nombre de SSR en v17, repetición de eventos en v18 e hidratación incremental en v19–v20.)

CSR crea tres problemas SEO distintos:

  1. Indexación tardía y menos fiable. Google procesa las aplicaciones JS en tres fases —“Crawling, Rendering, and Indexing” (traducción) «rastreo, renderizado e indexación»— y el renderizado se aplaza a una cola. Tu contenido no existe para la indexación hasta que se ejecuta ese renderizado.
  2. Peores Core Web Vitals. El LCP se resiente porque el navegador debe descargar y ejecutar un paquete antes de pintar contenido significativo.
  3. Los rastreadores que no son de Google ven el shell. Bingbot es mucho menos constante al renderizar JS y los bots de redes sociales o de vistas previas de enlaces generalmente no renderizan nada; por eso las etiquetas Open Graph y el contenido inyectado en el cliente nunca les llegan.

Cómo gestiona realmente Googlebot una aplicación Angular

Googlebot es evergreen: renderiza con una versión actual del motor V8 de Chrome y se actualiza junto con las versiones de Chrome. Evidence for this claim Googlebot uses an evergreen version of Chromium for rendering. Scope: Google Search rendering engine; this does not remove application-level rendering risks. Confidence: high · Verified: Google: Evergreen Googlebot Por tanto, puede ejecutar Angular. Pero su comportamiento tiene límites importantes que conviene tener en cuenta:

  • El renderizado se pone en cola, no es inmediato. Google documenta fases distintas de rastreo, renderizado e indexación y no publica un calendario fijo para indicar cuánto tarda el renderizado en ponerse al día con el rastreo. La forma abreviada que usa el sector es «dos oleadas»: primero se indexa el HTML sin procesar (un shell vacío en el Angular CSR) y después el DOM renderizado, cuando se ejecuta ese renderizado. SSR/prerenderizado evita la espera: el HTML está completo en la primera solicitud y no hay que esperar otro pase de renderizado para ese contenido.
  • El renderizador no conserva estado. No mantiene cookies, localStorage ni sessionStorage entre cargas y rechaza solicitudes de permisos. No hagas depender el contenido del estado del cliente.
  • No hace clic ni desplaza la página, y renderiza con una ventana gráfica muy alta. El contenido que queda detrás de una interacción no se verá.
  • Los recursos se almacenan en caché de forma agresiva: usa nombres de archivo con huella de contenido (el main.<hash>.js predeterminado de Angular) para que los paquetes actualizados no se sirvan obsoletos.

@angular/ssr: qué es y cómo cambió el nombre

El renderizado del lado del servidor ejecuta Angular en un servidor Node para cada solicitud y devuelve HTML completamente renderizado, que el navegador después hidrata (conecta los detectores de eventos al DOM existente en lugar de volver a renderizarlo). Todos los rastreadores —Googlebot, Bingbot y los bots sociales— reciben HTML completo en la primera solicitud, sin una segunda oleada.

El nombre confunde a mucha gente, así que conviene ser precisos: «Angular Universal» era la solución histórica de SSR, distribuida como paquete externo (@nguniversal/express-engine). Con Angular v17 (noviembre de 2023), SSR se integró directamente en la CLI y en Application Builder de Angular y pasó a llamarse @angular/ssr; el repositorio de Angular Universal está ahora en modo de mantenimiento. Evidence for this claim Modern Angular documents @angular/ssr as its server-side and hybrid rendering package. Scope: Current Angular documentation; historical Angular Universal details are not required for this claim. Confidence: high · Verified: Angular: Server-side and hybrid rendering La configuración se reduce a una línea:

# New project with SSR enabled
ng new my-app --ssr

# Add SSR to an existing project
ng add @angular/ssr

Esto sustituye al antiguo ng add @nguniversal/express-engine. Es la misma idea, con herramientas de primera clase.

Prerenderizado (generación estática / SSG)

El prerenderizado genera HTML estático para las rutas durante el build, así que ningún servidor se ejecuta en el momento de la solicitud y puedes desplegar en una CDN. Ofrece el TTFB/FCP/LCP más rápido y el menor coste operativo. En v17+ se configura por ruta mediante un archivo de rutas del servidor (app.routes.server.ts) usando RenderMode.Prerender, y outputMode: 'static' produce una aplicación completamente estática.

La limitación es que los datos deben estar disponibles durante el build, no puede haber contenido por usuario y los sitios muy grandes implican builds lentos. Es ideal para páginas de marketing, documentación y entradas de blog: precisamente el contenido que más necesita posicionarse y ser citado.

Renderizado híbrido: elige un modo por ruta

El gran avance de v17+ es que el modo de renderizado se decide por ruta y se configura en app.routes.server.ts:

  • RenderMode.Prerender: rutas estáticas (inicio, acerca de, entradas de blog).
  • RenderMode.Server: rutas dinámicas por solicitud (resultados de búsqueda, paneles con datos recientes).
  • RenderMode.Client: rutas solo internas que de todos modos no quieres indexar (interfaces de administración, pantallas solo para usuarios autenticados). Aquí CSR es realmente adecuado.

Esta es la regla práctica: prerenderiza lo estático, renderiza en el servidor lo que deba estar actualizado y recurre al renderizado del cliente solo para lo que no deba aparecer en el índice.

Hidratación y el problema que arruina el CLS

El SSR ingenuo tiene un defecto: el servidor envía HTML y después el navegador lo desecha y vuelve a renderizar desde cero, lo que provoca un destello y trabajo desperdiciado. La hidratación lo corrige: el navegador restaura la aplicación renderizada por el servidor y reutiliza el DOM coincidente en lugar de destruirlo y recrearlo; solo conecta la interactividad. Actívala en app.config.ts:

provideClientHydration()

Requisito previo: la hidratación es un complemento del lado del cliente para SSR, no un interruptor independiente. provideClientHydration() solo actúa en una ruta que ya se renderiza en el servidor (o está prerenderizada); no puede convertir el shell de una ruta RenderMode.Client en HTML del servidor. Primero activa SSR/prerenderizado para la ruta.

Dos capacidades más recientes importan para la UX relacionada con SEO; ninguna cambia la indexación de Search:

  • Repetición de eventos (v18+): captura las interacciones de usuario compatibles que ocurren antes de que termine la hidratación y las repite cuando finaliza. Reduce los clics perdidos para los tipos de interacción que admite; es una función de UX/interactividad, no de SEO, y no afecta a lo que indexa Google.
  • Hidratación incremental (vista previa para desarrolladores en v19, estable en v20): depende conjuntamente de SSR, hidratación, vistas aplazables y repetición de eventos; no es una función independiente. Usa bloques @defer con activadores de hidratación que controlan qué límites permanecen sin hidratar en el renderizado inicial; un límite hydrate never permanece sin hidratar en la primera carga, pero no necesariamente impide cargar sus dependencias en renderizados posteriores del cliente (por ejemplo, después de cambiar de ruta). El efecto relevante para SEO es indirecto: hidratar menos JavaScript al principio puede ayudar al LCP, pero la configuración de activadores es un detalle de renderizado, no un mecanismo de tiempo del rastreador.

El problema crítico: colocar @if (isPlatformBrowser(...)) directamente en una plantilla hace que el servidor y el cliente rendericen un marcado diferente —un desajuste de hidratación—, lo que produce un desplazamiento del diseño y daña el CLS. Usa afterNextRender() para el trabajo exclusivo del navegador en lugar de ramificar la plantilla según la plataforma.

Títulos y metaetiquetas: los servicios integrados de Angular

Angular no puede enlazar directamente el texto del elemento <title>, así que gestionas las etiquetas del encabezado mediante dos servicios de @angular/platform-browser:

  • Title: setTitle() / getTitle().
  • Meta: addTag(), addTags(), updateTag(), getTag(), removeTag(), con selectores como name='description' o property='og:title'.

Para lo básico no necesitas una biblioteca de terceros. Un servicio SEO compartido es el patrón más limpio:

@Injectable({ providedIn: 'root' })
export class SeoService {
  private title = inject(Title);
  private meta = inject(Meta);

  updatePage(title: string, description: string) {
    this.title.setTitle(title);
    this.meta.updateTag({ name: 'description', content: description });
    this.meta.updateTag({ property: 'og:title', content: title });
  }
}

Para los títulos en particular, el TitleStrategy del enrutador (Angular v14+) permite establecer un title directamente en la configuración de rutas, de modo que el título se actualice automáticamente al navegar, sin código por componente. Y no establezcas document.title = ... manualmente: usa el servicio Title para que funcione correctamente con SSR.

Estructura de las URL

  • Usa el enrutamiento predeterminado de la HTML5 History API (PathLocationStrategy): URL limpias como /products/shoes. Necesita <base href="/"> en index.html.
  • Nunca uses HashLocationStrategy / useHash: true para contenido público. Los identificadores de fragmento posteriores a # se eliminan antes de la solicitud HTTP, así que el servidor nunca los ve y Googlebot no puede resolver #/products de forma fiable; todo el sitio puede reducirse a una sola URL.
  • Enlaces reales <a href> para la navegación, incluso hacia rutas cargadas de forma diferida. El lazy loading está bien para el rendimiento, pero los enlaces a esas rutas siguen teniendo que ser anclas rastreables, no controladores de clic.

Datos estructurados (JSON-LD)

Google admite inyectar JSON-LD con JavaScript. El patrón robusto en Angular consiste en un servicio que crea un <script type="application/ld+json"> y lo añade a document.head, usando el token de inyección DOCUMENT de Angular en lugar de tocar document de forma global (lo que falla en el servidor). Mantén todo el schema en un solo lugar —no lo dividas entre el HTML estático y el DOM renderizado— y valida con Rich Results Test y URL Inspection.

Renderizado dinámico: una solución heredada, no un plan

El renderizado dinámico sirve una versión prerenderizada a los bots (mediante Puppeteer, Rendertron o prerender.io) y la SPA completa a los usuarios. Google es explícito: “dynamic rendering is a workaround and not a long-term solution,” (traducción) «el renderizado dinámico es una solución provisional y no una solución a largo plazo», y existen “better solutions than dynamic rendering” (traducción) «soluciones mejores que el renderizado dinámico», concretamente renderizado del lado del servidor, renderizado estático o hidratación. No es encubrimiento automáticamente siempre que sirvas contenido sustancialmente similar, pero añade otro servidor de renderizado, arriesga divergencias de contenido y no mejora los Core Web Vitals de los usuarios reales. Para una build Angular nueva, opta por @angular/ssr o el prerenderizado.

Errores habituales de SEO en Angular

  1. Enrutamiento con hash (URL con #): todo el sitio parece una sola URL.
  2. No llamar a Title/Meta: todas las páginas comparten título y descripción.
  3. No usar SSR/prerenderizado: el contenido solo existe después de la oleada de renderizado aplazada.
  4. Bloquear .js/.css en robots.txt: Google no puede renderizar y indexa un shell vacío.
  5. Devolver 200 en una vista de página no encontrada: es un soft 404; devuelve un 404 real o añade noindex.
  6. Usar document.title = ... en lugar del servicio Title.
  7. Usar isPlatformBrowser() dentro de @if en la plantilla: desajuste de hidratación → CLS.
  8. Tocar window/localStorage/document en código que se ejecuta en el servidor: el SSR falla.
  9. Dividir el schema entre el HTML sin procesar y el DOM renderizado.
  10. Probar en desarrollo local en vez de usar URL Inspection: confiar en suposiciones y no en Googlebot.

Cómo probar Angular para SEO

  • URL Inspection (Search Console) es la fuente de verdad: la vista de página rastreada muestra el HTML renderizado —el DOM después de que Googlebot ejecuta JavaScript, que es lo que se indexa—, además de mensajes de la consola JS y recursos bloqueados. Ejecuta el Live Test para renderizar bajo demanda.
  • Haz curl a la URL para ver el HTML sin procesar y previo a JS: un shell vacío significa CSR sin SSR.
  • Rich Results Test valida los datos estructurados.
  • Ahrefs Site Audit (con el renderizado JS activado) y Screaming Frog (modo JS) comparan el DOM sin procesar con el renderizado a escala.
  • Lighthouse / PageSpeed Insights miden el impacto en Core Web Vitals de tu elección de renderizado.

Angular no es malo para SEO: simplemente es diferente. Introduce HTML real en la respuesta, gestiona las etiquetas del encabezado, mantén las URL y los enlaces rastreables y deja que las propias herramientas de Google determinen qué se ha renderizado. Este tema acompaña a SEO de JavaScript y a las cuestiones de renderizado de CMS headless de este grupo; la lección subyacente es la misma: el modo de renderizado decide casi todo.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.