82

It seems that all web resources based on the subject of removing a custom post type slug ie

yourdomain.com/CPT-SLUG/post-name 

are now very outdated solutions often referencing pre WP version 3.5 installs. A common one is to:

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

within your register_post_type function. This no longer works and is misleading. So I ask the community in Q4 2020...

What are the modern and efficient ways to remove the Post Type Slug from a Custom Post Type post's URL from within the rewrite argument or anywhere else?

UPDATE: There seems to be several ways to force this to work with regex. Specifically the answer from Jan Beck should you be consistently willing to monitor content creation to ensure no conflicting page/post names are created.... However I'm convinced that this is a major weakness in WP core where it should be handled for us. Both as an option/hook when creating a CPT or an advanced set of options for permalinks. Please support the track ticket.

Footnote: Please support this trac ticket by watching/promoting it: https://core.trac.wordpress.org/ticket/34136#ticket

2
  • I guess I'm scratching my head as to why you would want to do that? Confused. Commented May 29, 2017 at 15:22
  • 5
    @MichaelEcklund because any CPT that is used to create public facing web pages has a forced slug name in the URL. There is actually a lot of wp devs looking to remove the slug safely. Commented Jul 18, 2017 at 0:11

15 Answers 15

102
+25

The following code will work, but you just have to keep in mind that conflicts can happen easily if the slug for your custom post type is the same as a page or post's slug...

First, we will remove the slug from the permalink:

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

    if ( 'events' != $post->post_type || 'publish' != $post->post_status ) {
        return $post_link;
    }

    $post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );

    return $post_link;
}
add_filter( 'post_type_link', 'na_remove_slug', 10, 3 );

Just removing the slug isn't enough. Right now, you'll get a 404 page because WordPress only expects posts and pages to behave this way. You'll also need to add the following:

function na_parse_request( $query ) {

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

    if ( ! empty( $query->query['name'] ) ) {
        $query->set( 'post_type', array( 'post', 'events', 'page' ) );
    }
}
add_action( 'pre_get_posts', 'na_parse_request' );

Just change "events" to your custom post type and you're good to go. You may need to refresh your permalinks.

15
  • 2
    can you please update, how to use this code for multiple post types
    – Abin
    Commented Jan 25, 2016 at 11:29
  • 1
    It fails with nginx because the condition 2 != count( $query->query ). With nginx, you can have $query->query as array('page' => '', 'name' => '...', 'q' => '...'). So @NateAllen, what is the meaning of that condition? Commented Nov 8, 2016 at 12:25
  • 5
    We need something better than this. Support to remove the slug built in so that we cannot create conflicting URLs later on. The way regular posts and pages create their URLs. Commented Jul 18, 2017 at 0:13
  • 4
    Is it just me or does this break some wordpress conditional tags like is_single() and is_singular()?
    – rob-gordon
    Commented Jul 18, 2017 at 18:53
  • 1
    This solution unfortunately caused some broken links and my blog stopped showing posts and was just a normal page. See a better solution below by Matt Keys. Commented Oct 8, 2018 at 0:10
32

Write following code into the taxonomy registration.

'rewrite' => [
  'slug' => '/',
  'with_front' => false
]

Most important thing that you have to do after code changing

After you’ve altered your custom post type taxonomy document, try to go to Settings > Permalinks and re-save your settings, else you will get 404 page not found.

