Building a static website with Angular Universal
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:
- http://localhost:4200 (the home page)
- http://localhost:4200/page.html (the list of elements)
- http://localhost:4200/page/1.html to http://localhost:4200/page/4.html (the details of the elements)
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 thefor
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.