Claude Media
Claude Codeでhtmxアプリを作る — curlで部分HTMLを検証する手順

Claude Codeでhtmxアプリを作る — curlで部分HTMLを検証する手順

Claude Codeでhtmxのサーバー描画アプリを作るとき、HX-Requestヘッダー付きのcurlで部分HTMLを検証する手順と、そのコマンドをCLAUDE.mdに固定する書き方を解説します。

htmxのアプリは、サーバーが返すHTMLの断片がそのまま画面になります。だから検証の中心は、ブラウザを開くことではなく、curl で部分HTMLの中身とステータスコードを読むことです。Claude Codeはコマンドの出力を文字で受け取れるので、この検証と相性が良い作業です。

この記事では、htmxのリクエストとレスポンスの約束事をcurlの検証コマンドに落とし、CLAUDE.mdとpermissionsに固定するまでを扱います。サーバー側の言語やフレームワークは問いません。

htmxの検証が「部分HTMLを読む作業」になる理由

htmxは、任意の要素がHTTPリクエストを発行し、返ってきたHTMLを指定した場所に差し込むライブラリです。ドキュメントには、サーバーは通常JSONではなくHTMLで応答する、と明記されています。

典型的なボタンは次の形です。

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML">
    Click Me!
</button>

クリックで /clicked にPOSTし、返ってきたHTMLで #parent-div を置き換えます。

ここで壊れやすい場所は3つです。

  • サーバーが返す断片の中身(完全なページを返していないか)
  • ステータスコード(htmxが差し込むかどうかを決める)
  • hx-target と hx-swap が指す先と、断片の構造の整合

どれもHTTPの応答を見れば判定できます。Claudeに「画面で確認して」と頼むより、応答を直接読ませるほうが速く、結果も再現します。

curlで確かめる7つの観点

HX-Requestの有無で応答が分かれるか

htmxが送るリクエストには HX-Request ヘッダーが付きます。値は常に true で、例外は履歴復元のリクエストに限り、htmx.config.historyRestoreAsHxRequest を無効にしている場合です。ほかにも HX-Target(ターゲット要素のid)、HX-Trigger(トリガー要素のid)、HX-Boosted(hx-boost経由)などが送られます。

サーバーはこのヘッダーを見て、断片だけを返すか、ページ全体を返すかを分けるのが定石です。プログレッシブエンハンスメントの節でも、HX-Request を見て通常のリクエストと区別する方法が挙がっています。

つまりcurlでは、同じURLを2通りに叩いて差を確かめます。

# htmxからのリクエストを模す: 部分HTMLが返るはず
curl -s -i -H "HX-Request: true" http://localhost:8000/items
 
# 通常のブラウザ遷移を模す: ページ全体が返るはず
curl -s -i http://localhost:8000/items

-i を付けるとステータス行とレスポンスヘッダーも出るので、後の観点も同じコマンドで見られます。

同じURLの断片とページが、キャッシュで取り違えられないか

同じURLが HX-Request の有無で別の内容を返す構成では、HTTPキャッシュが落とし穴になります。ブラウザや中間のキャッシュはURLだけで応答を引き当てるため、断片のキャッシュが通常遷移に、ページ全体のキャッシュがhtmxのリクエストに返されることがあります。結果は、画面に断片だけが出る、またはページが入れ子になる、といった不具合です。

htmxのCachingの節は、この構成では Vary: HX-Request を付けるよう求めています。付けると、キャッシュのキーがURLと HX-Request ヘッダーの組になります。Vary を付けられない場合の代替は、設定 getCacheBusterParam を true にして、htmxのGETにキャッシュ回避用のパラメーターを足す方法です。ETagを使うなら、内容ごとに別のETagを出す必要があります。

応答ヘッダーはcurlで直接見られます。

curl -s -I -H "HX-Request: true" http://localhost:8000/items | grep -i '^vary'

Vary: HX-Request の行が出れば、キャッシュ側の前提は満たされています。何も出ないなら、分岐を実装したサーバーかフレームワークの応答ヘッダー設定を見直します。

