Slugs numériques : garde-fou et régénération après un enregistrement sans titre

Avec la structure de permaliens /%category%/%postname%/, la règle
(.+?)/([^/]+)(?:/([0-9]+))?/?$ lit un dernier segment numérique comme un
numéro de page : l'article répond 404 tout en restant listé partout.

Deux chemins produisaient un tel slug :
- un titre vide à la première publication — le cœur retombe alors sur l'ID
  du post et ne régénère plus jamais le slug ensuite, même titre corrigé ;
  c'est ce que provoquait post-title-required.php, qui laisse le post
  s'écrire en publish avant d'annuler son statut ;
- un titre légitimement numérique (« 1917 »).

- inc/post-slug-guard.php : filtre wp_unique_post_slug limité à post/page,
  suffixe les slugs purement numériques (1917 → 1917-2). Les nav_menu_item
  et attachments, dont les slugs numériques sont normaux, ne sont pas touchés.
- pods-save-error-handler.php : l'annulation de statut vide aussi post_name
  quand le titre est vide, pour qu'il se régénère depuis le vrai titre.

Les deux articles déjà touchés en prod restent à re-sluguer à la main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w7RzAHv4r1Yahnm2NwDDG
This commit is contained in:
2026-08-27 17:47:53 +02:00
co-authored by Claude Opus 5
parent 3f62d882f4
commit b374d9c423
3 changed files with 88 additions and 2 deletions
+2
View File
@@ -39,6 +39,8 @@ require_once __DIR__ . '/inc/pods-custom-simple-decode.php';
require_once __DIR__ . '/inc/pods-taxonomy-sync.php';
require_once __DIR__ . '/inc/pods-save-error-handler.php';
require_once __DIR__ . '/inc/post-title-required.php';
// Slugs purement numériques → irroutables avec /%category%/%postname%/
require_once __DIR__ . '/inc/post-slug-guard.php';
// Helpers partagés des pages d'archives (filtres GET, rubriques, « Autres »)
require_once __DIR__ . '/inc/archive-filters.php';
// Helpers de données pour les templates
+17 -2
View File
@@ -62,11 +62,26 @@ add_filter( 'redirect_post_location', function ( $location ) {
! in_array( $original, [ 'publish', 'future', 'pending' ], true )
) {
global $wpdb;
$data = [ 'post_status' => $original ?: 'draft' ];
$formats = [ '%s' ];
// Le post vient d'être écrit en publish avec un titre vide : le cœur a
// alors figé son slug sur l'ID du post (wp_insert_post →
// sanitize_title( $post_title, $post_id )), et ne le régénère plus
// jamais ensuite, même une fois le titre corrigé — d'où un permalien
// numérique irroutable (cf. inc/post-slug-guard.php). On vide le slug
// pour qu'il soit reconstruit depuis le vrai titre à la publication.
if ( trim( (string) $post->post_title ) === '' ) {
$data['post_name'] = '';
$formats[] = '%s';
}
$wpdb->update(
$wpdb->posts,
[ 'post_status' => $original ?: 'draft' ],
$data,
[ 'ID' => $post_id ],
[ '%s' ],
$formats,
[ '%d' ]
);
clean_post_cache( $post_id );
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* Garde-fou sur les slugs d'articles et de pages.
*
* La structure de permaliens du site est /%category%/%postname%/. La règle de
* réécriture qui sert les articles est :
*
* (.+?)/([^/]+)(?:/([0-9]+))?/?$ => category_name=$1&name=$2&page=$3
*
* Le dernier groupe est un numéro de page (pagination interne d'un article via
* <!--nextpage-->). Un slug purement numérique est donc avalé comme numéro de
* page : l'article devient irroutable et répond 404, alors qu'il continue de
* s'afficher dans toutes les listes.
*
* Deux chemins produisent un tel slug :
* - un titre vide au moment de la première publication — le cœur retombe alors
* sur l'ID du post (wp-includes/post.php, wp_insert_post : sanitize_title(
* $post_title, $post_id)) et ne régénère plus jamais le slug ensuite ;
* - un titre légitimement numérique (« 1917 »).
*
* Ce module traite le second cas à la source ; le premier est traité en amont
* dans inc/pods-save-error-handler.php (le slug figé est vidé au moment où le
* statut est annulé, pour qu'il se régénère depuis le vrai titre).
*
* Portée volontairement limitée à post/page : les nav_menu_item et les
* attachments ont légitimement des slugs numériques et ne passent pas par
* cette règle de réécriture.
*/
/**
* Un slug purement numérique est-il à corriger pour ce type de contenu ?
* Fonction pure (testable) : la requête d'unicité vit dans le filtre.
*/
function thalim_slug_needs_guard( string $slug, string $post_type ): bool {
if ( ! in_array( $post_type, [ 'post', 'page' ], true ) ) {
return false;
}
return $slug !== '' && ctype_digit( $slug );
}
/**
* Suffixe les slugs purement numériques (-2, -3, …), en suivant la convention
* du cœur pour les slugs en conflit.
*/
add_filter( 'wp_unique_post_slug', function ( $slug, $post_id, $post_status, $post_type ) {
$slug = (string) $slug;
if ( ! thalim_slug_needs_guard( $slug, (string) $post_type ) ) {
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,
(int) $post_id
) );
$suffix++;
} while ( $exists && $suffix < 100 );
return $alt;
}, 10, 4 );