A Gulp recipe for timestamps

Making sure you serve your users the correct collection of assets is a real challenge, even for static websites, but you can combine some logic on a task runner and your site generator templating to solve this riddle.

A quick solution in Jekyll would be to get the last build time and put it at the end of the urls as a query parameter.

<link
  href="/css/main.css?v={{ site.time | date_to_xmlschema }}"
/>

The problem is that not every build is triggered by a change in our styles or scripts, it could be a change in a template or a post.

We don't want to generate new timestamps in these cases because we will force returning users to download assets again when they haven't really changed.

Come together

On one side, you might be using a task runner to process your assets, and probably each time you do that a new timestamp should be created.

On the other side, your site generator must know that somehow.

For Jekyll and Gulp, I thought a good communication channel could be a simple .json file which both are able to read.

The timestamps

Jekyll gives us a _data folder to place files whose content can be later accessed by its templating engine, so we're going to create a new timestamps.json file inside this directory.

{
  "scripts": 0,
  "styles": 0,
  "images": 0
}

Notice we are saving a timestamp for each type of resource.

The recipe

Each time a build modifies a file from one of these assets we need to update the corresponding timestamp.

var gulp = require('gulp');
var less = require('gulp-less');
var minify = require('gulp-clean-css');

// styles task
gulp.task('styles', function () {
  return (
    gulp
      .src('./styles/main.less')
      .pipe(less())
      .pipe(minify())
      .pipe(gulp.dest('./assets/'))
      // update timestamp for styles
      .pipe(updateTimestamp('styles'))
  );
});

This is a simple task in Gulp where we process less files and then minify the output, but at the end of it we pipe a custom method.

var gulp = require('gulp');
var util = require('gulp-util');
var file = require('gulp-file');

// import timestamps
var timestamps = require('./_data/timestamps.json');

// update timestamp
function updateTimestamp(stamp) {
  timestamps[stamp] = Date.now();

  return file('timestamps.json', JSON.stringify(timestamps, null, 2), {
    src: true
  }).pipe(gulp.dest('./_data'));
}

This method imports the timestamps.json data, updates a given timestamp and then overrides the file with the modification.

The Date.now method returns the number of milliseconds elapsed since 1 January 1970, generating a unique number each time is invoked which comes handy for us here.

Let's get this recipe together.

var gulp = require('gulp');
var less = require('gulp-less');
var minify = require('gulp-clean-css');
var file = require('gulp-file');

// import timestamps
var timestamps = require('./_data/timestamps.json');

// update timestamp
function updateTimestamp(stamp) {
  timestamps[stamp] = Date.now();

  return file('timestamps.json', JSON.stringify(timestamps, null, 2), {
    src: true
  }).pipe(gulp.dest('./_data'));
}

// styles task
gulp.task('styles', function () {
  return (
    gulp
      .src('./styles/main.less')
      .pipe(less())
      .pipe(minify())
      .pipe(gulp.dest('./assets/'))
      // update timestamp for styles
      .pipe(updateTimestamp('styles'))
  );
});

The method can be easily used in other tasks by changing the string passed to the updateTimestamp method.

Timestamps on templates

Since we are placing our timestamps file in the _data folder, its content is globally available for use under the site.data namespace in Jekyll.

<link
  href="/assets/main.css?v={{ site.data.timestamps.styles }}"
/>

The result will be similar to this.

<link href="/assets/main.css?v=1464493602124" />

Wrap-up

I've used Gulp and Jekyll in this article but it could be easily adapted to any site generator that accepts this type of data access through .json files and any modern task runner.

Updates

18 JUL 2020 — Though forcing the re-fetching of a new resource through a query in the URL might work, ideally the timestamp should be part of the file name to completely beat browser cache and force users to new versions of the project.

Do you want me to write for your publication? Click here to contact me via email.