Génération d’URL PrestaShop : Link, routes Symfony et _legacy_link

Comprendre les bons générateurs d’URL (Link, RouterInterface, getAdminLink, _legacy_link), éviter le hardcode et gérer SSL, multiboutique et SEO pour un site fiable.

Écran montrant des routes Symfony pour URL avec PrestaShop.

Table des matières :

  1. Cartographie de la génération d’URL dans PrestaShop (8.1 / 9.x)
  2. Classe Link : le générateur “legacy” qui tient encore tout le front
  3. Routes Symfony : RouterInterface, path() Twig, et génération d’URL BO propre
  4. _legacy_link : le pont entre Link::getAdminLink() et les routes Symfony
  5. Pièges récurrents : SSL, reverse proxy, multiboutique, et SEO (URLs non canoniques)
  6. Patterns robustes pour modules et thèmes : éviter le “hardcode” et tester la génération d’URL

Cartographie de la génération d’URL dans PrestaShop (8.1 / 9.x)

Une URL n’est pas un détail cosmétique dans un e‑commerce : c’est l’identifiant de ressource qui pilote le routage, le cache, le SEO, les permissions BO et une partie de la sécurité (redirections, mixed‑content, etc.). Le rappel de base est dans la spécification :

“A URI is a compact sequence of characters that identifies an abstract or physical resource.” — IETF RFC 3986, §1.1 (IETF RFC 3986)

Dans PrestaShop, la génération d’URL n’est pas unifiée. En 2026, même sur PrestaShop 9.x (Symfony 6.4), le front‑office reste majoritairement “legacy” (Dispatcher + Smarty + classe Link). Le back‑office est hybride : une partie est migrée vers Symfony (routes YAML/PHP, contrôleurs Symfony, Twig), une autre reste en contrôleurs legacy (AdminXxxController). Cette cohabitation explique la présence de mécanismes de pont comme _legacy_link.

Le point non négociable côté dev : ne jamais concaténer des bouts de chemin (/product.php?id=…, /admin123/index.php?controller=…) et espérer que ça tienne en multiboutique, multilangue, HTTPS forcé, reverse proxy et future migration. PrestaShop a déjà plusieurs couches de réécriture (Friendly URL, règles Dispatcher, éventuellement reverse proxy). Si vous court‑circuitez la génération officielle, vous fabriquez des URLs non canoniques, des doublons, des 404 silencieuses et du cache fragmenté.

Pour se donner un modèle mental simple, retenez la cartographie suivante (et assumez que, dans PrestaShop, le “bon” générateur dépend du contexte d’exécution) :

Contexte Générateur recommandé Ce que ça “sait” faire Symptôme si vous hardcodez
Front‑office (Smarty, contrôleurs legacy, modules FO) Link (getProductLink(), getPageLink(), getModuleLink()…) Friendly URL, domaine SSL, id_lang, id_shop, routes Dispatcher doublons d’URL, mauvais domaine en multiboutique, mixed content
Back‑office Symfony (contrôleurs Symfony, Twig BO) RouterInterface + Twig path()/url() routes nommées, préfixes, admin renommé, génération absolue/relative liens cassés lors d’un changement de route, dépendance au dossier admin
Back‑office legacy (AdminXxxController, modules historiques) Link::getAdminLink() tokens legacy, compatibilité menus/modules, chemin admin 403/redirect bouclée, “controller not found”, token invalide
Pont legacy ↔ Symfony _legacy_link (dans la route) compat ascendante : getAdminLink() renvoie une route Symfony régressions BO lors de migrations incrémentales

C’est aussi une question de “GEO” au sens très concret : dès que vous opérez en multi‑domaine (ex. example.fr / example.be) ou en multi‑langue (FR/NL/DE), une génération d’URL approximative produit immédiatement des incohérences observables par les utilisateurs (changement de domaine inattendu, page en mauvaise langue) et par les moteurs (duplication + canoniques contradictoires).

Classe Link : le générateur “legacy” qui tient encore tout le front

