Steve Taylor photo

Intercept WordPress 404s

So, I’ve got a WordPress site with a bunch of custom post types. On the archive pages for those posts, I have a taxonomy drop-down for a custom taxonomy called “theme” (I know, a slightly confusing label to use in WordPress, but that’s what it has to be). The idea is that you select the theme and the archive listing is filtered accordingly.

Now, for creating the drop-down I use get_terms. This has a hide_empty argument, which will omit terms that aren’t being used. The problem is, my “theme” taxonomy applies to all custom post types. So if a term is applied to any post anywhere, it is included, even if it’s not used at all for the post type being viewed, it appears in the drop-down. When you select it, you get a 404, because the request returns no posts.

How can I stay on the archive template and return a polite “There are no posts here for this theme”?

The taxonomy filter

First, you might be interested in the filtering technique I’m using for the taxonomy. It’s a pretty simple use of the pre_get_posts hook:

if ( ! is_admin() )
	add_filter( 'pre_get_posts', 'slt_archive_listing_filters' );
function slt_archive_listing_filters( $query ) {
	// Theme filtering
	if ( is_archive() && isset( $_REQUEST['locality_theme'] ) ) {
		$query->set( 'tax_query',  array( array(
			'taxonomy'	=> 'theme',
			'field'		=> 'id',
			'terms'	=> $_REQUEST['locality_theme']
	return $query;

First, make sure we’re not in the admin area—we don’t need to filter anything out there.

Then, if we’re on an archive page and the request variable is set that the drop-down is submitting, we append the taxonomy query to the query being performed.

The 404 interception

So, what if it returns no posts? Here we need to use the template_redirect hook:

add_action( 'template_redirect', 'slt_theme_filter_404', 0 );
function slt_theme_filter_404() {
	global $wp_query, $post;
	if ( $wp_query->post_count == 0 && $wp_query->query_vars['taxonomy'] == 'theme' ) {
		$wp_query->is_404 = false;
		$wp_query->is_archive = true;
		$wp_query->is_post_type_archive = true;
		$post = new stdClass();
		$post->post_type = $wp_query->query['post_type'];

OK, it’s really hacky. If there’s no posts (I had problems using the is_404() test) and there’s a “theme” taxonomy query being performed, we just dive in and force the $wp_query variables appropriately.

The hackiest bit is at the end—I’m sure it could be done better, but this was all I could come up with. For some reason (and I can’t say right here whether it’s to do with core functionality or my own modifications), the request needed get_post_type() to return the right type for the archive—and when no posts were returned, the $post object was empty. This bit at the end fakes it.

Hopefully these issue will be ironed out in a future WP release, but for now, hopefully these snippets will help someone else out.

UPDATE 12/8/12: Using the above $wp_query->is_404 = false; for something similar, I’ve noticed that the HTTP status is still set to 404. To override this, add status_header( '200' ); as well. See also this Gist.


  1. Hey, I just wanted to let you know I used your 404 intercept. If you look at the site I’m working on – when you search for an event on the Events page by month/year (May 2013 for example) if there are no results it was previously going to the 404, which is actually the homepage, for various reasons. I was having a hell of a time with it until I modded your intercept code. It worked perfectly! I just changed it to work for my custom post type instead of taxonomy.

    I wish WP would let us do custom 404s or search result pages within a hierarchy like we can with page and single, etc.

    Anyway, thanks for this!!!

  2. Hi Steve,

    The wrapped with SyntaxHighlighter contains HTML entities for the ampersand symbol. To fix it

    1. Go to “Your Profile” and disable the visual editor.
    2. Open Settings > Writing and uncheck “correct invalidly nested ……” and the “convert ….” option below it.
    3. Now open this article in the editor, CUT (don’t copy) and paste this piece of code to your notepad, edit the ampersand, add shortcode tags in the notepad itself.
    4. Copy this code and paste it in this article and UPDATE it do NOT preview it.

    This is how I got it working :-)

  3. but I’ve already got all those settings

    You do not have to permanently keep those settings that way. They should be disabled only when adding/editing SyntaxHighlighter shortcodes.

    After doing the first two steps edit this article and try what I’ve said in step 3 and 4 you’ll definitely get the ampersands properly.

  4. Filipe avatar Filipe

    Great! I was looking for something like this a long time ago!
    Despite it’s a ‘little bit hacky’, it does work without any problems and no side effect.

    Thank you.

  5. Lucian avatar Lucian

    Excellent work, thanks. Time functions (get_the_time, get_the_date) apparently no longer return anything after you run this, any idea what may be the issue?

  6. Steve Taylor avatar Steve Taylor

    @Lucian, not sure. I called it “really hacky” 5 years ago so it might be better to search for more recent / better approaches! There’s all sorts of issues: I could be using standard query vars for the filtering, and it may be possible these days to produce a drop-down that hides empty terms for the post type in question. And even if you end up needing to force a 404, there may be better ways of doing it. Thing is, what would you need get_the_time on a 404 for anyway?

  7. Lucian avatar Lucian

    My day archives are chained with rel/prev and this is a sort of fallback in case a day ends up empty, to still keep the chain intact until things are sorted out (a post forth or backdated to the empty day etc).

    I need the time to generate urls to next/prev links.

    Got it done by rebuilding the time from query args

Comments are closed.