Designing a Sulu CMS Installation: Zones, Dynamic Routing, and the Data Boundaries That Matter
An architecture note on Sulu CMS: how webspaces, dynamic routing, and the admin/frontend split define your data boundaries, plus the cache and search-index failure modes that bite in production.
22 Jun 2026, 16:09 UTC

If you are evaluating Sulu for a multi-site or multi-brand content platform, the decisions that hurt later are rarely about templates. They are about how you partition content, how Sulu resolves URLs at runtime, and where your data boundaries sit relative to Symfony's caching and search layers. This note walks through the smallest suitable design for a typical Sulu installation, the trust boundaries involved, and the conditions that should push you toward a more complex one.
Requirements worth stating first
Sulu is a Symfony-based CMS where the admin interface and the public frontend are logically separated. A typical project needs three things from the architecture: editors must be able to create and nest pages without developer involvement, URLs must reflect the content tree without hardcoded routes, and the frontend must stay fast even when the content tree grows. Everything in the design below follows from those three requirements.
The smallest suitable design
The core structural unit in Sulu is the webspace. A webspace defines a site: its domain, locales, and URL scheme. Inside a webspace, content lives in a tree of pages, and each page gets its URL segment from a property editors fill in. Sulu's routing layer resolves incoming requests by matching the full path against the published content tree rather than against a static Symfony route table.
For a single-brand site with one or two languages, the smallest design is: one webspace, one portal, one theme, and the default page templates. Do not introduce additional webspaces because you might need them later — each webspace multiplies your routing configuration, cache namespaces, and editor training surface.
A minimal webspace configuration (in config/webspaces/) looks like this:
<webspace xmlns="http://schemas.sulu.io/webspace/webspace">
<name>Example</name>
<key>example</key>
<localizations>
<localization language="en" default="true"/>
<localization language="de" shadow="auto"/>
</localizations>
<portals>
<portal>
<name>Example Portal</name>
<key>example-portal</key>
<environments>
<environment type="prod">
<urls>
<url language="en">example.com</url>
<url language="de">example.de</url>
</urls>
</environment>
</environments>
</portal>
</portals>
</webspace>This file is edited by a developer and deployed with the application; editors never touch it. That split is deliberate and is the first trust boundary.
Trust and data boundaries
There are three boundaries to keep straight:
- Code vs. content. Webspace definitions, page templates (XML files defining which fields a page type has), and Twig themes live in the repository and go through code review. Page instances, their URL segments, and their field values live in the database via Sulu's content repository layer, which sits on top of Doctrine ORM. Editors can break a page's content; they cannot break the routing scheme or template structure.
- Admin vs. frontend. The Sulu admin is a separate single-page application talking to Sulu's REST API. The frontend renders published documents only. Draft and published states are distinct rows/versions in the content storage, so an editor saving a draft never affects the live site until publish.
- Database vs. search index. Content discovery in the admin (and often frontend listing pages) goes through a search index, typically Elasticsearch in production setups. The index is a derived, rebuildable copy — the database remains the source of truth. Treat any divergence as an indexing bug, not data loss.
How dynamic routing actually resolves a request
When a request hits the frontend, Sulu's request analyzer determines the webspace, portal, locale, and segment from the host and path. The content router then looks up the remaining path in the route storage for that webspace and locale. Because URLs are data, not code, the result of this lookup is cached aggressively — which is where most operational problems originate.
After changing webspace configuration or deploying template changes, clear the relevant caches on the server (run from the project root, as the web server user or a user with write access to var/cache/):
php bin/adminconsole cache:clear --env=prod
php bin/websiteconsole cache:clear --env=prodSulu splits the admin and website kernels, so there are two consoles and two cache pools. Clearing only one is a common cause of "I deployed it but the frontend still shows the old template." The exact console names depend on your Sulu version (older versions use bin/adminconsole and bin/websiteconsole; verify against your project's bin/ directory). Never run cache clears as root in a way that leaves root-owned files in var/cache/ — the next web request will fail with permission errors.
Operational checks
- Routing check: create a test page in the admin, publish it, and request its URL from the frontend with
curl -I. A 200 with your theme's markup confirms the analyzer, route storage, and template resolution all agree. A 404 after publishing usually means the website cache was not cleared or the page's webspace URL does not match the host you requested. - Locale check: request the same page in each configured localization and confirm the URL scheme (per-domain vs. path-prefix) matches the webspace configuration.
- Search check: after bulk content changes, verify the index is current by searching for a recently published page in the admin. If it is missing, rebuild the index using Sulu's provided console command for your search integration rather than editing indexed documents by hand.
- Query check: on listing-heavy templates, enable the Symfony profiler in the dev environment and watch the Doctrine query count. Deeply nested content structures rendered naively can produce an N+1 pattern — one query per child page. Fix this in the template/controller by fetching children in one query through Sulu's content repository rather than lazy-loading each node.
Failure modes to plan for
Stale routing cache. Symptom: renamed or moved pages still resolve at old URLs, or new pages 404. Cause: cache invalidation not wired into your deployment. Fix: make both console cache clears part of the deploy script.
Index drift. Symptom: admin search shows unpublished or deleted content. Cause: the indexer missed events (queue worker down, or a bulk import bypassed the document manager). Fix: scheduled full reindex during low-traffic windows.
Framework upgrade breakage. Sulu pins to specific Symfony versions; a composer update that moves Symfony outside the supported range can break the CMS layer in non-obvious ways. Check the sulu/sulu (or sulu/cms) package constraints in composer.json before upgrading, and treat Symfony minor upgrades as a tested deployment, not a routine one. Verify current version compatibility against the official Sulu documentation for your target release, since supported ranges change between Sulu versions.
Conditions that should change this design
Move beyond one webspace when you have genuinely separate sites — different domains, different editorial teams, different navigation trees — not just different sections. Add a second portal when the same content must be served under different domains with different analytics or access rules. Introduce a custom route schema (e.g., date-based article URLs) only when editors actually need it, because custom route generation adds code you must maintain across Sulu upgrades. And if your frontend is a decoupled SPA consuming Sulu's API, the caching and locale-resolution concerns move to your API client — the webspace model stays, but the Twig layer and its cache largely drop out.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.