| | |

Vite 30 ⚡ Deployment — Static hosting & SPA routing rules

⏱️ Estimated read: 12 min | |

Vite builds a static site. The vite build command produces a dist/ directory containing index.html, the hashed JavaScript and CSS assets, and any files copied from the public/ directory. The deployment is the process of serving that directory from a static host, and the SPA routing rule is the configuration that makes client-side routing work when the user refreshes the page or opens a deep link. The build is the same regardless of the host. The routing rule is host-specific, and it is the single most common reason a deployed Vite application returns a 404 on a route that works in development.

The 404 is not a Vite bug. It is the browser asking the host for /dashboard, and the host looking for a file at that path, which does not exist because the application is a single index.html with a client-side router. The fix is a rewrite rule that tells the host to serve index.html for any path that does not match a real file. The rule is a one-liner on Netlify, a JSON object on Vercel, and a try_files directive on Nginx. The rule is the SPA routing configuration, and it is the first thing to verify after a deployment.

Key point: A Vite SPA deployment needs two things beyond the build: the correct base path if the app is not at the domain root, and a history-fallback rewrite that serves index.html for unmatched routes. The build command is npm run build, the output directory is dist, and the routing rule is host-specific.


Why the SPA routing rule is necessary

The file-system assumption. A static host is a file server. When the browser requests /dashboard, the host looks for a file named dashboard or a directory named dashboard with an index.html. A Vite SPA does not have those files. It has a single index.html and a JavaScript bundle that reads the URL and renders the correct component. The host returns a 404 before the JavaScript has a chance to run. The rewrite rule changes the host’s behavior: instead of returning a 404, it serves index.html with a 200 status, and the browser runs the SPA, which then reads the URL and renders the route .

The base path problem. The default base is /, which means the assets are referenced from the domain root. If the app is deployed under a subpath—/docs/ or /my-app/—the asset URLs point to the wrong location, and the page loads without CSS or JavaScript. The base option in vite.config.ts must match the deployment path. For a GitHub Pages project site, the base is /repo-name/. For a subdirectory behind a reverse proxy, the base is the subdirectory path .

The relative base alternative. The base: './' option produces relative asset paths that work regardless of the deployment directory. The vite-basepath plugin automates this, but the trade-off is that relative paths rely on modern browser features and can complicate the router’s basename configuration . The explicit base path is the more predictable choice for a known deployment target.

The trade-off. The rewrite rule is a host configuration, not an application configuration. A project that deploys to multiple hosts needs the rule in each host’s format. A project that uses hash-based routing (HashRouter instead of BrowserRouter) avoids the rule entirely, because the URL fragment is not sent to the server, but the URLs are less clean . The rewrite rule is the standard solution, and the hash router is the workaround for a host that cannot be configured.


a. The host-specific routing rules

The rewrite rule is expressed differently on each host. The rule is always “serve index.html for any path that does not match a static file.”

Netlify. The rule goes in a _redirects file in the public/ directory, or in a netlify.toml file. The _redirects syntax is a single line, and the 200 status makes it a rewrite rather than a redirect .

