Vite Manifest Not Found: Four Different Causes
When Vite manifest not found appears right after a deploy, the answer you find is almost always "run npm run build". Measuring six cases in Docker shows that the build fixes one of the four causes, and that two of them never produce an err…
Verification environment
- Laravel 13.23.0
- PHP 8.4.24
- Node 22.23.2
- Cases reproduced 6 — two of them return HTTP 200
- Key finding npm run build does not remove public/hot
- OS Docker Desktop (php:8.4-cli-bookworm)
When Vite manifest not found appears right after a deploy, the answer you find is almost always "run npm run build". Measuring six cases in Docker shows that the build fixes one of the four causes, and that two of them never produce an error at all.
Run
npm run build.
The awkward part is that two of those four causes throw nothing at all — HTTP 200, empty logs, and a page that is quietly broken.
The build actually runs here; no manifest.json was hand-written to make things look convincing.
Scripts: laravel-error-lab.
Environment
| Laravel | 13.23.0 |
| PHP | 8.4.24 |
| Node | 22.23.2 |
| Runtime | Docker Desktop / php:8.4-cli-bookworm |
Each case changes exactly one thing on disk, starting from a working build.
What the six cases actually do
| Case | Change | HTTP | Exception |
|---|---|---|---|
| A | (working case) | 200 | — |
| B | public/build deleted |
500 | ViteManifestNotFoundException |
| C | public/hot left behind (build present) |
200 | none |
| D | public/hot left behind and no build |
200 | none |
| E | manifest only at public/build/.vite/ |
500 | ViteManifestNotFoundException |
| F | @vite() names an entry not in the manifest |
500 | ViteException |
npm run build is the answer to B, and only B.
There are two exceptions, and they get conflated
People talk about "the manifest error" as one thing. The class and the message differ.
B / E:
Illuminate\Foundation\ViteManifestNotFoundException
Vite manifest not found at: /app/public/build/manifest.json
F:
Illuminate\Foundation\ViteException
Unable to locate file in Vite manifest: resources/js/does-not-exist.js.
F means the manifest was read fine — the entry you asked for simply is not in it. That happens when the path in @vite() disagrees with input in vite.config.js, and rebuilding does not fix it.
Reading the message to the end distinguishes them, but both start with "Vite manifest", so they collapse into the same search query.
B and E are indistinguishable
In case E the manifest file exists — at public/build/.vite/manifest.json. The error is nevertheless byte-identical to B, still naming public/build/manifest.json.
Laravel looks in exactly one place:
Illuminate/Foundation/Vite.php
56: protected $manifestFilename = 'manifest.json';
975: protected function manifestPath($buildDirectory)
977: return public_path($buildDirectory.'/'.$this->manifestFilename);
With laravel-vite-plugin the manifest lands at public/build/manifest.json. Plain Vite 5 and later writes to .vite/manifest.json, so dropping the plugin or hand-rolling the Vite config produces this state.
"The file is right there and it says it isn't" — you cannot tell until you look at the actual paths.
ls -la public/build/manifest.json public/build/.vite/manifest.json
The worst case is C, because nothing errors
This is the real point of the article.
If a file called public/hot is present, HTTP 200 comes back and no exception is thrown. The page renders perfectly.
Here is what that page then asks the browser to load:
<script type="module" src="http://127.0.0.1:5173/@vite/client"></script>
<link rel="stylesheet" href="http://127.0.0.1:5173/resources/css/app.css" />
<script type="module" src="http://127.0.0.1:5173/resources/js/app.js"></script>
127.0.0.1:5173 is the Vite dev server. Nothing like that runs in production. Fetching it the way a browser would:
http://127.0.0.1:5173/@vite/client -> connection refused (nothing is listening)
Nothing reaches the server logs. The status is 200. Uptime monitoring sees a healthy site. All that exists is a page with no styling.
Why
Illuminate/Foundation/Vite.php
239: return $this->hotFile ?? public_path('/hot');
1223: return is_file($this->hotFile());
isRunningHot() checks only whether the file exists. Not its contents, not whether a dev server is actually alive.
When it returns true, the manifest is never read at all — which is why case D (hot file present, no build) also returns 200. Whether you have build output stops mattering.
npm run dev creates public/hot. A clean exit removes it; an ungraceful kill leaves it behind, and from there it can be committed and shipped.
npm run build cannot fix this
The standard advice was measured against it:
after 'npm run build' with a hot file present: STILL THERE
The build does not delete public/hot. Rebuilding changes nothing, however many times you do it. What is needed is:
rm public/hot
public/hot also belongs in .gitignore. Laravel's stock file covers it — worth confirming if yours has been edited.
Working out which one you have
Three looks at the filesystem settle it.
# 1. is a hot file present? (if so this is your cause — and there is no error)
ls -la public/hot
# 2. where is the manifest?
ls -la public/build/manifest.json public/build/.vite/manifest.json
# 3. does the manifest contain what @vite() asks for?
cat public/build/manifest.json | head -20
| What you see | Case | Fix |
|---|---|---|
public/hot exists |
C / D | rm public/hot |
no public/build/manifest.json |
B | npm run build |
manifest only under .vite/ |
E | use laravel-vite-plugin, or align the output path |
| manifest present, entry missing | F | make @vite() and vite.config.js input agree |
If there is no error but the CSS is missing, start at 1. If you have a 500, read the class name in the message: ViteManifestNotFoundException sends you to 2, ViteException to 3.
Stopping it recurring on deploy
C is a development artifact reaching production, so the deploy can close it off:
# anywhere around the build step in your deploy script
rm -f public/hot
One line, and C and D both become structurally impossible.
FAQ
Q. I ran npm run build and it did not help
public/hot is probably still there. The build does not remove it — measured. Check ls -la public/hot and rm public/hot if it exists.
Note this case does not produce "Vite manifest not found" at all: you get HTTP 200 with the CSS and JS simply never loading.
Q. What is public/hot?
A file npm run dev creates, containing the Vite dev server URL. Laravel checks only whether it exists and, if so, points assets at the dev server instead of reading the manifest (Vite.php:1223). It never verifies the dev server is running.
A clean exit from npm run dev deletes it; a forced kill leaves it.
Q. My manifest.json exists but Laravel says it cannot find it
Laravel reads exactly one path: public/build/manifest.json (Vite.php:977). Plain Vite 5 and later writes .vite/manifest.json, so the two diverge when laravel-vite-plugin is not in use.
Check ls -la public/build/.vite/manifest.json. The error message is identical to the file-is-missing case, so nothing but the paths will tell you.
Q. Is Unable to locate file in Vite manifest the same problem?
No. That is ViteException, and the manifest loaded successfully — the entry named in @vite() is not in it.
Check that the path in @vite(['resources/js/app.js']) matches input in vite.config.js. Rebuilding will not fix it.
Q. Styling broke in production but nothing is erroring
Suspect public/hot. With that file present you get HTTP 200, no exception, and nothing in the server logs. The quickest confirmation is the browser devtools network tab: check whether assets are being requested from localhost:5173.
Reproducing this
git clone https://github.com/codelift-dev/laravel-error-lab
cd laravel-error-lab
docker compose build
docker compose run --rm lab bash vite-manifest.sh # reproduce all six
docker compose run --rm lab bash vite-manifest-detail.sh # exact exception text
Everything runs inside Docker. No PHP and no Node on the host.
Verified on Laravel 13.23.0 with Node 22.23.2. The Vite.php line numbers will drift, but two things have held since Laravel 9: isRunningHot() tests only for the hot file's existence, and the manifest path is fixed at public/build/manifest.json.
Related articles
- Laravel's 419 Page Expired Can Be Diagnosed Search for how to fix 419 Page Expired and you get the same list every time. Check @csrf. Check the session driver. Run php artisan config:clear. Check APP_KEY.
- We Actually Ran the Laravel 12 to 13 Upgrade There is no shortage of Laravel 13 upgrade guides. The problem is that most of the warnings in them were written without running the upgrade.