Angular SEO: Fix the Empty Shell with SSR and Prerendering

Angular SEO guide covering Angular Universal (@angular/ssr), SSR, static prerendering, Title and Meta services, sitemaps, and canonical tags for better search rankings.

A default Angular app ships as a near-empty HTML document. The browser downloads JavaScript, executes it, and then renders the page. For users on a fast connection that’s fine. For search crawlers, and especially for AI crawlers that often skip JavaScript execution entirely, the page looks blank. That is the core Angular SEO problem, and it has a straightforward fix.

This guide covers the two rendering strategies that solve it (SSR and prerendering via Angular Universal), how to set per-route titles and meta descriptions using Angular’s built-in services, and the supporting details that round out a properly indexed Angular site.

Why a default Angular app struggles with SEO

A default Angular build produces client-side rendered (CSR) output: one index.html with minimal content and a bundle of JavaScript that builds the DOM at runtime. Googlebot can render JavaScript, but it does so asynchronously, which means there is often a delay between when a page is crawled and when its content is indexed. Other crawlers, including the ones feeding ChatGPT, Perplexity, and Google’s AI Overviews, frequently do not render JavaScript at all. They see the empty shell and move on.

The consequences are predictable: thin or missing indexed content, no per-page titles or descriptions in search results, and no brand mentions in AI-generated answers. The single-page application SEO problem is well documented across frameworks. Angular is no exception. The fix is to get real HTML into the initial server response before any JavaScript runs.

SSR and prerendering with Angular Universal

Angular Universal is the server-side rendering solution for Angular. Since Angular 17, it ships as @angular/ssr and integrates directly into the Angular CLI, so you no longer need a separate package or complex setup.

@angular/ssr gives you three rendering modes in one package: SSR (server-rendered on request), static prerendering (SSG, rendered at build time), and partial hydration. Choosing between them depends on whether your content changes frequently (SSR) or is mostly static (prerendering). Most marketing sites, documentation, and blog-style Angular apps benefit from prerendering.

Adding @angular/ssr to an existing project

ng add @angular/ssr

That single command modifies angular.json, adds a server entry point (server.ts), and configures the build pipeline. After running it, ng build produces both a browser bundle and a server bundle. ng serve stays unchanged for local development.

Prerendering specific routes

By default, Angular’s build process discovers routes automatically by following RouterModule declarations. You can also specify routes explicitly in angular.json:

{
  "prerender": {
    "discoverRoutes": true,
    "routesFile": "routes.txt"
  }
}

Where routes.txt lists one path per line:

/
/about
/features
/pricing
/blog/post-one
/blog/post-two

Each listed route gets its own index.html in the build output, served as static HTML with no JavaScript required to display the initial content.

Setting page titles and meta descriptions

Every page needs a unique <title> and <meta name="description">. In a CSR Angular app these often get forgotten entirely, or set once at the root and never changed per route. Angular provides two purpose-built services and a router-level shortcut.

The Title and Meta services

Angular’s Title service (from @angular/platform-browser) sets the document title. The Meta service manages meta tags. Use them in a component’s constructor or ngOnInit:

import { Component, OnInit } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';

@Component({
  selector: 'app-features',
  templateUrl: './features.component.html'
})
export class FeaturesComponent implements OnInit {
  constructor(private title: Title, private meta: Meta) {}

  ngOnInit(): void {
    this.title.setTitle('Features - YourApp');
    this.meta.updateTag({
      name: 'description',
      content: 'Explore all the features YourApp offers. Built for teams who need fast, reliable tooling.'
    });
  }
}

updateTag is safer than addTag because it replaces an existing tag rather than duplicating it when navigating between routes.

Route-level titles via the Router

For simple titles that don’t depend on async data, Angular’s Router supports a title property directly on the route definition:

const routes: Routes = [
  {
    path: 'about',
    component: AboutComponent,
    title: 'About Us - YourApp'
  },
  {
    path: 'pricing',
    component: PricingComponent,
    title: 'Pricing - YourApp'
  }
];

The Router sets the title automatically on navigation. For data-driven titles (where the title includes a fetched product name or post title), use a custom TitleStrategy or set the title in the component after the data resolves.

Canonical tags

Duplicate content is a common Angular issue because query parameters often create multiple URLs for the same view. Set a canonical tag on every page to tell crawlers which URL is authoritative:

this.meta.updateTag({
  rel: 'canonical',
  href: 'https://yourdomain.com/current-path'
});

