Server Side Mustard Cut

Avatar of Chris Coyier
Chris Coyier on

The term “mustard cutting” in web design comes from developers at the BBC who wanted to serve different experiences of their site to different browsers on different devices. They were specifically trying to avoid User Agent sniffing:

But these days, with such a long tail of user agents now accessing the site, this becomes a fruitless exercise.

Instead, they used feature detection. Modernizr is the classic (awesome) example of that, but ala carte feature tests can be quite small and simple all by themselves. This is the logic the BBC used to determine if a browser cut the mustard or not:

if('querySelector' in document
     && 'localStorage' in window
     && 'addEventListener' in window) {
     // bootstrap the javascript application
     }

If that logic failed, the website still loads what they called a core experience. If that logic passed, additional resources would load for an enhanced experience.

Pretty cool.

Loading additional CSS and JavaScript is fairly easy

There are various ways to do it, typically involving an XHR for the resource. Filament Group has some very tiny, focused scripts just for this: loadCSS and loadJS.

Loading a bit of extra HTML via XHR is similarly easy. But…

It’s too hard to load an entirely different document client-side

Say you don’t need just a bit of extra CSS, scripts, or a bit of HTML. What you want is an entirely different document.

Your “core experience” and “enhanced experience” are entirely different sets of HTML, CSS, and JavaScript. Trying to do this client-side would mean trying to load a super bare-bones document, then trying to essentially re-create how the browser parser works. First you mustard-cut, then XHR for the right set of HTML you need, then either drop it into the DOM, or perhaps wait until you’ve XHR’d for the CSS so you don’t get a flash of unstyled document. Then XHR for all the scripts, and make sure that you execute them in order (tricky).

Hard to pull off.

Allow the server to serve the correct document based on mustard-cutting information

If the server knew the results of your mustard-cut, you could avoid all that trouble and serve the correct document right off the bat.

That’s the whole point of client-side mustard-cutting: it can only be done on the client. But… you could save that data to a cookie, and cookies can be read by the server.

It you had a cookie you could count on, you could do something like this in the routing of your site:

<?php
  // This is just a fake routing/controller kinda setup.
  if (isset($_COOKIE["mustard"])) {
    // Enhanced experience
    if ($_COOKIE["mustard"] == true) {
      include_once("enhanced.php");
    // Core experience
    } else {
      include_once("core.php");
    }
  // No cookie = core experience
  } else {
    include_once("core.php");
  }
?>

What happens on the first page view though?

That’s the tricky part here. That cookie won’t exist on the first page view. You could just let subsequent pages serve the correct experience, but that’s not likely to be acceptable.

Here comes the most controversial part: if you don’t have the cookie but can tell the browser supports them and they are enabled, you refresh the page.

Refresh the page?! Are you kidding?

Totally reasonable questions: How can a refresh possibly be a good user experience? Aren’t refreshes slow? Couldn’t you get caught in a refresh loop?

I think all of these things can be addressed.

At the very top of the document, if that cookie is not there and the browser does support cookies:

  1. Mustard-cut and save the data to a cookie with JavaScript
  2. If the mustard-cut data tells you you should be loading a different document: halt the page from loading/doing anything else (window.stop();) and refresh (location.reload(true);).

Upon refresh, the cookie will be there for the server.

It all happens so fast when it’s the very first thing a document does that I find it barely noticeable. This is what we’re doing for the editor page on CodePen, see:

Fresh page load on a new incognito window (no cookies saved). The desktop view is actually the default, but the refresh happens and the mobile view loads because of a mustard-cut.

The trick to avoiding a refresh loop is to only execute that part of the JavaScript if you’re sure cookies are supported and enabled.

The mustard-cutting script

Here’s a mustard-cut that only tests the screen width. Bear in mind a mustard-cut could be anything you want it to be that you can test client-side.

(function() {

  // If the browser supports cookies and they are enabled
  if (navigator.cookieEnabled) {

    // Set the cookie for 3 days
    var date = new Date();
    date.setTime(date.getTime() + (3 * 24 * 60 * 60 * 1000));
    var expires = "; expires=" + date.toGMTString();

    // This is where we're setting the mustard cutting information.
    // In this case we're just setting screen width, but it could
    // be anything. Think http://modernizr.com/
    document.cookie = "screen-width=" + window.outerWidth + expires + "; path=/";

    /*
      Only refresh if the WRONG template loads.

      Since we're defaulting to a small screen,
      and we know if this script is running the
      cookie wasn't present on this page load,
      we should refresh if the screen is wider
      than 700.

      This needs to be kept in sync with the server
      side distinction
    */
    if (window.outerWidth > 700) {

      // Halt the browser from loading/doing anything else.
      window.stop();

      // Reload the page, because the cookie will now be
      // set and the server can use it.
      location.reload(true);

    }

  }

}());

In fact, we don’t have to load that script at all if the cookie is already there, since if it is, we know the correct page has loaded already.

<?php
    // Run this script as high up the page as you can,
    // but only if the cookie isn't already present.
    if (isset($_COOKIE["screen-width"]) == 0) { ?>
      <script src="mobile-mustard.js"></script>
<?php } ?>

Possible Scenarios

  • The normal first time visitor: No cookie is present. Mustard-cut script will run and refresh the page quickly. They will get correct document based on cut.
  • The repeat visitor: Cookie is already present. They will get correct document based on cut.
  • Visitor with incorrect cookie: Perhaps they have a desktop browser but it was very narrow when the page loaded the first time, but they have since widened it. We can detect that with a CSS @media query and offer a link to correct the problem (see demo).
  • Visitor with cookies off: We serve our choice of documents. Could potentially be wrong. Serve the best likely case based on data.
  • Visitor in which JavaScript doesn’t run: We serve our choice of documents. Could potentially be wrong. Serve the best likely case based on data.

Possible Problems

For the record, I’m not saying this is the best possible solution to this problem. In fact, if you’re in a situation where you can do everything purely client-side, that’s probably better.

Here’s some potential problems with this solution:

  • Perhaps the reload is slower than I think it is. I didn’t do any testing of super old / super slow devices like I probably should have.
  • HTML caching could be a problem. I experienced this first hand when building the demo on a site that was using that method. The server serves a cached document, which then is determined to be the incorrect one and refreshed, causing the dreaded refresh loop. Solution: don’t HTML cache this page, or redirect to subdomains based on the cut.
  • Security settings that prevent server-side access to cookies created by JavaScript. If you can’t control that, that would be a problem.

I do use this technique in production though and haven’t had an issue in a lot of months, so I’m pretty happy with it.

Demo and Repo

Here’s a demo and the code is up on GitHub if you spot any fouls.

Also, I thought client hints was supposed to be the savior here, but I’m just not sure anymore where it fits into this situation.