戻るボタンで断片だけが表示されないか

hx-push-url などで履歴にURLを積んだページは、戻るボタンで以前の画面に戻れます。htmxは通常、履歴キャッシュ(ブラウザのlocalStorage)に残したスナップショットで復元します。キャッシュに無い場合は、そのURLにリクエストを送り直し、ページ全体のHTMLが返ってくることを前提に画面を復元します。このリクエストには HX-History-Restore-Request: true が付きます。

ここで断片を返すと、戻った画面が部分HTMLだけになります。historyRestoreAsHxRequest は既定が true で、履歴復元のリクエストにも HX-Request: true が付きます。HX-Request で分岐する構成では、これを false にして、履歴復元を通常のリクエストとして扱わせます。

<meta name="htmx-config" content='{"historyRestoreAsHxRequest": false}'>

履歴復元のリクエストは、curlで再現できます。HX-Request を付けず、HX-History-Restore-Request だけを付けるのがポイントです。

curl -s -H "HX-History-Restore-Request: true" \
  http://localhost:8000/items | grep -c "<html"

結果は 1 以上が期待値です。0 なら、履歴キャッシュを外れた戻る操作で、断片だけの画面になります。

断片に <html> が混ざっていないか

hx-swap の既定は innerHTML で、対象要素の中に返ってきた内容を入れます。ここに <html> や <head> まで含む完全なページが返ると、画面の中にページがもう一つ入れ子になります。

判定は grep で足ります。

curl -s -H "HX-Request: true" http://localhost:8000/items \
  | grep -c "<html" || true

部分HTMLの検証では、この結果が 0 であることを期待値にします。通常リクエスト側は逆に 1 以上になるはずです。

ステータスコードが差し込みの可否を決める

htmxの既定の応答処理は次のとおりです。

コード既定の動き
204既定の動き何も差し込まない(エラーではない)
2xx・3xx(204以外)既定の動き差し込む
4xx・5xx既定の動き差し込まず、エラーとして扱う

特に効くのが入力検証です。サーバーフレームワークが検証エラーに422を返すと、既定では4xxに該当するため差し込まれません。ドキュメントは、この422を差し込ませたい場合に <meta name="htmx-config"> の responseHandling へ {"code":"422", "swap": true} を足す例を示しています。

curl側では、ステータスコードだけを取り出して期待値と比べます。

curl -s -o /dev/null -w "%{http_code}\n" \
  -H "HX-Request: true" \
  -d "name=" http://localhost:8000/items

「空の名前をPOSTしたら422、本文にエラーメッセージの断片」を期待値として書いておけば、フロント側の responseHandling の設定漏れもテストの観点に入ります。

リダイレクトは302ではなく200で

ドキュメントには見落としやすい注意があります。HX-Redirect や HX-Location などの応答ヘッダーは、302のようなリダイレクト応答ではhtmxに渡りません。ブラウザがリダイレクトを内部で処理して、転送先の応答を返すためです。ヘッダーで制御したいときは200など別のコードを使います。

さらに、htmxでフォームを送る場合はPost/Redirect/Getパターンが不要になり、POSTの処理後に新しい断片を直接返せます。

curlはリダイレクトを自動では追わないので、この違いが応答に出ます。

curl -s -i -H "HX-Request: true" -d "name=apple" \
  http://localhost:8000/items | head -n 12

先頭の HTTP/1.1 302 が見えたら、htmxからは期待どおりに動かない実装です。

応答ヘッダーが付いているか

サーバーはレスポンスヘッダーでクライアントの動きを指示できます。ドキュメントに載っているものの一部です。

  • HX-Retarget: 差し込み先をCSSセレクターで差し替える
  • HX-Reswap: 差し込み方式を指定する(hx-swap と同じ値)
  • HX-Trigger: クライアント側のイベントを発火する
  • HX-Push-Url: 履歴にURLを積む

実装でこれらを使うなら、curlの -i 出力に出ているかを確かめます。

