Copied!
Laravel

TinyMCE Integration with Image Upload & Delete in Laravel

tinymce-integration
Shahroz Javed
Jul 27, 2025 . 32 views

Table Of Contents

 

1. Introduction

In this guide, we’ll integrate TinyMCE with Laravel to upload images and automatically delete them from the server when removed from the editor content. We’ll use Tiny Cloud’s CDN (with your own API key) and a small diffing mechanism to detect removed images.

2. Prerequisites

- Laravel app up & running
- CSRF token available in your Blade views
- Writable directory: public/uploads/tinymce
- Tiny Cloud API key and domain configured

⚠️ You must register, get your API key, and whitelist your domain at Tiny Cloud Domains. Do **not** ship your private key publicly.

3. Include TinyMCE via CDN (with your API key)

Replace YOUR_API_KEY with your own, and make sure your domain is whitelisted on Tiny Cloud.

<textarea name="tinymce_post" id="tinymce_post" cols="30" rows="10"></textarea>

<script src="https://cdn.tiny.cloud/1/YOUR_API_KEY/tinymce/8/tinymce.min.js"
        referrerpolicy="origin"
        crossorigin="anonymous"></script>

4. TinyMCE Init: Upload + Auto Delete

We’ll use the Promise-based images_upload_handler (the version you confirmed works), and a diff on content to delete removed images.

<script>
tinymce.init({
  selector: '#tinymce_post',
  plugins: 'image link media code lists',
  toolbar: 'undo redo | styles | bold italic | alignleft aligncenter alignright | bullist numlist | link image media | code',
  automatic_uploads: true,
  file_picker_types: 'image',

  images_upload_handler: function (blobInfo) {
    return new Promise(function (resolve, reject) {
      var xhr = new XMLHttpRequest();
      xhr.open('POST', "{{ route('tinymce-upload-image') }}");
      xhr.setRequestHeader('X-CSRF-TOKEN', '{{ csrf_token() }}');

      xhr.onload = function () {
        if (xhr.status !== 200) {
          reject('HTTP Error: ' + xhr.status);
          return;
        }
        var json;
        try {
          json = JSON.parse(xhr.responseText);
        } catch (e) {
          reject('Invalid JSON: ' + xhr.responseText);
          return;
        }
        if (!json || typeof json.location !== 'string') {
          reject('Invalid response');
          return;
        }
        resolve(json.location);
      };

      xhr.onerror = function () {
        reject('Image upload failed due to an XHR Transport error.');
      };

      var formData = new FormData();
      formData.append('file', blobInfo.blob(), blobInfo.filename());
      xhr.send(formData);
    });
  },

  setup: function (editor) {
    editor.on('init', function () {
      editor._currentImages = [];
    });

    const diffAndDelete = function () {
      const content = editor.getContent();
      const holder = document.createElement('div');
      holder.innerHTML = content;
      const imagesInEditor = Array.from(holder.querySelectorAll('img'))
        .map(img => img.getAttribute('src'));

      const prev = editor._currentImages || [];
      const removed = prev.filter(src => imagesInEditor.indexOf(src) === -1);
      editor._currentImages = imagesInEditor;

      removed.forEach(function (src) {
        fetch('{{ route('tinymce-delete-image') }}', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': '{{ csrf_token() }}'
          },
          body: JSON.stringify({ src })
        }).catch(console.error);
      });
    };

    editor.on('change keyup NodeChange', diffAndDelete);
  }
});
</script>
💡 Related: See our CKEditor version of the same upload + auto-delete flow.

5. Routes

Add upload and delete endpoints in routes/web.php.

Route::post('tinymce-upload-image', [PostController::class, 'tinymceUpload'])
    ->name('tinymce-upload-image');

Route::post('tinymce-delete-image', [PostController::class, 'tinymceDelete'])
    ->name('tinymce-delete-image');

6. Controller: Upload & Delete

Return JSON with a location key for TinyMCE to insert the image.

use Illuminate\Http\Request;
use Illuminate\Support\Str;

public function tinymceUpload(Request $request)
{
    if (!$request->hasFile('file')) {
        return response()->json(['error' => 'No file uploaded'], 422);
    }

    $file = $request->file('file');
    $name = Str::slug(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME))
        . '-' . time() . '.' . $file->getClientOriginalExtension();

    $file->move(public_path('uploads/tinymce'), $name);

    return response()->json([
        'location' => asset('uploads/tinymce/' . $name)
    ]);
}

public function tinymceDelete(Request $request)
{
    $src = $request->input('src');
    $relative = str_replace(env('ASSET_URL'), '', $src);
    $path = public_path($relative);

    if (file_exists($path)) {
        @unlink($path);
    }

    return response()->json(['status' => 'ok']);
}

7. Conclusion

That’s it! You now have a fully working TinyMCE integration with Laravel that uploads images, returns a proper JSON URL, and automatically deletes unused images from the server when they’re removed from the editor content. Remember to register, use your own API key, and whitelist your domain on Tiny Cloud for production.

13 Shares

Similar Posts