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:
- The app is served from the root, so the path is the route.
- The server is configured to answer any unknown path with
index.html, so reloading/products/42still 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 see | Cause |
|---|---|
| Blank screen, or your "not found" route, on first launch | No route matches /web/index.html |
| Links work, pull-to-refresh shows an empty page | Reload requests a path that is not a file |
| Back button exits the app instead of going back | Navigation replaced history entries instead of pushing them |
Fix 1 (recommended): hash routing
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
replacefor 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.