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.