L'essentiel en 5 points
- Le bouton « Configurer » n'apparaît que si la classe du module a une méthode getContent().
- HelperForm, l'outil historique du cœur, dessine le même formulaire en 1.7.8.11 et en 9.2.0, sans une ligne propre à chaque version.
- Chaque valeur reçue se vérifie avant d'être enregistrée : interrupteurs forcés à 0 ou 1, nombres bornés, texte débarrassé de ses balises.
- Changer une valeur par défaut dans le code ne change rien chez une boutique où le module est déjà installé.
- Le système de traduction classique tient dans un fichier PHP par langue. PrestaShop conseille le nouveau système aux modules qui ne visent pas plus vieux que la 1.7.6.
5.1Le bouton « Configurer » et getContent()
Dans le gestionnaire de modules, certains modules ont un bouton « Configurer », d'autres non. Tu t'es déjà demandé pourquoi ? La documentation de PrestaShop répond en une ligne : « PrestaShop will check if this method exists and will only display the “Configure” button if this method is declared. »
La méthode en question, c'est getContent(), et je la trouve bien nommée. Elle est appelée quand on ouvre la page de réglages, et elle fait deux choses. Si le formulaire vient d'être envoyé, elle enregistre. Dans tous les cas, elle affiche le formulaire.
public function getContent()
{
$out = '';
if (Tools::isSubmit('submitMarkdownTwin')) {
$in = [];
foreach (array_keys(Renderer::TYPES) as $t) {
$in['type_' . $t] = Tools::getValue('MDTWIN_TYPE_' . strtoupper($t));
}
foreach (['accept', 'discovery', 'noindex', 'schema', 'product_facts', 'llms', 'content_selector', 'skip_selector', 'llms_intro', 'llms_limit', 'cache_ttl'] as $k) {
$in[$k] = Tools::getValue('MDTWIN_' . strtoupper($k));
}
Settings::save($in);
$out .= $this->displayConfirmation($this->l('Settings saved.'));
}
$base = $this->context->link->getBaseLink();
return $out . $this->renderForm($base);
}Tools::isSubmit() regarde si le bouton d'envoi a été cliqué, Tools::getValue() lit un champ du formulaire, displayConfirmation() affiche le bandeau vert « Réglages enregistrés ». Trois outils du cœur, présents depuis toujours, et c'est tout !
Je te montre ce que ça donne dans l'administration de la 9.2.0, en français.

