Composición de plantillas: variables que llevan HTML ya renderizado

A veces una plantilla se hace demasiado grande para convivir con ella. Cientos de elementos <li> en una página de métodos de pago, decenas de apuntes editoriales en línea, cambios de orden que tienen que propagarse por cada variante — en algún momento una única plantilla gigante y anidada pasa de ayuda a cuello de botella. El siguiente paso es partirla en una tubería de plantillas pequeñas, unidas por variables que llevan HTML ya renderizado.

El cambio de mirada

Hasta aquí, en esta serie una variable ha sido un valor: un nombre de marca, un año, una lista de funciones separada por comas. Cadenas simples, sustituidas en la plantilla al renderizar.

El cambio de esta guía es pequeño y potente: el valor de una variable puede ser HTML ya resuelto. No "Acme Co.", sino <h3>Crypto deposits</h3><ul><li>BTC — fastest…</li>…</ul>. Al resolutor le da igual; simplemente sustituye.

Eso es lo que desbloquea la composición. Construyes la página a partir de una tubería de subplantillas pequeñas, cada una renderizada a un bloque de HTML, y las ensamblas con un orquestador de unas pocas líneas.

Sin composición

<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>

Eso es un monolito de 200 líneas. Añadir una moneda significa editar dentro de una cadena enorme de enumeraciones. Cambiar el orden es trabajo manual. Los matices editoriales por divisa quedan dispersos por todo el archivo.

Con composición

%CryptoSection%
%FiatSection%
%LimitsSection%

Tres líneas. Cada variable ya lleva el HTML totalmente resuelto de su parte de la página. Los valores vienen de una tubería que corre antes de renderizar el orquestador.

Por qué funciona — la tubería del motor

La referencia de sintaxis detalla el orden de resolución; las líneas que importan para la composición son:

  1. quitar comentarios;
  2. extraer las directivas #set / #def;
  3. combinar variables;
  4. expandir las referencias %var%;
  5. resolver enumeraciones {a|b|c};
  6. resolver permutaciones [a|b|c];
  7. posprocesar.

Las variables se expanden antes de resolver enumeraciones y permutaciones. Cuando corre esa etapa, %CryptoSection% ya está sustituida por el HTML que calculó el ensamblador. Sin sintaxis especial — sustituir una variable es literalmente reemplazar una cadena.

Incluso puedes mezclar capas: una permutación externa puede barajar secciones pre-renderizadas.

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

Cada sección se resuelve primero; luego la permutación reordena los bloques.

La tubería de tres niveles

El patrón vive en tres capas, cada una una etapa de refinamiento:

Nivel 1 — Plantillas de elemento (por id)

La unidad reutilizable más pequeña. Una plantilla por dato: por moneda, por método de pago, por nivel de plan, por entrada de FAQ, por referencia de producto.

/# 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>

Nivel 2 — Plantillas de sección

Envuelven la lista con estructura. Para los elementos ya unidos, usa una variable de marcador.

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

%CryptoItems% es «todos los elementos por id, resueltos y unidos en una sola cadena». Lo construye el ensamblador.

Nivel 3 — Orquestador

La plantilla a nivel de página. Solo referencia variables de sección pre-renderizadas.

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

Ese es el orquestador entero. Las reglas al editar: ¿cambia la descripción de una moneda? Se toca una plantilla de elemento. ¿Entra una divisa nueva? Se deja una plantilla de elemento y se añade el id a la lista activa. ¿Reordenar? Campo de orden, no cambio de plantilla.

Paso a paso — una página de métodos de pago

Un comercio acepta BTC, USDT y ETH en cripto, y Visa, Mastercard y SEPA en dinero corriente. Tres consultas y un puñado de plantillas producen la página entera.

Pseudocódigo del ensamblador que corre antes del render del orquestador:

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" : "",
  };
}

El render del orquestador recibe entonces esas variables junto a las normales de sitio y de runtime, y hace una última pasada.

Convenciones de nombres

Aquí la convención gana a la libertad, porque el ensamblador encuentra las plantillas por su id.

PatrónEjemplo
spintax.<entity>_item.<id>spintax.crypto_item.btc
spintax.<entity>_row.<id>spintax.crypto_row.btc (fila de tabla)
spintax.section.<key>spintax.section.crypto
spintax.<page-name>spintax.payment_options

Las variables siguen la misma forma:

  • %CryptoItems%, %FiatItems%, %LimitsRows% — bloques por id, ya unidos
  • %CryptoSection%, %FiatSection%, %LimitsSection% — secciones resueltas
  • %HasCrypto%, %HasFiat% — marcadores ('1' o '')

PascalCase para variables, snake_case para IDs, ASCII para ambos.

El almacenamiento es tu problema, no de spintax

El patrón funciona igual, vivan donde vivan las subplantillas:

  • tabla en base de datos (templates con id + cuerpo + idioma)
  • archivo JSON: { "crypto_item.btc": "<li>…</li>", … }
  • sistema de archivos: templates/crypto_item/btc.txt
  • campo de CMS por locale

El motor no necesita base de datos. Solo sustituye HTML resuelto en referencias de variable. El ensamblador es tu código, escrito en el runtime que mueve tus renders. Un plugin de WordPress, un Cloudflare Worker, un script de Node, una función en Postgres — mismo patrón.

