PrestaShop : 404 généralisées après activation des URL simplifiées

Toutes vos fiches produits sortent en 404 pendant que l’accueil, lui, s’affiche sans broncher, et derrière, le scénario varie rarement : plus personne ne renvoie vers index.php au bout de la chaîne. Reste à trouver pourquoi. Et la suite dépend du terrain, Apache, nginx, ou plusieurs boutiques dans la même installation. Voici ma façon de faire, invariable, qui part de ce que Tools::generateHtaccess() inscrit pour de bon dans le fichier (plutôt que des légendes qui circulent).

Ce que PrestaShop écrit vraiment dans le .htaccess

C’est Tools::generateHtaccess(), logée dans classes/Tools.php, qui refabrique ce fichier ligne après ligne. Rien n’y est figé, je peux vous le dire. Tout ce qui précède # ~~start~~ et tout ce qui suit # ~~end~~ est préservé, pendant que l’entre-deux repart de zéro à chaque fois, si bien que la directive glissée entre ces bornes s’évapore à la première sauvegarde des paramètres SEO (et personne ne vous préviendra). Posez donc vos ajouts à l’extérieur, comme les redirections htaccess qui servent le plus, et ils tiendront bon à chaque régénération.

Tout le 404 se joue en cinq lignes, repérables au commentaire Send all other traffic to dispatcher en 9.0, ou Dispatcher en 1.7 comme en 8. Ce qui existe pour de bon, fichiers et répertoires, passe au travers, le reste file chez index.php. Seulement voilà, ce bloc n’existe qu’à la condition que PS_REWRITING_SETTINGS affiche 1 du côté de la boutique visée, et avec un 0 vous obtenez un fichier bien présent, d’apparence complète, où pas une seule URL simplifiée n’aboutit (je l’ai constaté plus d’une fois). Cette valeur-là d’abord.

# Extrait du .htaccess ecrit par Tools::generateHtaccess() sur PrestaShop 9.0.0,
# boutique unique, URL simplifiees actives, a la racine du domaine.
# Ne rien modifier entre ~~start~~ et ~~end~~ : PrestaShop reecrit ce bloc
# a chaque enregistrement des parametres SEO et URL.

# ~~start~~ Do not remove this comment, Prestashop will keep automatically the code outside this comment when .htaccess will be generated again
# .htaccess automaticaly generated by PrestaShop e-commerce open-source solution
# https://www.prestashop-project.org

# Prevent directory listings
Options -Indexes

<IfModule mod_rewrite.c>
<IfModule mod_env.c>
SetEnv HTTP_MOD_REWRITE On
</IfModule>

RewriteEngine on
# Protect .git
RewriteRule \.git - [F,L]


#Domain: boutique.tld
RewriteRule . - [E=REWRITEBASE:/]
RewriteRule ^api(?:/(.*))?$ %{ENV:REWRITEBASE}webservice/dispatcher.php?url=$1 [QSA,L]
RewriteRule ^upload/.+$ %{ENV:REWRITEBASE}index.php [QSA,L]

# Rewrites for product images (support up to < 10 million images)
RewriteRule ^(([\d])(?:\-[\w-]*)?)/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/p/$2/$1$3 [L]
RewriteRule ^(([\d])([\d])(?:\-[\w-]*)?)/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/p/$2/$3/$1$4 [L]
RewriteRule ^(([\d])([\d])([\d])(?:\-[\w-]*)?)/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/p/$2/$3/$4/$1$5 [L]
RewriteRule ^(([\d])([\d])([\d])([\d])(?:\-[\w-]*)?)/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/p/$2/$3/$4/$5/$1$6 [L]
RewriteRule ^(([\d])([\d])([\d])([\d])([\d])(?:\-[\w-]*)?)/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/p/$2/$3/$4/$5/$6/$1$7 [L]
RewriteRule ^(([\d])([\d])([\d])([\d])([\d])([\d])(?:\-[\w-]*)?)/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/p/$2/$3/$4/$5/$6/$7/$1$8 [L]
RewriteRule ^(([\d])([\d])([\d])([\d])([\d])([\d])([\d])(?:\-[\w-]*)?)/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/p/$2/$3/$4/$5/$6/$7/$8/$1$9 [L]

