Generate and host static sites using Angular Universal and Amazon S3

In this post we look at how Angular Universal and Amazon S3 can be used to generate and host a static site. We also will look at how to support pages which have not been statically generated, but exist via an API.


One best practice in web architecture is to separate your backend API from the frontend, which has several advantages:

  • True separation of code and services
  • APIs can have a full suite of tests
  • APIs can be micro-services and hosted/scaled separately
  • Frontend can be static and hosted on cheaper static hosting
  • Frontend can define the CMS views or have an integrated UI
  • Frontend can utilize API versioning to lock releases


However there are also disadvantages to this architecture approach. One huge drawback with a traditional static frontend approach is that content created via an API is not available for AMP/fast load and SEO optimization.

It has been possible to workaround this limitation by having your backend CMS dynamically generate and deploy your static site, after each change is made in the CMS. This is heavy and requires tightly coupled frontend and backend code.

Something has since changed, we now have a great fullstack solution which moves the static generation to the frontend where really it should be. It also uncouples frontend code from backend CMS logic.

Angular Universal is a set of modules which compile your Angular TypeScript code into templates for serving either dynamically from a NodeJS server, or as static html templates. Once the static page has loaded, it will then load the JavaScript version on-top and update any content in realtime.

This approach means we can write our code once and support multiple delivery scenarios. Amazing!


1) Starting an Angular Universal project:

A quick starting point is to clone the official universal-starter repository:

git clone https://github.com/angular/universal-starter

Install the dependencies and run the project locally:

npm install
npm run start

To generate the static version, simply run:

npm run build:prerender && npm run serve:prerender

You can view the generated static site in the dist/browser folder.

Very useful, but let's push it further by including page title tags and ajax data rendered within the same static templates.


2) Adding title and meta tags:

Start by importing the Meta and Title modules into your app/home/home.component.ts file:

import { Meta, Title } from '@angular/platform-browser';

Then add them to the constructor:

constructor(
    private title: Title,
    private meta: Meta
) {}

Now you can set your title and meta tags using:

ngOnInit() {
    this.message = 'Hello';
    this.title.setTitle('Hello World');
    this.meta.updateTag({ name: 'description', content: 'Hello World description!' });
}

Run it locally and try generating the static version to see it working!


3) Loading dynamic content from an API

Now lets add some ajax content to the app/lazy/lazy.module.ts file:

import { HttpClient, HttpClientModule, HttpErrorResponse } from '@angular/common/http';

Then add the properties to the Class:

posts:any[];
constructor(private http: HttpClient) { }

Add the http get request:

ngOnInit() {
    const getPosts = this.http.get('https://jsonplaceholder.typicode.com/posts')
    .catch((error: HttpErrorResponse) => Observable.throw(error));
    getPosts.subscribe(res => this.posts = res)
}

And finally output the post data to the page:

@Component({
    selector: 'lazy-view',
    template: `<h3>i'm lazy</h3><pre>{{posts | json}}</pre>`
})

Now run again locally and with static gen to see the json data loaded correctly into the static page!


4) Static generation for S3 buckets

We want to host the statically generated version somewhere in the cloud. Google AppEngine is a good option, but i'm deciding to use Amazon S3 for it's ease and cost.

Since Amazon S3 buckets always contain a folder path in their url, generating the static site the regular way with a root / will cause issues. We need to pass a base href through to the build command. Unfortunately because the way the Angular Universal commands are chained, it's not possible to pass through the base url in one simple command.

Use the following commands one after another, and change the XX placeholder to match your S3 bucket address:

ng build --prod --base-href http://XX.s3-website-us-east-1.amazonaws.com
ng build --prod --app 1 --output-hashing=false --base-href http://XX.s3-website-us-east-1.amazonaws.com
npm run webpack:server && npm run generate:prerender

Zip the files at dist/browser and upload the an S3 bucket. Ensure you set the file permissions to 'Public'.

Also change the options to 'Hosting a static site' and set the index and error files to be index.html


5) Supporting static and dynamic routes on S3

Within the S3 bucket options, add a redirect rule which detects if a file is missing.

<RoutingRules>
  <RoutingRule>
    <Condition>
      <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
    </Condition>
    <Redirect>
      <ReplaceKeyPrefixWith>#!/</ReplaceKeyPrefixWith>
    </Redirect>
  </RoutingRule>
</RoutingRules>

It captures the path and passes it to JavaScript using a hashbang url. This means if a user navigates to /posts/234 and the static file was not generated, or does not exist, it will redirect to /!#/posts/234



We also need Angular to capture this hashbang path and try to load it via JavaScript instead of static files. In app.module.ts add:

import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { Inject } from '@angular/core';

Then we need to add an if statement to ensure code is only run when within a browser (skipped when Universal static generation runs). Then it can check the hashbang url and if it exists in JavaScript then load the url dynamically.

export class AppModule {
    constructor(@Inject(PLATFORM_ID) private platformId: Object) {
        if (isPlatformBrowser(this.platformId)) {
            var hash = location.hash.replace('#!/', '');
            if (hash.length > 1) {
                history.pushState({}, "entry page", hash);
            }
        }
    }
}

This way you get the full benefits of static hosting, but if the static file does not exist yet, it will fallback to index.html and JavaScript will attempt to render the template using the API.

You can download the completed example project from my fork at:

git clone https://github.com/kmturley/universal-starter/tree/s3-hosting-redirects

Hope that helps you get started using Angular Universal and Amazon S3!

No comments:

Post a Comment