Angular Universal

Angular Universal also known as server-side rendering is tool which allows server to pre-render Angular application while user hits your website for first time.

Server-side rendering has bunch of benefits for SEO, performance, and accesibility of your web-app!

In this guide we'll show you, how to implement Angular Universal working with MDB Angular and Express.js server!


Guide for Angular 9:

Create new Angular project:

Create new Angular Application using command:

        
            
        ng new project-name --style=scss
      
        
    

Install Angular Universal and update project files:

Use Angular Schematics to add Angular Universal to your project with one command:

        
            
        ng add @nguniversal/express-engine --clientProject project-name
      
        
    

Above command creates and updates following files in your application:

        
            
        CREATE src/main.server.ts (298 bytes)
        CREATE src/app/app.server.module.ts (318 bytes)
        CREATE tsconfig.server.json (325 bytes)
        CREATE server.ts (2015 bytes)
        UPDATE package.json (2110 bytes)
        UPDATE angular.json (5247 bytes)
        UPDATE src/main.ts (432 bytes)
        UPDATE src/app/app.module.ts (359 bytes)
      
        
    

Test your app

If you have done all the above steps correctly, you can now test your application with below command:

        
            
        npm run build:ssr && npm run serve:ssr
      
        
    


Guide for older versions (v8 and before)

Create new Angular project:

Create new Angular Application using command:

        
            
        ng new project-name --style=scss
      
        
    

Install necessary dependencies:

Inside your project directory install following packages, and add Universal configuration:

        
            
        cd project-name
      
        
    
        
            
        npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader@4.0.0 express webpack-node-externals
      
        
    
        
            
        ng generate universal project-name --client-project project-name
      
        
    

Above command creates and updates following files in your application:

        
            
        CREATE src/app/app.server.module.ts (318 bytes)
        CREATE src/tsconfig.server.json (245 bytes)
        UPDATE package.json (1476 bytes)
        UPDATE angular.json (3880 bytes)
        UPDATE src/main.ts (430 bytes)
        UPDATE src/app/app.module.ts (359 bytes)
      
        
    

Create server file:

In root directory of your application create server.ts file which is used to handle server-side rendering.

        
            
        // These are important and needed before anything else
        import 'zone.js/dist/zone-node';
        import 'reflect-metadata';
    
        import { renderModuleFactory } from '@angular/platform-server';
        import { enableProdMode } from '@angular/core';
    
        import * as express from 'express';
        import { join } from 'path';
        import { readFileSync } from 'fs';
    
        const domino = require('domino');
        const fs = require('fs');
        const path = require('path');
    
        // Faster server renders w/ Prod mode (dev mode never needed)
        enableProdMode();
    
        // Express server
        const app = express();
    
        const PORT = process.env.PORT || 4201;
        const DIST_FOLDER = join(process.cwd(), 'dist/browser');
    
        // Our index.html we'll use as our template
        const template = readFileSync(join(DIST_FOLDER, 'index.html')).toString();
        const win = domino.createWindow(template);
    
        global['window'] = win;
        global['Node'] = win.Node;
        global['navigator'] = win.navigator;
        global['Event'] = win.Event;
        global['Event']['prototype'] = win.Event.prototype;
        global['document'] = win.document;
    
        // * NOTE :: leave this as require() since this file is built Dynamically from webpack
        const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');
    
        const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader');
    
        app.engine('html', (_, options, callback) => {
          renderModuleFactory(AppServerModuleNgFactory, {
            // Our index.html
            document: template,
            url: options.req.url,
            // DI so that we can get lazy-loading to work differently (since we need it to just instantly render it)
            extraProviders: [
              provideModuleMap(LAZY_MODULE_MAP)
            ]
          }).then(html => {
            callback(null, html);
          });
        });
    
        app.set('view engine', 'html');
        app.set('views', DIST_FOLDER);
    
        // Server static files from dist folder
        app.get('*.*', express.static(DIST_FOLDER));
    
        // All regular routes use the Universal engine
        app.get('*', (req, res) => {
          res.render('index', { req });
        });
    
        // Start up the Node server
        app.listen(PORT, () => {
          console.log(`Node server listening on http://localhost:${PORT}`);
        });
      
        
    

Create webpack.server.config file:

Again in root directory of your application create webpack.server.config.js file. Above server file needs webpack to transpile your code to Javascript.

        
            
        // Work around for https://github.com/angular/angular-cli/issues/7200
        const path = require('path');
        const webpack = require('webpack');
        const nodeExternals = require('webpack-node-externals');
    
        module.exports = {
          mode: 'none',
          entry: {
            // This is our Express server for Dynamic universal
            server: './server.ts',
            // This is an example of Static prerendering (generative)
          },
          target: 'node',
          resolve: { extensions: ['.ts', '.js'] },
          // Make sure we include all node_modules etc
          externals: [/node_modules/, nodeExternals({
            whitelist: [
                /^@agm\/core/,
                /^hammerjs/
              ]
          })],
          output: {
            // Puts the output at the root of the dist folder
            path: path.join(__dirname, 'dist'),
            filename: '[name].js'
          },
          module: {
            rules: [
              { test: /\.ts$/, loader: 'ts-loader' },
              {
                // Mark files inside `@angular/core` as using SystemJS style dynamic imports.
                // Removing this will cause deprecation warnings to appear.
                test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/,
                parser: { system: true },
              },
            ]
          },
          plugins: [
            new webpack.ContextReplacementPlugin(
              // fixes WARNING Critical dependency: the request of a dependency is an expression
              /(.+)?angular(\\|\/)core(.+)?/,
              path.join(__dirname, 'src'), // location of your src
              {} // a map of your routes
            ),
            new webpack.ContextReplacementPlugin(
              // fixes WARNING Critical dependency: the request of a dependency is an expression
              /(.+)?express(\\|\/)(.+)?/,
              path.join(__dirname, 'src'),
              {}
            )
          ]
        }
      
        
    

