One Codebase, Many Sites: How Sulu's Webspace Config Handles Multi-Site, Multi-Language Content
Sulu's declarative webspace configuration lets one installation serve multiple brands and languages. Here's how portals, localizations, and fallback chains work — plus the honest trade-offs.
09 Dec 2025, 12:59 UTC

You have three brands, each with its own domain, each publishing in two or three languages, and marketing wants shared components across all of them. The classic answers are a fleet of separate CMS installs (a maintenance nightmare) or a plugin stack that bolts multi-site onto a single-site core (fragile). Sulu takes a different route: multi-site and multi-language are first-class configuration, defined in one declarative file per site.
The thesis of this piece is simple: if you understand Sulu's webspace concept, most of the hard parts of running several localized sites — routing, locale fallback, shared content schemas — fall out of configuration rather than custom code. The trade-off is that the configuration is dense and version-sensitive, so you need to verify behavior against your installed release.
Webspaces: the unit of "a site"
In Sulu, a webspace is the declarative definition of one site: its key, its name, the languages it supports, and how URLs map to those languages. Webspace configs live as XML files (typically in config/webspaces/) and are loaded by the kernel. A single Sulu installation can serve many webspaces from one admin and one content repository.
Each webspace defines portals — the entry points that tie a URL pattern to an environment. A portal contains environments (e.g. prod, dev) with URLs that can embed locale placeholders, so {host}/{localization}-style patterns let one definition cover example.com/en and example.com/de without duplicating config.
Languages per webspace are declared explicitly, with one marked as default. That declaration is what drives the admin UI (which locale tabs an editor sees) and the routing (which locale a given URL resolves to).
A concrete example: two brands, three locales
Say you run brand-a.com (English default, German fallback) and brand-b.de (German only). A trimmed webspace file for brand A looks conceptually like this:
<webspace xmlns="http://schemas.sulu.io/webspace/webspace">
<name>Brand A</name>
<key>brand_a</key>
<localizations>
<localization language="en" default="true"/>
<localization language="de"/>
</localizations>
<portals>
<portal>
<name>Brand A Portal</name>
<key>brand_a_portal</key>
<environments>
<environment type="prod">
<urls>
<url language="en">brand-a.com</url>
<url language="de">brand-a.com/de</url>
</urls>
</environment>
</environments>
</portal>
</portals>
</webspace>Check the exact schema against your Sulu version — the webspace XSD and supported attributes have changed across releases, so treat this as a shape, not copy-paste config. After editing, clear the cache (bin/console cache:clear in the project root, run as the deploy user) and confirm the site resolves: request both URLs and verify the locale switcher in the rendered page and the admin's locale tabs match what you declared.
Fallback chains: untranslated pages without 404s
Because content lives in a PHPCR repository (a node-based content store, accessed in Sulu via PHPCR-ODM) with localization built in, a page node can exist in en but not yet in de. Sulu's webspace config lets you define fallback behavior so a German visitor sees the English content instead of an error while translation is pending. Editors then create the de variant when ready — same node, new localized properties, with version history per locale.
This is the practical win over relational CMSes: you don't write COALESCE-style query logic or maintain a translation join table. Localization and versioning are properties of the repository, not your code.
Shared schemas, separate presentation
Both brands can reuse the same Structure Types (Sulu's content schema definitions, also XML) — a "page" with title, excerpt, and blocks — while each webspace's Twig templates render them differently. The schema is decoupled from presentation, so brand B can restyle without touching content, and editors get identical forms across sites. For dynamic listings, the built-in Smart Content component queries pages by criteria (tags, types, parent) and can be dropped into a template as a configurable block — no custom repository queries for "related articles" or "latest news".
The trade-offs, honestly
- Configuration density. Webspace XML is powerful but unforgiving. A wrong URL pattern or missing default localization produces confusing routing behavior. Validate by testing every locale URL after each change, and diff configs between upgrades — the format is version-sensitive.
- PHPCR operational cost. The node repository gives you versioning and localization for free, but backups, migrations, and scaling differ from a plain relational database. Your ops runbook needs repository-specific procedures, not just
mysqldump. - Smart Content performance. Unbounded criteria on a large repository make slow editorial previews and slow pages. Always set limits, constrain filters, and measure with realistic content volume.
Where to start
On an existing install, open config/webspaces/ and map each file to a domain you actually serve. Then run the fallback experiment: create a page in the default locale only, request it in a secondary locale, and confirm you get fallback content rather than a 404. That one test validates your routing, portal, and localization config at once — and tells you whether Sulu's webspace model is doing the work it promises before you commit a second brand to it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.