Skip to main content
code improvement
Source Link
arafatgazi
  • 205
  • 2
  • 4

Background

// remove cpt slug from permalinks
function remove_cpt_slug( $post_link, $post, $leavename ) {

    if ( $post->post_type != 'custom_post_type' ) {
        return $post_link;
    } else {
        // remove cpt slug from permalink
        $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
        return $post_link;
    }
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );
function rewrite_rule_remove_cpt_slug() {

    add_rewrite_rule(
        '(.?.+?)(?:/([0-9]+))?/?$',
        'index.php?custom_post_type=$matches[1]/$matches[2]&post_type=custom_post_type',
        'bottom'
    );
}
add_action( 'init', 'rewrite_rule_remove_cpt_slug', 1, 1 );

Here is what I have come up with while trying to create a solution that works on most if not all scenarios. This will properly remove CPT slug from CPT permalinks as well as instruct WordPress on finding CPT posts from those new permalinks. It doesn't rewrite rules in the database so you wouldn't need to resave your permalink structure. Besides, this solution is consistent with how WordPress actually parses requests to find posts from permalinks which helps make it a more acceptable solution.

Answer

Here is what I have come up with while trying to create a solution that works on most if not all scenarios. This will properly remove CPT slug from CPT permalinks as well as instruct WordPress on finding CPT posts from those new permalinks. It doesn't rewrite rules in the database so you won't need to resave your permalink structure. Besides, this solution is consistent with how WordPress actually parses requests to find posts from permalinks which helps make it a more acceptable solution.

// remove cpt slug from permalinkpermalinks
function remove_cpt_slug( $post_link, $post, $leavename ) {

    if ( $post->post_type != 'custom_post_type' ) {
        return $post_link;
    } else {
        // remove cpt slug from permalink
        $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
        return $post_link;
    }
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );


// instruct wordpress on how to find posts from the new permalinks
function parse_request_remove_cpt_slug( $query_vars ) {

    // return if pretty permalinkadmin isn'tdashboard enabled
    if ( ! get_optionis_admin( 'permalink_structure' ) ) {
        return $query_vars;
    }

    $cpt// =return 'custom_post_type';if pretty permalink isn't enabled
    if ( ! get_option( 'permalink_structure' ) ) {
    $pagename = $query_vars['pagename'];  return $query_vars;
    $name}

    $cpt = $query_vars['name'];'custom_post_type';

    // store post slug value to a variable
    if ( $pagenameisset( $query_vars['pagename'] ) ) {
        $slug = $pagename;$query_vars['pagename'];
    } elseif ( $nameisset( $query_vars['name'] ) ) {
        $slug = $name;$query_vars['name'];
    } else {
        global $wp;
        $uri
        $path = $wp->request;

        // use url path as slug
        if ( $path && strpos( $uri$path, '/' ) === false ) {
            $slug = $uri;$path;
        } else {
            $slug = false;
        }
    }

    if ( $slug ) {
        $post_match = get_page_by_path( $slug, 'OBJECT', $cpt );

        if ( ! is_admin() && $post_match ) {

            // remove any 404 not found error element, if any, from the query_vars array because a post match already exists in cpt
            if ( isset( $query_vars['error'] ) && $query_vars['error'] == 404 ) {
                unset( $query_vars['error'] );
            }

            // remove unnecessary elements from the original query_vars array
            // unset($query_vars['page']);
            unset($query_vars['pagename'] );
    
            // add necessary elements in the the query_vars array
            $query_vars['post_type'] = $cpt;
            $query_vars['name'] = $slug;
            $query_vars[$cpt] = $slug; // this constructs the "cpt=>post_slug" element
        }
    }

    return $query_vars;
}
add_filter( 'request', "parse_request_remove_cpt_slug" , 1, 1 );
  1. This solution intentionally leaves out Plain permalink structure Plain from its scope as it isn't one of the pretty permalink structures. So, it will work with all permalink structures apart from the PlainPlain one.

  2. As WordPress doesn't automatically prevent creating duplicate slugs across different post types, you may find problems accessing posts having the same post slugs because of not havinglosing uniqueness in CPT permalinks after removing the CPT slugs. This code doesn't include any functionality to prevent that behavior so you may want to find a separate solution to address that.

  3. In case there is a duplicate permalink, this code will prioritize your CPT over others and therefore display the post in your CPT when requested.

