PULSEPLAY_DIGITAL_LOGO
Ranjeet Sharma

Ranjeet Sharma

Head, Digital Solutions Delivery

June 14, 202310 min read284

Angular Universal In Practice - How to build SEO Friendly Single Page Apps with Angular

/20230614-o7mze-angular-universal

Angular Universal In Practice - How to build SEO Friendly Single Page Apps with Angular

There has been a lot of talk about Angular in the last few months and how to use it to build client apps, but one of its most important innovations is actually happening on the server.

This is a technology that could help enable a whole new type of web applications: Angular Universal. Let's find out more about it! Let's go over the following topics:

  • Advantages of single page apps
  • Why don't we use single page apps everywhere then?
  • Understanding the SEO implications of a single page app
  • What is Angular Universal, what does it enable?
  • Server-side rendering is really not just rendering on the server
  • A demo of a server side rendered single page app in action
  • Pre-rendering at build time - upload pre-rendered apps to Amazon S3
  • The killer feature of Angular Universal: Dependency Injection
  • Conclusions

Advantages of Single Page Applications

Single page apps have been around for a while, and frameworks like Angular, React or Ember are probably the Javascript libraries that get the most attention in the Javascript world.

The advantages of SPAs are really just one thing

The advantages of single page apps are potentially many:

  • when the user navigates on the page, only parts of it are replaced, making for a more fluid experience
  • after the first load of the page, only data goes over the wire when the user navigates the app: JSON is delivered to the browser and applied to HTML templates directly to the browser
  • this leads to better performance and opens the possibility for those backend services to be used for other things, as they just return data

We can boil this down to only one thing:

Single Page Apps can provide a much better User Experience!

And user experience is critical in the public Internet world. There are numerous studies that prove beyond any doubt that page drop off and user abandonment increase very quickly with page delay.

If a page takes 200ms more to load, this will have a potential high impact on business and sales (see this research by Google). This is even more so on mobile devices where apps tend to be slower.

Why not use SPAs everywhere then?

Given these stats and the fact the SPAs give a much-improved user experience, why isn't everybody using single page apps for everything?

They have been around for years, and any website with a navigation system could benefit from being built that way.

Understanding the SEO implications of a single page app

There is one major reason why single page apps are not used everywhere so far (with two separate causes):

Single page apps don't perform well on search engines

The two reasons for that are:

The search engine needs to "guess" when the page is complete

When a single page is retrieved, a search engine will see only very little HTML. Only when the MVC framework kicks in will the page actually be rendered fully using the data obtained from the server.

The problem is that the search engine needs to guess when the Javascript framework finishes rendering the page, so there is a risk of indexing incomplete content.

The second reason for why SPAs don't perform well with search engines is:

SPA deep links are hard to get indexed

