Twig

目次

第1回:Twig版チュートリアルをはじめよう


このチュートリアルで学ぶこと

静的HTMLサイトを部分的にCMS化する流れを、Twigテンプレートエンジンを使って体験します。

完成イメージは次のとおりです。

  • トップページ:「お知らせ一覧」が管理画面から更新できる状態になる

  • 詳細ページ:記事ごとにタイトル・本文・日付が動的に切り替わる

  • 一覧ページ:サムネイル画像・ページャーつきのお知らせ一覧が動く

このチュートリアルを終えると、「コーダーが書き上げた静的HTMLを、最小限の変更でCMS管理下に置く」という実務に直結するフローが身につきます。

なぜ Twig なのか

a-blog cms は Ver. 3.2 から、従来の標準テンプレートに加えて、Twig によるテンプレート記述をサポートしています。

標準テンプレートは a-blog cms 独自の記法で設計されており、できることは豊富です。一方で、ループや条件分岐といった制御構文が独自仕様になっているため、シンプルな出力は簡単にできても、少し複雑なカスタマイズをしようとすると途端に覚えることが増えるという側面があります。

Twig は PHP 製のテンプレートエンジンで、Craft CMS・Symfony・Drupal などでも広く採用されています。変数の出力・ループ・条件分岐の書き方が、PHP や JavaScript などの一般的なプログラミング言語に近いため、「Twig の書き方を覚える」というより「すでに知っている書き方がそのまま使える」という感覚で書けます。

たとえば条件分岐なら、標準テンプレートでは a-blog cms 固有の構文を覚える必要がありますが、Twig では次のように書けます。

{% if entry.tags %}
  {% for tag in entry.tags %}
    <span>#{{ tag.name }}</span>
  {% endfor %}
{% endif %}

「タグがあれば一覧を出す」という意図が、コードを読んだだけで伝わります。Twigに慣れたメンバーがいれば、a-blog cmsを知らなくてもテンプレートのロジックを読み解けます。これはチーム開発や長期保守において大きなメリットになります。

どちらの形式でも a-blog cms の機能はすべて利用できます。新規テーマをTwigで書く、あるいは既存テーマをTwig化してメンテナンス性を高める、どちらの用途にも使えます。


1. サンプルのウェブサイトを確認します

ここからは実際に、静的HTML を CMS化する手順を進めていきます。最初に、チュートリアルで使用するサンプルのウェブサイトを確認しましょう。

今回のチュートリアルでは、HTML ファイル 15枚 からなる静的なウェブサイトをサンプルとして使用します。デザインは Tailwind CSS を用いてスタイルを定義しており、メニューなどの動きには Alpine.js を利用しています。

sample_website

実際のサンプルサイトを確認

サイトの構成は以下のようになっています。

├── index.html            # トップページ(メインビジュアル・サービス紹介・お知らせ一覧)
├── news/
│   ├── index.html        # お知らせ一覧ページ
│   ├── page2.html        # お知らせ一覧 2ページ目
│   ├── 20250710.html     # お知らせ詳細(5記事分)
│   ├── ...
│   └── 20250807.html
├── service/
│   ├── index.html        # サービス一覧
│   ├── catering.html
│   ├── cooking-class.html
│   └── private-chef.html
├── contact/
│   ├── index.html        # お問い合わせフォーム
│   ├── confirm.html
│   └── thanks.html
├── css/
│   └── custom.css
└── images/

チュートリアルでは、このうち トップページの「お知らせ一覧」 と お知らせの詳細ページ(news フォルダ内の HTML) を題材に、静的 HTML を CMS 管理に置き換える流れを学びます。 まずは 「お知らせ部分を CMS化する」 ことで、手軽に「できた!」という達成感を体験しましょう。

相対パスとルート相対パスについて

HTML を CMS のテーマとして利用する際、画像や CSS へのパス指定方法によっては、表示が崩れることがあります。ここでは、相対パスとルート相対パスの違いを確認しておきましょう。

サンプルファイルには、相対パス版のファイルとルート相対パスの両方が同梱されています。

相対パス

現在のファイルを基準にして、そこから見た相対的な位置を指定する方法です。

<link rel="stylesheet" href="../css/custom.css">
<img src="./images/logo.png">
  • ../ は「1つ上の階層」を意味します。

  • ページの階層が深くなると、../../ のように複雑になりやすい。

  • パソコンでファイルを直接閲覧可能です。

ルート相対パス

サイトのルート(ドメイン直下 /)を基準にして指定する方法です。

<link rel="stylesheet" href="/css/custom.css">
<img src="/images/logo.png">
  • どのディレクトリにあるページからでも同じ指定が使える。

  • CMS 化したときに、テンプレートの場所に関わらず一貫して参照できる のでおすすめ。

  • ウェブサーバーのドキュメントルートにアップロードして閲覧が可能になります。

既存の静的 HTML サイトを CMS 化する場合、すでに用意されている HTML は 相対パス で書かれているケースが多いと思われます。
a-blog cms では相対パスでも動作しますので、どちらを使っても問題ありません。

ただし、これから新しくコーディングを行うサイトであれば、担当者には 「ルート相対パスでお願いします」 と伝えておくことをおすすめします。

ルート相対パスを使うことで、テンプレートの階層や呼び出し場所に依存せず、どのページからも一貫して正しく参照できる だけでなく、後からファイル構成を整理して階層を移動した場合でもパスが変わってしまう心配が少なくなり、のちのち実装が楽になります。


2. 動作環境を用意します

このチュートリアルでは、a-blog cms Ver. 3.2.x の環境で、Develop テーマを新規インストールしたところから始める 前提で説明しています。

本文中の例では https://ablogcms-tutorial.ddev.site/での利用を想定していますが、2-2 の ablogcms.io(無料お試し環境) を利用する場合でも問題ありません。その際は、以下のフォームで利用する URL を設定し、(変更する)ボタン をクリックしてください。

このドキュメントページ内のリンクが https://ablogcms-tutorial.ddev.site/ から指定した URL に書き換わり、クリックするとその環境に対応したページを開けるようになります。


2-1. ローカル環境に新規にインストールする

a-blog cms は動的な Web アプリケーションです。静的な HTML ファイルをただ置くだけでは動かず、動作するためにいくつかのソフトウェアが必要です。

  • Web サーバー — ブラウザからのリクエストを受け取り、処理結果をレスポンスとして返すソフトウェアです(Apache や Nginx など)

  • PHP インタープリター — a-blog cms 本体は PHP で書かれています。PHP のコードを解釈・実行するプログラムが必要です

  • データベース — 記事やユーザーなどのデータを保存・取得するための仕組みです(MySQL など)

これら3つを自分のパソコン上に個別にインストール・設定するのは手間がかかります。DDEV を使うと、これらをまとめてコンテナとして動かすことができ、バージョン管理や設定の手間を大幅に省けます。

DDEV とは

DDEV は、Web 開発用のローカル環境を素早く、確実に立ち上げるためのツールです。内部的には Docker を使ってサーバーを動かしていますが、複雑な Docker の設定を自分で書く必要はありません。コマンド数行で PHP・MySQL・Web サーバーが揃った環境が手に入ります。

すでに Docker を直接使ったことがある方なら、docker-compose.yml を手書きする代わりに、DDEV がその作業を引き受けてくれるイメージです。

なぜ DDEV を使うのか

MAMP は GUI で操作できるシンプルさが魅力ですが、チーム開発や複数プロジェクトの管理が複雑になりがちです。Docker を直接使う方法は柔軟性が高い反面、設定ファイルの記述に慣れが必要です。

DDEV はその中間にあたる選択肢で、以下のようなメリットがあります。

  • macOS・Windows・Linux すべてで同じように動く

  • プロジェクトごとに PHP バージョンを切り替えられる

  • ddev start ひとつで起動、ddev stop で停止とシンプル

  • チームで設定ファイルを共有すれば全員が同じ環境を再現できる

セットアップ手順

1. Docker をインストールする

DDEV は内部で Docker を使うため、先に Docker のインストールが必要です。お使いの OS に合わせて DDEV 公式ドキュメントを参照してインストールしてください。

Docker のインストール(公式ドキュメント)

複数のDocker provider がありますが、Docker 公式が提供する Docker Desktop の利用を推奨します。

2. DDEV をインストールする

次に DDEV 本体をインストールします。お使いの OS に合わせて DDEV の公式ドキュメントを参照してください。

DDEV のインストール(公式ドキュメント)

インストール後、以下のコマンドを実行してバージョンが表示されれば、正しくインストールされています。

ddev -v
3. プロジェクトフォルダを作成する

任意の作業ディレクトリに、プロジェクト用のフォルダを作成します。

mkdir ablogcms-tutorial
cd ablogcms-tutorial
4. a-blog cms のファイルを配置する

ダウンロードページから a-blog cms のパッケージをダウンロードし、展開してください。展開後、ablogcms ディレクトリの中身をプロジェクトフォルダ直下の web/ ディレクトリに配置してください。

ablogcms-tutorial/
└── web/          ← a-blog cms のファイルをここに展開する
5. DDEV プロジェクトを設定する

以下のコマンドで DDEV の設定ファイルを生成します。

ddev config --project-type=php --docroot=web --php-version=8.4 --database=mysql:8.4

PHP バージョンは動作条件を確認して、必要に応じて変更してください。

6. DDEV を起動する
ddev start

起動が完了すると、アクセス用の URL(例: https:/ablogcms-tutorial.ddev.site)が表示され、以下のます。

7. a-blog cms をインストールする

ブラウザで表示された URL にアクセスするとインストール画面が開きます。

データベースの接続設定は以下のように入力してください。

項目

データベースホスト

db

データベース名

db

ユーザー名

db

パスワード

db

db は DDEV が自動的に用意するデータベースコンテナのサービス名です。localhost や 127.0.0.1 では接続できませんのでご注意ください。

よく使うコマンド

コマンド

説明

ddev start

プロジェクトを起動する

ddev stop

プロジェクトを停止する(データは保持される)

ddev restart

プロジェクトを再起動する

ddev describe

プロジェクトの接続情報を表示する

ddev sequelace

Sequel Ace でデータベースを開く

ddev delete

プロジェクトとデータベースを削除する

ddev poweroff

すべてのプロジェクトとコンテナを停止する

その他のコマンドについては DDEV 公式ドキュメント(Commands) を参照してください。

あとは画面の指示に従って進め、必要な情報を入力しながらインストールを完了してください。途中で テーマの選択画面 が表示されますので、必ず 「Develop」 を選択してください。

2-2. 無料のお試しサーバー ablogcms.io を利用する

CMS を試しに触れる際に一番のハードルになるのは、動作環境の準備 です。この点をクリアするために、開発元では 30日間限定の無料お試しサーバー環境 を用意しています。

  • SFTP 接続が可能 : テーマファイルを編集してカスタマイズできます。

  • PHP のアップロードは不可 : カスタマイズはテーマファイルに限られます。

  • メール送信には注意 : フォームなどからのメール送信を行う場合は、SMTP 設定が必要です。

  • 完全無料 : お試し後に課金されることはありません。

  • 30日で環境は自動削除 : 期限が過ぎると利用できなくなります。

  • 繰り返し利用可能 : 必要であれば新しい環境を再度申し込むことができます。

インストール作業を省略して、すぐに a-blog cms を試したい方におすすめの方法 です。

https://www.ablogcms.io/

SCR-20250826-jssc

まとめ

この章でやったことのおさらいです。

  • サンプルサイトの構成と、このチュートリアルで扱う範囲を確認した

  • Twig と標準テンプレートの記法の違いを概観した

  • a-blog cms の動作環境を用意した

次の章では、Twigテンプレートの基本構文を学びます。実際にコードを書き始める前に、変数・ループ・条件分岐の3パターンを最小サンプルで確認しておきましょう。

次の章へ:Twig テンプレートの基本構文をおさえよう →

第2回:Twig テンプレートの基本


この章では Twig テンプレートエンジンについて学びます。すでに Twig テンプレートエンジンについて知っている方は飛ばしていただいて構いません。


この章でやること

  • Twig の3種類の構文(出力・制御・コメント)を理解する

  • 変数・条件分岐・ループをTwig Playgroundで実際に動かして体験する

  • a-blog cms 独自の要素(モジュール・グローバル変数・フィルター)との関係を把握する

なぜ Twig なのか

a-blog cms は Ver. 3.2 から、従来の標準テンプレートに加えて、Twig によるテンプレート記述をサポートしています。

標準テンプレートは a-blog cms 独自の記法で設計されており、できることは豊富です。一方で、ループや条件分岐といった制御構文が独自仕様になっているため、シンプルな出力は簡単にできても、少し複雑なカスタマイズをしようとすると途端に覚えることが増えるという側面があります。

Twig は PHP 製のテンプレートエンジンで、Craft CMS・Symfony・Drupal などでも広く採用されています。変数の出力・ループ・条件分岐の書き方が、PHP や JavaScript などの一般的なプログラミング言語に近いため、「Twig の書き方を覚える」というより「すでに知っている書き方がそのまま使える」という感覚で書けます。

たとえば条件分岐なら、標準テンプレートでは a-blog cms 固有の構文を覚える必要がありますが、Twig では次のように書けます。

{% if entry.tags %}
  {% for tag in entry.tags %}
    <span>#{{ tag.name }}</span>
  {% endfor %}
{% endif %}

「タグがあれば一覧を出す」という意図が、コードを読んだだけで伝わります。Twigに慣れたメンバーがいれば、a-blog cmsを知らなくてもテンプレートのロジックを読み解けます。これはチーム開発や長期保守において大きなメリットになります。

どちらの形式でも a-blog cms の機能はすべて利用できます。新規テーマをTwigで書く、あるいは既存テーマをTwig化してメンテナンス性を高める、どちらの用途にも使えます。

まず動かしてみよう — Twig Playground

a-blog cms の環境がなくても、Twig Playground を使えばブラウザ上でTwigの構文をすぐに試せます。

この章では各サンプルコードをそのまま貼り付けて実行できます。環境構築を待たずに、まず「書いて動かす」体験から始めましょう。

エディターにシンタックスハイライトを設定する

.twig ファイルを素のまま開くと、{{ }}{% %} が通常のテキストと同じ色で表示されてしまい、構造が非常に読みづらくなります。コードを書き始める前に、シンタックスハイライトの拡張機能を入れておきましょう。

VS Code を使っている場合は、拡張機能マーケットプレイスで 「Twig Language 2」を検索してインストールしてください。

インストール後、VS Code の再起動(または「ウィンドウの再読み込み」)が必要になることがあります。

シンタックスハイライトがあると、{{ }}{% %}{# #} の3種類がそれぞれ異なる色で表示され、HTMLとTwig構文の境界が一目でわかるようになります。コード量が増えるほどその効果を実感できます。

1. Twig の3種類の構文

Twig には HTML の中に埋め込む構文が3種類あります。これだけ覚えれば、基本的なテンプレートは読み書きできます。

構文

用途

{{ }}

変数や式を出力する

{{ title }}

{% %}

タグ。ループ・条件分岐などの制御を書く

{% for item in list %}

{# #}

コメント(HTML出力には含まれない)

{# ここはメモ #}

これら3種類のデリミタさえ区別できれば、Twig のテンプレートファイルは読めるようになります。


2. 変数の出力

{{ }} で変数を出力します。

{{ title }}
{{ entry.id }}

Twig Playground で試す:

{% set name = "a-blog cms" %}
<p>{{ name }} へようこそ!</p>

変数が持つプロパティには .(ドット)でアクセスします。

Twig Playground で試す:

{% set entry = {
  title: "新しい季節限定メニューが登場しました",
  date: "2025-08-07",
  category: {
    name: "新商品"
  }
} %}

<h1>{{ entry.title }}</h1>
<p>{{ entry.date }}</p>
<span>{{ entry.category.name }}</span>

entry.category.name のように、オブジェクトのネストも . をつなぐだけでアクセスできます。

グローバル変数も同じ記法

標準テンプレートでは %{ENTRY_TITLE} のようにグローバル変数だけ別の記法がありましたが、Twig ではグローバル変数も {{ }} で書けます。記法を使い分ける必要がありません。

{# 標準テンプレートのグローバル変数に相当 #}
<title>{{ ENTRY_TITLE }}</title>
<p>現在のカテゴリーコード:{{ CCD }}</p>

3. 変数の定義

{% set %} でテンプレート内に変数を定義できます。

Twig Playground で試す:

{% set greeting = "こんにちは" %}
{% set name = "山田" %}

<p>{{ greeting }}、{{ name }}さん!</p>

文字列の連結には ~(チルダ)演算子を使います。

Twig Playground で試す:

{% set fullName = "山田" ~ " " ~ "太郎" %}
<p>{{ fullName }}</p>

4. 条件分岐(if)

{% if %} / {% endif %} で条件分岐を書きます。PHP や JavaScript の if と同じ感覚で書けます。

Twig Playground で試す:

{% set isLoggedIn = true %}

{% if isLoggedIn %}
  <p>ログイン中です。</p>
{% else %}
  <p>ログインしてください。</p>
{% endif %}

条件式の記述例

比較演算子

  • ==(等しい)

  • !=(等しくない)

  • >(大きい)

  • <(小さい)

  • >=(以上)

  • <=(以下)

Twig Playground で試す:

{% set age = 18 %}

{% if age >= 18 %}
  <p>成人です。</p>
{% else %}
  <p>未成年です</p>
{% endif %}

論理演算子

  • and(かつ)

  • or(または)

  • not(否定)

Twig Playground で試す:

{% set isLogin = true %}
{% set isAdmin = true %}

{% if isLogin and isAdmin %}
    <p>ログイン済みの管理者です。</p>
{% endif %}

{% if not isLogin %}
    <p>ログインしてください。</p>
{% endif %}

is 演算子

  • empty: 空かどうか

  • defined: 定義されているかどうか

  • null: nullであるかどうか

  • even/odd: 偶数か奇数か

  • divisible by: 指定した数で割り切れるか

Twig Playground で試す:

{% set name = '山田' %}
{% set list = [] %}
{% set number = 6 %}

{% if name is defined %}
    <p>名前: {{ name }}</p>
{% endif %}

{% if list is empty %}
    <p>リストは空です。</p>
{% endif %}

{% if number is even %}
<p>
  {{ number }}は偶数です。
</p>
{% endif %}

{% if number is divisible by(2) %}
  <p>{{ number }}は2で割り切れます。</p>
{% elseif number is divisible by(3) %}
  <p>{{ number }}は3で割り切れます。</p>
{% else %}
  <p>{{ number }}は2でも3でも割り切れません。</p>
{% endif %}

in 演算子

Twig Playground で試す:

{% set colors = ["red", "blue", "green"] %}

{% if "blue" in colors %}
    <p>青色が含まれています。</p>
{% endif %}

他のプログラミング言語と同様に、括弧を使って条件式の優先度を制御できます。



5. ループ(for)

{% for %} / {% endfor %} でリストを繰り返し処理します。

Twig Playground で試す:

{% set fruits = ['Apple', 'Banana', 'Cherry'] %}

<ul>
  {% for fruit in fruits %}
    <li>{{ fruit }}</li>
  {% endfor %}
</ul>

loop — ループ内の特殊変数

ループの中では loop という特殊変数が使えます。「最後の要素以外にセパレーターを入れる」などの処理が、標準テンプレートで使っていた <!-- BEGIN glue --> より直感的に書くことができます。

Twig Playground で試す:

{% set tags = ["新商品", "季節限定", "夏メニュー"] %}

<div>
  {% for tag in tags %}
    #{{ tag }}{% if not loop.last %}、{% endif %}
  {% endfor %}
</div>

よく使う loop 変数をまとめます。

変数

内容

loop.index

現在のインデックス(1始まり)

loop.index0

現在のインデックス(0始まり)

loop.first

最初の要素なら true

loop.last

最後の要素なら true

loop.length

要素の総数


6. 階層データのツリー表示

カテゴリーやメニューなど、親子関係のあるデータを表示する場面はよくあります。親の中に子がネストしたHTML構造を作るには、macro(マクロ)を使った再帰呼び出しが有効です。階層の深さに関係なく同じテンプレートで処理できます。

Twig Playground で試す:

{% set categoryTree = {
  name: "ドリンク",
  children: [
    { name: "コーヒー", children: [] },
    { name: "ティー", children: [] }
  ]
} %}

{% macro renderTree(node) %}
  <li>
    {{ node.name }}
    {% if node.children is not empty %}
      <ul>
        {% for child in node.children %}
          {{ _self.renderTree(child) }}
        {% endfor %}
      </ul>
    {% endif %}
  </li>
{% endmacro %}

<ul>
  {{ _self.renderTree(categoryTree) }}
</ul>

macro(マクロ)について

macro(マクロ)は、HTML を出力する再利用可能な関数です。同じテンプレート内で似たような HTML を何度も書く場合に便利で、include ほど大げさにしたくないときの選択肢になります。

マクロの定義と呼び出し

{% macro 名前(引数) %} ... {% endmacro %} で定義し、同じファイル内では {{ _self.名前(引数) }} で呼び出します。

Twig Playground で試す:

{% macro navItem(label, path) %}
  <li class="nav-item"><a href="{{ path }}">{{ label }}</a></li>
{% endmacro %}

<nav>
  <ul>
    {{ _self.navItem('ホーム', '/') }}
    {{ _self.navItem('お知らせ', '/news') }}
    {{ _self.navItem('お問い合わせ', '/contact') }}
  </ul>
</nav>

同じ <li> の構造を3回書く代わりに、マクロ1つでまとめています。

再帰呼び出し

再帰とは、関数が自分自身を呼び出すことです。マクロも再帰できます。

ツリー構造のデータでは、どのノードも「名前を表示する」「子があれば子をループする」という同じ処理になります。階層が何段になっても同じロジックで扱えるため、再帰が向いています。

処理の流れ:

  1. renderTree(categoryTree) が呼ばれる(ルート「ドリンク」)

  2. 「ドリンク」を <li> で出力し、children が空でなければループ開始

  3. 子「コーヒー」に対して _self.renderTree(child) を呼ぶ

  4. 「コーヒー」を出力。children が空なので再帰せず終了

  5. 子「ティー」に対して同様に _self.renderTree(child) を呼ぶ

  6. 以下、子がなくなるまで繰り返し

終了条件が重要です。{% if node.children is not empty %} で子があるときだけ再帰するため、葉(子のいないノード)に到達すると再帰が止まります。この条件がなければ無限ループになります。

孫やひ孫がいる場合も、同じマクロが「自分の名前を出して、子がいれば子ごとに自分を呼ぶ」を繰り返すだけなので、階層の深さを意識せずに書けます。

別テンプレートのマクロを使う

他のテンプレートで定義したマクロを使うには、import タグで読み込みます。

{% import '_macros.twig' as macros %}
{{ macros.navItem('ホーム', '/') }}

7. フィルター(| パイプ)

値を変換・加工するには |(パイプ)でフィルターをつなぎます。

{# 日付フォーマット #}
{{ entry.date | date('Y年m月d日') }}

{# 文字数で切り詰め(a-blog cms 独自フィルター) #}
{{ entry.summary | trim(200, '...') }}

{# 画像リサイズ(a-blog cms 独自フィルター) #}
{{ entry.path | resizeImg(600) }}

| はいくつでもつなげられます。

{# 先頭から200文字を取り出してから小文字に #}
{{ text | slice(0, 200) | lower }}

Twig Playground で試す:

{% set date = "now" | date('Y年m月d日') %}
<p>今日は {{ date }} です。</p>

7. コメント

{# #} でコメントを書きます。コメントはHTMLとして出力されません。

{# ここはコメントです。HTML出力には含まれません #}
{# TODO: このモジュールのカテゴリーを後で確認する #}

HTML コメント(<!-- -->)はそのままソースに出力されますが、Twig コメントは出力されません。テンプレートへの作業メモにはTwigコメントを使いましょう。


8. インクルード

テンプレートの共通部分(ヘッダー・フッター・カードのHTML断片など)を別ファイルに切り出して再利用するには、include() 関数を使います。

たとえばカードコンポーネントを _card.twig として用意するとします。

{# _card.twig #}
<div class="card">
  <h2>{{ entry.title }}</h2>
  <p>{{ entry.summary | trim(100, '...') }}</p>
</div>

これを別のテンプレートから読み込むには次のように書きます。

{{ include('_card.twig') }}

変数を渡したい場合は第2引数にハッシュで指定します。

{{ include('_card.twig', { entry: featuredEntry }) }}

a-blog cms では、管理ボックスや管理UIのパーツもインクルードで読み込みます。

{# エントリー編集ボックスの読み込み #}
{{ include('admin/entry/action.twig', { entry }) }}

{# モジュール設定ボタンの読み込み #}
{{ include('/admin/module/setting.twig', { moduleInfo: entrySummary.moduleInfo }) }}

これらのファイルはa-blog cms本体が提供しているため、自分で作成する必要はありません。テンプレートの決まった位置に書くだけで管理UIが有効になります。

9. a-blog cms との組み合わせイメージ

Twig の基本構文を把握したところで、a-blog cms のモジュールと組み合わせると次のようになります。実際の実装は次章以降で進めますが、全体のイメージとして確認しておきましょう。

{% set entrySummary = module('V2_Entry_Summary', 'モジュールID名', { bid: BID, cid: CID }) %}
<div>
  <div class="acms-cssgrid acms-g-cols-1 acms-g-cols-md-3">
    {% for entry in entrySummary.items %}
    <div>
      {% if entry.mainImage.path %}
      <img
        src="{{ entry.mainImage.path|resizeImg(360) }}"
        alt="{{ entry.mainImage.alt }}"
        class="acms-img-responsive"
        width="{{ entry.mainImage.width }}"
        height="{{ entry.mainImage.height}}"
        loading="lazy"
        decoding="async"
      >
      {% endif %}
      <h3>{{ entry.title }}</h3>
      <p>{{ entry.summary }}</p>
      <p><a href="{{ entry.url }}" class="acms-btn">詳細をみる</a></p>
    </div>
    {% endfor %}
  </div>
</div>

module は a-blog cms 独自の関数ですが、その中身は完全なTwig構文で書けます。a-blog cms 固有の概念は少し覚えるだけで、制御構文は一般的なTwigの知識がそのまま使えるという構成です。


まとめ

この章でおさえたこと:

  • {{ }} で変数を出力し、{% %} で制御構文を書き、{# #} でコメントを書く

  • グローバル変数も通常の変数と同じ {{ }} 記法で書ける

  • {% if %} / {% for %} は一般的なプログラミング言語と同じ感覚で書ける

  • is empty / is defined / loop.last などの便利な記法がある

  • | フィルターで値を加工する(date()trim()resizeImg() など)

  • {{ include('ファイル名') }} で共通パーツを再利用する

  • Twig Playground で環境なしに構文を試せる

また、チュートリアルでは詳しく扱いませんが、発展情報として以下の内容も参考にしてください

次の章では、サンプルHTMLをa-blog cmsのTwigテーマとして設定します。まだCMS化はしませんが、「既存のHTMLがa-blog cmsを経由して表示される」状態を作ることが最初のゴールです。

第3回:静的HTMLをテーマとして設定する


この章でやること

  • サンプルHTMLをTwigテーマとして配置し、a-blog cmsで表示できる状態にする

  • 管理ボックスをテンプレートに組み込む

  • デバッグモードを有効にして、実装中のエラーをすぐに確認できるようにする

  • まだCMS化はしない――「HTMLがa-blog cmsを経由して処理される」状態を確認する

この章の終わりには、見た目はサンプルの静的サイトそのままで、a-blog cmsが裏側で動いている状態が完成します。


部分的に CMS 化ができる a-blog cms ですが、まずは どこも更新できない状態のまま 、サンプル一式を CMS のテーマとして設定 するところから始めます。最初の目的は、既存の HTML を崩さずに a-blog cms のテーマとして正しく認識させることです。

1. テーマ(themes)ディレクトリに全てのファイルをコピーする

a-blog cms では、themes/ ディレクトリの中にあるフォルダを テーマ として認識します。まずは、用意したサンプルサイト一式(ルート相対パス版)を以下のようにコピーしてください。

/themes/sample/
  ├── index.html
  ├── news/
  ├── service/
  ├── contact/
  ├── css/
  └── images/

2. テーマの設定を変更します

  1. a-blog cms にログインします。

  2. 管理ページ)のボタンをクリックして管理画面に入ります。

  3. 左メニューの「テーマ設定」から既存の「Develop 基本テーマ」 をクリックします。

  4. 「テーマディレクトリ名」の SELECTメニューに「sample」を選択してください。

  5. 保存してください。

次にテンプレートファイルの設定を変更します。

トップページ

_top.twig

一覧ページ

index.twig

詳細ページ

_entry.twig

エラーページ(404 Not Found)

404.twig

変更後は保存してください。

a-blog cms ではテンプレートの基本的なファイル名にルールがあり、 テーマ管理の画面(上記キャプチャ)でもそのルールが設定されている ことが確認できます。必要に応じて変更することも可能です。

3. ファイルを .twig 形式にリネームする

一般的には、トップページは _top.twig、一覧ページは index.twig、詳細ページは _entry.twig として作成するのが基本です。今回のチュートリアルでも、このルールに沿ってサンプルの HTML を活用していきます。

/themes/sample/
├── _top.twig    ← index.html をリネーム
├── news/
├── service/
├── contact/
├── css/
└── images/

index.html_top.twig にリネームすることで、a-blog cmsがトップページテンプレートとして認識します。

4. 表示を確認しましょう

これで https://ablogcms-tutorial.ddev.site/ にアクセスすると、まだ CMS を利用して更新できる状態にはなっていませんが、画像やページ遷移なども リンク切れになることなく、既存の HTML サイトがそのまま表示される ようになります。

さらに、CSS や JavaScript の読み込みについては a-blog cms によって以下のように書き換え処理が行われます。

<!-- 書き換え前 -->
<link rel="stylesheet" href="/css/custom.css">

<!-- 書き換え後(a-blog cmsによる自動処理) -->
<link rel="stylesheet" href="/themes/sample/css/custom.css?date=20250817215109">
  • /themes/sample/ が追記されて正しいテーマの場所を参照

  • ?date=… が付加され、キャッシュバスティング によりブラウザキャッシュを回避

画像も同様にパスが補完され、リンク切れを防ぎます。

<img src="/themes/sample/images/main_visual.jpg" alt="美味しそうな料理のメインビジュアル" class="w-full h-full object-cover" >

また、HTML の <head> タグの直前には、a-blog cms が生成していることを示す meta タグが自動的に追記されています。

<meta name="generator" content="a-blog cms" />

このタグが出力されていれば、サンプル HTML が単なる静的ファイルとして表示されているのではなく、a-blog cms を経由して処理されている ことが確認できます。


5. 管理用UIの設置を最初にする

テーマを切り替える前は、トップページに表示されていた (管理ページ) ボタンから管理画面へアクセスできていました。新しいテーマに切り替えたことでこの導線が消えています。運用時に困らないよう、先に管理用ボックスを設置 しておきます。

SCR-20250820-uilx

設置場所の目安

_top.twig<section id="about" ...> の上に以下を追加してください。

@include("/admin/action.html")

この状態で https://ablogcms-tutorial.ddev.site/ にアクセスすると、管理用 CSS が未読込のためレイアウトが崩れて表示 され、操作しづらいはずです。正常に表示できるよう、<head> タグ内に 管理 UI 用のリソース を追加します。

<link rel="stylesheet" href="/css/acms-admin.min.css">
{% set js = module('V2_Js') %}
<script src="{{ JS_LIB_JQUERY_DIR }}jquery-{{ JS_LIB_JQUERY_DIR_VERSION }}.min.js" charset="UTF-8"></script>
<script src="{{ ROOT_DIR }}acms.js{{ js.arguments }}" charset="UTF-8" id="acms-js"></script>
  • 1行目 : a-blog cms の管理用 CSS

  • 3行目 : a-blog cms で必要な jQuery のバージョンを読み込み

  • 4行目 : a-blog cms の管理用 JavaScript

これで、ページ上部に管理ボックスが正しく横並びで表示され、ページから直接「新規投稿」「管理画面へ移動」 「ログアウト」といった操作などが行えるようになります。

7. デバッグモードを有効にする(推奨)

次の章からテンプレートの実装を進めると、Twigの構文エラーや変数の参照ミスが発生することがあります。デバッグモードを有効にしておくと、エラーメッセージやインクルード先のパスが表示され、原因の特定がしやすくなります。

管理ユーザーごとにデバッグモードを有効にできます。管理画面の「ユーザー管理」から該当ユーザーを編集し、「デバッグモード 」を選択して保存してください。開発環境・本番環境を問わず、そのユーザーでログインしているときだけ有効になるため、本番環境でも安全に試せます。

デバッグモードとベンチマークモード


デバッグモードを有効にするとキャッシュが効かなくなり、エラーメッセージも表示されます。開発中は有効にしておくことを推奨しますが、本番公開時には無効にしてください。


まとめ

この章でやったこと:

  • サンプルHTMLを themes/sample/ にコピーし、テーマとして認識させた

  • index.html_top.twig にリネームし、Twig テンプレートとして設定した

  • a-blog cmsによるパス自動補完とメタタグ出力で「CMSを経由している」ことを確認した

  • 管理UI(@include("/admin/action.html"))と管理UI用リソースを _top.twig に追加した

まだCMS化はしていませんが、HTMLがa-blog cmsを経由して処理される状態が整いました。次の章からいよいよ実装を始めます。最初のターゲットは、更新頻度が高く効果を実感しやすい「お知らせ一覧」のCMS化です。


第4回:トップページのお知らせ一覧をCMS化する


この章でやること

  • 管理画面でカテゴリー・カスタムフィールド・サンプル記事を準備する

  • _top.twig のお知らせ一覧セクションをCMS化する

  • V2_Entry_Summary モジュールの関数形式の書き方を理解する

  • モジュールIDを作成して表示条件(カテゴリー・件数)を設定する

この章が終わると、管理画面で投稿した記事がトップページのお知らせ一覧に自動的に反映されます。


1. CMSのデータを準備する

テンプレートを実装する前に、管理画面でCMSのデータを準備します。必要な作業は「カテゴリーの作成」「カスタムフィールドの設定」「サンプル記事の投稿」の3つです。

1-1. カテゴリーを作成する

このチュートリアルでは、ルートブログの配下に「お知らせ」カテゴリーを作成します。

インストール時に Develop テーマを選択している場合はこの作業は不要です。

  1. 管理画面の左メニューからルートブログの「カテゴリー管理」を開く

  2. 「カテゴリーを追加」をクリック

  3. 以下のとおり設定して保存する

項目

設定値

カテゴリー名

お知らせ

カテゴリーコード

news

ステータス

公開


カテゴリーコードについて

カテゴリーコードはURLの一部になります。news と設定すると https://ablogcms-tutorial.ddev.site/news/ でアクセスできるようになり、サンプルサイトのディレクトリ構造と一致します。


1-2. カスタムフィールドを設定する

一覧ページでサムネイル画像を表示するため、エントリーにメディアフィールドを追加します。カスタムフィールドはHTMLで定義したフィールド構造をテンプレートファイルに書くことで管理画面の入力欄になる仕組みです。

まず以下のディレクトリ構成でファイルを作成します。

/themes/sample/
└── admin/
    └── entry/
        └── field.html   ← 新規作成

field.html に以下を記述します。メディアフィールドのHTMLはカスタムフィールドメーカーで生成します。

クイックサーチ機能を利用することでカスタムフィールドメーカーのページに移動することができます。MacOSであれば ⌘ + k 、Windows であれば Ctrl + k でクイックサーチを表示して「カスタムフィールドメーカー」と入力し、Enter キーを押下するか、メニュー内の「カスタムフィールドメーカー」をクリックしてください。

カスタムフィールドメーカーで「メディア」タイプを選択し、以下の設定で生成してください。

項目

設定値

タイトル

メイン画像

フィールド

entry_main_image

タイプ

メディア

生成されたHTMLを field.html に貼り付けて保存します。管理画面のエントリー編集画面を開くと、画像アップロード欄が追加されていることを確認できます。


1-3. サンプル記事を投稿する

テンプレートの動作確認用にサンプル記事を3件以上投稿します。

  1. 管理画面の「エントリー管理」から「エントリー作成」をクリック

  2. カテゴリーで「お知らせ」を選択

  3. ステータスを公開に設定

  4. タイトル・本文・メイン画像・タグを適当に設定して公開

記事が3件揃ったらテンプレートの実装に進みます。


2. 対象のHTMLを確認する

対象となるHTMLは、_top.twig 内の <section id="news"> です。

<section id="news" class="py-14 sm:py-20 bg-white">
  <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
    <h2 class="...">News</h2>
    <ul class="border-t border-gray-200">
      <li>
        <a href="/news/20250807.html" class="...">
          <time datetime="2025-08-07" class="...">2025.08.07</time>
          <span class="...">新商品</span>
          <span class="...">新しい季節限定メニューが登場しました</span>
        </a>
      </li>
      ...
    </ul>
    <div class="mt-8 text-center">
      <a href="/news/">お知らせ一覧へ →</a>
    </div>
  </div>
</section>

この <li> の繰り返し部分をCMSのデータに置き換えます。


3. V2_Entry_Summary モジュールのスニペットを書く

a-blog cms Twig版でエントリーの一覧を出力するには module() 関数でモジュールのデータを取得します。


Twig版はモジュールタグではなく関数で呼び出す

標準テンプレートでは <!-- BEGIN_MODULE Entry_Summary --> のようなタグ形式でしたが、Twig版では module() 関数を {% set %} で受け取る形式を使います。また、呼び出せるのは 名前が V2_ から始まるモジュールのみです。


まず、<ul class="border-t border-gray-200"> の手前でモジュールデータを取得し、<li> の繰り返し部分をループに置き換えます。

{% set entrySummary = module('V2_Entry_Summary') %}

<ul class="border-t border-gray-200">
  {% for entry in entrySummary.items %}
    <!-- 一覧表示の繰り返し部分 -->
  {% endfor %}
</ul>

次に <!-- 一覧表示の繰り返し部分 --> の位置に、元のHTMLにあった <li></li> をそのままコピーして貼ります。

{% set entrySummary = module('V2_Entry_Summary') %}

<ul class="border-t border-gray-200">
  {% for entry in entrySummary.items %}
    <li>
      <a href="/news/20250807.html" class="flex items-center gap-x-4 p-4 border-b border-gray-200 hover:bg-gray-50 transition-colors">
        <time datetime="2025-08-07" class="flex-shrink-0 text-sm text-gray-500">2025.08.07</time>
        <span class="flex-shrink-0 rounded-full bg-sky-100 px-2.5 py-0.5 text-xs font-semibold text-sky-800">新商品</span>
        <span class="font-medium text-gray-800 truncate">新しい季節限定メニューが登場しました</span>
      </a>
    </li>
  {% endfor %}
</ul>

この状態で https://ablogcms-tutorial.ddev.site/ にアクセスすると、「新しい季節限定メニューが登場しました」が何件も繰り返し表示されます。まだ固定のHTMLを繰り返しているだけですが、ループが動作していることを確認できます。


4. 変数に置き換える

固定値をCMSの変数に置き換えていきます。置き換える箇所はリンク先・日付・カテゴリー名・タイトルの4点です。

置き換え対応表

置き換え前(固定値)

置き換え後(Twig変数)

/news/20250807.html

{{ entry.url }}

2025-08-07(datetime属性)

{{ entry.date | date('Y-m-d') }}

2025.08.07(表示テキスト)

{{ entry.date | date('Y.m.d') }}

新商品

{{ entry.category.items[0].name }}

新しい季節限定メニューが登場しました

{{ entry.title }}

{% set entrySummary = module('V2_Entry_Summary') %}

<ul class="border-t border-gray-200">
  {% for entry in entrySummary.items %}
    <li>
      <a href="{{ entry.url }}" class="flex items-center gap-x-4 p-4 border-b border-gray-200 hover:bg-gray-50 transition-colors">
        <time datetime="{{ entry.date | date('Y-m-d') }}" class="flex-shrink-0 text-sm text-gray-500">
          {{ entry.date | date('Y.m.d') }}
        </time>
    {% if entry.category.items is not empty %}
        <span class="flex-shrink-0 rounded-full bg-sky-100 px-2.5 py-0.5 text-xs font-semibold text-sky-800">
          {{ entry.category.items[0].name }}
        </span>
        {% endif %}
        <span class="font-medium text-gray-800 truncate">{{ entry.title }}</span>
      </a>
    </li>
  {% endfor %}
</ul>

https://ablogcms-tutorial.ddev.site/ にアクセスすると、投稿したお知らせ記事がトップページに表示されます。確認できたら、元々HTMLに書かれていた静的な <li> 部分はすべて削除してください。

V2モジュールの変数の見方

V2モジュールでは、Twigテンプレートでモジュールを読み込んだ状態で、クイックサーチ機能から利用可能な変数を確認できる仕組みになっています。

クイックサーチの起動方法

管理者としてログイン中に、以下のショートカットキーを押すことでクイックサーチが開きます。

  • Windows / LinuxCtrl + K

  • Mac⌘(Command)+ K

デバッグモードでの変数確認

クイックサーチを開いた状態で、デバッグモードが有効になっているときに #(シャープ)を入力すると、現在のページで読み込まれているV2モジュールの変数一覧(変数表)が表示されます。

クイックサーチから変数表を表示
クイックサーチから変数表を表示
実際の値が入った変数表。JSON形式で表示されます。
実際の値が入った変数表が確認できる

変数名をクリックすると、その変数のパスがクリップボードにコピーされます。
これにより、Twigテンプレートへ変数を貼り付ける際に、手入力せず正確な変数名を利用できます。


5. モジュールIDを設定する

現在の状態では、お知らせ以外のカテゴリーの記事も混在して表示されることがあります。「お知らせ」カテゴリーだけを表示するには、モジュールIDを設定して表示条件を管理画面から指定します。

module() 関数の第2引数にモジュールIDを文字列で渡します。

{% set entrySummary = module('V2_Entry_Summary', 'news_top') %}

また、管理用インクルードをセクション直後に追加して、管理者がページをホバーしたときに「設定」ボタンが表示されるようにします。

{% set entrySummary = module('V2_Entry_Summary', 'news_top') %}

<section id="news" class="py-14 sm:py-20 bg-white">
  {{ include('/admin/module/setting.twig', { moduleInfo: entrySummary.moduleInfo }) }}
  <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
    <h2 class="...">News</h2>
    <ul class="border-t border-gray-200">
      {% for entry in entrySummary.items %}
        <li>...</li>
      {% endfor %}
    </ul>
    <div class="mt-8 text-center">
      <a href="/news/">お知らせ一覧へ →</a>
    </div>
  </div>
</section>

moduleInfo とは

moduleInfo は V2 モジュールが返す特別な変数です。module() 関数で取得したデータ(ここでは entrySummary)の moduleInfo プロパティとして参照できます。

この変数はログイン中の管理者に対してのみセットされます。内部ではブログID(bid)・モジュールID(mid)・管理画面の設定URL(url)・モジュールクラス名(name)・モジュールID(identifier)などが格納されており、admin/module/setting.twig に渡すことで、閲覧画面からモジュールIDの設定画面へ遷移する「編集」リンクを表示できます。一般ユーザーが閲覧する場合は moduleInfo は空となり、編集リンクは表示されません。


6. 管理画面でモジュールIDを作成する

テンプレートにモジュールIDを書いただけでは機能しません。管理画面側でモジュールIDを登録して表示条件を設定する必要があります。

  1. https://ablogcms-tutorial.ddev.site/ の管理ボックスから「(管理ページ)」をクリック

  2. 右メニューから「モジュールID」を選択

  3. 「モジュール作成」をクリック

  4. 以下のとおり設定する

条件設定

項目

設定値

モジュール

サマリー(V2_Entry_Summary)

モジュールID

news_top

名前

お知らせトップ用

カテゴリーID(引数)

お知らせ(「ID参照」ボタンから選択)


7. モジュールIDの表示設定を確認する

モジュールIDを作成すると「条件設定」タブの横に「表示設定」タブが追加されます。ここで件数と並び順を設定します。

表示設定

項目

設定値

表示順

日付(降順)

表示件数

5

この設定で、トップページのお知らせには新しい記事から5件が表示されるようになります。

https://ablogcms-tutorial.ddev.site/ にアクセスして、お知らせ一覧に「お知らせ」カテゴリーの記事だけが5件表示されていれば、この章の目標達成です。


まとめ

この章でやったこと:

  • module('V2_Entry_Summary', 'news_top') でモジュールデータを取得した

  • {% for entry in entrySummary.items %} でループを実装した

  • {{ entry.title }}{{ entry.url }}{{ entry.date | date('Y.m.d') }}{{ entry.category.items[0].name }} で各変数を出力した

  • モジュールID news_top を管理画面に登録し、カテゴリー・件数・ソート順を設定した

次の章では、一覧から記事をクリックしたときに表示される詳細ページを実装します。本文・タイトル・日付・タグが正しく出力されるようにしていきましょう。


第5回:詳細ページを作る


この章でやること

  • news/_entry.twig を作成し、詳細ページを表示できるようにする

  • V2_Entry_Body モジュールで記事本文を出力する

  • 記事ヘッダー(タイトル・日付・カテゴリー・タグ)をTwig変数で実装する

  • グローバル変数を使ってパンくずリストと<title>タグを動的にする

この章の終わりには、一覧から記事をクリックすると記事ごとの本文が表示される状態になります。


1. テンプレートを準備する

現在、トップページのお知らせ一覧からリンクをクリックしても「404 Not Found」になります。詳細ページ用のテンプレートファイル _entry.twig が存在しないためです。

既存の静的ページをひな形にしてテンプレートを作ります。

news/20250807.html をコピーして news/_entry.twig を作成してください。また、news ディレクトリ内の _entry.twig , index.html, page2.html を残して他のファイルを削除してください。

/themes/sample/
├── _top.twig
├── news/
│   └── _entry.twig   ← 追加
├── service/
├── contact/
├── css/
└── images/

この状態でトップページのお知らせ一覧からリンクをクリックすると、「404 Not Found」にはならず、20250807.html の内容がすべてのリンク先で表示されます。どの記事をクリックしても同じページが出る状態です。ここからCMS化を進めていきます。


2. V2_Entry_Body モジュールのスニペットを書く

news/_entry.twig のコードを確認すると、<article></article> の内側が、V2_Entry_Body モジュールを配置する場所です。

module() 関数でデータを取得し、<article> の内側に以下のスニペットを書いてください。

{% set entryBody = module('V2_Entry_Body', null, { eid: EID }) %}

{% for entry in entryBody.items %}
  <article>
    <header>
      <h1>{{ entry.title }}</h1>
      <!-- entry header contents -->
    </header>

    <div>
      {% if entry.body %}
        <div>
          {{ entry.body|raw }}
        </div>
      {% endif %}
    </div>

    <div class="acms-box-medium">
      {{ include('admin/entry/action.twig', { entry }) }}
    </div>
  </article>
{% endfor %}

詳細ページでは、URLで渡ってくる eid(エントリーID)に応じて表示する記事を切り替えます。第2引数に null(モジュールIDなし)、第3引数で eid: EID を渡しています。EID はグローバル変数で、現在表示しているページのURLから取得したエントリーIDが入っています。このように第3引数で eid を渡すことで、一覧からどの記事をクリックしても、クリックした記事の本文が表示されます。

トップページから最新のお知らせ記事をクリックすると、既存の静的HTMLの上にCMSが出力した記事本文が表示されることを確認できます。この時点ではほぼCSSが効いていないプレーンな状態ですが、一覧から詳細ページへの遷移が成立しています。https://ablogcms-tutorial.ddev.site/ でいくつかのお知らせ記事をクリックし、それぞれ異なる記事本文が表示されることを確認してください。

module() 関数の引数について

引数

内容

第1引数

V2モジュール名(例:V2_Entry_Summary)

第2引数

モジュールID(管理画面で設定されたIDを指定)

第3引数

URLコンテキスト(従来の ctx に該当。例:{ bid: BID, eid: EID } など)

※ 「モジュールID」と「URLコンテキスト」は、従来モジュール同様に省略可能です。

{% set entrySummary = module('V2_Entry_Summary') %}

3. 記事ヘッダー情報を整える

記事本文の前に表示するタイトル・日付・カテゴリー・タグを整えます。スニペット内の <!-- entry header contents --> の部分を以下のコードに置き換えてください。

変換前(静的HTML)

<p class="text-base font-semibold text-sky-600 text-center">
  <time datetime="2025-08-07">2025年8月7日</time>
</p>
<h1 class="mt-2 text-3xl font-bold ...">新しい季節限定メニューが登場しました</h1>
<div class="mt-6 flex flex-wrap items-center justify-center gap-x-4 gap-y-2">
  <a href="#" class="bg-sky-100 text-sky-800 ...">新商品</a>
  <a href="#" class="text-sm text-gray-500 ...">#季節限定</a>
  <a href="#" class="text-sm text-gray-500 ...">#夏メニュー</a>
</div>

変換後(Twig変数)

<p class="text-base font-semibold text-sky-600 text-center">
  <time datetime="{{ entry.datetime|date('Y-m-d') }}">
    {{ entry.datetime|date('Y年m月d日') }}
  </time>
</p>
<h1 class="mt-2 text-3xl font-bold ...">{{ entry.title }}</h1>
<div class="mt-6 flex flex-wrap items-center justify-center gap-x-4 gap-y-2">
  {% if entry.category.items is not empty %}
    <a href="{{ entry.category.items[0].url }}" class="bg-sky-100 text-sky-800 ...">
      {{ entry.category.items[0].name }}
    </a>
  {% endif %}
  {% for tag in entry.tags %}
    <a href="{{ tag.url }}" class="text-sm text-gray-500 hover:text-gray-700">
      #{{ tag.name }}
    </a>
  {% endfor %}
</div>

置き換えポイント解説

日付フォーマット

{{ entry.datetime|date('Y年m月d日') }}

日付は entry.datetime プロパティを使い、Twig標準の date フィルターでフォーマットします。

カテゴリーリンク

{% if entry.category.items is not empty %}
  <a href="{{ entry.category.items[0].url }}">{{ entry.category.items[0].name }}</a>
{% endif %}

カテゴリーは entry.category.items の配列で取得し、0個目(entry.category.items[0])のカテゴリーを表示します。

タグのループ

{% for tag in entry.tags %}
  <a href="{{ tag.url }}">#{{ tag.name }}</a>
{% endfor %}

4. 本文の表示について

手順2のスニペットにはすでに本文出力が含まれています。本文は entry.body で取得し、{{ entry.body|raw }} で出力します。prose prose-lgTailwind Typography プラグインのクラスで、本文テキストに読みやすいスタイルを一括適用します。

{% if entry.body %}
  <div class="mt-12 prose prose-lg max-w-none prose-sky">
    {{ entry.body|raw }}
  </div>
{% endif %}

ここまで実装できたら、<article></article> 内に残っている静的なコンテンツをすべて削除してください。管理画面で入力した本文の内容だけが表示される状態になります。


5. 詳細ページにも管理ボックスを設置する

詳細ページにも管理ボックスを設置し、記事を見ながら直接投稿・編集操作ができるようにします。

news/_entry.twig{% set entryBody = module('V2_Entry_Body') %} の直前に追加してください。

@include("/admin/action.html")

次に、前章と同様に <head> タグ内に管理UI用リソースを追加します。

    <link rel="stylesheet" href="/css/acms-admin.min.css">
    {% set js = module('V2_Js') %}
    <script src="{{ JS_LIB_JQUERY_DIR }}jquery-{{ JS_LIB_JQUERY_DIR_VERSION }}.min.js" charset="UTF-8"></script>
    <script src="{{ ROOT_DIR }}acms.js{{ js.arguments }}" charset="UTF-8" id="acms-js"></script>

これで管理ボックスが正しいデザインで表示され、記事本文の下にエントリー編集ボックスも現れます。


6. パンくずリストと <title> タグを動的にする

記事本文以外にも、タイトルを使っている箇所があります。このままでは、どの記事を開いても同じタイトルが表示されます。グローバル変数を使って動的に出力するよう修正します。

グローバル変数とは

通常の変数({{ entry.title }} など)はモジュールのデータ取得後にしか使えません。グローバル変数は、現在表示しているページのコンテキストから自動生成され、テンプレート内のどこでも使える変数です。Twig版では {{ 変数名 }} の記法でそのまま出力できます。

パンくずリスト

<li>
  <span class="flex items-center">
    <svg class="h-5 w-5 flex-shrink-0 text-gray-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" /></svg>
    <span class="ml-2 text-sm font-medium text-gray-500" aria-current="page">{{ ENTRY_TITLE }}</span>
  </span>
</li>

標準テンプレートとの差分:グローバル変数

変数

標準テンプレート形式

Twig形式

記事タイトル

%{ENTRY_TITLE}

{{ ENTRY_TITLE }}

サイトルートパス

%{ROOT_DIR}

{{ ROOT_DIR }}

カテゴリーコード

%{CCD}

{{ CCD }}

Twig版では通常の変数と同じ {{ }} 記法で書けるため、グローバル変数だけ別の記法を覚える必要がありません。


<title> タグ

<title>{{ ENTRY_TITLE }} | SAMPLEPLE</title>

https://ablogcms-tutorial.ddev.site/ でいくつかのお知らせ記事をクリックし、ブラウザのタブに記事ごとの正しいタイトルが表示されることを確認してください。


まとめ

この章でやったこと:

  • news/_entry.twig を作成し、module('V2_Entry_Body', null, { eid: EID }) で本文を出力した

  • {% for entry in entryBody.items %} ループ内で記事ヘッダー(タイトル・日付・カテゴリー・タグ)をTwig変数で実装した

  • {% for tag in entry.tags %} でタグの一覧を出力した

  • {{ ENTRY_TITLE }} などグローバル変数をパンくずリストと <title> タグに適用した

  • モジュールID news_body を作成し、URLからのエントリーID引数を設定した

次の章では最後のメインテンプレートである一覧ページを整えます。サムネイル画像の出力・校正オプション・ページャーのTwig実装を学びましょう。


第6回:一覧ページを整える


この章でやること

  • news/index.twig に V2_Entry_Summary モジュールを実装する

  • サムネイル画像・サマリー・タグを出力する

  • ページャー(「もっと見る」ボタン)を実装する

  • モジュールIDと画像フィールドの紐付けを確認する

この章で3つのメインテンプレート(トップ・詳細・一覧)がすべて揃い、サイトとしての基本的な動作が完成します。


1. テンプレートを準備する

現在、/news/ にアクセスすると、静的HTMLの news/index.html が表示されているか、または一覧ページ用のテンプレートが存在しないため404になるかページが表示されない場合があります。一覧ページをCMS化するには、テンプレートファイル news/index.twig が必要です。

既存の静的ページをひな形にしてテンプレートを作ります。

news/index.html をリネームして news/index.twig を作成してください。

/themes/sample/
├── _top.twig
├── news/
│   ├── index.twig   ← 追加
│   └── _entry.twig
├── service/
├── contact/
├── css/
└── images/

この状態で https://ablogcms-tutorial.ddev.site/news/ にアクセスすると、お知らせ一覧ページが表示されます。まだCMS化されていないため、静的HTMLの内容がそのまま表示されています。ここからモジュールIDの設定とテンプレートの実装を進めていきます。


2. 対象のファイルを確認する

news/index.twig を開くと、見出しの下にお知らせを繰り返し表示している部分があります。

<div class="border-b border-gray-200 pb-8 mb-12">
  <h1 class="text-3xl font-bold ...">News</h1>
  <p class="mt-4 text-lg text-gray-600">最新情報や季節限定のメニューなどをお届けします。</p>
</div>
<ul class="space-y-8">
  <li>
    ...(今回の対象)
  </li>
</ul>

この <ul><li> の繰り返し部分を V2_Entry_Summary モジュールに置き換えます。モジュールIDは news_index とします。


3. 管理画面でモジュールIDを先に作成する

今回はテンプレートの実装より先に管理画面の設定を行います。一覧ページで画像を表示するための「メインイメージ」設定は、管理画面側で先に指定しておく必要があるためです。

管理画面に移動し、モジュールID news_index を作成します。

条件設定

項目

設定値

モジュール

V2サマリー(V2_Entry_Summary)

モジュールID

news_index

名前

お知らせ一覧用

URLコンテキスト

チェックを入れる(/news/ のURL情報から自動的に「お知らせ」カテゴリーを取得)

ページ番号

チェックを入れる(2ページ目以降のURLを正しく処理するために必須)


URLコンテキストとは

トップページ用の news_top では「カテゴリーIDの参照」でお知らせカテゴリーを直接指定しました。 一覧ページでは「URLコンテキスト」を有効化します。「URLコンテキスト」有効化することで、/news/ というURLパスから a-blog cms が自動的に「お知らせ」カテゴリーを判定するため、カテゴリーIDを手動で指定する必要がありません。

ページ番号のチェックは忘れがちですが必須です。2ページ目以降のURL(/news/page/2/ など)の情報もモジュールIDに渡すことで、正しいページングが機能します。

URLコンテキスト


表示設定

項目

設定値

表示件数

3

表示順

日付(降順)

メインイメージ対象

カスタムフィールドにチェックを入れて entry_main_image を指定

「メインイメージ対象」を設定することで、カスタムフィールドで登録した画像を entry.mainImage.path で取得できるようになります。

表示件数を3、表示順を日付(降順)に設定している管理画面
メインイメージ対象をカスタムフィールドに設定している管理画面

4. 繰り返し表示される記事をTwig化する

管理画面の設定が完了したら、手順1で作成した news/index.twig のテンプレート実装に進みます。

まずモジュール呼び出しとループの骨格を書きます。

{% set entrySummary = module('V2_Entry_Summary', 'news_index') %}
<ul class="space-y-8">
  {{ include('/admin/module/setting.twig', { moduleInfo: entrySummary.moduleInfo }) }}
  {% for entry in entrySummary.items %}
    <li>
      ...(繰り返し表示される記事)
    </li>
  {% endfor %}
</ul>
...(もっと見るリンク)

{{ include('/admin/module/setting.twig', { moduleInfo: entrySummary.moduleInfo }) }} を追加することで、ログイン中の管理者が一覧表示部分にホバーすると「編集」リンクが表示され、閲覧画面から直接モジュールID(news_index)の設定を編集できるようになります。

次に <li> の中身を実装します。変換前の静的HTMLと変換後のTwig版を対比して見ていきましょう。

変換前(静的HTML)

<li>
  <a href="20250807.html" class="group block p-6 sm:p-8 rounded-lg ...">
    <article class="grid md:grid-cols-3 gap-8 items-start">
      <div class="md:col-span-1 overflow-hidden rounded-lg">
        <img src="/images/news_tomato_pasta.jpg" alt="季節のサラダ" class="...">
      </div>
      <div class="md:col-span-2">
        <p class="text-sm text-gray-500">
          <time datetime="2025-08-07">2025年8月7日</time>
        </p>
        <h2 class="...">新しい季節限定メニューが登場しました</h2>
        <p class="mt-3 text-gray-600 leading-relaxed">
          太陽の恵みをたっぷりと受けた新鮮な夏野菜と…
        </p>
        <div class="mt-4 flex flex-wrap items-center gap-x-4 gap-y-2">
          <span class="bg-sky-100 ...">新商品</span>
          <span class="text-sm text-gray-500">#季節限定</span>
          <span class="text-sm text-gray-500">#夏メニュー</span>
        </div>
        <p class="...">続きを読む →</p>
      </div>
    </article>
  </a>
</li>

変換後(Twig版)

<li>
    <a href="{{ entry.url }}" class="group block p-6 sm:p-8 rounded-lg hover:bg-gray-50 transition-colors duration-300">
        <article class="grid md:grid-cols-3 gap-8 items-start">
            <div class="md:col-span-1 overflow-hidden rounded-lg">
                {% if entry.mainImage and entry.mainImage.path %}
                  <img
                    src="{{ entry.mainImage.path | resizeImg(600) }}"
                    alt="{{ entry.mainImage.alt }}"
                    class="w-full h-56 object-cover shadow-md ..."
                    width="{{ entry.mainImage.width }}"
                    height="{{ entry.mainImage.height }}"
                    loading="lazy"
                    decoding="async">
                {% endif %}
            </div>
            <div class="md:col-span-2">
                <p class="text-sm text-gray-500"><time datetime="{{ entry.date | date('Y-m-d') }}">{{ entry.date | date('Y年m月d日') }}</time></p>
                <h2 class="mt-2 text-xl font-bold text-gray-900 transition-colors group-hover:text-sky-600">
                    {{ entry.title }}
                </h2>
                <p class="mt-3 text-gray-600 leading-relaxed">
                    {{ entry.summary | mb_trim(200, '...') }}
                </p>
                <div class="mt-4 flex flex-wrap items-center gap-x-4 gap-y-2">
                    {% if entry.category.items is not empty %}
                        <span class="bg-sky-100 text-sky-800 text-xs font-semibold px-2.5 py-0.5 rounded-full">{{ entry.category.items[0].name }}</span>
                    {% endif %}
                    {% for tag in entry.tags %}
                        <span class="text-sm text-gray-500">#{{ tag.name }}</span>
                    {% endfor %}
                </div>
                <p class="mt-4 inline-block font-semibold text-sky-600 transition-transform duration-300 group-hover:translate-x-1">
                    続きを読む &rarr;
                </p>
            </div>
        </article>
    </a>
</li>

置き換えポイント解説

画像の条件出力

{% if entry.mainImage and entry.mainImage.path %}
  <img
    src="{{ entry.mainImage.path | resizeImg(600) }}"
    alt="{{ entry.mainImage.alt }}"
    class="w-full h-56 object-cover shadow-md ..."
    width="{{ entry.mainImage.width }}"
    height="{{ entry.mainImage.height }}"
    loading="lazy"
    decoding="async">
{% endif %}

V2モジュールでは画像を entry.mainImage オブジェクトで受け取ります。pathaltwidthheight を個別に取得できるため、適切な alt テキストや実際の画像サイズも出力できます。

また、a-blog cms 独自の resizeImg フィルターを利用して画像をリサイズしています。

サマリーの文字数制限

{{ entry.summary | mb_trim(200, '...') }}

a-blog cms 独自の mb_trim フィルターでサマリーを200文字で切り詰め、末尾に「...」を付けます。

a-blog cms の Twig テンプレートでは「校正オプション」がそのままTwig フィルターとして利用できます。 他にも多くのフィルターがありますので、詳しくは公式ドキュメントの 「校正オプション」 をご覧ください。

ただし、Twig 標準のフィルターと名前が重複するものについてはプレフィックス acms_ を付与して指定する必要があります。


5. ページネーションを実装する

3件ごとにページが切り替わる「もっと見る」ボタンを実装します。V2モジュールではページネーション情報はモジュールデータの pagination プロパティに格納されています。

変換前(静的HTML)

<div class="mt-16 text-center">
  <a href="page2.html" class="...">もっと見る</a>
</div>

変換後(Twig版)

{% if entrySummary.pagination.nextPage %}
  <div class="mt-16 text-center">
    <a href="{{ entrySummary.pagination.nextPage.url }}" class="inline-flex items-center rounded-md border border-transparent bg-sky-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2">
      もっと見る
    </a>
  </div>
{% endif %}

entrySummary.pagination.nextPage が存在する(次のページがある)ときだけボタンを表示します。最終ページに達すると pagination.nextPage が空になり、ボタン全体が非表示になります。

https://ablogcms-tutorial.ddev.site/news/ にアクセスして、最新の3件が表示され、「もっと見る」をクリックすると2ページ目以降も正しく表示されることを確認してください。

ページネーションが実装できたら news/page2.html は不要なので削除してください。


6. カスタムフィールドと画像フィールドの紐付けを確認する

第4章で field.html に画像フィールド(entry_main_image)を定義しました。一覧ページでその画像を表示するには、モジュールID側でも「どのフィールドをメインイメージとして使うか」を指定する必要があります。

手順3で作成した news_index のモジュールID設定を開き、「表示設定」タブで以下を確認してください。

項目

設定値

メインイメージ対象

カスタムフィールドにチェックが入っているか確認

この設定があることで、テンプレート内で entry.mainImage.path として画像を取得できます。第4章で記事投稿時に画像をアップロード済みであれば、この時点で一覧ページに画像が表示されます。

7. 管理ボックスと管理用アセットを設置する

一覧ページにも管理ボックスを設置し、ログイン状態で「新規投稿」「管理画面へ移動」などの操作ができるようにします。また、管理UIが正しいデザインで表示されるよう、管理用アセットを読み込みます。

管理ボックスの設置

news/index.twig の見出しセクション(<h1>News</h1> を含む <div>)の直前に、以下を追加してください。

@include("/admin/action.html")

管理用アセットの読み込み

news/index.twig<head> タグ内の管理UI用リソースが含まれていない場合は、<head> タグ内に以下を追加してください。トップページ(_top.twig)や詳細ページ(_entry.twig)と同様の記述です。

<link rel="stylesheet" href="/css/acms-admin.min.css">
{% set js = module('V2_Js') %}
<script src="{{ JS_LIB_JQUERY_DIR }}jquery-{{ JS_LIB_JQUERY_DIR_VERSION }}.min.js" charset="UTF-8"></script>
<script src="{{ ROOT_DIR }}acms.js{{ js.arguments }}" charset="UTF-8" id="acms-js"></script>

https://ablogcms-tutorial.ddev.site/news/ にアクセスし、ログイン状態で管理ボックスが横並びで正しく表示されることを確認してください。


まとめ

この章でやったこと:

  • module('V2_Entry_Summary', 'news_index') 関数でモジュールデータを取得し、entrySummary.items でループした

  • {% if entry.mainImage and entry.mainImage.path %} で画像の条件出力を実装した

  • {{ entry.mainImage.path | resizeImg(600) }} で画像リサイズ、{{ entry.summary | mb_trim(200, '...') }} でサマリー切り詰めを行った

  • {% if entrySummary.pagination.nextPage %} でページャーを実装した

  • モジュールIDの「メインイメージ対象」に entry_main_image を指定して画像フィールドと紐付けた

次の最終章では、サンプルデータをインポートして完成を確認します。


第7回:サンプルデータをインポートして完成を確認しよう


この章でやること

  • 既存のお知らせエントリーを削除し、サンプルデータをインポートする

  • 完成確認チェックリストで動作を網羅的に確認する

  • 次のステップ(フォームのTwig実装)への道筋を把握する

1. エントリーを削除する

a-blog cms は、エントリー単位でのエクスポート&インポートが可能です。

  1. 管理画面の「エントリー管理」を開く

  2. カテゴリー「お知らせ」を選択して検索する

  3. 表示されたエントリーをすべてチェックする

  4. 一括操作のSELECTメニューから「ゴミ箱」を選択し、実行する

8-2. インポートする

  1. インポートデータ : news_improt_data.zip をダウンロードします。

  2. (エントリー作成)ボタンの右横にある 「その他」メニュー → 「インポート」 を選択します。

  3. エントリーインポート画面 が表示されます。

  4. ステータスを「公開でインポート」に設定し、ファイルを選択してインポートを実行します。

SCR-20250826-bdfw

インポートが完了したら、https://ablogcms-tutorial.ddev.site/news/ を開いて新しいお知らせが反映されていることを確認してください。

3. 完成確認チェックリスト

チュートリアルで実装した3つのメインテンプレート(トップ・一覧・詳細)が正しく動作しているか、以下の順序で確認しましょう。各項目は該当する章の実装内容に対応しています。

確認の流れ

トップページ → 一覧ページ → 詳細ページの順に、実際にクリックしながら確認すると効率的です。


トップページ(_top.twig)— 第4章で実装

#

確認項目

該当実装

1

お知らせ一覧に「お知らせ」カテゴリーの記事が5件表示される

モジュールID news_top の表示件数

2

日付が 2025.08.07 の形式で表示される

date('Y.m.d') フィルター

3

カテゴリー名が正しく表示される

{{ entry.category.items[0].name }}

4

記事タイトルをクリックすると詳細ページに遷移する

{{ entry.url }}

5

管理ボックスが横並びで表示される

@include("/admin/action.html")

6

管理者がモジュールにホバーすると「編集」リンクが表示される

{{ include('/admin/module/setting.twig', ...) }}


一覧ページ(news/index.twig)— 第6章で実装

#

確認項目

該当実装

1

お知らせが3件ずつ表示される

モジュールID news_index の表示件数

2

記事に画像が設定されている場合、サムネイルが表示される

{% if entry.mainImage and entry.mainImage.path %} + resizeImg(600)

3

サマリーが200文字で切り詰められて表示される

mb_trim(200, '...') フィルター

4

「もっと見る」をクリックすると2ページ目に遷移する

{% if entrySummary.pagination.nextPage %}

5

最終ページでは「もっと見る」が表示されない

entrySummary.pagination.nextPage が空のとき非表示


詳細ページ(news/_entry.twig)— 第5章で実装

#

確認項目

該当実装

1

記事ごとに異なるタイトル・本文が表示される

module('V2_Entry_Body', null, { eid: EID })

2

日付が 2025年8月7日 の形式で表示される

entry.datetime + date フィルター

3

カテゴリー名が表示され、クリックするとカテゴリーURLに遷移する

{{ entry.category.items[0].url }}

4

タグが設定されている記事では #タグ名 が表示される

{% for tag in entry.tags %}

5

ブラウザのタブに記事タイトルが表示される

{{ ENTRY_TITLE }}

6

パンくずリストに記事タイトルが表示される

{{ ENTRY_TITLE }}

7

エントリー編集ボックスが表示される(ログイン状態)

{{ include('admin/entry/action.twig', { entry }) }}


共通項目

#

確認項目

該当実装

1

すべてのページで管理ボックスが正しいデザインで表示される

acms-admin.min.css + acms.js

2

ログアウト状態では管理ボックスおよびエントリー編集ボックスが非表示になる

moduleInfo は管理者のみ

3

CSS・JavaScriptのパスに /themes/sample/ が自動付与されている

a-blog cms のパス補完

4

<meta name="generator" content="a-blog cms" /> が出力されている

CMS経由表示の確認


4. Twig 構文のエラーについて

Twig 構文にエラーがあるとデバッグモード時に以下のようなエラーメッセージが表示されます。

このようなエラーが出た場合、Twig のエラーメッセージを確認すると原因を特定しやすくなります。エラー画面には次の情報が表示されます。

  • エラー内容:何が問題か(例:Unknown "dete" filter

  • ファイル名と行番号:どのテンプレートの何行目か(例:news/_entry.twig line 137)

  • 修正のヒント:Twig が推測してくれる場合がある(例:Did you mean "date"?

フィルター名のタイプミス

フィルター名を間違えると「Unknown "○○" filter」と表示されます。たとえば datedete と打ち間違えると、次のようなエラーになります。

Unknown "dete" filter. Did you mean "date" in "news/_entry.twig" at line 137?

エラーメッセージの「Did you mean "date"?」を手がかりに、該当行のフィルター名を確認して修正してください。

5. 次のステップ

このチュートリアルではお知らせ機能のCMS化を扱いました。実務ではさらに以下の実装が必要になります。

お問い合わせフォームのCMS化

サンプルサイトの contact/ フォームはまだ静的なHTMLのままです。a-blog cmsにはフォーム機能が標準搭載されており、入力画面・確認画面・送信完了画面をテンプレートとして実装できます。

フォーム機能のドキュメントを参考に実装してみましょう。

テンプレート継承の活用

このチュートリアルでは各テンプレートに <head> や管理UIリソースを個別に書きました。Twigのテンプレート継承{% extends %}{% block %})を使うと、共通部分を親テンプレートに集約して保守性を高められます。

さらに詳しく学ぶ


まとめ

この章でやったこと:

  • サンプルデータをインポートし、完成確認チェックリストで動作を確認した

  • Twig 構文のエラーについての確認ポイントを把握した

  • フォームのCMS化・テンプレート継承など、次のステップへの道筋を確認した

このチュートリアルを終えて、静的HTMLを a-blog cms のTwig テーマとして動かす基本フローが身につきました。標準テンプレートとの最大の差分は「module() 関数でデータを取得し、モジュールごとの返り値の構造に応じて出力する記法」です。これらを押さえれば、モジュールIDによる設定管理など、a-blog cmsとしての仕組みは標準テンプレートと変わりません。ぜひ次のプロジェクトからTwig テンプレートでのテーマ開発を試してみてください。