0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HTMX を Grav CMS (Twig) で使う

Last updated at Posted at 2025-07-11

きっかけ

HTMX の作者たちによる著書「 ハイパーメディアシステム 」を読みました。
とても面白かったので、何か作りたいなと思いました。(金槌を持つと、釘が欲しくなります)

今回は、Grav という CMS で、既存のテンプレートをできるだけ壊さずに、HTMX を導入する方法を考えたいです。

Grav CMS について

PHP 製の CMS で、 PHP だけ(MySQLさえ不要)で動くので、セットアップが簡単です。
標準的なテンプレートエンジンである Twig を使っているので、他の( Python-Jinja や Ruby-Liquid などを使う) CMS でも応用が利くと思います。

HTMX には興味があっても、Grav には興味が無い方もいるかもしれませんから、Grav の解説部分は読み飛ばしても大丈夫なように書いていきます。

作りたいもの

以下のような処理を作りたいです。

HTMXでの理想的な処理

  1. 外部サイトからのリンクの時(普通のリクエスト)
    • そのページのコンテンツを取得する
    • HTMX からのリクエストか? -> HTMX ではない
    • 画面全体のテンプレートでレンダリング
      (ヘッダーやサイドバーも含めてレンダリングされる)
    • 画面全体をレスポンス
  2. 同一サイト内でのリンクの時( HTMX からの リクエスト)
    • そのページのコンテンツを取得する
    • HTMX からのリクエストか? -> HTMX である
    • コンテンツのフラグメントのみのテンプレートでレンダリング
      (ヘッダーやサイドバーはレンダリングされない)
    • フラグメントのみをレスポンス

理想としては、HTTPヘッダー( HX-Request: true )で分岐したいところですが、表示を確認しながら作れるように、今回は GET パラメータ( ?htmx=true )で分岐させてみようと思います。

Grav のセットアップ

(この節は、HTMX に関係ないので、 読み飛ばし可能 です。)

Grav には、あらかじめテーマやプラグイン、ひな形のページがセットになった、サンプルサイトが用意されています。 スケルトン といいます。

今回は、ブログサイトを作ってみたいので、 WordPress の2015年のデフォルトテーマを Grav に移植した TwentyFifteen Site というスケルトンを利用します。

当該スケルトンの zip ファイルをダウンロードして、 PHP と web サーバー(apache2 など)が動くフォルダに、zip ファイルを展開します。

# webroot となるディレクトリに移動
cd /var/www/【webroot】/

# スケルトンの zip ファイルをダウンロード
wget 【スケルトン.zip】

# zip ファイルを展開
unzip 【スケルトン.zip】

web サーバーがインストールされていなければ、 Grav 経由で PHP ビルトインサーバーを使ってください。

# Apache2 などの web サーバーが未インストールの時は Grav のサーバーを利用できます
bin/grav server

webroot のディレクトリを localhost などで開けば、サンプル記事付きのブログサイトが表示されます。

次のような表示になると思います。

grav blog 初期画面

さて、無事サイトが表示されたら、次に仕組みの部分です。
ルーティングから、コンテンツを確定し、テンプレートを確定する流れを見ていきます。

Grav のルーティングはシンプルです。 user/pages/ フォルダ下を確認してください。

ls -la user/pages/

デフォルトでは、次のようになっているかもしれません(一部省略しています)

user/pages/
├── 01.blog/
│   ├── blog.md
│   ├── focus-and-enjoy/
│   │   └── item.md
│   └── just-an-ordinary-day/
│       └── item.md
├── 02.about/
│   └── default.md
└── 03.contact/
    └── form.md

Grav では、このファイルシステムの通りにルーティングされます。
たとえば、

  • localhost/blog にアクセスすると、
    • user/pages/ フォルダ内の
    • 01.blog/blog.md のマークダウンがコンテンツとして確定し
  • localhost/blog/focus-and-enjoy にアクセスすると、
    • 同じく user/pages/ フォルダ内の
    • 01.blog/focus-and-enjoy/item.md のマークダウンがコンテンツとして確定し
  • localhost/about にアクセスすると、
    • 同じく user/pages/ フォルダ内の
    • 02.about/default.md のマークダウンコンがコンテンツとして確定します。

次に、テンプレートファイルは、上記で見つかったマークダウンのファイル名で決まります。

  • blog.md のマークダウンに対しては、
    • user/theme/twentyfifteen/templates/ フォルダ内の
    • blog.html.twig テンプレートでレンダリングされ、
  • item.md のマークダウンに対しては、
    • 同じく user/theme/twentyfifteen/templates/ フォルダ内の
    • item.html.twig テンプレートでレンダリングされ、
  • default.md のマークダウンに対しては、
    • 同じく user/theme/twentyfifteen/templates/ フォルダ内の
    • default.html.twig テンプレートでレンダリングされます。

テンプレートファイルにどのようなものがあるか、一覧を表示してみましょう。

ls -la user/theme/twentyfifteen/templates/

たとえば、以下のようになっているはずです。(これも一部省略)

