テンプレート合成:レンダリング済みHTMLを入れた変数
テンプレートが大きくなりすぎて、付き合いきれなくなることがあります。決済手段のページに数百の <li> 項目、本文に散らばる数十の編集上の注記、すべてのバリエーションに波及させなければならない並び順の変更 — ある時点から、巨大に入れ子になった1枚のテンプレートは助けではなくボトルネックになります。次の一手は、それを小さなテンプレートのパイプラインへ分け、レンダリング済みHTMLを持つ変数でつなぐことです。
見方を変える
このシリーズでここまで、変数は値でした:ブランド名、年、カンマ区切りの機能一覧。レンダリング時にテンプレートへ差し込まれる、ただの文字列です。
このガイドでの転換は小さく、そして強力です:変数の値は、解決済みのHTMLでもかまいません。"Acme Co." ではなく <h3>Crypto deposits</h3><ul><li>BTC — fastest…</li>…</ul> です。リゾルバーは気にしません。ただ差し込むだけです。
これで合成が可能になります。ページを、小さなサブテンプレートのパイプラインから組み立てます。それぞれがHTMLのかたまりにレンダリングされ、数行しかないオーケストレーターがそれらをまとめます。
合成なしの場合
<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>
これは200行のモノリスです。通貨を1つ足すには、巨大な列挙の連なりの中を編集することになります。並び替えは手作業です。通貨ごとの編集上の細かい注意が、ファイル中に散らばります。
合成ありの場合
%CryptoSection%
%FiatSection%
%LimitsSection%
3行です。各変数には、そのページ部分の完全に解決済みのHTMLがすでに入っています。値は、オーケストレーターがレンダリングされる前に走るパイプラインから来ます。
なぜ動くのか — エンジンのパイプライン
構文リファレンスが解決の順序を定めています。合成に関わるのは次の行です:
- コメントを取り除く;
#set/#defディレクティブを取り出す;- 変数を統合する;
%var%参照を展開する;- 列挙
{a|b|c}を解決する; - 順列
[a|b|c]を解決する; - 後処理する。
変数の展開は、列挙と順列の解決より前です。その段階が走る時点で、%CryptoSection% はアセンブラーが用意したHTMLにすでに置き換わっています。特別な構文はありません — 変数の差し込みは、文字どおり文字列の置換です。
層を混ぜることさえできます:外側の順列が、事前レンダリング済みのセクションをシャッフルできます。
[<sep="\n\n">%CryptoSection%|%FiatSection%|%LimitsSection%]
各セクションが先に完全に解決され、そのあと順列がかたまりを並べ替えます。
3層のパイプライン
このパターンは3つの層で成り立ちます。各層が1段階の絞り込みです:
レベル1 — アイテムのテンプレート(IDごと)
再利用できる最小単位。データ1件につき1テンプレート:通貨ごと、決済手段ごと、プランの段階ごと、FAQの項目ごと、商品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>
レベル2 — セクションのテンプレート
リストを構造で包みます。連結済みのアイテムにはプレースホルダー変数を使います。
/# spintax.section.crypto #/
<h3>{Crypto deposits|Cryptocurrencies accepted}</h3>
<p>{Pick from|We support} the following coins:</p>
<ul>%CryptoItems%</ul>
%CryptoItems% は「IDごとのアイテムをすべて解決して1つの文字列に連結したもの」です。作るのはアセンブラーです。
レベル3 — オーケストレーター
ページ単位のテンプレート。事前レンダリング済みのセクション変数だけを参照します。
/# spintax.payment_options #/
<h2>{Accepted payment methods|How to pay}</h2>
%CryptoSection%
%FiatSection%
%LimitsSection%
これでオーケストレーターは全部です。編集のルールはこうなります:通貨の説明を変える? アイテムのテンプレートを1つ編集します。新しい通貨を足す? アイテムのテンプレートを置き、IDを有効リストに加えます。並べ替える? 並び順のフィールドであって、テンプレートの変更ではありません。
通しで見る — 決済手段のページ
ある店舗が暗号資産としてBTC・USDT・ETHを、法定通貨としてVisa・Mastercard・SEPAを受け付けています。3つの問い合わせと数枚のテンプレートで、ページ全体ができあがります。
オーケストレーターのレンダリング前に走るアセンブラーの擬似コード:
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" : "",
};
}
オーケストレーターのレンダリングは、これらを通常のサイト変数・ランタイム変数と一緒に受け取り、最後のパスを走らせます。
命名規約
ここでは自由より規約が勝ちます。アセンブラーがテンプレートを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%— IDごとのかたまりを連結したもの%CryptoSection%、%FiatSection%、%LimitsSection%— 解決済みのセクション%HasCrypto%、%HasFiat%— マーカー('1'か'')
変数はPascalCase、IDはsnake_case、どちらもASCIIで。
保存先はあなたの問題であって、spintaxの問題ではない
サブテンプレートがどこに置かれていても、このパターンは同じように働きます:
- データベースのテーブル(
templatesにID+本文+言語) - JSONファイル:
{ "crypto_item.btc": "<li>…</li>", … } - ファイルシステム:
templates/crypto_item/btc.txt - ロケールごとのCMSフィールド
エンジンにデータベースは要りません。解決済みのHTMLを変数参照に差し込むだけです。アセンブラーはあなたのコードで、レンダリングを動かしているランタイムで書きます。WordPressプラグイン、Cloudflare Worker、Nodeスクリプト、Postgresの関数 — どれでも同じパターンです。
なぜ #include ではないのか。エンジンにはインクルードのディレクティブがあり、共有ブロックが1つだけなら、そちらが近道です。このパイプラインがそれに乗っていないのは3つの理由からです:インクルードされたテンプレートはそれ自体が1つの文書で — ランタイム変数は見えても、親の #set/#def は決して見えないため、確定済みの形を中へ渡せません。ホストがリゾルバーを入れている場所でしか動かず、私たちのうち2つ(playgroundとMCPサーバー)は意図的に持っていないので、そこでは行がそのまま残ります。そしてリゾルバーが参照を知らない場合、その行は何も言わずに出力から消えます。アセンブラーなら、解決・キャッシュ・エラー処理の3つとも自分のコードの中に置け、目に見えます。詳しい挙動は構文リファレンスのインクルードの節にあります。
IDごとの編集上の細かい注意
ここがいちばんの利点です。編集上の細かい注意は、各ページではなくデータのそばに置かれます。
/# 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>
IDごとのテンプレートが、その注意を一度だけ捉えます。3ページでも10ページでも1000ページでも、すべてが正しい注意書きを受け継ぎます。QIWIを「非推奨」に移すのはテンプレート1枚の編集で済み、すべてのレンダリングが同時に切り替わります。
合成がなければ、これらの注意はページをまたいで複製されたインライン文字列になります。監査の悪夢であり、規制のある業種ではじわじわ効く法的リスクです。
条件付きのフォールバック
エンジンの列挙構文で条件を表そうとする人をよく見かけます:
{%HasCrypto%|%HasFiat%||<p>Payment methods coming soon.</p>}
期待は「両方のフラグが空のときにフォールバックを出す」。ただの {a|b|c|d} 列挙での現実は、エンジンが4つの分岐から等確率で1つをランダムに選ぶ、です。出力は非決定的で、ページ上の候補に "1" まで含まれます。
列挙の分岐は一様なランダム選択で、変数を見ることは決してありません。値で選びたいなら、条件の前段パスを使ってください:
{?!HasCrypto?{?!HasFiat?<p>Payment methods coming soon.</p>}}
こう読みます:暗号資産もなく法定通貨もなければ、フォールバックを出す。条件はランダム選択が走る前に解決されるので、出力は変数によって完全に決まります。
条件付きspintaxが対応しない複合ロジックはアセンブラーに残します — 比較、&&/||、計算した値。ガード変数を先に計算し、{?Guard?…} で出し分けてください。専用のガイド条件付きspintaxが、3つの形、truthyの表、2パスのパイプライン、アンチパターンを詳しく扱っています。
合成を「しない」ほうがよいとき
合成にはコストがあります — 保守するテンプレートが3種類、配線するアセンブラー、整理する保存層。パイプラインが割に合うのは:
- 同じ構造を共有する似た項目が5件以上あるとき;
- IDごとの編集上の細かい注意や、並び順の要件があるとき;
- 複数のページが同じ項目集合を再利用するとき;
- 編集者が項目を個別に変更する必要があるとき。
合成を見送るのは:
- ページの項目が全部で1〜3件のとき;
- 項目がページをまたいで繰り返されないとき;
- 今後1年、構造が何も変わらないとき;
- あなた以外に誰も編集しないとき。
一度きりの会社紹介ページや単発の記事なら、自己完結した1枚のテンプレートのほうが速く、きれいで、デバッグも簡単です。
よくある間違い
| やらないこと | 理由 | 代わりにすること |
|---|---|---|
| 小さなページを合成する(項目3件以下、編集上の差異なし) | パイプラインの手間が節約を上回ります。 | 自己完結した1枚のテンプレートのままにする。 |
| 条件をspintaxの列挙で表す | エンジンは値を見ずにランダムに選ぶので、出力が非決定的になります。 | 変数1つの判定は {?VAR?then|else}。複合ロジックはアセンブラーで計算し {?Guard?…} で出し分ける。 |
| IDごとの注意をオーケストレーターやセクションに直接書く | 「1か所直せば全体に反映」という利点が消えます。 | 注意はそのIDの _item テンプレートに置く。 |
| アイテムとセクションの関心を1枚のテンプレートに混ぜる | ページが育つほどリファクタリングが苦痛になります。 | アイテム・セクション・オーケストレーターの3層をきれいに保つ。 |
| 並び順をオーケストレーターに焼き込む | 順序を変えるたびに、カタログ全体のページ編集が必要になります。 | 各項目の並び順フィールド1つを使って、アセンブラーで並べ替える。 |
| 空のセクションを短絡し忘れる | <ul> の無い空の <h3> が本番に出ます。 | 項目リストが空なら、アセンブラーから "" を返す。 |
レンダリング結果に残った未解決の %XxxItems% を信用する | 変数が無ければ、プレースホルダーはそのまま残ります。 | 本番HTMLに残る %…% を検出するQA工程を置く。 |
合成のチェックリスト
- 繰り返す項目のグループごとに、IDごとのテンプレートがある。
- 各セクションに、アイテムのプレースホルダーを参照する
_sectionテンプレートが1枚だけある。 - オーケストレーターはセクション変数だけを参照し、アイテム変数は参照しない。
- 並び順はデータから来ており、テンプレートの中身から来ていない。
- 変数1つの条件は
{?VAR?…}、複合ロジックはアセンブラー。列挙の分岐は使わない。 - 空のセクションは
""を返し、宙に浮いたマークアップを残さない。 - IDごとの編集上の注意が、セクションやオーケストレーターに複製されていない。
- サンプル5本が「全カテゴリ空」「暗号資産のみ」「法定通貨のみ」「全カテゴリあり」「非推奨の項目1件だけ」できれいに読める。
- どのレンダリング結果にも
%…%、{…}、[…]の残骸がない。
シリーズはひとまずここまでです。考え方、変数、順列、文法、そして合成が揃いました。次の記事を書きはじめるときは考え方に戻ってください — 進め方は毎回速くなります。