UPDATE: Due to recent changes to our website the code snippet (the valuable part of this blog post) got accidentally lost. If you read this already, please come back to see the sample code. Thanks!

Often you want to add CSS files, scripts, feed icons or even set Drupal's page title from the theme layer. The most obvious place to call Drupal's functions for these tasks is probably in a page preprocess function. However calling the following Drupal API functions from a theme or module's NAME_preprocess_page() function often doesn't work;

The reason they often don't work when called from NAME_preprocess_page() (aka HOOK_preprocess_page(), THEMENAME_preprocess_page() and – in the examples in this article – custom_preprocess_page()) is because any custom preprocess function that is defined in the theme layer (or even in modules in most circumstances) gets called after Drupal core's preprocess functions. By the time your custom_preprocess_page() gets called, Drupal core's template_preprocess_page() has already rendered the page.tpl.php variables $styles, $scripts, $head, $head_title, $title and $messages into HTML strings.

(This is not a bug in Drupal, but is due to the nature of overriding which is a fundamental principle on which Drupal's theme layer is designed.)

You can access the rendered HTML string from custom_preprocess_page() with $vars['styles'], $vars['scripts'], $vars['head'] etc., however adding new stylesheets, JS scripts, elements, Drupal messages or setting the title would require either;

  1. Instead of setting the new item, append it's rendered HTML string representation to the respective $vars variable
    • This often needs quite a bit of amount of difficult-to-maintain code.
  2. Setting the new item then re-rendering the HTML string contents of the respective $vars variable
    • This requires quite in depth understanding of the get/set function pair and/or research on api.drupal.org, and impacts performance by duplicating processing. It's also beyond the range of programming and PHP skills of many themers.

There are 3 workaround solutions that I know of for this problem.

  1. These functions can be successfully called from hook_init() in a custom module. The disadvantages of this solution are that you need to do it in a module (themes can not implement hook_init()), and that there is a performance hit, since the functions execute even on pages that are not requesting a themed HTML page, such as the first request for an imagecache image that hasn't yet been cached, asynchronous requests for JSON, HTML or other ajaxy data, and site services such as calls to xmlrpc.php, RSS feeds and other XML requests. drupal_set_message() will also not work if called from hook_init() since hook_init() is not called for pages that are cached for anonymous users, and pages with messages are not cached (IIRC).
  2. These functions can also be called from your theme's template.php in the global scope. Doing things in the global scope is a poor practice that should be avoided if possible, so this isn't ideal. However this solution does allow a theme to add these items. It doesn't solve the performance hit problems though; In fact it probably makes them slightly worse.
  3. While the performance hit is not significant in most cases, a more robust solution is sometimes required. This can be fairly readily achieved with hook_theme_registry_alter(). To do this, add the following code to a custom module (or theme) and rename the function prefixes from custom_ to the machine name of your module or theme. It should also work with these by putting the code into template.php though I haven't actually tested that.
/**
* Implementation of hook_theme_registry_alter().
*/
function custom_theme_registry_alter(&$theme_registry) {
// Makes sure the early preprocess page function gets called first.
array_unshift($theme_registry['page']['preprocess functions'], 'custom_early_preprocess_page');

// And the late preprocess page function is called last.  You probably won't need this.
$theme_registry['page']['preprocess functions'][] = 'custom_late_preprocess_page';
}

/**
* These are slightly different to most hook_preprocess_page() implementations;
*   1. They have a slightly different name to avoid auto-detection by the theme registry.
*   2. They are manually added to the theme registry by custom_theme_registry_alter().
*/

/**
* Implementation of hook_preprocess_THEMEHOOK().
*
* The very FIRST preprocess page function to be called.
* This allows it to add CSS, scripts etc. late in page rendering process, but not too late to miss being added to the page completely.
*/
function custom_early_preprocess_page(&$vars, $hook) {
// These WILL have the desired effect here.
drupal_add_css();
drupal_add_js();
drupal_add_feed();
drupal_html_header();
drupal_set_message();
}

/**
* Implementation of hook_preprocess_THEMEHOOK().
*
* The very LAST preprocess page function to be called.
* If necessary, HTML strings can be appended to rendered HTML strings here.  You probably won't need this.
*/
function target_late_preprocess_page(&$vars, $hook) {
// These will NOT have the desired effect here.
drupal_add_css();
drupal_add_js();
drupal_add_feed();
drupal_html_header();

// drupal_set_message() stores the data in the user's SESSION.  So the message will not show on THIS page, but it WILL show on the next page the same user requests from Drupal.
 
drupal_set_message();
}
?>