|
Hi
@Ash G:
I didn follow 100%
where you were specifically having problems so I'mt 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 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 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 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
: 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 evidentlyt 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 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 forrmallyn-cachable requests (i.e requests via
HTTPS
) and event cache if that's what you need (i.e. you might want to force a
a 1x1 pixel ad tracking .GIF
t 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 generatething 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 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:
There are several points tote:
-
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 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 andt 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
rightw
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)
:
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 need all those but given how finicky caching can be with different browsers and proxies I figure it doesn 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:
= 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'); echo_default_css(); echo_custom_css(); } exit;
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 =<<
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:
Epilogue:
But I would be remiss to leave the topic without making a closing comments.
Expires in
2020
? Probably Too Extreme.
First, I don really think you want to set
Expires
to the year 2020. Any browsers or proxies that respect
Expires
won 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!
|