# public/_redirects — the Netlify SPA rewrite
/*    /index.html   200

The netlify.toml form is the equivalent configuration with the build settings included.

# netlify.toml — the build and rewrite configuration
[build]
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Vercel. The rule goes in a vercel.json file at the project root. The rewrites array is the mechanism, and the source pattern matches every path .

// vercel.json — the Vercel SPA rewrite
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "framework": "vite",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

Cloudflare Pages. The rule goes in a _redirects file in the public/ directory, with the same syntax as Netlify. The build output directory is dist, and the build command is npm run build .

# public/_redirects — the Cloudflare Pages SPA rewrite
/*    /index.html   200

Nginx. The rule is the try_files directive in the location block. The directive checks the file system for the requested path, then the directory, and falls back to index.html .

# nginx.conf — the SPA rewrite
location / {
  try_files $uri $uri/ /index.html;
}

The Nginx rule is the same for a subpath deployment, but the location block must match the subpath, and the base in the Vite config must match it too .


b. The base path and the build configuration

The base option is the build-time configuration that tells Vite where the application will be served. The default is /, which works for a domain root deployment. A subpath deployment needs the subpath as the base, with a leading and trailing slash.

// vite.config.ts — the base path for a subdirectory deployment
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  // The base must match the deployment path.
  // For https://example.com/docs/, the base is '/docs/'.
  base: '/docs/',
});

The import.meta.env.BASE_URL is the runtime value that Vite exposes. It is the base path with the trailing slash, and it is used to construct URLs for public/ assets or for the router’s basename .

// The router basename from the Vite base
import { BrowserRouter } from 'react-router-dom';

// The basename is the base path without the trailing slash.
const basename = import.meta.env.BASE_URL.replace(/\/$/, '');

root.render(
  <BrowserRouter basename={basename}>
    <App />
  </BrowserRouter>
);

The vite-basepath plugin is the alternative for a project that does not know the final deployment path. It sets the base to ./ and provides a getBase() helper for the router. The plugin is the mechanism for a deployment that might be at the root or a subdirectory, but the explicit base is the simpler configuration when the path is known .


Complete Example Session

// ============================================
// PART 1: THE VITE CONFIG
// ============================================

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  // The base is '/' for a domain root deployment.
  // It is '/docs/' for a subpath deployment.
  base: '/',
});

// ============================================
// PART 2: THE NETLIFY RULE
// ============================================

// public/_redirects
// /*    /index.html   200

// The rule serves index.html for any path.
// The status 200 makes it a rewrite, not a redirect.

// ============================================
// PART 3: THE VERCEL RULE
// ============================================

// vercel.json
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "framework": "vite",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

// ============================================
// PART 4: THE CLOUDFLARE PAGES RULE
// ============================================

// public/_redirects
// /*    /index.html   200

// The same syntax as Netlify.
// The build output directory is dist.

// ============================================
// PART 5: THE NGINX RULE
// ============================================

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

// The directive checks the file system first.
// If the path is not a file or directory, index.html is served.

// ============================================
// PART 6: THE SUBPATH DEPLOYMENT
// ============================================

// vite.config.ts — the subpath base
export default defineConfig({
  base: '/docs/',
});

// The assets are referenced from /docs/assets/.
// The router basename must match the base.

// ============================================
// PART 7: THE ROUTER BASENAME
// ============================================

// The BrowserRouter basename from the Vite base.
import { BrowserRouter } from 'react-router-dom';

const basename = import.meta.env.BASE_URL.replace(/\/$/, '');

root.render(
  <BrowserRouter basename={basename}>
    <App />
  </BrowserRouter>
);

// The basename tells the router which path prefix to strip.

// ============================================
// PART 8: THE BUILD AND PREVIEW
// ============================================

// npm run build
// The output is in dist/.
//
// npm run preview
// The preview server serves dist/ and tests the production build.
// The preview server does not have the SPA rewrite rule,
// so deep links may 404 in preview. The host has the rule.

// ============================================
// PART 9: THE VERIFICATION
// ============================================

// 1. Deploy the dist/ directory to the host.
// 2. Open the root URL and verify the app loads.
// 3. Navigate to a route and refresh the page.
// 4. The app should load, not a 404.
// 5. Check the network tab for the asset URLs.
// 6. The assets should load from the correct base path.

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The build command is npm run build.
// [ ] The output directory is dist.
// [ ] The base path matches the deployment path.
// [ ] The SPA rewrite rule is configured on the host.
// [ ] The router basename matches the base path.
// [ ] A deep link loads after a refresh.
// [ ] The assets load from the correct path.

The ten parts covered the config, the four host rules, the subpath deployment, the router basename, the build and preview, the verification, and the checklist. The through-line is that the build is the same and the routing rule is the host-specific piece.


Quick Reference

The host routing rules

HostFileRule
Netlifypublic/_redirects/* /index.html 200
Netlifynetlify.toml[[redirects]] from = "/*" to = "/index.html" status = 200
Vercelvercel.json"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
Cloudflare Pagespublic/_redirects/* /index.html 200
Nginxnginx.conftry_files $uri $uri/ /index.html;

The base path

DeploymentbaseRouter basename
Domain root'/'import.meta.env.BASE_URL
Subpath /docs/'/docs/'/docs
Unknown path'./' (via plugin)getBase() from vite-basepath

The build settings

SettingValue
Build commandnpm run build
Output directorydist
Preview commandnpm run preview

Best Practices

✅ Do This:

// Add the SPA rewrite rule on the host
/*    /index.html   200                                          // ✅
// Set the base path for a subpath deployment
base: '/docs/'                                                   // ✅

// Set the router basename to match
<BrowserRouter basename={basename}>                              // ✅

❌ Don’t Do This:

// Deploy without a rewrite rule
// Deep links 404 on refresh.                                    // ❌

// Use the default base for a subpath deployment
base: '/' // assets load from the wrong path                    // ❌

// Use HashRouter as the permanent solution
// The URLs are less clean.                                      // ❌

Common Pitfalls

PitfallWhy It HappensFix
404 on refreshNo rewrite ruleAdd the host-specific rule
Blank pagebase does not match the deployment pathSet base to the correct path
Assets 404The asset URLs use the wrong baseSet base and rebuild
Router does not strip the subpathThe basename is not setSet basename to the base path
The preview server 404sThe preview server has no rewrite ruleTest deep links on the host, not in preview

Real-World Examples

1. The Netlify rule

/*    /index.html   200

2. The Vercel rule

{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }

3. The Cloudflare rule

/*    /index.html   200

4. The Nginx rule

try_files $uri $uri/ /index.html;

5. The subpath base

base: '/docs/'

6. The router basename

<BrowserRouter basename={import.meta.env.BASE_URL}>

7. The build command

npm run build

8. The output directory

dist

9. The preview command

npm run preview

10. The verification

Navigate to /dashboard and refresh. The app loads.

Visual

The SPA Routing Problem

┌──────────────────────────────────────────────────────────────┐
│  WHY THE 404 HAPPENS                                          │
│                                                              │
│  Browser requests /dashboard                                 │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ Static host looks   │                                    │
│  │ for /dashboard      │                                    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ File not found      │                                    │
│  │ → 404               │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  The SPA never runs because the host returns 404.            │
└──────────────────────────────────────────────────────────────┘

The Rewrite Rule

┌──────────────────────────────────────────────────────────────┐
│  THE RULE SERVES index.html FOR UNMATCHED PATHS               │
│                                                              │
│  Browser requests /dashboard                                 │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ Static host checks  │                                    │
│  │ for a real file     │                                    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ No file → serve     │                                    │
│  │ index.html (200)    │                                    │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  ┌─────────────────────┐                                    │
│  │ The SPA runs and    │                                    │
│  │ reads /dashboard    │                                    │
│  └─────────────────────┘                                    │
└──────────────────────────────────────────────────────────────┘

The Base Path

┌──────────────────────────────────────────────────────────────┐
│  THE BASE MUST MATCH THE DEPLOYMENT PATH                      │
│                                                              │
│  Domain root:                                                │
│  base: '/'                                                   │
│  https://example.com/assets/app.js                           │
│                                                              │
│  Subpath:                                                    │
│  base: '/docs/'                                              │
│  https://example.com/docs/assets/app.js                      │
│                                                              │
│  A mismatch produces a blank page or missing assets.        │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Build commandnpm run build
Output directorydist
SPA rewriteServe index.html for unmatched routes
Netlify rule/* /index.html 200
Vercel rulerewrites array in vercel.json
Cloudflare rule/* /index.html 200
Nginx ruletry_files $uri $uri/ /index.html
Base pathbase in vite.config.ts
Router basenameimport.meta.env.BASE_URL

Key takeaways:

  • A Vite SPA needs a history-fallback rewrite. The host must serve index.html for any path that does not match a real file. Without the rule, a refresh on a client-side route returns a 404 because the host looks for a file that does not exist .
  • The rewrite rule is host-specific. Netlify and Cloudflare Pages use a _redirects file. Vercel uses a vercel.json with a rewrites array. Nginx uses a try_files directive. The rule is the same in each case: serve the SPA shell .
  • The base path must match the deployment path. The default / works for a domain root. A subpath deployment needs base: '/docs/' (or the equivalent), and the router’s basename must match the base .
  • The build output is the same for every host. The dist/ directory contains the index.html, the hashed assets, and the public/ files. The deployment is the process of serving that directory with the host-specific routing rule.
  • The preview server is not the host. npm run preview serves the dist/ directory, but it does not have the SPA rewrite rule. A deep link may 404 in preview and work on the host. The verification of the routing rule happens on the deployed host, not in the preview server.

Remember: A Vite deployment is the dist/ directory plus two configuration pieces: the base path and the SPA rewrite rule. The build command is npm run build, the output directory is dist, and the routing rule is a one-liner on most hosts. The 404 on refresh is not a Vite bug; it is the host’s file-system assumption meeting the SPA’s client-side routing. The rewrite rule resolves the mismatch by serving the SPA shell for every unmatched path. The base path resolves the asset URL mismatch for a subpath deployment. The two configurations are the deployment.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!