tutoriales.com

¡Libera el Poder de Angular Universal! 🚀 Renderizado del Lado del Servidor (SSR) y Static Site Generation (SSG)

Este tutorial profundiza en Angular Universal, una tecnología clave para implementar Server-Side Rendering (SSR) y Static Site Generation (SSG) en aplicaciones Angular. Aprenderás a configurar, desarrollar y desplegar aplicaciones Universal para mejorar el SEO, el rendimiento y la experiencia del usuario. Incluye ejemplos prácticos y consejos avanzados.

Intermedio18 min de lectura9 views
Reportar error

Angular, por defecto, funciona como una aplicación de Single Page Application (SPA), lo que significa que la mayor parte del renderizado ocurre en el navegador del cliente. Si bien esto ofrece una experiencia de usuario fluida y reactiva, presenta desafíos significativos para el SEO (Search Engine Optimization) y el tiempo de carga inicial. Aquí es donde entra en juego Angular Universal.

Angular Universal es la tecnología que permite a tu aplicación Angular ejecutarse en un servidor, generando HTML estático que luego se envía al navegador. Esto no solo mejora drásticamente el tiempo de primera pintura (First Contentful Paint) sino que también hace que tu aplicación sea indexable por los motores de búsqueda, lo que es crucial para el SEO. En este tutorial, exploraremos a fondo Angular Universal, cubriendo tanto el Server-Side Rendering (SSR) como el Static Site Generation (SSG), y te guiaremos paso a paso para implementarlo en tus proyectos.

🎯 ¿Qué es Angular Universal y Por Qué es Crucial?

Angular Universal es una suite de herramientas que permite a las aplicaciones Angular renderizarse en el servidor. Tradicionalmente, una aplicación Angular (SPA) entrega un index.html casi vacío al navegador, que luego descarga los archivos JavaScript y construye dinámicamente el DOM. Para un motor de búsqueda, esto significa que la página inicial tiene muy poco contenido relevante para indexar.

Con Angular Universal, cuando un usuario (o un bot de búsqueda) solicita tu aplicación, el servidor ejecuta la aplicación Angular, renderiza la vista inicial en HTML y envía ese HTML ya renderizado al navegador. Una vez en el navegador, Angular toma el control mediante un proceso llamado hidratación, reusando la estructura DOM existente en lugar de recrearla, lo que garantiza una transición fluida a la aplicación interactiva de cliente.

💡 Beneficios Clave de Angular Universal

Los beneficios de adoptar Angular Universal son múltiples y significativos:

  • Mejora del SEO: Los motores de búsqueda pueden rastrear e indexar fácilmente el contenido completo de tu aplicación, ya que reciben un HTML pre-renderizado. Esto es fundamental para la visibilidad en línea.
  • Rendimiento Superior: El tiempo de carga inicial se reduce drásticamente porque el navegador recibe una página con contenido visible de inmediato, mejorando métricas como First Contentful Paint (FCP) y Largest Contentful Paint (LCP).
  • Mejor Experiencia de Usuario (UX): Los usuarios ven contenido rápidamente, lo que reduce la percepción de lentitud y mejora la retención.
  • Accesibilidad para Dispositivos y Navegadores Antiguos: Incluso si el JavaScript no se carga o ejecuta correctamente (por ejemplo, en navegadores muy antiguos o con conexiones lentas), el usuario aún puede ver el contenido principal de la página.
  • Previsualizaciones en Redes Sociales: Las tarjetas de redes sociales (Open Graph, Twitter Cards) pueden renderizarse correctamente porque los crawlers de estas plataformas también obtendrán el HTML pre-renderizado con meta-etiquetas adecuadas.
🔥 Importante: Aunque Universal mejora el rendimiento inicial, no es una bala de plata. La aplicación sigue siendo una SPA en el cliente después de la hidratación. Debes seguir aplicando buenas prácticas de optimización para el rendimiento general.

🛠️ Configuración Inicial de Angular Universal

Para empezar con Angular Universal en un proyecto existente, el CLI de Angular nos facilita enormemente la tarea. Si estás creando un proyecto nuevo, puedes añadirlo desde el principio. Para este tutorial, asumiremos que ya tienes un proyecto Angular.

