SEO for Universal Angular 2.0

Universal Angular 2 server side rendering for SEO & crawl-friendliness

There’s a lot of excitement around the release of Angular 2, for three reasons really:

  1. It’s been a long time coming (almost 2 years)
  2. The far more sane approach to modularity (component based UI)
  3. Server side rendering/universality

The last of these is what we’re going to be looking at here. Prior to Angular 2 (assuming you were using AngularJS) if you were hoping to have something that a search engine could understand, you’d have to follow the guidelines discussed here to fetch the output HTML and display that to a crawler instead of JS templates with no data.

The issues with that are fairly obvious – you can’t get meta data or content shown to a search engine, or a social crawler, which means no search rankings, and a poor social sharing experience. Fortunately, the requirement for that sort of system is now coming to an end. Angular 2 borrows a bunch of ideas from React, not least of which are the concept of component composed UI, and universal rendering with client-side JS pickup (see more on how React can be made to render server side for SEO here.)

Side note

The main challenge with this until now has been a philosophical one. React starts with JS and makes HTML what comes out. Angular starts with the aim of making HTML, and adding JS to it. Once you really understand this, the reasons for the differences between the two become obvious.

Making an Angular 2 App render server side

I’m going to expect familiarity with Angular 2 from here on, so if you’re not a developer, then be aware that here be monsters.

Let’s assume you’ve got an app basically up and running. The stack looks something like node, Express, Angular, gulp/grunt/broccoli/webpack/build chain of choice etc. If not, go to the Angular Universal quickstart guide and follow what it says. That will get you to a basic application which you can use.

The magic here is in Express having something to render beyond just the scaffold for Angular to fill in. With Angular 2 we can render entire pages on the server, with different data inserted based on the page, and therefore solve our no-data headaches. We do this with dependency injection.

Universal Header Template for Angular 2

At the top of your template component you’ll want to import and make available two objects, one called UniversalService, the other UniversalModel. We’ll get to what they look like later. Then in the head area, you’ll want to contain something like the following:


<!-- SEO -->
{{UniversalModel.title}}
<!-- Social -->
<!-- Social: OG -->
<!-- Social: Article -->
<!-- Social: Twitter -->
<!-- JS includes -->
<!-- CSS includes -->
<!-- Favicon -->

You’ll also need to tie the Universal service and model to that component. A basic version of that might look something like this:


export class HeaderTemplateComponent implements OnInit, OnDestroy {
  public universalModel: UniversalModel;
  private subscriber: EventEmitter;

  // Inject UniversalService with @param universalService
  constructor(private universalService: UniversalService) {}

  // Instantiate an empty model to populate and subscribe to universalService
  public ngOnInit() {
    this.universalModel = new UniversalModel();
    this.subscriber = this.universalService.eventEmitter.subscribe((universalModel) => {
      this.universalModel = universalModel;
    });
  }

  // unsubscribe from universalService
  public ngOnDestroy() { this.subscriber.unsubscribe() }
}

UniversalService

The first part of then making that template work is a service which allows the data from our back end to pass to components. That’d look like this:


import {Injectable, EventEmitter} from 'angular2/core';
import {UniversalModel} from './universal.model';

// Allow for DI
@Injectable()
export class UniversalService {
  public eventEmitter: EventEmitter;
  private universalModel: UniversalModel;

  // Instatiate all the things
  constructor() {
    this.eventEmitter = new EventEmitter();
    this.universalModel = new UniversalModel();
  }

  // Populate universalModel and emit an event to notify subscribers that there is now something here
  public set(universalModel: UniversalModel): void {
    this.universalModel = universalModel;
    this.eventEmitter.emit(this.universalModel);
  }
}

UniversalModel

The second part is creating a default model for our data:


export class UniversalModel {
  public title: string = '';
  public description: string = '';
  public robots: string = '';
  public keywords: string = '';
  public canonical: string = '';
  public publisher: string = '';
  public ogLocale: string = '';
  public ogType: string = '';
  public ogTitle: string = '';
  public ogDescription: string = '';
  public ogUrl: string = '';
  public ogSite_name: string = '';
  public ogUpdatedTime: string = '';
  public ogImage: string = '';
  public articlePublisher: string = '';
  public articleTag: string = '';
  public articleTag: string = '';
  public articleSection: string = '';
  public articlePublishedTime: string = '';
  public articleModifiedTime: string = '';
  public twitterCard: string = '';
  public twitterDescription: string = '';
  public twitterTitle: string = '';
  public twitterSite: string = '';
  public twitterImage: string = '';
  public twitterCreator: string = '';
}

Putting it all together

Now when you come to create components for parts of your application, you can use your Universal Header template and inject values to the Universal Model like this:


constructor(private universalService: UniversalService) {
  // Set up the data for UniversalModel. Would come from an API in real life. Must be a value set in UniversalModel
  let universalModel: UniversalModel =  {
    title: 'Builtvisible Homepage',
    description: 'The home page of Builtvisible, a digital marketing agency',
    canonical: 'https://builtvisible.com/',
    publisher: 'https://plus.google.com/+Builtvisible'
  };

  // Set the data for the service from the model
  universalService.set(universalModel);
}

Note that you can use the same principle to deal with authentication, checking for a logged in status and rendering different outcomes based on that flag. All you have to do is let the application know the user state, and it can then do different things based on that state value. We can also use this to render different blog posts in a CMS blog template, products in an ecommerce CMS and so on. Thanks to Angular’s adoption of DI, it becomes possible to render any conventional website on the server and deliver the page to the user ready to go.

Thanks to Johannes Werner for his work and inspiration for the code.

Comments are closed.

  • Hey, thanks for the very interesting and informative article!
    I’m trying to run this code alongside the github Universal repo (https://github.com/angular/universal) – specifically the “todo” example – and unfortunately I’m getting the following error: Type ‘EventEmitter’ is not assignable to ‘EventEmitter’. Type ‘{}’ is not assignable to type ‘UniversalModel’. Property ‘title’ is missing in type ‘{}’.

    Any idea where this might be coming from?


Join the Inner Circle

Industry leading insights direct to your inbox every month.