This article was updated on 10th December, 2017.
It is needless to say that front-end JavaScript frameworks have taken a leading position in modern web development. The richness these frameworks bring in to the web application is illustrated in most modern websites.
One of the reasons visitors experience this richness is because these sites are highly responsive. A visitor doesn’t have to wait for long to see a response after sending a request.
But this richness comes at a cost!
To create this richness, a lot of logic has to be run on the browser using JavaScript. The cost is, the dynamic content rendered on the page using JavaScript cannot be consistently read by all search engines.
Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering Angular, React, .NET Core, MVC, Azure and more. Subscribe to this magazine for FREE and download all previous, current and upcoming editions.
While all Search engines (SE’s) can scan the content that travels from the server to the client, not all SE’s have the capability to scan it when the content is built dynamically at the client’s end. So your site cannot be reached through all SE’s if you are rendering the content on the client side.
In addition to this, we don’t see readable previews when links of JavaScript applications are shared on social media. Again, this is because the social media sites do not run JavaScript while rendering previews, thus making the pages hard to share.
The solution to this problem is Server Side Rendering.
Some of you might think that this approach is taking us back to the days when we had everything rendered from a server. But no, this approach instead enables us to render the initial view of the application from the server and rest of the application, runs on the client side.
It has the following advantages over the traditional SPAs:
- As the initial page is rendered from the server, it can be parsed by the search engines and the content of the page can be reached using search engines
- The user doesn’t have to keep looking at the half-baked page, as the complete page with data, is rendered on the client’s system
- Social media platforms like Facebook and Twitter can show the preview of the site when the URL of a server rendered application is posted on these platforms.
Editorial Note: Web crawlers are becoming smarter now-a-days. Google made a statement in October 2015 implying that it can crawl and index dynamic content. Bing can too. However for a consistent SEO experience across all search engines, server side rendering still remains the preferred option.
Server Side Rendering in Angular
To support server side rendering in Angular, the Angular team created a project named Angular Universal.
Angular Universal is a package to add server rendering support to Angular applications. As Angular is a client framework and can’t be understood by server platforms, the Angular Universal package provides the required knowledge to the server engine.
As we will see shortly, the universal package can be used to pass the Angular module to the Node.js server. Before Angular 4, it was maintained as a separate repository, but it is now moved to the npm submodule @angular/platform-server.
The support for universal apps was added to Angular CLI in the version 1.3.0.
In this article, we will build an application using the pokémon API to show a list of pokémons and their details. The application would be rendered from the server.
Setting up the environment and application
To follow along the steps shown in this article, the following tools need to be installed on your system:
- Node.js (version 6 or later): Can be downloaded from the official site for Node.js and installed
- npm: A package manager for Node, it is installed along with Node.js
- Angular CLI: It is an npm package created by the Angular team to make the process of creating, testing and building an Angular application easier. It can be installed using the following command:
> npm install @angular/cli –g
Once these tools are installed, a new Angular application can be generated using the following command:
> ng new app-name
We will be building an application that consumes the Pokéapi and displays some information about the pokémons. As this is a universal application, let’s name it as pokemon-universal. Run the following command to create this application:
> ng new pokemon-universal
This command will take a few minutes to complete.
Once it completes, the command creates a new folder named after the project, creates a structure inside the folder and installs the npm packages. The following screenshot shows the structure in Visual Studio Code:
Figure 1 – Folder structure of the project
The following listing explains the vital parts of the project created:
- The src folder will contain the source files of the Angular application. The generated project comes with a few default files
- The file .angular-cli.json is the configuration to be used by Angular CLI
- The files karma.conf.js and protractor.conf.js contain the settings for the Karma test runner to run unit tests and the settings for protractor to run e2e tests
- The e2e folder would contain the end to end tests
- The file tsconfig.json contains the configuration to be used to compile TypeScript
- The linting rules to be used during TypeScript compilation are stored in the file tslint.json
You can run this application using the following command:
> ng serve
This command starts a server on the port 4200. Open a browser and change the URL to http://localhost:4200 to browse the application. Now the application is in the default SPA mode.
We need to make a set of changes to be able to run it from the server. Let’s do this in the next section.
Installing Packages
Let’s make the default application render from server before building the pokémon app.
For this, a couple of packages need to be installed to enable the application for server rendering. The following list describes them:
1. @angular/platform-server: This package is created by the Angular team to support server side rendering of Angular applications. This module will be used to render the Angular module from the server side Node.js code
2. express: Express is a Node.js framework for building web applications and APIs. This package will be used to serve the page
The following commands will install these packages. Open a command prompt, move to the folder where the sample app is created and run the following commands there:
> npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader express
Adding Required Files
We need to make a few changes to the Angular application to enable it for server rendering.
The BrowserModule imported in the AppModule located in the file app.module.ts has to be made server compatible by adding withServerTransition. Change it as shown in the following snippet:
imports: [
BrowserModule.withServerTransition({ appId: 'my-app' })
]
A new module has to be created specifically to run the code from the server. This module would import the main AppModule that starts the application and the ServerModule exported by the @angular/platform-server package. The new module won’t have components and other code blocks of its own.
Add a new file to the app folder and name it app-server.module.ts. Place the following code inside this file:
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule
],
declarations: [],
bootstrap: [AppComponent]
})
export class AppServerModule { }
We need a file to serve as the entry point for the application from the server. This file does nothing more than importing the AppServerModule created above. Add a new file to the src folder and name it main.server.ts.
The following snippet shows the code to be added to this file:
export { AppServerModule } from './app/app-server.module';
The TypeScript files have to be transpiled differently for server side rendering. For this, a different configuration file is needed for transpilation. Add a new file to the src folder and name it tsconfig.server.json.
Paste the following code in this file:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app-server.module#AppServerModule"
}
}
You will notice the following differences between this file and the tsconfig.app.json file located in the src folder:
- The module system to be transpiled is set to commonjs
- It has a new section named angularCompilerOptions and it specifies the path of the Angular module to be loaded from the server code
Adding Configuration for Server Rendering
Information about server rendering has to be fed to the Angular CLI.
For this, we need to add a new application entry in the file .angular-cli.json. If you open this file, you will see a property named apps assigned with an array that has a single entry. The existing entry is the configuration used for development and deployment of the application in the client-oriented mode. The new entry will be for server-oriented mode.
The following is the configuration entry to be added to the apps array:
{
"platform": "server",
"root": "src",
"outDir": "dist/server",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.server.ts",
"test": "test.ts",
"tsconfig": "tsconfig.server.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.scss"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
As you can see, most of the configuration in this entry is similar to the first entry, albeit some differences which are as follows:
- The first property platform set to server says that this application is going to be executed from the server. The ng build command uses this value to generate server friendly code when the build is executed
- The output path of this application is set to dist-server, which means the files created by this build would be placed in the dist-server folder
- It uses the new tsconfig.server.json file to transpile TypeScript
Also, modify the outdir property of the first application to dist/browser.
"outDir": "dist/browser"
Setting up the Node.js Server
The last file to be added to the application is the file that starts the node.js server. This file will use the JavaScript file produced by running production build using the server application configured in the .angular-cli.json file. It is then applied on the index.html page. The resultant index.html file would be served by the express engine.
Add a new file to the root of the application and name it server.ts. Add the following content to this file:
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
const PORT = process.env.PORT || 4201;
const DIST_FOLDER = join(process.cwd(), 'dist');
// Our index.html we'll use as our template
const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');
const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader');
app.engine('html', (_, options, callback) => {
renderModuleFactory(AppServerModuleNgFactory, {
// Our index.html
document: template,
url: options.req.url,
// DI so that we can get lazy-loading to work differently (since we need it to just instantly render it)
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
}).then(html => {
callback(null, html);
});
});
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
The most important parts of the code in the snippet have inline comments explaining them. As you see, the server code uses the static files from the dist folder to serve the client application.
The most vital part of the above file is shown in the following screenshot:
Figure 2 – Code snippet loading the Angular module in the server
The statement 25 gets object of the Angular module built for server rendering. This module is passed to the renderModuleFactory method along with the options. The options object contains the HTML content to be rendered, the URL to be served by the Angular application and the optional property extraProviders.
The URL is useful in the applications with routes. The HTML obtained in result of this operation is passed to the callback of the HTML engine, so that it would be rendered on the page. The extraProviders array is the platform level providers for the render request. Here the snippet passes the providers for lazy loading; this is done to make the server rendering work even for the routes that would be lazily loaded.
The server.ts file needs a webpack configuration file to generate the JavaScript file to run the server. Add a file named webpack.server.config.js to the root folder of the application and add the following code to it:
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: { server: './server.ts' },
resolve: { extensions: ['.ts', '.js'] },
target: 'node',
// this makes sure we include node_modules and other 3rd party libraries
externals: [/(node_modules|main\..*\.js)/],
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{ test: /\.ts$/, loader: 'ts-loader' }
]
},
plugins: [
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
// for "WARNING Critical dependency: the request of a dependency is an expression"
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, 'src'), // location of your src
{} // a map of your routes
),
new webpack.ContextReplacementPlugin(
/(.+)?express(\\|\/)(.+)?/,
path.join(__dirname, 'src')
)
]
}
Running the Default Application in Server Mode
Now we are good to build and run the application.
Run the following commands in the given sequence. The first command builds the client application, second command builds the code to be rendered from the server and the third command starts the Node.js server.
> ng build --prod
> ng build --app 1 --prod --output-hashing none
> webpack --config webpack.server.config.js --progress --colors
> node dist/server.js
Open your favorite browser and change the URL to http://localhost:4201. You will see the same application that we saw earlier. But the difference is, now the components are compiled on the server and the result is sent to the browser. The following screenshot taken on Chrome dev tools shows the content served in response to the index file:
Figure 3 – Content served for index.html from server
Typing the commands to build and run the client and server portions of the application is a painful and error prone task. To make it easier, add the following entries to the scripts section of the package.json file:
"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:ssr": "node dist/server.js",
"build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
After making any change, one can simply run the following two commands to build the application and start the server:
> npm run build:ssr
> npm run serve:ssr
Note: Some of these instructions may change in the future versions of angular-cli or platform-server. You can refer to the instructions on the official wiki of angular-cli if these steps don’t produce the desired result.
Building the Pokémon Explorer App
Now that we saw how the default application works when rendered from the server, let’s modify the sample to add two routes and see how they work when rendered from server.
The final application will have two routes.
One to show a list of pokémons and the second one to show the details of a pokémon selected on the first page. First, we need to add a service to fetch the data and create a model to represent the structure of a pokémon.
We need bootstrap in the application we are going to build. We need to install bootstrap from npm and add it to .angular-cli.json file to include it in the bundle. Run the following command to install the package:
> npm install bootstrap --save
Modify the styles property in both the applications configured in .angular-cli.json as following:
"styles": [
"styles.css",
"../node_modules/bootstrap/dist/css/bootstrap.css"
]
Fetching Pokémon Data
The following snippet shows the model classes required for the pokémon explorer. Add a new file to the app folder, name it pokemon.ts and add the following code to it:
export class Pokemon {
name: string;
id: number;
types = [];
stats = [];
sprites: Sprite[] = [];
imageurl: string;
get imageUrl() {
return `https://rawgit.com/PokeAPI/sprites/master/sprites/pokemon/${this.id}.png`;
}
}
export class Sprite {
name: string;
imageP
The service will make use of the above models to serve the data to the components. To add the service, you can run the following Angular CLI command:
> ng g s pokemon.service
Once the file is generated, add the following code to it:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise'
import { Pokemon } from './pokemon';
@Injectable()
export class PokemonService {
private baseUrl: string = 'https://pokeapi.co/api/v2';
constructor(private http: HttpClient) { }
listPokemons() {
return this.http.get(`${this.baseUrl}/pokedex/1/`)
.toPromise()
.then((res: any) => {
let pokemons: Pokemon[] = [];
let reducedPokemonEntries = JSON.parse(res._body).pokemon_entries.splice(0, 50);
reducedPokemonEntries.forEach((entry) => {
let pokemon = new Pokemon();
pokemon.name = entry.pokemon_species.name;
pokemon.id = entry.entry_number;
pokemons.push(pokemon);
});
return pokemons;
});
}
getDetails(id: number) {
return this.http.get(`${this.baseUrl}/pokemon/${id}/`)
.toPromise()
.then((res: any) => {
let response = JSON.parse(res._body);
let pokemon = new Pokemon();
pokemon.name = response.name;
pokemon.id = response.id;
response.types.forEach((type) => {
pokemon.types.push(type.type.name);
});
response.stats.forEach((stat) => {
pokemon.stats.push({
name: stat.stat.name,
value: stat.base_stat
});
});
for (let sprite in response.sprites) {
if (response.sprites[sprite]) {
pokemon.sprites.push({
name: sprite,
imagePath: response.sprites[sprite]
});
}
}
return pokemon;
});
}
}
The service has two methods. The first method listPokemons gets a list of pokémons by querying the pokedex API and takes the first fifty pokémons. This is done to show lesser amount of data on the page and keep the demo simple. If you want to see more number of pokémons, you can modify the logic.
The second method getDetails receives an id of the pokémon and gets the details of it by querying the pokémon API. To call the pokémon REST APIs, the service uses HttpClient. It is a new service added to the @angular/common package and it has a simplified API to invoke and intercept the HTTP based endpoints.
The service has to be registered as a provider in the module. The following snippet shows the modified AppModule:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { PokemonService } from './pokemon.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'my-app' }),
HttpModule
],
providers: [PokemonService],
bootstrap: [AppComponent]
})
export class AppModule { }
Building the Components
Let’s add the components required for the application.
We need two components to show the list of pokémons and to show the details of a pokémon. These components would be configured to render on different routes. Let’s get the components generated, then we will modify them to display the data we need to show.
> ng g c pokemon-list -m app.module
> ng g c pokemon-details -m app.module
Note: Notice the –m option added to the above commands. This option tells Angular CLI to register the generated component in the specified module. We don’t need to specify this option in most of the cases. But here we need it because we have multiple modules created at the root of the application.
As the name says, the pokemon-list component simply lists the pokémons. It calls the getList method of the PokemonService and displays the results in the form of boxes. Open the file pokemon-list.component.ts and replace the code of this file with the following:
import { Component, OnInit } from '@angular/core';
import { PokemonService } from '../pokemon.service';
import { Pokemon } from '../pokemon';
@Component({
selector: 'app-pokemon-list',
templateUrl: './pokemon-list.component.html',
styleUrls: ['./pokemon-list.component.css']
})
export class PokemonListComponent implements OnInit {
pokemonList: Pokemon[];
constructor(private pokemonService: PokemonService) { }
ngOnInit() {
this.pokemonService.listPokemons()
.then(pokemons => {
this.pokemonList = pokemons;
});
}
}
The template of this component has to be modified to show the list. The widget showing each pokémon will have a link to navigate to the details page. Replace the content in the file pokemon-list.component.html with the following:
<div *ngFor="let pokemon of pokemonList" class="col-md-3 col-md-offset-1 text-center box">
<div class="row">
<div class="col-md-12">
<img [src]="pokemon.imageUrl" height="150" width="150" />
</div>
<div class="row">
<div class="col-md-12 link">
<a [routerLink]="['/details',pokemon.id]">{{ pokemon.name | titlecase }}</a>
</div>
</div>
</div>
</div>
The pokemon-details component receives id of the pokémon whose details have to be displayed and it calls the getDetails method of the PokemonService using received id to fetch the details.
The following snippet shows this code. Replace the code in the file pokemon-details.component.ts with the following:
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { PokemonService } from '../pokemon.service';
import { Pokemon } from '../pokemon';
@Component({
selector: 'app-pokemon-details',
templateUrl: './pokemon-details.component.html',
styleUrls: ['./pokemon-details.component.css']
})
export class PokemonDetailsComponent implements OnInit {
id: number;
pokemon: Pokemon;
constructor(private route: ActivatedRoute,
private router: Router,
private pokemonService: PokemonService) { }
ngOnInit() {
this.route.paramMap.subscribe((params) => {
this.id = parseInt(params.get('id'));
this.pokemonService.getDetails(this.id)
.then((details) => {
this.pokemon = details;
});
});
}
}
This component has to display the details of the pokémon like name, types, statistics and images of the sprites. The following snippet shows the template, place this code in pokemon-details.template.html:
<div *ngIf="pokemon" class="details-container">
<img [src]="pokemon.imageUrl">
<div>{{pokemon.name | titlecase}}</div>
<h4>Types:</h4>
<ul>
<li *ngFor="let type of pokemon.types">
{{ type }}
</li>
</ul>
<h4>Stats:</h4>
<ul>
<li *ngFor="let stat of pokemon.stats">
{{ stat.name }}: {{ stat.value }}
</li>
</ul>
<h4>Sprites:</h4>
<div *ngFor="let sprite of pokemon.sprites" class="col-md-3">
<img [src]="sprite.imagePath" />
<br>
<span>{{sprite.name}}</span>
</div>
</div>
Adding Routes
Now that the components are ready, let’s add the routes and complete the application.
Add a new file in the src folder and name it app.routes.ts. As we have been discussing till now, we need to add two routes in the application. One to show the list of pokémons and the other to show details of a pokémon.
The following snippet shows the code. Add the following code to this file:
import { Routes, RouterModule } from '@angular/router';
import { PokemonListComponent } from './pokemon-list/pokemon-list.component';
import { PokemonDetailsComponent } from './pokemon-details/pokemon-details.component';
let routes: Routes = [
{
path: '',
component: PokemonListComponent
},
{
path: 'details/:id',
component: PokemonDetailsComponent
}
];
const routesModule = RouterModule.forRoot(routes);
export { routesModule };
This has to be added to the application module to make the routing work. We need to import the routesModule exported from the above file and add it to the imports array of the module.
The following snippet shows the modified module file:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { routesModule } from './app.routes';
import { AppComponent } from './app.component';
import { PokemonListComponent } from './pokemon-list/pokemon-list.component';
import { PokemonService } from './pokemon.service';
import { PokemonDetailsComponent } from './pokemon-details/pokemon-details.component';
@NgModule({
declarations: [
AppComponent,
PokemonListComponent,
PokemonDetailsComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'my-app' }),
HttpClientModule,
routesModule
],
providers: [
PokemonService
],
bootstrap: [AppComponent]
})
export class AppModule { }
The last change we need to make is, modify the template of AppComponent to load the routes. Open the file app.component.html and replace the content of this file with the following code:
<div class="container">
<div style="text-align:center">
<h1>
Explore the Pokemons!
</h1>
</div>
<router-outlet></router-outlet>
</div>
Now run the following commands to build and run the application:
> npm run build:ssr
> npm run serve:ssr
You will see the pokemon images displayed on the page as shown in Figure 4:
Figure – 3 Pokemon list
Move to the details page by clicking on one of the links and refresh the details page. You will see that the details page is rendered from the server. The following screenshot shows the HTML received from the server in response to the request made:
Figure – 4 HTML of pokemon list from server
Switch between the list and details pages and randomly refresh any of the pages. You will see that the content of the first load comes from the server. This model enables server rendering on all the pages in the application and hence the entire application would have a consistent SEO experience.
Notice that even if the complete page is served from the server, the browser makes an HTTP request to the pokémon API again and rebinds the data on the page. A user browsing this site won’t like this experience. Infact even as a developer, I don’t like this, as it makes a duplicate request to the API and makes the page slower.
This can be avoided using TransferState. It is discussed in the next section.
Transferring State from Server
To make the application perform better and to avoid duplicate call to the REST APIs, the application should have capability to send data from the server to the client. Once the client receives the data from the server, it will use it to paint its UI and avoid making a call to the API.
Angular supports this through the TransferState API.
TransferState is a service that stores key-value pairs and can be used to transfer the server state to client side. To use this, the modules AppServerModule and AppModule have to be supplied with the ServerTransferStateModule and BrowserTransferStateModule respectively.
Open the file app.module.ts and replace it with the following code:
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { routesModule } from './app.routes';
import { AppComponent } from './app.component';
import { PokemonService } from './pokemon.service';
import { PokemonListComponent } from './pokemon-list/pokemon-list.component';
import { PokemonDetailsComponent } from './pokemon-details/pokemon-details.component';
@NgModule({
declarations: [
AppComponent,
PokemonListComponent,
PokemonDetailsComponent
],
imports: [
BrowserModule.withServerTransition({appId: 'my-app'}),
routesModule,
HttpClientModule,
BrowserTransferStateModule
],
providers: [PokemonService],
bootstrap: [AppComponent]
})
export class AppModule { }
Open the file app.server.module.ts and replace it with the following code:
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule
],
bootstrap: [AppComponent],
})
export class AppServerModule { }
Now that the modules are included, the TransferState service can be used to store the state.
The state object has to be associated with a key. The key can be created using the makeStateKey function defined in the @angular/platform-browser module. The task of storing and retrieving the state has to be performed in the service pokemon.service, as this service is used to fetch data from the server.
Open the file pokemon.service.ts and replace it with the following code:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TransferState, makeStateKey } from '@angular/platform-browser'; //1
import 'rxjs/add/operator/toPromise';
import { Pokemon } from './pokemon';
const POKEMONS_KEY = makeStateKey('pokemons'); //2
const POKEMON_DETAILS_KEY = makeStateKey('pokemon_details');
@Injectable()
export class PokemonService {
private baseUrl: string = 'https://pokeapi.co/api/v2';
constructor(private http: HttpClient,
private state: TransferState) { } //3
listPokemons() {
let pokemons = this.state.get(POKEMONS_KEY, null as any); //4
if (pokemons) {
return Promise.resolve(pokemons);
}
return this.http.get(`${this.baseUrl}/pokedex/1/`)
.toPromise()
.then((res: any) => {
let pokemons: Pokemon[] = [];
let reducedPokemonEntries = res.pokemon_entries.splice(0, 50);
reducedPokemonEntries.forEach((entry) => {
let pokemon = new Pokemon();
pokemon.name = entry.pokemon_species.name;
pokemon.id = entry.entry_number;
pokemon.imageurl = `https://rawgit.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`;
pokemons.push(pokemon);
});
this.state.set(POKEMONS_KEY, pokemons as any); //5
return pokemons;
});
}
getDetails(id: number) {
let pokemonDetails: Pokemon = this.state.get(POKEMON_DETAILS_KEY, null as any); //6
if (pokemonDetails && pokemonDetails.id === id) {
return Promise.resolve(pokemonDetails);
}
return this.http.get(`${this.baseUrl}/pokemon/${id}/`)
.toPromise()
.then((res: any) => {
let response = res;
let pokemon = new Pokemon();
pokemon.name = response.name;
pokemon.id = response.id;
pokemon.imageurl = `https://rawgit.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`;
response.types.forEach((type) => {
pokemon.types.push(type.type.name);
});
response.stats.forEach((stat) => {
pokemon.stats.push({
name: stat.stat.name,
value: stat.base_stat
});
});
for (let sprite in response.sprites) {
if (response.sprites[sprite]) {
pokemon.sprites.push({
name: sprite,
imagePath: response.sprites[sprite]
});
}
}
this.state.set(POKEMON_DETAILS_KEY, pokemon as any); //7
return pokemon;
});
}
}
Modified lines of the pokémon.service are marked with numbered comments in the above snippet. The tasks performed by these new statements are described below:
1. Imports TransferState and metaStateKey from @angular/platform-browser
2. Creates keys to store the array of pokémons and the pokémon details objects using metaStateKey function
3. Injects the TransferState service
4. Gets the pokémons array from the state. Assigns null as the default value if the entry is not found in state. If the entry is found, it resolves the promise using the object. Otherwise, a call is made to the pokémon API
5. The pokémons array is stored in the state using POKEMONS_KEY
6. Gets the pokémon details object from the state. Assigns null as the default value if the entry is not found in state. If the entry is found, it resolves the promise using the object. Otherwise, a call is made to the pokémon API to get the details
7. The pokémons array is stored in the state using POKEMON_DETAILS_KEY
Now pokémon.service abstracts the logic of fetching the data. Interface of the methods is not modified, they still accept and return the same types of values. The methods handle the logic of fetching data from the right source.
To make the transfer state work, the Angular application must be bootstrapped after the DOM is loaded. This is because, the data transferred from the server is stored in an HTML element and this element should be accessible to Angular to get the data. Open the file main.ts and move the application bootstrap logic inside the DOMContentLoaded event as shown below:
document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
});
Save the files and run the application from server. Open the network tab in developer tools of your browser while loading the page. There won’t be any XHR requests to the pokémon API now. The following screenshot shows the loaded application on Chrome with developer tools showing the XHR requests:
Figure 5 – Pokémon list page and XHR requests when rendered from server
Go to a details page and refresh that page or open it in a new tab. You will notice that even this page doesn’t make any XHR calls.
But the pages will make XHR calls when you switch between the pages on the same tab, as the application runs in client’s context.
Setting Meta Information and Page Title
Meta information on pages is crucial for search engines. If the search string entered by a user contains any of the words added in meta section of the page, the possibility of getting this page in the search results is higher.
Meta information can be added to the page in Angular components. Similarly, adding a meaningful title to the page makes the page more usable, as the user would understand the purpose of the page from its title itself. Also, the title would be displayed on the page displaying search results and in the preview displayed on social media sites when the page is shared.
Meta tags and title can be set in the app.component and even in the components used for routes. The meta tags relevant for the whole application can be added in app.component. The tags specific to the routes can be added in the components used in routes.
To add this information to the pages, the services Meta and Title have to be imported from the package @angular/platform-browser and they have to be injected in the component. The Title service provides the method setTitle to set the title. And the Meta service provides the method addTags to set a set of tags to the page.
The following snippet shows the modified app.component with functionality to set the meta tags and title:
import { Component } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
constructor(meta: Meta, title: Title) {
title.setTitle('Explore Pokemons');
meta.addTags([
{
name: 'author', content: 'DotNetCurry'
},
{
name: 'keywords', content: 'angular-universal-seo'
},
{
name: 'description', content: 'Describes SEO with Angular'
}
]);
}
}
I am leaving it as an assignment to the reader to set the title and meta tags for the pokémon list and details pages.
After setting these values, run the application and now you will see the title appearing on the title bar of the browser and the meta tags added to the head section of the HTML rendered.
Conclusion
The wide usage of JavaScript frameworks has made it necessary that we provide consistent SEO experience even for those search engines that cannot scan and index dynamically generated content. Server side rendering is a big value add to business driven sites as they will enjoy the benefits of both being rich and searchable.
As we saw in this article, Angular has a very good support for server side rendering and it is now integrated with Angular-CLI as well. We should definitely use this feature to make our sites SEO consistent!
Download the entire source code of this article (Github)
This article was reviewed by Mahesh Sabnis and Suprotim Agarwal.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like AngularJS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active
blogger, an author at
SitePoint and at
DotNetCurry. He is rewarded with Microsoft MVP (Visual Studio and Dev Tools) and DZone MVB awards for his contribution to the community