Композиция шаблонов: переменная как готовая часть страницы
Иногда один шаблон разрастается настолько, что работать с ним уже невозможно. Сотни <li> на странице способов оплаты, десятки редакторских пометок прямо в разметке, изменение порядка сортировки, которое надо пробросить во все варианты — в какой-то момент один гигантский вложенный шаблон превращается из помощи в проблему. Следующий шаг — разбить его на пайплайн из маленьких шаблонов, связанных переменными, которые хранят уже отрендеренный HTML.
Смена оптики
До этого момента в серии переменная была значением: имя бренда, год, перечень возможностей через запятую. Обычные строки, подставляемые в шаблон при рендере.
Сдвиг небольшой, но сильный: значением переменной может быть уже отрендеренный фрагмент HTML. Не "Acme Co.", а <h3>Криптодепозиты</h3><ul><li>BTC — самый быстрый…</li>…</ul>. Движку всё равно — он просто подставляет строку.
Это и открывает композицию. Страница собирается из пайплайна маленьких шаблонов: каждый рендерится в свой фрагмент HTML, а сводит их вместе оркестратор — шаблон в несколько строк.
Без композиции
<h3>Криптодепозиты</h3>
<ul>
<li>{Bitcoin|BTC} — {самый быстрый|самый популярный} вариант, {подтверждение за 10–60 мин|обычно проходит за час}.</li>
<li>{Ethereum|ETH} — {смарт-контрактная сеть|программируемая платформа}, {2–5 мин на блок|быстрые блоки}.</li>
/# … ещё 8 криптомонет #/
</ul>
<h3>Фиатные депозиты</h3>
<ul>
/# … ещё 12 фиатных методов с редакторскими пометками #/
</ul>
<h3>Лимиты на ввод и вывод</h3>
<table>
/# … 20+ строк #/
</table>
Это монолит на 200 строк. Добавить монету — значит править внутри одной огромной цепочки вариантов. Поменять порядок — руками. Нюансы по каждой валюте размазаны по всему файлу.
С композицией
%CryptoSection%
%FiatSection%
%LimitsSection%
Три строки. Каждая переменная уже хранит готовый HTML своей части страницы. Значения приходят из пайплайна, который выполняется до рендера оркестратора.
Почему это работает — пайплайн движка
В справочнике по синтаксису прописан порядок раскрытия; для композиции важны такие шаги:
- удаление комментариев;
- сбор директив
#setи#def; - слияние переменных;
- подстановка ссылок вида
%var%; - выбор варианта в перечислениях
{a|b|c}; - раскрытие перестановок
[a|b|c]; - постобработка.
Переменные раскрываются до перечислений и перестановок. К моменту, когда дойдёт до них, %CryptoSection% уже подменён на тот HTML, который собрал ваш ассемблер. Никакого специального синтаксиса — это буквально подстановка строки.
Слои можно смешивать: внешняя перестановка перетасует уже отрендеренные секции.
[<sep="\n\n">%CryptoSection%|%FiatSection%|%LimitsSection%]
Каждая секция сначала рендерится целиком, а перестановка уже меняет местами готовые фрагменты.
Три уровня пайплайна
Паттерн живёт на трёх слоях, и каждый следующий собирает результат предыдущего:
Уровень 1 — шаблоны элементов (по id)
Наименьшая единица, которую можно переиспользовать. Один шаблон на одну запись: на монету, на способ оплаты, на тариф, на вопрос в FAQ, на товар.
/# spintax.crypto_item.btc #/
<li>{Bitcoin|BTC} — {самый быстрый|самый популярный} вариант, {подтверждение за 10–60 мин|обычно проходит за час}.</li>
/# spintax.crypto_item.eth #/
<li>Ethereum — {смарт-контрактная сеть|программируемая платформа}, {2–5 мин на блок|быстрые блоки}.</li>
Уровень 2 — шаблоны секций
Оборачивают список в структуру и подставляют склеенные элементы через переменную-плейсхолдер.
/# spintax.section.crypto #/
<h3>{Криптодепозиты|Принимаемые криптовалюты}</h3>
<p>{Доступны|Поддерживаются} следующие монеты:</p>
<ul>%CryptoItems%</ul>
%CryptoItems% — это «все элементы, отрендеренные и склеенные в одну строку». Сборкой занимается ассемблер.
Уровень 3 — оркестратор
Шаблон уровня страницы. Ссылается только на переменные с готовыми секциями.
/# spintax.payment_options #/
<h2>{Принимаемые способы оплаты|Как платить}</h2>
%CryptoSection%
%FiatSection%
%LimitsSection%
Это весь оркестратор. Правила редактирования: поменять описание монеты — правишь один шаблон элемента. Добавить новую валюту — кладёшь новый шаблон и добавляешь id в список активных. Порядок — это поле сортировки, а не правка шаблона.
Разбор — страница способов оплаты
Магазин принимает BTC, USDT, ETH из крипты и Visa, Mastercard, SEPA из фиата. Три запроса в БД и горстка шаблонов собирают всю страницу.
Псевдокод ассемблера, который запускается до рендера оркестратора:
function buildPaymentVars(merchantId, lang) {
// 1. Берём активные элементы в порядке отображения.
const cryptos = db.query("active cryptos for merchant ordered by sort", merchantId);
const fiats = db.query("active fiats for merchant ordered by sort", merchantId);
// 2. Рендерим шаблон каждого элемента и склеиваем результаты.
const cryptoItems = cryptos
.map(c => parser.process(templates.find(`crypto_item.${c.id}`, lang)))
.join("");
const fiatItems = fiats
.map(f => parser.process(templates.find(`payment_item.${f.id}`, lang)))
.join("");
// 3. Рендерим шаблоны секций с подставленными плейсхолдерами.
const cryptoSection = cryptos.length
? parser.process(templates.find("section.crypto", lang), { CryptoItems: cryptoItems })
: "";
const fiatSection = fiats.length
? parser.process(templates.find("section.fiat", lang), { FiatItems: fiatItems })
: "";
// 4. Лимиты собираются так же; LimitsRows — склеенные <tr>.
const limitsSection = (cryptos.length || fiats.length)
? parser.process(templates.find("section.limits", lang), { LimitsRows: buildLimitsRows(cryptos, fiats, lang) })
: "";
// 5. Возвращаем переменные, на которые ссылается оркестратор.
return {
CryptoSection: cryptoSection,
FiatSection: fiatSection,
LimitsSection: limitsSection,
HasCrypto: cryptos.length ? "1" : "",
HasFiat: fiats.length ? "1" : "",
};
}
Финальный рендер оркестратора получает эти переменные вместе с обычными переменными сайта и рантайма и делает последний проход.
Соглашения об именах
Здесь конвенция важнее свободы — ассемблер находит шаблоны по id.
| Паттерн | Пример |
|---|---|
spintax.<entity>_item.<id> | spintax.crypto_item.btc |
spintax.<entity>_row.<id> | spintax.crypto_row.btc (строка таблицы) |
spintax.section.<key> | spintax.section.crypto |
spintax.<page-name> | spintax.payment_options |
Переменные той же формы:
%CryptoItems%,%FiatItems%,%LimitsRows%— склеенные фрагменты элементов%CryptoSection%,%FiatSection%,%LimitsSection%— отрендеренные секции%HasCrypto%,%HasFiat%— маркеры ('1'или'')
PascalCase для переменных, snake_case для id, ASCII-only для обоих.
Хранилище — ваша задача, не движка
Паттерн работает одинаково, где бы ни лежали маленькие шаблоны:
- таблица БД (
templatesс id + body + lang) - JSON-файл:
{ "crypto_item.btc": "<li>…</li>", … } - файловая система:
templates/crypto_item/btc.txt - CMS-поле на каждую локаль
Движку БД не нужна. Он просто подставляет готовый HTML вместо ссылок на переменные. Ассемблер — это ваш код в той среде, где рендерятся страницы. WordPress-плагин, Cloudflare Worker, Node-скрипт, Postgres-функция — паттерн один.
Редакторские нюансы на уровне элемента
Это и есть главная выгода: нюансы живут вместе с данными, а не переписываются на каждой странице.
/# spintax.payment_item.visa — 3DS-предупреждение встроено #/
<li>Visa — {с защитой 3D Secure|с 3DS-проверкой} дебетовые и кредитные карты, {мгновенный депозит|немедленное подтверждение}.</li>
/# spintax.crypto_item.xrp — напоминание про destination tag в каждой монете #/
<li>XRP — быстро и {дёшево|с минимальными комиссиями}, {не забудьте destination tag|destination tag обязателен}.</li>
/# spintax.payment_item.qiwi — статус legacy в каждом методе #/
<li>QIWI — {поддержка legacy|устаревший вариант}, {принимается, но не рекомендуется|новым аккаунтам не советуем}.</li>
Шаблон элемента фиксирует нюанс один раз. Три страницы, десять, тысяча — все получают правильные предупреждения. Пометить QIWI устаревшим — правишь один шаблон, и все рендеры переключаются разом.
Без композиции эти нюансы расползлись бы по страницам отдельными строками, вписанными руками. Кошмар при аудите, а в регулируемых отраслях — ещё и тлеющий юридический риск.
Условный запасной вариант
Иногда авторы пытаются выразить условие через синтаксис перечислений:
{%HasCrypto%|%HasFiat%||<p>Способы оплаты появятся скоро.</p>}
Надежда: «показывать запасной блок, когда оба флага пусты». Реальность с обычным {a|b|c|d}: движок выбирает одну из четырёх веток равновероятно. Результат недетерминированный, и среди возможных исходов на странице — текст "1".
Ветки перечисления выбираются равновероятно и на переменные не смотрят. Для выбора по значению есть условный пред-проход:
{?!HasCrypto?{?!HasFiat?<p>Способы оплаты появятся скоро.</p>}}
Читается как: если нет ни крипты, ни фиата — рендерим запасной абзац. Условие раскрывается до случайного выбора веток, поэтому вывод полностью определён переменными.
Составная логика, которую условный spintax не выражает — сравнения, &&/||, вычисления — остаётся в ассемблере. Посчитайте guard-переменную там, а в шаблоне включайте блок через {?Guard?…}. Подробнее — в отдельном руководстве по условному синтаксису: три формы, таблица truthy, двухпроходный пайплайн, анти-паттерны.
Когда НЕ нужна композиция
У композиции есть цена: три типа шаблонов, которые нужно сопровождать, ассемблер, который нужно написать, и хранилище, которое нужно продумать. Пайплайн оправдан, когда:
- есть пять и больше однотипных элементов с одинаковой структурой;
- нужны редакторские нюансы на уровне элемента или сортировка;
- один и тот же набор элементов переиспользуется на разных страницах;
- редакторам нужно править элементы независимо друг от друга.
Не разбивайте шаблон, если:
- на странице один-три элемента;
- элементы не повторяются между страницами;
- структура не меняется и в ближайший год не изменится;
- кроме вас её никто не правит.
Для разовой страницы «о нас» или одной статьи самодостаточный шаблон быстрее, чище и проще в отладке.
Типичные ошибки
| Не надо | Почему | Вместо этого |
|---|---|---|
| Композиция для маленькой страницы (≤3 элемента, без редакторских нюансов) | Накладные расходы пайплайна выше выигрыша. | Один самодостаточный шаблон. |
| Кодировать условия ветками перечисления | Движок выбирает случайно, не по значению; результат недетерминированный. | Для проверки одной переменной — {?VAR?then|else}. Составную логику считать в ассемблере и включать через {?Guard?…}. |
| Вписывать нюансы элемента прямо в оркестратор или секцию | Теряется главное: правишь в одном месте — меняется везде. | Нюансы держать в шаблоне элемента _item. |
| Смешивать уровень элемента и уровень секции в одном шаблоне | Чем больше страница, тем больнее рефакторинг. | Три чистых уровня: элемент, секция, оркестратор. |
| Зашивать порядок сортировки в оркестратор | Смена порядка потребует правок на всех страницах каталога. | Сортировка в ассемблере по полю каждого элемента. |
| Забыть схлопнуть пустые секции | Пустой <h3> без <ul> уезжает в прод. | Возвращать "" из ассемблера, когда список пуст. |
Не проверять, не остались ли %XxxItems% в готовой странице | Если переменной не оказалось, плейсхолдер уезжает в вёрстку как есть. | Проверка на выходе: не осталось ли %…% в финальном HTML. |
Чеклист композиции
- У каждой группы повторяющихся элементов есть свой шаблон, привязанный к id.
- У каждой секции один шаблон
_section, который ссылается на плейсхолдеры элементов. - Оркестратор ссылается только на переменные-секции, не на переменные элементов.
- Порядок сортировки берётся из данных, а не из текста шаблона.
- Проверка одной переменной — через
{?VAR?…}; составная логика — в ассемблере, и никогда в ветках перечисления. - Пустые секции возвращают
"", а не остатки разметки. - Редакторские нюансы элемента не дублируются в секции и оркестраторе.
- Пять пробных рендеров читаются гладко: все категории пусты; только крипта; только фиат; все категории на месте; один устаревший элемент.
- Ни в одном рендере нет оставшихся
%…%,{…},[…].
На этом серия пока завершена. У вас есть подход, переменные, перестановки, грамматика и теперь композиция. Возвращайтесь к ментальной модели в начале следующей статьи — с каждым разом работа идёт быстрее.