Paso 1: Añadir Angular Universal al Proyecto

Abre tu terminal en la raíz de tu proyecto Angular y ejecuta el siguiente comando:

ng add @nguniversal/express-engine

Este comando hace varias cosas importantes:

  1. Instala los paquetes necesarios: @nguniversal/express-engine y sus dependencias.
  2. Modifica tu archivo angular.json para añadir una configuración de build y serve para el lado del servidor.
  3. Crea un archivo src/main.server.ts, que es el entry point de tu aplicación para el servidor.
  4. Crea un archivo src/app/app.module.ts (si no existe) y src/app/app.server.module.ts.
  5. Crea o modifica server.ts en la raíz de tu proyecto, que actuará como el servidor Node.js que renderizará tu aplicación.

Después de ejecutar el comando, tu angular.json tendrá una sección server bajo architect para tu proyecto, y una sección prerender si también quieres generar sitios estáticos.

🔎 Contenido de `angular.json` (ejemplo de configuración SSR)
{
  "projects": {
    "my-angular-app": {
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": { /* ... */ }
        },
        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/my-angular-app/server",
            "main": "src/main.server.ts",
            "tsConfig": "tsconfig.server.json",
            "inlineStyleLanguage": "scss"
          },
          "configurations": {
            "production": {
              "outputHashing": "media"
            },
            "development": {
              "optimization": false,
              "sourceMap": true,
              "extractAdditionalSources": false,
              "namedChunks": true
            }
          },
          "defaultConfiguration": "production"
        },
        "serve-ssr": {
          "builder": "@nguniversal/builders:ssr-dev-server",
          "options": {
            "browserTarget": "my-angular-app:build",
            "serverTarget": "my-angular-app:server"
          },
          "configurations": {
            "development": {
              "browserTarget": "my-angular-app:build:development",
              "serverTarget": "my-angular-app:server:development"
            },
            "production": {
              "browserTarget": "my-angular-app:build:production",
              "serverTarget": "my-angular-app:server:production"
            }
          },
          "defaultConfiguration": "development"
        },
        "prerender": {
          "builder": "@nguniversal/builders:prerender",
          "options": {
            "routesFile": "prerender.routes.txt"
          },
          "configurations": {
            "production": {
              "browserTarget": "my-angular-app:build:production",
              "serverTarget": "my-angular-app:server:production"
            },
            "development": {
              "browserTarget": "my-angular-app:build:development",
              "serverTarget": "my-angular-app:server:development"
            }
          },
          "defaultConfiguration": "production"
        }
      }
    }
  }
}

Paso 2: Entendiendo los Archivos Generados

  • src/main.server.ts: Este archivo es el entry point de tu aplicación para el entorno del servidor. Exporta AppServerModule, que es un NgModule específico para el servidor.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';

const bootstrap = () => bootstrapApplication(AppComponent, config);

export default bootstrap;
  • src/app/app.config.server.ts: Define la configuración del proveedor para el entorno del servidor. Incluye provideServerRendering().
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';

const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering()
]
};

export const config = mergeApplicationConfig(appConfig, serverConfig);
  • server.ts: Este es el archivo del servidor Node.js (usando Express por defecto) que se encargará de recibir las peticiones HTTP, renderizar tu aplicación Angular y devolver el HTML. Es personalizable y puedes añadir tu propia lógica de backend aquí.
import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import bootstrap from './src/main.server';

// The Express app is exported so that it can be used by serverless functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/my-angular-app/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index.html';

const commonEngine = new CommonEngine();

server.set('view engine', 'html');
server.set('views', distFolder);

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));

// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;

commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: distFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});

return server;
}

