In Vue Router 5, a Vite-built admin SPA is served by nginx under /admin/; how do you set the router base, route paths and fallback?
answer
- three layers must agree
- Vite base feeds BASE_URL
- records stay app-relative
- base added on write, stripped on read
- fallback to the sub-path's index.html
basics
~10 sSet Vite's base to /admin/, pass import.meta.env.BASE_URL to createWebHistory(), keep route paths app-relative like /users, and make nginx fall back to /admin/index.html for unknown paths under /admin/.
solid answer
~40 sThree layers have to agree. The build: Vite's `base: '/admin/'` prefixes asset URLs and exposes `import.meta.env.BASE_URL`. The router: `createWebHistory(import.meta.env.BASE_URL)`, which is what create-vue generates; the router adds the base to every URL it writes and strips it from the URL it reads, so route records stay `/users`, `route.path` is `/users`, and `router.resolve('/users').href` is `/admin/users`. Writing `/admin/users` in a record gives `/admin/admin/users`. The server: under `location /admin/`, `try_files` must fall back to `/admin/index.html`, not the root `index.html`. With no argument, `createWebHistory()` uses a `<base href>` tag if the page has one, otherwise `/`, which is why a forgotten base shows up as links that leave `/admin/`.
code
ts · 8 lines// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
base: '/admin/',
plugins: [vue()],
})go deeper
Remember that the prefix goes into the history factory's argument and route records stay like /users.
Explain how the router adds the base on write and strips it on read, what createWebHistory() falls back to with no argument, and why Vite's base must match.
Diagnose a broken sub-path deploy from its symptom, from asset 404s to doubled prefixes to reload 404s, and scope the nginx fallback so missing assets still fail loudly.
Decide whether one build must serve several prefixes, and weigh per-environment builds against runtime base discovery for deploy simplicity.
## Three layers, one prefix An app served from a **sub-path** such as `/admin/` has to know that prefix in three places, and each one fails differently when it is missing: 1. **The build**, so the HTML asks for `/admin/assets/...` instead of `/assets/...`. 2. **The router**, so it writes `/admin/users` into the address bar and knows that `/admin/users` means the `/users` route. 3. **The server**, so a reload of `/admin/users/7` returns the app's `index.html` instead of a 404. ## The build: Vite's `base` - `base: '/admin/'` in `vite.config.ts` makes Vite prefix every asset URL in the built `index.html`. - Vite exposes the same value to app code as `import.meta.env.BASE_URL`. - That constant is the single source of truth: the router should read it rather than repeat the string. ## The router: the history factory's `base` argument In Vue Router 5 the base is the **first argument of the history factory**; it is not a router option. The router normalizes it: - With no argument, `createWebHistory()` uses the `href` of a `<base>` tag in the page, with any origin stripped, or `/` when there is no tag. - A missing leading slash is added and a trailing slash is removed, so `'/admin/'` and `'/admin'` behave the same. - On **write** the base is prepended: `router.push('/users')` puts `/admin/users` in the address bar, and `router.resolve('/users').href` is `/admin/users`. - On **read** the base is stripped from `location.pathname`, with a case-insensitive prefix check, so `route.path` and `route.fullPath` are `/users`. The practical rule: **route records, `RouterLink` targets and `push()` calls never contain the base.** A record written as `/admin/users` produces `/admin/admin/users`. A top-level record without a leading slash, such as `users`, is not a shortcut either: the router throws because top-level route paths must start with `/`. ## The server: a fallback scoped to the sub-path For nginx, the fallback lives in the sub-path's `location` block and points at the sub-path's own `index.html`: ```nginx location /admin/ { try_files $uri $uri/ /admin/index.html; } ``` - A fallback to `/index.html` would serve whatever app lives at the root, or 404 if nothing does. - Real files under `/admin/assets/` are still served as files; only paths that match no file get the app shell. - A request for a missing script also receives HTML, which the browser rejects as the wrong content type; excluding the assets folder from the fallback keeps that failure a plain 404. ## Reading the symptoms | Symptom | Missing piece | |---|---| | Blank page, 404s for `/assets/*.js` | Vite `base` not set | | Links jump from `/admin/users` to `/users` | router base not passed | | `/admin/users` renders the not-found view | router base not passed, so the path does not match `/users` | | URLs read `/admin/admin/users` | base repeated inside route records | | Clicking works, reload returns 404 | no server fallback | ## Checking a sub-path deploy A short routine catches each layer before users do: 1. Build and open `dist/index.html`: script and style URLs must start with `/admin/`. 2. Click through the app and watch the address bar: every URL keeps the `/admin/` prefix exactly once. 3. Paste a deep URL such as `/admin/users/7` into a new tab: the right view must render, which proves the fallback. 4. Request a missing file such as `/admin/assets/nope.js`: decide deliberately whether it returns 404 or the app shell. 5. In code review, search route records and `push()` calls for the prefix; it should appear only in the Vite config. ## The hash-mode alternative If the nginx config belongs to another team and cannot change, `createWebHashHistory('/admin/')` produces `/admin/#/users`. The server only ever receives `/admin/`, so the default `index` handling is enough. The trade is uglier URLs and the loss of path-based routing on the server, which rarely matters for an internal admin tool but does for a public site. ## In unit tests The base rarely matters in tests: a memory history with no base keeps route paths as written. If a test does assert on `href` values, build its router with the same base the app uses, so `router.resolve('/users').href` matches production output.
- The same build must run under /admin/ in one environment and /backoffice/ in another. What changes?Vite bakes `base` into the built asset URLs, so the straightforward answer is one build per prefix. The route table does not change: each build's router reads its own `import.meta.env.BASE_URL`, and each nginx `location` block falls back to its own prefix's `index.html`.
- A developer adds `<base href="/admin/">` to index.html and calls createWebHistory() with no argument. Does that work?Yes. With no argument the router uses the `href` of the page's `<base>` tag, strips any origin and the trailing slash, and uses `/admin` as its base. It is still clearer to pass `import.meta.env.BASE_URL`, because the tag also changes how the browser resolves every relative URL on the page.
saying these in an interview costs you the question
- Put the /admin prefix into every route record's path.
- createWebHistory() reads Vite's BASE_URL on its own, so no argument is needed.
- Pass base as a createRouter() option, as in Vue Router 3.
- Point the nginx fallback at /index.html even when the app lives under /admin/.
- route.path includes the base, so compare it against '/admin/users'.