Create your account

Already have an account? Login here

Create account

Make your Angular App SEO-Friendly (Angular 4 + Universal)

By Gary simon - May 18, 2017

The Github Repo for this Tutorial: Visit the repo

By default, Angular apps are not search engine friendly. If you view the page source in a browser of a regular Angular app, it will only show what's inside the regular index.html -- the infamous loading.. content. To fix this, we need the app to be rendered on a server and then displayed to the client. This way, your content for each page (each component) is spit out for the browser and that lovely Googlebot to see.

But to a beginner-intermediate Angular developer, the topic of Angular SEO is usually a confusing one. Quick Google searches reveal outdated information and tutorials that no longer work.

Fortunately, I'm here to do my absolute best to make everything easy to understand and walk you through the setup process through this written tutorial, and a video tutorial that you can watch below (some people prefer watching instead of reading!).

So, let's get started.

If you prefer watching a video..

Be sure to Subscribe to the Official Coursetro Channel on Youtube for more awesome videos.

Get the Project Up and Running

As usual, we will start the project with the Angular CLI. Check out our Free Angular Course if you're new to the CLI.

> ng new ang4-seo --routing
> cd ang4-seo

The --routing flag will generate a quick routes file and add it to our app module.

Once we're in the new project folder, we'll use npm to install the platform-server, which is needed for server side rendering and generating HTML pages. We also must install the animations package (new to Angular v4), otherwise your app will result in an error:

> npm install --save @angular/platform-server @angular/animations

