**TL;DR.** Hreflang le dice a los motores de búsqueda qué variante de localidad servir a cada usuario. Asegúrate de cuatro cosas: (1) grupos recíprocos completos con auto-referencias, (2) códigos de idioma ISO correctos, (3) fallback x-default, (4) filtrado por entidad para que no publicites traducciones inexistentes. Cualquier otra cosa es un detalle de implementación.

## Qué hace hreflang

Hreflang es un atributo HTML (y anotación de mapa del sitio equivalente) que señala a Google qué idioma y región apunta una página. Hreflang implementado correctamente:

- Sirve a los usuarios españoles tu URL en español, no tu URL en inglés.
- Evita que tu URL en francés supere a tu URL en alemán en Alemania.
- Consolida la equidad de enlaces entre variantes de localidad en lugar de tratarlas como contenido duplicado.

Hreflang no mejora las clasificaciones. Controla qué variante de localidad se clasifica dónde.

## Paso 1: Decide dónde emitir hreflang

Tres opciones:

| Método               | Pros                                          | Contras                                      |
| -------------------- | --------------------------------------------- | -------------------------------------------- |
| Encabezado HTML      | Fácil de depurar; una fuente por página      | Agrega bytes a cada respuesta HTML           |
| Encabezado HTTP `Link` | Funciona para recursos no HTML (PDFs, etc.) | Difícil de depurar; raramente necesario      |
| Mapa del sitio XML   | Más compacto para catálogos grandes          | Menos visible para humanos; necesita disciplina de mapa del sitio |

El valor predeterminado para la mayoría de los sitios de comercio electrónico en 2026 es **encabezado HTML**. Cambia a solo mapa del sitio por encima de 50,000 URLs.

## Paso 2: Emitir un grupo completo

Para cada URL traducida, emite hreflang para sí misma y para cada otra variante.

Un grupo de 4 localidades (en, de, fr, es) para un producto:

```html
<link rel="alternate" hreflang="en" href="https://example.com/en/products/leather-bag" />
<link rel="alternate" hreflang="de" href="https://example.com/de/products/leather-bag" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/products/leather-bag" />
<link rel="alternate" hreflang="es" href="https://example.com/es/products/leather-bag" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products/leather-bag" />
```

Este grupo aparece en el encabezado de **cada** variante de localidad de este producto. La URL en alemán emite el mismo grupo, la URL en francés emite el mismo grupo, etc.

El grupo debe ser recíproco: A apunta a B, B apunta a A. Los grupos no recíprocos son ignorados silenciosamente por Google.

## Paso 3: x-default

`x-default` es el fallback para usuarios cuyo idioma del navegador no coincide con ninguna de tus variantes. Típicamente tu URL en inglés (o URL de localidad predeterminada).

```html
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products/leather-bag" />
```

La misma URL puede ser tanto `hreflang="en"` como `hreflang="x-default"`. Solo una URL en el grupo obtiene `x-default`.

## Paso 4: Filtrar entidades realmente traducidas

El mayor error práctico: emitir un grupo de 9 localidades en un producto que solo está traducido a 3 localidades. Las URLs publicitadas devuelven 404; Google ve el grupo roto y puede ignorar el hreflang de todo el sitio.

La solución: `availableLocales` por entidad.

```ts
// Pseudocódigo
interface ProductForSeo {
  slug: string;
  availableLocales: string[];  // por ejemplo, ["en", "de", "fr"]
}

interface StoreForSeo {
  supportedLocales: string[];  // por ejemplo, ["en", "de", "fr", "es", "it"]
  defaultLocale: string;
}

function buildHreflangCluster(product: ProductForSeo, store: StoreForSeo) {
  const effective = product.availableLocales.filter((l) =>
    store.supportedLocales.includes(l)
  );
  return effective.map((locale) => ({
    hreflang: locale,
    href: `https://example.com/${locale}/products/${product.slug}`,
  }));
}
```

El `src/lib/seo/storefront.ts` de Ordiko implementa esta intersección exacta.

## Paso 5: Códigos de idioma correctos

Usa códigos de idioma **ISO 639-1**:

| Idioma                  | Código     |
| ----------------------- | ---------- |
| Inglés                  | `en`      |
| Español                 | `es`      |
| Francés                 | `fr`      |
| Alemán                  | `de`      |
| Portugués               | `pt`      |
| Italiano                | `it`      |
| Ruso                    | `ru`      |
| Ucraniano               | `uk`      |
| Chino (Simplificado)    | `zh-Hans` |
| Chino (Tradicional)     | `zh-Hant` |
| Japonés                 | `ja`      |
| Coreano                 | `ko`      |
| Árabe                   | `ar`      |

Para idioma + región (cuando sirves una variante de localidad-región):

| Variante                | Código     |
| ----------------------- | ---------- |
| Inglés de EE. UU.      | `en-US`   |
| Inglés del Reino Unido  | `en-GB`   |
| Inglés canadiense       | `en-CA`   |
| Español de México       | `es-MX`   |
| Español de España       | `es-ES`   |
| Francés de Francia      | `fr-FR`   |
| Francés canadiense      | `fr-CA`   |
| Portugués de Brasil     | `pt-BR`   |
| Portugués de Portugal   | `pt-PT`   |

No uses:

- `en-EN` (inválido)
- `gb` (usa `en-GB`)
- `cn` (usa `zh-Hans`)
- `kr` (usa `ko`)

## Paso 6: Validar

Tres verificaciones:

1. **Auto-referencia**: cada URL debe incluirse a sí misma en su propio grupo.
2. **Reciprocidad**: cada variante en el grupo debe incluir cada otra variante.
3. **Alcance**: cada URL en el grupo debe devolver 200.

Herramientas:

| Herramienta                       | Qué verifica                                |
| ---------------------------------- | ------------------------------------------- |
| Google Search Console              | Errores hreflang en el mundo real a lo largo del tiempo |
| [hreflang.org/validator](https://hreflang.org) | Corrección del grupo                      |
| Sitebulb / Screaming Frog          | Reciprocidad y auto-referencia              |
| Ahrefs / Semrush                   | Informes de hreflang en auditoría del sitio |
| `curl` manual                      | Alcance por URL                            |

Patrón de prueba de Vitest para CI:

```ts
import { describe, it, expect } from 'vitest';

