Craft 5's New Content Model: Reusable Entry Types and Field Layouts in Practice
Craft 5 shipped a lot of things, but the one that actually changes how you build sites is the content model rework. Entry types stopped belonging to sections. Fields became reusable. Matrix blocks quietly turned into entries. And underneath all of it, the way Craft stores content on disk changed completely.
If you came over from Craft 4 and just kept modeling content the way you always did, you're leaving most of this on the table. I've rebuilt a handful of client content models around the new primitives now, and once it clicks, going back to the old way feels like working with one hand tied behind your back. Here's how the pieces fit together and how I actually use them.
What Actually Changed
There are three visible changes and one invisible one. The invisible one is the reason the other three are possible, so I'll start there in a second. First, the shape of it:
- Entry types are no longer owned by a section. They're top-level objects you create once and assign to as many sections — and Matrix fields — as you want.
- Fields are reusable, with per-layout overrides. The same field can appear in a dozen layouts, and each layout can override its label, handle, instructions, and whether it's required.
- Matrix blocks are entries now. The old "block type" concept is gone. A Matrix field nests real entries, using entry types as its block definitions.
The invisible change that makes all of this work: Craft moved custom field content out of the old wide content table and into a single JSON content column on each element. That sounds like plumbing, and it is, but it's load-bearing plumbing.
The Storage Change Nobody Talks About
In Craft 4 and earlier, every custom field got its own physical database column. Your Blog Body field lived in content.field_blogBody. Add a field, get a column. This is why big installs occasionally slammed into MySQL's row-size and column-count limits, and it's the reason a field could only ever mean one thing: a column can only have one name.
Craft 5 throws that out. Custom field content now lives in a single JSON content column on the elements_sites table, keyed by field. No more column-per-field. No more table-width ceiling.
That's the enabler for everything else. Because content is keyed inside a JSON blob rather than pinned to a rigidly named column, the same field definition can be dropped into ten different layouts and store ten different values without any of them colliding. Reuse stopped being a schema problem and became a UI decision. Keep this in the back of your mind — it explains why the new features behave the way they do.
entry.myField in Twig, ->myField in PHP. The change is under the hood. But it's worth knowing when you're debugging, writing a raw migration, or explaining to a client why the upgrade touched every content row.
Entry Types Are Now First-Class
This is the headline feature and the one that reshapes how you plan a build. In Craft 4, an entry type lived inside exactly one section. If your News section and your Press section needed the same fields, you built the entry type twice and kept them in sync by hand forever.
In Craft 5, entry types are their own thing. You create them under Settings → Entry Types, give each one a field layout, an icon, and a color, and then assign them to sections. One entry type can power a Channel, a Structure, and a Matrix field all at once.
# Entry types are defined once, centrally:
Entry Types
├── Article (icon: newspaper, color: blue)
├── Landing Page (icon: layout, color: purple)
├── Text Block (icon: paragraph, color: gray) ← used inside Matrix
└── Image Block (icon: image, color: gray) ← used inside Matrix
# Then assigned wherever they're needed:
Section: News → [ Article ]
Section: Press → [ Article ] ← same entry type, no duplication
Section: Pages → [ Landing Page ]
Matrix: Page Builder → [ Text Block, Image Block ] ← same entry types as blocks
The practical win: shared structure with a single source of truth. When the client asks to add a "Reading Time" field to articles, you add it to the Article entry type's layout once, and every section using that entry type gets it. No drift, no "why does News have the field but Press doesn't" tickets.
Querying doesn't change. You still filter by section and type with handles, and an entry type shared across sections just means the same type handle shows up in more than one place:
{# Every Article across BOTH the News and Press sections #}
{% set articles = craft.entries()
.section(['news', 'press'])
.type('article')
.orderBy('postDate DESC')
.all() %}
{% for article in articles %}
<h2>{{ article.title }}</h2>
{{ article.summary }}
{% endfor %}
article, not newsArticle. The whole point is that the entry type is reusable across contexts, so a location-specific name works against you the first time you reuse it somewhere else.
Reusable Fields and Layout Overrides
Here's the change that saves the most day-to-day busywork. In the old model, if you wanted a "Heading" field on your hero, a "Heading" field on your CTA, and a "Heading" field on your feature block, you either made one generic field with vague instructions or you made three near-identical fields — heroHeading, ctaHeading, featureHeading — and cluttered your field list.
Craft 5 lets you use one field in as many layouts as you like, and override how it presents in each one. When you drag a field into a layout, you can override its:
- Label — call it "Headline" in the hero and "Card Title" in the feature block
- Handle — so the template can reference
entry.headlinein one place andentry.cardTitlein another, backed by the same field - Instructions — give authors context that's specific to that layout
- Required — mandatory in one layout, optional in another
- Width — full-width here, 25% there
The base field stays the source of truth for the field type and its core settings; the layout instance carries the presentation overrides. So you build a small kit of well-considered fields — a Plain Text, a Rich Text, an Image, a Link, a set of relations — and compose most of your site out of them.
headline and eyebrow in the same layout, storing separate values, with zero risk of collision — because content is keyed per field instance in that JSON column, not by a shared physical column.
How I Think About the Field Kit
My rule of thumb now: build fields around data type and behavior, not around the page section they'll appear in. A short list I reuse on almost every project:
plainText— single-line and multi-line text, relabeled per userichText— a CKEditor field for body copyimage/images— single vs. multiple asset relationslink— a link field (I use my own FreeLink plugin for this) for buttons and CTAsrelatedEntries— a generic entries field, scoped per layout
Five or six primitives cover the overwhelming majority of what a marketing site needs. The specificity comes from the layout overrides, not from a sprawling field list. New developers picking up the project see a clean, comprehensible set of fields instead of two hundred one-off definitions.
Matrix Is Just Nested Entries Now
This is the change that trips people up during a Craft 4 to 5 migration, because it looks the same in the control panel but is completely different underneath. In Craft 4, Matrix had its own "block types" and its own element type (MatrixBlock). In Craft 5, a Matrix field nests real entries, and its blocks are just entry types — the same entry types you'd assign to a section.
What that buys you: Matrix blocks are now full entries. They can be queried like entries, they use the same reusable fields and shared entry types, they support the same field layout overrides, and an entry type you built for a Matrix can be reused in a section (and vice versa). The wall between "top-level content" and "block content" is gone.
In templates, you load the field and loop the nested entries, switching on the entry type's handle:
{% for block in entry.pageBuilder.all() %}
{% switch block.type.handle %}
{% case 'textBlock' %}
<div class="prose">{{ block.richText }}</div>
{% case 'imageBlock' %}
{% set image = block.image.one() %}
{% if image %}
<img src="{{ image.url }}" alt="{{ image.alt }}">
{% endif %}
{% case 'ctaBlock' %}
<a class="btn" href="{{ block.link.url }}">{{ block.link.label }}</a>
{% endswitch %}
{% endfor %}
Because blocks are entries, block.type.handle is just an entry type handle, and the nested entry query supports the normal entry query params. You can filter a Matrix field down to a single block type without looping-and-if:
{# Only the image blocks, newest first, max 3 #}
{% set images = entry.pageBuilder
.type('imageBlock')
.limit(3)
.all() %}
block.type.handle keep working. But craft.matrixBlocks() and the MatrixBlock element type are gone. Any code — templates, modules, GraphQL queries — that referenced them directly needs to be rewritten to query entries. This is the single most common thing I have to fix on a migration.
How I Model a Site Now
Putting it together, my process for a new Craft 5 build looks different than it did two years ago. Roughly:
- Design the field kit first. A dozen reusable fields, named by type and behavior. Resist the urge to make section-specific fields.
- Build entry types as composable content units. An "Article," a "Landing Page," and a set of small block-style entry types ("Text," "Image," "CTA," "Accordion"). Each gets a layout assembled from the field kit, with per-layout overrides for labels and handles.
- Assign entry types to sections. Sections become thin — mostly a choice of which entry types belong there, plus URL and localization settings.
- Wire the block entry types into a Matrix "Page Builder" field and drop that onto the Landing Page entry type.
- Reuse aggressively. The "CTA" block entry type that lives in the page builder is the same one that could anchor a section. The "Image" field in the hero is the same one in the gallery.
The result is a content model that's smaller, more consistent, and dramatically easier to extend. Adding a new page-builder block is "create an entry type, add it to the Matrix field." Adding a field to every article is a one-place change. The model stops fighting you.
Migrating an Existing Craft 4 Model
If you're coming from Craft 4, the upgrade handles the mechanical conversion for you: entry types get lifted out of their sections, Matrix block types become entry types, and content migrates into the new JSON column. Your site keeps working. But "keeps working" isn't the same as "takes advantage of the new model." A few things I always check after an upgrade:
- Consolidate duplicated fields. The migration doesn't merge your old
heroHeading/ctaHeading/featureHeadingfields — it can't know they're conceptually the same. Once you're on 5, you can gradually collapse them into a single reusable field with layout overrides. Do it incrementally, per section, not in one heroic pass. - Rewrite anything referencing
MatrixBlock. Search your codebase formatrixBlocks,MatrixBlock, andcraft\elements\MatrixBlock. Those all need to move to entry queries. - Recheck GraphQL and Element API schemas. Matrix blocks surfacing as entries changes the shape of headless responses. Front-ends consuming that data will need updates.
- Look for section-specific entry type handles you can now share. If News and Press both have an
article-shaped entry type, you can converge them.
I wrote a whole separate piece on the broader Craft 4 to 5 migration if you want the full checklist — this post is specifically about what to do with the content model once you're across.
Common Pitfalls
Over-sharing entry types
Reuse is the point, but don't force two things to share an entry type just because they look similar today. If an "Article" and a "Case Study" share four fields but are genuinely diverging content types with different futures, keep them separate. Shared structure is a convenience, not a mandate. The cost of splitting them later is higher than the cost of two lean entry types now.
Naming fields after their first use
The old habit of heroHeading dies hard. The moment you name a reusable field after one specific spot, you've undercut the reason it's reusable. Name the base field generically (plainText, heading) and let the layout override give it a context-specific handle where it matters.
Forgetting that block content is entry content
Because Matrix blocks are entries, they count as entries in queries, they have their own IDs, they participate in relations, and they show up in places you might not expect (like a global entry count). This is usually a feature, occasionally a surprise. When a report says you have 40,000 entries and you only authored 3,000, remember every page-builder block is one of them.
Editing entry type layouts on production
Same rule as always: content-model structure belongs in Project Config and flows through git, not through the production control panel. The new model makes structural changes so easy that it's tempting to "just add the field real quick" on the live site. Set CRAFT_ALLOW_ADMIN_CHANGES=false in production and route structural changes through your deploy pipeline. (More on that in my git workflow post.)
The Craft 5 content model rewards a bit of up-front thought. If you treat entry types as reusable content units and fields as a small composable kit, you end up with a model that's a pleasure to extend and easy to hand off. If you port your Craft 4 habits over unchanged, you get a Craft 4 model running on a Craft 5 engine — functional, but missing the whole reason the rework happened.
Planning a Craft 5 build or untangling a content model that grew sideways over the years? I help teams get this right.