DEV Community

Cover image for Painless Angular SSR with Nginx/Apache/Firebase & Angular CLI
Michele Da Rin
Michele Da Rin

Posted on • Originally published at blog.daudr.me

Painless Angular SSR with Nginx/Apache/Firebase & Angular CLI

This article has been previously published on my main blog

So you're building a new, phantasmagorical web site (let's say a brand new e-commerce for those absolutely unknown books that you find beautifully written) and you need your future clients to find it on the first search page on Google.

Serve your Angular app right

Photo by Alev Takil on Unsplash

After a lot of research, you decided to use Angular as FE framework because you think it's the best fit for that e-commerce, but when you finally deploy it on production and you search it you don't find even if it's some time that's been published, what's going on?

Google recently updated its crawler to pre-render javascript Single Page Applications like Angular ones, but this technology doesn't always work well (and you need to know that not everybody uses Google to surf the Internet).

Angular Universal to the rescue

Fortunately, the Angular Team built a nice (after some initial troubles) tool that helps us, a tool called Angular Universal.

Setting things up

Setting Angular Universal up is relatively easy, you just to type a single command in your terminal

ng add @nguniversal/express-engine --clientProject "your project that neeeds SSR"

This little command generates some files in your src/ folder:

  • main.server.ts -  Bootstrapper of the server app
  • app/app.server.module.ts  -  Server-side app module
  • server.ts -  Express web server
  • ts.config.server.json -  Typescript config for your server app
  • webpack.server.config.js -  Webpack server configuration

Verify if the SSR app works

To verify if your brand new Server-Side rendered app will work type in your terminal

npm run build:ssr
npm run serve:ssr

If all works well (you'd be really lucky) you should see the app working on localhost:4000, else continue reading to know some error catching.

Catching SSR errors

There are plenty of errors that could occur while trying to build our SSR app, let's try to understand the most common ones.

Using browser only namespaces

When the app is running in our server there are some standard objects that we cannot use, ie window, so we can do two things:

  1. Inject false objects to "deceive" the server and make our app working even on the server-side, to do this simply add these lines before the express() initialization in your server.ts file:

    ...
    const domino = require('domino');
    const fs = require('fs');
    const path = require('path');
    const templateA = fs.readFileSync(path.join('dist/browser', 'index.html')).toString();
    const win = domino.createWindow(templateA);
    win.Object = Object;
    win.Math = Math;
    global['window'] = win;
    global['document'] = win.document;
    global['branch'] = null;
    global['object'] = win.object;
    const app = express();
    ...
    
  2. Detect the parts where you're using these objects and make them run only in your browser app.
    In this case Angular makes us available two methods isPlatformBrowser and isPlatformServer exposed by @angular/common.
    Here's a little example:

    import { Component, Inject, PLATFORM_ID } from '@angular/core';
    import { isPlatformBrowser } from '@angular/common';
    
    @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
    })
    export class AppComponent {
        constructor(@Inject(PLATFORM_ID) private platformId: any) {}
    
        onActivate(event: any) {
            if (isPlatformBrowser(this.platformId)) {
                const scrollToTop = window.setInterval(() => {
                    const pos = window.pageYOffset;
                    if (pos > 0) {
                        window.scrollTo(0, pos - 50);
                    } else {
                        window.clearInterval(scrollToTop);
                    }
                }, 16);
            }
        }
    }
    

    This snippet runs the scrollTop only if this code runs on the browser.

Finish this all: Deploy to production

The best part arrived, the part where I'll walk you through server configuration, isn't it awesome?

Apache Configuration

If you're using Apache as the webserver you need to create or modify your .htaccess file:

<IfModule mod_rewrite.c>
 RewriteEngine On
 # If an existing asset or directory is requested go to it as it is
 RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
 RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
 RewriteRule ^ - [L]

 RewriteRule ^ /dist/browser/index.html
</IfModule>
<VirtualHost *:80>
 ServerName example.com # <-- modify here
 ServerAlias www.example.com # <-- modiy here
 <Proxy *>
  Order allow,deny
  Allow from all
 </Proxy>
 ProxyPreserveHost On
 ProxyRequests Off
 ProxyPass / http://localhost:4000/
 ProxyPassReverse / http://localhost:4000/
