Skip to content
← Back to blog
AngularSSRSEOPerformance

Angular SSR Setup for SEO: Step by Step

Firas Sayah·March 11, 2026·5 min read

Why SSR Matters for SaaS

Your SaaS landing pages, blog, and documentation need to rank in search engines. Single-page applications render content in the browser, which means search crawlers may see an empty page. Server-Side Rendering (SSR) solves this by generating HTML on the server before sending it to the client.

Adding SSR to an Existing Angular Project

Angular makes SSR straightforward with the official package:

ng add @angular/ssr

This command adds the server entry point, configures the build, and creates the necessary server files. For Nx workspaces:

npx nx g @angular/ssr:ng-add --project=web

Configuring the Server

The generated server.ts uses Express to serve your Angular application:

import { CommonEngine } from '@angular/ssr/node';
import express from 'express';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import bootstrap from './main.server';

const app = express();
const serverDir = dirname(fileURLToPath(import.meta.url));
const browserDir = join(serverDir, '../browser');
const engine = new CommonEngine();

app.get('*', (req, res, next) => {
  engine.render({
    bootstrap,
    documentFilePath: join(browserDir, 'index.html'),
    url: req.originalUrl,
    publicPath: browserDir,
  })
  .then(html => res.send(html))
  .catch(next);
});

Dynamic Meta Tags

Each page needs unique meta tags for SEO. Create a service that updates them:

@Injectable({ providedIn: 'root' })
export class SeoService {
  private meta = inject(Meta);
  private title = inject(Title);

  updateTags(config: SeoConfig) {
    this.title.setTitle(config.title);
    this.meta.updateTag({ name: 'description', content: config.description });
    this.meta.updateTag({ property: 'og:title', content: config.title });
    this.meta.updateTag({ property: 'og:description', content: config.description });
    this.meta.updateTag({ property: 'og:image', content: config.image });
    this.meta.updateTag({ property: 'og:url', content: config.url });
    this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
  }
}

Use it in your route components:

Cloudrix SaaS Starter ships with this pre-configured and tested.

Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.

Try the live demo →
@Component({ ... })
export class PricingPageComponent implements OnInit {
  private seo = inject(SeoService);

  ngOnInit() {
    this.seo.updateTags({
      title: 'Pricing - YourSaaS',
      description: 'Simple, transparent pricing for teams of all sizes.',
      image: 'https://yoursaas.com/og-pricing.png',
      url: 'https://yoursaas.com/pricing',
    });
  }
}

Structured Data for Rich Snippets

Add JSON-LD structured data to help search engines understand your content:

@Injectable({ providedIn: 'root' })
export class StructuredDataService {
  private doc = inject(DOCUMENT);

  setArticleSchema(article: { title: string; author: string; date: string; description: string }) {
    const schema = {
      '@context': 'https://schema.org',
      '@type': 'Article',
      headline: article.title,
      author: { '@type': 'Person', name: article.author },
      datePublished: article.date,
      description: article.description,
    };
    const script = this.doc.createElement('script');
    script.type = 'application/ld+json';
    script.textContent = JSON.stringify(schema);
    this.doc.head.appendChild(script);
  }
}

Pre-Rendering Static Pages

For pages that rarely change (landing, pricing, about), pre-render them at build time:

// app.config.server.ts
export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'pricing', renderMode: RenderMode.Prerender },
  { path: 'about', renderMode: RenderMode.Prerender },
  { path: 'blog/:slug', renderMode: RenderMode.Server },
  { path: '**', renderMode: RenderMode.Client },
];

Pre-rendered pages are generated once during the build and served as static HTML. This gives you the fastest possible time-to-first-byte.

Handling Platform-Specific Code

Some browser APIs are not available on the server. Use isPlatformBrowser to guard them:

import { isPlatformBrowser, PLATFORM_ID } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class AnalyticsService {
  private platformId = inject(PLATFORM_ID);

  trackPageView(url: string) {
    if (isPlatformBrowser(this.platformId)) {
      window.gtag('event', 'page_view', { page_path: url });
    }
  }
}

Canonical URLs

Prevent duplicate content issues with canonical tags:

setCanonicalUrl(url: string) {
  let link: HTMLLinkElement = this.doc.querySelector('link[rel="canonical"]');
  if (!link) {
    link = this.doc.createElement('link');
    link.setAttribute('rel', 'canonical');
    this.doc.head.appendChild(link);
  }
  link.setAttribute('href', url);
}

Performance Monitoring

After deploying SSR, monitor your Core Web Vitals. The key metrics are Largest Contentful Paint (under 2.5 seconds), First Input Delay (under 100 milliseconds), and Cumulative Layout Shift (under 0.1). SSR typically improves LCP dramatically since the browser receives pre-rendered HTML.

Get Started Today

All of these patterns are fully implemented in SaaS Starter. Skip 2-6 weeks of setup.

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.