1 ポイント 投稿者 GN⁺ 4 시간 전 | 1件のコメント | WhatsAppで共有
  • Go の html/template ベースのサーバーサイドレンダリングを維持しつつ、HTMX で動的なインタラクションを加え、1 つのレンダラーからページ全体と HTML フラグメントを選択的に返す
  • base.tmpl、ページテンプレート、再利用可能なフラグメントにマークアップを分け、Go 1.16 の embed.FS共有テンプレートセットを活用して、デプロイとレンダリング構造を単純化する
  • HX-Request に応じて検索結果をページ全体またはテーブル行として返し、Vary: HX-RequesthistoryRestoreAsHxRequest: false によってキャッシュや戻る操作での復元エラーを防ぐ
  • HTMX リクエストを別ページへ移動させる場合は、通常の 3xx ではなく HX-Redirect と 2xx レスポンスを使い、204・422・4xx・5xx ごとの responseHandling で差し替え範囲とエラー表示を制御する
  • ローカルストレージキャッシュと属性継承を無効にし、リクエストタイムアウトを設定する構成を出発点とする。規模が大きくなったら中間レイアウトテンプレートを追加し、管理者領域のような別画面構造にも対応できる

Go のサーバーレンダリングと HTMX を組み合わせる理由

  • HTMX は少ない JavaScript だけで動的なインタラクションを追加しながら、Go html/templateサーバーサイド HTML レンダリングがもたらす一貫性と安全性を維持できる
  • 実装で扱う軸は 3 つある
    • ページ全体と部分 HTML レスポンスをあわせてサポートするテンプレート構造
    • HTMX 環境におけるリダイレクトとエラー処理
    • HTMX のデフォルト値を変更する設定とその理由
  • ボタンを画像に差し替える基本機能から始め、ユーザー名やメールアドレスを入力している間に一覧を絞り込む検索画面へ拡張する
  • 同じテンプレートパターンは HTMX だけでなく、UnpolyHotwire のような HTML-over-the-wire ツールにも適用できる
  • HTMX の基本的な使い方を知らない場合は、先に公式ドキュメントを確認するのがよい

プロジェクトディレクトリと静的ファイル構成

  • プロジェクトは役割に応じて次の領域に分かれる
    • assets/html/base.tmpl: すべてのページが共有する HTML の骨格
    • assets/html/pages/: 個別ページのコンテンツ
    • assets/html/partials/: 複数箇所で再利用する HTML フラグメント
    • assets/static/cssassets/static/imgassets/static/js: 静的アセット
    • cmd/web/: サーバー起動、ハンドラー、HTML レンダラーのコード
  • 基本の骨格は次のコマンドで生成する
go mod init example.com/htmx
mkdir -p assets/static/css assets/static/img assets/static/js assets/html/partials assets/html/pages cmd/web
touch assets/efs.go assets/html/base.tmpl assets/html/partials/images.tmpl assets/html/pages/home.tmpl cmd/web/main.go cmd/web/handlers.go cmd/web/html.go
  • HTMX は CDN や NPM でインストールすることもできるが、コピーをダウンロードしてアプリケーションの静的ファイルとして直接配信する方式を主に使う
    • 構成が単純で、CDN 利用に伴うデメリットを避けられる
    • htmx.org@2.0.10 とクラスレス CSS フレームワーク bamboo.css@1.4.0 を使う
wget -P assets/static/js https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js
wget -P assets/static/css https://cdn.jsdelivr.net/npm/bamboo.css@1.4.0/dist/bamboo.min.css

ベース・ページ・部分テンプレートの分離

  • base.tmpl<head>、共通タイトル、<main> など文書全体の骨格を定義し、page:titlepage:content を挿入する
  • Bamboo CSS と HTMX スクリプトはベーステンプレートで読み込み、HTMX スクリプトには defer 属性を指定する
    • ブラウザーが HTML を解析している間にスクリプトを並列で取得する
    • HTML の解析と DOM 構築が終わった後にスクリプトを実行する
  • すべてのテンプレートはファイル名に依存せず、{{define}}...{{end}} で明示的な名前を付ける
    • Go コードは定義された名前だけでテンプレートを一貫して参照できる
    • page:title: は任意の区切り文字であり、page_titlepage-titlepageTitletitle のような名前も可能
  • ホームページのボタンには 2 つの動作を指定する
    • hx-get="/gopher": クリック時に GET /gopher リクエストを送る
    • hx-swap="outerHTML": レスポンス HTML でボタン自体を差し替える
  • partial:image:gopher は再利用可能な画像フラグメントで、width="{{.}}" によって呼び出し時に画像幅を受け取る
  • 特定のページでだけ使う HTML フラグメントは、共通の partials ディレクトリではなく、そのページファイル内に置くことができる
    • 複数ページで使うフラグメントは共通ディレクトリに置き、ページ専用のフラグメントは関連コンテンツのそばに配置するという区分である