# Rewrites for category images
RewriteRule ^c/([\d]+)(|_thumb)(\-[\.*\w-]*)/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/c/$1$2$3$4 [L]
RewriteRule ^c/([a-zA-Z_-]+)(|_thumb)(-[\d]+)?/.+(\.(?:jpe?g|webp|png|avif))$ %{ENV:REWRITEBASE}img/c/$1$2$3$4 [L]

# AlphaImageLoader for IE and fancybox
RewriteRule ^images_ie/?([^/]+)\.(jpe?g|png|gif)$ %{ENV:REWRITEBASE}js/jquery/plugins/fancybox/images/$1.$2 [L]

# Send all other traffic to dispatcher
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ %{ENV:REWRITEBASE}index.php [NC,L]
</IfModule>

# Suivent dans le fichier reel : les directives AddType pour les polices,
# le bloc mod_headers, la protection de composer.lock, puis le cache
# mod_expires si PS_HTACCESS_CACHE_CONTROL vaut 1.

#If rewrite mod isn't enabled
ErrorDocument 404 /index.php?controller=404

# ~~end~~ Do not remove this comment, Prestashop will keep automatically the code outside this comment when .htaccess will be generated again

La 9.0 a ses deux lignes à elle, Options -Indexes et la protection de .git, tandis que du côté de la 1.7.8 et de la 8.2 le générateur glisse plutôt deux règles d’images héritées dès l’instant où PS_LEGACY_IMAGES tourne. Mettez côte à côte deux fichiers de versions différentes, ces écarts vous sauteront aux yeux. C’est normal (ne creusez pas de ce côté).

Régénérer le fichier quand le back-office s’y refuse

On commence par le geste évident : éteindre les URL simplifiées, puis les rallumer aussitôt, du côté de Paramètres de la boutique, dans Trafic et SEO. Une condition, cela dit. Le fichier doit être inscriptible. Faute de quoi PrestaShop réagit d’une drôle de manière, parce que dans SetUpUrlsDataConfiguration une génération ratée fait reposer PS_REWRITING_SETTINGS sur 0, et le cœur affiche alors le message « Before being able to use this tool, you need to: Create a blank .htaccess in your root directory ». La case se décoche d’elle-même, vous criez au bug d’interface, pendant que le vrai problème reste entier du côté des droits. Personnellement, j’y ai perdu un bon moment avant de piger.

Back-office hors d’atteinte ? Posez plutôt votre propre script à la racine, le résultat sera identique. Je ne connais pas plus propre. Et recopier un .htaccess attrapé ailleurs passe à côté du contenu de ps_shop_url, ce qui démolit votre multiboutique.

<?php
/**
 * regen-htaccess.php
 * A deposer a la racine de la boutique, a executer une fois, puis a supprimer.
 * Fonctionne sur PrestaShop 1.7, 8.x et 9.x : la signature de
 * Tools::generateHtaccess() est identique sur les trois branches.
 */
require_once __DIR__ . '/config/config.inc.php';

// Sans cette valeur a 1, generateHtaccess() n'ecrit ni les regles d'images
// ni le renvoi final vers index.php, et les fiches produits restent en 404.
Configuration::updateValue('PS_REWRITING_SETTINGS', 1);

// generateHtaccess() relit ensuite PS_HTACCESS_CACHE_CONTROL,
// PS_HTACCESS_DISABLE_MULTIVIEWS et PS_HTACCESS_DISABLE_MODSEC dans
// ps_configuration, puis parcourt ps_shop_url pour ecrire un bloc par domaine.
if (Tools::generateHtaccess()) {
    echo 'Fichier .htaccess regenere.' . PHP_EOL;
} else {
    // fopen() en ecriture a echoue : droits, proprietaire ou fichier absent.
    echo 'Echec : le fichier .htaccess de la racine n est pas inscriptible.' . PHP_EOL;
}

