moonlight-poland / laravel-context-aware-thumbnails
Context-Aware on-demand thumbnail generation for Laravel with intelligent path organization, Smart Crop, AVIF/WebP support, Laravel Native Signed URLs, and variants system. Automatically organize thumbnails by user/post/album. Zero config, blazing fast! Laravel 12+ ready.
Package info
github.com/Moonlight4000/laravel-thumbnails
pkg:composer/moonlight-poland/laravel-context-aware-thumbnails
Requires
- php: ^8.1|^8.2|^8.3
- ext-gd: *
- illuminate/filesystem: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- intervention/image: ^2.7|^3.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0|^11.0
Suggests
- ext-imagick: Required for Imagick driver support and AVIF format generation (optional)
- intervention/image: Required for advanced image manipulation, Smart Crop, and better performance (recommended)
README
Intelligent On-Demand Image Thumbnails with Smart Crop & Modern Formats
Copyright © 2024-2026 Moonlight Poland. All rights reserved.
Contact: kontakt@howtodraw.pl
License: Commercial License - Free for personal use, paid for commercial use
Repository: https://github.com/Moonlight4000/laravel-thumbnails
Generate image thumbnails on-the-fly in Laravel with Context-Aware Thumbnails™ - the only package that organizes thumbnails exactly where your content lives!
No pre-generation needed. No Redis required. Smart organization included.™
🌟 What Makes Us Unique?
- 🎯 Context-Aware Organization™ - Thumbnails organized by user/post/album (no other package does this!)
- ⚛️ React/Vue/JavaScript Support - The ONLY Laravel thumbnail package with
sync-jscommand for frontend frameworks - 🔐 Signed URLs (Facebook-style) - Time-limited, cryptographically signed URLs to prevent hotlinking
- 🤖 Smart Crop with AI Energy Detection - Automatically focuses on important image areas
- 🚀 AVIF/WebP Support - Modern formats for 50%+ smaller file sizes
- 🔒 Commercial Licensing - Professional support & tamper detection included
📋 Table of Contents
- Why Choose This Over Other Packages?
- What Makes Context-Aware Thumbnails™ Special?
- License Notice
- Features
- Installation
- Quick Start
- Configuration
- Usage
- Context-Aware Thumbnails™
- Advanced Features
- Artisan Commands
- Testing
- Contributing
- License
- Credits
🏆 Why Choose This Over Other Packages?
📊 Complete Feature Comparison
| Feature | Laravel Smart Thumbnails™ (moonlight-poland) |
askancy/ laravel-smart-thumbnails |
lee-to/ laravel-thumbnails |
spatie/ laravel-medialibrary |
|---|---|---|---|---|
| 🎯 UNIQUE FEATURES | ||||
| Context-Aware Organization™ | ✅ ONLY US! | ❌ | ❌ | ❌ |
| Custom path templates | ✅ {user_id}/{post_id} |
❌ | ❌ | ⚠️ Limited |
| Per-user/post isolation | ✅ Built-in | ❌ Manual | ❌ Manual | ⚠️ Via DB |
| Commercial licensing | ✅ $500-$15k | ❌ MIT (free) | ❌ MIT | ✅ Spatie |
| 🖼️ IMAGE PROCESSING | ||||
| AVIF format support | ✅ v2.0+ | ✅ | ❌ | ✅ |
| WebP format support | ✅ v2.0+ | ✅ | ❌ | ✅ |
| Smart Crop (AI energy) | ✅ v2.0+ | ✅ | ❌ | ✅ |
| Crop/Fit/Resize methods | ✅ All 3 | ✅ SmartCrop | ✅ All 3 | ✅ Yes |
| Multiple drivers | ✅ GD/Imagick/Intervention | ✅ GD/Imagick | ⚠️ Intervention only | ✅ Yes |
| Quality control | ✅ Per size | ✅ Per variant | ✅ Global | ✅ Yes |
| 🛡️ ERROR HANDLING | ||||
| Silent/Strict modes | ✅ v2.0+ | ✅ | ❌ | ⚠️ Limited |
| Bulletproof fallbacks | ✅ | ✅ | ⚠️ Basic | ✅ |
| Never breaks app | ✅ | ✅ | ⚠️ Can throw | ✅ |
| ⚡ GENERATION | ||||
| On-demand (lazy) | ✅ Automatic | ✅ Automatic | ✅ Manual | ✅ Manual |
| Middleware fallback | ✅ Auto 404→generate | ❌ | ❌ | ❌ |
| Zero config | ✅ Works out-of-box | ⚠️ Requires setup | ⚠️ Setup needed | ❌ Complex |
| 📁 ORGANIZATION | ||||
| Subdirectory strategies | ✅ Context-aware | ✅ 5 strategies | ❌ Flat | ⚠️ Via DB |
| Hash-based distribution | ⚠️ Manual | ✅ Automatic | ❌ | ❌ |
| Date-based folders | ⚠️ Manual | ✅ Automatic | ❌ | ❌ |
| Handles millions of files | ✅ Yes | ✅ Yes | ⚠️ Slow | ✅ Yes |
| 🎨 VARIANTS & PRESETS | ||||
| Multiple sizes per preset | ✅ | ✅ Variants | ✅ | ✅ |
| Responsive images | ✅ | ✅ | ✅ | ✅ |
| Named presets | ✅ 'small', 'large' |
✅ | ✅ | ✅ |
| 🔧 DEVELOPER EXPERIENCE | ||||
| Blade directive | ✅ @thumbnail() |
❌ | ❌ | ❌ |
| Helper function | ✅ thumbnail() |
❌ | ✅ | ❌ |
| Eloquent trait | ✅ HasThumbnails |
❌ | ✅ | ✅ |
| React/Vue/JS | ✅ ONLY US! | ❌ | ❌ | ❌ |
| Auto-sync JS helper | ✅ sync-js |
❌ | ❌ | ❌ |
| Artisan commands | ✅ generate, clear, sync-js | ✅ purge, optimize | ❌ | ✅ Many |
| 📊 MONITORING | ||||
| Statistics & analytics | ✅ v2.0+ | ✅ Full | ❌ | ✅ |
| Performance metrics | ✅ v2.0+ | ✅ | ❌ | ⚠️ |
| Disk usage tracking | ✅ v2.0+ | ✅ | ❌ | ✅ |
| 🔒 SECURITY | ||||
| File validation | ✅ v2.0+ | ✅ | ⚠️ Basic | ✅ |
| Size limits | ✅ v2.0+ | ✅ | ❌ | ✅ |
| Extension whitelist | ✅ v2.0+ | ✅ | ❌ | ✅ |
| Signed URLs (Facebook-style) | ✅ v2.0.16+ | ❌ | ❌ | ⚠️ Via S3 |
| Time-limited links | ✅ v2.0.16+ | ❌ | ❌ | ❌ |
| Hotlinking prevention | ✅ v2.0.16+ | ❌ | ❌ | ⚠️ Via S3 |
| Tamper detection | ✅ Commercial only | ❌ | ❌ | ❌ |
| 💾 STORAGE | ||||
| Filesystem cache | ✅ | ✅ | ✅ | ✅ |
| Redis/Memcached tags | ❌ | ✅ | ❌ | ⚠️ |
| Multi-disk support | ✅ | ✅ | ✅ | ✅ |
| S3/Cloud storage | ✅ | ✅ | ✅ | ✅ |
| Database storage | ❌ | ❌ | ❌ | ✅ |
| 📦 INSTALLATION | ||||
| Installs | 🆕 New | 17 | ~500 | 50,000+ |
| Stars | ⭐ New | 1 | ~50 | 5,000+ |
| Maturity | 🆕 v2.0.1 | 🆕 v2.0 | ⚠️ v1.x | ✅ v11.x |
🎯 Which Package Should You Choose?
Choose Laravel Context-Aware Thumbnails™ (moonlight-poland) if you need:
- ✅ Context-Aware organization (unique feature!)
- ✅ Thumbnails organized by user/post/album automatically
- ✅ React/Vue/JavaScript support (ONLY package with sync-js!)
- ✅ Signed URLs (Facebook-style) - Time-limited, cryptographically signed protection
- ✅ Auto-strategy: Context-Aware for models, Hash for paths
- ✅ Smart Crop with energy detection (v2.0)
- ✅ AVIF/WebP modern formats (v2.0)
- ✅ Variants system for multiple sizes (v2.0)
- ✅ Daily usage statistics sent to Moonlight (v2.0)
- ✅ Blade directives and helpers for easy use
- ✅ Automatic middleware fallback
- ✅ Commercial support with licensing
- ✅ Simple filesystem-based solution
🔥 What Makes Context-Aware Thumbnails™ Special?
Other packages dump all thumbnails in one folder. We organize them exactly where your content lives:
❌ OTHER PACKAGES:
storage/thumbnails/
├── user1_avatar_thumb_small.jpg
├── post42_image_thumb_small.jpg
├── gallery_photo_thumb_small.jpg
└── ... 10,000+ files in one folder!
✅ CONTEXT-AWARE THUMBNAILS™:
storage/
├── user-posts/1/12/thumbnails/image_thumb_small.jpg
├── galleries/5/3/thumbnails/photo_thumb_medium.jpg
├── avatars/8/thumbnails/avatar_thumb_small.jpg
└── fanpages/42/photos/thumbnails/banner_thumb_large.jpg
Benefits:
- ✅ Delete post → thumbnails automatically deleted with folder
- ✅ Per-user backups → backup specific user folders
- ✅ CDN routing → route different contexts to different CDNs
- ✅ Filesystem performance → fewer files per directory = faster I/O
- ✅ Security → isolate user content with directory permissions
- ✅ Organization → find thumbnails instantly, no database queries
⚠️ License Notice
This is a COMMERCIAL package with a dual-licensing model:
- 🆓 FREE for personal/non-commercial use
- 💼 PAID for commercial use ($500-$15,000/year)
See LICENSE.md for details.
Contact: kontakt@howtodraw.pl
GitHub: https://github.com/Moonlight4000/laravel-thumbnails
✨ Features
- 🔥 Context-Aware Thumbnails™ - Organize thumbnails by user/post/album/any structure (UNIQUE!)
- 🚀 On-Demand Generation - Thumbnails generated only when requested (lazy loading)
- 🔐 Signed URLs (Facebook-style) - Time-limited, cryptographically signed URLs to prevent hotlinking
- 💾 Filesystem Cache - Fast subsequent loads, no Redis/Memcached needed
- 🔌 Zero Configuration - Sensible defaults, works out of the box
- 🎨 Multiple Drivers - GD (default), Imagick, or Intervention Image
- 📐 3 Resize Methods - Resize (proportional), Crop (exact size), Fit (with padding)
- 🔧 Fully Configurable - Custom sizes, quality, drivers, paths, and more
- 🎯 Blade Directive -
@thumbnail('path/image.jpg', 'small', 'post', ['user_id' => 1]) - ⚛️ React/Vue/JavaScript Helper - Full feature parity with PHP (sync-js command)
- 📦 Facade & Helpers - Multiple ways to use
- 🗑️ Auto Cleanup - Delete folder = thumbnails gone
- 🛠️ Artisan Commands - Generate or clear thumbnails via CLI
- ✅ Laravel 10 & 11 - Full support for modern Laravel
📦 Installation
composer require moonlight-poland/laravel-smart-thumbnails
Optional Dependencies (Recommended)
For best performance and advanced features, install these optional packages:
# Intervention Image - Required for Smart Crop and better performance composer require intervention/image # Imagick Extension - Required for AVIF format support # (Install via your system's package manager, e.g., apt install php-imagick)
What you get with optional dependencies:
- ✅ Smart Crop - AI-powered energy detection (requires Intervention Image)
- ✅ AVIF format - Modern image format with 50% smaller files (requires ext-imagick)
- ✅ Better performance - Intervention Image is faster than GD for large images
- ⚠️ Without them - Package falls back to GD (works, but limited features)
License Activation
For Personal (Free) use:
php artisan thumbnails:license --type=personal
For Commercial use:
# Enter your license key (from purchase email)
php artisan thumbnails:license YOUR-LICENSE-KEY
Contact for licensing: kontakt@howtodraw.pl
Optional: Publish Config
php artisan vendor:publish --tag=thumbnails-config
Make Sure Storage is Linked
php artisan storage:link
For React/Vue Apps: Generate JS Helper
REQUIRED if using React, Vue, or any JavaScript framework:
php artisan thumbnails:sync-js
This generates resources/js/utils/thumbnails.js with your config contexts.
When to run:
- ✅ After installation
- ✅ After changing
config/thumbnails.php - ✅ After adding new contexts
See React/Vue Usage section below for details.
🚀 Quick Start
Basic Usage (Blade)
{{-- Original image --}} <img src="{{ asset('storage/photos/cat.jpg') }}"> {{-- Thumbnail (auto-generated on first request!) --}} <img src="@thumbnail('photos/cat.jpg', 'small')">
That's it! 🎉
- First request: Generates thumbnail (~50-200ms)
- Next requests: Cached file served by Nginx (~1-5ms)
🔥 Context-Aware Thumbnails™ (UNIQUE FEATURE!)
The only Laravel package that organizes thumbnails exactly where your content lives!
Why Context Matters
Traditional packages dump all thumbnails into one folder. This causes:
- ❌ Messy filesystem (thousands of files in one directory)
- ❌ Difficult cleanup (delete post, but thumbnails remain)
- ❌ No per-user isolation
- ❌ CDN routing nightmare
- ❌ Slow backups (can't backup specific content types)
Context-Aware Thumbnails™ solves this:
{{-- USER POST CONTEXT --}} <img src="@thumbnail('image.jpg', 'small', 'post', ['user_id' => 1, 'post_id' => 12])"> {{-- Result: /storage/user-posts/1/12/thumbnails/image_thumb_small.jpg --}} {{-- GALLERY CONTEXT --}} <img src="@thumbnail('photo.jpg', 'medium', 'gallery', ['user_id' => 5, 'album_id' => 3])"> {{-- Result: /storage/galleries/5/3/thumbnails/photo_thumb_medium.jpg --}} {{-- AVATAR CONTEXT --}} <img src="@thumbnail('avatar.jpg', 'small', 'avatar', ['user_id' => 8])"> {{-- Result: /storage/avatars/8/thumbnails/avatar_thumb_small.jpg --}} {{-- NO CONTEXT (default) --}} <img src="@thumbnail('cat.jpg', 'small')"> {{-- Result: /storage/thumbnails/cat_thumb_small.jpg --}}
Configuration
Define custom contexts in config/thumbnails.php:
'contexts' => [ // User posts - separate per user and post 'post' => 'user-posts/{user_id}/{post_id}', // Gallery - separate per user and album 'gallery' => 'galleries/{user_id}/{album_id}', // Avatars - per user only 'avatar' => 'avatars/{user_id}', // Fanpage content 'fanpage' => 'fanpages/{fanpage_id}/{type}', // Your custom contexts 'product' => 'products/{category_id}/{product_id}', 'team' => 'companies/{company_id}/team', ],
PHP Usage
// In controllers $url = thumbnail('image.jpg', 'small', true, 'post', [ 'user_id' => auth()->id(), 'post_id' => $post->id ]); // Helper functions $url = thumbnail_url('photo.jpg', 'medium', 'gallery', [ 'user_id' => $user->id, 'album_id' => $album->id ]); // Facade use Thumbnail; $url = Thumbnail::generate('avatar.jpg', 'small', true, 'avatar', [ 'user_id' => $user->id ]);
Model Integration
use Moonlight\Thumbnails\Traits\HasThumbnails; class UserPost extends Model { use HasThumbnails; // Define default context for this model protected $thumbnailContext = 'post'; // Provide context data automatically public function getThumbnailContextData(): array { return [ 'user_id' => $this->user_id, 'post_id' => $this->id, ]; } } // In Blade - context applied automatically! <img src="{{ $post->thumbnail('image.jpg', 'small') }}"> {{-- Auto-uses 'post' context with user_id and post_id --}}
Benefits
✅ Perfect organization - thumbnails live with their content
✅ Easy cleanup - delete post folder, thumbnails gone
✅ Per-user isolation - great for multi-tenant apps
✅ CDN-friendly - route /user-posts/1/* to User 1's CDN
✅ Faster backups - backup specific content types
✅ Better performance - fewer files per directory
🎨 React / Vue / JavaScript Usage
🌟 UNIQUE FEATURE: We are the ONLY Laravel thumbnail package that provides seamless React/Vue/JavaScript integration with automatic context synchronization! Other packages only work with Blade.
IMPORTANT: For React/Vue apps, you need to generate a JavaScript helper that mirrors your PHP config.
Step 1: Generate JS Helper
php artisan thumbnails:sync-js
This creates resources/js/utils/thumbnails.js with your contexts from config/thumbnails.php.
Run this command whenever you:
- Change
config/thumbnails.php - Add new contexts
- Change filename patterns
Step 2: Import in React/Vue
✅ YES, the import is REQUIRED! Without it, your React/Vue components won't have thumbnail URLs.
// React Component import { getThumbnailUrl } from '@/utils/thumbnails'; function PostMedia({ post }) { const mediaFiles = post.media_files || []; return ( <div> {mediaFiles.map((media, index) => ( <img key={index} src={getThumbnailUrl(media.path, 'small')} alt={media.alt} /> ))} </div> ); }
Available Functions
import { getThumbnailUrl, // Basic usage getThumbnailUrlWithContext, // With Context-Aware buildContextPath, // Build context path only THUMBNAIL_CONTEXTS, // Available contexts THUMBNAIL_SIZES // Available sizes } from '@/utils/thumbnails'; // Basic usage (path already includes context) const url = getThumbnailUrl('user-posts/1/12/img.jpg', 'small'); // → /storage/user-posts/1/12/thumbnails/img_thumb_small.jpg // With crop method + WebP format const url = getThumbnailUrl('user-posts/1/12/img.jpg', 'small', { method: 'crop', // crop, fit, or resize format: 'webp', // webp, avif, jpg, png quality: 85, // 1-100 smart_crop: true // AI energy detection (v2.0+) }); // → /storage/user-posts/1/12/thumbnails/img_thumb_small_crop.webp?quality=85&smart_crop=1 // Context-Aware (filename + context + data) const url = getThumbnailUrlWithContext( 'img.jpg', // Just filename 'small', // Size 'post', // Context { user_id: 1, post_id: 12 }, // Context data { method: 'crop', format: 'webp' } // Options (optional) ); // → /storage/user-posts/1/12/thumbnails/img_thumb_small_crop.webp // Build context path only const path = buildContextPath('post', { user_id: 1, post_id: 12 }); // → user-posts/1/12
✅ Full Feature Parity with PHP
JavaScript helper supports ALL PHP features:
- ✅ Context-Aware paths - Organized by user/post/album
- ✅ Resize methods -
crop,fit,resize - ✅ Modern formats -
webp,avif,jpg,png - ✅ Quality control - 1-100
- ✅ Smart Crop - AI energy detection (v2.0+)
- ✅ On-demand generation - Middleware handles 404
Example with all options:
// React Component with Smart Crop + WebP function PostMedia({ post }) { return ( <div> {post.media_files.map((media, idx) => ( <img key={idx} src={getThumbnailUrl(media.path, 'medium', { method: 'crop', format: 'webp', quality: 90, smart_crop: true // AI focuses on important areas! })} alt={media.alt} /> ))} </div> ); }
PHP Backend Setup for React
In your PHP accessor (e.g., UserPost.php):
// Return ONLY path, React will build thumbnail URL public function getMediaFilesAttribute(): array { $mediaFiles = []; foreach ($this->attachments as $attachment) { if ($attachment['type'] === 'image') { $mediaFiles[] = [ 'type' => $attachment['type'], 'path' => $attachment['path'], // e.g., 'user-posts/1/12/img.jpg' 'url' => asset('storage/' . $attachment['path']), 'alt' => $attachment['original_name'], ]; } } return $mediaFiles; }
React will:
- Call
getThumbnailUrl(media.path, 'small') - Build URL:
/storage/user-posts/1/12/thumbnails/img_thumb_small.jpg - Browser requests thumbnail
- 404 on first request → middleware generates thumbnail
- 200 on next requests → cached file served by Nginx
Workflow
┌─────────────────────────────────────────────────────────┐
│ 1. Change config/thumbnails.php │
│ (add new context, change pattern, etc.) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. Run: php artisan thumbnails:sync-js │
│ Generates: resources/js/utils/thumbnails.js │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. Commit thumbnails.js to git │
│ (single source of truth in PHP, synced to JS) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. React uses getThumbnailUrl() automatically │
│ (always in sync with PHP config) │
└─────────────────────────────────────────────────────────┘
Vue Example
<template> <div v-for="media in post.media_files" :key="media.path"> <img :src="getThumbnailUrl(media.path, 'small')" :alt="media.alt" /> </div> </template> <script setup> import { getThumbnailUrl } from '@/utils/thumbnails'; const props = defineProps({ post: Object }); </script>
📦 Benefits
- ✅ Automatic Cleanup - Delete post folder = all thumbnails gone
- ✅ Per-User Isolation - Easy permissions & backups per user
- ✅ CDN Routing - Route different contexts to different CDNs
- ✅ Performance - Fewer files per directory = faster filesystem
- ✅ Organization - Find any thumbnail instantly
- ✅ Scalability - No "one folder with million files" problem
📐 Resize Methods
Choose how thumbnails should be generated:
1. Resize (Default - Proportional)
// config/thumbnails.php 'method' => 'resize',
- ✅ Preserves aspect ratio
- ✅ No cropping
- ⚠️ Final size may differ slightly from target
Use for: Product images, photos where full content must be visible
2. Crop (Exact Size - Center Crop)
// config/thumbnails.php 'method' => 'crop',
- ✅ Exact dimensions guaranteed
- ✅ Fills entire thumbnail
- ⚠️ May cut edges (center-focused)
Use for: Avatars, thumbnails in grids, cards
3. Fit (Preserve All - Add Padding)
// config/thumbnails.php 'method' => 'fit',
- ✅ Entire image visible
- ✅ Exact dimensions
- ⚠️ May have padding/borders
Use for: Logos, icons, images where nothing can be cut
Visual comparison:
Original: 800x600 → Target: 200x200
RESIZE: 200x150 (proportional, smaller)
CROP: 200x200 (center cropped)
FIT: 200x200 (padded top/bottom)
📚 Usage Methods
1. Blade Directive
<img src="@thumbnail('photos/image.jpg', 'small')"> <img src="@thumbnail('photos/image.jpg', 'medium')"> <img src="@thumbnail('photos/image.jpg', 'large')">
2. Facade
use Moonlight\Thumbnails\Facades\Thumbnail; $url = Thumbnail::thumbnail('photos/image.jpg', 'medium');
3. Helper Functions
// Get URL $url = thumbnail('photos/image.jpg', 'small'); // Aliases $url = thumbnail_url('photos/image.jpg', 'small'); $path = thumbnail_path('photos/image.jpg', 'small'); // Returns relative path
4. Service Injection
use Moonlight\Thumbnails\Services\ThumbnailService; class ImageController { public function show(ThumbnailService $thumbnails) { $url = $thumbnails->thumbnail('photos/image.jpg', 'medium'); } }
5. JavaScript (Frontend)
import { getThumbnailUrl } from 'moonlight-thumbnails'; const thumbUrl = getThumbnailUrl('photos/cat.jpg', 'small'); // Returns: /storage/photos/thumbnails/cat_thumb_small.jpg
⚙️ Configuration
Default Sizes
// config/thumbnails.php 'sizes' => [ 'small' => ['width' => 150, 'height' => 150], 'medium' => ['width' => 300, 'height' => 300], 'large' => ['width' => 600, 'height' => 600], // Add your custom sizes: 'avatar' => ['width' => 200, 'height' => 200], 'banner' => ['width' => 1200, 'height' => 400], ],
Drivers
'driver' => 'gd', // 'gd' (default), 'imagick', or 'intervention'
GD (built-in, no extra dependencies)
'driver' => 'gd',
Imagick (better quality, requires ext-imagick)
'driver' => 'imagick',
Intervention Image (most features, requires package)
composer require intervention/image
'driver' => 'intervention',
Quality & Performance
'quality' => 85, // JPEG quality (1-100) 'fallback_on_error' => true, // Return original on error 'cache_control' => 'public, max-age=31536000', // 1 year cache
🎯 Advanced Features
HasThumbnails Trait
Automatically delete thumbnails when model is deleted:
use Moonlight\Thumbnails\Traits\HasThumbnails; class UserPost extends Model { use HasThumbnails; // Define which fields contain images protected $thumbnailFields = ['cover_image', 'gallery_image']; } // Usage in model $post->thumbnail('cover_image', 'small'); // Get thumbnail URL $post->thumbnails('cover_image'); // Get all sizes: ['small' => 'url', ...]
Artisan Commands
# Generate thumbnails for specific image php artisan thumbnails:generate photos/image.jpg # Generate specific size php artisan thumbnails:generate photos/image.jpg --size=small # Force regenerate (overwrite existing) php artisan thumbnails:generate photos/image.jpg --force # Clear all thumbnails php artisan thumbnails:clear # Clear specific directory php artisan thumbnails:clear photos # Clear specific image thumbnails php artisan thumbnails:clear photos/image.jpg
Manual Management
use Moonlight\Thumbnails\Facades\Thumbnail; // Delete all thumbnails for an image Thumbnail::deleteThumbnails('photos/image.jpg'); // Clear all thumbnails in directory Thumbnail::clearAllThumbnails('photos'); // Clear ALL thumbnails in app Thumbnail::clearAllThumbnails();
🆕 V2.0 New Features
Smart Crop (AI Energy Detection)
Automatically detect the most important part of the image for intelligent cropping:
// config/thumbnails.php 'smart_crop' => [ 'enabled' => true, 'algorithm' => 'energy', // 'energy', 'faces', 'saliency' 'rule_of_thirds' => true, // Align focal point to rule of thirds ],
Usage:
{{-- Smart crop will detect focal point automatically --}} <img src="@thumbnail('photos/portrait.jpg', 'square', 'post', ['user_id' => 1], 'smart-crop')">
When to use:
- Portrait photos (focuses on face/eyes)
- Product photos (focuses on the product)
- Landscape photos (focuses on horizon/main subject)
Modern Image Formats (AVIF/WebP)
Automatically convert thumbnails to modern formats for 50% smaller file sizes:
// config/thumbnails.php 'formats' => [ 'auto_convert' => true, 'priority' => ['avif', 'webp', 'jpg'], // Try in order 'quality' => [ 'avif' => 85, 'webp' => 90, 'jpg' => 85, ], ],
How it works:
- Package checks available image libraries (GD, Imagick, Intervention)
- Selects best available format from priority list
- Generates thumbnail in optimal format
- Falls back to JPEG if modern formats unavailable
Performance:
- AVIF: ~50% smaller than JPEG (requires Imagick)
- WebP: ~30% smaller than JPEG (GD/Imagick)
- Automatic fallback ensures compatibility
🔐 Laravel Native Signed URLs Integration
Version 2.0.18+ uses Laravel's native URL::temporarySignedRoute() for signed URLs instead of custom Facebook-style implementation.
⚙️ Required Setup (4 steps)
1️⃣ Create StorageController
Create app/Http/Controllers/StorageController.php:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; class StorageController extends Controller { /** * Serve files from storage with signed URL validation * * Automatically routes audio files to AudioStreamController for * HTTP Range Request support (seeking in audio players). */ public function serve(Request $request, string $path) { $path = urldecode($path); // Optional: Delegate audio files to AudioStreamController // if (preg_match('/\.(mp3|wav|ogg|m4a|flac)$/i', $path)) { // return app(AudioStreamController::class)->stream($request, $path); // } $disk = config('thumbnails.disk', 'public'); $fullPath = Storage::disk($disk)->path($path); if (!file_exists($fullPath)) { abort(404, 'File not found'); } // Set cache headers based on signed URLs config $cacheControl = config('thumbnails.signed_urls.enabled') ? 'private, no-cache, must-revalidate' : 'public, max-age=31536000'; return response()->file($fullPath, [ 'Cache-Control' => $cacheControl, 'Content-Type' => mime_content_type($fullPath) ?: 'application/octet-stream', ]); } }
2️⃣ Add Route with Signed Middleware
Add to routes/web.php:
use App\Http\Controllers\StorageController; Route::get('/storage/{path}', [StorageController::class, 'serve']) ->where('path', '.*') ->middleware('signed') // Laravel's native signed middleware ->name('storage.serve');
⚠️ IMPORTANT: Place this route BEFORE any catch-all routes!
3️⃣ Disable Laravel's Auto-Routes
In config/filesystems.php, set serve => false:
'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), 'url' => env('APP_URL').'/storage', 'visibility' => 'public', 'serve' => false, // ✅ Disable auto-registration ], 'local' => [ 'driver' => 'local', 'root' => storage_path('app/private'), 'serve' => false, // ✅ Also disable for local ],
4️⃣ Remove public/storage Symlink
Laravel should route ALL /storage/* requests through the controller:
# Linux/Mac rm public/storage # Windows PowerShell Remove-Item public/storage # Or manually delete the folder/symlink
Why remove it?
- Symlink causes Apache/Nginx to serve files statically (bypassing Laravel)
- Static serving = NO middleware = NO signed URL validation
- Your images would load even with expired URLs! ❌
🎯 Enable Signed URLs
In .env:
THUMBNAILS_SIGNED_URLS=true # Enable for thumbnails THUMBNAILS_SIGNED_ORIGINALS=true # Also sign original images THUMBNAILS_URL_EXPIRATION=7200 # 2 hours (or 604800 for 7 days)
Generated URLs:
{{-- Before (no signed URLs) --}} <img src="/storage/user-posts/1/14/img.jpg"> {{-- After (with signed URLs) --}} <img src="/storage/user-posts/1/14/img.jpg?expires=1767553796&signature=abc123...">
✨ How It Works
original()/thumbnail()helpers generate signed URL usingURL::temporarySignedRoute()- Browser requests the signed URL
signedmiddleware validatesexpiresandsignatureparameters- If valid:
StorageControllerserves the file - If invalid/expired: Laravel returns
403 Invalid signature
ThumbnailFallback Middleware:
- When thumbnail doesn't exist (404), middleware generates it on-demand
- Returns
302 redirectto signed URL (if enabled) - Browser makes new request with signed URL → validated by
signedmiddleware - Works perfectly with React/Vue/JavaScript!
📊 URL Expiration Times
# 5 minutes (for one-time shares) THUMBNAILS_URL_EXPIRATION=300 # 1 hour (for temporary content) THUMBNAILS_URL_EXPIRATION=3600 # 2 hours (recommended for SPA apps) THUMBNAILS_URL_EXPIRATION=7200 # 7 days (Facebook-style) THUMBNAILS_URL_EXPIRATION=604800 # 30 days (for long-term content) THUMBNAILS_URL_EXPIRATION=2592000
🔒 Security Benefits
- ✅ Prevents hotlinking - Other sites can't steal your bandwidth
- ✅ Time-limited access - Links expire after set time
- ✅ No database required - Stateless validation
- ✅ Laravel native - Uses built-in
URL::temporarySignedRoute() - ✅ Cryptographically secure - HMAC-SHA256 signatures
🐛 Troubleshooting
Images still load after expiration?
- ✅ Check if
public/storagesymlink exists (delete it!) - ✅ Verify
serve => falseinconfig/filesystems.php - ✅ Clear cache:
php artisan optimize:clear
403 Invalid signature on valid URLs?
- ✅ Check if route is named
storage.serve - ✅ Verify
signedmiddleware is applied - ✅ Ensure route is placed BEFORE catch-all routes
React/Vue images not loading?
- ✅ Backend must return
thumbnailURL (not justpath) - ✅ Example:
'thumbnail' => thumbnail($path, 'large', true), // ✅ Returns signed URL 'thumbnail_small' => thumbnail($path, 'small', true),
// config/thumbnails.php 'formats' => [ 'auto_convert' => true, 'priority' => ['avif', 'webp', 'jpg'], // Try AVIF first, fallback to WebP, then JPG 'quality' => [ 'avif' => 75, 'webp' => 80, 'jpg' => 85, ], ],
Usage with Blade directive:
{{-- Automatically generates AVIF, WebP, and JPG variants --}} @thumbnail_picture('photos/sunset.jpg', 'large', 'post', ['user_id' => 5]) {{-- Output: <picture> <source srcset="/storage/.../sunset_thumb_large.avif" type="image/avif"> <source srcset="/storage/.../sunset_thumb_large.webp" type="image/webp"> <img src="/storage/.../sunset_thumb_large.jpg" alt="..."> </picture> --}}
File size comparison:
- AVIF: ~50% smaller than JPEG (best quality per byte)
- WebP: ~30% smaller than JPEG
- JPG: Original compression
Variants System (Generate Multiple Sizes)
Generate multiple thumbnail sizes at once with preset collections:
// config/thumbnails.php 'variants' => [ 'avatar' => [ ['width' => 50, 'height' => 50, 'method' => 'crop'], ['width' => 150, 'height' => 150, 'method' => 'crop'], ['width' => 300, 'height' => 300, 'method' => 'crop'], ], 'gallery' => [ ['width' => 300, 'height' => 200, 'method' => 'crop'], ['width' => 800, 'height' => 600, 'method' => 'resize'], ['width' => 1200, 'height' => 800, 'method' => 'resize'], ], ],
Usage:
// Generate all avatar sizes at once $variants = thumbnail_variant($user, 'avatar.jpg', 'avatar'); // Returns: ['50x50' => 'url', '150x150' => 'url', '300x300' => 'url'] // In Blade @foreach(thumbnail_variant($user, 'photo.jpg', 'gallery') as $size => $url) <img src="{{ $url }}" alt="Gallery {{ $size }}"> @endforeach
When to use:
- User avatars (small, medium, large)
- Gallery thumbnails (grid, lightbox, full-screen)
- Responsive images (different screen sizes)
Subdirectory Strategies (Performance at Scale)
Choose how thumbnails are organized on the filesystem:
// config/thumbnails.php 'subdirectory' => [ 'auto_strategy' => true, // Automatically select best strategy 'default' => 'hash-prefix', 'strategies' => [ 'context-aware' => [ 'priority' => 100, // Highest - used for models // Result: user-posts/1/12/thumbnails/image_thumb_small.jpg ], 'hash-prefix' => [ 'priority' => 1, // Lowest - fallback for string paths 'config' => [ 'levels' => 2, // e.g., a/b/ 'length' => 2, ], // Result: thumbnails/a/b/image_thumb_small.jpg ], 'date-based' => [ 'config' => [ 'format' => 'Y/m/d', // e.g., 2026/01/03/ ], // Result: thumbnails/2026/01/03/image_thumb_small.jpg ], ], ],
Performance Benefits:
| Files | Without Subdirs | With Hash Prefix |
|---|---|---|
| 1,000 | ⚠️ Slow | ✅ Fast |
| 10,000 | ❌ Very Slow | ✅ Fast |
| 100,000 | ❌ Unusable | ✅ Fast |
| 1,000,000 | ❌ Impossible | ✅ Fast |
Why: Operating systems slow down with >1000 files per directory.
Security Validation
Protect against malicious file uploads:
// config/thumbnails.php 'security' => [ 'max_file_size' => 10, // MB 'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif'], 'allowed_mime_types' => [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', ], 'max_dimensions' => [ 'width' => 10000, 'height' => 10000, ], 'block_svg' => true, // Prevent XXE attacks ],
Automatic validation: Package validates all images before processing.
Error Handling Modes
Control how the package behaves when errors occur:
// config/thumbnails.php 'error_mode' => 'silent', // 'silent', 'strict', 'fallback' 'placeholder_image' => 'images/placeholder.jpg', // For 'fallback' mode
Modes:
- silent (recommended for production): Log error, return original image
- strict (recommended for development): Throw exception
- fallback: Return placeholder image
Example:
{{-- If thumbnail generation fails, original image is returned (silent mode) --}} <img src="@thumbnail('photos/corrupted.jpg', 'small')"> {{-- Instead of crashing, shows original image --}}
Daily Usage Statistics (Privacy-Friendly)
Track thumbnail usage for analytics (commercial license holders only):
// config/thumbnails.php 'statistics' => [ 'enabled' => true, 'send_to_moonlight' => true, // Send to Moonlight dashboard ],
What's tracked:
- ✅ Daily usage count (how many thumbnails generated today)
- ✅ Methods used (resize, crop, fit)
- ✅ Popular sizes (which sizes are most used)
- ✅ PHP/Laravel versions
- ✅ Domain (where package is installed)
What's NOT tracked:
- ❌ Individual images (no filenames)
- ❌ User data (no emails, IPs, personal info)
- ❌ Image content (we never see your images)
View statistics: Commercial license holders can view stats at https://howtodraw.pl/developer/licenses
🏗️ How It Works
Architecture
User Request → /storage/photos/thumbnails/image_thumb_small.jpg
↓
[Nginx tries to serve]
↓ (404/403 - file doesn't exist)
[ThumbnailFallback Middleware]
↓
Parses URL:
- Original: photos/image.jpg
- Size: small
↓
ThumbnailService::thumbnail()
↓
Generates thumbnail (150x150)
Saves to: photos/thumbnails/image_thumb_small.jpg
↓
Returns thumbnail (200 OK)
Header: X-Thumbnail-Generated: on-demand
↓
[Next request → Nginx serves cached file directly]
File Structure
Before first request:
storage/app/public/photos/
└── cat.jpg (original, 2.5 MB)
After thumbnail request:
storage/app/public/photos/
├── cat.jpg (original, 2.5 MB)
└── thumbnails/
├── cat_thumb_small.jpg (150x150, 15 KB)
├── cat_thumb_medium.jpg (300x300, 45 KB)
└── cat_thumb_large.jpg (600x600, 120 KB)
💼 Licensing
Choose Your License
| License | Price | Best For | Limits |
|---|---|---|---|
| Personal | FREE | Hobby projects, open-source | Non-commercial only |
| Small Business | $500/year | Startups, freelancers | 1-10 devs, <$500k revenue |
| Medium Business | $1,500/year | Growing companies | 11-50 devs, $500k-$10M revenue |
| Enterprise | $15,000/year | Large corporations | 50+ devs, unlimited |
Full details: LICENSE.md
Contact for commercial licensing: kontakt@howtodraw.pl
Why Commercial License?
- 🛠️ Ongoing Development - New features, bug fixes, updates
- 💬 Priority Support - Fast response times
- 📖 Comprehensive Docs - Tutorials, examples, best practices
- 🔒 Security Updates - Critical patches within 24h
- 💼 Business Continuity - SLA for Enterprise customers
🆚 Comparison
| Feature | This Package | Traditional Solutions |
|---|---|---|
| Generation | On-demand (lazy) | Pre-generate all sizes |
| Performance | Fast (only needed) | Slow (generates unused) |
| Storage | Efficient | Wastes space |
| Setup | Zero config | Complex setup |
| Cache | Filesystem | Often needs Redis |
| Dependencies | ext-gd (built-in) | Various |
📖 Examples
Gallery with Thumbnails
@foreach($images as $image) <a href="{{ asset('storage/' . $image->path) }}"> <img src="@thumbnail($image->path, 'small')" alt="{{ $image->title }}" loading="lazy"> </a> @endforeach
Responsive Images
<img src="@thumbnail('photos/image.jpg', 'small')" srcset=" @thumbnail('photos/image.jpg', 'small') 150w, @thumbnail('photos/image.jpg', 'medium') 300w, @thumbnail('photos/image.jpg', 'large') 600w " sizes="(max-width: 600px) 150px, (max-width: 1200px) 300px, 600px" alt="Responsive image">
React Component
import { getThumbnailUrl } from 'moonlight-thumbnails'; function ImageGallery({ images }) { return ( <div className="grid grid-cols-3 gap-4"> {images.map(img => ( <img key={img.id} src={getThumbnailUrl(img.path, 'medium')} alt={img.title} loading="lazy" /> ))} </div> ); }
🤝 Contributing
This is a commercial package. We welcome:
- 🐛 Bug reports (GitHub Issues)
- 💡 Feature suggestions (GitHub Issues)
- 📖 Documentation improvements (PRs welcome)
Contact: kontakt@howtodraw.pl
📄 License
Commercial License with free personal tier.
See LICENSE.md for full terms.
💝 Credits
Inspired by:
Built with ❤️ by Moonlight Poland Team
📞 Support
GitHub Issues: https://github.com/Moonlight4000/laravel-thumbnails/issues
Email: kontakt@howtodraw.pl
⭐ If this package helped you, please star it on GitHub!