Files
Steffen Becker 96e5b2fc87 fix: the PWA install icon uses the current logo
The icon PNGs still carried the old two-leaf motif; only favicon.svg was
updated in the redesign, and Chrome takes the PNGs for the install icon.
Re-rendered both from the current logo, added a padded maskable variant
so Android's mask no longer clips the leaves, bumped the service-worker
cache so existing installs drop the stale icons, and removed the
unreferenced old favicon and wordmark files.
2026-08-21 15:39:19 +02:00

70 lines
3.2 KiB
JavaScript

// Offline support. Nothing here talks to a server at runtime — the document lives in
// localStorage/IndexedDB — so a cached shell is the whole working editor: dictionaries,
// fonts and the WASM speller included, each kept the first time it is asked for.
const CACHE = 'edentext-v2';
// The entry document under one key, whichever path asked for it. Relative, so it
// resolves against this script's own directory — the app root under any base path.
const SHELL = './';
// The build's asset names carry a content hash, and the entry document is the only
// place they are written down — so reading them out of it precaches the shell with no
// build step. What the app loads later (the speller, a dictionary, a picture decoder)
// is kept as it is asked for, which is from the **second** visit on: on the first the
// worker is still installing while those requests go out.
async function precache(cache) {
const response = await fetch(SHELL, { cache: 'reload' });
await cache.put(SHELL, response.clone());
const html = await response.text();
const assets = [...html.matchAll(/(?:src|href)="(\.?\/assets\/[^"]+)"/g)].map((m) => m[1]);
await cache.addAll([...new Set(assets), './favicon.svg', './manifest.webmanifest']);
}
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then(precache).then(() => self.skipWaiting()));
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
.then(() => self.clients.claim()),
);
});
// The document is network-first: served from the cache it would keep naming the assets
// of the build it was cached with, and a new one would never be fetched.
async function documentFrom(request) {
try {
const response = await fetch(request);
if (response.ok) (await caches.open(CACHE)).put(SHELL, response.clone());
return response;
} catch {
const cached = await caches.match(SHELL, { ignoreVary: true });
if (cached) return cached;
throw new Error('offline and no cached shell');
}
}
// Everything else is cache-first: the build's own files carry a content hash, so a
// cached one can never be stale.
// ponytail: the cache is never version-swept, so a redeploy's old assets stay in it
// until the browser evicts them. Sweeping needs the build id, which needs a build step.
async function assetFrom(request) {
// ignoreVary: a precached asset was fetched without the page's `Origin`, and a
// static server that answers `Vary: Origin` would make the two never match.
const cached = await caches.match(request, { ignoreVary: true });
if (cached) return cached;
const response = await fetch(request);
// An opaque response hides its status, so only a real same-origin one is kept.
if (response.ok && response.type === 'basic') (await caches.open(CACHE)).put(request, response.clone());
return response;
}
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;
if (new URL(request.url).origin !== self.location.origin) return;
event.respondWith(request.mode === 'navigate' ? documentFrom(request) : assetFrom(request));
});