This article is based off 4.4.3 of Angular Universal and 4.4.3 Angular. Angular Universal can help you improve SEO, perceived performance, and even in some cases page load performance itself. Getting started isn’t difficult. It’s powerful but it’s not magic. The goal of this guide is not only to get you started but to give you a deeper understanding of how Angular Universal works. This guide takes you from zero to universal with these steps: Create an minimal Angular build with the CLI.

Configure the CLI to create a universal bundle.

Set up a server.

Generate Angular Universal content per route.

Configure serving for static files. There’s some tedious configuration in the beginning, but then it’s smooth sailing. Open up a terminal. Let’s begin. Angular Universal is two parts Angular Universal isn't a command-line tool. It's more than running a command to "sprinkle in" server-side rendering. Angular Universal changes how you build, deliver, and load your site. To take advantage of Angular Universal you need to understand the big picture. Angular Universal is two parts: a universal bundle and a server.

Part One: The universal bundle Your Angular code won't run on a server out-of-the-box. The module format is incorrect and your code may be coupled to the browser. To get your code running on a server you need a universal bundle. A universal bundle is a build of your site converted for a commonjs environment like nodejs. The import statements turn into require() functions and any browser dependent code will break your server-side rendering. The browser's DOM is not available on the server. Angular uses a browser independent implementation of the DOM: domino. This indepent solution gives you basic DOM support, but it can't do everything the browser does. You'll have to cut any code that directly manipulates the DOM or uses browser API's unsupported by domino. I'll cover this in a later blog post. The good news is the Angular CLI makes this easy. The bad? This is where the tedious config starts.

Install the Angular CLI Avoid version pain. Make sure you at least have version 1.3.0 of the CLI installed. This article uses version 1.4.2 . npm i -g @angular/cli

Set up a minimal project Use the CLI to create a new project. The CLI provides a command that creates an Angular project with a minimal setup. ng new ssr-project --minimal Install platform server Angular Universal works through the @angular/platform-server module. It doesn't come with the Angular CLI setup. You need to install it separately. npm i @angular/platform-server --save Make sure you've installed the same version as the other Angular modules. The CLI may install an older version of Angular such as 2.4.2 and the @angular/platform-server install may install a newer version. A mismatch of Angular module versions is going to give you a bad time later on. Go to the package.json and make sure every @angular module is on the same version. This article uses Angular 4.4.3 . /package.json { "name": "ssr-project", "version": "0.0.0", "license": "MIT", "private": true, "dependencies": { "@angular/animations": "^4.4.3", "@angular/common": "^4.4.3", "@angular/compiler": "^4.4.3", "@angular/core": "^4.4.3", "@angular/forms": "^4.4.3", "@angular/http": "^4.4.3", "@angular/platform-browser": "^4.4.3", "@angular/platform-browser-dynamic": "^4.4.3", "@angular/platform-server": "^4.4.3", "@angular/router": "^4.4.3", "core-js": "^2.4.1", "rxjs": "^5.4.2", "zone.js": "^0.8.14" }, "devDependencies": { "@angular/cli": "1.4.3", "@angular/compiler-cli": "^4.4.3", "@angular/language-service": "^4.4.3", "typescript": "~2.3.3" } } Make the browser module aware of a server transition Open src/app/app.module.ts . Angular needs to know that this is a server-side rendered app. Use the BrowserModule.withServerTransition() method and provide a unique id. This method acts a generic interface between the client and the server. This allows Angular to do any specific processing to take over a server-side rendered site. /src/app/app.server.module.ts import { BrowserModule } from "@angular/platform-browser"; import { NgModule } from "@angular/core"; import { AppComponent } from "./app.component"; @NgModule({ declarations: [AppComponent], imports: [BrowserModule.withServerTransition({ appId: "ssr-app" })], providers: [], bootstrap: [AppComponent] }) export class AppModule {} Create an AppServerModule The browser needs an AppModule and the server needs one as well. This is a place where you can override or cancel-out any browser specific code for server compatibility. Create the file src/app/app.server.module.ts . /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 {} Create an entry point for the server module Each top-level Angular module needs an entry point file to start the application. Create a main.server.ts file an export the AppServerModule just like in main.ts . src/main.server.ts export { AppServerModule } from "./app/app.server.module"; Set up the server module's TypeScript configuration At the point you've created the ServerModule and it's entry point. However, there's no TypeScript configuration to build it to JavaScript. You might be thinking, "Oh, I'll just add it to the "files" array in the tsconfig.app.json ." There's a big problem with that. The current configuration is uses the es2015 module setting and you need commonjs . The Angular compiler also needs to customize the configuration to know what the entry module is for Angular Universal. The easiest way to fix this by creating a specific server configuration: src/tsconfig.server.json . /src/tsconfig.server.json { "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "baseUrl": "./", "module": "commonjs", "types": [] }, "exclude": ["test.ts", "**/*.spec.ts"], "angularCompilerOptions": { "entryModule": "app/app.server.module#AppServerModule" } } Tell the CLI how to build the universal bundle The CLI is a big abstraction on Webpack. It knows how to build Angular, so it sets up the configuration and you command it. You may need to customize your configuration in certain cases. Creating a universal bundle is one of those cases. Open the .angular-cli.json file. This file contains the default configuration options. To create a universal bundle you need to create a separater "app" entry. Add the following entry to the "apps" array. /.angular-cli.json { "platform": "server", "root": "src", "outDir": "functions", "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" } } This configuration is the bow on all the hard work you've done so far. It tells the CLI that this "app" runs on a server "platform" , has an entry file of main.server.ts and is built with the tsconfig.server.json configuration file. Now build it Use the CLI to specify that you want to build the server app entry. ng build --app 1 --prod --output-hashing none This should give you an output that looks something like this: Date: 2017-09-26T12:39:17.053Z Hash: 53b3312d5cbe854aac7c Time: 4791ms chunk {0} main.bundle.js (main) 7.13 kB [entry] [rendered] chunk {1} styles.bundle.css (styles) 0 bytes [entry] [rendered] Success! You have a universal bundle. What about those flags? The --prod flag minified and enabled AoT. The --output-hashing none flag removed any hashes from file names. These hashes are good for browser caching, but this makes them useless and frustrating on the server. Speaking of servers, it's time that you set one up.

