Composição de templates: variáveis como blocos de HTML já renderizados

Às vezes um template fica grande demais para conviver com ele. Centenas de itens <li> numa página de meios de pagamento, dezenas de observações editoriais inline, mudanças de ordenação que precisam se propagar por todas as variantes — em algum momento um único template gigante e aninhado vira o gargalo, não a ajuda. O passo seguinte é dividi-lo num pipeline de templates pequenos, ligados por variáveis que guardam HTML já renderizado.

A virada de chave

Até aqui, nesta série, uma variável foi um valor: um nome de marca, um ano, uma lista de recursos separada por vírgulas. Strings simples, substituídas no template na hora do render.

A virada deste guia é pequena e poderosa: o valor de uma variável pode ser HTML já resolvido. Não "Acme Co.", e sim <h3>Crypto deposits</h3><ul><li>BTC — fastest…</li>…</ul>. O resolvedor não se importa; ele só substitui.

É isso que libera a composição. Você monta a página a partir de um pipeline de subtemplates pequenos, cada um renderizado como um bloco de HTML, e depois montados por um orquestrador de poucas linhas.

Sem composição

<h3>Crypto deposits</h3>
<ul>
  <li>{Bitcoin|BTC}{fastest|the most popular} option, {confirms in 10–60 min|settles within an hour}.</li>
  <li>{Ethereum|ETH}{smart-contract chain|programmable network}, {2–5 min blocks|fast block times}.</li>
  /# … 8 more crypto items #/
</ul>
<h3>Fiat deposits</h3>
<ul>
  /# … 12 more fiat items, each with editorial notes #/
</ul>
<h3>Deposit and withdrawal limits</h3>
<table>
  /# … 20+ rows #/
</table>

Isso é um monólito de 200 linhas. Acrescentar uma moeda significa editar dentro de uma cadeia enorme de enumerações. Mudar a ordem é trabalho manual. As nuances editoriais de cada moeda ficam espalhadas pelo arquivo.

Com composição

%CryptoSection%
%FiatSection%
%LimitsSection%

Três linhas. Cada variável já guarda o HTML totalmente resolvido da sua parte da página. Os valores vêm de um pipeline que roda antes de o orquestrador ser renderizado.

Por que isso funciona — o pipeline do motor

A referência de sintaxe detalha a ordem de resolução; as linhas que importam para composição são:

  1. remover comentários;
  2. extrair as diretivas #set / #def;
  3. juntar variáveis;
  4. expandir as referências %var%;
  5. resolver enumerações {a|b|c};
  6. resolver permutações [a|b|c];
  7. pós-processar.

As variáveis expandem antes da resolução de enumerações e permutações. Quando essa etapa roda, %CryptoSection% já foi trocada pelo HTML que o montador calculou. Sem sintaxe especial — substituição de variável é literalmente troca de string.

Dá até para misturar camadas: uma permutação externa pode embaralhar seções pré-renderizadas.

[<sep="\n\n">%CryptoSection%|%FiatSection%|%LimitsSection%]

Cada seção é resolvida primeiro; depois a permutação reordena os blocos.

O pipeline de três níveis

O padrão vive em três camadas, cada uma um estágio de refinamento:

Nível 1 — Templates de item (por id)

A menor unidade reutilizável. Um template por item de dados: por moeda, por meio de pagamento, por plano, por entrada de FAQ, por SKU.

/# spintax.crypto_item.btc #/
<li>{Bitcoin|BTC}{fastest|the most popular} option, {confirms in 10–60 min|settles within an hour}.</li>

/# spintax.crypto_item.eth #/
<li>Ethereum — {smart-contract chain|programmable network}, {2–5 min blocks|fast block times}.</li>

Nível 2 — Templates de seção

Envolvem a lista com estrutura. Use uma variável de placeholder para os itens já juntados.

/# spintax.section.crypto #/
<h3>{Crypto deposits|Cryptocurrencies accepted}</h3>
<p>{Pick from|We support} the following coins:</p>
<ul>%CryptoItems%</ul>

%CryptoItems% é "todos os itens por id resolvidos e juntados numa única string". Quem monta isso é o montador.

Nível 3 — Orquestrador

O template no nível da página. Referencia apenas variáveis de seção pré-renderizadas.

/# spintax.payment_options #/
<h2>{Accepted payment methods|How to pay}</h2>
%CryptoSection%
%FiatSection%
%LimitsSection%

