Now, PWA is a marketing term. But generally, a PWA consists of:

  1. HTTPS,
  2. A web app manifest, and
  3. A service worker

This blog has all 3! To be honest, I’m not doing much with all this stuff. HTTPS is super important for security, even if this is a static site. But this is all courtesy of GitHub, I didn’t do anything.

The web app manifest allows this site to be installable on Windows and iOS and Android but 1. it’s hard to install and 2. it’s not that useful so I doubt anyone will make use of it.

The service worker… Now that is a different beast. It’s allowed me to add offline support to this site, which is pretty cool. Not super high impact but I do hope my offline page brings some joy to anyone who bumps into it.


One neat trick I came up with was to add the following snippet at the end of the offline page:

<script>
  addEventListener('online', () => location.reload());
</script>

If suddenly the device connects to the network it automatically reloads the page! From my testing this did not always work on mobile though. This might be because being connected to a network is not the same as being online. Maybe if I added a timeout the experience would be better? From my limited testing this current solution is already slower than refreshing the page so a timeout would only make the waiting worse. It’s likely there’s a better way to do this.


I did hit this snag when using cache.addAll to cache my offline page. It does cache everything correctly. But when the user is offline and tries to navigate somewhere and the worker returns the cached offline page this error occurs:

The FetchEvent for “http://page-the-user-was-trying-to-access” resulted in a network error response: a redirected response was used for a request whose redirect mode is not “follow”.

Apparently this is due to a new security restriction but I don’t get it. It all seems so cryptic to me. StackOverflow came to my rescue though. So now I’m wrapping a response with new Response(response.body). All tutorials that I saw simply used cache.addAll so either I’m doing something wrong or they’re already out of date. If you bump into the same problem now you know!


All in all it was a great learning opportunity. There’s not a lot of code but pretty much every single line of code lead me to learn something new. Now that I look at the final solution, it doesn’t seem like it was hard at all!

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
70
71
72
var CACHE_NAME = 'v1';

function cacheInitialResources() {
  return caches.open(CACHE_NAME).then(cache =>
    cache.addAll([
      '/',
      '/blog/',
      '/offline',
    ])
  );
}

function clearOldCaches() {
  return caches.keys()
    .then(cacheNames =>
      Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        })
      )
    )
    .then(() => clients.claim())
}

function fetchOrGoToOfflinePage(fetchEvent) {
  var eventRequest = fetchEvent.request;

  // going somewhere?
  if (eventRequest.mode === 'navigate') {
    return fetch(eventRequest)
      .catch(() =>
        caches.match(eventRequest.url).then(cachedPage =>
          cachedPage || caches.match('/offline').then(cachedOfflinePage =>
            // workaround for https://issues.chromium.org/issues/41288530
            cachedOfflinePage && new Response(cachedOfflinePage.body)
          )
        )
      );
  }

  // "hack" until GitHub pages supports a longer Cache-Control https://github.com/orgs/community/discussions/11884
  if (eventRequest.url.endsWith('.css')) {
    return Promise.all([
        fetch(eventRequest),
        caches.open(CACHE_NAME)
      ]).then(
        ([response, cache]) => cache.add(eventRequest.url, response).then(() => response),
        (error) => caches.match(eventRequest.url)
      );
  }

  return fetch(eventRequest);
}

function onInstall(installEvent) {
  skipWaiting();
  installEvent.waitUntil(cacheInitialResources());
}

function onActivate(activateEvent) {
  activateEvent.waitUntil(clearOldCaches);
}

function onFetch(fetchEvent) {
  fetchEvent.respondWith(fetchOrGoToOfflinePage(fetchEvent));
}

addEventListener('install', onInstall);
addEventListener('activate', onActivate);
addEventListener('fetch', onFetch);