user/theme/twentyfifteen/templates/
├── blog.html.twig
├── default.html.twig
├── item.html.twig
└── partials/
    ├── base.html.twig
    └── blog_item.html.twig

とりあえずここで押さえておくべきなのは、

  • ブラウザで localhost/blog/focus-and-enjoy にアクセス、
  • user/pages/01.blog/focus-and-enjoy/item.md のマークダウンコンテンツを取得、
  • user/theme/twentyfifteen/templates/item.html.twig のテンプレートでレンダリング

という一連の流れです。

Twig テンプレートを観察する

前の節を読み飛ばした方のために、 HTMX にかかわる大事なところを、再度書きます。

ブラウザで localhost/blog/focus-and-enjoy というブログ投稿にアクセスすると、 CMS 内部でいろいろ処理されたのちに、 user/theme/twentyfifteen/templates/item.html.twig ファイルがテンプレートとして呼ばれます。

ということで、まずはこの user/theme/twentyfifteen/templates/item.html.twig テンプレートを観察します。

item.html.twig テンプレート

内容の表示方法:

less user/theme/twentyfifteen/templates/item.html.twig

今回の場合、以下のようになっています( HTMX に関係しそうなところだけ、抜粋しています。元ファイルを知りたい方は、 GitHub で確認できます)。

{% embed 'partials/base.html.twig' %}
    {% block content %}
        {% include 'partials/blog_item.html.twig' %}
    {% endblock %}
{% endembed %}

embed タグは、twig のタグで、別のテンプレートの一部として、自身を埋め込むことができます。埋め込むために、 block という twig タグを使います。
一方、 include タグも twig タグですが、 embed の逆で、別のテンプレートを自身に埋め込みます。

ここでは、embed で呼び出すのが、partials/base.html.twig テンプレートで、include で呼び出すのが、 partials/blog_item.html.twig テンプレートです。

テンプレートの包含関係としては、

  • partials/base.html.twig は、 item.html.twig(自分自身)を含み、
  • item.html.twig(自分自身)は、 partials/blog_item.html.twig を含みます。

Twig 以外テンプレートエンジンにも、 embedinclude に相当する機能があるはずです。
この包含関係を知ることが、まずは大切です。

partials/base.html.twig テンプレート

次に、 partials/base.html.twig ファイルを観察します。

less user/theme/twentyfifteen/templates/partials/base.html.twig

これも抜粋です(元ファイルは GitHub から確認してください)。