function remove_cpt_slug( $post_link, $post, $leavename ) {

    if ( $post->post_type != 'custom_post_type' ) {
        return $post_link;
    } else {
        // remove cpt slug from permalink
        $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
        return $post_link;
    }
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );
function rewrite_rule_remove_cpt_slug() {

    add_rewrite_rule(
        '(.?.+?)(?:/([0-9]+))?/?$',
        'index.php?custom_post_type=$matches[1]/$matches[2]&post_type=custom_post_type',
        'bottom'
    );
}
add_action('init', 'rewrite_rule_remove_cpt_slug', 1, 1);

Here is what I have come up with while trying to create a solution that works on most if not all scenarios. This will properly remove CPT slug from CPT permalinks as well as instruct WordPress on finding CPT posts from those new permalinks. It doesn't rewrite rules in the database so you wouldn't need to resave your permalink structure. Besides, this solution is consistent with how WordPress actually parses requests to find posts from permalinks which helps make it a more acceptable solution.

// remove cpt slug from permalink
function remove_cpt_slug( $post_link, $post, $leavename ) {

    if ( $post->post_type != 'custom_post_type' ) {
        return $post_link;
    } else {
        // remove cpt slug from permalink
        $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
        return $post_link;
    }
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );


// instruct wordpress on how to find posts from the new permalinks
function parse_request_remove_cpt_slug( $query_vars ) {

    // return if pretty permalink isn't enabled
    if ( ! get_option( 'permalink_structure' ) ) {
        return $query_vars;
    }

    $cpt = 'custom_post_type';

    $pagename = $query_vars['pagename'];
    $name = $query_vars['name'];

    // store post slug value to a variable
    if ( $pagename ) {
        $slug = $pagename;
    } elseif ( $name ) {
        $slug = $name;
    } else {
        global $wp;
        $uri = $wp->request;

        // use url path as slug
        if ( strpos( $uri, '/' ) === false ) {
            $slug = $uri;
        }
    }

    if ( $slug ) {
        $post_match = get_page_by_path($slug, 'OBJECT', $cpt);

        if ( ! is_admin() && $post_match ) {

            // remove 404 not found error element, if any, from the query_vars array because a post match already exists in cpt
            if ( $query_vars['error'] == 404 ) {
                unset($query_vars['error']);
            }

            // remove unnecessary elements from the original query_vars array
            // unset($query_vars['page']);
            unset($query_vars['pagename']);
    
            // add necessary elements in the the query_vars array
            $query_vars['post_type'] = $cpt;
            $query_vars['name'] = $slug;
            $query_vars[$cpt] = $slug; // this constructs the "cpt=>post_slug" element
        }
    }

    return $query_vars;
}
add_filter('request', "parse_request_remove_cpt_slug" , 1, 1);
  1. This solution intentionally leaves out permalink structure Plain from its scope as it isn't one of the pretty permalink structures. So, it will work with all permalink structures apart from the Plain one.

  2. As WordPress doesn't automatically prevent creating duplicate slugs across different post types, you may find problems accessing posts having the same post slugs because of not having uniqueness in CPT permalinks after removing the CPT slugs. This code doesn't include any functionality to prevent that behavior so you may want to find a separate solution to address that.

  3. In case there is a duplicate permalink, this code will prioritize your CPT over others and therefore display the post in your CPT.

Background

// remove cpt slug from permalinks
function remove_cpt_slug( $post_link, $post, $leavename ) {

    if ( $post->post_type != 'custom_post_type' ) {
        return $post_link;
    } else {
        $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
        return $post_link;
    }
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );
function rewrite_rule_remove_cpt_slug() {

    add_rewrite_rule(
        '(.?.+?)(?:/([0-9]+))?/?$',
        'index.php?custom_post_type=$matches[1]/$matches[2]&post_type=custom_post_type',
        'bottom'
    );
}
add_action( 'init', 'rewrite_rule_remove_cpt_slug', 1, 1 );

Answer

