ヘッダーは1か所だけ ― 共通パーツを全ページに配る partial タグ
このブログは @nano-cms/core という自作の CMS の上で動いています。今回から 3 回に分けて、テンプレートを組み立てる仕組み ― Liquid テンプレートの「タグ」について書きます。1 回目は、いちばん身近で具体的な「使う側」の話です。題材は {% partial %} という 1 つのタグだけ。ヘッダーやフッターのような共通パーツを、コピーして回らずに全ページへ配る仕組みを見ていきます。
同じヘッダーを何十ページにもコピーすると、変更が重くなる
サイトを作ると、どのページにも同じものが載ります。サイト共通のヘッダー、フッター、採用セクションのナビゲーション、ブログのサイドバー ― このブログを含む keis-software.com でも、これらは何十ページにもまたがって同じ姿で出ています。
素朴に作ると、各ページの HTML に同じヘッダーを書き写すことになります。すると、電話番号をひとつ直す、メニューを 1 項目足す、それだけのために同じ修正を全ファイルに繰り返すことになります。直し忘れたページだけ古いまま残る、という事故も起きます。ページが増えるほど、この「共通部分のコピー」は重荷になっていきます。
欲しいのは逆向きの構造です。共通パーツの実体は 1 か所に置き、各ページは「そこを読み込む」とだけ書く。直すのは実体の 1 か所、反映は読み込んでいる全ページへ ― これを実現するのが {% partial %} タグです。
{% partial "名前" %} ― 名前で 1 つの共通パーツを読み込む
使い方は 1 行です。ページのテンプレートの、共通パーツを出したい場所にこう書きます。
<body>
{% partial "site-header" %}
<main>
... ページ固有の中身 ...
</main>
{% partial "site-footer" %}
</body>
"site-header" は共通パーツの名前です。この 1 行が、その名前で登録された共通パーツの中身に置き換わります。実際にこのサイトでは、site-header は採用トップ・募集要項・社員インタビュー・ブログ記事など読み込んでいる全ページに、site-footer はさらに広く、recruit-nav(採用セクションのナビ)や blog-sidebar(ブログのサイドバー)はそれぞれの関連ページに、という形で使い回されています。読み込む側はどのページでもこの 1 行のままで、中身の実体は別の 1 か所にあります。
ここが効きどころです。ヘッダーの中身を変えたいときに触るのは、site-header という実体ひとつだけ。各ページのテンプレートには一切手を入れません。それでいて、読み込んでいるページが次に生成されるときには、全ページが新しいヘッダーになります。「全ページを一括で書き換える」のではなく、「各ページが、いま公開されている site-header を読みにいく」という向きなので、ページが何ページあっても直す場所は 1 か所のままです。
共通パーツはどこにあるのか ― 版管理された 1 件のデータ
では site-header の実体はどこにあるのか。これは管理画面で編集する 1 件のデータとして、データベースに保管されています。ファイルをいじるのではなく、記事と同じように画面から編集して公開します。
共通パーツは記事と同じく版(リビジョン)を持ちます。編集中の下書きは WORKING(作業中)、確認を通したものは承認済み、そして公開された版 ― という履歴が残り、読み込み側のページに出るのは原則「公開された版」です。つまり共通パーツを直しても、公開操作をするまでは本番のページには出ません。下書きのまま安全に書きかけておけます。
読み込みのたびにデータベースへ問い合わせると重くなるので、一度読んだ共通パーツはメモリに覚えておきます。ただし「覚えたまま古くなる」ことは避けたいので、共通パーツの更新日時を見て、変わっていればキャッシュを捨てて読み直し、変わっていなければ覚えていた解析済みのテンプレートをそのまま使います。ヘッダーやフッターのように毎ページ出るパーツほどキャッシュに残りやすく、何度も同じものを解析し直さずに済みます。
同じ「site-header」でも、サイトごとに別物にできる
@nano-cms/core は 1 つの設置で複数のサイトを運営できます。このとき気になるのが、共通パーツの名前の衝突です。サイト A の「ヘッダー」とサイト B の「ヘッダー」は別物のはずなのに、同じ site-header という名前を使いたい。
これは、共通パーツを「サイト+名前」の組で一意に持つことで解決しています。同じ {% partial "site-header" %} と書いても、どのサイトのページを生成しているかによって、そのサイトの site-header が読み込まれます。名前は使い回せて、中身はサイトごとに独立 ― 読み込み側のタグは 1 種類のまま、サイトを増やしても書き換え不要です。
配信とプレビューで「読む版」が変わる
同じ {% partial "site-header" %} でも、文脈によって読み込む版が切り替わります。これが、共通パーツを安心して編集できる理由です。
本番の配信では、前述のとおり公開された版を読みます。一方、編集画面で「このヘッダーを直したらどう見えるか」を確かめるプレビューでは、まだ公開していない編集中の下書きを優先して読み込みます。さらに、その下書きの前後を目印で囲み、画面上でそのパーツの範囲を色付けして「いまここを見ています」と分かるようにします。ページ全体の中で、自分が編集している共通パーツがどこに出るのかを目で追えます。
予約公開と組み合わせると、もう一歩進みます。プレビューに「この日時の状態を見せて」という時刻を渡すと、その時刻までに公開予定の版を選んで読み込みます。共通パーツに予約した変更も織り込んだうえで、公開予定日のページの姿を、本番を動かさずに先取りして確認できます。「ヘッダーの差し替えを来週公開予定にしてあるが、その日のトップページは結局どう見えるのか」を、いま確かめられるということです。
パーツに値を渡す ― {% partial "名前" key=value %}
共通パーツは、ただ固定の中身を出すだけでなく、呼び出し側から値を渡して少しだけ姿を変えることもできます。名前の後ろに key=value を並べます。
{% partial "page-heading" title="募集要項" active="recruit" %}
こうして渡した title や active は、その共通パーツの中だけで使える変数になります。共通パーツ側では {{ title }} のように受け取って、見出しの文言を差し替えたり、いま見ているページに対応するメニュー項目を強調したりできます。値はその共通パーツを描画する間だけ有効で、描き終われば元に戻る ― 呼び出しごとに独立しているので、別のページの呼び出しに影響しません。
渡し方には小さな決まりがあります。"募集要項" のようにクォートで囲んだものは、その文字どおりの値(リテラル)として渡ります。クォートを付けないと、その名前の変数の中身として解決されます。たとえば title=post.title と書けば、ページが持っている記事タイトルがそのまま共通パーツに渡ります。固定文言はクォートで、ページのデータを渡したいときはクォート無しで、と使い分けます。
クォートの有無で値が文字列・数値・変数参照のどれになるか、名前や属性の細かな文法は {% partial %} 文法リファレンス にまとめています。
共通パーツは「実体は 1 か所・読み込みは各ページ」という形なので、ヘッダーやフッターの修正は 1 回で済み、直し忘れのページが残りません。編集は版管理されていて下書きのまま安全に置け、公開して初めて本番に出ます。プレビューでは編集中の版や予約済みの版を、ページ全体の中に置いた姿で先取り確認できます。複数サイトを運営しても、名前はサイトごとに独立しています。
使う側から見れば、共通パーツの扱いは {% partial "名前" %} という 1 行に集約されています。ここで自然と湧く疑問が 2 つあります。素の Liquid には {% if %} や {% for %} といったタグが最初から揃っているのに、なぜ partial という自作のタグをわざわざ足したのか。そして、足すならいくつも足したくなりそうなのに、なぜ自作タグは実質この 1 個だけで足りているのか ― この設計の話は なぜ Liquid に自作タグを 1 枚だけ足したのか ― テンプレートエンジンの拡げ方 に書きました。さらに、自分で新しいタグを足すには具体的に何を書くのか、という作り方は Liquid に自作タグを足す ― partial タグで見る作り方 で、この partial タグ自身を題材に解説します。

