Angular e SEO: SSR, hydration e meta tag senza sorprese

Angular e SEO: SSR, hydration e meta tag senza sorprese

Angular è una delle poche piattaforme SPA con un problema di indicizzazione tutto suo: a differenza di React o Vue, che affidano il rendering lato server a librerie esterne come Next.js o Nuxt, Angular offre un percorso ufficiale e integrato, mantenuto dallo stesso team che sviluppa il framework. Questo significa API diverse, comandi diversi e — soprattutto — errori diversi da quelli che si leggono nelle guide pensate per l'ecosistema React.

Questo articolo copre il percorso specifico di Angular: come funziona oggi il rendering lato server (ex Angular Universal), come impostare correttamente il tag title e i meta tag pagina per pagina, cosa succede durante l'hydration e dove si annidano gli errori più comuni quando un'app Angular passa dal server al browser senza che nessuno se ne accorga finché il traffico organico non ne paga il prezzo.

SPA e crawling: perché Angular richiede un piano di rendering

Un'applicazione Angular compilata per la produzione è, di base, un bundle JavaScript che costruisce l'intera pagina nel browser: il file HTML servito dal server contiene poco più di un tag app-root vuoto e i riferimenti agli script. Google è in grado di eseguire JavaScript per indicizzare questo tipo di pagine, ma lo fa in una fase di rendering separata dalla scansione iniziale: se lo script si blocca, una chiamata di rete fallisce, o un errore silenzioso interrompe il bootstrap dell'app, il motore di ricerca vede una pagina vuota, non un errore che un umano noterebbe subito.

Per questo Angular offre da anni un percorso per generare l'HTML già completo prima che il crawler lo veda: prima come modulo separato (Angular Universal), oggi come parte integrata del framework. La scelta non è "se" adottarlo per un sito con ambizioni organiche, ma quale delle modalità disponibili — rendering lato server a ogni richiesta, prerendering in fase di build, o una combinazione delle due per rotta — si adatta al tipo di contenuto pubblicato.

Dal modulo Angular Universal a @angular/ssr integrato

Fino a qualche versione fa, il rendering lato server in Angular richiedeva pacchetti separati sotto il namespace @nguniversal, una configurazione manuale di un motore Express e un file server mantenuto a parte dal resto del progetto. L'approccio funzionava, ma era facile disallineare la versione del modulo Universal da quella del core di Angular, e un aggiornamento del framework poteva rompere il rendering lato server.

Dalla versione 17, il supporto SSR è stato assorbito nel core del framework sotto il pacchetto @angular/ssr, installabile con il comando ng add @angular/ssr su un progetto esistente. Il comando genera automaticamente l'infrastruttura necessaria a distinguere il bundle destinato al browser da quello destinato al server, collegando i due tramite un file di bootstrap dedicato. Il risultato pratico per la SEO è che ogni rotta dell'applicazione, se raggiunta direttamente tramite un URL, restituisce HTML completo al primo colpo, senza dipendere dall'esecuzione di JavaScript lato crawler.

  • Aggiunge un entry point server che avvia un'istanza Node in ascolto sulle richieste in produzione
  • Crea i file di bootstrap dedicati al rendering lato server, separati da quelli usati nel browser
  • Aggiorna la configurazione di build in modo che produca sia il bundle client sia quello server nella stessa pipeline

Prerendering statico e rendering ibrido per rotta

Il rendering lato server a ogni richiesta ha un costo: ogni visita, umana o di un crawler, fa eseguire il bootstrap completo dell'applicazione su un processo Node prima di restituire l'HTML. Per pagine il cui contenuto non cambia a ogni richiesta — una pagina "chi siamo", un articolo del blog già pubblicato, una pagina di categoria stabile — questo lavoro è sprecato: lo stesso HTML potrebbe essere generato una sola volta in fase di build e servito come file statico.

Angular chiama questa modalità prerendering: durante la build, il toolchain visita ogni rotta conosciuta (incluse le rotte generate da un elenco fornito a build time) ed esegue lo stesso motore di rendering usato per l'SSR, salvando però il risultato come file HTML statico. Il vantaggio per la SEO è duplice: il crawler riceve HTML completo istantaneamente, senza attendere l'esecuzione di un processo server, e il costo di hosting scende perché non serve un server Node sempre attivo per quelle rotte.

Le versioni più recenti di Angular permettono di combinare le due modalità nella stessa applicazione, assegnando a gruppi di rotte diversi una strategia diversa: prerendering per le pagine di contenuto statico, rendering lato server per quelle che dipendono da dati utente o da una sessione, rendering solo client per aree autenticate che non hanno alcun valore per l'indicizzazione. Questa granularità evita l'errore comune di trattare l'intera applicazione come un blocco unico, quando in realtà solo una parte di essa deve davvero comparire nei risultati di ricerca.