describe('grupo hreflang', () => {
  it('cada URL de variante devuelve 200', async () => {
    const cluster = [
      'https://example.com/en/products/x',
      'https://example.com/de/products/x',
      'https://example.com/fr/products/x',
    ];
    for (const url of cluster) {
      const res = await fetch(url);
      expect(res.status).toBe(200);
    }
  });

  it('cada variante emite hreflang auto-referencial', async () => {
    for (const url of cluster) {
      const html = await fetch(url).then((r) => r.text());
      expect(html).toContain(`hreflang="${getLocale(url)}" href="${url}"`);
    }
  });
});
```

## Casos especiales

**Aplicaciones de una sola página**: emite hreflang del lado del servidor en el encabezado del documento. No confíes en la inyección del lado del cliente: el rastreador de Google puede no ejecutar tu JS.

**ccTLDs**: hreflang sigue aplicándose. `example.de` puede especificar `hreflang="de-DE"` para sí mismo y enlazar a `example.com/en/...` para `en`.

**Región sin diferencia de idioma**: si sirves `en-US` y `en-GB` con contenido idéntico, hreflang los diferencia. No compartas la URL: dale a cada una su propia URL incluso si el contenido es idéntico.

## Errores comunes

| Error                                   | Efecto                                          |
| --------------------------------------- | ------------------------------------------------ |
| Grupo no recíproco                      | Google ignora hreflang por completo              |
| URL de variante devuelve 404            | Google ignora la URL; puede degradar el grupo    |
| Código de idioma incorrecto              | Variante tratada como no dirigida                |
| Múltiples URLs x-default                | Una es ignorada arbitrariamente                  |
| Hreflang solo en URL canónica           | Subpáginas no tienen señal hreflang              |
| Mezcla de URLs absolutas y relativas     | Usa siempre URLs absolutas                       |
| Dominio diferente por localidad sin hreflang consistente | El grupo se rompe                       |

## Cómo Ordiko emite hreflang

`src/lib/seo/storefront.ts` calcula `alternates.languages` para cada objeto de metadatos de página de Next.js. La función toma:

- `store.supportedLocales`
- `entity.availableLocales` (opcional, predeterminado a store.supportedLocales)
- `store.defaultLocale` (usado para x-default)

Y emite el grupo de intersección en el encabezado HTML. La auto-referencia, la reciprocidad y el x-default están garantizados por la implementación. El filtrado por entidad es optativo a través del campo `availableLocales` en el cargador de entidades.

## FAQ

**¿Encabezado HTML o mapa del sitio — cuál es mejor?**
El encabezado HTML es más fácil de depurar porque puedes verificar con Ver Fuente. El mapa del sitio es más compacto para tiendas con más de 50k URLs porque hreflang se emite una vez por par de URL, no en el encabezado de cada página. Elige HTML para tiendas con menos de 50k URLs; mapa del sitio por encima de eso. Ordiko emite encabezado HTML por defecto.

**¿Qué código de idioma debo usar para el español de América Latina frente al español de España?**
es-MX para México, es-AR para Argentina, es-CO para Colombia, es-ES para España. La combinación idioma-región es compatible. Para una sola localidad de América Latina que sirva a múltiples países, es-419 (el código de la ONU para América Latina y el Caribe) es válido pero menos ampliamente soportado.

**¿Puedo tener múltiples URLs x-default?**
No. Una x-default por grupo. Si tu negocio sirve inglés a nivel global, el .com (o tu URL de localidad predeterminada) es el x-default. La misma URL puede ser tanto hreflang='en' como hreflang='x-default'.

**¿Cómo maneja Ordiko la corrección de hreflang?**
La biblioteca de SEO de la tienda de Ordiko (src/lib/seo/storefront.ts) intersecta los availableLocales de cada entidad con los locales soportados de la tienda al emitir alternates.languages. Un producto traducido solo a en + de solo publicitará esos dos locales, independientemente del conjunto de locales más amplio de la tienda.