To generate a thumbnail image in Laravel 13, install intervention/image-laravel, decode the uploaded file with the Image facade, call cover() or scale() to shrink it, then encode and save the result with the Storage facade. The original file and the thumbnail are stored side by side, so pages can load the small version while a detail view uses the full one.
This guide uses a small photo upload feature as the example. Each step keeps the code short so you can copy the pattern into an existing controller.
What do you need before you start?
- Laravel 13 (PHP 8.3 or newer, which Laravel 13 requires).
- Intervention Image 4 through the
intervention/image-laravelintegration package. - The GD extension (default driver) or Imagick enabled in PHP.
- The public disk linked with
php artisan storage:linkso files instorage/app/publicare reachable from the browser.
Step 1: Install Intervention Image for Laravel
composer require intervention/image-laravel
php artisan vendor:publish --provider="Intervention\Image\Laravel\ServiceProvider"The second command publishes config/intervention-image.php. GD is the default driver. If your server has Imagick, set IMAGE_DRIVER in .env to the Imagick driver class. Leave the other options alone for now; autoOrientation is already on, which rotates phone photos correctly based on their EXIF data.
Step 2: Add a column for the thumbnail path
Store both paths on the model so you never have to guess the thumbnail filename later.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('photos', function (Blueprint $table) {
$table->id();
$table->string('path');
$table->string('thumbnail_path');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('photos');
}
};Run php artisan migrate, then create a Photo model with path and thumbnail_path in its fillable list.
Step 3: Validate the upload
Never resize a file you have not validated. The image rule checks the file type, max limits the size in kilobytes and dimensions rejects tiny images that would look blurry when scaled.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePhotoRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'photo' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120', 'dimensions:min_width=300,min_height=300'],
];
}
}Step 4: Create the thumbnail in the controller
The controller stores the original, then decodes the same upload, crops it to a square and writes a WebP thumbnail.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StorePhotoRequest;
use App\Models\Photo;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Format;
use Intervention\Image\Laravel\Facades\Image;
class PhotoController extends Controller
{
public function store(StorePhotoRequest $request): RedirectResponse
{
$upload = $request->file('photo');
$name = Str::uuid()->toString();
$originalPath = $upload->storeAs('photos', $name . '.' . $upload->extension(), 'public');
$thumbnail = Image::decode($upload)
->cover(400, 400)
->encodeUsingFormat(Format::WEBP, quality: 75);
$thumbnailPath = 'photos/thumbs/' . $name . '.webp';
Storage::disk('public')->put($thumbnailPath, (string) $thumbnail);
Photo::create([
'path' => $originalPath,
'thumbnail_path' => $thumbnailPath,
]);
return back()->with('status', 'Photo uploaded.');
}
}A few details matter here. The filename comes from a UUID, not from the user, so nobody can overwrite another file. The thumbnail is encoded to WebP, which is usually much smaller than a JPEG at similar quality. Casting the encoded image to a string gives Storage::put() the raw bytes.
Should you use scale() or cover()?
scale(width: 400)keeps the aspect ratio. A landscape photo becomes 400 pixels wide and shorter than 400 pixels tall. Use it for galleries with flexible heights.cover(400, 400)resizes and crops to the exact size, keeping the centre by default. Use it for avatars and grid cards that must line up.scaleDown()andcoverDown()do the same but never enlarge an image that is already smaller than the target.resize(400, 400)forces both sizes and can stretch the image. It is rarely what you want for photos.
Step 5: Show the thumbnail in Blade
@foreach ($photos as $photo)
<a href="{{ Storage::url($photo->path) }}">
<img src="{{ Storage::url($photo->thumbnail_path) }}" width="200" height="200" loading="lazy" alt="Photo {{ $photo->id }}">
</a>
@endforeachSetting width and height lets the browser reserve space before the image loads, and loading="lazy" defers images below the fold.
Should thumbnails be generated in a queue?
For one small upload, resizing inside the request is fine. If users upload large files or you create several sizes per image, move the work into a queued job. Store the original first, dispatch a job with the photo id, and let the job write the thumbnails. The user gets a fast response and the worker does the heavy lifting.
Common mistakes
- Forgetting
storage:link. The files exist, but the browser gets a 404 becausepublic/storageis missing. - Running out of memory. Decoding a 6000 x 4000 photo needs far more memory than the file size suggests. Limit uploads with
maxanddimensions:max_width, or raisememory_limitfor workers only. - Using old method names. Code written for older Intervention versions uses
make()orread()andfit(). Version 4 usesdecode()andcover(). Check the version incomposer.lockbefore copying examples. - Keeping user filenames. Client filenames can collide or contain odd characters. Generate your own.
How do you verify it works?
- Upload a large landscape photo and check that
storage/app/public/photos/thumbscontains a.webpfile. - Open the thumbnail URL and confirm it is 400 x 400 pixels.
- Compare file sizes: the thumbnail should be a small fraction of the original.
- Add a feature test with
Storage::fake('public')andUploadedFile::fake()->image('photo.jpg', 1200, 800), post it, and assert both paths exist on the fake disk.
FAQ
Does Laravel have a built-in image resizer?
No. Laravel handles uploads and storage, but resizing needs a library such as Intervention Image.
Can I store thumbnails on S3?
Yes. Replace Storage::disk('public') with your S3 disk. The encoding code stays the same.
Which format is best for thumbnails?
WebP is a good default for modern browsers. Use JPEG if you must support very old clients.



