a-blog cms 3.2 で実装予定のtwigで過去のテンプレートを実装してみた
このエントリーは、a-blog cms Advent Calendar 2024の7日目のエントリーです。
バージョン3.2から採用予定のtwigテンプレートについて書いてみたいと思います。
基本的なことはtwigの公式などを見ていただくとして、過去に実装したテンプレートがtwigでどのように変更できるのかを見ていきます。
事例1
before
<!-- BEGIN_MODULE Entry_Summary id="news" -->
<!-- BEGIN entry:veil -->
    <ul>
        <!-- BEGIN entry:loop -->
        <li><span class="data"><time datetime="{date#Y}-{date#m}-{date#d}">{date#Y}.{date#m}.{date#d}</time></span>
            @include("/include/link-before.html", {"reptag": "<span>"})
            {title}
            @include("/include/link-after.html", {"reptag": "</span>"})
        </li>
        <!-- END entry:loop -->
    </ul>
<!-- END entry:veil -->
<!-- END_MODULE Entry_Summary -->
インクルードしているファイルの内容は以下になります。
<!-- BEGIN_IF [{reg_contents}[delnl]/nem/_or_/{reg_file@path}/nem] -->
<a href="<!-- BEGIN_IF [{reg_file@path}/nem] -->%{ARCHIVES_DIR}{reg_file@path}<!-- ELSE -->{url}<!-- END_IF -->">
<!-- ELSE_IF [{{reptag}}/nem] -->
{{reptag}}
<!-- END_IF --><!-- BEGIN_IF [{reg_contents}[delnl]/nem/_or_/{reg_file@path}/nem/_or_/{reg_link}/nem] -->
</a>
<!-- ELSE_IF [{{reptag}}/nem] -->{{reptag}}<!-- END_IF -->このテンプレートの概要は、
一覧ページからのエントリーのリンク先をカスタムフィールので値の内容によって a タグにするのか、指定した別のタグにするのかを制御しています。
after
これをtwigで書き直すと以下のようになるかと思います。
{% set newsList = module('V2_Entry_Summary', 'news') %}
{% if newsList.entries is not empty %}
  <ul>
  {% for entry in newsList.entries %}
    <li><span class="data"><time datetime="{{entry.datetime|date('Y-m-d')}}">{{entry.datetime|date('Y.m.d')}}</time></span>
      {% set regFile = entry.fields.reg_file.value.path %}
      {% set regContents = entry.fields.reg_contents.value %}
      {% if regFile is not empty %}
        {% set url = regFile %}
      {% else %}
        {% set url = entry.url %}
      {% endif %}
      
      {% include 'include/link-before.html.twig' with {'reptag': '<span>'} %}
        {{ entry.title }}
      {% include 'include/link-after.html.twig' with {'reptag': '</span>'} %}
    </li>
  {% endfor %}
  </ul>
{% endif %}この場合のインクルード先は下記です。
{% if regContents is not empty or regFile is not empty %}
  <a href="{{ ARCHIVES_DIR ~ url }}">
{% elseif reptag is not empty %}
  {{ reptag|raw }}
{% endif %}{% if regContents is not empty or regFile is not empty %}
  </a>
{% elseif reptag is not empty %}
  {{ reptag|raw }}
{% endif %}コードだけ記載してもアレなので以下説明です。
このTwigテンプレートは、ニュースリストを生成するためのコードで、ニュースエントリのタイトルとリンクを適切にフォーマットして出力します。以下にコード全体の処理内容を分解して説明します。
概要
- このテンプレートは 
V2_Entry_Summaryモジュールからニュースリスト (newsList) を取得し、それをHTMLのリスト (<ul><li>) として出力します。 - 各ニュースエントリには、日付、リンク、タイトルが含まれます。
 - ニュースエントリのリンクは条件によって異なる処理を行い、
include/link-before.html.twigとinclude/link-after.html.twigを利用してリンクの開始タグと終了タグを挿入します。 
コードの処理内容
1. ニュースリストの取得
{% set newsList = module('V2_Entry_Summary', 'news') %}
module('V2_Entry_Summary', 'news')を使ってnewsListを取得します。このモジュールは、ニュースエントリのデータ(例: タイトル、リンク、日付など)を提供します。
2. ニュースリストが空でない場合の処理
{% if newsList.entries is not empty %}
  <ul>
  ...
  </ul>
{% endif %}
newsList.entriesが空でない場合にのみ、HTMLリストを生成します。- リスト全体は 
<ul>タグで囲まれています。 
3. ニュースエントリのループ
{% for entry in newsList.entries %}
  <li>
    <span class="data">
      <time datetime="{{entry.datetime|date('Y-m-d')}}">{{entry.datetime|date('Y.m.d')}}</time>
    </span>
    ...
  </li>
{% endfor %}
- 各ニュースエントリ (
entry) をループで処理し、以下の情報を出力します:- 日付: 
<time>タグにISOフォーマット (Y-m-d) と表示用フォーマット (Y.m.d) の2つを埋め込み。 - タイトル: 後述のリンク処理を介して表示。
 
 - 日付: 
 
4. URLの決定
{% set regFile = entry.fields.reg_file.value.path %}
{% set regContents = entry.fields.reg_contents.value %}
{% if regFile is not empty %}
  {% set url = regFile %}
{% else %}
  {% set url = entry.url %}
{% endif %}
regFileに登録ファイルパスを取得し、regContentsに関連データを取得します。regFileが存在する場合は、それをリンクとして使用 (url = regFile)。- それ以外の場合は、
entry.urlを使用。 
5. リンクタグの生成
{% include 'include/link-before.html.twig' with {'reptag': '<span>'} %}
  {{ entry.title }}
{% include 'include/link-after.html.twig' with {'reptag': '</span>'} %}
- リンクの前後にテンプレートを挿入することで柔軟なリンク生成を可能にしています。
 include/link-before.html.twigとinclude/link-after.html.twigを利用して、以下のように動的なリンク構造を生成します:- 前タグ生成 (
link-before.html.twig): regContentsまたはregFileが空でない場合は<a>タグを生成。- 空の場合は、
reptagとして渡された内容 (<span>など) を出力。 - 後タグ生成 (
link-after.html.twig): - リンクが存在する場合は 
</a>を閉じる。 - 存在しない場合は、渡された 
reptagを出力。 
- 前タグ生成 (
 
include/link-before.html.twig と link-after.html.twig の役割
これらのテンプレートは、リンクタグを動的に生成する役割を担います。
link-before.html.twig
{% if regContents is not empty or regFile is not empty %}
  <a href="{{ ARCHIVES_DIR ~ url }}">
{% elseif reptag is not empty %}
  {{ reptag|raw }}
{% endif %}
regContentsまたはregFileが空でない場合:<a>タグを生成し、ARCHIVES_DIR(ベースURL)とurlを組み合わせたリンクを出力。
- 空の場合:
- 渡された 
reptagをそのまま出力。 
 - 渡された 
 
link-after.html.twig
{% if regContents is not empty or regFile is not empty %}
  </a>
{% elseif reptag is not empty %}
  {{ reptag|raw }}
{% endif %}
- リンクが生成された場合:
</a>タグを閉じます。
 - リンクがない場合:
- 渡された 
reptagを出力。 
 - 渡された 
 
出力されるHTML例
ニュースエントリに regFile がある場合:
  <ul>
    <li>
      <span class="data">
        <time datetime="2023-11-20">2023.11.20</time>
      </span>
      <a href="/archives/path/to/regFile">
        ニュースタイトル
      </a>
    </li>
  </ul>
regFile がなく、reptag が指定されている場合:
  <ul>
    <li>
      <span class="data">
        <time datetime="2023-11-20">2023.11.20</time>
      </span>
      <span>
        ニュースタイトル
      </span>
    </li>
  </ul>
ポイント
- 動的なリンク生成:
- 各エントリに応じてリンクを生成するか、
reptagで囲むかを動的に切り替えます。 
 - 各エントリに応じてリンクを生成するか、
 - モジュール化:
- リンク生成部分を別テンプレートに分けて、コードの再利用性を高めています。
 
 - 柔軟性:
- URLの選択や出力フォーマットが条件によって変わるため、様々な状況に対応可能です。
 
 
このテンプレートは、ニュースリストを柔軟かつ効率的に表示するための設計がされています。
どうでしょうか?
ちなみに上記の説明はAIにコードだけ投げて生成してみました。
AIっぽいのは気になりますが、ざっくり投げて得られる内容としては十分です。
事例2
before
<!-- BEGIN_MODULE Entry_Summary id="rel-news" -->
<ul>
    <!-- BEGIN entry:loop -->
    @include("/rendered/rel-title.html", {"eid": "{reg_id}"})
    <li>
        <a href="{url}">
            <!-- BEGIN_IF [{reg_file@path}/nem] -->
            <img src="%{ARCHIVES_DIR}{reg_file@path}" alt="{reg_file@alt}">
            <!-- END_IF -->
            <p class="title"><!-- GET_Rendered id="rel_title{reg_id}" --></p>
            <p class="sub_title">{title}</p>
            <p class="sub_title">{reg_contents}[striptags|mb_trim(100, '…')]</p>
        </a>
    </li>
    <!-- END entry:loop -->
</ul>
<!-- END_MODULE Entry_Summary --><!-- BEGIN_MODULE\ Entry_Summary ctx="eid/{{eid}}" -->
<!-- BEGIN\ entry:loop -->
<!-- BEGIN_SetRendered id="rel_title\{eid\}" -->\{title\}<!-- END_SetRendered -->
<!-- END\ entry:loop -->
<!-- END_MODULE\ Entry_Summary -->2例目はエスケープが必要なテンプレートです。
とても嫌です。
こちらの概要としては、
このテンプレートが置かれているのはエントリー詳細ページです。関連するニュースを出しているイメージです。
rel-news のモジュールIDで、 field/reg_campaign/eq/%{EID} となっており、自身のEIDをもっているお知らせを出力します。
単純にそれなら @include("/rendered/rel-title.html", {"eid": "{reg_id}"}) の部分は必要ありませんが、
このお知らせはさらに reg_id で別のエントリーとも紐付けをおこなっています。
この処理でそちらのエントリーのタイトルを持ってきます。
これをtwigへ変更します。
after
{% set newsList = module('V2_Entry_Summary', 'news') %}
<ul>
  {% for entry in newsList.entries %}
    {% set relTitle = include('include/rel-title.html.twig', {regId: entry.fields.reg_id.value} ) %}
      <li>
        <a href="{{ entry.url }}"> {% if entry.fields.reg_file.value.path is not empty %}
          <img src="{{ ARCHIVES_DIR ~ entry.fields.reg_file.value.path }}" alt="{{ entry.fields.reg_file.value.alt }}">
        {% endif %}
        <p class="title">{{relTitle}}</p>
        <p class="sub_title">{{ entry.title }}</p>
        {% if entry.fields.reg_contents.value %}
          <p class="sub_title">{{ entry.fields.reg_contents.value | striptags | mb_trim(100, '…')}}</p>
        {% endif %}
      </a>
    </li>
  {% endfor %}
</ul>インクルードしているファイルは以下です。
{% set relEntry = '' %}
{% if regId %}
  {% set ctx = 'eid/' ~ regId %}
  {% set relEntry = module('V2_Entry_Summary', '', ctx) %}
  {{relEntry.entries.0.title}}
{% endif %}
エスケープが取れました。 こちらも以下にAIによる説明を記載します。
このTwigテンプレートは、ニュースリストをループして関連情報を動的に生成するコードです。 include を用いて別ファイルのロジックを直接呼び出して処理する点が特徴です。
処理の概要
1. ニュースリストの取得
{% set newsList = module('V2_Entry_Summary', 'news') %}
module('V2_Entry_Summary', 'news')を使ってニュースリスト(newsList)を取得。- このリストには複数のニュースエントリが格納されています。
 
2. HTMLリストの開始
<ul>
  {% for entry in newsList.entries %}
- ニュースリスト内の各エントリをループ処理。
 - リスト全体を 
<ul>タグで囲み、各ニュースを<li>要素として出力します。 
3. 関連タイトル(relTitle)の取得
{% set relTitle = include('include/rel-title.html.twig', {regId: entry.fields.reg_id.value} ) %}
includeを使用して、include/rel-title.html.twigに処理を委託します。{regId: entry.fields.reg_id.value}でregIdを渡します。entry.fields.reg_id.valueが関連タイトルの取得に使われるIDです。
includeの戻り値がrelTitleに代入されます。
4. 各ニュースエントリのHTML構造
<li>
  <a href="{{ entry.url }}">
    {% if entry.fields.reg_file.value.path is not empty %}
      <img src="{{ ARCHIVES_DIR ~ entry.fields.reg_file.value.path }}" alt="{{ entry.fields.reg_file.value.alt }}">
    {% endif %}
    <p class="title">{{relTitle}}</p>
    <p class="sub_title">{{ entry.title }}</p>
    {% if entry.fields.reg_contents.value %}
      <p class="sub_title">{{ entry.fields.reg_contents.value | striptags | mb_trim(100, '…')}}</p>
    {% endif %}
  </a>
</li>
- リンク先:
entry.urlをリンク先に指定して<a>タグを生成します。
 - 画像の出力(条件付き):
entry.fields.reg_file.value.pathが空でない場合に画像を出力。ARCHIVES_DIRと画像パスを結合してsrcに設定。- 画像の 
alt属性にはentry.fields.reg_file.value.altを指定。 
 - タイトルの出力:
<p class="title">に関連タイトル(relTitle)を出力。
 - サブタイトルの出力:
- エントリ自体のタイトル(
entry.title)を<p class="sub_title">に出力。 entry.fields.reg_contents.valueが存在する場合、内容をトリムして表示。
 - エントリ自体のタイトル(
 
5. リストの終了
  {% endfor %}
</ul>
- ループを終了し、リスト全体のHTML構造を閉じます。
 
include/rel-title.html.twig の処理内容
以下のコードは、関連タイトルを取得するロジックとして呼び出されます。
{% set relEntry = '' %}
{% if regId %}
  {% set ctx = 'eid/' ~ regId %}
  {% set relEntry = module('V2_Entry_Summary', '', ctx) %}
  {{relEntry.entries.0.title}}
{% endif %}
処理手順
- 引数 
regIdを受け取る:- 外部テンプレートから渡される 
regIdを使って処理を開始。 
 - 外部テンプレートから渡される 
 - 条件分岐:
regIdが空でない場合に以下の処理を実行。
 - コンテキストの生成:
ctxを'eid/' ~ regIdの形式で生成。
 - 関連エントリの取得:
module('V2_Entry_Summary', '', ctx)を使って、関連するエントリデータを取得。
 - 関連タイトルの取得:
- 取得したエントリの最初のタイトル(
relEntry.entries.0.title)を返します。 
 - 取得したエントリの最初のタイトル(
 
全体のHTML出力例
入力データ例
entryデータ:url:/news/123title:新製品のお知らせfields.reg_id.value:567fields.reg_file.value.path:/images/news_image.jpgfields.reg_file.value.alt:新製品の画像fields.reg_contents.value:この製品の特徴を簡単に説明します。
- 関連データ:
relTitle:新製品発表会の概要
 
出力HTML例
<ul>
  <li>
    <a href="/news/123">
      <img src="/archives/images/news_image.jpg" alt="新製品の画像">
      <p class="title">新製品発表会の概要</p>
      <p class="sub_title">新製品のお知らせ</p>
      <p class="sub_title">この製品の特徴を簡単に説明します。</p>
    </a>
  </li>
</ul>
ポイント
ニュースリストの取得:
module('V2_Entry_Summary', 'news')でニュースリスト(newsList)を取得。
関連タイトルの取得を分離:
include('include/rel-title.html.twig')を使って、関連タイトル取得ロジックを外部テンプレートに分離。regIdをパラメータとして渡し、取得したタイトルをrelTitleに格納。
条件付き画像出力:
- ニュースエントリに画像パスが設定されている場合のみ 
<img>タグを出力。 
- ニュースエントリに画像パスが設定されている場合のみ 
 複数のタイトル情報を出力:
<p class="title">に関連タイトルを出力。<p class="sub_title">にニュースエントリ自体のタイトルとコンテンツを出力。
コンテンツのトリミング:
- 長いコンテンツを 
mb_trim(100, '…')を使って100文字に制限し、「…」で省略を表示。 
- 長いコンテンツを 
 関連タイトルの取得ロジック:
regIdをもとに動的に関連データを取得し、最初のエントリのタイトルを返す。
HTML構造の簡潔化:
- 各ニュースエントリを 
<li>タグでまとめ、リンク全体を<a>タグで囲む。 
- 各ニュースエントリを 
 再利用性の高いテンプレート設計:
includeと分離された関連タイトルの取得ロジックにより、他テンプレートでも活用可能。
条件付き処理の最適化:
- 画像や関連タイトルの有無を条件分岐でチェックし、存在する場合のみ表示。
 
まとめ
いかがでしたでしょうか?
割とコード量は増える感じがありますが、問題なくいままでと同じ処理は可能ですし、
いままで以上のことも可能になってきます。
AIを併用してのサイト制作が当たり前になってくるタイミングでのtwig併用はかなり有用であると思います。
このテンプレートを元に、〜な処理をtwigで書いて
で割といけるのではないかと思います。
それでもやっかいな仕様がありましたら、牧田へご相談いだたくとよいかと思います。
ご検討ください。
現場からは以上です。