Композиция шаблонов: переменная как готовый 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 своей части страницы. Значения приходят из пайплайна, который выполняется до рендера оркестратора.

Почему это работает — пайплайн движка

В справочнике по синтаксису прописан порядок раскрытия; для композиции важны такие шаги:

  1. strip-комментарии;
  2. извлечение #set-директив;
  3. merge переменных;
  4. раскрытие %var% ссылок;
  5. resolve перечислений {a|b|c};
  6. resolve permutations [a|b|c];
  7. пост-обработка.

Переменные раскрываются до 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 становится быстрее с каждым разом.


Продолжить серию