Template-Komposition: Variablen als fertig gerenderte HTML-Blöcke
Manchmal wird ein Template zu groß, um damit zu leben. Hunderte <li>-Einträge auf einer Seite mit Zahlungsmethoden, Dutzende redaktionelle Hinweise inline, Sortierwechsel, die sich durch jede Variante ziehen müssen — irgendwann ist das eine riesige, verschachtelte Template der Engpass statt der Hilfe. Der nächste Schritt: es in eine Pipeline kleiner Templates aufteilen, verbunden durch Variablen, die fertig gerendertes HTML enthalten.
Der gedankliche Schritt
Bisher war eine Variable in dieser Reihe ein Wert: ein Markenname, eine Jahreszahl, eine kommagetrennte Funktionsliste. Schlichte Strings, beim Rendern ins Template eingesetzt.
Der Schritt in diesem Leitfaden ist klein und wirkungsvoll: Der Wert einer Variablen darf bereits aufgelöstes HTML sein. Nicht "Acme Co.", sondern <h3>Crypto deposits</h3><ul><li>BTC — fastest…</li>…</ul>. Den Resolver kümmert das nicht; er setzt einfach ein.
Damit wird Komposition möglich. Sie bauen die Seite aus einer Pipeline kleiner Sub-Templates, von denen jedes zu einem HTML-Block rendert, und setzen sie mit einem Orchestrator zusammen, der nur wenige Zeilen lang ist.
Ohne Komposition
<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>
Das ist ein Monolith von 200 Zeilen. Eine Münze hinzuzufügen heißt, mitten in einer großen Aufzählungskette zu editieren. Sortierwechsel sind Handarbeit. Die redaktionellen Feinheiten pro Währung verteilen sich über die ganze Datei.
Mit Komposition
%CryptoSection%
%FiatSection%
%LimitsSection%
Drei Zeilen. Jede Variable enthält bereits das vollständig aufgelöste HTML für ihren Teil der Seite. Die Werte kommen aus einer Pipeline, die vor dem Render des Orchestrators läuft.
Warum das funktioniert — die Engine-Pipeline
Die Syntaxreferenz beschreibt die Auflösungsreihenfolge; für die Komposition zählen diese Zeilen:
- Kommentare entfernen;
#set- /#def-Direktiven herausziehen;- Variablen zusammenführen;
%var%-Referenzen expandieren;- Aufzählungen
{a|b|c}auflösen; - Permutationen
[a|b|c]auflösen; - nachbearbeiten.
Variablen expandieren vor der Auflösung von Aufzählungen und Permutationen. Wenn diese Stufe läuft, ist %CryptoSection% längst durch das HTML ersetzt, das der Assembler berechnet hat. Keine Sondersyntax — Variablenersetzung ist buchstäblich Stringersetzung.
Sie können die Ebenen sogar mischen: Eine äußere Permutation darf vorgerenderte Abschnitte mischen.
[<sep="\n\n">%CryptoSection%|%FiatSection%|%LimitsSection%]
Jeder Abschnitt wird zuerst vollständig aufgelöst, dann ordnet die Permutation die Blöcke um.
Die Pipeline mit drei Ebenen
Das Muster lebt auf drei Ebenen, jede eine Stufe der Verfeinerung:
Ebene 1 — Item-Templates (pro ID)
Die kleinste wiederverwendbare Einheit. Ein Template pro Datensatz: pro Münze, pro Zahlungsmethode, pro Tarifstufe, pro FAQ-Eintrag, pro Artikelnummer.
/# 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>
Ebene 2 — Abschnitts-Templates
Sie umschließen die Liste mit Struktur. Für die zusammengefügten Einträge nehmen Sie eine Platzhaltervariable.
/# spintax.section.crypto #/
<h3>{Crypto deposits|Cryptocurrencies accepted}</h3>
<p>{Pick from|We support} the following coins:</p>
<ul>%CryptoItems%</ul>
%CryptoItems% ist „alle Einträge pro ID, aufgelöst und zu einem String zusammengefügt". Das baut der Assembler.
Ebene 3 — Orchestrator
Das Template auf Seitenebene. Es referenziert ausschließlich vorgerenderte Abschnittsvariablen.
/# spintax.payment_options #/
<h2>{Accepted payment methods|How to pay}</h2>
%CryptoSection%
%FiatSection%
%LimitsSection%
Das ist der ganze Orchestrator. Die Regeln beim Editieren: Beschreibung einer Münze ändern? Ein Item-Template anfassen. Neue Währung? Ein Item-Template ablegen und die ID in die aktive Liste eintragen. Umsortieren? Sortierfeld, keine Template-Änderung.
Durchgespielt — eine Seite mit Zahlungsmethoden
Ein Händler nimmt BTC, USDT und ETH als Krypto sowie Visa, Mastercard und SEPA als klassisches Geld. Drei Abfragen und eine Handvoll Templates ergeben die ganze Seite.
Pseudocode des Assemblers, der vor dem Orchestrator-Render läuft:
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" : "",
};
}
Der Orchestrator-Render bekommt diese Variablen dann zusammen mit den üblichen Website- und Runtime-Variablen und macht einen letzten Durchgang.
Namenskonventionen
Hier schlägt Konvention die Freiheit, denn der Assembler findet Templates über ihre ID.
| Muster | Beispiel |
|---|---|
spintax.<entity>_item.<id> | spintax.crypto_item.btc |
spintax.<entity>_row.<id> | spintax.crypto_row.btc (Tabellenzeile) |
spintax.section.<key> | spintax.section.crypto |
spintax.<page-name> | spintax.payment_options |
Die Variablen folgen derselben Form:
%CryptoItems%,%FiatItems%,%LimitsRows%— zusammengefügte Blöcke pro ID%CryptoSection%,%FiatSection%,%LimitsSection%— aufgelöste Abschnitte%HasCrypto%,%HasFiat%— Marker ('1'oder'')
PascalCase für Variablen, snake_case für IDs, ASCII für beides.
Die Speicherung ist Ihr Problem, nicht das von Spintax
Das Muster funktioniert gleich, egal wo die Sub-Templates liegen:
- Datenbanktabelle (
templatesmit ID + Rumpf + Sprache) - JSON-Datei:
{ "crypto_item.btc": "<li>…</li>", … } - Dateisystem:
templates/crypto_item/btc.txt - CMS-Feld pro Locale
Die Engine braucht keine Datenbank. Sie setzt nur aufgelöstes HTML in Variablenreferenzen ein. Der Assembler ist Ihr Code, geschrieben in der Runtime, die Ihre Renders antreibt. Ein WordPress-Plugin, ein Cloudflare Worker, ein Node-Skript, eine Postgres-Funktion — dasselbe Muster.
Warum nicht #include? Die Engine hat eine Include-Direktive, und für einen einzelnen gemeinsamen Block ist sie der kürzere Weg. Diese Pipeline baut aus drei Gründen nicht darauf: Ein eingebundenes Template ist ein eigenes Dokument — es sieht Runtime-Variablen, aber nie die #set/#def des Elterntemplates, eine bereits gewürfelte Form lässt sich also nicht hineinreichen; es funktioniert nur dort, wo der Host einen Resolver installiert hat, und zwei der unseren haben bewusst keinen (der Playground und der MCP-Server), dort bleibt die Zeile schlicht wörtlich stehen; und kennt der Resolver die Referenz nicht, verschwindet die Zeile kommentarlos aus der Ausgabe. Ein Assembler hält Auflösung, Caching und Fehlerbehandlung in Ihrem eigenen Code, wo Sie alle drei sehen. Vollständiges Verhalten: der Abschnitt zu Includes in der Syntaxreferenz.
Redaktionelle Feinheiten pro ID
Das ist die eigentliche Stärke. Redaktionelle Feinheiten leben bei den Daten, nicht auf jeder Seite.
/# 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>
Jedes Template pro ID hält seine Feinheit einmal fest. Drei Seiten, zehn Seiten, tausend Seiten — alle erben die richtigen Hinweise. Setzen Sie QIWI auf „veraltet", indem Sie ein Template ändern; jeder Render kippt gleichzeitig mit.
Ohne Komposition wären diese Feinheiten Inline-Strings, über Seiten hinweg dupliziert. Ein Albtraum für jedes Audit und in regulierten Branchen ein schleichendes rechtliches Risiko.
Bedingter Ersatzinhalt
Man sieht Leute, die Bedingungen mit der Aufzählungssyntax der Engine ausdrücken wollen:
{%HasCrypto%|%HasFiat%||<p>Payment methods coming soon.</p>}
Die Hoffnung: „Zeig den Ersatz, wenn beide Flags leer sind." Die Realität bei einer schlichten {a|b|c|d}-Aufzählung: Die Engine zieht einen der vier Zweige zufällig, mit gleicher Wahrscheinlichkeit. Die Ausgabe ist nicht deterministisch und enthält "1" als mögliche Variante auf der Seite.
Aufzählungszweige sind eine gleichverteilte Zufallsauswahl — sie sehen nie eine Variable an. Für wertgesteuerte Wahl nehmen Sie den bedingten Vorlauf:
{?!HasCrypto?{?!HasFiat?<p>Payment methods coming soon.</p>}}
Zu lesen als: wenn kein Krypto und kein klassisches Geld, dann rendere den Ersatz. Die Bedingung löst vor dem Zufallsziehen auf, die Ausgabe ist also vollständig durch die Variablen bestimmt.
Zusammengesetzte Logik, die bedingtes Spintax nicht unterstützt, bleibt im Assembler — Vergleiche, &&/||, berechnete Werte. Berechnen Sie eine Wächtervariable vor und schalten Sie sie dann mit {?Guard?…} frei. Der eigene Leitfaden zu bedingtem Spintax behandelt die drei Formen, die Truthy-Tabelle, die Zwei-Durchgang-Pipeline und die Anti-Patterns im Detail.
Wann man NICHT komponiert
Komposition kostet etwas — drei Template-Arten zu pflegen, einen Assembler zu verdrahten, eine Speicherschicht zu ordnen. Die Pipeline lohnt sich, wenn:
- Sie fünf oder mehr ähnliche Einträge mit gemeinsamer Struktur haben;
- es redaktionelle Feinheiten pro ID oder Sortieranforderungen gibt;
- mehrere Seiten denselben Satz an Einträgen nutzen;
- die Redaktion Einträge unabhängig ändern muss.
Lassen Sie die Komposition weg, wenn:
- die Seite insgesamt einen bis drei Einträge hat;
- die Einträge sich über Seiten hinweg nicht wiederholen;
- sich an der Struktur im nächsten Jahr nichts ändert;
- niemand außer Ihnen sie bearbeitet.
Für eine einmalige Über-uns-Seite oder einen einzelnen Artikel ist ein in sich geschlossenes Template schneller, sauberer und leichter zu debuggen.
Häufige Fehler
| Nicht so | Warum | Stattdessen |
|---|---|---|
| Eine kleine Seite komponieren (≤3 Einträge, keine redaktionelle Varianz) | Der Aufwand der Pipeline übersteigt die Ersparnis. | Ein in sich geschlossenes Template behalten. |
| Bedingungen in Spintax-Aufzählungen codieren | Die Engine zieht zufällig statt wertbasiert; die Ausgabe ist nicht deterministisch. | {?VAR?then|else} für Prüfungen einer Variablen; zusammengesetzte Logik im Assembler berechnen und mit {?Guard?…} freischalten. |
| Feinheiten einer ID inline in Orchestrator oder Abschnitt schreiben | Der Vorteil „einmal ändern, überall wirksam" geht verloren. | Feinheiten im _item-Template dieser ID halten. |
| Item- und Abschnittsbelange in einem Template mischen | Refactoring wird mit wachsender Seite zur Qual. | Drei saubere Ebenen: Item, Abschnitt, Orchestrator. |
| Die Sortierung im Orchestrator festschreiben | Sortierwechsel erzwingen Seitenänderungen im ganzen Katalog. | Im Assembler nach einem einzigen Sortierfeld pro Eintrag sortieren. |
| Leere Abschnitte nicht kurzschließen | Ein leeres <h3> ohne <ul> darunter geht in Produktion. | Aus dem Assembler "" zurückgeben, wenn die Eintragsliste leer ist. |
Nicht aufgelösten %XxxItems%-Resten in der gerenderten Seite trauen | Eine fehlende Variable heißt: Der Platzhalter überlebt wörtlich. | Ein QA-Durchgang, der übrig gebliebene %…% im Produktions-HTML meldet. |
Kompositions-Checkliste
- Jede Gruppe wiederholter Einträge hat ihr eigenes Template pro ID.
- Jeder Abschnitt hat genau ein
_section-Template, das Item-Platzhalter referenziert. - Der Orchestrator referenziert nur Abschnittsvariablen, keine Item-Variablen.
- Die Sortierung kommt aus den Daten, nicht aus dem Template-Inhalt.
- Bedingungen auf eine Variable nutzen
{?VAR?…}; zusammengesetzte Logik bleibt im Assembler. Nie Aufzählungszweige. - Leere Abschnitte liefern
"", kein loses Markup. - Redaktionelle Feinheiten pro ID sind nicht im Abschnitt oder Orchestrator dupliziert.
- Fünf Beispiel-Renders lesen sich sauber bei „alle Kategorien leer", „nur Krypto", „nur klassisches Geld", „alle Kategorien vorhanden" und „ein einzelner veralteter Eintrag".
- Kein übrig gebliebenes
%…%,{…}oder[…]in irgendeinem Render.
Damit endet die Reihe vorerst. Sie haben die Denkweise, Variablen, Permutationen, Grammatik und jetzt die Komposition. Kehren Sie zur Denkweise zurück, wenn Sie den nächsten Artikel beginnen — der Ablauf wird jedes Mal schneller.