Prerender Angular Apps with less Configuration

So far every prerender solution for Angular discovers the routes to prerender via either static analysis or manual configuration. Now there is a third option available in angular-prerender which uses the prerendered HTML itself to discover further routes.

A dream became true

A little more than four years ago Minko Gechev presented the Guess.js project. While it is a truly exciting project, what excited me the most was a little implementation detail. Guess.js needs to know the routes of an application at build time and that's why Minko wrote the guess-parser.

The guess-parser is a tool to statically analyze the codebase of Angular or React apps. It has the ability to scan those apps for routes.

This is what made me start working on angular-prerender to make the prerendering of Angular apps as simple as possible.

How it all began

Before I started working on angular-prerender it was state of the art to use a custom prerendering script. But it was a burden to maintain the script since it needed to know what routes it should prerender. Configuring the script was a manual task and most of the times the script attempted to prerender a route which didn't exist anymore or it would not prerender a new route which it didn't know about yet.

But thanks to the guess-parser this configuration step could be automated. It scans an Angular app for its routes which can then be prerendered using Angular Universal. All that is left to do for angular-prerender is to make sure everything works. I was excited that this setup makes it possible to prerender Angular apps with a single command.

After a while Scully was presented which builds on the same concept but chose to render pages with Puppeteer instead of using Angular Universal.

And finally this technique became also the official prerender builder in Angular itself.

The missing pieces

However there are some routes which the guess-parser can detect but they can't be prerendered without providing additional information. Those routes are dynamic routes which contain route parameters.

Let's take a website for a record label as an example. It surely has a page for each release of that label. The route definition for those pages may look somehow like this.

{
  component: ReleaseComponent,
  path: '/releases/:catNo'
}

To prerender this route we need to know what values the :catNo path parameter might have. Or in other words, we need a list of the releases with their catalog numbers.

No matter which of the tools you choose to prerender an Angular app it will provide a way to configure dynamic routes manually. There are for example a couple of command line arguments to configure angular-prerender. The prerender builder takes a few prerendering options, too. And Scully can even be configured with a dedicated config file.

I think Scully's approach to configure dynamic routes is the most intuitive and flexible. It supports router plugins and comes with a core plugins to extract route parameters from a JSON document.

In case of our record label example the config file might look like this.

export const config = {
  routes: {
    '/releases/:catNo': {
      catNo: {
        property: 'catNo',
        // url: the URL of the API
      },
      type: 'json'
    }
  }
};

This config file tells Scully to download a JSON file from the given URL. The file should contain an array of objects which all have a catNo property. Scully uses that property to prerender the dynamic route for every release.

It's worth mentioning that this should also work with angular-prerender (fingers crossed) since it has experimental support for Scully plugins as well.

It's for sure good to know that route parameters can be provided explicitly. But what if there was a way to do this without any configuration?

Reusing what is already there

Going back to our record label example from before, I think it's safe to assume that the pages for individual releases are linked to from another page. It's very likely that there is a page which lists all releases. This could be its route configuration.

{
  component: ReleasesComponent,
  path: '/releases'
}

This page probably does the same API call to get all releases that we configured above. And it most likely displays links to the detail pages for each release. What if we use those links on that page to prerender the detail pages? After all this is exactly the info we need.

Version 10.1 of angular-prerender introduces a new flag which does exactly that. It's the --recursive flag and it can be used like this.

npx angular-prerender --recursive

When enabling the --recursive flag (it might be enabled by default in the next major version) angular-prerender will scan each prerendered HTML document for its links. If those links are internal links and haven't been prerendered before they will be prerendered as well.

This means angular-prerender will discover the pages for each individual release on its own. Once it prerendered the static page for all releases it scans it for further routes and prerenders those as well.

This even works with paginated overview pages. Let's say our record label published more than ten releases but the overview page only showed the ten most recent. There would probably still be a link on the page going to another page which lists the older releases. In recursive mode angular-prerender would follow that link as well.

The beauty of recursion is that it goes on until it's done. But sometimes this is not what we want. And therefore angular-prerender also provides a way to stop it. Anytime a route is discovered which is meant to be excluded (by using the --exclude-routes flag) it will be ignored.

One flag to rule them all

In theory the new --recursive flag may work on its own without using the guess-parser at all. By default angular-prerender attempts to prerender the root document if no routes could be found and it should be possible to reach all other pages from there via links. It's at least a good practice to structure your website like this if you care about SEO.

But there are of course exceptions and it is no problem to use all methods to discover routes in parallel.

Since the guess-parser only supports routes defined with Angular's built-in router this new flag allows to use other routers like the Angular Component Router for static apps without any cumbersome configuration, too.

After all it's a bit embarrassing that it took me four years to realize that routes could also be discovered like this. It's one of the goals of prerendering to make a page crawlable so it seems to be a no-brainer to use that feature already during the prerendering process. But as they say better late than never. I hope this new feature makes prerendering Angular apps yet a little easier.

Many thanks to Sander Elias for reviewing this article.