Server Side rendering with Angular universal 10

31/08/20 dannyVersion Française

What are we going to do ?

We will apply Server Side Rendering in our Web Application.
We will use the Angular version 10.0.14 javascript framework.

We will use an existing project whose characteristics are

  • Generated with Angular CLI
  • Routing
  • Lazy Loading
  • Framework CSS Bootstrap

All created sources are indicated at the end of the tutorial.

The final application is at the following address


Before you start

To be visited by a large number of users, a website must fulfill two essential conditions.

  • Display as quickly as possible.
  • Be well referenced by search engines.

The technique that allows you to do this is named.

  • Server Side Rendering


We will apply this technique in an Angular project.
For this we will use technology advocated by Google teams

  • Angular Universal.

This technology will improve the SEO (Search Engine Optimization) of our site.


Creation of the Angular project

To be able to continue this tutorial we obviously have to use certain elements

  • Node.js : The javascript plateform.
  • Git : The version-control system.
  • Angular CLI : The command line interface for Angular.
  • Visual Studio code : The source-code editor.

You can consult the following tutorial which explains in detail how to do


We will use an existing project

# Make a demo directory (the name is arbitrary)
mkdir demo

# Go to this directory
cd demo

# Get the source code on your workstation
git clone https://github.com/ganatan/angular-example-features.git

# Go to the directory that was created
cd angular-example-features

# Run installation of dependencies
npm install

# Run the program
npm run start

# Check by launching the command in your browser
http://localhost:4200/

Theory

Web pages generated with javascript frameworks use javascript.
Search engines currently have trouble interpreting javascript.

We will check this notion in a practical way.
We will execute our application with the corresponding script.

# Running the application
npm run start

# Viewing the site in the browser
http://localhost:4200/

We will check the source code produced in the corresponding page.
Using the Chrome browser you must type Ctrl + U to see the html code.

We notice that the code "Features" that appears in the browser does not appear in the 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">
</head>

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

</html>

By clicking on main.js we open this file which contains the text "Features"

Let's run a compilation with npm run build.
The dist/angular-starter directory contains the main-es5.js and main-es2015.js files.
Let's open these files with our VS code editor, then do a search (Ctrl + F) of the text "Features".
They contain the text "Features".

These files are used to generate the main.js file.

The main.js file is a javascript file, so it will be misinterpreted by search engines.

We will see later in this tutorial that once the SSR applied the code appears directly in the HTML code and will be well interpreted by the search engines.


Installation

The tool we will use to apply the SSR to our project is

  • Angular universal version 9.1.1

The latest version of this tool is available below

Angular Universal can generate static pages through a process called Server side rendering (SSR).

The procedure to follow is detailed on the Angular official website.
https://angular.io/guide/universal


We will use a simple CLI command

# Installation
ng add @nguniversal/express-engine

Angular universal

As a reminder angular CLI uses via the ng add directive the principle of schematics to modify our code and adapt it to the new functionality (here the ssr).

Many operations were done automatically on our project.

If we had to carry out this operation manually here are the different steps that we should have followed.

  • Installing new dependencies
  • Editing the main.ts file
  • Editing the app.module.ts file
  • Editing the angular.json file
  • Creating the src/app/app.server.module.ts file
  • Creating the src/main.server.ts file
  • Creating the server.ts file
  • Creating the tsconfig.server.json file
  • Creating the webpack.server.config.js file
  • Editing the angular.json file
  • Editing the package.json file


Installation of dependencies.


# Install the new dependencies in 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();
}

document.addEventListener('DOMContentLoaded', () => {
  platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));
});

Editing the app.module.ts file
In this tutorial we will add the appId value to identify the application.

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 { }

Editing the angular.json file.

Editing outputPath

  • dist/angular-starter/browser instead of 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",
  "aot": true,
  "assets": ["src/favicon.ico", "src/assets"],
  "styles": [
    "src/styles.css",
    "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
    "node_modules/bootstrap/dist/css/bootstrap.min.css",
    "src/assets/params/css/index.css"
  ],
  "scripts": [
    "node_modules/jquery/dist/jquery.min.js",
    "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
    "src/assets/params/js/index.js"
  ]
},

Creating the app.server.module.ts file

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 {}

Creating the main.server.ts file
We will change the code for the lint test to work properly.

src/main.server.ts
import { enableProdMode } from '@angular/core';

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

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

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

Creating the server.ts file

The port used by default is 4000 we can change it if necessary in this file.

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(): void {
  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';


Creating files

  • tsconfig.server.json
tsconfig.server.json
{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app-server",
    "module": "commonjs",
    "types": [
      "node"
    ]
  },
  "files": [
    "src/main.server.ts",
    "server.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "./src/app/app.server.module#AppServerModule"
  }
}

