I have an Angular application and I just implemented SSR with Angular Universal. Locally I have everything running as expected. No errors on the yarn build:ssr or yarn dev:ssr. When I start the server and inspect the source I get all the meta tags, styling and html.
When I check the source of the page on the server I see some styling:
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8">
<title>Moviese.at</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="Some movie title">
<meta property="og:image" content="https://ogp.me/logo.png">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<style type="text/css">@font-face{font-family:'Roboto';font-style:normal
etc...
<body class="mat-typography">
<app-root></app-root>
<script src="runtime.d486b7bb3a252bc1.js" type="module"></script><script src="polyfills.fbae836bfb070669.js" type="module"></script><script src="main.bee0fdd870e27142.js" type="module"></script>
</body>
I've ran the yarn build:ssr command on the server and yarn serve:ssr and everything is starting without errors.
server.ts:"
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { createWindow } from 'domino';
import { AppServerModule } from './src/main.server';
const scripts = readFileSync('dist/angular-movieseat/browser/index.html').toString();
const window = createWindow(scripts) as Window;
(global as any).window = window;
(global as any).document = window.document;
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/angular-movieseat/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';
I added this:
const scripts = readFileSync('dist/angular-movieseat/browser/index.html').toString();
const window = createWindow(scripts) as Window;
(global as any).window = window;
(global as any).document = window.document;
To avoid rendering these elements on the server. And finally my Nginx server config:
#http
server {
listen 80;
listen [::]:80;
server_name mo***s.at www.mo***s.at
return 301 https://$host$request_uri;
}
#https
server {
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/mo***s.at/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/mo***s.at/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name *.mo***s.at;
access_log /var/log/nginx/nginx.vhost.access.log;
error_log /var/log/nginx/nginx.vhost.error.log;
root /root/angular-mo***se**/dist/angular-mo***se**/browser;
index index.html index.htm;
location / {
try_files $uri $uri/ /index.html?$args;
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /api/ {
proxy_pass http://localhost:4000/;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires max;
log_not_found off;
}
}
This was quite complex/error prone (at least for me). I'm not that great with server configuration. I googled a bit with regards to Nginx and Angular Universal SSR and I tried a few different Nginx server block configurations until I happened to find one somewhere that worked for me:
Something else that took me quite a while to figure out was that if you use
meta.addTag({ ... })in yourapp.componentand in another component you also dometa.addTag({ ... })you get both tags which confused me for quite some time. You want to dometa.updateTag({ ... })if you just need 1 tag.To debug my server I used https://github.com/angular-university/angular-universal-course as my clean project.