Disclaimer: This is a version I found useful and it worked for my particular case. It may not work for you, but you can post a comment bellow and maybe we can figure it out.

Interesting issue. If you're a WordPress developer than you might have come across this issue in your endeavors to become an ultimate WordPress ninja.

I came across this while working on a recent project for an in-house project at the agency I work.

Having a custom post type and wanting the slug removed from the URL is tricky and is case depended. I tried a lot of plugins and solutions until I came across one (Sadly I can't find the original link, but if you do find it, please let me know in the comments below.) that I later tweaked for my own personal use (The version you are about to read is my version!), because, as I said this issue is case depended.

So, let us get down to the nitty-gritty. All you need are the following lines of code, that should be placed in your theme's functions.php file, and your permalink structure should be /%postname%/.

_Advice: Before placing these functions in your file you need to replace each occurrence of CUSTOM_POST_TYPE_NAME and, depending on your case, replace this line _

$post_name = $query->get('name');

with this one

$post_name = $query->get('pagename');

_and you should be good to go. _

If you want a more detailed explanation on how this works, keep reading the rest of the article after the following code section.

                add_filter('post_type_link','custom_post_type_link', 10, 3); 
    function custom_post_type_link($permalink, $post, $leavename) { 

        $url_components = parse_url($permalink); 
        $post_path = $url_components['path']; 
        $post_name = end(explode('/', trim($post_path, '/'))); 

        if(!empty($post_name)) { 
            switch($post->post_type) { 
                case 'CUSTOM_POST_TYPE_NAME': 
                    $permalink = str_replace($post_path, '/' . $post_name . '/', $permalink); 
                break; 
            } 
        } 
        return $permalink; 
    } 

    function custom_pre_get_posts($query) { 
        global $wpdb; 

        if(!$query->is_main_query()) { 
            return; 
        } 

        $post_name = $query->get('name'); 
        $post_type = $wpdb->get_var( $wpdb->prepare( 'SELECT post_type FROM ' . $wpdb->posts . ' WHERE post_name = %s LIMIT 1', $post_name ) ); 

        switch($post_type) { 
            case 'CUSTOM_POST_TYPE_NAME': 
                $query->set('CUSTOM_POST_TYPE_NAME', $post_name); 
                $query->set('post_type', $post_type); 
                $query->is_single = true; 
                $query->is_page = false; 
            break; 
        } 

        return $query; 
     } 
     add_action('pre_get_posts','custom_pre_get_posts');

Ok, you got to code, you placed it in your functions.php file but you want to understand the ways of the force.

Ok, let's dissect this sucker. The first thing that we have to do is to remove the actual slug from the actual URL. We can do this by getting the permalink for the current post and dissecting it using parse_url function (Very useful function!).
In the end (See what I did there?) we get the post name from the URL. Now we check the post type and if the current post has the desired type we proceed in removing the slug using str_replace on the permalink.

In order for this function to work we need it to be attach to a hook. Luckly, good guy WordPress has a filter called post_type_link that is used

prior to printing by the function get_post_permalink.

Ok, you managed to get until this step but you get a 404, right? Right. Well that's because WordPress doesn't recognize that post as a custom post type anymore. But we can fix that and trick WordPress into thinking that the requested post is in fact a custom post type.

The last function does this very thing. We need the global $wpdb variable, because we're going to play with the DB a bit (Don't worry we won't break anything important!). After checking if we're on the main query we proceed in getting the name for the current post.

The next logical step is to get the post type for the requested post. We do this with a simple SQL select. And now for the fun part. If the post type is our desired post type than we do the following actions. We set the value for the custom post type, the requested post. The basic syntax for query set is this:

$query->set('key', 'value')

WordPress needs to know what type the requested post is. After that we need to specify that the post is indeed a custom post type. And now for some little developer fairy dust and magic. We remind dear old WordPress that our post is in fact a post and it should be treated like one, that's where is_single comes in and is_page walks away. In order for this little trick to work, WordPress has a nifty little action called pre_get_posts that is

called after the query variable object is created, but before the actual query is run.

You can change all the stuff you want using this hook, before "all the gates open".

Useful resources:

  • Debug Bar with WP_DEBUG and SAVEQUERIES set to true in wp_config.php

And that was our lesson in WordPress slug removing (Nasty little buggers!).

Until next time, code long and prosper!

Stefan