É o orquestrador inteiro. Regras de edição: mudou a descrição de uma moeda? Edite um template de item. Entrou uma moeda nova? Solte um template de item e acrescente o id à lista ativa. Reordenar? Campo de ordenação, não mudança de template.

Passo a passo — uma página de meios de pagamento

Um lojista aceita BTC, USDT e ETH em cripto, e Visa, Mastercard e SEPA em moeda comum. Três consultas e um punhado de templates produzem a página inteira.

Pseudocódigo do montador que roda antes do render do orquestrador:

function buildPaymentVars(merchantId, lang) {
  // 1. Pull active items, in display order.
  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. Resolve each per-id template, join the chunks.
  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. Resolve each section template with item placeholders.
  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. Limits section is similar; LimitsRows are joined <tr> chunks.
  const limitsSection = (cryptos.length || fiats.length)
    ? parser.process(templates.find("section.limits", lang), { LimitsRows: buildLimitsRows(cryptos, fiats, lang) })
    : "";

  // 5. Return the variables the orchestrator references.
  return {
    CryptoSection:  cryptoSection,
    FiatSection:    fiatSection,
    LimitsSection:  limitsSection,
    HasCrypto:      cryptos.length ? "1" : "",
    HasFiat:        fiats.length   ? "1" : "",
  };
}

O render do orquestrador então recebe essas variáveis junto com as normais de site e de runtime, e faz a passagem final.

Convenções de nome

Aqui a convenção vale mais que a liberdade, porque o montador acha os templates pelo id.

PadrãoExemplo
spintax.<entity>_item.<id>spintax.crypto_item.btc
spintax.<entity>_row.<id>spintax.crypto_row.btc (linha de tabela)
spintax.section.<key>spintax.section.crypto
spintax.<page-name>spintax.payment_options

As variáveis seguem o mesmo formato:

  • %CryptoItems%, %FiatItems%, %LimitsRows% — blocos por id, já juntados
  • %CryptoSection%, %FiatSection%, %LimitsSection% — seções resolvidas
  • %HasCrypto%, %HasFiat% — marcadores ('1' ou '')

PascalCase para variáveis, snake_case para IDs, só ASCII nos dois.

Armazenamento é problema seu, não do spintax

O padrão funciona igual, não importa onde os subtemplates moram:

  • tabela de banco (templates com id + corpo + lang)
  • arquivo JSON: { "crypto_item.btc": "<li>…</li>", … }
  • sistema de arquivos: templates/crypto_item/btc.txt
  • campo de CMS por locale

O motor não precisa de banco de dados. Ele só substitui HTML resolvido nas referências de variável. O montador é código seu, escrito no runtime que comanda os seus renders. Um plugin de WordPress, um Cloudflare Worker, um script Node, uma função no Postgres — mesmo padrão.

Por que não #include? O motor tem uma diretiva de include, e para um único bloco compartilhado ela é o caminho mais curto. Este pipeline não é construído sobre ela, por três motivos: um template incluído é um documento próprio — ele enxerga variáveis de runtime, mas nunca os #set/#def do pai, então uma forma já sorteada não pode ser passada para dentro dele; ele só funciona onde o host instalou um resolvedor, e dois dos nossos deliberadamente não têm um (o playground e o servidor MCP), onde a linha simplesmente fica literal; e quando o resolvedor não conhece a referência, a linha some da saída sem dizer nada. Um montador mantém resolução, cache e tratamento de erro no seu próprio código, onde você enxerga os três. Comportamento completo: a seção de includes da referência de sintaxe.

Nuances editoriais por id

É aqui que o padrão brilha. As nuances editoriais moram junto do dado, não em cada página.

/# spintax.payment_item.visa — 3DS warning baked in #/
<li>Visa — {3DS-protected|with 3D Secure} debit and credit cards, {instant deposit|immediate confirmation}.</li>

/# spintax.crypto_item.xrp — destination-tag reminder per coin #/
<li>XRP — fast and {cheap|low-fee}, {do not forget the destination tag|destination tag is required}.</li>

/# spintax.payment_item.qiwi — legacy status per method #/
<li>QIWI — {legacy support|now legacy}, {accepted but discouraged|not recommended for new accounts}.</li>

Cada template por id captura a nuance uma vez. Três páginas, dez páginas, mil páginas — todas herdam os avisos certos. Passe o QIWI para "descontinuado" editando um template; todo render vira ao mesmo tempo.