Ce fichier, je le supprime sitôt la vérification bouclée, et vous ferez pareil (tout de suite, oui). Envie d’aller plus loin dans l’audit ? Tout est sur le blog.

Apache sans mod_rewrite ou sans AllowOverride

Vous aurez beau soigner votre .htaccess, il ne servira à rien si la réécriture n’est pas chargée, ou si les surcharges sont bloquées sur le répertoire. Dans les deux cas, même symptôme. Je les sépare d’une seule commande. Sans mod_rewrite, Apache enjambe le bloc <IfModule mod_rewrite.c> sans lever la moindre erreur, donc votre accueil répond pendant que tout le reste finit par tomber sur ErrorDocument 404, directive posée un peu plus loin.

Sans AllowOverride, personne n’ouvre le fichier. Apache 2.4 arrive d’ailleurs réglé comme ça d’usine, sur la valeur None. Côté PrestaShop, c’est Tools::modRewriteActive() qui tranche : elle tente d’abord apache_get_modules() (absente sous PHP-FPM), avant de basculer sur HTTP_MOD_REWRITE, une variable que le fichier généré dépose au moment de s’écrire (oui, tout seul).

# 1. Modules reellement charges par Apache (Debian, Ubuntu).
apachectl -M 2>/dev/null | grep -E 'rewrite|auth_basic|env_module'

# 2. Activation de mod_rewrite puis rechargement du service.
sudo a2enmod rewrite
sudo systemctl reload apache2

# 3. Sans AllowOverride, le .htaccess est lu puis ignore en silence.
#    Il faut au minimum FileInfo pour les directives de reecriture.
grep -Rn 'AllowOverride' /etc/apache2/apache2.conf /etc/apache2/sites-enabled/

# 4. Test cote PrestaShop : le bloc genere pose SetEnv HTTP_MOD_REWRITE On.
#    Tools::modRewriteActive() lit cette variable quand apache_get_modules()
#    n'est pas disponible, ce qui est le cas sous PHP-FPM.
grep -n 'HTTP_MOD_REWRITE' .htaccess

# 5. Verification de bout en bout, sans navigateur ni cache.
curl -sI 'https://boutique.tld/2-mon-produit.html' | head -n 1
curl -sI 'https://boutique.tld/index.php?id_product=2&controller=product' | head -n 1

En mutualisé, vous ne décidez d’aucun de ces deux réglages, c’est l’affaire de votre hébergeur. Mettez-lui côte à côte l’adresse réécrite et l’adresse technique, ça lui pose le problème en deux lignes. Autant en profiter pour fixer une bonne fois le format de vos adresses, vu que retirer les identifiants des URL se décide depuis le même panneau.

Sous nginx, le .htaccess ne sert à rien

Jamais nginx n’ira lire une configuration posée dans un répertoire, donc le .htaccess que PrestaShop dépose à la racine ne sera jamais regardé, sans que le back-office s’en doute. Les URL simplifiées s’allument, la case demeure cochée, pendant que votre front crache du 404. Ouvrez le journal d’erreurs nginx, une seule ligne suffira à vous le confirmer, avec ces requêtes parties chercher sur le disque un répertoire physique qui n’existe nulle part.

Du coup, c’est le vhost qui décide de tout, et la 9 dispose d’un modèle de référence dans la documentation officielle. Trois morceaux y pèsent vraiment, et je les vérifie dans cet ordre : le try_files posé dans location /, qui joue le rôle de l’aiguillage vers le dispatcher, puis les réécritures d’images, enfin ^/api/, sans quoi votre webservice tombe lui aussi en 404.

