Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Simomir's avatar

Laravel 10 - asset returning wrong url

Hello everyone. Newbie guy here with probably stupid question. So I've setup fresh Laravel 10 project with Livewire, TailwindCss and so on. I wrote down few routes some of which are in a group with a prefix. I've put some css and js files inside the public folder and wanted to reference them in the base Blade layout file which roughly looks like this:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <link rel="apple-touch-icon" sizes="76x76" href="{{ asset('assets/images/apple-icon.png') }}">
        <link rel="icon" type="image/png" href="{{ asset('favicon.ico') }}">
        <title>Document</title>
        <!-- Fonts and Icons -->
        <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet" />
        <!-- Nucleo Icons -->
        <link href="{{ asset('assets/css/nucleo-icons.css') }}" rel="stylesheet" />
        <link href="{{ asset('assets/css/nucleo-svg.css') }}" rel="stylesheet" />
        <!-- Popper -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.5/umd/popper.min.js"></script>
        <link rel="stylesheet" href="{{ asset('assets/css/perfect-scrollbar.css') }}">
        <!-- Main Styling -->
        <link href="{{ asset('assets/css/styles.css') }}?v=1.0.3" rel="stylesheet" />
        @vite(['resources/css/app.css', 'resources/js/app.js'])
        @livewireStyles
    </head>
    <body class="m-0 font-sans antialiased font-normal text-size-base leading-default bg-gray-50 text-slate-500">
        {{ $slot }}


        <!-- plugin for charts  -->
        <script src="{{ asset('assets/js/plugins/chartjs.min.js') }}" async></script>
        <!-- plugin for scrollbar  -->
        <script src="{{ asset('assets/js/plugins/perfect-scrollbar.min.js') }}" async></script>
        <!-- GitHub buttons -->
        <script async defer src="https://buttons.github.io/buttons.js"></script>
        <!-- main script file  -->
        <script src="{{ asset('assets/js/soft-ui-dashboard-tailwind.js') }}?v=1.0.3" async></script>
        @livewireScripts
    </body>
</html>

When I start the server via php artisan serve and also npm run dev the page is loading but i noticed some things are missing and then saw in the dev tools window bunch of 404 errors regarding the assets (only the ones which are not cdns). What i noticed is asset() instead of returning "public/assets/......." it returns "prefix/assets/...." where prefix is the same word as in the route group prefix ive setup.

I would be very grateful for any advice on the matter. Thank you in advance.

0 likes
31 replies
Simomir's avatar

Tried the url function and still getting those 404 errors in the console.

Simomir's avatar

I guess no one has an advice on the matter.

kokoshneta's avatar

Can you show your route definition for this group?

Simomir's avatar

@kokoshneta Sure thing.

Route::get('/', function () {
    return view('welcome');
});

Route::prefix('humge')->group(function () {
    Route::get('/login', Login::class)->name('login');
    Route::get('/logout')->name('logout');
    Route::get('/dashboard', Dashboard::class)->name('dashboard')->middleware('auth');
});

Thats all I got in web.php and have views only for the login and dashboard and then started banging my head why all the assets throw 404 in the console.

Hope that helps and thank you for responding.

kokoshneta's avatar

@Simomir Are your login and dashboard classes single-action controllers with an __invoke() method? And what exactly is supposed to happen when you hit the logout route? There’s no action defined!

But apart from that, I don’t see anything there that should be able to affect the return value of asset().

Simomir's avatar

@kokoshneta Those are Livewire component classes and logout is just a placeholder route. It does nothing for now till I figure out the asset issue. I tried to put ASSET_URL in the .env file, tried secure_asset function, tried the url function but it the end instead of 'host/assets/css/xxxx' I see 'host/humge/assets/css/xxxx' returned from the asset function in the console. All css, js files are inside public/assets folder respectively in the css / js sub folders.

Snapey's avatar

What is the URL for a page? Trying to work out why you think that public should be part of the URL ?

Have you messed with index.php and server.php in order to make it work on a shared server?

Did you copy the site from a production server?

Simomir's avatar

@Snapey No messing with index.php or server.php. Just created a blank Laravel 10 project, installed Livewire, Alpine JS, followed the instructions to config them, downloaded a free html/css/js template that is using tailwind css, configured that one too in the app.js and colors and stuff in the tailwind conf file. Then created the base.blade.php component file to hold everything css, js related. Then as per Livewire instructions created app.blade.php file in layouts folder. That app.blade.php is using the above mentioned and as shown in the initial post base.blade.php file. The app.blade.php contains if statements to determine what navbar /sidebar/ footer to show depending if the user is logged in or not.

