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.
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:
-
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 yourserver.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(); ...
-
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 methodsisPlatformBrowser
andisPlatformServer
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)