Building static but progressive Web Apps with Angular

This is a tutorial about how to combine two techniques which are discussed a lot right now albeit they seem to be unrelated at first. We are going to build an Angular app that serves precompiled HTML and at the same time also enables offline support. We achieve that by using angular-prerender which is currently the only tool for prerendering that supports this combination.

Static sites are very popular right now. They seem to be new and shiny but technically they are not much different from what could have been done with Microsoft FrontPage twenty years ago. The difference is that we can now create static sites backed by powerful frontend frameworks. And that allows us to turn the prerendered HTML and CSS that is served by static sites into an interactive experience that fulfills the expectations of today's users.

That way we get the benefits of both worlds. Static sites load fast and don't even require JavaScript. But if available we can load a full frontend framework to enrich the user experience.

On the other end of the spectrum there are Progressive Web Apps (also known as PWAs). It's an umbrella term for many different technologies which close the gap between web apps and native apps. The core technology of a PWA is a Service Worker. It serves many purposes but one of the most prominent ones is to enable offline support. To achieve that it downloads and caches all the data necessary to render the page without any internet connection. A Service Worker fully relies on JavaScript.

At first glance there is not much which those two techniques have in common. But they both can be used to achieve the same goal: Delivering the content as fast as possible. Static sites speed up the very first load and PWAs speed up consecutive loads. Luckily Angular offers tools to get started with both techniques in no time.

We begin this tutorial by scaffolding a simple project which we then turn into a PWA at first before we also turn it into a static website.

Scaffolding the project

Let's get started by building a simple website with the Angular CLI. Thanks to npx we can use the CLI without having to install it globally.

npx @angular/cli new static-progressive-web-app \
    --defaults \
    --routing

The command above will create an Angular application with all default settings. The only explicitly required feature is routing.

Once the CLI is done we can navigate into the newly created static-progressive-web-app directory to create some modules.

ng generate module about --module app --route about
ng generate module contact --module app --route contact
ng generate module home --module app --route home

Our website does now consist of three pages. There is an about page, a contact page and a home page.

A small tweak is necessary to change the path for the home page since it is currently available under /home. We need to change the path from 'home' to '' in the router configuration of the AppRoutingModule which is defined in the src/app/app-routing.module.ts file. This ensures that the home page is actually served at the root of our page.

The list of routes supported by our website does now look like this:

/
/about
/contact

Each of this pages has its own module. That means when we open the page in the browser only the currently needed module is loaded. The other two modules will be loaded lazily. This is of course a bit pointless as long as the modules are that small. But it will pay off as the bundles become bigger. It will make sure our page becomes interactive as soon as possible.

Before we checkout our new website in the browser let's quickly replace the default content of the template of the AppComponent defined in the src/app/app.component.html file with a basic menu.

<h1>Welcome to {{ title }}!</h1>
<ul>
  <li>
    <h2><a routerLink="/">home</a></h2>
  </li>
  <li>
    <h2><a routerLink="/about">about</a></h2>
  </li>
  <li>
    <h2><a routerLink="/contact">contact</a></h2>
  </li>
</ul>
<router-outlet></router-outlet>

Now it's time to take a look at the project. Let's spin up the dev server by running the following command.

ng serve --open

So far this is very similar to Angular's official guide on lazy-loading feature modules. Our website is not yet static nor is it a Progressive Web App.

Turning it into a Progressive Web App

Let's change the latter first by adding a Service Worker. Thanks to the power of Angular's schematics this can be done by running a single command.

ng add @angular/pwa

The Service Worker is not active in dev mode which is why we can't see it in action when using the dev server as shown above. Instead we need to create a production build and then use a simple http server to serve the files.

ng build --prod
npx http-server \
    -c-1 \
    --port 4200 \
    dist/static-progressive-web-app

The reason for adding a Service Worker was to enable offline support. In order to achieve that it has to cache our website. So what does the Service Worker actually cache?

The answer to that question can be found in the src/ngsw-config.json file. It's the Service Worker configuration and it specifies which files should be loaded when the Service Worker gets registered in the browser. During the build this configuration gets used to create the dist/ngsw.json file which does then contain the final list of files.

For our purpose the default settings work just great. All the HTML, CSS and JS files are cached. This means our website doesn't need a network connection anymore once the Service Worker is loaded.

After we visted the website served by running the command above at localhost:4200 once we can switch off the network connection but it remains possible to navigate and reload the page.

For more details about how to configure the cache and what else could be done with a Service Worker please have a look at the official article on getting started with Service Workers.

Turning it into a static website

Now we can go offline and our website still keeps working. But the very first load is still not optimal. The HTML file is mostly empty and our users have to wait for the JavaScript to be downloaded, parsed, and executed until they can see something on the screen.

To change that we need to prerender the website. Since we want to prerender a PWA we are going to use angular-prerender because it's the only tool which can turn a PWA into a static website without further adjustments.

It's worth mentioning that since version 9 Angular Universal includes a builder to prerender Angular apps as well. It works very much the same but it misses some of the features that are provided by angular-prerender. And as we will see in a bit one of those features is necessary when prerendering a PWA.

Prerendering means that we run Angular at build time to determine the HTML structure of every route. It's done with Node.js and works by using a temporary server. That's why we need to create a server side app first. And sure enough there is a schematic which helps us with that.

ng generate universal \
    --client-project static-progressive-web-app

Next we need to install angular-prerender as a dev dependency. It's a command line tool that scans our app for its routes and then renders each of them one by one. It stores the result for each route as a separate HTML file.

npm install angular-prerender --save-dev

As mentioned above an ngsw.json file is created when we build a PWA with the Angular CLI. That file also contains references to the index.html file and even stores the hash of that file. If we now prerender our app the content of that file changes and therefore the hash is not valid anymore.

Another problem is that the Service Worker will use the index.html file to render any route when there is no internet connection. Since our index.html is prerendered this has the odd effect that we will see the content of the home page for a split second even though we navigated to one of the other pages. To avoid that angular-prerender will preserve the original index.html file and replace all references to it inside the ngsw.json file. All we have to do is set the --preserve-index-html flag.

ng build --prod
ng run static-progressive-web-app:server
npx angular-prerender --preserve-index-html
npx http-server \
    -c-1 \
    --port 4200 \
    dist/static-progressive-web-app/browser

And that's it. If we run the commands above and open view-source:localhost:4200 in Firefox or Chrome to inspect the source code we see that the full HTML is available right from the start. We now have a Progressive Web App which also is a static website. Search engines will have no problem to index our website and our users will see the page as fast as possible.

The website even works without JavaScript. We can verify that by disabling JavaScript in Firefox or by disabling it in Chrome. For some reason both browsers still keep executing the Service Worker even though JavaScript is disabled. But we can circumvent that problem by opening a new private window in Firefox or by using an incognito window in Chrome to visit the page as if we never had visited the site with JavaScript enabled before.

I think this technique is perfect for small pages like blogs or documentations. But it can also be used for parts of bigger applications like landing pages.

However some projects may require a little more complex setup as the one presented here. It can get tricky when we need to handle dynamic content or when we need to invalidate the cache. But these are stories of their own and maybe good topics for future tutorials. Please let me know if you are interested in such a post.

Special thanks to François Guézengar and Danny Koppenhagen for reviewing this article and giving me valuable feedback.