Bill Baer /bɛːr/

Skip to main content

Banner

 
Bill Baer /bɛːr/
Bill Baer is a Senior Product Manager for Microsoft 365 at Microsoft in Redmond, Washington.

Setting up a Service Worker with Hugo

Setting up a Service Worker with Hugo

Setting up a Service Worker with Hugo

  Hugo Service Worker

Static sites are fast, we all generally agree to that, but they could be faster… One way we can achieve this is through a good service worker led caching strategy, for example, pre-fetching resources that the user is likely to need in the near future, such as the next few pictures in a photo album.

In this post we’ll walk through how to build out a basic service worker for a Hugo website (which is largely similar to the use of service workers in any other context).

Working 9 to 5

If you’re unfamiliar with service workers, check out Mozilla’s service worker documentation at https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API. In brief, a service worker is a proxy that lives between the page and the network, most often a background script that supports an offline first experience. That said, service workers can neither directly interact with the page nor directly access the DOM as it runs on a separate thread; however, a service worker can communicate with pages through messages (I.e. post messages).

In this post, we’ll create a basic service worker with two caches, a static cache we can manually fill with assets that we don’t want to wait for and a runtime cache to pick up assets captured through navigation.

Make the caches and put in the stuffs

To get started we’ll need to create a new file at the root of our site. For the purposes of this walkthrough, we’ll call it service-worker.js. A service worker needs to be in the root of your site in order to do its thing. For Hugo, you’ll want to place service-worker.js in your /static folder.

Once you have service-worker.js, the first thing we’ll need to do is create our static cache (precache) which we’ll fill with the assets we want immediately available. To do this copy the code below into your service-worker.js.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Set cache version
const version = "0.0.1";

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(`precache-${version}`)
      .then((cache) => {
        // Take an array of URLs, retrieve them, and add the resulting response objects to the cache (precache).
        return cache.addAll([
          "./",
          "index.html",
          "offline.html",
        ]);
      })
      // Ensures that updates to the underlying service worker take effect immediately for both the current client and all other active clients.
      // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting
      .then(self.skipWaiting())
  );
});

const version sets the version of the precache. You can change the version to remove the previous cache from the client.

install is the service worker’s install event, installation is attempted when a downloaded file is found to be new.

return cache.addAll holds the precache items. As a best practice, the static cache should be limited to the static assets you wish to cache, usually comprised of static, uniquely definable assets such as those in your /static folder (i.e. JavaScript, stylesheets, images, etc.). In the snippet above, we’re adding “index.html” to the static cache as well as including an alias “./” for index.html.

In addition, we’re adding an offline page to the cache to handle conditions where the cache cannot be called.

While tempting, but not recommended, it’s possible to fill the cache with all of your posts/content. You simply need to generate a list of relative urls for all of your pages. With Hugo it’s easy to range over a collection of pages in a section, for example, posts. The script below is a simple example of ranging over all the content in posts and generating a list of relative URIs that can be used for the purposes of precaching.

1
2
3
4
{{ $section := "posts" }}
{{ range (where .Site.Pages "Section" $section ) }}
  <p>'{{ .RelPermalink }}',</p>
{{ end }}

You can see how this works here. In this example I’m prepending and appending each URI with ’ and ‘, respectively which makes it easy to copy and paste as-is into the service worker snippet above.

Activate

The next step in our service worker’s lifecycle is activation (through an activate event). Activation is generally a good time to clean up old caches and other things associated with the previous version of your service worker.

Here we’ll do exactly that, manage our caches. In the steps above we initialized and populated our static cache with static assets that are necessary for our site. Now we’ll activate both our static cache and a new dynamic cache that we’ll fill through navigation events as “runtime” as well as cleanup old caches.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
self.addEventListener("activate", (event) => {
  const currentCaches = [`precache-${version}`, "runtime"];
  event.waitUntil(
    caches
      .keys()
      .then((cacheNames) => {
        return cacheNames.filter(
          (cacheName) => !currentCaches.includes(cacheName)
        );
      })
      .then((cachesToDelete) => {
        return Promise.all(
          cachesToDelete.map((cacheToDelete) => {
            return caches.delete(cacheToDelete);
          })
        );
      })
      // Allows the active service worker to set itself as the controller for all clients within its scope.
      // https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
      .then(() => self.clients.claim())
    );
  });

Go fetch, good doggo

The fetch event is where all the heavy lifting is. This is where we pull from the static cache and both retrieve and add content to the runtime cache ‘return cache.put(event.request, response.clone()).then(()’ as one or more pages is navigated. The offline page is triggered in our catch and in the event it is unavailable, we fallback again with a Response object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
self.addEventListener("fetch", event => {
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches
        .match(event.request)
        .then((cachedResponse) => {
          if (cachedResponse) {
            return cachedResponse;
          }
          return caches.open("runtime").then((cache) => {
            return fetch(event.request)
              .then((response) => {
                return cache.put(event.request, response.clone()).then(() => {
                  return response;
                });
              })
              // If we can't get a response, fallback to an offline page
              .catch(() => {
                  return caches.open(`precache-${version}`).then((cache) => {
                    return cache.match("/offline/");
                    console.log("Fetch failed; returning offline page instead.");
                  });
                  // When the fallback response is not available,
                  // return a Response object       
                  return new Response('Network error', {
                    status: 408,
                    headers: { 'Content-Type': 'text/plain' },
                  });           
              });
            });
          })
        );
      }
    });

Your completed service-worker.js should now look as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const version = "0.6.27";

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(`precache-${version}`)
      .then((cache) => {
        return cache.addAll([
          "./",
          "index.html",
          "offline.html",
        ]);
      })
      .then(self.skipWaiting())
  );
});

self.addEventListener("activate", (event) => {
  const currentCaches = [`precache-${version}`, "runtime"];
  event.waitUntil(
    caches
      .keys()
      .then((cacheNames) => {
        return cacheNames.filter(
          (cacheName) => !currentCaches.includes(cacheName)
        );
      })
      .then((cachesToDelete) => {
        return Promise.all(
          cachesToDelete.map((cacheToDelete) => {
            return caches.delete(cacheToDelete);
          })
        );
      })
      .then(() => self.clients.claim())
    );
  });

self.addEventListener("fetch", event => {
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches
        .match(event.request)
        .then((cachedResponse) => {
          if (cachedResponse) {
            return cachedResponse;
          }
          return caches.open("runtime").then((cache) => {
            return fetch(event.request)
              .then((response) => {
                return cache.put(event.request, response.clone()).then(() => {
                  return response;
                });
              })
              .catch(() => {
                  return caches.open(`precache-${version}`).then((cache) => {
                    return cache.match("/offline/");
                    console.log("Fetch failed; returning offline page instead.");
                  });
                  return new Response('Network error', {
                    status: 408,
                    headers: { 'Content-Type': 'text/plain' },
                  });
              });
            });
          })
        );
      }
    });

Now that we have our service worker in place, we’ll need to register it. Our registration will need to be available to all pages on the site. As a common practice the registration event can be included in the sites’ footer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register("/service-worker.js")
    .then((registration) => {
      console.log("Service worker registration successful, scope is:", registration.scope);
    })
    .catch((error) => {
      console.log("Service worker registration failed, error:", error);
    });
  }
</script>

That’s pretty much all we need to do for a basic service worker. To test if it’s working, run hugo server or your preferred build command and browse to your site. Open dev tools and check the console to see if your service worker was successfully registered.

If it has been, open Applications (in Chromium) and validate that there are two caches available, your precache and runtime caches. The precache should be populated with the assets you specified in the install event and the runtime cache filled as you navigate around your site.

If you want to see if offlining is working, click Network and set the value to offline. Now visit a page that you haven’t visited before to ensure it’s not being served from either of the existing caches.

Resources

To learn more about the general concepts around service workers, check out Google’s PWA labs at https://github.com/google-developer-training/pwa-training-labs.

| | Permalink to this article
Fingerprint for this articlef4a159d35034e6fcce41624e02977632
 
 

Comments

 
 

Comments

John M.

Thu, 08 Dec. 2022, 23:35 UTC

Super helpful! Glad I found this!

Skip to footer

Social Links