How to add numeric slug for child page in WordPress 5.9?

Updated Answer@Quasimodo confirmed that they’re only interested in posts of the page type and that the numeric slugs are four-digit years, e.g. 2019.

What can I do to allow numeric slugs for child pages?

I would use the pre_wp_unique_post_slug hook that runs prior to wp_unique_post_slug, which means we’ll be filtering the slug before a unique one is generated (by WordPress), and thus our callback must be able to generate a unique slug if necessary.

Here’s an example, tried and tested working with WordPress v5.9.3:

add_filter( 'pre_wp_unique_post_slug', 'my_pre_wp_unique_post_slug', 10, 6 );
function my_pre_wp_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type, $post_parent ) {
    $allowed_post_status = array( 'publish', 'private' ); // allows numerical slug only for these statuses

    if ( preg_match( '/^\d{4}$/', $slug ) && $post_parent &&
        'page' === $post_type && in_array( $post_status, $allowed_post_status )
    ) {
        return my_unique_page_slug( $slug, $post_ID, $post_type );
    }

    return $override_slug;
}

// This function is based on the code in the wp_unique_post_slug() function, lines
// 5023-5054 in WordPress v5.9.3, except that $check_sql doesn't include the `AND post_parent = %d`,
// which means we only want unique slug for each Page (i.e. post of type 'page').
function my_unique_page_slug( $slug, $post_ID, $post_type="page" ) {
    global $wpdb;

    $check_sql       = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type IN ( %s, 'attachment' ) AND ID != %d LIMIT 1";
    $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_ID ) );

    if ( $post_name_check ) {
        $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 ) );
            $suffix++;
        } while ( $post_name_check );
        $slug = $alt_post_name;
    }

    return $slug;
}

PS: Just so that you know, the wp_unique_post_slug hook could be used, but your callback would need to return or work on the value of $original_slug (last parameter from the hook) instead of $slug (first parameter from the hook).

So the above example would now let us use a number as the slug for child Pages (post type page), however, we still need to ensure the URL/permalink works as expected — because by default, example.com/page-slug/<number> is treated as a paginated request and thus WordPress will attempt to load page 2, 3, etc. of the Page with the slug page-slug.

  • The above also explains why WordPress by default disallows number as a (post) slug.

So step 2 for you would be this — add a custom rewrite rule which ensures the permalink example.com/parent-slug/<number> loads the Page with the slug <number> and is child of parent-slug (or whatever the correct/actual slug is):

add_action( 'init', 'my_add_page_rewrite_rules' );
function my_add_page_rewrite_rules() {
    add_rewrite_rule(
        '^(.+/\d{4})(?:/(\d+))?/?$',
        'index.php?pagename=$matches[1]&page=$matches[2]',
        'top' );
}

Or you can instead use the page_rewrite_rules hook:

add_filter( 'page_rewrite_rules', 'my_page_rewrite_rules' );
function my_page_rewrite_rules( $page_rewrite ) {
    return array(
        '^(.+/\d{4})(?:/(\d+))?/?$' => 'index.php?pagename=$matches[1]&page=$matches[2]',
    ) + $page_rewrite;
}

And remember, there’s a step 3 — flush the rewrite rules by simply visiting wp-admin → Settings → Permalinks.


As for this:

But for some reason, I just get a critical error: “Allowed memory size exhausted”.

That’s because, as @birgire said in his comment, “you’re creating an infinite loop by re-applying the wp_unique_post_slug filter in the end of your callback“.

So whenever wp_unique_post_slug() got called (e.g. by wp_insert_post()),

  1. apply_filters() (in wp_unique_post_slug()) called your closure/callback
  2. Your callback called apply_filters()
  3. apply_filters() (in your callback) called your callback
  4. Steps 2 & 3 were repeated over and over again…

And eventually, PHP threw a fatal/critical error because your code consumed way too much memory.. :/