Hreflang en PrestaShop multitienda con un idioma por dominio: el problema que nadie resuelve bien (y cómo lo solucioné)

Si tienes una multitienda en PrestaShop donde cada tienda tiene su propio dominio y su propio idioma, tarde o temprano te vas a topar con el mismo problema: los hreflang entre tiendas. PrestaShop no lo resuelve de forma nativa en este escenario. Aquí explico cómo lo resolví con un módulo a medida.

GC
Hreflang en PrestaShop multitienda con un idioma por dominio: el problema que nadie resuelve bien (y cómo lo solucioné)

El escenario real

El cliente es OriginalPaella, con cinco dominios independientes:

  • originalpaella.es → español (es-ES)
  • originalpaella.com → inglés (en)
  • originalpaella.fr → francés (fr-FR)
  • originalpaella.it → italiano (it-IT)
  • originalpaella.pt → portugués (pt-PT)

Cada tienda tiene un único idioma. Cada una vive en un dominio diferente. Y Google necesita saber que todas estas páginas son versiones alternativas de lo mismo.

El problema con el hreflang por defecto de PrestaShop

PrestaShop gestiona los hreflang de forma nativa, pero está pensado para un escenario diferente: una tienda con varios idiomas bajo el mismo dominio. Así genera algo como:

<link rel="alternate" hreflang="es" href="https://tienda.com/es/" />
<link rel="alternate" hreflang="en" href="https://tienda.com/en/" />
<link rel="alternate" hreflang="fr" href="https://tienda.com/fr/" />

Eso funciona bien con subdirectorios por idioma bajo un mismo dominio. Pero si cada idioma vive en un dominio distinto y en una tienda de PrestaShop diferente, el sistema nativo no cruza la información entre tiendas. Cada tienda genera sus propios hreflang solo con sus propios idiomas, sin mencionar las demás.

⚠️ Consecuencia SEO: Google no sabe que originalpaella.es/paella-valenciana y originalpaella.com/valencian-paella son la misma página en distintos idiomas. Pierdes la señalización geográfica e idiomática y arriesgas canibalización entre tus propios dominios.

La solución: un módulo propio

No encontré ningún módulo en el Marketplace de PrestaShop que resolviera exactamente este escenario (1 idioma por tienda, varios dominios propios). Los que existen trabajan dentro de una sola tienda o requieren configuraciones demasiado complejas para lo que es, en el fondo, un problema sencillo.

Así que desarrollé op_hreflangmultishop, un módulo ligero que hace exactamente lo que se necesita y nada más.

Cómo funciona el módulo

1. Mapa de tiendas e hreflang

El módulo define un array que relaciona cada id_shop de PrestaShop con su código hreflang:

$shopMap = [
    1 => 'es-ES',
    2 => 'en',
    4 => 'fr-FR',
    9 => 'it-IT',
];

Sencillo y directo. Cada vez que se carga una página, el módulo sabe a qué tiendas debe apuntar y con qué etiqueta de idioma.

2. Detección del tipo de página

El módulo se engancha en el hook displayHeader y detecta qué tipo de página está cargando: home, producto, categoría, CMS o blog (integración con el módulo stblog de ST Themecraft). Si la página no es ninguna de estas, no genera nada.

switch ($controller->php_self) {
    case 'index':
        $pageType = 'index';
        break;
    case 'product':
        $pageType = 'product';
        $idProduct = (int) $product->id;
        break;
    case 'category':
        $pageType = 'category';
        $idCategory = (int) Tools::getValue('id_category');
        break;
    // ...
}

Para el blog de stblog, la detección es diferente porque funciona como módulo de frontend (con fc=module), así que se detecta por los parámetros GET:

if ($fc === 'module' && $module === 'stblog') {
    if ($controllerName === 'article' && $idStBlog) {
        $pageType = 'stblog_post';
    } elseif ($controllerName === 'category' && $idStBlogCategory) {
        $pageType = 'stblog_category';
    } else {
        $pageType = 'stblog_home';
    }
}

3. Construcción de URLs por tienda

La parte más delicada. Para generar la URL correcta en cada tienda, el módulo cambia temporalmente el contexto de PrestaShop a la tienda de destino:

$ctx->shop = new Shop($idShop);
$ctx->link = new Link($ctx);
$langs = Language::getLanguages(true, $idShop);
$idLang = (int) ($langs[0]['id_lang'] ?? 0);

Esto permite usar los helpers nativos de Link para generar URLs correctas con friendly URLs, slugs traducidos y dominios propios de cada tienda:

