Fix: 404 Not Found on direct URLs in Vercel (SPA Routing)

The Symptom

You deployed a Single Page Application (SPA) built with Vanilla JS, React, Vite, or Vue to Vercel. Clicking internal links works fine, but when you manually type a URL (like /dashboard) or refresh the page, you get a hard Vercel 404 error, or a blank white screen with this console error:

404: NOT_FOUND
Code: NOT_FOUND

OR

Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced.

The Cure

Vercel's edge network is looking for a physical /dashboard/index.html file that doesn't exist. You must instruct Vercel to route all traffic back to your master index.html file so your client-side JavaScript can take over the routing.

Immediate Resolution:

Create a vercel.json file in the absolute root of your project directory and add this configuration:

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

The Breakdown

Single Page Applications (SPAs) have only one real HTML file: index.html. When you click a link in your app to go to /settings, the browser doesn't actually request a new file from the server. Instead, your JavaScript (using the History API or a router library) intercepts the click and injects the "Settings" component into the DOM locally.

However, when you press Refresh, the browser bypasses your local JavaScript and asks the Vercel server directly for a file located at /settings/index.html. Because you didn't generate that physical file, Vercel throws a 404. The rewrites rule catches every request sent to the server and forces Vercel to serve the master index.html, allowing your JavaScript router to wake up, look at the URL bar, and render the correct view.

Prevention & Caveats

If you are using Serverless functions alongside your SPA, update your vercel.json to exclude the API directory from the rewrite:

json (Safe Rewrite)
{
  "rewrites": [
    {
      "source": "/((?!api/.*).*)",
      "destination": "/index.html"
    }
  ]
}

Developer Logs

Loading logs...