Plugin Architecture Is Editor Experience
Lessons from building the component architecture behind the new django CMS website.
When we design CMS components, it is easy to think primarily about implementation: which plugins exist, which plugins can contain other plugins, how templates are structured, and where configuration belongs.
But every one of those architectural decisions eventually appears in an editor's interface.
A wrapper becomes another thing an editor has to understand. A generic component becomes another decision they have to make. An implementation-oriented name becomes terminology they have to learn.
Plugin architecture is therefore not just a technical concern.
Plugin architecture is editor experience.
A useful principle follows from this: The plugin tree should model the editor's intent, not the developer's implementation.
I didn't start with this as an abstract theory about CMS architecture. It emerged from building components, watching how they were used, and repeatedly discovering that abstractions that made sense to developers did not necessarily make sense to editors.
Much of that thinking emerged while I was building djangocms-frontend (version 2), which I created to make it easier to build reusable, editor-facing components without unnecessarily tying their content structure to a particular frontend implementation.
The django CMS website has since become an important testing ground for these ideas. We work with the implementation architecture, but we also have to live with the editorial experience that architecture creates.
Over time, that has changed how I think about component systems.
The examples below come from that experience. They are specific to django CMS, but I think the principles behind them apply much more broadly to composable content systems.
Model Intent, Not Implementation
Editors don't think primarily in terms of components, containers, wrappers, or template abstractions.
They think about what they are trying to create.
They want to add a hero. Create a list of cards. Insert a text section. Add a call to action.
The CMS should reflect that mental model.
A plugin called ContainerPlugin describes an implementation concept. The editor still has to understand what that container is for.
A plugin called Hero describes intent.
Good plugin names answer the question: What am I trying to create?
This sounds like a small naming decision, but it points to a larger architectural principle.
Implementation abstractions should not leak unnecessarily into the editorial model.
The plugin interface has to translate technical structure into concepts that make sense to the people creating content.
Give Every Level of Hierarchy Meaning
Nested plugins are useful when the hierarchy itself represents something meaningful.
One pattern we use on the django CMS website is the distinction between sections and items. A section defines a type of content area, while items represent repeatable elements within it.
For example:
class CardsPlugin(CMSPluginBase):
allow_children = True
child_classes = ["CardPlugin"]
class CardPlugin(CMSPluginBase):
parent_classes = ["CardsPlugin"]
For the editor, this becomes:
Cards
└── Card
└── Card
└── Card
Both levels mean something.
Cards represents the collection. Card represents an item within that collection.
More importantly, the pattern is predictable. Once an editor understands how one such section works, similar sections require little additional explanation.
Problems start when hierarchy exists primarily for technical reasons.
Consider:
Narrow Section
└── Content
└── Text
If Content does not affect the meaning, appearance, or behavior of the page, it is difficult to explain why the editor should have to create and manage it.
A simpler structure is:
Narrow Section
└── Text
└── Image
The editor's mental model is now straightforward: I added a section. Now I add content to it.
A useful test is: If an editor has to interact with something, what does that thing mean to them?
If there is no good answer, the abstraction may belong in the implementation rather than the editorial interface.
Put Invariants in the Content Model
Another important distinction is between content that defines a component and content that can be composed inside it.
Consider a hero with a required main image.
It could be modeled entirely through child plugins:
Hero
└── Image
└── Image
But this creates ambiguity.
Which image is the hero image? Is the first one special? What happens if the editor doesn't add one? Can there be two?
If a hero requires exactly one main image, that image is not really optional composition.
It is part of what makes the hero a hero.
Model it accordingly:
class HeroPlugin(CMSPluginBase):
model = HeroModel # includes title, text, main_image
allow_children = True
child_classes = ["LinkPlugin", "ImagePlugin"]
The required image becomes a field on the hero itself. Optional links or additional images can remain child plugins.
This gives us another general principle:
Required elements belong to the component's content model. Repeatable or optional elements are good candidates for composition.
The technical model then communicates the semantics of the component rather than leaving editors to reconstruct those semantics themselves.
Use Composition When Composition Has Meaning
Child plugins are powerful because they allow editors to compose content.
But composition should be intentional.
They work particularly well for repeatable or structured content:
People
└── Person
└── Person
Accordion
└── Accordion Item
└── Accordion Item
Cards
└── Card
└── Card
In each case, the parent-child relationship has meaning.
What child plugins should not become is a generic mechanism for reproducing an internal component tree inside the CMS.
A simple rule is useful here: If removing a plugin doesn't change the meaning, behavior, or appearance of the page, ask whether that plugin needs to exist in the editorial interface at all.
Sometimes the answer will still be yes because of technical constraints.
But technical necessity should not automatically become editorial structure.
Hide Integration Complexity from Editors
Third-party integrations are another place where implementation architecture can easily leak into the editorial experience.
Suppose a page should contain a section showing the latest blog articles.
A straightforward implementation might require editors to create a section, add a heading, then add and configure a plugin provided by djangocms-stories.
Technically, those pieces may come from different systems.
Editorially, however, they describe one thing:
Latest Blog Articles.
This is a pattern we use on the django CMS website.
The fact that functionality happens to come from another django CMS package doesn't mean editors should have to understand that package boundary.
Instead, we can encapsulate the integration in a component containing the fields relevant to the editorial use case:
- heading
- number of posts
- post item template
- other presentation options
With djangocms-frontend, the component template can render the required djangocms-stories plugin internally using the {% plugin %} template tag:
{% plugin "BlogLatestEntriesPlugin" template=... %}
The editor now creates and configures one meaningful component instead of reconstructing an integration every time.
The larger principle is: Integration boundaries are implementation details unless editors have a reason to care about them.
A CMS should encapsulate those details where possible.
This is also a good example of why I care about separating editorial architecture from implementation architecture.
Developers still need the boundaries. Editors often don't. Good architecture doesn't eliminate boundaries. It puts them where they belong.
Don't Repeat Context the CMS Already Knows
The same principle applies to naming child plugins.
It can be tempting to call plugins Hero Link, Hero Image, or Cards Card to make their purpose explicit.
But if a Link plugin can only be created inside a Hero, the CMS already provides that context.
Repeating it makes the interface harder to scan without giving the editor new information.
Prefer:
Hero
└── Link
└── Image
over:
Hero
└── Hero Link
└── Hero Image
Constraints in the model can carry information that does not need to be repeated in labels.
Good architecture does not merely expose information.
It also knows which information can remain implicit.
Treat Flexibility as a Cost
CMS architecture often gravitates toward flexibility.
A generic container that can contain anything feels reusable. Allowing many plugin combinations feels powerful. Exposing more configuration feels capable.
I have become increasingly skeptical of treating that flexibility as an unconditional benefit.
Every degree of freedom given to an editor creates another decision:
-
Should I use a section or a container?
-
Should this image be a child plugin or a field?
-
Which type of wrapper do I need?
-
Can this plugin go here?
-
Which of these five similar plugins should I choose?
Each individual decision may be small. Together, they determine whether a CMS feels obvious or complicated. That means: Flexibility should have to justify its cognitive cost.
Good CMS architecture deliberately constrains some possibilities in exchange for clearer intent, more predictable output, and a simpler editing experience.
The goal is not maximum flexibility. The goal is enough flexibility to cover real use cases with as little ambiguity as possible.
This is one of the lessons that became clearer to me through building real component systems rather than designing them in the abstract.
A technically elegant abstraction is not necessarily a good product abstraction.
What Building the django CMS Website Taught Me
These principles did not start as a design manifesto. They emerged gradually from building and evolving the django CMS website and the tools around it.
djangocms-frontend gave me a way to define reusable components while keeping their editorial structure separate from many of the details of their frontend implementation.
The website then gave us a real environment in which to test those abstractions. Some ideas survive. Others turn out to expose too much implementation detail, create unnecessary choices, or require editors to understand concepts that should never have crossed the architecture boundary.
Over time, a pattern becomes difficult to ignore:
Whenever implementation details leak into the plugin tree, editors pay the cost.
Whenever the content model instead reflects editorial intent, the system becomes easier to understand and harder to misuse.
That's why I think the lesson extends beyond django CMS. Every composable CMS has to decide how much of its underlying component architecture to expose to editors.
-
Developers may need to think about wrappers, rendering boundaries, reusable components, integrations, and template architecture.
-
Editors shouldn't need to. They should think about heroes, cards, people, articles, calls to action, and whatever other concepts belong to their content.
A good content system translates technical architecture into an editorial model organized around intent, meaning, and predictable constraints. That translation is not decoration around the architecture. It is part of the architecture.
Plugin Architecture Is Product Design
The plugin tree is an interface.
Naming is interface design.
Nesting is interface design.
Constraints are interface design.
Deciding what becomes a field and what becomes a child plugin is interface design.
So when evaluating CMS architecture, I think we should ask not only: "Is this technically clean and reusable?", but also: "What mental model does this architecture create for the editor?".
A well-designed system uses clear names, meaningful hierarchy, deliberate constraints, and predictable patterns.
Technical complexity stays behind the interface whenever editors don't benefit from seeing it.
When that works, editors don't think about plugins at all. They just build pages or write posts.
The best plugin architecture is therefore not the one that exposes the most composability. It is the one that turns technical possibilities into an editorial vocabulary people can understand.
Architecture isn't behind the editor experience. It creates the editor experience.
The harder question is where to draw that boundary. How much flexibility should a CMS expose before composition becomes configuration? Which constraints belong in the CMS, and which should be left to individual projects?
I'm interested in how other CMS implementations approach that trade-off — and where their editors benefit from, or struggle with, the architecture underneath.