Create an SEO friendly Angular application and deploy it on Serverless framework

Maciej Treder
Aug 28 · 10 min read

You can build search-engine optimized (SEO) friendly single page applications (SPA’s) with Angular Universal, a technology that runs your Angular application on the server. Additionally, you can reduce the cost of running those applications with AWS Lambda, an event-driven, serverless computing platform provided as a part of Amazon Web Services (AWS). This piece will show you how to build and deploy Angular Universal projects on AWS Lambda using Serverless Framework, an open-source command line interface for building and deploying serverless applications.

In this piece we will:

  • Create an Angular application that contains two routes and makes calls to an external API
  • Add server-side rendering for SEO purposes
  • Set up a Serverless Framework framework configuration
  • Deploy the app on AWS Lambda

To accomplish these tasks, you will need to create an AWS account and install the following:

To configure the AWS CLI you’ll need the following from your AWS account:

  • Access Key ID
  • Secret Key
  • Default region

Your root user will have all the rights necessary to deploy and run the Angular app as a Lambda function. You can get an Access Key ID and a Secret Key for your root user by generating a new pair in the Identity and Access Management (IAM) console.

If you do not want to use your root user ID for this project you can set up an IAM role and policy for an API to invoke Lambda functions. This process has a number of steps and is not recommended if you are new to AWS.

Be sure you have completed all these tasks successfully before proceeding.

Set Up the Angular Project and Run Hello World!

The first step we must take in every Angular project is the initialization and installation of the package:

ng new angularSeo --style css --routing false
cd angularSeo

Let’s make a few small changes to the generated project. First, let’s add some style to the <app-root> in the src/index.html file. Replace the existing code with the following:

Next, replace the existing content of src/app/app.component.html with:

<h1>Hello World!</h1>

Finally, add the following to src/styles.css:

Note, in the above, that we are using my favorite fancy background:

Download it, and place it in src/assets/img catalog.

Let’s run the application by typing:

ng serve

After opening, in your browser you should see:

You can find all the code up to this point in a GitHub repository which you can clone:

git clone -b tutorial1_step1 angularSeo
cd angularSeo/
npm install
ng serve

Components, Routing, and Services

Our app isn’t really complicated so far. Let’s add some routes, components, and a service to it:

ng g c first
ng g c second
ng g c menu
ng g s echo

Ok, we have a lot of files. We will start with src/app/first/first.component.ts; it will be really straightforward. Replace the default contents with the following:

