Angular 4 with server side rendering (aka Angular Universal)

Burak Tasci
Mar 29, 2017 · 5 min read

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.

Image for post
Image for post

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.

Image for post
Image for post

Here comes the Angular Universal!

What is Universal

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

# 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

The following bootstrapping structure is what is recommended by Universal:

Image for post

main-browser.ts (client.ts)

// 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

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

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)

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

# 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

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!

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

Credits:

Burak Tasci

In depth articles about software technologies

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store