Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

How to Generate a Thumbnail Image in Laravel 13 with Intervention Image

๐Ÿ› ๏ธ Laravel Tutorials5 min read

How to Generate a Thumbnail Image in Laravel 13 with Intervention Image

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
How to Generate a Thumbnail Image in Laravel 13 with Intervention Image

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-laravel integration package.
  • The GD extension (default driver) or Imagick enabled in PHP.
  • The public disk linked with php artisan storage:link so files in storage/app/public are reachable from the browser.

Step 1: Install Intervention Image for Laravel

Code
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.

JavaScript
<?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.

JavaScript
<?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.

JavaScript
<?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() and coverDown() 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

PHP
@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>
@endforeach

Setting 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 because public/storage is missing.
  • Running out of memory. Decoding a 6000 x 4000 photo needs far more memory than the file size suggests. Limit uploads with max and dimensions:max_width, or raise memory_limit for workers only.
  • Using old method names. Code written for older Intervention versions uses make() or read() and fit(). Version 4 uses decode() and cover(). Check the version in composer.lock before copying examples.
  • Keeping user filenames. Client filenames can collide or contain odd characters. Generate your own.

How do you verify it works?

  1. Upload a large landscape photo and check that storage/app/public/photos/thumbs contains a .webp file.
  2. Open the thumbnail URL and confirm it is 400 x 400 pixels.
  3. Compare file sizes: the thumbnail should be a small fraction of the original.
  4. Add a feature test with Storage::fake('public') and UploadedFile::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.

References

Article Index & Topic Tags
LaravelImageUploadIntervention-Image

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.