Référencement & SEO angularforall.com

- SEO Angular SPA : SSR et Prerendering guide

Seo-Angular Ssr-Angular Prerendering Angular-Universal Spa-Seo Core-Web-Vitals Angular Seo-Technique Indexation Seo
SEO Angular SPA : SSR et Prerendering guide

Maîtrisez le SEO Angular avec SSR (Angular Universal) et prerendering. Stratégies d'indexation pour SPA, CSR vs SSR vs SSG, meta tags dynamiques et CWV.

1. Pourquoi les SPA Angular posent-ils un problème SEO ?

Une SPA Angular (Single Page Application) génère son HTML côté client (CSR — Client-Side Rendering) via JavaScript. Quand Googlebot visite la page, il reçoit d'abord un document HTML quasi-vide, puis doit exécuter JavaScript avant de voir le contenu réel.

Google indexe les SPA Angular avec un délai (deferred indexing). Le contenu JS est crawlé, mais avec une latence de quelques jours à semaines — pénalisant les mises à jour fraîches.

Comment Googlebot traite-t-il le JavaScript ?

Googlebot utilise un moteur Chromium pour rendre le JavaScript, mais ce processus est asynchrone et différé. Selon Google, le rendu JS peut prendre jusqu'à plusieurs jours après le crawl initial du HTML brut.

Étape Googlebot Sans SSR (CSR) Avec SSR
1. Crawl HTML brut HTML vide (app-root uniquement) HTML complet avec contenu
2. Indexation provisoire Page vide ou titre générique Contenu complet indexé
3. Queue JS rendering Attente file d'exécution JS (délai 3-14j) Pas nécessaire (HTML déjà complet)
4. Re-indexation Mise à jour différée après rendu JS Indexation immédiate et précise
LCP mesuré 3-5 secondes (attente JS) 0.8-1.5 secondes (HTML prêt)

Symptômes d'un mauvais SEO Angular

  • Pages non indexées — Google Search Console affiche des erreurs "Crawled but not indexed"
  • Titres génériques — Googlebot affiche le titre du composant racine, pas des pages enfants
  • Meta descriptions manquantes — aucun <meta name="description"> visible au crawl initial
  • LCP élevé — Core Web Vitals dégradés à cause du temps de chargement JS
  • Partage social cassé — Facebook/Twitter ne voient que le HTML vide (Open Graph manquant)
  • Sitemaps mal exploités — les URLs Angular avec #/route (hash routing) sont ignorées par Google
Les URLs avec # (hash fragment) ne sont JAMAIS indexées par Google. Migrez vers le PathLocationStrategy (sans #) avant toute stratégie SSR.

2. CSR vs SSR vs SSG vs Prerendering : quel rendu choisir pour le SEO ?

Angular 17+ supporte nativement 4 modes de rendu. Le choix dépend du type de contenu : dynamique, semi-statique ou statique.

Mode Génération HTML SEO Cas d'usage Complexité
CSR (Client-Side Rendering) Navigateur (JS) ⚠️ Faible Dashboards privés, apps connectées Simple
SSR (Server-Side Rendering) Serveur (Node.js) ✅ Excellent E-commerce, actualités, contenu dynamique Moyenne
SSG (Static Site Generation) Build time ✅ Excellent Blogs, docs, landing pages fixes Simple
Prerendering Build time (routes ciblées) ✅ Très bon Routes connues statiques + SPA hybride Moyenne
Hybride (Angular 17+) Mix SSR + SSG par route ✅ Optimal Sites complexes multi-typologies Élevée

Recommandations par type de projet Angular

  • Blog / Documentation → SSG ou Prerendering (contenu statique, build rapide)
  • E-commerce → SSR avec cache (pages produits dynamiques, stock en temps réel)
  • SaaS / Dashboard → CSR pour les pages privées + SSR/prerendering pour les pages publiques (landing, pricing)
  • Site vitrine → Prerendering (toutes routes connues au build, TTFB optimal)
  • Application hybride → Rendu hybride Angular 17 (par route : SSR ou SSG selon le contenu)

