Choosing Between Sulu Webspaces and a Single Multi‑Locale Webspace
A decision guide for Sulu CMS: compare native multi‑site Webspaces against a single Webspace with multiple locales. Covers routing, admin UX, permissions, deployment, and a concrete validation checklist.
22 Oct 2025, 02:16 UTC

Decision and constraints
When a Sulu project must serve more than one website or region, you have two supported architectural paths:
- Multiple Webspaces – each site gets its own
Webspaceconfiguration, isolated content tree, theme, security context, and base URL. - Single Webspace with multiple locales – one content tree shared across locales; pages exist once and are translated.
The choice is driven by three practical constraints:
- Structural independence – do the sites need different page types, templates, or navigation hierarchies?
- Team ownership – will separate editorial teams manage each site with no cross‑access?
- Operational simplicity – do you prefer a single routing table, one cache pool, and one search index?
Sulu 2.6+ implements both patterns on the same codebase and database; the difference is purely configuration and request‑time resolution.
Supported options at a glance
| Aspect | Multiple Webspaces | Single Webspace + Locales |
|---|---|---|
| Content tree | Separate root per Webspace (webspace_id partitions sulu_page) | One tree; each page holds translations for every configured locale |
| Routing | Per‑Webspace route collections loaded dynamically; resolved by WebspaceResolver from Host header | Single route collection; locale prefix generated automatically (e.g. /en/about, /de/ueber-uns) |
| Admin UI | Top‑level tabs – editors see only their Webspace | Locale switcher inside the same tree |
| Permissions | Roles scoped per Webspace automatically (no implicit cross‑Webspace inheritance) | Roles apply to the whole tree; locale‑level restrictions require custom voters |
| Themes & assets | Separate Webpack Encore output directories (e.g. public/build/webspace-a/) | Single build output; CSS/JS shared across locales |
| Search (MassiveSearch/Elasticsearch) | Per‑Webspace indexes (sulu_<key>_<locale>) or global with filter | Global index with locale field; simpler ops |
| Deployment impact | Shared DB schema; migrations lock tables for all Webspaces | Same schema, but only one migration run – identical impact |
| Configuration complexity | Higher – config/webspaces/*.xml, request listener, kernel environment per host | Lower – one webspace.xml with multiple <localization> entries |
Trade‑off analysis
When to choose multiple Webspaces
- Sites have distinct information architectures (different page types, navigation depth, or template sets).
- Editorial teams are organizationally separate and must not see each other’s content.
- Branding or design systems diverge enough to require isolated asset pipelines.
- You can afford the extra configuration overhead and the operational discipline of sanitising the
Hostheader at the reverse‑proxy layer.
When to choose a single Webspace with locales
- All regions share the same page structure and template library; only copy differs.
- Content reuse across locales is frequent (e.g. global news, shared product pages).
- Team works on all locales together; a single permission matrix is sufficient.
- You want the simplest routing, cache, and search setup.
Risk note: Webspace resolution trusts the Host header. Ensure your edge layer (nginx, Varnish, Cloudflare) validates or rewrites the header before it reaches Symfony; otherwise cache poisoning or routing bypass is possible.
Concrete implementation & validation (multiple Webspaces)
The following steps assume a fresh Sulu 2.6+ project with Symfony Flex. Run commands from the project root as the application user (typically www-data or your deploy user).
1. Define two Webspace configurations
# config/webspaces/site-a.xml
<?xml version="1.0" encoding="UTF-8"?>
<webspace key="site_a">
<name>Site A</name>
<url>https://site-a.example.com</url>
<localizations>
<localization language="en" default="true"/>
<localization language="fr"/>
</localizations>
<theme>site-a</theme>
<security>
<role>ROLE_SULU_EDITOR_SITE_A</role>
</security>
</webspace>
# config/webspaces/site-b.xml
<?xml version="1.0" encoding="UTF-8"?>
<webspace key="site_b">
<name>Site B</name>
<url>https://site-b.example.com</url>
<localizations>
<localization language="de" default="true"/>
<localization language="en"/>
</localizations>
<theme>site-b</theme>
<security>
<role>ROLE_SULU_EDITOR_SITE_B</role>
</security>
</webspace>
Each <theme> value must match a directory under templates/ (e.g. templates/site-a/, templates/site-b/).
2. Register the WebspaceResolver (if not already present)
# config/services.yaml
services:
Sulu\Bundle\WebsiteBundle\Routing\WebspaceResolver:
arguments:
$webspaces: '@sulu_website.webspace_manager'
tags: ['kernel.event_listener', { event: kernel.request, method: onKernelRequest, priority: 32 }]
3. Verify theme isolation
php bin/adminconsole sulu:website:dump-template --webspace=site_a
php bin/adminconsole sulu:website:dump-template --webspace=site_b
Each command should output the template paths belonging to the respective theme only. If both list the same files, check the <theme> value and template directory naming.
4. Confirm routing per host
php bin/console debug:router --show-controllers | grep -E 'site_a|site_b'
You should see two distinct route collections prefixed by the Webspace key. For a manual request test (requires a running web server or PHP built‑in server):
curl -H 'Host: site-a.example.com' http://localhost/
curl -H 'Host: site-b.example.com' http://localhost/
Each response must render the correct base template (inspect the HTML <body> class or a known landmark).
5. Database sanity check
php bin/console doctrine:query:sql "SELECT DISTINCT webspace_id FROM sulu_page;"
Expect two rows (e.g. 1 and 2) corresponding to site_a and site_b.
6. Search index naming (optional)
If you use MassiveSearch with per‑Webspace indexes, verify the index pattern:
curl -X GET "localhost:9200/_cat/indices/sulu_*?v"
Indices should follow sulu_site_a_en, sulu_site_a_fr, sulu_site_b_de, sulu_site_b_en.
7. Asset build namespacing
# webpack.config.js (Encore)
const siteA = Encore.getConfig('site-a')
.setOutputPath('public/build/site-a')
.setPublicPath('/build/site-a')
.addEntry('app', './assets/site-a/app.js');
const siteB = Encore.getConfig('site-b')
.setOutputPath('public/build/site-b')
.setPublicPath('/build/site-b')
.addEntry('app', './assets/site-b/app.js');
module.exports = [siteA, siteB];
Run yarn encore production (or npm run build) and confirm both public/build/site-a/ and public/build/site-b/ exist with distinct manifests.
Validation checklist for the single‑Webspace alternative
- Create one
config/webspaces/main.xmlwith multiple<localization>entries. - Run
php bin/adminconsole sulu:website:dump-template– only one theme should appear. - Check
debug:router– routes contain locale prefixes but no Webspace prefix. - Inspect
sulu_page– a singlewebspace_idvalue. - Generate a sitemap for each locale via
bin/console sulu:website:dump-sitemap --locale=en(repeat per locale). Ensure theRequestContextis set correctly in the command.
Limitations & practical verification
- All Webspaces share the same database connection pool; a long‑running migration blocks every site. Schedule migrations during low‑traffic windows and monitor replica lag if you run read replicas.
- Console commands that generate absolute URLs (sitemap, email rendering) must explicitly set the
RequestContextper Webspace; otherwise the default host is used. - Permission changes are immediate but cached in the Symfony security token; clear the cache (
php bin/console cache:clear) after role assignments. - The provided validation steps have not been executed in a live environment; treat them as a verification plan for your staging pipeline.
Bottom line
Choose multiple Webspaces when structural, editorial, or design independence outweighs the added configuration and operational discipline. Choose a single Webspace with locales when the sites are essentially the same structure in different languages and you value operational simplicity. Both patterns are fully supported in Sulu 2.6+ and can coexist in the same codebase if future requirements shift.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.