La classe Link (cœur PrestaShop) est le point d’entrée standard pour générer des URLs front‑office et une partie des URLs back‑office. Techniquement, Link s’appuie sur la configuration Friendly URL, les domaines shop/ssl, la langue, et les règles du Dispatcher (routes legacy du FO : product_rule, category_rule, etc.). C’est exactement la couche qui permet de passer d’un “contrôleur + paramètres” à une URL réécrite stable.

En module FO, les méthodes les plus utilisées restent : getPageLink(), getProductLink(), getCategoryLink(), getCMSLink() et getModuleLink(). Exemple (PrestaShop 8.1/9.x, PHP 8.1+) :

// Dans un ModuleFrontController ou un service legacy
$link = $this->context->link;

// URL d’une page CMS (id=12), en HTTPS, langue courante
$cmsUrl = $link->getCMSLink(
    12,
    null,
    true, // SSL
    (int) $this->context->language->id,
    (int) $this->context->shop->id
);

// URL d’un contrôleur front de module
$moduleUrl = $link->getModuleLink(
    'mymodule',
    'callback',
    ['order_ref' => $orderReference],
    true // SSL
);

Le piège classique est de générer “en aveugle” sans contrôler id_lang, id_shop et le mode SSL. En multiboutique, l’URL “correcte” dépend du shop context, pas du shop par défaut. Et derrière un reverse proxy, si PrestaShop ne voit pas le schéma HTTPS (headers X-Forwarded-Proto/trusted proxies), Link peut sortir des URLs en http:// même si votre navigateur est en https://, ce qui casse les canoniques et déclenche du mixed content.

Un cas très concret (et fréquent) : email transactionnel ou callback paiement. Ici, une URL relative est insuffisante : il vous faut une URL absolue, au bon domaine, avec le bon schéma. En pratique :

  • FO vers le client : utilisez Link avec SSL (et, si nécessaire, forcez id_shop et id_lang).
  • BO interne : évitez de bricoler __PS_BASE_URI__ : c’est précisément là que les multi‑domaines et sous‑répertoires vous piègent.

Sur le plan perf, Link est utilisé en boucle partout (listings, blocs, menus). Le cœur a des caches statiques, mais vous pouvez quand même vous tirer une balle dans le pied : par exemple, générer des milliers de liens produit dans un export “maison” en appelant getProductLink() objet par objet déclenche souvent du surcoût (résolution des slugs, calculs contextuels). Une approche pragmatique quand vous avez de la volumétrie :

  • Réduisez le nombre d’appels : générez uniquement les liens nécessaires (ex. pas de liens “détails produit” dans un export qui ne les consomme pas).
  • Stabilisez le contexte une fois (shop/langue/SSL) avant de boucler.
  • Profilez avant d’optimiser : si votre TTFB ou votre CPU explose, la génération d’URL peut devenir une part non négligeable, surtout sur des pages catalogue très denses.

Si vous avez déjà une pipeline perf/caching (Varnish/Redis/OPcache), le moindre doute sur la volumétrie mérite un profilage (voir aussi les sujets serveur dans Cache PrestaShop : Varnish, Redis, Memcached et OPcache côté serveur et TTFB PrestaShop : réduire le Time To First Byte sous 200 ms).

Routes Symfony : RouterInterface, path() Twig, et génération d’URL BO propre

Côté Symfony, la règle est connue et documentée :

“The Routing component maps an HTTP request to a set of configuration variables.” — Symfony Documentation, Routing (Symfony Routing documentation)

En BO (parties migrées), vous devez générer vos URLs via le routeur Symfony (RouterInterface / UrlGeneratorInterface) ou via les helpers Twig (path(), url()). Ça vous donne un résultat robuste face au renommage du dossier admin, aux changements de préfixe, et aux évolutions de routes. Exemple dans un contrôleur Symfony (module ou override BO), compatible PrestaShop 9.x (Symfony 6.4) :

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;

final class AdminMyController
{
    public function __construct(private RouterInterface $router) {}

    public function backToProducts(): string
    {
        return $this->router->generate('admin_products_index');
    }

    public function absoluteDocs(): string
    {
        return $this->router->generate(
            'admin_products_index',
            [],
            UrlGeneratorInterface::ABSOLUTE_URL
        );
    }
}