Necessary scripts:

To your package.json file, add following scripts to handle building and serving your Universal application.

        
            
        "build:client-and-server-bundles": "ng build --prod && ng run project-name:server:production",
        "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
        "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
        "serve:ssr": "node dist/server"
      
        
    

Modify angular.json file

The last step in our Universal Guide is to modify angular.json file to change in it output path, and provide some server building config.
Copy the code below, search for all project-name occurrences and replace them with the name of your project.

        
            
        {
          "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
          "version": 1,
          "newProjectRoot": "projects",
          "projects": {
            "project-name": {
              "root": "",
              "sourceRoot": "src",
              "projectType": "application",
              "prefix": "app",
              "schematics": {
                "@schematics/angular:component": {
                  "styleext": "scss"
                }
              },
              "architect": {
                "build": {
                  "builder": "@angular-devkit/build-angular:browser",
                  "options": {
                    "outputPath": "dist/browser",
                    "index": "src/index.html",
                    "main": "src/main.ts",
                    "polyfills": "src/polyfills.ts",
                    "tsConfig": "src/tsconfig.app.json",
                    "assets": [
                      "src/favicon.ico",
                      "src/assets"
                    ],
    
                    "styles": [
                      "src/styles.scss"
                    ],
    
                    "scripts": [
                    ]
                  },
                  "configurations": {
                    "production": {
                      "fileReplacements": [{
                        "replace": "src/environments/environment.ts",
                        "with": "src/environments/environment.prod.ts"
                      }],
                      "optimization": true,
                      "outputHashing": "all",
                      "sourceMap": false,
                      "extractCss": true,
                      "namedChunks": false,
                      "aot": true,
                      "extractLicenses": true,
                      "vendorChunk": false,
                      "buildOptimizer": true
                    }
                  }
                },
                "serve": {
                  "builder": "@angular-devkit/build-angular:dev-server",
                  "options": {
                    "browserTarget": "project-name:build"
                  },
                  "configurations": {
                    "production": {
                      "browserTarget": "project-name:build:production"
                    }
                  }
                },
                "extract-i18n": {
                  "builder": "@angular-devkit/build-angular:extract-i18n",
                  "options": {
                    "browserTarget": "project-name:build"
                  }
                },
                "test": {
                  "builder": "@angular-devkit/build-angular:karma",
                  "options": {
                    "main": "src/test.ts",
                    "polyfills": "src/polyfills.ts",
                    "tsConfig": "src/tsconfig.spec.json",
                    "karmaConfig": "src/karma.conf.js",
                    "styles": [
                      "src/styles.scss"
                    ],
                    "scripts": [],
                    "assets": [
                      "src/favicon.ico",
                      "src/assets"
                    ]
                  }
                },
                "lint": {
                  "builder": "@angular-devkit/build-angular:tslint",
                  "options": {
                    "tsConfig": [
                      "src/tsconfig.app.json",
                      "src/tsconfig.spec.json"
                    ],
                    "exclude": [
                      "**/node_modules/**"
                    ]
                  }
                },
                "server": {
                  "builder": "@angular-devkit/build-angular:server",
                  "options": {
                    "outputPath": "dist/server",
                    "main": "src/main.server.ts",
                    "tsConfig": "src/tsconfig.server.json"
                  },
                  "configurations": {
                    "production": {
                      "fileReplacements": [{
                        "replace": "src/environments/environment.ts",
                        "with": "src/environments/environment.prod.ts"
                      }]
                    }
                  }
                }
              }
            },
            "project-name-e2e": {
              "root": "e2e/",
              "projectType": "application",
              "architect": {
                "e2e": {
                  "builder": "@angular-devkit/build-angular:protractor",
                  "options": {
                    "protractorConfig": "e2e/protractor.conf.js",
                    "devServerTarget": "project-name:serve"
                  }
                },
                "lint": {
                  "builder": "@angular-devkit/build-angular:tslint",
                  "options": {
                    "tsConfig": "e2e/tsconfig.e2e.json",
                    "exclude": [
                      "**/node_modules/**"
                    ]
                  }
                }
              }
            }
          },
          "defaultProject": "project-name"
        }
      
        
    

Test your app

If you have done all the above steps correctly, you can now test your application with below command:

        
            
        npm run build:ssr && npm run serve:ssr