Alternatively, include a <link rel="canonical"> in your index.html template and update it server-side in your SSR setup for each request. Either approach works. The important thing is that every rendered page has exactly one canonical pointing to the clean, preferred URL.

Angular’s <a routerLink="/path"> renders as a standard <a href="/path"> in the HTML output. Crawlers follow href attributes, so this is crawler-compatible by default. Two things to watch:

Don’t use click handlers for navigation. (click)="navigate()" with router.navigate() produces no <a href> in the DOM. Crawlers cannot follow it. Use routerLink instead.

Lazy-loaded modules still need prerendering. If a feature module is lazy-loaded, its routes are not rendered until that module loads. Angular’s route discovery handles this during the prerender build, but if you’re manually specifying routes in routes.txt, include the lazy-loaded paths explicitly.

Sitemaps

Angular doesn’t generate a sitemap automatically. You have two options:

Static sitemap. For a prerendered site with a known set of URLs, maintain a sitemap.xml in src/assets/ and reference it in robots.txt. Copy it to the build output as part of your deployment pipeline.

Build-time generation. For larger sites, generate the sitemap during the build by scripting against your routes list or content source. Node scripts that write sitemap.xml to the dist folder work well here and can run as a post-build step in your CI pipeline.

Submit the sitemap to Google Search Console and link to it from robots.txt:

User-agent: *
Allow: /
Sitemap: https://yourdomain.com/sitemap.xml

Structured data

For Angular apps serving product, article, or organisation content, JSON-LD structured data helps search engines and AI systems understand what a page is about. Add it to the <head> via Angular’s Meta service or inject a <script type="application/ld+json"> block directly in the SSR render:

// In your SSR server entry or a per-component approach
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'SoftwareApplication',
  name: 'YourApp',
  applicationCategory: 'BusinessApplication',
  url: 'https://yourdomain.com'
};

// Inject as a script tag in the server-rendered HTML

A cleaner pattern for SSR is to use Angular’s DOCUMENT injection token to append the script element to the <head> during server rendering. The tag is then included in the initial HTML response, which is what crawlers read.

Angular SEO and AI citation

Getting cited in AI search answers, not just ranking on Google, is increasingly where organic visibility happens. AI systems like ChatGPT, Perplexity, and Google AI Overviews tend to pull from pages that have clear structure: descriptive headings, concise prose, and facts that can be extracted without running JavaScript. A prerendered Angular app with proper <title>, meta description, and structured data is much more likely to get picked up than a CSR app that returns an empty shell to the crawler.

The AI SEO guide covers citation mechanics in detail. The short version for Angular: make sure the content that answers user questions exists in the initial server response. If it only appears after a user interaction or after an API call completes client-side, AI crawlers will never see it.

Tools like Fokal track how an Angular site shows up in AI engine results over time, running target queries on a schedule so you can see whether SSR or content changes are actually improving citation rate.

Common Angular SEO mistakes

MistakeEffectFix
No SSR or prerenderingCrawlers see empty shellAdd @angular/ssr, enable prerendering for static routes
Single <title> set at app rootEvery page shows the same titleSet per-route titles with Title service or Router title property
(click) navigation instead of routerLinkCrawlers can’t follow linksReplace with routerLink on <a> elements
No canonical tagsDuplicate content from query paramsAdd canonical via Meta service on every route
Missing <meta name="description">No description in search resultsSet with Meta.updateTag in every route component
No sitemapImportant pages not discoveredGenerate and submit a sitemap.xml

What to do next

If your Angular app is currently CSR-only, the highest-impact change is adding @angular/ssr and enabling prerendering for your key routes. That one step gets real HTML in front of crawlers immediately.

After that, work through the checklist below. Most items take under an hour each once SSR is running.

Angular SEO checklist:

  • Run ng add @angular/ssr and confirm prerender output includes all key routes
  • Set a unique <title> on every route using Title service or Router title property
  • Add a <meta name="description"> to every route via Meta.updateTag
  • Add canonical tags to every route
  • Replace any (click)-based navigation with routerLink
  • Generate and submit a sitemap.xml to Google Search Console
  • Add JSON-LD structured data for your primary page types
  • Test the rendered HTML (not the browser view) with curl or Google’s URL Inspection tool to confirm content is in the initial response

For the broader platform SEO picture across frameworks, see the JavaScript SEO guide which covers how different rendering strategies affect crawlability. If you’re evaluating Angular against other frameworks for SEO, the single-page application SEO guide compares the tradeoffs directly.

Eight minutes to something you can ship.