Aller au contenu
Rechercher
Discutons
Menu

Dossier module PrestaShop · chapitre 4 sur 6

Créer de nouvelles adresses dans un module PrestaShop avec moduleRoutes

Avec le crochet moduleRoutes, un module ajoute ses propres règles d'adresse au routeur de PrestaShop. Markdown Twin s'en sert pour doubler chaque page d'une adresse en .md, et il a fallu déjouer la redirection canonique, qui renvoyait poliment chaque jumeau vers sa page HTML. Voici le code, et ce qu'il a donné en 9.2.0 et en 1.7.8.11.

Par Sébastien GrillotTesté au labo : 19 octobre 2026Publié : 19 octobre 2026
Testé sur
PrestaShop 9.2.0 · PHP 8.3.32
Et sur
PrestaShop 1.7.8.11 · PHP 7.4.33
Crochets
moduleRoutes · actionDispatcherBefore

L'essentiel en 5 points

  1. Une route, c'est une règle d'adresse, un contrôleur, des mots-clés et des paramètres. Un module les déclare dans le crochet moduleRoutes.
  2. Pour donner un jumeau à une page, Markdown Twin reprend la règle de la boutique, lui ajoute .md, et envoie vers le contrôleur natif de la page.
  3. La redirection canonique de PrestaShop compare l'adresse demandée à l'adresse officielle de la page. Le module retire le .md de l'adresse demandée avant ce contrôle.
  4. Un blog qui déclare ses propres routes a ses jumeaux aussi. Au labo, /blog/la-route-de-la-lavande-n106.md répond en Markdown, en 9.2.0 comme en 1.7.8.11.
  5. Sans URL simplifiées, Markdown Twin 1.0.0 n'a pas de repli, et son lien de découverte devient une adresse cassée.

4.1Une route, c'est quoi ?

Je commence par une question toute bête. Quand un visiteur demande /3-vetements, comment PrestaShop sait-il qu'il faut afficher la catégorie n° 3 ? Il passe l'adresse à son routeur, le Dispatcher, qui la compare à une liste de règles. {id}-{rewrite} pour une catégorie, {id}{-:id_product_attribute}-{rewrite}.html pour un produit en 9.2.0. La première règle qui colle désigne le contrôleur à lancer, et les morceaux entre accolades deviennent ses paramètres.

Une route, c'est ça, ni plus ni moins. Une règle, un contrôleur, des mots-clés (les morceaux entre accolades, avec ce qu'ils acceptent), et des paramètres fixes.

Un module ajoute les siennes dans le crochet moduleRoutes. Pour comprendre, je te propose de partir du plus simple, le petit module de blog qu'on a écrit pour le labo. Il déclare trois routes, l'accueil du blog, un article, une rubrique.

public function hookModuleRoutes($params)
{
    $p = ['fc' => 'module', 'module' => $this->name];
    $rewrite = ['regexp' => '[_a-zA-Z0-9\pL\pS-]*', 'param' => 'rewrite'];

    return [
        'module-laboblog-home' => ['controller' => 'blog', 'rule' => 'blog', 'keywords' => [], 'params' => $p],
        'module-laboblog-news' => ['controller' => 'blog', 'rule' => 'blog/{rewrite}-n{n}',
            'keywords' => ['rewrite' => $rewrite, 'n' => ['regexp' => '[0-9]+', 'param' => 'n']], 'params' => $p],
        'module-laboblog-category' => ['controller' => 'blog', 'rule' => 'blog/{rewrite}-c{c}',
            'keywords' => ['rewrite' => $rewrite, 'c' => ['regexp' => '[0-9]+', 'param' => 'c']], 'params' => $p],
    ];
}
laboblog.php : les routes du module de blog de labo (jamais livré, il sert aux tests)

Lis la deuxième route avec moi. L'adresse /blog/la-route-de-la-lavande-n106 colle à la règle blog/{rewrite}-n{n}. Le routeur en tire rewrite = la-route-de-la-lavande et n = 106, et lance le contrôleur blog du module laboblog, grâce aux paramètres fc et module. Le contrôleur n'a plus qu'à lire n et à afficher l'article 106.

