docs / preparing your site

Single-page app routing inside an APK

History-mode routers expect a server that answers every path with index.html. An APK has no such server. Here is what breaks, and the three ways to route that work.

7 min read·7 sections·updated Sep 2026
TL;DR

The app opens /web/index.html and has no fallback for other paths, so history-mode routes either match nothing on launch or 404 on reload. Switch the router to hash mode (createHashRouter, createWebHashHistory(), withHashLocation()). The Android back button then walks your routes naturally.

What a history-mode router assumes

A router in history mode (BrowserRouter, createWebHistory(), Angular's default PathLocationStrategy) keeps the route in the real URL path: /about, /products/42. Two things make that work on a website:

  1. The app is served from the root, so the path is the route.
  2. The server is configured to answer any unknown path with index.html, so reloading /products/42 still loads the app.

Neither holds in an APK. The page is /web/index.html, so the router's first look at location.pathname sees /web/index.html — a route nobody defined. And the app's file server is literal: a reload of /web/products/42 asks for a file that does not exist and gets a 404.

The symptoms

You seeCause
Blank screen, or your "not found" route, on first launchNo route matches /web/index.html
Links work, pull-to-refresh shows an empty pageReload requests a path that is not a file
Back button exits the app instead of going backNavigation replaced history entries instead of pushing them

A hash router keeps the route after the #: index.html#/products/42. The fragment never reaches the file server, so every launch and reload loads index.html, and the router reads the route from the hash.

import { createHashRouter, RouterProvider } from 'react-router-dom'
const router = createHashRouter([
  { path: '/', element: <Home /> },
  { path: '/products/:id', element: <Product /> },
])
root.render(<RouterProvider router={router} />)
import { createRouter, createWebHashHistory } from 'vue-router'
export default createRouter({ history: createWebHashHistory(), routes })
provideRouter(routes, withHashLocation())
kit: { router: { type: 'hash' } }

Your <Link to="/about"> and router.push('/about') calls do not change; the router adds the # itself.

Fix 2: a memory router

A memory router keeps the route in JavaScript only (createMemoryRouter, createMemoryHistory()). The URL never changes, which is invisible in an app anyway. The catch: the WebView's history has no entries for your routes, so the Android back button closes the app from any screen unless you handle it yourself. Prefer hash routing unless you have a reason.

Fix 3: keep history mode with a basename

If you must keep path URLs, set the router's base to /web and add a route that treats /index.html as home:

createBrowserRouter([
  { path: '/', element: <Home /> },
  { path: '/index.html', element: <Navigate to="/" replace /> },
  …
], { basename: '/web' })

In-app navigation then works, but anything that reloads a deep URL — pull-to-refresh on a sub-page, a location.reload() — still 404s. Turn pull-to-refresh off in the builder if you go this way.

The back button

The app's back button calls the WebView's own "go back" while there is history, and closes the app when there is none. Hash navigation creates real history entries, so back walks your routes exactly like a browser. Two habits keep it predictable:

  • Use replace for redirects (login → dashboard), so back does not bounce the user into the redirect again.
  • Close modals and drawers with a route or a history.back()-aware pattern if you want back to close them — otherwise back skips past them to the previous screen.

Multi-page sites need none of this

A site made of real HTML files (index.html, about.html, blog/post.html) linked with relative hrefs has no router. Every link loads a real file, and back works as in a browser.

FAQ

Will hash URLs hurt my SEO?

Not in the app — app screens are not crawled. If the same build also runs on your website, you can keep history mode there and switch to hash mode only for the APK build with an environment variable.

Can I make the app fall back to index.html for unknown paths?

Not from your ZIP: the app serves files literally and returns 404 for anything that is not a file. Hash routing avoids needing a fallback at all.

Does Next.js need hash routing?

Next.js has no hash mode. Its static export pre-renders each page and navigates on the client, which works with basePath '/web'; avoid full reloads of deep URLs. See the framework recipes.

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