Guide SPA SEO
Comment faire application monopages (React Router, Vue Router, Angular Router) crawlable et indexable — le app-shell et soft-404 problèmes, History API vs hash routing, per-route HTML via SSR/prerendering, per-route canonicals et titles, et sitemap generation pour client-side routes.
A application monopage charge un document et swaps views avec JavaScript au lieu de requesting a nouveau page depuis le serveur. Nombreux SPAs implement que avec an 'app shell' — le serveur renvoie un URL's worth de réel HTML, a near-empty shell, jusqu’à JS renders chaque route — mais c’est a courant implementation choice, pas a règle chaque SPA suit; vérifier ce que a direct requête à chaque route en réalité renvoie avant assuming il. Le corriger, où a bare app-shell setup est le problème, a deux independent halves: addressability (History API, pas hash/#! fragments, donc chaque view a a réel URL — a navigateur soft navigation via le History API changements l’URL et UI mais ne fait pas itself créer a nouveau serveur réponse) et contenu availability (SSR, prerendering, or a meta-framework donc chaque de ceux URLs peut retourner unique HTML on requête). On top de que, vérifier chaque route's titre, canonical, et robots state les deux on direct entry et après rendu, handle soft 404s (routers tend à garder a 200 status pour 'introuvable' views — redirection à une URL que itself 404s, or ajouter a rendu noindex, though an initial noindex peut causer rendu à être skipped), et assurez-vous chaque route dans votre sitemap en réalité resolves à réel, indexable HTML — il y a aucun special SPA sitemap format.
TL;DR — A application monopage (SPA) charge un page depuis le serveur et alors utilise JavaScript à switch “pages” sans a complet reload. Le catch: nombreux SPAs send le même near-empty starting page aucun matter qui URL a moteur de recherche demande pour — c’est a courant risque de comment SPAs sont généralement construit, pas a guarantee de chaque SPA. À corriger il, assurez-vous chaque route a son propre réel URL et son propre réel HTML — généralement par rendu pages on le serveur or pre-building les.
Ce que an SPA is
A application monopage commonly mises à jour client-side views et routes sans a complet document navigation. Preuve à l’appui de cette affirmation Primary standard or official documentation supporting the adjacent article claim. Portée : Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Niveau de confiance : élevé · Vérifié : MDN: Single-page application Google peut rendre JavaScript SPAs, mais crawlable URLs, liens, status handling, et rendu contenu remain necessary. Preuve à l’appui de cette affirmation Primary standard or official documentation supporting the adjacent article claim. Portée : Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Niveau de confiance : élevé · Vérifié : Google: JavaScript SEO basics
«A single-page application is a website built as one HTML page. When you click around — from the products page to the about page — JavaScript swaps out what’s on screen instead of asking the server for a whole new page. React (with React Router), Vue (with Vue Router), and Angular all work this way by default. It feels fast and app-like, which is why it’s popular. » (Traduction) (Résumé en français de la section six, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Le risque est ce que le serveur sends. Nombreux SPAs utiliser an “app shell” implementation:
le premier temps anyone (notamment Google) charge le app, le serveur renvoie a
mostly-empty shell, et le contenu réel obtient construit dans le navigateur afterward. c’est
un courant façon à construire an SPA — pas le definition de un, et pas quelque chose chaque
SPA fait. Mais quand a site fait ship a bare app shell, le échec mode est réel: si
Google demande le serveur pour /about et /products séparément, il peut obtenir le exact
même blank shell pour les deux, parce que rien à propos de le route modifié ce que le serveur
sent. Le façon à know si votre app est dans que position est à vérifier ce que a direct
requête à chaque route en réalité renvoie — pas à assume il depuis le fact que c’est an
SPA.
Pourquoi que hurts SEO
Moteur de recherches besoin to voir votre content to rank it. With a bare SPA, three choses tend to go incorrect:
- Chaque URL semble le même à le serveur. Deep liens, shares, et robots d’exploration tout land on le même shell.
- 404 pages encore dire “200 OK.” A JavaScript router peut montrer a “pas trouvé” screen pendant que lune page technically rapports success, donc Google peut indexer vide pages.
- Le incorrect URLs. Older SPAs utilisé addresses comme
example.com/#/products. Google ne peut pas reliably indexer ceux.
Comment corriger it (La version courte)
- Donner chaque view a réel URL en utilisant le navigateur’s History API (clean paths comme
/products), pas#-fondé ones. - Send réel HTML pour chaque URL. Rendre pages on le serveur (SSR) or pre-construire les ahead de temps (prerendering). Si vous êtes starting fresh, a framework que fait ce pour vous — Suivant.js, Nuxt, SvelteKit, Angular’s SSR — saves vous le trouble.
- Donner chaque page son propre titre et description que modifier quand le route changements.
- Faire liens réel liens (
<a href>), pas clickable<div>s.
Vouloir le mechanics — pourquoi hash routing fails, le “most restrictive directive” trap avec canonicals, et comment generate a sitemap pour client-side routes? Switch à le Avancé tab.
TL;DR — An SPA’s core SEO risque n’est pas “JavaScript” dans le abstract — c’est que client-side routing changements ce que le utilisateur voit sans modification ce que le serveur voudrait retourner. Le corriger a deux independent halves personnes constantly conflate: addressability (History API, unique per-route URLs, aucun
#!fragments) et contenu availability (SSR, prerendering/SSG, or a meta-framework). Faire seulement le premier et vous obtenir a tidy sitemap de URLs que tout rendre le même shell. On top de les deux, chaque route nécessite son propre canonical/titre/description dans le rendu DOM, vous ont à handle soft 404s que garder a200, et chaque route dans le sitemap a à independently resolve à réel HTML.
Ce que SPA SEO en réalité is
SPA is an application architecture, pas a guaranteed SEO échec state. Preuve à l’appui de cette affirmation Primary standard or official documentation supporting the adjacent article claim. Portée : Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Niveau de confiance : élevé · Vérifié : MDN: Single-page application Server rendering, prerendering, or carefully implemented client rendering peut expose content, but none guarantees indexation. Preuve à l’appui de cette affirmation Primary standard or official documentation supporting the adjacent article claim. Portée : Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Niveau de confiance : élevé · Vérifié : Google: JavaScript SEO basics
Ce est le deep dive on application monopages précisément — client-side routing avec React Router, Vue Router, or Angular Router, où après le premier charger le serveur jamais sends un autre complet page. Il sits alongside le broader JavaScript SEO guide (qui covers parity, lazy-loading, infinite scroll, et JS rendu généralement) et le per-framework écrire-ups pour React, Suivant.js, Nuxt, Angular, Vue, Svelte, et Astro. Ici Je’m seulement interested dans le routing couche et ce que il fait à exploration et indexation.
Pourquoi bare client-side-routed SPAs échouer at SEO
Un URL, un HTML réponse — the app-shell problem
«Google describes the failure mode precisely: “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.” In a bare
SPA, the “app shell” is all the server ever returns. Fetch /products fresh and you
get the same near-empty document you’d get for /about. The content only diverges
after the browser runs your JavaScript and your router decides what to show.
» (Traduction) (Résumé en français de la section vingt-deux, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
c’est le entier problème dans un sentence: client-side routing changements ce que le utilisateur voit sans modification ce que le serveur voudrait retourner. Tout downstream — soft 404s, contenu dupliqué, manquant titles — est a symptom de que.
Le serveur jamais sees qui “page” was requested (pourquoi hash routing fails)
«Older SPAs load views from URL fragments — example.com/#/products. Google’s exact
words: “A SPA may use URL fragments (for example https://example.com/#/products) for
loading different views.” This fails for a specific, mechanical reason: browsers
never send the fragment (anything after #) to the server in the HTTP request. The
server literally cannot know which “page” was requested, so it can’t return different
content or a different status code for it. A crawler that fetches the URL once gets
identical HTML regardless of the fragment.
» (Traduction) (Résumé en français de la section vingt-cinq, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
c’est aussi pourquoi Google formally deprecated son 2009 AJAX-exploration scheme back dans
October 2015: “En bref: Nous sont ne … plus recommending le AJAX exploration proposal nous
fait back dans 2009.” Le old _escaped_fragment_ workaround let serveurs pre-rendre
fragment routes on requête — mais il patched autour le routing problème plutôt que
fixing il. Google’s propre recommendation replacing il est le History API, covered ci-dessous.
Soft 404s — client-side routers garder a 200 pour everything
«This one is close to inevitable in a bare SPA. Client-side routers, by design, keep the
original page’s 200 status for every virtual navigation — including “not found”
states. Google flags it explicitly: “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.” And the why: “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.”
» (Traduction) (Résumé en français de la section vingt-huit, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Le résultat est vide or erreur views getting indexé as thin 200 pages. Google
documents exactly deux strategies ici, precisely: redirection (or faire a complet requête) à
une URL whose serveur renvoie a réel 404/erreur status, or ajouter a noindex balise à le
erreur view avec JavaScript. Watch le deuxième un: si que noindex est présent depuis le
very premier paint plutôt que ajouté après le app decides le route est invalide, il peut
causer Google à skip rendu lune page altogether — donc vérifier ce que a direct requête
renvoie avant quelconque JS exécute, pas seulement ce que montre up dans le rendu DOM afterward.
Shared-shell duplicates — a possible causer, pas an automatic diagnosis
il y a a deuxième, sneakier échec mode: distinct routes obtenir indexé as duplicates de chaque autre parce que ils rendre bas à le même shared header/nav/footer boilerplate. Gary Illyes décrit un façon ce se produit: “Je ont a bunch de emails dans mon inbox où le problème est que le centerpiece took forever à charger, donc rendu timed out (mon la plupart probable explanation) et nous étaient left avec a bunch de pages que seulement avait le boilerplate. Avec seulement le boilerplate, ceux pages sont dups.” His corriger: “Essayer à restructure le js calls tel que le contenu (notamment marginal boilerplate) charge premier.” (Relayed via a LinkedIn post, qui resists automated verification; treat as élevé-confidence secondary.)
«Notice Illyes frames it as “my most likely explanation,” not a confirmed diagnosis — that’s worth taking literally. “Render timeout” isn’t something you can diagnose from symptoms alone; you need request-level evidence (what a direct fetch actually returns), rendered-output evidence (whether the centerpiece content is present after rendering), and Search Console evidence (duplicate/indexing signals) before attributing a route’s duplicate-content problem to a timeout specifically, rather than to a bug, a blocked resource, or a genuinely identical shell. There’s also no fixed, published timeout to design around — Google’s own basics doc says a page “may stay on this queue for a few seconds, but it can take longer than that,” without committing to a number. Don’t plan around an assumed render-queue duration; instead, make sure primary content loads as early as possible regardless of how long rendering ends up taking. » (Traduction) (Résumé en français de la section trente-deux, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
The fix: obtenir réel HTML per route
The complet fix has two independent parts que personnes conflate constantly:
- URL addressability — History API, unique per-route URLs, aucun fragments.
- Content availability — SSR, static prerendering/SSG, or a meta-framework.
Faire seulement #1 et vous obtenir clean, shareable URLs que tout encore retourner le même blank shell. Pour a route vous en réalité vouloir indexé, vous besoin les deux.
c’est pas a universal mandate à ajouter SSR everywhere, though. SSR et prerendering reduce comment beaucoup vous depend on a robot d’exploration successfully executing votre JavaScript — ils ne pas become mandatory le instant a site est technically an SPA. Avant picking an architecture pour a donné route, vérifier three choses: si que route nécessite à classer à tout (an internal admin panel ne fait pas), ce que a direct, JS-gratuit requête à il déjà renvoie (some setups déjà send meaningful HTML), et qui robots d’exploration vous en réalité besoin à satisfy (Google renders JS fairly reliably; Bing et la plupart AI robots d’exploration moins donc — voir ci-dessous). CSR que déjà passes ceux vérifications ne fait pas besoin à become SSR simplement parce que le site est an SPA.
Rendu côté serveur (SSR)
Le serveur exécute votre app pour chaque requête et renvoie fully-formed HTML pour que route, alors le client “hydrates” il dans a actif SPA. Ce est le la plupart robust option parce que le robot d’exploration obtient complet contenu on le premier récupérer, aucun JS execution requis.
Static prerendering / SSG
Au lieu de rendu per requête, vous construire chaque route’s HTML ahead de temps à deploy. Perfect pour contenu que ne fait pas modifier per utilisateur. Depuis mon propre JavaScript SEO writing: quelconque kind de SSR, static rendu, et prerendering setup est going à être fine pour recherche moteurs — le chose à éviter est leaving contenu locked behind client-seulement rendu.
Or: don’t hand-roll it — utiliser a meta-framework
Pour a nouveau construire, le honest recommendation est à pas hand-roll React-Router-seulement or Vue-Router-seulement client-side routing à tout. A meta-framework — Suivant.js, Nuxt, Angular avec son SSR package, SvelteKit, Remix — donne vous SSR/SSG et route-based metadata out de le box, qui sidesteps ce entier category de problème. (Chaque de ceux a son propre deep dive on ce site.) Être honest à propos de le flip side: retrofitting SSR sur an existing hand-rolled SPA est réel engineering fonctionner — a migration project, pas a config toggle.
Dynamic rendering as a stopgap, pas a destination
«You can serve crawlers a separately-rendered HTML snapshot (dynamic rendering). Both Google and Bing accept it — Bing “recommend[s] dynamic rendering as a great alternative for websites relying heavily on JavaScript” (relayed from Bing’s 2018 blog; the two short bingbot-capability quotes below are directly verified, this longer one is not independently re-checked) — but Google is clear it’s a workaround: “Dynamic rendering was a workaround and not a long-term solution… Instead, we recommend that you use server-side rendering, static rendering, or hydration as a solution.” (Relayed from Google’s dynamic-rendering doc; wording matches what’s already cited in this site’s broader JavaScript SEO guide.) It’s a bridge, not an architecture. » (Traduction) (Résumé en français de la section quarante-cinq, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«## History API vs. hash routing (#!)
» (Traduction) (Résumé en français de la section quarante-six, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Five states, pas two
Testing an SPA route obtient confusing parce que “does it work” en réalité spans five différent, separately-verifiable states:
| State | Ce que c’est |
|---|---|
| Serveur réponse | Le bytes a fresh, JS-gratuit HTTP requête à une URL en réalité obtient back. |
| Rendu DOM | Ce que a navigateur (or Googlebot’s renderer) construit après executing JavaScript contre que serveur réponse. |
| Recherche processing | Comment Google séparément explore le serveur réponse, plus tard renders lune page, et indexes fondé on les deux. |
| Navigateur complet-document navigation | An réel nouveau HTTP requête à une URL — le seulement state que peut modifier le serveur réponse or code d’état. |
| Navigateur soft navigation | A History API transition (pushState/replaceState) que changements le visible URL, navigateur history, et on-screen UI. |
Le un everyone conflates: a soft navigation changements l’URL et le UI, mais il fait
pas par itself créer a nouveau HTTP réponse or status — que seulement se produit on a complet
navigation (or an equivalent direct requête, comme curl). Testing a route par clicking
via le app depuis le homepage exercises soft navigation; testing il par requesting
l’URL directement exercises le serveur réponse. Les deux matter, et ils peut disagree.
Ce que the History API donne vous
«Google’s recommendation is unambiguous: “We recommend using the History API to load
different content based on the URL in a SPA.” (In the live doc “History API” is
a link, so this deep link targets the lead-in clause.) The History API
(pushState/replaceState) lets your router change the visible URL to a real,
bookmarkable path — /products, not /#/products — without a full reload. That fixes
the addressability half: every view now has a URL a server could respond to
differently.
» (Traduction) (Résumé en français de la section cinquante-deux, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Pourquoi fragment/hash URLs sont invisible à le serveur — et à Google
«Because the fragment never reaches the server (see above), the History API is the only
way to give each route a URL the server can actually serve. Google puts a floor under
it in its basics doc: “don’t use fragments to load different page content. The
following example is a bad practice, because Googlebot can’t reliably resolve the
URLs.” This is the same guidance I’ve written elsewhere — use normal-looking URLs like
/products, not hash URLs like /#/products, because Google can’t reliably index the
hash ones.
» (Traduction) (Résumé en français de la section cinquante-quatre, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
The 2015 deprecation, briefly
«The hash-bang (#!) era ended with Google’s 2015 post: “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,” and
“you can use the History API pushState() to ensure accessibility for a wider range of
browsers (and our systems).” One nuance worth keeping: Google didn’t instantly deindex
old hash-bang sites — “we’ll generally crawl, render, and index the #! URLs” — but
“we can still try” is not “you should still do this.”
» (Traduction) (Résumé en français de la section cinquante-six, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«## Making each route independently indexable » (Traduction) (Résumé en français de la section cinquante-sept, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«Clean URLs and real HTML get you crawled. To get indexed correctly, each route needs its own signals. » (Traduction) (Résumé en français de la section cinquante-huit, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«### Per-route canonical tags — and the “most restrictive directive” trap » (Traduction) (Résumé en français de la section cinquante-neuf, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Chaque route nécessite son propre rel=canonical dans le rendu DOM. Le trap: si a
placeholder canonical (or a noindex) ships dans le raw HTML shell et JavaScript est
supposed à overwrite il plus tard, vous pouvez obtenir a conflict. Google resolves conflicts
entre le raw et rendu versions par taking le plus restrictive signal — as j’ai
put il avant, Google va choisir le la plupart restrictive statements entre le HTML et
le rendu version de une page. A stray noindex or incorrect canonical baked dans le
shell peut silently suppress le entier route même après votre JS “corrections” il.
Per-route titles et meta descriptions via JavaScript
«Setting these in JS is fine — Google says so directly: “You can use JavaScript to set
or change the meta description as well as the <title> element.” The requirement is
that they land in the rendered DOM Google evaluates, not just flash briefly. Give each
page its own title and description that update when the route changes.
» (Traduction) (Résumé en français de la section soixante-deux, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Réel <a href> liens entre routes
«Make your inter-route links real anchors with href attributes, not click handlers on
<div>s. Google discovers URLs by extracting hrefs; a <div onClick> that navigates
via the router is invisible to the crawler’s link extraction. And don’t lean on
client-side state to carry content across those navigations — Google’s renderer “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.”
» (Traduction) (Résumé en français de la section soixante-quatre, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Sitemap generation pour SPA routes
There’s aucun special “SPA sitemap” format — it’s an accounting problem
Le sitemap protocol est unchanged pour SPAs. Le réel fonctionner est making certain chaque route vous liste independently resolves à réel, unique, rendu HTML. A sitemap de 500 client-side routes est worthless si ceux routes tout retourner le même shell. Donc le sitemap est downstream de votre rendu stratégie, pas a substitute pour il.
Handling dynamic/parameterized routes (/product/:id)
Pour apps avec parameterized routes, vous pouvez’t hand-maintain le liste. Le sitemap
generator a à exécuter contre le même données source le app utilise — a construire script or a
serveur endpoint que enumerates chaque id — donc le sitemap et le app jamais drift
apart. Ceux enumerated URLs sont seulement worth listing une fois ils sont SSR’d or prerendered.
«### Keeping the sitemap in sync with what’s server-resolvable » (Traduction) (Résumé en français de la section soixante-dix, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Regenerate le sitemap as partie of votre construire or on a schedule tied to votre content
source. A route que 404s (or worse, soft-404s at 200) but sits in votre sitemap is a
crawl-budget and quality signal vous don’t vouloir.
Testing ce que Google en réalité sees
ne pas spot-vérifier un URL. Le entier point de le SPA problème est que routes peut differ dans le navigateur mais pas on le wire, donc tester plusieurs routes à le raw-HTML level:
- Récupérer raw HTML per route avec
curl(a Googlebot utilisateur-agent où pertinent) et confirmer le contenu est unique per URL, pas le shared shell. - Inspection d’URL dans Search Console — comparer le exploré/rendu HTML pour plusieurs routes, et confirmer le per-route titre, description, et canonical sont présent.
- Confirmer erreur routes retourner le correct signal — a “pas trouvé” view devrait soit
redirection à a réel erreur status or carry
noindexdans le rendu DOM.
Courant myths à propos de SPA SEO
«- “Google can’t index SPAs at all.” Outdated. Google runs evergreen Chromium and generally renders SPA content. The real risks are specific: soft 404s, hash routing, render timeouts, and non-Google crawlers (Bing less reliably, most AI crawlers not at all). » (Traduction) (Résumé en français de la section soixante-dix-sept, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- “Adding the History API fixes SPA SEO.” It fixes addressability only. If the server still returns the same shell for every route, Google still has to execute JS to see anything. » (Traduction) (Résumé en français de la section soixante-dix-sept, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.) «- “Hash routing still works as a fallback.” Deprecated since 2015 and recommended against ever since. » (Traduction) (Résumé en français de la section soixante-dix-sept, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.) «- “Client-side rendering is a ranking penalty.” No direct CSR penalty exists. The damage is indirect — failed/delayed rendering, soft 404s, and duplicate clustering from render timeouts reduce what gets indexed. » (Traduction) (Résumé en français de la section soixante-dix-sept, sous-partie quatre : le texte source est conservé pour vérification lors de la relecture francophone.) «- “Pre-rendering for bots is cloaking.” Not when the content matches what users eventually see — only the when/where of rendering differs, not the content. » (Traduction) (Résumé en français de la section soixante-dix-sept, sous-partie cinq : le texte source est conservé pour vérification lors de la relecture francophone.) «- “You need a special SPA sitemap generator.” No special format exists; the work is making every listed route resolve to real HTML. » (Traduction) (Résumé en français de la section soixante-dix-sept, sous-partie six : le texte source est conservé pour vérification lors de la relecture francophone.)
FAQs
Peut Google index a application monopage? Yes, si chaque route resolves to réel, unique HTML (via SSR/prerendering) at a réel URL. Bare client-only SPAs souvent don’t.
Do I besoin SSR, or is rendu côté client ever okay? CSR peut fonctionner pour content que doesn’t besoin to rank, but pour anything vous vouloir indexé reliably, prerender or SSR it — don’t bet on the renderer executing votre JS in temps.
Est hash (#!) routing mauvais pour le SEO? Oui — le fragment jamais reaches le serveur, donc
Google ne peut pas reliably resolve ceux URLs. Utiliser le History API.
Fait chaque route besoin son propre balise canonical? Oui, dans le rendu DOM — et faire
certain rien plus restrictive (a stray noindex or incorrect canonical) ships dans le raw
shell.
Is a prerendering service cloaking? Aucun, provided le contenu served to bots matches ce que utilisateurs voir.
Devrait Je utiliser a meta-framework au lieu de building routing myself? Pour a nouveau construire, généralement oui — Suivant.js/Nuxt/Angular-SSR/SvelteKit donner vous SSR/SSG et per-route metadata pour gratuit.
Pourquoi fait mon SPA retourner 200 pour pages que ne pas exist? Parce que le client-side
router garde le original 200 status pour virtual navigations. Corriger il avec a JS
redirection à a réel erreur status or a rendu noindex.
AI summary
A condensed prendre on the Avancé version:
«- SPA SEO = client-side routing specifically (React Router, Vue Router, Angular
Router). An SPA is defined by loading one document and swapping views with
JavaScript — an “app shell” that sends near-empty HTML for every URL is one common
implementation, not a rule every SPA follows; check what a direct request actually
returns rather than assuming.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- The core risk: client-side routing can change what the user sees without
changing what the server would return. On a bare app-shell SPA, fetching /products
and /about directly can get byte-identical raw HTML.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
«- Five states get conflated: server response, rendered DOM, Search processing,
browser full-document navigation, and browser soft navigation. A History API
transition changes the URL and UI but doesn’t itself create a new server response or
status.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)
«- Three symptoms of a bare shell: the app-shell problem, soft 404s (routers keep a
200 for “not found” — Google’s two documented fixes are redirecting to a URL that
itself 404s, or a JS-added noindex; an initial noindex can cause rendering to be
skipped), and shared-shell duplicates — which “render timeout” (Illyes) is one
possible cause of, not an automatic diagnosis without request/render/Search evidence.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie quatre : le texte source est conservé pour vérification lors de la relecture francophone.)
«- Hash routing fails mechanically: the fragment after # never reaches the server,
so the server can’t differ by route. Deprecated by Google in 2015; History API gives
addressability, not a complete indexability fix.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie cinq : le texte source est conservé pour vérification lors de la relecture francophone.)
«- The fix has two independent halves where a bare shell is the problem:
addressability (History API, unique per-route URLs) and content availability (SSR,
prerendering/SSG, or a meta-framework) — but SSR isn’t a universal requirement; base
the call on whether the route needs to rank, what it already returns directly, and
which crawlers need to render it.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie six : le texte source est conservé pour vérification lors de la relecture francophone.)
«- Per route: own canonical/title/description in the rendered DOM; watch the
“most restrictive directive” trap where a raw-HTML noindex/canonical overrides your
JS. Real <a href> links; don’t rely on client state (WRS clears storage/cookies
across loads).
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie sept : le texte source est conservé pour vérification lors de la relecture francophone.)
«- Sitemaps: listing a route doesn’t prove it resolves — no special format exists;
every listed route must independently resolve to real HTML, and dynamic routes need a
generator tied to the app’s data source.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie huit : le texte source est conservé pour vérification lors de la relecture francophone.)
«- Dynamic rendering is a Google/Bing-accepted stopgap, not a long-term architecture.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie neuf : le texte source est conservé pour vérification lors de la relecture francophone.)
«- Test multiple routes at the raw-HTML level (direct requests), not just by
navigating within the app.
» (Traduction) (Résumé en français de la section quatre-vingt-dix, sous-partie dix : le texte source est conservé pour vérification lors de la relecture francophone.)
«## Official documentation » (Traduction) (Résumé en français de la section quatre-vingt-treize, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«Primary-source documentation from the search engines. » (Traduction) (Résumé en français de la section quatre-vingt-quatorze, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«Google » (Traduction) (Résumé en français de la section quatre-vingt-quinze, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- Understand the JavaScript SEO basics — the app-shell model, “Use the History API instead of fragments,” and setting titles/descriptions via JS. » (Traduction) (Résumé en français de la section quatre-vingt-quinze, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.) «- Fix Search-related JavaScript problems — the SPA soft-404 section, “Don’t use URL fragments to load different content,” and the History API recommendation. » (Traduction) (Résumé en français de la section quatre-vingt-quinze, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.) «- Dynamic Rendering as a workaround — why dynamic rendering is a stopgap, not a long-term solution. » (Traduction) (Résumé en français de la section quatre-vingt-quinze, sous-partie quatre : le texte source est conservé pour vérification lors de la relecture francophone.) «- Deprecating our AJAX crawling scheme (2015) — the formal end of hash-bang crawling and the pushState recommendation. » (Traduction) (Résumé en français de la section quatre-vingt-quinze, sous-partie cinq : le texte source est conservé pour vérification lors de la relecture francophone.) «- Build and submit a sitemap — general sitemap protocol (there’s no SPA-specific format). » (Traduction) (Résumé en français de la section quatre-vingt-quinze, sous-partie six : le texte source est conservé pour vérification lors de la relecture francophone.)
«Bing / Microsoft » (Traduction) (Résumé en français de la section quatre-vingt-seize, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- bingbot Series: JavaScript, Dynamic Rendering, and Cloaking. Oh My! — bingbot’s JS-rendering capabilities and its dynamic-rendering stance. » (Traduction) (Résumé en français de la section quatre-vingt-seize, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
«## Quotes from the source » (Traduction) (Résumé en français de la section quatre-vingt-dix-neuf, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
On-le-record statements depuis Google et Bing. Chaque lien est a deep lien que jumps à le quoted passage on le page source.
«Google — the app-shell problem » (Traduction) (Résumé en français de la section cent un, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- “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.” Jump to quote » (Traduction) (Résumé en français de la section cent deux, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Google — hash/fragment routing et le History API
«- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” Jump to quote » (Traduction) (Résumé en français de la section cent quatre, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- “We recommend using the History API to load different content based on the URL in a SPA.” Jump to quote (In the live doc “History API” is a link, so this deep link targets the lead-in clause; the full sentence is present verbatim in order.) » (Traduction) (Résumé en français de la section cent quatre, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.) «- “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” Jump to quote » (Traduction) (Résumé en français de la section cent quatre, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)
Google — SPA soft 404s
«- “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.”
Jump to quote
» (Traduction) (Résumé en français de la section cent six, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- “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.”
Jump to quote
» (Traduction) (Résumé en français de la section cent six, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
Google — meta balises via JavaScript, et stateless rendu
«- “You can use JavaScript to set or change the meta description as well as the <title> element.”
Jump to quote
» (Traduction) (Résumé en français de la section cent huit, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- “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.”
Jump to quote
» (Traduction) (Résumé en français de la section cent huit, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
Google — deprecating hash-bang exploration (2015)
«- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” Jump to quote » (Traduction) (Résumé en français de la section cent dix, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- “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.” Jump to quote » (Traduction) (Résumé en français de la section cent dix, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
Google — dynamic rendering is a workaround (relayed; wording matches what’s déjà cited in ce site’s broader JavaScript SEO guide, pas independently re-verified ce réussir)
«- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” Jump to quote » (Traduction) (Résumé en français de la section cent douze, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«Bing — bingbot and JavaScript » (Traduction) (Résumé en français de la section cent treize, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- “bingbot is generally able to render JavaScript.” — bingbot Series, Bing Webmaster Blog. Read the post » (Traduction) (Résumé en français de la section cent quatorze, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- “bingbot does not necessarily support all the same JavaScript frameworks that are supported in the latest version of your favorite modern browser.” — same post. Read the post » (Traduction) (Résumé en français de la section cent quatorze, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
«Gary Illyes, Google — render timeouts create duplicates (relayed via a LinkedIn post, which resists automated verification; treat as high-confidence secondary) » (Traduction) (Résumé en français de la section cent quinze, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- “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.” Read the post » (Traduction) (Résumé en français de la section cent seize, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Remarque: plusieurs pages sources (Google’s dynamic-rendu doc, le deux plus long Bing lines, et le Illyes LinkedIn post) resist fiable automated re-verification; ceux sont relayed depuis secondary or summarized sources et flagged inline. Le Google JS-basics, corriger-recherche-JavaScript, et 2015 deprecation citations étaient verified as exact substrings.Qui rendering chemin devrait I prendre?
Fonctionner top-down. Le question est toujours “fait le robot d’exploration obtenir réel HTML pour ce route sans executing mon JS?” — et commencer par confirming le route en réalité nécessite à être indexé et checking ce que a direct, JS-gratuit requête déjà renvoie; pas chaque route nécessite SSR, et some déjà retourner usable HTML.
1. Sont vous starting a nouveau construire?
- Oui → Utiliser a meta-framework (Suivant.js, Nuxt, Angular avec SSR, SvelteKit, Remix). SSR/SSG et per-route metadata come construit dans. Arrêter ici.
- Aucun → Continuer.
2. Fait votre content modifier per utilisateur / per requête?
- Aucun (mostly static content) → Prerender / SSG at construire temps. Simplest robust fix — every route is réel HTML on disk.
- Yes → Rendu côté serveur (SSR) so chaque requête renvoie route-specific HTML.
3. ne peut pas faire SSR or prerendering correct maintenant (legacy hand-rolled SPA)?
- Utiliser dynamic rendu as a temporary bridge — servir robots d’exploration a rendu snapshot. Plan le migration à SSR/SSG; ne pas treat ce as le destination.
4. Whatever chemin vous chose, confirmer tout de ces per route:
- Réel URL via le History API (aucun
#!fragments). - Unique titre + meta description + canonical dans le rendu DOM.
- Rien plus restrictive (
noindex, incorrect canonical) baked dans le raw shell. - Réel
<a href>liens entre routes. - Erreur routes retourner a réel erreur status or a rendu
noindex(aucun soft 404s). - Chaque route dans le sitemap independently resolves à réel HTML.
SPA SEO checklist
«Addressability
» (Traduction) (Résumé en français de la section cent vingt-neuf, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] Each view has a real URL via the History API (/products), not a fragment
(/#/products).
» (Traduction) (Résumé en français de la section cent vingt-neuf, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] Inter-route links are real <a href> anchors, not <div onClick> handlers.
» (Traduction) (Résumé en français de la section cent vingt-neuf, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)
«Content availability » (Traduction) (Résumé en français de la section cent trente, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- [ ] Every route returns real, unique HTML without the crawler executing your JS (SSR, prerendering/SSG, or a meta-framework). » (Traduction) (Résumé en français de la section cent trente, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.) «- [ ] Route content loads before the render window closes (avoid boilerplate-only duplicates). » (Traduction) (Résumé en français de la section cent trente, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)
«Per-route indexability
» (Traduction) (Résumé en français de la section cent trente et un, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] Unique <title> and meta description per route, present in the rendered DOM.
» (Traduction) (Résumé en français de la section cent trente et un, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] Unique rel=canonical per route in the rendered DOM.
» (Traduction) (Résumé en français de la section cent trente et un, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] No stray noindex/placeholder canonical in the raw shell that a more restrictive
signal could lock in.
» (Traduction) (Résumé en français de la section cent trente et un, sous-partie quatre : le texte source est conservé pour vérification lors de la relecture francophone.)
«Errors
» (Traduction) (Résumé en français de la section cent trente-deux, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] “Not found” routes redirect to a URL whose server returns a real error status or
carry noindex added by JavaScript (no soft 404s at 200).
» (Traduction) (Résumé en français de la section cent trente-deux, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] If using the noindex route, confirm it’s added after the app decides the route
is invalid, not present from the very first paint — an initial noindex can cause
Google to skip rendering the page.
» (Traduction) (Résumé en français de la section cent trente-deux, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)
«Sitemap
» (Traduction) (Résumé en français de la section cent trente-trois, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] Every listed route independently resolves to real HTML.
» (Traduction) (Résumé en français de la section cent trente-trois, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
«- [ ] Dynamic routes (/product/:id) enumerated from the app’s data source, not
hand-maintained.
» (Traduction) (Résumé en français de la section cent trente-trois, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)
«Verification » (Traduction) (Résumé en français de la section cent trente-quatre, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- [ ] Raw HTML checked across multiple routes (curl), confirming unique content per URL. » (Traduction) (Résumé en français de la section cent trente-quatre, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.) «- [ ] URL Inspection confirms rendered content, title, description, and canonical per route. » (Traduction) (Résumé en français de la section cent trente-quatre, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)
«## The mental models » (Traduction) (Résumé en français de la section cent trente-sept, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
1. Le deux halves vous doit pas conflate. Addressability (History API, unique URLs, aucun fragments) et contenu availability (SSR, prerendering, or a meta-framework) sont independent. Clean URLs avec aucun per-route HTML = a tidy sitemap de blank shells. Vous besoin les deux.
«2. “What would the server return, fresh?”
The whole SPA problem is that the browser view diverges from the server response. For
any route, ask what a curl with no JS execution gets back. If it’s the shell, the
crawler may see the shell too.
» (Traduction) (Résumé en français de la section cent trente-neuf, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
3. Le status-code par défaut est 200 — notamment pour erreurs.
Client-side routers garder le original 200. Assume chaque “erreur” view est a soft 404
jusqu’à vous avez deliberately fait il retourner a réel erreur status or a rendu noindex.
4. Le la plupart-restrictive-directive trap.
Google reconciles raw vs. rendu par taking le plus restrictive signal. A noindex
or incorrect canonical dans le shell peut override votre JS “corriger.” Audit le raw HTML, pas simplement
le rendu DOM.
5. Meta-framework premier pour nouveau construit. Hand-rolling client-side routing signifie owning chaque un de ces problèmes. Picking a framework que fait SSR/SSG et per-route metadata est choosing pas à ont les.
SPA SEO cheat sheet
Routing
| Approach | URL exemple | Reaches le serveur? | Google-safe? |
|---|---|---|---|
| History API | /products | Yes | Yes (recommended) |
| Hash routing | /#/products | Aucun (fragment jamais sent) | Aucun — deprecated 2015 |
Hash-bang (#!) | /#!/products | Aucun | Aucun — legacy AJAX scheme, deprecated |
Rendering strategies
| Stratégie | Robot d’exploration obtient réel HTML sans running JS? | Meilleur pour |
|---|---|---|
| SSR | Oui | Per-utilisateur / per-requête contenu |
| Prerendering / SSG | Oui | Mostly-static contenu |
| Meta-framework (Suivant/Nuxt/etc.) | Oui (construit dans) | Nouveau construit |
| Dynamic rendu | Oui, pour bots seulement | Temporary bridge on legacy SPAs |
| Bare CSR (client-seulement) | Aucun | Rien vous vouloir indexé reliably |
Per-route doit-haves (tout dans le rendu DOM)
- Unique URL (History API) · unique
<title>· unique meta description · uniquerel=canonical· réel<a href>liens · erreur routes avec a réel status ornoindex.
Rapide facts
- Fragment après
#est jamais sent à le serveur — c’est pourquoi hash routing fails. - Client-side routers garder
200pour tout, notamment “pas trouvé” → soft 404s. - Google reconciles raw vs. rendu par taking le plus restrictive signal.
- Aucun special SPA sitemap format exists — chaque listed route doit resolve à réel HTML.
SPA SEO anti-patterns
Shipping a bare CSR SPA et submitting a complet sitemap. Le sitemap listes 500 routes; tout 500 retourner le même shell à a fresh récupérer. Le sitemap ne fait pas faire contenu exist — rendu fait.
Ajout le History API et appel il fait. Clean URLs corriger addressability, pas contenu. Sans SSR/prerendering le serveur encore renvoie le shell pour chaque route.
Hash / hash-bang routing “as a fallback.” The fragment jamais reaches le serveur, so le serveur can’t differ by route. Deprecated since 2015; pas a fallback, a dead fin.
Navigating with <div onClick> au lieu de <a href>.
Google extracts hrefs to découvrir URLs. Click-handler navigation is invisible to lien
extraction, so ceux routes may jamais be trouvé.
A placeholder noindex or canonical dans le raw shell “that JS will overwrite.”
Google takes le plus restrictive de raw vs. rendu. Le shell’s directive peut win et
silently suppress le route.
Returning 200 pour “pas trouvé” views.
Soft 404s obtenir thin/vide pages indexé. Redirection à a réel erreur status or ajouter a
rendu noindex.
Chargement route contenu slowly, après le boilerplate. Rendre timeouts laisser seulement le shared shell, et chaque route collapses dans a duplicate de chaque autre (Illyes). Charger le centerpiece contenu premier.
Relying on client state à carry contenu à travers navigations. Le renderer clears Local/Session Storage et cookies à travers page charge — contenu que seulement exists dans client state ne va pas être là quand Google renders le suivant route.
Courant SPA indexation problèmes
Every route renvoie the même HTML
Symptom: /products et /about ont différent navigateur views mais identical raw
réponses. Probable causer: History API routing provides addressability sans SSR
or prerendering. Corriger: Produce route-spécifique HTML et confirmer a direct requête à
chaque chemin contient son propre titre, corps copy, et metadata.
«### Missing routes appear as successful pages » (Traduction) (Résumé en français de la section cent soixante-huit, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«Symptom: A nonexistent route shows a not-found view while returning 200.
Likely cause: The client router owns the error after the server has already sent a
successful shell. Fix: Return the right server status; if that cannot ship yet,
redirect to a URL with a real error status or render noindex. Confirm with a fresh
direct request, not an in-app navigation.
» (Traduction) (Résumé en français de la section cent soixante-neuf, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«### Routes collapse into duplicates after rendering » (Traduction) (Résumé en français de la section cent soixante-dix, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
«Symptom: Search engines cluster distinct URLs or retain only shared navigation. Likely cause: Route content loads too late and rendering captures boilerplate. Fix: Prioritize primary content in the server response or earliest render path. Confirm several routes expose unique content before optional scripts finish. » (Traduction) (Résumé en français de la section cent soixante-onze, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)
Testing ce que le serveur en réalité renvoie per route
Le SPA trap est que routes differ dans le navigateur mais pas on le wire. Vérifier plusieurs routes à le raw-HTML level, pas un.
Récupérer 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
doneDiff deux routes’ raw HTML (sont ils le même shell?)
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)"Vérifier le per-route titre et canonical dans le raw réponse (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["'"'"'][^>]*>'Vérifier le HTTP status de a “pas trouvé” route (soft-404 detector)
# A missing route should NOT return 200
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-route-does-not-existDans le navigateur DevTools console — inspect le rendu titre/canonical
// 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 — flag click-handler “links” que aren’t réel anchors
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 — trouver fragment/hash liens a robot d’exploration ne peut pas resolve (paste dans DevTools console)
$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. Outils pour auditing an SPA
- Inspection d’URL (Recherche Google Console) — voir le exploré vs. rendu HTML pour a route et confirmer le per-route titre, description, et canonical en réalité land.
- Résultats enrichis Tester / Inspection d’URL “View crawled page” — Google’s propre rendre de a unique URL, utile pour confirming contenu survives rendu.
curlavec a Googlebot UA — le fastest façon à comparer le raw HTML de plusieurs routes et catch a shared shell.- Screaming Frog SEO Spider (JavaScript rendu mode) — explorer le site avec et sans rendu à comparer raw vs. rendu contenu et titles à travers chaque route.
- Ahrefs Site Audit — surfaces indexability problèmes, manquant/duplicate titles et canonicals, et soft-404-comme patterns à travers routes.
- DebugBear — SPA-oriented performances monitoring; rendre timing matters parce que lent routes peut temps out dans boilerplate duplicates.
- Bing Webmaster Outils — Inspection d’URL — bingbot renders JS moins consistently que Googlebot, donc confirmer votre routes on Bing’s side aussi.
Prove an SPA route est independently indexable
Tester direct-entry HTML parity
Tester à exécuter: Ouvrir representative routes dans a nouveau session et récupérer le même URLs
avec curl. Attendu résultat: Chaque URL renvoie son propre principal contenu et head
signaux sans prior app state. Échec interpretation: Le route dépend on
client navigation or stored state. Monitoring window: Immediate. Rollback
trigger: A route fonctionne seulement après entering via le homepage.
Tester error status handling
Tester à exécuter: Requête a known-invalide route directement et inspect les deux status et
rendu directives. Attendu résultat: A genuine erreur status, or le documented
fallback de a rendu noindex/redirection à an erreur réponse. Échec
interpretation: Le SPA est generating soft 404s. Monitoring window: Immediate.
Rollback trigger: Invalide paths ship as indexable 200 pages.
Tester metadata isolation
Tester à exécuter: Comparer le raw et rendu titre, robots balise, et canonical à travers au moins three routes. Attendu résultat: Chaque route a un prévu, internally consistent définir. Échec interpretation: Le shared shell leaks metadata entre routes or JS overwrites il aussi late. Monitoring window: Immediate locally et après recrawl dans Inspection d’URL. Rollback trigger: Quelconque route inherits un autre route’s canonical or a restrictive shell directive.
Testez vos connaissances: SPA SEO
Five rapide questions on making application monopages crawlable and indexable. Pick an réponse pour chaque, alors vérifier.
Ressources utiles
Mon connexe writing
- JavaScript SEO Problèmes & Meilleur Practices — mon complet JS-SEO guide, notamment le “ne pas utiliser fragments dans URLs” et app-shell/duplicate-contenu sections que ce article construit on.
- Le Beginner’s Guide à SEO technique — où rendu et exploration fit dans le bigger picture.
«My speaking » (Traduction) (Résumé en français de la section deux cents cinq, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- How Search Works (SlideShare) — my walkthrough of crawling, rendering, indexing, and ranking, which is the pipeline an SPA has to survive. (My standing disclaimer applies: “This is my understanding of systems… not going to be 100% complete or accurate.”) » (Traduction) (Résumé en français de la section deux cents cinq, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.)
«From around the industry » (Traduction) (Résumé en français de la section deux cents six, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- Google — Understand the JavaScript SEO basics — the app-shell model and “Use the History API instead of fragments.” » (Traduction) (Résumé en français de la section deux cents six, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.) «- Google — Fix Search-related JavaScript problems — the SPA soft-404 section and the History API recommendation. » (Traduction) (Résumé en français de la section deux cents six, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.) «- Google — Deprecating our AJAX crawling scheme (2015) — the formal end of hash-bang crawling. » (Traduction) (Résumé en français de la section deux cents six, sous-partie quatre : le texte source est conservé pour vérification lors de la relecture francophone.) «- Bing — bingbot Series: JavaScript, Dynamic Rendering, and Cloaking. Oh My! — bingbot’s JS-rendering stance. » (Traduction) (Résumé en français de la section deux cents six, sous-partie cinq : le texte source est conservé pour vérification lors de la relecture francophone.) «- It’s Not Cloaking To Use A Pre-Render Service For Blank HTML Pages For SPA Development (Search Engine Roundtable, 2015) — coverage of Gary Illyes on pre-rendering SPAs not being cloaking. (SER’s paraphrase of Illyes, dated 2015 — secondary.) » (Traduction) (Résumé en français de la section deux cents six, sous-partie six : le texte source est conservé pour vérification lors de la relecture francophone.) «- SEO for Single Page Applications (Nuxt SEO) — a framework-side walkthrough of the same problems. » (Traduction) (Résumé en français de la section deux cents six, sous-partie sept : le texte source est conservé pour vérification lors de la relecture francophone.) «- How To Optimize Single Page Applications For SEO (DebugBear) — the rendering/performance angle on SPA indexability. » (Traduction) (Résumé en français de la section deux cents six, sous-partie huit : le texte source est conservé pour vérification lors de la relecture francophone.) «- SPA (glossary) (MDN) — a neutral definition of the single-page-application pattern. » (Traduction) (Résumé en français de la section deux cents six, sous-partie neuf : le texte source est conservé pour vérification lors de la relecture francophone.)
Videos
- Recherche Google Central (YouTube) — Martin Splitt’s JavaScript SEO series covers rendu, le History API, et le SPA échec modes discussed ici. Channel
Journal des modifications
Mis à jour le 18 juil. 2026.
Résumé éditorial et détails enregistrés des changements.Détails des changements
-
Les notes détaillées des changements sont actuellement disponibles en anglais.
-
Les notes détaillées des changements sont actuellement disponibles en anglais.
-
Les notes détaillées des changements sont actuellement disponibles en anglais.
-
Les notes détaillées des changements sont actuellement disponibles en anglais.
-
Les notes détaillées des changements sont actuellement disponibles en anglais.
Comparaison complète indisponible — aucun instantané antérieur n’a été archivé pour cette révision.