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