Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Favicon Fetcher Laravel Package

ashallendesign/favicon-fetcher

A Laravel package for fetching website favicons. Pass it a URL and it locates and retrieves the site's icon, making it easy to add favicons to bookmarks, link previews, dashboards, and directory listings with a simple API and Laravel integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its config:

composer require ashallendesign/favicon-fetcher
php artisan vendor:publish --tag="favicon-fetcher-config"

The service provider is auto-discovered. The single entry point you'll use 90% of the time is the Favicon facade:

use AshAllenDesign\FaviconFetcher\Facades\Favicon;

$favicon = Favicon::fetch('https://ashallendesign.co.uk');

$favicon->getFaviconUrl();    // https://.../favicon.png
$favicon->getIconSize();      // "32x32" (driver-dependent, may be null)
$favicon->getIconType();      // "image/png"
$favicon->getIconExtension(); // "png"

Look first at config/favicon-fetcher.php. It holds the default driver, a block per driver (favicon-kit, google, duckduckgo, unavatar), cache settings, and the fallback list. Set the default from .env rather than editing the file:

FAVICON_FETCHER_DRIVER=google

The canonical first use case is "show the site icon next to a URL the user gave us" — bookmarks, link shorteners, RSS readers, admin CRUD lists, newsletter link cards. Minimal working flow:

  1. User submits https://example.com/some/deep/page.
  2. Favicon::fetch($url) (drivers normalise to the host for you).
  3. Persist getFaviconUrl() on your model. Never call fetch() from a Blade template.

If you want to inspect what a driver actually returns before committing to a design, run Favicon::fetch('https://github.com') in php artisan tinker and dd() the DTO — the getters are thin, and API-based drivers often leave getIconSize()/getIconType() null.

Implementation Patterns

Store the result, never the call. Add a column and fill it once:

Schema::table('bookmarks', function (Blueprint $table) {
    $table->string('favicon_url')->nullable()->after('url');
    $table->timestamp('favicon_fetched_at')->nullable();
});

Your index page then just renders <img src="{{ $bookmark->favicon_url }}">, which stays fast and works when the upstream provider is down.

Do the fetch in a queued job. Favicon lookups are third-party network calls; they don't belong in a request cycle that a user is waiting on, and bulk imports will blow past timeouts:

final class FetchBookmarkFavicon implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public array $backoff = [10, 60, 300];

    public function __construct(public Bookmark $bookmark) {}

    public function handle(): void
    {
        $favicon = Favicon::fetch($this->bookmark->url);

        $this->bookmark->update([
            'favicon_url'        => $favicon?->getFaviconUrl(),
            'favicon_fetched_at' => now(),
        ]);
    }
}

Dispatch from model events (created, and on url change) and add a nightly command that re-fetches rows older than ~30 days.

Wrap driver selection in a resolver. Providers are flaky and inconsistent; a small chain keeps one outage from blanking every icon:

class FaviconResolver
{
    public function resolve(string $url): ?string
    {
        foreach (['google', 'duckduckgo', 'unavatar'] as $driver) {
            try {
                $favicon = Favicon::driver($driver)->fetch($url);
            } catch (Throwable) {
                continue;
            }

            if ($favicon && $this->looksValid($favicon->getFaviconUrl())) {
                return $favicon->getFaviconUrl();
            }
        }

        return null;
    }
}

Bind this in the container and inject it into the job so tests can swap one class instead of mocking the facade.

Use fetchAll() when the choice matters. For a "pick your icon" UI, or to prefer the largest/highest-quality asset:

$best = Favicon::fetchAll($url)
    ->sortByDesc(fn ($f) => $this->area($f->getIconSize())) // parse "180x180"
    ->first();

Fall back to sort order svg → png → ico when sizes are missing.

Blade with an initials fallback. Don't ship broken images:

