Notes des deux correctifs du 2026-08-27 + CLAUDE.md
- 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
This commit is contained in:
@@ -37,7 +37,7 @@ Timber/Twig. Chaque `*.php` charge un Twig de `templates/`. `base.twig` est le l
|
||||
### Plugins maison
|
||||
|
||||
- **`thalim-hal-importer/`** — import publications HAL (structure 254015). Admin : Outils → HAL Import. Voir le README du plugin pour le mapping doc types → catégories (10 types : ART, COUV, OUV, COMM, ISSUE, PROCEEDINGS, THESE, HDR, SON, VIDEO). Les catégories sont résolues **par slug** et les IDs Pods **par nom** ; toute écriture de relation Pods passe par `includes/class-pods-storage.php` (dépendance dure à Pods 3.x documentée dans le fichier — ne pas écrire `wp_podsrel` ailleurs).
|
||||
- **`thalim-newsletter/`** — composition et export HTML des digests mensuels. Admin : Outils → Newsletter. Les constantes `THALIM_NL_CAT_*` sont résolues par slug au chargement (transient 1 j, fallback IDs historiques) ; le pod/champ `categorie` est résolu par nom dans `class-admin-page.php`.
|
||||
- **`thalim-newsletter/`** — composition et export HTML des digests mensuels. Admin : Outils → Newsletter. Les constantes `THALIM_NL_CAT_*` sont résolues par slug au chargement (transient 1 j, fallback IDs historiques) ; le pod/champ `categorie` est résolu par nom dans `class-admin-page.php`. Éligibilité, tri et affichage reposent sur la **date d'événement** (`date_de_debut` > `datetime` > `post_date`), jamais sur la date de publication ; les contenus au-delà du mois sont proposés dans un bloc « À venir » (horizon 12 mois), décochés par défaut. Voir le README du plugin.
|
||||
|
||||
### Plugins WP requis
|
||||
|
||||
@@ -91,7 +91,8 @@ Les annonces ont une `date_de_debut` / `date_de_fin` (champ date Pods) ou un `da
|
||||
| `post-card-helpers.php` | `thalim_get_card_data($post_id)` / `thalim_get_cards_data($posts)` : données pour `partials/post-card.twig`. Inclut la résolution catégorie parente pour le code couleur (`parent_slug`), la première image (medium), la `card_event_date`, et la redirection `#seance-{ID}` pour les séances de séminaire |
|
||||
| `pods-conditional-required.php` | Patch : Pods n'évalue pas sa propre logique conditionnelle côté serveur lors de la validation des champs *required*. On la rejoue dans `pods_api_pre_save_pod_item_post` et on désactive `required` sur les champs masqués |
|
||||
| `pods-save-error-handler.php` | Quand Pods déclenche un `wp_die()` à la sauvegarde admin, intercepter (`pods_error_die`), stocker tous les `pods_meta_*` dans un transient, rediriger vers `post.php?action=edit`, annuler le statut si le post passait à `publish`, et restaurer les champs côté React via `get_post_metadata` + JS dans `admin_footer`. **Mécanisme partagé** avec : |
|
||||
| `post-title-required.php` | Force un titre non-vide à la sauvegarde, réutilise le même mécanisme transient/restore |
|
||||
| `post-title-required.php` | Force un titre non-vide à la sauvegarde, réutilise le même mécanisme transient/restore. **Attention** : le post est bien écrit avec son titre vide avant d'être annulé — c'est là que le cœur fige un slug numérique, d'où le vidage de `post_name` dans `pods-save-error-handler.php` |
|
||||
| `post-slug-guard.php` | Suffixe les slugs purement numériques des `post`/`page` (`1917` → `1917-2`). Avec la structure `/%category%/%postname%/`, un dernier segment numérique est lu comme un numéro de page par la règle `(.+?)/([^/]+)(?:/([0-9]+))?/?$` : l'article répond 404 tout en restant listé partout. Ne touche pas aux `nav_menu_item` ni aux `attachment`, dont les slugs numériques sont normaux |
|
||||
| `admin-users-filter.php` | Dropdown « Statut » sur `users.php`, filtre via meta `role_1`/`role_2`/`role_3` |
|
||||
|
||||
### Avatars (chaîne de fallback)
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
# Newsletter — retour commanditaire du 27/08/2026
|
||||
|
||||
> Note de travail rédigée le 2026-08-27, **avant toute modification du code**.
|
||||
> Contexte : la newsletter de juillet 2026 est la première réellement envoyée.
|
||||
> Le commanditaire remonte deux bugs à corriger.
|
||||
> Plugin concerné : `wp-data/wp-content/plugins/thalim-newsletter/`
|
||||
> (état au commit `021f322`).
|
||||
|
||||
## 1. Le mail, en clair
|
||||
|
||||
Deux demandes :
|
||||
|
||||
1. **Les annonces futures n'apparaissent pas dans la liste à cocher du backoffice.**
|
||||
Pour la NL de juillet, ils ont dû créer les newsletters d'août, septembre et
|
||||
octobre uniquement pour aller y récupérer des portions de HTML et les recoller
|
||||
à la main dans celle de juillet (ex. : la séance du séminaire Thalim du
|
||||
9 octobre). Demande : **lister toutes les annonces à venir, sans limite
|
||||
d'horizon**. Les filtres sur le passé, eux, leur conviennent.
|
||||
|
||||
2. **Les dates affichées sont les dates de publication, pas les dates de
|
||||
l'événement annoncé.** Deux exemples cités :
|
||||
- l'appel à contribution « La reconnaissance des modernes… » affiche
|
||||
« 7 juillet » (sa date de publication) au lieu de « Jusqu'au 1 septembre 2026 » ;
|
||||
- les événements culturels affichent la date de publication et non la date
|
||||
prévue (ex. la visite insolite du 8 octobre).
|
||||
|
||||
Les deux points sont **confirmés dans le code** (vérifications ci-dessous, faites
|
||||
sur le dump de dev daté du 2026-07-03).
|
||||
|
||||
## 2. Demande 1 — horizon futur
|
||||
|
||||
### Ce que fait le code
|
||||
|
||||
Chaque catégorie a une « fenêtre » `[début, fin]`
|
||||
(`includes/class-post-query.php:21-38`). Un item n'est retenu pour le mois M que si :
|
||||
|
||||
```
|
||||
début_fenêtre <= fin_du_mois ET fin_fenêtre >= début_du_mois
|
||||
```
|
||||
|
||||
(`includes/class-post-query.php:303-304`)
|
||||
|
||||
C'est la condition **`début_fenêtre <= fin_du_mois`** qui coupe tout le futur.
|
||||
|
||||
| Catégories | Fenêtre | Horizon futur max (NL de juillet) |
|
||||
|---|---|---|
|
||||
| Colloques (10), Communications (13) | `date_de_debut − 35 j → date_de_fin` | ~**35 jours** |
|
||||
| Séances de séminaire (sous 11) | `[1er du mois, fin du mois + 5 j]` | **5 jours** (`SEANCE_WINDOW_MARGIN_DAYS`, l. 38 et 203) |
|
||||
| Appels (8), Soutenances (14) | `datetime → date_de_fin` | dépend de `datetime`, **vide en pratique** → retombe sur `post_date` |
|
||||
| Ouvrages (15), Articles (16) | `datetime → +3 mois` | idem, piloté par la publication |
|
||||
| **Toutes les autres** (dont Événements culturels 18, Médias 19, Captations 23…) | `datetime → +35 j` | idem, piloté par la publication |
|
||||
|
||||
La séance du 9 octobre citée dans le mail correspond exactement au cas
|
||||
`SEANCE_WINDOW_MARGIN_DAYS = 5` : elle ne pouvait pas sortir dans la NL de juillet.
|
||||
|
||||
### Nuance importante
|
||||
|
||||
Pour les catégories où `datetime` n'est jamais renseigné (18, 19, 14…), la fenêtre
|
||||
est en réalité calculée sur la **date de publication**. Conséquence : un événement
|
||||
d'octobre publié en juin apparaît bien dans la NL de juin/juillet (mais avec la
|
||||
mauvaise date), alors que le même événement publié en septembre n'apparaîtra jamais
|
||||
dans celle de juillet. L'éligibilité est donc incohérente des deux côtés.
|
||||
|
||||
### Volume
|
||||
|
||||
Au 3 juillet 2026 (date du dump), seules **5 annonces** avaient une date
|
||||
d'événement dans le futur. Ouvrir tout le futur ne fait donc pas exploser la liste,
|
||||
mais le nombre grimpera en pleine saison (rentrée).
|
||||
|
||||
## 3. Demande 2 — dates affichées
|
||||
|
||||
### Ce que fait le code
|
||||
|
||||
`format_date_for_category()` (`includes/class-html-exporter.php:542`) ne lit
|
||||
`date_de_debut` / `date_de_fin` **que** pour les catégories en fenêtre
|
||||
`debut_minus35_to_fin` (colloques et communications). Pour toutes les autres, il lit
|
||||
`datetime`, sinon `post_date`.
|
||||
|
||||
### Ce que dit la base (posts publiés depuis 2025)
|
||||
|
||||
| Catégorie | ont `datetime` | ont `date_de_debut` | ont `date_de_fin` |
|
||||
|---|---|---|---|
|
||||
| Événements culturels (18) | **0 / 73** | 69 / 73 | 67 / 73 |
|
||||
| Soutenances (14) | 0 / 17 | 17 / 17 | 17 / 17 |
|
||||
| Médias (19) | 0 / 48 | 21 / 48 | 21 / 48 |
|
||||
| Appels à contribution (8) | 0 / 18 | 1 / 18 | 1 / 18 |
|
||||
|
||||
Donc pour les événements culturels, c'est **systématiquement** la date de
|
||||
publication qui sort. Idem pour les soutenances. Sur les appels récents, `datetime`
|
||||
vaut souvent littéralement `0000-00-00`, et la date limite est bien stockée dans
|
||||
`date_de_fin` — champ utilisé pour l'éligibilité mais **jamais affiché**, et sans
|
||||
préfixe « Jusqu'au ».
|
||||
|
||||
### Trois conséquences supplémentaires, non mentionnées dans le mail
|
||||
|
||||
- **Le tri interne à chaque section** utilise la même date fausse
|
||||
(`ORDER BY`, `includes/class-post-query.php:305`) : les événements culturels sont
|
||||
classés par date de publication.
|
||||
- **Le backoffice affiche la bonne date, l'email la mauvaise** : la pastille de la
|
||||
liste à cocher (`includes/class-admin-page.php:356`) lit bien `date_de_debut` en
|
||||
premier. D'où la surprise à la lecture du mail réellement envoyé.
|
||||
- **Le thème a déjà la logique attendue** : `thalim_get_agenda_card_data()`
|
||||
(`wp-data/wp-content/themes/thalim/inc/ajax.php:180-215`) produit exactement
|
||||
« Le X de H1 à H2 » / « Du X au Y » / « Jusqu'au X », sur la convention
|
||||
`date_de_debut > datetime > post_date`. Le plugin ne la réutilise pas : il a
|
||||
réimplémenté une version partielle.
|
||||
|
||||
## 4. Modifications à faire
|
||||
|
||||
1. **Requête — supprimer le plafond futur.**
|
||||
Garder le critère de fin (ne pas ressortir ce qui est terminé avant le mois),
|
||||
mais accepter n'importe quelle date de début à venir : lever la condition
|
||||
`début_fenêtre <= fin_du_mois`, et pour les séances remplacer
|
||||
`[début du mois, fin du mois + 5 j]` par `[début du mois, +∞[`.
|
||||
|
||||
2. **Dates — aligner sur la convention du thème.**
|
||||
`date_de_debut > datetime > post_date` pour **toutes** les catégories, et
|
||||
reprendre les libellés de `thalim_get_agenda_card_data()` : « Du … au … »,
|
||||
« Jusqu'au … » quand seule `date_de_fin` existe (cas des appels), heures quand
|
||||
elles sont renseignées. Le plus propre est de **factoriser** cette logique
|
||||
plutôt que de la dupliquer une troisième fois (thème + exporter + hint admin).
|
||||
|
||||
3. **Tri** — trier par date d'événement réelle, pas par `datetime` / `post_date`.
|
||||
|
||||
## 5. Points à arbitrer avec le commanditaire
|
||||
|
||||
- **Cochage par défaut.** Aujourd'hui, sur un mois neuf, *tout est coché*
|
||||
(`render_sections_html()`, `$check_all`). Si on injecte tout le futur, la NL de
|
||||
juillet partirait pré-cochée avec des événements de mars suivant.
|
||||
Proposition : passé et mois courant cochés par défaut, futur au-delà du mois
|
||||
listé mais **décoché**, dans un sous-bloc « À venir » visuellement distinct.
|
||||
C'est ce qui change le plus leur geste quotidien → à valider avant de coder.
|
||||
|
||||
- **« Jusqu'au » sur les appels** suppose que `date_de_fin` soit saisie côté
|
||||
rédaction. Sur les appels publiés depuis janvier 2026, un seul sur sept l'a.
|
||||
Sans ce champ, aucune date correcte n'est récupérable et on retombera sur la date
|
||||
de publication → à leur signaler comme consigne de saisie.
|
||||
|
||||
## 6. Repères techniques
|
||||
|
||||
- Dump de dev utilisé pour les vérifications : contenu jusqu'au **2026-07-03**
|
||||
(le post « La reconnaissance des modernes… » n'y est donc pas ; l'analyse porte
|
||||
sur la logique, pas sur ce post précis).
|
||||
- Newsletters enregistrées en base de dev : avril, mai, juin 2026.
|
||||
- Conteneur de base : `thalim-dev-db-1` (`docker compose up -d db`).
|
||||
- Catégories vérifiées en base le 2026-08-27 : les IDs historiques du README
|
||||
(8, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20) sont toujours exacts.
|
||||
À noter : `Séance de séminaire` (12) a pour parent `Séminaires` (11), pas 0.
|
||||
|
||||
## 7. Au passage (hors périmètre du retour)
|
||||
|
||||
- `PARENT_COLORS` (`includes/class-html-exporter.php:35-41`) est encore indexé sur
|
||||
des term_ids en dur (1, 3, 4, 5, 6), contrairement à la convention du projet
|
||||
(résolution par slug). À traiter si on repasse dans ce fichier.
|
||||
|
||||
## 8. Implémentation — 2026-08-27
|
||||
|
||||
Faite dans `thalim-plugin-newsletter`.
|
||||
|
||||
**Les trois modifications demandées :**
|
||||
|
||||
1. **Horizon futur ouvert** — le plafond « début de fenêtre ≤ fin du mois » est
|
||||
remplacé par une fenêtre « à venir » de **12 mois**
|
||||
(`FUTURE_HORIZON_MONTHS`), qui démarre à la fin du mois de la newsletter ou
|
||||
à aujourd'hui si ce mois est déjà passé. Les séances suivent la même règle
|
||||
(la marge de 5 jours, `SEANCE_WINDOW_MARGIN_DAYS`, est supprimée).
|
||||
2. **Dates** — `Thalim_NL_Post_Query::event_date_label()` centralise les
|
||||
libellés du thème (« Le X de H1 à H2 », « Du X au Y », « Jusqu'au X »,
|
||||
« X à H ») sur la convention `date_de_debut > datetime > post_date`. Utilisée
|
||||
par l'export **et** par la pastille de la liste à cocher, qui affichent donc
|
||||
désormais la même chose. Les ouvrages restent en année seule.
|
||||
3. **Tri** — `sql_event_date_order()` remplace le tri par date de publication
|
||||
dans toutes les catégories.
|
||||
|
||||
**Écarts et ajouts par rapport à la note, décidés en cours d'implémentation :**
|
||||
|
||||
- **Horizon borné plutôt qu'ouvert.** Un futur strictement illimité faisait
|
||||
remonter tout l'historique postérieur au mois demandé : ouvrir la newsletter
|
||||
de janvier 1999 saturait les 128 Mo de PHP (fatal). Le double plafond
|
||||
(12 mois, et jamais avant aujourd'hui) borne la liste sans rien retirer des
|
||||
cas réels — la séance du 13 novembre 2026 entre bien dans la newsletter de
|
||||
juillet 2026.
|
||||
- **Fenêtres de fin recalées sur la date d'événement.** Les fenêtres « +35 j »
|
||||
et « +3 mois » partaient de la date de publication : un événement annoncé plus
|
||||
de 35 jours à l'avance sortait de la fenêtre avant d'avoir eu lieu et manquait
|
||||
dans la newsletter de son propre mois. Vérifié sur quatre événements culturels
|
||||
annoncés 36 à 137 jours à l'avance : tous absents avant, tous présents après.
|
||||
C'est probablement l'autre moitié du « certains items du futur
|
||||
n'apparaissaient pas » du commanditaire.
|
||||
- **`merge_selected_items()`** — filet de sécurité : une sélection déjà
|
||||
enregistrée est réinjectée dans la liste même si ses items sont sortis de la
|
||||
fenêtre. Sans ça, rouvrir puis réenregistrer une ancienne newsletter
|
||||
l'amputait silencieusement (le formulaire ne soumet que ce qui est affiché).
|
||||
Vérifié : la newsletter de juin restitue ses 54 items cochés.
|
||||
|
||||
**Point à valider par le commanditaire** (l'arbitrage du §5 n'avait pas été
|
||||
tranché) : sur un mois neuf, tout est coché **sauf** le bloc « À venir », qui
|
||||
est listé, visuellement distinct et décoché. Les séances à venir suivent la même
|
||||
règle sans bloc séparé (elles restent groupées sous leur séminaire).
|
||||
|
||||
**Non traité** : la saisie de `date_de_fin` sur les appels à contribution reste
|
||||
une consigne éditoriale — sans ce champ, aucun « Jusqu'au … » n'est possible.
|
||||
@@ -0,0 +1,237 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user