How To Get A Jekyll Blog Work Offline Using Service Workers

jekyll blog

Short Bytes: Did you know that with the advent of Service Workers, one could start making websites work offline! Such web apps are called PWAs (Progressive Web Apps). In this how-to, I’m going to help you out to use a service worker to make a Jekyll-based blog/website work offline and some cool things that come with it.

(NOTE: The snippets of code in the article are taken from my blog’s code repository. It can be referred if needed. If you are new to Jekyll, you could read 3 Series Article on CSS Tricks.)

BACKGROUND

Earlier there used to be a yuk YAML file based App Cache, which was very hard coded in nature and couldn’t be used to cache assets and web pages dynamically. Enter, Service Workers. Simple plain event based Javascript API to do dynamic service worker caching to store assets, so that they could be used to serve the web pages when there’s no network.

Service workers landed in Chrome Canary in 2014, but its spec is still being revised/added upon and designed. In 2015 and 2016, Google’s Chrome team spread the word out heavily of this new technology in the browsers. Only Apple has not supported this (even at the time of writing this article) on their devices (for unknown reasons) [They also aren’t actively participating in any spec discussions about service workers too].

What is a service worker? Basically, it is a web worker on steroids. One characteristic of a web worker is that all the tasks of a web worker run separately (asynchronously) from the main JavaScript execution thread (event loop). This feature helps run CPU or memory intensive tasks, for example, complicated calculations without compromising the performance of your User Interface of the web app.

Service Worker lets us cache (store for a long time) assets, such as JavaScript, CSS, HTML, image, font files in the service worker cache of the browser, so the next time the user loads that page, it is loaded almost instantly. And also since in this caching strategy, the browser looks for the availability of the assets first from service worker cache, the web page is served even when one is offline! If any asset is not there in the cache, then a network request is sent to fetch it.

Service worker also enables the push notifications that are common to see on many websites these days including Facebook, Whatsapp on web and Twitter. We’ll primarily be talking about the offline feature. This how-to is specific to Jekyll, however, most of the service worker code can be generally applied to any website.

Since Jekyll serves static contents (it’s a static site generator, duh!), our service worker code would be very basic and easy to understand.

LET’S ROLL:

On all the relevant pages, the following piece of script gets executed. It’s doing following things:

  1.  Checking for the existence of service worker API in the browser and registering the service worker.
  2. When the service worker has been activated, put a nice little toast/chip message to the user that the website is ready to be used offline now.
function showOfflineToast() {
  let offlineToast = document.querySelector('.offline-ready');
  offlineToast.classList.add('active');
  setTimeout(function(){ 
    offlineToast.className = offlineToast.className.replace("active", "").trim();
  }, 5500);
}

// (1)
if (navigator.serviceWorker) {
  navigator.serviceWorker.register('/sw.js').then(function(reg) {
      if (!reg.installing) return;
      console.log("[*] ServiceWorker is installing...");

      var worker = reg.installing;

      worker.addEventListener('statechange', function() {
          if (worker.state == 'redundant') {
              console.log('[*] Install failed');
          }
          if (worker.state == 'installed') {
              console.log('[*] Install successful!');
          }
          // (2)
          if (worker.state == 'activated' && !navigator.serviceWorker.controller) {
            showOfflineToast();
          }
      });
  });
}

gdad-s-river.github.io-(iPhone 6)

You can add this piece of code in a file say serviceWorker.html inside the includes directory of your Jekyll code and include it in default.html using Jekyll’s liquid templating engine

<!DOCTYPE html>
<html>

  {% include head.html %}

  <body>
    {% include header.html %}

    <div class="page-content">
      <div class="wrapper">
        {{ content }}
      </div>
    </div>

    {% include footer.html %}

    <!-- Contains the above code in a script tag-->
    {% include serviceWorker.html %} 

    <div class="offline-ready">Site is Ready for Offline Use</div>
  </body>

</html>

Now to the actual service worker code that does the magic. This code resides in sw.js at your Jekyll Code’s Root.

//sw.js
---
layout: null
---

const staticCacheName = "gdad-s-river-static-v61";

console.log("installing service worker");

const filesToCache = [
  "/",
  {% for page in site.html_pages %}
    '{{ page.url }}',
  {% endfor %}
  {% for post in site.posts %}
    '{{ post.url }}',
  {% endfor %}

  // can be automated rather than manual entries
  "/assets/images/bhavri-github-callbacks.png",
  "/assets/images/bhavri-github-issues.png",
  "/assets/images/jakethecake-svg-line-anime.png",
  "/assets/images/svg-animated-mast-text-shapes-tweet.png",
  "css/main.css",
  "/about/",
  "/index.html"
];

staticCacheName is the cache version which is to be updated every time I make some changes to the cached responses (for example I make a change in say a CSS file or a blog post). And, I’m just defining what requests I want to intercept and cache in an array (used in the next snippet).

//sw.js

self.addEventListener("install", function(e){
  self.skipWaiting();
  e.waitUntil(
    caches.open(staticCacheName).then(function(cache){
      return cache.addAll(filesToCache);
    })
  )
});

self.skipWaiting, is to say, that every time this sw.js file changes, the newer version of the service worker shouldn’t be queued, but made active immediately (One could ask for user prompt to refresh the page giving a message like Webpage has been updated/changed, click Refresh to load new posts or whatever.), throwing away the older version.

java-and-android-square-ad-1

e.waitUntil quoting from MDN website:

“The ExtendableEvent.waitUntil() method extends the lifetime of the event. In service workers, extending the life of an event prevents the browser from terminating the service worker before asynchronous operations within the event have completed.”

I open up a cache named gdad-s-river-static-v61, which returns a promise with my cache name, and then I call cache.addAll (which uses the fetch API in the background), which fetches all the requests in the array provided and caches them.

On to the next snippet!

//sw.js

self.addEventListener("activate", function(e){
  e.waitUntil(
    caches.keys().then(function(cacheNames){
      return Promise.all(
        cacheNames.filter(function(cacheName){
          return cacheName.startsWith("gdad-s-river-static-")
            && cacheName != staticCacheName;
        }).map(function(cacheName){
          return cache.delete(cacheName);
        })
      )ß
    })
  )
});

When service worker activates, I’m ensuring that, any service worker which is not the latest version gets deleted. For example, if my latest cache version is say gdad-s-river-static-v61, and someone is still on gdad-s-river-static-v58, on his/her next visit, I want that client to not care about pumping one version at a time, but forthrightly delete that version to install the latest one.

//sw.js

self.addEventListener("fetch", function(e){
  e.respondWith(
     caches.match(e.request).then(function(response) {
       return response || fetch(e.request);
     })
   )
});

In the fetch event, I’m simply telling the service worker, how to respond to a particular request made (since we are hijacking the response giving power, service workers only work on secure aka https origin websites). I tell it to match the request to those cached in the browser, and if it doesn’t find that particular response to the request, fetch it through the network, else serve it from the cache.

Tada! Service worker made Jekyll powered blog offline!

SURPRISE! THE COOL THING:

Bummer: This wouldn’t work on iOS devices.

If you add a web app manifest.json file at your root of the project like this:

{
  "name": "gdad-s-river",
  "short_name": "gdad-s-river",
  "theme_color": "#2196f3",
  "background_color": "#2196f3",
  "display": "standalone",
  "Scope": "/",
  "start_url": "/",
  "icons": [
    {
     "src": "assets/images/favicon_images/android-icon-36x36.png",
     "sizes": "36x36",
     "type": "image\/png",
     "density": "0.75"
    },
    {
     "src": "assets/images/favicon_images/android-icon-48x48.png",
     "sizes": "48x48",
     "type": "image\/png",
     "density": "1.0"
    },
    {
     "src": "assets/images/favicon_images/android-icon-72x72.png",
     "sizes": "72x72",
     "type": "image\/png",
     "density": "1.5"
    },
    {
     "src": "assets/images/favicon_images/android-icon-96x96.png",
     "sizes": "96x96",
     "type": "image\/png",
     "density": "2.0"
    },
    {
     "src": "assets/images/favicon_images/android-icon-144x144.png",
     "sizes": "144x144",
     "type": "image\/png",
     "density": "3.0"
    },
    {
     "src": "assets/images/favicon_images/android-icon-192x192.png",
     "sizes": "192x192",
     "type": "image\/png",
     "density": "4.0"
    }
  ]
}

and add it in the head.html file inside the head tag,

<head>
 <!-- some stuff -->
	<link rel="manifest" href="/manifest.json">
 <!-- some stuff -->
</head>

Then, on the second visit of your website (within 5 minutes), will prompt the user to add your website to your home screen (to reside with an icon, just like other native apps), which you’ll be engaged with just like an app.

Screenshot_20170123-232947

RESOURCES

  • Details of service worker’s offline features and caching strategies can be found on this awesome’s Jake Archibald’s offline cookbook.
  • A very detailed free Udacity course about everything you’d ever need to know about service workers and IndexDB.

Did you find this article on Jekyll Blog interesting and helpful? Don’t forget to share your views and feedback.

Also Read: Ecosia — The Search Engine That Plants Trees

Similar Posts