Композиция шаблонов: переменная как готовый HTML-кусок
Иногда один шаблон становится слишком большим, чтобы с ним можно было жить. Сотни <li> в странице способов оплаты, десятки inline-редакторских пометок, изменение порядка сортировки, которое надо пробросить во все варианты — в какой-то момент один гигантский вложенный шаблон превращается из помощи в проблему. Следующий шаг — разбить его на пайплайн из маленьких шаблонов, связанных переменными, которые хранят уже отрендеренный 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 строк. Добавление монеты — правка внутри одной большой enum-цепочки. Изменение порядка — руками. Per-currency editorial nuances размазаны по файлу.
С композицией
%CryptoSection%
%FiatSection%
%LimitsSection%
Три строки. Каждая переменная уже хранит полностью разрешённый HTML своей части страницы. Значения приходят из пайплайна, который выполняется до рендера оркестратора.
Почему это работает — пайплайн движка
В справочнике по синтаксису прописан порядок раскрытия; для композиции важны такие шаги:
- strip-комментарии;
- извлечение
#set-директив; - merge переменных;
- раскрытие
%var%ссылок; - resolve перечислений
{a|b|c}; - resolve permutations
[a|b|c]; - пост-обработка.
Переменные раскрываются до resolve enum/perm. К моменту, когда дойдёт до них, %CryptoSection% уже подменён на тот HTML, который собрал ваш ассемблер. Никакого специального синтаксиса — это буквально подстановка строки.
Можно даже смешивать слои: внешний permutation способен перетасовать предрендеренные секции.
[<sep="\n\n">%CryptoSection%|%FiatSection%|%LimitsSection%]
Каждая секция сначала разрешается полностью, потом permutation меняет местами уже готовые куски.
Три уровня пайплайна
Паттерн живёт на трёх слоях, каждый — стадия уточнения:
Уровень 1 — Item-шаблоны (по id)
Минимальная переиспользуемая единица. Один шаблон на единицу данных: на монету, на платёжный метод, на тариф, на FAQ, на SKU.
/# spintax.crypto_item.btc #/
<li>{Bitcoin|BTC} — {самый быстрый|самый популярный} вариант, {подтверждение за 10–60 мин|обычно проходит за час}.</li>
/# spintax.crypto_item.eth #/
<li>Ethereum — {смарт-контрактная сеть|программируемая платформа}, {2–5 мин на блок|быстрые блоки}.</li>
Уровень 2 — Section-шаблоны
Оборачивают список в структуру. Используют переменную-плейсхолдер для склеенных элементов.
/# spintax.section.crypto #/
<h3>{Криптодепозиты|Принимаемые криптовалюты}</h3>
<p>{Доступны|Поддерживаются} следующие монеты:</p>
<ul>%CryptoItems%</ul>
%CryptoItems% — это «все per-id item'ы, разрешённые и склеенные в одну строку». Сборкой занимается ассемблер.
Уровень 3 — Оркестратор
Шаблон уровня страницы. Ссылается только на переменные с готовыми секциями.
/# spintax.payment_options #/
<h2>{Принимаемые способы оплаты|Как платить}</h2>
%CryptoSection%
%FiatSection%
%LimitsSection%
Это весь оркестратор. Правила редактирования: поменять описание монеты — правишь один item-шаблон. Добавить новую валюту — кладёшь новый item-шаблон, добавляешь id в активный список. Перестановка — поле сортировки, не правка шаблона.
Walkthrough — страница способов оплаты
Магазин принимает BTC, USDT, ETH из крипты и Visa, Mastercard, SEPA из фиата. Три запроса в БД и горстка шаблонов выдают всю страницу.
Псевдокод ассемблера, который запускается до рендера оркестратора:
function buildPaymentVars(merchantId, lang) {
// 1. Тащим активные item'ы в порядке отображения.
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. Резолвим каждый per-id шаблон, склеиваем куски.
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. Резолвим section-шаблоны с подставленными плейсхолдерами.
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" : "",
};
}
Финальный рендер оркестратора получает эти переменные вместе с обычными site/runtime-переменными и делает последний проход.
Соглашения об именах
Здесь конвенция важнее свободы — ассемблер находит шаблоны по 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%— склеенные per-id куски%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 в ссылки на переменные. Ассемблер — это ваш код, на любом runtime, который рендерит страницы. WordPress-плагин, Cloudflare Worker, Node-скрипт, Postgres-функция — паттерн один.
Per-id editorial nuances
Это и есть киллер-фича. Редакторские нюансы живут вместе с данными, а не в каждой странице.
/# 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>
Каждый per-id шаблон фиксирует нюанс один раз. Три страницы, десять, тысяча — все наследуют правильные предупреждения. Перевести QIWI в «deprecated» — правишь один шаблон; все рендеры переключаются одновременно.
Без композиции эти нюансы превратились бы в inline-строки, размноженные по страницам. Аудит-кошмар, а в регулируемых индустриях ещё и медленно тлеющий правовой риск.
Quasi-IF и его пределы
Иногда авторы пытаются выразить условие через enum-синтаксис движка:
{%HasCrypto%|%HasFiat%||<p>Способы оплаты появятся скоро.</p>}
Надежда: «показывать fallback, когда оба флага пусты». Реальность: движок выбирает одну из четырёх веток с равной вероятностью. Результат недетерминированный, и среди возможных исходов на странице — текст "1".
Spintax не умеет вычислять условия. Выбор ветки — равномерный random.
Реальное условие делается в ассемблере:
const HasCrypto = cryptos.length > 0 ? "1" : "";
const HasFiat = fiats.length > 0 ? "1" : "";
const PaymentsSummary = (HasCrypto || HasFiat)
? ""
: "<p>Способы оплаты появятся скоро.</p>";
// оркестратор ссылается на %PaymentsSummary% напрямую
Одна строка в ассемблере. Детерминированно. Тестируемо. Чисто ложится в композицию.
Когда НЕ нужна композиция
У композиции есть накладные расходы — три типа шаблонов сопровождать, ассемблер связывать, слой хранилища выстраивать. Пайплайн оправдан, когда:
- есть пять и больше похожих item'ов с одинаковой структурой;
- нужны per-id editorial nuances или сортировка;
- один и тот же набор item'ов переиспользуется на разных страницах;
- редакторам нужно править item'ы независимо.
Не композируйте, если:
- на странице 1–3 item'а;
- item'ы не повторяются между страницами;
- структура не меняется и в ближайший год не будет;
- кроме вас её никто не правит.
Для разовой страницы «о нас» или одной статьи самодостаточный шаблон быстрее, чище и легче дебажится.
Типичные ошибки
| Не надо | Почему | Вместо этого |
|---|---|---|
| Композиция для маленькой страницы (≤3 элемента, без editorial nuances) | Накладные расходы пайплайна выше выигрыша. | Один самодостаточный шаблон. |
| Кодировать условия через enum-ветки | Движок выбирает случайно, не по значению; результат недетерминированный. | Считать условие в ассемблере и передавать готовое значение переменной. |
| Inline per-id nuances в orchestrator или section | Теряется выгода «правка в одном месте — пропагация везде». | Нюансы хранить в per-id _item-шаблоне. |
| Смешивать item-уровень и section-уровень в одном шаблоне | Рефакторинг становится больным по мере роста страницы. | Три чистых уровня: item, section, orchestrator. |
| Хардкодить порядок сортировки в orchestrator | Изменение порядка требует правок страниц по всему каталогу. | Сортировка в ассемблере по полю каждого элемента. |
| Забыть закоротить пустые секции | Пустой <h3> без <ul> уезжает в прод. | Возвращать "" из ассемблера, когда список пустой. |
Доверять оставшимся %XxxItems% в готовой странице | Отсутствующая переменная значит, что плейсхолдер выжил буквально. | QA-проход, проверяющий любые оставшиеся %…% в финальном HTML. |
Чеклист композиции
- Каждая группа повторяющихся элементов имеет свой per-id шаблон.
- Каждая секция имеет один
_section-шаблон, который ссылается на плейсхолдеры элементов. - Оркестратор ссылается только на section-переменные, не на item-переменные.
- Порядок сортировки берётся из данных, а не из контента шаблона.
- Условия (if/else) живут в ассемблере, а не в spintax-перечислениях.
- Пустые секции возвращают
"", а не остатки разметки. - Per-id editorial nuances не дублируются в section или orchestrator.
- Пять сэмплов рендера читаются чисто на «все категории пусты», «только крипта», «только фиат», «все категории присутствуют», «один legacy-элемент».
- Ни в одном рендере нет оставшихся
%…%,{…},[…].
На этом серия пока завершена. У вас есть mindset, переменные, permutations, грамматика и теперь композиция. Возвращайтесь к ментальной модели в начале следующей статьи — workflow становится быстрее с каждым разом.