public/ is the document root + Blade layout

Laravel Favicon Generator

Laravel has the shortest path of any framework here: public/ is literally the web server document root, so a file dropped there is live immediately. The work is all in the Blade layout and the asset helper.

Drop an image
Drop a logo or image
PNG, JPG, WebP · square 512×512+ works best · non-square images are padded, never cropped · processed in your browser

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 fileGoes toNotes
favicon.icopublic/favicon.icoOverwrite it — Laravel ships a zero-byte placeholder at this exact path.
favicon-16x16.pngpublic/favicon-16x16.pngReferenced with the asset() helper.
favicon-32x32.pngpublic/favicon-32x32.pngReferenced with the asset() helper.
favicon-48x48.pngpublic/favicon-48x48.pngOptional — already inside the .ico.
apple-touch-icon.pngpublic/apple-touch-icon.pngiOS home screen, 180×180.
android-chrome-192x192.pngpublic/android-chrome-192x192.pngManifest icon.
android-chrome-512x512.pngpublic/android-chrome-512x512.pngManifest 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.

How it works

  1. Generate the set: Drop a square 512px logo into the tool above and download the complete set.
  2. Overwrite public/favicon.ico: Laravel ships a zero-byte file at that exact path. Check the byte count first — replacing it is often the whole fix.
  3. Find every Blade layout with a head: App and guest layouts are usually separate. Add the tags to each, or extract a partial and include it.
  4. Use asset() and check APP_URL: The helper follows ASSET_URL to a CDN and APP_URL for the scheme. An http APP_URL behind an HTTPS proxy blocks the icons as mixed content.

Frequently asked questions

Where does the favicon go in a Laravel app?
public/, which is already the web server document root. A file placed there is served at the matching URL immediately with no build step, no config and no PHP involved. public/favicon.ico is reachable at /favicon.ico the moment you save it.
Why is my Laravel favicon not showing at all?
Check the byte count of public/favicon.ico. Laravel ships a zero-byte placeholder at that path so the inevitable browser request does not 404, and a zero-byte file is not a valid image — browsers render the grey globe exactly as if it were missing. Overwriting it is usually the whole fix.
Which Blade file do I edit?
Whichever layout your views render through. Traditional apps use resources/views/layouts/app.blade.php; recent starter kits use resources/views/components/layouts/app.blade.php; Inertia projects use the single resources/views/app.blade.php. Grep the views directory for <head> — most apps have more than one.
Should I use asset() or a plain path?
asset(). It builds URLs from APP_URL, or ASSET_URL when set, so moving static files to a CDN later is a config change rather than a template hunt. A hardcoded path also breaks on any subdirectory deployment.
My icons load over http on an https site.
APP_URL is still http while a load balancer terminates TLS, so every asset() URL comes out insecure and the browser blocks it as mixed content. Fix APP_URL and configure Laravel to trust the proxy headers. secure_asset() forces https per call but only masks the cause.
Can I put favicons in resources/ and let Vite handle them?
No. Vite hashes output filenames, and a hashed name cannot answer the browser request for /favicon.ico that arrives regardless of any tag. Keep them in public/ where the filenames are fixed and the web server serves them without touching PHP.
Do the icons still work in maintenance mode?
Yes. Files in public/ are served directly by nginx or Apache without the application booting, so the tab icon keeps working even while php artisan down is in effect. That is a side effect of the document root arrangement, not something you configure.

All Image Tools

Solutions by use case