Part Two: The server A server is the second part of an Angular Universal app. The server is where you will use Angular's @angular/platform-server . This module generates static HTML and CSS from the universal bundle. There is an Angular Universal integration with .NET, but for this example you're going to use a nodejs server. You've been writing in TypeScript this whole time so it won't be too different. Configure the server code for TypeScript It's not required to write the server code in TypeScript, but it's useful. Create a server folder at the root of your project. Inside that folder create yet another configuration file: tsconfig.functions.json . You'll use this configuration file to build your server code to the functions folder in a commonjs format. /server/tsconfig.functions.json { "compilerOptions": { "module": "commonjs", "target": "es2015", "outDir": "../functions" } } Build the browser app There are two entries in your "apps" array. The one you created is for the server and the original one is for the browser. When the browser apps builds it stores the resulting files in the dist folder. Angular Universal requires the index.html document to generate the server-side rendered page. You need to build the browser app and move the index.html into the functions folder. ng build --prod && mv dist/index.html functions Install express You're going to use express.js as your server framework. It's easy to use and has integrations across hosting platforms. You'll also install a helper library: fs-extra . npm i express @types/express fs-extra @types/fs-extra --save Set up the express app Create a file at server/index.ts . This file is this entry point to your server. Here you'll create an express app, listen for requests, and use Angular Universal to generate HTML and CSS from your universal bundle. /server/index.ts import "zone.js/dist/zone-node"; import * as express from "express"; import { enableProdMode } from "@angular/core"; import { renderModuleFactory } from "@angular/platform-server"; import * as fs from "fs-extra"; enableProdMode(); const app = express(); app.get("**", async (request, response) => { // Do magical Angular universal stuff here }); const PORT = process.env.PORT || 3022; app.listen(PORT, () => { console.log(`Listening on ${PORT}...`); }); This server code does the following things: Imports zone.js: A node-specific version of zone.js. Things will totally break if you forget this.

A node-specific version of zone.js. Things will totally break if you forget this. Imports express: This is the server framework.

This is the server framework. Imports Angular Universal: The renderModuleFactory from @angular/platform-server . This function is responsible for generating HTML and CSS from a universal bundle.

The from . This function is responsible for generating HTML and CSS from a universal bundle. Creates an express app : An app is where you attach http handlers like: GET and POST methods.