# Extrait du vhost publie dans la documentation officielle PrestaShop 9
# (Basics > Installation > Advanced > Nginx). Nginx ne lit jamais le .htaccess :
# ces regles doivent vivre dans la configuration du serveur.
server {
    listen 80;
    listen 443 ssl http2;

    server_name boutique.tld www.boutique.tld;

    # Chemin du repertoire PrestaShop.
    root /var/www/boutique;
    index index.php;

    client_max_body_size 16M;

    # Equivalent du ErrorDocument 404 ecrit dans le .htaccess.
    error_page 404 /index.php?controller=404;

    # Images produits, de la profondeur 1 a la profondeur 8.
    rewrite ^/(\d)(-[\w-]+)?/.+\.(jpe?g|png|webp|avif)$ /img/p/$1/$1$2.$3 last;
    rewrite ^/(\d)(\d)(-[\w-]+)?/.+\.(jpe?g|png|webp|avif)$ /img/p/$1/$2/$1$2$3.$4 last;
    rewrite ^/(\d)(\d)(\d)(-[\w-]+)?/.+\.(jpe?g|png|webp|avif)$ /img/p/$1/$2/$3/$1$2$3$4.$5 last;
    rewrite ^/(\d)(\d)(\d)(\d)(-[\w-]+)?/.+\.(jpe?g|png|webp|avif)$ /img/p/$1/$2/$3/$4/$1$2$3$4$5.$6 last;
    rewrite ^/(\d)(\d)(\d)(\d)(\d)(-[\w-]+)?/.+\.(jpe?g|png|webp|avif)$ /img/p/$1/$2/$3/$4/$5/$1$2$3$4$5$6.$7 last;
    rewrite ^/(\d)(\d)(\d)(\d)(\d)(\d)(-[\w-]+)?/.+\.(jpe?g|png|webp|avif)$ /img/p/$1/$2/$3/$4/$5/$6/$1$2$3$4$5$6$7.$8 last;
    rewrite ^/(\d)(\d)(\d)(\d)(\d)(\d)(\d)(-[\w-]+)?/.+\.(jpe?g|png|webp|avif)$ /img/p/$1/$2/$3/$4/$5/$6/$7/$1$2$3$4$5$6$7$8.$9 last;

    # Images de categories.
    rewrite ^/c/([\w.-]+)/.+\.(jpe?g|png|webp|avif)$ /img/c/$1.$2 last;

    # Webservice : equivalent de la regle ^api du .htaccess.
    rewrite ^/api/?(.*)$ /webservice/dispatcher.php?url=$1 last;

    # Coeur de la reecriture : tout ce qui n existe pas sur le disque
    # part vers le dispatcher PrestaShop.
    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    # Remplacer admin-dev par le nom reel du repertoire d administration.
    location /admin-dev/ {
        try_files $uri $uri/ /admin-dev/index.php$is_args$args;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }

    location ~ ^/(app|bin|cache|classes|config|controllers|docs|localization|override|src|tests|tools|translations|var|vendor)/ {
        deny all;
    }

    location ~ [^/]\.php(/|$) {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        try_files $fastcgi_script_name =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $request_filename;
        fastcgi_index index.php;
        fastcgi_pass unix:/var/run/php/php-fpm.sock;
    }
}

Passez la syntaxe à nginx -t avant de recharger quoi que ce soit, je ne m’en dispense jamais, parce qu’une bourde dans le vhost éteint le site en entier, quand la même bourde dans un .htaccess ne vous vaut qu’une 500.

Multiboutique : un bloc de règles par domaine

Dès qu’il y a plusieurs boutiques, chaque entrée de ps_shop_url reçoit son propre jeu de règles. En tête de chaque bloc, un commentaire #Domain:, et quand le mode multiboutique tourne, une condition RewriteCond %{HTTP_HOST} devant chaque règle. Résultat, la boutique à qui il manque son URL principale n’apparaît dans aucun bloc, et toutes ses pages sortent en 404 (toutes, oui).