Go バイナリに HTML と静的アセットを含める

  • Go 1.16 でファイル埋め込みが追加されて以降は、実行時にディスクから読む代わりに、HTML と静的アセットを Go バイナリに含めることができる
  • //go:embed "html" "static" ディレクティブで 2 つのディレクトリを embed.FS に入れ、fs.Sub() を使って 2 つのサブファイルシステムに分離する
    • HTMLFiles: html をルートに使うテンプレートファイルシステム
    • StaticFiles: static をルートに使う静的ファイルシステム
  • 分離して得られる利点は 2 つある
    • HTML コードと静的ファイルコードが互いのファイルに不要にアクセスしない
    • ファイルを開くときに html/ または static/ のプレフィックスを付ける必要がない
  • sub()panic() を使いたくない場合は、エラーを返して main() で 2 つの変数を初期化することもできる
  • 現在の実装で fs.Sub() が失敗するにはディレクトリ値が有効なパスでない必要があるが、固定文字列 "html""static" はこの検査を通るため、実行時パニックのリスクは非常に低い

共有テンプレートセットと htmlRenderer

  • htmlRenderer はレンダリングの流れを次のように管理する
    • 起動時に共有テンプレートセットを一度パースする
    • レンダリングのたびに共有セットを複製する
    • リクエストに必要なページテンプレートを追加でパースする
    • 指定した名前のテンプレートを実行し、HTTP レスポンスとして送信する
  • newHTMLRenderer()template.FuncMap を登録した後、共通テンプレートをパースする
    • nowtime.Now に接続する
    • 他のユーザー定義テンプレート関数も同じ場所に追加できる
  • render()sharedTemplates.Clone() でリクエストごとのテンプレートセットを作り、additionalTemplateFiles があれば ParseFS() で拡張する
  • テンプレートはまず bytes.Buffer に実行し、その後ステータスコードを記録してレスポンス本文として送信する
  • サーバー初期化時に "base.tmpl""partials/*.tmpl" を共有セットとしてパースする
    • ベーステンプレートとすべての共通フラグメントをレンダラーで常に利用できる
    • ページ別テンプレートだけをハンドラー呼び出し時に追加する
  • 静的ファイルは http.FileServerFS(assets.StaticFiles)/static/ パスで提供し、サーバーは :5051 で実行される

ページ全体と HTML フラグメントのレンダリング

  • ホームページのハンドラーは次の呼び出しで文書全体を返す
app.html.render(w, 200, nil, "base", "pages/home.tmpl")
  • 共有セットに pages/home.tmpl を追加し、base テンプレートを 200 OK で実行する呼び出しである
  • /gopher ハンドラーは追加ファイルをパースせず、共有セットにすでに含まれている partial:image:gopher だけを実行する
width := 100
app.html.render(w, http.StatusOK, width, "partial:image:gopher")
  • HTMX は /gopher から受け取った画像を、hx-swap="outerHTML" の設定に従ってボタンと置き換える
  • この構造には次の利点がある
    • テンプレートと静的アセットがバイナリに含まれるため、デプロイが容易になる
    • 同じ render() で HTML ページ全体と特定の HTML 断片の両方を返せる
    • 基本テンプレートと部分テンプレートにより、マークアップの重複を減らせる
    • 部分テンプレートを基本テンプレート、ページコンテンツ、ほかの部分テンプレートでも再利用できる

