きっかけ
HTMX の作者たちによる著書「 ハイパーメディアシステム 」を読みました。
とても面白かったので、何か作りたいなと思いました。(金槌を持つと、釘が欲しくなります)
今回は、Grav という CMS で、既存のテンプレートをできるだけ壊さずに、HTMX を導入する方法を考えたいです。
Grav CMS について
PHP 製の CMS で、 PHP だけ(MySQLさえ不要)で動くので、セットアップが簡単です。
標準的なテンプレートエンジンである Twig を使っているので、他の( Python-Jinja や Ruby-Liquid などを使う) CMS でも応用が利くと思います。
HTMX には興味があっても、Grav には興味が無い方もいるかもしれませんから、Grav の解説部分は読み飛ばしても大丈夫なように書いていきます。
作りたいもの
以下のような処理を作りたいです。
- 外部サイトからのリンクの時(普通のリクエスト)
- そのページのコンテンツを取得する
- HTMX からのリクエストか? -> HTMX ではない
- 画面全体のテンプレートでレンダリング
(ヘッダーやサイドバーも含めてレンダリングされる) - 画面全体をレスポンス
- 同一サイト内でのリンクの時( 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 のルーティングはシンプルです。 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 以外テンプレートエンジンにも、 embed や include に相当する機能があるはずです。
この包含関係を知ることが、まずは大切です。
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.twig で embed するテンプレートを 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 のフラグメントが返るか、ブラウザから確認してみます。
後者は、サイドバーが無くなり、スタイルが当たっておらず、フラグメントだけが表示されていることが分かります。
これで、 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点です
-
hx-getで、?htmx=true付きの URL へ GET リクエストを送ります -
hx-targetは、このテンプレートを含む直接の親にあたる<main id="main">タグです -
hx-swapは、 main タグ内を入れ替えたいので、innerHTMLにします -
hx-push-urlにより、ブラウザの戻るボタンに対応します
結果
devtools で、非同期通信になっているかどうかの確認ができます。
ブラウザで F12 キーなどで devtools を開き、「ネットワーク」タブを開いてください。
無事、ブログ記事の遷移を非同期でできるようになりました。
以下同様に、さまざまなページを分岐 embed や include ごとに分岐させ、リンクを htmx 化させていけば良さそうです。
またもし、htmx し忘れたリンクがあっても大丈夫です。非同期通信にはならないものの、ただのリンクとして遷移するだけなので、表示に問題が出ることはありません。
感想
ここまでの説明を見てもらえば分かる通り、Grav (Twig) による HTMX の導入は、それほど難しくないと感じました。
今回チャレンジしたのが、 GET リクエストのみのブログサイトだったので、簡単だったのかもしれません。
ここに CRUD 操作が入り込むと、また難易度が変わると思います。
また機会があれば、他の種類のサイトにも挑戦してみたいです。




