Server Side rendering avec Angular universal 13

23/12/21 danny

Nous allons appliquer le Server Side Rendering dans notre Application Web.

Nous utiliserons pour cela le framework javascript Angular version 13.1.1

Server side Rendering avec Angular

Qu’allons nous faire ?


Le projet Angular de base que nous utiliserons dispose déjà des caractéristiques suivantes

  • Généré avec Angular CLI
  • Le Routing
  • Le Lazy Loading
  • Le framework CSS Bootstrap

Tous les sources utilisés sont indiqués en fin de tutoriel.

L' application finale est à l'adresse suivante 


Avant de commencer

Pour être visité par un grand nombre d'utilisateurs, un site web se doit de remplir deux conditions essentielles.

  • S'afficher le plus rapidement possible.
  • Etre bien référencé par les moteurs de recherche.

La technique qui permet de le faire porte un nom.

  • Le Rendu Côté Serveur ou Server Side Rendering en anglais.


Nous allons appliquer cette technique dans un projet Angular.
Pour cela nous emploierons la technologie préconisée par les équipes de Google 

  • Angular Universal.

Cette technologie permettra d'améliorer le référencement naturel ou SEO (Search Engine Optimization) de notre site.


Création du projet Angular

Pour pouvoir continuer ce tutoriel nous devons bien evidemment disposer de certains éléments

  • Node.js : La plateforme javascript
  • Git : Le logiciel de gestion de versions. 
  • Angular CLI : L'outil fourni par Angular.
  • Visual Studio code : Un éditeur de code.

Vous pouvez consulter le tutoriel suivant qui vous explique en détails comment faire


Nous allons utiliser un projet existant

# Créez un répertoire demo (le nom est ici arbitraire)
mkdir demo

# Allez dans ce répertoire
cd demo

# Récupérez le code source sur votre poste de travail
git clone https://github.com/ganatan/angular-modules.git

# Allez dans le répertoire qui a été créé
cd angular-modules

# Exécutez l'installation des dépendances (ou librairies)
npm install

# Exécutez le programme
npm run start

# Vérifiez son fonctionnement en lançant dans votre navigateur la commande
http://localhost:4200/

Théorie

Les pages web générées avec des framework javascript utilise javascript.
Les moteurs de recherche ont à l'heure actuelle du mal à interpréter le javascript.

Nous allons vérifier cette notion de façon pratique.

Nous allons éxécuter notre application avec le script correspondant.

# Exécution de l'application
npm run start

# Affichage du site dans le navigateur
http://localhost:4200/

Nous allons vérifier le code source produit dans la page correspondante.
En utilisant le navigateur Chrome il faut taper Ctrl + U pour voir le code html.

On remarque que le code "Features" qui s'affiche dans le navigateur n'apparait pas dans le code.

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>AngularStarter</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">

    <!-- Global site tag (gtag.js) - Google Analytics -->
    <script async="" src="https://www.googletagmanager.com/gtag/js?id=YOUR-ID"></script>
    <script>
        window.dataLayer = window.dataLayer || [];
        function gtag() { dataLayer.push(arguments); }
        gtag('js', new Date());

        gtag('config', 'YOUR-ID');
    </script>

    <link rel="stylesheet" href="styles.css">
</head>

<body>
    <app-root></app-root>
    <script src="runtime.js" type="module"></script>
    <script src="polyfills.js" type="module"></script>
    <script src="styles.js" defer></script>
    <script src="scripts.js" defer></script>
    <script src="vendor.js" type="module"></script>
    <script src="main.js" type="module"></script>
</body>

</html>

En cliquant sur main.js nous ouvrons ce fichier qui contient  le texte "Features"

Exécutons une compilation avec npm run build.
Le répertoire dist/angular-starter contient le fichier main.js.
Ouvrons ce fichier avec notre éditeur VS code , puis effectuons une recherche (Ctrl + F) du texte "Features".
Il contient le texte "Features".

Le fichier main.js est un fichier javascript, il sera donc mal interprété par les moteurs de recherche.

Nous verrons plus loin dans ce tutoriel qu'une fois le SSR appliqué le code apparait directement dans le code HTML et sera ainsi bien interprété par les moteurs de recherche.


Installation

L'outil que nous utiliserons pour appliquer le SSR à notre projet est

  • Angular universal version 13.0.1

La dernière version de cet outil est disponible ci-dessous


Angular Universal permet de générer des pages statiques via un processus appelé Server side rendering (SSR).

La procédure à suivre est détaillée sur le site officiel d'Angular.
https://angular.io/guide/universal