入力と同時に更新されるユーザー検索

  • 検索機能は 2 つの経路に分かれる
    • GET /users: すべてのユーザー情報を含む HTML ページ全体
    • GET /users/search: 名前またはメールアドレスが検索語と一致するユーザーのテーブル行だけを返す HTML 断片
  • 検索入力欄は次の HTMX 属性を組み合わせる
    • hx-get="/users/search" で検索リクエストを送る
    • hx-trigger="input changed delay:500ms, keyup[key=='Enter']" で入力変更後 500ms が経過するか、Enter を押すとリクエストする
    • 検索語は GET /users/search?query=foo 形式のクエリ文字列として渡される
    • hx-target="#search-results" でレスポンスを <tbody id="search-results"> の内部に入れる
    • hx-push-url="true" でリクエストごとにアドレスバーを更新し、ブラウザ履歴に項目を追加する
  • users:rows はユーザー行だけをレンダリングする別テンプレートである
    • 各行には名前とメールアドレスが表示される
    • IsGopher が真なら、partial:image:gopher を幅 24 でレンダリングする
  • listUserssearchUsers はどちらも pages/users.tmpl を共有セットに追加するが、実行するテンプレートは異なる
    • listUsers: base を実行してページ全体を返す
    • searchUsers: users:rows を実行して一致する行だけを返す
  • 検索語が空ならユーザー一覧全体を使い、値があれば strings.Contains() で名前とメールアドレスを検査する
  • 実際のアプリケーションでは次の改善を検討できる
    • 一覧ハンドラと検索ハンドラを 1 つにまとめる
    • エラー処理とロギングのヘルパーを中央集約する
    • ミドルウェアで Content Security Policy ヘッダーとパニックリカバリを追加する
    • サーバーのタイムアウトを適切に設定する

HX-Request でページ全体と部分レスポンスを区別する

  • ユーザーが /users/search?query=leo をブラウザで直接開いたり、共有されたリンクから訪問したりすると、テーブル行に相当する部分 HTML だけが表示される場合がある
  • HTMX リクエストには常に HX-Request: true ヘッダーが含まれるため、これを基準にレスポンス形式を決める
func isHTMXRequest(r *http.Request) bool {
    return r.Header.Get("HX-Request") == "true"
}
  • 検索ハンドラのデフォルト実行テンプレートを base にしておき、HTMX リクエストのときだけ users:rows に変更する
    • 直接訪問した場合は、検索結果を含むページ全体を受け取る
    • HTMX リクエストには <tbody> に入れる行の断片だけを返す

Vary ヘッダーと戻る操作の復元

  • 同じ URL が HX-Request の値に応じて異なるレスポンスを返すため、キャッシュにもこの違いを知らせる必要がある
  • すべてのレンダリングレスポンスに Vary: HX-Request を追加する
    • 厳密には、個別ハンドラで必要な場合だけ設定するほうが無駄は少ない
    • レンダラーで一括設定すれば、設定漏れによって起こり得るバグを避けられる
  • hx-push-urlhx-boost がブラウザ履歴を追加すると、HTMX は完成したページ HTML をブラウザのローカルストレージにキャッシュする
    • デフォルトのキャッシュは最大 10ページ
    • キャッシュにない地点まで戻ると、HTMX はサーバーに該当 URL を再リクエストする
  • デフォルトの復元リクエストには HX-Request: true が含まれるため、サーバーがページ全体ではなく部分 HTML を返してしまう可能性がある
  • HX-Request でレスポンス種別を選ぶなら、historyRestoreAsHxRequest: false を設定する
    • キャッシュミス時の復元リクエストから HX-Request: true を除外する
    • サーバーがそのリクエストに HTML ページ全体を返すようになる
  • HTMX の設定は base.tmplhtmx-config メタタグに入れる

HTMX リクエストを別ページへリダイレクトする

  • HTMX は成功メッセージのような HTML 断片を即座に置き換えられるため、一般的な Post/Redirect/Get フローが必要なケースを減らせる
  • ログイン成功後にプロフィールへ移動するように、完全に別ページへ送る必要がある場合は、通常の 3xx レスポンスだけでは処理できない
    • ブラウザが HTMX より先に 3xx レスポンスに従う
    • HTMX はリダイレクト後の最終レスポンスだけを確認し、そのコンテンツを既存のターゲットに置き換える
  • HTMX リクエストには 2xx レスポンスと HX-Redirect ヘッダーを一緒に使う
    • HX-Redirect: /foo/bar はブラウザをその URL に移動させ、ページ全体を再読み込みする
    • 移動先リクエストには HX-Request: true は含まれない
  • JavaScript が無効、または HTMX が読み込まれていない環境もサポートするなら、HTMX ではないリクエストには通常の 3xx レスポンスを維持する必要がある
