L'erreur de canonical qui masque vos traductions

agentready.md publie en dix langues avec un préfixe dans le chemin. Chaque page existe en /about, /es/about, /ja/about et sept autres. Pendant des mois, les neuf traductions ont servi la même ligne dans leur <head> :

<link rel="canonical" href="https://agentready.md/about">

/es/about disait aux moteurs de recherche : je suis un doublon de la page anglaise, indexez celle-là. /ja/about disait la même chose. Les sept autres aussi. Dix langues, une seule indexable.

Ce qu'un canonical déclare réellement

rel="canonical" n'est pas une indication sur l'URL la plus propre. C'est une affirmation : cette page et celle que je nomme sont la même, et c'est l'autre qui mérite l'index. Les liens, les signaux de classement et l'entrée dans l'index vont tous à la cible. Cette page cesse de concourir.

C'est exactement le comportement voulu quand /produit?couleur=bleu pointe vers /produit. C'est exactement l'inverse de ce qu'il faut pour une traduction. Une page en espagnol n'est pas un doublon de la page anglaise : c'est une autre page, pour un autre lecteur, et elle doit se classer seule sur les requêtes en espagnol.

Pourquoi le bug a survécu si longtemps

Notre routeur retire le préfixe de langue avant le routage : une requête vers /es/about arrive au gestionnaire sous la forme /about, la langue voyageant à part. Le choix est sain, les routes se déclarent une fois et fonctionnent dans toutes les langues, mais request.url n'a plus le préfixe, et le partiel <head> construisait le canonical à partir de ce chemin nu.

En anglais, le bug produit le bon résultat, puisque l'anglais n'a pas de préfixe. Toutes les pages que vous regardez en développant dans votre langue sont correctes. Cela ne casse que dans les neuf que vous ne lisez pas.

Le correctif tient en un appel, qui remet le préfixe avant d'imprimer l'URL :

<%# request.url a déjà perdu le préfixe /es/ — localizedUrl le remet. %>
<% const canonicalHref = localizedUrl(canonicalPath); %>
<link rel="canonical" href="<%= baseUrl %><%= canonicalHref %>">

La règle : chaque version est canonique d'elle-même

Canonical autoréférent, toujours. /es/about déclare /es/about. /ja/about déclare /ja/about. Aucune exception pour les traductions. Si deux versions se ressemblent beaucoup, la même fiche produit à trois mots près, cela ne change rien : une langue n'est pas un doublon.

hreflang : chaque version les liste toutes

Le canonical dit quelle page indexer. hreflang dit à quel lecteur montrer laquelle des pages indexées. Il faut les deux, et deux règles de hreflang sont régulièrement ratées.

Chaque version les liste toutes, y compris elle-même. Dix langues, donc dix liens alternate sur les dix pages, autoréférence comprise. Oublier cette autoréférence est le défaut le plus fréquent dans un ensemble par ailleurs correct.

Les liens doivent être réciproques. Si la page anglaise nomme l'espagnole et que l'espagnole ne lui rend pas la pareille, le moteur écarte la relation, souvent tout l'ensemble, faute de pouvoir la vérifier. N'importe qui peut se déclarer traduction d'un autre. Seule l'autre page peut le confirmer.

Générez-les depuis une liste unique plutôt que d'en maintenir dix :

<% ['en','es','fr','de','pt','it','ja','zh','ko','ru'].forEach(l => { %>
<link rel="alternate" hreflang="<%= l %>" href="<%= baseUrl %><%= localizedUrl(canonicalPath, l) %>">
<% }) %>
<link rel="alternate" hreflang="x-default" href="<%= baseUrl %><%= canonicalPath %>">

Utilisez le code de langue seul (es) quand une version sert tous les locuteurs. N'ajoutez une région (es-ES, pt-BR) que si vous publiez vraiment des versions distinctes par marché ; inventer des régions que vous n'avez pas fragmente l'ensemble pour rien.

x-default n'est pas une traduction de plus

x-default désigne la version à servir au lecteur dont vous ne publiez pas la langue. Quelqu'un cherche en néerlandais, vous n'avez pas de néerlandais : il reçoit ce que pointe x-default.

Ce n'est pas « la langue la plus importante », ni une entrée supplémentaire dans la rotation. Le nôtre reste sur l'URL anglaise sans préfixe, qui est déjà le repli de tout le site. Le faire pointer vers une page à langue propre, /es/about, envoie en espagnol tout lecteur pour lequel vous n'avez pas de traduction, ce qui est pire que le défaut.

Vérifiez-le en une ligne

L'échec entier se voit depuis le terminal. Parcourez vos langues et affichez ce que chaque page déclare :

for p in "" es/ fr/ de/ pt/ it/ ja/ zh/ ko/ ru/; do
  printf '%-4s ' "${p:-en}"
  curl -s "https://exemple.com/${p}about" | grep -o '<link rel="canonical"[^>]*>'
done

Une sortie correcte nomme sur chaque ligne son propre chemin :

en   <link rel="canonical" href="https://exemple.com/about">
es   <link rel="canonical" href="https://exemple.com/es/about">
fr   <link rel="canonical" href="https://exemple.com/fr/about">

Dix lignes avec la même URL, c'est le bug de cet article. Lancez-le sur les gabarits que vous avez modifiés, pas seulement sur l'accueil : le canonical se construit souvent par gabarit, et un site à trois gabarits peut n'être juste que dans deux.

Les autres façons de casser tout ça

Des paramètres de tracking dans le canonical. Si vous construisez le canonical depuis l'URL de requête brute, un visiteur arrivé sur ?utm_source=newsletter obtient un canonical qui pointe vers ?utm_source=newsletter. Chaque partage crée une URL qui se déclare canonique, soit un ensemble sans limite d'URL pour une seule page. Construisez-le depuis le chemin et ne réintégrez que les paramètres qui identifient vraiment une autre page, une liste filtrée ou une pagination, dans un ordre fixe, pour qu'une page ne puisse pas écrire son nom de deux manières.

Un canonical qui pointe vers une redirection. Si /es/about déclare /es/about/ et que cela fait un 301 vers /es/about, vous donnez au robot une boucle à résoudre et une raison de se méfier du signal. Pointez vers l'URL qui répond 200.

Deux canonical qui se contredisent. Le <link> du HTML et un en-tête HTTP Link: <...>; rel="canonical" sont aussi valables l'un que l'autre, et les CDN, extensions et proxys inverses en ajoutent. Quand ils divergent, le résultat est indéfini et rarement celui que vous vouliez. Regardez aussi les en-têtes :

curl -sI https://exemple.com/es/about | grep -i "^link:"

Rien du tout, c'est la bonne réponse, sauf si vous l'avez mis exprès.

Analysez votre site · Générateur de meta tags · Checklist de préparation à l'IA