Mejora el SEO de tu aplicación Angular

2
12302

En este tutorial vamos a hablar de cómo mejorar el SEO de cualquier aplicación de Angular 4 haciendo server rendering con Angular Universal.

Índice de contenidos


1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: LUbuntu 16.04
  • Visual Studio Code 1.16.1
  • @angular/cli 1.4.2
  • @angular 4.4.1


2. Introducción

Una de las cosas que tenemos que tener muy en cuenta cuando desarrollamos una aplicación single page con la tecnología que sea es que no es muy «seo friendly», dado que requiere de una carga inicial de la aplicación (el típico loading…) que hace que las arañas no puedan ver el contenido de la aplicación y no puedan indexar correctamente la página.

Para resolver este problema muchas tecnologías optan por el «server rendering» o renderizado de páginas en el servidor, lo que quiere decir que al cliente que solicita la información se le sirve HTML estático. Hasta ahora en Angular no era algo trivial, y digo hasta ahora porque se ha creado el siguiente repositorio de angular-cli con la solución, tanto de server rendering como prerendering pudiendo hacer uso del lazy loading de módulos:

https://github.com/angular/universal-starter/tree/master/cli

Así que si estás a punto de empezar un proyecto con requerimiento de SEO te aconsejo que te bases directamente en este repositorio y si lo que quieres es adaptar un proyecto ya existente, sigue leyendo que a continuación te explico los pasos a seguir partiendo del repositorio anterior.


3. Vamos al lío

Así que ya tienes una aplicación con Angular en producción y alguien de negocio se ha acordado ahora del SEO porque pensaba que esto de Angular ya lo traía por defecto… bueno no te preocupes y sigue estos pasos.

1. Vamos a instalar las dependencias necesarias:


$> npm run install --save @angular/platform-server
$> npm run install --save @nguniversal/express-engine
$> npm run install --save @nguniversal/module-map-ngfactory-loader
$> npm run install --save-dev cpy-cli

Las tres primeras son necesarias para la implementación del servidor que va a renderizar las páginas, y la última la vamos a utilizar en la fase de build.

1. Creamos el fichero server.js en la raíz del proyecto con este contenido:

require('zone.js/dist/zone-node');
require('reflect-metadata');
const express = require('express');
const fs = require('fs');

const { platformServer, renderModuleFactory } = require('@angular/platform-server');
const { ngExpressEngine } = require('@nguniversal/express-engine');
// Import module map for lazy loading
const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader');

// Import the AOT compiled factory for your AppServerModule.
// This import will change with the hash of your built server bundle.
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require(`./dist-server/main.bundle`);

const app = express();
const port = 8000;
const baseUrl = `http://localhost:${port}`;

// Set the engine
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');

app.set('views', './');
app.use('/', express.static('./', {index: false}));

app.get('*', (req, res) => {
  res.render('index', {
    req,
    res
  });
});

app.listen(port, () => {
	console.log(`Listening at ${baseUrl}`);
});

Es el fichero que va a levantar nuestro servidor de NodeJS en el puerto que especifiquemos en la constante «port». A destacar el uso LAZY_MODULE_MAP para soportar la carga lazy de módulos secundarios.

2. Creamos el fichero src/app/app.server.module.ts con el siguiente contenido:

import {NgModule} from '@angular/core';
import {ServerModule} from '@angular/platform-server';
import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader';

import {AppModule} from './app.module';
import {AppComponent} from './app.component';

@NgModule({
  imports: [
    // The AppServerModule should import your AppModule followed
    // by the ServerModule from @angular/platform-server.
    AppModule,
    ServerModule,
    ModuleMapLoaderModule,
  ],
  // Since the bootstrapped component is not inherited from your
  // imported AppModule, it needs to be repeated here.
  bootstrap: [AppComponent],
})
export class AppServerModule {}

En este fichero estamos importando el módulo principal de nuestra aplicación (AppModule),
el módulo para nuestro servidor (ServerModule) y el módulo que permite el «lazy loading» (ModuleMapLoaderModule). Además establecemos en la propiedad bootstrap cuál es nuestro componente principal (AppComponent)