function run(): void {
const port = process.env['PORT'] || 4000;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

// Webpack will replace 'require' with '__webpack_require__' Do not remove this comment.
const mainModule = require.main;
if (mainModule && mainModule.filename === __filename) {
run();
}

Paso 3: Ejecutar la Aplicación en Modo SSR (Desarrollo)

Para probar tu aplicación con SSR en desarrollo, usa el siguiente comando:

npm run dev:ssr
# O si prefieres usar ng run directamente:
ng run my-angular-app:serve-ssr

Esto construirá tanto tu aplicación cliente como la parte del servidor, y luego iniciará un servidor Node.js que servirá tu aplicación con SSR. Abre tu navegador en http://localhost:4000 (o el puerto que se indique en la consola) y verás la página cargada con contenido HTML directamente, incluso antes de que el JavaScript del cliente se ejecute.

💡 Consejo: Para verificar que SSR está funcionando, deshabilita JavaScript en tu navegador o usa la vista de "código fuente" de la página. Deberías ver el contenido de tu aplicación dentro del HTML.

🔄 Server-Side Rendering (SSR) en Profundidad

El SSR es el corazón de Angular Universal. Consiste en renderizar la aplicación Angular en el servidor para generar una versión HTML completa antes de enviarla al cliente. Esta es la técnica más común y dinámica.

📌 Ciclo de Vida del SSR

  1. Petición del Cliente: El navegador (o un bot) solicita una URL de tu aplicación.
  2. Servidor Node.js: El servidor (ej: Express) intercepta la petición.
  3. Ejecución de Angular: El servidor ejecuta la versión server-side de tu aplicación Angular.
  4. Renderizado a HTML: Angular genera el HTML de la vista inicial de la ruta solicitada.
  5. Envío al Cliente: El servidor envía el HTML pre-renderizado al navegador.
  6. Carga y Ejecución del Cliente: El navegador carga el HTML, los CSS y los bundles JavaScript de la aplicación Angular.
  7. Hidratación: Angular en el cliente detecta que hay HTML pre-existente, lo "hidrata" (reusa el DOM existente y adjunta event listeners) y toma el control de la aplicación, comportándose como una SPA normal.
Navegador / Bot Servidor Node.js (Universal) Aplicación Angular (Server-side) HTML Renderizado Servidor Node.js (Universal) HTML al Navegador Carga JS / CSS Hidratación de Angular Aplicación Angular Interactiva (Client-side)

Consideraciones al Desarrollar con SSR

Cuando desarrollas para SSR, hay varias cosas que debes tener en cuenta, ya que el entorno del servidor es diferente al del navegador.

  • Acceso al DOM y window/document: En el servidor no existe window ni document global. Si tu código intenta acceder a estas APIs directamente, causará errores. Usa inyecciones de PLATFORM_ID o DOCUMENT de @angular/common para verificar si estás en el navegador (isPlatformBrowser) o en el servidor (isPlatformServer) antes de ejecutar código específico del cliente.
import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Component({
selector: 'app-my-component',
template: '<p>Este es mi componente</p>'
})
export class MyComponent implements OnInit {
constructor(@Inject(PLATFORM_ID) private platformId: Object) { }

ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Este código solo se ejecutará en el navegador
console.log('Estoy en el navegador');
document.title = 'Título de la página';
} else {
// Este código solo se ejecutará en el servidor
console.log('Estoy en el servidor');
}
}
}
  • Estados de Componentes: Asegúrate de que tus componentes no dependan de un estado que solo esté disponible después de la carga completa de JavaScript. El HTML inicial debe ser consistente con la vista que se mostrará. Usa TransferState (ver más adelante) para transferir datos del servidor al cliente.

  • APIs del Servidor: Puedes aprovechar el entorno del servidor para realizar operaciones que normalmente no harías en el cliente, como acceder a un sistema de archivos local (si tu servidor lo permite y está bien asegurado) o consumir servicios internos sin exponerlos públicamente.

  • Precarga de Datos (Data Preloading): Es fundamental que los datos que necesita tu componente para renderizarse inicialmente estén disponibles antes de que Angular intente renderizarlo en el servidor. Esto se logra generalmente a través de los Resolver de Angular Router o de llamadas a APIs dentro de ngOnInit (asegurándote de que sean promesas o Observables que se resuelvan).

// Ejemplo de Resolver para precargar datos
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class PostResolver implements Resolve<any> {
constructor(private postService: PostService) { }

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
const postId = route.paramMap.get('id');
return this.postService.getPost(postId);
}
}

