Welling Guzman

Control WordPress 404 Error

I have site built with Wordpress and I wanted to catch all 404 page that Wordpress try to display and make my own custom 404 page depending on the requested page, or in fact I just wanted to whether or not show a 404 page.

If the user, let's say, go to http://mysite.com/someword on mysite, and if that is not a post/page it would probably display a 404 page, but I didn't want that behavior I wanted to search if someword is a category slug or a tag and display posts in that category or tag, otherwise would show the normal 404 page.

So, this is what I did, I added a action to template_redirect hook, that is executed before WordPress determine which page is going to be loaded.

<?php
add_action('template_redirect', '_custom_redirect');
function _custom_redirect()
{
  global $wp_query;

  if ( $wp_query->is_404() )
  {
    // Here I take over 404 page
  }
}

So now, I know when it's a 404 page, I need to get the string it was passed on the url, in this case someword, so I can check if the string match a category or a tag name with that name.

This string is stored in a array named query_vars with the key name. query_vars is a variable member of WP_Query which WordPress uses to execute the main query.

<?php
add_action('template_redirect', '_custom_redirect');
function _custom_redirect()
{
  global $wp_query;

  if ( $wp_query->is_404() )
  {
    $page_name = $wp_query->query_vars['name'];

    if ( ! $page_name )
      return;

    if ( ($category =  get_category_by_slug( $page_name )) )
    {
      // Here it found a category
    }
    elseif ( ($tag = get_term_by('slug', $page_name, 'post_tag') ) )
    {
      // Here it found a tag
    }
    else
    {
      // Is not a category or a tag
      return;
    }
  }
}

After this code WordPress would still "believe" it's a 404 page, I needed to add some more lines to changed this.

<?php
add_action('template_redirect', '_custom_redirect');
function _custom_redirect()
{
  global $wp_query;

  if ( $wp_query->is_404() )
  {
    $page_name = $wp_query->query_vars['name'];

    if ( ! $page_name )
      return;

    if ( ($category =  get_category_by_slug( $page_name )) )
    {
      // Here it found a category
      $wp_query->is_category = true;
    }
    elseif ( ($tag = get_term_by('slug', $page_name, 'post_tag') ) )
    {
      // Here it found a tag
      $wp_query->is_tag = true;
    }
    else
    {
      // Is not a category or a tag
      return;
    }

    $wp_query->is_404 = false;
    status_header( 200 );
  }
}

This is pretty much what I wanted to do, now I can get a tag or a category object if one exists, otherwise WordPress will keep its own process and will display its normal 404 page.

If I actually got a tag or category object, I need to query all posts under it.

<?php
add_action('template_redirect', '_custom_redirect');
function _custom_redirect()
{
  global $wp_query;

  if ( $wp_query->is_404() )
  {
    $page_name = $wp_query->query_vars['name'];

    if ( ! $page_name )
      return;

    $query_args = '';
    if ( ($category =  get_category_by_slug( $page_name )) )
    {
      // Here we found a category
      $wp_query->is_category = true;
      $query_args = 'category_name=' . $category->slug;
    }
    elseif ( ($tag = get_term_by('slug', $page_name, 'post_tag') ) )
    {
      // Here we found a tag
      $wp_query->is_tag = true;
      $query_args = 'tag=' . $tag->slug;
    }
    else
    {
      // Is not a category or a tag
      return;
    }

    $wp_query->is_404 = false;
    $wp_query->is_archive = true;
    status_header( 200 );

    query_posts( $query_args );
  }
}

If status_header( 200 ); isn't added the HTTP status will always be a 404, so this line change the status code from 404 to 200.

This is about it, but I want a little bit more, I want if is_category(); or is_tag(); functions are used, it has to return true. In order to make this to happen I needed to set a couple of variables more.

<?php
$wp_query->set('category_name', $category->slug);
$wp_query->set('cat', $category->term_id);

$wp_query->set('tag', $tag->slug);
$wp_query->set('tag_id', $tag->term_id);

$wp_query->set() will set a variable to query_vars array, which as I mentioned before is used by WordPress main query. With this variables set, when WordPress executes is_category(); or is_tag(); would have category or tag specific variable values to check if is a category/tag or not.

Final code

<?php
add_action('template_redirect', '_custom_redirect');
function _custom_redirect()
{
  global $wp_query;

  if ( $wp_query->is_404() )
  {
    $page_name = $wp_query->query_vars['name'];

    if ( ! $page_name )
      return;

    $query_args = '';
    if ( ($category =  get_category_by_slug( $page_name )) )
    {
      // Here we found a category
      $wp_query->is_category = true;
      $wp_query->set('category_name', $category->slug);
      $wp_query->set('cat', $category->term_id);

      $query_args = 'category_name=' . $category->slug;
    }
    elseif ( ($tag = get_term_by('slug', $page_name, 'post_tag') ) )
    {
      // Here we found a tag
      $wp_query->is_tag = true;
      $wp_query->set('tag', $tag->slug);
      $wp_query->set('tag_id', $tag->term_id);

      $query_args = 'tag=' . $tag->slug;
    }
    else
    {
      // Is not a category or a tag
      return;
    }

    $wp_query->is_404 = false;
    $wp_query->is_archive = true;
    status_header( 200 );

    query_posts( $query_args );
  }
}

This would be helpful too if you want to log/register/email requested pages that ends up being a 404 page.

NOTE: if you don't have any category or tag template page will returns a 404 page template, just take that in mind.