3. Creamos el fichero src/tsconfig.server.json con el siguiente contenido:

{
    "extends": "../tsconfig.json",
    "compilerOptions": {
      "outDir": "../out-tsc/app",
      "baseUrl": "./",
      // Set the module format to "commonjs":
      "module": "commonjs",
      "types": []
    },
    "exclude": [
      "test.ts",
      "**/*.spec.ts"
    ],
    // Add "angularCompilerOptions" with the AppServerModule you wrote
    // set as the "entryModule".
    "angularCompilerOptions": {
      "entryModule": "app/app.server.module#AppServerModule"
    }
  }
  

Fíjate como en las opciones de compilación de Angular le especificamos la ruta del módulo creado en el paso anterior.

4. Creamos el fichero main.server.ts con el siguiente contenido:

import { environment } from './environments/environment';
import { enableProdMode } from '@angular/core';

if (environment.production) {
  enableProdMode();
}

export {AppServerModule} from './app/app.server.module';

Aquí indicamos el export a la clase AppServerModule que creamos anteriormente.

5. Editamos el fichero app.module.ts para hacerlo compatible con Universal añadiendo la función .withServerTransition() y especificando un ID de aplicación, que puede ser el que se nos ocurra. En la práctica es dejar el fichero tal cual lo tengas y añadir al módulo BrowserModule lo siguiente:

BrowserModule.withServerTransition({appId: 'angular-app'}),

6. Añadimos una nueva app llamada «server» en .angular-cli añadiendo lo siguiente a la propiedad «apps»

...
,
    {
      "name": "server",
      "platform": "server",
      "root": "src",
      "outDir": "dist/dist-server",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.server.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.server.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
...

Este paso es muy importante y nos permite tener los dos tipos de aplicaciones en un mismo proyecto, el «normal» para desarrollar del modo común y el «server» para que nuestra aplicación se pueda servir desde el servidor en vez de renderizarse en cliente.

Especificamos un nuevo directorio de salida y le indicamos que haga uso de los ficheros que hemos ido creando: main.server.ts y tsconfig.server.json

7. Añadimos nuevos script al fichero package.json

{
...
"build:universal": "ng build --prod && ng build --prod --app 'server' --output-hashing=false && cpy ./server.js ./dist",
"serve:universal": "npm run build:universal && cd dist && node server"
...
}

8. Probamos el resultado

Hechas estas configuraciones es momento de arrancar nuestra aplicación para comprobar el resultado. Para ello ejecutamos:

$> npm run serve:universal

Terminado el proceso nos dirá que la aplicación está corriendo el puerto 8000 o el que hayamos especificado en la constante port del fichero server.js

Nos conectamos a esta URL y el efecto más inmediato de que todo ha ido bien es que no vemos el típico «Loading…» (o el contenido que tengamos entre las etiquetas del selector principal en el index.html) cuando la aplicación carga, ni tan siquiera cuando forzamos un refresco de pantalla. Además si cargamos módulos secundarios con «lazy loading» tenemos que ver que solo se descargan cuando se solicitan.


4. Conclusiones

Como ves, uno de los puntos menos fuertes de las SPAs, el equipo de Angular lo ha solucionado de una forma elegante y casi transparente al desarrollador. Seguro que en futuras versiones de angular-cli pondrán el típico flag para ejecutar todos estos pasos de configuración de forma automática.

Así que ahora cuando te planteen el tema del SEO con una aplicación de Angular ya no te tienes que echar a temblar y ya puedes decir, «por supuesto el framework lo soporta» 🙂

Recordad que esta técnica y otras muchas más las encontraréis en la guía práctica de Angular y también ofrecemos cursos in-house y online.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.

2 COMENTARIOS

  1. Muy completa la información. Para ser un Framework mantenido por Google. Resulta algo ironico que no pueda leer el contenido apropiadamente.
    Universal resolvió mi mas grande problema de SEO. Por cierto, aunque ya exista un proyecto de Universal Master estaría bien que tuvieras un demo de un proyecto simple donde implementes el código. Algo que no vi aqui es como se definen rutas dentro de otras carpetas. por ejemplo *.com/blog/SEO/2018/

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad