Using HTTP Headers to Serve Styles

Published 15 years, 5 months past

How many times have you played out the following scenario?

  1. Makes local changes to your style sheet(s).
  2. Upload the changes to the staging server.
  3. Switch to your browser and hit “reload”.
  4. Nothing happens.
  5. Force-reload. Nothing happens.
  6. Go back to make sure the upload is finished and successful.
  7. Reload again.  Still nothing.
  8. Try sprinkling in !important.  Upload, reload, nothing.
  9. Start swearing at your computer.
  10. Check Firebug to see what’s overriding your new styles.  Discover they aren’t being applied at all.
  11. Continue in that vein for several minutes before realizing you were hitting reload while looking at the live production server, not the staging server.
  12. Go to the staging server and see all your changes.
  13. Start swearing at your own idiocy.

This happened to me all the time as we neared completion of the redesign of An Event Apart.  It got to the point that I would deliberately add obvious, easily-fixable-later errors to the staging server’s styles, like a light red page background.

Now that we’re launched and I have time to actually, you know, think about how I do this stuff, it occurred to me that what I should have done is create a distinct “staging” style sheet with the obvious error or other visual cue.  Maybe repeat the word “staging” along the right side of the page with a background image, like a watermark:

html {background: url(staging-bg.png) 100% 50% repeat-y;}

Okay, cool.  Then I just need to have that served up with every page on the staging server, without it showing up on the production server.

One way to do that is just make sure the image file never migrates to production.  That way, even if I accidentally let the above CSS get onto production, the user will never see it.  But that’s inelegant and wasteful, and fragile to boot: if the styles accidentally migrate, who’s to say the image won’t as well?  And while I’m sure there are all kinds of CMS and CVS and Git and what-have-you tricks to make sure that doesn’t happen, I am both clumsy and lazy.  Not only do I have great faith in my ability to screw up my use of such mechanisms, I don’t really want to be bothered to learn them in the first place.

So: why not send the link to the style sheet using HTTP headers?  Yeah, that’s the ticket!  I can just add a line to my .htaccess file on the staging server and be done.  Under Apache, which is what I use:

Header add Link "</staging.css>;rel=stylesheet;type=text/css;media=all"

Those angle brackets are, so far as I can tell, absolutely mandatory, so bear that in mind.  And of course the path in those brackets can be absolute, unlike what I’ve shown here.  I’m sure there are simple PHP equivalents, which I’ll leave to others to work out.  I really didn’t need to add the media=all part, but what the heck.

Seems so simple, doesn’t it?  Almost… too simple.  Like there has to be a catch somewhere.  Well, there is.  The catch is that this is not supported by all user agents.  Internet Explorer, for one; Safari, for another.  It does work in Opera and Gecko browsers.  So you can’t deploy this on your production server, unless of course you want to use it as a way to hide CSS from both IE and Safari.  (For whatever reason.)  It works great in Gecko-based production environments like mine, though.

I looked around for a quick how-to on do this, and couldn’t find one.  Instead, I found Anne van Kesteren’s test page, whose headers I sniffed in order to work out the proper value syntax; and a brief page on the Link header that didn’t mention CSS at all.  Nothing seemed to put the two of them together.  Nothing until now, that is.

