How to Change the Slug of a Custom Post Type in WordPress

Quick Summary
- Set the slug in the post type's rewrite argument, or change it through the register_post_type_args filter when a plugin or parent theme registers the post type.
- Open Settings > Permalinks once afterwards: the screen rebuilds the rewrite rules when it loads, and until then the new slug returns 404 errors.
- WordPress only sometimes guesses the new address for an old URL, so add 301 redirects from every old URL pattern to the new slug.
The URL slug of a custom post type comes from the slug key of its rewrite argument. If you register the post type yourself, change it there: 'rewrite' => array( 'slug' => 'comand' ). If a plugin or parent theme registers it, change it with the register_post_type_args filter instead of editing the plugin. Either way, open Settings > Permalinks once afterwards so WordPress rebuilds its URL rules, and redirect the old URLs.
How do you change the slug of a post type a plugin registers?
WordPress passes the arguments of every post type through the register_post_type_args filter just before it registers the type. Add this to your child theme’s functions.php file or to a small custom plugin:
// Change the slug of a custom post type registered by a plugin or parent theme.
add_filter( 'register_post_type_args', 'comand_register_post_type_args', 10, 2 );
function comand_register_post_type_args( $args, $post_type ) {
if ( 'command' === $post_type ) {
if ( ! isset( $args['rewrite'] ) || ! is_array( $args['rewrite'] ) ) {
$args['rewrite'] = array();
}
$args['rewrite']['slug'] = 'comand';
}
return $args;
}- The function receives the post type’s arguments (
$args) and its key ($post_type). Theifcheck makes sure only one post type changes. - Replace
'command'with the key of your post type and'comand'with the slug you want in the URL. Single posts then live at addresses like/comand/post-name/. - The filter sees the arguments exactly as the plugin passed them, before WordPress fills in its defaults. So
rewritecan be missing,true,falseor an array. Theis_array()check turns anything that isn’t an array into one first. Without it, a plugin that uses'rewrite' => truemakes theslugline fail on PHP 8 with “Cannot use a scalar value as an array”. - Only the
slugkey changes. Other rewrite settings the plugin set, such as'with_front' => false, stay as they were. If the plugin sets'rewrite' => falseon purpose, this code turns pretty URLs on for the post type. - If you rename the function, change the name in
add_filter()too.
On WordPress 6.0 and later you can also use the filter named after one post type, register_{post_type}_post_type_args. It only runs for that type, so it needs no if check:
add_filter( 'register_command_post_type_args', 'comand_command_post_type_args' );
function comand_command_post_type_args( $args ) {
if ( ! isset( $args['rewrite'] ) || ! is_array( $args['rewrite'] ) ) {
$args['rewrite'] = array();
}
$args['rewrite']['slug'] = 'comand';
return $args;
}Both filters must be added before the plugin registers its post type, which usually happens on the init hook. Code at the top level of functions.php or of a plugin is early enough; the same code inside an init callback that runs after the plugin’s is too late.
Why does the new slug return a 404 error?
WordPress stores its URL rules in the database and only rebuilds them when they are flushed. Until then, the new slug has no rule and returns “Page not found”. Open Settings > Permalinks in the dashboard: that screen calls flush_rewrite_rules() every time it loads, so the rules are rebuilt without changing any setting. Clicking Save Changes does the same. It’s the same fix as for custom post type pages that return a 404 right after the post type is registered.
Don’t put flush_rewrite_rules() in code that runs on every request: the function reference calls it an expensive operation. In your own plugin, flush once in the activation hook, after registering the post type. For a theme or site plugin that you deploy to several environments, a version number that triggers one flush per slug change, as in how I structure a WordPress project, saves opening the Permalinks screen on each one.
Do the old URLs redirect to the new slug?
Only sometimes, so add redirects yourself. When a request ends in a 404, WordPress’s redirect_canonical() tries to guess the right post: it looks for a published post whose slug starts with the name in the old address and sends a 301 redirect to it. With the /%postname%/ permalink structure, that guess sends /command/post-name/ to /comand/post-name/. But:
- It depends on the permalink structure. With
/blog/%postname%/, for example, WordPress doesn’t find a post name in the old address, so nothing is guessed. - It only ever redirects to a single post, so the old archive and its pagination, such as
/command/and/command/page/2/, never reach the new archive. - It matches the start of a slug only, so it can pick an unrelated post with a similar name.
This snippet sends every old address under the old slug to the same path under the new one, with a permanent 301 redirect:
// Send old /command/... addresses to the new /comand/... slug.
add_action( 'template_redirect', 'comand_redirect_old_slug', 9 );
function comand_redirect_old_slug() {
global $wp;
if ( ! is_404() ) {
return;
}
if ( preg_match( '#^command(/.*)?$#', $wp->request, $m ) ) {
$rest = isset( $m[1] ) ? $m[1] : '';
wp_safe_redirect( home_url( user_trailingslashit( 'comand' . $rest ) ), 301 );
exit;
}
}- It only acts on requests that would return a 404, so nothing that still exists at an old address is redirected away.
$wp->requestis the requested path without the domain and query string, for examplecommand/post-name. The pattern keeps everything after the old slug, so/command/page/2/goes to/comand/page/2/, while an address like/commander/isn’t touched. Query strings are not carried over.- Priority 9 runs it before
redirect_canonical(), which runs at the default priority 10, so WordPress doesn’t guess first.
A redirect plugin or a rule in your server configuration does the same job; what matters is that every old URL pattern gets a 301.
Common problems
- Nothing changes. The
ifcompares the post type key, the first argument ofregister_post_type(), not its label or its old slug. The key appears afterpost_type=in the address bar when you open the post type’s list screen in the admin. Also check that the rewrite rules were rebuilt. - The URL starts with /blog/ or another prefix. If your permalink structure starts with a fixed part, such as
/blog/%postname%/, post type URLs get it too, becausewith_frontdefaults totrue. Set$args['rewrite']['with_front'] = false;in the same function to drop it. - Old links in content and menus. Links typed into post content or added to menus as custom links keep the old address. The redirect catches them, but update the ones you control.
Frequently asked questions
Does the archive URL change too?
Yes, if the post type is registered with 'has_archive' => true, because the archive then uses the rewrite slug. If has_archive is set to its own string, the archive keeps that address; change $args['has_archive'] in the same function to move it.
Can I rename the post type key instead?
Not safely. Every post is saved with its post type key in the database, so after a key change the existing posts no longer appear under the post type until they are updated too. Changing the rewrite slug only changes the URLs.
Related articles
- registering a custom post type: where the rewrite argument is set in the first place
- custom post type single pages that return 404: flushing rules and the other causes

