Skip to main content
WordPress made easy with the drag & drop Total WordPress Theme!Learn More

Remove Custom Post Type Slug from URL in WordPress

Last modified: December 2, 2023

The following article will show you how to remove the slugs from your custom post types in WordPress without a plugin by adding some custom code to your site.

Whenever you add a new post type in WordPress (we recommend our Post Types Unlimited plugin for doing that) the individual posts will have a URL with the following format:

site.com/post_type_name/post-slug

While this may make sense for a variety of site structures, you may not want the post type slug in your url.

Why Does WordPress add the Post Type Name in the URL?

The reason WordPress adds the post type name in the URL is to prevent conflicts between different post types. For example; let’s say you upload a picture to your site named “new-york” but you also create a custom post type named “places” where one of your posts has the slug “new-york”. If the custom post type didn’t have a slug (places) it would end up with the same url as the image. In this scenario you would never be able to view the post , instead the image attachment page would be displayed when trying to access site.com/new-york/

How to Remove the Slug?

In an ideal world the WordPress register_post_type function would have a built-in argument to remove the post type name from the slug. Unfortunately this doesn’t exist as a default option so we need to get a bit creative. Below is a custom Class you can use to remove the slug from one or multiple post types.

Copy and paste this snippet into your child theme functions.php file, custom plugin or via the Code Snippets plugin then change the $this->types array to include an array of the post types you want to remove slugs from.

final class My_Remove_Post_Type_Slugs {

    /**
     * Define our $types variable.
     */
    protected $types = [];

    /**
     * Constructor.
     */
    public function __construct() {

        // CHANGE THIS ARRAY
        $this->types = [
            'portfolio' => 'portfolio-item',
        ];

        add_filter( 'post_type_link', [ $this, 'post_type_link' ], 10, 3 );
        add_action( 'pre_get_posts', [ $this, 'parse_request' ] );
        add_action( 'template_redirect', [ $this, 'template_redirect' ] );
    }

    /**
     * Remove the post type name from the post type link.
     */
    public function post_type_link( $post_link, $post, $leavename ) {
        $post_type = $post->post_type;

        if ( ! array_key_exists( $post_type, $this->types ) || 'publish' !== $post->post_status ) {
            return $post_link;
        }

        $post_link = str_replace( '/' . $this->types[$post_type] . '/', '/', $post_link );

        return $post_link;
    }

    /**
     * Trick WordPress to allow our post types to render without a custom slug.
     */
    public function parse_request( $query ) {
        if ( ! $query->is_main_query() ) {
            return;
        }

        // Only noop our very specific rewrite rule match.
        if ( 2 != count( $query->query ) || ! isset( $query->query['page'] ) ) {
            return;
        }

        // 'name' will be set if post permalinks are just post_name, otherwise the page rule will match.
        if ( ! empty( $query->query['name'] ) ) {
            $array = [ 'post', 'page' ];
            $array = array_merge( $array, array_keys( $this->types ) );
            $query->set( 'post_type', $array );
        }
    }

    /**
     * Redirect the old URL's with the slugs.
     */
    public function template_redirect() {
        if ( is_admin() || is_preview() || ! is_singular( array_keys( $this->types ) ) ) {
            return;
        }
        $post_permalink = trailingslashit( get_permalink() );
        $current_url    = trailingslashit( $this->get_current_url() );
        $post_slug      = get_post_field( 'post_name', get_post() );
        if ( ( $post_permalink && $current_url )
            && ( $post_permalink !== $current_url )
            && ( false !== strpos( $post_permalink, $post_slug ) && false !== strpos( $current_url, $post_slug ) )
        ) {
            wp_safe_redirect( $post_permalink, 301 );
            exit;
        }
    }

    /**
     * Get the current URL helper method.
     */
    protected function get_current_url() {
        global $wp;
        if ( $wp ) {
            return home_url( add_query_arg( [], $wp->request ) );
        }
    }

}
new My_Remove_Post_Type_Slugs;

Modify the Code to fit your needs

Once you add the code to your site you do need to make some edits so that it works. This snippet does not loop through all post types and remove the slugs as this would be inefficient and cause conflicts with 3rd party plugins. So you need to modify the code.

Locate the following:

// CHANGE THIS ARRAY
$this->types = [
    'post_type_1_name' => 'post_type_1_slug',
];

This array needs to be modified to include the post types you want to modify. Notice how you must define the post type name as the key and the post type slug as the value. While the default slug for custom post types in WordPress is the post type name this may not always be the case. So, in order for the code to work correctly it needs to know the name and slug of each post type.

Example:

Lets say you have the following custom post types on your site “cities, states, countries” with the following slugs respectfully “city, state, country”, your array will look like this:

$this->types = [
    'cities'    => 'city',
    'states'    => 'state',
    'countries' => 'country',
];

How does the Code Work?

This class has 3 main parts.

  1. First we hook into post_type_link to remove the post type name from the post link which is the link returned by get_permaink().
  2. Second we hook into pre_get_posts to make sure the post content will display. By default only posts and pages are allowed to have URLs without the cpt slug so we need to include our post types into the post_type argument.
  3. Last we hook into template_redirect in order to redirect the post type URLs that contain the post type name slug. This prevents potential 404 errors or duplicate content issues.

Important Notes

In order for the code to work correctly there are a couple things you need to keep in mind.

Re-save your permalinks: After you add the code to your site and modify it to include your post types, you need to go to Settings > Reading and re-save your permalink settings otherwise nothing will work.

Only change the URLs if needed: It’s generally better to keep the post type slug in the URL to prevent conflicts and it can even help with site structure and SEO. Only remove them if you absolutely need to.

2 Comments

  1. Nathan

    Hi there,

    I have tested your solution and it works perfectly but as soon as I want to add to update my permalink structure to `/blog/%postname%/` in Settings > Permalink Structure > Custom Structure, I land on a 404 when trying to reach the CPT urls.
    Do you have a workaround for this custom permalink structure?

    Thanks a lot 🙂

    • AJ Clarke

      Make sure when you register your custom post type that you have set the “with_front” sub-parameter of the “rewrite” parameter to false. You probably don’t want the blog slug added to your custom post type anyway. The default value of this parameter is true, so it must be set to false. If you are using my Post Types Unlimited plugin to add your post type (you should!) search for the “With Front” field (near the bottom of the settings) and disable it.

Leave a Reply

Your email address will not be published. Required fields are marked *

Learn how your comment data is processed by viewing our privacy policy here.