Server-Side Rendering (SSR) renders your Angular app on the server before sending it to the browser. This improves initial load time, SEO, and social media previews. Here's how to add it to your Angular project.
Why SSR?
By default, Angular apps are client-side rendered (CSR) — the browser downloads JavaScript, executes it, and then renders the page. This means:
- Search engine crawlers may see a blank page
- Users on slow connections wait longer for meaningful content
- Social media link previews don't work properly
SSR solves all of these by delivering fully rendered HTML from the server.
Prerequisites
- Angular 17+ (this guide uses the latest Angular CLI)
- An existing Angular project
- Node.js 18+
Step 1: Add Angular SSR
Angular now ships SSR support as a first-party package. Run:
ng add @angular/ssrThis command will:
- Install
@angular/ssrandexpress - Create a
server.tsfile in your project root - Update
angular.jsonwith server build configuration - Update
app.config.tsto include SSR providers
Step 2: Understand the Generated Files
server.ts
This is your Express server entry point:
import { CommonEngine } from '@angular/ssr/node';
import express from 'express';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import bootstrap from './src/main.server';
const serverDir = dirname(fileURLToPath(import.meta.url));
const browserDir = resolve(serverDir, '../browser');
const app = express();
const commonEngine = new CommonEngine();
app.get('**', express.static(browserDir, {
maxAge: '1y',
index: 'index.html',
}));
app.get('**', (req, res, next) => {
commonEngine
.render({
bootstrap,
documentFilePath: join(browserDir, 'index.html'),
url: req.originalUrl,
publicPath: browserDir,
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
app.listen(4000, () => {
console.log('Server listening on http://localhost:4000');
});app.config.server.ts
This configures the server-side application:
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);Step 3: Handle Browser-Only APIs
SSR runs your code on the server where window, document, and localStorage don't exist. You need to guard these:
import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, PLATFORM_ID } from '@angular/core';
@Component({
selector: 'app-example',
template: `<p>{{ message }}</p>`,
})
export class ExampleComponent {
message = '';
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
this.message = window.innerWidth > 768 ? 'Desktop' : 'Mobile';
}
}
}Alternatively, use the afterNextRender lifecycle hook in Angular 17+:
import { Component, afterNextRender } from '@angular/core';
@Component({
selector: 'app-example',
template: `<p>Chart goes here</p>`,
})
export class ChartComponent {
constructor() {
afterNextRender(() => {
// Safe to use browser APIs here
import('chart.js').then((Chart) => {
// Initialize chart
});
});
}
}Step 4: Build and Run
Build your application with SSR:
ng buildThis produces both browser and server bundles in the dist/ folder. Run the server:
node dist/<your-project>/server/server.mjs
Visit http://localhost:4000 and view the page source — you'll see fully rendered HTML.
Step 5: Enable Hydration
Hydration lets Angular reuse the server-rendered DOM instead of re-rendering it. It's enabled by default in app.config.ts:
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideClientHydration(),
],
};This avoids the "flicker" where the page re-renders after JavaScript loads.
Step 6: SEO with SSR
Now that your app is server-rendered, you can fully leverage SEO. Angular provides built-in services to manage meta tags and titles dynamically on the server.
Set Page Titles and Meta Tags
Use Angular's Title and Meta services in your components:
import { Component, OnInit } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
@Component({
selector: 'app-product-page',
template: `<h1>{{ product.name }}</h1>`,
})
export class ProductPageComponent implements OnInit {
product = { name: 'Angular Book', description: 'Learn Angular SSR' };
constructor(private title: Title, private meta: Meta) {}
ngOnInit() {
this.title.setTitle(`${this.product.name} | My Store`);
this.meta.updateTag({ name: 'description', content: this.product.description });
this.meta.updateTag({ property: 'og:title', content: this.product.name });
this.meta.updateTag({ property: 'og:description', content: this.product.description });
this.meta.updateTag({ property: 'og:type', content: 'product' });
this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
}
}Because the page is server-rendered, these tags are present in the HTML response — crawlers and social media scrapers will see them immediately.
Dynamic SEO with Route Resolvers
For data-driven pages, set meta tags after fetching data using a resolver or within ngOnInit:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Title, Meta } from '@angular/platform-browser';
@Component({
selector: 'app-blog-post',
template: `<article><h1>{{ post.title }}</h1></article>`,
})
export class BlogPostComponent implements OnInit {
post: any;
constructor(
private route: ActivatedRoute,
private title: Title,
private meta: Meta,
) {}
ngOnInit() {
this.post = this.route.snapshot.data['post'];
this.title.setTitle(this.post.title);
this.meta.updateTag({ name: 'description', content: this.post.excerpt });
this.meta.updateTag({ property: 'og:title', content: this.post.title });
this.meta.updateTag({ property: 'og:description', content: this.post.excerpt });
this.meta.updateTag({ property: 'og:image', content: this.post.coverImage });
}
}Add Canonical URLs
Prevent duplicate content issues by setting canonical URLs:
import { DOCUMENT } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-seo',
template: '',
})
export class SeoComponent implements OnInit {
constructor(
@Inject(DOCUMENT) private doc: Document,
private router: Router,
) {}
ngOnInit() {
const link: HTMLLinkElement =
this.doc.querySelector('link[rel="canonical"]') ||
this.doc.createElement('link');
link.setAttribute('rel', 'canonical');
link.setAttribute('href', `https://yourdomain.com${this.router.url}`);
this.doc.head.appendChild(link);
}
}Generate a Sitemap
Create a sitemap so search engines discover all your pages. Add a server route in server.ts:
app.get('/sitemap.xml', (req, res) => {
const pages = ['/', '/about', '/blog', '/blog/post-1', '/blog/post-2'];
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmln="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages
.map(
(page) => `<url>
<loc>https://yourdomain.com${page}</loc>
<changefreq>weekly</changefreq>
</url>`,
)
.join('\n ')}
</urlset>`;
res.header('Content-Type', 'application/xml');
res.send(sitemap);
});Structured Data (JSON-LD)
Add structured data so search engines show rich results:
import { DOCUMENT } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
@Component({
selector: 'app-blog-post',
template: `<article><h1>{{ post.title }}</h1></article>`,
})
export class BlogPostComponent implements OnInit {
post = { title: 'SSR in Angular', author: 'Sabaoon', date: '2026-02-18' };
constructor(@Inject(DOCUMENT) private doc: Document) {}
ngOnInit() {
const script = this.doc.createElement('script');
script.type = 'application/ld+json';
script.text = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: this.post.title,
author: { '@type': 'Person', name: this.post.author },
datePublished: this.post.date,
});
this.doc.head.appendChild(script);
}
}SEO Checklist for Angular SSR
| Task | Why It Matters |
|---|---|
Set unique <title> per page | Primary ranking signal |
Add meta description | Shown in search results |
| Add Open Graph tags | Social media previews |
| Set canonical URLs | Prevents duplicate content penalties |
| Generate a sitemap | Helps crawlers discover pages |
| Add structured data (JSON-LD) | Enables rich snippets in search results |
| Enable hydration | Prevents layout shift, improves Core Web Vitals |
Use TransferState | Avoids double data fetching, faster TTI |
Step 7: Deploy
SSR requires a Node.js server, so you can't deploy to static hosting. Popular options:
- Vercel — auto-detects Angular SSR
- Netlify — supports Angular SSR via serverless functions
- Firebase App Hosting — first-party support for Angular SSR
- Docker — containerize the Node.js server
For Vercel, just push your code and it handles the rest. For Docker:
FROM node:20-alpine
WORKDIR /app
COPY dist/ ./dist/
COPY package.json ./
RUN npm install --production
EXPOSE 4000
CMD ["node", "dist/<your-project>/server/server.mjs"]
Common Pitfalls
- Third-party libraries using
window— Wrap them inisPlatformBrowserchecks or useafterNextRender - HTTP calls on the server — Use absolute URLs since the server doesn't know your domain. Use the
REQUESTtoken to get the current URL - State transfer — Use
TransferStateto avoid duplicate HTTP calls between server and client - Large bundle size — SSR doesn't fix this. Still optimize your bundles with lazy loading
Summary
Adding SSR with full SEO support to Angular is straightforward:
- Run
ng add @angular/ssr - Guard browser-only APIs
- Enable hydration
- Set dynamic titles, meta tags, and Open Graph tags per page
- Add canonical URLs, a sitemap, and structured data
- Build and deploy to a Node.js-capable host
Your Angular app now loads faster, ranks better in search engines, shows rich previews on social media, and provides a better experience for all users.