Adding a service worker to Jekyll

Here is how we added a service worker to this very site

A service worker is a script that your browser runs in the background. It is separate from the code on your website, and it allows the browser to do lots of cool things, like setting up offline access and preloading important content.

We wanted to add it to tosbourn.com to help with caching primarily and make some core pages available offline for people with spotty internet connections.

There were three files we had to create or edit to add a service worker to our Jekyll website;

  • Create the service worker JS
  • Create an offline page
  • Edit the main template page to tell it to load in the service worker

Service Worker JS

At the root of our project, create a file called service-worker.js with the following code;

'use strict';

const offlineCache = 'offline-2021-06-17'; // The cache key, you can override this to manually clear the cache
const offlinePage = '/offline/'; // Where your offline page lives
const contactPage = '/contact/'; // A key page you want cached
const hireUs = '/hire-us/'; // Another key page to cache

// Delete caches that do not match the current version of the service worker.
function clearOldCaches() {
  return caches.keys().then(keys => {
    return Promise.all(
      keys
        .filter(key => key.indexOf(offlineCache) !== 0)
        .map(key => caches.delete(key))
    );
  });
}

// Precache specific pages
function cacheOfflinePage() {
  return caches.open(offlineCache)
    .then(cache => {
      return cache.addAll([offlinePage, contactPage, hireUs]);
    })
    .catch(error => {})
}

// Install the service worker
 self.addEventListener('install', event => {
  event.waitUntil(
    cacheOfflinePage()
    .then( () => self.skipWaiting() )
  );
});

// Activate the service worker
self.addEventListener('activate', event => {
  event.waitUntil(
    clearOldCaches()
      .then(() => self.clients.claim())
  );
});

// Try and serve up cached page, or else show networked page, or else show offline page

self.addEventListener('fetch', event => {
  let request = event.request;

  event.respondWith(
    caches.match(request)
      .then(response => {
      return response || fetch(request)
        .then( response => {
          // NETWORK
          if (response && response.ok) {
            let copy = response.clone();
            caches.open(offlineCache)
              .then( cache => cache.put(request, copy) );
          }
            return response;
        })
      .catch( error => {
        // OFFLINE
        if (request.mode == 'navigate') {
          return caches.match(offlinePage);
        } 
      });
    })
  );
});

You will want to edit the pages you wish to cache; I would suggest keeping the number to a minimum. You don’t want your user to cache a lot of content they may never actually need.

Offline Page

In the above script we referenced const offlinePage = '/offline/'; in a few places. This is the page that will get displayed if the browser cannot connect to the internet. Offline pages are a useful tool to help control what the user sees when their device is offline.

If you want your offline page to be accessible via /offline/, you would create an offline.md or offline.html file in the root of your project with the content you want. We have;

---
layout: page
title: Offline
description: It looks like you do not have internet access; please try again soon. Some of our pages will continue to work.
---

You could include information like a phone number or offline ways of accessing content.

Loading the service worker

We need a way of telling the browser to look for our service worker. You do this by adding some JavaScript to every page you want to be managed by the service worker; I would suggest it appears on all of them.

Our setup has a layout called default.md, which is the template all other layouts end up inheriting. We edited this page and added the following near the bottom of the page;


{% if jekyll.environment != "development" %}
    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker
          .register("/service-worker.js")
          .then(function(registration) {
            console.log("Success: " + registration.scope);
          })
          .catch(function(err) {
            console.log("Error: " + err);
          });
      }
    </script>
{% endif %}

Within the script tags, we check for serviceWorker inside the navigator object, and if it is there, we register our service worker.

You’ll note we wrap this in some liquid if jekyll.environment != "development" - this will only put out the service worker if you aren’t in development. This is because caching stuff locally can end up confusing things as you are making changes.

Breaking the cache

If you want to break the cache to ensure that anyone visiting your website has to talk to the network to get a fresh version of a page, edit the offlineCache value we set in our service-worker.js file. I tend to use a datestamp like 2021-06-17, so I can easily understand when the last time I broke the cache was.


Recent posts View all

Ruby

Testing Routes with RSpec

Testing routes can give you more confidence and help drive application development; here is how to do it with RSpec

Ruby

How to ignore Bullet in RSpec tests

Using Bullet during a test can pick up mistakes but also has false negatives; here is an easy way to ignore them