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

Facehash Laravel Package

saade/facehash

Generate deterministic SVG avatar faces from any string in Laravel. Pure PHP output, no GD, Imagick, or external services needed. Consistent, unique avatars with sizes, shapes, colors, and variants.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation

    composer require saade/facehash
    

    Requires PHP 8.2+ and Laravel 11, 12 or 13 (the v1.3.0 CI matrix covers PHP 8.2–8.5 across all three). The service provider and Facehash facade are auto-discovered.

  2. Publish the config (optional but recommended)

    php artisan vendor:publish --tag=facehash-config
    

    The package ships a config/facehash.php holding the default palette and route.middleware. You only need your own copy if you want to change either.

  3. First use case Render a deterministic avatar for a user — the same name always produces the same face, colours and tilt:

    use Saade\Facehash\Facades\Facehash;
    
    $svg = Facehash::for($user->name)->format();
    

    Drop it straight into Blade:

    {!! Facehash::for($user->name)->format() !!}
    

    Or skip rendering entirely and let the package serve it over HTTP (see below).

  4. Where to look first

    • The docs and playground at https://facehash.saade.dev — the playground is the fastest way to see what a name produces and to sanity-check option combinations before writing code.
    • config/facehash.php in the package source, for the palette and route settings.
    • FaceSvgData — the value object returned by the builder; get() exposes the parsed paths (now including an eyes key), which is the hook for custom rendering.
    • The Pest suite under tests/ for edge cases around palettes, blink markup and non-ASCII input.

Implementation Patterns

Core Workflows

  1. Inline SVG in Blade (no HTTP round-trip)

    <span class="avatar">
        {!! Facehash::for($user->name)->format() !!}
    </span>
    

    Best when avatars appear in lists you're already rendering server-side; there's no extra request and no cache-invalidation problem.

  2. The built-in HTTP route The package registers a route that returns the SVG with a one-year, immutable, publicly cacheable response — ideal for <img> tags and CDN offload:

    <img src="{{ route('facehash', ['name' => $user->name]) }}" alt="{{ $user->name }}">
    

    Confirm the exact route name and parameters with:

    php artisan route:list --name=facehash
    

    Because the response is immutable, treat the URL as a content address: if the rendered face for a name changes, the URL must change too (see Gotchas).

  3. Custom palettes Set a house palette once in config/facehash.php, then override per call where you need a variant:

    $svg = Facehash::for($user->name)
        ->colors(['#0f172a', '#38bdf8', '#f472b6'])
        ->format();
    

    colors([]) (or a palette that resolves to nothing) no longer throws — it falls back to the configured palette, and to the shipped one if the config is empty too. That makes it safe to pass a user- or tenant-supplied palette straight through.

  4. Animated / expressive avatars

    $svg = Facehash::for($user->name)->blink()->format();
    

    Blink is opt-in and only changes markup when enabled — if you never call blink(), the output is byte-for-byte what it was before v1.3.0. Note the v1.3.0 fix: both halves of an eye now blink in step.

  5. Custom rendering from the SVG data

    $data = Facehash::for($user->name)->data();   // FaceSvgData
    $paths = $data->get();
    
    foreach ($paths['eyes'] as $path) {
        // $path knows which eye it belongs to — useful for per-eye styling
    }
    

    The new eyes key on FaceSvgData::get() lets you target left/right eye paths individually for CSS animation or theming instead of regex-ing the SVG string.

  6. Bulk generation Since output is fully derived from the name plus options, don't backfill a database column — store the name you hash and render on demand. If you do need to precompute (e.g. writing SVGs to S3), the operation is pure and safe to fan out across queue workers:

    User::query()->chunkById(500, function ($users) {
        foreach ($users as $user) {
            Storage::disk('avatars')->put(
                "u/{$user->id}.svg",
                Facehash::for($user->name)->format()
            );
        }
    });
    
  7. Caching strategy

    • HTTP route: already cached for a year — don't wrap it in another layer.
    • Inline rendering: cache the SVG string yourself, keyed on everything that affects output:
      $svg = Cache::remember(
          'facehash:'.md5($user->name.'|'.implode(',', $palette).'|'.(int) $blink),
          now()->addDay(),
          fn () => Facehash::for($user->name)->colors($palette)->blink()->format()
      );
      

