docs / preparing your site

Framework recipes

Each framework's production build is a folder of static files. These recipes cover the settings that make that folder behave inside an app — and the framework-specific traps that are not about paths at all.

9 min read·6 sections·updated Sep 2026
TL;DR

Set a relative base (./) — or /web/ for Next.js, Nuxt, Flutter web and Expo — switch the router to hash mode, bake the production API URL into the build, disable or guard service-worker registration, and zip the output folder's contents.

The pattern every recipe follows

  1. Base path — assets must not be requested from /. Relative (./) where the tool supports it; /web/ where it does not.
  2. Routing — hash mode, for the reasons in SPA routing.
  3. Build-time configuration — anything read from environment variables is frozen into the bundle when you build.
  4. Service workers — unnecessary in an app whose files are already local; guard the registration.
  5. Output — zip the contents of the build folder, not the project.
Framework Build ConfiguratorThe exact base path, router and build settings for Vite, CRA, Angular, Next.js, Nuxt, SvelteKit, Flutter web and more Open tool

Quick reference

ToolBase settingZip this folder
Vite (React, Vue, Svelte, Solid, Preact)base: './'dist/
Create React App"homepage": "."build/
Vue CLIpublicPath: './'dist/
Angular 17+--base-href ./dist/<app>/browser/
SvelteKitadapter-static, paths.relative: truebuild/
Next.jsoutput: 'export', basePath: '/web'out/
Nuxt 3ssr: false, app.baseURL: '/web/'.output/public/
Parcel--public-url ./dist/
Flutter web--base-href /web/build/web/
Expo (web)experiments.baseUrl: "/web"dist/

Environment variables are baked in

import.meta.env.VITE_API_URL, process.env.REACT_APP_API_URL and NEXT_PUBLIC_* are replaced by their values at build time. Whatever was in .env when you ran the build is what ships. Two consequences:

  • Build with production values — an app pointing at http://localhost:3000 cannot reach anything on a phone.
  • Every such value is readable by anyone who unzips the APK. Public client keys (Firebase config, Supabase anon key, Stripe publishable key) are designed for that. Secret keys never belong in a front-end build, app or not.

PWA plugins and service workers

vite-plugin-pwa, Workbox, Angular's service worker and Create React App's serviceWorkerRegistration all try to register a service worker. In the app, that registration fails — service-worker requests do not go through the app's file server — and the app does not need one, because its files are already on the device. Either leave the plugin out of the APK build or guard the call:

if ('serviceWorker' in navigator && location.hostname !== 'appassets.androidplatform.net') {
  navigator.serviceWorker.register('./sw.js')
}

The same check is a reliable way to detect "running inside the app" anywhere in your code.

Framework-specific notes

Vite

Modern Vite output — <script type="module" crossorigin>, code-split chunks, dynamic import() — runs as-is, because the app serves a real https origin. You do not need @vitejs/plugin-legacy or single-file plugins for the app's sake; add legacy output only if you target very old WebViews.

Angular

Zip browser/, not dist/<app>/ — the latter also holds server/ and 3rdpartylicenses.txt and has no index.html at its top. If you use Angular's service worker (@angular/service-worker), disable it in the APK build's configuration.

Next.js

Only the static export applies: no API routes, middleware, server actions, ISR or next/image optimisation. Next.js cannot emit relative asset URLs, so basePath: '/web' makes its absolute URLs land in the right folder. Client-side navigation with <Link> works; avoid full-page reloads to deep URLs.

Nuxt

Use ssr: false with nuxi generate for a single index.html, and router.options.hashMode: true. Server routes in server/ do not exist in the app — call a deployed API instead.

Flutter web

Build with --no-web-resources-cdn so the CanvasKit renderer is bundled; otherwise it is downloaded from Google at startup and the app is blank offline. It adds several megabytes — check the size limit. If you have Flutter source, Flutter's own Android build produces a native app that is usually the better choice.

Verifying any build

  1. Open the output index.html in an editor: no src="/ or href="/ except /web/ ones.
  2. Serve the output folder from a sub-path locally — npx serve from the parent folder and open /dist/ — to simulate not being at the root.
  3. Upload to the builder and use the preview, which serves files the same way the app does.

FAQ

Do I need to convert my Vite build away from ES modules?

No. Module scripts work in the app because it serves your files from a real https origin. That advice applied to converters that load pages from file://.

Where do I put my API keys?

Public client identifiers (Firebase config, Supabase anon key, publishable payment keys) can go in the build — they are designed to be public. Secret keys must stay on a server you control; the app calls that server.

Can I use Tailwind, Sass or TypeScript?

Yes — they compile away at build time. The ZIP only contains the output CSS and JavaScript, which the app runs like any other.

Tools for this step

Unzip it on a phone today

Upload the ZIP, name the app, pick an icon — and download a signed APK a few minutes later. Free builds, no watermark, no Android Studio.

Convert a ZIP — free site.zip → app-release.apk