Building a static website with Angular Universal

Philippe Martin
5 min readMar 31, 2017

You can get a more up-to-date version at https://leanpub.com/angular-universal

In the previous Angular v4 Universal Demystified, we have seen how to call the renderModuleFactory method from the Angular Universal API to generate an HTML file for a page of an Angular app. In this article we will use this technique to build our first static website. You can get the complete sources from this github repo at tag step-02 (git checkout step-02).

Structure of the website

In this article, we will create a mini-website with a home page and a list of pages with a master/detail interface. For this, we will create 3 components: an HomeComponent, a PageComponent displaying the list of elements and PageIdComponent displaying the details of an element.

Here is the router configuration for this mini-website:

// app.module.tsimports: [
[...]
RouterModule.forRoot([
{ path: '', component: HomeComponent },
{ path: 'page.html', component: PageComponent },
{ path: 'page/:id', component: PageIdComponent }
])
[...]
]

We place the main menu and the router outlet in the AppComponent:

// app.component.html<h1>app</h1>
<ul>
<li><a [routerLink]="['/']">Home</a></li>
<li><a [routerLink]="['/page.html']">Pages</a></li>
</ul>
<router-outlet></router-outlet>

In the PageComponent we display a list of pages:

// page.component.html<h2>Page list</h2>
<ul>
<li><a [routerLink]="['/page', '1.html']">Page 1</a></li>
<li><a [routerLink]="['/page', '2.html']">Page 2</a></li>
<li><a [routerLink]="['/page', '3.html']">Page 3</a></li>
<li><a [routerLink]="['/page', '4.html']">Page 4</a></li>
</ul>

And finally in the PageIdComponent we display the details of a component. We also embed the PageComponent so the list of pages is still visible n the detail view:

// page-id.component.html<h2>Page details</h2>
Page {{id | async}}
<app-page></app-page>
// page-id.component.tsimport { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/pluck';
import 'rxjs/add/operator/map';
@Component({
selector: 'app-page-id',
templateUrl: './page-id.component.html',
styleUrls: ['./page-id.component.css']
})
export class PageIdComponent implements OnInit {
public id: Observable<string>; constructor(private route: ActivatedRoute) { } ngOnInit() {
this.id = this.route.params.pluck('id')
.map((s: string) => s.replace(/.html$/, ''));
}

}

Generating the static website

At this point we can navigate into our Angular app, for example running ng serve and navigating to http://localhost:4200. The different pages are:

We can now compile our app and run our previous script to generate our different HTML pages for our website:

$ ./node_modules/.bin/ngc
$ webpack
$ mkdir static
$ node dist/server/main.js / > static/index.html
$ node dist/main.js /page.html > static/page.html
$ mkdir static/page
$ node dist/main.js /page/1.html > static/page/1.html
$ node dist/main.js /page/2.html > static/page/2.html
$ node dist/main.js /page/3.html > static/page/3.html
$ node dist/main.js /page/4.html > static/page/4.html

We obtain simple HTML files which we can deploy in any webserver serving static files, for example surge.sh:

$ cd static
$ surge . ng-universal-demystified-1.surge.sh

We can now navigate into our static website at the address http://ng-universal-demystified-1.surge.sh and we can see that the files are HTML-only with no Javascript embedded. This means that the web crawlers from the different search engines will be able to grab the full contents of your website.

Adding Meta information to pages

If you want your site to be correctly referenced by search engines and fully functional for your users, you will need to add some meta-information to your pages, essentially a title and a description.

The Angular platform-browser API gives us two services to set title and meta tags of our pages: Title and Meta.

In this example, we will create an SeoService which will use these two services:

// seo.service.tsimport { Injectable } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
@Injectable()export class SeoService {
readonly siteTitle = 'My First Static Webiste';
constructor(private title: Title, private meta: Meta) { } setTitle(title: string[]): SeoService {
title.push(this.siteTitle);
this.title.setTitle(title.join(' | '));
return this;
}
setDescription(description: string): SeoService {
this.meta.updateTag({
name: 'description',
content: description
});
return this;
}
}

Next, we have to use this service for each generated page of the app. The best place to call this service is from the router: if we use it from components, we will loose the faculty to reuse the components in the app. In our case for example, the PageComponent is used twice: 1. at the /page.html url as a component inserted in the router outlet from the router and 2. at the urls likepage/1.html, inserted as a child of the PageIdComponent.

So let’s use this service from canActivate guards. Here is the new router configuration:

// app.module.tsRouterModule.forRoot([
{
path: '', component: HomeComponent,
canActivate: [SeoGuard],
data: {
title: ['Home page'],
desc: 'My First Static Website built with Angular Universal'
}
}, {
path: 'page.html', component: PageComponent,
canActivate: [SeoGuard],
data: {
title: ['Elements List'],
desc: 'My List of elements in my Static Website'
}
}, {
path: 'page/:id', component: PageIdComponent,
canActivate: [SeoPageIdGuard]
}
])

For pages without params like the home page and the list page, we can define titles and descriptions at route configuration time. So we can create a generic SeoGuard, getting the title and description directly from the route configuration, through the data attribute.

// seo.guard.tsimport { SeoService } from './seo.service';
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
@Injectable()
export class SeoGuard implements CanActivate {
public constructor(private seo: SeoService) { } public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
this.seo
.setTitle(route.data['title'])
.setDescription(route.data['desc']);
return true;
}
}

For paramterized pages like the details page, we can create a specific guard:

// seo-page-id-guard.tsimport { SeoService } from './seo.service';
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
@Injectable()
export class SeoPageIdGuard implements CanActivate {
public constructor(private seo: SeoService) { } public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
const id = route.params['id'].replace(/.html$/, '');
this.seo
.setTitle([ 'Element #' + id, 'Elements List' ])
.setDescription('Details of the element #' + id);
return true;
}
}

Generating the static website again

That’s it! It’s time to generate again our static pages ready for search engine indexation.

For some automation, we will write a file with the list of urls of our website:

// sitemap.txtpage.html
page/1.html
page/2.html
page/3.html
page/4.html

We can now execute this command to generate again all our static pages:

for i in '' `cat sitemap.txt`; do 
node dist/main.js /$i > static/${i:-index.html};
done

Note the '' entry in the for loop and the ${i:-index.html} substitution. This trick is used to map the / url to the /index.html file.

And finally, we can upload again these html pages to a static webserver:

$ cd static
$ surge . ng-universal-demystified-2.surge.sh

After this step, you can go and read how to Build a dyanmic website with Angular Universal.

--

--

Philippe Martin
Philippe Martin

Responses (5)