Gotchas and Tips

Upgrade issues from v1.2.0

  1. Non-ASCII names now hash differently. The hash reads UTF-16 code units like the upstream JavaScript library, so "Zoë", "José" and "日本語" now resolve to the same face, colour and tilt as the JS reference. ASCII names are unaffected. Two consequences:

    • If you previously worked around mismatched avatars by transliterating names, you can drop that shim.
    • Every user whose name contains non-ASCII characters gets a new avatar. If you serve the HTTP route with the default one-year immutable cache, browsers and CDNs will keep showing the old image until it expires. Bust it by adding a version segment to the URL (?v=2, or a /v2/avatar/... path) and letting the old URL age out.
  2. Published config still has 'middleware' => ['web']. route.middleware now defaults to [], but a config file you published earlier keeps the old value. With web attached, session and XSRF cookies get set on a publicly cacheable response — which is both a cache-poisoning smell and a correctness problem behind a shared CDN. Change it to [] unless you genuinely depend on session state in that route:

    'route' => [
        'middleware' => [], // was ['web']
    ],
    
  3. Blink markup changed. Markup without blink() is byte-for-byte unchanged, but with blink() the eye classes on some faces are different. If you have CSS or JS targeting the old classes, diff the output before shipping.

Pitfalls

  1. Empty palettes are safe, null may not be. colors([]) falls back gracefully as of v1.3.0; that guarantees an array is accepted. Coerce nullable inputs rather than passing them blindly:

    ->colors($tenant->palette ?? [])
    
  2. Immutable caching means you can't edit a face in place. Any change to input normalisation, palette defaults, or the hashing itself (as in v1.3.0) is invisible to anyone who already cached the URL. Version your avatar URLs from day one.

  3. Determinism is exact-match. Input that differs by whitespace or case will produce a different avatar — the package doesn't normalise for you. If you want "John" and "john" to share a face, normalise at the call site:

    Facehash::for(Str::lower(trim($user->name)))->format();
    
  4. Don't use the output as an identifier. It's an avatar, not a stable user key — collisions are possible and the algorithm can change between releases.

Debugging

  • Compare against the playground. https://facehash.saade.dev is the reference for what a given name should look like; if production disagrees, you're on an older version or a stale cached URL.
  • Inspect the served route's headers to confirm the cache contract:
    curl -sI https://yourapp.test/avatar/Zo%C3%AB | grep -i -E 'cache-control|set-cookie'
    
    You want long-lived Cache-Control and no Set-Cookie. A cookie here almost always means a published config still carrying 'middleware' => ['web'].
  • Check the route registration when <img> tags 404: php artisan route:list --name=facehash.
  • Run the package's own suite if you're patching locally — it's Pest-based and runs on PHP 8.2–8.5 against Laravel 11–13.

Extension Points

  1. Palette config — config/facehash.php is the cheapest customisation; per-call colors() overrides it when you need per-tenant or per-context theming.
  2. FaceSvgData::get() — the supported seam for custom rendering. The eyes key lets you build your own Blade partial or <svg> with your own classes rather than post-processing a string.
  3. Route override — since route.middleware is configurable, you can attach your own middleware (throttling, tenant resolution) or bypass the package route entirely and serve from a controller that calls the facade, giving you full control over cache headers.
  4. Version pinning — the 1.2 → 1.3 non-ASCII hash change is a reminder that the rendered output is not guaranteed stable across minor releases. Pin the constraint and re-check the changelog for hash-affecting changes before upgrading.
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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style