Розробка користувацького шаблону Sulu
Шаблон в Sulu складається з двох частин: XML-описання (що редагує менеджер) та Twig-шаблона (як це відображається). XML реєструє тип сторінки в системі, Twig виконує контент. Не потрібно писати контролер — Sulu використовує DefaultController, або напишіть користувацький для додання даних.
Повний XML-шаблон
<!-- config/templates/service.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>service</key>
<view>pages/service</view>
<controller>App\Controller\Website\ServiceController::indexAction</controller>
<cacheLifetime type="seconds">1800</cacheLifetime>
<properties>
<property name="title" type="text_line" mandatory="true">
<meta><title lang="uk">Заголовок</title></meta>
<tag name="sulu.rlp.part"/>
</property>
<property name="intro" type="text_area" colspan="12">
<meta><title lang="uk">Вступний текст</title></meta>
<params>
<param name="rows" value="3"/>
</params>
</property>
<block name="content_blocks" default-type="text" colspan="12">
<meta><title lang="uk">Блоки контенту</title></meta>
<types>
<type name="text">
<meta><title lang="uk">Текст</title></meta>
<properties>
<property name="text" type="text_editor">
<meta><title lang="uk">Текст</title></meta>
</property>
</properties>
</type>
<type name="image_text">
<meta><title lang="uk">Зображення + текст</title></meta>
<properties>
<property name="image" type="single_media_selection">
<meta><title lang="uk">Зображення</title></meta>
</property>
<property name="text" type="text_editor">
<meta><title lang="uk">Текст</title></meta>
</property>
<property name="image_position" type="select">
<meta><title lang="uk">Позиція зображення</title></meta>
<params>
<param name="values" type="collection">
<param name="left" title="Ліворуч"/>
<param name="right" title="Праворуч"/>
</param>
</params>
</property>
</properties>
</type>
<type name="cta">
<meta><title lang="uk">Заклик до дії</title></meta>
<properties>
<property name="heading" type="text_line">
<meta><title lang="uk">Заголовок</title></meta>
</property>
<property name="button_text" type="text_line">
<meta><title lang="uk">Текст кнопки</title></meta>
</property>
<property name="button_link" type="text_line">
<meta><title lang="uk">Посилання</title></meta>
</property>
</properties>
</type>
</types>
</block>
<section name="sidebar">
<meta><title lang="uk">Бічна панель</title></meta>
<properties>
<property name="show_form" type="checkbox">
<meta><title lang="uk">Показати форму зворотного зв'язку</title></meta>
<params>
<param name="defaultValue" value="true"/>
</params>
</property>
<property name="related_services" type="smart_content">
<meta><title lang="uk">Подібні послуги</title></meta>
<params>
<param name="provider" value="pages"/>
<param name="types" value="service"/>
<param name="max_per_page" value="4"/>
</params>
</property>
</properties>
</section>
</properties>
</template>
Twig-шаблон
{# templates/pages/service.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}{{ content.title }} | {{ app.request.host }}{% endblock %}
{% block body %}
<div class="page-service">
<div class="container">
<header class="page-header">
<h1>{{ content.title }}</h1>
{% if content.intro %}
<p class="lead">{{ content.intro }}</p>
{% endif %}
</header>
<div class="service-layout {% if content.show_form %}service-layout--with-sidebar{% endif %}">
<main class="service-content">
{% for block in content.content_blocks %}
{% if block.type == 'text' %}
<div class="block block--text">
{{ block.text|raw }}
</div>
{% elseif block.type == 'image_text' %}
{% set img = sulu_resolve_media(block.image, locale) %}
<div class="block block--image-text block--image-{{ block.image_position }}">
{% if img %}
<figure>
<img
src="{{ img.thumbnails['service-block'] }}"
alt="{{ img.title }}"
loading="lazy"
>
</figure>
{% endif %}
<div class="block__text">{{ block.text|raw }}</div>
</div>
{% elseif block.type == 'cta' %}
<div class="block block--cta">
{% if block.heading %}
<h2>{{ block.heading }}</h2>
{% endif %}
<a href="{{ block.button_link }}" class="btn btn--primary">
{{ block.button_text }}
</a>
</div>
{% endif %}
{% endfor %}
</main>
{% if content.show_form %}
<aside class="service-sidebar">
{% include 'snippets/contact-form.html.twig' %}
{% if content.related_services %}
<div class="related-services">
<h3>Подібні послуги</h3>
{% for service in content.related_services %}
<a href="{{ service.url }}" class="related-link">
{{ service.title }}
</a>
{% endfor %}
</div>
{% endif %}
</aside>
{% endif %}
</div>
</div>
</div>
{% endblock %}
Користувацький контролер
// src/Controller/Website/ServiceController.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 ServiceController extends AbstractController
{
public function __construct(
private readonly TemplateAttributeResolverInterface $resolver,
private readonly TestimonialRepository $testimonials
) {}
public function indexAction(
StructureInterface $structure,
bool $preview = false,
bool $partial = false
): Response {
$attributes = $this->resolver->resolve(
[
'content' => $structure->getContent(),
'testimonials' => $this->testimonials->findByService(
$structure->getUuid(),
limit: 3
),
],
$structure,
!$partial,
$preview
);
$view = $partial ? 'pages/service_partial.html.twig' : 'pages/service.html.twig';
return $this->render($view, $attributes);
}
}
Базовий шаблон та блоки Twig
{# templates/base.html.twig #}
<!DOCTYPE html>
<html lang="{{ app.request.locale }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ sulu_content_path('/', webspace, locale) }}{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
{% endblock %}
</head>
<body>
{% block header %}
{% include 'snippets/header.html.twig' %}
{% endblock %}
{% block body %}{% endblock %}
{% block footer %}
{% include 'snippets/footer.html.twig' %}
{% endblock %}
{% block javascripts %}
<script src="{{ asset('build/app.js') }}" defer></script>
{% endblock %}
</body>
</html>
Розширення Sulu Twig
{# навігація #}
{% set navigation = sulu_navigation_root_flat('main', 3) %}
{% for item in navigation %}
<a href="{{ item.url }}"
class="{{ item.uuid == content.uuid ? 'active' : '' }}">
{{ item.title }}
</a>
{% endfor %}
{# навігаційна ціль #}
{% set breadcrumb = sulu_breadcrumb() %}
{% for crumb in breadcrumb %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% endfor %}
{# фрагмент з системи фрагментів #}
{{ sulu_snippet('footer_info', 'default', locale)|raw }}
{# URL сторінки за UUID #}
<a href="{{ sulu_content_path(content.link, webspace, locale) }}">Посилання</a>
Реєстрація шаблону в Webspace
<!-- config/packages/webspaces/example.xml -->
<templates>
<template type="page">service</template>
</templates>
Після додання шаблону — очистити кеш та переіндексувати:
php bin/console cache:clear
php bin/console sulu:document:initialize
Часові рамки
Один шаблон з блочним редактором, користувацьким контролером та Twig: 2–3 дні. Повний набір шаблонів (5–8 типів) для корпоративного сайту: 1,5–2 тижні.







