Website Development on Sulu CMS
Sulu is enterprise CMS on Symfony. Headless out of box, REST and JSON:API, multisite, multilingual, Webspaces. Built on Doctrine ORM, works with MySQL/MariaDB/PostgreSQL. Target audience — corporate sites, portals, multibranded platforms.
Architecture
my-sulu-project/
├── assets/ # frontend resources (webpack/vite)
├── bin/
├── config/
│ ├── packages/
│ │ ├── sulu.yaml # Sulu config
│ │ └── webspaces/
│ │ └── example.xml
│ ├── routes_admin.yaml
│ ├── routes_website.yaml
│ └── services.yaml
├── src/
│ ├── Controller/
│ │ ├── Admin/ # custom API for backoffice
│ │ └── Website/ # public controllers
│ ├── Entity/ # Doctrine entities
│ ├── EventSubscriber/
│ └── Twig/
├── templates/ # Twig templates
│ ├── base.html.twig
│ └── pages/
│ ├── homepage.html.twig
│ └── article.html.twig
└── public/
Webspace Configuration
<!-- config/packages/webspaces/example.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>Example</name>
<key>example</key>
<localizations>
<localization language="en" default="true"/>
<localization language="ru"/>
</localizations>
<default-templates>
<default-template type="page">homepage</default-template>
<default-template type="home">homepage</default-template>
</default-templates>
<templates>
<template type="page">homepage</template>
<template type="page">article</template>
<template type="page">article-list</template>
<template type="page">contact</template>
</templates>
<portals>
<portal>
<name>Example</name>
<key>example</key>
<environments>
<environment type="prod">
<urls>
<url language="en">example.com</url>
<url language="ru">ru.example.com</url>
</urls>
</environment>
<environment type="dev">
<urls>
<url language="en">example.localhost</url>
</urls>
</environment>
</environments>
</portal>
</portals>
</webspace>
Page Template
<!-- config/templates/article.xml -->
<?xml version="1.0" encoding="utf-8"?>
<template xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template
http://schemas.sulu.io/template/template-1.0.xsd">
<key>article</key>
<view>pages/article</view>
<controller>Sulu\Bundle\WebsiteBundle\Controller\DefaultController::indexAction</controller>
<cacheLifetime>3600</cacheLifetime>
<properties>
<property name="title" type="text_line" mandatory="true">
<meta>
<title lang="en">Title</title>
</meta>
<tag name="sulu.rlp.part"/>
</property>
<property name="article" type="text_editor" colspan="12">
<meta>
<title lang="en">Article Text</title>
</meta>
</property>
<property name="header_image" type="single_media_selection" colspan="12">
<meta>
<title lang="en">Image</title>
</meta>
<params>
<param name="types" value="image"/>
</params>
</property>
<section name="seo">
<meta>
<title lang="en">SEO</title>
</meta>
<properties>
<property name="seo_title" type="text_line" colspan="6">
<meta><title lang="en">SEO Title</title></meta>
</property>
<property name="seo_description" type="text_area" colspan="6">
<meta><title lang="en">SEO Description</title></meta>
</property>
</properties>
</section>
</properties>
</template>
Twig Template
{# templates/pages/article.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}{{ content.seo_title ?: content.title }} | {{ app.request.host }}{% endblock %}
{% block body %}
<article class="page-article">
<header class="article-header">
{% if content.header_image %}
{% set image = sulu_resolve_media(content.header_image, 'en') %}
<figure>
<img
src="{{ image.thumbnails['800x450'] }}"
srcset="{{ image.thumbnails['400x225'] }} 400w,
{{ image.thumbnails['800x450'] }} 800w"
alt="{{ image.title }}"
>
</figure>
{% endif %}
<h1>{{ content.title }}</h1>
</header>
<div class="article-body">
{{ content.article|raw }}
</div>
</article>
{% endblock %}
Custom Controller
// src/Controller/Website/ArticleListController.php
namespace App\Controller\Website;
use Sulu\Bundle\WebsiteBundle\Resolver\TemplateAttributeResolverInterface;
use Sulu\Component\Content\Compat\StructureInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class ArticleListController extends AbstractController
{
public function __construct(
private readonly TemplateAttributeResolverInterface $resolver,
private readonly ArticleRepository $repository
) {}
public function indexAction(StructureInterface $structure, bool $preview = false): Response
{
$page = max(1, (int) $this->container->get('request_stack')
->getCurrentRequest()
->query->get('page', 1));
$articles = $this->repository->findPublished(page: $page, limit: 12);
$attributes = $this->resolver->resolve([
'content' => $structure->getContent(),
'articles' => $articles,
'pagination' => [
'current' => $page,
'total' => $articles->getTotalPages(),
],
], $structure, true, $preview);
return $this->render('pages/article-list.html.twig', $attributes);
}
}
Sulu Media and Resize
{% set media = sulu_resolve_media(content.image, locale) %}
{# predefined format #}
<img src="{{ media.thumbnails['sulu-400x400'] }}" alt="{{ media.title }}">
{# arbitrary resize via URL parameters #}
<img src="{{ media.url }}?w=800&h=600&fm=webp&q=85" alt="">
Formats configured in config/packages/sulu.yaml:
sulu_media:
image_format_files:
- '%kernel.project_dir%/config/image-formats.xml'
<!-- config/image-formats.xml -->
<formats>
<format key="article-cover">
<commands>
<command>
<action>resize</action>
<parameters>
<parameter name="x">800</parameter>
<parameter name="y">450</parameter>
</parameters>
</command>
</commands>
</format>
</formats>
Smart Content and Auto Lists
<property name="articles" type="smart_content">
<meta><title lang="en">Articles</title></meta>
<params>
<param name="provider" value="pages"/>
<param name="types" value="article"/>
<param name="max_per_page" value="6"/>
<param name="page_parameter" value="p"/>
</params>
</property>
In template:
{% for article in content.articles %}
{% include 'snippets/article-card.html.twig' with { page: article } %}
{% endfor %}
Development Timelines
Corporate site with two languages, 5–8 page types, media manager and custom templates: 4–6 weeks. With custom controllers, Smart Content, external service integration: 6–10 weeks.







