BAY logo
October 9, 2025
7 min read
8 views

Angular SEO: How to Make Your App Shine on Google

Getting good SEO with Angular used to be challenging. Search engines didn’t always handle JavaScript-heavy apps well, and developers had to find creative ways to get pages indexed. Fortunately, modern Angular offers robust tools like Angular Universal, pre-rendering, and flexible meta management. With these, your Angular apps can now perform well in both speed and SEO. This guide breaks down everything you need to know in a clear and practical way. Whether you’re optimizing a small personal project or a large enterprise application, these techniques will help your app rank better and load faster.

1. SSR vs. Prerendering (SSG): What’s the Difference?

Both Server-Side Rendering (SSR) and Pre-rendering (SSG) improve how search engines see your app, but they serve different purposes.

SSR (Server-Side Rendering)

  • When to use: For pages that change often or show user-specific content.

  • How it works: The server generates the page for every request, delivering fully-rendered HTML to the user and crawler.

  • Best for: Product pages, dashboards, or social feeds.

Prerendering (Static Site Generation – SSG)

  • When to use: For pages that rarely change.

  • How it works: HTML pages are generated during the build process and served directly from a CDN.

  • Best for: Landing pages, about pages, blogs, or documentation.

Pro Tip: You can use both. SSR for dynamic content and prerendering for static content provides the best balance of performance and cost.

2. Setting Up SSR in Angular

SSR improves SEO by letting crawlers read pre-rendered HTML. Setting it up is straightforward:

ng add @nguniversal/express-engine

Then add scripts to your package.json:

"scripts": {
  "build:ssr": "ng build && ng run my-app:server",
  "serve:ssr": "node dist/my-app/server/main.js"
}

Deploy this build to a Node-compatible environment like Firebase, Vercel, or AWS. Starting with Angular 17, hydration makes SSR even smoother by enabling instant interactivity.

3. Dynamic Meta Tags That Update Automatically

Instead of setting meta tags manually in each component, you can automate updates based on route data.

Example:

import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { Title, Meta } from '@angular/platform-browser';
import { filter, map, mergeMap } from 'rxjs/operators';

constructor(private router: Router, private route: ActivatedRoute, private title: Title, private meta: Meta) {}

ngOnInit() {
  this.router.events.pipe(
    filter(event => event instanceof NavigationEnd),
    map(() => this.route),
    map(route => {
      while (route.firstChild) route = route.firstChild;
      return route;
    }),
    mergeMap(route => route.data)
  ).subscribe(data => {
    this.title.setTitle(data['title'] || 'My Angular App');
    this.meta.updateTag({ name: 'description', content: data['description'] || 'An example of Angular SEO optimization.' });
  });
}

This ensures your meta tags and titles update dynamically with route changes.

4. Canonical Links and Multi-Language Support

Canonical tags help search engines identify the main version of a page, avoiding duplicate content issues.

import { DOCUMENT } from '@angular/common';
import { Inject } from '@angular/core';

constructor(@Inject(DOCUMENT) private doc: Document) {}

addCanonical(url?: string) {
  const link: HTMLLinkElement = this.doc.createElement('link');
  link.setAttribute('rel', 'canonical');
  link.setAttribute('href', url ?? this.doc.URL);
  this.doc.head.appendChild(link);
}

If your app has multiple languages, use <link rel="alternate" hreflang="..."> for each localized version.

5. Pre-render Static Pages for Speed

Pre-rendering is ideal for pages that don’t need to change frequently. Angular provides a built-in way to do it:

ng run my-app:prerender

This creates static HTML files for your routes that load instantly from a CDN. It’s a simple way to improve both performance and crawlability.

6. Automate Your Sitemap

Manually writing a sitemap is inefficient. Instead, automate it using a package like sitemap-generator-cli:

npx sitemap-generator https://yourdomain.com -o dist/browser/sitemap.xml

Include a robots.txt file so crawlers can find it:

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

Custom Sitemap in Angular SSR (Express)

If you’re running Angular Universal with the Express engine, you can generate a dynamic sitemap on the server at request time. This is useful when URLs come from a CMS, database, or API (e.g., blog posts, product pages) and change frequently.

Add a /sitemap.xml endpoint to your server.ts before the Angular Universal engine middleware:

// server.ts (or main.server.ts depending on your setup)
import 'zone.js/node';
import express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';

