Back to Hub
ENGINEERING •
By Yash Panchal · Last updated:

Offline-First on GitHub Pages: What Works, Where It Ends

NitroIDE runs on GitHub Pages. That means there is no server: no Node process, no database, no code that runs on request. Pages are static files, and GitHub serves them exactly as they are — it cannot even set custom response headers, which is why my security headers live at the Cloudflare layer instead.

And yet the editor works offline. Open it once with a connection, kill your wifi, reload, and you can still write code, run the preview, and keep your projects. That is not magic and it is not a contradiction. It is a service worker doing a small number of jobs, each with a deliberate strategy — plus an honest list of things it cannot do.

Here is the recipe, including the parts where “offline-first” stops being true.

What “offline” actually means here

First, the honest boundary: the first visit needs the network. A service worker cannot cache what the browser has never downloaded. Offline-first is a promise about the second visit onward, not the first. Anyone selling you “works offline” without that sentence is selling you the demo, not the product.

Once the shell is cached, the app itself — editor, preview, console, saved projects — needs no network at all. Projects live in localStorage (the subject of my Oct 22 article), the preview runs entirely client-side in a sandboxed iframe, and nothing phones home. The network is an input you may use, never a dependency the app waits on.

Rule 1: precache the app shell, and only the app shell

On install, the service worker caches the small set of files the app cannot boot without: the HTML shell, the main script, the stylesheet. Everything else is cached on first use, not up front.

Precaching the world feels thorough and is actually harmful: a huge install step that can fail halfway, stale copies of files nobody opened, and a first visit that gets slower in the name of a later visit that may never come. Cache the boot path. Let the rest earn its place.

Rule 2: network-first for your own code, cache-first for vendor code

This is the split that matters most, and it is the one I got wrong before I got it right.

Your own pages and your main script are network-first: try the network, fall back to the cache when it fails. The reason is the trap I wrote about on Oct 14 — a cache-first HTML file is how users get stuck on an old version of your app and never know it. Fresh code wins whenever the network exists; the cache is the fallback, not the authority.

Heavy vendor assets are the opposite: cache-first. Monaco, fonts, and icon files are large, change rarely, and are usually versioned by filename or path. Serving them from cache makes repeat loads fast, and a stale copy of a font is invisible in a way a stale copy of your app logic is not. Same cache, opposite strategies, decided file by file — not one global policy.

Rule 3: version the cache and delete the old one

Every service worker version gets its own cache name, and on activation the worker deletes every cache that is not the current one. No migration, no merge, no “keep the last two just in case.”

This is what makes Rule 2 safe to live with: when you ship, the old world is removed in one step. Users are either on the new cache or they are about to be. The failure mode to fear is not “old cache lingers” — it is two versions of your app sharing state, which is exactly what the clean-slate activation prevents.

Rule 4: never cache the API

NitroIDE's contact form posts to a Cloudflare Worker backend, and those /api/* requests bypass the cache entirely — never stored, never served stale.

This rule generalizes to anything with side effects or fresh state: form submissions, auth, anything that is not a static file. A cached POST response is a bug report waiting to happen, and a stale GET of “current state” is worse than a slow one. If it is not a file you shipped, do not put it in the file cache.

Rule 5: cache navigations under the bare path

NitroIDE share links look like ?code=... — every shared snippet is a unique URL serving the same HTML shell. Cache each one separately and your cache grows with every link anyone opens, which is exactly the bloat the v25 audit fixed.

So document navigations are cached under the bare path, query string stripped. One entry serves every share link. The code itself rides in the URL, so nothing is lost — the shell is identical, and the snippet arrives with the navigation. Any app with share links, search params, or tracking params needs this rule or it will cache the same page a thousand times under a thousand names.

Where it honestly ends

Offline-first on a static host has real edges, and you should say them out loud:

First load needs the network. No way around it. Your offline story starts at visit two.

Anything server-shaped needs the network. Form submissions, AI calls to a provider, importing a gist — all of these fail offline, and should fail loudly rather than silently pretending.

“Clear site data” wipes everything. The cache and localStorage go together. Offline persistence is not a backup; that is why NitroIDE exports to ZIP.

HTTPS and a registered scope are required. Service workers do not run on plain HTTP, and a worker only controls pages under its scope. Both are free on GitHub Pages — but they are requirements, not defaults you can skip.

Storage quotas exist. Browsers cap cache storage, and eviction under pressure is real. The shell is small by design; that is a feature, not an accident.

How to test it (the part people skip)

Put the browser in offline mode in DevTools — not airplane mode on your whole machine, which also kills the DevTools connection in confusing ways — and do a normal reload, not a hard reload. A hard reload bypasses the service worker and tests nothing. Then check three things: the app boots, the editor works, and a previously opened project loads. Then go back online and confirm the new version arrives on the next visit — that is Rule 2 doing its job.

That test is the whole pitch in one gesture: no server, no signup, no connection — and the tool still works. The static host did not make that possible. A small, opinionated cache policy did.

Try it

I build NitroIDE — a browser-based IDE where your code never leaves your machine: nitroide.com

⭐ If you enjoyed this, star NitroIDE on GitHub: github.com/nitroideofficial/nitroide

Frequently asked questions

Can a static site on GitHub Pages really work offline?

Yes, after the first visit: a service worker caches the app shell and serves it when the network is gone. The first visit always needs the network — a worker cannot cache what the browser has never downloaded.

Should HTML pages be cache-first or network-first?

Network-first for your own pages and scripts, so users never get silently trapped on an old version. Cache-first suits heavy vendor assets like Monaco, fonts, and icons, which change rarely and are harmless if briefly stale.

What should a service worker never cache?

Anything with side effects or fresh state — API calls, form submissions, auth. A cached POST response is a bug waiting to happen, and a stale GET of current state is worse than a slow one.

How do you test offline mode properly?

Use DevTools offline mode and do a normal reload, not a hard reload — a hard reload bypasses the service worker and tests nothing. Then check the app boots, the editor works, and a previously opened project loads.