elfeffe / image-resizer
Image Resizer Controller
Requires
- php: ^8.4
- bepsvpt/blurhash: ^4.1.0
- intervention/image: ^4.0
- laravel/framework: ^12.0 || ^13.0
- spatie/laravel-package-tools: ^1.93.0
- webwizo/laravel-shortcodes: dev-master
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Laravel package that provides image resizing, optimization, and lazy loading with BlurHash support for smooth progressive image loading.
Features
- Image Resizing: Automatic image resizing with WebP support
- BlurHash Integration: Smooth progressive loading with multi-color blur placeholders
- Lazy Loading: Smart lazy loading with IntersectionObserver
- LQIP Support: Low Quality Image Placeholders with dominant color extraction
- Self-Contained: No external dependencies, works independently
- Mobile Optimized: Responsive design with proper aspect ratio handling
- SEO Friendly: Optimized for search engines with proper image attributes
- Google Indexing: Real src attributes ensure images are properly indexed
SEO & Google Indexing
🔍 The package is fully SEO-optimized and Google-friendly!
✅ SEO Features:
- Real
srcattributes: Images have actual URLs immediately, not placeholder data attributes - Proper
srcsetattributes: Responsive image sets are visible to crawlers - Native lazy loading: Uses browser's
loading="lazy"attribute - NoScript fallback: Images work even when JavaScript is disabled
- Structured markup: Proper
<picture>elements with multiple formats - Image dimensions: Width and height attributes for layout stability
- Alt text support: Full accessibility and SEO description support
🤖 How Google Sees Your Images:
<!-- What Google's crawler sees immediately: --> <picture> <source srcset="/images/nail-design-400.webp 400w, /images/nail-design-800.webp 800w" type="image/webp"> <source srcset="/images/nail-design-400.jpg 400w, /images/nail-design-800.jpg 800w" type="image/jpeg"> <img src="/images/nail-design-800.jpg" srcset="/images/nail-design-400.jpg 400w, /images/nail-design-800.jpg 800w" alt="Beautiful nail art design with flowers" width="800" height="600" loading="lazy" /> </picture>
🚀 Zero SEO Impact:
- No lazy loading penalties: Real URLs are present from server-side rendering
- Google Images indexing: All image URLs are discoverable immediately
- Fast Core Web Vitals: Native lazy loading improves page speed scores
- Accessibility compliant: Screen readers see proper image information
- Progressive enhancement: BlurHash and smooth loading enhance UX without affecting SEO
📊 SEO vs UX Balance:
| Aspect | SEO Benefit | UX Enhancement |
|---|---|---|
Real src attributes |
✅ Google sees actual URLs | ✅ Images work without JS |
loading="lazy" |
✅ Faster page load scores | ✅ Native browser optimization |
| BlurHash placeholders | ✅ No impact on crawling | ✅ Smooth progressive loading |
| WebP + JPEG sources | ✅ Modern format indexing | ✅ Optimal format per browser |
| Responsive srcsets | ✅ Mobile-friendly signals | ✅ Perfect images per device |
Result: You get the best of both worlds - perfect SEO indexing AND amazing user experience!
Installation
- Install the package via Composer:
composer require elfeffe/image-resizer
-
Publish the configuration:
php artisan vendor:publish --tag=image-resizer-config
-
Publish the migrations:
php artisan vendor:publish --tag=image-resizer-migrations php artisan migrate
-
Publish the assets:
php artisan vendor:publish --tag=image-resizer-assets
-
Optimize for Apache servers (optional but recommended):
For maximum performance, install the htaccess optimization rules:
Option A: Automated Installation (Recommended)
php artisan image-resizer:install-htaccess
This command will:
- ✅ Safely merge rules into your existing
.htaccessfile - ✅ Create a backup before making changes
- ✅ Check for existing rules to avoid duplicates
- ✅ Place rules in the correct location (after
RewriteEngine On)
Option B: Manual Integration
php artisan vendor:publish --tag=image-resizer-htaccess
This creates
.htaccess-image-resizerin yourpublic/directory. Then manually add the contents to your existingpublic/.htaccessfile, right afterRewriteEngine On:RewriteEngine On # Image Resizer Performance Optimization # This rule serves cached images directly from filesystem when available, # falling back to Laravel dynamic processing when not cached yet. # Image Resizer Rule - Check for cached file first, then fallback to Laravel RewriteCond %{REQUEST_URI} ^/image_resizer/([^/]*)/w/([^/]*)/h/([^/]*)/([^/]*)/([^.]*)\.(.*)$ RewriteCond %{DOCUMENT_ROOT}/storage/image_resizer/%1/%2x%3/%4.%6 -f RewriteRule ^image_resizer/([^/]*)/w/([^/]*)/h/([^/]*)/([^/]*)/([^.]*)\.(.*)$ /storage/image_resizer/$1/$2x$3/$4.$6 [L] # Your existing rules...
- ✅ Safely merge rules into your existing
-
For Nginx servers (alternative):
Add this to your Nginx configuration:
location / { # Image Resizer optimization - serve cached files directly rewrite ^/image_resizer\/([^/]*)\/w\/([^/]*)\/h\/([^/]*)\/([^/]*)\/([^.]*)\.(.*)$ /storage/image_resizer/$1/$2x$3/$5.$6 last; # Your existing try_files... try_files $uri $uri/ /index.php?$query_string; }
Safety Features
The package includes several safety features to protect your existing configuration:
Automated Installation Safety:
- 🔒 Automatic backups: Creates
.htaccess.backup.YYYY-MM-DD-HH-MM-SSbefore any changes - 🔍 Duplicate detection: Won't add rules if they already exist
- 🎯 Precise placement: Inserts rules in the correct location automatically
- 🛡️ Non-destructive: Never overwrites your existing
.htaccessfile - 🔄 Reversible: Use
--forceflag to reinstall if needed
Manual Integration Safety:
- 📁 Separate file: Published as
.htaccess-image-resizer(doesn't overwrite existing) - 👀 Review first: You can inspect the rules before adding them
- 🎛️ Full control: You decide exactly where to place the rules
Troubleshooting:
# Reinstall rules (useful after Laravel updates) php artisan image-resizer:install-htaccess --force # Check if rules are working curl -I https://yoursite.com/image_resizer/123/w/800/h/600/resize/test.jpg # Look for: X-Image-Resizer: true (Laravel processing) # Or standard Apache headers (direct file serving)
Storage & serving
Resized images are stored on the image_resizer disk. By default that disk is
local (storage/app/public/image_resizer) and URLs are app-relative — the
historical behaviour, including the Apache/Nginx direct-serve fast paths
(image-resizer:install-htaccess).
For object storage (Backblaze B2, Cloudflare R2, AWS S3) set
IMAGERESIZER_STORAGE_DRIVER=s3. Resized images then live in the bucket under
the configured root prefix and are never written to the app disk.
Configuration
| Key | Env | Default | Purpose |
|---|---|---|---|
storage.disk |
IMAGERESIZER_STORAGE_DISK |
image_resizer |
Disk name registered for resizes |
storage.driver |
IMAGERESIZER_STORAGE_DRIVER |
local |
local or s3 |
storage.root |
IMAGERESIZER_STORAGE_ROOT |
image_resizer |
Bucket key prefix (s3 only) |
storage.url |
IMAGERESIZER_STORAGE_URL |
— | Public bucket base URL (s3 disk) |
storage.visibility |
IMAGERESIZER_STORAGE_VISIBILITY |
public |
Object visibility |
storage.s3.key |
IMAGERESIZER_STORAGE_KEY |
— | |
storage.s3.secret |
IMAGERESIZER_STORAGE_SECRET |
— | |
storage.s3.region |
IMAGERESIZER_STORAGE_REGION |
— | |
storage.s3.bucket |
IMAGERESIZER_STORAGE_BUCKET |
— | |
storage.s3.endpoint |
IMAGERESIZER_STORAGE_ENDPOINT |
— | |
storage.s3.use_path_style_endpoint |
IMAGERESIZER_STORAGE_PATH_STYLE |
false |
|
serve.url |
IMAGERESIZER_SERVE_URL |
— | Public base for resize URLs (CDN/CNAME/bucket) |
serve.mode |
IMAGERESIZER_SERVE_MODE |
cdn_origin |
cdn_origin or redirect |
Serving modes
local(default): cached files at{id}/{w}x{h}/{type}.{ext}served by the web server fast path or generated on demand. Unchanged historical behaviour.s3+cdn_origin: point a CDN pull zone (e.g. Bunny) at the app and setserve.urlto the CDN URL. Emitted URLs point at the CDN; on a cache miss the CDN pulls the app, which GETs the object from the bucket, or generates it, stores it (withContent-TypeandCache-Control: public, max-age=2628000), and streams it. This mode never redirects (loop-safe). Works with a private bucket.s3+redirect: setserve.urlto the public bucket/CDN URL. Emitted URLs stay on the app route; the app 301s to the bucket object once it exists (generating + storing it on first request). Requiresserve.urland a public-read bucket.
type=original redirects to the original media URL, and is refused unless allow_original is on (see Access control).
Notes
- s3 mode requires Laravel's S3 driver (
league/flysystem-aws-s3-v3) in the host project. - Object storage keys are URL-shaped (
{id}/w/{w}/h/{h}/{type}/{file}under the disk root) soserve.url+ URL path resolves to the object. - Storage failures are never silent: the endpoint returns 500 and logs the error.
- When a CDN sits in front of the app, keep edge 404 caching disabled so missing media does not poison the cache.
Access control
/image_resizer/{id}/w/{w}/h/{h}/{type}/{file} takes any media id and asks for no login, so unless a project closes it a counter
loop (/image_resizer/1/…, /2/…) lists everything the project stores. A project that sets none of the flags below is served
exactly as it was before these rules existed (same URLs, same cache layout, same answers; one thing apart: only the route's own
segments are read, see Every project). It opts in with strict, or with signed, which includes strict.
| Key | Env | Default | Purpose |
|---|---|---|---|
signed |
IMAGERESIZER_SIGNED |
false |
Sign every URL the builders make and refuse any other request; implies strict |
signature_key |
IMAGERESIZER_SIGNATURE_KEY |
app.key |
The HMAC key |
strict |
IMAGERESIZER_STRICT |
false |
Hold the route to the types, the size limit and the original gate, without signing |
allow_original |
IMAGERESIZER_ALLOW_ORIGINAL |
false |
Serve type=original in a strict project |
max_dimension |
IMAGERESIZER_MAX_DIMENSION |
4096 |
The largest w or h in a strict project; more is a 422 |
throttle |
IMAGERESIZER_THROTTLE |
unset | Requests a minute per address (named limiter image-resizer); unset or 0 is no limit |
prune.schedule |
IMAGERESIZER_PRUNE_SCHEDULE |
true |
false: the package does not schedule image-resizer:prune, the host runs it |
Strict (strict, or signed)
- Sizes:
wandhabovemax_dimensionare a 422. A width or height that is not a positive number (0,None) still means the source's own size, and every spelling of a size (0100,h/None) is one cache entry. The builders ask for the limit at most. - Types:
resize,fit,null(the file re-encoded at its own size, whatgetMediaHtml()asks for without a type),crop(never implemented, so also the file at its own size; two Neoteo themes still ask for it) andoriginal; any other is a 422.resizeandfitnever go above the source: afitbox larger than the image crops it to the box's ratio at its own size. originalis a 403 unlessallow_originalis on. No builder of this package makes such a URL.allow_originalis not a confidentiality control in a project that does not sign:type=null,crop,w/0andw/4096return the same picture at full size. Only signing keeps a stranger from it.
Throttle
Above throttle requests a minute from one address the route answers 429 with Retry-After. It is off unless the host sets a number.
A cache hit counts, and a page of thumbnails asks for one request each, so a dense page needs a high number. Behind a proxy or a CDN
the address is the proxy's unless the host trusts it, and a CDN pull zone sends every visitor from a few addresses: size it for them.
A host that wants another rule defines the limiter image-resizer itself.
Every project
- Route segments only: a
?type=originalor?img=7in the query string changes nothing; the code reads the path's own segments. Laravel'sRequest::__get()prefers a query-string value to a route parameter, which let a visitor steer the controller, past a signature and past a host that checks the route'simg(PostFiber's tenant guard does).
Signed URLs
With signed on, getFriendlyImageUrl(), getMediaUrl() and getMediaHtml() end every URL in ?s=, and the route answers 403 to
any request whose s is not the signature of its own path, before it reads the database or a disk (a cache hit included):
s = hash_hmac('sha256', "{id}|{w}|{h}|{type}|{name}.{ext}", signature_key ?? app.key) // hex, the path's segments as the URL carries them
Elfeffe\ImageResizer\Support\Access::signature($id, $w, $h, $type, $file) makes it. The file name and the format are signed too:
the encoder follows ext, and an object-storage resize is kept under a key that carries the name.
- A URL is valid for as long as the key is: it survives a deploy, and rotating
app.key(orsignature_key) turns every URL already handed out, in pages, feeds and emails, into a 403 and every cache folder into a miss (APP_PREVIOUS_KEYSis not honoured: do not rotate it casually). - Resizes are cached under a token derived from the id (
Access::folder()), not the id. A file kept at{id}/{w}x{h}/{type}.{ext}is reachable by anyone who knows three small numbers (/storage/image_resizer/…when the disk is linked intopublic, the Apache and Nginx fast paths, a public bucket), and none of them can check a signature. The signature is never part of a path: all the signed URLs of a variant share one file. Turningsignedon leaves the old{id}folders unread by the app; delete them once, as the user that wrote them (image-resizer:prunewould only in time, and never while someone still fetches them: it goes by the last read). - Never build a resizer URL by hand and never rewrite its
w,hortypein JavaScript or in a consumer of an API: that is a 403. Ask the builders for the size you need. - The fast paths of
image-resizer:install-htaccessand the Nginx rewrite above never find a file in a project that signs; do not rely on them there. A CDN pull zone must forward the query string to the app (?s=is part of the URL), and the CDN's own cache must not ignore it. serve.mode=redirecthands the object's own URL to whoever passes the check, and that bucket is public-read: whoever has seen the redirect can keep reading the object. Usecdn_originwith a private bucket for confidential media.
Shortcodes (optional)
The [media-library-item] shortcode is registered only when webwizo/laravel-shortcodes is installed and its provider registered (it
is a Composer suggestion, not a requirement: its provider replaces Laravel's view factory). A project without it boots and renders as
before; a project that uses the shortcode requires the package itself.
Usage
Basic Usage
Add the trait to your model:
use Elfeffe\ImageResizer\Traits\HasImageResizer; class YourModel extends Model { use HasImageResizer; }
In Blade Views
Include the package styles and scripts in your layout:
@imageResizerStyles @imageResizerScripts
Use the helper method to display images:
{!! $model->getMediaHtml($media, 800, 600) !!}
Different Ways to Use the Package
The package provides multiple methods for working with images, depending on your needs:
1. Complete HTML Output (Recommended) - Zero Configuration
Use getMediaHtml() when you want the complete image markup with lazy loading, BlurHash, and optimization:
{!! $model->getMediaHtml($media, 800, 600) !!}
🚀 This is completely automatic! The method handles everything for you:
- ✅ Lazy loading - Images load when they come into view
- ✅ BlurHash placeholders - Smooth progressive loading
- ✅ WebP optimization - Modern format with JPEG fallback
- ✅ Responsive srcsets - Multiple sizes for different screens
- ✅ SEO attributes - Proper width, height, and alt attributes
- ✅ Mobile optimization - Works perfectly on all devices
No additional configuration needed! Just add @imageResizerStyles and @imageResizerScripts to your layout once, and every getMediaHtml() call will automatically include lazy loading and all optimizations.
2. Image URLs Only (For Custom Markup)
When you need just the image URLs without HTML (for custom implementations, JSON APIs, or building your own markup):
// Get optimized image URL $imageUrl = $model->getMediaUrl($media, 800, 600); // Get WebP version URL $webpUrl = $model->getMediaUrl($media, 800, 600, 'webp'); // Get srcset string for responsive images $srcset = $model->getMediaSrcset($media, [400, 600, 800, 1200]); $webpSrcset = $model->getMediaSrcset($media, [400, 600, 800, 1200], 'webp');
⚠️ Note: When using individual URLs, you'll need to implement your own lazy loading logic if desired.
3. Image Data Object (For Complex Implementations)
Get all image data as an object for advanced use cases:
$imageData = $model->getMediaData($media, 800, 600); // Returns object with: // $imageData->src (main image URL) // $imageData->srcset (responsive srcset) // $imageData->srcsetWebp (WebP srcset) // $imageData->blurHash (BlurHash string) // $imageData->lqipColor (dominant color) // $imageData->width (actual width) // $imageData->height (actual height)
⚠️ Note: This gives you the data, but you'll need to build the lazy loading functionality yourself.
4. BlurHash and LQIP Data Only
For cases where you only need the placeholder data:
// Get BlurHash string $blurHash = $media->getCustomProperty('blurhash'); // Get LQIP color $lqipColor = $media->getCustomProperty('lqip_color'); // Check if BlurHash is available $hasBlurHash = !empty($media->getCustomProperty('blurhash'));
Use Cases for Different Methods
✅ Use getMediaHtml() when:
- Building standard web pages
- You want automatic lazy loading with zero setup
- You need SEO optimization
- You want BlurHash integration out-of-the-box
- You want everything handled automatically (recommended for 90% of use cases)
✅ Use image URLs/data when:
- Building JSON APIs
- Creating custom image components
- Using JavaScript frameworks (Vue, React, etc.)
- Building custom lazy loading implementations
- Creating image galleries with specific markup
- You need full control over the HTML structure
📱 API/JSON Response Example:
// In your API controller public function getImages() { $images = $this->model->getMedia('gallery')->map(function ($media) { return [ 'id' => $media->id, 'src' => $this->model->getMediaUrl($media, 800, 600), 'srcset' => $this->model->getMediaSrcset($media, [400, 600, 800, 1200]), 'webp_srcset' => $this->model->getMediaSrcset($media, [400, 600, 800, 1200], 'webp'), 'blurhash' => $media->getCustomProperty('blurhash'), 'lqip_color' => $media->getCustomProperty('lqip_color'), 'alt' => $media->getCustomProperty('alt', ''), 'width' => $media->getCustomProperty('width'), 'height' => $media->getCustomProperty('height'), ]; }); return response()->json($images); }
🎨 Custom Blade Component Example:
{{-- resources/views/components/custom-image.blade.php --}} @php $imageData = $model->getMediaData($media, $width, $height); @endphp <div class="custom-image-container" data-blurhash="{{ $imageData->blurHash }}"> <div class="custom-placeholder" style="background-color: {{ $imageData->lqipColor }}"> <span class="loading-text">Loading amazing image...</span> </div> <picture class="custom-picture"> <source data-srcset="{{ $imageData->srcsetWebp }}" type="image/webp"> <source data-srcset="{{ $imageData->srcset }}" type="image/jpeg"> <img data-src="{{ $imageData->src }}" alt="{{ $alt }}" class="custom-img {{ $class }}" width="{{ $imageData->width }}" height="{{ $imageData->height }}" > </picture> </div> {{-- Usage --}} <x-custom-image :model="$product" :media="$product->getFirstMedia('gallery')" :width="400" :height="300" alt="Product image" />
⚛️ JavaScript Framework Integration:
// Fetch image data for Vue/React components fetch('/api/images') .then(response => response.json()) .then(images => { images.forEach(image => { // Use image.src, image.srcset, image.blurhash, etc. // Build your custom image component }); });
Advanced Usage
Responsive Images
For responsive images (auto-height):
{!! $model->getMediaHtml($media, 800, null, 'resize', ['alt' => 'Description']) !!}
Custom CSS Classes
{!! $model->getMediaHtml($media, 800, 600, 'resize', ['alt' => 'Description'], 'custom-filename', 'custom-css-class') !!}
Manual Template Usage
You can also use the template directly:
@include('image-resizer::placeholder', [ 'src' => $imageSrc, 'srcset' => $imageSrcset, 'srcsetWebp' => $imageSrcsetWebp, 'width' => 800, 'height' => 600, 'blurHash' => $blurHash, 'lqipColor' => '#f0f0f0', 'class' => 'my-image-class', 'attributeString' => 'alt="My Image"' ])
Critical: Parent Container Setup
⚠️ IMPORTANT: To ensure lazy loading works properly, parent containers must have defined dimensions. If the parent container has 0 width or height, the IntersectionObserver cannot detect the image, and lazy loading will never trigger.
✅ Correct Parent Container Examples:
Grid Layout (Recommended)
<!-- CSS Grid with defined aspect ratio --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> @foreach($images as $image) <div class="aspect-square"> <!-- This ensures proper dimensions --> {!! $image->getMediaHtml($media, 400, 400) !!} </div> @endforeach </div>
Flexbox Layout
<!-- Flexbox with min-height --> <div class="flex flex-wrap gap-4"> @foreach($images as $image) <div class="w-80 h-80 flex-shrink-0"> <!-- Fixed dimensions --> {!! $image->getMediaHtml($media, 320, 320) !!} </div> @endforeach </div>
Responsive Cards
<!-- Card layout with proper container --> <div class="max-w-sm mx-auto bg-white rounded-lg shadow-md overflow-hidden"> <div class="h-48"> <!-- Fixed height for image container --> {!! $model->getMediaHtml($media, 400, 192) !!} </div> <div class="p-4"> <h3 class="font-bold">Card Title</h3> <p class="text-gray-600">Card content...</p> </div> </div>
Masonry/Gallery Layout
<!-- Masonry with aspect ratio containers --> <div class="columns-1 md:columns-2 lg:columns-3 gap-4"> @foreach($images as $image) <div class="break-inside-avoid mb-4"> <!-- Let the image determine height but ensure width --> <div class="w-full min-h-[200px]"> {!! $image->getMediaHtml($media, 400, null) !!} </div> </div> @endforeach </div>
❌ Incorrect Parent Container Examples:
No Defined Dimensions
<!-- BAD: Container may collapse to 0 height --> <div> <!-- No dimensions defined --> {!! $model->getMediaHtml($media, 400, 400) !!} </div>
Undefined Parent Width
<!-- BAD: Parent width undefined --> <div class="some-undefined-container"> {!! $model->getMediaHtml($media, 400, 400) !!} </div>
Nested Containers Without Dimensions
<!-- BAD: Nested containers without proper sizing --> <div class="wrapper"> <div class="inner"> <!-- No dimensions --> {!! $model->getMediaHtml($media, 400, 400) !!} </div> </div>
🔧 Debugging Container Issues:
If images aren't loading, check the container dimensions:
// Add this to your browser console to debug document.querySelectorAll('[data-blurhash]').forEach(container => { const rect = container.getBoundingClientRect(); console.log('Container dimensions:', { width: rect.width, height: rect.height, element: container }); if (rect.width === 0 || rect.height === 0) { console.warn('Container has 0 dimensions - lazy loading will not work:', container); } });
💡 Best Practices:
- Always define container dimensions using CSS classes like
w-80 h-80,aspect-square, orh-48 - Use aspect-ratio utilities for responsive layouts:
aspect-video,aspect-square,aspect-[4/3] - Set min-height for containers when height is dynamic:
min-h-[200px] - Test on mobile to ensure containers maintain proper dimensions on smaller screens
- Use fixed dimensions for critical above-the-fold images
BlurHash Integration
The package automatically generates BlurHash strings for all uploaded images. BlurHash creates smooth, multi-color blur placeholders that provide a better user experience compared to solid color placeholders.
How it works:
- Image Upload: When an image is uploaded, a job is dispatched to calculate both LQIP color and BlurHash
- Storage: BlurHash and LQIP data are stored in the media's
custom_properties - Display: The template automatically uses BlurHash for fixed-dimension images and LQIP color for responsive images
- Progressive Loading: BlurHash renders immediately, then fades to the actual image when loaded
Manual BlurHash Generation
You can manually generate BlurHash for existing images:
php artisan image-resizer:calculate-lqip
Configuration
The package configuration is located at config/image-resizer.php:
return [ 'blurhash' => [ 'component_x' => 6, // Horizontal detail (1-9) 'component_y' => 4, // Vertical detail (1-9) 'max_size' => 128, // Max processing size for performance ], 'lqip' => [ 'quality' => 20, // LQIP quality (1-100) 'blur' => 5, // Blur radius ], 'formats' => [ 'webp' => true, // Generate WebP versions 'jpeg' => true, // Generate JPEG versions ], ];
Encoding and cache pruning
Every resize is written with its metadata stripped (EXIF and ICC, once the orientation they describe has been applied). JPEG and WebP use IMAGERESIZER_QUALITY (default 78). With IMAGERESIZER_AVIF=true the <picture> offers an AVIF <source> before the WebP one, which browsers that take it prefer (about 10–30 % smaller for the same look); it costs more CPU per resize, so it is off by default. IMAGERESIZER_AVIF_QUALITY (default 60, AVIF's own scale) and IMAGERESIZER_AVIF_SPEED (libheif's 0 slowest and best to 10 fastest, default 7) tune it. The .avif extension is always accepted by the route.
Cached resizes nobody has asked for in IMAGERESIZER_PRUNE_DAYS days (default 30; 0 turns the schedule off) are deleted every night at 04:40 by image-resizer:prune; on a local disk the last read counts as the last use, on a remote disk the file's age does, and folders left empty are removed. php artisan image-resizer:prune --days=7 --dry-run shows what a run would delete. A file counts as deleted only once it is gone: the files the user running the command may not remove are reported apart and the command exits with a failure. The crontab's schedule:run often runs as another user than the web server that wrote the cache; set IMAGERESIZER_PRUNE_SCHEDULE=false and run image-resizer:prune from a queued job (the queue worker runs as the web user).
Preparing resizes before the first view
A resize is made by the first request for its URL, and that request downloads the whole original first: from a bucket that is about half a second plus the transfer, again for every size of the same image, with a web worker held the whole time. A page of new thumbnails queues behind those downloads. A model can say which resize URLs its views will ask for, and the package makes them in a queued job right after the image is stored, reading the original once for all of them, so the first view finds them cached.
Opt in by defining the public method imageResizerWarmUrls(Media $media): array on the model, returning the URLs as it builds them for its views (getMediaUrl(), getFriendlyImageUrl()):
use Elfeffe\ImageResizer\Traits\HasImageResizer; use Spatie\MediaLibrary\MediaCollections\Models\Media; class Post extends Model implements HasMedia { use HasImageResizer, InteractsWithMedia; public function imageResizerWarmUrls(Media $media): array { return [ $this->getMediaUrl($media, 320, 240, 'fit'), $this->getMediaUrl($media, 640, null, 'resize', null, 'image/webp'), ]; } }
- Every URL is read the way the route reads a request for it, so what is made is exactly the file that URL is served from: a signed project's signatures are checked, a strict project's spelling is the canonical one, and the file is kept where the route looks for it (the cache disk, or the bucket). A URL the route would refuse, that is not this image's, that is
type=originalor that is not a resizer URL is skipped. One that is cached already is left as it is. WarmResizesJobis queued onIMAGERESIZER_QUEUEfive seconds after a media is created (after commit; the row is written before its file is), forimage/*except SVG and GIF. It is unique per media for an hour, may run 120 seconds and is tried 3 times. It is best effort, like the placeholder job: a media that is gone, a file that is not there, is empty or cannot be decoded ends it quietly, and a resize it did not make is made by the first request, as ever. A failure of the bucket fails the job, which is tried again, as it fails a request.- A model that does not define the method never gets a job, and nothing changes for a project that defines it on none:
HasImageResizerhas no default for it. php artisan image-resizer:warmqueues the job for every existing image of the models that define the method, for the images stored before the method was there and after a project changes its sizes (only the sizes that are missing are made).--model=limits it to a model class or morph alias, and may be repeated.
Blade Directives
The package provides convenient Blade directives:
@imageResizerStyles
Includes the package CSS:
@imageResizerStyles
Outputs:
<link rel="stylesheet" href="/vendor/image-resizer/css/image-resizer.css">
@imageResizerScripts
Includes the package JavaScript:
@imageResizerScripts
Outputs:
<script src="/vendor/image-resizer/js/image-resizer.js"></script>
It is a classic script, so the build is an IIFE (vite.config.js): it declares nothing in the page's global scope (only the
window.__imageResizerStarted flag) and cannot clash with another classic script's names. A bundle in ES format once
broke filament-breezy.js with "Identifier 'p' has already been declared"; BuiltScriptIsSelfContainedTest guards it.
After rebuilding, republish into each app (php artisan vendor:publish --tag=image-resizer-assets --force).
Building Assets
The package includes its own build system for development:
Development
cd packages/elfeffe/image-resizer
npm install
npm run dev
Production Build
cd packages/elfeffe/image-resizer
npm run build
Publishing Updated Assets
After making changes to the package assets:
# Build inside the package cd packages/elfeffe/image-resizer npm run build # Publish to the main project cd /path/to/your/project php artisan vendor:publish --tag=image-resizer-assets --force
Performance Optimization
Apache/Nginx Optimization
The package includes smart caching that dramatically improves performance:
How It Works:
-
First request:
/image_resizer/123/w/800/h/600/resize/nail-design.jpg- Processed by Laravel (dynamic)
- Cached to filesystem:
storage/image_resizer/123/800x600/resize.jpg - Returns processed image with headers
-
Subsequent requests: Same URL
- ⚡ Served directly from filesystem (bypasses Laravel entirely)
- 10x faster than dynamic processing
- Zero server load for cached images
Performance Benefits:
- 🚀 First load: ~200ms (Laravel processing)
- ⚡ Cached loads: ~20ms (direct file serving)
- 📈 Scalability: Handles thousands of image requests with minimal server load
- 🔄 Automatic: No manual cache management needed
Server Configuration:
- Apache: Use the published
.htaccessrules (see installation) - Nginx: Use the provided rewrite rules (see installation)
- Without optimization: Still works great, just processes every request through Laravel
AlpineJS Integration
The package is built entirely with AlpineJS for seamless integration:
- Zero conflicts: Works perfectly with existing AlpineJS applications
- Native lazy loading: Uses browser's
loading="lazy"attribute - Smart enhancement: Automatically detects and enhances images
- Livewire support: Re-enhances images on Livewire navigation/updates
- Dynamic content: Handles images added dynamically to the DOM
BlurHash Performance
- BlurHash is calculated asynchronously via Laravel jobs
- Processing size is limited to 128px for performance
- BlurHash generation is optimized for typical web images
- Canvas rendering is optimized for immediate display
Browser Support
- Modern browsers: Full BlurHash, native lazy loading, and WebP support with AlpineJS enhancement
- Older browsers: Graceful fallback to JPEG images, immediate loading without lazy loading
- No JavaScript: Images load normally using native browser
loading="lazy"attribute - Screen readers: Full accessibility with proper alt text and semantic markup
- Mobile: Optimized for touch devices and variable viewports with responsive srcsets
AlpineJS Requirement: This package requires AlpineJS to be loaded in your application for BlurHash transitions. Images will still work without AlpineJS, but BlurHash enhancement won't be available.
Troubleshooting
Images Not Loading
- Check container dimensions: Make sure parent containers have defined width/height
- Verify published assets: Run
php artisan vendor:publish --tag=image-resizer-assets --force - Check image URLs: Since we use real
srcattributes, verify the image URLs are accessible - Test without JavaScript: Images should load even with JS disabled thanks to native lazy loading
- Check console for errors: Use browser dev tools to debug JavaScript issues
- Test container dimensions: Use the debugging script provided in the "Parent Container Setup" section
Note: With the SEO-friendly approach, images will load using native browser lazy loading even if JavaScript fails. BlurHash is an enhancement, not a requirement.
BlurHash Not Displaying
- Verify job processing: Make sure Laravel queues are running
- Check media custom_properties: Verify BlurHash data is stored
- Regenerate BlurHash: Run
php artisan image-resizer:calculate-lqip
Performance Issues
- Optimize image sizes: Use appropriate dimensions for your use case
- Enable WebP: Configure WebP generation in the config
- Queue optimization: Use dedicated queue for image processing
Development
Package Structure
packages/elfeffe/image-resizer/
├── src/
│ ├── Traits/HasImageResizer.php
│ ├── Jobs/CalculateLqipJob.php
│ ├── Jobs/WarmResizesJob.php
│ └── ImageResizerServiceProvider.php
├── resources/
│ ├── views/placeholder.blade.php
│ ├── css/image-resizer.css
│ └── js/image-resizer.js
├── config/image-resizer.php
└── package.json
Testing
Run the package tests:
cd packages/elfeffe/image-resizer
./vendor/bin/phpunit
License
MIT License. See LICENSE.md for details.
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests
- Submit a pull request
Support
For issues and questions, please use the GitHub issue tracker.