Angular Universal For The Rest Of Us

This article has been updated to match the redesign of Angular Universal introduced in Angular v4.

Hello you, Web developer,

Let me guess. You have been working on your Web project for months, which is most likely a Web application and more specifically, a “Single Page Application”. But now comes the time for your application to be shipped and published to millions of users and … Search engines. Yep! In order for your application to be successful, it has to be indexed by search engines. So now you’re starting to wonder: damn! I need to add SEO support!


In your journey, you are starting to realize that this is not going to be an easy task. Because we all know how hard is it to setup a whole pipeline for server side rendering support of an SPA. But hey, you’re a developer and you like challenges, right?

After a couple of days (or weeks?) of trials and errors, you managed to add SEO support. Congratulations!

However, your users are starting to complain about the startup time of your application. Let’s be honest, 6 or 10 seconds to display the first page is not an ideal experience. Right? We, humans, are not patient. When I visit a website, I need it to show me what I came looking for almost instantly; I can wait for 3 seconds but definitely not 10! Life is too short to wait staring at an empty page with a spinner, for 10 seconds.

So now you have one more issue to solve and it is not one of the easiest ones. Besides, your other users — the Web crawlers — report that your application is not “social media friendly”. What this means is that when a user shares a link of your application on social medias, the generated preview of your application is kind of “broken”: it shows a blank preview or, in the best case, only some parts of the page. This is an expected result, because at the end of the day, the social media backend generates a screenshot of what gets rendered when it requests your application.

So now you have two issues to solve. You still like challenges, right? However, your PM usually tells you that time is money and these issues must be fixed for… well… yesterday!?

What if I tell you that you can avoid these kind of issues? These are exactly the kind of tasks that Angular Universal helps you solve. If you are using Angular, of course.

Yeah! That is right. Angular has a native support for server side rendering thanks to the official Universal module.

Let’s take a look at Universal, and see how it works. But most importantly, how Universal can help you ship SEO-instant-rendered-social-media friendly Angular applications.


What is Universal?

The official site states that Universal is: Server-side Rendering for Angular apps. However, here is how I like to define Angular Universal:

Universal is Angular for the Headless Web.

You do not need a browser container anymore, aka a WebView, to run Angular. Since it is not tied to the DOM, Angular can run anywhere, where there is a JavaScript runtime.

I shamefully borrowed the term “Headless Web” from Paul Kinlan. I strongly recommend reading his blog post where he gives his vision about what the Web should be in the future.

The big picture

Before using Universal, let’s take a look at the overall architecture.

Angular Universal High Level Architecture

This diagram illustrates the ability of Universal to run your typical Angular Web application outside the browser. Obviously we need a JavaScript runtime, that is why we support Node.js (which is powered by the V8 engine) by default. However, some efforts have started to support other server side technologies such as PHP, Java, Python, Go … and I’m happy to say that Universal already works on the .Net stack thanks to Steve’s work.

Now that your application can be interpreted outside the browser —let’s take a server as an example— the client which requested your SPA will receive a static fully rendered page of the requested route/URL. This page contains all the related resources, i.e. images, stylesheets, fonts… even data coming through your Angular service.

In fact, Universal takes care of handling and fetching all required data before rendering the page— that you usually fetch with Angular’s Http service. Universal is capable of rewiring some of the default Angular’s providers so they can work on the target platform. There is more. When the client receives the rendered page, it also receives the original Angular application — we only help your application loads almost instantly. Once loaded, Angular takes care of the rest.

But what happens when Angular bootstraps over the rendered page? Won’t we have some sort of states issues? Or maybe the user would want to use your application just after they see the rendered page — which makes sense? How would you then handle state? How does Universal itself handle state?

We got you covered…

In fact, Universal comes bundled with the Preboot.js library which sole role is to make sure of that both states are synced.

What Preboot.js does under the hood is simply and intelligently record the events that occur before Angular bootstraps; and play them back after Angular has completed loading. Simple, isn’t it?

Renderers

Universal is made possible thanks to Angular’s rendering abstractions. In fact, when you write your application code, that logic gets parsed into an AST by Angular’s compiler — we are really simplifying things here . That AST is then consumed by the Angular’s rendering layer, which uses an abstract renderer that is not tied to the DOM. Angular allows you to use different renderers. By default Angular ships the DOMRenderer so your application can be rendered in a browser, which is probably 95% of the use cases.

And that’s where Universal comes in. Universal comes with bunch of prerenderers, for all the mainstream technologies and build tools.

Dependency Injection and Providers

Another area where Angular shines in is with its DI system. Angular is in fact the only front-end framework that implements this design pattern which allows to accomplish so many great tasks easily (see IOC). Thanks to DI you could for instance swap two different implementations at run time, which is heavily used in testing.

In Universal, we take advantage of this DI system and provide you with many services that are specific to the targeted platform. For Node, we provide a custom ServerModule that implements Node’s server specific APIs such as requests rather than Browser’s XHR. Universal is also shipped with a custom renderer specific to Node, and of course we provide you with a bunch of prerenderers — as we call them — such as Express renderer or Webpack renderer for your Node backend technologies. For other non-JavaScript technologies, such as .NetCore or Java, you should expect other prerenderers as well.


Hello World

The good news is that a Universal application is no different from a classic Angular application. Usually your application logic remains the same, literally. Of course, this would be true if you strictly follow Angular’s guidelines and best practices. One of THE MOST IMPORTANT best practices I’d recommend you to follow is:

Whenever it’s possible, please think twice before touching the DOM directly. Make sure to use the Angular Renderer or rendering abstraction every time you want to interact with the browser’s DOM.

The, the only extra thing you need to provide is a second bootstrapping configuration. A configuration for your target platform or backend, e.g. Node, .Net…

