A Better 404 Page

Advertisement

A lot of funny 404 pages have been shared recently: carefully crafted memes, funny GIFs, even the odd interactive game. But if the 404 doesn’t help your visitors, then what’s the point?

A visitor could find themselves on a 404 page for one of many reasons: a mistyped address, a bad link from somewhere else, a deleted page or content that has moved elsewhere. While you can prevent errors from moved pages with redirects, you can’t control people’s mistakes.

Being Helpful

People who land on your website are looking for its content, usually via a link. They would have clicked that link expecting one thing, so why show them a hand-drawn panda? Instead, your 404 page should get them to where they need to be.

This problem isn’t new, and we’ve seen a lot of ideas on how to handle it. Showing a search form or linking to the home page is reasonable. Yet those are passive solutions that don’t solve the visitor’s problem. A more direct approach would be to guess where the visitor intended to go and suggest that page.

Suggesting The Right Page

One way to suggest the right page is to search for it yourself and present the result. Luckily, we don’t have to write a search engine to do this (although, if you have one handy, good for you!). Instead, we can use Google’s Custom Search API1.

We can use Google’s Custom Search API to suggest the right page.
We can use Google’s Custom Search API to suggest the right page.

Google’s Custom Search API is a tool for searching within an individual website. When set up, it enables you to retrieve what it considers to be the best match from your website. It does need a search phrase, though. So, to give Google something to search with, we’ll use the path of the URL that the user is currently on.

Caveat: Limits Abound

Before jumping into the “how to” part, it’s worth noting that the free tier for this API has a limit of 100 calls per day. You might want to go light on the testing while working on it. I managed to burn through the 100 calls in less than an hour, and I had to wire part of it together without seeing the result till the next day.

While someone with a small website might be fine with this limit, paid upgrades are available. Google’s API documentation mentions a price of $5 per 1000 queries2 and up to 10,000 queries per day.

Setting Up

Before using the Custom Search API, we need to let Google know who we are and get some access keys.

Search Engine ID

We need to go through a few steps before we can fly through those 100 API requests. First, register your site-specific search engine3.

  • Select “Add.”
  • Input your website’s URL (yoursite.com) in “Sites to search.”
  • Hit “Create.”

You now need to find your “Search engine ID.” Click “Edit” on the search engine that you created, then the “Search engine ID” button. Take note of that code!

Setting up the Google’s Custom Search API.4
Setting up the Google’s Custom Search API. (View large version5)

Developer API Access

Next, go to the Developers Console6.

If you don’t yet have a project, select the “New Project” option and fill in the form.

Under “APIs,” activate the “Custom Search API” by switching the “Off” button to “On.” Then, select “Credentials,” then “Create New Key,” and choose the “Browser Key” option. Take note of the API key!

7
(View large version8)

JavaScript

Armed with both a search engine ID and an API key, you can now start hitting the API. The code below requires jQuery9. It does some AJAX JSON stuff, so I’d rather lean on the framework to ensure that it works across browsers.

Before creating functions, we should consider the environment that our code will run in. Because we’re writing JavaScript on the front end, our code might run alongside other plugins and scripts. So, let’s build a little space to cleanly separate our functions from everything else:

function createCustomSearch() {
// Private variables and methods here
}

Within our customSearch object, we can define methods and variables safely away from the global context. First, let’s set up some variables to use later:

function createCustomSearch() {
  // Some private variables for this object
  var context = this;
  var dialog = document.querySelector('dialog');
  // Your keys
  var engineID = 'YOUR_ENGINE_ID';
  var apiKey = 'YOUR_API_KEY';
}

Replace the keys with those we generated earlier.

Initially, we establish a local context by storing this in a variable. We’ll use this to access a showDialog method later.

Trying A Search

First, we’ll add a method that tries a custom search:

function customSearchConstructor() {
  // Some private variables for this object
  var context = this;
  var dialog = document.querySelector('dialog');
  // Your keys
  var engineID = 'YOUR_ENGINE_ID';
  var apiKey = 'YOUR_API_KEY';
  this.trySearch = function(phrase) {
    var queryParams = {
      cx: engineID,
      key: apiKey,
      num: 10,
      q: phrase,
      alt: 'JSON'
    }
    var API_URL = 'https://www.googleapis.com/customsearch/v1?';
    // Send the request to the custom search API
    $.getJSON(API_URL, queryParams, function(response) {
      if (response.items && response.items.length) {
        console.log(response.items[0].link);
      }
    });
  };
}

