La mayoría de los consejos de WordPress hreflang son " instale un complemento multilingüe y lo maneje." Está bien cuando el complemento es propietario de sus traducciones. Es la respuesta incorrecta en el caso de que llegue con más frecuencia: un sitio con versiones en dos o tres idiomas creadas como subdirectorios separados, instalaciones separadas o un conjunto de páginas traducidas manualmente, donde nadie quiere un marco de traducción completo y su esquema de base de datos para cuatro páginas.
Mantengo las propiedades de WordPress en esa forma, incluidos los sitios de documentación donde las traducciones están escritas a mano y en vivo /de/ y /es/. En el hreflang hay cuarenta líneas en un archivo de tema, se lee en una matriz y nunca ha cambiado. Esta guía es ese código, además de las partes que salen mal cuando un complemento emite hreflang junto con su tema.
Se encuentra debajo del guía completa de hreflang, al lado del Siguiente versión.js.
TL;Dr: WordPress no emite hreflang por sí solo. Gancho
wp_head, recorra un mapa de ubicación e imprima el conjunto recíproco completo, incluidox-defaulten cada página del grupo. Cree URL conhome_url()o un origen por localidad en lugar de una codificación, mantenga cada página y #39;s canónico autorreferencial y verifique que ningún complemento de SEO ya esté emitiendo un bloque competidor. Para configuraciones de múltiples sitios,get_sites()te da el mapa; para un solo sitio con páginas traducidas, una publicación de metacampo de campo es suficiente.
¿wordpress agrega hreflang por sí solo?
No. Core genera un enlace canónico, un enlace corto y varios enlaces de alimentación y REST wp_head, pero nada sobre alternativas lingüísticas. get_locale() sabe en qué idioma está el sitio y ninguna parte del núcleo lo convierte en una anotación.
Los complementos llenan el vacío. Polylang y WPML emiten hreflang para las traducciones que administran, y la mayoría de los complementos SEO lo emitirán si configura una relación de traducción que puedan ver. Si ya está usando uno de esos y cubre todo su sitio, úselo y omita este artículo. La razón para realizar un nómina es que sus traducciones existen fuera de lo que el complemento conoce, y un complemento que emite un conjunto parcial es peor que uno que no emite ninguno.
La versión mínima: una matriz, un gancho
Para un sitio cuyas traducciones son un pequeño conjunto conocido de prefijos 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 );
Tres cosas que esto hace bien y que los bloques escritos a mano normalmente no hacen.
Imprime el conjunto completo en cada página, incluida la página 's propio idioma. La reciprocidad se satisface estructuralmente: no hay ninguna rama que pueda omitir una entrada, porque no hay ninguna rama.
x-default es una fila en la misma matriz, por lo que se emite mediante el mismo bucle y no se puede olvidar en una plantilla.
Se escapan las URL. esc_url en un href en wp_head no es opcional y los valores de hreflang se analizan esc_attr por la misma razón.
Trabajando el camino actual
El camino es la parte que necesita cuidado, porque tiene que ser el camino dentro el idioma, no el URI de solicitud completo.
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 su instalación alemana está disponible en example.com/de y el visitante está encendido example.com/de/preise/, esto regresa /preise/, que luego recibe el prefijo de cada lugar y #39;s origen. El manejo de la barra diagonal es más importante de lo que parece: /preise y /preise/ son URL diferentes y apuntar una anotación a la forma no canónica es desajuste de barra final eso produce errores de etiqueta de retorno en un conjunto que de otro modo se vería perfecto. user_trailingslashit respeta la configuración de enlace permanente del sitio 's, por lo que coincide con cualquier servicio de WordPress.
La suposición aquí es que las URL traducidas son estructuralmente paralelas: /pricing/ mapas a /de/pricing/. Si tu slugs alemán también está traducido, /de/preise/, esta función no es suficiente y necesitas un mapeo explícito, que es la siguiente sección.
Cuando slugs difiere: un mapeo post-meta
Traducidos slugs son mejores para los usuarios y para la clasificación, y significan que la ruta no se puede derivar. Almacene las contrapartes en su lugar:
/**
* 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;
}
Entonces el wp_head la devolución de llamada fusiona la entrada actual de post' en ese mapa antes de imprimir, por lo que el conjunto es completo y autorreferencial:
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 )
);
}
}
El costo de mantenimiento es que el meta debe escribirse en ambos lados. Una interfaz de usuario de administrador con un selector de contraparte es la solución honesta; un comando WP-CLI que completa el mapa desde un CSV es el rápido. Lo que no debes hacer es completarlo en un solo lado, porque una declaración unidireccional es exactamente la falta etiqueta de retorno fracaso.
Multisitio: deriva el mapa de la red
Si cada idioma es un sitio en una red multisitio, el mapa ya existe:
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;
}
Almacenar en caché el resultado en un transitorio; switch_to_blog en un wp_head enganchar cada solicitud es un costo real en una red de cualquier tamaño. Y ten cuidado con get_locale() bajo switch_to_blog: refleja el sitio cambiado sólo si el sitio's WPLANG la opción está configurada, como es el caso habitual pero no garantizada en redes configuradas mediante una constante.
¿qué enfoque se adapta a su sitio?
| combate | acceso | Esfuerzo |
|---|---|---|
| Paralelo slugs, pocas localidades | Matriz estática más ruta derivada | Más bajo; una función |
| Traducido slugs | Mapa post-meta contraparte | Moderado; necesita un flujo de trabajo de edición |
| Multisitio, un sitio por idioma | Derivar de get_sites(), cachemat |
Código bajo, necesita disciplina de caché |
| Polylang o WPML gestionando traducciones | Utilice la salida plugin's | Verifique que emita x-default |
| Complemento SEO con campos de traducción manual | Úselo y luego verifique el cabezal renderizado | Esté atento a bloques duplicados |
En su lugar, coloque hreflang en el mapa del sitio
Si los encabezados de su página ya están abarrotados o tiene suficientes ubicaciones como para que el bloque sea más largo que el resto del encabezado, el mapa del sitio es la alternativa. WordPress 5.5 y envío posterior mapas de sitios principales, y son filtrables, aunque los filtros no se diseñaron teniendo en cuenta las alternativas y la declaración del espacio de nombres <urlset> no es algo que el núcleo te permita tocar limpiamente.
La ruta pragmática es un mapa del sitio independiente servido a partir de una regla de reescritura en lugar de una pelea 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;
} );
Dos cosas para hacer bien. El xmlns:xhtml declaración en <urlset> es requerido; sin él, las entradas alternativas se ignoran y el archivo aún se valida como un mapa del sitio, que es nuevamente el patrón de falla silenciosa. Y reescriba las reglas una vez después de agregar la regla, en la activación en lugar de en cada solicitud.
Una vez hecho esto, retire la salida del cabezal. Emitir ambos es mezclando métodos de implementación, lo que te da dos fuentes que flotan.
El problema del bloque duplicado
Esta es la falla específica de WordPress y la que vale la pena verificar antes de escribir cualquier código. Varios complementos emiten hreflang y más de uno puede estar activo a la vez. Un complemento SEO configurado con relaciones de traducción, un complemento multilingüe y su nueva función temática se imprimirán felizmente wp_head, y el resultado son dos o tres bloques con contenidos diferentes y posiblemente dos x-default entradas.
El manejo de Google & #39;s de anotaciones contradictorias dentro de una página es desconfiar de ellas. Entonces, un sitio que no tenía hreflang y ahora tiene tres bloques puede estar peor que antes.
Verifique la salida renderizada en lugar de la configuración del complemento:
curl -s https://example.com/de/preise/ | grep 'rel="alternate"' | grep hreflang
Cuente las líneas y cuente los distintos códigos. Una entrada por localidad más una x-default, y nada repetido. Si un complemento emite un conjunto que no puede configurar, la trampilla de escape habitual es eliminar su acción wp_head en lugar de agregar el tuyo arriba:
// Example shape: find the real hook and callback with `has_action`.
add_action( 'init', function () {
remove_action( 'wp_head', 'some_plugin_hreflang_output', 10 );
} );
Etiquetas canónicas, y no romperlas
Emite WordPress rel="canonical" para vistas singulares a través rel_canonical(), y apunta al enlace permanente propio de post', que es lo que quieres. La mayoría de los complementos de SEO lo reemplazan por el suyo propio, también autorreferencial de forma predeterminada.
La regla de hreflang vs canónico se aplica sin cambios: cada página se canonicaliza a sí misma y hreflang describe las relaciones entre esas páginas autocanónicas. La forma de romperlo con sabor a WordPress es una configuración de complemento que canonicaliza las publicaciones traducidas a "primario" versión de idioma, que se lee como una opción de deduplicación sensata y elimina silenciosamente sus traducciones del índice. Si una configuración ofrece canonicalizar entre idiomas, déjelo desactivado.
Verificando el resultado
Obtenga dos páginas hermanas y compare sus bloques de anotaciones. Deberían ser idénticos, y sólo los canónicos difieren. Luego verifique que cada URL que nombró devuelva 200 sin redirigir, porque las ubicaciones retiradas y los enlaces permanentes modificados son la forma en que un conjunto correcto se pudre en uno roto.
Pegue el conjunto en el generador hreflang gratuito para validar los códigos: comprueba cada valor con la forma ISO y marca subetiquetas no reconocidas como en-UK, detecta duplicados, rechaza URL relativas y advierte cuando el conjunto no tiene respaldo. Se ejecuta completamente en el navegador, lo cual importa cuando las URL describen un sitio cliente que aún no es público. Luego use Search Console's Informe de orientación internacional para la verificación en curso y el guía de herramientas Hreflang para el flujo de trabajo que lo rodea.
Preguntas frecuentes
¿wordpress agrega etiquetas hreflang automáticamente?
No. Core emite un enlace canónico y varios enlaces principales, pero nada sobre alternativas de lenguaje. Hreflang proviene de un complemento o del suyo propio wp_head salida.
¿cómo agrego hreflang en WordPress sin un complemento?
Gancho wp_head e imprime un <link rel="alternate" hreflang> línea para cada ubicación en un mapa que controlas, incluido un x-default entrada, en cada página del grupo. Escapa de los valores con esc_attr y esc_url.
¿a dónde van las etiquetas hreflang en un tema de WordPress?
En una función enganchada a wp_head, idealmente en un pequeño complemento mu en lugar del tema, por lo que cambiar de tema no elimina silenciosamente tus anotaciones.
¿qué pasa si mis páginas traducidas tienen diferentes slugs?
No derivar nada. Almacene los ID de publicación o URL de la contraparte en el meta de publicación y lea el mapa en el momento del renderizado. Completelo en ambos lados o creará una etiqueta de retorno faltante.
¿polylang y WPML manejan hreflang?
Sí, para las traducciones que gestionan. Verificar el resultado incluye x-default y que ningún segundo complemento emite un bloque competidor.
¿por qué tengo dos bloques hreflang en una página?
Dos cosas están imprimiendo wp_head: normalmente un complemento SEO más un complemento multilingüe, o un complemento más tu propia función. Se desconfía de las anotaciones contradictorias en una página, así que elimine una en lugar de superponerlas.
¿cada versión del idioma WordPress debería tener su propio canónico?
Sí, autorreferencial. No utilice una configuración de complemento que canonicaliza las publicaciones traducidas a un idioma principal, porque eso las elimina del índice y cancela el conjunto hreflang.
¿cómo agrego hreflang a través de una red multisitio de WordPress?
Bucle get_sites(), cambie a cada blog para leer su ubicación y home_url()y almacene en caché el mapa resultante en un transitorio. Ejecutando switch_to_blog en cada solicitud wp_head es caro en una red grande.



