In this tutorial, we are going to learn how to integrate your Angular-CLI application with Angular Universal.
Before going into the actual coding part, lets understand what exactly Angular Universal is and how it works.
These days websites are amazing, they are rich and using a lots of Javascript code to generate their pages. Mostly Single page applications (SPA) frameworks are probably getting attention in the Javascript Market in the past years. Handling most of the data processing on the client side, boiler-plating the content on every page, maintaining the “state”, and omitting the overhead latency on switching pages are just some of its net benefits.
But, we have got a small problem, the application has to be indexed by Search engines.
Many search engines and social networks expect plain HTML to utilise the meta tags and relevant page contents. They can not determine when the javascript framework completes rendering the page. As a result they get served with the incomplete HTML content which is very useless to them. This behaviour even gets more painful when you people try to share the website link and you see an image like the one below when they do.
image is taken from https://scotch.io/
So, to address this, our application has to be rendered on the server side as well for search engines and social networks. SSR is a reliable, flexible and efficient way to ensure all search engines & social networks can access your content. Here comes the Angular Universal.
What is Angular Universal?
Angular Universal is the standalone project the Angular team is working on to make building universal apps a smooth experience. Its main purpose is to allow server to process angular code and templates. It loads our app on the server first, and then drops it to the browser once ready.
Here are some benefits of adding universal(SSR) into our app.
- Facilitate web crawlers (SEO) – All your routes are crawlable and understandable for search engines and social networks.
- First time users of your application will instantly see a server rendered view which greatly improves perceived performance and the overall user experience.
- All the first HTTP requests are made on the server side and users should not wait for them on the client-side.
- Improves performance on mobile and low-powered devices
The below video demonstrates the difference of initial page rendering and perceived page load time when SSR (aka Angular Universal) is used. The SSR version on the other hand, the users will instantly see a server rendered view.
Screen capture demonstrating the difference in initial page rendering and perceived page load times for non SSR (left) and SSR (right).
Getting Started
1 2 |
$ npm install -g @angular/cli $ ng new angular-server-side-rendering |
1 |
$ npm install --save @angular/platform-server @nguniversal/express-engine reflect-metadata |
Prepare our app for server side rendering (path: src/app/app.module.ts)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ // Add .withServerTransition() to support Universal rendering. // The application ID can be any identifier which is unique on // the page. BrowserModule.withServerTransition({ appId: 'my-app-id' }) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } |
Next, create a app.server.module.ts which is similar like app.module.ts file
path: src/app/app.server.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 { } |
Create a main.server.ts file in this path src/main.server.ts to export our server module.
1 |
export { AppServerModule } from './app/app.server.module'; |
Next, create tsconfig.server.json file for the server in this path src/tsconfig.server.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "baseUrl": "./", /* For now, Node only understand CommonJS modules, so you can't stick to es2015 here */ "module": "commonjs", "types": [] }, "exclude": [ "test.ts", "**/*.spec.ts" ], "angularCompilerOptions": { "entryModule": "app/app.server.module#AppServerModule" } } |
Create a second app for server in the CLI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "angular-server-side-rendering" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } }, /* create a second profile for the server bundle here. */ { "platform": "server", "root": "src", "outDir": "dist-server", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.server.ts", "test": "test.ts", "tsconfig": "tsconfig.server.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json", "exclude": "**/node_modules/**" }, { "project": "src/tsconfig.spec.json", "exclude": "**/node_modules/**" }, { "project": "e2e/tsconfig.e2e.json", "exclude": "**/node_modules/**" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } } |
Now, lets the CLI create our bundles, run the following command to see if it builds correctly.
1 2 |
/* Build the client bundle and server bundle */ $ ng build --prod && ng build --prod --app 1 --output-hashing=none |
The above command will produce “dist” and “dist-server” folders in our root directory. Here you can see we added output-hashing to none to the server bundle. It’s important here, this generates a clean (main.bundle.js) file without any hash.
This (main.bundle.js) file exports module called AppServerModuleNgFactory that we are going to load in our server.js file. This is the module we can use to pre-render the whole thing.
Now lets create our server.js file in the root directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
require('zone.js/dist/zone-node'); require('reflect-metadata'); const express = require('express'); const { enableProdMode } = require('@angular/core'); const { ngExpressEngine } = require('@nguniversal/express-engine'); const { AppServerModuleNgFactory } = require(`./dist-server/main.bundle`); enableProdMode(); function angularRouter(req, res) { res.render('index', {req, res}); } const app = express(); const port = 3000; /* Root route before static files, or it will serve a static index.html, without pre-rendering */ app.get('/', angularRouter) // set the engine app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory }) ); app.set('view engine', 'html'); app.set('views', 'dist'); // to serve our static files. app.use(express.static(`${__dirname}/dist`)); // direct all routes to index.html app.get('*', angularRouter); app.listen(port, () => { console.log(`Listening on http://localhost:${port}`); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
{ "name": "angular-server-side-rendering", "version": "0.0.0", "license": "MIT", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build --prod", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", "build:universal": "ng build --prod && ng build --prod -app 1 --output-hashing=none", "serve:universal": "npm run build:universal && node server" }, "private": true, "dependencies": { "@angular/animations": "^5.2.0", "@angular/common": "^5.2.0", "@angular/compiler": "^5.2.0", "@angular/core": "^5.2.0", "@angular/forms": "^5.2.0", "@angular/http": "^5.2.0", "@angular/platform-browser": "^5.2.0", "@angular/platform-browser-dynamic": "^5.2.0", "@angular/platform-server": "^5.2.9", "@angular/router": "^5.2.0", "@nguniversal/express-engine": "^5.0.0", "core-js": "^2.4.1", "reflect-metadata": "^0.1.12", "rxjs": "^5.5.6", "zone.js": "^0.8.19" }, "devDependencies": { "@angular/cli": "~1.7.0", "@angular/compiler-cli": "^5.2.0", "@angular/language-service": "^5.2.0", "@types/jasmine": "~2.8.3", "@types/jasminewd2": "~2.0.2", "@types/node": "~6.0.60", "codelyzer": "^4.0.1", "jasmine-core": "~2.8.0", "jasmine-spec-reporter": "~4.2.1", "karma": "~2.0.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "^1.2.1", "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.1.2", "ts-node": "~4.1.0", "tslint": "~5.9.1", "typescript": "~2.5.3" } } |
build:universal will first build the client app, then build the server app.
serve:universal runs the build:universal command first, then runs the server.
Lets run the npm run serve:universal command from root directory.Now, the server will be listening on http://localhost:3000.
Browse this http://localhost:3000 link and inspect the page and click on the view page source to see the rendered html by the server.
Basically, when you run ng serve command, your application will render on the client side, but when you run npm run serve:universal command, the application actually renders at server side(not a browser), and thats how search engines and social networks understands your content.
Thats it. You can refer the below github link to see the full code.
https://github.com/sivagrid/angular-server-side-rendering