Deux points pratiques qui évitent des bugs “invisibles” :

  • Relatif vs absolu : en BO, path() suffit pour naviguer. Dès que vous sortez du navigateur (email, webhook, export, PDF), passez à url() (Twig) ou ABSOLUTE_URL (PHP), sinon vous livrez des liens incomplets.
  • Routes vs query string : une route nommée explicite (admin_products_index) est plus stable que des paramètres “historiques” (controller=AdminProducts). C’est précisément l’intérêt de Symfony, et c’est ce qui rend le BO maintenable à long terme.

Dans Twig (BO), path('route_name', {...}) est la base. PrestaShop utilise Twig de manière large côté BO ; si vous avez besoin de réviser la mécanique d’héritage/inclusion côté templates, voir Twig PHP : syntaxe essentielle, héritage de templates et includes. Là aussi, le gain n’est pas “propre” uniquement : c’est la seule façon de rester compatible quand le cœur renomme une route ou change ses paramètres.

Attention néanmoins au BO hybride : tout n’est pas en Symfony. Si vous générez une URL vers une page legacy (un AdminXxxController non migré), le routeur Symfony ne sait rien de controller=AdminXxx. Il faut alors revenir sur les primitives Link/getAdminLink() ou sur le pont _legacy_link (section suivante). Inversement, générer en legacy une URL vers une page migrée Symfony est possible (et souhaitable) si vous passez par les mécanismes de compat du cœur.

Enfin, pour éviter les surprises en recette, prenez l’habitude d’inspecter les routes réellement disponibles sur l’instance (et pas celles que vous “pensez” exister) :

# Liste des routes (utile pour vérifier un renommage ou un préfixe)
bin/console debug:router

# Filtrage (ex. tout ce qui touche aux produits)
bin/console debug:router | grep -i product

_legacy_link : le pont entre Link::getAdminLink() et les routes Symfony

_legacy_link est un attribut (dans les defaults de route Symfony) utilisé par PrestaShop pour faire correspondre une “identité legacy” (typiquement AdminProducts ou AdminProducts:editproduct) à une route Symfony moderne. Le but est pragmatique : le back‑office historique a des milliers de liens construits autour de controller=AdminXxx&token=…. Sans pont, une migration incrémentale vers Symfony casserait les menus, les modules tiers, les redirections et une bonne partie des hooks BO.

Exemple typique de route (format YAML) avec compat legacy — structure courante dans le core PrestaShop (la forme exacte de _controller peut varier selon les versions) :

admin_products_index:
  path: /sell/catalog/products/
  methods: [GET]
  defaults:
    _controller: 'PrestaShopBundle:Admin/Sell/Catalog/Product:index'
    _legacy_controller: AdminProducts
    _legacy_link: AdminProducts

admin_products_edit:
  path: /sell/catalog/products/{productId}/edit
  methods: [GET]
  defaults:
    _controller: 'PrestaShopBundle:Admin/Sell/Catalog/Product:edit'
    _legacy_controller: AdminProducts
    _legacy_link: AdminProducts:editproduct
    _legacy_parameters:
      id_product: productId

Concrètement, ce mapping permet à Link::getAdminLink('AdminProducts') (ou à des helpers BO similaires) de produire la route Symfony au lieu d’une URL legacy. C’est la seule stratégie viable pour la compat modules : un module qui ne connaît que AdminOrders peut continuer à fonctionner alors que la page est passée sur une route Symfony.

Mini‑scénario (typiquement ce qui casse en upgrade) :

  • Vous avez un module “SAV” qui ajoute un bouton “Éditer le produit” depuis une page BO custom.
  • En PrestaShop 8.x, votre lien legacy getAdminLink('AdminProducts', true, [], ['id_product' => 123, 'updateproduct' => 1]) “fonctionne”.
  • En PrestaShop 9.x, la page produit est migrée Symfony, et l’URL canonique BO devient une route (avec un chemin différent).
  • Si la route expose correctement _legacy_link + _legacy_parameters, votre module continue de pointer vers la bonne page sans modification.

