all blog posts

Improving Angular SEO with Angular Universal for Bots, Angular SPA for Visitors

In this article we will look at why Single Page Applications (SPAs) are terrible for SEO, why you still want to use SPAs, and how to achieve great SEO with Angular SSR.

The Problem

The problem with any Single Page Application is that it renders in the browser. I'll show you what that means. When you visit, this is what your browser downloads:

<!doctype html>
<html lang="en">
  <meta charset="utf-8">
  <base href="/">
  <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="scripts.js"></script><script type="text/javascript" src="vendor.js"></script><script type="text/javascript" src="main.js"></script>

That's it. No content at all, only javascript. After the initial download, javascript kicks in and starts rendering the website, which is what you see when you visit the site.

However, an average search engine does not wait for the site to render. It will fetch the HTML shown above and leave it at that. You can imagine that's terrible for SEO; there are no keywords, no headers, no content or anything to index at all.

The cool thing about SPAs, however, is that you hardly need any infrastructure to be able to deliver your site to thousands or even millions of users. All the hard work is done by the visitor's browser, so all you need to is deliver the files. You can use S3 or any other cheap public object store for that.

If you use S3, it will scale automatically with the popularity of your site. When you have 10 visitors per day, the cost is about 0. When you have a million, you pay more. But there is nothing you need to do to scale. Which is awesome.

So there is a clear benefit to SPAs. But if your SEO is terrible and no search engine can find your website, what good is infinite and cheap scaling?

The Solution

The solution lies in Angular Universal. With Angular Universal you can write and host your application as an SPA, but you can also use Server Side Rendering (SSR) to render the application on a backend, instead of the user's browser.

Server Side Rendering

SSR has a number of benefits:

  • Data transfer is a lot lower, because you transfer the render result instead of the render library.
  • The time to first byte (TTFB) is a lot lower, for the same reasons.
  • The page is fully rendered, so any search engine will be perfectly able to parse it.

The downside is that you need to have a backend. This means:

  • You need at least two servers, for redundancy.
  • You need a networking environment (a VPC if you're in AWS).
  • You need a load balancer.
  • You need a system to scale your instances.
  • You need to pay for running an instance, even when there are no visitors.
  • You need to patch your instances.

That's a lot to think about and manage. So what can we do to get the best of both worlds?

Have your cake and eat it too

The answer is to serve the SPA to your real world visitors and serve your SSR application to web crawlers. The number of search engine crawls will not change much whether you're a hardly known website or one of the most popular sites in the world. But it makes an enormous difference in real human visitors.

By making sure that the real humans use the SPA, you get close to infinite scalability. And because only the web crawlers use the SSR application, the server hosting it can remain very small and the environment needs hardly any redundancy.

Setting up Angular Universal

There are a number of resources focussed on setting up Angular Universal, but I couldn't find any good and clear description suited to my use case. In the end I checked out this repository, which compiled and ran on my first try. Then I looked at what they did and applied the same solution to my project. Another complete example (including source) can be found at

To give you some handholds:

  • Add universal to your project with ng add @ng-toolkit/universal
  • You need ts-loader, webpack and webpack-cli:
    • npm install --save-dev ts-loader webpack-cli
    • npm install webpack-cli -g
  • You need module-map-ngfactory-loader and express:
    • npm install --save @nguniversal/module-map-ngfactory-loader express
  • I needed the following new files and copied them from the example repositories:
    • server.ts
    • webpack.server.config.js
    • src/main.server.ts
    • src/tsconfig.server.json
    • src/app/app.server.module.ts

In angular.json I added the server section:

  "server": {
    "builder": "@angular-devkit/build-angular:server",
    "options": {
      "outputPath": "dist/cloudbanshee-server",
      "main": "src/main.server.ts",
      "tsConfig": "src/tsconfig.server.json"

I updated package.json though the npm cli (see above).

In src/app/app.module.ts I added BrowserModule.withServerTransition({ appId: 'cloudbanshee' }) to imports.

I'm using a build script to build the SPA and the bundle for SSR in one go. The main parts are:

ng build --aot --prod --build-optimizer=true --vendor-chunk=true
ng run cloudbanshee:server
webpack --config webpack.server.config.js --progress --colors

To test if your application is usable as SPA and SSR app, run:

  • ng serve to host the SPA on port 4200
  • node dist/server.js to run SSR on port 4000

Deploying both the SPA and SSR

As described in this blog post, I'm using CloudFront and Lambda@Edge to separate crawler traffic from human traffic. Crawler traffic gets sent to the SSR app running on an EC2 instance, while all other traffic is sent to the SPA in an S3 bucket.

CloudFront with two Origins

Hosting the SPA is as simple as copying the compiled files to S3. For SSR, I also upload the compiled files to S3, but then I trigger an EC2 instance to fetch those files and reload the application.

To run SSR, I use pm2 and nginx on a single Amazon Linux instance in an auto scaling group.


With this architecture, you can really have the best of both worlds. Infinite scaling with an SPA on S3, and perfect SEO with an SSR app on EC2. Of course it's a bit of a hassle to set up, but I think it's more than worth it.

If you have any questions or remarks, reach out to me on Twitter.

Related blog posts

all blog posts