Reading This Site Offline

12 July 2026 · technology

This site learned a small trick recently: it works without the internet. I implemented it out of curiosity on how it would be done, but I'm glad I did. If someone tries to read one of my blogs while on a plane, now they can, so long as it was preloaded. Read a few posts over breakfast, open the site again, and the pages you visited are still there, styled, functional. If the connection dies mid-browse, it will still work. Nice, right?

This is done via a service worker, and this post walks through mine: ~100 lines of code, no framework, no build step, in keeping with this site's general philosophy of doing things with the smallest possible amount of infrastructure. If you run a static site anywhere (GitHub Pages included), this is a good philosophy, in my opinion.

Quick jargon guide

  • Service worker: a small script the browser installs alongside your site and runs in the background. It sits between your pages and the network, and can answer requests itself, including when there is no network.
  • Cache (the browser kind): a local store of responses the service worker can save and replay. Lives on the visitor's device, controlled by your code.
  • Network-first: a strategy: try the internet, fall back to the cache. Fresh when online, functional when not.
  • Stale-while-revalidate: the inverted strategy: answer instantly from cache, then fetch a new copy in the background for next time. Fast always, fresh eventually, annoying when you are refreshing a page for an update, though.
  • Precache: the short list of files saved at install time, before you've visited anything, the skeleton the site can't render without.
  • Cache invalidation: famously one of the two hard problems in computer science. A service worker is a machine for having this problem on purpose.
  • giscus: the comment system under my posts. It stores every comment as a GitHub Discussion and loads in an embedded frame from giscus.app, so the conversation lives on GitHub rather than on this site.

Two strategies

Everything the worker does comes down to one decision, made per request: who do you trust more, the network or the cache?

Pages get network-first. HTML is where mistakes live: a typo fixed, a broken link repaired, a post updated. When you're online I always want you reading the newest deploy, so the worker tries the network, saves a copy of whatever comes back, and only reaches for that copy when the fetch fails:

// HTML: network-first
fetch(req).then(function (res) {
  var copy = res.clone();
  caches.open(PAGES_CACHE).then(function (c) { c.put(req, copy); });
  return res;
}).catch(function () {
  return caches.match(req).then(function (hit) {
    return hit || caches.match('./offline.html');
  });
});

The chain reads as a 'politeness ranking': fresh page if possible, your cached copy if not, and a dedicated offline page as the final backup. Every page you visit while online becomes a page you own while offline, the cache is your personal reading history.

Assets get stale-while-revalidate. CSS, JavaScript, fonts, thumbnails, these change rarely and block rendering while they load, so the priorities invert. The worker answers from cache immediately and refreshes in the background:

// Assets: stale-while-revalidate
caches.match(req).then(function (hit) {
  var fetching = fetch(req).then(function (res) {
    if (res && res.status === 200) {
      var copy = res.clone();
      caches.open(ASSETS_CACHE).then(function (c) {
        c.put(req, copy);
        if (req.destination === 'image') trimCache(ASSETS_CACHE, IMG_LIMIT);
      });
    }
    return res;
  }).catch(function () { return hit; });
  return hit || fetching;
});

Worst case, you see a stylesheet that's one visit out of date. In exchange, repeat visits render instantly, offline or not. That trimCache call is the one piece of housekeeping, meaning that image caches are capped at 200 entries, oldest evicted first, so browsing my whole gallery doesn't fill up your phone!

What I deliberately don't cache

The worker ignores anything cross-origin; the full-size photographs stay on GitHub's release servers, caching those would mean warehousing megabytes per photo on your device for a click-through you probably won't repeat. Comments are giscus, which lives in an iframe and belongs to GitHub; offline, it simply doesn't appear, which is the correct behaviour. Analytics likewise gets no offline resurrection, if the network can't see you, neither should it.

Offline mode should preserve the reading, not fake the internet. The precache reflects the same idea: it's just the offline page, the stylesheet, the core scripts, and posts.json (so the blog index still works), so about eleven files, everything else earns its place in your cache by you actually opening it.

A moody grey sea under heavy clouds, distant fish-farm pens near a rocky headland
© Ken Reid. All rights reserved.

The footguns

I'm not really a website developer or designer, beyond hobbyist endeavors like this website and a couple of landing pages for research labs I worked in, so much of this section is just what I learned while, well, learning this stuff. Service workers have a deserved reputation for one specific misery: the cache that would not die. Ship a worker with a careless cache-first strategy and your visitors can be pinned to an old version of your site for days: including, delightfully, an old version of the service worker itself. The classic developer experience is editing a file, refreshing, seeing no change, and spending an hour debugging to find it's actually working as intended and you just turn off your monitor for a break and see your sad reflection staring back at yourself. Ahem, anyway.

I'll also admit what this isn't: it isn't a full progressive web app. There's no install banner, no background sync. Those are all possible and all, for a site whose job is being read, beside the point.

Try it

Open a few posts, then turn on airplane mode and keep clicking. The pages you visited load; the search on the blog index still filters; the pages you didn't visit hand you a courteous offline page instead of a browser error. Turn the network back on and the whole arrangement dissolves back into an ordinary website. Much like an IT worker, if it's doing the job correctly, you won't even notice it's there.

Common questions

Why not just cache the entire site up front?

Precaching everything downloads megabytes a first-time visitor never asked for, most of which they'll never read. As much as I'd like all my visitors to read all my website, most just swing by for something that interests them, then off they go.

Does this let the site track me offline?

No, rather the opposite. The caches live in your browser, managed by your browser, and I have no visibility into them whatsoever. Offline visits send me nothing: no analytics, no logs, no signal you exist. It's the most private way to read the site.

Why not use Workbox or a PWA framework?

Workbox is excellent and I'd reach for it on a complex app.

How do updates reach me if I'm serving from cache?

Pages are network-first, so any online visit gets the latest deploy automatically, the cache only speaks when the network can't.


Back to all posts