Changes to the angular.json file

The "server", "serve-ssr" and "prerender" property is added after the e2e property.

angular.json
"e2e": {
    "builder": "@angular-devkit/build-angular:protractor",
    "options": {
      "protractorConfig": "e2e/protractor.conf.js",
      "devServerTarget": "angular-starter:serve"
    },
    "configurations": {
      "production": {
        "devServerTarget": "angular-starter:serve:production"
      }
    }
  },
  "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"
          }
        ],
        "sourceMap": false,
        "optimization": true
      }
    }
  },
  "serve-ssr": {
    "builder": "@nguniversal/builders:ssr-dev-server",
    "options": {
      "browserTarget": "angular-starter:build",
      "serverTarget": "angular-starter:server"
    },
    "configurations": {
      "production": {
        "browserTarget": "angular-starter:build:production",
        "serverTarget": "angular-starter:server:production"
      }
    }
  },
  "prerender": {
    "builder": "@nguniversal/builders:prerender",
    "options": {
      "browserTarget": "angular-starter:build:production",
      "serverTarget": "angular-starter:server:production",
      "routes": [
        "/"
      ]
    },
    "configurations": {
      "production": {}
    }
  }

Editing the package.json file

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

Updates

We can take this opportunity to update the dependencies of the package.json file and adapt the version descriptors.

The following dependencies

  • @nguniversal/express-engine

Can be updated with versions

  • 10.0.2

The file will eventually contain the following dependencies.

  "dependencies": {
    "@angular/animations": "10.0.14",
    "@angular/common": "10.0.14",
    "@angular/compiler": "10.0.14",
    "@angular/core": "10.0.14",
    "@angular/forms": "10.0.14",
    "@angular/platform-browser": "10.0.14",
    "@angular/platform-browser-dynamic": "10.0.14",
    "@angular/platform-server": "10.0.14",
    "@angular/router": "10.0.14",
    "@fortawesome/fontawesome-free": "5.14.0",
    "@nguniversal/express-engine": "10.0.2",
    "bootstrap": "4.5.2",
    "express": "4.17.1",
    "jquery": "3.5.1",
    "rxjs": "6.6.2",
    "tslib": "2.0.1",
    "zone.js": "0.11.1"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "0.1000.8",
    "@angular/cli": "10.0.8",
    "@angular/compiler-cli": "10.0.14",
    "@nguniversal/builders": "10.0.2",
    "@types/express": "4.17.7",
    "@types/node": "14.6.2",
    "@types/jasmine": "3.5.14",
    "@types/jasminewd2": "2.0.8",
    "codelyzer": "6.0.0",
    "jasmine-core": "3.6.0",
    "jasmine-spec-reporter": "5.0.2",
    "karma": "5.1.1",
    "karma-chrome-launcher": "3.1.0",
    "karma-coverage-istanbul-reporter": "3.0.3",
    "karma-jasmine": "4.0.1",
    "karma-jasmine-html-reporter": "1.5.4",
    "protractor": "7.0.0",
    "ts-node": "9.0.0",
    "tslint": "6.1.3",
    "typescript": "3.9.7"
  }

Erreurs

The server.ts file contains a piece of code that generates the following error

  • Type 'Express' is not assignable to type 'void'


We will modify the code to resolve this issue.
In particular by removing the void type.


The correct file will be as follows

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() {
  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';

browserslistrc File

Angular Version 10 create a new file.
browserslistrc

It can generate an error.
For example on the application used by www.ganatan.com

I offer two files

  • The default file that can generate an error
  • The adapted file without error
.browserslistrc with errors
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries

# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support

# You can see what browsers were selected by your queries by running:
#   npx browserslist

last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.
.browserslistrc without errors
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries

# You can see what browsers were selected by your queries by running:
#   npx browserslist

> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

Conclusion


It only remains to test all the previous scripts and finalize with the SSR.

# Development
npm run start
http://localhost:4200/

# Tests
npm run lint
npm run test
npm run e2e

# AOT Compilation
npm run build

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

Finally we will check the source code produced in the page corresponding to the SSR compilation.
Using the Chrome browser you must type Ctrl + U to see the html code.

We notice that the code "Features" appears this time in the browser.

The page will therefore be well interpreted by the search engines.

Note
Some versions of Angular 9 do not allow you to check the SSR result in your browser.
However SSR works on the server side with google robots

To check it use the curl software

Then check the contents of the ssr-results.txt file.
You will see in the HTML code appear the desired text.

Other proof Use SEOQUAKE (SEO Toolbox) to check SEO on angular.ganatan.com/

I apply the SSR on www.ganatan.com and angular.ganatan.com