¿Por qué no #include? El motor sí tiene una directiva de inclusión, y para un único bloque compartido es el camino más corto. Esta tubería no se apoya en ella por tres razones: una plantilla incluida es un documento aparte — ve las variables de runtime, pero nunca los #set/#def del padre, así que una forma ya tirada no se le puede pasar; solo funciona donde el anfitrión instaló un resolutor, y dos de los nuestros deliberadamente no tienen uno (el playground y el servidor MCP), donde la línea simplemente queda literal; y cuando el resolutor no conoce la referencia, la línea desaparece de la salida sin decir nada. Un ensamblador mantiene resolución, caché y manejo de errores en tu propio código, donde ves los tres. Comportamiento completo: la sección de inclusiones de la referencia de sintaxis.

Matices editoriales por id

Aquí está la ventaja decisiva. Los matices editoriales viven junto al dato, no en 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 plantilla por id captura el matiz una vez. Tres páginas, diez páginas, mil páginas — todas heredan los avisos correctos. Pasa QIWI a «obsoleto» editando una plantilla; todos los renders cambian a la vez.

Sin composición, esos matices serían cadenas en línea duplicadas entre páginas. Pesadilla de auditoría y riesgo legal a fuego lento en sectores regulados.

Reserva condicional

Se ve gente intentando expresar condicionales con la sintaxis de enumeración del motor:

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

La esperanza: «mostrar la reserva cuando ambas banderas están vacías». La realidad con una enumeración {a|b|c|d} normal: el motor elige una de las cuatro ramas al azar, con igual probabilidad. La salida no es determinista e incluye "1" como variante posible en la página.

Las ramas de enumeración son un sorteo uniforme — nunca miran una variable. Para elegir por valor, usa la pasada previa condicional:

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

Léelo así: si no hay cripto y no hay dinero corriente, renderiza la reserva. El condicional se resuelve antes de que corra el sorteo de ramas, así que la salida queda del todo determinada por las variables.

La lógica compuesta, que el spintax condicional no soporta, se queda en el ensamblador — comparaciones, &&/||, valores calculados. Precalcula una variable de guarda y luego ábrele paso con {?Guard?…}. La guía dedicada de spintax condicional cubre en detalle las tres formas, la tabla truthy, la tubería de dos pasadas y los antipatrones.

Cuándo NO componer

La composición tiene coste — tres clases de plantilla que mantener, un ensamblador que cablear, una capa de almacenamiento que organizar. La tubería sale a cuenta cuando:

  • tienes cinco o más elementos parecidos que comparten estructura;
  • hay matices editoriales por id o requisitos de orden;
  • varias páginas reutilizan el mismo conjunto de elementos;
  • la redacción necesita modificar elementos de forma independiente.

Sáltate la composición cuando:

  • la página tiene de uno a tres elementos en total;
  • los elementos no se repiten entre páginas;
  • nada de la estructura va a cambiar en el próximo año;
  • nadie más que tú la va a editar.

Para una página «sobre nosotros» puntual o un artículo suelto, una plantilla autocontenida es más rápida, más limpia y más fácil de depurar.

Errores frecuentes

No hagas estoPor quéHaz esto
Componer una página pequeña (≤3 elementos, sin varianza editorial)El coste de la tubería supera el ahorro.Mantener una plantilla autocontenida.
Codificar condicionales en enumeraciones spintaxEl motor sortea, no mira valores; la salida no es determinista.Usar {?VAR?then|else} para comprobar una variable; calcular la lógica compuesta en el ensamblador y abrirla con {?Guard?…}.
Poner los matices de un id en línea en el orquestador o la secciónSe pierde la ventaja de «editar una vez, propagar a todo».Mantener los matices en la plantilla _item de ese id.
Mezclar asuntos de elemento y de sección en una plantillaRefactorizar se vuelve doloroso según crece la página.Tres niveles limpios: elemento, sección, orquestador.
Fijar el orden en el orquestadorCambiar el orden obliga a editar páginas por todo el catálogo.Ordenar en el ensamblador con un único campo de orden por elemento.
Olvidar cortocircuitar las secciones vacíasUn <h3> vacío, sin <ul> debajo, llega a producción.Devolver "" desde el ensamblador cuando la lista de elementos esté vacía.
Fiarte de los %XxxItems% sin resolver en la página renderizadaUna variable ausente significa que el marcador sobrevive literalmente.Una pasada de QA que marque cualquier %…% sobrante en el HTML de producción.

Checklist de composición

  • Cada grupo de elementos repetidos tiene su propia plantilla por id.
  • Cada sección tiene una única plantilla _section que referencia marcadores de elemento.
  • El orquestador referencia solo variables de sección, nunca variables de elemento.
  • El orden viene de los datos, no del contenido de la plantilla.
  • Los condicionales sobre una variable usan {?VAR?…}; la lógica compuesta se queda en el ensamblador. Nunca ramas de enumeración.
  • Las secciones vacías producen "", no marcado suelto.
  • Los matices editoriales por id no están duplicados en la sección ni en el orquestador.
  • Cinco renders de muestra se leen limpios en «todas las categorías vacías», «solo cripto», «solo dinero corriente», «todas las categorías presentes» y «un único elemento obsoleto».
  • Ningún %…%, {…} o […] sobrante en ningún render.

Con esto termina la serie por ahora. Tienes la mentalidad, las variables, las permutaciones, la gramática y ahora la composición. Vuelve a la mentalidad cuando empieces el siguiente artículo — el flujo se hace más rápido cada vez.


Continuar la serie