Skip to main content

How to Add Server-Side Rendering (SSR) in Angular

February 18, 2026

</>

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/ssr

This command will:

  • Install @angular/ssr and express
  • Create a server.ts file in your project root
  • Update angular.json with server build configuration
  • Update app.config.ts to 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 build

This 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

TaskWhy It Matters
Set unique <title> per pagePrimary ranking signal
Add meta descriptionShown in search results
Add Open Graph tagsSocial media previews
Set canonical URLsPrevents duplicate content penalties
Generate a sitemapHelps crawlers discover pages
Add structured data (JSON-LD)Enables rich snippets in search results
Enable hydrationPrevents layout shift, improves Core Web Vitals
Use TransferStateAvoids 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

  1. Third-party libraries using window — Wrap them in isPlatformBrowser checks or use afterNextRender
  2. HTTP calls on the server — Use absolute URLs since the server doesn't know your domain. Use the REQUEST token to get the current URL
  3. State transfer — Use TransferState to avoid duplicate HTTP calls between server and client
  4. 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:

  1. Run ng add @angular/ssr
  2. Guard browser-only APIs
  3. Enable hydration
  4. Set dynamic titles, meta tags, and Open Graph tags per page
  5. Add canonical URLs, a sitemap, and structured data
  6. 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.

Recommended Posts