- NEWSLETTER-RETOURS-2026-08-27.md : retour du commanditaire sur la newsletter de juillet (annonces à venir absentes, dates de publication affichées à la place des dates d'événement), vérification dans le code et implémentation. - NOTE-SLUG-NUMERIQUE-404-2026-08-27.md : annonces publiées répondant 404 (slug numérique incompatible avec /%category%/%postname%/), diagnostic et implémentation. - CLAUDE.md : nouveau module inc/post-slug-guard.php, piège du titre vide dans post-title-required.php, et bascule de la newsletter sur la date d'événement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w7RzAHv4r1Yahnm2NwDDG
238 lines
11 KiB
Markdown
238 lines
11 KiB
Markdown
# Annonces en 404 : slugs numériques et permaliens `/%category%/%postname%/`
|
|
|
|
> Note rédigée le 2026-08-27, **avant toute modification du code**.
|
|
> Origine : signalement du commanditaire — une annonce publiée, visible dans les
|
|
> listes, renvoie un 404 au clic.
|
|
> Cas de référence en prod : « Charte d'usage des moyens informatiques du CNRS »,
|
|
> permalien `https://thalim.cnrs.fr/le-laboratoire/vie-du-labo-intranet/44210/`.
|
|
> Deuxième cas confirmé en prod : l'annonce dont le titre est « 1917 ».
|
|
|
|
## 1. Symptôme
|
|
|
|
L'annonce est bien publiée. Elle apparaît normalement dans les index (listes de
|
|
catégorie, agenda, recherche) parce que ces écrans manipulent l'objet post. Mais
|
|
son **permalien n'est pas routable** : au clic, WordPress rend le 404 du thème
|
|
(`templates/404.twig`). L'URL courte `?p={ID}` ne sauve pas la situation — elle
|
|
fait un 301 vers ce même permalien cassé.
|
|
|
|
## 2. Diagnostic
|
|
|
|
La structure de permaliens du site est `/%category%/%postname%/`. La règle de
|
|
réécriture qui sert les articles est :
|
|
|
|
```
|
|
(.+?)/([^/]+)(?:/([0-9]+))?/?$ => index.php?category_name=$1&name=$2&page=$3
|
|
```
|
|
|
|
Le dernier groupe est un **numéro de page** (pagination interne d'un article via
|
|
`<!--nextpage-->`). Conséquence : quand le slug d'un article est purement
|
|
numérique, il est avalé comme numéro de page, et le segment précédent — qui est
|
|
en réalité la sous-catégorie — est pris pour le slug de l'article.
|
|
|
|
Reproduit en local sur l'article ID 30497 (titre « 1917 », slug `1917`) :
|
|
|
|
```
|
|
URL demandée : /manifestations-scientifiques/communications/1917/ → HTTP 404
|
|
règle matchée : (.+?)/([^/]+)(?:/([0-9]+))?/?$
|
|
query_vars : {"category_name":"manifestations-scientifiques",
|
|
"name":"communications", ← pris pour le slug de l'article
|
|
"page":"1917"} ← pris pour un numéro de page
|
|
```
|
|
|
|
WordPress cherche donc un article nommé `communications` dans la catégorie
|
|
`manifestations-scientifiques`, page 1917. Il n'existe pas → 404. Le cas
|
|
`/le-laboratoire/vie-du-labo-intranet/44210/` est exactement le même.
|
|
|
|
## 3. Deux causes distinctes, un seul symptôme
|
|
|
|
### Cause A — titre vide au moment de la première publication (cas « Charte d'usage »)
|
|
|
|
Cœur de WordPress, `wp-includes/post.php:4954` :
|
|
|
|
```php
|
|
if ( empty( $data['post_name'] ) && ! in_array( $data['post_status'], array( 'draft', 'pending', 'auto-draft' ), true ) ) {
|
|
$data['post_name'] = wp_unique_post_slug( sanitize_title( $data['post_title'], $post_id ), … );
|
|
}
|
|
```
|
|
|
|
`sanitize_title( $titre, $fallback )` renvoie le `$fallback` — donc **l'ID du
|
|
post** — quand le titre est vide. Le slug est figé à cet instant, et n'est jamais
|
|
régénéré ensuite (la condition exige `post_name` vide) : corriger le titre et
|
|
republier ne change rien.
|
|
|
|
Le chemin qui produit ça est notre propre garde-fou. `inc/post-title-required.php`
|
|
le dit dans son en-tête : « the post saves (with empty title), the status is
|
|
reverted to draft if needed ». Séquence :
|
|
|
|
1. clic sur **Publier** avec le champ Titre vide ;
|
|
2. `wp_insert_post` écrit la ligne en `publish` → **slug figé à l'ID** ;
|
|
3. `save_post` (priorité 5) détecte le titre vide, pose le transient de restauration ;
|
|
4. `redirect_post_location` (`inc/pods-save-error-handler.php:56-73`) remet le
|
|
statut à `draft` et renvoie vers l'écran d'édition avec le message
|
|
« Le champ Titre est obligatoire » ;
|
|
5. l'utilisateur saisit le titre et republie — le slug numérique reste.
|
|
|
|
Le même scénario existe via la validation Pods (`inc/pods-save-error-handler.php`)
|
|
si un champ obligatoire manque **et** que le titre est vide à ce premier
|
|
enregistrement.
|
|
|
|
À noter : ce n'est **pas** un doublon de titre. Un doublon produirait
|
|
`charte-dusage-…-2`, jamais l'ID.
|
|
|
|
### Cause B — titre légitimement numérique (cas « 1917 »)
|
|
|
|
Aucun bug ici : le titre « 1917 » donne le slug `1917`, qui est correct du point
|
|
de vue de WordPress mais irroutable avec cette structure de permaliens. Cette
|
|
cause est indépendante de la cause A et survivra à sa correction. Elle touche
|
|
aussi tout contenu entré par import SQL direct ou par l'importateur HAL.
|
|
|
|
## 4. Correctif proposé — deux volets
|
|
|
|
Les volets A et B empêchent le problème de réapparaître. Les deux articles déjà
|
|
touchés se corrigent à la main (§5) : leur nombre étant connu et limité à deux,
|
|
un rattrapage automatique au routage n'est pas justifié.
|
|
|
|
### Volet A — interdire la naissance de slugs purement numériques
|
|
|
|
Nouveau module `inc/post-slug-guard.php`, chargé depuis `functions.php` juste
|
|
après `post-title-required.php`.
|
|
|
|
```php
|
|
/**
|
|
* Un slug purement numérique est irroutable avec la structure
|
|
* /%category%/%postname%/ : le dernier segment est avalé comme numéro de page
|
|
* par la règle (.+?)/([^/]+)(?:/([0-9]+))?/?$. On suffixe donc ces slugs,
|
|
* en suivant la convention du cœur (-2, -3, …).
|
|
*
|
|
* Portée volontairement limitée à post/page : nav_menu_item et attachment
|
|
* ont légitimement des slugs numériques (38 et 4 occurrences en base de dev)
|
|
* et ne passent pas par cette règle de réécriture.
|
|
*/
|
|
add_filter( 'wp_unique_post_slug', function ( $slug, $post_id, $post_status, $post_type ) {
|
|
if ( ! in_array( $post_type, [ 'post', 'page' ], true ) || ! ctype_digit( (string) $slug ) ) {
|
|
return $slug;
|
|
}
|
|
global $wpdb;
|
|
$suffix = 2;
|
|
do {
|
|
$alt = $slug . '-' . $suffix;
|
|
$exists = (int) $wpdb->get_var( $wpdb->prepare(
|
|
"SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1",
|
|
$alt, $post_type, $post_id
|
|
) );
|
|
$suffix++;
|
|
} while ( $exists );
|
|
return $alt;
|
|
}, 10, 4 );
|
|
```
|
|
|
|
Effet : un futur article intitulé « 1917 » obtient `1917-2`, routable.
|
|
Limite connue : un import SQL direct contourne `wp_insert_post`, donc ce filtre —
|
|
après toute reprise de données massive (migration, importateur HAL), rejouer la
|
|
requête d'inventaire du §5.
|
|
|
|
### Volet B — ne pas figer un slug issu d'un titre vide
|
|
|
|
Dans `inc/pods-save-error-handler.php`, bloc d'annulation du statut (l. 63-72),
|
|
vider `post_name` **uniquement s'il est numérique** — c'est-à-dire s'il a été
|
|
généré faute de titre. Un slug saisi à la main sur un brouillon est ainsi préservé.
|
|
|
|
```php
|
|
$update = [ 'post_status' => $original ?: 'draft' ];
|
|
$formats = [ '%s' ];
|
|
|
|
// Slug figé à l'ID faute de titre : le vider pour qu'il soit régénéré
|
|
// depuis le vrai titre à la prochaine publication.
|
|
if ( ctype_digit( (string) $post->post_name ) ) {
|
|
$update['post_name'] = '';
|
|
$formats[] = '%s';
|
|
}
|
|
|
|
$wpdb->update( $wpdb->posts, $update, [ 'ID' => $post_id ], $formats, [ '%d' ] );
|
|
```
|
|
|
|
Effet : le scénario « Charte d'usage » ne peut plus se produire — au moment où
|
|
l'utilisateur corrige son titre et republie, WordPress régénère un slug propre.
|
|
|
|
## 5. Remise en état des deux articles existants
|
|
|
|
Les volets A et B n'agissent qu'à la création du slug : ils ne réparent pas les
|
|
deux articles déjà en 404. Il faut donc éditer chacun d'eux et corriger le
|
|
permalien à la main :
|
|
|
|
| Article | Slug actuel | Slug à poser |
|
|
|---|---|---|
|
|
| Charte d'usage des moyens informatiques du CNRS | `44210` | `charte-dusage-des-moyens-informatiques-du-cnrs` |
|
|
| 1917 | `1917` | `1917-2` (ou tout slug non purement numérique) |
|
|
|
|
Les anciennes URL numériques étant déjà en 404, ce changement ne casse aucun lien
|
|
existant. À faire **après** le déploiement du volet A, sans quoi rien ne garantit
|
|
qu'un futur enregistrement ne régénère pas un slug numérique.
|
|
|
|
Inventaire des contenus concernés, **exécuté en production le 2026-08-27** :
|
|
|
|
```sql
|
|
SELECT ID, post_type, post_status, post_name, post_title
|
|
FROM wp_posts
|
|
WHERE post_type IN ('post','page')
|
|
AND post_name REGEXP '^[0-9]+$'
|
|
ORDER BY post_type, ID;
|
|
```
|
|
|
|
Résultat : **2 articles seulement**, les deux déjà connus — « Charte d'usage des
|
|
moyens informatiques du CNRS » (slug `44210`) et « 1917 » (slug `1917`). Le
|
|
périmètre de rattrapage est donc entièrement circonscrit. Les `nav_menu_item` et
|
|
`attachment` à slug numérique sont normaux et hors périmètre (le volet A ne les
|
|
touche pas).
|
|
|
|
## 6. Éléments pour la réponse au commanditaire
|
|
|
|
- L'annonce est bien publiée, rien n'est perdu ; c'est son **adresse** qui est
|
|
invalide, d'où l'incohérence « visible dans la liste, 404 au clic ».
|
|
- Cause : l'annonce a été publiée une première fois alors que le champ Titre
|
|
était encore vide. WordPress a alors fabriqué son adresse à partir de son
|
|
numéro interne, et ne la régénère plus ensuite, même après correction du titre.
|
|
- Ce n'est pas lié à un doublon de titre.
|
|
- Deux annonces sont concernées sur tout le site, la leur et une autre : elles
|
|
sont réparées à la main en corrigeant leur permalien, et redeviennent
|
|
accessibles immédiatement.
|
|
- Un correctif côté code est prévu pour que le cas ne se reproduise plus, sans
|
|
intervention de leur part.
|
|
- Consigne utile en attendant ce correctif : **saisir le titre avant le premier
|
|
enregistrement**.
|
|
|
|
## 7. Contexte technique
|
|
|
|
- WordPress 7.0, structure de permaliens `/%category%/%postname%/`.
|
|
- Règle de réécriture concernée : rang 111 sur 123
|
|
(`(.+?)/([^/]+)(?:/([0-9]+))?/?$`).
|
|
- Vérifications faites sur l'environnement de dev local (dump au 2026-07-03),
|
|
conteneurs `thalim-dev-db-1` et `wordpress`.
|
|
- Constat annexe repéré au passage : il existe des articles publiés partageant le
|
|
même `post_name`, ce qui est impossible via l'API WordPress — trace d'imports
|
|
SQL directs (migration / importateur HAL). Sans rapport avec ce 404, mais un
|
|
lien peut y renvoyer vers le mauvais article. À traiter séparément.
|
|
|
|
## 8. Implémentation — 2026-08-27
|
|
|
|
Faite dans `thalim-theme`, conforme aux volets A et B ci-dessus.
|
|
|
|
- **Volet A** — nouveau module `inc/post-slug-guard.php`, chargé depuis
|
|
`functions.php` après `post-title-required.php`. Filtre `wp_unique_post_slug`
|
|
limité à `post`/`page`, plus le helper pur `thalim_slug_needs_guard()`.
|
|
Vérifié : `1917` → `1917-2`, `44210` → `44210-2`, `9999` → `9999-2` ;
|
|
`charte-dusage-…` inchangé ; `99999` en `nav_menu_item` et en `attachment`
|
|
inchangé.
|
|
- **Volet B** — dans `inc/pods-save-error-handler.php`, le bloc d'annulation de
|
|
statut vide aussi `post_name`. **Écart assumé avec la note** : la condition
|
|
n'est pas « slug numérique » mais « **titre vide** ». Le volet A transforme en
|
|
effet `44210` en `44210-2`, qui n'est plus numérique — la condition d'origine
|
|
n'aurait donc jamais déclenché une fois les deux volets en place. Tester le
|
|
titre vide vise directement la cause et reste juste dans tous les cas.
|
|
Vérifié de bout en bout : publication sans titre → slug `44021-2` ; annulation
|
|
du statut → `draft` + slug vidé ; saisie du titre puis publication → slug
|
|
`charte-d-usage-des-moyens-informatiques-du-cnrs`, permalien routable.
|
|
Le post de test a été supprimé définitivement.
|
|
- **Non fait** : la remise en état des deux articles existants (§5), qui reste à
|
|
faire à la main en prod après déploiement.
|