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.
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 — Angular construye la página en el navegador con JavaScript de forma predeterminada, así que el HTML sin procesar que ve primero un buscador está casi vacío. La solución es enviar HTML real y terminado mediante el renderizado del lado del servidor integrado de Angular (
@angular/ssr) o el prerenderizado. Después aplica lo básico: un título y una descripción únicos en cada página, URL limpias (sin#) y enlaces reales<a href>.
El problema en una frase
Una aplicación Angular predeterminada envía al navegador un pequeño shell HTML —en esencia, un <div> vacío— junto con un gran paquete de JavaScript. El navegador ejecuta ese JavaScript y después rellena la página con contenido. Eso se denomina renderizado del lado del cliente (CSR).
El problema es que, cuando un buscador descarga esa URL por primera vez, ve el shell vacío. Google puede ejecutar el JavaScript para ver el contenido real, pero lo hace después, en un paso separado y no siempre de forma fiable. Evidence for this claim Google can render JavaScript but processes rendering as a stage after crawling. Scope: Google Search rendering; other crawler behavior is not covered by this record. Confidence: high · Verified: Google: JavaScript SEO basics Otros rastreadores —Bing y los bots que generan vistas previas sociales— a menudo ni siquiera pueden ejecutar JavaScript. Por eso no ven nada.
La solución: enviar HTML terminado
En lugar de hacer que el navegador (o el rastreador) construya la página, la construyes de antemano o en un servidor y envías el HTML completo. Angular ofrece dos vías principales:
- Renderizado del lado del servidor (SSR): un servidor ejecuta Angular para cada solicitud y devuelve la página completa. Es adecuado para contenido que cambia a menudo. Evidence for this claim Angular supports server-side rendering and build-time prerendering through its SSR tooling. Scope: Current Angular SSR and prerender features. Confidence: high · Verified: Angular: Server-side and hybrid rendering
- Prerenderizado: Angular construye archivos HTML estáticos para tus páginas durante el build, de modo que no necesitas un servidor. Es la opción más rápida y funciona muy bien para entradas de blog y páginas de marketing.
Angular moderno (versión 17 en adelante) incorpora ambas opciones directamente en sus herramientas. Se añade con un comando: ng add @angular/ssr. (Tal vez hayas oído el nombre antiguo Angular Universal: era la misma idea como complemento separado. Angular la integró en el núcleo en la v17 y la renombró.)
Los demás aspectos básicos
- Da a cada página un título y una descripción únicos. Angular no lo hace por ti: debes configurarlos en el código mediante sus servicios integrados
TitleyMeta. Sin esto, todas las páginas comparten un título. - Usa URL limpias, no URL con hash. El enrutamiento predeterminado de Angular crea URL como
/products/shoes. Evita el estilo antiguo con «hash» (/#/products): los buscadores no pueden gestionar de forma fiable la parte posterior a#. - Usa enlaces reales. La navegación debe emplear enlaces
<a href>reales, no botones ni controladores de clic, o Google no podrá seguirlos.
El error más común
«Google no puede indexar Angular» es un mito: puede renderizar JavaScript. Pero es más lento y menos fiable que enviar HTML real, y los rastreadores que no son de Google no pueden hacerlo. Por tanto, todo lo que quieras que se encuentre debe renderizarse en el servidor o prerenderizarse.
¿Quieres la versión detallada —renderizado híbrido por ruta, hidratación, los servicios SEO con código y cómo probar lo que realmente ve Google—? Cambia a la pestaña Avanzado.
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 renderingTL;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 serviciosTitle/Meta(o elTitleStrategydel enrutador), enrutamiento HTML5 History —nuncaHashLocationStrategy—, 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.
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:
- 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.
- Peores Core Web Vitals. El LCP se resiente porque el navegador debe descargar y ejecutar un paquete antes de pintar contenido significativo.
- 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,
localStoragenisessionStorageentre 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>.jspredeterminado 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/ssrEsto 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
@defercon activadores de hidratación que controlan qué límites permanecen sin hidratar en el renderizado inicial; un límitehydrate neverpermanece 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 comoname='description'oproperty='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="/">enindex.html. - Nunca uses
HashLocationStrategy/useHash: truepara 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#/productsde 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
- Enrutamiento con hash (URL con
#): todo el sitio parece una sola URL. - No llamar a
Title/Meta: todas las páginas comparten título y descripción. - No usar SSR/prerenderizado: el contenido solo existe después de la oleada de renderizado aplazada.
- Bloquear
.js/.cssenrobots.txt: Google no puede renderizar y indexa un shell vacío. - Devolver
200en una vista de página no encontrada: es un soft 404; devuelve un404real o añadenoindex. - Usar
document.title = ...en lugar del servicioTitle. - Usar
isPlatformBrowser()dentro de@ifen la plantilla: desajuste de hidratación → CLS. - Tocar
window/localStorage/documenten código que se ejecuta en el servidor: el SSR falla. - Dividir el schema entre el HTML sin procesar y el DOM renderizado.
- 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
curla 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.
Resumen de IA
Una síntesis de la versión avanzada:
- El valor predeterminado de Angular es el renderizado del lado del cliente (CSR): un shell HTML casi vacío. Eso implica una indexación tardía o poco fiable, peor LCP y rastreadores que no son de Google (Bing y redes sociales) que no ven nada.
- Googlebot es evergreen y renderiza JS, pero el renderizado se pone en cola («dos oleadas»: primero HTML sin procesar y después DOM renderizado, sin calendario fijo), es sin estado (sin cookies ni almacenamiento), no hace clic ni desplaza la página y aplica una caché agresiva a los recursos. SSR/prerenderizado evita la espera: el HTML está completo en la primera solicitud y no hay que esperar otro pase de renderizado.
@angular/ssres la solución moderna: SSR está integrado en la CLI de Angular desde la v17 y sustituye a Angular Universal (@nguniversal/express-engine), que se mantenía como paquete externo. Configuración:ng new --ssrong add @angular/ssr.- El prerenderizado (HTML estático durante el build) es la opción más rápida y se puede desplegar en una CDN; es ideal para contenido estático de marketing, blogs y documentación, aunque se limita a los datos disponibles durante el build.
- El renderizado híbrido establece el modo por ruta en
app.routes.server.ts:RenderMode.Prerender(estático),RenderMode.Server(dinámico) yRenderMode.Client(páginas internas no indexadas). - La hidratación (
provideClientHydration()) reutiliza el HTML del servidor en una ruta ya SSR/prerenderizada; no crea HTML del servidor para CSR. La repetición de eventos (v18+) captura interacciones compatibles previas a la hidratación y las repite después; la hidratación incremental (vista previa en v19 / estable en v20) depende conjuntamente de SSR, hidratación, vistas aplazables y repetición de eventos, y usa activadores de hidratación de@deferpara controlar qué límites permanecen sin hidratar en la carga inicial (hydrate neverno bloquea cargas posteriores en el cliente). Ninguna cambia la indexación de Search. EvitaisPlatformBrowser()en@ifde la plantilla: provoca un desajuste de hidratación → CLS; usaafterNextRender(). - Usa los servicios integrados
TitleyMeta(no necesitas una biblioteca de terceros); elTitleStrategydel enrutador (v14+) establece automáticamente los títulos por ruta. - Usa el enrutamiento HTML5 History, nunca URL con hash (
#); la navegación debe emplear anclas reales<a href>. El JSON-LD puede inyectarse mediante el tokenDOCUMENT. - El renderizado dinámico es una solución provisional, no una estrategia: Google recomienda SSR, renderizado estático o hidratación.
- Prueba con URL Inspection (HTML renderizado),
curl(HTML sin procesar), Rich Results Test y un rastreador que renderice JS. AngularJS ≠ Angular: los consejos antiguos de AngularJS no se aplican.
Documentación oficial
Documentación de fuentes primarias de Google y Angular.
- Conceptos básicos de SEO de JavaScript: las fases de rastreo → renderizado → indexación, los enlaces rastreables
<a href>, las advertencias sobre enrutamiento con fragmentos, los soft 404s y los datos estructurados inyectados con JS. - Renderizado dinámico (solución provisional): por qué es una solución provisional, el matiz del encubrimiento y las alternativas de SSR, renderizado estático e hidratación.
- Renderizado en la web (web.dev: Addy Osmani y Jason Miller): definiciones canónicas de SSR, CSR, renderizado estático e hidratación, y sus compensaciones de rendimiento.
- Herramienta URL Inspection: cómo ver el HTML renderizado que Google realmente indexa y los mensajes de la consola JS.
Angular
- Angular: renderizado del lado del servidor e híbrido (SSR): la guía oficial de
@angular/ssr, las rutas del servidor yRenderMode. - Angular: servicio
Title:setTitle()/getTitle(). - Angular: servicio
Meta:addTag(),updateTag()y los selectores. - Angular: referencia del enrutador:
PathLocationStrategyfrente al enrutamiento con hash yTitleStrategy. - Presentación de Angular v17 (blog del equipo de Angular): la versión que convirtió SSR en una función de primera clase de la CLI.
- Angular Universal (modo de mantenimiento): el paquete histórico, ahora sustituido por
@angular/ssr.
Citas de la fuente
Declaraciones públicas de Google y de mis propios textos. Cada enlace de buscador es un enlace profundo que salta al pasaje citado de la página de origen.
Google: cómo se procesan las aplicaciones JavaScript
- “Google processes JavaScript web apps in three main phases: 1. Crawling 2. Rendering 3. Indexing.” (traducción) «Google procesa las aplicaciones web JavaScript en tres fases principales: 1. rastreo, 2. renderizado y 3. indexación». — Documentación de Google Search Central. Jump to quote
- “Google can only discover your links if they are <a> HTML elements with an href attribute.” (traducción) «Google solo puede descubrir tus enlaces si son elementos HTML <a> con un atributo href». — Documentación de Google Search Central. Jump to quote
Google: el renderizado dinámico es una solución provisional
- “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 y no una solución a largo plazo para los problemas del contenido generado con JavaScript en los buscadores». — Documentación de Google Search Central. Jump to quote
web.dev: preferir SSR / renderizado estático (Addy Osmani y Jason Miller)
- “Rendering an app on the server to send HTML, rather than JavaScript, to the client.” (traducción) «Renderizar una aplicación en el servidor para enviar HTML, en lugar de JavaScript, al cliente»: la definición del renderizado del lado del servidor. Jump to quote
Patrick Stox (trabajo propio: SEO de JavaScript: una guía definitiva)
- “JavaScript is not bad for SEO, and it’s not evil. It’s just different from what many SEOs are used to.” (traducción) «JavaScript no es malo para el SEO ni es malvado. Simplemente es diferente de lo que muchos profesionales del SEO conocen».
- “Any kind of SSR, static rendering, and prerendering setup is going to be fine for search engines.” (traducción) «Cualquier configuración de SSR, renderizado estático y prerenderizado funcionará bien para los buscadores».
Lista de comprobación de SEO para Angular
Una comprobación rápida para confirmar que una aplicación Angular es rastreable e indexable:
- El contenido público se sirve como HTML real mediante
@angular/ssr(SSR) o prerenderizado, no con CSR completo. - El modo de renderizado se establece por ruta en
app.routes.server.ts: prerenderiza las rutas estáticas, renderiza las dinámicas en el servidor y usa el cliente solo para páginas internas no indexadas. - La hidratación está activada (
provideClientHydration()) y ninguna plantilla usaisPlatformBrowser()dentro de@if(usaafterNextRender()). - Cada página establece un título único (con el servicio
Titleo elTitleStrategydel enrutador) y una descripción única (con el servicioMeta). - Las etiquetas Open Graph / Twitter Card se establecen con el servicio
Metapara las vistas previas sociales. - El enrutamiento usa la HTML5 History API (predeterminada) con
<base href="/">, noHashLocationStrategy/useHash: true. - Toda la navegación usa anclas reales
<a href>, incluidos los enlaces a rutas cargadas de forma diferida. -
robots.txtno bloquea los recursos.jsni.css. - Las vistas de página no encontrada del cliente devuelven un
404real o llevannoindex(sin soft 404s). - No se accede a
window/localStorage/documentdesde código que se ejecuta en el servidor (usa el tokenDOCUMENTo guards de plataforma). - El JSON-LD está en un solo lugar y supera Rich Results Test.
- Has verificado el HTML renderizado en URL Inspection, no solo en el desarrollo local.
Modelos mentales
1. Introduce HTML real en la respuesta. Casi todo problema de SEO en Angular se reduce a una pregunta: ¿el rastreador recibe HTML terminado en la primera solicitud o un shell que tiene que renderizar? SSR y prerenderizado responden «sí». CSR responde «algún día, quizá». Empieza aquí cada auditoría.
2. La regla de decisión de renderizado por ruta.
- Contenido estático (inicio, acerca de, blog, documentación) →
RenderMode.Prerender. - Contenido dinámico que debe estar actualizado (búsqueda, datos en vivo) →
RenderMode.Server. - Páginas internas o autenticadas que no quieres indexar →
RenderMode.Clientes adecuado.
3. «Angular Universal» y «@angular/ssr» son la misma idea en épocas distintas.
Universal era el paquete externo; la v17 absorbió SSR en la CLI y le cambió el nombre. Si usas Angular moderno, necesitas @angular/ssr; el repositorio de Universal está en modo de mantenimiento.
4. Hidrata, no vuelvas a renderizar.
El SSR ingenuo envía HTML y después lo desecha. provideClientHydration() lo reutiliza, pero solo en una ruta que ya sea SSR/prerenderizada: no crea HTML del servidor para una ruta CSR. La repetición de eventos y la hidratación incremental (@defer) reducen el JavaScript inicial. Nunca ramifiques una plantilla con isPlatformBrowser(): así obtienes un desajuste de hidratación y un impacto en CLS.
5. Las etiquetas del encabezado son tu responsabilidad, no la de Angular.
Aquí no hay un Yoast. Establece títulos y descripciones deliberadamente con los servicios Title/Meta (o TitleStrategy), página por página. «Todas las páginas comparten un título» es el fallo predeterminado, no mala suerte.
6. Diseña para un bot sin estado y con URL limpias.
Enrutamiento con History API, enlaces reales <a href>, sin depender de cookies o almacenamiento y sin contenido oculto tras un clic. Después deja que URL Inspection —no tu portátil— te diga qué se ha renderizado.
SEO para Angular: ficha rápida
Modos de renderizado
| Modo | Dónde se construye el HTML | SEO | Ideal para | Configuración de Angular |
|---|---|---|---|---|
| Prerender (SSG) | Durante el build → archivos estáticos | ✅ Mejor | Marketing, blog y documentación estáticos | RenderMode.Prerender |
| SSR | Servidor, por solicitud | ✅ Excelente | Contenido dinámico que debe estar actualizado | RenderMode.Server / @angular/ssr |
| Client (CSR) | En el navegador | ⚠️ Arriesgado | Páginas internas/autenticadas (no indexadas) | RenderMode.Client |
| Dynamic rendering | Servidor de bots separado | Solo solución provisional | Aplicaciones heredadas que no pueden migrar | Puppeteer / Rendertron / prerender.io |
Comandos de configuración
| Objetivo | Comando |
|---|---|
| Proyecto nuevo con SSR | ng new my-app --ssr |
| Añadir SSR a una aplicación existente | ng add @angular/ssr |
| Activar la hidratación | provideClientHydration() en app.config.ts |
Reglas rápidas para encabezado y enrutamiento
- Títulos/descripciones: servicios
Title+Metade Angular (no necesitas una biblioteca de terceros). - Títulos automáticos por ruta:
TitleStrategydel enrutador (v14+) mediante la propiedadtitlede la ruta. - Enrutamiento: HTML5 History API +
<base href="/">. NuncauseHash: true. - Enlaces: anclas reales
<a href>, incluso hacia rutas cargadas de forma diferida. - JSON-LD: inyéctalo mediante el token
DOCUMENTy mantenlo en un solo lugar.
Nombres
- Angular Universal = paquete externo antiguo (
@nguniversal/express-engine), en modo de mantenimiento. @angular/ssr= el mismo SSR, integrado en la CLI desde la v17.- AngularJS (v1.x) ≠ Angular (v2+): son frameworks distintos; los consejos antiguos de AngularJS no se aplican.
Trampas habituales
isPlatformBrowser()en@ifde la plantilla → desajuste de hidratación → CLS. UsaafterNextRender().window/localStorage/documenten el servidor → fallo de SSR.- Bloquear
.js/.cssen robots.txt → Google no puede renderizar.
Comprueba si una aplicación Angular se renderiza realmente en el servidor
La forma más rápida de saber si una URL usa CSR o SSR/prerenderizado es obtener el HTML sin procesar (antes de que se ejecute JavaScript) y buscar tu contenido real. Una aplicación Angular con CSR devuelve un <app-root> casi vacío; una aplicación con SSR/prerenderizado devuelve marcado terminado.
macOS / Linux
# Raw HTML as the server sends it — this is the "first fetch", pre-JS
curl -sL -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
https://example.com/page/ -o raw.html
# Is your real headline in the raw HTML? Empty result = CSR with no SSR
grep -o "Your headline text" raw.html
# A near-empty <app-root> is the tell-tale CSR signature
grep -o "<app-root></app-root>" raw.htmlWindows (PowerShell)
$ua = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
Invoke-WebRequest -Uri "https://example.com/page/" -UserAgent $ua -OutFile raw.html
Select-String -Path raw.html -Pattern "Your headline text"
Select-String -Path raw.html -Pattern "<app-root></app-root>"Si falta el titular y ves un <app-root> desnudo, el contenido depende del renderizado: añade SSR o prerenderizado. (Para el DOM renderizado, usa «View Crawled Page → rendered HTML» de URL Inspection; un curl normal no puede ejecutar JS.)
Confirma que no bloqueas el JS/CSS de Angular en robots.txt
macOS / Linux
curl -sL https://example.com/robots.txt | grep -iE "disallow.*\.(js|css)|Disallow:\s*/(assets|dist|main)"Un Disallow que coincida con tu paquete significa que Google no puede renderizar bien la página: casi siempre es un error.
Herramientas para depurar el SEO de Angular
- URL Inspection (Google Search Console): la fuente de verdad. Ejecuta un Live Test y lee el HTML renderizado (el DOM después de que Googlebot ejecute tu JS de Angular), la captura de pantalla, los recursos de la página (qué se cargó y qué se bloqueó) y los mensajes de la consola JavaScript.
- Rich Results Test: confirma que tu JSON-LD aparece en la salida renderizada después de cualquier cambio de renderizado.
curl: obtiene el HTML sin procesar y previo a JS para distinguir CSR (<app-root>vacío) de SSR/prerenderizado.- Ahrefs Site Audit (con renderizado JS activado): rastrea con Chrome sin interfaz y compara el DOM sin procesar con el renderizado, mostrando a escala metadatos ausentes, canonicals rotas y problemas de indexabilidad.
- Screaming Frog SEO Spider (modo de renderizado JS): compara el contenido sin procesar con el renderizado por URL.
- Lighthouse / PageSpeed Insights: mide el impacto en Core Web Vitals de tu estrategia de renderizado (CSR frente a SSR y prerenderizado).
- Angular CLI / DevTools: confirma que
outputMode, las rutas del servidor y la hidratación del build están configurados como esperas.
Recursos que merecen tu tiempo
Mis textos relacionados
- SEO de JavaScript: una guía definitiva: mi guía completa sobre renderizado, paridad del DOM, la regla de la directiva más restrictiva y qué configuraciones de renderizado son seguras. El SEO para Angular es una aplicación específica de todo esto.
- Guía para principiantes de SEO técnico: dónde encajan el renderizado y el rastreo en el panorama general.
Mis charlas
- SEO de JavaScript: Ungagged 2019 (SlideShare): comportamiento de renderizado sin estado de Googlebot, ventana gráfica y caché, además de los enfoques de renderizado de aquella época. (Aviso permanente: la recomendación de renderizado dinámico de esa presentación está desactualizada; Google lo ha llamado desde entonces una solución provisional.)
Del sector
- Renderizado en la web (web.dev): texto de referencia de Addy Osmani y Jason Miller sobre las compensaciones entre SSR, CSR, renderizado estático e hidratación.
- Renderizado del lado del servidor e híbrido (SSR) (angular.dev): guía oficial de
@angular/ssrsobre rutas del servidor,RenderMode, prerenderizado e hidratación. - Presentación de Angular v17 (blog del equipo de Angular): la versión que convirtió SSR en una función de primera clase de la CLI e introdujo el paquete
@angular/ssr. - Angular Universal (modo de mantenimiento) (GitHub): paquete histórico de SSR que sustituyó
@angular/ssr, útil para entender el cambio de nombre. - Guía de SEO para Angular (Search Engine Journal, Jamie Indigo): el recorrido clásico de indexación en dos oleadas; sólido en los fundamentos, aunque anterior al cambio de nombre de v17.
- La guía de SSR para Angular (Angular Architects, Alexander Thalhammer, marzo de 2025): guía de configuración de SSR con mucho código; compara los ejemplos con la documentación oficial de
@angular/ssrpara detectar cambios de API posteriores a su publicación. - r/TechSEO: la comunidad para depurar el renderizado y la indexación.
Antipatrones de SEO en Angular
Errores concretos que veo repetidamente en aplicaciones Angular; cada uno es un hábito que merece una comprobación directa, no solo un riesgo teórico.
Publicar una build solo con CSR y darla por terminada
La salida predeterminada de ng new no tiene SSR ni prerenderizado. Es la forma más rápida de iniciar un proyecto y la más fácil de acabar con un <app-root> vacío en la primera solicitud. Por qué está mal: el HTML sin procesar que ve un rastreador no contiene nada, así que la indexación depende por completo del pase de renderizado aplazado de Google y los demás bots no tienen una segunda oportunidad. Qué hacer: añade @angular/ssr al principio del proyecto (ng new my-app --ssr) o ejecuta ng add @angular/ssr en uno existente antes de publicar algo que quieras que se encuentre.
Usar HashLocationStrategy para rutas públicas
El enrutamiento con hash (useHash: true, URL como /#/products/shoes) sigue siendo el predeterminado en algunos tutoriales y plantillas antiguas de Angular. Por qué está mal: todo lo posterior a # se elimina en el cliente antes de que la solicitud llegue al servidor, así que el servidor y Googlebot solo ven una URL para toda la aplicación. Qué hacer: usa el enrutamiento predeterminado de la HTML5 History API (PathLocationStrategy) con <base href="/"> en index.html.
Ramificar una plantilla con isPlatformBrowser()
Envolver contenido en @if (isPlatformBrowser(platformId)) parece la forma obvia de proteger el código exclusivo del navegador. Por qué está mal: el servidor renderiza una rama y el cliente la otra durante la hidratación, lo que produce un desajuste de hidratación; Angular tiene que reconciliar la diferencia y el resultado visible es un desplazamiento del diseño que aparece como CLS. Qué hacer: usa afterNextRender() para el trabajo exclusivo del navegador, de modo que la propia plantilla se renderice igual en servidor y cliente.
Establecer document.title directamente en lugar de usar el servicio Title
Funciona en el desarrollo local, así que es un atajo tentador. Por qué está mal: el acceso directo al DOM como document.title = '...' no funciona bien con SSR; el servidor no tiene un document global en el mismo sentido que el navegador y pierdes las ventajas de integración con el enrutador de la gestión de títulos propia de Angular. Qué hacer: inyecta el servicio Title de Angular (setTitle()) o configura los títulos por ruta con el TitleStrategy del enrutador.
Tocar window, localStorage o document en código que se ejecuta durante SSR
Un componente o servicio que lee localStorage o comprueba window.innerWidth durante su construcción funciona en el navegador y bloquea el renderizado del servidor. Por qué está mal: ninguno de esos globales existe en el proceso de servidor Node que ejecuta tu build SSR, así que el renderizado lanza un error y la solicitud devuelve un 500s o vuelve silenciosamente a una respuesta vacía. Qué hacer: protege ese código con afterNextRender() o inyecta el token DOCUMENT de Angular en lugar del global, y prueba la build SSR localmente (ng build + servir la salida SSR), no solo con ng serve.
Tratar el renderizado dinámico como una solución permanente
Levantar Puppeteer o un servicio como Rendertron para servir una instantánea prerenderizada a los bots resuelve el síntoma inmediato. Por qué está mal: es un sistema adicional que mantener, puede desviarse de lo que ven los usuarios reales y Google ha dicho directamente que es una solución provisional, no una solución a largo plazo. Qué hacer: migra a @angular/ssr o prerenderizado para que cada solicitante —bot o persona— reciba el mismo HTML real desde la misma canalización.
¿Qué modo de renderizado debe usar esta ruta?
Angular v17+ permite establecer el modo de renderizado por ruta en app.routes.server.ts. La pregunta no es «¿SSR o prerenderizado?» para toda la aplicación, sino esta misma pregunta para cada ruta.
Choosing a rendering mode for an Angular route
Prompts para tareas de SEO en Angular
Prompts listos para copiar para las tareas específicas de SEO en Angular de este artículo. Pega la entrada descrita y comprueba la salida con tu propio criterio: ahorran tiempo en las partes mecánicas, pero no sustituyen las pruebas con URL Inspection.
1. Comparar el HTML sin procesar con el renderizado de una ruta
Pega la salida de curl -sL <url> (HTML sin procesar) y el panel «rendered HTML» de un Live Test de URL Inspection (o de un rastreo con renderizado JS de Ahrefs/Screaming Frog) para la misma URL.
Here is the raw HTML for [URL] (fetched with curl, before JavaScript runs):
[paste raw HTML]
Here is the rendered HTML for the same URL (from Google Search Console URL
Inspection's Live Test, or a JS-rendering crawler):
[paste rendered HTML]
Compare the two. List: (1) content present in rendered but missing from raw —
this is what depends on client-side rendering, (2) any <title>, meta description,
or JSON-LD that differs between the two versions, (3) whether the raw HTML shows
a near-empty <app-root> (a sign of CSR with no SSR/prerendering).Devuelve una lista breve de lo que depende de CSR y de cualquier divergencia de título/meta/schema entre las versiones sin procesar y renderizada: son las dos cosas que merece la pena corregir primero.
2. Revisar una implementación de servicio Title/Meta
Pega tu SeoService de Angular (o equivalente) que llama a los servicios Title y Meta.
Here is an Angular service that sets page titles and meta tags:
[paste the service's TypeScript code]
Check it against these rules: (1) titles are set via the Title service's
setTitle(), never document.title directly, (2) description is set with
meta.updateTag({ name: 'description', ... }) not addTag() (which can duplicate
the tag on repeat calls), (3) Open Graph tags use the property selector, not
name, (4) nothing in this code reads window/localStorage/document directly in a
way that would break during SSR. Flag any line that violates one of these and
suggest the fix.Devuelve un aprobado/fallo línea por línea según esas cuatro reglas, con un fragmento corregido para todo lo que señales.
3. Auditar errores de modo de renderizado en app.routes.server.ts
Pega tu archivo de configuración de rutas del servidor.
Here is my Angular app.routes.server.ts, which sets RenderMode per route:
[paste the file]
For each route, tell me: is RenderMode.Prerender used on anything that depends
on per-request or per-user data (a mistake — it would bake stale/wrong data into
the static build)? Is RenderMode.Client used on anything that looks like public,
indexable content (a missed-SEO-opportunity)? Is RenderMode.Server used on fully
static content where Prerender would be faster and cheaper? List each route with
its current mode and whether it matches the decision rule: static data →
Prerender, must-be-fresh → Server, non-indexed internal → Client.Devuelve un veredicto por ruta y marca cualquiera cuyo RenderMode no coincida con lo que realmente necesita.
Ponte a prueba: SEO para Angular
Cinco preguntas rápidas sobre cómo hacer que las aplicaciones Angular sean rastreables e indexables. Elige una respuesta para cada una y después comprueba el resultado.
Registro de cambios
Actualizado el 8 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 17 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.
La comparación completa no está disponible: no se archivó una instantánea anterior para esta revisión.