skip to content

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?

level: middleimportance: should knowfreq 44%

answer

  1. three layers must agree
  2. Vite base feeds BASE_URL
  3. records stay app-relative
  4. base added on write, stripped on read
  5. fallback to the sub-path's index.html

basics

~10 s

Set 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 s

Three 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
ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  base: '/admin/',
  plugins: [vue()],
})

go deeper

for a junior

Remember that the prefix goes into the history factory's argument and route records stay like /users.

for a middle

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.

for a senior

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.

for a principal

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'.