Why can posts never have a number as the link?

Why Suffixes Are Appended to (Some) Numeric Post-Slugs

WordPress posts can almost always have numeric permalinks, the exceptions being when they might conflict with another post, a feed, or a post post-type date archive, or are otherwise reserved. The wp_unique_post_slug() function mitigates these conflicts by appending a suffix.

Potential date-archive conflicts arise when a numeric %postname% meets one of three conditions:

/*
* Potential date clashes are as follows:
*
* - Any integer in the first permastruct position could be a year.
* - An integer between 1 and 12 that follows 'year' conflicts with 'monthnum'.
* - An integer between 1 and 31 that follows 'monthnum' conflicts with 'day'.
*/

So, when using /%postname%/ alone as a permalink structure, numeric post-names will always result in appended slugs as WordPress considers them to represent a year archive – this is one of a good handful of reasons to use more robust permalink structures.

You could argue that WordPress shouldn’t need to account for the numbers from 0 to the year that the blog was started, or perhaps even hundreds or millions of years for now. However, in accounting for such unlikely scenarios, the code makes no assumptions regarding environment or configuration (a user could configure their system as though the year were ‘1’, or publish posts with a date many thousands of years from now to reinforce fictitious lore), which is an arguably more responsible approach.


Solution

As of 4.3.1, WordPress’s behavior would remain unchanged were a “base” somehow prepended to date-based post archives as one can do for custom post-types. So the only direct solution is to use something other than /%postname%/ for your permalink structure (for example, a static non-numeric prefix: /blog/%postname%/. Or adding the post’s category: /%category%/%postname%/).

Alternately, manually adding any non-numeric, URL-friendly character to each post’s slug will prevent a suffix from being added.


Possible Hacks/Work-Arounds

All other solutions get somewhat “hacky” in that they no longer leverage WordPress’s core behaviors to solve the problem but rather try to work around them by applying tools in unusual and often convoluted ways.

For instance, you could possibly use a 'wp_unique_post_slug' filter to ignore unlikely conflicts:

// Make some "reasonable" assumptions about the dates of this blog's content.
// These assume standard date & time settings for the host.
define( 'REASONABLE_YEAR_ARCHIVE_LOWER', 2003 );  // Unlikely that this blog published content before WP's initial release.
define( 'REASONABLE_YEAR_ARCHIVE_UPPER', 3000 );  // Probably won't publish content after/with the year 3000.

add_filter( 'wp_unique_post_slug', 'wpse_210487_reasonably_unique_post_slug', 10, 6 );

/**
 * If WordPress modified a post-slug to avoid conflicts with date archives when
 * %postname% is the first permastruct, undo the modification if the slug is an
 * "unreasonable" year. NOTE: this is a hack, and has a high potential for
 * introducing new bugs, particularly after the year
 * REASONABLE_YEAR_ARCHIVE_UPPER.
 */
function wpse_210487_reasonably_unique_post_slug( $slug, $post_ID, $post_status, $post_type, $post_parent, $original_slug ) {
  global $wpdb, $wp_rewrite;

  $permastructs   = array_values( array_filter( explode( "https://wordpress.stackexchange.com/", get_option( 'permalink_structure' ) ) ) );

  // Don't re-process the slug if:
  if( 'post' !== $post_type                              // - the item is not a 'post'
    || $original_slug === $slug                          // - the item's slug was not altered to resolve a conflict
    || 0 !== array_search( '%postname%', $permastructs ) // - the permalink structure does not start with %postname%
    || ! preg_match( '/^[0-9]+$/', $slug )               // - the slug is not strictly numeric
    )
    return $slug;

  // If the original slug conflicted with another post, use the WordPress-altered slug
  $check_sql       = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1";
  $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $original_slug, $post_type, $post_ID ) );
  if( $post_name_check )
    return $slug;

  // If the original slug conflicted with a feed, use the WordPress-altered slug
  if ( is_array( $wp_rewrite->feeds ) && in_array( $original_slug, $wp_rewrite->feeds ) )
    return $slug;

  // If the original slug was otherwise reserved, use the WordPress-altered slug
  if( apply_filters( 'wp_unique_post_slug_is_bad_flat_slug', false, $original_slug, $post_type ) )
    return $slug;

  // Otherwise, if the numeric slug will never reasonably conflict with a year archive, undo WordPress's slug modification
  $slug_num = intval( $original_slug );
  if( REASONABLE_YEAR_ARCHIVE_LOWER > $slug_num || REASONABLE_YEAR_ARCHIVE_UPPER < $slug_num )
    return $original_slug;

  return $slug;
}

Note that I have not tested the above code and strongly discourage it’s use. It could well be that even in applying the intended slug, it still gets directed to the date-based archive.

There may also be other (probably also hacky) solutions to be found in applying the Rewrite API.

Leave a Comment