404 sur les fiches produit après le passage à PrestaShop 9
Le symptôme
Après le passage à PrestaShop 9, les fiches produit atteintes par les anciennes adresses, celles qui ont le nom de la catégorie devant l’ID (/categorie/12-nom-du-produit.html), répondent 404. Les adresses sans catégorie (/12-nom-du-produit.html) fonctionnent et la boutique se parcourt normalement. Le dégât vient de l’extérieur : les liens d’autres sites, les favoris et les pages déjà indexées par Google pointent toujours vers les anciennes adresses, et à la prochaine visite ils trouvent une page d’erreur à la place du produit.
Ce que ça semblait être, et que ça n’était pas
Le premier soupçon, devant un 404 après une migration, est en général le .htaccess : règles de réécriture perdues, mod_rewrite désactivé. Ce n’est pas le cas ici : la requête arrive à PrestaShop, et la page que l’on voit est la page « introuvable » du thème, générée par la boutique.
Le deuxième soupçon est le nom du produit : si le link_rewrite a changé, l’adresse ne correspond plus. Mais dans ce cas PrestaShop ne répond pas 404. Avec le bon ID et le mauvais nom, il redirige par un 301 vers l’adresse correcte, à condition que la redirection canonique soit active avec l’option « 301 Moved Permanently » (PS_CANONICAL_REDIRECT = 2). Le 404 n’apparaît que lorsque l’adresse a une forme qu’aucune règle ne reconnaît.
La vraie cause
PrestaShop construit et reconnaît les adresses avec une règle par type de page. Pour les produits, la règle par défaut dans les versions 1.7 et 8 est la suivante (la forme de 1.7.1.2, 1.7.8.10 et 8.1) :
{category:/}{id}{-:id_product_attribute}-{rewrite}{-:ean13}.html
c’est-à-dire le nom de la catégorie par défaut du produit, puis l’ID, la déclinaison éventuelle, le nom et l’EAN13 éventuel. Dans la 9.0, la règle par défaut, dans le fichier classes/Dispatcher.php, est devenue :
{id}{-:id_product_attribute}-{rewrite}.html
Plus de catégorie, plus d’EAN13. Nous l’avons vérifié en comparant les sources de 1.7.1.2, 1.7.8.10 et 8.1 (règle avec la catégorie) à celles de 9.0.0 et 9.1.4 (règle sans). C’est la seule règle par défaut qui change entre la 8.1 et la 9.0, et la modification figure dans le changelog officiel de la 9.0 (n° 37467, « SEO delete category in URL product scheme during installation »).
La règle par défaut ne vaut toutefois que tant que personne n’en a enregistré une. Le Dispatcher charge d’abord les règles par défaut, puis les écrase avec celles enregistrées dans la table ps_configuration, avec des clés comme PS_ROUTE_product_rule et PS_ROUTE_category_rule : ce sont les valeurs de la page SEO et URL du back-office. Ces clés apparaissent quand quelqu’un enregistre cette page : dans une boutique qui ne l’a jamais enregistrée, elles manquent en général. Dans notre boutique, qui tourne sous la 9.1, il n’y en a pas une seule. Une boutique qui n’a jamais enregistré la page utilise la règle par défaut de la version en cours d’exécution : en changeant de version, les adresses changent, sans que personne n’ait rien touché.
Le projet PrestaShop l’a reconnu comme un problème sérieux de SEO. Le signalement n° 40063 (« Legacy Category/Product URLs Return 404 With New Default Routing ») a été clos le 26 novembre 2025 par une correction dans le module de mise à jour officiel, la PR n° 1535 (« Persist current product route when upgrading to 9.0.0 »), qui enregistre la règle utilisée lorsqu’il n’y en a pas. La correction protège celui qui met à jour avec ce module. Elle ne protège pas celui qui monte une installation neuve de PrestaShop 9 et y importe les données de l’ancienne boutique, ce qui est la migration décrite dans le signalement (de 1.6.1.4 à 9.0.1, avec installation propre et importation) : là, le schéma est le nouveau dès le premier jour.
L’essai, réalisé le 9 octobre 2026 sur PrestaShop 9.1 sans règle enregistrée (une copie de test de notre boutique, plus des requêtes en lecture seule sur la boutique elle-même), avec le produit 2 et sa catégorie par défaut compliance-ue :
/2-eu-withdrawal-button.html: 200./compliance-ue/2-eu-withdrawal-button.html: 404./2-un-autre-nom.html(bon ID, mauvais nom) : 301 vers/2-eu-withdrawal-button.html.
La correction
Il y a deux voies. La A rétablit l’ancien schéma et fonctionne même si le site avec les nouvelles adresses est déjà en ligne. La B garde le nouveau schéma, plus court, et redirige les anciennes adresses.
Voie A : revenir à l’ancien schéma.
- Dans le back-office : Paramètres de la boutique, Trafic et SEO, SEO et URL, section « Format des URLs », champ « Route vers les produits ». Dans le code, ce champ du formulaire est relié à la clé
PS_ROUTE_product_rule. - Saisir la règle que l’ancienne boutique utilisait. Si elle n’avait jamais été modifiée, c’est celle par défaut jusqu’à la 8.x :
{category:/}{id}{-:id_product_attribute}-{rewrite}{-:ean13}.html. Si elle était personnalisée, il faut remettre celle-là. - Enregistrer et tester un produit.
Nous avons testé l’effet en écrivant cette clé sur la copie de test (pas depuis le formulaire, mais c’est la clé que le formulaire écrit). L’ancienne adresse avec la catégorie répond de nouveau 200. L’adresse sans catégorie, que quelqu’un a pu lier entre-temps, devient un 301 vers celle avec la catégorie, et non un 404. Une catégorie erronée dans l’adresse est elle aussi corrigée par un 301.
Voie B : garder le nouveau schéma et rediriger les anciennes adresses. Avec Apache, dans le fichier .htaccess, avant le bloc compris entre les commentaires « ~~start~~ » et « ~~end~~ » de PrestaShop : ce qui se trouve hors de ce bloc, PrestaShop le conserve lorsqu’il régénère le fichier (le commentaire le dit lui-même). Remplacer la liste des langues par les préfixes de sa propre boutique :
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^(it|en|fr|es|de|pl|nl)/[^/]+/([0-9]+(-[0-9]+)?-[^/]+\.html)$ https://%{HTTP_HOST}/$1/$2 [R=301,L]
RewriteCond %{REQUEST_URI} !^/(it|en|fr|es|de|pl|nl)/
RewriteRule ^[^/]+/([0-9]+(-[0-9]+)?-[^/]+\.html)$ https://%{HTTP_HOST}/$1 [R=301,L]
</IfModule>
La première règle réécrit les adresses avec préfixe de langue et catégorie, la seconde celles de la langue par défaut, qui dans notre boutique n’a pas de préfixe. La condition entre les deux évite de toucher aux nouvelles adresses de la forme /en/12-nom.html. Testé sur la copie de test : /compliance-ue/2-eu-withdrawal-button.html et /en/compliance-ue/2-eu-withdrawal-button.html répondent 301 vers les adresses sans catégorie, tandis que /2-eu-withdrawal-button.html, /en/2-eu-withdrawal-button.html et une page CMS restent en 200. Si le nom du produit a changé entre-temps, le deuxième saut est fait par PrestaShop avec la redirection canonique.
Comment vérifier que ça tient, et comment l’éviter
- Avant de toucher à quoi que ce soit, lire ce qui est enregistré :
SELECT name, value FROM ps_configuration WHERE name LIKE 'PS_ROUTE%';(avec le préfixe des tables de sa propre installation). Aucune ligne signifie que la boutique utilise les règles par défaut de la version installée. - Prendre un échantillon d’anciennes adresses produit dans l’ancien sitemap ou dans la Search Console et contrôler leur code de réponse avec
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' ADRESSE. Un 200 ou un 301 vers la bonne fiche conviennent. Jamais un 404. - Tester aussi un produit avec déclinaisons, dont l’adresse contient l’ID de la déclinaison (
/12-3-nom.html), et refaire l’essai avec le préfixe de chaque langue. - Après chaque changement de schéma, régénérer le sitemap : c’est un fichier écrit par le module au moment de la génération, et il contient les adresses du schéma en vigueur ce jour-là.
- Avant de migrer, copier la règle produit utilisée depuis le champ « Route vers les produits » et la garder dans le projet. Si l’on monte une installation neuve et que l’on importe les données, définir et enregistrer cette règle avant d’ouvrir le site : à partir de là, c’est une valeur enregistrée et elle ne dépend plus de la valeur par défaut de la version.
- Si l’on met à jour avec le module de mise à jour, tester d’abord sur une copie et comparer le champ « Route vers les produits » avant et après, ce qui est aussi le contrôle indiqué dans la PR de la correction.
Pour gérer les redirections depuis le back-office plutôt que dans le .htaccess, on peut utiliser cll_seoredirect, notre module de redirections 301/302 et de suivi des 404 : il accepte des règles à expressions régulières avec substitution des groupes ($1), si bien que la règle de la voie B s’y écrit de la même façon. Nous l’avons testée sur la copie de test. Le problème et les corrections décrits ici valent de toute façon avec n’importe quel outil.
