At WordPress.com VIP one of the features we have on our platform is automated concatenation of Javascript and CSS files when registered through the core WordPress wp_enqueue__*()
functions.
We do this using the nginx-http-concat
plugin:
This plugin was written to work with nginx, but the server running derrick.blog
is Apache. I’ve worked around this and have nginx-http-concat
running fully in WordPress, with added caching.
The bulk of the plugin is this file, which does all of the work of caching and calling the nignx-http-concat
plugin:
<?php
// phpcs:disable WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.ValidatedSanitizedInput, WordPress.VIP.FileSystemWritesDisallow, WordPress.VIP.RestrictedFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents, WordPress.WP.AlternativeFunctions.json_encode_json_encode
if ( isset( $_SERVER['REQUEST_URI'] ) && '/_static/' === substr( $_SERVER['REQUEST_URI'], 0, 9 ) ) {
$cache_file = WP_HTTP_CONCAT_CACHE . '/' . md5( $_SERVER['REQUEST_URI'] );
$cache_file_meta = WP_HTTP_CONCAT_CACHE . '/meta-' . md5( $_SERVER['REQUEST_URI'] );
if ( file_exists( $cache_file ) ) {
if ( time() - filemtime( $cache_file ) > 2 * 3600 ) {
// file older than 2 hours, delete cache.
unlink( $cache_file );
unlink( $cache_file_meta );
} else {
// file younger than 2 hours, return cache.
if ( file_exists( $cache_file_meta ) ) {
$meta = json_decode( file_get_contents( $cache_file_meta ) );
if ( null !== $meta && isset( $meta->headers ) ) {
foreach ( $meta->headers as $header ) {
header( $header );
}
}
}
$etag = '"' . md5( file_get_contents( $cache_file ) ) . '"';
ob_start( 'ob_gzhandler' );
header( 'x-http-concat: cached' );
header( 'Cache-Control: max-age=' . 31536000 );
header( 'ETag: ' . $etag );
if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
if ( strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) < filemtime( $cache_file ) ) {
header( 'HTTP/1.1 304 Not Modified' );
exit;
}
}
echo file_get_contents( $cache_file ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
$output = ob_get_clean();
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
die();
}
}
ob_start( 'ob_gzhandler' );
require_once 'nginx-http-concat/ngx-http-concat.php';
$output = ob_get_clean();
$etag = '"' . md5( file_get_contents( $output ) ) . '"';
$meta = array(
'headers' => headers_list(),
);
header( 'x-http-concat: uncached' );
header( 'Cache-Control: max-age=' . 31536000 );
header( 'ETag: ' . $etag );
if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
if ( strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) < filemtime( $cache_file ) ) {
header( 'HTTP/1.1 304 Not Modified' );
exit;
}
}
file_put_contents( $cache_file, $output );
file_put_contents( $cache_file_meta, json_encode( $meta ) );
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
die();
}
Code language: HTML, XML (xml)
This little bit of code in wp-config.php
is what calls the above file, before WordPress even initializes, to make this as speedy as possible:
define( 'WP_HTTP_CONCAT_CACHE', dirname(__FILE__) . '/wp-content/cache/http-concat-cache' );
require_once dirname(__FILE__) . '/wp-content/mu-plugins/emrikol-defaults/config-nginx-http-concat.php';
Code language: PHP (php)
Finally, in an mu-plugin
these lines enable the nginx-http-concat
plugin:
require_once( plugin_dir_path( __FILE__ ) . 'emrikol-defaults/nginx-http-concat/cssconcat.php' );
require_once( plugin_dir_path( __FILE__ ) . 'emrikol-defaults/nginx-http-concat/jsconcat.php' );
Code language: PHP (php)
All of this could definitely be packed into a legit plugin, and even leave room for other features, such as:
- An admin UI for enabling/disabling under certain condition
- A “clear cache” button
- A cron event to regularly delete expired cache items
As it is now though, I’m just leaving it be to see how well it works. Wish me luck 🙂
Leave a Reply