Implementing a More Useful 404 in CodeIgniter

An oft-overlooked part of any site is the usefulness of its 404 (aka “Page Not Found”) error page. For a user, the 404 is an unexpected barrier to the content they actually wanted, so the least we can do is soften the blow somewhat by providing some useful information to help them get back on track.

What's more, we as developers can make our own lives easier by having the 404 page provide us with some useful information, so that if there is a link that needs fixing somewhere, we can find and fix it with the minimum of fuss.

Dean Frickey provided an excellent solution in his A List Apart article, “A More Useful 404”, detailing the ways in which we can improve on the standard, boilerplate 404 error.

Dean's solution involves first doing a little detective work to work out exactly how the user ended up at the error. Then, we can do two things to make the 404 more useful. Firstly, we can display a more relevant error message to the user, and secondly, we can notify ourselves via email with the details of the problem.

The very first stage of implementing the more useful 404 in CI is to extend the Exceptions class. We're only concerned with the show_404() method here, so your initial extended class will look something like this:

class MY_Exceptions extends CI_Exceptions {

	public function __construct() 
	{
		parent::CI_Exceptions();
	}

	/**
	 * 404 Page Not Found Handler
	 *
	 * @access	private
	 * @param	string
	 * @return	string
	 */
	function show_404($page = '')
	{
		$heading = "404 Page Not Found";
		$message = array('Sorry, the page you requested was not found. ');

		log_message('error', '404 Page Not Found --> '.$page);
		echo $this->show_error($heading, $message, 'error_404', 404);
		exit;
	}

}

The only difference here from the normal show_404() is that we have turned $message into an array. Save that in your application/libraries folder as MY_Exceptions.php and you're good to go.

Now we've extended the standard class, we can start adding functionality. Firstly we'll need to divine the referring site (or lack of) – this is easily accomplished with a couple of lines of code:

if(isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] != '')
{
	// broken link somewhere, so send an email
	// and display the right info to the user.

	$referer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
}

Notice that we can't use the handy CI functions here, as at this point the system isn't fully initialised. Luckily, we only need a few native PHP functions, and they are pretty straightforward.

The parse_url() function enables us to just get the part of the URL we're interested in, so in this case we're stripping away everything except the TLD itself. This avoids confusion if, for example, the referring page has a url like "http://www.example.com/article/something_about_google" - we don't want our system to see the word "google" and decide that the source is a search engine.

We'll deal with cases where the user has come direct later. Now we have the referring site, let's try and work out what kind of link the user came from. The first check is to see if it's an internal link. This is easy to check for, just see if your domain name appears anywhere in the referring site's URL:

if(strpos($referer, '[your_domain_here]') !== FALSE) // is it a broken internal link?
{
	$message[0] .= 'It looks like we have a broken link on the site - we have been notified and we\'ll get it fixed as soon as possible.';
}

This feels a bit of a kludge because you actually have to type in your own URL, as we can't yet access the config variables at this point. It's only a slight hack to work around the problem though, and shouldn't be a deal-breaker in most cases.

Next, we want to see if the referrer is a search engine. The easiest way to do this is to set up an array with a list of search engines we want to check against. Now, there's no way we could ever hope to cover every single search engine out there, but if we include the most common ones in our list, we should be able to account for between 98-99% of searches, which isn't bad going.

Once the list is set up, we can then go through it and see if our referring URL is one of our chosen search engines. If the referrer isn't internal, and isn't a search engine, we'll lump it into the generic category of “external sites”:

// search engines to look out for, should account for around 98-99% of searches
// (source: http://www.hitwise.com/us/datacenter/main/dashboard-10133.html )
$search_engines = array(
	'www.google.',
	'search.yahoo.',
	'bing.',
	'ask.',
	'alltheweb.',
	'altavista.',
	'search.aol',
	'baidu.'
);

