As part of my efforts to speed up my themes for my clients I am delivering my CSS via a dynamic PHP file. At the end of the called for example my_theme_css.php:

This allows me to add Expiry headers to my CSS and JavaScript like so:

header("Expires: Thu, 31 Dec 2020 20:00:00 GMT");
header('Content-type: text/css');

I then want to allow the user to add thir own custom CSS so I have set the following up:

header("Expires: Thu, 31 Dec 2020 20:00:00 GMT");
header('Content-type: text/css');


/** Custom CSS **/
if(get_theme_mod('my_custom_css') != '') {
  $my_custom_css = get_theme_mod('my_custom_css'); 
  echo $my_custom_css; 

I have then added the applicable theme options. But what I am finding is that get_theme_mod() doesn't work due to scope issue (I imagine.) Any suggestions on how to get around this?

I could add the following code:

if(get_theme_mod('my_custom_css') != '') {
  $my_custom_css = get_theme_mod('my_custom_css'); 
  echo $my_custom_css; 

Within <style></style> tags in the header and that would work fine, expect I don't want the CSS inline, I want it on the main CSS file with the expiry header.

  • Wouldn't a static css file be better for performance? There is also some good advice I got on a similar question I asked.
    – Chris_O
    Commented Sep 9, 2010 at 4:13
  • @Chris_O If this was for a few style rules I would say yes but for heavier usage doing it inline means 100's of extra lines downloaded everytime a page is loaded versus one external style sheet with a far future expiry date that can be cached Commented Sep 9, 2010 at 5:51
  • @Ash G: Provide a static CSS file and setup caching in the Webserver properly. That's my suggestion.
    – hakre
    Commented Sep 9, 2010 at 9:18
  • @hakre This will be used in cases where access at that level on the server is not available or where the end user isn't sufficiently knowledgeable to make those changes. For this reason the code needs to be at the theme level. Commented Sep 9, 2010 at 11:54
  • @Ash G: If that's the case, that's relying on PHP then, true.
    – hakre
    Commented Sep 10, 2010 at 16:45

2 Answers 2


Hi @Ash G:

I didn't follow 100% where you were specifically having problems so I'm not sure I really can answer your issues point-by-point but I can explain how to do this from ground up. And unless what you are doing is a good bit more involved then you mentioned it's a little bit more work than I think you were anticipating but it is still completely doable. And even if I'm covering lots of ground that you already know there's a good chance other's with less knowledge or experience will find this via Google and be helped by it too.

Bootstrap WordPress with /wp-load.php

The first thing we need to do in your my_theme_css.php file is bootstrap WordPress' core library functions. The following line of code loads /wp-load.php. It uses $_SERVER['DOCUMENT_ROOT'] to locate the website's root so you don't have to worry about where you store this file on your server; assuming DOCUMENT_ROOT is set correctly as it always should be for WordPress then this will bootstrap WordPress:


So that's the easy part. Next comes the tricky part...

PHP Scripts Must Handle All Caching Logic

Here's where I'll bet you might have stumbled because I sure did as I was trying to figure out how to answer your question. We are so used to the caching details being handled by the Apache web server that we forget or even don't realize we have to do all the heavy lifting ourselves when we load CSS or JS with PHP.

Sure the expiry header may be all we need when we have a proxy in the middle but if the request makes it to the web server and the PHP script just willy-nilly returns content and the "Ok" status code well in essence you'll have no caching.

Returning "200 Ok" or "304 Not Modified"

More specifically our PHP file that returns CSS needs to respond correctly to the request headers sent by the browser. Our PHP script needs to return the proper status code based upon what those headers contain. If the content needs to be served because it's a first time request or because the content has expired the PHP script should generate all the CSS content and return with a "200 Ok".

On the other hand if we determine based on the cache-related request headers that the client browser already has the latest CSS we should not return any CSS and instead return with a "304 Not Modified". The too lines of code for this are respectively (of course you'd never use them both one line after another, I'm only showing that way here for convenience):

header('HTTP/1.1 200 Ok');
header('HTTP/1.1 304 Not Modified');

Four Flavors of HTTP Caching

Next we need to look at the different ways HTTP can handle caching. The first is what you mention; Expires:

  • Expires: This Expires header expectz a date in the (PHP gmdate() function) format of 'D, d M Y H:i:s' with a ' GMT' appended (GMT stands for Greenwich Mean Time.) Theoretically if this header is served the browser and downstream proxies will cache until the specified time passes after which it will start requesting the page again. This is probably the best known of caching headers but evidently not the preferred one to use; Cache-Control being the better one. Interestingly in my testing on localhost with Safari 5.0 on Mac OS X I was never able to get the browser to respect the Expires header; it always requested the file again (if someone can explain this I'd be grateful.) Here's the example you gave from above:

header("Expires: Thu, 31 Dec 2020 20:00:00 GMT");

  • Cache-Control: The Cache-Control header is easier to work with than the Expires header because you only need to specify the number of time in seconds as max-age meaning you don't have to come up with an exact date format in string form that is easy to get wrong. Additionally Cache-Control allows several other options such as being able to tell the client to always re-validated the cache using the mustrevalidate option and public when you want to force caching for normally non-cachable requests (i.e requests via HTTPS) and even not cache if that's what you need (i.e. you might want to force a a 1x1 pixel ad tracking .GIF not to be cached.) Like Expires I was also unable to get this to work in testing (any help?) The following example caches for a 24 hour period (60 seconds by 60 minutes by 24 hours):

header("Cache-Control: max-age=".60*60*24.", public, must-revalidate");

  • Last-Modified / If-Modified-Since: Then there is the Last-Modified response header and If-Modified-Since request header pair. These also use the same GMT date format that the Expires header use but they do a handshake between client and server. The PHP script needs to send a Last-Modified header (which, by the way, you should update only when your user last updates their custom CSS) after which the browser will continue to send the same value back as an If-Modified-Since header and it's the PHP script's responsibility to compare the saved value with the one sent by the browser. Here is where the PHP script needs to make the decision between serving a 200 Ok or a 304 Not Modified. Here's an example of serving the Last-Modified header using the current time (which is not what we want to do; see the later example for what we actually need):

header("Last-Modified: " . gmdate('D, d M Y H:i:s', time()).'GMT');

And here is how you'd read the Last-Modified returned by the browser via the If-Modified-Since header:

$last_modified_to_compare = $_SERVER['HTTP_IF_MODIFIED_SINCE'];

  • ETag / If-None-Match: And lastly there's the ETag response header and the If-None-Match request header pair. The ETag which is really just a token that our PHP sets it to a unique value (typically based on the current date) and sends to the browser and the browser returns it. It the current value is different from what the browser returns your PHP script should regenerate the content an server 200 Ok otherwise generate nothing and serve a 304 Not Modified. Here's an example of setting an ETag using the current time:

header("ETag: " . md5(gmdate('D, d M Y H:i:s', time()).'GMT'));

And here is how you'd read the ETag returned by the browser via the If-None-Match header:

$etag_to_match = $_SERVER['HTTP_IF_NONE_MATCH'];

Now that we've covered all that let's look at the actual code we need:

Serving the CSS file via init and wp_enqueue_style()

You didn't show this but I figured I would show it for the benefit of others. Here's the function call that tells WordPress to use my_theme_css.php for it's CSS. This can be stored in the theme's functions.php file or even in a plugin if desired:

function add_php_powered_css() {
  if (!is_admin()) {
    $version = get_theme_mod('my_custom_css_version',"1.00");
    $ss_dir = get_stylesheet_directory_uri();

There are several points to note:

  • Use of is_admin() to avoid loading the CSS while in the admin (unless you want that...),
  • Use of get_theme_mod() to load the CSS with a default version of 1.00 (more on that in a bit),
  • Use of get_stylesheet_directory_uri() to grab the correct directory for the current theme, even if the current theme is a child theme,
  • Use of wp_enqueue_style() to queue the CSS to allow WordPress to load it at the proper time where 'php-powered-css' is an arbitrary name to reference as a dependency later (if needed), and the empty array() means this CSS has no dependencies (although in real world it would often have one or more), and
  • Use of $version; Probably the most important one, we are telling wp_enqueue_style() to add a ?ver=1.00 parameter to the /my_theme_css.php URL so that if the version changes the browser will view it as a completely different URL (Much more on that in a bit.)

Setting $version and Last-Modified when User Updates CSS

So here's the trick. Every time the user updates their CSS you want to serve the content and not wait until 2020 for everyone's browser cache to timeout, right? Here's a function that combined with my other code will accomplish that. Every time you store CSS updated by the user, use this function or functionality similar to what's contained within:

function set_my_custom_css($custom_css) {
  $new_version = round(get_theme_mod('my_custom_css_version','1.00',2))+0.01;
  set_theme_mod('my_custom_css_last_modified',gmdate('D, d M Y H:i:s',time()).' GMT');

The set_my_custom_css() function automatically increments the current version by 0.01 (which was just an arbitrary increment value I picked) and it also sets the last modified date to right now and finally stores the new custom CSS. To call this function it might be as simple as this (where new_custom_css would likely get assigned via a user submitted $_POST instead of by hardcoding as you see here):

$new_custom_css = 'body {background-color:orange;}';

Which brings us to the final albeit significant step:

Generating the CSS from the PHP Script

Finally we get to see the meat, the actual my_theme_css.php file. At a high level it tests both the If-Modifed-Since against the saved Last-Modified value and the If-None-Match against the ETag which was derived from the saved Last-Modified value and if neither have changed just sets the header to 304 Not Modifed and branches to the end.

If however either of those have changed it generates the Expires, Cache-Control. Last-Modified and Etag headers as well as a 200 Ok and indicating that the content type is text/css. We probably don't need all those but given how finicky caching can be with different browsers and proxies I figure it doesn't hurt to cover all bases. (And anyone with more experience with HTTP caching and WordPress please do chime in if I got any nuances wrong.)

There are a few more details in the following code but I think you can probably work them out on your own:


  $s = $_SERVER;


  $max_age = 60*60*24; // 24 hours
  $now = gmdate('D, d M Y H:i:s', time()).'GMT';
  $last_modified = get_theme_mod('my_custom_css_last_modified',$now);
  $etag = md5($last_modified);

  if (strtotime($s['HTTP_IF_MODIFIED_SINCE']) >= strtotime($last_modified) || $s['HTTP_IF_NONE_MATCH']==$etag) {
    header('HTTP/1.1 304 Not Modified');
  } else {
    header('HTTP/1.1 200 Ok');
    header("Expires: " . gmdate('D, d M Y H:i:s', time()+$max_age.'GMT'));
    header("Cache-Control: max-age={$mag_age}, public, must-revalidate");
    header("Last-Modified: {$last_modified}");
    header("ETag: {$etag}");
    header('Content-type: text/css');

function echo_custom_css() {
  $custom_css = get_theme_mod('my_custom_css');
  if (!empty($custom_css))
    echo "\n{$custom_css}";

function echo_default_css() {
  $default_css =<<<CSS
body {background-color:yellow;}
  echo $default_css;

So with these three major bits of code; 1.) the add_php_powered_css() function called by the init hook, 2.) the set_my_custom_css() function called by whatever code allows the user to update their custom CSS, and lastly 3.) the my_theme_css.php you should pretty much have this licked.

Further Reading

Aside from those already linked I came across a few other articles that I thought were really useful on the subject so I figured I should link them here:


But I would be remiss to leave the topic without making a closing comments.

Expires in 2020? Probably Too Extreme.

First, I don't really think you want to set Expires to the year 2020. Any browsers or proxies that respect Expires won't re-request even after you've made many CSS changes. Better to set something reasonable like 24 hours (like I did in my code) but even that will frustrate users for the day during which you make changes in the hardcoded CSS but forget to but the served version number. Moderation in all things?

This Might All Be Overkill Anyway!

As I was reading various articles to help me answer your question I came across the following from Mr. Cache Tutorial himself, Mark Nottingham:

The best way to make a script cache-friendly (as well as perform better) is to dump its content to a plain file whenever it changes. The Web server can then treat it like any other Web page, generating and using validators, which makes your life easier. Remember to only write files that have changed, so the Last-Modified times are preserved.

While all this code I wrote it cool and was fun to write (yes, I actually admitted that), maybe it's better just to generate a static CSS file every time the user updates their custom CSS instead, and let Apache do all the heavy lifting like it was born to do? I'm just sayin...

Hope this helps!

  • Hi Mike,Thanks for this. You always give such excellent answers! I was trying to avoid the whole wp_load thing though ( ottopress.com/2010/dont-include-wp-load-please ) Commented Sep 9, 2010 at 11:50
  • @Ash G: heh. Well that's why yours wasn't working! ;-) get_theme_mod() is part of the WordPress API so without /wp-load.php you don't get it! And the API is not modular, it's all or nothing. But I'll argue while Otto's is right in general it's not a big a concern if you are caching for 24 hours or longer with Expires or Cache-Control. And if you really wanted to optimize you could remove the /wp-load.php and use raw MySQL calls to replace the get_theme_mod() function if you like; it's trivial code, only downside is itbreaks the API's encapsulation of theme mod storage details. Commented Sep 9, 2010 at 16:21
  • @Ash G: Of course there's always the last part of my answer that I started with "This Might All Be Overkill Anyway!"... :-) Commented Sep 9, 2010 at 16:30
  • @AshG: Oh, and thanks for the compliment. I learn best by teaching others and I'm trying to become expert on everything there is to know about coding for WordPress so that's why I put the effort into the answers. Helping others helps me; that'a a good win-win, no? :-) Commented Sep 9, 2010 at 16:33
  • 1
    Hi @Ash G: Well you could use file_get_contents() and scan through /wp-config.php looking for $table_prefix and DB_HOST, DB_NAME, DB_USER, and DB_PASSWORD; that wouldn't be hard code to write nor code with too much overhead (compared to all of /wp-load.php) Points to ponder. :) And thanks for the pick and up-vote. Commented Sep 10, 2010 at 0:30

Try coding it like:

<?php $my_custom_css = get_theme_mod('my_custom_css');
  if(!empty($my_custom_css)) {
    echo $my_custom_css;

That should get you around any scope issues with the get_theme_mod() function.

  • Thanks for the help. This doesn't work though. I don't understand why this would get around the scope issue? Commented Sep 9, 2010 at 5:53
  • I don't understand why there would be a scope issue with a function in the first place. But I thought I'd demonstrate the way I've seen similar tasks done in other plugins.
    – dgw
    Commented Sep 10, 2010 at 22:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.