Composing Pages with Sulu's Block System: A Developer's Guide to Structured Content
Sulu's block system lets developers define reusable, type-safe components while editors compose pages by drag-and-drop. Learn how to register custom blocks, restrict types per zone, and avoid common pitfalls like cache misses and translation gaps.
27 Jul 2026, 03:45 UTC

The Problem: Rigid Templates vs. Editorial Freedom
Content management often forces a choice: lock editors into fixed page templates, or give them a WYSIWYG editor that produces inconsistent markup. Sulu CMS solves this with a block system that lets developers define reusable, type-safe components while editors compose pages by dragging and dropping those components into designated zones. The result is a middle ground where design systems stay intact and editorial workflows stay flexible.
How Block Areas Work in Practice
Each page template is an XML file. Inside, a <property type="block_area"> declares a content zone. Editors see this zone in the admin UI at /admin/content/pages and can add blocks from a configured pool—text, image, snippet, internal link, or any custom type you register. Blocks reorder via drag-and-drop; each block persists as a separate PHPCR node under the page document, enabling granular versioning, translation, and workspace staging without duplicating the entire page.
Registering a Custom Block Type
Suppose you need a "Callout" block with a headline, body text, and an optional link. The steps:
- Create the block class implementing
BlockTypeInterface(or extendAbstractBlockType). Define the template path and any JavaScript for the admin preview. - Add the Twig template at
templates/blocks/callout.html.twig. Variables map directly from the block's parameters—no controller logic required. - Declare parameters in XML (e.g.,
config/blocktypes/callout.xml) with<params>for headline, body, link URL, and link text. Mark translatable fields withtranslatable="true". - Register as a service in
config/services.yamlwith thesulu.block_typetag. - Clear the Symfony cache (
bin/console cache:clear). Forgetting this step causes "Block type not found" errors in the admin.
After cache clear, the Callout block appears in the block selection modal for any block_area that allows it via <allowed-types>.
Worked Example: Restricting Blocks per Zone
In a multi-site installation sharing a block library, you might want the homepage hero zone to accept only hero and callout blocks, while the sidebar zone allows text, image, and snippet. In the page template XML:
<property name="heroZone" type="block_area">
<allowed-types>
<type>hero</type>
<type>callout</type>
</allowed-types>
</property>
<property name="sidebarZone" type="block_area">
<allowed-types>
<type>text</type>
<type>image</type>
<type>snippet</type>
</allowed-types>
</property>
This enforces design-system consistency at the template level. Editors never see disallowed blocks in the selection modal for that zone.
Trade-offs and Limitations
- No native nested block areas. Blocks cannot contain other block areas. Complex layouts typically use a single block with internal structure (e.g., a grid block with column parameters) or reference a snippet block that points to another page.
- Translation caveat. Only parameters explicitly marked
translatable="true"in the block type XML are localized. Non-translatable params silently fall back to the default locale—audit your block definitions if you run multilingual sites. - Admin performance with many blocks. Pages exceeding ~50 blocks load the entire block tree in one request, degrading the admin UI. Move heavy reusable sections into snippet blocks to keep the block count low.
- Cache dependency. Every new block class or XML change requires a cache clear. Automate this in your deployment pipeline.
Verifying the Setup
After adding a block type, confirm it works end-to-end:
- Create a test page template with a
block_areaallowing your new type. - In the admin, add the block, fill parameters, save as draft.
- Run
bin/console phpcr:node:dump /cms/content/pages/<page-uuid>to inspect the PHPCR node tree—each block should appear as a child node with properties matching your parameters. - Switch workspace to
stage, publish, and check the page's version tab for block-level history.
Next Steps
Start small: define one block area in a template, register two block types (text and image), and verify drag-and-drop reordering in the admin. Then layer on custom blocks, <allowed-types> restrictions, and translation markers as your design system grows. The block system's strength is that developers control rendering (Twig templates, type-safe parameters) while editors control composition—without either side stepping on the other's toes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.