func redirect(w http.ResponseWriter, r *http.Request, url string, code int) {
    if isHTMXRequest(r) {
        w.Header().Set("HX-Redirect", url)
        w.WriteHeader(http.StatusNoContent)
        return
    }

    http.Redirect(w, r, url, code)
}
  • ログイン後に /profile へ移動する場合は、次のように呼び出せる
redirect(w, r, "/profile", http.StatusSeeOther)

HX-RedirectHX-Location の違い

  • HX-Location はページ全体を再読み込みせず、リダイレクトに似た動作を行う
    • 対象 URL の HTML を取得する
    • レスポンスを <body> に置き換える
    • ブラウザ履歴に新しい項目を追加する
  • 対象 URL をリクエストするときは HX-Request: true が含まれる
  • 対象パスが HX-Request に応じて部分 HTML を返す場合、問題が起こる可能性がある
    • ハンドラは HX-Location によって来たリクエストと通常の HTMX リクエストを区別できない
    • 前者にはページ全体が必要だが、後者には部分レスポンスが必要である
  • 履歴復元とは異なり、HX-Location による移動で HX-Request: true をオフにする設定はない
  • 多くの場合、ページ全体の再読み込みを受け入れて HX-Redirect を使うほうが簡単で安全
  • 移動先パスが常に HTML 全体だけを返すように構成されているなら、HX-Location も利用できる

204・422・4xx・5xx レスポンスの処理

  • HTMX はデフォルトでは 4xx または 5xx レスポンスを DOM に置き換えない
    • 現在の画面をそのまま維持する
    • エラーは開発者ツールのコンソールに記録される
  • 存在しないテンプレート名が原因でサーバーが 500 Internal Server Error を返しても、デフォルト設定ではユーザーが画面上でエラーを確認できない
  • エラーメッセージをユーザーに見せるため、4xx と 5xx レスポンスは <body> 全体に置き換える よう設定する
  • 例外は 422 Unprocessable Content である
    • フォーム検証エラーを含むフォームを返すときに使う
    • 既存の HTMX ターゲットにレスポンスが正常に置き換えられる必要がある
  • responseHandling はステータスコードに応じて次のように設定する
    • 204 No Content: DOM を変更しない
    • 422 Unprocessable Content: 既存のターゲットに通常どおり置き換える
    • その他の 4xx と 5xx: レスポンスを <body> に置き換える
    • それ以外のレスポンス: 既存のターゲットに通常どおり置き換える
"responseHandling":[
    {"code":"204", "swap": false},
    {"code":"422", "swap": true},
    {"code":"[45]..", "swap": true, "target": "body"},
    {"code":"...", "swap": true}
]
  • 特定のインタラクションでエラーを <body> ではない別の対象に入れる必要がある場合は、response target 拡張機能でデフォルト設定を上書きできる

ブラウザのアドレスバーの現在の URL を確認する

  • HTMX は hx-boosthx-push-urlhx-replace-url を使わない限り、AJAX リクエストとレスポンス置換の過程でブラウザの URL を変更しない
  • そのため、Go ハンドラーの r.URL と、ユーザーがアドレスバーで見ている URL が異なる場合がある
  • HTMX は各リクエストの HX-Current-URL ヘッダーで、ブラウザが現在表示している URL を渡す
  • ヘルパー関数はこのヘッダーを url.URL としてパースし、ヘッダーがなければ r.URL を返す
func browserURL(r *http.Request) (*url.URL, error) {
    cu := r.Header.Get("HX-Current-URL")
    if cu != "" {
        return url.Parse(cu)
    }

    return r.URL, nil
}

HTMX のデフォルト値を変更する設定

  • historyCacheSize: 0 で HTMX の ローカルストレージのページキャッシュを完全にオフにする
    • ローカルストレージのキャッシュはバグやセキュリティ問題の原因になり得る
    • 戻るボタンを押すと、キャッシュの代わりにサーバーから HTML を再取得する
    • 今後の HTMX バージョンでは、同じ理由からローカルストレージのキャッシュはデフォルトで無効化される予定
  • disableInheritance: trueHTMX 属性の継承を無効化する
    • 属性は常に明示的に宣言するほうが明確
    • バグや意図しない動作のリスクを下げる
    • 今後の HTMX バージョンでは、属性の継承もデフォルトで無効化される予定
  • includeIndicatorStyles: false で、HTMX がインジケータースタイルを注入しないようにする
    • インジケータースタイルを他の CSS ルールとあわせて自分で定義し、一貫性を保つ
  • HTMX リクエストにはデフォルトでタイムアウトがなく、サーバーが応答するまで待つ
    • Go アプリケーションのタイムアウトとデッドラインの扱い方に応じて、timeout をミリ秒単位で指定できる
    • 出発点として timeout: 5000 を使う
  • 基本設定には historyRestoreAsHxRequest: false と、ステータスコード別の responseHandling も含まれる
