Multiboutique PrestaShop : afficher les produits d'un autre site avec Homefeatured
Comment afficher les produits d'une boutique B sur la page d'accueil d'une boutique A en multiboutique PrestaShop. Guide complet avec code Smarty et SQL.
En bref : Pour afficher les produits d'une boutique B sur la page d'accueil d'une boutique A en multiboutique PrestaShop, la solution recommandée est un module personnalisé avec requête SQL ciblant explicitement l'id_shop source, combiné à une génération d'URLs via Shop::setContext() pour pointer vers le bon domaine.
Le défi du cross-site en multiboutique PrestaShop
Le mode multiboutique de PrestaShop permet de gérer plusieurs sites e-commerce depuis un seul back-office. Mais une limitation revient régulièrement : comment afficher sur la boutique A les produits qui appartiennent à la boutique B ?
Par défaut, le module Homefeatured (remplacé par ps_featuredproducts depuis PrestaShop 1.7) ne récupère que les produits associés à la boutique courante. Les requêtes SQL internes filtrent systématiquement par id_shop, ce qui empêche tout affichage cross-site natif.
Ce guide détaille deux approches pour contourner cette limitation, de la plus simple à la plus robuste.
Comprendre le filtrage multiboutique dans PrestaShop
Avant de modifier quoi que ce soit, il faut comprendre comment PrestaShop isole les données par boutique.
Le mécanisme Shop::addSqlRestriction
Chaque requête SQL générée par PrestaShop passe par la méthode Shop::addSqlRestriction(). Cette méthode ajoute automatiquement une clause WHERE qui filtre par id_shop ou par id_shop_group selon le contexte.
Dans le module Homefeatured (PS 1.6) ou ps_featuredproducts (PS 1.7+), la requête de récupération des produits ressemble à :
SELECT p.id_product, pl.name, pl.link_rewrite, i.id_image
FROM ps_product p
INNER JOIN ps_product_shop ps ON (ps.id_product = p.id_product AND ps.id_shop = {id_shop_courante})
INNER JOIN ps_product_lang pl ON (pl.id_product = p.id_product AND pl.id_shop = {id_shop_courante})
WHERE p.active = 1
Le filtre ps.id_shop = {id_shop_courante} est la raison pour laquelle les produits de la boutique B n'apparaissent jamais sur la boutique A.
Approche 1 : partager les produits entre boutiques (la plus simple)
La méthode la plus directe consiste à associer les produits aux deux boutiques depuis le back-office, puis à personnaliser le template pour rediriger les liens vers le bon domaine.
Étape 1 — Associer les produits aux deux boutiques
Depuis le back-office PrestaShop :
- Passez en contexte **"Toutes les boutiques"** via le sélecteur multiboutique
- Ouvrez chaque produit concerné
- Dans l'onglet **Associations**, cochez les deux boutiques
- Sauvegardez
- **Maintenance double** : chaque modification de produit doit être répétée ou synchronisée
- **URLs en dur** : le domaine cible est codé dans le template
- **Stocks partagés** : le stock est commun aux deux boutiques (sauf configuration avancée)
- **Cachez les résultats** : les requêtes cross-shop ajoutent des jointures supplémentaires. Utilisez le cache PrestaShop (`Cache::store()`) avec un TTL de 1 heure minimum
- **Indexez correctement** : vérifiez que `ps_product_shop` a bien un index composite sur `(id_product, id_shop)`
- **Canonical URLs** : assurez-vous que chaque produit a une URL canonique qui pointe vers sa boutique principale pour éviter le contenu dupliqué
- **Attribut `rel="nofollow"`** : sur les liens cross-site de la page d'accueil, envisagez d'ajouter `rel="nofollow"` si vous voulez que le jus SEO reste sur chaque domaine respectif
- **Documentez la configuration** : notez quelle boutique est la source et quelle est la cible
- **Testez les deux sens** : vérifiez les URLs générées depuis chaque boutique
- **Surveillez les 404** : après une mise à jour de produit, vérifiez que les liens cross-site restent valides
Pour une association en masse, utilisez cette requête SQL (à adapter) :
-- Associer tous les produits de la boutique 2 à la boutique 1
INSERT IGNORE INTO ps_product_shop (id_product, id_shop, id_category_default, id_tax_rules_group,
on_sale, online_only, ecotax, minimal_quantity, price, wholesale_price, unity, unit_price_ratio,
additional_shipping_cost, customizable, uploadable_files, text_fields, active, redirect_type,
available_for_order, available_date, show_condition, condition, show_price, indexed, visibility,
cache_default_attribute, advanced_stock_management, date_add, date_upd, pack_stock_type)
SELECT id_product, 1, id_category_default, id_tax_rules_group,
on_sale, online_only, ecotax, minimal_quantity, price, wholesale_price, unity, unit_price_ratio,
additional_shipping_cost, customizable, uploadable_files, text_fields, active, redirect_type,
available_for_order, available_date, show_condition, condition, show_price, indexed, visibility,
cache_default_attribute, advanced_stock_management, date_add, date_upd, pack_stock_type
FROM ps_product_shop
WHERE id_shop = 2;
Attention : n'oubliez pas de dupliquer également les entrées dans
ps_product_langpour la boutique cible, sinon les noms et descriptions seront vides.
Étape 2 — Personnaliser le template pour les liens cross-site
Une fois les produits visibles sur les deux boutiques, le problème suivant est que les liens pointent vers le domaine de la boutique courante au lieu de la boutique d'origine.
Pour corriger cela, il faut surcharger le template de la liste produits pour réécrire les URLs.
#### Sur PrestaShop 1.6 (Smarty)
Créez un fichier home-product-list.tpl dans votre thème :
{foreach from=$products item=product name=homeProducts}
<div class="product-container">
<div class="product-image-container">
{* Déterminer le domaine cible selon la boutique d'origine du produit *}
{assign var='target_domain' value='https://www.boutique-b.com'}
<a class="product_img_link"
href="{$target_domain}/{$product.link_rewrite|escape:'html':'UTF-8'}-{$product.id_product}.html"
title="{$product.name|escape:'html':'UTF-8'}">
<img class="img-responsive"
src="{$link->getImageLink($product.link_rewrite, $product.id_image, 'home_default')}"
alt="{$product.name|escape:'html':'UTF-8'}" />
</a>
</div>
<div class="product-meta">
<h5>
<a href="{$target_domain}/{$product.link_rewrite|escape:'html':'UTF-8'}-{$product.id_product}.html">
{$product.name|truncate:45:'...'|escape:'html':'UTF-8'}
</a>
</h5>
<span class="price">{$product.price|escape:'html':'UTF-8'}</span>
</div>
</div>
{/foreach}
Puis dans homefeatured.tpl, remplacez l'appel au template standard :
{if isset($products) && $products}
{include file="$tpl_dir./home-product-list.tpl" class='homefeatured tab-pane' id='homefeatured'}
{else}
<ul id="homefeatured" class="homefeatured tab-pane">
<li class="alert alert-info">{l s='No featured products at this time.' mod='homefeatured'}</li>
</ul>
{/if}
#### Sur PrestaShop 1.7+ / 8.x
Le module ps_featuredproducts utilise un système de templates différent. Surchargez le template dans votre thème :
themes/votre-theme/modules/ps_featuredproducts/views/templates/hook/ps_featuredproducts.tpl
La logique reste identique : intercepter le lien produit et remplacer le domaine.
Limites de cette approche
Approche 2 : requête SQL cross-shop dans un module personnalisé (recommandé)
Pour une solution plus propre et maintenable, la meilleure approche consiste à créer un module qui effectue directement une requête cross-shop.
Le principe
Au lieu de laisser PrestaShop filtrer par boutique, on écrit une requête qui cible explicitement la boutique source :
<?php
/**
* Module d'affichage cross-boutique pour la page d'accueil
* Compatible PrestaShop 1.7 / 8.x
*/
class AcCrossShopFeatured extends Module
{
public function __construct()
{
$this->name = 'ac_crossshopfeatured';
$this->version = '1.0.0';
$this->author = 'Alexandre Carette';
parent::__construct();
$this->displayName = $this->l('Cross-Shop Featured Products');
}
public function hookDisplayHome($params)
{
$targetShopId = (int) Configuration::get('AC_CROSSSHOP_TARGET_ID');
$categoryId = (int) Configuration::get('AC_CROSSSHOP_CATEGORY_ID');
$limit = (int) Configuration::get('AC_CROSSSHOP_LIMIT') ?: 8;
$idLang = (int) $this->context->language->id;
$sql = new DbQuery();
$sql->select('p.id_product, pl.name, pl.link_rewrite, pl.description_short');
$sql->select('ps.price, i.id_image');
$sql->from('product', 'p');
$sql->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . $targetShopId);
$sql->innerJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . $idLang . ' AND pl.id_shop = ' . $targetShopId);
$sql->leftJoin('image_shop', 'i', 'i.id_product = p.id_product AND i.id_shop = ' . $targetShopId . ' AND i.cover = 1');
if ($categoryId) {
$sql->innerJoin('category_product', 'cp', 'cp.id_product = p.id_product AND cp.id_category = ' . $categoryId);
}
$sql->where('ps.active = 1');
$sql->where('ps.visibility IN ("both", "catalog")');
$sql->orderBy('p.date_add DESC');
$sql->limit($limit);
$products = Db::getInstance()->executeS($sql);
// Récupérer le domaine de la boutique cible
$targetShopUrl = $this->getShopUrl($targetShopId);
$this->context->smarty->assign([
'crossProducts' => $products,
'targetShopUrl' => $targetShopUrl,
]);
return $this->display(__FILE__, 'views/templates/hook/crossshop-featured.tpl');
}
private function getShopUrl(int $shopId): string
{
$shop = new Shop($shopId);
$ssl = Configuration::get('PS_SSL_ENABLED') ? 'https://' : 'http://';
return $ssl . $shop->domain_ssl;
}
}
Le template associé
{if $crossProducts && count($crossProducts) > 0}
<section class="featured-products cross-shop-products">
<h2>{l s='Sélection de notre boutique partenaire' mod='ac_crossshopfeatured'}</h2>
<div class="products-grid">
{foreach from=$crossProducts item=product}
<article class="product-miniature">
<a href="{$targetShopUrl}/{$product.link_rewrite}-{$product.id_product}.html">
<img src="{$link->getImageLink($product.link_rewrite, $product.id_image, 'home_default')}"
alt="{$product.name|escape:'html':'UTF-8'}" loading="lazy" />
</a>
<h3>
<a href="{$targetShopUrl}/{$product.link_rewrite}-{$product.id_product}.html">
{$product.name|escape:'html':'UTF-8'}
</a>
</h3>
<span class="price">{Tools::displayPrice($product.price)}</span>
</article>
{/foreach}
</div>
</section>
{/if}
Construction correcte des URLs cross-boutique
L'un des pièges majeurs du cross-site multiboutique est la génération des URLs produit. Par défaut, $link->getProductLink() génère toujours une URL relative à la boutique courante.
Le problème
Si vous êtes sur boutique-a.com et affichez un produit de boutique-b.com, l'appel standard :
$link->getProductLink($product);
…retournera https://boutique-a.com/produit-1.html au lieu de https://boutique-b.com/produit-1.html.
La solution propre en PHP
Utilisez le contexte de la boutique cible temporairement :
// Sauvegarder le contexte actuel
$currentShopId = Shop::getContextShopID();
// Basculer vers la boutique cible
Shop::setContext(Shop::CONTEXT_SHOP, $targetShopId);
$targetLink = new Link();
$productUrl = $targetLink->getProductLink(
$product['id_product'],
$product['link_rewrite'],
null,
null,
$idLang,
$targetShopId
);
// Restaurer le contexte original
Shop::setContext(Shop::CONTEXT_SHOP, $currentShopId);
Cette méthode est la seule qui respecte les règles de réécriture d'URL configurées pour chaque boutique (URL simplifiée ou non, préfixe de langue, etc.).
Gestion des images en cross-boutique
Les images produit sont un autre point de vigilance. En multiboutique, les images sont partagées physiquement sur le serveur mais leur association est filtrée par id_shop dans la table ps_image_shop.
Si vous avez associé vos produits aux deux boutiques (approche 1), les images sont déjà accessibles. Sinon, vous devrez construire manuellement l'URL de l'image :
$imageUrl = _PS_BASE_URL_ . '/img/p/'
. implode('/', str_split((string) $idImage))
. '/' . $idImage . '-home_default.jpg';
Ou plus proprement via le Link object :
$imageUrl = $link->getImageLink($product['link_rewrite'], $product['id_image'], 'home_default');
Bonnes pratiques pour le multiboutique cross-site
Performance
SEO
Maintenance
Conclusion
L'affichage cross-boutique dans PrestaShop multiboutique n'est pas supporté nativement, mais reste tout à fait réalisable. L'approche la plus maintenable est le module personnalisé avec requête SQL ciblée, car elle offre un contrôle total sur les données affichées et les URLs générées. Pour des besoins ponctuels, le partage de produits avec surcharge de template reste une solution rapide et fonctionnelle.
Questions fréquentes
Tout ce que vous devez savoir sur ce sujet.
Un projet PrestaShop ?
Discutons-en directement.
193 projets livrés
Lire sur le blog

Alexandre Carette
Expert PrestaShop & Architecture E-commerce
Développeur PrestaShop depuis 2014, 193 projets livrés. Je conçois des architectures headless Nuxt + PrestaShop et des outils d'automatisation IA pour les e-commerçants.