if(strpos($referer, '[your_domain_here]') !== FALSE) // is it a broken internal link?
{
	…
}
else
{				
	$source_text = 'another site';
	
	foreach($search_engines as $search_engine)
	{
		if(strpos($referer, $search_engine) !== FALSE) // bad search engine result?
		{
			$source_text = 'a search engine';						
			break; // no point continuing to loop once we have found a match
		}
	}
	
	$message[0] .= 'It looks like you came from ' . $source_text . ' with a broken link - we have been notified and we\'ll get it fixed as soon as possible.';
}

That's the detective work done. Notice that each time we are adding to $message[0]. Each array item in $message will be rendered as a paragraph, so appending to $message[0] means our text appears in the first paragraph. You can add other paragraphs as easily as adding new array items, like this:

$message[] = 'In the meantime, why not <a href="/about">find out more about us</a>, <a href="/blog">have a read of our blog</a>, or <a href="/portfolio">check out our portfolio</a>? You\'re more than welcome to <a href="/contact">drop us a line</a>, too.';

Once that's done, we want the system to notify us of the error via email:

// send email notifying admin of broken link - have to use native function
// as the CI system is not yet instantiated :(
$to = 'errors@your.domain';
$subject = 'Broken link';
$email_text = 'Broken link at: ' . $_SERVER['HTTP_REFERER'];
$headers = 'From: system@your.domain' . "\r\n" .
	'Reply-To: system@your.domain' . "\r\n" .
	'X-Mailer: PHP/' . phpversion();

mail($to, $subject, $email_text, $headers);

The last piece of extra functionality is to handle the case where users have come direct:

if(isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] != '')
{
	…
}
else // no referer, so probably came direct
{
	$message[0] .= 'It looks like you came directly to this page, either by typing the URL or from a bookmark. Please make sure the address you have typed or bookmarked is correct - if it is, then unfortunately the page is no longer available.';
	$message[] = 'But all is not lost - why not <a href="/about">find out more about us, <a href="/blog">have a read of our blog, or <a href="/portfolio">check out our portfolio? You\'re more than welcome to <a href="/contact">drop us a line, too.';

}

That's it for the MY_Exceptions class. The last few lines are the same as standard, just to log and render the error. The final code should look like this.

Last but by no means least, if you haven't already created a custom application/errors/error_404.php (and why not?!), you can create a complete custom page which fits with your main design, rather than the standard CI one. Just remember to echo $message at some point!

Tags: CodeIgniter PHP Tutorial Usability

Found this post useful or interesting? Please share it with your friends!

Feedback

Sorry, feedback is now closed on this post, but please feel free to get in touch if you would like to talk about it!

  1. goldkante on

    elseif(strpos($referer, $search_engines) !== FALSE) // bad search engine result?
    {
        $message[0] .= 'It looks like you came from a search engine with a broken link - we have been notified and we\'ll get it fixed as soon as possible.';
    }

    this code won’t work since strpos doesn’t accept an array as haystack…

  2. Kris on

    Good spot, goldkante - have corrected it now. Not quite as elegant, but definitely works!

  3. Pingback: Finding the HTTP Referrer in CodeIgniter - Simian Studios - Bespoke Web Design & Development on 14th July 2010 at 2:20pm

    traffic hitting my post on custom 404 error handling in CodeIgniter recently, looking for

Or find posts by tag →

Friends & Influences

  1. Aching Brain
  2. Andy Budd
  3. Anthony Killeen
  4. Ben Everard
  5. Cameron Moll
  6. Dan Cederholm
  7. Dan Mall
  8. Dave Shea
  9. Elliot Jay Stocks
  10. Jamie Knight
  11. Jamie Rumbelow
  12. Jason Santa Maria
  13. Jeff Croft
  14. Jeffrey Zeldman
  15. Jeremy Keith
  16. Jon Hicks
  17. Khoi Vinh
  18. Mark Boulton
  19. Matt Croucher
  20. Nocturnal Monkey
  21. Sarah Parmenter
  22. Shaun Inman
  23. Simon Collison
  24. Tim Van Damme
  25. Toby Howarth