Skip to main content

Upgrading my site from CKEditor 4 to CKEditor 5

Published on

My site runs on Drupal 9.5. I started preparing to upgrade to Drupal 10 right after 10.0.0 was released, but then I got hit with CKEditor 4 to CKEditor 5 blockers. The Linkit, CodeSnippet, and Entity Embed modules supported Drupal 10 but didn't support CKEditor 5. I could have updated to Drupal 10 and leveraged the CKEditor 4 contributed module, but I wanted to wait.

I will say, though. As we approach Drupal 10.1.0, more users will find a much easier upgrade path to Drupal 10 and CKEditor 5.

Linkit module is Drupal 10  / CKEditor 5 ready!

First, the Linkit module is ready! On 5 March 2023, 6.0.0-beta4 was released. I missed this and found the 6.1.0-rc1 release from 9 April 2023. I had to fix my composer.json requirement and then run composer update drupal/linkit.

diff --git a/composer.json b/composer.json
index d35fb20c..051b9037 100644
--- a/composer.json
+++ b/composer.json

-        "drupal/linkit": "^[email protected]",
+        "drupal/linkit": "^[email protected]",

CodeSnippet – now in Drupal core 10.1.0!

Well, it turns out CodeSnippet is coming to Drupal core! Drupal core has supported CKEditor 5 code blocks since 9.3.x and a migration path from the CKEditor CodeTag module. But, the code block configuration was not exposed. Drupal 10.1.0 will now provide this functionality out of the box, mostly deprecating the CodeSnippet module! I say "mostly" because no code-highlighting libraries are provided (like Highlight.js or Prism.) There is also an issue currently RTBC'd, and I am sure to land, which provides the migration path from CodeSnippet to the CKEditor 5 code blocks plugin:

There was just one problem. I don't want to wait for 10.1.0 to be released in June. I want to ensure everything works fine now without upgrading Drupal to 10. Luckily, both of the patches for CKEditor 5 apply to 9.5! I could add the patches to my composer.json and have the code blocks with CodeSnippet migration.

diff --git a/composer.json b/composer.json
index d35fb20c..051b9037 100644
--- a/composer.json
+++ b/composer.json

     "extra": {
+        "composer-exit-on-patch-failure": true,
+        "patchLevel": {
+            "drupal/core": "-p2"
+        },
         "patches": {
+            "drupal/core": {
+                "CKE5 configure code block": "",
+                "CKE5 codesnippet": ""
+            }
         "drupal-scaffold": {
             "locations": {

The only downside is that CodeSnippet provided Highlight.js integration out of the box. Two contributed modules are available that Wim Leers tested to provide code highlighting: highlight.php and highlight.js Input Filter. However, neither has security coverage. One module only supports PHP highlighting, while the latter supports all. The latter plugin loads all of Highlight.js and not only the required languages. Neither work exactly how I'd like. 

I will write a filter plugin that checks for any code[class^="language-"] instances to attach the proper libraries. I'll share that once done!

Entity Embed, a time before Media Library

My site is old. The oldest timestamp I can find is 1305360870. That is May 14, 2011. I'm unsure if that's correct because this site has been migrated from WordPress to Drupal 7 and then to Drupal 8. Either way, I've collected some technical debt along the way. When I migrated from Drupal 7 to Drupal 8, I created a filter plugin to keep Drupal 7 media embed tokens working. I then started to use Entity Embed for placing media in my content. Then the Media Library module was released, and I stopped using Entity Embed. I never migrated anything because it just worked.

However, CKEditor 5 finally forced me to address this technical debt. Entity Embed isn't Drupal 10 ready. It almost is (see #3309747 and #3272732.) Since I don't use it, I didn't want it to be a blocker. So I finally wrote a migration to update 60 blog posts away from Entity Embed.

Luckily Entity Embed and Media work nearly the same.

// entity_embed
<drupal-entity data-entity-uuid=" data-entity-type="" data-embed-button="" data-entity-embed-display="">

// media
<drupal-media data-entity-uuid=" data-entity-type="" data-view-mode="">

That's an easy upgrade path! 

  • Replace drupal-entity with drupal-media
  • Drop data-embed-button
  • Replace data-entity-embed-display with data-view-mode

Here is the update hook I wrote to migrate the blog posts using Entity Embed in batches of 5 at a time.

 * @param array{'#finished': bool, ids: ?array<int, int[]>} $sandbox
function mglaman_dev_update_9003(array &$sandbox): void {
  $storage = \Drupal::entityTypeManager()->getStorage('node');
  if (!isset($sandbox['ids'])) {
    $query = $storage->getQuery()->accessCheck(FALSE)->condition('body.value', '%drupal-entity%', 'LIKE');
    $ids = $query->execute();
    $sandbox['ids'] = array_chunk($ids, 5);
  $batch = array_shift($sandbox['ids']);
  if (is_array($batch)) {
    /** @var \Drupal\node\NodeInterface[] $nodes */
    $nodes = $storage->loadMultiple($batch);
    foreach ($nodes as $node) {
      $body_field = $node->get('body')->first();
      assert($body_field instanceof FieldItemInterface);
      // @phpstan-ignore-next-line
      $text = $body_field->value;
      $search = [
      $replace = [
      $text = str_replace($search, $replace, $text);
      // @phpstan-ignore-next-line
      $body_field->value = $text;

  $sandbox['#finished'] = count($sandbox['ids']) === 0;

Then I could uninstall Entity Embed and the Embed module from my site!

Lost functionality with Editor Advanced Link

I am using the Editor Advanced link module to allow setting custom data attributes on links within CKEditor. That approach is now broken. I don't think it'd be possible for that module to provide a fix. So I will write a custom CKEditor 5 plugin to add my custom link data attributes.

Edit: In the comments, Wim Leers has pointed to some documentation that may make this easier than I thought:…

What else am I looking forward to with CKEditor 5?

Auto formatting was just committed for Drupal 10.1.0's release! This allows typing ` and automatically getting code formatting. It's the Markdown-in-WYSIWYG formatting in Confluence, Notion, Slack, and other editors.