5. Как сделать тему
⚠️ Целевая модель.Сейчас витрина — SSR-Razor с CSS-темами (19 шт., меняют вид, но не компоновку). Этот раздел описывает, как темы должны работать — Liquid-бандлы в одном рантайме. По нему реализуем рантайм тем.
Тема = бандл файлов (шаблоны + стили + скрипты + настройки), который рантайм витрины исполняет на объектной модели. Тема владеет видом витринных экранов; корзину/оплату/запись/подсчёты держит платформа.
Структура темы
theme/
theme.json # манифест: имя, вертикаль, версия, вход
templates/ # страницы (Liquid)
home.liquid
category.liquid
product.liquid
cart.liquid
booking.liquid
sections/ # переиспользуемые блоки
header.liquid hero.liquid product-card.liquid footer.liquid
assets/
theme.css theme.js # свои стили/скрипты (без внешних CDN)
settings.json # схема настроек, редактируемых владельцем
checkout.json # темизация checkout: токены + разрешённые блоки
Наследование: тема переопределяет только нужные файлы; отсутствующие берутся из базовой темы. «Лёгкая» тема = home.liquid + product-card.liquid + theme.css.
Шаблоны и объекты
Каждый templates/*.liquid получает контекст своей страницы (таблица контекста). Пример карточки:
{% for p in products %}
<a class="card" href="/p/{{ p.slug | default: p.id }}">
<img src="{{ p.imageUrl }}" alt="{{ p.name }}">
<div class="card__name">{{ p.name }}</div>
<div class="card__price">
{{ p.effectivePrice }} {{ shop.currency }}
{% if p.oldPrice %}<s>{{ p.oldPrice }}</s>{% endif %}
</div>
{% if p.type == 'product' and shop.flags.hasOrders %}
<button data-sf-add="{{ p.id }}">В корзину</button>
{% endif %}
</a>
{% endfor %}
Liquid — песочница: нет доступа к файлам, БД, сети, C#. Только объекты контекста + фильтры (форматирование, не вычисление денег).
Поведение: JS-API рантайма
Динамику (корзина, варианты, слоты) тема делает через готовый JS рантайма — не реализует логику сама. Два способа:
1. Data-атрибуты (декларативно, без кода):
<button data-sf-add="{{ p.id }}">В корзину</button>
<button data-sf-qty="+1">+</button>
<span data-sf-cart-count></span>
Рантайм сам вешает обработчики, обновляет корзину, перерисовывает счётчик.
2. JS-API (императивно):
sf.cart.add(itemId, qty) // → Promise<cart>
sf.cart.setQty(itemId, qty)
sf.cart.get()
sf.product.selectVariant(options) // {Цвет:'Красный',Размер:'M'} → variant
sf.booking.availability(serviceId, staffId, date)
sf.booking.hold(slot)
Все мутации идут в API рантайма (same-origin) → ядро добавляет ключ. Итоги возвращает сервер; тема их только показывает.
Checkout — токены + блоки, не разметка
Checkout платформенный (безопасность/PCI). Тема влияет через checkout.json:
- Токены: палитра, шрифты, скругления, лого — checkout наследует бренд.
- Блоки: вставить свои элементы в разрешённые слоты (баннер доверия, промо, поле «комментарий»).
- Разметку платёжных полей тема НЕ трогает.
Настройки темы (для владельца)
settings.json описывает, что владелец меняет в панели без кода:
[
{"key":"accent","type":"color","label":"Акцент","default":"#4F46E5"},
{"key":"grid","type":"select","label":"Сетка","options":["cozy","standard","compact"]},
{"key":"heroImage","type":"image","label":"Заглавная картинка"}
]
Значения приходят в Liquid как settings.accent и т.п.
Ограничения (песочница)
Тема не может: выполнять серверный код; ходить в ядро напрямую или к сторонним хостам с сервера; считать/менять цены, итоги, остатки; трогать разметку/логику оплаты; читать данные чужих магазинов. Всё это — платформа.
Чек-лист «моя тема готова»
-
home / category / productрендерятся из объектов контекста - карточка ведёт на
/p/{slug}, add-to-cart черезdata-sf-add/sf.cart - корзина показывает
cart.linesиcart.total(с сервера) - услуги (если
hasServices) ведут в запись -
checkout.jsonзадаёт токены бренда - нет внешних CDN, нет попыток считать деньги на клиенте