What is offline by default
Every file in your ZIP: HTML, CSS, JavaScript, images, fonts, audio, video, JSON, WebAssembly. They are read from the APK, never from the network — so a site that only uses its own files needs nothing on this page. The checklist is about everything else.
1. Libraries and stylesheets from CDNs
Search your HTML for https:// in <script src> and <link href>. jQuery, Bootstrap, Tailwind's CDN build, Font Awesome, Alpine, Chart.js — each one is a hole. Download them into the ZIP and link them relatively.
2. Web fonts
A Google Fonts <link> is two network hops: a stylesheet from fonts.googleapis.com, then font files from fonts.gstatic.com. Offline, text falls back to the phone's default font — readable, but the layout shifts. Localize them (the CDN localizer handles the whole chain) or self-host WOFF2 files with @font-face. Icon fonts (Material Icons, Font Awesome) matter more: offline they render as ligature words or empty boxes.
3. Images and media from elsewhere
Logos hot-linked from your website, avatars from Gravatar, product photos from your CMS, a YouTube embed. Bundle what is static. For dynamic images, give <img> a size and a background colour so a missing picture leaves a neat block instead of a collapsed layout, and use onerror to swap in a local placeholder.
4. Data from your API
Keep the last good response and show it when the network fails:
async function loadMenu() {
try {
const res = await fetch('https://api.example.com/menu', { cache: 'no-store' })
const data = await res.json()
localStorage.setItem('menu', JSON.stringify(data))
return data
} catch {
const cached = localStorage.getItem('menu')
if (cached) return JSON.parse(cached)
// first launch with no network: fall back to the copy shipped in the ZIP
return (await fetch('data/menu.json')).json()
}
}
Shipping a data/menu.json snapshot inside the ZIP means even the very first launch has content. For larger data, IndexedDB (directly, or via a small wrapper such as idb-keyval) holds hundreds of megabytes.
5. Tell the user what needs a connection
const setOnline = () => document.body.classList.toggle('offline', !navigator.onLine)
addEventListener('online', setOnline)
addEventListener('offline', setOnline)
setOnline()
With a class on the body, CSS can disable the "Send" button and show a quiet banner rather than letting a request fail silently. navigator.onLine only knows whether there is a network, not whether your server is reachable — still wrap real requests in try/catch.
6. Third-party scripts
Analytics, chat widgets, ads and maps load more code at runtime and are useless offline anyway. They must simply fail quietly: load them with async, never let your own code depend on them having loaded (window.gtag && gtag(…)), and keep them out of the critical path of first render.
7. The airplane-mode test
- Install the built APK. Open it once online, then close it from recent apps.
- Turn on airplane mode (and make sure Wi-Fi is off too).
- Open the app from its icon. Visit every screen; use every button.
- Note anything unstyled, blank, spinning forever or silently doing nothing — each is a network dependency from the list above.
Why a cold start matters: an app that was open when you went offline may still have CDN files in the WebView's memory cache. Only a fresh launch shows what a user on a train sees.