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 '15 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 '15 at 9:52

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

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
  • There are no posts whatsoever which can conflict with my permalink. The permalink is definitely not taken. – Sleuteltje May 3 '15 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 '15 at 9:59
  • 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 '15 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 '15 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 '15 at 8:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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