11
  • 11
    Tried this out. It gives the desired result for my custom post type links. However it 'catches' all POST or PAGE post type slugs and tries to resolve them as a URL for my custom post type, then 404s. (yes I've saved permalinks).
    – Matt Keys
    Commented Oct 5, 2017 at 20:49
  • 14
    This doesn't work. Gives 404 even when you've updated permalinks.
    – Christine Cooper
    Commented Nov 12, 2017 at 18:02
  • 2
    As I highlighted in my last comment, you will get an 404 error even after you have updated permalinks. Please give it a try yourself.
    – Christine Cooper
    Commented Nov 15, 2017 at 20:11
  • 10
    Again, even after re-saving the permalink settings, posts and pages no longer work (404)
    – amklose
    Commented Feb 13, 2018 at 19:18
  • 4
    As others have stated, this does work for the CPT posts themselves. But it's causing a 404 for regular Pages now.
    – Garconis
    Commented Jul 3, 2019 at 16:52
32

Looking through the answers here I think there is room for a better solution that combines some things I learned above and adds auto-detection and prevention of duplicate post slugs.

NOTE: Make sure you change 'custom_post_type' for your own CPT name throughout my example below. There are many occurrences, and a 'find/replace' is an easy way to catch them all. All of this code can go in your functions.php or in a plugin.

Step 1: Disable rewrites on your custom post type by setting rewrites to 'false' when you register the post:

register_post_type( 'custom_post_type',
    array(
        'rewrite' => false
    )
);

Step 2: Manually add our custom rewrites to the bottom of the WordPress rewrites for our custom_post_type

function custom_post_type_rewrites() {
    add_rewrite_rule( '[^/]+/attachment/([^/]+)/?$', 'index.php?attachment=$matches[1]', 'bottom');
    add_rewrite_rule( '[^/]+/attachment/([^/]+)/trackback/?$', 'index.php?attachment=$matches[1]&tb=1', 'bottom');
    add_rewrite_rule( '[^/]+/attachment/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$', 'index.php?attachment=$matches[1]&feed=$matches[2]', 'bottom');
    add_rewrite_rule( '[^/]+/attachment/([^/]+)/(feed|rdf|rss|rss2|atom)/?$', 'index.php?attachment=$matches[1]&feed=$matches[2]', 'bottom');
    add_rewrite_rule( '[^/]+/attachment/([^/]+)/comment-page-([0-9]{1,})/?$', 'index.php?attachment=$matches[1]&cpage=$matches[2]', 'bottom');
    add_rewrite_rule( '[^/]+/attachment/([^/]+)/embed/?$', 'index.php?attachment=$matches[1]&embed=true', 'bottom');
    add_rewrite_rule( '([^/]+)/embed/?$', 'index.php?custom_post_type=$matches[1]&embed=true', 'bottom');
    add_rewrite_rule( '([^/]+)/trackback/?$', 'index.php?custom_post_type=$matches[1]&tb=1', 'bottom');
    add_rewrite_rule( '([^/]+)/page/?([0-9]{1,})/?$', 'index.php?custom_post_type=$matches[1]&paged=$matches[2]', 'bottom');
    add_rewrite_rule( '([^/]+)/comment-page-([0-9]{1,})/?$', 'index.php?custom_post_type=$matches[1]&cpage=$matches[2]', 'bottom');
    add_rewrite_rule( '([^/]+)(?:/([0-9]+))?/?$', 'index.php?custom_post_type=$matches[1]', 'bottom');
    add_rewrite_rule( '[^/]+/([^/]+)/?$', 'index.php?attachment=$matches[1]', 'bottom');
    add_rewrite_rule( '[^/]+/([^/]+)/trackback/?$', 'index.php?attachment=$matches[1]&tb=1', 'bottom');
    add_rewrite_rule( '[^/]+/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$', 'index.php?attachment=$matches[1]&feed=$matches[2]', 'bottom');
    add_rewrite_rule( '[^/]+/([^/]+)/(feed|rdf|rss|rss2|atom)/?$', 'index.php?attachment=$matches[1]&feed=$matches[2]', 'bottom');
    add_rewrite_rule( '[^/]+/([^/]+)/comment-page-([0-9]{1,})/?$', 'index.php?attachment=$matches[1]&cpage=$matches[2]', 'bottom');
    add_rewrite_rule( '[^/]+/([^/]+)/embed/?$', 'index.php?attachment=$matches[1]&embed=true', 'bottom');
}
add_action( 'init', 'custom_post_type_rewrites' );

NOTE: Depending on your needs, you may want to modify the above rewrites (disable trackbacks? feeds?, etc). These represent the 'default' types of rewrites that would have been generated if you didn't disable rewrites in step 1

Step 3: Make permalinks to your custom post type 'pretty' again

function custom_post_type_permalinks( $post_link, $post, $leavename ) {
    if ( isset( $post->post_type ) && 'custom_post_type' == $post->post_type ) {
        $post_link = home_url( $post->post_name );
    }

    return $post_link;
}
add_filter( 'post_type_link', 'custom_post_type_permalinks', 10, 3 );

NOTE: You can stop here if you are not worried about your users creating a conflicting (duplicate) post in another post type that will create a situation where only one of them can load when the page is requested.

Step 4: Prevent duplicate post slugs

function prevent_slug_duplicates( $slug, $post_ID, $post_status, $post_type, $post_parent, $original_slug ) {
    $check_post_types = array(
        'post',
        'page',
        'custom_post_type'
    );
    
    if ( ! in_array( $post_type, $check_post_types ) ) {
        return $slug;
    }

    if ( 'custom_post_type' == $post_type ) {
        // Saving a custom_post_type post, check for duplicates in POST or PAGE post types
        $post_match = get_page_by_path( $slug, 'OBJECT', 'post' );
        $page_match = get_page_by_path( $slug, 'OBJECT', 'page' );

        if ( $post_match || $page_match ) {
            $slug .= '-duplicate';
        }
    } else {
        // Saving a POST or PAGE, check for duplicates in custom_post_type post type
        $custom_post_type_match = get_page_by_path( $slug, 'OBJECT', 'custom_post_type' );

        if ( $custom_post_type_match ) {
            $slug .= '-duplicate';
        }
    }

    return $slug;
}
add_filter( 'wp_unique_post_slug', 'prevent_slug_duplicates', 10, 6 );

NOTE: This will append the string '-duplicate' to the end of any duplicate slugs. This code cannot prevent duplicate slugs if they already exist prior to implementing this solution. Be sure to check for duplicates first.

I would love to hear back from anyone else who gives this a go to see if it worked well for them as well.

10
  • 2
    Just tested it and it seems like it is working so far.
    – Christine Cooper
    Commented Nov 12, 2017 at 18:02
  • 5
    This worked for me. Unlike other solutions on the page, it didn't break normal pages or the blog layout, and didn't cause infinite redirects. It even shows the correct URL in the "Permalink" area when editing those cpt pages. Pretty good solution here, only caveat is that the archive page doesn't work. REMEMBER to swap out "custom_post_type" and refresh your permalinks afterwards. Commented Oct 8, 2018 at 0:08
  • 2
    @MattKeys, the default Permalink Settings have a Custom Structure of /%category%/%postname%/. When adding your code, the CPT slugs look OK (although are missing the trailing slash) ... and the conflict checker works too. But the actual post results on a 404.
    – Garconis
    Commented Jul 3, 2019 at 14:39
  • 1
    Works great! However, I had to add a query variable in step 2 add_rewrite_tag( "%custom_post_type%", '([^/]+)', "post_type=custom_post_type&name=" ); to see the CPT pages instead of an 404 message
    – Philipp
    Commented May 8, 2021 at 16:16
  • 2
    This is the only solution that worked for me. I just had to add a trailing slash to the end of return $post_link in step 3.
    – EmilyH
    Commented Aug 26, 2021 at 19:44
16

I tried to figure this out not long ago and the short answer from what I know is no. Not from within the rewrite argument at least.

The long explanation becomes apparent if you look at the actual code of register_post_type in wp-includes/post.php line 1454:

add_permastruct( $post_type, "{$args->rewrite['slug']}/%$post_type%", $permastruct_args );

You can see it prefixes $args->rewrite['slug'] to the %$post_type% rewrite tag. One could think "let's just set the slug to null then" until you look a few lines up:

if ( empty( $args->rewrite['slug'] ) )
    $args->rewrite['slug'] = $post_type;

You can see that the function always expects a slug value that is not empty and otherwise uses the post type.

3
  • Thanks @JanBeck . Is there a major reason for this to exist? Why not hack this core file with a conditional to omit certain post types from this rule? Commented Sep 30, 2015 at 18:02
  • 9
    You should award the answer to Jan Beck. WordPress needs the post_type slug to route requests properly. This rule prevents naming conflicts between native WP pages (which render without the slug) and any custom defined post types. If you hack the slug out then WordPress won't know the difference between a page named "picnic" and an event (custom post type) named "picnic".
    – dswebsme
    Commented Sep 30, 2015 at 19:34
  • 3
    @dswebsme Agreed, but there are situations where you absolutely must change the URL. So other than why you can't natively and shouldn't, how do you do so efficiently? Commented Oct 1, 2015 at 13:27
16

Plugin Roundup

It's almost 2020 and a lot of these answers don't work. Here's my own roundup of the current options:

  • Matt Keys answer seems to be the only one on the right track if you want a custom code solution. None of the plugins I found can do everything listed here, especially the duplicate checking. This approach seems like a really good opportunity for a plugin if anyone wanted to take that on.
  • Permalink Manager Lite
    • 👍 Best of the free plugins I tried.
    • 👍 Gives full control over all Page/Post/CPT complete permalink structure and allows them to be the same. The GUI is by far the most feature-rich.
    • 👍 Allows full override per-post as well and lets you see what the original/default would be and reset to the default if needed.
    • 👍 Supports multi-site.
    • 👎 Does not check for duplicates between post types, which is sad. If a page and a CPT have the same URL, the page loads and the CPT is inaccessible. No warnings or errors, you just have to do your own manual checking for duplicates.
    • 💵 All taxonomy features are in the PRO version. The upgrade nags are pretty heavy.
  • Custom Permalinks
    • 👍 The free version does a lot. Taxonomy permalinks and premium support seem to be the only things withheld from the pro version.
    • 👍 Allows you to change the full permalink for any individual page/post/CPT.
    • 👍 Supports multi-site.
    • 👎 Does not allow you to change the default structure so you your Custom Post Types will still be example.com/cpt-slug/post-title but you can change them individually.
    • 👎 Does not check for duplicates between post types, which is sad.
  • Custom Post Type Permalinks
    • 👍 Allows non-developer users to change the things that are easy to change already with register_post_type
    • 👎 Does not allow you to change the CPT base slug - only the part that comes after that - so pretty much useless for developers and the issue in this question.
  • remove base slug... - dead for several years now... do not use.
3
  • 1
    The plugin Permalink Manager Lite is definitively the best solution : steady, robust, clean, and the free version allow you to remove the slug base. And it works with Polylang too ! Tested on Wordpress 5.4, with TwentyTwenty Theme, without any other plugin activated. Works like a charm on Custom Post Type, no matter if you have created a hierarchical one (with child post and grandchild post). From everyone who wants a clean solution.
    – PhpDoe
    Commented Apr 4, 2020 at 11:57
  • 1
    Nice roundup. The code from my answer has run on a client site for years now without any issues. The one downside that some have pointed out in my solution is that it does not work on the 'default' permalinks for the built in 'blog' post type (post). Instead there must be a prefix like: /blog/%postname%/. For those using WordPress as a CMS, they may already are doing this, but for others this might be a blocker unfortunately.
    – Matt Keys
    Commented Feb 1, 2021 at 20:23
  • Used plugin manager lite and it will work for my situation Commented Jan 28, 2022 at 17:51
8

In response to my previous answer: you could of course set the rewrite parameter to false when registering a new post type and handle the rewrite rules yourself like so

<?php
function wpsx203951_custom_init() {

    $post_type = 'event';
    $args = (object) array(
        'public'      => true,
        'label'       => 'Events',
        'rewrite'     => false, // always set this to false
        'has_archive' => true
    );
    register_post_type( $post_type, $args );

    // these are your actual rewrite arguments
    $args->rewrite = array(
        'slug' => 'calendar'
    );

    // everything what follows is from the register_post_type function
    if ( is_admin() || '' != get_option( 'permalink_structure' ) ) {

        if ( ! is_array( $args->rewrite ) )
            $args->rewrite = array();
        if ( empty( $args->rewrite['slug'] ) )
            $args->rewrite['slug'] = $post_type;
        if ( ! isset( $args->rewrite['with_front'] ) )
            $args->rewrite['with_front'] = true;
        if ( ! isset( $args->rewrite['pages'] ) )
            $args->rewrite['pages'] = true;
        if ( ! isset( $args->rewrite['feeds'] ) || ! $args->has_archive )
            $args->rewrite['feeds'] = (bool) $args->has_archive;
        if ( ! isset( $args->rewrite['ep_mask'] ) ) {
            if ( isset( $args->permalink_epmask ) )
                $args->rewrite['ep_mask'] = $args->permalink_epmask;
            else
                $args->rewrite['ep_mask'] = EP_PERMALINK;
        }

        if ( $args->hierarchical )
            add_rewrite_tag( "%$post_type%", '(.+?)', $args->query_var ? "{$args->query_var}=" : "post_type=$post_type&pagename=" );
        else
            add_rewrite_tag( "%$post_type%", '([^/]+)', $args->query_var ? "{$args->query_var}=" : "post_type=$post_type&name=" );

        if ( $args->has_archive ) {
            $archive_slug = $args->has_archive === true ? $args->rewrite['slug'] : $args->has_archive;
            if ( $args->rewrite['with_front'] )
                $archive_slug = substr( $wp_rewrite->front, 1 ) . $archive_slug;
            else
                $archive_slug = $wp_rewrite->root . $archive_slug;

            add_rewrite_rule( "{$archive_slug}/?$", "index.php?post_type=$post_type", 'top' );
            if ( $args->rewrite['feeds'] && $wp_rewrite->feeds ) {
                $feeds = '(' . trim( implode( '|', $wp_rewrite->feeds ) ) . ')';
                add_rewrite_rule( "{$archive_slug}/feed/$feeds/?$", "index.php?post_type=$post_type" . '&feed=$matches[1]', 'top' );
                add_rewrite_rule( "{$archive_slug}/$feeds/?$", "index.php?post_type=$post_type" . '&feed=$matches[1]', 'top' );
            }
            if ( $args->rewrite['pages'] )
                add_rewrite_rule( "{$archive_slug}/{$wp_rewrite->pagination_base}/([0-9]{1,})/?$", "index.php?post_type=$post_type" . '&paged=$matches[1]', 'top' );
        }

        $permastruct_args = $args->rewrite;
        $permastruct_args['feed'] = $permastruct_args['feeds'];
        add_permastruct( $post_type, "%$post_type%", $permastruct_args );
    }
}
add_action( 'init', 'wpsx203951_custom_init' );

You can see the add_permastruct call now doesn't include the slug anymore. I tested two scenarios:

  1. When I created a page with the slug "calendar" that page is overwritten by the post type archive which also uses the "calendar" slug.

enter image description here

  1. When I created a page with the slug "my-event" and an event (CPT) with the slug "my-event", the custom post type is displayed.

enter image description here

  1. Any other pages do not work either. If you look at the picture above it becomes clear why: the custom post type rule will always match against a page slug. Because WordPress has no way of identifying if it's a page or a custom post type that does not exist, it will return 404. That's why you need a slug to identify either the page or CPT. A possible solution would be to intercept the error and look for a page that might exist similar to this answer.
3
  • So if the goal is to remove the slug for CPT's couldn't we name the CPT something unique that wouldn't collide since it will never be seen in the URL anyways? Or is the post-name the possible conflict if named the same as a page? Commented Oct 2, 2015 at 14:35
  • I have updated my answer to show that this does actually break all pages. Without a slug, WP will look for a CPT instead of a page and if it doesn't find it, return an error. So it's actually not related to the post-name.
    – Jan Beck
    Commented Oct 2, 2015 at 15:49
  • 1
    I see. There should be rewrite rules that append '-1' to future conflicting URL's like native WP posts vs pages. I've created a trac ticket core.trac.wordpress.org/ticket/34136#ticket would love your thoughts. Commented Oct 2, 2015 at 23:22
7

Background

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:

// 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 );

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.

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.

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 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 );