I servizi Title e Meta: aggiornare i tag pagina per pagina

Un'app Angular parte da un singolo file index.html, con un solo tag title e un solo blocco di meta tag statici. Se non si interviene, ogni rotta dell'applicazione — la home, una pagina prodotto, un articolo — condivide lo stesso titolo e la stessa meta description definiti in quel file, indipendentemente da cosa l'utente stia effettivamente visitando. È un errore facile da introdurre, e si scopre solo controllando la SERP, quando decine di pagine mostrano lo stesso snippet.

Angular risolve il problema con due servizi iniettabili: Title, che imposta il contenuto del tag title, e Meta, che permette di aggiungere o aggiornare meta tag arbitrari come description, Open Graph o robots. Nessuno dei due si aggiorna da solo: vanno richiamati esplicitamente, tipicamente dentro il componente di ciascuna rotta o in un resolver associato alla rotta, ogni volta che la navigazione cambia pagina. Un pattern comune è centralizzare questa logica in un servizio dedicato che ascolta gli eventi del router e applica i tag corretti leggendo i dati statici della rotta o quelli risolti da un resolver.

Un dettaglio che genera bug silenziosi: il metodo di aggiornamento dei meta tag li modifica solo se il selettore passato corrisponde esattamente a un tag già presente; se il nome dell'attributo non coincide con precisione — per esempio per una differenza tra l'uso di name e di property nei tag Open Graph — il framework ne crea uno nuovo invece di sovrascrivere quello vecchio, e la pagina finisce con due meta description diverse nello stesso head. Vale la pena controllare l'HTML restituito dal server, non l'albero finale nel browser.

  • Impostare un titolo specifico per ogni pagina tramite il servizio Title, non un valore fisso ereditato da index.html
  • Aggiornare, non duplicare, la meta description tramite il servizio Meta a ogni cambio di rotta
  • Impostare un meta tag robots a noindex sulle rotte che non devono essere indicizzate, ricordando di rimuoverlo quando la stessa rotta torna indicizzabile

Router, URL canonici e rotte che il framework non gestisce da solo

Il router di Angular intercetta la navigazione e aggiorna l'URL nella barra degli indirizzi senza ricaricare la pagina, ma non tocca in automatico né il tag canonical né l'attributo lang dell'elemento html. Sono entrambi elementi che, per un sito multi-rotta o multilingua, vanno gestiti a mano con la stessa logica usata per title e meta: un listener sugli eventi di navigazione che aggiorna un link canonical iniettato dinamicamente nella sezione head.

L'errore più comune qui è dimenticare che il canonical calcolato lato server durante l'SSR o il prerendering deve corrispondere esattamente all'URL con cui quella pagina è stata generata, incluso il trattamento di parametri di query, slash finali e maiuscole o minuscole. Se il canonical viene calcolato lato client dopo il bootstrap, e in quel momento l'URL del browser non riflette ancora la forma scelta come canonica per la rotta — per esempio perché contiene un parametro di tracciamento — il tag finisce per puntare a una variante sbagliata della stessa pagina.

Un secondo punto delicato riguarda il caricamento differito dei moduli per rotta, la modalità con cui Angular carica in modo lazy le parti dell'applicazione non ancora visitate. Dal punto di vista SEO questo non è un problema in sé, perché il codice differito non impedisce all'HTML pre-renderizzato di essere completo — ma lo diventa se il contenuto testuale di una sezione dipende dal caricamento di quel modulo invece di essere già presente nell'HTML iniziale: un'analisi che si affida solo all'HTML statico rischia di non trovarlo.

Hydration non distruttiva: dove si rompe silenziosamente

Fino alla versione 16 incluso, il comportamento predefinito dopo l'SSR era la hydration distruttiva: il browser scartava l'intero DOM ricevuto dal server e lo ricostruiva da zero eseguendo l'applicazione lato client. Funzionava, ma produceva un lampo visibile di contenuto che si ricarica — un problema di stabilità visiva simile a quello che i Core Web Vitals misurano — e sprecava il lavoro già svolto dal server.

