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:
<?php
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:
<?php
header("Expires: Thu, 31 Dec 2020 20:00:00 GMT");
header('Content-type: text/css');
?>
p{color:blue;}
/** Custom CSS **/
<?php
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:
<?php
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.
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; assumingDOCUMENT_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):
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
header expectz a date in the (PHPgmdate()
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 onlocalhost
with Safari 5.0 on Mac OS X I was never able to get the browser to respect theExpires
header; it always requested the file again (if someone can explain this I’d be grateful.) Here’s the example you gave from above:Cache-Control
header is easier to work with than theExpires
header because you only need to specify the number of time in seconds asmax-age
meaning you don’t have to come up with an exact date format in string form that is easy to get wrong. AdditionallyCache-Control
allows several other options such as being able to tell the client to always re-validated the cache using themustrevalidate
option andpublic
when you want to force caching for normally non-cachable requests (i.e requests viaHTTPS
) and even not cache if that’s what you need (i.e. you might want to force a a 1×1 pixel ad tracking .GIF not to be cached.) LikeExpires
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):Last-Modified
response header and If-Modified-Since
request header pair. These also use the same GMT date format that theExpires
header use but they do a handshake between client and server. The PHP script needs to send aLast-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 anIf-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 a200 Ok
or a304 Not Modified
. Here’s an example of serving theLast-Modified
header using the current time (which is not what we want to do; see the later example for what we actually need):And here is how you’d read the
Last-Modified
returned by the browser via theIf-Modified-Since
header:ETag
response header and theIf-None-Match
request header pair. TheETag
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 server200 Ok
otherwise generate nothing and serve a304 Not Modified
. Here’s an example of setting anETag
using the current time:And here is how you’d read the
ETag
returned by the browser via theIf-None-Match
header:Now that we’ve covered all that let’s look at the actual code we need:
Serving the CSS file via
init
andwp_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’sfunctions.php
file or even in a plugin if desired:There are several points to note:
is_admin()
to avoid loading the CSS while in the admin (unless you want that…),get_theme_mod()
to load the CSS with a default version of1.00
(more on that in a bit),get_stylesheet_directory_uri()
to grab the correct directory for the current theme, even if the current theme is a child theme,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 emptyarray()
means this CSS has no dependencies (although in real world it would often have one or more), and$version
; Probably the most important one, we are tellingwp_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
andLast-Modified
when User Updates CSSSo 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: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 (wherenew_custom_css
would likely get assigned via a user submitted$_POST
instead of by hardcoding as you see here):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 theIf-Modifed-Since
against the savedLast-Modified
value and theIf-None-Match
against theETag
which was derived from the savedLast-Modified
value and if neither have changed just sets the header to304 Not Modifed
and branches to the end.If however either of those have changed it generates the
Expires
,Cache-Control
.Last-Modified
andEtag
headers as well as a200 Ok
and indicating that the content type istext/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:
So with these three major bits of code; 1.) the
add_php_powered_css()
function called by theinit
hook, 2.) theset_my_custom_css()
function called by whatever code allows the user to update their custom CSS, and lastly 3.) themy_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:
Cache it! Solve PHP Performance Problems – A typical SitePoint Uber-Article; five (5) long pages of all things caching and PHP, covering HTTP and other forms of caching.
CSS Caching Hack – This is what I did with
$version
but he explains more in-depth than I did.php: howto control page caching – Good details on using HTTP headers with PHP.
Epilogue:
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 respectExpires
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:
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!
Try coding it like:
That should get you around any scope issues with the
get_theme_mod()
function.