Si vous développez vos pages BO en Symfony dans un module, et que vous voulez que les anciens liens controller=AdminMyLegacy continuent à rediriger proprement, vous devez déclarer _legacy_controller, _legacy_link et (si besoin) _legacy_parameters. La valeur de _legacy_link est particulièrement importante parce que beaucoup de liens historiques distinguent des “actions” via le suffixe (ex. AdminProducts:editproduct) : ce n’est pas décoratif, c’est ce qui permet de retomber sur la bonne route et de mapper les bons paramètres.

Le point faible (et il faut le dire) : c’est un mécanisme non standard Symfony, donc facile à casser si vous “nettoyez” des routes sans comprendre la rétro‑compatibilité. Quand _legacy_link est absent ou incohérent, vous retombez sur des URLs legacy générées à l’ancienne, ou sur des 404 en BO selon les chemins de code. En PrestaShop 9.x, le CLI est suffisamment exploitable pour auditer ça avec bin/console debug:router (voir aussi le contexte CLI/compat dans PrestaShop 9.1 : compatibilité PHP 8.1–8.5, CLI et nouveautés développeurs et PrestaShop 9 : nouveautés techniques Symfony 6.4, API et performances).

Pièges récurrents : SSL, reverse proxy, multiboutique, et SEO (URLs non canoniques)

Premier piège : domaine/protocole faux. En production derrière HAProxy/Nginx/Cloudflare, si vous ne déclarez pas correctement les proxies de confiance (et les headers forwardés), PrestaShop peut croire qu’il sert du HTTP alors que l’utilisateur est en HTTPS. Résultat : Link génère des http://…, vos canoniques divergent, certains navigateurs bloquent des assets, et vos règles de cache (Varnish) se fragmentent par schéma. Si vous opérez avec un reverse proxy, vérifiez la chaîne réseau et les headers côté systemd/HAProxy (cf. HAProxy 3.2 : déploiement systemd, configuration frontend/backend, ACL).

Checklist rapide “reverse proxy / SSL” (symptômes → causes probables) :

  • URLs en http:// dans le HTML alors que le site est en HTTPS → X-Forwarded-Proto non transmis, proxies non déclarés comme “trusted”, terminaison TLS mal comprise par PHP.
  • Assets bloqués (mixed content) → base URI/Media Server incohérents, thème qui hardcode des URLs absolues.
  • Cache fragmenté (http et https cache séparés) → variation involontaire du schéma, canoniques contradictoires.

Deuxième piège : multiboutique/multidomaine. Context est une variable globale, mutable, et ça se voit quand vous générez des URLs en tâche de fond (cron, worker, import) : si le shop context n’est pas forcé (Shop::setContext()/Context::getContext()->shop), vous sortez des liens pointant vers le mauvais domaine ou la mauvaise langue. Pour des automatisations (stocks/prix/commandes), c’est un classique : une URL “valide” techniquement, mais qui fait du cross‑domain involontaire.

Un exemple “GEO” réaliste : une enseigne a boutique.fr (FR) et boutique.be (BE) en multiboutique. Un import PIM lancé en CLI génère et enregistre des liens (ou envoie des emails) pendant que le contexte shop est resté sur la boutique FR. Résultat : des emails envoyés aux clients belges qui pointent vers boutique.fr (mauvaise TVA affichée, mauvaise langue, confusion, voire erreur de livraison). Les workflows d’automatisation doivent systématiquement fixer le contexte avant de générer des liens (sur ce sujet d’orchestration, voir Automatisation PrestaShop : orchestrer commandes, stocks et prix via MCP).

Troisième piège : SEO et gestion des changements de slugs. PrestaShop génère des URLs “propres” via link_rewrite, mais le cœur ne fournit pas nativement une stratégie robuste de redirections 301 quand un link_rewrite change (produit/catégorie/CMS). Résultat : vous fabriquez facilement des 404 historiques indexées. Si vous touchez aux slugs (migration, import PIM, refonte), prévoyez une couche de redirection (Nginx/Apache, module dédié, table de mapping).