// En tu configuración de rutas:
const routes: Routes = [
{
path: 'post/:id',
component: PostDetailComponent,
resolve: { post: PostResolver }
}
];

Optimización con TransferState

Uno de los problemas comunes con SSR es la doble llamada a la API. Si un componente hace una llamada HTTP en ngOnInit para obtener datos, esta llamada se ejecutará tanto en el servidor (para renderizar el HTML) como nuevamente en el cliente (cuando Angular se hidrata y ngOnInit vuelve a ejecutarse). Esto es ineficiente y puede causar flashes de contenido si los datos del cliente tardan en llegar.

TransferState de @angular/platform-browser resuelve este problema. Permite serializar y transferir datos desde la aplicación del servidor al cliente, de modo que el cliente pueda reutilizarlos sin necesidad de volver a solicitarlos.

  1. Proveedores en el Servidor: Asegúrate de que BrowserTransferStateModule y ServerTransferStateModule estén importados en sus respectivos módulos (AppModule y AppServerModule). El ng add ya suele configurarlo.

  2. Usar TransferState en tu Servicio/Componente:

import { Injectable, makeStateKey, TransferState } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

const POST_KEY = makeStateKey<any>('postData');

@Injectable({ providedIn: 'root' })
export class PostService {
constructor(private http: HttpClient, private transferState: TransferState) { }

getPost(id: string): Observable<any> {
// Intenta obtener del estado transferido primero
const storedPost = this.transferState.get(POST_KEY, null);
if (storedPost) {
return of(storedPost);
}

// Si no está en el estado transferido, haz la llamada HTTP
return this.http.get(`/api/posts/${id}`).pipe(
tap(post => {
// Guarda los datos en el estado transferido solo en el servidor
this.transferState.set(POST_KEY, post);
})
);
}
}

Con TransferState, la llamada HTTP solo se realiza una vez en el servidor. Los datos se incrustan en el HTML resultante y luego se recuperan en el cliente, evitando la doble petición.

⚡ Static Site Generation (SSG) con Angular Universal

Mientras que SSR renderiza la página bajo demanda en el servidor para cada petición, Static Site Generation (SSG) genera todas las páginas posibles de tu aplicación en HTML estático durante el tiempo de compilación (build time). Estos archivos HTML se pueden servir desde un CDN, lo que ofrece un rendimiento y una escalabilidad increíbles.

SSG es ideal para sitios con contenido que no cambia con frecuencia, como blogs, sitios de documentación, portafolios, páginas de marketing, etc. Para contenido altamente dinámico (como un panel de usuario personalizado), SSR es la opción más adecuada.

📌 ¿Cómo Funciona SSG con Angular Universal?

El proceso es similar al SSR, pero en lugar de renderizar en respuesta a una petición HTTP, Angular Universal utiliza una lista de rutas para visitar cada una de ellas y guardar el HTML resultante como un archivo estático en la carpeta dist.

  1. Definir Rutas para Pre-renderizar: Necesitas decirle a Universal qué rutas deben ser pre-renderizadas. Esto se hace típicamente en un archivo prerender.routes.txt o mediante una función que genera las rutas.
  2. Ejecutar el Comando de Prerrenderizado: El CLI de Angular tiene un comando específico para esto.
  3. Generación de Archivos Estáticos: Angular compila tu aplicación, ejecuta la versión del servidor para cada ruta especificada y guarda el HTML de cada una en una carpeta de salida.
  4. Despliegue: Despliegas estos archivos HTML estáticos (junto con los bundles JS y CSS de Angular) en un servidor web estático o CDN.

Implementación de SSG

El comando ng add @nguniversal/express-engine ya añadió una configuración prerender a tu angular.json.

  1. Crear el archivo prerender.routes.txt: En la raíz de tu proyecto, crea un archivo llamado prerender.routes.txt y lista las rutas que quieres pre-renderizar, una por línea.
/
/about
/blog
/blog/post-1
/blog/post-2
<div class="callout tip">💡 <strong>Consejo:</strong> Para rutas dinámicas (como `/blog/:id`), deberás generar estas rutas programáticamente. Puedes crear un script Node.js que lea de tu API de blogs, por ejemplo, y genere este archivo `prerender.routes.txt` antes de ejecutar el comando `prerender`.</div>

