Go templ + htmx で構築するモダン Web アプリ(2026)

前端工程(更新: 2026年7月20日)

はじめに

10 年間 SPA が支配してきたが、2026 年チームは再考中:管理画面・社内ツールの 90% はクライアントルーティング不要。SEO と初回表示速度が命。フルスタックは同一言語で。Go の templ + htmx はこのための技術:コンパイル時型安全テンプレート + HTML 属性駆動インタラクション——JS 不要のレスポンシブ Web。


templ:型安全 Go テンプレート

html/template は実行時文字列解析——タイポはレンダリング時まで発見不能。templ は Go コードにコンパイル——全エラーをコンパイル時検出。

package views
templ Header(title string) { <header><h1>{ title }</h1><nav><a href="/">Home</a></nav></header> }
templ Layout(title string) {
  <!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@2.0.4"></script></head>
  <body>@Header(title)<main>{ children... }</main></body></html>
}

{ title } 型チェック。{ children... } スロット。コンポーネント間呼出はゼロランタイムオーバーヘッド——コンパイル時インライン化。


htmx:ハイパーメディア駆動

「任意の HTML 要素が HTTP リクエスト発行しレスポンスでページ任意箇所を置換」——JS なし。

属性 目的
hx-post/get HTTP トリガー hx-post="/submit"
hx-target 対象要素 hx-target="#result"
hx-swap 置換モード hx-swap="outerHTML"
hx-trigger トリガー hx-trigger="keyup changed delay:500ms"
hx-indicator ローディング hx-indicator="#spinner"

ライブ検索

templ SearchPage() {
  <input type="search" hx-get="/search" hx-trigger="keyup changed delay:300ms"
    hx-target="#results" hx-indicator="#spinner"/>
  <div id="spinner" class="htmx-indicator">読込中...</div>
  <div id="results"></div>
}
func searchHandler(w http.ResponseWriter, r *http.Request) {
    q := r.URL.Query().Get("q")
    views.SearchResults(searchDB(q)).Render(r.Context(), w)
}

結果断片のみ返却——htmx が #results に自動置換。

フォーム+インライン検証

templ CreateForm(errors map[string]string) {
  <form hx-post="/create" hx-target="this" hx-swap="outerHTML">
    <input type="text" name="title"/>
    if errors["title"] != "" { <span class="error">{ errors["title"] }</span> }
    <button>作成</button>
  </form>
}

失敗時、同じフォーム断片をエラー付きで返却。hx-target="this" でその場置換——ポップアップ不要、JS不要。


完全実装:ブログ CRUD

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /posts", listPosts)
    mux.HandleFunc("POST /posts", createPost)
    mux.HandleFunc("PUT /posts/{id}", updatePost)
    mux.HandleFunc("DELETE /posts/{id}", deletePost)
    http.ListenAndServe(":8080", mux)
}

削除——JS ゼロ

templ PostCard(p Post) {
  <div id={"post-"+p.ID} class="post-card">
    <h2>{ p.Title }</h2>
    <button hx-delete={fmt.Sprintf("/posts/%d",p.ID)}
      hx-target={"#post-"+fmt.Sprintf("%d",p.ID)}
      hx-swap="delete" hx-confirm="削除しますか?">削除</button>
  </div>
}

hx-swap="delete" で対象要素を直接削除——ハンドラは 200 OK 返すだけ。

ハンドラ例

func createPost(w http.ResponseWriter, r *http.Request) {
    title:=r.FormValue("title"); slug:=r.FormValue("slug")
    errors:=validate(title,slug)
    if len(errors)>0 { views.CreateForm(errors).Render(r.Context(),w); return }
    post,_:=db.Insert(title,slug)
    views.PostCard(post).Render(r.Context(),w)
}

SSE リアルタイム

templ LiveFeed() {
  <div hx-sse="connect:/events"><div hx-sse="swap:message">待機中...</div></div>
}
func eventsHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type","text/event-stream")
    flusher:=w.(http.Flusher)
    for msg:=range globalChan {
        fmt.Fprintf(w,"event: message\ndata: <p>%s</p>\n\n",msg)
        flusher.Flush()
    }
}

Socket.IO 不要。WebSocket 管理不要。


templ + htmx vs React SPA

次元 templ+htmx React SPA
初回描画 サーバーHTML、白画面なし SSR/SSG 必要
SEO 完璧 追加設定
インタラクション HTML 属性、JS 不要 JSX+hooks
状態管理 サーバーが真実 クライアント+同期
ビルド templ generate+go build Vite+TS
成果物 単一バイナリ(~15MB) 静的+Node/CDN
コールドスタート ミリ秒 SSR ランタイム依存
チーム要件 Goのみ フロントエンド要
アニメーション Alpine.js(~15KB) 豊富な生態系

中核差:templ+htmx = ハイパーメディア駆動(サーバーが中間状態をレンダリング、クライアントは部分置換のみ)。React = クライアントサイド(クライアントが状態所有)。


デプロイ

templ generate && go build -o myapp . && ./myapp

単一バイナリ。Docker は scratch から ~15MB。

パフォーマンス:レスポンスキャッシュ、静的アセット nginx/CDN、DB 接続プール。


FAQ

Q1:ファイルアップロード?——hx-encoding="multipart/form-data" + <input type="file">

Q2:ドラッグ&ドロップ?——Alpine.js(~15KB)で局所処理、htmx はサーバー処理。

Q3:コンポーネント多すぎ?——200+でも templ generate ~1.5秒 (M3 Pro)。

Q4:フロントエンド協業?——views/ を渡す。templ 構文は JSX に近い。


結論

SPA の「代替」ではない——異なるパラダイム。状態はサーバー、インタラクションは HTML。アプリが「表示+フォーム+検索」なら、半分のコード量でより良いパフォーマンス。

学習パス:カウンター→hx-post→フォーム検証→検索→デプロイ。

#Go#templ#htmx#超媒体#Web开发#SSR