Ce point est directement lié à la consolidation d’URLs : si plusieurs URLs mènent au même contenu (variantes de paramètres, chemins alternatifs), vous obtenez une dilution des signaux SEO. La documentation Google rappelle l’intérêt de consolider les duplicats (canonical, redirections, cohérence interne) : Consolidate duplicate URLs — Google

Pour une approche structurée côté SEO technique et perf, voir SEO PrestaShop : roadmap 3 mois audit, contenu, performance et popularité.

Enfin, n’oubliez pas un piège “bête” mais coûteux : les paramètres marketing (utm_*, gclid, etc.). Si votre thème/module réutilise l’URL courante (au lieu de régénérer une URL canonique via Link) pour construire des liens internes, vous propagez les paramètres partout, ce qui augmente mécaniquement le nombre d’URLs crawlables et complique l’analyse (logs, cache, analytics).

Patterns robustes pour modules et thèmes : éviter le “hardcode” et tester la génération d’URL

Le pattern sain en module : FO = Link, BO Symfony = RouterInterface, BO legacy/compat = Link::getAdminLink(). Si vous essayez d’unifier en “tout Symfony” ou “tout legacy”, vous allez perdre sur l’un des deux axes (compatibilité ou modernité).

Côté Symfony module, déclarez un service (autowiring/autoconfigure) et injectez explicitement le routeur. Côté legacy, restez sur $this->context->link ou injectez Link via le container si vous êtes déjà en environnement Symfony.

Pour les modules qui doivent survivre à des migrations 8 → 9 (et aux migrations incrémentales BO), un pattern très rentable est un “URL provider” interne : il tente de générer via route Symfony si elle existe, sinon retombe sur getAdminLink() ou getModuleLink(). Ce n’est pas élégant, mais c’est réaliste dans l’écosystème PrestaShop.

Une manière simple de rendre ce pattern opérationnel (pas seulement “joli”) est de définir quelques cas de test fonctionnels, centrés sur les risques réels :

  • Multiboutique : même action, deux domaines → vérifiez que l’URL générée pointe bien sur le domaine du shop courant.
  • Multilangue : produit/CMS en FR et EN → vérifiez que id_lang est respecté quand le contexte n’est pas initialisé (CLI).
  • HTTPS : site forcé en HTTPS derrière proxy → vérifiez que l’URL sort en https://.
  • Compat BO : un lien construit via getAdminLink('AdminProducts') doit arriver sur la page Symfony correspondante si _legacy_link est en place.

Ajoutez une petite suite de tests (au minimum des tests d’intégration sur un shop Docker) et faites tourner ça en CI pour détecter les régressions de route/tokens. Sur l’industrialisation, vous pouvez aligner avec une CI reproductible (voir Intégration continue PrestaShop : BuildKit, GitHub Actions et build reproductible) et avec des outils de qualité (voir PHPStan et Rector : industrialiser la qualité du code PHP).

Enfin, ne sous‑estimez pas l’impact “architectural” : la génération d’URL traverse la sécurité (BO), le cache (CDN/Varnish), et l’observabilité (liens cassés, 404). Une URL mal générée n’est pas un bug isolé, c’est une fuite systémique. Si vous développez des modules Symfony sérieux (Doctrine, services, contrôleurs), vous avez intérêt à suivre les conventions PrestaShop plutôt que de bricoler des chemins (voir Symfony PrestaShop : développer des modules robustes avec Doctrine). Le cœur est imparfait, mais les primitives Link, le routeur Symfony et _legacy_link sont précisément là pour absorber la complexité réelle (multistore, compat, admin renommé, etc.).

En synthèse opérationnelle (à garder en revue de code) :

  • Si vous voyez une concaténation d’URL en dur → demandez “pourquoi pas Link / RouterInterface ?”.
  • Si vous voyez un lien BO construit en legacy vers une page migrée → vérifiez _legacy_link et _legacy_parameters.
  • Si vous voyez des URLs absolues dans un thème → vérifiez qu’elles suivent bien le domaine SSL et le contexte boutique.
  • Si vous changez des slugs → planifiez la stratégie de redirection avant la mise en prod.



À lire aussi