Month: June 2018

  • CSS & JS Concatenation in WordPress

    CSS & JS Concatenation in WordPress

    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 🙂

  • New Beginning

    I’m trying to force myself to do a few new things:

    • Blog on a schedule
    • Learn new stuff

    Because of that, I’m starting over on derrick.blog and playing around here.  I’ll be testing new WordPress stuff here, especially Gutenberg.

  • Gutenberg and Markdown

    Gutenberg and Markdown

    Update:

    Jetpack now has its own built-in Markdown block:

    I’d highly recommend using it instead 🙂


    Original post below:

    I’ve been playing around with Gutenberg a tiny bit recently and have realized that, at least in my case, it kind of eats the Jetpack Markdown module and doesn’t offer full markdown support.

    I honestly know nothing about writing blocks for Gutenberg, but luckily GitHub user nuzzio does 🙂

    This recent, but now closed PR to Jetpack was pretty much everything necessary to build a working block.

    I’ve put my code up on GitHub as a WordPress plugin:

    Like I said, I don’t know what I’m doing–so consider this plugin super beta.