From b374d9c423cf56d5d3f0c1762eaab362fb368644 Mon Sep 17 00:00:00 2001 From: Valentin Le Moign Date: Thu, 27 Aug 2026 17:47:53 +0200 Subject: [PATCH] =?UTF-8?q?Slugs=20num=C3=A9riques=20:=20garde-fou=20et=20?= =?UTF-8?q?r=C3=A9g=C3=A9n=C3=A9ration=20apr=C3=A8s=20un=20enregistrement?= =?UTF-8?q?=20sans=20titre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_011w7RzAHv4r1Yahnm2NwDDG --- functions.php | 2 + inc/pods-save-error-handler.php | 19 ++++++++- inc/post-slug-guard.php | 69 +++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 inc/post-slug-guard.php diff --git a/functions.php b/functions.php index 06f6e3e..3410976 100755 --- a/functions.php +++ b/functions.php @@ -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 diff --git a/inc/pods-save-error-handler.php b/inc/pods-save-error-handler.php index f806770..3df64a6 100644 --- a/inc/pods-save-error-handler.php +++ b/inc/pods-save-error-handler.php @@ -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 ); diff --git a/inc/post-slug-guard.php b/inc/post-slug-guard.php new file mode 100644 index 0000000..8f34a2f --- /dev/null +++ b/inc/post-slug-guard.php @@ -0,0 +1,69 @@ + category_name=$1&name=$2&page=$3 + * + * Le dernier groupe est un numéro de page (pagination interne d'un article via + * ). 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 );