Series Overview
- Part 1 — SSR in Angular: Why, When, and How it Works
- Part 2 — Adding SSR to a New or Existing Angular App (Step‑by‑Step)
- Part 3 — Data Fetching, TransferState, and Caching Without Double Requests
- Part 4 — SEO Essentials for SSR: Meta Tags, Structured Data, and Images
- Part 5 — Authentication, Cookies, and Platform‑Aware Code in SSR
- Part 6 — Deploying & Tuning SSR: Node, Vercel, Firebase, Cloudflare, NGINX
- Appendix — Common Errors, Debugging Playbook, and Checklists
Part 1 — SSR in Angular: Why, When, and How it Works
What is SSR?
Server‑Side Rendering (SSR) means Angular renders your initial HTML on the server and ships it to the browser. The browser then hydrates that HTML into a live Angular app.
Why SSR vs CSR
- Faster First Contentful Paint (FCP) & Largest Contentful Paint (LCP) on slow networks/devices
- SEO for content pages (marketing, blogs, product pages)
- Social sharing (OpenGraph/Twitter cards render correctly)
- Perceived performance: users see real content sooner
CSR (client‑side rendering) is still fine for app‑like dashboards behind auth. Many production apps run hybrid: SSR for public routes, CSR for private routes.
How Angular SSR Works (high level)
- Request hits server (Node/Edge runtime).
- Angular renders the route to an HTML string using your app code.
- Server returns HTML + critical CSS + serialized TransferState.
- Browser loads, Angular hydrates the DOM and re‑uses the server HTML, skipping re‑render.
Key Terms
- Hydration: Client attaches event listeners and reuses existing DOM from SSR.
- TransferState: A per‑request JSON store that moves data from server to client to avoid re‑fetching.
- Platform Server/Browser: Angular provides DI tokens to run different code per platform.
When NOT to use SSR
- All content is fully private behind auth, with no SEO need.
- Real‑time dashboards where SSR cost doesn’t pay back.
- Extremely dynamic pages where caching is complex and edge‑generated HTML would quickly stale (consider CSR + API or incremental pre‑render instead).
Part 2 — Adding SSR to a New or Existing Angular App (Step‑by‑Step)
Works for Angular 17–19 and standalone apps.
1) Add SSR tooling
ng add @angular/ssr
This command:
- Adds server build targets in
angular.json
- Wires up client hydration (
provideClientHydration()
) - Configures a Node server entry (
server.ts
)
2) App bootstrap (main.ts)
Make sure you’re using standalone APIs (recommended):
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideClientHydration } from '@angular/platform-browser';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration(),
provideHttpClient(withFetch()),
],
});
3) Server bootstrap (server.ts)
// server.ts
import 'zone.js/node';
import { createServer } from 'http';
import { join } from 'node:path';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { ngExpressEngine } from '@nguniversal/express-engine';
import bootstrap from './src/main.server';
const __filename = fileURLToPath(import.meta.url);
const __dirname = join(__filename, '..');
async function run() {
const server = express();
const distFolder = join(process.cwd(), 'dist/your-app/browser');
const indexHtml = 'index.html';
server.engine('html', ngExpressEngine({ bootstrap }));
server.set('view engine', 'html');
server.set('views', distFolder);
// Static assets
server.get('*.*', express.static(distFolder, { maxAge: '1y' }));
// All routes SSR
server.get('*', (req, res) => {
res.render(indexHtml, { req });
});
const port = process.env['PORT'] ?? 4000;
createServer(server).listen(port, () => {
console.log(`SSR server listening on http://localhost:${port}`);
});
}
run();
Note: In Angular 17+, the
@angular/ssr
builder can also serve without a custom Express server. Use Express when you need custom headers, cookies, or proxies.
4) Build & run
# Development SSR (watch):
ng run your-app:serve-ssr
# Production SSR build:
ng run your-app:build:ssr
# Start the production server (Node):
node dist/your-app/server/server.mjs
5) Folder structure (after SSR)
/ src
main.ts
main.server.ts
server.ts
/ dist/your-app/
/browser <-- client bundle
/server <-- server bundle
6) Platform‑safe code patterns
import { inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
const platformId = inject(PLATFORM_ID);
if (isPlatformBrowser(platformId)) {
// Safe to use window/document
}
if (isPlatformServer(platformId)) {
// Server‑only logic (cookies, headers)
}
Part 3 — Data Fetching, TransferState, and Caching Without Double Requests
The problem
If you fetch data on the server and again in the browser, you waste bandwidth and slow hydration.
The solution: TransferState
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { makeStateKey, TransferState } from '@angular/platform-browser';
@Injectable({ providedIn: 'root' })
export class ProductsService {
private http = inject(HttpClient);
private ts = inject(TransferState);
getProducts() {
const KEY = makeStateKey<any>('products');
const cached = this.ts.get(KEY, null);
if (cached) return of(cached);
return this.http.get('/api/products').pipe(
tap(data => this.ts.set(KEY, data))
);
}
}
Route‑level data with resolvers (SSR‑friendly)
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { ProductsService } from './products.service';
export const productsResolver: ResolveFn<any> = () => {
return inject(ProductsService).getProducts();
};
Wire up in your routes:
{
path: 'products',
loadComponent: () => import('./products.component').then(m => m.ProductsComponent),
resolve: { products: productsResolver }
}
Avoiding API base URL issues
On the server you likely need absolute URLs or to proxy.
// server.ts — proxy example
server.use('/api', createProxyMiddleware({
target: 'https://api.yourdomain.com',
changeOrigin: true,
}));
Streaming SSR & defer
blocks
For non‑critical regions, render shell + stream later:
<!-- products.component.html -->
<h1>Products</h1>
@defer (on viewport) {
<products-grid [items]="products"></products-grid>
} @placeholder {
<skeleton-grid></skeleton-grid>
}
SSR renders the shell; non‑critical parts load post‑hydration.
HTTP Interceptors for cookies/headers on server
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
export const serverHeadersInterceptor: HttpInterceptorFn = (req, next) => {
const request = inject(REQUEST, { optional: true });
if (request) {
req = req.clone({ setHeaders: { 'x-req-id': request.headers['x-request-id'] ?? '' } });
}
return next(req);
};
Part 4 — SEO Essentials for SSR: Meta, Structured Data, and Images
Title & Meta Service
import { Title, Meta } from '@angular/platform-browser';
import { inject } from '@angular/core';
export function setProductMeta(p: Product) {
const title = inject(Title);
const meta = inject(Meta);
title.setTitle(`${p.name} — Buy Now`);
meta.updateTag({ name: 'description', content: p.description.slice(0, 150) });
meta.updateTag({ property: 'og:title', content: p.name });
meta.updateTag({ property: 'og:type', content: 'product' });
meta.updateTag({ property: 'og:image', content: p.imageUrl });
meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
}
Structured data (JSON‑LD)
import { DOCUMENT } from '@angular/common';
export function injectProductJsonLd(p: Product) {
const doc = inject(DOCUMENT);
const script = doc.createElement('script');
script.type = 'application/ld+json';
script.text = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: p.name,
image: [p.imageUrl],
description: p.description,
sku: p.sku,
offers: {
'@type': 'Offer',
priceCurrency: 'USD',
price: p.price,
availability: 'https://schema.org/InStock'
}
});
doc.head.appendChild(script);
}
Canonicals & robots
Add a canonical link per route:
import { DOCUMENT } from '@angular/common';
export function setCanonical(url: string) {
const doc = inject(DOCUMENT);
let link: HTMLLinkElement | null = doc.querySelector('link[rel="canonical"]');
if (!link) {
link = doc.createElement('link');
link.setAttribute('rel', 'canonical');
doc.head.appendChild(link);
}
link.setAttribute('href', url);
}
Ensure robots.txt
and sitemap.xml
are accessible from /
(can be served as static assets).
Images
- Provide explicit
width
/height
to improve CLS. - Use modern formats (AVIF/WebP) and responsive sources (
<img srcset>
). - Lazy‑load non‑critical images (
loading="lazy"
).
Part 5 — Authentication, Cookies, and Platform‑Aware Code in SSR
Prefer cookies for SSR
JWT stored in localStorage
is not available on the server. Use HTTP‑only cookies for SSR‑friendly auth.
Reading cookies/headers on the server
import { inject } from '@angular/core';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
export function readAuthCookie() {
const req = inject(REQUEST, { optional: true }) as any;
return req?.cookies?.auth ?? null;
}
Guarding routes
import { CanActivateFn } from '@angular/router';
export const authGuard: CanActivateFn = () => {
const token = readAuthCookie();
return !!token; // SSR + CSR
};
Platform checks (no window
on server)
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { PLATFORM_ID, inject } from '@angular/core';
const pid = inject(PLATFORM_ID);
if (isPlatformBrowser(pid)) {
// browser‑only code
}
CSRF & stateful APIs
If you use cookie auth, enable CSRF protection (e.g., double submit cookie) and set SameSite
and Secure
attributes appropriately.
Part 6 — Deploying & Tuning SSR
Build for production
ng run your-app:build:ssr
This outputs optimized client & server bundles under dist/
.
Option A — Node + NGINX (PM2)
- Serve static from
dist/your-app/browser
- Proxy dynamic routes to Node SSR server
- Enable gzip/br compression and caching headers for static assets
Example PM2 ecosystem file:
module.exports = {
apps: [{
name: 'your-app-ssr',
script: 'dist/your-app/server/server.mjs',
instances: 'max',
exec_mode: 'cluster',
env: { NODE_ENV: 'production', PORT: 4000 },
}]
};
Option B — Vercel
- Use the Angular SSR output as a serverless/edge function via Vercel adapter (or
vercel.json
routing). Map/*
to SSR handler; cache static assets.
Option C — Firebase Hosting + Functions
- Host static under Hosting; proxy dynamic SSR to a Function. Set proper
cache-control
for static.
Option D — Cloudflare Workers
- Bundle the server with a Workers adapter and KV/Assets for static files. Keep the server code edge‑safe (no Node‑specific APIs).
Performance Tuning Checklist
- ✅ Compress (gzip/br) static assets
- ✅ Long‑term cache for
*.js, *.css, *.woff2, *.png, *.webp
with hashes - ✅ HTML: short TTL or no cache (unless using ISR)
- ✅ Avoid blocking APIs in SSR path; add timeouts/fallbacks
- ✅ Use
TransferState
to avoid duplicate fetches - ✅ Split routes & use
@defer
for non‑critical UI - ✅ Monitor LCP/CLS/INP via RUM (e.g., Web‑Vitals)
Appendix — Common Errors & Debugging Playbook
“window is not defined”
You used a browser‑only API during SSR. Guard with isPlatformBrowser
or move to an effect that only runs on the client.
Double fetching after hydration
Use TransferState
and/or route resolvers. Ensure keys are stable.
API calls failing only on server
Absolute vs relative URLs, missing auth cookies, or CORS. Consider server‑side proxy.
Memory leaks in Node
Avoid long‑lived caches keyed by request objects; prefer LRU keyed by URL/user.
Debugging tips
- Log
process.pid
to confirm clustering - Add request IDs to trace renders
- Use
NODE_OPTIONS=--max-old-space-size=1024
to tune memory in constrained envs - Validate rendered HTML for meta and JSON‑LD before hydration
Ready‑to‑Use Templates
SSR‑ready providers (app.config.ts)
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { serverHeadersInterceptor } from './server-headers.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
provideHttpClient(withFetch(), withInterceptors([serverHeadersInterceptor])),
],
};
Environment‑aware API base URL
import { inject } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
export function apiBaseUrl() {
const pid = inject(PLATFORM_ID);
return isPlatformServer(pid) ? 'https://api.yourdomain.com' : '/api';
}
Minimal Express server with compression & security headers
import compression from 'compression';
import helmet from 'helmet';
server.use(compression());
server.use(helmet({ contentSecurityPolicy: false }));
# Angular SSR — Deep‑Dive Blog Series (for Angular 17–19)
> A practical, code‑heavy series you can ship with. Brought to you by **Sitegator** — your trusted partner for Angular, WordPress, and modern web solutions. [Contact Sitegator](#contact-sitegator).
**Series Overview**
* **Part 1** — [SSR in Angular: Why, When, and How it Works](#part-1--ssr-in-angular-why-when-and-how-it-works)
* **Part 2** — [Adding SSR to a New or Existing Angular App (Step‑by‑Step)](#part-2--adding-ssr-to-a-new-or-existing-angular-app-step-by-step)
* **Part 3** — [Data Fetching, TransferState, and Caching Without Double Requests](#part-3--data-fetching-transferstate-and-caching-without-double-requests)
* **Part 4** — [SEO Essentials for SSR: Meta Tags, Structured Data, and Images](#part-4--seo-essentials-for-ssr-meta-structured-data-and-images)
* **Part 5** — [Authentication, Cookies, and Platform‑Aware Code in SSR](#part-5--authentication-cookies-and-platform-aware-code-in-ssr)
* **Part 6** — [Deploying & Tuning SSR: Node, Vercel, Firebase, Cloudflare, NGINX](#part-6--deploying--tuning-ssr)
* **Appendix** — [Common Errors, Debugging Playbook, and Checklists](#appendix--common-errors--debugging-playbook-and-checklists)
---
References
## Part 1 — SSR in Angular: Why, When, and How it Works
Learn more about Angular’s SSR architecture in the [official Angular documentation](https://angular.dev/guide/ssr).
... *(content unchanged except for SEO links below)* ...
* **Social sharing**: Learn about [OpenGraph meta tags](https://ogp.me/) and [Twitter cards](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards) for better previews.
---
## Part 2 — Adding SSR to a New or Existing Angular App (Step‑by‑Step)
Follow the [Angular SSR package guide](https://angular.dev/guide/ssr) for the latest updates.
---
## Part 3 — Data Fetching, TransferState, and Caching Without Double Requests
For API proxying, you can also check [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware).
---
## Part 4 — SEO Essentials for SSR: Meta, Structured Data, and Images
Useful resources:
* [Google Search Central: JavaScript SEO basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)
* [Schema.org Product Specification](https://schema.org/Product)
---
## Part 5 — Authentication, Cookies, and Platform‑Aware Code in SSR
Learn more about [SameSite cookie security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) on MDN.
---
## Part 6 — Deploying & Tuning SSR
* [Vercel Angular Deployment Guide](https://vercel.com/guides/deploying-angular-with-vercel)
* [Firebase Hosting + Functions](https://firebase.google.com/docs/hosting/frameworks/angular)
* [Cloudflare Workers & Angular SSR](https://developers.cloudflare.com/workers/)
---
## Appendix — Common Errors & Debugging Playbook
See [Angular SSR Troubleshooting](https://angular.dev/guide/ssr#troubleshooting) for updated error fixes.
---
## Contact Sitegator
📧 Email: [support@sitegator.com](mailto:support@sitegator.com)
🌐 Website: [https://www.sitegator.com](https://www.sitegator.com)
📞 Phone: +91‑8779301717
Stay connected with Sitegator for expert guidance in Angular SSR, WordPress, and modern web development.