Sem composição, essas nuances seriam strings inline duplicadas entre páginas. Pesadelo de auditoria e risco jurídico de fogo lento em setores regulados.

Fallback condicional

Você vai ver gente tentando expressar condicionais com a sintaxe de enumeração do motor:

{%HasCrypto%|%HasFiat%||<p>Payment methods coming soon.</p>}

A esperança é "mostrar o fallback quando as duas flags estiverem vazias". A realidade com uma enumeração {a|b|c|d} comum: o motor escolhe um dos quatro ramos ao acaso, com probabilidade igual. A saída é não determinística e inclui "1" como variante possível na página.

Ramos de enumeração são sorteio uniforme — eles nunca olham para variáveis. Para escolha guiada por valor, use a passagem prévia condicional:

{?!HasCrypto?{?!HasFiat?<p>Payment methods coming soon.</p>}}

Leia assim: se não tem cripto e não tem moeda comum, renderize o fallback. O condicional resolve antes de o sorteador de ramos rodar, então a saída fica inteiramente determinada pelas variáveis.

Lógica composta, que o spintax condicional não suporta, fica no montador — comparações, &&/||, valores calculados. Pré-calcule uma variável de guarda e depois libere com {?Guard?…}. O guia dedicado de spintax condicional cobre em detalhe as três formas, a tabela de truthy, o pipeline de duas passagens e os antipadrões.

Quando NÃO compor

Composição tem custo — três tipos de template para manter, um montador para ligar, uma camada de armazenamento para organizar. O pipeline compensa quando:

  • você tem cinco ou mais itens parecidos, com a mesma estrutura;
  • existem nuances editoriais por id ou exigências de ordenação;
  • várias páginas reaproveitam o mesmo conjunto de itens;
  • quem escreve precisa alterar itens de forma independente.

Pule a composição quando:

  • a página tem de um a três itens no total;
  • os itens não se repetem entre páginas;
  • nada na estrutura vai mudar no próximo ano;
  • ninguém além de você vai editar.

Para uma página "sobre" pontual ou um artigo solto, um template autocontido é mais rápido, mais limpo e mais fácil de depurar.

Erros comuns

Não façaPor quêFaça
Compor uma página pequena (≤3 itens, sem variação editorial)O custo do pipeline é maior que a economia.Mantenha um template autocontido.
Codificar condicionais em enumerações de spintaxO motor sorteia, não olha valores; a saída é não determinística.Use {?VAR?then|else} para checagens de uma variável; calcule lógica composta no montador e libere com {?Guard?…}.
Colocar nuances de um id inline no orquestrador ou na seçãoPerde o benefício de "edite uma vez, propague em todo lugar".Mantenha as nuances no template _item daquele id.
Misturar preocupações de item e de seção num template sóRefatorar vira sofrimento conforme a página cresce.Três níveis limpos: item, seção, orquestrador.
Fixar a ordenação no orquestradorMudar a ordem exige editar páginas por todo o catálogo.Ordene no montador, por um único campo de ordenação em cada item.
Esquecer de curto-circuitar seções vaziasUm <h3> vazio, sem <ul> embaixo, vai para produção.Retorne "" do montador quando a lista de itens estiver vazia.
Confiar em %XxxItems% não resolvidos na página renderizadaVariável ausente significa placeholder sobrevivendo literalmente.Uma passagem de QA que sinalize qualquer %…% sobrando no HTML de produção.

Checklist de composição

  • Cada grupo de itens repetidos tem o seu template por id.
  • Cada seção tem um único template _section que referencia placeholders de item.
  • O orquestrador referencia apenas variáveis de seção, nunca variáveis de item.
  • A ordenação vem do dado, não do conteúdo do template.
  • Condicionais de uma variável usam {?VAR?…}; lógica composta fica no montador. Nunca ramos de enumeração.
  • Seções vazias produzem "", não marcação solta.
  • As nuances editoriais por id não estão duplicadas na seção nem no orquestrador.
  • Cinco renders de amostra leem bem nos casos "todas as categorias vazias", "só cripto", "só moeda comum", "todas as categorias presentes" e "um único item descontinuado".
  • Nenhum %…%, {…} ou […] sobrando em nenhum render.

Por ora, é o fim da série. Você tem a mentalidade, as variáveis, as permutações, a gramática e agora a composição. Volte à mentalidade quando começar o próximo artigo — o fluxo fica mais rápido a cada vez.


Continuar a série