<meta
    name="htmx-config"
    content='{
        "includeIndicatorStyles": false,
        "historyCacheSize": 0,
        "historyRestoreAsHxRequest": false,
        "responseHandling":[
            {"code":"204", "swap": false},
            {"code":"422", "swap": true},
            {"code":"[45]..", "swap": true, "target": "body"},
            {"code":"...", "swap": true}
        ],
        "timeout": 5000
    }'
>

大規模アプリケーション向けのページ別レイアウト

  • ベーステンプレートとページ別テンプレートだけでは足りない場合、2 つの階層の間に レイアウトテンプレートを追加できる
  • 管理者ページのように、通常ページとは異なるタイトル、ナビゲーションメニュー、本文構造が必要な領域に適している
  • basepage:content を直接呼び出す代わりに layout を呼び出す
<body>
    {{template "layout" .}}
</body>
  • layouts/admin.tmpl は管理者領域の共通構造を定義し、その内部で page:content を呼び出す
    • 管理者領域のタイトル
    • ユーザーと注文へ移動するナビゲーションメニュー
    • ページ別コンテンツが入る <main>
  • ハンドラーは render() に、使用するレイアウトとページテンプレートをまとめて渡す
app.html.render(
    w,
    200,
    nil,
    "base",
    "layouts/admin.tmpl",
    "pages/admin-orders.tmpl",
)
  • 共有のベーステンプレートを維持しながら、パスごとのレイアウトとページコンテンツを組み合わせられるため、管理者領域のように別構造が必要な画面も同じレンダリングパターンで拡張できる