case 'product':
    $url = $ctx->link->getProductLink($idProduct, null, null, null, $idLang, $idShop);
    break;

case 'category':
    $url = $ctx->link->getCategoryLink($idCategory, null, $idLang, null, $idShop);
    break;

⚠️ Importante: después de generar la URL, el contexto se restaura a la tienda original. Si no lo haces, rompes el resto del proceso de renderizado de la página.

4. El x-default

El estándar de Google recomienda definir un x-default para indicar la versión «genérica» que se debe mostrar cuando ningún hreflang coincide con el idioma del usuario. En este caso, se asigna a la tienda española:

if ($hreflangCode === 'es-ES') {
    $hreflangs['x-default'] = $url;
}

5. Resultado en el <head>

El resultado en el HTML de cada página es exactamente esto — generado en todas las tiendas, apuntando a todas las demás:

<link rel="alternate" hreflang="es-ES" href="https://originalpaella.es/paella-valenciana/" />
<link rel="alternate" hreflang="en" href="https://originalpaella.com/valencian-paella/" />
<link rel="alternate" hreflang="fr-FR" href="https://originalpaella.fr/paella-valencienne/" />
<link rel="alternate" hreflang="it-IT" href="https://originalpaella.it/paella-valenciana/" />
<link rel="alternate" hreflang="x-default" href="https://originalpaella.es/paella-valenciana/" />

✓ Resultado: Google lee este bloque en todas las tiendas, entiende la relación entre los dominios y consolida correctamente las señales de relevancia geográfica e idiomática.

Detalles técnicos que importan

Integración con stblog

La URL de los artículos y categorías del blog requiere incluir el link_rewrite correcto para cada idioma (el slug traducido). El módulo carga las clases de stblog si están disponibles y construye la URL con los parámetros adecuados:

if (class_exists('StBlogClass')) {
    $blog = new StBlogClass($id, $idLang);
    if (!empty($blog->link_rewrite)) {
        $params['rewrite'] = $blog->link_rewrite;
    }
}

// Limpieza de parámetros residuales
$url = preg_replace('/(\\?|&)(id_blog|id_st_blog|blog_id_category|id_st_blog_category)=\\d+/i', '', $url);

Manejo de errores

Todo el bloque de generación de URLs está envuelto en un try/catch con \Throwable. Si una tienda no tiene el producto, la categoría o la página CMS disponible (porque no está traducida), el módulo simplemente ignora esa tienda y sigue con las demás. No rompe la página, no lanza errores al usuario.

¿Cuándo usar este enfoque?

Este módulo es la solución correcta si tu setup es exactamente este:

  • PrestaShop multitienda activado
  • Una tienda por cada idioma/mercado
  • Un dominio propio por tienda
  • Un único idioma activo por tienda

ℹ️ Si tu estructura es diferente: un dominio con subdirectorios por idioma, o una tienda con varios idiomas activos, el sistema nativo de PrestaShop ya lo gestiona razonablemente bien y no necesitas esto.

El contexto de PrestaShop: el mayor obstáculo del desarrollo

El principal obstáculo no fue la lógica en sí, sino entender bien cómo manipular el contexto de PrestaShop de forma segura para generar URLs de otras tiendas sin romper nada. La documentación oficial de PS8 en este punto es escasa, y hay que bucear bastante en el código fuente para entender el ciclo de vida del objeto Context y cómo se relaciona con Link.

Cuidado con las páginas que no existen en todas las tiendas: un artículo de blog que existe en español pero no tiene versión francesa puede generar URLs rotas si no validas el resultado antes de incluirlo en el hreflang. El try/catch con \Throwable es imprescindible aquí.

¿Merece la pena desarrollarlo? Depende de tu setup

Si llevas una multitienda de PrestaShop con dominios independientes por idioma, el hreflang entre tiendas es algo que no puedes ignorar desde el punto de vista SEO. Y como PrestaShop no lo resuelve de caja en este escenario, o lo desarrollas tú o te quedas sin él.

El módulo que he descrito aquí es lo suficientemente sencillo como para que cualquier desarrollador con experiencia en PS8 lo entienda, lo adapte a sus id_shop y lo use en producción sin demasiado esfuerzo.

¿Tienes una multitienda PrestaShop con dominios por idioma y necesitas el hreflang correcto?

Puedo desarrollar o adaptar este módulo a tu configuración específica. Cuéntame tu setup y lo vemos.