テンプレート合成:レンダリング済み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がすでに入っています。値は、オーケストレーターがレンダリングされるに走るパイプラインから来ます。

なぜ動くのか — エンジンのパイプライン

構文リファレンスが解決の順序を定めています。合成に関わるのは次の行です:

  1. コメントを取り除く;
  2. #set / #def ディレクティブを取り出す;
  3. 変数を統合する;
  4. %var% 参照を展開する
  5. 列挙 {a|b|c} を解決する;
  6. 順列 [a|b|c] を解決する;
  7. 後処理する。

変数の展開は、列挙と順列の解決より前です。その段階が走る時点で、%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件だけ」できれいに読める。
  • どのレンダリング結果にも %…%{…}[…] の残骸がない。

シリーズはひとまずここまでです。考え方、変数、順列、文法、そして合成が揃いました。次の記事を書きはじめるときは考え方に戻ってください — 進め方は毎回速くなります。


シリーズを続ける