Due to the lack of HTML5 History support in browsers, single page apps based their navigation URLs in HTML bookmark anchors (URLs with #, like /home#section1). These are not easily indexed as separate pages by search engines, there are ways to do it but it's a pain and there will always be difficulties getting this indexed right as opposed to using just plain HTML.

The conclusion could be that there is no point in having the most easily navigable site if the way it's built prevents from having good SEO.

Now the Good News

The good news is that none of these two reasons are 100% accurate anymore! Google has started to index better single page apps.

And the recent deprecation of IE9 means that HTML5 History is available almost everywhere, making the use of anchor URLs not needed anymore for SPAs, we can just use plain URLs (like /home/section1).

Also, there is still the issue of performance: a single page app will be slower due to the large Javascript amounts that it needs and large startup time, and will therefore perform worse than an HTML-based solution.

And this means page drop offs, this issue will not go away anytime soon especially on mobile. Is there any way to have the best of all worlds, and get both instant navigation, SEO friendliness and high performance on mobile?

The answer is Yes, with Angular Universal.

What is Angular Universal, what does it enable?

Simply put, Angular Universal enables us to build apps that have both the performance and user engagement advantages of single page apps combined with the SEO friendliness of static pages.

Server-side rendering is really not just rendering on the server

Angular Universal has several other features other than giving a solution for rendering HTML on the server. Based on the term "server-side rendering", we could think that what Angular Universal does is similar to, for example, a server-side template language like Jade. But there is a lot more functionality to it.

With Angular Universal, you get that initial HTML payload rendered on the server, but you also boot a trimmed down version of Angular on the client and from there Angular takes over the page as a single page app, generating from there all the HTML on the client instead of the server.

So the end result that you get is the same, it's a running single page application, but now because you got the initial HTML payload from the server you get a much better startup time and also a fully SEO indexable app.

Pre-rendering at build time - upload pre-rendered apps to Amazon S3

Another possibility that Angular Universal opens is pre-rendering of content that does not frequently change at build time. This will be possible using either Grunt, Gulp or Webpack plugins. This is what a Gulp configuration will look like, that pre-renders the content of our application to a file:

import * as gulp from 'gulp';

import {gulpPrerender} from '@angular/universal';

import {App} from './app';

gulp.task("prerender", () => {

  return gulp.src(['index.html'])

      .pipe(gulpPrerender({

          directives: [App]

      }));

});

And then simply upload this to an Amazon S3 bucket using the Amazon CLI:

aws s3 cp prerendered/*.html  s3://your-amazon-s3-bucket/your-site --recursive

  --expires 2100-01-01T00:00:00Z --acl public-read

  --cache-control max-age=2592000,public --content-encoding gzip

If we link this bucket to a Cloudfront CDN distribution, we have a very affordable and scalable website.

What if the user starts interacting with the page immediately?

There is an initial lag between the moment that the plain HTML is rendered and presented to the user and the moment that Angular kicks in on the client side and takes over as a SPA.

In that timeframe, the user might click on something or even start typing in a search box. What Angular Universal allows via its preboot functionality is to record the events that the user is triggering, and play them back once Angular kicks in.

This way Angular will be able to respond to those events, like for example by showing the search results on a Typeahead list.

But what does this look like on the server in terms of code?

How to render HTML with Angular in the server

The way that the content is rendered on the server is by using the express Angular Universal rendering engine expressEngine :

import {createEngine} from 'angular2-express-engine';

import { UniversalModule } from 'angular2-universal';

app.engine('.html', createEngine({}));

app.set('views', __dirname);

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

@NgModule({

  bootstrap: [ App ],

  declarations: [ App, ... ],

  imports: [

      UniversalModule,

      ...

  ],

  providers: [...],

})

export class MainModule {

}

function ngApp(req, res) {

  let baseUrl = '/';

  let url = req.originalUrl || '/';

  let config = {

      req,

      res,

      requestUrl: url,

      baseUrl,

      ngModule: MainModule,

      preboot: false,

      originUrl: ROOT_URL

  };

  console.log("rendering ...");

  res.render('index', config, (err, html) => {

      res.status(200).send(html);

  });

}

There is also a Hapi engine available if you prefer to use Hapi instead of Express. There are also server rendering engines coming up for all sorts of platforms: C#, Java, PHP.

How to start with Angular Universal?

The best place to start is the official starter universal-starter, with the starter we get a running application which includes an express server with server side rendering working out of the box.

What is innovative about Angular Universal is its simplicity of use. One of the main design features of Angular Universal is the way that it uses dependency injection.

Server Side Rendering Development is not like coding for the client only

Most of the time we want our application to do the exact same thing on the server as on the client, by not depending directly on browser APIs.

The reality of server-side rendering development is that that is not always possible, and there are certain things that we want to do differently on the client and on the server.

Take for example the rendering of a chart: you probably want to call a third party library that uses SVG. We cannot call it on the server, so how do we render it?

Another example, how can we render authenticated pages? Because the content depends on who is currently logged in.

Using Dependency Injection to implement Authentication

To handle authentication, this is one way to do it:

  • On the client we want the identity of the user to be taken from a token available either on a cookie or in the browser local storage.
  • on the server, while rendering the request we want the identity token to be read from an HTTP request header.

How to have the same page output while navigating to that page on the client via a router transition vs on the server via a browser refresh?

First define a Service interface

The first step is to define an interface that your service will provide, which is independent of the runtime:

export interface AuthenticationService {

  signUp(activationLink:boolean);

  login();

  logout();

  isLoggedIn();

  isLoggedInWithGitHub();

}

Then we provide two implementations for this interface, one for the client and the other for the server. For example on the server there is no occasion for the login method being called, so we throw an error:

export class ServerAuthenticationService implements AuthenticationService {

  constructor(private authenticationId, private isUserLoggedIn:boolean) {

  }

  signUp() {

      throw new Error("Signup event cannot be called while doing server side rendering");

  }

  login() {

      throw new Error("Login event cannot be called while doing server side rendering");

  }

  logout() {

      throw new Error("Logout event cannot be called while doing server side rendering");

  }

  isLoggedIn() {

      return this.isUserLoggedIn;

  }

  isLoggedInWithGitHub() {

      ...

  }

}

While on the client, we are going to trigger the Auth0 lock screen (a third-party browser only library) to provide a sign-in form:

@Injectable()

export class ClientAuthenticationService implements AuthenticationService {

  signUp(activationLink = false) {

      this.messagesService.clearAll();

      const lock = new Auth0Lock(AUTH0_API_KEY, AUTH0_SUB_DOMAIN);

      lock.showSignup(this.loginConfig(activationLink), this.loginCallback());

  }

  ....

}

We then inject different implementations of the interface on the server and on the client, for the same injection token:

// define an injection name

export const authenticationService = new OpaqueToken("authenticationService");

// inject on the server

provide(authenticationService, {

  useFactory: () => new ServerAuthenticationService(req.authenticationId,isLoggedIn)

})

// inject on the client

provide(authenticationService, {

useFactory: (messagesService: MessagesService, ngZone: NgZone, cookieService: CookieService, userService: UserService, router:Router, gitHubService : GitHubService) => {

  return new ClientAuthenticationService(messagesService, ngZone, cookieService, userService, router, gitHubService);

  },

deps: [MessagesService, NgZone, CookieService, UserService, Router, GitHubService]

})])

And this is how we can do something different on the client and on the server in Angular Universal, by leveraging the Angular dependency injection container.

In fact, Angular Universal is built also using this notion: for example the way that HTML is rendered is by instead of injecting a DOM renderer while bootstrapping the framework, it injects a server renderer which generates HTML using the parse5 HTML serialization library.