Static Hosting

A built Vue app is a folder of static files, and every host needs one rewrite rule so deep links reach index.html instead of a 404.

Syntaxtry_files $uri $uri/ /index.html; # nginx: serve the app shell for unmatched paths

npm run build writes a folder of static files. By default that folder is dist, set by Vite's build.outDir option. There is no Node process in the result and nothing to keep running. Any host that can serve files can serve a Vue single-page application: an S3 bucket, GitHub Pages, Netlify, Cloudflare Pages, nginx on a box you own.

The one rule every host needs

Vue Router's default history, createWebHistory(), puts real paths in the address bar: /invoices/42. That path exists only inside the JavaScript that is already loaded. The server has no invoices/42 file. So a first visit to that URL, or a refresh while on it, asks the server for something that is not there, and the server answers 404.

The fix is a rewrite: serve index.html for any request that does not match a real file. In nginx that is try_files $uri $uri/ /index.html;. On Netlify it is one line in _redirects. On Vercel it is a rewrites entry in vercel.json. The Vue Router guide lists the equivalent for Apache, Caddy, IIS, Firebase and Azure Static Web Apps.

What trips people up

With the fallback in place the server stops returning 404 for anything. Every unknown path now answers index.html with status 200. Add a catch-all route so the app renders its own not-found view.

Then there is the subdirectory deploy. If the app lives at /app/, set Vite's base to '/app/' and pass the same string to createWebHistory(). Set one and not the other and either the assets 404 or no route ever matches.

Example

Example · vue
<script setup lang="ts">
import { useRoute } from 'vue-router'

const route = useRoute()
</script>

<template>
  <section class="not-found">
    <h1>Page not found</h1>
    <p>
      Nothing is routed to <code>{{ route.fullPath }}</code>.
    </p>
    <router-link to="/">Back to the dashboard</router-link>
  </section>
</template>

<style scoped>
.not-found {
  padding: 3rem 1rem;
  text-align: center;
}

.not-found code {
  word-break: break-all;
}
</style>

When to use it

  • Publishing a built Vue app to an S3 bucket or Cloudflare Pages and fixing the 404 on refresh.
  • Hosting the app under a subdirectory such as example.com/admin/ without breaking asset URLs.
  • Giving a mistyped URL a real not-found page instead of an empty router-view.

More examples

src/router/index.ts

The catch-all pattern `/:pathMatch(.*)` is the one the Vue Router guide gives for this. It has to be last, because routes are matched in order. Reach for this the moment you turn on the server rewrite: the rewrite takes 404s away from the server, so the app has to produce them instead. `createWebHistory(import.meta.env.BASE_URL)` is what `npm create vue@latest` scaffolds, and it is why a subdirectory deploy needs only Vite's `base` changed.

Example · ts
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import NotFoundView from '../views/NotFoundView.vue'

const router = createRouter({
  // import.meta.env.BASE_URL is Vite's `base` option, so the router and the
  // asset URLs always agree about where the app is mounted.
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/', name: 'home', component: HomeView },
    {
      path: '/invoices/:id',
      name: 'invoice',
      component: () => import('../views/InvoiceView.vue'),
    },
    // The catch-all. Without it, an unknown path renders a blank router-view.
    { path: '/:pathMatch(.*)', name: 'not-found', component: NotFoundView },
  ],
})

export default router

nginx site config

This is the standalone server block from the Vue Router guide, aimed at the official nginx image's document root. `try_files` tries the request as a file, then as a directory, then falls back to the app shell. Use it when you control the web server; the shorter form is just the `location /` block dropped into a site you already have.

Example · nginx
# /etc/nginx/conf.d/default.conf
server {
  listen 80;
  server_name localhost;
  root /usr/share/nginx/html;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

Managed host rewrite rules

The same rule on two managed hosts, both taken from the Vue Router guide. The `200` on the Netlify line matters: it rewrites rather than redirecting, so the browser keeps the original URL and the router can read it. The `_redirects` file goes in `public/`, because Vite copies that directory to the output root unchanged.

Example · text
# Netlify or Cloudflare Pages -- public/_redirects
/*  /index.html  200

# Vercel -- vercel.json
{
  "rewrites": [{ "source": "/:path*", "destination": "/index.html" }]
}

Discussion

  • Be the first to comment on this lesson.