Add Server-Side Rendering(SSR) to My Angular Webpack Project

TLTR: Webpack -> Angular CLI -> Angular Universal

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

A Problem I try to solve: I got this old Angular project, which uses Webpack to compile, bundle and build. I need to add server-side rendering to it.

It seems to be a difficult task; however, the process was easier than I expected. Here are the things I did:

Step 1: Transform from Webpack to Angular CLI

All the materials I found online about adding Angular Universal is on the assumption that the application is using Angular CLI. Instead of trying to figure out how to workaround with Webpack, I simply transform the application into using Angular CLI.

There are 2 ways you could transform from a Webpack application to Angular CLI one:

  1. Create a dummy and empty Angular CLI application using ng new {new project name} and copy contents you need from your newly created one to your current application. For more information, you could check out this blog post: https://www.codepedia.org/ama/fast-faster-angular-cli-how-i-converted-my-angular-project-to-use-angular-cli. It is essential “copy and paste”.
  2. In your current project directory, create and add the bits and pieces that you need to run Angular CLI.
Image for post
Image for post

In this blog post, I am going to show you how to do the 2nd way:

  1. Install Angular CLI and add it to your project’s devDependencies.
npm install --save-dev @angular/cli

2. Create your ownangular.json. Copy the angular.json from https://github.com/angular/angular/blob/master/aio/angular.json and modify to fit your project. You probably need to change it to something like:

{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"{ your project name }": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "th",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/browser",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.json"
],
"styles": [
"src/styles/index.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"serviceWorker": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "treehole:build"
},
"configurations": {
"production": {
"browserTarget": "{ your project name }:build:production"
}
}
},
...
},
"defaultProject": "{ your project name }"
}

3. Create environment files.

In above angular.json, we reference the environment files under src/environments/environment.ts and src/environments/environment.prod.ts, so we need to create those files.

Image for post
Image for post

envrionment.ts:

export const environment = {
production: false,
};

environment.prod.ts:

export const environment = {
production: true,
};

4. Try to replace your Webpack environment variables with the ones you created above. For example, for my project, there is a Webpack variable __PROUDUCTION__, I could replace this with envirnoment.production.

// before:
new webpack.DefinePlugin({
__DEV__: !isProduction,
__PRODUCTION__: isProduction,
}),
// main.ts
__PRODUCTION__ ? main() : bootloader(main);

// after:
// main.ts
environment.production ? main() : bootloader(main);

4. Change the scripts in package.json to using Angular CLI, similar to scripts below:

"start": "ng serve",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",

You might not need all the commands listed above. Try each command to see whether it works or not.

Image for post
Image for post

It is unlikely to work for the first run, you might need to tweak angular.json to make it work.

5. Remember to clean up your no-longer-used Webpack files and remove unused libraries from dependencies in package.json.

Step 2: Add Angular Universal

There are 2 ways to add Angular Universal:

  1. If you are lucky, you could add Angular Universal using 1 command:
ng add @nguniversal/express-engine --clientProject { your project name }

2. Add bits and pieces you need for angular universal.

Since my project is pretty old, I got a console error when I try to use way #1. Instead of trying to figure out which library version I should use, I decided to add files manually. I follow the instructions here: https://github.com/angular/angular-cli/wiki/stories-universal-rendering.

After I did everything in the instructions, of course, again, it is unlikely to work on the first run, I got console errors.

Image for post
Image for post

Tips to Debug

Image for post
Image for post
  • The most common errors I got are undefined errors related to window , document, or localstorage. This happens because these objects only exist in your browser, it does not exist in the server. You need to mock these objects in server environment. My solution is to create a whole bunch of mock classes for browser objects and also mock the functions these objects used:
export class MockWindow {
addEventListener = () => {};
}
export class MockDocument {}export class MockStorage {
length = 3;
key() {
return '';
}
getItem() {
return '{}';
}
removeItem() {
return '';
}
setItem() {
return '';
}
clear() {
return '';
}
}
// app.server.ts
@NgModule({
...
providers: [
{ provide: LOCAL_STORAGE, useClass: MockStorage },
{ provide: SESSION_STORAGE, useClass: MockStorage },
{ provide: WINDOW, useClass: MockWindow },
{ provide: DOCUMENT, useValue: MockDocument },
],
...
})
// app.browser.ts
@NgModule({
...
providers: [
{ provide: LOCAL_STORAGE, useValue: localStorage },
{ provide: SESSION_STORAGE, useValue: sessionStorage },
{ provide: WINDOW, useValue: window },
{ provide: DOCUMENT, useValue: document },
],
...
})
// inside service class
constructor(@Inject(WINDOW) public window: Window) {}
  • Important note: if your service class use browser objects such as window, document, navigator…, make sure you provide your class in this way:
{ provide: { your service class name}, useClass: { your service class name}, deps: [WINDOW, DOCUMENT] }

It caused me a few hours to debug one issue because I forgot to list all the class dependencies in deps.

Custom Base Href

For my project, the base href is not the root. I have to change the server.ts.

For example, in my index.html, this my base href:

<base href="/custom-base-href/">

In server.ts, I need to to change the router path:

app.use(`/custom-base-href`, createRoutes());function createRoutes() {
const router = express.Router();

// Server static files from /browser
router.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

// All regular routes use the Universal engine
router.get('*', (req, res) => {
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});

return router;
}

Custom Routes Outside Angular

My project has routes outside Angular. For example, when the site hits another URL like /documentation, it is serving other static HTML that is not a part of the Angular app. To acheive this, I have to add this in server.ts:

// Serving documentation files from /documentation
router.use('/documentation', express.static(DOCS_FOLDER));

In Conclusions…

Here are some problems I ran in when I add server-side rendering to my Angular Webpack app. Unfortunately, it has to a big-bang approach and this is just a beginning. To maintain the codebase and make sure SSR is always working is also a struggle as well. Make sure to always check SSR locally if you add browser objects like window, document… and functions that don’t need to be imported like setTimeout, setInterval

A frontend web developer in Toronto

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store