// Example: fetch URLs from your data layer (CMS, DB, API)
async function getDynamicUrls(): Promise<Array<{ path: string; lastmod?: string; changefreq?: string; priority?: number }>> {
  // Replace with real data calls
  const posts = [
    { slug: 'hello-angular-seo', updatedAt: '2025-09-20' },
    { slug: 'universal-ssr-guide', updatedAt: '2025-10-01' }
  ];
  return posts.map(p => ({
    path: `/blog/${p.slug}`,
    lastmod: p.updatedAt,
    changefreq: 'weekly',
    priority: 0.7
  }));
}

function xmlEscape(s: string) {
  return s.replace(/[&<>"]+/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]!));
}

function buildSitemapXml(baseUrl: string, urls: Array<{ path: string; lastmod?: string; changefreq?: string; priority?: number }>) {
  const items = urls.map(u => {
    const loc = `${baseUrl}${u.path}`;
    const lastmod = u.lastmod ? `<lastmod>${u.lastmod}</lastmod>` : '';
    const changefreq = u.changefreq ? `<changefreq>${u.changefreq}</changefreq>` : '';
    const priority = typeof u.priority === 'number' ? `<priority>${u.priority.toFixed(1)}</priority>` : '';
    return `<url><loc>${xmlEscape(loc)}</loc>${lastmod}${changefreq}${priority}</url>`;
  }).join('');

  return `<?xml version="1.0" encoding="UTF-8"?>
` +
         `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${items}</urlset>`;
}

export function app(): express.Express {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  // --- Sitemap route (dynamic) ---
  server.get('/sitemap.xml', async (req, res) => {
    const baseUrl = `${req.protocol}://${req.get('host')}`; // handles http/https

    // Static routes you always want in the sitemap
    const staticRoutes = [
      { path: '/', changefreq: 'daily', priority: 1.0 },
      { path: '/about', changefreq: 'monthly', priority: 0.5 },
      { path: '/contact', changefreq: 'monthly', priority: 0.5 }
    ];

    // Merge static + dynamic routes
    const dynamicRoutes = await getDynamicUrls();
    const urls = [...staticRoutes, ...dynamicRoutes];

    const xml = buildSitemapXml(baseUrl, urls);
    res.setHeader('Content-Type', 'application/xml');
    res.setHeader('Cache-Control', 'public, max-age=3600'); // cache 1 hour
    res.send(xml);
  });

  // (your existing static file hosting & Angular Universal engine come after this)
  // server.use(...)
  // server.get('*', (req, res) => {
  //   res.render(indexHtml, { req });
  // });

  return server;
}

Notes and options

  • If you have localized routes, generate one <url> per locale or output a sitemap index with one file per language.

  • For very large sites, split your sitemap into multiple files (max 50,000 URLs per file) and serve a sitemap-index.xml that references each file.

  • If you use a CDN in front of SSR, keep the Cache-Control header and also configure edge caching with a suitable TTL.

Finally, point to this dynamic sitemap from robots.txt:

Sitemap: https://yourdomain.com/sitemap.xml

7. Boost Performance and Core Web Vitals

Performance is a key factor in SEO. Focus on these optimizations:

  • Use lazy loading for feature modules.

  • Enable HTTP caching and gzip/Brotli compression.

  • Use Angular CLI budgets to monitor bundle size.

  • Test your app with Lighthouse and PageSpeed Insights.

  • Use ngOptimizedImage to improve image loading and reduce LCP issues.

8. Structured Data (Schema Markup)

Structured data enhances how your site appears in search results by adding extra context for search engines.

const schema = {
  '@context': 'https://schema.org',
  '@type': 'WebPage',
  'name': 'Angular SEO Implementation Guide',
  'description': 'An SEO optimization guide for Angular developers.'
};

const script = document.createElement('script');
script.type = 'application/ld+json';
script.text = JSON.stringify(schema);
document.head.appendChild(script);

9. Test and Validate

Before finishing, make sure everything works properly:

  • Use Google Search Console to check indexing.

  • Validate structured data with the Rich Results Test.

  • Use Fetch as Google to confirm your rendered pages include meta tags.

Conclusion

Modern Angular makes SEO much easier than before. Use SSR for dynamic pages, prerendering for static ones, dynamic meta tags for automation, and optimization tools for speed. Combine that with sitemaps and structured data, and your app will be ready to compete in search rankings.

References

8 views
1 likes

Keywords

angular seoangular universalserver-side renderingssrpre-renderingssgstatic site generationmeta tagsdynamic meta tagsangular title serviceangular meta servicecanonical taghreflangalternate tagsitemap generationrobots.txtcore web vitalspage speedlighthousepagespeed insightsngOptimizedImagestructured dataschema markupgoogle search consolerich results testangular performancelazy loadingangular 17 seojavascript seofrontend seowebsite optimizationsearch engine optimizationangular development