Angular SSR Setup for SEO: Step by Step
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.
- Free version: GitHub
- Pro ($249): Get SaaS Starter Pro