Server Side rendering with Angular universal 14
What are we going to do?
We will apply Server Side Rendering in our Web Application.
We will use the Angular version 14.0.4 javascript framework.
We will use an existing project whose characteristics are
- Generated with Angular CLI
- Routing
- Lazy Loading
- Framework CSS Bootstrap
All created sources are indicated at the end of the tutorial.
The final application is at the following address
Before you start
To be visited by a large number of users, a website must fulfill two essential conditions.
- Display as quickly as possible.
- Be well referenced by search engines.
The technique that allows you to do this is named.
- Server Side Rendering
We will apply this technique in an Angular project.
For this we will use technology advocated by Google teams
- Angular Universal.
This technology will improve the SEO (Search Engine Optimization) of our site.
Creation of the Angular project
To be able to continue this tutorial we obviously have to use certain elements
- Node.js : The javascript plateform.
- Git : The version-control system.
- Angular CLI : The command line interface for Angular.
- Visual Studio code : The source-code editor.
You can consult the following tutorial which explains in detail how to do
We will use an existing project
# Make a demo directory (the name is arbitrary)
mkdir demo
# Go to this directory
cd demo
# Get the source code on your workstation
git clone https://github.com/ganatan/angular-modules.git
# Go to the directory that was created
cd angular-modules
# Run installation of dependencies
npm install
# Run the program
npm run start
# Check by launching the command in your browser
http://localhost:4200/
Theory
Web pages generated with javascript frameworks use javascript.
Search engines currently have trouble interpreting javascript.
We will check this notion in a practical way.
We will execute our application with the corresponding script.
# Running the application
npm run start
# Viewing the site in the browser
http://localhost:4200/
We will check the source code produced in the corresponding page.
Using the Chrome browser you must type Ctrl + U to see the html code.
We notice that the code "Features" that appears in the browser does not appear in the code.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AngularStarter</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async="" src="https://www.googletagmanager.com/gtag/js?id=YOUR-ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'YOUR-ID');
</script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<app-root></app-root>
<script src="runtime.js" type="module"></script>
<script src="polyfills.js" type="module"></script>
<script src="styles.js" defer></script>
<script src="scripts.js" defer></script>
<script src="vendor.js" type="module"></script>
<script src="main.js" type="module"></script>
</body>
</html>
By clicking on main.js we open this file which contains the text "Features"
Let's run a compilation with npm run build.
The dist/angular-starter directory contains the main.js file.
Let's open these files with our VS code editor, then do a search (Ctrl + F) of the text "Features".
They contain the text "Features".
These files are used to generate the main.js file.
The main.js file is a javascript file, so it will be misinterpreted by search engines.
We will see later in this tutorial that once the SSR applied the code appears directly in the HTML code and will be well interpreted by the search engines.
Installation
The tool we will use to apply the SSR to our project is
- Angular universal version 13.0.1
The latest version of this tool is available below
Angular Universal can generate static pages through a process called Server side rendering (SSR).
The procedure to follow is detailed on the Angular official website.
https://angular.io/guide/universal
We will use a simple CLI command
# Installation
ng add @nguniversal/express-engine
Angular universal
As a reminder angular CLI uses via the ng add directive the principle of schematics to modify our code and adapt it to the new functionality (here the ssr).
Many operations were done automatically on our project.
If we had to carry out this operation manually here are the different steps that we should have followed.
- Installing new dependencies
- Editing the main.ts file
- Editing the app.module.ts file
- Editing the angular.json file
- Creating the src/app/app.server.module.ts file
- Creating the src/main.server.ts file
- Creating the server.ts file
- Creating the tsconfig.server.json file
- Creating the webpack.server.config.js file
- Editing the angular.json file
- Editing the package.json file
Installation of dependencies.
# Install the new dependencies in package.json
npm install --save @angular/platform-server
npm install --save @nguniversal/express-engine
npm install --save express
npm install --save @nguniversal/builders
npm install --save @types/express
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
function bootstrap() {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
};
if (document.readyState === 'complete') {
bootstrap();
} else {
document.addEventListener('DOMContentLoaded', bootstrap);
}
Editing the app.module.ts file
In this tutorial we will add the appId value to identify the application.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HomeComponent } from './modules/general/home/home.component';
import { NotFoundComponent } from './modules/general/not-found/not-found.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
NotFoundComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'angular-starter' }),
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Editing the angular.json file.
Editing outputPath
- dist/angular-starter/browser instead of dist/angular-starter.
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/angular-starter/browser",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/assets/params/css/fonts.googleapis.min.css",
"src/styles.css"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
},
Creating the app.server.module.ts 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,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
Creating the main.server.ts file
We will change the code for the lint test to work properly.
/***************************************************************************************************
* Initialize the server environment - for example, adding DOM built-in types to the global scope.
*
* NOTE:
* This import must come before any imports (direct or transitive) that rely on DOM built-ins being
* available, such as `@angular/elements`.
*/
import '@angular/platform-server/init';
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
export { AppServerModule } from './app/app.server.module';
export { renderModule } from '@angular/platform-server';
Creating the server.ts file
The port used by default is 4000 we can change it if necessary in this file.
import 'zone.js/dist/zone-node';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/angular-starter/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';
Creating files
- tsconfig.server.json
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/server",
"target": "es2019",
"types": [
"node"
]
},
"files": [
"src/main.server.ts",
"server.ts"
],
"angularCompilerOptions": {
"entryModule": "./src/app/app.server.module#AppServerModule"
}
}
Changes to the angular.json file
The "server", "serve-ssr" and "prerender" property is added after the test property.
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/angular-starter/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json"
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"development": {
"optimization": false,
"sourceMap": true,
"extractLicenses": false
}
},
"defaultConfiguration": "production"
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",
"configurations": {
"development": {
"browserTarget": "angular-starter:build:development",
"serverTarget": "angular-starter:server:development"
},
"production": {
"browserTarget": "angular-starter:build:production",
"serverTarget": "angular-starter:server:production"
}
},
"defaultConfiguration": "development"
},
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routes": [
"/"
]
},
"configurations": {
"production": {
"browserTarget": "angular-starter:build:production",
"serverTarget": "angular-starter:server:production"
},
"development": {
"browserTarget": "angular-starter:build:development",
"serverTarget": "angular-starter:server:development"
}
},
"defaultConfiguration": "production"
}
}
Editing the package.json file
"scripts": {
...
"dev:ssr": "ng run angular-starter:serve-ssr",
"serve:ssr": "node dist/angular-starter/server/main.js",
"build:ssr": "ng build && ng run angular-starter:server",
"prerender": "ng run angular-starter:prerender"
...
}
Updates
We can take this opportunity to update the dependencies of the package.json file and adapt the version descriptors.
The following dependencies
- @nguniversal/express-engine
Can be updated with versions
- 13.0.1
The file will eventually contain the following dependencies.
"dependencies": {
"@angular/animations": "13.1.1",
"@angular/common": "13.1.1",
"@angular/compiler": "13.1.1",
"@angular/core": "13.1.1",
"@angular/forms": "13.1.1",
"@angular/platform-browser": "13.1.1",
"@angular/platform-browser-dynamic": "13.1.1",
"@angular/platform-server": "13.1.1",
"@angular/router": "13.1.1",
"@fortawesome/fontawesome-free": "5.15.4",
"@nguniversal/express-engine": "13.0.1",
"bootstrap": "5.1.3",
"express": "4.17.2",
"rxjs": "7.4.0",
"tslib": "2.3.1",
"zone.js": "0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "13.1.2",
"@angular/cli": "13.1.2",
"@angular/compiler-cli": "13.1.1",
"@nguniversal/builders": "13.0.1",
"@types/express": "4.17.13",
"@types/jasmine": "3.10.2",
"@types/node": "17.0.0",
"jasmine-core": "3.10.1",
"karma": "6.3.9",
"karma-chrome-launcher": "3.1.0",
"karma-coverage": "2.1.0",
"karma-jasmine": "4.0.1",
"karma-jasmine-html-reporter": "1.7.0",
"typescript": "4.5.4"
}
Conclusion
It only remains to test all the previous scripts and finalize with the SSR.
# Development
npm run start
http://localhost:4200/
# Tests
npm run lint
npm run test
npm run e2e
# AOT Compilation
npm run build
# SSR Compilation
npm run build:ssr
npm run serve:ssr
http://localhost:4000/
Finally we will check the source code produced in the page corresponding to the SSR compilation.
Using the Chrome browser you must type Ctrl + U to see the html code.
We notice that the code "Features" appears this time in the browser.
The page will therefore be well interpreted by the search engines.
Note
Some versions of Angular 9 do not allow you to check the SSR result in your browser.
However SSR works on the server side with google robots
To check it use the curl software
- For example on localhost
curl http://localhost:4000/ > ssr-results.txt
- On Live Demo
curl https://angular.ganatan.com/ > ssr-results.txt
Then check the contents of the ssr-results.txt file.
You will see in the HTML code appear the desired text.
Other proof Use SEOQUAKE (SEO Toolbox) to check SEO on angular.ganatan.com/
I apply the SSR on www.ganatan.com and angular.ganatan.com
Source code
The source code used at the beginning of the tutorial is available on github
https://github.com/ganatan/angular-react-modules
The source code obtained at the end of this tutorial is available on github
https://github.com/ganatan/angular-react-ssr
The tutorials for obtaining the application are as follows
- Step 1 : Getting started with Angular
- Step 2 : Routing with Angular
- Step 3 : Lazy loading with Angular
- Step 4 : Bootstrap with Angular
- Step 5 : Server Side Rendering with Angular
- Step 6 : HttpClient with Angular
- Step 7 : Transfer State with Angular
- Step 8 : Progressive Web App with Angular
- Step 9 : Search Engine Optimization with Angular
The source code of the application is available on github
https://github.com/ganatan/angular-app