Skip to: site navigation/presentation
Skip to: Thoughts From Eric

Using HTTP Headers to Serve Styles

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.

41 Responses»

    • #1
    • Comment
    • Thu 22 Jan 2009
    • 0849
    Eric Meyer wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 0903
    Emil Björklund wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 0917
    Simon Pascal Klein wrote in to say...

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

    • #4
    • Comment
    • Thu 22 Jan 2009
    • 0921
    Daryn St. Pierre wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 0922
    Alexis Deveria wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 0925
    Mike Pirnat wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 0928
    Matthew Brundage wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 0929
    Cindy Prosser wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 0936
    Mark wrote in to say...

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

    • #10
    • Comment
    • Thu 22 Jan 2009
    • 0941
    Eric Meyer wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 0942
    Dan Wilkinson wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1006
    Alan Bristow wrote in to say...


    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
    • Comment
    • Thu 22 Jan 2009
    • 1029
    John Lascurettes wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1048
    Zachary Johnson wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1100
    Jonathan Snook wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1104
    Alan Hogan wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1116
    Peter J. wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1127
    Alan Hogan wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1157
    Mark Sanborn wrote in to say...

    What ever happened to ctrl+shift+r ?

    Am I missing something here?

    • #20
    • Comment
    • Thu 22 Jan 2009
    • 1207
    Nicholas Piasecki wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1209
    Zachary Johnson wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1249
    Eric Meyer wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1324
    Zachary Johnson wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1429
    David Baron wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1445
    NatalieMac wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1603
    Sherri wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1830
    Alan Hogan wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1920
    Ben Buchanan wrote in to say...

    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
    • Comment
    • Thu 22 Jan 2009
    • 1936
    Peter Wilson wrote in to say...


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

    • #30
    • Comment
    • Thu 22 Jan 2009
    • 2012
    Michiel van der Blonkq wrote in to say...

    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
    • Comment
    • Fri 23 Jan 2009
    • 0516
    Bramus! wrote in to say...

    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
    • Comment
    • Fri 23 Jan 2009
    • 0556
    Peter Holloway wrote in to say...

    “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
    • Comment
    • Fri 23 Jan 2009
    • 1640
    Timothy wrote in to say...

    Awesome. This helps a lot. Thank you!

    • #34
    • Comment
    • Sat 24 Jan 2009
    • 0559
    Thomas Scholz wrote in to say...

    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
    • Comment
    • Sun 25 Jan 2009
    • 1141
    angela wrote in to say...

    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
    • Comment
    • Mon 26 Jan 2009
    • 1648
    Cris wrote in to say...

    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
    • Tue 27 Jan 2009
    • 0556
    • #38
    • Comment
    • Thu 5 Feb 2009
    • 0949
    Micheil wrote in to say...

    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
    • Comment
    • Wed 11 Feb 2009
    • 1335
    AMammenT wrote in to say...

    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
    • Mon 23 Feb 2009
    • 1447
    • #41
    • Comment
    • Wed 21 May 2014
    • 0244
    Potherca wrote in to say...

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

Leave a Comment

Line and paragraph breaks automatic, e-mail address required but never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Remember to encode character entities if you're posting markup examples! Management reserves the right to edit or remove any comment—especially those that are abusive, irrelevant to the topic at hand, or made by anonymous posters—although honestly, most edits are a matter of fixing mangled markup. Thus the note about encoding your entities. If you're satisfied with what you've written, then go ahead...

January 2009
December February