Moving your WordPress posts to a /blog/%category%/ permalink structure is good for SEO — but the moment you save the new structure, every existing URL starts returning a 404. This guide fixes that in two steps, and every old URL redirects itself from then on.
No redirect plugin. No redirect table to maintain. New and recategorised posts are handled automatically.
Before You Start: Check for Slug Conflicts
If a page and a post share the same slug, that page will start redirecting to the post. Rename one of them before you begin.
Most sites have no overlap. To be certain, run this in phpMyAdmin — it should return nothing:
SELECT a.post_name FROM wp_posts a
JOIN wp_posts b ON b.post_name = a.post_name
AND b.post_type = 'page' AND b.post_status = 'publish'
WHERE a.post_type = 'post' AND a.post_status = 'publish';
Step 1: Change the WordPress Permalink Structure
In WordPress admin go to Settings → Permalinks, choose Custom Structure, and enter:
/blog/%category%/%postname%/
Click Save Changes. Your posts now live at the new URLs.
Every old URL returns a 404 from this moment — step 2 fixes that, so do it straight away.
Step 2: Add the 301 Redirect Code
This catches any old URL and sends it to the correct new one — for every post you already have, and every post you publish later.
add_action( 'template_redirect', function () {
if ( ! is_404() || is_admin() ) return;
$uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
$path = wp_parse_url( $uri, PHP_URL_PATH );
if ( ! is_string( $path ) || '' === trim( $path, '/' ) ) return;
$segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) );
if ( empty( $segments ) ) return;
// already a new-style URL
if ( 'blog' === $segments[0] && count( $segments ) >= 3 ) return;
$last = urldecode( end( $segments ) );
if ( preg_match( '/^(page|feed|amp|embed)$/i', $last ) && count( $segments ) > 1 ) {
array_pop( $segments );
$last = urldecode( end( $segments ) );
}
$slug = sanitize_title( $last );
if ( '' === $slug ) return;
$match = get_posts( array(
'name' => $slug,
'post_type' => 'post',
'post_status' => 'publish',
'numberposts' => 1,
'fields' => 'ids',
) );
if ( empty( $match ) ) return;
$target = get_permalink( $match[0] );
if ( ! $target ) return;
if ( untrailingslashit( (string) wp_parse_url( $target, PHP_URL_PATH ) ) === untrailingslashit( $path ) ) return;
$query = wp_parse_url( $uri, PHP_URL_QUERY );
if ( ! empty( $query ) ) $target .= ( strpos( $target, '?' ) === false ? '?' : '&' ) . $query;
wp_safe_redirect( $target, 301 );
exit;
}, 1 );
Only change the highlighted 'blog' if your permalink uses a different prefix.
Where to Add the Code
wp-content/mu-plugins/redirect.php, starting with <?php. Always active, and it survives theme changes.functions.php. Never the parent theme — updates will wipe it.How to Verify the Redirects Are Working
Open an old post URL. It should jump straight to the new address. To confirm properly:
curl -sI https://yoursite.com/old-post-slug/ | grep -iE "^(HTTP/|location:)"
You want a 301 and a location: header containing /blog/….
Troubleshooting Permalink Redirect Issues
?x=123 to the URL — if that redirects, the code works and only the cache is stale.Why You Don't Need a Redirect Plugin
Redirect plugins store one rule per URL, which you then maintain by hand forever. This code stores nothing — it looks up each post as the request arrives, so new posts and recategorised posts are handled on their own.
Works with any permalink structure · no posts or pages are modified