Next, we'll head into /src/app/app.module.ts and make a slight adjustment:

  declarations: [
  imports: [
    BrowserModule.withServerTransition({appId: 'ang4-seo-pre'}),
  providers: [],
  bootstrap: [AppComponent]

The only line that is changed is the BrowserModule. Here, we've added a .withServerTransition which takes in an appId that is shared between the client and the server. .withServerTransition allows Universal to replace the HTML it generated on its own.

Save this file.

Create a Server App Module

Create a new file called /src/app/app.server.module.ts and paste in the following code:

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';

imports: [
bootstrap: [AppComponent]
export class AppServerModule { }

This is used to bootstrap the app from the server. If you compare it against /src/app/app.module.ts, they're very similar. The main takeaway here is that this module is for the server.

Creating the Express Server

Naturally, we need a server to render the app before pushing it to the client. So, create a new file called /src/server.ts and paste the following:

import 'reflect-metadata';
import 'zone.js/dist/zone-node';
import { platformServer, renderModuleFactory } from '@angular/platform-server'
import { enableProdMode } from '@angular/core'
import { AppServerModuleNgFactory } from '../dist/ngfactory/src/app/app.server.module.ngfactory'
import * as express from 'express';
import { readFileSync } from 'fs';
import { join } from 'path';

const PORT = 4000;


const app = express();

let template = readFileSync(join(__dirname, '..', 'dist', 'index.html')).toString();

app.engine('html', (_, options, callback) => {
  const opts = { document: template, url: options.req.url };

  renderModuleFactory(AppServerModuleNgFactory, opts)
    .then(html => callback(null, html));

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

app.get('*.*', express.static(join(__dirname, '..', 'dist')));

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

app.listen(PORT, () => {
  console.log(`listening on http://localhost:${PORT}!`);

I won't go into detail about everything that's happening here, as much of it is specific to Express and extends outside of the scope of this tutorial.

But, the heart of what's specific to server-side rendering (Universal) here is the renderModuleFactory method.

It takes in AppServerModuleNgFactory that the server generates after building the project, which is a compiled Angular app:

import { AppServerModuleNgFactory } from '../dist/ngfactory/src/app/app.server.module.ngfactory'

Note that it's located within the standard /dist folder that's generated after running the ng build command. Then it serves it as HTML.

TypeScript Config Adjustments

Open /src/ and add the server.ts file we created earlier as an exclude:

  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "module": "es2015",
    "baseUrl": "",
    "types": []
  "exclude": [
    "server.ts",    // Right here

Then, open /tysconfig.json and add angularCompilerOptions:

  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "baseUrl": "src",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "typeRoots": [
    "lib": [
  "angularCompilerOptions": {                             // Here
      "genDir": "./dist/ngfactory",
      "entryModule": "./src/app/app.module#AppModule"

angularCompilerOptions allows us to specify a genDir property, which is where the generated ngfactory files for the components and modules will go, and an entryModule which accepts the path of our main bootstrapped module. The #AppModule at the end of the path is the name of the exported class.

Adjust the Run Scripts

Inside /package.json we'll adjust the scripts property:

  // Other properties removed for brevity

  "scripts": {
    "prestart": "ng build --prod && ngc",
    "start": "ts-node src/server.ts"

  // Other properties removed for brevity


We add a pre in front of the start command to first run the ng build --prod && ngc command. After that, it launches the server based on our server.ts config.

Now, you can execute the following command in the console to run the app!

> npm run start

If all goes smooth, the output will look something like this:

> ang4-seo-pre@0.0.0 prestart C:\Users\gary\code\ang4-seo-pre
> ng build --prod && ngc

Hash: 831974a6aced5d532e0d
Time: 6395ms
chunk    {0} polyfills.6ce0bf58446ef5c2585b.bundle.js (polyfills) 157 kB {4} [initial] [rendered]
chunk    {1} main.94206ad8fdce0f306470.bundle.js (main) 21.3 kB {3} [initial] [rendered]
chunk    {2} styles.d41d8cd98f00b204e980.bundle.css (styles) 69 bytes {4} [initial] [rendered]
chunk    {3} vendor.20c071d0dcfbd58f1c48.bundle.js (vendor) 1.1 MB [initial] [rendered]
chunk    {4} inline.6729d86a888b9e64ae89.bundle.js (inline) 0 bytes [entry] [rendered]

> ang4-seo-pre@0.0.0 start C:\Users\gary\code\ang4-seo-pre
> ts-node src/server.ts

listening on http://localhost:4000!

If you visit http://localhost:4000 in a browser, you can view the page source and now the source actually shows app works! when it would typically show the default Loading.. text defined in index.html.

Let's give it a better test and generate a couple components with routes. We'll even add custom title and meta tags for each page.

Generate Components

Using the CLI to generate components is easy, but when you run the standard command, such as ng g c home, you might run into an error:

> ng g c home
installing component
Error locating module for declaration
        SilentError: Multiple module files found:

To avoid this, we can specify the module by adding a flag. So, run the following commands to generate 2 components:

> ng g c home --module=app.module.ts
> ng g c about --module=app.module.ts

Setup the Routes

Let's hop into the /src/app/app-routing.module.ts file that the CLI generated with the --routing flag:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about/about.component';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
    path: '',
    component: HomeComponent
    path: 'about',
    component: AboutComponent

  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
export class AppRoutingModule { }

Here, we're importing our About and Home components that we just generated, and we're adding them each to Routes.

App Component Template

Let's visit the /src/app.component.html file and change it to this:

  <li><a routerLink="/">Home</a></li>
  <li><a routerLink="about">About</a></li>


We're creating a navigation with a router-outlet for our home and about components.

Adding Title and Meta Tags to our Components

A cornerstone of on-page SEO factors are unique titles, meta descriptions and meta keywords. Fortunately, the process of defining these elements is straightforward for each component.

In /src/app/home/home.component.ts import up top:

import { Meta, Title } from "@angular/platform-browser";

Then, in the constructor:

export class HomeComponent implements OnInit {

  constructor(meta: Meta, title: Title) {

    title.setTitle('My Spiffy Home Page');

      { name: 'author',   content: ''},
      { name: 'keywords', content: 'angular seo, angular 4 universal, etc'},
      { name: 'description', content: 'This is my Angular SEO-based App, enjoy it!' }


  ngOnInit() {


We're using dependency injection to create an instance of both Meta and Title, then we're using both to set the titles and various meta tags that we wish to include for this component.

Save it.

About Component

Now, we'll repeat the same exact process above for the /src/app/about/about.component.ts file. Just be sure to change the title and meta tags to something unique so that we can see them change momentarily.

Let's also change the template slightly in /src/app/about/about.component.html:

<p>And this is my lovely description that I want the Google to see!</p>

Run it!

Visit the console, ctrl-c out if the server is still running and then run:

> npm run start

With any luck, it will run without error and you can view the page source of the first page and notice the title and 3 meta keywords are there as specified within the home component.

Then, click on the about link and check out the page source again. Wala!

Hopefully, this tutorial helped you out quite a bit and put you on the way to SEO success.

This tutorial is largely based on the example located here, however, it lacked any explanation. Kudos to Éverton Roberto Auler though!

Skip the manual setup stuff

The project that I just created can be cloned at Github.  Just visit this link and you can be up n' running quickly!


Share this post

Chat with us

Say something about this awesome post!