Haven't touched shared or production or whatever servers. Everything is on my laptop.

kahilu's avatar

Does this happen when you when you start the server?

Simomir's avatar

@kahilu Yes. On initial load in the console is as follows:

[vite] connecting.......

127.0.0.1:8000/humge/assets/css/xxxxxx <404>

127.0.0.1:8000/humge/assets/js/xxxxxx <404>

[vite] connected.

couple of warning for failed loading of an asset

kokoshneta's avatar

This appears to be an old issue going back several Laravel versions.

The asset() helper gets the URL from the containerised app instance, which seemingly, in some circumstances, includes the route group prefix in its base path (or whichever property is actually relevant for asset URLs).

Oddly, I can’t replicate it directly in Laravel itself. My asset() function returns the correct path even inside prefixed groups. Try adding this to your route file and tell us what the output is when you visit the page:

Route::prefix('humge')->group(function () {
    Route::get('/login', Login::class)->name('login');
    Route::get('/logout')->name('logout');
    Route::get('/dashboard', Dashboard::class)->name('dashboard')->middleware('auth');
	Route::get('tst', function () {
		dump(asset('assets/css'));
	});
});

On my local machine, that returns https://example.test/assets/css, even inside the group.

kokoshneta's avatar

@Simomir So that does give the correct result. Very odd.

I can only assume it must be related to Livewire, then, since you say your classes are Livewire component classes. Can you show one of your Livewire classes? And preferably also the view associated with it.

Simomir's avatar

@kokoshneta Sure. Here is the Login class:

<?php

namespace App\Http\Livewire;

use App\Models\User;
use Carbon\Carbon;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\MessageBag;
use Livewire\Component;

class Login extends Component
{
    public string $email;
    public string $password;
    public bool $remember_me = false;

    protected $rules = [
        'email' => ['required', 'email', 'exists:users,email'],
        'password' => ['required']
    ];

    final public function mount(): void
    {
        if (Auth::check()) {
            $this->redirect(route('dashboard'));
        }
    }

    final public function login(): Application|\Illuminate\Foundation\Application|RedirectResponse|Redirector|MessageBag
    {
        $this->validate();

        $user = User::where(['email' => $this->email])->first();

        if(Hash::check($this->password, $user->password)) {
            Auth::login($user, $this->remember_me);
            Auth::user()->last_logged_in_at = Carbon::now()->toDateTimeString();
            Auth::user()->save();
            return redirect(route('dashboard'));
        } else {
            return $this->addError('password', 'Password is incorrect');
        }
    }

     final public function render(): View
    {
        return view('livewire.login');
    }
}

and here is the view to it :