<!DOCTYPE html>
<html lang="ja">
    <head>(ここで JS や CSS の読み込みなどしています。 HTMX も、ここで読み込む予定です)</head>
    <body>
        {% include 'partials/sidebar.html.twig' %} {# <- サイドバーを読み込み #}
        <main id="main" class="site-main" role="main">
            {% block content %}{% endblock %}
        </main>
        <footer id="colophon" class="site-footer" role="contentinfo">
            (フッターの中身は関係ないので省略)
        </footer>
    </body>
</html>

ここで注目してほしいのは、 {% block content %} {% endblock %} です。
先ほどの item.html.twig テンプレート内の {% block content %} {% endblock %} の中身が、ここに埋め込まれます。

つまり、この {% block content %} {% endblock %} の外側にあるサイドバーやフッターは、コンテンツとは別に読み込まれているわけです。
(細かいことを言うと、サイドバーなどにコンテンツごとにカスタマイズされる部分もありますが、今回の記事では目をつむります)

{% block content %} {% endblock %} は、<main id="main"> タグ内で直下に読み込まれるので、HTMX で差し替えるなら、 main#main で target を指定して、 innerHTML を swap すれば良さそうです。

partials/blog_item.html.twig テンプレート

最後に、 item.html.twig ファイルが include する partials/blog_item.html.twig も見ていきます。

less user/theme/twentyfifteen/templates/partials/blog_item.html.twig

このテンプレートは、さまざまな if 文が入り乱れているので、HTMX に関連しそうな、ブログの「次の記事」リンクのところだけ抜粋しておきます(元ファイルは、 GitHub から確認してください)。

{% if not page.isFirst %}
    <div class="nav-next">
        <a href="{{ page.nextSibling.url }}" rel="next">
            <span class="post-title">{{ page.nextSibling.header.title }}</span>
        </a>
    </div>
{% endif %}

Grav では、 twig 上で page という変数(オブジェクト)が使えます。

  • {% if not page.isFirst %} ... {% endif %} で、最新の記事ではない(つまり、その記事より新しい記事がある)ときに、タグの中身を表示します
  • page.nextSibling.url は、次の記事のルーティング URL を文字列で書き込みます
  • page.nextSibling.header.title は、次の記事のタイトルです

このリンクを、通常の HTML リンクとして使うのではなく、 HTMX 経由で GET リクエストを送れるようにすれば、 HTMX が活用できそうです。

ここまでで、レンダリングに使われる各テンプレートの内容と、それぞれの包含関係が分かりました。
次の節から、実際に編集していきます。

HTMX 化に向けて Twig ファイルを編集

ここからは、具体的に twig を編集していきます。

?htmx=true で HTML フラグメントを表示

まずは、 item.html.twigembed するテンプレートを URL の GET パラメータによって分岐させたいです。
いろいろな方法があると思いますが、今回は、以下のようにしました。

変更前と比較したい場合はここをクリック
{% embed 'partials/base.html.twig' %}
    {% block content %}
        {% include 'partials/blog_item.html.twig' %}
    {% endblock %}
{% endembed %}
{% if uri.query('htmx') == 'true' %}
    {% set basetemplate = 'partials/htmx-base.html.twig' %}
{% else %}
    {% set basetemplate = 'partials/base.html.twig' %}
{% endif %}
{% embed basetemplate %}
    {% block content %}
        {% include 'partials/blog_item.html.twig' %}
    {% endblock %}
{% endembed %}

少し補足説明が必要かもしれません:

  • uri.query() というのは、Grav 特有の uri オブジェクトの query メソッドで、引数にパラメータのキーを渡すと、対応する値を返します
  • set というのは、Twig タグで、 Twig 内で変数を定義できるものです

分岐の if 文自体は、シンプルな書き方なので迷うことは無いと思います。
embed するテンプレートの場所を、分岐しているだけです。

そして、ここに出てくる partials/htmx-base.html.twig テンプレートを新たに作っておきましょう。
内容は、次のような簡単なものです。

{% block content %}
{% endblock %}

フラグメントだけ返せれば良いので、このような書き方になります。

ここまでで、本当に HTML のフラグメントが返るか、ブラウザから確認してみます。

  • localhost/blog/focus-and-enjoy にアクセス:
    通常の画面

  • localhost/blog/focus-and-enjoy?htmx=true にアクセス:
    GET パラメータで分岐された画面

後者は、サイドバーが無くなり、スタイルが当たっておらず、フラグメントだけが表示されていることが分かります。
これで、 GET パラメータの ?htmx=true で分岐できるようになりました。

HTMX を読み込む

次に、HTMX を読み込んでいきます。
読み込むのは、<head> タグ内なので、user/theme/twentyfifteen/templates/partials/base.html.twig に以下を書き込みます。

変更前と比較したい場合はここをクリック
<!DOCTYPE html>
<html lang="ja">
    <head>
        (ここで JS や CSS の読み込みなどしています)
    </head>
    <body>
        (関係ないので省略)
    </body>
</html>
<!DOCTYPE html>
<html lang="ja">
    <head>
        (ここで JS や CSS の読み込みなどしています)
        <script src="/path/to/htmx.min.js"></script>
    </head>
    <body>
        (関係ないので省略)
    </body>
</html>

/path/to/htmx.min.js の場所は、呼び出せるならどこでも良いと思います。
( Grav では、js の保存場所は user/theme/twentyfifteen/js/ が推奨されており、 script という Grav オリジナルのカスタムタグを使って{% script 'theme://js/htmx.min.js' %} のような呼び出しが可能です。)

HTMX は、依存関係が無いので、バンドラも不要ですし、読み込みの順番を気にせず書けて便利です。

アンカーリンクに HTMX 属性追加

最後に、アンカーリンクを HTMX からリクエストできるように編集します。

変更前と比較したい場合はここをクリック
{% if not page.isFirst %}
    <div class="nav-next">
        <a href="{{ page.nextSibling.url }}" rel="next">
            <span class="post-title">{{ page.nextSibling.header.title }}</span>
        </a>
    </div>
{% endif %}
{% if not page.isFirst %}
    <div class="nav-next">
        <a href="{{ page.nextSibling.url }}" rel="next"
            hx-get="{{ page.nextSibling.url }}?htmx=true"
            hx-target="main#main" hx-swap="innerHTML"
            hx-push-url="{{ page.nextSibling.url }}">
            <span class="post-title">{{ page.nextSibling.header.title }}</span>
        </a>
    </div>
{% endif %}

ポイントは、以下の4点です

  1. hx-get で、 ?htmx=true 付きの URL へ GET リクエストを送ります
  2. hx-target は、このテンプレートを含む直接の親にあたる <main id="main"> タグです
  3. hx-swap は、 main タグ内を入れ替えたいので、innerHTML にします
  4. hx-push-url により、ブラウザの戻るボタンに対応します

結果

devtools で、非同期通信になっているかどうかの確認ができます。
ブラウザで F12 キーなどで devtools を開き、「ネットワーク」タブを開いてください。

image.png

無事、ブログ記事の遷移を非同期でできるようになりました。

以下同様に、さまざまなページを分岐 embed や include ごとに分岐させ、リンクを htmx 化させていけば良さそうです。
またもし、htmx し忘れたリンクがあっても大丈夫です。非同期通信にはならないものの、ただのリンクとして遷移するだけなので、表示に問題が出ることはありません。

感想

ここまでの説明を見てもらえば分かる通り、Grav (Twig) による HTMX の導入は、それほど難しくないと感じました。
今回チャレンジしたのが、 GET リクエストのみのブログサイトだったので、簡単だったのかもしれません。

ここに CRUD 操作が入り込むと、また難易度が変わると思います。
また機会があれば、他の種類のサイトにも挑戦してみたいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?