5.2Le formulaire, dessiné par HelperForm
Tu n'écris pas une ligne de HTML pour ce formulaire, et crois-moi, c'est reposant. Tu le décris dans un tableau PHP, champ par champ, et HelperForm le dessine avec le style de l'administration. La documentation la présente elle-même comme « the legacy way of creating configuration pages for modules using HelperForm ».
L'ancienne façon, donc : celle de toujours.
Pourquoi pas la nouvelle, avec Symfony et ses formulaires ?
Parce que la boutique visée tourne en 1.7.8, et que je voulais le même module en 9.2.0 aussi. La 1.7.8.11 embarque Symfony 3.4.49, la 9.2.0 Symfony 6.4.46. Je ne parierais pas un euro sur un formulaire Symfony qui tourne tel quel sur les deux. HelperForm, lui, est là dans les deux versions. Au labo, la même page de réglages s'est affichée dans les deux administrations, à partir du même code.
Voici le début de la description du formulaire. Un interrupteur Oui / Non revenant six fois, on l'a rangé dans une petite fonction.
$switch = function ($name, $label, $desc) {
return [
'type' => 'switch', 'label' => $label, 'name' => $name, 'desc' => $desc, 'is_bool' => true,
'values' => [['id' => $name . '_on', 'value' => 1, 'label' => $this->l('Yes')], ['id' => $name . '_off', 'value' => 0, 'label' => $this->l('No')]],
];
};
$fields = [
'form' => [
'legend' => ['title' => $this->l('Markdown Twin'), 'icon' => 'icon-file-text'],
// [...]
'input' => [
// [...]
$switch('MDTWIN_ACCEPT', $this->l('Content negotiation'), $this->l('The normal URL answers in Markdown to "Accept: text/markdown". Browsers never ask for Markdown: visitors always get the HTML page.')),
// [...]
['type' => 'text', 'label' => $this->l('llms.txt entries'), 'name' => 'MDTWIN_LLMS_LIMIT', 'class' => 'fixed-width-sm', 'desc' => $this->l('Maximum number of products listed (10 to 2000).')],
],
'submit' => ['title' => $this->l('Save'), 'name' => 'submitMarkdownTwin'],
],
];
$h = new HelperForm();
$h->module = $this;
$h->name_controller = $this->name;
$h->token = Tools::getAdminTokenLite('AdminModules');
$h->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name;
$h->default_form_language = (int) $this->context->language->id;
$h->submit_action = 'submitMarkdownTwin';
// [...] les valeurs actuelles, lues dans la configuration
$h->fields_value = $values;
return $h->generateForm([$fields]);Regarde la ligne du jeton, Tools::getAdminTokenLite('AdminModules'). Le formulaire part avec le jeton de l'administration, celui que PrestaShop contrôle sur la page des modules. C'est gratuit, et c'est HelperForm qui le glisse dans le formulaire.
5.3Ne rien enregistrer sans le vérifier
Garde ça en tête, un formulaire d'administration, c'est une porte d'entrée comme une autre. Dans son exemple de page de réglages, la documentation de PrestaShop vérifie d'ailleurs la valeur reçue avec la classe Validate avant de l'enregistrer.
Markdown Twin fait le tri dans Settings::save(), et j'aime bien sa façon de faire : les interrupteurs deviennent 0 ou 1, quoi qu'on envoie. Les types de pages sont pris dans une liste fermée. Les nombres sont bornés, de 10 à 2 000 produits pour /llms.txt, de 0 à 86 400 secondes pour le cache. Et les textes perdent leurs balises.
\Configuration::updateValue(self::PREFIX . 'LLMS_LIMIT', max(10, min(2000, (int) (isset($in['llms_limit']) ? $in['llms_limit'] : $d['llms_limit']))));
\Configuration::updateValue(self::PREFIX . 'CACHE_TTL', max(0, min(86400, (int) (isset($in['cache_ttl']) ? $in['cache_ttl'] : $d['cache_ttl']))));Un employé qui tape « 999999 » dans le champ du sommaire en aura 2 000. Pas de message d'erreur, mais pas de sommaire d'un million de lignes non plus ;)
5.4Le piège des valeurs par défaut
Regarde bien le champ « Zone de contenu » sur la capture. Tu ne remarques rien ? Il contient #main, main, [role="main"], #content-wrapper, #content, body.
Or, dans le code de la version 1.0.0, la valeur par défaut est différente. #content-wrapper est passé en deuxième position, parce que certaines pages de blog n'ont pas de #main, et que main y attrapait l'en-tête et le menu avec le contenu.
Alors, pourquoi le labo affiche-t-il l'ancienne ? J'avoue que la première fois, ça surprend :)
Parce que le module y a été installé avant ce changement. À l'installation, les valeurs par défaut sont écrites dans la configuration de la boutique, une fois. Ensuite, c'est la valeur en base qui compte, et changer le défaut dans le code ne la touche pas.
public static function install()
{
foreach (self::defaults() as $k => $v) {
if (false === \Configuration::get(self::PREFIX . strtoupper($k))) {
\Configuration::updateValue(self::PREFIX . strtoupper($k), $v);
}
}
return true;
}C'est voulu, et je trouve que c'est très bien comme ça. Un marchand qui a réglé son module ne veut pas voir ses réglages écrasés à chaque mise à jour. Mais ça veut dire une chose, pour toi qui écris le module, et je te la souligne. Si une nouvelle version doit vraiment changer une valeur chez les boutiques qui ont déjà le module, c'est un script de mise à jour qui doit le faire. On en parle dans le chapitre sur la livraison.
5.5Traduire le module en français
Tout le module est écrit en anglais, je te l'ai dit. C'est ce que conseille la documentation, « we suggest writing all your wordings in English, and then translating them to other languages. » Et une chaîne qui n'a pas de traduction s'affiche en anglais, sans erreur.
PrestaShop a deux systèmes de traduction pour les modules. Le système classique est le plus ancien, et la documentation le présente comme « a heritage from previous versions of PrestaShop ». Le nouveau système, par domaines de traduction, est disponible pour les modules depuis la 1.7.6. Leur page de comparaison est nette : si ton module ne vise pas plus vieux que la 1.7.8, c'est le nouveau qui est recommandé.
Markdown Twin utilise le classique. Pas le plus moderne, mais le plus simple à fabriquer. Chaque phrase passe par $this->l(), et les traductions françaises vivent dans un seul fichier, translations/fr.php.
<?php
global $_MODULE;
$_MODULE = [];
$_MODULE['<{markdowntwin}prestashop>markdowntwin_bd55e0753e089474f86518e6c39a4566'] = 'Markdown Twin';
$_MODULE['<{markdowntwin}prestashop>markdowntwin_ecd7cbe91f0211a5568325e9a22b1646'] = 'Un jumeau Markdown de chaque page pour les assistants et agents d\'IA : le même contenu sans l\'interface, avec les faits et les données structurées de la page. C\'est toujours la page HTML que les moteurs de recherche indexent.';La clé a l'air barbare, je te l'accorde, mais elle se lit. Le nom du module entre chevrons, puis un nom de fichier (par défaut, le nom technique du module), puis l'empreinte MD5 de la phrase anglaise. Change un seul mot de la phrase anglaise, et l'empreinte change, la traduction ne colle plus.
D'habitude, ce fichier sort de l'administration, dans International › Traductions, qui lit le code du module et propose un formulaire. Chez Kōeki, on le génère avec un petit script, qui relit toutes les phrases $this->l('…') du module, calcule leurs empreintes, et refuse de finir s'il en manque une traduction. Comme ça, une phrase ajoutée sans traduction ne peut pas partir en livraison.
Le module est écrit, réglable, traduit. Il reste le plus important, et ma partie préférée : le tester pour de vrai, le relire avec un œil méchant, et le livrer. Tout est dans le dernier chapitre.
Sources
Historique de ce chapitre
- 19 octobre 2026 : première version, page de réglages de Markdown Twin 1.0.0 en 9.2.0 et 1.7.8.11.

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.

