Angular server-side rendering with @ng-toolkit/universal

Introduce server-side rendering with @ng-toolkit/universal, and make it readable for search engine robots.

Maciej Treder
May 27, 2018 · 4 min read

SPA pitfall — SEO

Most of the Single Page Applications have one big problem. They are rendering and working on the client side. Now you can shout at me: “Hey! This is an advantage, not pitfall!”. And you are completely right. The problem starts when your application renders ONLY on the client side.

How crawlers explores the web?

The problem is that search engine robots/crawlers work similar to the curl command. Why? Launching browser and executing JavaScript is an expensive task. There is no time and resources to do that when you need to search thousands of pages.

Let’s create an angular app and check, how robots see it:

ng new myApp
cd myApp
ng serve

Now in the other terminal window execute curl command and take a look at the output:

$ curl localhost:4200<!doctype html><html lang="en"><head><meta charset="utf-8"><title>MyApp</title><base href="/"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" type="image/x-icon" href="favicon.ico"></head><body><app-root></app-root><script type="text/javascript" src="runtime.js"></script><script type="text/javascript" src="polyfills.js"></script><script type="text/javascript" src="styles.js"></script><script type="text/javascript" src="vendor.js"></script><script type="text/javascript" src="main.js"></script></body></html>

Do you see the problem? What we got inside <body>? Nothing. App bootstrap node; and some JavaScript. How can we solve it?

Server-side rendering (Angular Universal)

The solution is Angular Universal. An Angular extension for nodeJS, which launches the application on the server and generate full HTML response depending on income request. Now, the flow from the user request, to the rendered and interactive web app, looks like that:

  1. User requests the page

Ok. We know the theory. Let’s go with practice now. You can go forward with the guide from Angular Universal creators then you will probably end with the Universal Starter repository.

Angular 6 is released, with new great CLI tools, and they propose me to make a manual setup or start with the boilerplate? Give me other solution!

@ng-toolkit/universal — one command to add Angular Universal to any project

That’s where @ng-toolkit/universal comes into the game. Type one simple command:

ng add @ng-toolkit/universal

And boom! You got it! The whole setup is done for you!

Let’s launch the app:

npm run build:prod
npm run server

And check the curl comand in separate terminal window:

$ curl localhost:8080
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8"><title>MyApp</title><base href="/"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" type="image/x-icon" href="favicon.ico"><link rel="stylesheet" href="styles.34c57ab7888ec1573f9c.css"><style ng-transition="app-root"></style></head><body><app-root _nghost-c0="" ng-version="6.0.3"><div _ngcontent-c0="" style="text-align:center"><h1 _ngcontent-c0=""> Welcome to app! </h1><img _ngcontent-c0="" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==" width="300"></div><h2 _ngcontent-c0="">Here are some links to help you start: </h2><ul _ngcontent-c0=""><li _ngcontent-c0=""><h2 _ngcontent-c0=""><a _ngcontent-c0="" href="https://angular.io/tutorial" rel="noopener" target="_blank">Tour of Heroes</a></h2></li><li _ngcontent-c0=""><h2 _ngcontent-c0=""><a _ngcontent-c0="" href="https://github.com/angular/angular-cli/wiki" rel="noopener" target="_blank">CLI Documentation</a></h2></li><li _ngcontent-c0=""><h2 _ngcontent-c0=""><a _ngcontent-c0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2></li></ul></app-root><script type="text/javascript" src="runtime.a66f828dca56eeb90e02.js"></script><script type="text/javascript" src="polyfills.2f4a59095805af02bd79.js"></script><script type="text/javascript" src="main.178573f0f1b826344a91.js"></script><script id="app-root-state" type="application/json">{}</script></body></html>

Better? Much better! Now search engine robots sees that your app have some content!


That’s not all. You can go forward and use another great tool from @ng-toolkit:

ng add @ng-toolkit/serverless

And… Your application with server-side rendering is ready to deploy on AWS Lambda (read more about Serverless here)!


What about window and localStorage?

NodeJS and browsers can run in 99% cases same code. But there are some things which can be run only on NodeJS or only on Browser. Now I would like to focus on what NodeJS is not supporting, and how @ng-toolkit/universal helps you address those problems.

window — that’s the thing which we don’t have on server-side. No browser = no window. Let’s say we want to execute the following code in our application:

public ngOnInit(): void {    console.log(window.URL);}

After building and running the project: npm run build:prod;npm run server we will run into the following issue when we navigate to http://localhost:8080:

ERROR ReferenceError: window is not defined

That’s why @ng-toolkit/universal comes with wrapper for window object. It can be easily injected into your components and services:

import { LOCAL_STORAGE , WINDOW} from '@ng-toolkit/universal';
import { Component, OnInit , Inject} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(@Inject(WINDOW) private window: Window) {}
public ngOnInit(): void {
console.log(this.window.URL);
}
}

It’s super-simple!

Another functionality, missing on NodeJS and wrapped by @ng-toolkit/universal is localStorage. Again, we can simply inject it:

import { LOCAL_STORAGE , WINDOW} from '@ng-toolkit/universal';
import { Component, OnInit , Inject} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(@Inject(LOCAL_STORAGE) private localStorage: any) {}
}

Thanks for reading!

Support @ng-toolkit project by starring it on GitHub, sharing with friends and placing a donation on OpenCollective or DonorBox or LiberaPay.

In case of bug, please report it on GitHub.

Maciej Treder, https://www.maciejtreder.com

Follow me on Twitter and GitHub to be notified about my new activities.

Maciej Treder

Written by

Senior Software engineer at Akamai Tech; Twilio Champion; Author of ng-toolkit project Enthusiast of Angular

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