Nous allons utiliser une simple commande CLI

# Installation
ng add @nguniversal/express-engine

Angular universal

Pour rappel angular CLI utilise via la directive ng add le principe des schematics pour modifier notre code et l'adapter à la nouvelle fonctionnalité (ici le ssr).

De nombreuses opérations ont été effectuées automatiquement sur notre projet.

Si nous avions dû réaliser cette opération manuellement voici les différentes étapes que nous aurions dû suivre.

  • Installation des nouvelles dépendances nécessaires
  • Modification du fichier main.ts
  • Modification du fichier app.module.ts
  • Modification du fichier angular.json
  • Création du fichier src/app/app.server.module.ts
  • Création du fichier src/main.server.ts
  • Création du fichier server.ts
  • Création du fichier tsconfig.server.json
  • Modification du fichier angular.json
  • Modification du fichier package.json

Installation des dépendances.

# Installer les nouvelles dépendances dans package.json
npm install --save @angular/platform-server
npm install --save @nguniversal/express-engine
npm install --save express
npm install --save @nguniversal/builders
npm install --save @types/express
src/main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

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

function bootstrap() {
     platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));
   };


if (document.readyState === 'complete') {
  bootstrap();
} else {
  document.addEventListener('DOMContentLoaded', bootstrap);
}

Modification du fichier app.module.ts
Dans ce tutoriel nous allons rajouter la valeur appId pour identifier l'application.

Dans notre cas angular-starter.

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HomeComponent } from './modules/general/home/home.component';
import { NotFoundComponent } from './modules/general/not-found/not-found.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NotFoundComponent,
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'angular-starter' }),
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Modification du fichier angular.json.
Modification de outputPath

  • dist/angular-starter/browser à la place de dist/angular-starter.
angular.json
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/angular-starter/browser",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css",
              "src/assets/params/css/fonts.googleapis.min.css",
              "src/styles.css"
            ],
            "scripts": [
              "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
            ]
          },

Création du fichier app.server.module.ts

src/app/app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

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

@NgModule({
  imports: [
    AppModule,
    ServerModule,
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

Création du fichier main.server.ts

src/main.server.ts
/***************************************************************************************************
 * Initialize the server environment - for example, adding DOM built-in types to the global scope.
 *
 * NOTE:
 * This import must come before any imports (direct or transitive) that rely on DOM built-ins being
 * available, such as `@angular/elements`.
 */
import '@angular/platform-server/init';

import { enableProdMode } from '@angular/core';

import { environment } from './environments/environment';

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

export { AppServerModule } from './app/app.server.module';
export { renderModule } from '@angular/platform-server';

Création du fichier server.ts

Le port utilisé par défaut est 4000 nous pouvons le changer si nécessaire dans ce fichier.

server.ts
import 'zone.js/dist/zone-node';

import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';

// 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/angular-starter/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

  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 Universal engine
  server.get('*', (req, res) => {
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  });

  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__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
  run();
}

export * from './src/main.server';

Création du fichier

  • tsconfig.server.json
tsconfig.server.json
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "outDir": "./out-tsc/server",
    "target": "es2019",
    "types": [
      "node"
    ]
  },
  "files": [
    "src/main.server.ts",
    "server.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "./src/app/app.server.module#AppServerModule"
  }
}

Modifications du fichier angular.json

Les propriétés "server", "serve-ssr" et "prerender" sont rajoutées après la propriété test.

angular.json
        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/angular-starter/server",
            "main": "server.ts",
            "tsConfig": "tsconfig.server.json"
          },
          "configurations": {
            "production": {
              "outputHashing": "media",
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            },
            "development": {
              "optimization": false,
              "sourceMap": true,
              "extractLicenses": false
            }
          },
          "defaultConfiguration": "production"
        },
        "serve-ssr": {
          "builder": "@nguniversal/builders:ssr-dev-server",
          "configurations": {
            "development": {
              "browserTarget": "angular-starter:build:development",
              "serverTarget": "angular-starter:server:development"
            },
            "production": {
              "browserTarget": "angular-starter:build:production",
              "serverTarget": "angular-starter:server:production"
            }
          },
          "defaultConfiguration": "development"
        },
        "prerender": {
          "builder": "@nguniversal/builders:prerender",
          "options": {
            "routes": [
              "/"
            ]
          },
          "configurations": {
            "production": {
              "browserTarget": "angular-starter:build:production",
              "serverTarget": "angular-starter:server:production"
            },
            "development": {
              "browserTarget": "angular-starter:build:development",
              "serverTarget": "angular-starter:server:development"
            }
          },
          "defaultConfiguration": "production"
        }
      }