<div>
    <main class="mt-0 transition-all duration-200 ease-soft-in-out">
        <section>
            <div class="relative flex items-center p-0 overflow-hidden bg-center bg-cover min-h-75-screen">
                <div class="container z-10">
                    <div class="flex flex-wrap mt-0 -mx-3">
                        <div class="flex flex-col w-full max-w-full px-3 mx-auto md:flex-0 shrink-0 md:w-6/12 lg:w-5/12 xl:w-4/12">
                            <div class="relative flex flex-col min-w-0 mt-32 break-words bg-transparent border-0 shadow-none rounded-2xl bg-clip-border">
                                <div class="p-6 pb-0 mb-0 bg-transparent border-b-0 rounded-t-2xl">
                                    <h3 class="relative z-10 font-bold text-transparent bg-gradient-cyan bg-clip-text">
                                        Welcome Back!
                                    </h3>
                                </div>
                                <div class="flex-auto p-6">
                                    @if(Session::has('status'))
                                        <div id="alert" class="relative p-4 pr-12 mb-4 text-white border border-solid rounded-lg bg-gradient-dark-gray border-slate-100">
                                            {{ Session::get('status') }}
                                            <button type="button" onclick="alertClose()"
                                                class="box-content absolute top-0 right-0 p-2 text-white bg-transparent border-0 rounded w-4-em h-4-em text-size-sm z-2">
                                                <span aria-hidden="true" class="text-center cursor-pointer">&#10005;</span>
                                            </button>
                                        </div>
                                    @endif

                                    <form wire:submit.prevent="login">
                                        <!--Email Field-->
                                        <label class="mb-2 ml-1 font-bold text-size-xs text-slate-700">Email</label>
                                        <div class="mb-4">
                                            <input wire:model.lazy="email" type="email"
                                                class="focus:shadow-soft-primary-outline text-size-sm leading-5.6 ease-soft block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-3 py-2 font-normal text-gray-700 transition-all focus:border-fuchsia-300 focus:outline-none focus:transition-shadow"
                                                name="email" placeholder="Email" aria-label="Email"
                                                aria-describedby="email-addon" required autofocus />
                                            @error('email')
                                                <x-alert>{{ $message }}</x-alert>
                                            @enderror
                                        </div>
                                        <!--Password Field-->
                                        <label class="mb-2 ml-1 font-bold text-size-xs text-slate-700">Password</label>
                                        <div class="mb-4">
                                            <input wire:model.lazy="password" type="password"
                                                class="focus:shadow-soft-primary-outline text-size-sm leading-5.6 ease-soft block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-3 py-2 font-normal text-gray-700 transition-all focus:border-fuchsia-300 focus:outline-none focus:transition-shadow"
                                                placeholder="Password" name="password" aria-label="Password"
                                                aria-describedby="password-addon" required />
                                            @error('password')
                                                <p class="text-size-sm text-red-500">{{ $message }}</p>
                                            @enderror
                                        </div>
                                        <!--Remember Me -->
                                        <div class="min-h-6 mb-0.5 block pl-12">
                                            <input wire:model="remember_me"
                                                class="mt-0.54 rounded-10 duration-250 ease-soft-in-out after:rounded-circle after:shadow-soft-2xl after:duration-250 checked:after:translate-x-5.25 h-5-em relative float-left -ml-12 w-10 cursor-pointer appearance-none border border-solid border-gray-200 bg-slate-800/10 bg-none bg-contain bg-left bg-no-repeat align-top transition-all after:absolute after:top-px after:h-4 after:w-4 after:translate-x-px after:bg-white after:content-[''] checked:border-slate-800/95 checked:bg-slate-800/95 checked:bg-none checked:bg-right"
                                                type="checkbox" id="rememberMe">
                                            <label class="mb-2 ml-1 font-normal cursor-pointer select-none text-size-sm text-slate-700" for="rememberMe">
                                                Remember me
                                            </label>
                                        </div>
                                        <!--Submit Button-->
                                        <div class="text-center">
                                            <button type="submit"
                                                class="inline-block w-full px-6 py-3 mt-6 mb-0 font-bold text-center text-white uppercase align-middle transition-all bg-gradient-cyan border-0 rounded-lg cursor-pointer shadow-soft-md bg-x-25 bg-150 leading-pro text-size-xs ease-soft-in tracking-tight-soft bg-cyan-400 hover:scale-102 hover:shadow-soft-xs active:opacity-85">
                                                Sign in
                                            </button>
                                        </div>
                                    </form>
                                </div>
                            </div>
                        </div>
                        <div class="w-full max-w-full px-3 lg:flex-0 shrink-0 md:w-6/12">
                            <div class="absolute top-0 hidden w-3/5 h-full -mr-32 overflow-hidden -skew-x-10 -right-40 rounded-bl-xl md:block">
                                <div class="absolute inset-x-0 top-0 z-0 h-full -ml-16 bg-cover skew-x-10" style="background-image:url('{{ asset('assets/images/curved-images/curved6.jpg') }}')"></div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </section>
    </main>
    <script>
        function alertClose() {
            document.getElementById("alert").style.display = "none";
        }
    </script>
</div>
Simomir's avatar

@kokoshneta by default Livewire uses app.blade.php as layout for the other components (if I am not mistaken). And app.blade.php uses base.blade.php because its injected in the $slot position inside base.blade.php (again if I am not mistaken). Of cource I can be more explicit by typing "->layout(app.blade.php)" in the login view, but from what I've red its more often used when you want layout other than the default one.

Simomir's avatar

@kokoshneta I will try it since you pointed it out but havent considered it since all the content is there. Its just some of the styling is missing - for example "Welcome back" should be a little bit bigger and that sort ot things. Its more like TailwindCSS is working fine, but the custom css, js files in the public directory arent.

kokoshneta's avatar