This trySearch method takes a phrase and sends it along with your keys as a request to Google’s API. The response is then checked, and the first link that it finds will be logged to the console. You would call it like so:

var customSearch = new customSearchConstructor();
customSearch.trySearch('cat');

Assuming that your website contains pages about cats (and whose doesn’t?), you should see something logged in your console.

Getting The Search Phrase

Next, we’ll write some code to get the path from the URL of the page. This path will become the search phrase.

$(document).ready(function() {
  var customSearch = new customSearchConstructor();
  var path = window.location.pathname;
  var phrase = decodeURIComponent(path.replace(/\/+/g, ' ').trim());
  customSearch.trySearch(phrase);
});

Within the jQuery ready method, we’ll pick up the pathname part of the current URL and create a search phrase from it. We’ll decode the URI, replace any forward slashes with spaces, and send the result to the trySearch method.

Replacing Strings With JavaScript

One handy thing to know is how to replace a global regular expression in JavaScript. This one is a set of matches separated by pipes:

/\/+/g

The first and last forward slashes (/) are there to contain the expression. Within it, we escape a backslash character (\/) so that it is treated as an actual character. The + will match any instances of multiple slashes, and the g then tells it to replace every instance in the string.

Showing The Redirect

In my first version, I had the page redirect immediately. That was fun but not a great experience for the visitor. The page would load, flicker and jump elsewhere.

An alternative approach is to present the option as an overlay and as a link that the visitor can click. This way, the visitor better understands what has happened and sees a clear way to proceed. And they will have the option not to proceed if the result doesn’t suit them.

Other Approaches

Showing a single result is one way to go about this, but it would be worth considering more than the first result. If you wish to give the visitor more options, then your 404 page could show all of the returned pages as a set. Depending on the quality of the results from the custom search, this might be better.

For this example, let’s assume that the first result returned is always the most relevant, and we’ll present a single option in the form of a dialog overlay.

Also, consider cases in which no results are returned. Ensure that your 404 page contains some helpful message or content.

Starting A Dialog

If we find a result, let’s show it as a modal that prompts the user. To help with this, we’ll be able to use the new dialog10 element11 in the near future. Originally intended for dialogue from movies, the element is back and can now show any content that needs to be popped up in front of other content. In other words, we now have a native HTML5 modal element.

Let’s define the dialog in HTML:

<dialog>
  <h2>
    Hey, is this what you meant?
    <span class="suggestion"></span>
    <span class="nope">No thanks</span>
  </h2>
</dialog>

Polyfill For Older Browsers

Before calling the JavaScript that will show and hide this dialog, we need to consider older browsers. The dialog element is very new and so isn’t supported everywhere. To fix this, we can use the helpful polyfill provided by Google12.

This polyfill requires a little JavaScript. The following external script will need to be called before we create the dialog:

<script src="https://cdn.rawgit.com/GoogleChrome/dialog-polyfill/master/dialog-
polyfill.js"></script>

This script includes a registerDialog method that wraps the dialog selector with a few handy functions that reproduce the native API. We can use it in our customSearch object:

function createCustomSearch() {
  …
  var dialog = document.querySelector('dialog');
  // Apply the polyfill
  dialogPolyfill.registerDialog(dialog);
  …
}

Showing And Hiding

We now have a dialog element, with extra methods added by the registerDialog polyfill. Let’s add some methods to show and hide the element:

function createCustomSearch() {
  …
  this.showDialog = function (url) {
    var suggestedLink = $('<a></a>');
    // Verify that the suggested URL is from this domain
    var hostname = new RegExp(location.host);
    if (hostname.test(url)) {
      suggestedLink.attr('href', url);
      suggestedLink.text(url);
      $('dialog .suggestion').html(suggestedLink);
      dialog.showModal();
    }
  };
  this.hideDialog = function () {
    dialog.close();
  };
}

