Start here

HTML, CSS and JS to APK: how to structure the folder

Where index.html goes, why every path must be relative, what to do with images, fonts and libraries, and the one ZIP mistake that produces a blank app every time.

8 min read Updated September 2026

A folder converts to an APK cleanly when index.html is at its root, every path in it is relative, nothing points at the internet, and file names match their references exactly, including case. The builder copies the folder into the app's assets and loads index.html; from then on the WebView resolves paths exactly as a browser opening the file from a local disk would. Get those four things right and a multi-page site with images, fonts and libraries works offline in the app the same as it does on your desktop.

The reference layout

my-app/
├── index.html          ← must be here, at the top level
├── about.html
├── css/
│   └── style.css
├── js/
│   ├── vendor/
│   │   └── chart.min.js   ← downloaded, not linked from a CDN
│   └── app.js
├── img/
│   ├── logo.png
│   └── hero.webp
└── fonts/
    └── inter.woff2

And in index.html:

<link rel="stylesheet" href="css/style.css">
<img src="img/logo.png" alt="Logo">
<script src="js/vendor/chart.min.js"></script>
<script src="js/app.js"></script>

Note what is absent: no leading slash on any path, no https:// anywhere, no type="module".

Rule 1 — index.html at the root

The builder looks for index.html at the top level of the ZIP. The classic mistake is zipping the folder rather than its contents, so the archive contains my-app/index.html instead of index.html. Most builders (including this one) look one level down and cope, but nested deeper — site/dist/index.html — and the app opens to a blank screen or a "file not found". On macOS, select the files inside the folder, right-click, Compress. On Windows, same: select the contents, Send to → Compressed folder.

Rule 2 — every path is relative

On a web server /css/style.css means "from the site root". Inside an APK there is no site root — / is the root of the device's file system, and the stylesheet is not there. Write css/style.css or ./css/style.css instead. From a page in a subfolder, go up with ../css/style.css. This applies to href, src, CSS url(), and any path your JavaScript builds. Static-site generators and frameworks often emit root-relative paths by default; look for a "base path" or "relative URLs" option and set it before exporting.

Offline Readiness CheckerScan your HTML for the things that break once it is bundled inside an APK Open the tool

Rule 3 — nothing from the internet

A <script src="https://cdn…"> works only while the phone is online, and it delays the first paint even then. Download the file into js/vendor/ and reference it locally. Same for CSS frameworks, icon fonts and web fonts: Google Fonts can be downloaded as .woff2 files and declared with @font-face. Analytics tags are the exception you may keep on purpose — they fail silently offline — but consider whether an offline app needs one.

Rule 4 — names match exactly

Windows and macOS file systems ignore case; Android's does not. Logo.PNG on disk and logo.png in the HTML works on your laptop and 404s in the app. Use lower-case names with hyphens for everything and the problem never arises. Avoid spaces too — my image.png must be written my%20image.png in HTML and someone will forget.

Frameworks and build tools

ToolWhat to setThen
Vitebase: './' in vite.config.jsBuild; the dist folder is your app. Convert type="module" to a classic script — see the fixes guide.
Create React App"homepage": "." in package.jsonBuild; zip the build folder's contents.
Next.jsoutput: 'export', assetPrefix: './', trailingSlash: trueStatic export only; server features do not apply.
Hugo / Jekyll / EleventyRelative URLs option (relativeURLs = true in Hugo)Build; zip the output folder's contents.
Hand-written HTMLNothingFollow the four rules.

Multi-page navigation

Links between your own pages work as they do on a desktop: <a href="about.html"> loads the bundled file. The Android back button walks back through the pages, and exits the app from the first one. Links to external sites open in the phone's browser — usually what you want, since the user is leaving your bundle. Keep a way back to index.html on every page; there is no address bar to type into.

Checking before you zip

  1. Open index.html from disk in Chrome. Click every link, load every page. Watch the console (F12) for red 404s — each one is a wrong path or a wrong name.
  2. Disconnect from the network and reload. Anything that disappears was coming from the internet.
  3. Paste each page into the Offline Readiness Checker for the things the console does not show — module scripts, root paths in JavaScript, fetch calls.
  4. Zip the contents. Check the ZIP's size against the tier limit. Upload.

Questions people ask

Does the ZIP have to be named anything in particular?

No. What matters is that index.html is at the top level inside it. The builder ignores the archive's own name.

Can I have subfolders?

Yes, any depth. Reference files with relative paths from the page that uses them, going up with ../ where needed.

My exported site uses paths like /assets/app.js. Do I have to fix every one?

Yes, or re-export with a relative base path — every static-site tool and bundler has that option. A find-and-replace of href="/ with href="./ works for hand-written sites.

Do fonts in a fonts/ folder work offline?

Yes, declared with @font-face and a relative src. WOFF2 is the format to use; it is the smallest and every Android WebView supports it.

How big can the ZIP be?

5 MB of extracted content on the free tier, 18 MB on Pro. Images are usually what fills it — resize them to phone width and convert to WebP.

Read next

Your HTML, installed on a phone today

Upload the file or ZIP, pick a name and an icon, and download a signed Android APK in minutes. Free to start — no Android Studio, no code changes, no card.

Convert HTML to APK — free