@Simomir Ah, yes, you’re right. I’ve only used full-page Livewire components once or twice, and I’d forgotten that the view() function there works a little differently from Laravel’s own.

All right, so we’ve established that asset() works correctly in the route file. Let’s see if it works in the component itself. What does this do?

// Component
class Login extends Component {
     final public function render(): View {
		dump(asset('assets/css');
        return view('livewire.login');
    }
}
// Login view
<div>
	<header>
		{{ dump(asset('assets/css')) }}
	</header>

	<main…>
		[rest of content]
	</main>
</div>

Ideally, that should output http://127.0.0.1:8000/assets/css twice at the top of the layout, but I’m guessing it probably won’t. What does it output – one correct, one incorrect? Or both incorrect?

1 like
Simomir's avatar

@kokoshneta Inside the view:

"http://127.0.0.1:8000/assets/css" // app/Http/Livewire/Login.php:52
http://127.0.0.1:8000/assets/css

Inside the console:

GET http://127.0.0.1:8000/humge/assets/css/perfect-scrollbar.css net::ERR_ABORTED 404 (Not Found)

GET http://127.0.0.1:8000/humge/assets/js/perfect-scrollbar.js net::ERR_ABORTED 404 (Not Found)
kokoshneta's avatar

@Simomir Wow. That is truly bizarre. The last one of those is inside the same Blade component as the ones that don’t work!

What happens if you also add {{ dump(asset('assets/css')) }} right before {{ $slot }} in your base layout? Do you then get three identical ones, or is it two correct and one wrong?

Simomir's avatar

@kokoshneta 3 identical ones. By all means i should not get any errors in the console and everything should run smooth as butter but....... I'm starting to think it has something to do with Livewire itself or Vite or something. Just a wild guess but so far is all I got.

kokoshneta's avatar

@Simomir Curiouser and curiouser, said Alice!

If you open the source view once the page has loaded, what are the actual paths shown in the <link href="XXX"> tags in the page source? Are they absolute paths or relative ones?

Simomir's avatar

@kokoshneta


        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <link rel="apple-touch-icon" sizes="76x76" href="http://127.0.0.1:8000/assets/images/apple-icon.png">
        <link rel="icon" type="image/png" href="http://127.0.0.1:8000/assets/images/favicon.png">
        <title>Document</title>
        <!-- Fonts and Icons -->
        <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet">
        <!-- Nucleo Icons -->
        <link href="http://127.0.0.1:8000/assets/css/nucleo-icons.css" rel="stylesheet">
        <link href="http://127.0.0.1:8000/assets/css/nucleo-svg.css" rel="stylesheet">
        <!-- Popper -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.5/umd/popper.min.js"></script>
        <link rel="stylesheet" href="http://127.0.0.1:8000/assets/css/perfect-scrollbar.css">
        <!-- Main Styling -->
        <link href="http://127.0.0.1:8000/assets/css/styles.css?v=1.0.3" rel="stylesheet">
        <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>        <!-- Livewire Styles -->
<style>
    [wire\:loading], [wire\:loading\.delay], [wire\:loading\.inline-block], [wire\:loading\.inline], [wire\:loading\.block], [wire\:loading\.flex], [wire\:loading\.table], [wire\:loading\.grid], [wire\:loading\.inline-flex] {
        display: none;
    }

    [wire\:loading\.delay\.shortest], [wire\:loading\.delay\.shorter], [wire\:loading\.delay\.short], [wire\:loading\.delay\.long], [wire\:loading\.delay\.longer], [wire\:loading\.delay\.longest] {
        display:none;
    }

    [wire\:offline] {
        display: none;
    }

    [wire\:dirty]:not(textarea):not(input):not(select) {
        display: none;
    }

    input:-webkit-autofill, select:-webkit-autofill, textarea:-webkit-autofill {
        animation-duration: 50000s;
        animation-name: livewireautofill;
    }

