From e64a44a2755e602540cd42899e991f52f5b95582 Mon Sep 17 00:00:00 2001 From: Valentin Le Moign Date: Thu, 27 Aug 2026 17:48:18 +0200 Subject: [PATCH] Notes des deux correctifs du 2026-08-27 + CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_011w7RzAHv4r1Yahnm2NwDDG --- CLAUDE.md | 5 +- NEWSLETTER-RETOURS-2026-08-27.md | 202 ++++++++++++++++++++++ NOTE-SLUG-NUMERIQUE-404-2026-08-27.md | 237 ++++++++++++++++++++++++++ 3 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 NEWSLETTER-RETOURS-2026-08-27.md create mode 100644 NOTE-SLUG-NUMERIQUE-404-2026-08-27.md diff --git a/CLAUDE.md b/CLAUDE.md index 0221f6f..1e644b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) diff --git a/NEWSLETTER-RETOURS-2026-08-27.md b/NEWSLETTER-RETOURS-2026-08-27.md new file mode 100644 index 0000000..8565434 --- /dev/null +++ b/NEWSLETTER-RETOURS-2026-08-27.md @@ -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. diff --git a/NOTE-SLUG-NUMERIQUE-404-2026-08-27.md b/NOTE-SLUG-NUMERIQUE-404-2026-08-27.md new file mode 100644 index 0000000..4c35780 --- /dev/null +++ b/NOTE-SLUG-NUMERIQUE-404-2026-08-27.md @@ -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 +``). 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.