Considerations:

  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.

2
  • As of writing, this solution is the best one. It's simple and correct. Its logic is easy to understand and be customized. In fact, I customized this solution to remove taxonomy slugs too.
    – Tom Nguyen
    Commented Jan 23 at 7:27
  • To prevent creating duplicate slugs, you can read and customize the code in "Step 4" of the answer by Matt Keys.
    – Tom Nguyen
    Commented Jan 30 at 15:07
2

and we can make some changes to above-mentioned function:

function na_parse_request( $query ) {

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

if ( ! empty( $query->query['name'] ) ) {
    $query->set( 'post_type', array( 'post', 'events', 'page' ) );
}
}

to:

function na_parse_request( $query ) {

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

if ( ! empty( $query->query['name'] ) ) {

    global $wpdb;
    $pt = $wpdb->get_var(
        "SELECT post_type FROM `{$wpdb->posts}` " .
        "WHERE post_name = '{$query->query['name']}'"
    );
    $query->set( 'post_type', $pt );
}
}

in order to set right post_type value.

2

This worked for me: 'rewrite' => array('slug' => '/')

1
  • 3
    This doesn't work. Gives 404 even when you've updated permalinks.
    – Christine Cooper
    Commented Nov 12, 2017 at 18:02
2

For anyone reading this that had trouble with child posts like I did I found the best way was to add your own rewrite rules.