2. Ejecutar el comando de Prerrenderizado:

ng run my-angular-app:prerender --routesFile prerender.routes.txt
O simplemente:
npm run prerender
Este comando compilará tu aplicación para el navegador y el servidor, y luego ejecutará el proceso de prerrenderizado. Los archivos HTML resultantes se encontrarán en la carpeta `dist/my-angular-app/browser` (o donde esté configurado tu `outputPath` para el navegador), dentro de subcarpetas que corresponden a tus rutas (ej: `dist/my-angular-app/browser/blog/post-1/index.html`).

<div class="callout warning">⚠️ <strong>Advertencia:</strong> El `ng add` para Universal añade `index.original.html` para el servidor. Asegúrate de que tu `prerender` genere los `index.html` correctos en las subcarpetas. A veces es necesario ajustar la configuración o el proceso si hay conflictos.</div>

SSG vs. SSR: ¿Cuándo Usar Cuál?

CaracterísticaServer-Side Rendering (SSR)Static Site Generation (SSG)
---------
Momento de RenderizadoBajo demanda, por cada petición del usuarioEn tiempo de compilación (build time)
Uso IdealContenido dinámico y personalizado (ej: dashboard de usuario, tiendas online con stock variable)Contenido estático o que cambia poco (ej: blogs, documentación, sitios de marketing)
---------
RendimientoRápido, pero depende de la carga del servidorExtremadamente rápido, servido desde CDN
EscalabilidadRequiere gestión de servidores, puede escalar horizontalmenteAltamente escalable, coste bajo (servidor de archivos estáticos)
---------
Tiempo de BuildRápido, solo se construye una vez la aplicaciónPuede ser largo si hay muchas páginas a pre-renderizar
ComplejidadMayor, necesita un servidor Node.js en producciónMenor, solo se requiere un servidor de archivos estáticos
📌 Nota: Es posible combinar SSR y SSG en la misma aplicación. Por ejemplo, pre-renderizar las páginas estáticas de tu blog y usar SSR para el perfil de usuario. Esto se logra configurando tu servidor Express para servir los archivos estáticos pre-renderizados primero, y si una ruta no se encuentra, entonces invocar al motor de SSR.

🛡️ Desafíos Comunes y Soluciones con Angular Universal

Implementar Angular Universal puede presentar algunos desafíos. Aquí te presento los más comunes y cómo abordarlos.

1. Acceso a APIs Específicas del Navegador

Como mencionamos, window, document, localStorage, sessionStorage no existen en el entorno Node.js del servidor. Acceder a ellos directamente causará errores.

Solución:

Usa PLATFORM_ID para determinar si el código se está ejecutando en el navegador. También puedes usar inyectables como DOCUMENT de @angular/common para acceder al DOM de forma segura.

import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, DOCUMENT } from '@angular/common';

@Component({ /* ... */ })
export class MySafeComponent {
  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    @Inject(DOCUMENT) private document: Document
  ) { }

  ngOnInit() {
    if (isPlatformBrowser(this.platformId)) {
      // Código seguro para el navegador
      console.log('Ancho de ventana:', window.innerWidth);
      this.document.title = 'Mi Título';
      localStorage.setItem('theme', 'dark');
    }
  }
}

Para servicios que dependen de estas APIs (ej: un servicio de notificaciones que usa window.alert), considera envolverlos en una lógica de isPlatformBrowser o proporcionar una implementación mock para el servidor.

2. Problemas con Bibliotecas de Terceros

Muchas bibliotecas de JavaScript no están diseñadas pensando en SSR y pueden intentar acceder a APIs del navegador globalmente, incluso si no se usan directamente en el componente actual.

Solución:

  • Verifica la Compatibilidad: Busca versiones de las bibliotecas que sean compatibles con SSR o que permitan una inicialización deferida (solo en el cliente).
  • Importaciones Dinámicas: Usa import() dinámico para cargar las bibliotecas solo en el navegador:
