Migrating TinyMCE from v5.9 to v8.3 in a Laravel 8 → 11 Upgrade
Introduction
When migrating a Laravel blog application from Laravel 8 to Laravel 11, of the most challenging frontend tasks is upgrading TinyMCE — the rich text editor used in the admin panel. This isn't a simple version bump. You're dealing with two simultaneous breaking changes:
- Build system: webpack.mix.js (Laravel Mix) → Vite 5 (Laravel's new default)
- Editor: TinyMCE 5.9.2 → TinyMCE 8.3.1 (three major versions, with significant plugin removals and API changes)
This article walks through a real-world migration, with actual code from a production Laravel blog, covering every decision point and the reasoning behind the chosen approach.
1. The Starting Point: Laravel 8 + Mix + TinyMCE 5.9.2
Build System: webpack.mix.js
In the Laravel 8 setup, TinyMCE was installed as an npm dependency and bundled through Laravel Mix (a webpack wrapper):
// webpack.mix.js
const mix = require('laravel-mix');
const theme = process.env.BLOG_THEME;
mix.js('resources/js/app.js', 'public/js')
.js('resources/js/admin.js', 'public/js')
.js('resources/js/close.js', 'public/js')
.sass(`resources/sass/admin/admin.scss`, 'public/css')
.sass(`resources/sass/${theme}/app.scss`, 'public/css');
if (mix.inProduction()){
mix.sass(`resources/sass/fontawesome.scss`, 'public/css');
}
The admin.js entry point pulled in TinyMCE along with other admin scripts:
// resources/js/admin.js (Laravel 8)
import bsCustomFileInput from 'bs-custom-file-input';
bsCustomFileInput.init();
require('./scripts/tinymce');
require('./scripts/sidebar');
require('./scripts/proper-url');
require('./scripts/hidden-form');
require('./scripts/post-date');
TinyMCE 5 Configuration
The editor configuration lived in a dedicated script file:
// resources/js/scripts/tinymce.js (Laravel 8 / TinyMCE 5.9.2)
import tinymce from 'tinymce/tinymce.min.js';
tinymce.baseURL = '/js/tinymce';
const editorConfig = {
path_absolute: '/auth/',
selector: 'textarea#content',
plugins: [
"advlist autolink lists link image charmap print preview hr anchor pagebreak",
"searchreplace wordcount visualblocks visualchars code fullscreen",
"insertdatetime media nonbreaking save table directionality",
"emoticons template paste textpattern"
],
toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter " +
"alignright alignjustify | bullist numlist outdent indent | link image media",
fullscreen_native: true,
visualblocks_default_state: false,
end_container_on_empty_block: true,
block_unsupported_drop: true,
relative_urls: false,
height: "250",
templates: [
{
title: "HTML",
description: "A HTML HLJS Code Block",
content: "<h4>HTML</h4><pre><code class='html hljs xml'>HTML</code></pre>",
},
{
title: "CSS",
description: "A CSS HLJS Code Block",
content: "<h4>CSS</h4><pre><code class='css hljs'>.css { value: 1px; }</code></pre>",
},
{
title: "JS",
description: "A JS HLJS Code Block",
content: "<h4>Javascript</h4><pre><code class='javascript hljs'>" +
"function test() { console.log('test'); }</code></pre>",
},
{
title: "PHP",
description: "A PHP HLJS Code Block",
content: "<h4>PHP</h4><pre><code class='php hljs'>a = 1;</code></pre>",
},
],
file_picker_callback: function(callback, value, meta){
var x = window.innerWidth || document.documentElement.clientWidth ||
document.getElementsByTagName('body')[0].clientWidth;
var y = window.innerHeight || document.documentElement.clientHeight ||
document.getElementsByTagName('body')[0].clientHeight;
var cmsURL = editorConfig.path_absolute +
'laravel-filemanager?editor=' + meta.fieldname;
if (meta.filetype == 'image') {
cmsURL = cmsURL + "&type=Images";
} else {
cmsURL = cmsURL + "&type=Files";
}
tinymce.activeEditor.windowManager.openUrl({
url: cmsURL,
title: 'Filemanager',
width: x * 0.8,
height: y * 0.8,
resizable: "yes",
close_previous: "no",
(api, message) => {
callback(message.content);
}
});
},
style_formats: [
{ title: 'Headers', items: [
{ title: 'h1', block: 'h1' },
{ title: 'h3', block: 'h3' },
{ title: 'h4', block: 'h4' },
{ title: 'h5', block: 'h5' },
{ title: 'h6', block: 'h6' }
]},
{ title: 'Blocks', items: [
{ title: 'p', block: 'p' },
{ title: 'div', block: 'div' },
{ title: 'pre', block: 'pre' }
]},
{ title: 'Containers', items: [
{ title: 'section', block: 'section', wrapper: true, merge_siblings: false },
{ title: 'blockquote', block: 'blockquote', wrapper: true },
{ title: 'aside', block: 'aside', wrapper: true },
{ title: 'figure', block: 'figure', wrapper: true }
]}
]
};
tinymce.init(editorConfig);
Asset Deployment (TinyMCE 5)
A critical detail: TinyMCE 5's JS was imported and bundled via webpack, but the editor's runtime assets (skins, icons, themes, plugins) had to be manually placed in public/js/tinymce/. The directory looked like this:
public/js/tinymce/
├── icons/
│ └── default/
├── plugins/
│ ├── advlist/
│ ├── autolink/
│ ├── bbcode/
│ ├── charmap/
│ ├── code/
│ ├── colorpicker/
│ ├── contextmenu/
│ ├── paste/
│ ├── print/
│ ├── template/
│ ├── textcolor/
│ ├── textpattern/
│ ... (40+ plugin directories)
├── skins/
│ ├── content/
│ └── ui/
└── themes/
├── mobile/
└── silver/
The line tinymce.baseURL = '/js/tinymce' told TinyMCE where to find these assets at runtime.
2. Why You Can't Just Upgrade In-Place
Three factors make an incremental upgrade impractical:
Build System Mismatch
Laravel 11 uses Vite 5 by default. Vite uses native ES modules, while Laravel Mix used CommonJS via webpack. This means:
require()calls don't work — you needimportstatements- Vite's output goes to
public/build/instead ofpublic/js/andpublic/css/ - Asset references use
@vite()Blade directive instead ofmix()helper
TinyMCE's Module Loading Problem
TinyMCE's architecture relies on dynamically loading plugins, skins, and icons relative to a base URL. When you try to bundle TinyMCE's core through Vite's ES module system, these dynamic loads break — Vite tree-shakes and code-splits in ways that conflict with TinyMCE's internal module resolver.
Massive Plugin API Changes
TinyMCE 8 dropped or merged many v5 plugins. Using old plugin names causes initialization failures:
| TinyMCE 5 Plugin | TinyMCE 8 Status |
|---|---|
print |
Removed (use browser print) |
hr |
Removed (merged into core) |
paste |
Removed (merged into core) |
template |
Removed |
textpattern |
Removed (merged into core) |
textcolor |
Removed (merged into core) |
colorpicker |
Removed (merged into core) |
contextmenu |
Removed (merged into core) |
imagetools |
Removed |
noneditable |
Removed (merged into core) |
toc |
Removed |
legacyoutput |
Removed |
tabfocus |
Removed (merged into core) |
fullpage |
Removed |
bbcode |
Removed |
spellchecker |
Removed (use browser spellcheck) |
advlist |
Still available |
autolink |
Still available |
code |
Still available |
codesample |
Still available (enhanced with Prism.js) |
image |
Still available |
link |
Still available |
lists |
Still available |
table |
Still available |