1

My blog archive URLs were like:

http://example.com/2014/03

I changed my site's permalink from /%postname%/ to /%postid%/%postname%/, and using Redirection plugin, I set a redirection like: redirection 1

and it's working. But the problem occurs in the archive URLs. The new archive URL generated with a /date/ basename before the dates, like:

http://example.com/date/2014/03

I'm trying to use the same Redirection plugin to redirect the URL, but failed: redirection 2

I then tried writing .htaccess on my own (with no .htaccess writing experience) with:

Redirect permanent http://example.com/([0-9]+)/([0-9]+) http://example.com/date/([0-9]+)/([0-9]+)

with the help of htaccess editor and this blog. But failed too.

How can I let my blog not to get 404 on such archive URL? I'm afraid, I'm dumb with rewrite rule till now. :(

2

I'm working on a plugin that handle things like that. While it's in development I want to share some code derived form my plugin that can help you, I hope.

If you look at your sources urls, they are easily to be recognize: you have 2 cases in which you need to redirect:

  1. the url contain 2 pieces and both are numeric, first of 4 digits, second is between 1 and 12
  2. the url contain one piece and it is a valid post slug

Looking at pieces count is very easy, is a matter of a count() and a explode(), looking at valid post slug needs to be queried in database.

Workflow

  1. Get the url
  2. Explode the url pieces and count them: if count isn't 1 or 2 do nothing
  3. Checking url pieces and apply redirect if needed:
    • if count is 1 check if it a valid post slug and if so redirect
    • if count is 2, check if pieces are like /%year%/%monthnum% and if so redirect

The number #3 can be improved in performance using transient-based cache.

1. Get the url

Let's write a function that get the url pieces. It must strips the home_url() part from urls.

function get_url_pieces() {
  $home_path = trim( parse_url( home_url(), PHP_URL_PATH ), '/' );
  $full = trim( str_replace( $home_path, '', add_query_arg( array() ) ), '/' );
  $sane_array = explode( '?', $full );
  // removing any query string
  $qs = array();
  if ( isset( $sane_array[ 1 ] ) ) parse_str( $sane_array[ 1 ], $qs );
  $stripped = trim( $sane_array[ 0 ], '/\\' );
  $pieces = ! empty( $stripped ) ? array_filter( explode( '/', $stripped ) ) : array();
  return $pieces;
}

2. Explode the url pieces and count them: if count isn't 1 or 2 do nothing

This part of workflow should be run as easy as possible, 'after_setup_theme' is a good place, because is available for both plugins and url and is pretty early.

add_action( 'after_setup_theme', function() {
  if ( is_admin() ) return;
  $url_pieces = get_url_pieces();
  if ( is_array($url_pieces) ) {
    if ( count( $url_pieces ) === 1 ) {
      my_post_redirect( $url_pieces );
    } elseif ( in_array( count($url_pieces), array(2, 3), TRUE ) ) {
      my_date_redirect( $url_pieces );
    }
  }
} );

3. Checking url pieces and apply redirect if needed

First function for 2 pieces urls

function my_date_redirect( Array $pieces ) {
  $cached = redirect_if_cached( $pieces );
  // if we are here url is not cached, see redirect_if_cached() below
  // let's check date format
  if ( ! in_array( count($pieces), array( 2, 3), TRUE ) ) return;
  if ( ! strlen("{$pieces[0]}") === 4 ) return;
  $day = ! isset( $pieces[2] ) ? '01' : $pieces[2];
  if ( ! checkdate ( (int)$pieces[1], (int) $day, (int) $pieces[0] ) ) return;
  // that's a valid date
  // cache and redirect
  $redirect = "date/{$pieces[0]}/{$pieces[1]}";
  if ( isset($pieces[2]) ) $redirect .= "/{$pieces[2]}";
  $cached[ serialize($pieces) ] = $redirect;
  set_transient('my_redirects', $cached);
  wp_safe_redirect( home_url( $redirect ), 301 );
  exit();
}

Second function for 1 pieces urls

function my_post_redirect( Array $pieces ) {
  $cached = redirect_if_cached( $pieces );
  // if we are here url is not cached, see redirect_if_cached() below
  if ( ! count( $pieces ) === 1 ) return;
  global $wpdb;
  $query =  "SELECT ID, post_name FROM {$wpdb->posts} WHERE (post_status = 'publish'";
  $query .= ( is_user_logged_in() ) ? " OR post_status = 'private')" : ')';
  $query .= " AND post_type = 'post' AND post_name = %s";
  $post = $wpdb->get_row( $wpdb->prepare( $query, $pieces[0] ) );
  if ( empty($post) ) return;
  // that's a valid slug
  // cache and redirect
  $redirect = "{$post->ID}/{$pieces[0]}";
  $cached[ serialize( $pieces ) ] = $redirect;
  set_transient( 'my_redirects', $cached );
  wp_safe_redirect( home_url( $redirect ), 301 );
  exit();
}

Implementing cache

function redirect_if_cached ( Array $pieces ) {
  $cached = array_filter( (array) get_transient('my_redirects') );
  $key = serialize( $pieces );
  if ( array_key_exists( $key, $cached ) ) {
    wp_safe_redirect( home_url( $cached[$key] ), 301 );
    exit();
  }
  // if url is not cached return what's currently cached
  return $cached;
}
  • Thanks @G. M. for your patient detailed answer. Mine is a live site. Can I test your code in my site with no change? Is it the exact answer of my question, or an idea on how to solve this? – Mayeenul Islam Apr 6 '14 at 5:02
  • @MayeenulIslam after your comment I've tested my code and found that it worked perfectly for single post and monthly date archives, but not for daily date archives. So I've edited it, and now it works for daily date archives too. However it doesn't work for yearly date archives. Do you want I make it works for yearly date archives too? – gmazzap Apr 6 '14 at 11:14
  • Why not? If so, it'd be a masterpiece for me and for others in future. And why not it'd be a great assistance for me too? I'd be thankful to you for that. Anyways, before implementing the code, I'd like to ask something: What is the PHP_URL_PATH in the first function? – Mayeenul Islam Apr 7 '14 at 4:19

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.