1件のコメント

 
GN⁺ 4 시간 전
Hacker News の意見
  • Go + HTMX が好きで、テンプレート、コンポーネント、部分 HTML の型安全性を高めるために a-h/templ と一緒に使っている ツールセット全体を Go、Unix、SQLite で構成した GUS スタックと呼んで公開した: https://housecat.com/blog/the-gus-stack-go-unix-sqlite exe.dev の GUTS スタックに大きく触発されたが、TypeScript の代わりに HTMX を使っている: https://exe.dev/docs/guts スタックトレース付きエラーには cockroachdb/errors、型安全な HTML には templ、OpenAPI 仕様の生成には fuego、SQL コード生成には sqlc、純 Go の SQLite には modernc.org/sqlite、マイグレーションには goose、永続化ワークフローには dbos、Chrome/CDP テストと自動化には rod を使っている この組み合わせは、自分でコーディングする場合でもエージェントの助けを借りてコーディングする場合でも、単一バイナリをビルドしてデプロイするプロセス全体で生産的だ

    • HTMX を論じるときに Hyperscript がしかるべき注目を受けていないのは残念だ サーバー往復や別個の JavaScript 関数なしで DOM を操作できるため Go + HTMX と非常に相性がよく、すでに作業している HTML の拡張のように感じられる 宣言型プログラミングを好まない人もいるだろうが、複数の決済ゲートウェイを扱う Open Payment Host では、複雑な DOM 操作まで Hyperscript だけで実装できた https://hyperscript.org/, https://openpaymenthost.com/
    • HTMX、またはハイパーテキスト・Unix・Go・SQLite をまとめて HUGS スタックに改名することもできそうだ
    • Go・Templ・HTMX を GoTH スタックと呼ぶ例も見たことがあり、sqlc まで加えると GoTHs や GHosT になる go generate ./... をビルド段階として受け入れると得るものは多く、goverter で sqlc モデルとテンプレートオブジェクト・戻り値の間の変換も生成している ボイラープレートの約 50% が自動生成され、型安全性が非常に良くなる
    • 最近は型安全な SQL のために jet を好んで使っている コード生成には実行中のデータベースが必要だが、コードを書く前にまずマイグレーションを適用するよう強制される点も利点だと見ている https://github.com/go-jet/jet
    • 独自のデータベースすら不要かもしれないリアルタイム API ダッシュボードのような小規模なリアクティブ Web アプリには、最近 PAHG テンプレートをよく使っている Pico.css・Alpine.js・HTMX・Go の組み合わせで、もちろん SQLite も接続できる https://github.com/hiAndrewQuinn/pahg-template
  • HTMX は多くの用途で素晴らしいが、チームメンバーが同意しなければ導入は非常に難しい 本格的な技術ではないという反発が続き、さまざまなバグがまず HTMX のせいにされ、後で誤解だと分かってもすでに信頼は損なわれている これまで働いた中で最も優秀なチームでもそうだったし、結局は戦う相手を選ばなければならないという教訓を得た Go の html/template もインターフェイスが妙に不自然だと思う。『A Philosophy of Software Design』が勧めるように複雑さは内部に押し込めるべきなのに、HTML をレンダリングするたびにテンプレートの複製を気にしなければならない構造は不必要に複雑だ

    • 私も HTMX は好きだが、チーム導入では同じ現実を経験した 簡単な Web アプリに提案したが、何年も JSON を返して JS/jQuery でレンダリングしてきた開発者は、各段階で既存の問題を新しい方法で解くことに苦労し、別の開発者は試そうとさえせず、チームではずっと Blazor ばかりが話題に上っていた どんな新しいアプローチでも難しかっただろうが、HTMX には実質的に機会がなかった 小規模チームなので JavaScript の 95% を避け、サーバー側でコードと HTML を生成すればコードベースはより整理され、テストもしやすくなるはずなのに残念だ。個人プロジェクトでは引き続き HTMX を使って良い流れを見つけており、Web 開発が大きく単純化されたと感じている
    • コンサルタントの立場でチームを積極的に指導し、HTMX を学ぶと実際の作業環境である ブラウザへの理解が深まる一方、React などは開発者をブラウザから隔離すると説明したが、私も同じ経験をした: https://www.reddit.com/r/htmx/s/DuXyGgsCWK html/template は基本的なセキュリティと標準組み込みという点が良いが、動的な性質のために答えが定まらない部分が多すぎるし、Templ も大規模な商用アプリをいくつか書いてみると開発体験に物足りなさがあった そこで gsx を作った: https://github.com/gsxhq/gsx
    • テンプレートをレンダリングするたびに複製する必要はなく、一度コンパイルしてから実行するだけでよい 元記事の問題は、メインテンプレートでページタイトルを指定しようとしているために自分で作り出したものだ。タイトルをコンテキストで渡し、base.tmplBASE_BEGINBASE_END に分けて最終テンプレートで使えばよい ページごとのタイトルを実行時に渡す必要はあるが、元記事の方式も 多言語対応を始めれば結局は破綻する
    • 当社ではまったく逆で、どんな問題があっても React SPA は確立された技術だという理由で、スケールしない事実を説得するのが不可能だ 各問題を個別には見ても、それらがすべて同じ構造に由来しているという大きな絵につなげられない
  • 最近のプロジェクトで HTMX をとても楽しく使っている AngularJS と React 以前の Web を知っている立場としては、実際のページを作り、JavaScript の量を最小限に抑えられる点が特に良い 純粋な JavaScript でも可能だが、HTMX は繰り返し出てくるイベント処理のボイラープレートを代わりに担ってくれる 現代のフロントエンド哲学が好きでないなら試してみる価値があり、公式サイトの最初の例はごく基本的だが、もう少し学ぶとずっと強力になる

    • 控えめに SvelteKit も勧めたい 表面的にはまた別の JavaScript フロントエンドフレームワークのように見えるが、実際に使ってみると HTMX 系が目指すハイパーテキスト中心の設計を見事に支援している
  • かなり大きなプロジェクトを HTMX + Go で作ってみたが、まだ十分ではなく、今後も望むレベルに到達するかは確信できない 単純な CRUD アプリや管理ダッシュボードには素晴らしいが、相互につながったコンポーネント、共有状態、複雑なインタラクションが増えると、管理が急速に難しくなる React が嫌で HTMX を選んだものの、結局バックエンドは Go のままにし、フロントエンドは SvelteKit の SPA モードに変更した。両者をきれいに分離することで、複雑な UI をはるかに簡単に開発・保守できた Svelte は JSX という別言語よりも HTML の自然な拡張のように感じられ、状態管理とコンポーネントモデルがシンプルで、新しい $state 構文も特に良い

    • HTMX のサイトでも、そうした用途には向かないという トレードオフを明確に示しているが、関連する記事を見つけるには少し深く掘る必要がある
    • HTMX 開発陣によるフロントエンド側の兄弟プロジェクトである Hyperscript は、フロントエンドの状態のような機能を扱う。これも使ってみたのか気になる: https://hyperscript.org/docs/reactivity/
    • HTMX の哲学の核心は、そもそも 共有状態でつながったコンポーネントが本当に必要なのかを問うところにあるのではないかと思う 結局、ほとんどのアプリはデータベースに対して CRUD を行う
  • 最近のサイドプロジェクトは、LLM の助けがあるかどうかに関係なく、ほぼすべて Go + HTMX で書いている Opus と GPT もこの組み合わせを非常にうまく扱い、素早く作って始められ、単一バイナリなのでデプロイとホスティングも便利 高速な反復開発にとても良いスタック

  • HTMX を使うなら、バックエンドでも React のように共通の HTML 片を関数として抽出して コンポーネント化しやすい HTML 生成方式を強く勧める HTMX では状況に応じて、特定の HTML のすぐ隣にタグを付けたり外したりする必要があるなど、バックエンドが生成する HTML に柔軟性が必要になる。たとえば最上位で `` タグを見つけるとページタイトルを更新する 伝統的な文字列ベースのテンプレートエンジンではこうした作業は難しいが、言語埋め込み型 HTML ライブラリでは簡単で、言語全体の抽象化機能を活用できるため HTMX と特に相性が良い JavaScript バックエンドなら https://github.com/WebReflection/uhtml-ssr のようなタグ付きテンプレートリテラル関数、Go なら https://www.gomponents.com/、Scala なら ScalaTags を使える

  • Alex Edwards の本と Learn Go with Tests で初めて Go に触れ、今でも勧めている 古いプロジェクトの一部を HTMX に移行してみたくなった

    • Go の Web 開発に少しでも興味があるなら、Alex Edwards の Let's GoLet's Go Further はどちらも読むべき これまで読んだプログラミング言語の入門書の中で、最も取りつきやすく、楽しく、実用的な本たちだった
  • Kotlin + HTMX ベースのフレームワークを個人用途で作った: https://github.com/reubenfirmin/zoned Web アプリ全体をエンドツーエンドで型安全にできるか確認することが目標で、JSX に似た DSL で HTML を書く Kotlinx.html を使っている 最初の 90% は自分で作り、最後の 10% と README は公開を仕上げるために Claude の助けを借りた

  • Go も Alex Edwards も好きだが、HTMX を使った結果にはいつも失望してきた コードベースの複雑さがアプリ自体の 2 倍の速さで増えていくように感じ、奇妙な回避策なしには抜け出せない境界条件にいつも出くわす Node パッケージを嫌う理由は理解できるが、HTMX はそれに対する過剰補償のように見える。JSON と格闘しないことで節約した時間より、アプリの見た目と使い心地をきちんと作るのにかかる時間のほうが 3 倍大きい Mantine テンプレートなら 2 分で構成して優れた UI コンポーネントを活用でき、ビルド済みの静的アセットを含めれば同じように単一の Go バイナリにできる: https://github.com/mantinedev/vite-min-template

    • Rails でハイパーメディアに初めて触れたときは便利で簡単だったので、Go/HTMX の摩擦は意外だったが、何度も試す中で 抽象化が重要だという点を学んだ コンポーネント生成器とリバースルーターがないとかなり苦痛で、Rails はそれらを標準で提供していたため気にする必要がなかった 最大の利点は、計算とデータが一か所にあること。LLM も自己完結したハイパーメディアアプリをうまく使ってテストでき、JavaScript を待たないので反復サイクルが短くなる ただし、すぐ使えるコンポーネントライブラリがないため始めにくいという点には同意する
    • HTMX を使うには 設計哲学全体を変える必要があった 必ずしも欠点というより良い変化かもしれないが、かなり大きな転換であり、成果物はモバイルアプリを真似るよりも Web 固有の形に近くなる
    • Mantine では作れるが HTMX では作れなかった UI の具体例が何なのか気になる
  • Go + Datastar のほうがシンプルなので、はるかに好んでいる

    • Go と Datastar はぴったりの手袋のようによく合う テンプレートには a-h/templ を加えたいところで、Datastar もサポートしている Datastar がここまで知られていないことに驚く
    • 私も Datastar を好んでいるが、HTMX4 の新機能が差をかなり縮めている エコシステムが良いパターンに収束していく様子は喜ばしい