import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Component({ /* ... */ })
export class MyLibComponent implements OnInit {
constructor(@Inject(PLATFORM_ID) private platformId: Object) { }

ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
import('third-party-lib').then(lib => {
lib.init();
// Usa la librería aquí
});
}
}
}
  • Proveer un Mock: Si la librería es pequeña, puedes crear un servicio mock para el servidor que simplemente no haga nada.
// src/app/services/third-party.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class ThirdPartyService {
doSomething() { console.log('Haciendo algo!'); }
}

// src/app/services/third-party.server.service.ts (mock)
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class ThirdPartyServerService {
doSomething() { console.log('Mock: No haciendo nada en el servidor.'); }
}

// En app.module.ts (o app.config.ts si usas standalone)
// Para el entorno de cliente
// providers: [ ThirdPartyService ]

// En app.server.module.ts (o app.config.server.ts)
// providers: [
//   { provide: ThirdPartyService, useClass: ThirdPartyServerService }
// ]

3. Fugas de Memoria en el Servidor

El servidor renderiza muchas instancias de tu aplicación. Si no se liberan los recursos correctamente, pueden ocurrir fugas de memoria.

Solución:

  • Evitar Estados Globales: Asegúrate de que tus servicios y componentes no guarden estados globales persistentes que puedan ser compartidos entre diferentes peticiones.
  • Limpieza de Suscripciones: Siempre desuscríbete de Observables (ngOnDestroy) para evitar que sigan existiendo después de que un componente se haya destruido en el servidor.
  • Servicios con providedIn: 'root': Los servicios que usas de esta manera son singletons. Si un singleton mantiene referencias a objetos específicos de una petición, podría haber fugas. Considera usar providedIn: 'platform' o inyectar una instancia fresca para cada petición si es necesario.

4. Caché Incorrecto del Servidor

Si tu servidor Node.js no está configurado correctamente para el caché, podría servir contenido obsoleto o dinámico de forma estática.

Solución:

  • Configura Cabeceras de Caché: Asegúrate de que tu servidor Express (o el que uses) envíe las cabeceras de caché adecuadas (Cache-Control, Expires, ETag, etc.) según el tipo de contenido. Para HTML dinámico de SSR, generalmente querrás no-cache o un caché de corta duración.
// En server.ts
server.get('*', (req, res, next) => {
res.set('Cache-Control', 'public, max-age=600, stale-while-revalidate=600'); // Ejemplo para caché de 10 min
// ... tu lógica de renderizado ...
});

🚀 Despliegue de Aplicaciones Angular Universal

El despliegue de una aplicación Universal implica dos partes principales: la aplicación cliente (estática) y el servidor (Node.js).

1. Construir para Producción

Primero, necesitas construir tu aplicación para producción. Esto generará los bundles optimizados para el cliente y para el servidor.

npm run build:ssr
# Esto ejecuta internamente:
# ng build --configuration production
# ng run my-angular-app:server:production

Esto creará las carpetas dist/my-angular-app/browser (para la parte cliente) y dist/my-angular-app/server (para el servidor Node.js).

2. Ejecutar el Servidor Node.js

El archivo server.js (generado a partir de server.ts) se encargará de servir tu aplicación Universal. Necesitas ejecutarlo en un entorno que soporte Node.js.

node dist/my-angular-app/server/main.js

Este comando iniciará tu servidor Express y comenzará a escuchar peticiones en el puerto configurado (por defecto, 4000).

3. Plataformas de Despliegue

Hay varias opciones populares para desplegar aplicaciones Node.js con SSR:

  • Heroku: Fácil de configurar. Necesitas añadir un Procfile para especificar cómo iniciar tu aplicación Node.js.
// Procfile
web: node dist/my-angular-app/server/main.js
  • Vercel / Netlify: Ofrecen funciones serverless que pueden ejecutar tu código Node.js. Vercel tiene soporte nativo para ng deploy y SSR de Angular. Netlify Functions también puede usarse, pero requiere una configuración más manual del servidor Express para funcionar como una función.
    💡 Consejo: Para Vercel, si tienes la versión más reciente de Angular y Universal, a menudo es tan simple como conectar tu repositorio y dejar que Vercel detecte y configure automáticamente el build y despliegue SSR.
  • AWS (EC2, Lambda con API Gateway): EC2 es una VM donde puedes instalar Node.js y ejecutar tu aplicación. Para Lambda, necesitarías adaptar tu server.ts para que la función app() sea invocable por Lambda y API Gateway (con el paquete serverless-express).
  • Google Cloud (Compute Engine, Cloud Run, App Engine): Opciones similares a AWS. Cloud Run es ideal para servicios containerizados y serverless con escalado automático.