Pourquoi moduleRoutes, et pas un autre crochet ? Parce que c'est le seul qui existe dans les deux versions. La 9.2.0 a un crochet plus souple, actionAfterLoadRoutes, qui permet de modifier toutes les routes après leur chargement. La 1.7.8.11 ne le connaît pas, il n'apparaît nulle part dans son code. Pour un module qui doit tourner sur les deux, je n'ai pas hésité longtemps.

4.2Des adresses en .md pour toutes les pages

Pour Markdown Twin, je voulais une adresse en .md pour chaque page de la boutique. Les produits, les catégories, les pages CMS, les marques, les fournisseurs, et l'accueil.

L'idée, je te la donne en une phrase : pour chaque règle du cœur, on déclare la même règle suivie de .md, vers le même contrôleur, avec un paramètre en plus, mdtwin=1, qui dit « c'est le jumeau qu'on veut ».

public function hookModuleRoutes($params)
{
    $props = (new ReflectionClass('Dispatcher'))->getDefaultProperties();
    $core = isset($props['default_routes']) ? $props['default_routes'] : [];
    $routes = [];
    foreach (self::ROUTES as $id) {
        if (empty($core[$id])) {
            continue;
        }
        // ponytail: one rule for all languages (the default language's custom rule), per-language custom rules not handled
        $rule = Configuration::get('PS_ROUTE_' . $id) ?: $core[$id]['rule'];
        $routes['markdowntwin_' . $id] = [
            'controller' => $core[$id]['controller'],
            'rule' => $rule . '.md',
            'keywords' => $core[$id]['keywords'],
            'params' => ['mdtwin' => 1],
        ];
    }
    $routes['markdowntwin_index'] = ['controller' => 'index', 'rule' => 'index.md', 'keywords' => [], 'params' => ['mdtwin' => 1]];
markdowntwin.php : la première moitié de hookModuleRoutes, Markdown Twin 1.0.0

Deux détails comptent, et je les trouve plus importants que le code lui-même :

D'abord, et c'est ce que je préfère, le module ne recopie aucune règle. Il lit celles du cœur dans la classe Dispatcher, puis regarde si la boutique les a modifiées dans ses réglages d'adresses (les valeurs PS_ROUTE_… de la configuration). C'est pour ça que le même code donne /19-customizable-mug.html.md en 9.2.0 et /accessoires-de-maison/6-mug-the-best-is-yet-to-come.html.md en 1.7.8.11. Chaque boutique garde ses propres adresses, et le module suit !

Ensuite, regarde le commentaire. Une boutique peut avoir une règle différente par langue, et le module n'en prend qu'une, celle de la langue par défaut. C'est une limite assumée, notée dans le code. Le code de la 9.2.0 prévient d'ailleurs, à propos de moduleRoutes, que « these routes are not multilanguage ». Si ta boutique a des règles d'adresse par langue, ce module ne les suivra pas toutes.

Et je n'ai pas testé de boutique multilingue, je préfère te le dire.

Les jumeaux, mesurés au labo le 19 octobre 2026

  • 9.2.0 : /index.md, /19-customizable-mug.html.md, /1-1-hummingbird-printed-t-shirt.html.md (une déclinaison), /3-vetements.md, /content/1-livraison.md.
  • 1.7.8.11 : /index.md, /accessoires-de-maison/6-mug-the-best-is-yet-to-come.html.md, /3-vetements.md, /content/1-livraison.md.
  • Toutes répondent 200, en text/markdown; charset=utf-8, par le contrôleur natif de la page, sans aucune redirection.
  • Les adresses HTML d'origine répondent comme avant, avec leur page HTML.

4.3Le piège de la redirection canonique

Sur le papier, c'était fini. En vrai, PrestaShop a une politesse qui gâche tout, la redirection canonique !

Chaque contrôleur de page connaît l'adresse officielle de sa page, et avant d'afficher quoi que ce soit, il la compare à l'adresse demandée. Si elles diffèrent, il redirige vers l'officielle. Pour le SEO, je trouve ça très bien, un produit n'a qu'une adresse. Mais pour la fiche du mug, l'adresse officielle est /19-customizable-mug.html, pas /19-customizable-mug.html.md. D'après le code, chaque jumeau serait donc renvoyé vers sa page HTML !

Je te montre la comparaison, dans le code de la 9.2.0.

$match_url = rawurldecode(Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
if (!preg_match('/^' . Tools::pRegexp(rawurldecode($canonical_url), '/') . '([&?].*)?$/', $match_url)) {
    // [...]
    $redirect_type = Configuration::get('PS_CANONICAL_REDIRECT') == 2 ? '301' : '302';
classes/controller/FrontController.php de PrestaShop 9.2.0, dans canonicalRedirection() (extrait)

Tu vois ce qu'elle lit ? $_SERVER['REQUEST_URI'], l'adresse demandée.

Or le crochet actionDispatcherBefore est appelé juste avant le contrôleur. Markdown Twin y repère une demande de jumeau, et retire le .md de REQUEST_URI. Quand le contrôleur fait sa comparaison, il voit l'adresse HTML, qui est la bonne. Pas de redirection.

$path = (string) parse_url($uri, PHP_URL_PATH);
if (Tools::getValue('mdtwin') && preg_match('#\.md$#i', $path)) {
    $page = preg_match('#(^|/)index\.md$#i', $path) ? substr($path, 0, -8) : substr($path, 0, -3);
    $query = (string) parse_url($uri, PHP_URL_QUERY);
    $public = $page . ('' !== $query ? '?' . $query : '');
    // the shop routes and checks its canonical URL as for the HTML page
    $_SERVER['REQUEST_URI'] = $public;
    self::$publicUri = $public;
    self::$mode = 'suffix';
    // a redirect met on the way (old URL, missing combination…) keeps asking for the twin of its target
    header_register_callback([__CLASS__, 'twinRedirect']);
}
markdowntwin.php : hookActionDispatcherBefore, Markdown Twin 1.0.0 (extrait)

Le module garde en mémoire qu'il s'agit d'un jumeau (self::$mode), et c'est ce que lit ensuite actionOutputHTMLBefore, au chapitre des crochets, pour remplacer la page.

4.4Et les redirections en chemin ?

Une adresse fausse, ça arrive. Un ancien nom de produit, une déclinaison qui n'existe plus. PrestaShop redirige alors vers la bonne page, et c'est parfait pour un humain. Pour un agent qui demandait le jumeau, il faut que la redirection mène au jumeau de la bonne page, pas à sa version HTML.

C'est le rôle de header_register_callback(), une fonction de PHP qui appelle ton code au moment où les en-têtes partent. Markdown Twin y relit l'en-tête Location, et lui ajoute le .md.

Mesuré au labo, en 9.2.0 : /19-mauvais-nom.html.md a répondu 302, avec Location: http://127.0.0.1:8092/19-customizable-mug.html.md. L'agent reste dans le monde du Markdown ;)

4.5Les adresses des autres modules : le cas du blog

La boutique pour laquelle on a écrit ce module a un blog, dans un module à part. Ses articles ont leurs propres adresses, déclarées par leur propre moduleRoutes. Les règles du cœur ne les couvrent pas.

Et je ne crois pas que ce soit un cas isolé ! En 9.2.0, Apparence › Positions montre cinq modules branchés sur moduleRoutes. Deux sont les nôtres, les trois autres sont arrivés avec l'installation de PrestaShop.

Les modules branchés sur le crochet moduleRoutes dans PrestaShop 9.2.0
Le crochet moduleRoutes en 9.2.0 : Klaviyo, PrestaShop Checkout, le serveur MCP et Markdown Twin, plus le blog de labo, sous la coupe

Comment faire ? Markdown Twin demande à PrestaShop la liste des modules branchés sur moduleRoutes, appelle leur méthode pour connaître leurs routes, garde celles qui mènent à un contrôleur de module, et les double avec un .md. La liste vient de Hook::getHookModuleExecList('moduleRoutes'), une méthode du cœur, présente dans les deux versions.

Au premier essai, surprise : les jumeaux du blog répondaient… en HTML :(

La cause était dans actionDispatcherBefore. PrestaShop lui passe le type de contrôleur, et le module n'acceptait que les pages de la boutique (Dispatcher::FC_FRONT). Les pages d'un module sont d'un autre type, Dispatcher::FC_MODULE. Une fois ce type ajouté, /blog/la-route-de-la-lavande-n106.md a répondu 200 en Markdown, en 9.2.0 comme en 1.7.8.11, avec type: module/laboblog dans son en-tête YAML.

Retiens la leçon, je crois qu'elle vaut pour tous les crochets qui reçoivent un type de contrôleur. Pour PrestaShop, une page de module n'est pas une page de la boutique, mais une page à part.

4.6/llms.txt, un contrôleur de module

Le sommaire /llms.txt n'est le jumeau de rien. Il lui faut sa propre page, donc son propre contrôleur, et c'est l'occasion de te montrer comment on en écrit un. Dans un module, un contrôleur de boutique suit trois règles, que la documentation de PrestaShop pose ainsi : il vit dans controllers/front/, il hérite de ModuleFrontController, et son nom suit ce format, « in CamelCase, the class name follows this format: <ModuleClassName><FileName>ModuleFrontController. »

Je te traduis : pour le fichier controllers/front/llms.php du module markdowntwin, ça donne une classe MarkdownTwinLlmsModuleFrontController.

class MarkdownTwinLlmsModuleFrontController extends ModuleFrontController
{
    public function initContent()
    {
        if (!MarkdownTwin\Settings::get('llms')) {
            Tools::redirect('index.php?controller=404');
        }
        header('Content-Type: text/plain; charset=utf-8');
        header('X-Robots-Tag: noindex');
        header('Cache-Control: public, max-age=3600');
        echo MarkdownTwin\LlmsTxt::build();
        exit;
    }
}
controllers/front/llms.php de Markdown Twin 1.0.0

Sans route, ce contrôleur répondrait à une adresse du genre /module/markdowntwin/llms. Une dernière route dans moduleRoutes, avec la règle llms.txt et les paramètres fc=module et module=markdowntwin, lui donne l'adresse attendue. Au labo, /llms.txt a répondu 200 en text/plain, avec 49 lignes pour la boutique de démo de la 9.2.0.

4.7Et sans URL simplifiées ?

Tu le savais ? PrestaShop peut tourner sans réécriture d'adresses. Les pages ont alors des adresses du genre index.php?id_product=19&controller=product, et aucune règle de route ne s'applique.

La documentation donne l'outil qui marche dans les deux cas, getModuleLink() : « The generated address handles automatically HTTP or HTTPS environments, with or without URL rewriting. »

Markdown Twin 1.0.0 ne l'utilise pas partout, et je ne vais pas te le cacher, c'est un des trois défauts trouvés à la relecture. URL simplifiées désactivées, il n'y a plus de route en .md, et le lien de découverte devient /index.php.md, une adresse qui ne mène nulle part. Le bon repli, c'est un paramètre ajouté à l'adresse de la page (index.php?…&mdtwin=1), et getModuleLink() pour /llms.txt. Si tu écris un module qui fabrique des adresses, teste-le dans les deux modes dès le premier jour, pas à la fin comme on l'a fait :D

Les adresses sont là, les jumeaux répondent. Reste à laisser le marchand régler tout ça depuis son administration, en français. C'est le chapitre sur les réglages et les traductions.

Sources

Historique de ce chapitre

  • 19 octobre 2026 : première version, routes testées au labo sur PrestaShop 9.2.0 et 1.7.8.11.
Sébastien Grillot

Co-fondateur de Kōeki · Consultant SEO et IA

Sébastien Grillot

Consultant SEO depuis 2009 : c'est ici, sur Inside da WeB, qu'il a monté sa première agence, spécialisée dans l'e-commerce. Il construit aujourd'hui ses propres outils avec l'IA, modules PrestaShop compris.

FermerLes modules branchés sur le crochet moduleRoutes dans PrestaShop 9.2.0