I understand there are 2 different types when it comes to a custom post type: -page -post.

Where page supports child pages, and post does not.

However, when i try to make a child page with only numbers in it's slug, wordpress puts -2 behind it. Example:


instead of what i would have wanted:


Why is this, and how can i solve it? I have searched for hours, but i can't seem to find a solution, other than it's perhaps a limitation in Wordpress, do to a conflict with it's date permalink system. I'm not using this system, but could this be true?

EDIT, some more information: There are no posts whatsoever which can conflict with my permalink. The permalink is definitely not taken.

I get this behaviour with a completely new install of wordpress, and only 1 custom post type. The only posts in the wordpress database are 'parent' and '02'. Where '02' turns into '02-2'.

I was wondering if maybe pagination /slug/page/02 was maybe the reason numeric slugs were not accepted?

It is important to note, I only get this with numeric slugs, /parent/child/ is not a problem.

I've seen something about overriding the filters, but won't that simply hide the problem? I prefer to solve it.

The code I use to register my custom post type:

    $labels = array(
    'name'                => _x( $options['euthus_posttype_meervoud'], 'Post Type General Name', 'text_domain' ),
    'singular_name'       => _x( $options['euthus_posttype'], 'Post Type Singular Name', 'text_domain' ),
    'menu_name'           => __( $options['euthus_posttype_meervoud'], 'text_domain' ),
    'name_admin_bar'      => __( $options['euthus_posttype_meervoud'], 'text_domain' ),
    'parent_item_colon'   => __( 'Parent Item:', 'text_domain' ),
    'all_items'           => __( 'All Items', 'text_domain' ),
    'add_new_item'        => __( 'Add New Item', 'text_domain' ),
    'add_new'             => __( 'Add New', 'text_domain' ),
    'new_item'            => __( 'New Item', 'text_domain' ),
    'edit_item'           => __( 'Edit Item', 'text_domain' ),
    'update_item'         => __( 'Update Item', 'text_domain' ),
    'view_item'           => __( 'View Item', 'text_domain' ),
    'search_items'        => __( 'Search Item', 'text_domain' ),
    'not_found'           => __( 'Not found', 'text_domain' ),
    'not_found_in_trash'  => __( 'Not found in Trash', 'text_domain' ),
$rewrite = array(
    'slug'                => $options['euthus_posttype_baseurl'],
    'with_front'          => true,
    'pages'               => false,
    'feeds'               => true,
$args = array(
    'label'               => __( 'euthus_childs', 'text_domain' ),
    'description'         => __( $options['euthus_posttype_meervoud'], 'text_domain' ),
    'labels'              => $labels,
    'supports'            => array( 'title', 'thumbnail', 'page-attributes',),
    'hierarchical'        => true,
    'public'              => true,
    'show_ui'             => true,
    'show_in_menu'        => true,
    'menu_position'       => 20,
    'menu_icon'           => 'dashicons-networking',
    'show_in_admin_bar'   => true,
    'show_in_nav_menus'   => true,
    'can_export'          => true,
    'has_archive'         => true,
    'exclude_from_search' => false,
    'publicly_queryable'  => true,
    'rewrite'             => $rewrite,
    'capability_type'     => 'page',
register_post_type( 'euthus_childs', $args );

As I understand, the 'capability_type' must be set to page and not post to allow for hierarchical, where setting it to 'post' does not allow this.

  • The '-2' postfix WordPress add usually when a post with this slug already exist. Did you check it?
    – Emetrop
    May 2, 2015 at 17:45
  • Yes, There are no other posts which can conflict with the permalink. Even with a completely new (empty) install, i get this behaviour.
    – Sleuteltje
    May 3, 2015 at 9:52

2 Answers 2


As you guessed and @Rarst suspected, there's a pagination check in wp_unique_post_slug() for hierarchical post types:

preg_match( "@^($wp_rewrite->pagination_base)?\d+$@", $slug )

which will match any numeric only slug, optionally preceded by "page". To get around this you could use the 'wp_unique_post_slug' filter, basically replicating the original code without the pagination check, eg

add_filter( 'wp_unique_post_slug', function ( $slug, $post_ID, $post_status, $post_type, $post_parent, $original_slug ) {
    if ( $slug !== $original_slug && is_post_type_hierarchical( $post_type ) ) {

        global $wpdb, $wp_rewrite;

        $slug = $original_slug; // Undo any previous processing.

        // The following is just a copy & paste of the WP code without the pagination check.
        $feeds = $wp_rewrite->feeds;
        if ( ! is_array( $feeds ) )
            $feeds = array();

        if ( 'nav_menu_item' == $post_type )
            return $slug;

         * Page slugs must be unique within their own trees. Pages are in a separate
         * namespace than posts so page slugs are allowed to overlap post slugs.
        $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type IN ( %s, 'attachment' ) AND ID != %d AND post_parent = %d LIMIT 1";
        $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_ID, $post_parent ) );

         * Filter whether the post slug would make a bad hierarchical post slug.
         * @since 3.1.0
         * @param bool   $bad_slug    Whether the post slug would be bad in a hierarchical post context.
         * @param string $slug        The post slug.
         * @param string $post_type   Post type.
         * @param int    $post_parent Post parent ID.
        if ( $post_name_check || in_array( $slug, $feeds ) || apply_filters( 'wp_unique_post_slug_is_bad_hierarchical_slug', false, $slug, $post_type, $post_parent ) ) {
            $suffix = 2;
            do {
                $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
                $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_ID, $post_parent ) );
            } while ( $post_name_check );
            $slug = $alt_post_name;
    return $slug;
}, 10, 6 );
  • Can you update your answer to work with WordPress 5.9? I tried remove the preg_match from the current code of wp_unique_post_slug, but the resulting hook leads to errors.
    – Quasimodo
    Mar 17, 2022 at 16:03

It would be more correct to say that CPTs can be hierarchical and non–hierarchical. Page and post are just respective examples of such, and by the way as native post types they aren't quite the same thing as CPTs.

Clearly, when you have multiple CPTs in a site it's important that not a single combination of slugs leads to ambiguous permalink, that might refer to more than one post.

When WP generates post slugs wp_unique_post_slug() checks and modifies slug as necessary to achieve that.

It is hard to guess with certainty why your specific slug gets modified, without seeing rest of the data.

In a nutshell:

  • WP considers it to be insufficiently unique
  • there are filters that allow you to override this behavior
  • however enforcing non–unique slug might explode in interesting ways
  • 1
    There are no posts whatsoever which can conflict with my permalink. The permalink is definitely not taken.
    – Sleuteltje
    May 3, 2015 at 9:54
  • 2
    As I said — impossible to guess without working on your data. You think it's not taken, WordPress thinks it is.
    – Rarst
    May 3, 2015 at 9:59
  • 1
    Yes, I understand Wordpress thinks it is. I'm trying to understand where this is coming from, and how to solve it :). I don't understand where the conflict in permalink is coming from, since it's a fresh Wordpress Install, with no other posts in the database. See my updated question for the code which i use to register the custom post type.
    – Sleuteltje
    May 3, 2015 at 10:06
  • 1
    Your guess about pagination is right - the culprit is the test preg_match( "@^($wp_rewrite->pagination_base)?\d+$@", $slug ) which (for hierarchical post types only) in the function cited by Rarst will match any numeric only slug, optionally preceded by "page". See also core.trac.wordpress.org/ticket/14238.
    – bonger
    May 7, 2015 at 2:14
  • @bonger that should be its own answer :) I suspected that bit, but it was late night for me and I hadn't got to testing it
    – Rarst
    May 7, 2015 at 8:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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