import { Component } from '@angular/core';@Component({
template: '<h1>Hello World!</h1>'
export class FirstComponent {}

Edit the second component, src/app/second/second.component.ts, which is a little bit more complex:

Note: Your development environment or linter may call out makeCall(). Don’t worry about this: we’ll resolve it below when we create the service.

We introduced a few mechanisms here. First is the use of an external template and an external style (templateUrl and stylesUrls). Angular gives us the ability to create HTML templates and CSS stylesheets outside of the component class.

Here is how the template for the second component, src/app/second/second.component.html, should look:

<h1>Second component</h1>
<h2>This component injects EchoService</h2>
Response is: <span>{{response | async | json}}</span>

And the stylesheet,src/app/second/second.component.css, should look like this:

span {
color: purple;
display: block;
background: #ccc;
padding: 5px;

Another mechanism we introduced in this component is dependency injection. If you take a close look at the constructor in second.component.ts, you will see a parameter of type EchoService. Angular will try to initialize and pass an object of the EchoService type to our SecondComponent class when it is initialized. Dependency injection is a technique used to implement an architectural paradigm known as inversion of control (IoC).

We also introduced an Observable type and an async pipe in the template. Are you familiar with Promises? An Observable is one step further. This asynchronous type emits values pushed to it by other functions. You can reuse it as many times as you want, subscribe multiple listeners, and more (map, filter, pass to another observable, etc.). You can read more about it at the RxJS GitHub page.

The ‘async’ pipe in the second.component.html template is a special Angular mechanism to display our variable in the view template only when it is evaluated. In other words, the value pushed to the HTML at runtime is sent by the EchoService observable.

Last, but not least, we implemented the OnInit interface and the ngOnInit lifecycle hook. Interfaces in TypeScript work the same way as interfaces in other languages; if you implement it, you must implement all the methods declared in it. In this particular case, we need to implement the ngOnInit() method. This method is one of the “Angular lifecycle hooks”, methods called automatically by the Angular engine at different stages of view initialization, destruction, and other events. According to the Angular documentation:

ngOnInit — Initialize the directive/component after Angular first displays the data-bound properties and sets the directive/component’s input properties. Called once, after the first ngOnChanges().

We injected a service into the second component. Now we can create it. Replace the default contents of src/app/echo.service.ts with the following:

This service contains only one method, which makes a GET request to the URL.

Ok. That magic looks awesome, but you are probably asking “How does Angular know what to inject, where to inject it, and where are all those classes initialized?”. Those are great questions! The answer is: NgModule, the entry point of our app. Time to take a look at what is going on inside of it and import one more module which is necessary for EchoService: HttpClientModule (src/app/app.module.ts):

What do we have here..?

  • Imports — links to other NgModules
  • Declarations — a list of components used in the application
  • Bootstrap — the name of the component we want to load as a “main” one
  • Providers — a list of services used across the application

Ok, now it’s time to declare routing. Start by creating a routing module:

ng generate module app-routing --flat --module=app

We can now add our routes to src/app/app-routing.module.ts, export RouterModule from it, and remove redundant code (the declarations array and CommonModule import):

We can add some links in the MenuComponent file, src/app/menu/menu.component.ts:

Do you see a connection between routerLink and the routes declared in AppModule? Great! This is how we are linking stuff in Angular: routerLink is an Angular built-in directive which takes path as a parameter and matches it with path declared in RouterModule.forRoot(). When there is a match, it loads the given component into a <router-outlet> component, which we are going to add into src/app/app.component.html right now. Replace the code from this file with:


Our app is ready. Time to launch it:

ng serve -o

This is what you should see in your browser:

When you click the ‘Second component’ button, you’ll see this if everything is working correctly:

All the code up to this point is in this GitHub repository you can clone:

git clone -b tutorial1_step2 angularSeo
cd angularSeo/
npm install
ng serve -o

Search-Engine Optimization (SEO)

Our app looks ready to deploy. But there are some issues if we “think” like a network crawler.

Run our application:

ng serve

Take a look at our website. You can do this by inspecting the page source or by running the curl command below (if you have cURL installed):

What’s the problem here? How do single page apps work? In fact, they are pure HTML, with tons of JavaScript attached and executed in the user’s browser.

Are crawlers able to do the same? GoogleBot can render JavaScript, but other crawlers (like Facebook, LinkedIn, Twitter, Bing) can’t. Also, websites that “look” like static ones, and don’t expect extra resource requirements from the crawler to read them, are positioned higher in search engine ranking because of their better performance.

How could we solve this issue? It’s super simple! Type the following in your command line:

ng add @ng-toolkit/universal

What just happened? @ng-toolkit updated our project with Angular Universal functionality, the technology that runs your Angular application on the server. We have a couple of new files created.

Changes have been made to the src/app/app.module.ts file, which was the entry point of our app. What @ng-toolkit did is remove the bootstrap attribute from the @NgModule annotation and remove BrowserModule and add NgtUniversalModuleand CommonModule to the imports array.

Where is our app bootstrapped now? In fact, it depends. If you are looking for the bootstrap used by the browser, we should navigate to the src/app/app.browser.module.ts, this is where it resides:

What @ng-toolkit has also done, is the creation of the src/app/app.server.module.ts. This is an entry point for the code which will be executed server-side.

Now we can take a look at the project configuration file, angular.json. What we will find there is a new builder added to our project.

(Ellipsis (“...”) in a code block indicates a section redacted for brevity.)

As you can see, the entry file for this build is src/main.server.ts, which has been added to our project as well. By looking at this file we can determine the entry point used by the Angular compiler to create the server-side build:

Here it is: src/app/app.server.module.ts, which is the server-side rendering the equivalent of src/app/app.browser.module.ts, the module that bootstraps the app for browser rendering.

As you probably noticed, @ng-toolkit also made changes in the package.json file. We have a couple of new scripts there:

"build:server:prod": "ng run angularSeo:server && webpack --config webpack.server.config.js --progress --colors",
"build:browser:prod": "ng build --prod",
"build:prod": "npm run build:server:prod && npm run build:browser:prod",
"server": "node local.js"

The two most important are at the end of the list:

  • build:prod, which runs the Angular compiler against the browser and server builds and then creates a server.js file, using the Webpack configuration added by @ng-toolkit.
  • server, which is used to start Node.js with the compiled application.

Give them a try:

npm run build:prod
npm run server

We can try to behave like a network crawler:

curl localhost:8080

Boom! If we inspect the page source we can see what was generated server-side. Look for the links generated for the buttons, shown below:

If you don’t see the expected results or encountered an error, you can find all the code up to this point in this GitHub repository, which you can clone:

git clone -b tutorial1_step3 angularSeo
cd angularSeo/
npm install
npm run build:prod
npm run server


Awesome! Our app is fully developed and SEO friendly. We are 95% ready for deployment.

Let’s discuss the remaining 5%.

Usually, we would build our application and publish everything that is under the dist folder to some hosting service (for example, Amazon S3). The “problem” is that we introduced the server-side rendering mechanism, which needs Node.js running on the backend machine. Do we need a cost-consuming EC2 instance running for 24 hours per day?

Nope. We are going to use AWS Lambda, a Function as a Service (FaaS) environment together with Serverless Framework. By using @ng-toolkit again, we will set up a basic configuration for the Serverless Framework.

ng add @ng-toolkit/serverless

This command creates the serverless.yml file, which provides configuration for the Serverless Framework, and the lambda.js file, which provides the entry point for the AWS Lambda function. It also makes some minor changes in the framework.

To configure the project to run in your default AWS region, edit the serverless.yml file and replace the value of region: with the name of your default AWS region. For example, replace eu-central-1 with us-east-2.

We also have new scripts in package.json. Make use of one of those:

You should be able to navigate to your application at the URL found in the deploy command output, like that shown above. The URL will include your default region if you changed the value in serverless.yml.

When you click the ‘Second component button, the output should look like this:

As a last step, check that URL with the curl command or by inspecting the page source:

Perfect. Our app is live on the web.

You can find all the code up to this point in a GitHub repository which you can clone:

git clone -b tutorial1_step4 angularSeo
cd angularSeo/
npm install
npm run build:serverless:deploy


Today we have successfully developed and deployed an Angular application on AWS Lambda. You have learned how introduce routing, modules, and services in Angular, all of that with server-side rendering for SEO optimization purposes. These techniques enable you to deploy your Angular application on the web in a way that’s accessible to search engines while minimizing your operational costs. With AWS Lambda you pay just for the compute time required to serve your visitor’s requests instead of paying for each minute the server is online.


GitHub repository:

Also, check out: for more Angular and Angular + Serverless Framework features

You can also contact me at: or @maciejtreder on GitHub, Twitter, StackOverflow, and LinkedIn.

Better Programming

Advice for programmers.

Maciej Treder

Written by

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

Better Programming

Advice for programmers.

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