The ghost of my old website
I migrated this blog to Plain a couple of weeks ago. New CMS, new build, deployed, live. I checked it on my laptop — and saw the old site.
Odd. I refreshed. Still old. I pushed another change, waited, refreshed again. Still old. But when I opened the same URL in a private window, there was the new site, exactly as it should be. On my phone: old in Safari, new in private mode. And — the clue that cracked it — if I added a ? to the end of the homepage URL, the new site appeared instantly.
A stray query string slipping past the cache, private mode showing the truth: that's the fingerprint of a service worker.
My old site ran on Jekyll's Chirpy theme, which ships a PWA — and a PWA registers a service worker that caches your pages and serves them offline-first. A lovely feature, right up until you move to a different site at the same address. The worker is still installed in every returning visitor's browser, dutifully handing them the old pages it cached, ignoring the fact that the actual site changed underneath it.
The cruel part: deleting the old worker's file doesn't help. When the browser checks that path for an update and gets a 404, it just keeps the worker it has. A missing file is not an eviction notice. The old site can haunt a browser indefinitely.
So you have to serve the eviction notice yourself — a self-destruct service worker at the same path. Something tiny:
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (e) => e.waitUntil((async () => {
for (const key of await caches.keys()) await caches.delete(key);
await self.registration.unregister();
for (const c of await self.clients.matchAll()) c.navigate(c.url);
})()));
On its next update check, a stale browser downloads this, unregisters the old worker, wipes its caches, and reloads to the real site. New visitors never touch it — nothing on the new site registers a worker, so it just sits there as an antidote for the browsers that need one.
Because Plain plugins are just folders, this became a ten-line plugin — reset-sw — that publishes that worker at the old PWA paths. I dropped it into my config, deployed, and the ghost was gone.
Then I did the thing you can only do when your CMS is open source: I sent it upstream. The Jekyll importer now watches for pwa: enabled in your old config and warns you about exactly this, so the next person leaving Chirpy is handed the fix before they ever meet the ghost.
If your freshly migrated site keeps showing its past life — check for a service worker. It's probably still in there, doing its job a little too well.