Why SEO in Angular is different

Angular is an SPA (Single Page Application) framework. By default, the server sends an empty index.html with a JavaScript bundle that builds the page in the browser. This presents two problems for SEO:

  1. Google renders JavaScript, but not always completely or quickly. Other search engines (Bing, DuckDuckGo) have limited support
  2. Social media crawlers (Facebook, Twitter, LinkedIn) don't execute JavaScript. Without SSR, your Open Graph tags are invisible

The solution is Server-Side Rendering (SSR): the server generates the complete HTML and sends it to the browser. Angular 21 has built-in SSR and configuring it is easier than ever.

Enabling SSR in Angular 21

If your project doesn't have SSR, add it with:

ng add @angular/ssr

This generates:

  • server.ts: The Express server
  • src/app/app.config.server.ts: Server configuration
  • Updates in angular.json for SSR build

Verify it works

ng serve
# Open view-source:http://localhost:4200
# You should see rendered HTML, not an empty <app-root>

The SeoService: your central tool

Centralize all SEO logic in a service. The first code block shows the full implementation.

How to use it in components

@Component({
  selector: 'app-blog-post',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<!-- ... -->`,
})
export class BlogPostComponent {
  private readonly seo = inject(SeoService);
  private readonly route = inject(ActivatedRoute);

  constructor() {
    effect(() => {
      const post = this.post();
      if (post) {
        this.seo.updateSeo({
          title: post.title,
          description: post.excerpt,
          url: `/blog/${post.slug}`,
          image: post.coverImage,
          type: 'article',
          publishedAt: post.publishedAt,
          updatedAt: post.updatedAt,
          author: post.author,
          tags: post.tags,
        });
      }
    });
  }
}

Essential meta tags

Every page should have at minimum:

Tag Purpose
<title> Title in search results
meta description Description below the title
og:title Title on social media
og:description Description on social media
og:image Image when sharing (1200x630px)
og:url Canonical URL
canonical Prevents duplicate content

Structured Data with JSON-LD

Structured data helps Google understand the type of content on your page and display rich snippets in search results.

The second code block shows a service that injects JSON-LD for articles and courses.

Rich snippets you can get

  • Articles: Date, author, image in results
  • Courses: Provider, level, price in results
  • FAQ: Expandable questions and answers
  • Breadcrumb: Hierarchical navigation
  • Organization: Logo and company data
addBreadcrumb(items: Array<{ name: string; url: string }>): void {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: items.map((item, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      name: item.name,
      item: `https://xbemorelearn.web.app${item.url}`,
    })),
  };

  this.injectSchema(schema);
}

SEO in routes

You can use Angular Router resolvers to update SEO before the component loads. The third code block shows this pattern.

For static routes

Use the resolver directly with fixed data:

{
  path: 'cursos',
  resolve: {
    seo: () => {
      inject(SeoService).updateSeo({
        title: 'Cursos de desarrollo web',
        description: 'Cursos gratuitos de Angular, TypeScript, CSS y más.',
        url: '/cursos',
      });
      return true;
    },
  },
}

For dynamic routes

SEO is updated in the component when data is available:

{
  path: ':slug',
  loadComponent: () => import('./course-detail')
    .then(m => m.CourseDetailComponent),
  // The component updates SEO with course data
}

Sitemap and robots.txt

robots.txt

Create src/robots.txt and add it to assets in angular.json:

User-agent: *
Allow: /

Sitemap: https://xbemorelearn.web.app/sitemap.xml

Static sitemap

For apps with static or pre-rendered content, generate a sitemap at build time:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://xbemorelearn.web.app/</loc>
    <lastmod>2026-03-06</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://xbemorelearn.web.app/blog</loc>
    <lastmod>2026-03-06</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.9</priority>
  </url>
  <url>
    <loc>https://xbemorelearn.web.app/cursos</loc>
    <lastmod>2026-03-06</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.9</priority>
  </url>
</urlset>

Dynamic sitemap

To generate the sitemap automatically with each build, create a prebuild script:

import { writeFileSync } from 'fs';
import { BLOG_POSTS } from '../src/app/data/blog/posts.data';
import { ALL_COURSES } from '../src/app/data/courses';

const baseUrl = 'https://xbemorelearn.web.app';

const staticPages = [
  { url: '/', priority: '1.0', changefreq: 'weekly' },
  { url: '/blog', priority: '0.9', changefreq: 'daily' },
  { url: '/cursos', priority: '0.9', changefreq: 'weekly' },
  { url: '/rutas', priority: '0.8', changefreq: 'monthly' },
];

const blogPages = BLOG_POSTS.map(post => ({
  url: `/blog/${post.slug}`,
  priority: '0.7',
  changefreq: 'monthly' as const,
  lastmod: post.updatedAt,
}));

const coursePages = ALL_COURSES.map(course => ({
  url: `/cursos/${course.slug}`,
  priority: '0.8',
  changefreq: 'monthly' as const,
}));

const allPages = [...staticPages, ...blogPages, ...coursePages];

const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${allPages.map(page => `  <url>
    <loc>${baseUrl}${page.url}</loc>
    <changefreq>${page.changefreq}</changefreq>
    <priority>${page.priority}</priority>
  </url>`).join('\n')}
</urlset>`;

writeFileSync('src/sitemap.xml', sitemap);

Pre-rendering static routes

Angular can pre-render routes at build time, generating static HTML files:

// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'blog', renderMode: RenderMode.Prerender },
  { path: 'cursos', renderMode: RenderMode.Prerender },
  { path: 'blog/:slug', renderMode: RenderMode.Prerender },
  { path: 'cursos/:slug', renderMode: RenderMode.Prerender },
  { path: '**', renderMode: RenderMode.Server },
];

Pre-rendered routes are pure static HTML: they don't need a server and are served instantly from CDN. It's the best option for content that doesn't change frequently.

Performance as an SEO factor

Google uses Core Web Vitals as a ranking factor:

Largest Contentful Paint (LCP)

The largest visible element should load in less than 2.5 seconds:

  • Pre-render critical pages
  • Optimize images (WebP/AVIF, lazy loading)
  • Minimize blocking CSS and JS

Cumulative Layout Shift (CLS)

Content should not shift after loading:

  • Set dimensions on images (width and height)
  • Use NgOptimizedImage which handles this automatically
  • Reserve space for asynchronous content

Interaction to Next Paint (INP)

Interactions should respond in less than 200ms:

  • Use OnPush change detection
  • Avoid heavy computations on the main thread
  • Use signals for granular updates

Auditing your SEO

Free tools

  • Google Search Console: Real data on how Google sees your site
  • Lighthouse: Technical audit integrated in Chrome
  • Schema.org Validator: Validate your structured data
  • Open Graph Debugger: Facebook Sharing Debugger
  • Twitter Card Validator: Preview Twitter Cards

SEO checklist for each page

  • Unique and descriptive title (50-60 characters)
  • Unique meta description (150-160 characters)
  • Clean and descriptive URL
  • Complete Open Graph tags
  • Canonical URL configured
  • JSON-LD structured data
  • Images with alt text
  • Correct heading hierarchy (a single H1)
  • Above-the-fold content visible without JavaScript
  • Load time under 3 seconds

Conclusion

SEO in Angular with SSR is not black magic. It's a set of well-defined technical practices: dynamic meta tags, structured data, sitemap, pre-rendering, and performance. The centralized SeoService gives you a single point to manage all of this consistently.

The investment in SEO has compounding returns: every well-optimized page attracts organic traffic that grows over time. For a learning platform like Bemore Learn, this means more students discovering the content without spending on advertising.