This post is a follow-up to an earlier guide, Using CloudPanel to Deploy a Laravel App.
If your Laravel app works locally but returns 404 for flux.js or livewire.js in production, this is often an Nginx routing issue. If you're using CloudPanel as suggested in the guide, the default vhost pattern generated by CloudPanel will definitely lead to this issue, but the root cause is Nginx behaviour itself.
The Symptom
After deployment, you may see one of these fail in the browser network tab:
/flux/flux.js/flux/flux.min.js/livewire/livewire.js/livewire/livewire.min.js/livewire-<hash>/livewire.js(Livewire 3 with hashed endpoint prefix)/livewire-<hash>/livewire.min.js
Locally, everything can look fine (especially with php artisan serve), but production Nginx returns 404.
Why This Happens
Neither Livewire nor Flux serve their JavaScript from public/build. Both register Laravel routes that stream the asset from the vendor directory at request time:
- Livewire exposes
/livewire/livewire.js,/livewire/livewire.min.js, and (in recent Livewire 3 releases) a hashed variant like/livewire-<hash>/livewire.js. - Flux exposes
/flux/flux.jsand/flux/flux.min.js.
The @livewireScripts and @fluxScripts Blade directives render <script> tags pointing at those URLs, so the browser requests them on every page load.
The issue happens when your Nginx config has a static-asset location similar to this (CloudPanel's default Laravel vhost includes it):
location ~* ^.+\.(css|js|jpg|jpeg|gif|png|ico|gz|svg|svgz|ttf|otf|woff|woff2|eot|mp4|ogg|ogv|webm|webp|zip|swf|map|mjs)$ {
expires max;
access_log off;
}
That regex matches anything ending in .js, including /flux/flux.js and /livewire/livewire.js. Because no physical file exists at those paths, Nginx returns 404 before Laravel ever sees the request.
The Robust Fix
Keep your static asset optimization, but add a fallback to backend routing when the file does not exist:
location ~* ^.+\.(css|js|jpg|jpeg|gif|png|ico|gz|svg|svgz|ttf|otf|woff|woff2|eot|mp4|ogg|ogv|webm|webp|zip|swf|map|mjs)$ {
try_files $uri @backend;
add_header Access-Control-Allow-Origin "*";
add_header alt-svc 'h3=":443"; ma=86400';
expires max;
access_log off;
}
location @backend {
proxy_pass http://127.0.0.1:8080; # Or whatever local (to the server) url your app is available at
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
}
This is the safest general solution because it preserves static delivery for real files while forwarding framework-managed URLs (Flux, Livewire and anything else served through Laravel) to the backend.
Alternative Fix
If you'd rather not touch the static-asset block, you can special-case the Livewire and Flux paths before the static regex so they always reach Laravel:
location ^~ /livewire/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Livewire 3 with hashed endpoint prefix
location ~ ^/livewire-[a-f0-9]+/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ^~ /flux/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
That works, but the try_files fallback approach is usually more future-proof, since other Laravel packages can register similar virtual asset routes (Filament, Nova, Pulse, Telescope, etc.).
Verify the Fix
Run these after reloading Nginx and clearing/rebuilding Laravel caches:
php artisan route:list --path=flux
php artisan route:list --path=livewire
curl -I https://your-domain.com/flux/flux.min.js
curl -I https://your-domain.com/livewire/livewire.min.js
Expected result:
- Both Flux and Livewire routes appear in
route:list. curl -IreturnsHTTP/1.1 200 OK(withContent-Type: text/javascriptorapplication/javascript) for each asset URL.
If you're on a recent Livewire 3 build, also confirm the hashed prefix variant by checking the actual <script src="..."> rendered in your page's HTML and curling that URL too.
Final Takeaway
This is one of those issues where local and production behavior differ: php artisan serve always routes through Laravel, while production Nginx may short-circuit .js requests as static files.
If you use Nginx (including CloudPanel-generated vhosts), make sure missing static-looking URLs can still fall through to your Laravel backend. The try_files $uri @backend; pattern reliably fixes both the Flux 404 and the Livewire 404, and it keeps the door open for any future package that ships JavaScript via a Laravel route.