Last updated February 14, 2010. Created by justafish on May 5, 2008.
Edited by corey.aufang, bekasu. Log in to edit this page.

Drupal's built-in CSS aggregation works by creating the aggregated files under the "files/css" folder, one could have any number of these as if one css file isn't needed on a particular page compared to another then a new file will be generated and printed appropriately by the $styles variable in page.tpl.php. However, this setup will not work for some people, an example being having drupal running on multiple front end servers with no shared space for "files" (they could be rsynced around from a main server to which editors are adding content) and with page caching turned on. What will happen in this case is there will be windows in which css does not appear on some front end servers because the cached page is being returned without checking to see if the css file actually exists or not.

  • Visitor hits server 1 and generates a cached page and aggregated css
  • Cached page is served to Visitor, this time from server 2
  • CSS file referenced in the cached page does not exist on server 2

A solution to this is to move CSS aggregation duties to template.php and have css files cached under the theme directory (this directory should probably then be excluded from any rsyncing going on between servers, however it will still work even if you don't). Each time a user hits a page on one of the front end servers a string is created from the md5 of the file names and their last modified time. If a file already exists with this string as a name then it's served up, otherwise it's generated and saved in the 'cache' directory.

So firstly make a subdirectory of your theme named 'cache' and change the permissions appropriately so whichever user is executing the code can write to it.

If you're using the Zen theme add this to your sub-theme's template.php (check to see if you already have the function, just add this code into the bottom of it if you do, minus the function declaration) and change STARTERKIT to the name of your theme.

<?php
function STARTERKIT_preprocess_page(&$vars) {
   
$css = drupal_add_css();
   
$css_arr = array();
   
$modifiedDates = '';
   
$fileString = '';
    foreach(
$css['all']['module'] as $css_module => $css_module_on) {
        if(
file_exists($_SERVER['DOCUMENT_ROOT'] . base_path() . $css_module)) {
           
$modifiedDates .= filemtime($_SERVER['DOCUMENT_ROOT'] . base_path() . $css_module);
           
$css_arr[] = $_SERVER['DOCUMENT_ROOT'] . base_path() . $css_module;
           
$fileString .= $css_module . ',';
        }
    }
    foreach(
$css['all']['theme'] as $css_theme => $css_theme_on) {
        if(
file_exists($_SERVER['DOCUMENT_ROOT'] . base_path() . $css_theme)) {
           
$modifiedDates .= filemtime($_SERVER['DOCUMENT_ROOT'] . base_path() . $css_theme);
           
$css_arr[] = $_SERVER['DOCUMENT_ROOT'] . base_path() . $css_theme;
           
$fileString .= $css_theme . ',';
        }
    }
   
$combinedContent = '';
   
$fileName = base_path() . path_to_subtheme() . '/cache/' . md5($fileString . $modifiedDates) . '.css';
   
$file = $_SERVER['DOCUMENT_ROOT'] . $fileName;
    if(!
file_exists($file)) {
        foreach(
$css_arr as $css_file) {
           
$combinedContent .= PHP_EOL.PHP_EOL.file_get_contents($css_file);
        }
       
$fh = fopen($file, 'w');
       
fwrite($fh, $combinedContent);
       
fclose($fh);
    }
   
$vars['styles_aggregated'] = '<style type="text/css" media="all">@import "' . $fileName . '";</style>';
}
?>

If you're using an ordinary theme then you will need to add the following into your template.php (again, you might already have this function).

<?php
function  _phptemplate_variables($hook, $vars) {
   switch(
$hook) {
     case
'page' :
       
$css = drupal_add_css();
       
$css_arr = array();
       
$modifiedDates = '';
       
$fileString = '';
        foreach(
$css['all']['module'] as $css_module => $css_module_on) {
            if(
file_exists($_SERVER['DOCUMENT_ROOT'] . base_path() . $css_module)) {
               
$modifiedDates .= filemtime($_SERVER['DOCUMENT_ROOT'] . base_path() . $css_module);
               
$css_arr[] = $_SERVER['DOCUMENT_ROOT'] . base_path() . $css_module;
               
$fileString .= $css_module . ',';
            }
        }
        foreach(
$css['all']['theme'] as $css_theme => $css_theme_on) {
            if(
file_exists($_SERVER['DOCUMENT_ROOT'] . base_path() . $css_theme)) {
               
$modifiedDates .= filemtime($_SERVER['DOCUMENT_ROOT'] . base_path() . $css_theme);
               
$css_arr[] = $_SERVER['DOCUMENT_ROOT'] . base_path() . $css_theme;
               
$fileString .= $css_theme . ',';
            }
        }
       
$combinedContent = '';
       
$fileName = base_path() . path_to_theme() . '/cache/' . md5($fileString . $modifiedDates) . '.css';
       
$file = $_SERVER['DOCUMENT_ROOT'] . $fileName;
        if(!
file_exists($file)) {
            foreach(
$css_arr as $css_file) {
               
$combinedContent .= PHP_EOL.PHP_EOL.file_get_contents($css_file);
            }
           
$fh = fopen($file, 'w');
           
fwrite($fh, $combinedContent);
           
fclose($fh);
        }
       
$vars['styles_aggregated'] = '<style type="text/css" media="all">@import "' . $fileName . '";</style>';
        break;
   }
   return
$vars;
}
?>

And finally, don't forget to replace the $styles variable in your page.tpl.php with $styles_aggregated

Looking for support? Visit the Drupal.org forums, or join #drupal-support in IRC.

Comments

To get this to work on Drupal 6 I made the following changes.
Change path_to_subtheme() to path_to_theme()
Add . 'subdir/' after each occurrence base_path() where subdir is the subdirectory of the web root our install is in.
Don't forget to flush the cache on the performance page.

JZ

One issue with this is that all relative url()'s in your CSS will break by aggregating the CSS in a new directory.

I wanted alot more than simply aggregating the css files into one, also for other media formats.

I came up with this.

<?php
function YOURSTYLE_preprocess_page(&$vars, $hook) {
   
$css = drupal_add_css(); // We need all CSS Files to process
   
foreach ($css as $cssMedia => $cssSource) {
        foreach (
$cssSource as $cssSourceName => $cssFileArr) {
            foreach (
$cssFileArr as $cssFile => $cssFileOn) {
               
$GLOBALS['cssFile'] = $cssFile;
                if (
file_exists($cssFile)) {
                   
$cssFileContent = file_get_contents($cssFile);
                   
$cssFileContent = preg_replace_callback('#url\((\'|")(.+)\1\)#', '_inline_css_image', $cssFileContent); // See function _inline_css_image
                   
$GLOBALS['cssOutput'][$cssMedia]['fileContent'] .= $cssFileContent . "\n";
                   
$GLOBALS['cssOutput'][$cssMedia]['modifiedDates'] .= filemtime($cssFile);
                   
$GLOBALS['cssOutput'][$cssMedia]['fileString'] .= $cssFile . ',';
                }
            }
        }
       
// COPIED FROM common.inc -> drupal_load_stylesheet AND MODIFIED
        // Perform some safe CSS optimizations.
        // Regexp to match comment blocks.
       
$comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
       
// Regexp to match double quoted strings.
       
$double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
       
// Regexp to match single quoted strings.
       
$single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
       
$GLOBALS['cssOutput'][$cssMedia]['fileContent'] = preg_replace_callback(
                       
"<$double_quot|$single_quot|$comment>Sus", // Match all comment blocks along
                       
"_process_comment", // with double/single quoted strings
                       
$GLOBALS['cssOutput'][$cssMedia]['fileContent']);   // and feed them to _process_comment().
       
$GLOBALS['cssOutput'][$cssMedia]['fileContent'] = preg_replace(
                       
'<\s*([@{}:;,]|\)\s|\s\()\s*>S', // Remove whitespace around separators,
                       
'\1', $GLOBALS['cssOutput'][$cssMedia]['fileContent']);   // but keep space around parentheses.
        ////////////////////////////////////////////////////////////////////////////////////////////
   
}
    unset(
$GLOBALS['cssFile']);
   
$overallSuccess = true;
    foreach (
$GLOBALS['cssOutput'] as $cssMedia => $cssFileData) {
       
$cssCacheFilePath = base_path() . path_to_theme() . '/cache/' . $cssMedia . "_" . md5($cssFileData['fileString'] . $cssFileData['modifiedDates']) . ".css";
       
$cssCacheFile = $_SERVER['DOCUMENT_ROOT'] . $cssCacheFilePath;
       
$fileCreationSuccess = true;
        if (!
file_exists($cssCacheFile)) {
           
// Delete old cache files
           
$cacheLs = scandir(dirname($cssCacheFile));
           
$oldCssCacheFiles = preg_grep("#^" . $cssMedia . "\_#", $cacheLs);
            foreach (
$oldCssCacheFiles as $oldCssCacheFile) {
               
unlink(dirname($cssCacheFile) . "/" . $oldCssCacheFile);
            }
           
// Create new file
           
$fileCreationSuccess = file_put_contents($cssCacheFile, $cssFileData['fileContent']);
        }
       
// Lets get sure everything went right
       
if ($fileCreationSuccess == true || $fileCreationSuccess != false) {
           
$vars['styles_optimized'] .= '<link type="text/css" rel="stylesheet" media="' . $cssMedia . '" href="' . $cssCacheFilePath . '" />';
        } else {
// Or take a note
           
$overallSuccess = false;
        }
    }
    if (!
$overallSuccess) { // If something went wrong, it's better to fall back
       
$vars['styles_optimized'] = $vars['styles'];
    }
    unset(
$GLOBALS['cssOutput']);
    return
$vars;
}
/**
* Used as Callback function for preg_replace_callback in our hook_preprocess_page function
* @param array $match This will be the Matches and stuff we get
* @return string Override of the url('/path') part, or the exact part if url'ed file not found.
*/
function _inline_css_image($match) {// If the string is a
   
if (strpos($match[2], "/") == 0 && file_exists($_SERVER['DOCUMENT_ROOT'] . $match[2])) { // Check for file relative to root.
       
$match[2] = $_SERVER['DOCUMENT_ROOT'] . $match[2];
    } else if (
file_exists(dirname($GLOBALS['cssFile']) . "/" . $match[2])) { // Check for file relative from css file
       
$match[2] = dirname($GLOBALS['cssFile']) . "/" . $match[2];
    } else {
// Fallback if nothing catched
       
return 'url(\'' . $match[2] . '\')';
    }
    return
'url(data:' . file_get_mimetype($match[2]) . ';base64,' . base64_encode(file_get_contents($match[2])) . ')';
}
?>

What it (should do) does in short:

  • replacing url() paths with base64 data. This saves alot of requests.
  • Aggregating all files of a media type into one css file
  • Use some compressions normaly done by drupal
  • Cleaning up the cache directory if there was a change of the css files and the css files have to be reaggregated.

Note that the theme I made this for is based on Zen. I don't know if this works with other themes!

Please let me know about bugs or so. This code might be stupid at some points, forgive me. :)

Edit: This is also a workaround for private downloads and css compression!