Sulu Multilingual Webspaces Configuration
Sulu builds multilinguality and multisite capability around the Webspace concept. One Webspace equals one site with a set of languages and portals (subdomains/URL prefixes). Multiple Webspaces in one instance equals full multisite with shared backoffice and independent content.
Webspace Principles
- Webspace — logical site (example.com, shop.example.com)
- Portal — access variant to Webspace (production URL, staging URL)
- Localization — content language (en, de, fr)
- URL — language binding to domain or path
One Webspace can be served from multiple domains. The reverse is not possible — one domain always belongs to one Webspace.
Multilingual Webspace
<!-- config/packages/webspaces/main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<webspace xmlns="http://schemas.sulu.io/webspace/webspace"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/webspace/webspace
http://schemas.sulu.io/webspace/webspace-1.1.xsd">
<name>Main Site</name>
<key>main</key>
<localizations>
<localization language="en" default="true" xDefault="true"/>
<localization language="de"/>
<localization language="fr"/>
</localizations>
<shadow-base-language>en</shadow-base-language>
<default-templates>
<default-template type="page">default</default-template>
<default-template type="home">homepage</default-template>
</default-templates>
<templates>
<template type="page">default</template>
<template type="page">article</template>
<template type="home">homepage</template>
</templates>
<excluded-templates>
<excluded-template>overview</excluded-template>
</excluded-templates>
<portals>
<portal>
<name>Main</name>
<key>main</key>
<environments>
<environment type="prod">
<urls>
<url language="en" redirect="false">example.com</url>
<url language="de">de.example.com</url>
<url language="fr">fr.example.com</url>
</urls>
</environment>
<environment type="stage">
<urls>
<url language="en">stage.example.com</url>
</urls>
</environment>
<environment type="dev">
<urls>
<url language="en">example.localhost</url>
<url language="de">de.example.localhost</url>
</urls>
</environment>
</environments>
</portal>
</portals>
</webspace>
Multisite: Multiple Webspaces
<!-- config/packages/webspaces/blog.xml -->
<webspace>
<name>Blog</name>
<key>blog</key>
<localizations>
<localization language="en" default="true"/>
</localizations>
<portals>
<portal>
<name>Blog</name>
<key>blog</key>
<environments>
<environment type="prod">
<urls>
<url language="en">blog.example.com</url>
</urls>
</environment>
</environments>
</portal>
</portals>
</webspace>
Each Webspace has an independent content tree. In the backoffice they appear as separate sites. Media library is shared.
Security Configuration per Webspace
# config/packages/security.yaml
sulu_security:
checker:
enabled: true
security:
providers:
sulu_backend:
id: sulu_security.user_provider
firewalls:
admin:
pattern: /admin
provider: sulu_backend
main:
pattern: '^/'
stateless: false
Editor access rights can be limited to specific Webspace:
Backoffice → Users → Edit User → Webspace Permissions
Shadow Pages — Content Synchronization
Shadow allows making a page of one language a "shadow" of another — it displays the base language content without creating a separate translation.
// Programmatically create shadow via API
$document = $this->documentManager->find('/cmf/main/contents/about', 'en');
$document->setShadowLocale('de'); // take content from de
$document->setShadowLocalesEnabled(true);
$this->documentManager->persist($document, 'en');
$this->documentManager->flush();
In the backoffice — "Shadow Page" toggle on each page.
Language Switcher in Twig
{# templates/snippets/language-switcher.html.twig #}
{% set currentLocale = app.request.locale %}
<nav class="lang-switcher">
{% for locale in ['en', 'de', 'fr'] %}
{% set url = sulu_content_path(null, webspace, locale) %}
<a
href="{{ url }}"
hreflang="{{ locale }}"
lang="{{ locale }}"
class="{{ locale == currentLocale ? 'active' : '' }}"
{% if locale == currentLocale %}aria-current="page"{% endif %}
>
{{ locale|upper }}
</a>
{% endfor %}
</nav>
{# hreflang in <head> for SEO #}
{% block hreflang %}
{% for locale in ['en', 'de', 'fr'] %}
<link rel="alternate"
hreflang="{{ locale }}"
href="{{ sulu_content_path(null, webspace, locale) }}">
{% endfor %}
<link rel="alternate"
hreflang="x-default"
href="{{ sulu_content_path(null, webspace, 'en') }}">
{% endblock %}
Language-Aware Navigation
// src/Twig/NavigationExtension.php
class NavigationExtension extends AbstractExtension
{
public function __construct(
private readonly NavigationMapperInterface $navigationMapper
) {}
public function getFunctions(): array
{
return [
new TwigFunction('app_navigation', [$this, 'getNavigation']),
];
}
public function getNavigation(
string $context,
string $webspaceKey,
string $locale,
int $depth = 2
): array {
return $this->navigationMapper->getNavigation(
null,
$webspaceKey,
$locale,
$depth,
false,
$context
);
}
}
{% set nav = app_navigation('main', webspace, locale, 2) %}
{% for item in nav %}
<a href="{{ item.url }}"
{% if item.uuid == content.uuid %}aria-current="page"{% endif %}>
{{ item.title }}
</a>
{% if item.children %}
<ul>
{% for child in item.children %}
<li><a href="{{ child.url }}">{{ child.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
URL Strategies for Languages
Three URL configuration variants:
Subdomains: en.example.com, de.example.com — different URLs in Webspace portal.
Path prefix:
<url language="en">/</url>
<url language="de">/de</url>
<url language="fr">/fr</url>
Separate domain per language:
<url language="en">example.com</url>
<url language="de">example.de</url>
<url language="fr">example.fr</url>
Initialization After Webspace Change
php bin/console cache:clear
php bin/console sulu:document:initialize
php bin/console sulu:phpcr:init --user=admin
php bin/console sulu:webspace:copy-locale main --from=en --to=de
sulu:webspace:copy-locale copies the page tree structure from one language to another — helpful when adding new language to already populated site.
Timeline
Adding second language to working site: 1–2 days. Multisite setup with two Webspaces from scratch: 2–3 days. Full configuration (3 languages, 2 Webspaces, shadow pages, hreflang, navigation): 4–5 days.