Here is what I have come up with while trying to create a solution that works on most if not all scenarios. This will properly remove CPT slug from CPT permalinks as well as instruct WordPress on finding CPT posts from those new permalinks. It doesn't rewrite rules in the database so you won't need to resave your permalink structure. Besides, this solution is consistent with how WordPress actually parses requests to find posts from permalinks which helps make it a more acceptable solution.

// remove cpt slug from permalinks
function remove_cpt_slug( $post_link, $post, $leavename ) {

    if ( $post->post_type != 'custom_post_type' ) {
        return $post_link;
    } else {
        $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
        return $post_link;
    }
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );


// instruct wordpress on how to find posts from the new permalinks
function parse_request_remove_cpt_slug( $query_vars ) {

    // return if admin dashboard 
    if ( is_admin() ) {
        return $query_vars;
    }

    // return if pretty permalink isn't enabled
    if ( ! get_option( 'permalink_structure' ) ) {
        return $query_vars;
    }

    $cpt = 'custom_post_type';

    // store post slug value to a variable
    if ( isset( $query_vars['pagename'] ) ) {
        $slug = $query_vars['pagename'];
    } elseif ( isset( $query_vars['name'] ) ) {
        $slug = $query_vars['name'];
    } else {
        global $wp;
        
        $path = $wp->request;

        // use url path as slug
        if ( $path && strpos( $path, '/' ) === false ) {
            $slug = $path;
        } else {
            $slug = false;
        }
    }

    if ( $slug ) {
        $post_match = get_page_by_path( $slug, 'OBJECT', $cpt );

        if ( ! is_admin() && $post_match ) {

            // remove any 404 not found error element from the query_vars array because a post match already exists in cpt
            if ( isset( $query_vars['error'] ) && $query_vars['error'] == 404 ) {
                unset( $query_vars['error'] );
            }

            // remove unnecessary elements from the original query_vars array
            unset( $query_vars['pagename'] );
    
            // add necessary elements in the the query_vars array
            $query_vars['post_type'] = $cpt;
            $query_vars['name'] = $slug;
            $query_vars[$cpt] = $slug; // this constructs the "cpt=>post_slug" element
        }
    }

    return $query_vars;
}
add_filter( 'request', "parse_request_remove_cpt_slug" , 1, 1 );
  1. This solution intentionally leaves out Plain permalink structure from its scope as it isn't one of the pretty permalink structures. So, it will work with all permalink structures apart from the Plain one.

  2. As WordPress doesn't automatically prevent creating duplicate slugs across different post types, you may find problems accessing posts having the same post slugs because of losing uniqueness in CPT permalinks after removing the CPT slugs. This code doesn't include any functionality to prevent that behavior so you may want to find a separate solution to address that.

  3. In case there is a duplicate permalink, this code will prioritize your CPT over others and therefore display the post in your CPT when requested.

Source Link
arafatgazi
  • 205
  • 2
  • 4

Even after looking around everywhere, I couldn't find a proper solution for removing CPT slug from permalinks that actually works and is consistent with how WordPress actually parses requests. As it seems, everyone else looking for the same solution is in the same boat as me.

As it turns out, this is actually a two-part solution.

  1. Remove CPT slug from permalinks
  2. Instruct WordPress on how to find posts from the new permalinks

The first part is quite straightforward and many existing answers already have it right. This is what it looks like:

function remove_cpt_slug( $post_link, $post, $leavename ) {

    if ( $post->post_type != 'custom_post_type' ) {
        return $post_link;
    } else {
        // remove cpt slug from permalink
        $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
        return $post_link;
    }
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );

Now, the second part is where things get ugly. After solving the first part, your CPT permalinks don't have CPT slugs anymore. But, now, the problem is WordPress doesn't know how to find your posts from those new permalinks because all it knows is CPT permalinks have CPT slugs. So, without a CPT slug in the permalink, it can't find your post. That's why when you make a request for your posts at this point, it throws a 404 not found error.

So, all you need to do now is instruct WordPress on how to find your posts using the new permalinks. But this is the part where existing answers don't do very well. Let's take a look at a few of those answers for example:

The below function works pretty well but it will only work if your permalink structure is set to Post name.