The main issue I was having was that WordPress treats the redirect from pages that are 2 levels (child posts) deep a little differently than it treats 3 levels deep (child of child posts).

That means when I have /post-type/post-name/post-child/ I can use /post-name/post-child and it will redirect me to the one with post-type in front but if I have post-type/post-name/post-child/post-grandchild then I can't use post-name/post-child/post-grandchild.

Taking a look into the rewrite rules it looks like it matches for things other than pagename at the first and second levels (I think the second level matches attachment) and then does something there to redirect you to the proper post. At three levels deep it doesn't work.

First thing you need to do is to remove the post type link from children as well. This logic should happen here if you look at Nate Allen's answer above:

$post_link = str_replace( '/' . $post->post_type . '/', '/', $post_link );

Myself I used a mix of different conditionals to check if the post had children and whatnot in order to get to the right permalink. This part isn't too tricky and you'll find examples of people doing it elsewhere.

The next step though is where things change from the given answer. Instead of adding things to the main query (which worked for custom posts and their children but not the further children) I added a rewrite that went to the bottom of the WordPress rules so that if pagename didn't check out and it was about to hit a 404 it would do one last check to see if a page within the custom post type had the same name otherwise it would throw out the 404.

Here is the rewrite rule I used assuming 'event' is the name of your CPT