La hydration non distruttiva, stabile a partire dalla versione 17 e attivata aggiungendo il provider dedicato nella configurazione dell'applicazione, cambia strategia: Angular confronta il DOM ricevuto dal server con quello che si aspetterebbe di generare lato client e, se corrispondono, riusa i nodi esistenti invece di ricrearli. Il vantaggio indiretto è reale — meno lavoro nel thread principale al caricamento, meno instabilità visiva — ma il meccanismo introduce una nuova classe di errori: quando il confronto fallisce, Angular non ha altra scelta che scartare quella porzione di DOM e ricostruirla, riproducendo esattamente il problema che l'hydration doveva risolvere, ma solo per quell'area, con un avviso in console che in produzione quasi nessuno controlla.

Le cause più comuni di un mismatch sono un contenuto che dipende da un valore casuale, dall'ora corrente o dal fuso orario del server — diverso da quello dell'utente — generato in modo diverso tra le due esecuzioni; codice di terze parti come widget o script di tracciamento che manipola il DOM prima che Angular completi l'hydration, alterando la struttura che il framework si aspettava di trovare; e l'uso diretto di API di manipolazione del DOM fuori dai meccanismi di binding di Angular, che il framework non riesce a tracciare durante il confronto.

  • Controllare la console del browser in produzione per avvisi di hydration mismatch, non solo durante lo sviluppo locale
  • Isolare qualsiasi accesso a window, document o localStorage dentro un controllo che verifica se il codice sta eseguendo nel browser, per evitare sia errori di bootstrap lato server sia mismatch lato client
  • Evitare che il contenuto visibile dipenda da valori non deterministici tra le due esecuzioni, come l'orario, numeri casuali o dati non ancora arrivati al momento del rendering

Errori frequenti e una checklist di verifica pratica

Molti dei problemi SEO osservati su applicazioni Angular in produzione non derivano da una scelta architetturale sbagliata, ma da un dettaglio di configurazione lasciato al valore predefinito. Prima di considerare concluso il lavoro sul rendering lato server di un progetto Angular, vale la pena controllare questi punti uno per uno, direttamente sull'HTML restituito dal server e non su quello visibile nel browser con la pagina già interattiva.

  • Richiedere l'URL con un client a riga di comando e leggere l'HTML grezzo: titolo, meta description e contenuto testuale principale devono essere già presenti, senza eseguire JavaScript
  • Controllare che una rotta impostata come non indicizzabile mantenga quel meta tag anche dopo che l'utente ha navigato altrove e poi è tornato sulla stessa rotta lato client, non solo al primo caricamento lato server
  • Verificare che l'attributo lang sull'elemento html cambi davvero per le rotte in lingue diverse, e non resti fissato al valore statico scritto una volta sola in index.html
  • Controllare i tempi di risposta del server sotto carico: un rendering lato server lento aumenta il tempo di risposta per ogni visita di un crawler, non solo per gli utenti, e un budget di scansione limitato penalizza le pagine più lente
  • Assicurarsi che le chiamate di rete necessarie per popolare il contenuto della pagina siano completate prima che il server restituisca l'HTML, altrimenti chi legge quell'HTML riceve uno stato di caricamento vuoto invece del contenuto finale

Domande frequenti

Angular Universal e Angular SSR sono la stessa cosa?

Sì, dal punto di vista funzionale: Angular Universal era il nome storico del progetto e dei pacchetti @nguniversal, mentre @angular/ssr è la sua integrazione ufficiale nel core del framework a partire dalla versione 17. Il compito — generare HTML lato server — non è cambiato, ma è cambiato il modo in cui viene configurato e mantenuto insieme al resto del progetto.

Serve davvero l'hydration, o basta l'SSR?

L'SSR da solo risolve già il problema principale di indicizzazione, perché fa arrivare HTML completo al crawler. L'hydration riguarda cosa succede dopo, quando il browser rende l'app interattiva: non è strettamente necessaria per l'indicizzazione, ma la sua assenza, o un'implementazione con molti mismatch, può peggiorare le metriche di stabilità visiva percepite dagli utenti reali.

Un'app Angular senza SSR può comunque essere indicizzata?

In molti casi sì, perché Google è in grado di eseguire JavaScript in una fase di rendering separata dalla scansione iniziale. Il rischio è il ritardo tra la scansione e il rendering effettivo, e la possibilità che errori silenziosi nel bootstrap dell'app impediscano al crawler di vedere il contenuto reale. L'SSR riduce questo rischio, non lo introduce.

Il prerendering funziona anche per pagine generate da dati dinamici, come un catalogo prodotti?

Funziona se l'elenco completo delle rotte da generare è conosciuto al momento della build, per esempio leggendo un elenco di ID prodotto da una fonte dati in quel momento. Per contenuto che cambia molto più spesso della frequenza delle build, il rendering lato server per richiesta resta la scelta più adatta.

Tutti gli articoli