Favicon Setup for Laravel — Free, Fast & Private
Do not put these in resources/. That directory is Vite input: files there are bundled, fingerprinted and renamed, and reaching them requires the @vite directive and an import from your JavaScript entry point. Favicons need fixed, predictable filenames at the URL root, which is precisely what public/ already provides.
Where each generated file goes in Laravel
The generator above emits seven files with fixed names. Static assets in Laravel live in public/, and the markup is written in resources/views/layouts/app.blade.php. This is the mapping between the two.
| Generated file | Goes to | Notes |
|---|---|---|
| favicon.ico | public/favicon.ico | Overwrite it — Laravel ships a zero-byte placeholder at this exact path. |
| favicon-16x16.png | public/favicon-16x16.png | Referenced with the asset() helper. |
| favicon-32x32.png | public/favicon-32x32.png | Referenced with the asset() helper. |
| favicon-48x48.png | public/favicon-48x48.png | Optional — already inside the .ico. |
| apple-touch-icon.png | public/apple-touch-icon.png | iOS home screen, 180×180. |
| android-chrome-192x192.png | public/android-chrome-192x192.png | Manifest icon. |
| android-chrome-512x512.png | public/android-chrome-512x512.png | Manifest icon and splash. |
The zero-byte favicon.ico nobody notices
Every fresh Laravel installation includes public/favicon.ico, and it is an empty file — zero bytes. It exists so that the browser request that always happens gets a 200 rather than filling your log with 404s. Because it is present and the correct size on disk to look plausible in a directory listing, people assume it is a real placeholder icon and go looking elsewhere when nothing shows up in the tab.
A zero-byte .ico is not a valid image, so browsers fall back to the grey globe exactly as if the file were missing. Overwriting it with the generated favicon.ico is often the entire fix, and it is worth checking first before touching any Blade template. Confirm it with a byte count rather than by eye.
Check before you debug anything else
$ ls -l public/favicon.ico -rw-r--r-- 1 you staff 0 Jan 1 00:00 public/favicon.ico # ^ zero bytes — that is the bug
Blade layouts, and where yours actually is
Laravel does not prescribe one layout path, and the answer differs by how the project was started. A traditional application keeps it at resources/views/layouts/app.blade.php and pages extend it. Recent starter kits use an anonymous Blade component at resources/views/components/layouts/app.blade.php, invoked as <x-layouts.app>. A brand-new project with no scaffolding may have nothing but resources/views/welcome.blade.php, which contains its own complete head.
Whichever it is, the head goes there once and covers every view that renders through it. The failure mode specific to Laravel is having more than one: an app layout for authenticated pages and a guest layout for login and registration is a very common split, and it is easy to add icons to one and forget the other. Grep the views directory for <head> and fix every hit, or extract a partial and include it from each.
If you use Inertia, resources/views/app.blade.php is the single root document for the entire SPA and is the only place that needs the tags. Livewire pages render through whatever layout they declare, so the same multiple-layouts warning applies.
resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="{{ asset('favicon.ico') }}" sizes="16x16 32x32 48x48">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('favicon-16x16.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('apple-touch-icon.png') }}">
<link rel="manifest" href="{{ asset('site.webmanifest') }}">
<title>{{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>{{ $slot ?? '' }}@yield('content')</body>
</html>asset(), APP_URL, and mixed content
The asset helper builds a URL from APP_URL, or from ASSET_URL when that is set — which is how you move static files onto a CDN without editing a single template. It is worth using even for a favicon whose path you could type from memory, because the day the CDN arrives, every helper call follows and every hardcoded path does not.
The classic production bug is an APP_URL still set to http:// while the site is served over HTTPS behind a load balancer. Every asset URL then comes out as http, the browser blocks them as mixed content, and the favicon is among the casualties. Fix APP_URL, and configure Laravel to trust the proxy headers so the framework knows the original request was secure. secure_asset forces https on a single call, but it treats a symptom rather than the cause.
On a subdirectory deployment, APP_URL should include the path and the helper handles the rest. This is the one case where hardcoding a leading slash reliably breaks.
Why Vite must not touch these files
Laravel builds front-end assets with Vite, and the @vite directive resolves the hashed output filenames through public/build/manifest.json. That pipeline is right for CSS and JavaScript, where content hashing enables immutable caching, and wrong for favicons, where the filename is part of the contract. A hashed favicon-a8f3c2.ico cannot answer the browser request for /favicon.ico, which arrives whether or not any tag mentions it.
So keep the seven files in public/ and out of resources/. They are not imported by anything, they never appear in the Vite manifest, and they are served directly by the web server without PHP even starting. That is also why they are the fastest thing your site serves and why they keep working when the application itself is in maintenance mode.
What belongs where
resources/ css/app.css → Vite input, hashed on build js/app.js → Vite input, hashed on build public/ build/ ← Vite output, hashed filenames, do not edit favicon.ico ← fixed name, served directly by the web server favicon-32x32.png apple-touch-icon.png android-chrome-192x192.png android-chrome-512x512.png site.webmanifest
Whatever you paste, the artwork itself never leaves your machine. The set above is rendered with the Canvas API and the .ico container is assembled in JavaScript on your device, so an unreleased logo stays unreleased. If the 16px preview looks like mush, that is the honest signal to simplify the mark before it ships to a Laravel tab near you.