</VirtualHost>

<VirtualHost *:443>
 ServerName example.com # <-- modify here
 ServerAlias www.example.com # <-- modify here
 <Proxy *>
  Order allow,deny
  Allow from all
 </Proxy>
 ProxyPreserveHost On
 ProxyRequests Off
 ProxyPass / https://localhost:4000/
 ProxyPassReverse / https://localhost:4000/
</VirtualHost>

Nginx configuration

In case you using Nginx instead you need to configure the configuration under the /etc/nginx/sites-available/www.example.com file:

upstream your_upstream_config {
 server 127.0.0.1:4000;
}

server {
 listen 443 ssl http2;
 server_name www.example.com; # <--- modify here
 root /home/"your user"/path/to/dist/browser;
 location / {
  try_files $uri $uri @backend;
 }

 location @backend {
  proxy_pass http://your_upstream_config;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_http_version 1.1;
  proxy_set_header X-NginX-Proxy true;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_cache_bypass $http_upgrade;
  proxy_redirect off;
  proxy_set_header X-Forwarded-Proto $scheme;
 }
}

server {
 listen 80;
 server_name www.example.com # <-- modify here
 return 301 https://$server_name$request_uri?;
}

Bonus - Firebase config 🤩🤩🤩

If you're using Firebase hosting there are some things we have to add and two ways you can accomplish what you've already done with Apache or Nginx: AppEngine or Cloud Functions.

Polyfills your firebase project

Firebase uses Websockets and XHR not included in Angular that we need to polyfill. So we run in our terminal:

npm install ws xhr2 bufferutil utf-8-validate -D

and we add these two new lines to our server.ts files:

(global as any).WebSocket = require('ws');
(global as any).XMLHttpRequest = require('xhr2');
...

Deploy on AppEngine

Add a app.yaml at the root of the project with this line:

runtime: nodejs10

And then, after you install the Google Cloud CLI, run in your terminal:

gcloud app deploy

We also need to change the start script in our package.json:

// ...
"scripts": {
  "start": "npm run serve:ssr",
  // ...
},
// ...

And that's all, you should see your app running on Google Cloud! 👌

Using Firebase Cloud Functions

In order to deploy our app using Firebase Cloud Functions we to need to init it and modify some files:

To init Firebase run

firebase init

and select hosting and functions

Then modify your funtions/package.json file:

{
  "hosting": {
    // ...
    "rewrites": [
      { "source": "**", "funtion": "ssr" }
    ]
  },
    // ...
}

Remove your express listener in server.ts:

// Start up the Node server
// app.listen(PORT, () => {
//   console.log(`Node Express server listening on http://localhost:${PORT}`);
// })

Update your webpack.server.config.js:

output: {
  // Puts the output at the root of the dist folder
  path: path.join(__dirname, 'dist'),
  library: 'app',
  libraryTarget: 'umd',
  filename: '[name].js',
},

Make sure to rebuild your app running npm run build:ssr

We then move in our functions directory to install fs-extra

cd functions
npm install fs-extra --save

The Firebase function needs to access our built app, so let's create a simple node script called cp-angular,js that copies our app in the functions folder:

const fs = require('fs-extra');

(async() => {

    const src = '../dist';
    const copy = './dist';

    await fs.remove(copy);
    await fs.copy(src, copy);

})();

Then in our functions/package.json we add a build script:

{
  "name": "functions",
  "engines": {
    "node": "10"
  },
  "scripts": {
    "build": "node cp-angular && tsc"
  }
}

Then we can finally write our functions, so our functions/index.js file will look something like this:

import * as functions from 'firebase-functions';
const universal = require(`${process.cwd()}/dist/server`).app;

export const ssr = functions.https.onRequest(universal);

To verify that all is working right you can use these commands:

cd functions
npm run build
firebase serve

And if all looks good just deploy it:

firebase deploy

Done! 😎

That's all, you're now a master of Angular Universal!

Top comments (0)