@if ($bookmark->favicon_url)
    <img src="{{ $bookmark->favicon_url }}" alt=""
         class="size-8 rounded"
         onerror="this.replaceWith(Object.assign(document.createElement('span'),{textContent:'{{ Str::substr($bookmark->title, 0, 1) }}'}))">
@else
    <span class="size-8 grid place-items-center rounded bg-gray-200">
        {{ Str::upper(Str::substr($bookmark->title, 0, 1)) }}
    </span>
@endif

Re-host the icon if you care about privacy or availability. Provider URLs (google.com/s2/favicons, icons.duckduckgo.com, unavatar.io) leak every visitor's IP and referrer to a third party, and can 404 later. Download once in the job and put it on your own disk:

$response = Http::timeout(5)->get($favicon->getFaviconUrl());

if ($response->successful() && $response->body() !== '') {
    Storage::disk('public')->put("favicons/{$bookmark->id}.png", $response->body());
}

Custom drivers are the real extension point. If you run an internal icon service or want to scrape <link rel="icon"> yourself, implement the package's fetcher contract (fetch() / fetchAll() returning the Favicon DTO or a collection) and register it under drivers in the config, then select it with Favicon::driver('internal').

Testing. Mock the facade so no test touches the network:

Favicon::shouldReceive('fetch')
    ->once()
    ->with('https://example.com')
    ->andReturn($this->fakeFavicon('https://cdn.test/favicon.png'));

Construct the DTO directly for the fake, or bind a stub implementation of the contract in the container. Assert the job writes the column and that the view renders the fallback on null.

Gotchas and Tips

  • The returned URL is not proof the icon exists. Several drivers build a URL from the domain without checking the response. If you persist it, validate once with a HEAD/GET (or accept occasional broken icons) — otherwise you'll render 404s indefinitely.
  • null vs exception. Failing drivers may return null, throw, or return a URL to a generic globe icon. Wrap the call in try/catch (Throwable) and null-check; don't rely on one style.
  • Normalise the input first. Pass scheme + host (or at least parse with parse_url()) rather than a full deep link. Reject non-http(s) schemes and private/loopback hosts — you're passing a user-controlled string to an external service, and that's an SSRF surface if you ever swap in a self-hosted driver or download the icon yourself.
  • config:cache in production. Driver and cache settings come from the config file; changing FAVICON_FETCHER_DRIVER in .env on a cached deployment does nothing until you re-cache. Put the env var in your deploy checklist.
  • Two layers of cache. The package caches by URL + driver, but you should also cache your own negative result (a null sentinel with favicon_fetched_at). Otherwise every page load or every re-run of the job re-hits a provider that has already told you "no".
  • Switching drivers doesn't invalidate your stored column. The package cache key includes the driver; your database column does not. If you change the default driver, run a backfill — the old provider's URLs will keep working or silently rot.
  • Size and type getters are best-effort. getIconSize() frequently returns null or a non-WxH string; don't call explode('x', ...) without a guard. When you save a downloaded file, whitelist the extension (png, ico, svg, jpg, webp) instead of trusting getIconExtension() — it's a string from a third party, not a validated MIME type.
  • SVG icons and <img>. A small number of sites serve SVG favicons that render at unexpected intrinsic sizes. Set explicit width/height on the <img> and don't rely on the icon's own dimensions.
  • Free providers rate-limit and disappear. favicon-kit has a history of outages and Google's endpoint is undocumented and subject to change. Configure a fallback list, cap concurrency on the queue (a RateLimited job middleware of ~5/s is plenty), and set a short HTTP timeout in the config so a hung provider doesn't stall workers.
  • Don't fetch on save synchronously in a saving observer — inside a DB transaction, a slow provider holds the transaction open. Use created/updated with dispatch() (after commit) or DB::afterCommit().
  • Version pinning. This is a thin wrapper over third-party endpoints, so provider behaviour changes between releases more often than the API does. Pin the major version in composer.json and skim the changelog for driver renames before bumping.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo