La plupart des conseils WordPress hreflang sont " ; installez un plugin multilingue et il le gère." ; C'est bien lorsque le plugin est propriétaire de vos traductions. C'est la mauvaise réponse dans le cas où je frappe le plus souvent : un site avec deux ou trois versions linguistiques construites sous forme de sous-répertoires séparés, d'installations séparées ou d'un ensemble de pages traduites manuellement, où personne ne veut d'un cadre de traduction complet et de son schéma de base de données pour quatre pages.
Je conserve les propriétés WordPress sous cette forme, y compris les sites de documentation sur lesquels les traductions sont écrites à la main et sous lesquelles elles vivent /de/ et /es/. Le hreflang il y a quarante lignes dans un fichier de thème, il lit à partir d'un tableau, et il n'a jamais dérivé Ce guide est ce code, plus les parties qui tournent mal quand un plugin émet hreflang à côté de votre thème.
Il se trouve sous le guide complet de hreflang[TRADUCTION], à côté du Version suivante.JS.
tl;dr : WordPress n'émet pas de hreflang tout seul Hook
wp_head(en), bouclez sur une carte locale, et imprimez l'ensemble réciproque complet comprenantx-defaultsur chaque page du groupe. Construire des URL avechome_url()ou une origine par locale plutôt qu'un codage en dur, conservez chaque page' ; s canonique autoréférentielle, et vérifiez qu'aucun plugin SEO n'émet déjà un bloc concurrent Pour les configurations multi-sites, desget_sites()vous donne la carte ; pour un seul site avec des pages traduites, un post meta field mapping homologues suffit.
WordPress ajoute-t-il hreflang tout seul ?
No. Core génère un lien canonique, un lien court et diverses liaisons de flux et REST wp_head[TRADUCTION], mais rien dans le langage ne s'alterne. get_locale() sait dans quelle langue se trouve le site et aucune partie du noyau ne transforme cela en annotation.
Les plugins comblent le vide Polylang et WPML émettent un hreflang pour les traductions qu'ils gèrent, et la plupart des plugins SEO l'émettront si vous configurez une relation de traduction qu'ils peuvent voir Si vous utilisez déjà l'un de ceux-ci et qu'il couvre l'ensemble de votre site, utilisez-le et sautez cet article La raison pour laquelle il faut lancer manuellement est que vos traductions existent en dehors de tout ce que le plugin sait, et un plugin qui émet un ensemble partiel est pire que celui qui n'en émet aucun.
La version minimale : un tableau, un crochet
Pour un site dont les traductions sont un petit ensemble connu de préfixes d'URL :
<?php
/**
* Hreflang annotations for a hand-translated multilingual site.
* Drop in the theme's functions.php or a small mu-plugin.
*/
function mysite_hreflang_map(): array {
return [
'en-US' => 'https://example.com',
'de-DE' => 'https://example.com/de',
'es-ES' => 'https://example.com/es',
'x-default' => 'https://example.com',
];
}
function mysite_print_hreflang(): void {
$path = mysite_current_path(); // e.g. '/pricing/'
foreach ( mysite_hreflang_map() as $hreflang => $origin ) {
printf(
'<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
esc_attr( $hreflang ),
esc_url( untrailingslashit( $origin ) . $path )
);
}
}
add_action( 'wp_head', 'mysite_print_hreflang', 1 );
Trois choses sont correctes et ce n'est généralement pas le cas des blocs manuscrits.
Il imprime l'ensemble complet sur chaque page[TRADUCTION], y compris la langue de page' ; s. La réciprocité est satisfaite structurellement : il n'y a pas de branche qui pourrait omettre une entrée, parce qu'il n'y a pas de branche du tout.
x-default est une ligne dans le même tableau4, il est donc émis par la même boucle et ne peut pas être oublié sur un seul modèle.
Les URL sont échappées. esc_url sur un href en wp_head n'est pas facultatif et les valeurs hreflang passent esc_attr pour la même raison.
Travailler sur le chemin actuel
Le chemin est la partie qui a besoin de soins, car il doit être le chemin à l'intérieur la langue, pas l'URI de demande complète.
function mysite_current_path(): string {
$home = wp_parse_url( home_url(), PHP_URL_PATH ) ?: '';
$req = wp_parse_url( $_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH ) ?: '/';
// Strip the language prefix this install is served under.
if ( $home && str_starts_with( $req, $home ) ) {
$req = substr( $req, strlen( $home ) );
}
return user_trailingslashit( '/' . ltrim( $req, '/' ) );
}
Si votre installation allemande vit à example.com/de et le visiteur est allumé example.com/de/preise/, ça revient /preise/[traduction], qui est ensuite préfixé avec chaque locale' ; s origine La gestion de la barre oblique arrière compte plus qu'elle n'en a l'air : /preise et /preise/ sont différentes URL, et pointer une annotation vers la forme non canonique est le inadéquation de barre oblique arrière cela produit des erreurs de balise de retour sur un ensemble qui semble par ailleurs parfait. user_trailingslashit respecte le paramètre de lien permanent du site' ; s, il correspond donc à tout ce que WordPress sert.
L'hypothèse ici est que les URL traduites sont structurellement parallèles : /pricing/ cartes à /de/pricing/. Si votre allemand slugs est traduit aussi, /de/preise/1, cette fonction n'est pas suffisante et vous avez besoin d'un mappage explicite, qui est la section suivante.
Lorsque slugs diffère : un mappage post-méta
Traduit slugs sont meilleurs pour les utilisateurs et pour le classement, et ils signifient que le chemin ne peut pas être dérivé Stockez plutôt les homologues :
/**
* Meta key holds a JSON map: {"de-DE": 123, "es-ES": 456} of post IDs,
* or absolute URLs for external installs.
*/
function mysite_alternates_for( int $post_id ): array {
$raw = get_post_meta( $post_id, '_hreflang_alternates', true );
$map = $raw ? json_decode( $raw, true ) : [];
if ( ! is_array( $map ) ) {
return [];
}
$out = [];
foreach ( $map as $hreflang => $target ) {
$url = is_numeric( $target ) ? get_permalink( (int) $target ) : $target;
if ( $url ) {
$out[ $hreflang ] = $url;
}
}
return $out;
}
Puis le wp_head callback fusionne la publication actuelle et la version n°39 ; sa propre entrée sur cette carte avant l'impression, de sorte que l'ensemble est complet et autoréférentiel :
function mysite_print_hreflang_mapped(): void {
if ( ! is_singular() ) {
return;
}
$post_id = get_queried_object_id();
$alts = mysite_alternates_for( $post_id );
if ( count( $alts ) < 1 ) {
return; // Nothing to declare: a one-entry cluster is not a cluster.
}
$alts[ mysite_hreflang_for_locale( get_locale() ) ] = get_permalink( $post_id );
$alts['x-default'] = $alts['en-US'] ?? get_permalink( $post_id );
foreach ( $alts as $hreflang => $url ) {
printf(
'<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
esc_attr( $hreflang ),
esc_url( $url )
);
}
}
Le coût de maintenance est que le méta doit être écrit des deux côtés Une interface utilisateur admin avec un sélecteur de contrepartie est la solution honnête ; une commande WP-CLI qui remplit la carte à partir d'un CSV est la plus rapide Ce que vous ne devez pas faire, c'est la remplir d'un seul côté, car une déclaration à sens unique est exactement la balise de retour manquante échec.
Multisite : dérivez la carte du réseau
Si chaque langue est un site dans un réseau multisite, la carte existe déjà
function mysite_network_hreflang_map(): array {
$map = [];
foreach ( get_sites( [ 'number' => 50 ] ) as $site ) {
switch_to_blog( (int) $site->blog_id );
$map[ mysite_hreflang_for_locale( get_locale() ) ] = home_url();
restore_current_blog();
}
return $map;
}
Mettre en cache le résultat dans un transitoire ; switch_to_blog dans un wp_head accrocher à chaque demande est un coût réel sur un réseau de toute taille Et soyez prudent avec get_locale() sous switch_to_blog: il reflète le site commuté uniquement si le site' ;s WPLANG l'option est définie, ce qui est le cas habituel mais non garanti sur les réseaux configurés via une constante.
Quelle approche correspond à votre site ?
| installation | s'approche | Effort |
|---|---|---|
| Parallèle slugs, peu de lieux | Tableau statique plus chemin dérivé | Le plus bas ; une fonction |
| Traduit slugs | Carte de contrepartie post-méta | Modéré ; a besoin d'un workflow d'édition |
| Multisite, un site par langue | Dériver de get_sites(), mis en cache |
Code bas, nécessite une discipline de cache |
| Polylang ou WPML gérant les traductions | Utilisez le plugin' ; s sortie | Vérifiez qu'il émet x-default |
| plugin SEO avec champs de traduction manuelle | Utilisez-le, puis vérifiez la tête rendue | Surveillez les blocs en double |
Mettre hreflang sur le plan du site à la place
Si vos têtes de page sont déjà encombrées, ou si vous avez suffisamment de lieux pour que le bloc soit plus long que le reste de la tête, le plan du site est l'alternative WordPress 5.5 et versions ultérieures principaux sitesmaps4, et ils sont filtrables, bien que les filtres n'aient pas été conçus en tenant compte des alternatives et de la déclaration de l'espace de noms <urlset> ce n’est pas quelque chose que le noyau vous permet de toucher proprement.
La voie pragmatique est un plan de site autonome servi à partir d'une règle de réécriture plutôt que d'un combat avec wp_sitemaps:
add_action( 'init', function () {
add_rewrite_rule( '^sitemap-i18n\.xml$', 'index.php?mysite_i18n_sitemap=1', 'top' );
} );
add_filter( 'query_vars', fn( $vars ) => array_merge( $vars, [ 'mysite_i18n_sitemap' ] ) );
add_action( 'template_redirect', function () {
if ( ! get_query_var( 'mysite_i18n_sitemap' ) ) {
return;
}
header( 'Content-Type: application/xml; charset=UTF-8' );
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ';
echo 'xmlns:xhtml="http://www.w3.org/1999/xhtml">' . "\n";
foreach ( mysite_translated_paths() as $path ) {
echo " <url>\n <loc>" . esc_url( home_url( $path ) ) . "</loc>\n";
foreach ( mysite_hreflang_map() as $hreflang => $origin ) {
printf(
' <xhtml:link rel="alternate" hreflang="%s" href="%s"/>' . "\n",
esc_attr( $hreflang ),
esc_url( untrailingslashit( $origin ) . $path )
);
}
echo " </url>\n";
}
echo '</urlset>';
exit;
} );
Deux choses pour bien se mettre. Le xmlns:xhtml déclaration sur <urlset> est requis ; sans cela, les entrées alternatives sont ignorées et le fichier est toujours valide comme plan du site, ce qui est à nouveau le modèle d'échec silencieux. Et réécrivez les règles une fois après avoir ajouté la règle, lors de l'activation plutôt que sur chaque demande.
Après avoir fait cela, supprimez la sortie de la tête. Émettre les deux est mélange des méthodes de mise en œuvre(en), ce qui vous donne deux sources qui dérivent.
Le problème du duplicate-block
Il s'agit de l'échec spécifique à WordPress et de celui qui mérite d'être vérifié avant d'écrire du code Plusieurs plugins émettent un hreflang, et plusieurs peuvent être actifs à la fois Un plugin SEO configuré avec des relations de traduction, un plugin multilingue et votre nouvelle fonction de thème seront tous heureusement imprimés wp_head4, et le résultat est deux ou trois blocs avec des contenus différents et éventuellement deux x-default entrées.
Google' ; la gestion des annotations contradictoires sur une page consiste à se méfier d'elles. Ainsi, un site qui n’avait pas de hreflang et qui compte désormais trois blocs peut être dans une situation pire qu’auparavant.
Vérifiez la sortie rendue plutôt que les paramètres du plugin :
curl -s https://example.com/de/preise/ | grep 'rel="alternate"' | grep hreflang
Comptez les lignes et comptez les codes distincts Une entrée par locale plus une x-default(en), et rien de répété Si un plugin émet un ensemble vous ne pouvez pas configurer à distance, la trappe d'échappement habituelle est de supprimer son action de wp_head plutôt que d'ajouter le vôtre par-dessus :
// Example shape: find the real hook and callback with `has_action`.
add_action( 'init', function () {
remove_action( 'wp_head', 'some_plugin_hreflang_output', 10 );
} );
Étiquettes canoniques, et ne pas les casser
WordPress émet rel="canonical" pour des vues singulières à travers rel_canonical()&, et il pointe sur le post' ; s propre permalien, qui est ce que vous voulez La plupart des plugins SEO le remplacent par le leur, également autoréférentiel par défaut.
La règle de hreflang vs canonique s'applique inchangé : chaque page se canonique sur elle-même, et hreflang décrit les relations entre ces pages auto-canoniques La façon dont WordPress le casse est un paramètre de plugin qui canonique les publications traduites vers un " ; primary" ; version linguistique, qui se lit comme une option de déduplication sensée et supprime discrètement vos traductions de l'index Si un paramètre propose de canoniser dans toutes les langues, laissez-le désactivé.
Vérification du résultat
Récupérez deux pages fraternelles et comparez leurs blocs d'annotation Elles devraient être identiques, avec seulement le différent canonique Vérifiez ensuite que chaque URL que vous avez nommée renvoie 200 sans redirection, car les lieux retirés et les permaliens modifiés sont la façon dont un ensemble correct pourrit en un ensemble cassé.
Collez l'ensemble dans le générateur hreflang gratuit pour valider les codes : il vérifie chaque valeur par rapport à la forme ISO, signale les sous-étiquettes non reconnues comme en-UK., attrape les doublons, rejette les URL relatives, et avertit lorsque l'ensemble n'a pas de repli Il s'exécute entièrement dans le navigateur, ce qui importe lorsque les URL décrivent un site client qui n'est pas encore public Ensuite, utilisez Search Console' ; s International Targeting report pour la vérification en cours, et le guide d'outils Hreflang pour le workflow autour.
Questions fréquentes
WordPress ajoute-t-il automatiquement des balises hreflang ?
No. Core émet un lien canonique et divers liens de tête mais rien sur les alternances de langage Hreflang provient d'un plugin ou du vôtre wp_head sortie.
Comment ajouter hreflang dans WordPress sans plugin ?
Crochet wp_head et imprimez un <link rel="alternate" hreflang> ligne pour chaque lieu d'une carte que vous contrôlez, y compris un x-default entrée, sur chaque page du groupe Échapper aux valeurs avec esc_attr et esc_url.
Où vont les balises hreflang dans un thème WordPress ?
Dans une fonction accrochée à wp_head(Idéalement dans un petit plugin mu plutôt que dans le thème, donc changer de thème ne supprime pas silencieusement vos annotations.
Et si mes pages traduites avaient des slugs différents ?
Ne rien dériver Stockez les ID ou URL des messages homologues dans post meta et lisez la carte au moment du rendu. Remplissez-le des deux côtés, ou vous créez une balise de retour manquante.
Polylang et WPML gèrent-ils le hreflang ?
Oui, pour les traductions qu'ils gèrent Vérifier la sortie comprend x-default et qu'aucun deuxième plugin n'émet un bloc concurrent.
Pourquoi ai-je deux blocs hreflang sur une page ?
Deux choses sont imprimées wp_head: habituellement un plugin SEO plus un plugin multilingue, ou un plugin plus votre propre fonction Les annotations contradictoires sur une page sont méfiantes, alors supprimez-en une plutôt que la superposition.
Chaque version en langage WordPress doit-elle avoir son propre canonique ?
Oui, auto-référentiel N'utilisez pas un paramètre de plugin qui canonise les publications traduites dans une langue principale, car cela les supprime de l'index et annule l'ensemble hreflang.
Comment ajouter hreflang sur un réseau multisite WordPress ?
Boucle get_sites(), passez à chaque blog pour lire son lieu et home_url()&, et mettre en cache la carte résultante dans un transitoire En cours d'exécution switch_to_blog sur chaque demande en wp_head est cher sur un grand réseau.



