infernalmedia/responsive-image-craft
Responsive Image Craft generates responsive image sets, srcset attributes and picture markup for Laravel apps, with on-the-fly resizing, caching and Blade helpers so every visitor gets a sharp, correctly sized image on any device or screen density, with no manual resizing required.
This is a pre-1.0 package (0.0.x), so treat the API as unstable and pin an explicit version in composer.json rather than floating on ^0.0.
composer require infernalmedia/responsive-image-craft:0.0.12
php artisan vendor:publish --tag="responsive-image-craft-config"
Where to look first, in order:
config/responsive-image-craft.php) — this defines your breakpoint/width sets, output formats (WebP/AVIF), storage disk, and cache path. Almost every "why did it generate that file?" question is answered here.0.0.x tag).First use case — replacing a hand-written <img> in a Blade view:
{{-- before --}}
<img src="{{ $post->hero_url }}" alt="{{ $post->title }}">
{{-- after --}}
<x-responsive-image
:src="$post->hero_url"
:alt="$post->title"
sizes="(min-width: 1024px) 50vw, 100vw"
/>
The win is that you stop maintaining srcset strings by hand and stop forgetting a width. Verify the output by viewing source, not by eyeballing the rendered page — browsers silently resolve srcset, so a broken set looks fine locally and only shows up as a blurry or oversized download.
Start with one image slot (a hero or card thumbnail) on a single page, confirm the generated files exist on the disk, then roll out.
Keep sizes beside the markup, not in config. The sizes support introduced in 0.0.10 is the piece that makes srcset actually useful, but the correct value is dictated by layout, not by the image. A card in a 3-column grid and a full-bleed hero need different sizes even at identical widths. Passing it per call site keeps layout and image honest:
{{-- grid card: ~33vw on desktop, full width on mobile --}}
<x-responsive-image :src="$post->hero_url" sizes="(min-width: 768px) 33vw, 100vw" />
{{-- hero: full viewport --}}
<x-responsive-image :src="$page->banner_url" sizes="100vw" />
Centralise width sets in config, centralise sizes in components. Wrap the package's output in your own Blade component (<x-hero-image>, <x-card-image>) so sizes strings live in one place per layout role. When a designer changes the grid, you edit one component instead of grepping for 33vw.
Attach generation to model events, not controllers. If the package offers an on-demand generate/regenerate path, trigger it from an observer so images created via seeders, Nova/Filament forms, and imports all get derivatives:
protected static function booted(): void
{
static::saved(fn (Post $post) => $post->wasChanged('hero')
? GenerateResponsiveImages::dispatch($post)
: null);
}
Dispatch generation to a queue. Resizing is CPU-bound. Doing it synchronously in a saved hook will make form submissions feel broken on large uploads, and will time out on shared hosting. Ensure the derivative files are written to a shared disk (S3) so queue workers and web nodes see the same files.
Cache the derivative URLs, not the markup. If you're caching rendered fragments with @cache, an image regeneration won't invalidate the cached HTML. Cache a model accessor that returns the resolved srcset array, keyed on the model's updated_at, and render from that.
Integration tips
.gitignore and to your deploy's warmup step, otherwise the first request after each deploy pays the resize cost.<img> tag — HTML attribute order is an implementation detail that will churn.Storage::fake() in tests and point the package's disk config at it; you'll catch path/URL assumptions early.srcset entry count and URLs resolve. With undocumented releases shipping, this is your cheapest regression detector across bumps.You are on 0.0.x — read every diff, and assume the release notes won't help. The versions published since the last assessment (0.0.11 and 0.0.12) shipped with release notes consisting solely of a link to the compare/v0.0.11...v0.0.12 changelog — no summary, no listed additions, fixes, or deprecations. That means you cannot judge a bump from the release page at all. Pin exactly (0.0.12) and read compare/v0.0.10...v0.0.12 — the full range, not just the final tag — before bumping. Pre-1.0 semver makes no promises: a 0.0.13 can rename a config key or a component attribute without a major-version signal. Treat every bump as unverified until you've diffed it and smoke-tested a real image in staging.
A patch bump with silent notes is not a "safe" bump. It's tempting to assume a 0.0.x increment is cosmetic when nothing is documented. The absence of notes is not evidence of a small change — it's an absence of evidence. Re-run your golden-image test and check one generated hero (file count, formats, srcset URLs returning 200) after any upgrade.
sizes being wrong is worse than absent. Without sizes, browsers assume 100vw and over-download on small slots. With a wrong sizes (e.g. claiming 25vw for a container that's actually 50vw), the browser picks a file that's too small and you get visible softness on retina displays. sizes must describe the rendered CSS width of the image, so it has to change whenever your breakpoints do. Keep them in sync with your Tailwind/CSS grid definitions, ideally in the same file.
sizes cannot express container queries. If your layout uses container queries or a runtime-measured width, a static sizes attribute will be approximate. Mitigate by rounding up (over-declaring widths costs bytes, under-declaring costs quality) and by keeping the largest generated width generous.
Don't generate 15 widths "to be safe." Every width is a file, a storage cost, and a slower generation pass. 3–5 widths per format covers the vast majority of layouts. Audit the actual srcset output and delete unused entries from the config.
Format support is environment-dependent. WebP is near-universal, but AVIF generation depends on the GD/Imagick build compiled into your PHP image. A locally-working config can silently produce nothing in production. Verify derivative files exist after deploy with a real image, and check gd_info() / Imagick::queryFormats() on the production container.
Stale derivatives after an in-place image replacement. If the uploaded file keeps the same path (common with fixed filenames like hero.jpg), some workflows overwrite the original while leaving old derivatives in place, and browsers keep serving the cached old version. Prefer content-hashed filenames, or explicitly purge derivatives on replacement, and bust the CDN.
Storage disk vs. URL mismatch. The classic failure mode: files land on the configured disk, but the generated URLs assume public//storage. If srcset entries 404 while the files clearly exist, check config/filesystems.php for the disk's url key and whether php artisan storage:link has run.
Deletion leaks. Confirm what the package cleans up on model delete. If it doesn't remove derivatives, add a deleting observer — otherwise a long-lived media library grows unbounded and nobody notices for a year.
Debugging checklist when an image renders but looks wrong: view source and confirm (a) srcset has >1 entry, (b) each URL returns 200 with the expected Content-Type, (c) sizes matches the actual layout width at the breakpoint you're testing, (d) the largest width is at least 2× the largest rendered width if you care about retina.
Extension points worth knowing: the config file is the sanctioned place to override width sets and formats; the Blade component is the sanctioned place to override markup. Prefer wrapping the package over forking it — at 0.0.x, with undocumented releases landing at this cadence, a fork will diverge within one or two releases. If you need behaviour the config can't express (custom naming, non-standard markup), keep the override in your own component layer so a package bump stays a one-line change.
How can I help you explore Laravel packages today?