3. Comment implémenter SSR dans Angular 17+ avec @angular/ssr ?

Angular 17 a unifié Angular Universal sous @angular/ssr. L'ajout du SSR à un projet existant se fait en une commande, et la configuration est nativement intégrée.

Installation et configuration SSR

# Ajouter SSR à un projet Angular existant
ng add @angular/ssr

# Structure générée automatiquement :
# - server.ts           → serveur Express + gestionnaire de requêtes
# - app.config.server.ts → configuration spécifique serveur
# - angular.json        → target "server" ajoutée

# Build SSR
ng build --configuration=production

# Lancer le serveur SSR en prod
node dist/monapp/server/server.mjs

Configuration server.ts (Angular 17+)

// server.ts — généré par ng add @angular/ssr
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';

export function app(): express.Express {
  const server = express();
  const serverDistFolder = dirname(fileURLToPath(import.meta.url));
  const browserDistFolder = resolve(serverDistFolder, '../browser');
  const indexHtml = join(serverDistFolder, 'index.server.html');

  const commonEngine = new CommonEngine();

  // Servir les assets statiques
  server.get('*.*', express.static(browserDistFolder, { maxAge: '1y' }));

  // Toutes les routes → SSR
  server.get('*', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;

    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
  });

  return server;
}

// Démarrer le serveur
function run(): void {
  const port = process.env['PORT'] || 4000;
  const server = app();
  server.listen(port, () => {
    console.log(`SSR server running on http://localhost:${port}`);
  });
}

run();

Gérer les APIs Browser dans SSR (window, localStorage, document)

// Injecter PLATFORM_ID pour détecter SSR vs Browser
import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Component({ selector: 'app-root', templateUrl: './app.component.html' })
export class AppComponent {
  constructor(@Inject(PLATFORM_ID) private platformId: object) {
    if (isPlatformBrowser(this.platformId)) {
      // Code spécifique navigateur uniquement
      const user = localStorage.getItem('user');
      window.scrollTo(0, 0);
    }

    if (isPlatformServer(this.platformId)) {
      // Code spécifique serveur SSR
      // Pré-charger données pour SEO
    }
  }
}

// Alternative : utiliser TransferState pour partager données SSR→Browser
import { TransferState, makeStateKey } from '@angular/platform-browser';

const ARTICLES_KEY = makeStateKey<any[]>('articles');

@Injectable()
export class ArticlesService {
  constructor(
    private http: HttpClient,
    private transferState: TransferState,
    @Inject(PLATFORM_ID) private platformId: object
  ) {}

  getArticles(): Observable<any[]> {
    if (isPlatformServer(this.platformId)) {
      // SSR : fetch HTTP + stocker dans TransferState
      return this.http.get<any[]>('/api/articles').pipe(
        tap(data => this.transferState.set(ARTICLES_KEY, data))
      );
    }

    // Browser : récupérer depuis TransferState (évite double requête)
    const cached = this.transferState.get<any[]>(ARTICLES_KEY, []);
    this.transferState.remove(ARTICLES_KEY);
    return cached.length ? of(cached) : this.http.get<any[]>('/api/articles');
  }
}
TransferState est critique pour la performance SSR : sans lui, l'application fait les requêtes HTTP deux fois — une sur le serveur (SSR) et une dans le navigateur (hydratation). Cela génère des requêtes inutiles et peut causer des flickering visuels.

4. Prerendering Angular : générer des pages statiques au build

Le prerendering génère des fichiers HTML statiques pour des routes définies au moment du build. C'est l'option la plus simple pour améliorer le SEO sans infrastructure Node.js en production.

Configuration prerendering dans angular.json

// angular.json — configuration prerender
{
  "projects": {
    "mon-app": {
      "architect": {
        "prerender": {
          "builder": "@angular-devkit/build-angular:prerender",
          "options": {
            "routes": [
              "/",
              "/about",
              "/blog",
              "/blog/angular-seo-guide",
              "/blog/core-web-vitals",
              "/contact",
              "/pricing"
            ],
            "routesFile": "routes.txt"
          },
          "configurations": {
            "production": {
              "browserTarget": "mon-app:build:production",
              "serverTarget": "mon-app:server:production"
            }
          }
        }
      }
    }
  }
}

Fichier routes.txt (routes dynamiques)

# routes.txt — liste des URLs à prerender
/
/about
/contact
/pricing
/blog
/blog/angular-seo-guide
/blog/core-web-vitals
/blog/schema-org-guide
/products
/products/laptop-pro
/products/smartphone-max

Script pour générer routes.txt dynamiquement

// generate-routes.ts — script Node.js pour générer routes.txt avant build
import { writeFileSync } from 'fs';
import { fetchAllBlogSlugs, fetchAllProductSlugs } from './src/app/api/slugs';

async function generateRoutes() {
  const staticRoutes = ['/', '/about', '/contact', '/pricing', '/blog', '/products'];

  // Récupérer slugs depuis CMS / API
  const blogSlugs = await fetchAllBlogSlugs();
  const productSlugs = await fetchAllProductSlugs();

  const blogRoutes = blogSlugs.map((slug: string) => `/blog/${slug}`);
  const productRoutes = productSlugs.map((slug: string) => `/products/${slug}`);

  const allRoutes = [...staticRoutes, ...blogRoutes, ...productRoutes];
  writeFileSync('routes.txt', allRoutes.join('\n'));
  console.log(`✅ ${allRoutes.length} routes générées dans routes.txt`);
}

generateRoutes();
# Pipeline de build complet
node generate-routes.ts        # Génère routes.txt dynamiquement
ng run mon-app:prerender       # Prerender toutes les routes
# → Résultat : dist/mon-app/browser/blog/angular-seo-guide/index.html
# → Servir avec Nginx comme fichiers statiques (TTFB < 50ms)
Avec prerendering, AUCUN serveur Node.js n'est nécessaire en production. Nginx sert directement les fichiers HTML statiques — TTFB optimal, CDN-friendly, infrastructure simple.

5. Balises Meta et Title dynamiques dans Angular : la base du SEO

Sans SSR ou prerendering, les balises <title> et <meta> Angular ne sont visibles que dans le navigateur. Avec SSR, elles sont présentes dans le HTML initial — indexées immédiatement par Google.

Service SEO Angular réutilisable

// seo.service.ts — service centralisé pour Title + Meta
import { Injectable } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

interface SeoConfig {
  title: string;
  description: string;
  keywords?: string;
  ogImage?: string;
  canonical?: string;
  noIndex?: boolean;
}

@Injectable({ providedIn: 'root' })
export class SeoService {
  constructor(private title: Title, private meta: Meta, private router: Router) {}

  // Appeler depuis chaque composant page ou résolveur de route
  updateSeo(config: SeoConfig): void {
    const fullTitle = `${config.title} | AngularForAll`;

    // Balise title
    this.title.setTitle(fullTitle);

    // Meta description
    this.meta.updateTag({ name: 'description', content: config.description });

    // Keywords
    if (config.keywords) {
      this.meta.updateTag({ name: 'keywords', content: config.keywords });
    }

    // Open Graph (Facebook, LinkedIn)
    this.meta.updateTag({ property: 'og:title', content: fullTitle });
    this.meta.updateTag({ property: 'og:description', content: config.description });
    this.meta.updateTag({ property: 'og:type', content: 'website' });
    this.meta.updateTag({ property: 'og:url', content: this.router.url });
    if (config.ogImage) {
      this.meta.updateTag({ property: 'og:image', content: config.ogImage });
    }

    // Twitter Card
    this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
    this.meta.updateTag({ name: 'twitter:title', content: fullTitle });
    this.meta.updateTag({ name: 'twitter:description', content: config.description });

    // Noindex si page privée ou en construction
    if (config.noIndex) {
      this.meta.updateTag({ name: 'robots', content: 'noindex, nofollow' });
    } else {
      this.meta.updateTag({ name: 'robots', content: 'index, follow' });
    }
  }
}

// blog-post.component.ts — utilisation dans un composant
@Component({ selector: 'app-blog-post', templateUrl: './blog-post.component.html' })
export class BlogPostComponent implements OnInit {
  constructor(private seoService: SeoService, private route: ActivatedRoute) {}

  ngOnInit(): void {
    this.route.data.subscribe(data => {
      const article = data['article']; // Via resolver
      this.seoService.updateSeo({
        title: article.title,
        description: article.excerpt,
        keywords: article.tags.join(', '),
        ogImage: article.coverImage,
        canonical: `https://monsite.com/blog/${article.slug}`
      });
    });
  }
}

Gestion des balises Canonical dans Angular

// canonical.service.ts — injecter les balises link canonical
import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class CanonicalService {
  constructor(@Inject(DOCUMENT) private doc: Document) {}

  setCanonical(url: string): void {
    let link: HTMLLinkElement = this.doc.querySelector('link[rel=canonical]') as HTMLLinkElement;

    if (!link) {
      link = this.doc.createElement('link');
      link.setAttribute('rel', 'canonical');
      this.doc.head.appendChild(link);
    }

    link.setAttribute('href', url);
  }

  // Appel dans app.component.ts + chaque page
  // this.canonicalService.setCanonical(`https://monsite.com${this.router.url}`);
}

6. Impact du SSR sur les Core Web Vitals Angular

Le SSR et le prerendering transforment radicalement les Core Web Vitals d'une application Angular. Le LCP (Largest Contentful Paint) est le métrique le plus impacté.

Core Web Vital Angular CSR (typique) Angular SSR Seuil Google "Bon"
LCP (Largest Contentful Paint) 3.5 - 6s 0.8 - 2s ≤ 2.5s
INP (Interaction to Next Paint) 200 - 400ms 80 - 150ms ≤ 200ms
CLS (Cumulative Layout Shift) 0.15 - 0.4 0.02 - 0.08 ≤ 0.1
TTFB (Time to First Byte) 200 - 500ms 50 - 200ms (avec cache) ≤ 800ms
FCP (First Contentful Paint) 2.5 - 4s 0.5 - 1.5s ≤ 1.8s

Optimisations performance Angular SSR

  • HTTP Cache-Control — configurer Cache-Control: s-maxage=300 pour cacher les pages SSR dans un CDN (Cloudflare, Fastly)
  • Lazy loading modules — diviser l'application en chunks (loadComponent / loadChildren) pour réduire le bundle initial
  • Image optimization — utiliser NgOptimizedImage (Angular 15+) pour lazy loading, preloading et srcset automatiques
  • TransferState — éviter les doubles requêtes HTTP entre SSR et hydratation browser
  • HTTP/2 Server Push — sur Nginx, pousher les assets critiques (CSS, fonts) dès le premier octet
  • Compression Brotli — Nginx brotli on; réduit la taille des bundles JS de 25-30%

NgOptimizedImage pour LCP (Angular 15+)

// app.module.ts / app.config.ts
import { NgOptimizedImage } from '@angular/common';

// Template HTML — utiliser ngSrc au lieu de src
// <img ngSrc="assets/hero.webp" width="800" height="450" priority>
// - priority → fetchpriority="high" automatique (améliore LCP)
// - width/height → évite CLS (pas de layout shift)
// - Lazy loading automatique pour les images sans priority

// Avec CDN ImageKit ou Imgix
providers: [
  provideImageKitLoader('https://ik.imagekit.io/mon-compte/')
]
// Angular génère automatiquement : srcset, sizes, formats WebP

7. Injecter des données structurées JSON-LD dans Angular SSR

Les données structurées (Schema.org) doivent être dans le HTML initial pour être lues par Google et les moteurs IA. Avec SSR, elles sont servies côté serveur — sans délai de rendu JS.