function parse_request_remove_cpt_slug( $query ) {

    if ( ! $query->is_main_query() || 2 != count( $query->query ) || ! isset( $query->query['page'] ) ) {
        return;
    }

    if ( ! empty( $query->query['name'] ) ) {
        global $wpdb;
        $cpt = $wpdb->get_var("SELECT post_type FROM $wpdb->posts WHERE post_name = '{$query->query['name']}'");

        // Add CPT to the list of post types WP will include when it queries based on the post name.
        $query->set( 'post_type', $cpt );
    }
}
add_action( 'pre_get_posts', 'parse_request_remove_cpt_slug' );

The below function works well for your custom post type regardless of the permalink structure however it will throw an error on all other post types.

function rewrite_rule_remove_cpt_slug() {

    add_rewrite_rule(
        '(.?.+?)(?:/([0-9]+))?/?$',
        'index.php?custom_post_type=$matches[1]/$matches[2]&post_type=custom_post_type',
        'bottom'
    );
}
add_action('init', 'rewrite_rule_remove_cpt_slug', 1, 1);

There is another answer that is supposed to work as a standalone solution but ends up causing more issues than solutions like throwing errors in your CPT posts as well as on others. This one requires modifying the rewrite argument in your CPT registration as follows:

'rewrite' => array( 'slug' => '/', 'with_front' => false )

So far, all the existing answers I found are like the above ones. Either they work partially or don't work anymore. This is probably because WordPress doesn't give a streamlined way to remove CPT slug from custom post type permalinks and therefore these answers are either based on considering particular scenarios or based on a hacky way.

Here is what I have come up with while trying to create a solution that works on most if not all scenarios. This will properly remove CPT slug from CPT permalinks as well as instruct WordPress on finding CPT posts from those new permalinks. It doesn't rewrite rules in the database so you wouldn't need to resave your permalink structure. Besides, this solution is consistent with how WordPress actually parses requests to find posts from permalinks which helps make it a more acceptable solution.

Make sure to replace custom_post_type with your own custom post type name. It appears once in every function so two occurrences in total.

// remove cpt slug from permalink
function remove_cpt_slug( $post_link, $post, $leavename ) {

    if ( $post->post_type != 'custom_post_type' ) {
        return $post_link;
    } else {
        // remove cpt slug from permalink
        $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );
        return $post_link;
    }
}
add_filter( 'post_type_link', 'remove_cpt_slug', 10, 3 );


// instruct wordpress on how to find posts from the new permalinks
function parse_request_remove_cpt_slug( $query_vars ) {

    // return if pretty permalink isn't enabled
    if ( ! get_option( 'permalink_structure' ) ) {
        return $query_vars;
    }

    $cpt = 'custom_post_type';

    $pagename = $query_vars['pagename'];
    $name = $query_vars['name'];

    // store post slug value to a variable
    if ( $pagename ) {
        $slug = $pagename;
    } elseif ( $name ) {
        $slug = $name;
    } else {
        global $wp;
        $uri = $wp->request;

        // use url path as slug
        if ( strpos( $uri, '/' ) === false ) {
            $slug = $uri;
        }
    }

    if ( $slug ) {
        $post_match = get_page_by_path($slug, 'OBJECT', $cpt);

        if ( ! is_admin() && $post_match ) {

            // remove 404 not found error element, if any, from the query_vars array because a post match already exists in cpt
            if ( $query_vars['error'] == 404 ) {
                unset($query_vars['error']);
            }

            // remove unnecessary elements from the original query_vars array
            // unset($query_vars['page']);
            unset($query_vars['pagename']);
    
            // add necessary elements in the the query_vars array
            $query_vars['post_type'] = $cpt;
            $query_vars['name'] = $slug;
            $query_vars[$cpt] = $slug; // this constructs the "cpt=>post_slug" element
        }
    }

    return $query_vars;
}
add_filter('request', "parse_request_remove_cpt_slug" , 1, 1);

Considerations:

  1. This solution intentionally leaves out permalink structure Plain from its scope as it isn't one of the pretty permalink structures. So, it will work with all permalink structures apart from the Plain one.

  2. As WordPress doesn't automatically prevent creating duplicate slugs across different post types, you may find problems accessing posts having the same post slugs because of not having uniqueness in CPT permalinks after removing the CPT slugs. This code doesn't include any functionality to prevent that behavior so you may want to find a separate solution to address that.

  3. In case there is a duplicate permalink, this code will prioritize your CPT over others and therefore display the post in your CPT.