Using Dynamic Hashes in File Names with WordPress’ Enqueue Function

The following is a guest post by Pascal Klau, a self-described trainee web developer from Germany working with WordPress, Gulp, and more recently VueJS and Webpack.

During my journey of learning web technology, I stumbled upon the importance of file caching. It's a pretty important thing for web page performance. I also sometimes notice that when a file was changed, and you refreshed the page, the browser can still be using the old version (because of the cache). There is a solution!

Hashes Break Caches

Adding a hash in the file name lets the browser think it is a totally different file. Thus, "breaking the cache". For example, if the file `main.3hZ9.js` changes to `main.8a3s.js`, the browser will definitely re-download it. This renaming can be done automatically by task runners.

There is a problem for those of us using WordPress though. You need to manually enqueue the files by name but you can't know the randomly generated hash value.

wp_enqueue_script('main', get_template_directory_uri() . '/js/main.3hZ9.js', array('jquery'), null, true);

Adding the Hashes with Gulp

Before we tackle the WordPress issue, here is how to add hashes.

Since I'm using Gulp, gulp-hash came in handy. If you are using Grunt, there are equivalents.

// ES5
var gulp = require('gulp'),
    hash = require('gulp-hash');

var options = {
    hashLength: 4,
    template: '<%= name %>.<%= hash %><%= ext %>'
};

gulp.src('./js/**/*.js')
  .pipe(hash(options))
  .pipe(gulp.dest('build'));
// ES6 + Gulp 4
import gulp from 'gulp'
import hash from 'gulp-hash'

let options = {
  hashLength: 4,
  template: '<%= name %>.<%= hash %><%= ext %>'
};

export function hashing() {
  return gulp.src('./js/**/*.js')
    .pipe(hash(options))
    .pipe(gulp.dest('build'))
};

const dev = gulp.series(hashing);
export { dev };
export default dev;

The WordPress Solution

Here's how we can enqueue the hashed files automatically! The trick is that we don't need to know the hash name, we'll have PHP find the file anyway.

First we get ready to look through the file system at the directory where our asset is.

function enqueue_files() {
  $dirJS = new DirectoryIterator(get_stylesheet_directory() . '/js');
}

Now we loop through all the files and filter for the ones that end in `.js`.

foreach ($dirJS as $file) {

  if (pathinfo($file, PATHINFO_EXTENSION) === 'js') {

  }

}

Dependencies need a name in WordPress, so we can base that off the file name. "main" would be better than "main.3hZ9" (because it doesn't change), so we can strip the hash off for the enqueue name.

foreach ($dirJS as $file) {

    if (pathinfo($file, PATHINFO_EXTENSION) === 'js') {
        $fullName = basename($file);    // main.3hZ9.js
        $name = substr(basename($fullName), 0, strpos(basename($fullName), '.')); // main
    }
}

In WordPress, any asset you enqueue may need dependencies. You declare these yourself by passing an array of them to the enqueue function. Here, we're checking the file and adding dependencies if needed:

foreach ($dirJS as $file) {

    if (pathinfo($file, PATHINFO_EXTENSION) === 'js') {
        $fullName = basename($file);
        $name = substr(basename($fullName), 0, strpos(basename($fullName), '.'));

        switch($name) {

            case 'main':
                $deps = array('vendor');
                break;

            default:
                $deps = null;               
                break;

        }
    }
}

All together now!

We have everything we need to enqueue the asset now, so here it is all together:

foreach ($dirJS as $file) {

  if (pathinfo($file, PATHINFO_EXTENSION) === 'js') {
    $fullName = basename($file);
    $name = substr(basename($fullName), 0, strpos(basename($fullName), '.'));

    switch($name) {

        case 'main':
            $deps = array('vendor');
            break;

        default:
            $deps = null;               
            break;

    }

    wp_enqueue_script( $name, get_template_directory_uri() . '/js/' . $fullName, $deps, null, true );

  }

}

It works the same way for CSS files except the slightly different WP enqueue function.

What about PHP filemtime?

With WordPress, you can add a cache-breaking URL parameter in its enqueue function with filemtime. It adds a timestamp every time you modify the file:

$file = get_template_directory_uri() . '/js/main.js'
wp_enqueue_script( 'main', $file, null, filemtime($file), true );

// => [...]/main.js?1203291283

Why am I not using this technique?

I am bundling JavaScript files into `main.js` and `vendor.js` (in my case, with Webpack). When editing `main.js`, the timestamp for `vendor.js` changes as well. So it would be downloaded every time without being any different at all.

Hashes, on the other hand, stay the same.

Conclusion

Now you can add hashes dynamically to files when they change, through Gulp. And you can enqueue those files in WordPress without needing to know the new file names directory.