Modification du fichier package.json

package.json
"scripts": {
    ...
    "dev:ssr": "ng run angular-starter:serve-ssr",
    "serve:ssr": "node dist/angular-starter/server/main.js",
    "build:ssr": "ng build && ng run angular-starter:server",
    "prerender": "ng run angular-starter:prerender"
    ...
}

Mise à jour

Nous pouvons en profiter pour mettre à jour les dépendances du fichier package.json et adapter les descripteurs de version.

Remarque
les dépendances Express doivent être mises à jour.
​​​​​​​Dans le cas contraire elles provoquent une erreur.


Les dépendances suivantes

  • @nguniversal/express-engine

Peuvent être mises à jour avec les versions

  • 13.0.1

Le fichier contiendra au final les dépendances suivantes.

  "dependencies": {
    "@angular/animations": "13.1.1",
    "@angular/common": "13.1.1",
    "@angular/compiler": "13.1.1",
    "@angular/core": "13.1.1",
    "@angular/forms": "13.1.1",
    "@angular/platform-browser": "13.1.1",
    "@angular/platform-browser-dynamic": "13.1.1",
    "@angular/platform-server": "13.1.1",
    "@angular/router": "13.1.1",
    "@fortawesome/fontawesome-free": "5.15.4",
    "@nguniversal/express-engine": "13.0.1",
    "bootstrap": "5.1.3",
    "express": "4.17.2",
    "rxjs": "7.4.0",
    "tslib": "2.3.1",
    "zone.js": "0.11.4"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "13.1.2",
    "@angular/cli": "13.1.2",
    "@angular/compiler-cli": "13.1.1",
    "@nguniversal/builders": "13.0.1",
    "@types/express": "4.17.13",
    "@types/jasmine": "3.10.2",
    "@types/node": "17.0.0",
    "jasmine-core": "3.10.1",
    "karma": "6.3.9",
    "karma-chrome-launcher": "3.1.0",
    "karma-coverage": "2.1.0",
    "karma-jasmine": "4.0.1",
    "karma-jasmine-html-reporter": "1.7.0",
    "typescript": "4.5.4"
  }

Conclusion

Il ne reste plus qu'à tester tous les scripts précédents et finaliser par le SSR.

# Développement
npm run start
http://localhost:4200/

# Tests
npm run test

# AOT Compilation
npm run build

# SSR Compilation
npm run build:ssr
npm run serve:ssr
http://localhost:4000/

Enfin nous allons vérifier le code source produit dans la page correspondante à la compilation SSR..
En utilsant le navigateur Chrome il faut taper Ctrl + U pour voir le code html.

On remarque que le code "Features" s'affiche cette fois dans le navigateur.
La page sera dès lors bien interprétée par les moteurs de recherche.

Remarque
Certaines versions d'Angular 9 ne permettent pas de vérifier le résultat SSR dans votre navigateur.
Pourtant SSR fonctionne côté serveur avec les robots google.

Pour le vérifier utilisez le logiciel curl

Puis vérifiez le contenu du fichier ssr-results.txt
Vous verrez dans le code HTML apparaitre le texte voulu.

Autre preuve Utilisez SEOQUAKE (SEO Toolbox) pour vérifier le SEO sur angular.ganatan.com/

J'applique ainsi le SSR sur www.ganatan.com et angular.ganatan.com


Code source

Le code source utilisé en début de tutoriel est disponible sur github
https://github.com/ganatan/angular-modules

​​​​​​​Le code source de ce tutoriel est disponible sur GitHub.
Utilisez git pour récupérer ce code et vérifier son fonctionnement.

Il vous suffit de vous rendre à l'adresse suivante
https://github.com/ganatan/angular-ssr

Et n'oubliez pas les P'tits Loups si  le code source vous plait vous savez ce qu'il vous reste à faire.

Un star sur github peut changer un homme

Un star sur github peut changer un homme

Comment créer une application From scratch ?

Créez votre compte ganatan

Téléchargez gratuitement vos guides complets

Démarrez avec angular CLI Démarrez avec angular CLI

Gérez le routing Gérez le routing

Appliquez le Lazy loading Appliquez le Lazy loading

Intégrez Bootstrap Intégrez Bootstrap


Utilisez Python avec Angular Utilisez Python avec Angular

Utilisez Django avec Angular Utilisez Django avec Angular

Utilisez Flask avec Angular Utilisez Flask avec Angular

Ganatan site Web avec Angular