The following bootstrapping structure is what is recommended by Universal:

Angular Universal Application Structure

Let’s explain the role of each file…

app.component.ts

This component and all other Angular API/features such as the component template, Directives, Services, Pipes…etc remain the same. Nothing fancy:

import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app works!';
}

browser.module.ts

In this module, you should put everything specific to the browser environment:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './index';
@NgModule({
bootstrap: [ AppComponent ],
declarations: [ AppComponent ],
imports: [
BrowserModule.withServerTransition({appId: 'some-app-id'}),
...
]
})
export class AppBrowserModule {}

Please note that you need to initialise the BrowserModule with withServerTransition() method. This will make sure the browser-based application will transition from a server-rendered application.

server.module.ts

This module is dedicated to your server environment. The ServerModule provide a set of providers from the @angular/platform-server package.

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppComponent, AppBrowserModule } from './browser.module';
@NgModule({
bootstrap: [ AppComponent ],
declarations: [ AppComponent ],
imports: [
ServerModule,
AppBrowserModule,
...
]
})
export class AppServerModule {}
In AppServerModule, you should import both ServerModule and AppBrowserModule in order for them to share the same appId, the transition ID used by AppBrowserModule.

client.ts

This file is responsible of bootstrapping your application on the client. Nothing new here, just the usual bootstrapping process (in AOT mode):

import { platformBrowser } from '@angular/platform-browser';
import { AppModuleNgFactory } from './ngfactory/src/app.ngfactory';
import { enableProdMode } from '@angular/core';
enableProdMode();
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

server.ts

This file is really specific to your server/backend environment. Here, we are targeting Node.js and more precisely the Express framework to take care of handling all client requests and the rendering process. For that, we are using and registering the ngExpressEngine (see the next paragraph) which represents the Angular Universal rendering engine for Express:

import { 
platformServer,
renderModuleFactory
} from '@angular/platform-server';

import {
AppServerModuleNgFactory
} from './ngfactory/src/app.server.ngfactory';
import { enableProdMode } from '@angular/core';
import { AppServerModule } from './server.module';
import * as express from 'express';
import {ngExpressEngine} from './express-engine';

enableProdMode();

const app = express();

app.engine('html', ngExpressEngine({
baseUrl: 'http://localhost:4200',
bootstrap: [AppServerModuleNgFactory]
}));


app.set('view engine', 'html');
app.set('views', 'src')

app.get('/', (req, res) => {
res.render('index', {req});
});

app.listen(8200,() => {
console.log('listening...')
});

Implementing a simple a renderer for Express should be an easy task :

const fs = require('fs');
const path = require('path');
import {renderModuleFactory} from '@angular/platform-server';

export function ngExpressEngine(setupOptions){
return function(filePath, options, callback){
    renderModuleFactory(setupOptions.bootstrap[0], {
document: fs.readFileSync(filePath).toString(),
url: options.req.url
})

.then(string => {
callback(null, string);
});
  }
}

The only important part here is the renderModuleFactory method. What this method does is basically bootstrap the Angular application into a virtual DOM tree parsed from the document, and serialise the resulting DOM state to a string, which you pass to the Express engine API.

You can of course add some caching mechanism to this renderer to avoid reading from disk on each request. Here is a naive example:

const fs = require('fs');
const path = require('path');
import {renderModuleFactory} from '@angular/platform-server';
const cache = new Map();
export function ngExpressEngine(setupOptions){
return function(filePath, options, callback){
    if (!cache.has(filePath)){
const content = fs.readFileSync(filePath).toString();
cache.set(filePath, content);
}
    renderModuleFactory(setupOptions.bootstrap[0], {
document: cache.get(filePath),
url: options.req.url
})
.then(string => {
callback(null, string);
});
  }
}

Since you have total control over the server rendered content, you can easily add any SEO support you would like. We can imagine using the Meta and Title provided by @angular/platform-browser:

import { Component } from '@angular/core';
import { Meta, Title } from "@angular/platform-browser";
@Component({
selector: 'home-view',
template: `<h3>Home View</h3>`
})
export class HomeView {
constructor(seo: Meta, title: Title) {
title.setTitle('Current Title Page');
seo.addTags([
{name: 'author', content: 'Wassim Chegham'},
{name: 'keywords', content: 'angular,universal,iot,omega2+'},
{
name: 'description',
content: 'Angular Universal running on Omega2+'
}
]);

}
}
Meta and Title built-in services in action

You can also add any other Social Medias Tags or even Open Graph Tags.

Of course, this information could come from any data source, such an API, and be stored in a model for instance:

// SEO model
export class SEO {
public title: string = '';
public canonical: string = '';
public robots: string = '';
public description: string = '';
public keywords: string = '';
}

So now, you may be wondering: Oh! boy, that’s a lot of boilerplate! Well, we’ll help you with that. Let’s talk about the tooling…


Tooling

Angular Universal CLI support is currently in a Fork of the Angular CLI here, but hopefully will become integrated in the regular CLI sometime soon.. But till then, you can get started with Angular Universal quickly and easily by using the Official Angular Universal Starter, created by my buddy PatrickJs. If you are a .Net developer, please check this other starter by my bro Mark Pieszak.


That’s It

Dear Web developer, you now have all the necessary information to get started with Angular Universal and make your next Angular Application more performant and SEO friendly and so much more…

Don’t forget to give a huge shoutout for Jeff Whelpley and PatrickJS and also for all of The Universal Project contributors for making Angular Universal rock!

Edit: Don’t get me wrong, Universal is not only about server side rendering or SEO. It gives you so much more capabilities and power when you need to run your application outside the browser. We’ll talk about this in another blog post.

Follow @manekinekko to learn more about the web platform.