Le deuxième piège, ce sont les répertoires virtuels. Dès que virtual_uri contient quelque chose, deux règles de plus sortent (deux, pas une), chargées de renvoyer le chemin virtuel sur le chemin physique. Oubliez ce virtual_uri après un passage sur domaine dédié, et vous récoltez des redirections croisées d’une enseigne à l’autre (j’en ai vu deux se renvoyer la balle des jours durant). Tout ça se lit directement en base, alors passez la requête d’en dessous sur votre installation.

-- Adapter le prefixe ps_ a celui de la boutique.

-- 1. Ce que generateHtaccess() lit pour construire ses blocs #Domain.
--    physical_uri vaut / a la racine, virtual_uri est vide hors boutique virtuelle.
SELECT su.id_shop_url, su.id_shop, s.name AS boutique,
       su.domain, su.domain_ssl, su.physical_uri, su.virtual_uri,
       su.main, su.active
FROM ps_shop_url su
JOIN ps_shop s ON s.id_shop = su.id_shop
ORDER BY su.id_shop, su.main DESC;

-- 2. Boutiques sans URL principale : le domaine n apparait alors dans aucun
--    bloc du .htaccess, et toutes ses pages tombent en 404.
SELECT s.id_shop, s.name
FROM ps_shop s
LEFT JOIN ps_shop_url su ON su.id_shop = s.id_shop AND su.main = 1
WHERE s.deleted = 0 AND su.id_shop_url IS NULL;

-- 3. Etat de la reecriture, valeur globale puis surcharges par boutique.
--    PS_REWRITING_SETTINGS a 0 sur une seule boutique suffit a la casser.
SELECT name, id_shop_group, id_shop, value
FROM ps_configuration
WHERE name IN ('PS_REWRITING_SETTINGS', 'PS_CANONICAL_REDIRECT',
               'PS_HTACCESS_DISABLE_MULTIVIEWS', 'PS_HTACCESS_DISABLE_MODSEC',
               'PS_SHOP_DOMAIN', 'PS_SHOP_DOMAIN_SSL')
ORDER BY name, id_shop;

Après une correction faite directement en SQL, régénérez le fichier dans la foulée, puisque les blocs #Domain: se figent au moment précis où ils sont écrits.

Les contrôles que je fais avant de refermer

Je commence toujours par relever les statuts HTTP depuis le terminal. Imitez-moi, et laissez votre navigateur au placard, parce qu’un 404 gardé en mémoire par lui, ou par un CDN, vous collera aux basques bien après coup. Mon tour de piste tient en quatre appels : fiche produit, catégorie, page CMS, image produit, ce qui couvre d’un coup les quatre catégories de règles. Sur du multilingue, je double chaque appel, un avec le préfixe de langue, un sans, vu que supprimer le /fr/ des URL déplace le comportement attendu.

Enchaînez sur le menu Performances pour Smarty, puis sur Symfony en version 9, où front, admin et admin-api occupent chacun un répertoire distinct dans var/cache. Pour le référencement, je surveille quinze jours ce qu’affiche la Search Console côté indexation. Les URL déjà connues retrouvent toutes seules leur 200, sans que personne n’ait à intervenir, même si le pic de 404 vous restera gravé dans les rapports un bon moment.

Une limite, pendant que j’y pense. Ce que je vous décris là remet vos adresses debout, point final, et ça ne rattrapera pas une miette du référencement perdu pendant la panne, les positions reviendront à leur propre rythme sans que vous y puissiez grand-chose. Les adresses définitivement perdues méritent une page 404 capable de retenir le visiteur. Et si tout ça dépasse ce que vous pouvez faire seul, c’est chez moi que ça finit, en développement sur mesure.

Questions fréquentes

Pourquoi mes pages tombent en 404 juste après avoir activé les URL simplifiées ?

