Years pass by, but valid indexing of Angular and other single-page applications is a real challenge for search engine robots. Java-script comprehension remains a soft spot even for Google. Dozens of less advanced bots not to mention.
However, it is 2019, and this means the time has come to deal with Angular SEO issues.
You are going to find out:
- How to fix the problem of displaying dynamic content in “View source code”
- How to create a server based on Node.js and Express
- How to configure the application server-side pre-rendering
SEO Friendly Angular 4
From the very outset, the Angular has a lot of useful instruments for optimization of app development, but unfortunately, it doesn’t appear user-friendly to search machines. When you decide to develop the Angular application, you’ll probably face some problems.
Rendering dynamic content in the «source code view»
Angular doesn’t provide a processing of the dynamic content to ‘source code’ and because of this search engines basically can’t process it. If you open Angular app/website with the dynamic content and right-click and then click on the ‘View page source’ button in the context menu, or even simply use the hotkey (CTRL + U or Command + Alt + U) – you’ll see that there is no content inside Angular components. To solve this problem, we’ll use the pre-rendering of our HTML layout on the server-side.
1. First of all, you need to install the packages necessary to launch the server:
$ npm install --save @angular/platform-server @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader @nguniversal/common
$ npm install --save-dev cpy-cli reflect-metadata express
2. Then you need to configure the main app file, which is located (as a rule) in /src/ app/app.module.ts. Then you should add withServerTransition method to the imported module BrowserModule and point the id of our application in the parameters. You also need to connect TransferHttpCacheModule. As a result, your code should look something like this:
// Angular Modules
import { NgModule } from ‘@angular/core';
import { RouterModule } from ‘@angular/router'; // control the transition state between routers.
import { BrowserModule } from ‘@angular/platform-browser'; // сonfigure the interaction between client and server side
import { TransferHttpCacheModule } from ‘@nguniversal/common’; // HTTP interceptor that avoids Http requests duplicating on the client side
// App Components
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
],
imports: [
BrowserModule.withServerTransition({appId: 'my-app'}),
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full'}
]),
TransferHttpCacheModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
The withServerTransition method allows Universal (a tool that allows us to init the app code on the server) to inject the HTML tree generated by the server in the app.
3. Also, you need to generate a new module for the server. To do this, create a file into directory (you can also change the directory if necessary) /src/app/app.server.module.ts and include the server modules there:
import { NgModule } from '@angular/core';
import { ServerTransferStateModule, ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule,
ServerTransferStateModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule { }
4. The next step is setting the general settings to compile and run a code. Create a main.server.ts server file and a TypeScript compilation configuration file. Also, you have to create a file to define static routes (paths).
The main server file may be located in the src/main.server.ts (as a rule this is the best practice) and have the following contents:
export { AppServerModule } from './app/app.server.module';
The TypeScript config for rendering on the server side should have similar structure as the main application config, we need to point only one specific value angularCompilerOptions, in which we will specify the module for compilation. TypeScript (which should be named tsconfig.server.json) configuration file for the server should be located in the root folder and have the following contents:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": [
"node"
]
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}
The static.paths.ts that contain static path should be located in project root folder. In this file you can define static routes:
export const ROUTES = [
'/'
];
5. The next step is to create an Express server, which will render our HTML. Express server file (server.ts) should be located in the directory root folder and contain the following code:
import { enableProdMode } from '@angular/core';
import ‘zone.js/dist/zone-node';
import { join } from 'path';
import { readFileSync } from ‘fs';
import 'reflect-metadata';
import * as express from 'express';
// Enable production mode
enableProdMode();
// Express server
const app = express();
const PORT = process.env.PORT || 3838;
const DIST_FOLDER = join(process.cwd(), 'dist');
// For the template, we use index.html file.
const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');
// Express
import {ngExpressEngine} from '@nguniversal/express-engine';
// Import module map for lazy loading
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';
// Universal express-engine
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
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'), {
maxAge: '1y'
}));
// 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 Express server listening on http://localhost:${PORT}`);
});
The details of the code description, in this case, do not matter, since this is a completely different topic for a single article, Express is an individual tool that needs to be studied separately.
6. Specify settings for the server in the file angulat.cli.json.
You need to add the following piece of code to your angular.cli.json file and register the paths to files that we created earlier if they are different from those specified in the example:
{
"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.css"],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
7. Also, you need to create webpack.server.config.js file in root folder for a correct compilation of the code. This file have to contain next code:
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
// This is our Express server for Dynamic universal
server: './server.ts',
},
target: 'node',
resolve: { extensions: ['.ts', '.js'] },
// Make sure we include all node_modules etc
externals: [/(node_modules|main..*.js)/,],
output: {
// Puts the output at the root of the dist folder
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{ test: /.ts$/, loader: 'ts-loader' }
]
},
plugins: [
new webpack.ContextReplacementPlugin(
// fixes WARNING Critical dependency: the request of a dependency is an expression
/(.+)?angular(\|/)core(.+)?/,
path.join(__dirname, 'src'), // location of your src
{} // a map of your routes
),
new webpack.ContextReplacementPlugin(
// fixes WARNING Critical dependency: the request of a dependency is an expression
/(.+)?express(\|/)(.+)?/,
path.join(__dirname, 'src'),
{}
)
]
}
8. Configuring server launch.
To configure the server launch, it’s enough to register the alias in the package.json file. To do this, specify the following aliases in the script block:
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"lint": "ng lint --fix",
"build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
"build:prerender": "npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:prerender",
"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
"generate:prerender": "cd dist && node prerender",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors",
"serve:prerender": "cd dist/browser && http-server",
"serve:ssr": "node dist/server",
"server": "npm run build:ssr && npm run serve:ssr"
}
Hooray, everything is ready. Now launch the server itself with the command in the console:
npm run server
Then we proceed to the application site and see that our content is displayed in the View Source Code.