I have a few sites on a shared host that is running Apache 2. I would like to compress the HTML, CSS and Javascript that is delivered to the browser. The host has disabled mod_d
Sorry about the delay - it's a busy week for me.
Assumptions:
.htaccess
is in the same file as compress.php
static
subdirectoryI started my solution from setting the following directives in .htaccess:
RewriteEngine on
RewriteRule ^static/.+\.(js|ico|gif|jpg|jpeg|png|css|swf)$ compress.php [NC]
It's required that your provider allows you to override mod_rewrite
options in .htaccess
files.
Then the compress.php file itself can look like this:
<?php
$basedir = realpath( dirname($_SERVER['SCRIPT_FILENAME']) );
$file = realpath( $basedir . $_SERVER["REQUEST_URI"] );
if( !file_exists($file) && strpos($file, $basedir) === 0 ) {
header("HTTP/1.0 404 Not Found");
print "File does not exist.";
exit();
}
$components = split('\.', basename($file));
$extension = strtolower( array_pop($components) );
switch($extension)
{
case 'css':
$mime = "text/css";
break;
default:
$mime = "text/plain";
}
header( "Content-Type: " . $mime );
readfile($file);
You should of course add more mime types to the switch statement. I didn't want to make the solution dependant on the pecl fileinfo
extension or any other magical mime type detecting libraries - this is the simplest approach.
As for securing the script - I do a translation to a real path in the file system so no hacked '../../../etc/passwd' or other shellscript file paths don't go through.
That's the
$basedir = realpath( dirname($_SERVER['SCRIPT_FILENAME']) );
$file = realpath( $basedir . $_SERVER["REQUEST_URI"] );
snippet. Although I'm pretty sure most of the paths that are in other hierarchy than $basedir will get handled by the Apache before they even reach the script.
Also I check if the resulting path is inside the script's directory tree. Add the headers for cache control as pilif suggested and you should have a working solution to your problem.
What I do:
js
and stylesheets in a css
dir, respectively.In the Apache configuration, I add directives like so:
<Directory /data/www/path/to/some/site/js/>
AddHandler application/x-httpd-php .js
php_value auto_prepend_file gzip-js.php
php_flag zlib.output_compression On
</Directory>
<Directory /data/www/path/to/some/site/css/>
AddHandler application/x-httpd-php .css
php_value auto_prepend_file gzip-css.php
php_flag zlib.output_compression On
</Directory>
gzip-js.php in the js
directory looks like this:
<?php
header("Content-type: text/javascript; charset: UTF-8");
?>
…and gzip-cs.php in the css
directory looks like this:
<?php
header("Content-type: text/css; charset: UTF-8");
?>
This may not be the most elegant solution, but it most certainly is a simple one that requires few changes and works well.
You can try your luck with mod_rewrite.
Create a script that takes a local static file name as input, through e.g. $_SERVER['QUERY_STRING']
and outputs it in compressed form. Many providers don't allow configuring mod_rewrite
with .htaccess
files or have it completely disabled though.
If you haven't used rewrite before, I recommend a good beginner's guide, like probably this one. This way you can make the apache redirect all requests for a static file to a php script. style.css will be redirected to compress.php?style.css for instance.
As always be extremely cautious on the input you accept or you have an XSS
exploit on your hands!
Instead of gzipping on the fly when users request the CSS and JavaScript files, you could gzip them ahead of time. As long as Apache serves them with the right headers, you’re golden.
For example, on Mac OS X, gzipping a file on the command line is as easy as:
gzip -c styles.css > styles-gzip.css
Might not be the sort of workflow that works for you though.
what ever you do, be careful about caching on the client side:
Browsers do all sort of tricks to try and minimize the bandwith and there are many ways in the HTTP protocol to do that, all of which are dealt with by apache - if you are just serving a local file.
If you are not, then it's your responsibility.
Have a look at least at the ETag and the If-Modified-Since mechanics which are supported by all current browsers and seem to be the most robust way to query the server for updated content.
A possible way to serve a CSS file to browsers using the If-Modified-Since-Header is something like this (the empty headers to turn off any non-caching headers PHP sends per default):
$p = 'path/to/css/file'
$i = stat($p);
if ($_SERVER['HTTP_IF_MODIFIED_SINCE']){
$imd = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if ( ($imd > 0) && ($imd >= $i['mtime'])){
header('HTTP/1.0 304 Not Modified');
header('Expires:');
header('Cache-Control:');
header('Last-Modified: '.date('r', $i['mtime']));
exit;
}
}
header('Last-Modified: '.date('r', $i['mtime']));
header('Content-Type: text/css');
header('Content-Length: '.filesize($p));
header('Cache-Control:');
header('Pragma:');
header('Expires:');
readfile($p);
The code will use the if-modified-since-header the browser sends to check if the actual file on the server has changed since the date the browser has given. If so, the file is sent, otherwise, a 304 Not Modified is returned and the browser does not have to re-download the whole content (and if it's intelligent enough, it keeps the parsed CSS around in memory too).
There is another mechanic involving the server sending a unique ETag-Header for each piece of content. The Client will send that back using an If-None-Match header allowing the server to decide not only on the date of last modification but also on the content itself.
This just makes the code more complicated though, so I have left it out. FF, IE and Opera (probably Safari too) all send the If-Modified-Since header when they receive content with a Last-Modified header attached, so this works fine.
Also keep in mind that certain versions of IE (or the JScript-Runtime it uses) still have problems with GZIP-transferred content.
Oh. And I know that's not part of the question, but so does Acrobat in some versions. I've had cases and cases of white screens while serving PDFs with gzip transfer encoding.