From d266d8c2ea29c728a2bfe8c025d965128cc75aea Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Tue, 3 Feb 2026 12:29:45 +0200 Subject: [PATCH 1/3] Add canonical redirect for categories with children Fixes https://core.trac.wordpress.org/ticket/18734 --- src/wp-includes/canonical.php | 22 ++++++++ tests/phpunit/includes/testcase-canonical.php | 27 ++++++++++ tests/phpunit/tests/canonical.php | 50 +++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 9315ba7fb7ff9..9c3a08ff470f4 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -346,6 +346,28 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { $tax_url = get_term_link( (int) $obj->term_id, $obj->taxonomy ); if ( $tax_url && ! is_wp_error( $tax_url ) ) { + $category_permastruct = $wp_rewrite->get_category_permastruct(); + + // Redirect if the current category path doesn't match the canonical path. + if ( is_category() + && ! empty( $obj->parent ) + && $category_permastruct + && str_contains( $category_permastruct, '%category%' ) + && ! empty( $wp->query_vars['category_name'] ) + ) { + $category_path_slug = get_category_parents( $obj->term_id, false, '/', true ); + + if ( $category_path_slug && ! is_wp_error( $category_path_slug ) ) { + $category_path_slug = untrailingslashit( $category_path_slug ); + $category_path = str_replace( '%category%', $category_path_slug, $category_permastruct ); + $request_category_path = str_replace( '%category%', $wp->query_vars['category_name'], $category_permastruct ); + + if ( $category_path !== $request_category_path ) { + $redirect['path'] = str_replace( $request_category_path, $category_path, $redirect['path'] ); + } + } + } + if ( ! empty( $redirect['query'] ) ) { // Strip taxonomy query vars off the URL. $qv_remove = array( 'term', 'taxonomy' ); diff --git a/tests/phpunit/includes/testcase-canonical.php b/tests/phpunit/includes/testcase-canonical.php index 33dbb3d9f438b..c98f7c15578e5 100644 --- a/tests/phpunit/includes/testcase-canonical.php +++ b/tests/phpunit/includes/testcase-canonical.php @@ -259,6 +259,33 @@ public static function generate_shared_fixtures( WP_UnitTest_Factory $factory ) ) ); + // Insert a few posts in each category to enable pagination. + for ( $p = 0; $p < 6; $p++ ) { + self::$post_ids[] = $factory->post->create( + array( + 'post_title' => 'Post in category parent ' . $p, + 'post_type' => 'post', + 'post_category' => array( self::$terms['/category/parent/'] ), + ) + ); + + self::$post_ids[] = $factory->post->create( + array( + 'post_title' => 'Post in category child-1 ' . $p, + 'post_type' => 'post', + 'post_category' => array( self::$terms['/category/parent/child-1/'] ), + ) + ); + + self::$post_ids[] = $factory->post->create( + array( + 'post_title' => 'Post in category child-2 ' . $p, + 'post_type' => 'post', + 'post_category' => array( self::$terms['/category/parent/child-1/child-2/'] ), + ) + ); + } + self::$term_ids[ $tag1 ] = 'post_tag'; } diff --git a/tests/phpunit/tests/canonical.php b/tests/phpunit/tests/canonical.php index 886b09312910e..37848f589d701 100644 --- a/tests/phpunit/tests/canonical.php +++ b/tests/phpunit/tests/canonical.php @@ -126,6 +126,56 @@ public function data_canonical() { 17174, ), + // Child categories with missing parent category slugs in the URL. + array( + '/category/child-1/', + array( + 'url' => '/category/parent/child-1/', + 'qv' => array( 'category_name' => 'parent/child-1' ), + ), + ), + array( + '/category/child-2/', + array( + 'url' => '/category/parent/child-1/child-2/', + 'qv' => array( 'category_name' => 'parent/child-1/child-2' ), + ), + ), + array( + '/category/parent/child-2/', + array( + 'url' => '/category/parent/child-1/child-2/', + 'qv' => array( 'category_name' => 'parent/child-1/child-2' ), + ), + ), + array( + '/category/too/many/parents/child-1/', + array( + 'url' => '/category/parent/child-1/', + 'qv' => array( 'category_name' => 'parent/child-1' ), + ), + ), + array( + '/category/child-1/page/2/', + array( + 'url' => '/category/parent/child-1/page/2/', + 'qv' => array( + 'category_name' => 'parent/child-1', + 'paged' => 2, + ), + ), + ), + array( + '/category/child-1/child-2/page/2/', + array( + 'url' => '/category/parent/child-1/child-2/page/2/', + 'qv' => array( + 'category_name' => 'parent/child-1/child-2', + 'paged' => 2, + ), + ), + ), + // Categories & intersections with other vars. array( '/category/uncategorized/?tag=post-formats', From 73c6ae6e6787a57e943f71f006d81e77be5844cd Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Tue, 3 Feb 2026 12:53:39 +0200 Subject: [PATCH 2/3] Use the helper to resolve this data --- src/wp-includes/canonical.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 9c3a08ff470f4..122a0805e2a46 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -347,20 +347,21 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { if ( $tax_url && ! is_wp_error( $tax_url ) ) { $category_permastruct = $wp_rewrite->get_category_permastruct(); + $category_name = get_query_var( 'category_name' ); // Redirect if the current category path doesn't match the canonical path. if ( is_category() && ! empty( $obj->parent ) && $category_permastruct && str_contains( $category_permastruct, '%category%' ) - && ! empty( $wp->query_vars['category_name'] ) + && $category_name ) { $category_path_slug = get_category_parents( $obj->term_id, false, '/', true ); if ( $category_path_slug && ! is_wp_error( $category_path_slug ) ) { $category_path_slug = untrailingslashit( $category_path_slug ); $category_path = str_replace( '%category%', $category_path_slug, $category_permastruct ); - $request_category_path = str_replace( '%category%', $wp->query_vars['category_name'], $category_permastruct ); + $request_category_path = str_replace( '%category%', $category_name, $category_permastruct ); if ( $category_path !== $request_category_path ) { $redirect['path'] = str_replace( $request_category_path, $category_path, $redirect['path'] ); From f5beb2fffe3df4eb0910d10a20c5754d5cbae5eb Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Tue, 3 Feb 2026 13:19:18 +0200 Subject: [PATCH 3/3] Use the request input for comparison since it matches the request path --- src/wp-includes/canonical.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 122a0805e2a46..8b3027bf7de2e 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -347,21 +347,20 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { if ( $tax_url && ! is_wp_error( $tax_url ) ) { $category_permastruct = $wp_rewrite->get_category_permastruct(); - $category_name = get_query_var( 'category_name' ); - // Redirect if the current category path doesn't match the canonical path. + // Ensure the correct parent-child category path is used in permalink. if ( is_category() && ! empty( $obj->parent ) && $category_permastruct && str_contains( $category_permastruct, '%category%' ) - && $category_name + && ! empty( $wp_query->query['category_name'] ) ) { $category_path_slug = get_category_parents( $obj->term_id, false, '/', true ); if ( $category_path_slug && ! is_wp_error( $category_path_slug ) ) { $category_path_slug = untrailingslashit( $category_path_slug ); $category_path = str_replace( '%category%', $category_path_slug, $category_permastruct ); - $request_category_path = str_replace( '%category%', $category_name, $category_permastruct ); + $request_category_path = str_replace( '%category%', $wp_query->query['category_name'], $category_permastruct ); if ( $category_path !== $request_category_path ) { $redirect['path'] = str_replace( $request_category_path, $category_path, $redirect['path'] );