DEV Community

Cover image for Laravel 13 Summernote Image Upload Tutorial
Muhammad Yasir Amin
Muhammad Yasir Amin

Posted on Originally published at yasiramin.dev

Laravel 13 Summernote Image Upload Tutorial

To upload images from the Summernote editor in Laravel 13, run php artisan storage:link, create a route and controller that validates the file and stores it on the public disk, return the image URL as JSON, and wire Summernote's onImageUpload callback to post the file to that route and call insertImage with the returned URL. Without this, Summernote embeds images as huge base64 strings in your post body.

This tutorial uses Laravel 13 (PHP 8.3 or newer) and assumes Summernote is already loaded on a Blade page with jQuery. Check Summernote's own site for the current download or CDN links and versions.

Why not keep Summernote's default base64 images?

  • The database column grows quickly, since each image is stored as text inside the post.
  • Pages get heavy and slow to render.
  • You cannot resize, cache or serve images from a CDN.

Uploading real files fixes all three.

Step 1: How do I make uploads publicly reachable?

php artisan storage:link
Enter fullscreen mode Exit fullscreen mode

This creates a symbolic link from public/storage to storage/app/public, so files stored on the public disk are reachable by URL.

Step 2: How do I create the upload route?

use App\Http\Controllers\EditorImageController;

Route::post('/editor/images', [EditorImageController::class, 'store'])
    ->middleware('auth')
    ->name('editor.images.store');
Enter fullscreen mode Exit fullscreen mode

Keep the route behind authentication so strangers cannot fill your disk.

Step 3: What does the controller look like?

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class EditorImageController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        $request->validate([
            'image' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
        ]);

        $path = $request->file('image')->store('editor', 'public');

        return response()->json(['url' => Storage::disk('public')->url($path)]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The image rule checks that the file is an image, mimes restricts the extensions, and max:2048 limits the size to 2048 kilobytes. Laravel names the stored file with a random hash, which avoids name collisions. I ran php -l on this controller to confirm its syntax. I did not run a real upload for this article.

I left out SVG on purpose. SVG files can contain scripts, so only allow them if you sanitise them.

Step 4: How do I connect Summernote to the route?

Add the CSRF token as a meta tag in your layout, then configure the callback:

<meta name="csrf-token" content="{{ csrf_token() }}">

<textarea id="body" name="body"></textarea>

<script>
$(function () {
    $('#body').summernote({
        height: 320,
        callbacks: {
            onImageUpload: function (files) {
                Array.from(files).forEach(uploadImage);
            }
        }
    });

    function uploadImage(file) {
        const data = new FormData();
        data.append('image', file);

        fetch("{{ route('editor.images.store') }}", {
            method: 'POST',
            headers: {
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
                'Accept': 'application/json'
            },
            body: data
        })
        .then(function (response) {
            if (!response.ok) { throw new Error('Upload failed'); }
            return response.json();
        })
        .then(function (json) {
            $('#body').summernote('insertImage', json.url);
        })
        .catch(function () {
            alert('Image upload failed. Check the file type and size.');
        });
    }
});
</script>
Enter fullscreen mode Exit fullscreen mode

The Accept: application/json header makes Laravel return validation errors as JSON, not as a redirect to a form page. Summernote's onImageUpload callback replaces its default base64 behaviour, and insertImage places the image at the cursor.

Step 5: How do I save and show the post safely?

The editor produces HTML. When you display it with {!! $post->body !!}, you output raw HTML, so a malicious user could store a script. Sanitise the HTML on save with a library such as HTML Purifier, or only allow trusted authors to write posts. Never output untrusted HTML unescaped.

How do I check that it works?

  1. Open the editor page and click the picture icon.
  2. Choose a small PNG. The image should appear in the editor.
  3. In browser developer tools, confirm the image src starts with your site URL and /storage/editor/, not data:image.
  4. Check that the file exists in storage/app/public/editor.
  5. Try a 5 MB file. The upload should fail with the alert.

What are the common mistakes?

  • 419 error. The CSRF token is missing from the request headers.
  • 404 on the image URL. You forgot php artisan storage:link, or the APP_URL does not match the site address.
  • 422 error. The file failed validation, for example it is too large.
  • PHP limits. upload_max_filesize and post_max_size in php.ini must be at least as large as your Laravel limit.
  • Public disk on a multi-server setup. Local files are not shared between servers. Use a shared disk such as S3 when you scale out.
  • Orphaned files. Images uploaded and then removed from the post remain on disk. Clean them up with a scheduled job if that matters.

FAQ

Does this work with Laravel 12?

Yes. The file storage and validation APIs used here are the same.

Can I store the images on S3?

Yes. Configure an S3 disk, then use that disk name in store('editor', 's3') and Storage::disk('s3').

Is Summernote still a good choice?

It works and is simple, but it depends on jQuery. For a new project, compare it with other editors based on your front-end stack.

References


Originally published on yasiramin.dev.

Top comments (0)