It ran perfectly on localhost. You deployed it, opened the address, and got a blank white page — or the text with no styling, or everything except one image.
Almost every time, the deployed page is asking for a file at an address where that file does not exist. The dev server on your machine is forgiving in ways a real host is not: it serves from the root, on a Mac or a Windows PC it reads a disk that ignores upper and lower case, it answers any path, and it reads your .env. Production takes each of those away. Open the browser's Network tab, reload, and the first request that failed — or came back as an HTML page when it should not have — tells you which one you hit.
The 30-second version
Console messages below were copied from Chromium 153 on 6 November 2026, loading a small Vite app deployed each of the wrong ways.
| What you see | Why | Fix |
|---|---|---|
Blank page; scripts and CSS 404 under /assets/… instead of /repo/assets/… |
The site is served from a subfolder and the build points at / |
Set Vite's base, Next's basePath or Create React App's homepage, and rebuild |
Blank page; Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html" |
A script address returned an HTML page with status 200 — usually an SPA fallback serving index.html |
Same as above: find which address, then fix the path |
Same error with a type that is not JavaScript (in our test, "image/png") |
You published the project folder, not the build output | Point the host at dist, build or out |
A folder starting with _ is missing on GitHub Pages |
Jekyll skips it | Add an empty .nojekyll, or deploy with an Actions workflow |
| One image 404s that works on your machine | The name differs only in upper/lower case | Match the case exactly; rename with git mv |
Blank page when you double-click index.html |
A built site is not meant to open from disk | Serve it: npx vite preview, which honours base; another local server must put the site at the same path it will have online |
| Styles or scripts blocked only on the live HTTPS site | An http:// address inside an https:// page |
Use https:// or a relative address |
| An API address or feature flag is wrong in production | The value is compiled in at build time, and the build that ran did not have the one you expect | Set it where the build runs, then build and deploy again |
What is actually happening
Your dev server — vite, next dev, npm start — is built to never get in your way. Vite serves your project at http://localhost:5173/, so every path that starts with / points at the top of your project. It reads files through your operating system, and on macOS and Windows the disk treats Logo.png and logo.png as the same file. It compiles source on the fly, so you never notice which folder the finished files would live in.
A host serves a folder of finished files at whatever address it gives you, from a server that cares about case, over HTTPS. Each difference is one of the causes below.
Find the first failing request
Before changing anything, find out which file is missing and what address the page used for it.
- Open the deployed site and press F12 (Cmd+Option+I on a Mac).
- Choose the Network tab, then reload the page.
- Find the first row in red, or with a status of 404. Click it and look at the full address it requested.
- Compare that address with where the file actually is on the deployed site.
The difference between those two addresses is the bug. /assets/index.js versus /my-repo/assets/index.js is a subfolder problem. /images/logo.png versus /images/Logo.png is a case problem. A request that returned 200 but whose response is your own HTML page is the next section.
When the error says "MIME type"
This is the message people search for, and it is misleading:
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.It sounds like the host is configured wrong. Usually it is not. The browser asked for a script, and the address it used returned an HTML page with a 200 status — on a host set up for single-page apps, your own index.html, which a fallback rule serves for every path it does not recognise. A real 404 looks different: in our test it printed only Failed to load resource: the server responded with a status of 404 (Not Found), with no MIME line. Module scripts must arrive with a JavaScript MIME type, so the browser refuses it. Fix the address and the error goes away.
Older Chrome versions say Expected a JavaScript module script without the -or-Wasm. Same meaning. And when the stylesheet suffers the same fate, Chromium 153 printed nothing at all about it in our test — not in the console, not in the Issues panel — and the page simply rendered unstyled. If your console does show a line beginning Refused to apply style from, it is the same problem. Either way, check the CSS request in the Network tab and you will find it answered with your HTML.
The site lives in a subfolder
This is the most common cause, and GitHub Pages is where most people meet it. A user or organisation site lives at https://<owner>.github.io, but a project site lives at https://<owner>.github.io/<repository>. Your build still points every asset at /assets/…, which on that host is the top of the domain — outside your site.
Every bundler has one setting for this, and it has to be set before you build:
- Vite: set
baseinvite.config.js, or passvite build --base=/my-repo/. Vite's own GitHub Pages instructions say'/'for<USERNAME>.github.ioor a custom domain, and'/<REPO>/'for a project page. - Next.js (static export):
basePath. It is inlined at build time, so changing it means rebuilding.next/linkapplies it for you; thesrcofnext/imagedoes not. - Create React App: the
homepagefield inpackage.json.
// vite.config.js — for https://<owner>.github.io/my-repo/
import { defineConfig } from "vite";
export default defineConfig({
base: "/my-repo/",
});Vite also accepts base: "./", which makes every asset address relative to the file that uses it. That works at any subfolder without knowing its name, with one catch we measured: on a host that serves index.html for every path, a visitor who lands directly on /about/team gets a page whose ./assets/… resolves to /about/assets/…, and the site is blank again. If your app has client-side routes, use the explicit path.
Hosts that give every site its own hostname — a Netlify or Vercel project domain, a GitHub user site, a subdomain on dropto.run — serve at the root, so the default base of / is already right. Hosting a Vite app covers the rest of that setup.
You published the wrong folder
The folder you work in is not the folder a host should serve. A build writes the finished site somewhere else — dist for Vite, build for Create React App, out for a Next.js static export — and only that folder belongs online.
Publish the project folder instead and the browser receives your source. In our test, index.html loaded main.js unbuilt, main.js tried to import ./Logo.png as if it were code, and Chromium said so precisely:
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "image/png".Each host names the setting differently. Netlify's publish directory is the folder holding the deploy-ready files, and anything outside it is not deployed. Vercel serves only the Output Directory. Cloudflare Pages has a build output directory with presets per framework. On GitHub Pages it is whatever your workflow uploads, or the branch folder you chose.
GitHub Pages and folders that start with an underscore
If you publish from a branch, GitHub Pages runs Jekyll on it first, and Jekyll does not build files or folders that start with _, . or #. A build that writes _app/ or _next/ loses that folder silently. GitHub's documentation gives two ways out: deploy with an Actions workflow, which it recommends, or put an empty file called .nojekyll at the root of what you publish.
A filename differs only in case
<img src="images/logo.png"> and a file called Logo.png work on your Mac and 404 on the host. MDN's advice is blunt: most web servers are case-sensitive, so pick lowercase names and stick to them.
The trap comes when you fix it. Rename Logo.png to logo.png in Finder or Explorer, commit, deploy — and it still 404s. On a case-insensitive disk, Git treats the two names as the same file and keeps remembering the old one. Your commit never contained the rename. Tell Git explicitly:
git mv images/Logo.png images/logo.png
git commit -m "Lowercase the logo filename"Things that were only true on localhost
You opened the build by double-clicking it. A built index.html opened from disk is blank, and the console blames CORS. With the default base, the page asks for file:///assets/… — the root of your disk. With a relative base the addresses are right, but Vite's build loads its script as a module and its stylesheet with crossorigin, both of which need CORS — and browsers treat every local file as its own origin, so both were blocked in our test. Check a build with npx vite preview, which serves it under the same base it was built for, never by double-clicking. Why AI-made pages go blank the same way has the full list of errors.
An http:// address inside an https:// page. Your dev server ran on plain http://localhost, so a script or stylesheet loaded from an http:// address worked. On a live HTTPS site, Chrome blocks it:
Mixed Content: The page at 'https://…' was loaded over HTTPS, but requested an insecure script 'http://…/s.js'. This request has been blocked; the content must be served over HTTPS.Images are treated differently — Chrome upgrades them to HTTPS and they load if the other server supports it. Scripts, stylesheets, fetch() calls and fonts are blocked. Change the address to https://, or make it relative if the file is on your own site.
An environment variable from your .env. Vite puts every VITE_* value into the bundle at build time. If the host builds your site, it never sees a .env you kept out of Git, and the value is empty; if you change the value on the host afterwards, Vercel notes that changes only apply to new deployments. Set it on the host and deploy again. And because it ends up in the bundle, anyone can read it — a secret does not belong there.
When the problem is something else
Three cases look similar and are not covered here:
- The home page works, but refreshing any other page gives a 404. That is a single-page app without a fallback rule: why a static site 404s on refresh.
- The site works, but shows yesterday's version. That is a cache: why a site still shows the old version.
- The code needs a server — PHP, a Node API route, server-side rendering. A static host cannot run it, whatever the path. What "static" actually means explains the line.
When not to do this
Three fixes that look reasonable and make things worse:
- Do not hard-code your domain into every path,
https://me.github.io/my-repo/assets/…. It fixes today's address and breaks the day you move to a custom domain. The bundler setting exists so that the address is decided in one place. - Do not "fix" the MIME error in the server's MIME configuration. The server is almost always right: the address returned an HTML page. Changing what type HTML is served as hides the missing file instead of finding it.
- Do not deploy
vite previewas your server. It is the right way to check a build locally, and Vite says it is not meant as a production server.
Questions
Why does my Vite app show a blank page on GitHub Pages?
It is a project site, served at /<repo>/, and the build asks for /assets/…. Set base: "/<repo>/" in vite.config.js, rebuild and deploy again.
Should I use base: "./" or base: "/my-repo/"?
Use the explicit path if your app has client-side routes. A relative base resolves against the address the visitor is on, so a deep link such as /about/team asks for its assets in the wrong place. For a single page with no routes, ./ is fine.
Does "the server responded with a MIME type of text/html" mean my server is misconfigured?
Rarely. It means the script's address returned an HTML page with a 200 status — usually an SPA fallback serving index.html for a path that has no file. Find that request in the Network tab and fix its path.
Why can't I just double-click index.html in dist?
With the default base, asset paths start at the root of your disk. With a relative one the addresses are right, but Vite's build loads its code as a module script, which browsers refuse to load from file://. Use npx vite preview, which serves the build under the base it was built for.
The image works on my Mac but 404s on the host. Why?
The name differs only in upper or lower case. macOS and Windows disks ignore case by default; most web servers do not.
I renamed the file to lowercase and it still 404s. Why?
Git on a case-insensitive disk kept the old name, so the rename never reached the commit. Use git mv OldName newname and commit again.
I changed an environment variable on the host and nothing happened. Why?
The value was compiled into the files at build time. Changing it does nothing until the site is built and deployed again.