// json-ld.service.ts — injecter Schema.org dans <head>
import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class JsonLdService {
  constructor(@Inject(DOCUMENT) private doc: Document) {}

  injectSchema(schema: object): void {
    // Supprimer ancien schema si présent
    const existing = this.doc.querySelector('script[type="application/ld+json"][data-json-ld]');
    if (existing) existing.remove();

    const script = this.doc.createElement('script');
    script.type = 'application/ld+json';
    script.setAttribute('data-json-ld', ''); // Marqueur pour re-injection
    script.textContent = JSON.stringify(schema);
    this.doc.head.appendChild(script);
  }
}

// blog-post.component.ts — injecter Article schema
ngOnInit(): void {
  this.jsonLdService.injectSchema({
    '@context': 'https://schema.org',
    '@type': 'Article',
    'headline': this.article.title,
    'description': this.article.excerpt,
    'image': this.article.coverImage,
    'author': {
      '@type': 'Person',
      'name': 'AngularForAll',
      'url': 'https://angularforall.com'
    },
    'publisher': {
      '@type': 'Organization',
      'name': 'AngularForAll',
      'logo': { '@type': 'ImageObject', 'url': 'https://angularforall.com/logo.png' }
    },
    'datePublished': this.article.publishedAt,
    'dateModified': this.article.updatedAt,
    'url': `https://angularforall.com/blog/${this.article.slug}`
  });
}
Avec SSR, le JSON-LD est présent dans le HTML avant hydratation. Google le lit immédiatement, sans attendre l'exécution JavaScript. Impact direct sur les rich snippets dans les SERPs.

8. Comment valider que Google indexe correctement votre SPA Angular ?

Valider l'indexation SSR nécessite plusieurs outils complémentaires pour vérifier le rendu HTML, les balises meta, les Core Web Vitals et les erreurs de crawl.

Outil Ce qu'il vérifie Accès
Google Search Console Coverage, URL Inspection, rendu Googlebot Gratuit (propriété vérifiée)
Rich Results Test Structured data, rich snippets eligibility Gratuit (search.google.com)
Screaming Frog Crawl complet, title/meta/H1, JS rendering Gratuit 500 URLs / Payant
PageSpeed Insights Core Web Vitals (lab + field data) Gratuit
curl --user-agent Googlebot HTML brut servi à Googlebot (sans JS) CLI (voir ci-dessous)
Lighthouse CLI SEO score, accessibilité, performance npm install -g lighthouse

Vérification rapide en ligne de commande

# Tester le HTML brut vu par Googlebot (sans JS)
curl -A "Googlebot/2.1 (+http://www.google.com/bot.html)" \
  https://monsite.com/blog/angular-ssr-guide \
  | grep -E "<title>|<meta name=.description|<h1|og:title"

# Résultat attendu AVEC SSR :
# <title>Angular SSR Guide | AngularForAll</title>
# <meta name="description" content="Guide complet...">
# <h1>Comment implémenter SSR dans Angular 17+</h1>

# Résultat SANS SSR (CSR only) :
# <title>MonApp</title>  (titre générique uniquement)
# (pas de meta description)

# Lighthouse audit automatisé
lighthouse https://monsite.com/blog/angular-ssr-guide \
  --output=json --output-path=./seo-report.json \
  --only-categories=seo,performance
  • GSC URL Inspection — vérifier "Rendu" dans les détails d'indexation (HTML rendu doit montrer le contenu complet)
  • Robots.txt — s'assurer que /, /assets/ et les chunks JS ne sont pas bloqués
  • Sitemap.xml — soumettre via GSC, vérifier que toutes les URLs SSR/prerendered sont listées
  • canonical — chaque page Angular doit avoir une balise canonical pointant vers son URL canonique
  • Open Graph — tester avec Facebook Sharing Debugger et LinkedIn Post Inspector
  • Status codes — vérifier que les routes 404 Angular retournent HTTP 404 (pas 200 avec HTML "not found")