    @keyframes livewireautofill { from {} }
</style>
    <link href="./assets/css/perfect-scrollbar.css" type="text/css" rel="stylesheet"><script src="./assets/js/perfect-scrollbar.js" type="text/javascript" async="true"></script>

You can see that the last 2 links are complete copies of 2 of the links above except the path returned from asset().

ALso thse were in the console:

TypeError: document.head is null inject.js:567:9
GEThttp://127.0.0.1:8000/humge/assets/css/perfect-scrollbar.css
[HTTP/1.1 404 Not Found 170ms]

GEThttp://127.0.0.1:8000/humge/assets/js/perfect-scrollbar.js
[HTTP/1.1 404 Not Found 391ms]

[vite] connecting... client.ts:19:8
[vite] connected. client.ts:134:14
Loading failed for the <script> with source “http://127.0.0.1:8000/humge/assets/js/perfect-scrollbar.js”. login:1:1

The first one is new one. The thing i notice is the vite loading - it starts after the assets failed.

kokoshneta's avatar

@Simomir Okay, so that shows something else is clearly going on. In the base template, you have one link to the Perfect Scrollbar CSS file, and one to the JavaScript file – but in the rendered page source, you have two links to the CSS file: one matching the location in the template (which has the correct path and presumably loads correctly), and another that appears to have been injected right above the JS link (this is the one that fails).

That means that it’s not actually the asset() function itself that doesn’t work – it’s something else causing the latter two links to be redefined as relative links. That could be Vite-related, or perhaps Livewire-related. Where is inject.js coming from?

Simomir's avatar

@kokoshneta i think i got the root of the problem. As i said all these custom css, js files are from free html/css/js template (hate writing css) and in the dashboard-tailwind.js is the following code:

let page = window.location.pathname.split("/").pop().split(".")[0];
let aux = window.location.pathname.split("/");
let to_build = (aux.includes('pages')?'../':'./');
let root = window.location.pathname.split("/")
// if (!aux.includes("pages")) {
//   page = "dashboard";
// }

loadStylesheet(to_build + "assets/css/perfect-scrollbar.css");
loadJS(to_build + "assets/js/perfect-scrollbar.js", true);

if (document.querySelector("nav [navbar-trigger]")) {
  loadJS(to_build + "assets/js/navbar-collapse.js", true);
}

if (document.querySelector("[data-target='tooltip']")) {
  loadJS(to_build + "assets/js/tooltips.js", true);
  loadStylesheet(to_build + "assets/css/tooltips.css");
}

if (document.querySelector("[nav-pills]")) {
  loadJS(to_build + "assets/js/nav-pills.js", true);
}

if (document.querySelector("[dropdown-trigger]")) {
  loadJS(to_build + "assets/js/dropdown.js", true);

}

if (document.querySelector("[fixed-plugin]")) {
  loadJS(to_build + "assets/js/fixed-plugin.js", true);
}

if (document.querySelector("[navbar-main]")) {
  loadJS(to_build + "assets/js/sidenav-burger.js", true);
  loadJS(to_build + "assets/js/navbar-sticky.js", true);
}

if (document.querySelector("canvas")) {
  loadJS(to_build + "assets/js/chart-1.js", true);
  loadJS(to_build + "assets/js/chart-2.js", true);
}
kokoshneta's avatar
Level 27

@Simomir Aha! That would certainly do it, yes – clearly not meant to be used together with asset().

Easiest way to fix it would then be to just set the to_build variable to an empty string, since in your case you always want the assets to be root-based.

Simomir's avatar

@kokoshneta There's some progress. The css, js asset errors when away and I see some stuff that was missing is now there. BUT not quite 100% solution. Still getting these in the console:

TypeError: document.head is null inject.js:567:9
[vite] connecting... client.ts:19:8
[vite] connected. client.ts:134:14
Feature Policy: Skipping unsupported feature name “clipboard-write”. 2 web-client-content-script.js:1:128628
Feature Policy: Skipping unsupported feature name “clipboard-write”. web-client-content-script.js:1:189536
kokoshneta's avatar

@Simomir Well, I don’t know exactly what inject.js is or where it’s coming from (could be part of your package, could be something else), but web-client-content-script.js looks like it’s part of a browser extension, so probably not related to your project at all (I assume you don’t have JavaScript files in your project that are nearly 200,000 lines long).

1 like
Simomir's avatar

@kokoshneta that inject.js may be related to the package yes. About the other one ye its very possible to be an extension. I try the routes on at least 2 browsers, one of which is full of extensions. Somewhere I've red that Firefox for example throws warnings and stuff that are not present in other browsers. I will mark you answer as best because the problem I stated in the initial post is resolved. Can't express my full gratitude for you spending time and helping me out. Eternal thanks and wish you all the best and cya around in the forum.

Please or to participate in this conversation.