Quick answer: put
index.htmlat the top level of the ZIP with your CSS, JavaScript, images and fonts alongside it, use relative paths and lowercase filenames, keep external assets inside the archive, and avoid<script type="module">. The whole ZIP is unpacked into the app and served from the device, so the result works offline.
The shape of a good ZIP
mysite.zip ├── index.html ← must be here, at the root ├── about.html ├── css/style.css ├── js/main.js ├── img/logo.png └── fonts/body.woff2
The most common mistake is compressing the folder instead of what is inside it, which nests everything under mysite/. Some converters flatten a single wrapper folder; do not depend on it. Open the folder, select its contents, and compress those.
Mac users: the Finder adds a __MACOSX/ folder of metadata to archives. It is harmless and is normally dropped, but it is not part of your site.
The entry page rule
The app opens one page first and needs to know which. index.html at the root is the convention every converter understands. If your first page is home.html or main.html, rename it — or make sure you set the entry page explicitly. With no entry page found, the app launches to nothing.
Paths inside an app
A web server resolves /css/style.css from the site root. An app has no server and no root; pages come from a directory on the phone. So:
| How you wrote it | Inside the app |
|---|---|
css/style.css | Fine — relative to the page |
./img/logo.png | Fine |
../shared/lib.js | Fine, if that folder is in the ZIP |
/css/style.css | Unreliable — nothing to be root-relative to |
C:\Users\me\site\style.css | Never |
https://cdn.example.com/lib.js | Works online, missing offline |
Case matters on the phone
macOS and Windows usually ignore case in filenames; Android does not. <img src="Photo.JPG"> finds photo.jpg on your laptop and finds nothing on the device. It is the classic "it worked before I built it" bug. Lowercase every filename and it cannot happen.
What file:// takes away
Bundled pages are opened from a file:// origin, which browsers deliberately restrict. Three things stop working quietly:
- ES modules —
type="module"scripts are fetched under CORS rules that file:// cannot meet, so nothing runs and the console mentions CORS. fetch()of local files — reading a bundleddata.jsonfails the same way; remote HTTPS APIs are fine.- Service workers — they need a secure origin and never register. Harmless, since the app is already offline, but any logic inside one is dead code.
The escape hatch is to serve bundled files from a tiny HTTP server inside the app so the origin becomes http://localhost. A converter that does this makes modern build output work; if yours does not, bundle everything into a single classic script.
Leave these out
Everything in the archive ends up inside the app, and everything inside an app can be extracted by anyone who has it. So exclude:
node_modules/and other build-time dependencies;.mapfiles and unminified duplicates of minified files;- design sources, PSDs and camera-resolution originals;
.git/,.env, backups, and anything with a key or password in it.
Pre-flight checklist
index.htmlis at the root of the archive.- All paths relative; all filenames lowercase, no spaces.
- Fonts, scripts and styles are inside the ZIP unless the app will always be online.
- No
type="module"— or localhost serving confirmed. - Double-clicking
index.htmlfrom your file manager shows a working page. If it fails fromfile://on your own computer, it fails in the app.