How the Service Worker on This Site Works
Browse around this site while connected to the internet, go offline, and you can still read the pages you've visited. If you try to access a page you haven't visited yet, you'll see a friendly offline page instead of a browser error – and that page even knows which language you were browsing in. This may sound like witchcraft but is actually something browsers have supported for quite some time.
The basics of service workers
When you visit a website with a service worker, your browser installs a small helper on first visit that sits between the website and the internet. Every time you click a link or load a page, the service worker sees that request first.
Why does that matter? Because the service worker can save copies of pages you visit. When you come back later – even without an internet connection – it can show you those saved copies instead of an error message. It's like having a librarian who remembers every book you've read and can hand you a copy even when the library is closed.
Two remembering strategies
I use two different strategies depending on what you're requesting:
Cache-first for files that rarely change – Fonts, styling, and scripts are things that don't change often. When you request them, the service worker checks its memory first. If the file was already remembered – cool – it gets served from memory instead of fetching it from the internet again.
Network-first for pages that change often – Blog posts, the homepage, and other content can change at any time. When you request them, the service worker tries to fetch the current version from the internet first. If that works – cool – it gets saved and served from memory next time, in case you're offline. If the internet isn't reachable, the service worker checks its memory – and shows you the saved version or the offline page.
Offline page?
The offline page is a special page that the service worker saves immediately on first visit – before you've ever seen it. When you try to access a page offline that isn't in memory, the service worker shows this page instead of a browser error. It lists all the pages you've already visited.
The service worker also remembers which language you were browsing in. If you were reading German pages and then go offline, you'll get the German offline page. If you were browsing in English, the English one.
Breaking it down – for the nerds
Ye who enter here, abandon all hope of simple explanations. From here on out, it gets technical.
Version control
The version number is how I invalidate old caches. When I deploy changes, I bump the version. The activate event then cleans up any caches that don't match the current version names.
I use two separate caches: one for static assets and one for HTML pages. This lets me manage them independently.
const CACHE_VERSION = '1.0.8';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const PAGES_CACHE = `pages-${CACHE_VERSION}`; Pre-caching essentials
The offline pages (both English and German) are pre-cached so they're always available when you need them. Same with the CSS, fonts, and theme switcher – the site should look and feel right even offline.
const STATIC_ASSETS = [
'/offline/',
'/de/offline/',
'/assets/css/style.css',
'/assets/js/theme-switcher.js',
'/assets/fonts/Lexend-Variable.woff2',
'/assets/fonts/Lora-Variable.woff2'
]; Language-aware offline page
This is a small touch I'm quite pleased with. The service worker remembers which language you were browsing in. If you go offline while reading German pages, you'll see the German offline page. If you haven't browsed yet, it falls back to your browser language. English is the default.
function getOfflinePageUrl() {
if (userLang === 'de') return '/de/offline/';
if (userLang === 'en') return '/offline/';
const browserLang = navigator.language?.slice(0, 2);
if (browserLang === 'de') return '/de/offline/';
return '/offline/';
} The fetch handler
The fetch event listener is where the magic happens. It checks every request, tracks the language based on URL path, and routes to the appropriate caching strategy.
Communication with the page
The offline page can ask the service worker "what pages do you have cached?" and display them as links. It's a two-way conversation: the page sends a message, the service worker responds with data.
self.addEventListener('message', async (event) => {
if (event.data?.type === 'GET_CACHED_PAGES') {
// ... respond with list of cached pages
}
}); What I like about this approach
It's progressive enhancement. The site works fine without a service worker – you just don't get offline support. Nothing breaks if service workers aren't available.
It's respectful of language. A small detail, but getting the German offline page when you were browsing in German feels right.
What could be improved
I don't currently cache images. For a text-heavy site like this one, that's probably fine. But if I start adding more images, I might want a strategy for those too.
Try it yourself
Turn on airplane mode and refresh this page. If you've visited it before, it should still work. Then try visiting a page you haven't seen yet. You'll get the offline page with a list of what's available.
That's it. About 150 lines of JavaScript to make a website work offline.
The full script
Here's the complete service worker with comments explaining each part.
const CACHE_VERSION = '1.0.8';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const PAGES_CACHE = `pages-${CACHE_VERSION}`;
// Assets to pre-cache on install
const STATIC_ASSETS = [
'/offline/',
'/de/offline/',
'/assets/css/style.css',
'/assets/js/theme-switcher.js',
'/assets/fonts/Lexend-Variable.woff2',
'/assets/fonts/Lora-Variable.woff2'
];
// Track user's language preference based on browsing
let userLang = null;
// Install: Pre-cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
// Activate: Clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter((key) => key !== STATIC_CACHE && key !== PAGES_CACHE)
.map((key) => caches.delete(key))
);
}).then(() => self.clients.claim())
);
});
// Get the appropriate offline page URL based on language preference
function getOfflinePageUrl() {
// 1. Use tracked browsing language if available
if (userLang === 'de') return '/de/offline/';
if (userLang === 'en') return '/offline/';
// 2. Fall back to browser language
const browserLang = navigator.language?.slice(0, 2);
if (browserLang === 'de') return '/de/offline/';
// 3. Default to English
return '/offline/';
}
// Fetch: Handle requests with appropriate strategy
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Determine if this is an asset or a page
const isAsset = url.pathname.startsWith('/assets/');
const isHTML = request.headers.get('Accept')?.includes('text/html');
// Track language preference from HTML page visits
if (isHTML) {
userLang = url.pathname.startsWith('/de/') ? 'de' : 'en';
}
if (isAsset) {
// Assets: Cache-first
event.respondWith(cacheFirst(request, STATIC_CACHE));
} else if (isHTML) {
// Pages: Network-first, fallback to offline page
event.respondWith(networkFirstWithOffline(request));
}
});
// Cache-first strategy for assets
async function cacheFirst(request, cacheName) {
const cached = await caches.match(request);
if (cached) {
return cached;
}
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(cacheName);
cache.put(request, response.clone());
}
return response;
} catch (error) {
// Asset not available offline
return new Response('', { status: 404 });
}
}
// Network-first for pages, cache on success, offline fallback
async function networkFirstWithOffline(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(PAGES_CACHE);
cache.put(request, response.clone());
}
return response;
} catch (error) {
// Try to serve from cache
const cached = await caches.match(request);
if (cached) {
return cached;
}
// Fallback to appropriate offline page based on language
const offlineUrl = getOfflinePageUrl();
const offlinePage = await caches.match(offlineUrl);
if (offlinePage) {
return offlinePage;
}
return new Response('Offline', { status: 503 });
}
}
// Message handler: Respond with list of cached pages
self.addEventListener('message', async (event) => {
if (event.data?.type === 'GET_CACHED_PAGES') {
const cache = await caches.open(PAGES_CACHE);
const requests = await cache.keys();
const pages = requests
.map((request) => {
const url = new URL(request.url);
return url.pathname;
})
.filter((path) => path !== '/offline/' && path !== '/de/offline/')
.sort();
event.source.postMessage({
type: 'CACHED_PAGES',
pages: pages
});
}
}); Resources on the subject
- Going Offline – Jeremy Keith's book that taught me the fundamentals
- Jake Archibald's blog – Deep dives into service workers from the person who literally wrote the spec
- The Offline Cookbook – Jake Archibald's guide to caching strategies
- Service Worker API – MDN's comprehensive documentation