We’ve got two methods here. The first, showDialog, takes a URL, places it in the dialog element, and calls the showModal method provided by the polyfill.

To protect ourselves, we’re verifying the URL. Because we’re expecting this script to return another page from the same website, we verify that the returned URL’s host name and the local website’s host name are the same.

The URL is then used to generate an anchor, which we place in the dialog HTML.

The second method, hideDialog, hides the modal using its own close method.

Styling The Dialog

Lastly, let’s add some style. The default modal style is a bit too boxy. We’ll make it subtler and give it a dark background with some CSS:

dialog {
  display: none;
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  text-align: center;
  color: #fff;
  border: none;
  background: none;
}

dialog[open] {
  display: block;
}

dialog[open]:before {
  position: fixed;
  z-index: -1;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  content: '';
  background: rgba(0,0,0,.8);
}

dialog span {
  display: block;
}

dialog span.suggestion {
  font-size: 1.75em;
  line-height: 2.5em;
}

dialog h2 {
  line-height: 1.5em;
  padding-top: 2em;
}

dialog a {
  padding: .25em;
  border-radius: .25em;
  background: rgba(200,200,255,.9);
}

dialog .nope {
  font-size: .75em;
  cursor: pointer;
  color: #aaa;
}

We’re referring directly to the dialog in this CSS. For more flexibility, you might prefer to refer to it by a class.

Tweak the various styles to fit your design. The main goal of this CSS is to define how the dialog looks, and have it display: block when given the class of open. The other styles, from position to color, are entirely up to you.

Wiring In The Search

Next, we need to adjust that trySearch method from earlier to use the dialog. We do this by placing the showDialog method call within the JSON response callback. Here’s the full script:

function customSearchConstructor() {
  // Some private variables for this object
  var context = this; // Keeps the parent context available so that we can call local methods
  var dialog = document.querySelector('dialog');
  // Apply the polyfill
  dialogPolyfill.registerDialog(dialog);
  // Your keys
  var engineID = 'YOUR_ENGINE_ID';
  var apiKey = 'YOUR_API_KEY';
  this.trySearch = function(phrase) {
    var queryParams = {
      cx: engineID,
      key: apiKey,
      num: 10,
      q: phrase,
      alt: 'JSON'
    }
    var API_URL = 'https://www.googleapis.com/customsearch/v1?';
    // Send the request to the custom search API
    $.getJSON(API_URL, queryParams, function(response) {
      if (response.items && response.items.length) {
        context.showDialog(response.items[0].link);
      }
    });
  };
  this.showDialog = function (url) {
    var suggestedLink = $('<a></a>');
    // Verify that the suggested URL is from this domain
    var hostname = new RegExp(location.host);
    if (hostname.test(url)) {
      suggestedLink.attr('href', url);
      suggestedLink.text(url);
      $('dialog .suggestion').html(suggestedLink);
      dialog.showModal();
    }
  };
  this.hideDialog = function () {
    dialog.close();
  };
}
$(document).ready(function() {
  var customSearch = new customSearchConstructor();
  var path = window.location.pathname;
  var phrase = decodeURIComponent(path.replace(/\/+/g, ' ').trim());
  customSearch.trySearch(phrase);
  $('dialog .nope').click(function() {
    customSearch.hideDialog();
  });
});

Live Demo

You can see this code in action on my 404 page13. Typing something like …/mac/plus/article/ will result in a 40414 that recommends the CSS Mac Plus blog.

Fallbacks And Other Strategies

API limits aside, it’s possible that a match isn’t found for the mistyped URL. In this case, showing the visitor some helpful content would be a good idea. Depending on your website, you could show recent articles or recently updated pages or perhaps even a custom search box.

Google’s Custom Search Engine15 gives us the option to get some embedding code. Select your existing engine and then the “Get code” button to find this. Whatever content you decide to show as a fallback, it will be better for your visitors than showing a funny picture. It might not be as much fun, but it will help visitors find what they need.

I hope you’ve enjoyed this article. If you want to share it, please double-check that the URL is correct. Or don’t. I’m sure it’ll be fine.

Front page image credits: OpenSource.com16

