Angular v9 & Universal: SSR and prerendering out of the box!

Angular v9 & Universal: SSR and prerendering out of the box!

Posted on

Angular Universal - Advanced techniques series

This article is part of a growing series around advanced techniques in Angular with Universal. If you get excited about this article be sure to check the others!

  1. Better sharing on social media platforms with Angular Universal
  2. Outputting JSON-LD with Angular Universal
  3. Creating a simple memory cache for your Angular Universal website or application
  4. Angular v9 & Universal: SSR and prerendering out of the box!

Target audience

This article and guide is intented to help anybody using Angular v9 getting started with server-side-rendering (SSR) and prerendering their application. Please be advised that these instructions are only working for new or updated apps using Angular v9. Getting the same result for Angular v2-v8 requires more and custom setup.

Example project

For the example project we are using a bare minimum angular-cli generated project with routing enabled. A home (/) and about (/about) route and their components were generated as well. By using a lazy-loaded news module, an extra dynamic overview (/news) and newsdetail (/news/:id) route were configured as well.

The data for this overview and detail page is a simple JSON data object we load from the static assets folder (/assets/news.json) and looks like this:

updated angular.json

[
  {
    "id": 1,
    "title": "Newsitem #1",
    "short": "Lorem ipsum dolor sit amet, ...",
  },
  {
    "id": 2,
    "title": "Newsitem #2",
    "short": "Lorem ipsum dolor sit amet, ..."
  }
]

The following animation shows how our application behaves using the routes and loading the dynamic data. The full source code for this basic example application can be found here.

Adding Server-side-rendering (SSR) to your application

Normally your Angular app is only rendered as soon as your browser loads it. In this case, the only responsibility of the webserver is serving the static files of your Angular app (everything that is in the dist folder after a successful build).

With SSR, the specific route of your application, for example /about, is completely rendered on the server, just as it would render in your browser. This allows search engines like Google and social-media platforms like Facebook to index and show previews of the pages of your application, because the full HTML is available from the initial load from the server, without JavaScript required.

Getting started

To get started all you need to do is add the @nguniversal/express-engine package using the angular-cli:

ng add @nguniversal/express-engine@next
# as soon as v9 is released you can drop the "@next"

The ng add command updates your application by adding the necessary files and updating the angular.json configuration file. There are 3 new configurations added to the architect section: server, serve-ssr and prerender. All those 3 configurations have their own purpose and together they allow us to achieve the required results. Let's walk trough the changes.

updated angular.json

{
    "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
    "version": 1,
    "newProjectRoot": "projects",
    "projects": {
    "ng-v9-universal": {
      "..": "..",
      "architect": {
        "..": "..",
        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/ng-v9-universal/server",
            "main": "server.ts",
            "tsConfig": "tsconfig.server.json"
          },
          "configurations": {
            "production": {
              "..": ".."
            }
          }
        },
        "serve-ssr": {
          "builder": "@nguniversal/builders:ssr-dev-server",
          "options": {
            "browserTarget": "ng-v9-universal:build",
            "serverTarget": "ng-v9-universal:server"
          },
          "configurations": {
            "production": {
              "browserTarget": "ng-v9-universal:build:production",
              "serverTarget": "ng-v9-universal:server:production"
            }
          }
        },
        "prerender": {
          "builder": "@nguniversal/builders:prerender",
          "options": {
            "browserTarget": "ng-v9-universal:build:production",
            "serverTarget": "ng-v9-universal:server:production",
            "routes": []
          },
          "configurations": {
            "production": {}
          }
        }
      }
    }
  }
  "..": ".."
}

The server run option

A server is required to run the Angular build on your system, or the back-end, therefore the server.ts file is added to the root of your project. This TypeScript file contains all the necessary setup to render your routes and serve the static assets of your application. As you can see on line 13-15, the output path for this server is defined, as well as a tsConfig that defines how to build the server TypeScript code. Just as with the browser build we can also define the production specific environment variables.

Building this server is as easy as building your Angular application. Just run the following command in your terminal, and the server will be build into dist/ng-v9-universal/server.

npm run build:ssr

The serve-ssr run option

While developing your application you'll probably just run ng serve. This command will setup a dev-server for you with hot-reloading and everything else that will provide you with a nice developer experience.

Testing the server-side-rendering part can be done while developing as well. To do this, just run the application as ng run ng-v9-universal:serve-ssr and the server source files will be watched next to the application source files. Changing the code of the application or the server will automatically restart the application because of live-reloading.

Now if you reload any page, for example the news overview (/news), the initial payload will contain the fully rendered HTML for the news overview, including the dynamic data.

The prerender run option

A final addition is the possibility to prerender the routes of your application. For this to work the prerender builder guesses static routes using guess-parser, build by @mgechev. The routing modules of your application are analyzed and the routes found are prerendered.

To prerender the routes of your application, just run:

npm run prerender

Rendering more (dynamic) routes

As shown in the angular.json config file at line 41, it is possible to list other known routes of the application. In our case for example we could list the /news, /news/1 and /news/2 routes. This will instruct the builder to also prerender those extra routes. Especially for lazy-loaded modules this might be important, as these are not so easily guessed by guess-parser.

To render more dynamic routes, like the news detail pages in our sample application, we need some more logic. Basically you need to find a way to let the prerender know what routes to prerender. This can be done by, for example, defining a list-routes.js script that will list all the routes in a text file. This file can then by passed to the prerender script as follows:

npm run prerender --routesFile routes.txt

An example of a script to list up all the required routes is shown below. All the routes listed in this text file are added to the routes already defined in the routes array of the angular.json config file before prerendering.

scripts/list-routes.js

const fs = require('fs');
const axios = require('axios');
const endOfLine = require('os').EOL;
const newsDataPath = 'http://localhost:4200/assets/news.json';
const routesFile = './routes.txt';

axios.get(newsDataPath).then(res => {
  const routes = [];
  res.data.forEach(newsitem => {
    routes.push('news/' + newsitem.id);
  });
  fs.writeFileSync(routesFile, routes.join(endOfLine), 'utf8');
}).catch(e => console.log(e));

The scripts in our package.json now look like this:

updated package.json

{
  "name": "ng-v9-universal",
  "version": "0.0.0",
  "scripts": {
    "..", "..",
    "list-routes": "node ./scripts/list-routes.js",
    "prerender": "npm run list-routes && ng run ng-v9-universal:prerender --routesFile routes.txt"
  },
  "..": ".."
}

Important: While running the npm run prerender command, be sure to have the application running as well. The static file /assets/news.json needs to be available for the application to prerender all routes! You can do this by just running ng serve in another terminal.

Conclusion

I have started this blog with Angular v2 and Universal and back in 2016 it was not easy getting it set up. Universal with Angular v9 has improved developer experience a lot and implementing it is now just a matter of following clearly defined steps.

Testing your application and the server-side rendered version of it is now available as one command. Just run npm run dev:ssr to get going!

Prerendering your static routes is easy, except if you are using lazy loaded routes, these are not easy to guess by guess-parser. You still need a way to render all your routes. This can be done by listing the routes in a file and feeding it to the builder by using the --routesFile option. How you get to know the list of routes is up to you.

Further reading

  1. Angular Universal v9: What's New ?
  2. Vikram @vikerman: My last set of tweets from the Angular team

By reading this article I hope you can find a solution for your problem. If it still seems a little bit unclear, you can hire me for helping you solve your specific problem or usecase. Sometimes even just a quick code review or second opinion can make a great difference.