Make an Offline Web Treatment: Freeze a Self-Contained Edition or Maintain a Cached One?
Make an Offline Web Treatment: Freeze a Self-Contained Edition or Maintain a Cached One?
An offline treatment is not simply a page that happens to open without Wi‑Fi. It is a promise about which edition the reader has, which assets came with it, and what will happen when a newer edition exists.
That promise points to two different routes. A frozen self-contained bundle says: this edition, exactly, as delivered. A served cached edition says: this edition, installed, and later revisions can replace it. Both can work. They fail in opposite ways. The frozen bundle fails if it still reaches for a remote font or an embedded video it did not actually package. The cached edition fails if installation, activation, and page control are treated as one event, or if a new worker starts serving new assets to an old document.
The useful answer is to choose one primary offline promise, declare it in the treatment, and make the asset set and update boundary visible. MDN's Using Service Workers documentation describes installation, activation, and control as distinct lifecycle stages. That distinction is the hinge. It is not enough to say "service worker active"; the reader needs to know which edition is available and whether the open page matches it.
Inventory what the reading experience depends on
Start with the full asset set, not the first screen. For a treatment, that usually means:
- The HTML document
- Stylesheets
- Scripts
- Fonts, including any that arrive from a remote font service
- Images
- Media files, such as video or audio
- Captions, transcripts, or poster images that the media depends on
- Embedded services, such as a video host, a map, or a comment widget
- Optional analytics or enhancement scripts
Separate essential content from optional enhancement. If a remote font is part of the approved typographic look, it is essential. If an analytics script is not needed to read the treatment, it can be omitted offline. If a remote video embed is the only way to see the pitch reel, it is essential, and the offline route must either replace it with a local file or declare that the reel is unavailable offline.
Take a fictional treatment, The Drowned Orchard. Edition A has six essential assets: index.html, styles.css, app.js, assets/orchard-cover.jpg, a 12-second assets/irrigation.mp4 loop, and assets/OrchardSans.woff2. Edition B changes the title card, swaps in assets/orchard-cover-b.jpg, keeps the same video, and adds assets/irrigation-captions.vtt.
That inventory is the first useful artifact. A silent network request can make an apparently complete local copy fail at the moment it matters. A stylesheet that imports a font from a remote URL will fall back or fail offline. An iframe pointed at a hosted player will show a blank frame. The treatment may look finished on the author's machine because the browser has a warm cache. The reader on a plane does not have that cache.
Record what can lawfully and practically be included. Do not copy remote material indiscriminately. If the font license does not allow redistribution, either license it or choose a declared local fallback and accept that the fallback is a different reading edition. If the video is too large to bundle, decide whether the offline edition can omit it or whether the frozen route is the wrong choice.
Make the frozen-bundle route genuinely self-contained
The frozen route packages one identifiable edition and removes or explicitly replaces remote dependencies. A simple structure might look like this:
orchard-treatment-a/
index.html
styles.css
app.js
edition.json
assets/
orchard-cover.jpg
irrigation.mp4
OrchardSans.woff2
The edition.json file gives the reader and the team a single place to see what they have:
{
"edition": "A",
"label": "The Drowned Orchard — First Draft",
"built": "2026-09-18",
"offlineRoute": "frozen",
"assets": [
"index.html",
"styles.css",
"app.js",
"assets/orchard-cover.jpg",
"assets/irrigation.mp4",
"assets/OrchardSans.woff2"
]
}
A frozen bundle has a clear update promise: none. If edition B exists, it is a new bundle, with a new folder name, a new edition marker, and possibly a new manifest. Do not overwrite edition A in place and assume the reader will see B. The reader may have a copy on a laptop, a tablet, or a USB drive. The safest frozen route treats each approved edition as a separate object.
The route also needs a defined opening method. Double-clicking index.html is not the same as serving the folder from a local static server. Under a local file origin, some browser features are restricted or unavailable. Service workers generally do not run from file://, and module scripts, fetch requests, and some media behaviors can differ from an HTTP origin. If the treatment needs those features, either use a small local server or build a different package, such as a single HTML file with styles and scripts inlined. A folder of files is not proof that the actual reading interactions work.
Test the actual opening route without relying on an already warmed browser. If the intended route is a local server, open it from that server, not from the file system. If the intended route is a double-clicked HTML file, check that route with the fonts, images, and media in the package. The frozen bundle makes one promise: if it opens, it opens as the declared edition. That promise is only as good as the route you actually check.
Treat the cached route as an edition lifecycle
The cached route is for a served edition that should refresh later. It usually uses a service worker to install a versioned asset set. The page might live at https://example.test/treatment/, with a worker that caches the edition under a name such as orchard-v1.
The lifecycle stages are separate. Installation puts assets into a cache. Activation allows the new worker to take over. Control is whether a particular page is being handled by that worker. MDN's documentation on Clients.claim() notes that an active worker can claim in-scope clients, including pages loaded without it or under a different worker. That is useful, but it also means control is not the same as coherence. A page can be controlled by a worker that does not match the assets the page expects.
A deliberate cached edition should show the reader what is available. For example:
- "Online. Edition B is available for offline use."
- "Edition A is available offline."
- "Edition B installing: 3 of 7 assets cached."
- "Edition B is incomplete. Edition A is still available offline."
Those states are more useful than a single "offline ready" badge. They tell the reader whether they can safely close the laptop and open the treatment on a plane, and they tell the team where an installation stopped.
A minimal worker sketch might look like this:
const EDITION = 'v2';
const CACHE = `orchard-${EDITION}`;
const ASSETS = [
'/treatment/v2/index.html',
'/treatment/v2/styles.css',
'/treatment/v2/app.js',
'/treatment/v2/assets/orchard-cover-b.jpg',
'/treatment/v2/assets/irrigation.mp4',
'/treatment/v2/assets/OrchardSans.woff2',
'/treatment/v2/assets/irrigation-captions.vtt',
'/treatment/v2/edition.json'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE).then(cache => cache.addAll(ASSETS))
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Serve the treatment's navigation request from this edition's document.
if (url.pathname === '/treatment/') {
event.respondWith(caches.match('/treatment/v2/index.html'));
return;
}
event.respondWith(
caches.match(event.request).then(hit => hit || fetch(event.request))
);
});
The fetch handler keeps the reader's navigation URL, /treatment/, connected to the edition the worker is serving; without it, the cached /treatment/v2/index.html is a file no one opens directly.
Using cache.addAll() for the essential set makes the installation atomic in a useful sense: if any required asset fails, the installation fails, and the new edition does not quietly become available without its media or font. Optional assets can use a separate cache or a separate step so that a failed analytics file does not block the treatment itself.
The cached route’s cost is maintenance. You now have a worker script, a cache naming scheme, an update interface, a storage budget, and a set of browsers and contexts to check. If the team cannot support that, the frozen bundle is the more honest choice.
Keep an update from mixing old text with new assets
The central danger in the cached route is that worker freshness and document freshness can diverge. An open page can be older than the worker that controls it. A new worker can be installed and waiting while an older edition is still in use. MDN's skipWaiting() documentation describes it as a request to activate a waiting worker. Activation does not itself prove that the cached assets and the open document form a coherent new edition.
Versioned asset paths prevent most mixed states. Edition A references /treatment/v1/styles.css, /treatment/v1/app.js, and /treatment/v1/assets/orchard-cover.jpg. Edition B references /treatment/v2/.... The worker can cache both sets during a transition. An old A document keeps requesting A assets, and the worker can still serve them from the A cache. A new B document requests B assets, and the worker serves B.
Without that separation, both editions may share a cache key such as /treatment/styles.css. If the B worker serves B styles to an A document, the result is old text with new layout. If the A worker is still in control of a B document, the new cover image may be missing. The reader sees a broken edition and cannot tell whether the problem is the network, the install, or the treatment itself.
The update boundary should be explicit. When edition B has finished installing and its essential assets are complete, show the reader: "Edition B is ready. Update to switch." The switch is two steps: activate the waiting worker, then reload. Activating means either posting the waiting worker a message that calls self.skipWaiting(), or closing every tab the old worker controls and reopening the treatment; a plain reload alone can leave the page under Edition A's worker. The reload is a coherent boundary. The new document and the new assets are requested together. Do not force every open client onto the new worker the moment it installs, and do not delete resources that an open client still needs. Keep the previous edition’s cache until the transition is complete, or at least until no open client is expected to need it.
If the treatment displays an edition marker, make it part of the document as well as the manifest. For example, app.js can read a data-edition attribute from the page and compare it to the edition.json cached with the offline edition. If they differ, the treatment can say: "This page is Edition A; the offline cache is Edition B. Activate the update, then reload, to switch editions." That is a more useful failure state than a subtly mixed layout.
Check the failures that distinguish the routes
Four states separate the frozen and cached routes.
Never-installed offline access. With the frozen bundle, the treatment works if the local route works. With the cached route, a never-installed browser has no prior cached edition. It may still have a normal HTTP cache, but that is not a reliable offline promise. If the reader must open the treatment on a plane without installing anything first, the frozen bundle is the only honest route. The cached route should instead show a clear unavailable state or a small authorized fallback, and the team should tell reviewers to open the treatment once online before travel.
A fully available edition. Both routes can pass. Check more than the first screen. Play the 12-second loop offline. Seek within it. Check the poster image, the captions, and the font rendering. A cached first screen with a missing video is a partial success, not an offline edition.
An interrupted update. Edition B begins installing, and the network drops. If the worker uses an atomic essential cache, B does not activate, and Edition A remains available. If the worker caches assets one at a time and activates early, the reader can end up with a half-edition. Suppose Edition A is open while B installs. The expected result is that A continues to read as A, and B is reported as incomplete. If that is not what happens, the problem is not the reader’s connection; it is the update boundary.
Missing or cleared cached assets. Storage can be evicted, or the reader can clear site data. With the frozen bundle, the files remain unless they are deleted. With the cached route, the cached edition can disappear. The treatment should not promise guaranteed offline availability after storage loss. If it loads at all, it can show: "The offline edition is unavailable. Reconnect to restore it." If it does not load, the browser’s offline error is the honest result. A small fallback page can help only if it is stored somewhere that has not also been cleared.
Media deserves its own check. Video often uses range requests, and a service worker that simply caches the whole file may not handle every playback pattern. The frozen bundle can play a local file, but codec support, autoplay policy, and fullscreen behavior can still differ from the hosted site. The cached route should test the actual video offline, not just the HTML around it. If the media is essential and cannot be made reliable in the chosen route, the treatment should say so before a reviewer discovers it on a plane.
Choose the route the team can actually support
Choose the frozen self-contained bundle when the treatment must be an approved, fixed edition. It limits update ambiguity, avoids installation lifecycle risk, and can be archived as a single object. Its cost is that every revision is a new delivery, and its offline promise depends on the actual local opening route.
Choose the deliberately installed cached edition when the treatment is a living document that reviewers will revisit. It can refresh later, but only if the team maintains a versioned asset set, a visible edition identity, an explicit update boundary, and a fallback for incomplete or missing assets. The cached route is not a one-time build; it is a small lifecycle.
Either way, write down the declared delivery model before you ship it:
- Which edition is this?
- Which route supplies it offline?
- What is the complete essential asset set?
- What happens on a never-installed device?
- What happens when an update is interrupted?
- What happens when storage is cleared?
- How does the reader see which edition is open?
The last question is the one that keeps the two routes from blending into an unverifiable promise. A successful online visit is not proof of offline delivery. A frozen folder is not proof of a coherent local experience. A service worker is not proof that the open page and the cached assets belong together. The modest complete fallback—clearly labeled, visibly editioned, and checked under the offline conditions it claims—is worth more than a seamless availability story that has not been tested.
Frequently asked questions
What is the difference between a frozen self-contained bundle and a cached offline edition?
A frozen bundle delivers one identifiable edition exactly as packaged and makes no update promise; a later revision is a new bundle with a new folder name and edition marker. A cached edition is served and installed, usually through a service worker, so later revisions can replace it—but only with a maintained versioned asset set, visible edition identity, explicit update boundary and fallback for incomplete or missing assets.
What should the asset inventory include for an offline treatment?
Start with the full set the reading experience depends on: HTML, stylesheets, scripts, fonts including remote font services, images, media, captions, transcripts, poster images, embedded services such as a video host or map, and optional analytics or enhancement scripts. Separate essential content from optional enhancement, and record what can lawfully and practically be included. A remote font or hosted video can make an apparently complete local copy fail when it matters.
Can a frozen bundle simply be opened by double-clicking index.html?
Not necessarily. Under a local file origin, some browser features are restricted; service workers generally do not run from file://, and module scripts, fetch requests and some media behavior can differ from an HTTP origin. If the treatment needs those features, use a small local server or build a different package such as a single HTML file with styles and scripts inlined. Test the actual opening route without relying on a warm browser cache.
How do you keep an update from mixing old text with new assets in the cached route?
Use versioned asset paths so an old document keeps requesting its own edition's assets while a new document requests the new set. When the new edition has installed and its essential assets are complete, show an explicit update boundary: activate the waiting worker and reload; a plain reload alone can leave the page under the old worker. Keep the previous edition's cache until the transition is complete, and make the edition marker part of the document as well as the manifest.
What offline failures should be tested for both routes?
Test never-installed offline access—frozen works only if the local route works, while a cached route has no prior edition and should show an honest unavailable state or authorized fallback. Test a fully available edition beyond the first screen: play and seek the media, check poster, captions and font rendering. Test an interrupted update, where an atomic essential cache should leave the old edition available and report the new one incomplete. Test missing or cleared cached assets: storage can be evicted or site data cleared, so the cached route should not promise guaranteed offline availability after storage loss, and media may use range requests that a simple whole-file cache does not handle.