(ds, il, al)

Footnotes

  1. 1 https://developers.google.com/custom-search/
  2. 2 https://developers.google.com/custom-search/json-api/v1/overview
  3. 3 https://www.google.com/cse/manage/all
  4. 4 http://www.smashingmagazine.com/wp-content/uploads/2014/08/02_new_engine_form-large-opt.jpg
  5. 5 http://www.smashingmagazine.com/wp-content/uploads/2014/08/02_new_engine_form-large-opt.jpg
  6. 6 https://console.developers.google.com/project
  7. 7 http://www.smashingmagazine.com/wp-content/uploads/2014/08/04b_api_key-large-opt.jpg
  8. 8 http://www.smashingmagazine.com/wp-content/uploads/2014/08/04b_api_key-large-opt-500x169.jpg
  9. 9 http://jquery.com/
  10. 10 http://updates.html5rocks.com/2013/09/dialog-element-Modals-made-easy
  11. 11 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
  12. 12 https://github.com/GoogleChrome/dialog-polyfill
  13. 13 http://hop.ie/smashing/404/
  14. 14 http://hop.ie/mac/plus/article/
  15. 15 https://www.google.com/cse/
  16. 16 https://www.flickr.com/photos/opensourceway/6554315319/

↑ Back to topShare on Twitter

Donovan is a Dublin-based front-end developer with a passion for CSS, animation and making the web fun. He writes tutorials at Learnsome.co and blogs on other web topics at Hop.ie.

Advertising
  1. 1

    Thanks @donovan. Love that. If you are proactive about tracking your 404′s in Analytics and cleaning them up, the 100 API calls won’t be an issue for most sites.

    We use a simple search input for the standard google site search on our 404 http://theinformr.com/ada/ but the idea of guessing the page the user wanted and avoiding that potential bounce immediately or after another click is a great user experience hack.

    0
    • 2

      I agree, if you’re actively using GA to it’s full potential then you can see where users are going wrong, create a redirect and ensure that they don’t reach a 404 to begin with.

      But if they do, they reach a small Star Wars bit: http://airwalk-design.com/404 ;)

      0
      • 3

        Ummm Daniel, that 404 goes against everything this article is saying.

        0
        • 4

          I agree, it’s not the most helpful. My point was that if the user never reaches the 404 then it shouldn’t really matter. Sure, if I had a moment or two I’d re-think it and offer a better UX (for the sake of it), but my efforts go towards redirecting old or inactive webpages to the most relevant active webpage without ever hitting a 404.

          0
  2. 5

    Awesome trick Donovan! It makes a lot of sense, but I couldn’t have thought about it myself. I foresee this catching on very quickly, as it should.

    0
  3. 6

    Great article you have there. If there is someone looking for a wordpress plugin for 404 pages feel free to check this link : http://bit.ly/1sPLiZ7 . I’ll try to add the GA feature on it :)

    Cheers!

    0
  4. 7

    Terry Alyn Maraccini

    August 14, 2014 10:12 pm

    This is all really elegant stuff, but can we still have the pandas?

    0
  5. 9

    My team implemented a similar approach. Using javascript to run a search to find possible alternative pages.
    Since I stopped working with the company, they have reverted back to the traditional “you screwed up” style 404 page.
    My current main client uses google analytics to record an event for all 404 pages encountered. This feeds into a process to notify someone and resolve the error.
    I’m planning to have the best of both worlds with an automatic search for the right page when a 404 is encountered and triggering an analytics event.
    And both organisations have hundreds of people working on the web, and over 100 web-enabled systems in use. Although preventing 404s is good practise, anyone whose worked in a large organisation would be aware that this is not practical as the sole quality tactic.

    0
    • 10

      Nice approach. You can’t anticipate all errors, so maintaining a human check while doing what we can with automation is a good balance.

      0
  6. 11

    Great and a very informative post for me. Thanks a lot for sharing. Keep sharing.

    0
  7. 12

    I setup my own search (include search by voice) -> http://www.hr2b.com/notfound

    0
  8. 13

    Deleted resources should respond with another status code. Moved resources should redirect the user agent to the new url. 404 is for NOT FOUND resources.

    0

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top