This post is a transliteration of my journey to implement server side rendering for Angular on Azure. All the text that is highlighted in yellow is specific to my project, so you will need to modify it for your setup.
ng generate universal
To begin run “ng generate universal” this will create a few
files and modify angular.json
Create an HttpHandler
Create an HttpHandler which will run node on the server and
generate the server-side rendered output:
using
System.Diagnostics;
using
System.IO;
using
System.Web;
namespace
AngularSSR {
public class AngularSSRHandler:
IHttpHandler {
public void
ProcessRequest(HttpContext context) {
var
request = context.Request;
var
response = context.Response;
var
basepath = context.Server.MapPath("~");
var
urlPathRaw = request.Url.PathAndQuery;
const string
prefix = "/v2/prerender/";
const string
staticPrefix = "/prerender/v2/ang6";
if
(urlPathRaw.StartsWith(staticPrefix)) {
return;
}
if
(!urlPathRaw.StartsWith(prefix)) {
return;
}
var
urlPath = urlPathRaw.Substring(prefix.Length);
var
universalPath = Path.Combine(basepath, "ang6/universal.js");
var
prerenderPath = Path.Combine(basepath, "prerender.html");
var
indexPath = Path.Combine(basepath, "ang6/index.html");
var
arguments = $"{universalPath} /{urlPath}";
var psi
= new ProcessStartInfo("node.exe",
arguments) {
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = basepath,
CreateNoWindow = false,
};
var
process = Process.Start(psi);
if
(process == null) return;
process.BeginOutputReadLine();
process.BeginErrorReadLine();
var sw =
new StringWriter();
var
swErr = new StringWriter();
process.OutputDataReceived += (s,
e) => { sw.Write(e.Data); };
process.ErrorDataReceived += (s, e)
=> { swErr.Write(e.Data); };
process.WaitForExit();
var ok =
process.ExitCode == 0;
var
output = sw.ToString();
var
outputError = swErr.ToString();
if (!string.IsNullOrEmpty(outputError))
{
response.Write("<!--
prerender error <pre>\n");
response.Write(outputError);
response.Write("\n</pre> -->");}
if (!string.IsNullOrEmpty(output))
{
response.Write("<!-- prerender output <pre>\n");
response.Write(output);
response.Write("\n</pre> -->");}
if (ok)
{
var
prerendered = File.ReadAllText(prerenderPath);
response.Write(prerendered);
}
else {
var
index = File.ReadAllText(indexPath);
response.Write(index);
}
response.End();
}
public bool
IsReusable => false;
}
}
Specify Node Version in Azure Webapp
Add an application setting called WEBSITE_NODE_DEFAULT_VERSION with a value of 8.9.4
Modify web.config
Add the handler to web.config.
<system.webServer>
<handlers>
<add verb="*" path="v2/prerender/*" name="AngularSSRHandler" type="AngularSSR.AngularSSRHandler, AngularSSR"/>
</handlers>
</system.webServer>
Redirect all requests to the handler first.
<system.webServer>
<rewrite>
<rules>
<rule name="AngularJS
Routes" stopProcessing="true">
<match url="(.*)" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
</conditions>
<action type="Rewrite" url="prerender/{R:1}" />
</rule>
</rules>
</rewrite>
</system.webServer>
Modify angular.json
In angular.json add a “configurations” key under the server
architecture of your project:
"projects": {
"ang6": {
"server": {
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "none",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false
}
}
}
}
},
},
Refactor Animations
First, remove the import of BrowserAnimationsModule from
your app.module.ts, then create a module called app.browser.module.ts which
will be your new main module, and in it import BrowserAnimationsModule
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserAnimationsModule,
AppModule,
],
bootstrap: [AppComponent],
})
export class AppBrowserModule {}
Also update your main.ts with the new module
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppBrowserModule } from './app/app.browser.module';
import { environment } from './environments/environment';
import 'hammerjs';
if (environment.production) {
enableProdMode();
}
document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic().bootstrapModule(AppBrowserModule)
.catch(err => console.log(err));
});
Then update your app.server.module.ts (which was generated
by “ng generate universal”) to include the NoopAnimationsModule
import { NgModule } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
NoopAnimationsModule,
ServerModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
Angular Entry Point for SSR
Create a server entry point for angular called universal.js:
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { createWindow } from 'domino';
import { renderModuleFactory } from '@angular/platform-server';
import { writeFileSync, readFileSync } from 'fs';
const fakeStorage = {
length: 0,
clear: () => { },
getItem: (_key) => null,
key: (_index) => null,
removeItem: (_key) => { },
setItem: (_key, _data) => { }
};
var url = process.argv[2] || '/';
import { AppServerModuleNgFactory } from './dist/ang6-server/main';
const indexHtml = readFileSync('./ang6/index.html', 'utf-8').toString();
var win = createWindow(indexHtml);
global['window'] = win;
global['document'] = win.document;
global['navigator'] = win.navigator;
global['localStorage'] = fakeStorage;
global['DOMTokenList'] = win.DOMTokenList;
global['Node'] = win.Node;
global['Text'] = win.Text;
global['HTMLElement'] = win.HTMLElement;
global['MutationObserver'] = getMockMutationObserver();
function getMockMutationObserver() {
return class {
observe(node, options) {
}
disconnect() {
}
takeRecords() {
return [];
}
};
}
Object.defineProperty(win.document.body.style,
'transform', {
value: () => {
return {
enumerable: true,
configurable: true
};
},
});
Object.defineProperty(win.document.body.style,
'box-shadow', {
value: () => {
return {
enumerable: true,
configurable: true
};
},
});
global['getComputedStyle'] = win.getComputedStyle;
import 'hammerjs';
renderModuleFactory(AppServerModuleNgFactory,
{
document: indexHtml + '\n\n',
url
})
.then(html => {
console.log('Pre-rendering successful, saving
prerender.html');
writeFileSync('./prerender.html', html);
})
.catch(error => {
console.error('Error occurred:', error);
});
Webpack SSR Configuration
Create a webpack.config.js for the ssr. Note that we are using 'development' mode because using 'none' or 'production' doesn't work due to some kind of bug in webpack (https://github.com/angular/angular-cli/issues/10787)
var path = require('path');
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports
= {
mode: 'development',
entry: {
universal: './universal.js'
},
target: 'node',
plugins: [
new
UglifyJsPlugin({
sourceMap: true,
uglifyOptions: {
output: {
beautify: false,
},
compress: true,
mangle: true,
},
})
],
output: {
filename: 'universal.js',
path: path.resolve(__dirname, 'dist/ang6')
}
};
Modify package.json
Add a build:ssr script and a webpack script to package.json
"scripts": {
"build:ssr": "ng
build --i18n-locale=en-UK --prod --base-href /v2 --deploy-url /v2/ang6/ && ng run ang6:server:production
&& npm run webpack",
"webpack": "webpack"
},