Comments (42)

  1. Followup: Also note that if you’re in an environment where your IP is static and you’re only editing CSS and you don’t mind testing those CSS changes on your live productoin server, then there’s a way to use redirects based on IP to get a somewhat similar effect. Not exactly the same, I grant you, but similar.

  2. Sweet – been having the exact problem myself (refreshing the wrong tab etc, generally confusing testing/staging/production), so this will be helpful straight away.


  3. Almost too ingenious. Nifty idea—thanks mate. (:

  4. Hi Eric. This is a great post. I would’ve never thought to serve style sheets in such a manner but I can totally see where it would come in use. I’m going to keep this in my arsenal for later use.

    Also, I’ve done loads of bone head things like that as well. We all have our moments. The other day I was editing a template for a module on a CMS and wondering why the changes weren’t being applied. Turned out I was editing the entirely wrong template. This went on for about 10 minutes before I realized it.


  5. Good post, especially since there seems to be a distinct lack of information on the web about using HTTP headers to serve CSS. Guess that’s mostly due to the lack of support in IE…

    And thanks for making me feel less idiotic knowing that that scenario doesn’t just happen to me!

  6. We use a Greasemonkey script that knows what environment we’re looking at (development, staging, production) and injects an appropriately-colored and labeled div up at the top of the page to let us know what we’re looking at. Tremendously useful. (Of course this is pretty much a Firefox-only solution, though we’ve also cooked up an IE-compatible equivalent as well.)

  7. Foolproof… until the .htaccess file accidentally migrates itself to the production server. :-)

    However, for small one- or two-person projects, an alternative would be the Stylish extension, which allows Firefox to implement custom styles on any domain (or subdomain). This way, any staging code is confined locally to the developer’s browser and does not appear on the staging server at all. Use in tandem with Server Switcher and you’re good to go.

  8. Must admit to a sore butt myself from kicking it when I make the same silly mistake. So much time can be wasted on a project.

    Though maybe I should be kicking myself again for not having thought of getting a solution earlier ;)

  9. Brilliant. Applying that to my project sites right…NOW.

  10. Foolproof… until the .htaccess file accidentally migrates itself to the production server.

    Yeah, there’s that. My deep and abiding fear of .htaccess files makes it much less likely that I’ll migrate it by mistake, but it’s still a risk.

    I really like the Stylish/Greasemonkey ideas, at least for solo projects. When working with geographically distributed teams, it only works if everyone’s a developer, which is almost never the case for me.

  11. Great tip! It might be worth noting that if you do by chance actually specify an html background image in your real style sheet, it will override the CSS in the staging one. Perhaps obvious…but when we’re talking about stupid mistakes, you never know!

    Now can you come up with a way to add the closing brace I accidentally deleted while I wonder why only half my style sheet is being applied, or a way to automatically fix my CSS when I’m refering to the wrong selector all together?!

  12. Nice.

    As you say “I”m sure there are simple PHP equivalents”. PHP not being my strong-point, I recently did something more with PHP than I had before and used:

    $theUrlIwantToTest = $_SERVER['HTTP_REFERER'];

    to test for the URL that I had been referred from. Guessing PHP would also allow one to test the URL a page was displaying on and if it partially met that of the test server then one could serve the extra style sheet.

    I’m not PHP-y-enough to say what that code would be, but guessing it would be small and I _think_ it would make it more cross-broswer than the .htaccess solution perhaps?

  13. I know that idiocy of reload all to well.

    Using Safari Stand though, I used Site Alteration to deliver one custom stylesheet when viewing prod and another for dev. It’s been fantastic. Now, it naturally only works on the machine for which I have it installed, but that’s where I do all the work anyway.

    I know there’s extensions out there to do the same thing for Firefox. I just haven’t bothered, since I always open what I’m working on Firefox from Safari (using Developer > Open Page WIth menu).

  14. This suits me well:

    <?php if ($_SERVER[‘HTTP_HOST’] != ‘’) {echo ‘<p style=”font-size: 0.75em; padding:2px; background:#F90; color:#FFF; font-weight:bold; position:fixed; top:0; left:0; right: 0; z-index:999; -webkit-box-shadow: 0 3px 3px rgba(0,0,0,0.4);” onclick=” = \’none\’;”>This is the test site.</p>’;} ?>

  15. Rarely am I surprised by any new techniques anymore but this impressed me. Well done!

    To those adverse to .htaccess files and who may have a little more control over their servers, I highly recommend popping the details into the httpd.conf. Highly unlikely it’ll get moved over in migration. Avoid .htaccess altogether, and turn off .htaccess in the httpd.conf, and you should see a performance boost as Apache no longer has to search up the tree to find an .htaccess file.

  16. 1) If you wanted to be absolutely sure you”ll never accidentally serve up a “STAGING” CSS file, instead if having the Link header point directly to the CSS file, it could point to the PHP file I included below.

    2) To ensure the .htaccess files”s accidental copying to a live server would not start sending needless files to live site visitors, the PHP file below could be trivially changed so as to write <link> tags instead of sending headers and included in your website / theme files.

    3) Serving a JavaScript file instead of the CSS file could work as well, and could even test for the hostname before applying itself. (Of course, you wouldn”t want to serve that file to the live site anyway, as it would be a pointless performance and load-time hit.)


    * Serve up a file if and only if the hostname matches the pattern 
    * below (e.g. has 'staging' in it).
    * Alan Hogan - <> 2009-01-22
    * Inspired by Eric Meyer <>
    if(preg_match("/staging|test|dev|localhost|127\.0\.0\.1/", $_SERVER['HTTP_HOST'])){
    	header('HTTP/1.1 302 Found');
    	header('Location: /css/staging.css'); //or perhaps a javascript file
    } else {
  17. As you noted, support for Link is spotty; that appears to be why it was dropped from RFC 2616. Felt it worth pointing out, though, that while it does work for stylesheets in Firefox, it’s not consistently supported for other values.

  18. After testing, I fixed two syntax errors (*ducks things being chucked at him*) in that first line of code.

    It works great now!

    if(preg_match(‘/staging|test|dev|localhost|127\.0\.0\.1/’, $_SERVER[‘HTTP_HOST’])){

    [I’ve edited the original comment to include this fix. Thanks for the correction, Alan! -E.]

  19. What ever happened to ctrl+shift+r ?

    Am I missing something here?

  20. Somewhat simpler: just add something to the query string, it’ll get ignored but the browser will see it as different. I append the file modification timestamp as the query string:

    <link rel=”stylesheet” type=”text/css” href=”/Styles/Skiviez.css?128767908260000000″ media=”screen,print” />

    This way, if I touch the file, it’s automatically re-downloaded on the next request because the timestamp will have changed.

  21. If this header stuff isn’t cross browser and you’re going to use PHP anyway, then why not just wrap an if statement around the code that echos out the link tag?

    <?php if ($_SERVER[‘HTTP_HOST’] != “”) { ?>
    <link rel=”stylesheet” href=”/css/staging.css” type=”text/css” media=”all” />
    <?php } ?>

  22. You’re kind of missing the whole point, Mark. You might want to read the article again.

    That works well for PHP environments, Zachary, but not every page is based on PHP. My original goal was to have something that would work no matter what kind of page was being served up. Perhaps I wasn’t clear on that point.

  23. You make a valid point, Eric =)

    I guess I saw all the other PHP creeping into the comments and wanted to get the quick and dirty solution on the table.

  24. The HTTP Link header has been tested in Ian Hickson’s import tests for ages; those tests probably date back to around 1999 or 2000. (I think it might only be tested in part 2; I didn’t find any tests in part 1 with a quick scan.)

  25. Great solution to a common problem. Now, have you figured out how to solve the issue of typos in the selectors and typos in the path to the stylesheet? Gets me every time. At least I’m blond, so I have an excuse.

  26. Alas not everything is hosted on Apache…
    I’ll go look at the greasemonkey and stylish extensions to see if those work on IIS.

  27. Sample staging.css (which looks like this):

    body::before {
    content: 'STAGING SITE';
    position: fixed;
    top: 3px;
    left: 3px;
    font-size: 16px;
    display: block;
    color: white;
    text-shadow: 0.1em 0.1em 0.3em black;
    -o-text-shadow: 0.1em 0.1em 0.3em black;
    -moz-text-shadow: 0.1em 0.1em 0.3em black;
    -webkit-text-shadow: 0.1em 0.1em 0.3em black;
    -ms-text-shadow: 0.1em 0.1em 0.3em black;
    opacity: 0.8;
    -moz-opacity: 0.8;
    -moz-opacity: 0.8;
    -o-opacity: 0.8;
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
    z-index: 999;

  28. Great technique! Pity it’s not supported by more browsers. Given that this only works in Firefox and Opera, you could also do this by running user stylesheets. It does mean everyone has to set it up, but you never have to worry about accidentally publishing it.

  29. @Sherri

    You can add custom headers is IIS too, details are on technet.

  30. how about a solution to

    – an accidental quote char in the stylesheet, breaking it in FF, and

    – background: url (image.jpg) no-repeat 0 0;

    in that last one, check the space after url. In FF this works, in IE it breaks.

    Well actually that background url problem I usually spot right away because I know it so well. The accidental quote was a really hard one recently, because I use an elegant font in my stylesheet editor (topstyle). The quote (`) was hardly visible.

  31. In the past I’ve played with this “problem” too … ended up coding a firefox extension that colors the location bar based on which URL you’re at.

    Only problems with my approach (and things I had coded quickly):
    – it only worked on Firefox 2, Windows. Then Firefox 3 came out and I dropped the project
    – no support for other browsers (IE, Safari, Opera, …)

    Great to see your approach on the subject … and the posted PHP solution in the comments here is neat too!


  32. “Try sprinkling in !important. Upload, reload, nothing.” – I’m glad it’s not just me that does this ;)

    We also use the coloured background idea to distinguish between test and production databases – just a bit of CSS in the database connection file does the trick.

  33. Awesome. This helps a lot. Thank you!

  34. I add a parameter with the result of filemtime() to my stylesheets. I just have to do the client side caching on my own side.

  35. Never thought about doing it like that – pretty interesting. I’ve never liked .htaccess though – not all servers have it enabled. You can use a service like http header viewer to make sure that your trick is working

  36. Foolproof… until the .htaccess file accidentally migrates itself to the production server. :-)

    True, true. And to prevent that — provided I have the privileges — I would put the ‘Header’ line in a <Directory> section of my apache configuration file (httpd.conf).

    <Directory /path/to/webroot>
    Header add Link "</staging.css>;rel=stylesheet;type=text/css;media=all"

    Now that the directive is safely stashed away from my development webroot, I’ve further reduced the possibility of accidental migration. Or at least I’ve passed the responsibility for it to my server admin ;)

    By the way, how standard is it for Apache admins to enable mod_headers? I know my host has it, but I don’t know if it’s common practice.

  37. Pingback ::

    Max Design - standards based web design, development and training » Some links for light reading (27/1/09)

    […] Using HTTP Headers to Serve Styles […]

  38. I do something sort of similar on a few websites I work on, where by I add SB or Staging to the page title, so I know what I’m looking at.

  39. This approach is really interesting. I propose a slight variant below which should support any browser. It continues to depend on Apache, but moves away from using HTTP headers to just serving the modified css using rewrite rules.

    1) Staging CSS:

    @import “public.css”

    html {background: url(staging-bg.png) 100% 50% repeat-y;}

    2) Production.css:
    @import “public.css”

    3) Public.css: Style rules for the production environment

    3) Use an Apache rewrite rule on staging servers to rewrite requests for production.css to requests for staging.css

    This does cost an extra hop even in production, so it may not be optimal for production sites. One way to address this would be to setup a rewrite rule in production that rewrites requests for production.css to public.css. This saves the hop, but nothing breaks if the rule is omitted.

  40. Pingback ::

    Dev Blog AF83 » Blog Archive » Veille technologique (x2) : Annonces, Contenus, Conférences, Méthodes, Agilité, Développment, Langages, Editeurs, Outils, Bases de données, Protocoles, Bibliothèques, SEO, Ergonomie, etc.

    […] : Eric Meyer propose d’utiliser les headers HTTP pour servir une balise style, afin de distinguer les serveurs de développement de ceux de production […]

  41. You might find this page with Test Cases for HTTP Link header field (RFC 5988) useful as wel ;-)

  42. Pingback ::

    Adding CSS to a Page via HTTP Headers - Impressive Webs

    […] that could justify use for this in production is as a way to include some Firefox-only CSS, which Eric Meyer mentions as a possibility in an old post on this subject. But it’s not guaranteed to always only work in […]

Add Your Thoughts

Meyerweb dot com reserves the right to edit or remove any comment, especially when abusive or irrelevant to the topic at hand.

HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <em> <i> <q cite=""> <s> <strong> <pre class=""> <kbd>

if you’re satisfied with it.

Comment Preview