404 Angular SSR : retourner le bon status HTTP

// not-found.component.ts — retourner 404 HTTP côté serveur
import { Component, OnInit, Inject, PLATFORM_ID, Optional } from '@angular/core';
import { RESPONSE } from '@angular/ssr/tokens';
import { Response } from 'express';
import { isPlatformServer } from '@angular/common';

@Component({ selector: 'app-not-found', template: `<h1>Page introuvable (404)</h1>` })
export class NotFoundComponent implements OnInit {
  constructor(
    @Optional() @Inject(RESPONSE) private response: Response,
    @Inject(PLATFORM_ID) private platformId: object
  ) {}

  ngOnInit(): void {
    if (isPlatformServer(this.platformId) && this.response) {
      this.response.status(404); // Réponse HTTP 404 correcte pour Googlebot
    }
  }
}

FAQ — SEO Angular SPA, SSR et Prerendering

Pourquoi le SEO est-il problématique dans les applications Angular SPA ?

Les SPA Angular génèrent le HTML côté client (CSR) : Googlebot voit une page vide avant exécution JS. Sans SSR ou prerendering, les crawlers n'indexent pas le contenu dynamique.

Quelle différence entre SSR, SSG et prerendering pour Angular ?

SSR génère le HTML à chaque requête serveur (contenu dynamique). SSG génère des fichiers statiques au build (pages fixes). Prerendering pré-génère des routes spécifiques au déploiement.

Comment implémenter SSR dans Angular 17+ ?

Ajouter SSR avec `ng add @angular/ssr`. Angular 17+ intègre le rendu hybride nativement. Configurer `app.config.server.ts` et `server.ts` pour Express. Déployer Node.js côté serveur.

Le prerendering Angular améliore-t-il les Core Web Vitals ?

Oui, massivement. LCP passe de 3-5s (CSR) à 0.8-1.5s (prerendering) car le HTML est prêt sans attendre JS. CLS réduit car contenu stable dès le chargement.

Quels outils vérifier que Google indexe bien une SPA Angular ?

Google Search Console (Coverage + URL Inspection). Rich Results Test pour structured data. `fetch as Googlebot` en GSC. Screaming Frog + rendering JavaScript pour auditer.

Conclusion : SSR et Prerendering sont incontournables pour le SEO Angular

Le SEO Angular nécessite de sortir du mode CSR par défaut. Angular 17+ rend cette transition simple avec ng add @angular/ssr et le rendu hybride natif. Le choix entre SSR (Node.js en prod) et prerendering (fichiers statiques) dépend de la dynamicité de votre contenu.

Pour la plupart des sites Angular (blogs, vitrines, e-commerce à catalogue fixe), le prerendering est le meilleur compromis : SEO excellent, infrastructure simple, TTFB optimal. Réservez le SSR pur aux applications avec contenu entièrement dynamique (stock en temps réel, contenu personnalisé).

Checklist finale — SEO Angular SPA

  • PathLocationStrategy — plus de # dans les URLs (HashLocationStrategy → interdit)
  • SSR ou prerendering — ng add @angular/ssr + configurer routes prerender
  • SeoService — Title + Meta + OpenGraph dynamiques depuis resolver de route
  • CanonicalService — balise canonical sur chaque route Angular
  • JsonLdService — Schema.org injecté côté serveur (Article, FAQPage, Product)
  • TransferState — éviter double requêtes HTTP SSR→browser
  • NgOptimizedImage — LCP optimisé, lazy loading automatique
  • 404 HTTP corrects — composant NotFound retourne status 404 côté serveur
  • Robots.txt — ne pas bloquer /assets/ ni les chunks JS
  • Sitemap.xml — généré dynamiquement et soumis à GSC
  • GSC URL Inspection — vérifier rendu Googlebot post-déploiement
  • Lighthouse SEO — score ≥ 95 (audit automatisé en CI/CD)

Partager