function rewrite_rules_for_removing_post_type_slug()
{
    add_rewrite_rule(
        '(.?.+?)(?:/([0-9]+))?/?$',
        'index.php?event=$matches[1]/$matches[2]&post_type=event',
        'bottom'
    );
}

add_action('init', 'rewrite_rules_for_removing_post_type_slug', 1, 1);

Hope this helps someone else, I couldn't find anything else that had to do with child of child posts and removing the slug from those.

1
  • There seems to be a typo in the regex. Between '(:' a '?' is needed to use it as non-capturing subpattern => '(?:'. The third ? seems misplaced as it allows an empty first subpattern. Propably it should be positioned between ( and :. Without this typo the expression will be the same as the one which can be found for the build-in post type 'page'.
    – jot
    Commented Oct 28, 2019 at 19:46
1

Had the same problems here and there seems to be no movement on wordpress site. In my particular situation where for single blogposts the structure /blog/%postname%/ was needed this solution

https://kellenmace.com/remove-custom-post-type-slug-from-permalinks/

ended in a bunch of 404s

But together with this wonderful approach, which is not using the backend permalink strukture for the blogpost it finally works like charme. https://www.bobz.co/add-blog-prefix-permalink-structure-blog-posts/

Thanks a bunch.

1
  • Link-only answers are discouraged. If you found an answer in this article that is different from the other answers on the page, please put a summary and code example in your answer. Commented Dec 16, 2019 at 22:50
1

For me worked now:

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

Insert inside register_post_type() function.

2
  • 2
    This solution breaks simple pages
    – mihdan
    Commented Jun 23, 2021 at 14:36
  • I tried and it worked. This is simple code you have to use register_post_type('wporg_product', array( 'labels' => array( 'name' => 'Portfolio', 'singular_name' => 'Portfolio', ), 'menu_icon' => 'dashicons-id', 'rewrite' => array( 'slug' => 'portfolio', 'with_front' => false ), // my custom slug ) ); After changing this code, you have to save permalink setting once.
    – Rohit
    Commented Aug 4 at 8:38
1

This is what worked for me. Replace podcast with your CPT slug:

add_action('init', function () {
    register_post_type(
        'podcast',
        [
            'rewrite' => false,
        ]
    );
});

add_filter('post_type_link', function ($post_link, $post, $leavename) {
    if (isset($post->post_type) && $post->post_type === 'podcast') {
        $post_link = home_url($post->post_name);
    }

    return $post_link;
}, 10, 3);

add_action('init', function () {
    add_rewrite_rule('(.+?)/?$', 'index.php?podcast=$matches[1]', 'bottom');
});
1

"Remove CPT base" plugin works.

https://wordpress.org/plugins/remove-cpt-base/

-1

You dont need so much hard-code. Just use lightweight plugin:

It has customizable options.

5
  • Now I know why you got downvoted, it prevents normal page links resolving. I didn't see it because I was getting cached copies of the existing pages despite refreshing.
    – Walf
    Commented Sep 14, 2017 at 2:36
  • @Walf Can you tell me about the issue in details?
    – T.Todua
    Commented Sep 14, 2017 at 8:17
  • Following links to pages (that weren't the custom post type) from the main menu gave 404 errors, as if the page did not exist; that's it.
    – Walf
    Commented Sep 14, 2017 at 11:45
  • @Walf can you give me any example url of your occasion? (you can cover domain name if you want, i just need an ex example) thanks, i wil update it
    – T.Todua
    Commented Sep 15, 2017 at 7:31
  • 3
    "This plugin has been closed as of September 19, 2018 and is not available for download. This closure is permanent." Commented Dec 16, 2019 at 22:51

Not the answer you're looking for? Browse other questions tagged or ask your own question.