Server Side rendering with Angular universal 14

Updated : 11/12/22 danny

What are we going to do?

We will apply Server Side Rendering in our Web Application.
We will use the Angular version 14.0.4 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-modules.git

# Go to the directory that was created
cd angular-modules

# 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">

    <!-- 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>

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.js file.
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 13.0.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();
}

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


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

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",
            "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"
            ]
          },

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
/***************************************************************************************************
 * 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';

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


Creating files

  • 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"
  }
}

Changes to the angular.json file

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

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

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 && ng run angular-starter:server",
    "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

  • 13.0.1

The file will eventually contain the following dependencies.


  "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


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