Angular 4 with server side rendering (aka Angular Universal)

Single page application (SPA) frameworks are probably getting the most attention in the JavaScript world in the past years. Handling most of the processing at the client, boiler-plating the content on every page, maintaining the “state”, and omitting the overhead latency on switching pages are just some of its net benefits.

SPA’s provide an awesome User Experience!

Hell yeah, but we’ve got a small problem: the application has to be indexed by search engines!

Many search engines and social networks such as Facebook and Twitter expect plain HTML to utilize the meta tags and relevant page contents. They cannot determine when the JavaScript framework completes rendering the page. As a result, they can only see a very little part of HTML.

SPA’s suck against search engines!

Although Google is fully able to crawl and render most dynamic websites, it’s a mess when people try to share the website link on social networks.

So, we need some real SEO support!

True! We need the search engines, social networks and users of the application see a server-rendered view — as server-side rendering is a reliable, flexible and efficient way to ensure all search engines & social networks can fetch the page content.

Here comes the Angular Universal!

What is Universal

The official site states that Universal is “Server-side Rendering for Angular apps”. It’s the middleware that sits between node.js and Angular.

Simply put, it offers best of both worlds: the user experience and high performance and of SPA’s combined with the SEO friendliness of static pages.

Getting started

It’s highly recommended to clone the @ng-seed/universal repository, showcasing both Angular Universal and lean Angular on a single project (using node.js + express), with common patterns and best practices.

# clone the repo
git clone https://github.com/ng-seed/universal.git
cd universal

Then, install the dependencies.

# use npm (or yarn) to install the dependencies
npm install

This application uses platform-server delivered with Angular 4.0.0, custom implementations of ng-express-engine and state-transfer (both published on npm under @nglibs scope) until they got officially published on npm.

Furthermore, you’ll have a starter/seed application with most features of angular2-webpack-starter (by AngularClass) such as async/lazy routes, SCSS compilation (both inline and external), dev/prod modes, AoT compilation via @ngtools/webpack, tests, TsLint/Codelyzer, @types and maybe more.

Bootstrapping configuration

What you need to provide is bootstrapping configuration for both the server and the browser. A configuration for your target platform or back-end, e.g. Node, .Net…

The following bootstrapping structure is what is recommended by Universal:

main-browser.ts (client.ts)

This file is responsible of bootstrapping your application on the client.

// polyfills
import 'zone.js/dist/zone';
import 'reflect-metadata';
import 'rxjs/Observable';
import 'rxjs/add/operator/map';
// angular
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
// libs
import { bootloader } from '@angularclass/bootloader';
// app
import { AppBrowserModule } from './app/app.browser.module';
export function main(): any {
return platformBrowserDynamic().bootstrapModule(AppBrowserModule);
}
bootloader(main);

server.ts

This file is specific to your server/back-end configuration.

Import ngExpressEngine using the mapping '@ngx-universal/express-engine' on your server configuration (ex: server.ts) and bootstrap the AppServerModule (considering app.server.module is the server module in Angular Universal application) using ngExpressEngine as follows:
// polyfills
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import 'rxjs/Rx';
// angular
import { enableProdMode } from '@angular/core';
// libs
import * as express from 'express';
import * as compression from 'compression';
import { ngExpressEngine } from '@ngx-universal/express-engine';
// module
import { AppServerModule } from './app/app.server.module';
enableProdMode();
const server = express();
server.use(compression());
/**
* Set view engine
*/
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule
})
);
server.set('view engine', 'html');
server.set('views', 'public');
/**
* Point static path to `public`
*/
server.use('/', express.static('public', {index: false}));

/**
* Catch all routes and return the `index.html`
*/
server.get('*', (req, res) => {
res.render('../public/index.html', {
req: req,
res: res
});
});

/**
* Port & host settings
*/
const PORT = process.env.PORT || 8000;
const HOST = process.env.BASE_URL || 'localhost';
const baseUrl = `http://${HOST}:${PORT}`;

server.set('port', PORT);

/**
* Begin listening
*/
server.listen(server.get('port'), () => {
console.log(`Express server listening on ${baseUrl}`);
});

app.browser.module.ts

This module contains everything specific to the browser environment.

Import BrowserStateTransferModule using the mapping '@ngx-universal/state-transfer' and append BrowserStateTransferModule.forRoot({...}) within the imports property of app.browser.module (considering the app.browser.module is the browser module in Angular Universal application).
...
import { BrowserStateTransferModule } from '@ngx-universal/state-transfer';
...

@NgModule({
bootstrap: [LayoutMainComponent],
imports: [
BrowserModule.withServerTransition({
appId: 'my-app-id'
}),
BrowserStateTransferModule.forRoot(),
AppModule
]
})
export class AppBrowserModule {
}

app.server.module.ts (app.node.module.ts)

And, this module is dedicated to your server/back-end environment.

Import ServerStateTransferModule and StateTransferService using the mapping '@ngx-universal/state-transfer' and append ServerStateTransferModule.forRoot({...}) within the imports property of app.server.module (considering the app.server.module is the server module in Angular Universal application).
...
import { ServerStateTransferModule, StateTransferService } from '@ngx-universal/state-transfer';
...
@NgModule({
bootstrap: [AppComponent],
imports: [
BrowserModule.withServerTransition({
appId: 'my-app-id'
}),
ServerModule,
ServerStateTransferModule.forRoot(),
AppModule
]
})
export class AppServerModule {
constructor(private readonly stateTransfer: StateTransferService) {
}
  ngOnBootstrap = () => {
this.stateTransfer.inject();
}

}

app.module.ts

Import TransferHttpModule using the mapping '@ngx-universal/state-transfer' and append TransferHttpModule.forRoot({...}) within the imports property of app.module (considering the app.module is the core module in Angular application).
...
import { TransferHttpModule } from '@ngx-universal/state-transfer';
...

@NgModule({
bootstrap: [AppComponent],
imports: [
BrowserModule,
TransferHttpModule.forRoot(),
...
],
...
})
export class AppModule {
...
}

Build steps

The build steps and webpack configuration are all explained at the ng-seed/universal repository. Running the following npm commands will make this example app running.

# dev build (Universal)
npm run build:universal-dev
# prod build (Universal)
npm run build:universal-prod

# start the server (Angular Universal)
npm run serve

Gotcha’s

When building Universal components in Angular, there are a few things to keep in mind. The main one is, if you want Universal to keep working as expected, then you have to stay away from the DOM.

Do not ever touch the DOM!

This does not mean that you cannot perform DOM operations but do not do that with the native solutions (document.domMethod() or $('dom-element')).

Hail to the king, baby!

Now you have all the necessary information to get started with Universal, and enjoying the server-side rendering on Angular 4.

Burak Tasci (fulls1z3)
https://www.linkedin.com/in/buraktasci
http://stackoverflow.com/users/7047325/burak-tasci
https://github.com/fulls1z3

Credits: