
Converting from Matrix fields to CKEditor in Craft 5
As part of Craft's entrification plan, during the version 5 upgrade, Matrix blocks were converted to entries. Converting from Matrix to CKEditor takes the plan further still and does away with Matrix fields altogether. In this way, CKEditor merges together the capabilities of Matrix blocks and Redactor into one, long-form content editor. The result: the smoothest authoring experience yet, for developers and content-editors alike.
We wanted to get to a point where we could offer this to our customers. We started on our own site first
Our site is running Craft 5 - the conversion command is only available here - so Matrix blocks had already been converted into entry types. For the avoidance of confusion, all Matrix entry types will be referred to as blocks. CKEditor nested entries, as just that.
How To: According to Craft
There's a command for that. This is its documentation, in full.
You can use the
ckeditor/convert/matrix
command to convert a Matrix field over to CKEditor. Each of the Matrix field's [blocks] will be assigned to the CKEditor field, and field values will be a mix of HTML content extracted from one of the nested entry types of your choosing (if desired) combined with nested entries.
php craft ckeditor/convert/matrix
The command will generate a new content migration, which will need to be run on other environments (via
craft up
) in order to update existing elements' field values.
Apparently, all I needed to do was run the command, grab the generated migrations, upload these to live along with the project config, and run craft up
. And that would be it! Yay!
And That Was It?
Not quite. That was a long way from being, 'it.'
To illustrate what happened, I'll use a hypothetical Matrix field with the handle contentBlocks
containing three blocks: banner
; image
; and text
.
Running the Command
./craft ckeditor/convert/matrix contentBlocks
The CLI then asked an unexpected question, with some possible options.
Which entry type should HTML content be extracted from?
- text (Text)
- image (Image)
- banner (Banner)
Choose: (text,image,banner,none,?):
After a lot of fruitless internet searching, I opted for none
as my answer.
The CLI had another question to ask.
Which CKEditor config should be used for this field?
(default,simple,simple-2,standard,?):
Again, there is not much online to help with how to answer this question. I opted for default
.
I checked an entry that used contentBlocks
in the CMS. There, the Matrix field had gone, replaced by a CKEditor field containing nested entries named to match the blocks they'd replaced. From that point of view, running the command had been a complete success. Content could be edited right away.
On the frontend, there was an immediate error with the Twig.

Based on what we had in place to render the Matrix field, and the little I had learned, fixing that seemed easy.
The Twig as We Had It
To render the Matrix field's content, contentBlocks.twig
had included a for
loop containing a switch
statement.
{% for block in entry.contentBlocks.all() %}
{% set colour = "pink" %}
{% set textColour = "mid-gray" %}
{% set ctaColour = "pink" %}
{% set bgColour = "bg-white" %}
{% switch block.type %}
{% case "banner" %}
{% set bannerImage = block.image.one() %}
{% if bannerImage %}
<header class="full-width-header {{ colour }} {{ bgColour }}">
<img src="{{ bannerImage.getUrl() }}"
alt="{{ bannerImage.alt or bannerImage.title }}"
...
>
{% if block.heading %}
<h1>{{ block.heading }}</h2>
{% endif %}
</header>
{% endif %}
{% case "text" %}
{% if block.heading %}
<h2>{{ block.heading }}</h2>
{% endif %}
{% if block.content %}
<div class="content-block text-content {{ textColour }}">
{{ block.content }}
</div>
{% endif %}
{% case "image" %}
{% set asset = block.asset %}
{% set target = block.targetUrl %}
{% if target %}
<a href="{{ target }}"
class="{{ ctaColour }}"
>
{% endif %}
{% if asset %}
<img src="{{ asset.getUrl() }}"
alt="{{ asset.alt or asset.title }}"
...
>
{% endif %}
{% if target %}
</a>
{% endif %}
{% endswitch %}
{% endfor %}
The problems started, with this line:
{% for block in entry.contentBlocks.all() %}
CKEditor doesn't understand .all()
. Neither does it have blocks
, it has nested-entries. When converting to CKEditor, Craft assumes a template structure like this, which we didn't have.
_partials/
└-- entry/
└-- banner.twig
└-- text.twig
└-- image.twig
To make that happen for our site, I moved the contents of each case
into an appropriately named Twig file, in the right directory. I also replaced every instance of .block
with .entry
. In the parent file, I replaced this
{% for block in entry.contentBlocks.all() %}
with this
{{ entry.contentBlocks }}
Knowing that that should work was thanks to about five seconds of a CraftQuest video I'd found.
This resulted in a different error.

If you never need to pass a variable into a partial, then writing {{ entry.contentBlocks }}
, along with a well-adjusted template structure, will work perfectly.
CKEditor and Craft work together here. They assume that the code to render CKEditor's nested-entries will be found in partials named exactly to match the entry.type.handle
in _partials/entry
.
Passing Variables
To pass variables to these partials I needed some way of looping over the nested entries, just as had been done with the blocks in the Matrix field. For this, chunk
is your friend.
{% if entry.contentBlocks|length %}
{% for chunk in entry.contentBlocks %}
{% if chunk.type == 'entry' %}
{% set block = chunk.entry %}
{% set colour = "pink" %}
{% set textColour = "mid-gray" %}
{% set ctaColour = "pink" %}
{% set bgColour = "bg-white" %}
{% include "_partials/entry/" ~ block.type.handle with {
colour: colour,
textColour: textColour,
ctaColour: ctaColour,
bgColour: bgColour,
block: block
} %}
{% endif %}
{% endfor %}
{% endif %}
A chunk
is any child of a CKEditor field, whether that be directly entered content, or a nested-entry. With that in place, everything rendered as it should. Well ... once I'd gone into the Twig files and switched instances of .entry
back to .block
.
Everything rendered as it should?
Nearly. The merging together of the capabilities of Matrix blocks and Redactor allows for directly entered content. There's no need for any nested entries at all. With the template set up as above, none of that content would appear.
A slight modification to the above code fixes that.
block: block
} %}
{% else %}
{{ chunk }}
{% endif %}
{% endfor %}
{% endif %}
Directly entered content was now rendered, inheriting the site's native styling.
To pass particular classes to this content, I used the Retcon plugin. It provides a Twig filter, retconAttr
. It searches for user specified HTML tags, applying defined attributes.
block: block
} %}
{% else %}
{{ chunk | retconAttr(
'p',
{ class: 'paragraph-class' }
) | retconAttr(
'ul',
{ class: 'list' }
) | retconAttr(
'h3',
{ class: 'header' }
) | retconAttr(
'a',
{ class: 'link underline' }
)
}}
{% endif %}
{% endfor %}
{% endif %}
So, Was That It?
Like many other Craft sites, contentBlocks
was not the only Matrix field we had. Completing the process multiple times worked great, until there was a Matrix block that had the same handle as one I had previously converted.
The command converts Matrix blocks into entries with named types. The entry type handle will match the handle of the former Matrix block. Duplicates are handled with the minimum amount of effort. The second text
handle becomes text2
, and so on. This new handle is what Craft expects the Twig template to be called.
In a very simple site, this is fine. You would create text.twig
and text2.twig
in _partials/entry
and all would be great.
The old block handle remains, as the user defined entry handle. This is targetable with block.type.handle
as I did. I'd created a text2.twig
file (matching Craft's entry handle), but Craft tried to render text.twig
(the user defined entry handle) instead, and failed.
For our site, with only a few previous blocks with the same handle, removing the user defined entry handles in the CMS was an easy fix. On a more complex site it wouldn't be.
What I Would Have Done Differently
We plan to roll this out to other Craft sites. Other sites with a lot more Matrix fields than ours and perhaps a large number of blocks with matching handles. Removing every user defined entry handle would be a big task. There would also be an unmanageablely large _partials/entry
directory, hindering future development.
A one line change would fix that.
{% include "_partials/entry/" ~ block.type.handle <further code> %}
to this
{% include "_partials/entry/<myMatrixFieldHandle>/" ~ block.type.handle <further code> %}
along with a better organised template structure.
_partials/
└-- entry/
└-- contentBlocks
└-- banner.twig
└-- text.twig
└-- image.twig
└-- articleBlocks
└-- someBlock.twig
└-- text.twig
Comments added to the top of files would indicate what is calling those partials.
{# Content Blocks - Text -#}
Small changes, but extremely helpful for future developers.
TL;DR
Craft 5 allows you to convert Matrix fields to CKEditor with a CLI command. That, with some careful templating, provides a smoother editing experience.
However, the process is more involved than the docs suggest. The conversion works: content is editable right away. But, rendering that content can require significant template restructuring. CKEditor fields don't use blocks or .all()
: nested entries are used instead.
In most cases, you'll need to:
-
Replace
entry.contentBlocks.all()
with a loop overentry.contentBlocks
, usingchunk
. -
Pass variables into your included partials, using
with{variableOne: variableOne, variableTwo: variableTwo}
. - Update your folder structure and file naming, using directories named after CKEditor fields, to make future editing easier.
- Make sure you handle directly entered content. To style that, use the Retcon plugin.
Converting from Matrix to CKEditor is achievable. But, be prepared to tweak your templates, adjust naming conflicts, and think carefully about how your partials are organised.

Mark Syred
Mark works with Craft CMS, WordPress and Laravel to support the ongoing functioning of customers' websites. He also focuses on improving site security and overall performance.