laravel-context-aware-thumbnails maintained by moonlight-poland
🖼️ Laravel Context-Aware Thumbnails™
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!