SPA e SEO
Come rendere le applicazioni a pagina singola (React Router, Vue Router, Angular Router) indicizzabili e scansionabili — i problemi dell'app-shell e dei soft-404, History API vs routing con hash, HTML per rotta tramite SSR/prerendering, canonical e title per rotta, e generazione della sitemap per rotte lato client.
Lingue
Un'applicazione a pagina singola carica un unico documento e cambia le viste con JavaScript invece di richiedere una nuova pagina al server. Molte SPA implementano questo con un 'app shell' — il server restituisce l'HTML reale di un solo URL, una shell quasi vuota, finché il JS non renderizza ogni rotta — ma questa è una scelta implementativa comune, non una regola che ogni SPA segue; controlla cosa restituisce effettivamente una richiesta diretta a ciascuna rotta prima di assumerlo. La soluzione, dove un setup app-shell nudo è il problema, ha due metà indipendenti: indirizzabilità (History API, non frammenti hash/#!, così ogni vista ha un URL reale — una navigazione soft del browser tramite History API cambia l'URL e l'interfaccia ma non crea di per sé una nuova risposta del server) e disponibilità dei contenuti (SSR, prerendering o un meta-framework così che ciascuno di quegli URL possa restituire HTML unico su richiesta). In aggiunta, verifica il title, il canonical e lo stato robots di ogni rotta sia all'ingresso diretto che dopo il rendering, gestisci gli errori soft “pagina non trovata” (i router tendono a mantenere uno stato 200 per le viste 'non trovato' — reindirizza a un URL che restituisce effettivamente uno stato di errore, o aggiungi un noindex renderizzato, anche se un noindex iniziale può causare la mancata esecuzione del rendering), e assicurati che ogni rotta nella tua sitemap risolva effettivamente in HTML reale e indicizzabile — non esiste un formato speciale di sitemap per SPA.
TL;DR — Una single-page application (SPA) carica una pagina dal server e poi usa JavaScript per cambiare “pagina” senza un ricaricamento completo. Il problema: molte SPA inviano la stessa pagina iniziale quasi vuota indipendentemente dall’URL richiesto da un motore di ricerca — questo è un rischio comune del modo in cui le SPA sono solitamente costruite, non una garanzia di ogni SPA. Per risolverlo, assicurati che ogni rotta abbia il suo vero URL e il suo vero HTML — di solito renderizzando le pagine sul server o pre-costruendole.
Cos’è una SPA
Una single-page application aggiorna comunemente viste e rotte lato client senza una navigazione completa del documento. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Google può renderizzare SPA JavaScript, ma URL crawlable, link, gestione dello stato e contenuto renderizzato rimangono necessari. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
Una single-page application è un sito web costruito come una singola pagina HTML. Quando clicchi in giro — dalla pagina dei prodotti alla pagina “chi siamo” — JavaScript sostituisce ciò che è sullo schermo invece di chiedere al server una pagina completamente nuova. React (con React Router), Vue (con Vue Router) e Angular funzionano tutti così per impostazione predefinita. Sembra veloce e simile a un’app, ed è per questo che è popolare.
Il rischio è ciò che il server invia. Molte SPA usano un’implementazione “app shell”:
la prima volta che qualcuno (incluso Google) carica l’app, il server restituisce una
shell quasi vuota, e il contenuto reale viene costruito nel browser successivamente. Questo è
un modo comune di costruire una SPA — non la definizione di una, e non qualcosa che ogni
SPA fa. Ma quando un sito invia una app shell vuota, la modalità di errore è reale: se
Google chiede al server /about e /products separatamente, può ottenere la stessa
shell vuota per entrambi, perché nulla della rotta ha cambiato ciò che il server
ha inviato. Il modo per sapere se la tua app è in quella posizione è controllare cosa una
richiesta diretta a ciascuna rotta restituisce effettivamente — non assumerlo dal fatto che sia una
SPA.
Perché questo danneggia la SEO
I motori di ricerca devono vedere il tuo contenuto per classificarlo. Con una SPA vuota, tre cose tendono ad andare storte:
- Ogni URL appare uguale al server. Link profondi, condivisioni e crawler finiscono tutti sulla stessa shell.
- Le pagine 404 dicono ancora “200 OK”. Un router JavaScript può mostrare una schermata “non trovato” mentre la pagina riporta tecnicamente successo, quindi Google può indicizzare pagine vuote.
- Gli URL sbagliati. Le SPA più vecchie usavano indirizzi come
example.com/#/products. Google non può indicizzare in modo affidabile quelli.
Come risolverlo (la versione breve)
- Dai a ogni vista un URL reale usando l’API History del browser (percorsi puliti come
/products), non quelli basati su#. - Invia HTML reale per ogni URL. Renderizza le pagine sul server (SSR) o pre-costruiscile in anticipo (prerendering). Se stai iniziando da zero, un framework che fa questo per te — Next.js, Nuxt, SvelteKit, Angular SSR — ti risparmia la fatica.
- Dai a ogni pagina il suo titolo e la sua descrizione che cambiano quando la rotta cambia.
- Usa link reali (
<a href>), non<div>cliccabili.
Vuoi i meccanismi — perché il routing con hash fallisce, la trappola della “direttiva più restrittiva” con i canonical, e come generare una sitemap per rotte lato client? Passa alla scheda Avanzate.
TL;DR — Il rischio SEO principale di una SPA non è “JavaScript” in astratto — è che il routing lato client cambia ciò che l’utente vede senza cambiare ciò che il server restituirebbe. La soluzione ha due metà indipendenti che le persone confondono costantemente: indirizzabilità (History API, URL unici per rotta, niente frammenti
#!) e disponibilità del contenuto (SSR, prerendering/SSG, o un meta-framework). Fai solo la prima e ottieni una sitemap ordinata di URL che renderizzano tutti la stessa shell. In aggiunta a entrambe, ogni rotta deve avere il suo canonical/titolo/descrizione nel DOM renderizzato, devi gestire i soft 404 che mantengono un200, e ogni rotta nella sitemap deve risolversi indipendentemente in HTML reale.
Cos’è realmente la SEO per SPA
SPA è un’architettura applicativa, non uno stato di fallimento SEO garantito. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Il rendering lato server, il prerendering o un rendering lato client implementato con cura possono esporre i contenuti, ma nessuno garantisce l’indicizzazione. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
Questo è l’approfondimento su applicazioni a pagina singola nello specifico — routing lato client con React Router, Vue Router o Angular Router, dove dopo il primo caricamento il server non invia mai un’altra pagina completa. Si affianca alla guida più ampia sulla SEO JavaScript (che copre parità, caricamento lazy, scorrimento infinito e rendering JS in generale) e agli articoli per framework specifici per React, Next.js, Nuxt, Angular, Vue, Svelte e Astro. Qui mi interessa solo il livello di routing e cosa fa a crawling e indicizzazione.
Perché le SPA con routing lato client puro falliscono nella SEO
Un URL, una risposta HTML — il problema dell’app shell
Google descrive la modalità di errore con precisione: “Some JavaScript sites may use the app
shell model where the initial HTML does not contain the actual content and Google needs
to execute JavaScript before being able to see the actual page content.” (traduzione) «Alcuni siti JavaScript possono usare il modello app shell in cui l’HTML iniziale non contiene il contenuto effettivo e Google deve eseguire JavaScript prima di poter vedere il contenuto effettivo della pagina.» In una SPA pura, l‘“app shell” è tutto ciò che il server restituisce. Recupera /products da zero e ottieni lo stesso documento quasi vuoto che otterresti per /about. Il contenuto diverge solo dopo che il browser esegue il tuo JavaScript e il tuo router decide cosa mostrare.
Questo è l’intero problema in una frase: il routing lato client cambia ciò che l’utente vede senza cambiare ciò che il server restituirebbe. Tutto ciò che ne consegue — soft 404, contenuti duplicati, titoli mancanti — è un sintomo di questo.
Il server non vede mai quale “pagina” è stata richiesta (perché il routing con hash fallisce)
Le SPA più vecchie caricano le viste da frammenti di URL — example.com/#/products. Le parole esatte di Google: “A SPA may use URL fragments (for example https://example.com/#/products) for
loading different views.” (traduzione) «Una SPA può usare frammenti di URL (ad esempio https://example.com/#/products) per caricare viste diverse.» Questo fallisce per un motivo specifico e meccanico: i browser non inviano mai il frammento (qualsiasi cosa dopo #) al server nella richiesta HTTP. Il server letteralmente non può sapere quale “pagina” è stata richiesta, quindi non può restituire contenuti diversi o un codice di stato diverso per essa. Un crawler che recupera l’URL una volta ottiene HTML identico indipendentemente dal frammento.
Questo è anche il motivo per cui Google ha deprecato formalmente il suo schema di crawling AJAX del 2009 nell’ottobre 2015: “In short: We are no longer recommending the AJAX crawling proposal we
made back in 2009.” (traduzione) «In breve: non raccomandiamo più la proposta di crawling AJAX che abbiamo fatto nel 2009.» Il vecchio workaround _escaped_fragment_ permetteva ai server di pre-renderizzare le route con frammenti su richiesta — ma aggirava il problema del routing invece di risolverlo. La raccomandazione di Google che lo sostituisce è la History API, trattata di seguito.
Soft 404 — i router lato client mantengono un 200 per tutto
Questo è quasi inevitabile in una SPA pura. I router lato client, per progettazione, mantengono lo stato 200 della pagina originale per ogni navigazione virtuale — inclusi gli stati “non trovato”. Google lo segnala esplicitamente: “In a single-page application (SPA), this can be
especially difficult. To prevent error pages from being indexed, you can use one or both
of the following strategies.” (traduzione) «In un’applicazione a pagina singola (SPA), questo può essere particolarmente difficile. Per impedire che le pagine di errore vengano indicizzate, puoi usare una o entrambe le seguenti strategie.» E il perché: “When a SPA is using client-side JavaScript
to handle errors they often report a 200 HTTP status code instead of the appropriate
status code.” (traduzione) «Quando una SPA usa JavaScript lato client per gestire gli errori, spesso riporta un codice di stato HTTP 200 invece del codice di stato appropriato.»
Il risultato è che viste vuote o di errore vengono indicizzate come pagine 200 sottili. Google documenta esattamente due strategie qui, precisamente: reindirizza (o effettua una richiesta completa) a un URL il cui server restituisce un vero 404/codice di errore, oppure aggiungi un tag noindex alla vista di errore con JavaScript. Attenzione alla seconda: se quel noindex è presente dal primissimo paint piuttosto che aggiunto dopo che l’app decide che la route non è valida, può far sì che Google salti del tutto il rendering della pagina — quindi verifica cosa restituisce una richiesta diretta prima che venga eseguito qualsiasi JS, non solo ciò che appare nel DOM renderizzato successivamente.
Duplicati shell condivisi: una causa possibile, non una diagnosi automatica
C’è un secondo, più subdolo modo di fallire: rotte distinte vengono indicizzate come duplicati l’una dell’altra perché vengono renderizzate nello stesso boilerplate condiviso di header/nav/footer. Gary Illyes ha descritto un modo in cui ciò accade: “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out (my most likely explanation) and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (traduzione) «Ho un mucchio di email nella mia casella di posta in cui il problema è che il contenuto principale ha impiegato un’eternità a caricarsi, quindi il rendering è andato in timeout (la mia spiegazione più probabile) e ci siamo ritrovati con un mucchio di pagine che avevano solo il boilerplate. Con solo il boilerplate, quelle pagine sono duplicati.» La sua soluzione: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (traduzione) «Cerca di ristrutturare le chiamate js in modo che il contenuto (incluso il boilerplate marginale) si carichi per primo.» (Riportato tramite un post su LinkedIn, che resiste alla verifica automatizzata; trattalo come fonte secondaria ad alta affidabilità.)
Nota che Illyes la definisce “my most likely explanation” (traduzione) «la mia spiegazione più probabile», non una diagnosi confermata: vale la pena prenderlo alla lettera. Il timeout di rendering non si può diagnosticare dai soli sintomi; servono prove a livello di richiesta (cosa restituisce una fetch diretta), prove dell’output renderizzato (se il contenuto principale è presente) e dati di Search Console (segnali di duplicazione o indicizzazione). Solo così puoi distinguere un timeout specifico da un bug, una risorsa bloccata o una shell realmente identica. Inoltre, non esiste un timeout fisso e pubblicato attorno a cui progettare: il documento di base di Google afferma che una pagina “may stay on this queue for a few seconds, but it can take longer than that,” (traduzione) «può restare in questa coda per alcuni secondi, ma potrebbe volerci più tempo», senza indicare una durata precisa. Assicurati quindi che il contenuto principale si carichi il prima possibile, indipendentemente dal tempo complessivo richiesto dal rendering.
La soluzione: ottieni HTML reale per ogni rotta
La soluzione completa ha due parti indipendenti che la gente confonde di continuo:
- Indirizzabilità URL — History API, URL unici per rotta, niente frammenti.
- Disponibilità del contenuto — SSR, prerendering statico/SSG, o un meta-framework.
Se fai solo #1 ottieni URL puliti e condivisibili che restituiscono tutti la stessa shell vuota. Per una rotta che vuoi davvero indicizzare, ti servono entrambe.
Non è però un mandato universale per aggiungere SSR ovunque. SSR e prerendering riducono quanto dipendi dal fatto che un crawler esegua con successo il tuo JavaScript — non diventano obbligatori nel momento in cui un sito è tecnicamente una SPA. Prima di scegliere un’architettura per una data rotta, verifica tre cose: se quella rotta deve posizionarsi affatto (un pannello admin interno no), cosa restituisce già una richiesta diretta e senza JS (alcune configurazioni inviano già HTML significativo), e quali crawler devi effettivamente soddisfare (Google renderizza JS in modo abbastanza affidabile; Bing e la maggior parte dei crawler AI meno — vedi sotto). Un CSR che supera già questi controlli non deve diventare SSR solo perché il sito è una SPA.
Server-side rendering (SSR)
Il server esegue la tua app per ogni richiesta e restituisce HTML completo per quella rotta, poi il client lo “idrata” trasformandolo in una SPA viva. Questa è l’opzione più robusta perché il crawler riceve contenuto completo al primo fetch, senza bisogno di eseguire JS.
Prerendering statico / SSG
Invece di renderizzare per richiesta, costruisci l’HTML di ogni rotta in anticipo al deploy. Perfetto per contenuti che non cambiano per utente. Dai miei scritti sul SEO JavaScript: qualsiasi configurazione di SSR, rendering statico e prerendering andrà bene per i motori di ricerca — la cosa da evitare è lasciare il contenuto bloccato dietro un rendering solo client-side.
Oppure: non reinventare la ruota — usa un meta-framework
Per una nuova build, la raccomandazione onesta è di non creare a mano un routing lato client solo con React-Router o solo con Vue-Router. Un meta-framework — Next.js, Nuxt, Angular con il suo pacchetto SSR, SvelteKit, Remix — ti offre SSR/SSG e metadati basati sulle route out of the box, il che aggira l’intera categoria di problemi. Sii onesto sul rovescio della medaglia: adattare SSR a una SPA fatta a mano esistente è un vero lavoro di ingegneria — un progetto di migrazione, non un interruttore di configurazione.
Rendering dinamico come soluzione temporanea, non come destinazione
Puoi servire ai crawler uno snapshot HTML renderizzato separatamente (rendering dinamico). Sia Google che Bing lo accettano — Bing “raccomanda il rendering dinamico come una grande alternativa per i siti web che dipendono fortemente da JavaScript” (riportato dal blog di Bing del 2018; le due brevi citazioni sulle capacità di bingbot qui sotto sono verificate direttamente, questa più lunga non è stata ricontrollata in modo indipendente) — ma Google è chiaro che è una soluzione temporanea: “Il rendering dinamico era una soluzione temporanea e non una soluzione a lungo termine… Invece, ti consigliamo di usare il rendering lato server, il rendering statico o l’idratazione come soluzione.” (Riportato dal documento di Google sul rendering dinamico; la formulazione corrisponde a quanto già citato nella guida più ampia sul SEO JavaScript di questo sito.) È un ponte, non un’architettura.
API History vs. routing con hash (#!)
Cinque stati, non due
Testare una route SPA diventa confuso perché “funziona” in realtà copre cinque stati diversi e verificabili separatamente:
| Stato | Cosa è |
|---|---|
| Risposta del server | I byte che una richiesta HTTP fresca e senza JavaScript a un URL riceve effettivamente. |
| DOM renderizzato | Ciò che un browser (o il renderer di Googlebot) costruisce dopo aver eseguito JavaScript su quella risposta del server. |
| Elaborazione della ricerca | Come Google separatamente esegue il crawl della risposta del server, successivamente renderizza la pagina e indicizza in base a entrambi. |
| Navigazione completa del documento nel browser | Una nuova richiesta HTTP effettiva a un URL — l’unico stato che può cambiare la risposta del server o il codice di stato. |
| Navigazione soft del browser | Una transizione dell’API History (pushState/replaceState) che cambia l’URL visibile, la cronologia del browser e l’interfaccia utente sullo schermo. |
Quello che tutti confondono: una navigazione soft cambia l’URL e l’interfaccia utente, ma non crea da sola una nuova risposta HTTP o uno stato — questo accade solo con una navigazione completa (o una richiesta diretta equivalente, come curl). Testare una route cliccando attraverso l’app dalla homepage esercita la navigazione soft; testarla richiedendo direttamente l’URL esercita la risposta del server. Entrambi contano, e possono essere in disaccordo.
Cosa ti dà l’API History
La raccomandazione di Google è inequivocabile: “Ti consigliamo di usare l’API History per caricare contenuti diversi in base all’URL in una SPA.” (Nel documento live “History API” è un link, quindi questo deep link punta alla frase introduttiva.) L’API History (pushState/replaceState) permette al tuo router di cambiare l’URL visibile in un percorso reale e condivisibile — /products, non /#/products — senza un ricaricamento completo. Questo risolve la metà dell’indirizzabilità: ogni vista ora ha un URL a cui un server potrebbe rispondere in modo diverso.
Perché gli URL con frammento/hash sono invisibili al server — e a Google
Poiché il frammento non raggiunge mai il server (vedi sopra), l’API History è l’unico modo per dare a ogni route un URL che il server possa effettivamente servire. Google mette un limite nel suo documento di base: “non usare frammenti per caricare contenuti di pagina diversi. L’esempio seguente è una cattiva pratica, perché Googlebot non può risolvere in modo affidabile gli URL.” Questa è la stessa guida che ho scritto altrove — usa URL dall’aspetto normale come /products, non URL con hash come /#/products, perché Google non può indicizzare in modo affidabile quelli con hash.
La deprecazione del 2015, brevemente
L’era dell’hash-bang (#!) è terminata con il post di Google del 2015: “Times have changed. Today, as
long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are
generally able to render and understand your web pages like modern browsers.” (traduzione) «I tempi sono cambiati. Oggi, purché tu non impedisca a Googlebot di eseguire la scansione dei file JavaScript o CSS, in genere riusciamo a renderizzare e comprendere le pagine web come i browser moderni.» Google aggiunge: “you can use the History API pushState() to ensure accessibility for a wider range of
browsers (and our systems).” (traduzione) «puoi usare pushState() della History API per garantire l’accessibilità a una gamma più ampia di browser (e ai nostri sistemi)». Una sfumatura importante: Google non ha rimosso subito dall’indice i vecchi siti hash-bang — “we’ll generally crawl, render, and index the #! URLs” (traduzione) «in genere eseguiremo la scansione, il rendering e l’indicizzazione degli URL #!» — ma il fatto che possa ancora provarci non significa che sia una soluzione consigliata.
Rendere ogni route indicizzabile in modo indipendente
URL puliti e HTML reale ti fanno indicizzare. Per essere indicizzato correttamente, ogni route ha bisogno dei propri segnali.
Tag canonical per route — e la trappola della “direttiva più restrittiva”
Ogni route ha bisogno del proprio rel=canonical nel DOM renderizzato. La trappola: se un
canonical segnaposto (o un noindex) viene inserito nel guscio HTML grezzo e JavaScript dovrebbe
sovrascriverlo in seguito, puoi ottenere un conflitto. Google risolve i conflitti
tra la versione grezza e quella renderizzata prendendo il segnale più restrittivo — come ho
già detto, Google sceglierà le dichiarazioni più restrittive tra l’HTML e
la versione renderizzata di una pagina. Un noindex fuori posto o un canonical errato incorporato nel
guscio può sopprimere silenziosamente l’intera route anche dopo che il tuo JS lo “corregge”.
Titoli e meta description per route tramite JavaScript
Impostarli in JS va bene — Google lo dice direttamente: “You can use JavaScript to set
or change the meta description as well as the <title> element.” (traduzione) «Puoi usare JavaScript per impostare o modificare la meta description e l’elemento title.» Il requisito è
che finiscano nel DOM renderizzato che Google valuta, non solo che appaiano brevemente. Dai a ogni
pagina il proprio titolo e la propria descrizione che si aggiornano quando cambia la route.
Veri link <a href> tra le route
Rendi i link tra le rotte vere ancore con attributi href, non elementi gestiti al clic come
<div>. Google scopre gli URL estraendo gli href; un <div onClick> che naviga
tramite il router è invisibile all’estrazione dei link del crawler. Non fare affidamento
nemmeno sullo stato lato client per trasportare contenuti attraverso quelle navigazioni: il renderer di Google afferma “WRS does
not retain state across page loads: Local Storage and Session Storage data are cleared
across page loads. HTTP Cookies are cleared across page loads.” (traduzione) «WRS non conserva lo stato tra i caricamenti delle pagine: i dati di Local Storage e Session Storage vengono cancellati tra un caricamento e l’altro. Anche i cookie HTTP vengono cancellati.»
Generazione della sitemap per le route SPA
Non esiste un formato speciale di “sitemap SPA” — è un problema di contabilità
Il protocollo sitemap è invariato per le SPA. Il lavoro vero è assicurarsi che ogni route che elenchi in modo indipendente risolva in HTML reale, unico e renderizzato. Una sitemap di 500 route lato client è inutile se quelle route restituiscono tutte lo stesso guscio. Quindi la sitemap è a valle della tua strategia di rendering, non un suo sostituto.
Gestione delle route dinamiche/parametriche (/product/:id)
Per le app con route parametriche, non puoi mantenere l’elenco a mano. Il generatore di sitemap
deve essere eseguito sulla stessa fonte dati che usa l’app — uno script di build o un
endpoint del server che enumera ogni id — così la sitemap e l’app non divergono mai.
Quegli URL enumerati valgono la pena di essere elencati solo una volta che sono SSR’d o prerenderizzati.
Mantenere la sitemap sincronizzata con ciò che è risolvibile dal server
Rigenera la sitemap come parte della tua build o con una pianificazione legata alla tua fonte di contenuti.
Una route che restituisce 404 (o peggio, soft-404 a 200) ma che è nella tua sitemap è un
segnale di crawl-budget e di qualità che non vuoi.
Testare ciò che Google vede davvero
Non controllare a campione un singolo URL. Il punto centrale del problema SPA è che le route possono differire nel browser ma non sul filo, quindi testa più route a livello di HTML grezzo:
- Recupera l’HTML grezzo per ogni route con
curl(un user-agent di Googlebot dove pertinente) e conferma che il contenuto sia unico per URL, non la shell condivisa. - URL Inspection in Search Console — confronta l’HTML crawlerizzato/rendered per diverse route e conferma che title, description e canonical per route siano presenti.
- Conferma che le route di errore restituiscano il segnale giusto — una vista “non trovato” dovrebbe
reindirizzare a un vero stato di errore o portare
noindexnel DOM rendered.
Miti comuni sulla SEO per SPA
- “Google non può indicizzare le SPA.” Superato. Google esegue Chromium evergreen e generalmente renderizza il contenuto delle SPA. I rischi reali sono specifici: soft 404, hash routing, timeout di rendering e crawler non Google (Bing meno affidabile, la maggior parte dei crawler AI per niente).
- “Aggiungere la History API risolve la SEO per SPA.” Risolve solo l’indirizzabilità. Se il server restituisce ancora la stessa shell per ogni route, Google deve comunque eseguire JS per vedere qualcosa.
- “L’hash routing funziona ancora come fallback.” Deprecato dal 2015 e sconsigliato da allora.
- “Il rendering lato client è una penalità di ranking.” Non esiste una penalità CSR diretta. Il danno è indiretto — rendering fallito/ritardato, soft 404 e clustering duplicato da timeout di rendering riducono ciò che viene indicizzato.
- “Il pre-rendering per i bot è cloaking.” Non quando il contenuto corrisponde a ciò che gli utenti vedono alla fine — solo il quando/dove del rendering differisce, non il contenuto.
- “Serve un generatore di sitemap speciale per SPA.” Non esiste un formato speciale; il lavoro è far sì che ogni route elencata risolva in HTML reale.
FAQ
Google può indicizzare una single-page application? Sì, se ogni route risolve in HTML reale, unico (tramite SSR/prerendering) a un URL reale. Le SPA solo client spesso no.
Mi serve SSR, o il rendering lato client è mai accettabile? Il CSR può funzionare per contenuti che non devono posizionarsi, ma per qualsiasi cosa vuoi indicizzare in modo affidabile, prerenderizzala o usa SSR — non scommettere che il renderer esegua il tuo JS in tempo.
L’hash (#!) routing è dannoso per la SEO? Sì — il frammento non arriva mai al server, quindi
Google non può risolvere in modo affidabile quegli URL. Usa la History API.
Ogni route ha bisogno del proprio tag canonical? Sì, nel DOM rendered — e assicurati
che nulla di più restrittivo (un noindex fuori posto o un canonical errato) venga inviato nella shell
raw.
Un servizio di prerendering è cloaking? No, a condizione che il contenuto servito ai bot corrisponda a ciò che vedono gli utenti.
Dovrei usare un meta-framework invece di costruire il routing da solo? Per una nuova build, di solito sì — Next.js/Nuxt/Angular-SSR/SvelteKit ti danno SSR/SSG e metadati per route gratuitamente.
Perché la mia SPA restituisce 200 per pagine che non esistono? Perché il router
lato client mantiene lo stato 200 originale per le navigazioni virtuali. Risolvilo con un reindirizzamento
JS a un vero stato di errore o un noindex rendered.
Riepilogo AI
Una sintesi della versione Advanced:
- SEO per SPA = routing lato client specificamente (React Router, Vue Router, Angular Router). Una SPA è definita dal caricamento di un singolo documento e dallo scambio di viste con JavaScript — un “app shell” che invia HTML quasi vuoto per ogni URL è un’implementazione comune, non una regola che ogni SPA segue; controlla cosa restituisce effettivamente una richiesta diretta piuttosto che assumere.
- Il rischio principale: il routing lato client può cambiare ciò che l’utente vede senza
cambiare ciò che il server restituirebbe. Su una SPA a guscio vuoto, recuperare
/productse/aboutdirettamente può restituire HTML grezzo byte-identico. - Cinque stati vengono confusi: risposta del server, DOM renderizzato, elaborazione di Search, navigazione completa del browser e navigazione soft del browser. Una transizione dell’API History cambia l’URL e l’interfaccia ma non crea di per sé una nuova risposta del server o uno stato.
- Tre sintomi di un guscio vuoto: il problema dell’app shell, gli errori soft “pagina non trovata” (i router mantengono un
200per “non trovato” — le due soluzioni documentate di Google sono reindirizzare a un URL che a sua volta restituisce uno stato di errore, o unnoindexaggiunto via JS; unnoindexiniziale può causare l’omissione del rendering), e i duplicati da shell condivisa — di cui il “render timeout” (Illyes) è una possibile causa, non una diagnosi automatica senza prove di richiesta/rendering/Search. - Il routing con hash fallisce meccanicamente: il frammento dopo
#non raggiunge mai il server, quindi il server non può differenziarsi per rotta. Deprecato da Google nel 2015; l’API History fornisce indirizzabilità, non una soluzione completa di indicizzabilità. - La soluzione ha due metà indipendenti quando il problema è un guscio vuoto: indirizzabilità (API History, URL unici per rotta) e disponibilità dei contenuti (SSR, prerendering/SSG, o un meta-framework) — ma SSR non è un requisito universale; basa la decisione su se la rotta deve posizionarsi, su cosa restituisce già direttamente, e su quali crawler devono renderizzarla.
- Per rotta: canonical/title/description propri nel DOM renderizzato; attenzione alla trappola
della “direttiva più restrittiva” dove un
noindex/canonical in HTML grezzo sovrascrive il tuo JS. Link reali<a href>; non fare affidamento sullo stato del client (WRS cancella storage/cookie tra i caricamenti). - Sitemap: elencare una rotta non prova che si risolva — non esiste un formato speciale; ogni rotta elencata deve risolversi indipendentemente in HTML reale, e le rotte dinamiche necessitano di un generatore collegato alla fonte dati dell’app.
- Il rendering dinamico è un ripiego accettato da Google/Bing, non un’architettura a lungo termine.
- Testa più rotte a livello di HTML grezzo (richieste dirette), non solo navigando all’interno dell’app.
Documentazione ufficiale
Documentazione di fonte primaria dai motori di ricerca.
- Comprendi le basi del SEO JavaScript — il modello app-shell, “Usa l’API History invece dei frammenti,” e l’impostazione di title/description via JS.
- Risolvi i problemi JavaScript legati alla Ricerca — la sezione sui soft 404 nelle SPA, “Non usare frammenti URL per caricare contenuti diversi,” e la raccomandazione sull’API History.
- Rendering dinamico come soluzione temporanea — perché il rendering dinamico è un ripiego, non una soluzione a lungo termine.
- Deprecazione del nostro schema di crawling AJAX (2015) — la fine formale del crawling con hash-bang e la raccomandazione su pushState.
- Crea e invia una sitemap — protocollo generale per le sitemap (non esiste un formato specifico per le SPA).
Bing / Microsoft
- Serie bingbot: JavaScript, Rendering Dinamico e Cloaking. Oh Mio! — le capacità di rendering JS di bingbot e la sua posizione sul rendering dinamico.
Citazioni dalla fonte
Dichiarazioni ufficiali di Google e Bing. Ogni link è un deep link che salta al passaggio citato nella pagina di origine.
Google — il problema dell’app shell
- “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (traduzione) «Alcuni siti JavaScript possono utilizzare il modello app shell in cui l’HTML iniziale non contiene il contenuto effettivo e Google deve eseguire JavaScript prima di poter vedere il contenuto effettivo della pagina.» Vai alla citazione
Google — routing tramite hash/frammenti e History API
- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (traduzione) «Una SPA può utilizzare frammenti di URL (ad esempio https://example.com/#/products) per caricare viste diverse.» Vai alla citazione
- “We recommend using the History API to load different content based on the URL in a SPA.” (traduzione) «Raccomandiamo di utilizzare la History API per caricare contenuti diversi in base all’URL in una SPA.» Vai alla citazione (Nel documento live “History API” è un link, quindi questo deep link punta alla frase introduttiva; la frase completa è presente verbatim nell’ordine.)
- “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (traduzione) «non utilizzare frammenti per caricare contenuti di pagina diversi. L’esempio seguente è una cattiva pratica, perché Googlebot non può risolvere in modo affidabile gli URL.» Vai alla citazione
Google — errori soft “pagina non trovata” nelle SPA
- “In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” (traduzione) «In un’applicazione a pagina singola (SPA), questo può essere particolarmente difficile. Per evitare che le pagine di errore vengano indicizzate, puoi utilizzare una o entrambe le seguenti strategie.» Vai alla citazione
- “When a SPA is using client-side JavaScript to handle errors they often report a
200HTTP status code instead of the appropriate status code.” (traduzione) «Quando una SPA utilizza JavaScript lato client per gestire gli errori, spesso riporta un codice di stato HTTP 200 invece del codice di stato appropriato.» Vai alla citazione
Google — meta tag tramite JavaScript e rendering senza stato
- “You can use JavaScript to set or change the meta description as well as the
<title>element.” (traduzione) «Puoi utilizzare JavaScript per impostare o modificare la meta description e l’elemento title.» Vai alla citazione - “WRS does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” (traduzione) «WRS non mantiene lo stato tra i caricamenti delle pagine: i dati di Local Storage e Session Storage vengono cancellati tra i caricamenti delle pagine. I cookie HTTP vengono cancellati tra i caricamenti delle pagine.» Vai alla citazione
Google — deprecazione del crawling hash-bang (2015)
- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (traduzione) «In breve: non raccomandiamo più la proposta di crawling AJAX che abbiamo fatto nel 2009.» Vai alla citazione
- “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers.” (traduzione) «I tempi sono cambiati. Oggi, purché tu non stia bloccando Googlebot dal crawling dei tuoi file JavaScript o CSS, siamo generalmente in grado di rendere e comprendere le tue pagine web come i browser moderni.» Vai alla citazione
Google — il rendering dinamico è una soluzione temporanea (riportato; la formulazione corrisponde a quanto già citato nella guida più ampia sul SEO JavaScript di questo sito, non riverificato in modo indipendente in questa passata)
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” (traduzione) «Il rendering dinamico era una soluzione temporanea e non una soluzione a lungo termine per i problemi con i contenuti generati da JavaScript nei motori di ricerca.» Vai alla citazione
Bing — bingbot e JavaScript
- “bingbot is generally able to render JavaScript.” (traduzione) «bingbot è generalmente in grado di eseguire il rendering di JavaScript.» (Serie su bingbot, Bing Webmaster Blog.) Leggi il post
- “bingbot does not necessarily support all the same JavaScript frameworks that are supported in the latest version of your favorite modern browser.” (traduzione) «bingbot non supporta necessariamente tutti gli stessi framework JavaScript supportati nell’ultima versione del tuo browser moderno preferito.» (Stesso post.) Leggi il post
Gary Illyes, Google — i timeout di rendering creano duplicati (riportato tramite un post su LinkedIn, che resiste alla verifica automatizzata; trattare come fonte secondaria ad alta affidabilità)
- “the centerpiece took forever to load, so rendering timed out… and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (traduzione) «il pezzo centrale ha impiegato un’eternità a caricarsi, quindi il rendering è andato in timeout… e siamo rimasti con un mucchio di pagine che avevano solo il boilerplate. Con solo il boilerplate, quelle pagine sono duplicati.» Leggi il post
Quale percorso di rendering dovrei scegliere?
Procedi dall’alto verso il basso. La domanda è sempre “il crawler ottiene HTML reale per questa rotta senza eseguire il mio JavaScript?” — e inizia confermando che la rotta debba effettivamente essere indicizzata e controllando cosa restituisce già una richiesta diretta senza JavaScript; non ogni rotta ha bisogno di SSR, e alcune restituiscono già HTML utilizzabile.
1. Stai iniziando una nuova build?
- Sì → Usa un meta-framework (Next.js, Nuxt, Angular con SSR, SvelteKit, Remix). SSR/SSG e metadati per rotta sono integrati. Fermati qui.
- No → Continua.
2. I tuoi contenuti cambiano per utente / per richiesta?
- No (contenuti per lo più statici) → Prerender / SSG al momento della build. La soluzione robusta più semplice — ogni rotta è HTML reale su disco.
- Sì → Rendering lato server (SSR) così ogni richiesta restituisce HTML specifico per la rotta.
3. Non puoi fare SSR o prerendering adesso (SPA legacy fatta a mano)?
- Usa il rendering dinamico come ponte temporaneo — servi ai crawler un’istantanea renderizzata. Pianifica la migrazione a SSR/SSG; non trattarlo come la destinazione.
4. Qualunque percorso tu abbia scelto, conferma tutto questo per ogni rotta:
- URL reale tramite l’API History (niente frammenti
#!). - Titolo univoco + meta description + canonical nel DOM renderizzato.
- Niente di più restrittivo (
noindex, canonical errato) incorporato nello shell grezzo. - Collegamenti
<a href>reali tra le rotte. - Le rotte di errore restituiscono un vero stato di errore o un
noindexrenderizzato (niente errori soft “pagina non trovata”). - Ogni rotta nella sitemap si risolve in modo indipendente in HTML reale.
Checklist SEO per SPA
Indirizzabilità
- Ogni vista ha un URL reale tramite l’API History (
/products), non un frammento (/#/products). - I collegamenti tra rotte sono ancore
<a href>reali, non gestori<div onClick>.
Disponibilità dei contenuti
- Ogni rotta restituisce HTML reale e univoco senza che il crawler esegua il tuo JavaScript (SSR, prerendering/SSG o un meta-framework).
- I contenuti della rotta si caricano prima che la finestra di rendering si chiuda (evita duplicati solo boilerplate).
Indicizzabilità per rotta
-
<title>e meta description unici per rotta, presenti nel DOM renderizzato. -
rel=canonicalunico per rotta nel DOM renderizzato. - Nessun
noindex/canonical segnaposto errato nella shell grezza che un segnale più restrittivo potrebbe bloccare.
Errori
- Le rotte “non trovate” reindirizzano a un URL il cui server restituisce un vero stato di errore oppure portano
noindexaggiunto da JavaScript (niente errori soft “pagina non trovata” a200). - Se si usa la rotta
noindex, conferma che venga aggiunta dopo che l’app decide che la rotta non è valida, non presente dal primo paint — unnoindexiniziale può far sì che Google salti il rendering della pagina.
Sitemap
- Ogni rotta elencata risolve in modo indipendente a HTML reale.
- Le rotte dinamiche (
/product/:id) enumerate dalla fonte dati dell’app, non mantenute a mano.
Verifica
- HTML grezzo controllato su più rotte (curl), confermando contenuto unico per URL.
- URL Inspection conferma contenuto renderizzato, titolo, descrizione e canonical per rotta.
I modelli mentali
1. Le due metà che non devi confondere. L’indirizzabilità (History API, URL unici, niente frammenti) e la disponibilità dei contenuti (SSR, prerendering o un meta-framework) sono indipendenti. URL puliti senza HTML per rotta = una sitemap ordinata di shell vuote. Servono entrambe.
2. “Cosa restituirebbe il server, fresco?”
L’intero problema delle SPA è che la vista del browser diverge dalla risposta del server. Per qualsiasi rotta, chiediti cosa ottieni con un curl senza esecuzione di JS. Se è la shell, anche il crawler potrebbe vedere la shell.
3. Il codice di stato predefinito è 200 — inclusi gli errori.
I router lato client mantengono il 200 originale. Considera ogni vista “errore” come un soft 404 finché non hai deliberatamente fatto sì che restituisca un vero stato di errore o un noindex renderizzato.
4. La trappola della direttiva più restrittiva.
Google riconcilia grezzo e renderizzato prendendo il segnale più restrittivo. Un noindex o un canonical errato nella shell può sovrascrivere il tuo “fix” via JS. Controlla l’HTML grezzo, non solo il DOM renderizzato.
5. Meta-framework prima di tutto per le nuove build. Implementare a mano il routing lato client significa gestire tutti questi problemi. Scegliere un framework che fa SSR/SSG e metadati per rotta significa scegliere di non averli.
Cheat sheet SEO per SPA
Routing
| Approccio | Esempio URL | Raggiunge il server? | Sicuro per Google? |
|---|---|---|---|
| History API | /products | Sì | Sì (consigliato) |
| Hash routing | /#/products | No (il frammento non viene mai inviato) | No — deprecato 2015 |
Hash-bang (#!) | /#!/products | No | No — schema AJAX legacy, deprecato |
Strategie di rendering
| Strategia | Il crawler ottiene HTML reale senza eseguire JS? | Ideale per |
|---|---|---|
| SSR | Sì | Contenuti per utente / per richiesta |
| Prerendering / SSG | Sì | Contenuti per lo più statici |
| Meta-framework (Next/Nuxt/ecc.) | Sì (integrato) | Nuove build |
| Dynamic rendering | Sì, solo per i bot | Ponte temporaneo su SPA legacy |
| CSR puro (solo client) | No | Niente che vuoi indicizzare in modo affidabile |
Elementi indispensabili per rotta (tutti nel DOM renderizzato)
- URL unico (History API) ·
<title>unico · meta description unica ·rel=canonicalunico · link<a href>reali · rotte di errore con stato reale onoindex.
Fatti rapidi
- Il frammento dopo
#non viene mai inviato al server — è per questo che l’hash routing fallisce. - I router lato client mantengono
200per tutto, incluso “non trovato” → errore soft “pagina non trovata”. - Google riconcilia grezzo vs. renderizzato prendendo il segnale più restrittivo.
- Non esiste un formato speciale di sitemap per SPA — ogni rotta elencata deve risolvere a HTML reale.
Anti-pattern SEO per SPA
Pubblicare una SPA CSR pura e inviare una sitemap completa. La sitemap elenca 500 rotte; tutte e 500 restituiscono la stessa shell a un fetch fresco. La sitemap non fa esistere i contenuti — è il rendering che lo fa.
Aggiungere la History API e considerare il lavoro finito. Gli URL puliti risolvono l’indirizzabilità, non i contenuti. Senza SSR/prerendering il server restituisce ancora la shell per ogni rotta.
Routing hash / hash-bang “come fallback.” Il frammento non raggiunge mai il server, quindi il server non può differenziare per rotta. Deprecato dal 2015; non è un fallback, ma un vicolo cieco.
Navigare con <div onClick> invece di <a href>.
Google estrae gli href per scoprire gli URL. La navigazione tramite gestore di clic è invisibile all’estrazione
dei link, quindi quelle rotte potrebbero non essere mai trovate.
Un noindex o canonical segnaposto nello shell grezzo “che JS sovrascriverà.”
Google prende il più restrittivo tra grezzo e renderizzato. La direttiva dello shell può vincere e
sopprimere silenziosamente la rotta.
Restituire 200 per viste “non trovato”.
Gli errori soft “pagina non trovata” fanno indicizzare pagine sottili o vuote. Reindirizza a un vero stato di errore o aggiungi un
noindex renderizzato.
Caricare il contenuto della rotta lentamente, dopo il boilerplate. I timeout di rendering lasciano solo lo shell condiviso, e ogni rotta collassa in un duplicato di ogni altra (Illyes). Carica prima il contenuto principale.
Affidarsi allo stato del client per trasportare il contenuto tra le navigazioni. Il renderer cancella Local/Session Storage e i cookie tra i caricamenti di pagina — il contenuto che esiste solo nello stato del client non sarà presente quando Google renderizza la rotta successiva.
Problemi comuni di indicizzazione SPA
Ogni rotta restituisce lo stesso HTML
Sintomo: /products e /about hanno viste browser diverse ma risposte grezze identiche.
Causa probabile: Il routing History API fornisce indirizzabilità senza SSR
o prerendering. Correzione: Produci HTML specifico per rotta e conferma che una richiesta diretta a
ciascun percorso contenga il proprio titolo, testo del corpo e metadati.
Le rotte mancanti appaiono come pagine di successo
Sintomo: Una rotta inesistente mostra una vista non trovato mentre restituisce 200.
Causa probabile: Il router client gestisce l’errore dopo che il server ha già inviato uno
shell di successo. Correzione: Restituisci lo stato server corretto; se non può ancora essere distribuito,
reindirizza a un URL con un vero stato di errore o renderizza noindex. Conferma con una nuova
richiesta diretta, non con una navigazione in-app.
Le rotte collassano in duplicati dopo il rendering
Sintomo: I motori di ricerca raggruppano URL distinti o mantengono solo la navigazione condivisa. Causa probabile: Il contenuto della rotta viene caricato troppo tardi e il rendering cattura il boilerplate. Correzione: Dai priorità al contenuto primario nella risposta del server o nel percorso di rendering più precoce. Conferma che diverse rotte espongano contenuto unico prima che gli script opzionali finiscano.
Testare cosa restituisce effettivamente il server per rotta
La trappola SPA è che le rotte differiscono nel browser ma non sul filo. Controlla più rotte a livello di HTML grezzo, non una sola.
Fetch raw HTML per route (macOS / Linux)
# Fetch a few routes with a Googlebot UA and compare — they should NOT be identical
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
for path in / /products /about /product/123; do
echo "=== $path ==="
curl -s -A "$UA" "https://example.com$path" | wc -c # byte counts should differ
doneConfronta l’HTML grezzo di due route (sono lo stesso guscio?)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
diff <(curl -s -A "$UA" https://example.com/products) \
<(curl -s -A "$UA" https://example.com/about) \
&& echo "IDENTICAL — bare shell, content is client-only" \
|| echo "Different — routes return distinct HTML (good)"Controlla il titolo e canonical per rotta nella risposta grezza (grep / regex)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
curl -s -A "$UA" https://example.com/products \
| grep -Eio '<title>[^<]*</title>|<link[^>]+rel=["'"'"']canonical["'"'"'][^>]*>'Controlla lo stato HTTP di una rotta “non trovata” (rilevatore di soft 404)
# A missing route should NOT return 200
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-route-does-not-existNella console DevTools del browser — ispeziona il titolo/canonical renderizzato
// Run on each route after client-side navigation to confirm JS set them
console.log('title:', document.title);
console.log('description:',
document.querySelector('meta[name="description"]')?.content);
console.log('canonical:',
document.querySelector('link[rel="canonical"]')?.href);Bookmarklet — segnala i “link” con gestore di clic che non sono veri anchor
javascript:(()=>{const bad=[...document.querySelectorAll('[onclick],div[role="link"]')]
.filter(e=>!e.closest('a[href]'));
bad.forEach(e=>e.style.outline='3px solid red');
alert(bad.length+' non-anchor clickable(s) outlined — these are invisible to link extraction');})();XPath — trova i link con frammento/hash che un crawler non può risolvere (incolla nella console DevTools)
$x('//a[starts-with(@href, "#") or contains(@href, "/#/")]')
.map(a => a.getAttribute('href'));
// Any results are hash-routed links; migrate them to History API paths. Strumenti per l’audit di una SPA
- URL Inspection (Google Search Console) — vedi l’HTML strisciato rispetto a quello renderizzato per una rotta e conferma che titolo, descrizione e canonical per rotta arrivino davvero.
- Rich Results Test / URL Inspection “View crawled page” — il rendering di Google di un singolo URL, utile per confermare che il contenuto sopravviva al rendering.
curlcon un user agent Googlebot — il modo più rapido per confrontare l’HTML grezzo di diverse rotte e individuare una shell condivisa.- Screaming Frog SEO Spider (modalità rendering JavaScript) — striscia il sito con e senza rendering per confrontare contenuto e titoli grezzi rispetto a quelli renderizzati su ogni rotta.
- Ahrefs Site Audit — evidenzia problemi di indicizzabilità, titoli e canonical mancanti/duplicati e pattern simili a soft-404 tra le rotte.
- DebugBear — monitoraggio delle prestazioni orientato alle SPA; i tempi di rendering contano perché le rotte lente possono andare in timeout trasformandosi in duplicati boilerplate.
- Bing Webmaster Tools — URL Inspection — bingbot renderizza JS in modo meno coerente di Googlebot, quindi conferma le tue rotte anche dal lato di Bing.
Dimostra che una rotta SPA è indicizzabile in modo indipendente
Testa la parità HTML con ingresso diretto
Test da eseguire: Apri rotte rappresentative in una nuova sessione e recupera gli stessi URL con curl. Risultato atteso: Ogni URL restituisce il proprio contenuto primario e i segnali head senza stato precedente dell’app. Interpretazione del fallimento: La rotta dipende dalla navigazione client o dallo stato salvato. Finestra di monitoraggio: Immediata. Trigger di rollback: Una rotta funziona solo dopo essere entrati dalla homepage.
Testa la gestione degli stati di errore
Test da eseguire: Richiedi direttamente una rotta nota come non valida e ispeziona sia lo stato sia le direttive renderizzate. Risultato atteso: Un vero stato di errore, oppure il fallback documentato di un noindex renderizzato o un redirect a una risposta di errore. Interpretazione del fallimento: La SPA sta generando errori soft “pagina non trovata”. Finestra di monitoraggio: Immediata. Trigger di rollback: I percorsi non validi vengono pubblicati come pagine 200 indicizzabili.
Testa l’isolamento dei metadati
Test da eseguire: Confronta titolo, tag robots e canonical grezzi e renderizzati su almeno tre rotte. Risultato atteso: Ogni rotta ha un set previsto e internamente coerente. Interpretazione del fallimento: La shell condivisa fa trapelare metadati tra le rotte oppure JS li sovrascrive troppo tardi. Finestra di monitoraggio: Immediata in locale e dopo il re-strisciamento in URL Inspection. Trigger di rollback: Qualsiasi rotta eredita il canonical di un’altra rotta o una direttiva restrittiva della shell.
Mettiti alla prova: SEO per SPA
Cinque domande rapide su come rendere le applicazioni a pagina singola strisciabili e indicizzabili. Scegli una risposta per ciascuna, poi controlla.
Risorse che meritano il tuo tempo
I miei scritti correlati
- JavaScript SEO Issues & Best Practices — la mia guida completa al JS-SEO, incluse le sezioni “non usare frammenti negli URL” e app-shell/contenuti duplicati su cui questo articolo si basa.
- The Beginner’s Guide to Technical SEO — dove rendering e crawling si inseriscono nel quadro più ampio.
I miei interventi
- Come funziona la Ricerca (SlideShare) — la mia spiegazione di crawling, rendering, indicizzazione e ranking, ovvero la pipeline che una SPA deve superare. (Vale la mia dichiarazione di esonero: “This is my understanding of systems… not going to be 100% complete or accurate.” (traduzione) «Questa è la mia comprensione dei sistemi… non sarà completa o accurata al 100%.»)
Dal settore
- Google — Nozioni di base sulla SEO per JavaScript — il modello app shell e la raccomandazione “Use the History API instead of fragments” (traduzione) «Usa la History API invece dei frammenti».
- Google — Risolvere i problemi JavaScript legati alla Ricerca — la sezione sui soft 404 nelle SPA e la raccomandazione sulla History API.
- Google — Deprecating our AJAX crawling scheme (2015) — la fine formale del crawling con hash-bang.
- Bing — Serie su bingbot: JavaScript, rendering dinamico e cloaking — la posizione di bingbot sul rendering JS.
- Usare un servizio di prerendering per pagine HTML vuote nello sviluppo di SPA non è cloaking (Search Engine Roundtable, 2015) — copertura delle dichiarazioni di Gary Illyes sul fatto che il prerendering delle SPA non sia cloaking. (Parafrasi secondaria di SER, datata 2015.)
- SEO per applicazioni a pagina singola (Nuxt SEO) — una panoramica lato framework degli stessi problemi.
- Come ottimizzare le applicazioni a pagina singola per la SEO (DebugBear) — l’angolo rendering/prestazioni sull’indicizzabilità delle SPA.
- SPA (glossario) (MDN) — una definizione neutra del pattern single-page application.
Video
- Google Search Central (YouTube) — la serie JavaScript SEO di Martin Splitt copre rendering, History API e le modalità di errore delle SPA discusse qui. Canale
Cronologia modifiche
Aggiornato il 20 ago 2026.
Riepilogo editoriale e dettagli registrati delle modifiche.Dettagli delle modifiche
-
Le note dettagliate sulle modifiche sono attualmente disponibili in inglese.
Confronto completo non disponibile — non è stata archiviata alcuna istantanea precedente per questa revisione.
Aggiornato il 18 lug 2026.
Riepilogo editoriale e dettagli registrati delle modifiche.Dettagli delle modifiche
-
Le note dettagliate sulle modifiche sono attualmente disponibili in inglese.
-
Le note dettagliate sulle modifiche sono attualmente disponibili in inglese.
-
Le note dettagliate sulle modifiche sono attualmente disponibili in inglese.
-
Le note dettagliate sulle modifiche sono attualmente disponibili in inglese.
-
Le note dettagliate sulle modifiche sono attualmente disponibili in inglese.
Confronto completo non disponibile — non è stata archiviata alcuna istantanea precedente per questa revisione.