El error de canonical que esconde tus traducciones

agentready.md se publica en diez idiomas con prefijo en la ruta. Cada página existe en /about, /es/about, /ja/about y siete más. Durante meses, las nueve traducciones llevaban esta misma línea en su <head>:

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

/es/about le estaba diciendo a los buscadores: soy un duplicado de la página en inglés, indexa esa. Lo mismo /ja/about y lo mismo las otras siete. Diez idiomas, uno indexable.

Qué dice de verdad un canonical

El rel="canonical" no es una pista sobre qué URL queda más limpia. Es una afirmación: esta página y la que nombro somos la misma, y la buena para el índice es la otra. Los enlaces, las señales de posicionamiento y la propia entrada en el índice se le acreditan al destino. Esta página deja de competir.

Es justo lo que quieres cuando /producto?color=azul apunta a /producto, y justo lo que no quieres en una traducción. Una página en español no es un duplicado de la inglesa: es otra página, para otro lector, y tiene que posicionar por su cuenta en las búsquedas en español.

Por qué sobrevivió tanto tiempo

Nuestro router quita el prefijo de idioma antes de enrutar, así que una petición a /es/about llega al handler como /about y el idioma viaja aparte. Es un diseño sensato, porque las rutas se declaran una vez y valen para todos los idiomas, pero deja request.url sin prefijo, y la plantilla del <head> construía el canonical con esa ruta pelada.

En inglés el fallo produce el resultado correcto, porque el inglés no lleva prefijo. Todas las páginas que miras mientras desarrollas en tu idioma están bien. Solo se rompe en los nueve que no lees.

El arreglo es una llamada, devolverle el prefijo antes de imprimir la URL:

<%# request.url ya ha perdido el prefijo /es/ — localizedUrl se lo devuelve. %>
<% const canonicalHref = localizedUrl(canonicalPath); %>
<link rel="canonical" href="<%= baseUrl %><%= canonicalHref %>">

La regla: cada versión es canonical de sí misma

Canonical autorreferente, siempre. /es/about declara /es/about. /ja/about declara /ja/about. Con las traducciones no hay excepción. Y si dos versiones se parecen muchísimo, la misma ficha de producto con tres palabras cambiadas, da igual: el idioma no es duplicación.

hreflang: cada versión las lista todas

El canonical dice qué página indexar. El hreflang dice a qué lector enseñar cuál de las indexadas. Hacen falta los dos, y hay dos reglas del hreflang que casi todo el mundo se salta.

Cada versión las lista todas, incluida ella misma. Diez idiomas son diez enlaces alternate en las diez páginas, con la autorreferencia dentro. Olvidarse de esa autorreferencia es el defecto más común en conjuntos por lo demás correctos.

Los enlaces tienen que ser recíprocos. Si la página en inglés nombra a la española y la española no le devuelve el nombre, el buscador descarta la relación, y a menudo el conjunto entero, porque no puede verificarla. Cualquiera puede decir que es la traducción de otro. Solo la otra página lo confirma.

Genéralos desde una lista en lugar de mantener diez:

<% ['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 %>">

Usa el código de idioma a secas (es) cuando una sola versión sirve a todos los hablantes. Añade región (es-ES, pt-BR) solo si de verdad publicas versiones distintas por mercado; inventarte regiones que no tienes parte el conjunto para nada.

x-default no es otra traducción

El x-default señala qué versión servirle a alguien cuyo idioma no publicas. Si buscan en neerlandés y tú no tienes neerlandés, se llevan lo que apunte ahí.

No es «el idioma más importante» ni una entrada más de la rotación. El nuestro se queda en la URL inglesa sin prefijo, que ya es el respaldo de toda la web. Si lo apuntas a una página con idioma propio, /es/about, mandas a español a todo lector para el que no tienes traducción, y eso es peor que el defecto.

Compruébalo en una línea

El fallo entero se ve desde la terminal. Recorre tus idiomas e imprime lo que declara cada página:

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

La salida correcta nombra en cada línea su propia ruta:

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

Diez líneas con la misma URL son el fallo de este artículo. Pásalo por las plantillas que hayas tocado y no solo por la portada: el canonical suele construirse por layout, y una web con tres layouts puede estar bien en dos.

Las otras formas de romperlo

Parámetros de seguimiento dentro del canonical. Si construyes el canonical con la URL de la petición tal cual, quien llegue con ?utm_source=newsletter recibe un canonical que apunta a ?utm_source=newsletter. Cada vez que alguien comparte el enlace nace una URL nueva que se declara canonical, o sea un conjunto sin límite de URLs sirviendo una sola página. Constrúyelo desde el pathname y devuelve solo los parámetros que identifican una página distinta de verdad, un listado filtrado o una paginación, y en un orden fijo, para que una página no pueda escribir su propio nombre de dos maneras.

Canonical apuntando a una redirección. Si /es/about declara /es/about/ y eso hace un 301 de vuelta a /es/about, le has dado al rastreador un bucle que resolver y un motivo para desconfiar de la señal. Apunta a la URL que devuelve 200.

Dos canonical que se contradicen. El <link> del HTML y una cabecera HTTP Link: <...>; rel="canonical" son igual de válidos, y los meten CDNs, plugins y proxies inversos. Cuando no coinciden, el resultado queda al azar y casi nunca es el que querías. Mira también las cabeceras:

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

Que no salga nada es la respuesta buena, salvo que la hayas puesto tú a propósito.

Analiza tu web · Generador de meta tags · Checklist de preparación para IA