Despliegue de SSG (Sitios Estáticos)

Para SSG, el despliegue es aún más sencillo. Una vez que has ejecutado ng run my-angular-app:prerender, simplemente sube el contenido de la carpeta dist/my-angular-app/browser a cualquier proveedor de hosting estático o CDN.

  • Netlify / Vercel: Ideales para SSG. Conectas tu repositorio, configuras el comando de prerender como tu comando de build, y ellos se encargarán del resto.
  • GitHub Pages / GitLab Pages: Excelentes opciones gratuitas para sitios estáticos.
  • AWS S3 + CloudFront: Una combinación robusta y escalable para servir contenido estático a nivel global.
  • Firebase Hosting: Sencillo y rápido de configurar para sitios estáticos.

✨ Consejos Avanzados y Buenas Prácticas

  • Optimización del server.ts: Personaliza tu archivo server.ts para manejar rutas de API, redirecciones, o cualquier lógica de backend que necesites. Asegúrate de que los archivos estáticos (dist/my-angular-app/browser) se sirvan eficientemente con las cabeceras de caché correctas.
  • Manejo de Errores en SSR: Implementa un manejo robusto de errores en tu servidor Node.js para las peticiones de SSR. Si un error ocurre durante el renderizado, es mejor devolver una página de error 500 que colgar el servidor.
  • Meta-etiquetas Dinámicas: Utiliza el servicio Meta y Title de @angular/platform-browser para establecer dinámicamente el título y las meta-etiquetas (como Open Graph para redes sociales) en el servidor. Esto es crucial para el SEO y las previsualizaciones.
import { Component, OnInit } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';

@Component({ /* ... */ })
export class DynamicPageComponent implements OnInit {
constructor(private titleService: Title, private metaService: Meta) { }

ngOnInit() {
const pageTitle = 'Mi Página Dinámica con SSR';
const pageDescription = 'Esta es una descripción optimizada para SEO.';

this.titleService.setTitle(pageTitle);
this.metaService.updateTag({ name: 'description', content: pageDescription });
this.metaService.updateTag({ property: 'og:title', content: pageTitle });
this.metaService.updateTag({ property: 'og:description', content: pageDescription });
// ... más meta tags para Open Graph, Twitter Cards, etc.
}
}
  • Loading States y Skeleton Screens: Aunque SSR mejora el FCP, la hidratación puede tardar. Considera usar skeleton screens o indicadores de carga para las partes de la UI que tardan en interactuar después de que el HTML inicial ha sido servido.
  • Auditorías con Lighthouse: Usa Lighthouse (integrado en Chrome DevTools) para auditar el rendimiento, SEO y accesibilidad de tu aplicación con Universal. Te dará feedback valioso sobre cómo seguir mejorando.
  • Pruebas End-to-End (E2E): Asegúrate de que tus pruebas E2E consideren el flujo de SSR. Verifica que el contenido esté presente en el DOM inicial antes de que JavaScript se cargue completamente.

Conclusión

Angular Universal es una herramienta poderosa que transforma la forma en que tus aplicaciones Angular son percibidas por los usuarios y los motores de búsqueda. Al implementar Server-Side Rendering (SSR) o Static Site Generation (SSG), no solo mejoras drásticamente el rendimiento inicial y la experiencia del usuario, sino que también aseguras una mejor visibilidad en los resultados de búsqueda. Si bien presenta algunos desafíos relacionados con el entorno del servidor, las soluciones y las buenas prácticas que hemos cubierto te permitirán superarlos y construir aplicaciones robustas, rápidas y optimizadas.

¡Anímate a integrar Angular Universal en tus próximos proyectos y lleva tus aplicaciones al siguiente nivel! 🚀

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!