Parce qu’à la fin, plus rien ne pointe vers index.php. Pour que ce bloc atterrisse dans le .htaccess, il faut PS_REWRITING_SETTINGS à 1 chez vous, et pour qu’il soit lu ensuite, il faut un Apache qui charge mod_rewrite et laisse passer les surcharges sur ce répertoire.

Éteignez l’option puis rallumez-la, menu Paramètres de la boutique, onglet Trafic et SEO, et la régénération part toute seule. Et quand la case se décoche d’elle-même une fois enregistré, c’est que le fichier n’a pas pu s’écrire. PrestaShop rabat alors le réglage sur 0 en vous priant de poser à la racine un .htaccess vide.

Comment corriger les 404 sur un serveur nginx ?

Tout passe par le vhost. nginx ne regarde jamais un fichier .htaccess. La pièce maîtresse, c’est try_files $uri $uri/ /index.php$is_args$args; posée dans location / : fichier présent, elle le sert, fichier absent, elle bascule sur le dispatcher de PrestaShop.

Complétez avec les réécritures d’images, sans quoi vos visuels produits resteront introuvables, plus la règle rewrite ^/api/?(.*)$ /webservice/dispatcher.php?url=$1 last; dès l’instant où le webservice vous sert. Du côté de la documentation développeur de la 9, vous trouverez un vhost complet, bonne base de départ (partez de celui-là, pas d’un bout de configuration pêché sur un forum).

Peut-on régénérer le .htaccess sans accéder au back-office ?

Oui, bien sûr. La méthode statique Tools::generateHtaccess(), publique et rigoureusement la même en 1.7, en 8 et en 9, je l’appelle depuis n’importe où. Déposez chez vous un script qui charge config/config.inc.php avant de l’appeler, et le fichier se refabrique sur les valeurs réelles que portent ps_shop_url et ps_configuration.

Deux précautions. Assurez-vous que PS_REWRITING_SETTINGS soit bien à 1 au moment de l’appel, sans ça le fichier se régénère amputé de ses règles utiles. Et jetez le script derrière. Oublié sur place, il vous laisse à la racine un PHP exécutable par n’importe qui.

Pourquoi une seule boutique de mon installation multiboutique renvoie des 404 ?

Parce qu’à chaque ligne de ps_shop_url répond son propre paquet de règles dans le fichier, reconnaissable à son commentaire #Domain:, qu’une condition sur HTTP_HOST vient filtrer. Si une boutique n’a pas d’URL marquée main, ou si son domaine en base ne colle pas à ce qu’on sert pour de vrai, elle repart les mains vides.

L’autre grand classique, c’est le virtual_uri qui traîne. Rempli, il pousse le cœur à écrire deux règles de plus, lesquelles envoient le chemin virtuel sur le chemin physique. Une boutique installée depuis sur son propre domaine hérite donc de règles qui l’expédient chez la voisine. Remettez ces deux valeurs d’aplomb en base, et régénérez derrière.

mod_rewrite est-il obligatoire pour les URL simplifiées ?

Oui, sous Apache. PrestaShop enferme la totalité de ses règles dans un bloc <IfModule mod_rewrite.c>. Module absent, Apache enjambe ce bloc sans lever la moindre erreur, et c’est la directive ErrorDocument 404 posée en dehors qui prend le relais, d’où une 404 bien propre au lieu d’une 500.

Prévoyez aussi un AllowOverride calé au moins sur FileInfo là où vit le site, sans quoi plus personne n’ouvre ce fichier, et l’interprétation n’a jamais lieu. Pour savoir si le module est là, le cœur passe par Tools::modRewriteActive(), qui bascule sur HTTP_MOD_REWRITE, variable que pose le fichier généré, chaque fois que apache_get_modules() manque à l’appel. En général, votre hébergeur lèvera le doute en deux minutes, sinon la page contact est là pour ça.

Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Un projet ? Parlons-en.

Devis gratuit et sans engagement. Je réponds dans les douze heures.