: An app is where you attach http handlers like: and methods. Creates a greedy ** route : This route handles any GET request to the server.

: This route handles any request to the server. Launches the server: Uses a default port of 3022. Require the assets for Angular Universal You can now respond to all GET requests. To use Angular Universal you need to first get the assets it requires. This includes the universal bundle, the index.html , and the current url. /server/index.ts import "zone.js/dist/zone-node"; import * as express from "express"; import { enableProdMode } from "@angular/core"; import { renderModuleFactory } from "@angular/platform-server"; import * as fs from "fs-extra"; const app = express(); app.get("**", async (request, response) => { // renderModuleFactory parameters const { AppServerModuleNgFactory } = require(__dirname + "/main.bundle"); const document = await fs.readFile(__dirname + "/index.html", "utf8"); const url = request.path; const options = { document, url }; }); const PORT = process.env.PORT || 3022; app.listen(PORT, () => { console.log(`Listening on ${PORT}...`); }); The additions you made do the following things: Requires the universal bundle : The AppServerModuleNgFactory is from the universal bundle. This is an AoT version of your AppServerModule . A require is used because no module or type information is available for TypeScript to use.

: The is from the universal bundle. This is an AoT version of your . A require is used because no module or type information is available for TypeScript to use. Reads the index.html : Angular Universal needs the index.html document. Using the fs-extra library you retrieve the file back as a Promise .

: Angular Universal needs the document. Using the library you retrieve the file back as a . Retrieves the current url: Angular Universal also needs the url for the path you are trying to render. This helps it work with the router. Server-side render your Angular app Now you can get to the good part. Using Angular Unviersal to server-side render your app. /server/index.ts import "zone.js/dist/zone-node"; import * as express from "express"; import { enableProdMode } from "@angular/core"; import { renderModuleFactory } from "@angular/platform-server"; import * as fs from "fs-extra"; const app = express(); app.get("**", async (request, response) => { // renderModuleFactory parameters const url = request.path; const { AppServerModuleNgFactory } = require(__dirname + "/main.bundle"); const document = await fs.readFile(__dirname + "/index.html", "utf8"); const options = { document, url }; try { // generate the server-side rendered html const html = await renderModuleFactory(AppServerModuleNgFactory, options); response.send(html); } catch (e) { console.log(e); response.status(500).send("¯_(ツ)_/¯"); } }); const PORT = process.env.PORT || 3022; app.listen(PORT, () => { console.log(`Listening on ${PORT}...`); }); Angular Universal's renderModuleFactory takes in the universal bundle and a set of options. In your case you provide the index.html document and current url as the options. This returns a promise containing the server-side rendered HTML if successful. If there's an error you can catch it and send back a 500 status page. Build and run the server Build the server code to test your Angular Universal app. node_modules/.bin/tsc -p server/tsconfig.functions.json Now you can run the server. node functions/index.js Open the browser and view source Open up the app in your favorite browser. If you're running off the default Angular CLI project you should see something like "app Works!" on the screen. Right-click and view the source of the document. You should see the content of your app inside the source of the document like below: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>SsrProject</title> <base href="/" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <link rel="icon" type="image/x-icon" href="favicon.ico" /> <link href="styles.hash.bundle.css" rel="stylesheet" /> </head> <body> <app-root ng-version="4.4.3"> <p> app Works! </p> </app-root> <script type="text/javascript" src="inline.hash.bundle.js"></script> <script type="text/javascript" src="polyfills.hash.bundle.js"></script> <script type="text/javascript" src="vendor.hash.bundle.js"></script> <script type="text/javascript" src="main.hash.bundle.js"></script> </body> </html> What if you don't see your content? If your source looks like your regular non-server-side rendered app then a few things may have happened. When Angular Universal encounters an unsupported DOM API or some error in your universal bundle it will return the default index.html . You'll need to inspect any places where you could be directly accessing the DOM. I've hit this error when I've used the PerformanceObserver API. One solution is to use the isPlatformBrowser helper function to guard against browser dependent code. import { isPlatformBrowser } from "@angular/common"; Inspecting 404s Inspect the Network panel in your browser's DevTools. You'll notice that you receive back a set of 404 s. These are your static assets for your site. It's the browser version of your app.