Building a Dynamic website with Angular Universal

We have previously demystified Angular v4 Universal and built a static website with Angular Universal. However, in many cases, it is interesting to have a polymorphic website: one static version that can be crawled and referenced by search engines and served by static web servers, and another dynamic version, executed in the browser with no more need of a web server.

Introduction

The first “static” form of this polymorphic website can be used to show the first loaded page to web crawlers (which, anyway, are crawling only single pages) and users, when the second “dynamic” form can be used to display the following pages to users.

If you get our precedent “static” example served at http://ng-universal-demystified-2.surge.sh/, you can access any page of the website directly, for example http://ng-universal-demystified-2.surge.sh/page/2.html. This page is stored in the web server as a static HTML page and its full content will be transferred by your browser. If you navigate to another page from this one, the browser will again transfer all the HTML content of the new page from the web server.

In our new “polymorphic” version of our website, the first HTML page will still be loaded from the server as a full HTML page, but all subsequent pages will be built dynamically from the browser, with no extra loads from the web server (but probably some loads from the API server).

In return, for the browser to be able to build subsequent pages, the first page needs to be able to start the Angular app. We will see in this article how to embed the code of the Angular app in the static pages of the website, so they are able to bootstrap it.

Building the dynamic pages

Remember that the renderModuleFactory method accepts an options.document parameter, specifying the HTML document in which the rendered content will be embedded. In our first “static” version, we were using a very simple HTML file with no extra dynamic content.

For our new “dynamic” version, we will have to provide a document embedding the Angular app to be bootstrapped. For this, we first need to build the Angular app, with the following command:

$ ng build -prod -aot

As a result, we get the following files in the dist/ directory:

$ ls -1 dist/
favicon.ico
index.html
inline.9f974b8cf51779d7128e.bundle.js
main.191ad35f7cfc52e74aef.bundle.js
polyfills.2d45a4c73c85e24fe474.bundle.js
styles.d41d8cd98f00b204e980.bundle.css
vendor.d34179ea6890cc7f0b2c.bundle.js

and we can see that the created index.html file is embedding the js and css files needed to bootstrap the Angular app:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>NgUniversalDemystified</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.d41d8cd98f00b204e980.bundle.css" rel="stylesheet"/>
</head>
<body>
<app-root>Loading...</app-root>
<script type="text/javascript"
src="inline.9f974b8cf51779d7128e.bundle.js"></script>
<script type="text/javascript"
src="polyfills.2d45a4c73c85e24fe474.bundle.js"></script>
<script type="text/javascript"
src="vendor.d34179ea6890cc7f0b2c.bundle.js"></script>
<script type="text/javascript"
src="main.191ad35f7cfc52e74aef.bundle.js"></script>
</body>
</html>

We can now use this document as a base to embed our generated content. Let’s modify our main.server.ts for this task:

import 'zone.js/dist/zone-node';
import { renderModuleFactory } from '@angular/platform-server'
import { enableProdMode } from '@angular/core'
import { AppServerModuleNgFactory } from './src/app.server.module.ngfactory'
import * as fs from 'fs';
import * as path from 'path';
enableProdMode();
const args = process.argv.slice(2);
if (args.length != 3) {
process.stdout.write("Usage: node dist/main.js <document> <distDir> <url>\n");
process.exit();
}
const indexFileContent = fs.readFileSync(args[0], 'utf8');
renderModuleFactory(AppServerModuleNgFactory, {
document: indexFileContent,
url: args[2]
}).then(string => {
let destUrl = args[2];
if (destUrl == '/')
destUrl = 'index.html'
const targetDir = args[1] + '/' + destUrl;
targetDir.split('/').forEach((dir, index, splits) => {
if (index !== splits.length - 1) {
const parent = splits.slice(0, index).join('/');
const dirPath = path.resolve(parent, dir);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
}
});
fs.writeFileSync(targetDir, string);
console.log(targetDir);
});

This new version gets as first argument the document to use, as second second argument the directory where the resulting files will be created and as third argument the url of the web page to build. We can now compile and execute the new version of our script:

$ ./node_modules/.bin/ngc
$ webpack
$ mkdir dynamic
$ node dist/main.js dist/index.html dynamic /
$ node dist/main.js dist/index.html dynamic page.html
$ node dist/main.js dist/index.html dynamic page/1.html
[...]

And finally, we can upload these html pages to a static web server, accompanied by the files necessary for bootstrapping the Angular app:

$ cd dist
$ cp favicon.ico \
inline.9f974b8cf51779d7128e.bundle.js \
main.191ad35f7cfc52e74aef.bundle.js \
polyfills.2d45a4c73c85e24fe474.bundle.js \
styles.d41d8cd98f00b204e980.bundle.css \
vendor.d34179ea6890cc7f0b2c.bundle.js \
../dynamic
$ cd ../dynamic
$ surge . ng-universal-demystified-3.surge.sh

Bonus: internationalize your app

What if you want to use the i18n Angular feature with Universal?

Let’s add some translated strings to our app:

// home.component.html
<p i18n>home works!</p>

Then let’s extract translations and create translations files for different languages:

$ ng xi18n --output-path src/i18n
$ cd src/i18n
$ cp messages.xlf messages.en.xlf
$ cp messages.xlf messages.fr.xlf
// messages.en.xlf
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="17785a2c638b514855df2e77832da93e04a3c944" datatype="html">
<source>home works!</source>
<target>It works!</target>
</trans-unit>
</body>
</file>
</xliff>
// messages.fr.xlf
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="17785a2c638b514855df2e77832da93e04a3c944" datatype="html">
<source>home works!</source>
<target>Ça marche !</target>
</trans-unit>
</body>
</file>
</xliff>

Now, when you create your client and server builds, you have to specify the language you want to use. For example for the french language:

$  ng build -prod -aot \
--i18n-file=src/i18n/messages.fr.xlf \
--i18n-format=xlf --locale=fr
$ ./node_modules/.bin/ngc \
--i18nFile=src/i18n/messages.fr.xlf \
--i18nFormat=xlf --locale=fr
$ webpack
$ node dist/main.js dist/index.html dynamic-fr /
[...]