curl -s -i -H "HX-Request: true" -d "name=apple" \
  http://localhost:8000/items | grep -i "^hx-"

検証スクリプトにまとめてCLAUDE.mdから呼ぶ

観点が7つあると、毎回Claudeにcurlを組み立てさせるのは非効率です。スクリプトにして、CLAUDE.mdには1行で書きます。

#!/usr/bin/env bash
# scripts/check-htmx.sh — 部分HTMLの検証(サーバー起動済み前提)
set -u
BASE="${BASE:-http://localhost:8000}"
fail=0
 
check() {  # check <説明> <期待> <実測>
  if [ "$2" = "$3" ]; then echo "ok   $1"
  else echo "FAIL $1 (期待=$2 実測=$3)"; fail=1; fi
}
 
frag=$(curl -s -H "HX-Request: true" "$BASE/items")
full=$(curl -s "$BASE/items")
has_html() { printf '%s' "$1" | grep -q '<html' && echo yes || echo no; }
check "断片に<html>が無い" no "$(has_html "$frag")"
check "通常遷移は<html>を含む" yes "$(has_html "$full")"
check "空入力は422" 422 "$(curl -s -o /dev/null -w '%{http_code}' \
  -H 'HX-Request: true' -d 'name=' "$BASE/items")"
check "追加は302でない" 200 "$(curl -s -o /dev/null -w '%{http_code}' \
  -H 'HX-Request: true' -d 'name=apple' "$BASE/items")"
vary=$(curl -s -I -H 'HX-Request: true' "$BASE/items" \
  | grep -ci '^vary:.*hx-request')
check "Vary: HX-Requestが付く" 1 "$vary"
hist=$(curl -s -H 'HX-History-Restore-Request: true' "$BASE/items")
check "履歴復元はページ全体を返す" yes "$(has_html "$hist")"
 
exit $fail

エンドポイントやフィールド名は自分のアプリに合わせて書き換えてください。期待値はアプリごとの仕様で、htmx自体が決めるものではありません。

CLAUDE.mdには、スクリプトの存在と使い方を具体的に書きます。CLAUDE.mdは強制設定ではなく文脈として読み込まれるため、「Run npm test before committing」のように検証できる粒度で書くのが推奨されています。目安は1ファイル200行以内で、長くなる指示はパス別のルールに分けます。

# htmxアプリの規約
 
## 検証(UIを触る変更のたびに実行)
- サーバー起動: `make dev`(常駐するので別シェルで実行)
- 部分HTML検証: `scripts/check-htmx.sh`
- 新しい `hx-*` エンドポイントを足したら、同スクリプトに
  断片の構造・ステータス・ヘッダーのcheckを1件以上足す
 
## htmxの約束
- `HX-Request: true` のときは断片だけを返す(`<html>` を含めない)
- 入力検証エラーは422と、エラー文言を含む断片を返す
- 成功時にリダイレクトしない。断片を200で直接返す
- 断片とページを同じURLで返し分けるときは `Vary: HX-Request` を付ける
- 履歴に積むURLは、`HX-History-Restore-Request` 付きでページ全体を返す
- `hx-target` を使う要素には必ずidを付ける

「新しいエンドポイントを足したらcheckも足す」という1行がポイントです。これでClaudeが機能を足すたびに、検証も同時に育ちます。

読み込まれたかどうかは、セッション内で /context を実行し、Memory filesの一覧にCLAUDE.mdが出ているかで確かめられます。

permissionsでcurlの実行を通しやすくする

検証のたびに許可を求められると、反復が止まります。permissionsでは、Bash(npm run *) のようにコマンドの先頭を固定した許可ルールを書けます。スクリプトだけを通す形が安全です。

{
  "permissions": {
    "allow": [
      "Bash(scripts/check-htmx.sh)",
      "Bash(make dev)"
    ]
  }
}

注意点が1つあります。Bash(curl http://github.com/ *) のように引数でURLを絞る許可ルールは、オプションの順序・別プロトコル・リダイレクト・変数展開で簡単にすり抜けられるため脆いとされています。そこで、curl そのものを広く許可するのではなく、curl の中身をスクリプトの中に閉じ込め、スクリプトだけを許可します。接続先をローカルホストに限る検証は、スクリプト側の BASE で固定できます。

Claudeに頼むときの指示の出し方

実装を頼む指示は、成果物と判定方法をセットにします。

/items に商品の一覧と追加フォームをhtmxで作ってください。
- 追加は hx-post、成功時は一覧の末尾に行の断片を足す(beforeend)
- 空入力は422で、エラー文言の断片を返す
- 終わったら scripts/check-htmx.sh を実行し、全項目 ok の出力を見せて報告する

「出力を見せて報告する」を入れると、Claudeが実行せずに完了と言ってしまうことを防げます。テストの自動実行をhooksで強制する方法は、Claude Codeのhooksでテストを自動実行するにあります。htmx向けのスクリプトも、同じ枠組みで終了時に走らせられます。

部分テンプレートで起きやすい不具合と見分け方

症状curlで見るところ疑う原因
画面にページが入れ子になるcurlで見るところ断片に <html> が含まれる疑う原因HX-Request の分岐がない
ボタンを押しても何も起きないcurlで見るところステータスが204、または4xx疑う原因差し込まれない応答コード
フォームのエラーが表示されないcurlで見るところ検証エラーが422疑う原因responseHandling に422の設定がない
成功してもページ遷移するcurlで見るところ先頭が302疑う原因リダイレクトで返している
戻るで断片だけが表示されるcurlで見るところ履歴復元のリクエストに <html> が無い疑う原因履歴キャッシュ外の復元にページ全体を返していない
戻る・進むで別の画面が出るcurlで見るところVary ヘッダーが無い疑う原因断片とページがキャッシュで混ざっている
表の行を足すと壊れるcurlで見るところ断片が <tr> だけ疑う原因<template> で包む必要(hx-swap-oobの場合)

最後の行は、帯域外スワップ(Out of Band Swaps)の注意です。<tr> や <td> はHTMLの仕様上、単独ではDOMに置けない要素があり、hx-swap-oob を使うときは <template> タグで包みます。通常のスワップ対象に直接差し込む場合とは扱いが違う点に気をつけてください。

curlで見えないのは、差し込み後の画面の見た目と、ブラウザ上のイベントの連鎖です。そこはPlaywrightなどのブラウザ自動テストに分担し、curl検証は「サーバーの応答が約束どおりか」に絞るのが切り分けやすい構成です。

他の検証手段との役割分担

サーバーフレームワークごとのテスト機能(テストクライアントでHTTPを叩く方式)でも、同じ観点を自動テストに書けます。たとえばKtorなら、実サーバーを立てずに応答を検証する方法をClaude CodeでKtorサーバーを作るで扱っています。型検査が主役になるフロントエンド寄りの構成との違いは、Nuxtアプリの開発手順が参考になります。

手段向いているもの弱いもの
curlスクリプト向いているもの応答の形・ステータス・ヘッダー。言語を問わない弱いもの画面の見た目、ブラウザ側のイベント
サーバー側のテスト機能向いているものルーティング・ビジネスロジックと一体の検証弱いものフレームワークごとに書き方が違う
ブラウザ自動テスト向いているもの差し込み後の実際の画面弱いもの遅く、環境構築が重い

最初はcurlスクリプトで始め、不具合が出た観点だけを他の手段に足していく進め方で、費用をかけすぎずに済みます。

まとめ

htmxのサーバー描画アプリでは、応答が約束どおりかどうかがほぼそのまま品質になります。HX-Request の有無、キャッシュの Vary、履歴復元、<html> の混入、ステータスコード、リダイレクト、応答ヘッダーの7観点をcurlスクリプトに固定し、CLAUDE.mdに「エンドポイントを足したらcheckも足す」と1行書いておけば、Claudeの反復がそのまま回帰テストを育てます。

この記事を共有:XはてブLinkedIn