loading...
Angular

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

samvloeberghs profile image Sam Vloeberghs Originally published at samvloeberghs.be ・6 min read

Originally published at https://samvloeberghs.be on January 5, 2020

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:

// /assets/news.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.

Example app animation

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.

SSR animation

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

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

Posted on by:

samvloeberghs profile

Sam Vloeberghs

@samvloeberghs

Sam is a freelance software architect and Internet entrepreneur, currently focussing on frontend technologies. Co-organiser of NG-BE and Angular Belgium meetup.

Angular

This is where we write about all things Angular. It's meant to be a place for Angular community and people interested in Angular and the Angular ecosystem.

Discussion

markdown guide
 

Great stuff, SSR has come a long way! One question.... how would one use the pre-rendered routes once they're generated? I have used express-engine SSR for a blog site, but it's not pre-rendered, just renders when served (which works for SEO and first load time)

 

They are typically generated as all index.html files in a directory structure that resembles the routing hierarchy of your website. For example, a route /blog/2019/12/01/latest is saved on the filesystem as /blog/2019/12/01/latest/index.html. A webserver typically serves the index.html as default file when visiting the directory

 

Thanks for article Sam. Kind-of funny how SSR; which was the only way (for 20 years or more e.g. CGI, ASP.NET, MVC etc.); faded away around the time of AngularJS, Angular, Vue and React. Alas we see it back again in this article.

Do you see SSR as having a better footprint for testing?

 

Hi, can you elaborate a bit on "footprint for testing"? Not sure what you mean.. Thanks alot!

 

Jasmine testing does not really do unit testing because all outbound HTML requests must be mocked, that bypasses a lot of the ability to perform a natural test on any component in my opinion.

With server side rendering each request is easily able to be tested using http requests only, which I would think is a lot better than Jasmine trying to do component testing.

 

Great article! Thanks for including the dynamic routes script.

 

Thanks for the supportive feedback! :)