htmx 逆引きレシピ
ログイン処理を扱うには?

公開日:
最終更新日:

管理画面では「未ログインならログインへ」「保存後は詳細へ」「権限不足なら403へ」など、遷移の分岐が必ず発生します。
この判断をフロント側で頑張ると複雑になりがちですが、htmxならサーバー主導でスッキリ扱えます。

このページでは、HX-Redirect / HX-Location を使い、ログイン誘導・保存後遷移・権限エラーを1つのデモにまとめます。
「準SPAのまま自然に遷移する」実装パターンとして、そのまま業務UIに流用できます。

使用するhtmx属性

  • hx-boost:リンク/フォーム送信をAJAX化して“準SPA”にする(既存SSRに後付けしやすい)
  • hx-target:差し替え先を指定(このデモでは #REDIRECT_APP を更新する)
  • hx-select:レスポンスから必要な部分だけ抜き出す(#REDIRECT_APP だけ反映)
  • hx-swap:差し替え方を指定(outerHTML で箱ごと置換し、入れ子崩れを防ぐ)
  • HX-Redirect:サーバー側の判断で別ページへ遷移させる(未ログイン/権限不足の誘導に強い)
  • HX-Location:遷移先を指示して“次の画面を読み直す”(保存後→詳細などに最適)

利用シーン

  • 「未ログインならログインへ」:編集/保存を押した瞬間にログイン画面へ誘導したい
  • 「保存後に詳細ページへ」:作成/更新のあと“詳細へ遷移”して確認させたい
  • 「権限不足なら403画面へ」:admin専用の操作を、権限がないユーザーに見せないようにしたい

リダイレクト/ログイン誘導を扱う

このデモは、遷移の分岐をサーバーで判断し、htmxのヘッダでクライアントへ指示します。
結果として、JSを書かずに「ログイン誘導」「保存後遷移」「403表示」を自然な導線で実装できます。

PHP(メイン画面/_redirect_auth.php)

<?php
// 型を厳密に扱う
declare(strict_types=1);

// セッションを開始する
session_start();

// HTMLとして返す
header('Content-Type: text/html; charset=UTF-8');

// HTMLエスケープ関数
function h(string $s): string {
	// 特殊文字をエスケープする
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// htmxリクエストか判定する
function isHx(): bool {
	// HX-Requestヘッダを見る
	return (($_SERVER['HTTP_HX_REQUEST'] ?? '') === 'true');
}

// ログイン状態を判定する
$isLogin = (bool)($_SESSION['is_login'] ?? false);

// ロールを取得する(user/admin)
$role = (string)($_SESSION['role'] ?? 'user');

// 初期データが無ければ作る
if (!isset($_SESSION['records']) || !is_array($_SESSION['records'])) {

	// 初期一覧を作る
	$_SESSION['records'] = [
		['id' => 801, 'title' => '申請:端末貸与',       'owner' => 'tanaka', 'status' => 'OPEN'],
		['id' => 802, 'title' => '申請:権限変更',       'owner' => 'sato',   'status' => 'PENDING'],
		['id' => 803, 'title' => '申請:アカウント追加', 'owner' => 'suzuki', 'status' => 'DONE'],
	];
}

// 一覧を取り出す
$records = (array)$_SESSION['records'];
?>
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>リダイレクト/ログイン誘導 | htmx</title>
	<link rel="stylesheet" media="print" href="/css/C32.table.css">

	<link rel="stylesheet" href="/css/C01.destyle.css?2026-01-03">
	<link rel="stylesheet" href="/css/C11.vars.css?2026-01-03">

	<link rel="stylesheet" href="/css/C12.str.css?2026-01-03">
	<link rel="stylesheet" href="/css/C13.num.css?2026-01-03">
	<link rel="stylesheet" href="/css/C14.color.css?2026-01-03">
	<link rel="stylesheet" href="/css/C21.base.css?2026-01-03">
	<link rel="stylesheet" href="/css/C22.form.css?2026-01-03">
	<link rel="stylesheet" href="/css/C23.parts.css?2026-01-03">
	<link rel="stylesheet" href="/css/C31.calendar.css?2026-01-03">
	<link rel="stylesheet" href="/css/C32.table.css?2026-01-03">

	<!-- htmx -->
	<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body class="P1rm W800 CENTER">

	<!--
		この箱だけを“準SPA”化する
		- hx-boost:リンク/フォームをAJAX化
		- hx-target/hx-select:この箱だけ差し替え
		- hx-swap:outerHTMLで入れ子事故を防ぐ
	-->
	<section
		id="REDIRECT_APP"
		class="SECTION"
		hx-boost="true"
		hx-target="#REDIRECT_APP"
		hx-select="#REDIRECT_APP"
		hx-swap="outerHTML"
	>

		<h1 class="FS2rm">リダイレクト/ログイン誘導を扱うには?</h1>

		<p class="HTMX-NOTE">
			未ログインならログインへ/保存後は詳細へ/権限不足なら403へ…を1つのデモでまとめます。<br>
			遷移の判断はサーバー側で行い、<code>HX-Redirect</code> と <code>HX-Location</code> で誘導します。
		</p>

		<!-- 状態表示 -->
		<div class="CARD">
			<strong>ログイン:</strong><?= $isLogin ? 'YES' : 'NO' ?> 
			<strong>権限:</strong><?= h($role) ?>
		</div>

		<!-- ログイン/ログアウト -->
		<div class="FORM__FOOT MB2rm">
			<?php if (!$isLogin): ?>
				<a class="BTN is-ok" href="/htmx/demo/_redirect_auth_login.php?next=/htmx/demo/_redirect_auth.php">
					ログインへ
				</a>
			<?php else: ?>
				<a class="BTN is-ghost" href="/htmx/demo/_redirect_auth_logout.php">
					ログアウト
				</a>
			<?php endif; ?>

			<a class="BTN" href="/htmx/demo/_redirect_auth_admin.php">
				管理者ページ(admin専用)
			</a>
		</div>


		<!-- 保存フォーム(ログイン必須) -->
		<section class="SECTION MB32">

			<h2 class="FS24">保存 → 詳細へ(ログイン必須)</h2>

			<p class="HTMX-NOTE">
				保存に成功したら、<code>HX-Location</code> で詳細ページへ遷移します(準SPAのまま)。<br>
				未ログインの場合は <code>HX-Redirect</code> でログイン画面へ飛ばします。
			</p>

			<form class="FORM" method="post" action="/htmx/demo/_redirect_auth_save.php">
				<label>
					タイトル
					<input type="text" name="title" value="" placeholder="例:申請:メーリングリスト追加">
				</label>

				<label>
					担当
					<input type="text" name="owner" value="" placeholder="例:tanaka">
				</label>

				<label>
					ステータス
					<select name="status">
						<option value="OPEN">OPEN</option>
						<option value="PENDING">PENDING</option>
						<option value="DONE">DONE</option>
					</select>
				</label>

				<button class="BTN is-ok MT16" type="submit">保存して詳細へ</button>
			</form>
		</section>


		<!-- 一覧(詳細はログイン必須) -->
		<section class="SECTION MB32">

			<h2 class="FS24">一覧(詳細はログイン必須)</h2>

			<div class="TABLE_WRAPPER">
			<table class="TABLE">
				<thead>
					<tr>
						<th class="W15pc">ID</th>
						<th>タイトル</th>
						<th class="W15pc">担当</th>
						<th class="W15pc">ステータス</th>
						<th>操作</th>
					</tr>
				</thead>
				<tbody>
					<?php foreach ($records as $r): ?>
						<tr>
							<td><?= h((string)$r['id']) ?></td>
							<td><?= h((string)$r['title']) ?></td>
							<td><?= h((string)$r['owner']) ?></td>
							<td><span class="BADGE"><?= h((string)$r['status']) ?></span></td>
							<td>
								<a class="BTN" href="/htmx/demo/_redirect_auth_detail.php?id=<?= h((string)$r['id']) ?>">
									詳細を見る(ログイン必須)
								</a>
							</td>
						</tr>
					<?php endforeach; ?>
				</tbody>
			</table>
			</div>
		</section>

	</section>

</body>
</html>

PHP(ログイン画面/_redirect_auth_login.php)

<?php
// 型を厳密に扱う
declare(strict_types=1);

// セッションを開始する
session_start();

// HTMLとして返す
header('Content-Type: text/html; charset=UTF-8');

// HTMLエスケープ関数
function h(string $s): string {
	// 特殊文字をエスケープする
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// next(ログイン後に戻る先)を受け取る
$next = (string)($_GET['next'] ?? '/htmx/demo/_redirect_auth.php');

// 空ならデフォルトへ
if ($next === '') $next = '/htmx/demo/_redirect_auth.php';
?>
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>ログイン | リダイレクト</title>

	<!-- htmx -->
	<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>

	<section
		id="REDIRECT_APP"
		class="SECTION"
		hx-boost="true"
		hx-target="#REDIRECT_APP"
		hx-select="#REDIRECT_APP"
		hx-swap="outerHTML"
	>

		<h1>ログイン</h1>

		<p class="HTMX-NOTE">
			ログイン成功後は、<code>next</code> に戻します。<br>
			(未ログインでアクセスしたページを“そのまま再開”できます)
		</p>

		<form class="FORM" method="post" action="/htmx/demo/_redirect_auth_login_do.php">
			<input type="hidden" name="next" value="<?= h($next) ?>">

			<label>
				ユーザー名(ダミー)
				<input type="text" name="user" value="mikan">
			</label>

			<label>
				権限(デモ用)
				<select name="role">
					<option value="user">user</option>
					<option value="admin">admin</option>
				</select>
			</label>

			<button class="BTN is-ok" type="submit">ログイン</button>
			<a class="BTN is-ghost" href="/htmx/demo/_redirect_auth.php">戻る</a>
		</form>

	</section>

</body>
</html>

PHP(ログイン処理 → 次へ/_redirect_auth_login_do.php)

<?php
// 型を厳密に扱う
declare(strict_types=1);

// セッションを開始する
session_start();

// htmxリクエストか判定する
function isHx(): bool {
	// HX-Requestヘッダを見る
	return (($_SERVER['HTTP_HX_REQUEST'] ?? '') === 'true');
}

// next(ログイン後に戻る先)を受け取る
$next = (string)($_POST['next'] ?? '/htmx/demo/_redirect_auth.php');

// ロールを受け取る
$role = (string)($_POST['role'] ?? 'user');

// ロールが空ならuser
if ($role === '') $role = 'user';

// ログイン済みにする
$_SESSION['is_login'] = true;

// ロールを保存する
$_SESSION['role'] = $role;

// セッション固定化対策(デモでもやっておく)
session_regenerate_id(true);

// htmxならHX-Redirectで遷移させる
if (isHx()) {
	// リダイレクト先を指示する
	header('HX-Redirect: ' . $next);
	// 終了する
	exit;
}

// 通常リクエストならLocationで遷移させる
header('Location: ' . $next);

// 終了する
exit;

PHP(ログアウト/_redirect_auth_logout.php)

<?php
// 型を厳密に扱う
declare(strict_types=1);

// セッションを開始する
session_start();

// ログイン状態を消す
unset($_SESSION['is_login']);

// ロールも消す
unset($_SESSION['role']);

// もとの画面へ戻す
header('Location: /htmx/demo/_redirect_auth.php');

// 終了する
exit;

PHP(保存 → 詳細へ/_redirect_auth_save.php)

<?php
// 型を厳密に扱う
declare(strict_types=1);

// セッションを開始する
session_start();

// htmxリクエストか判定する
function isHx(): bool {
	// HX-Requestヘッダを見る
	return (($_SERVER['HTTP_HX_REQUEST'] ?? '') === 'true');
}

// ログイン状態を判定する
$isLogin = (bool)($_SESSION['is_login'] ?? false);

// 未ログインならログインへ飛ばす
if (!$isLogin) {

	// ログイン後はメインへ戻す
	$next = '/htmx/demo/_redirect_auth.php';

	// htmxならHX-Redirectで遷移
	if (isHx()) {
		// ログイン画面へ誘導する
		header('HX-Redirect: /htmx/demo/_redirect_auth_login.php?next=' . rawurlencode($next));
		// 終了する
		exit;
	}

	// 通常ならLocationで遷移
	header('Location: /htmx/demo/_redirect_auth_login.php?next=' . rawurlencode($next));
	// 終了する
	exit;
}

// タイトルを受け取る
$title = (string)($_POST['title'] ?? '');

// 担当を受け取る
$owner = (string)($_POST['owner'] ?? '');

// ステータスを受け取る
$status = (string)($_POST['status'] ?? 'OPEN');

// 空対策
if ($title === '') $title = '申請:新規作成';

// 空対策
if ($owner === '') $owner = 'tanaka';

// IDを作る(簡易)
$newId = random_int(900, 99999);

// 一覧を取り出す
$records = (array)($_SESSION['records'] ?? []);

// 先頭に追加する
array_unshift($records, [
	'id'     => $newId,
	'title'  => $title,
	'owner'  => $owner,
	'status' => $status,
]);

// セッションに保存する
$_SESSION['records'] = $records;

// 詳細URLを作る
$detailUrl = '/htmx/demo/_redirect_auth_detail.php?id=' . $newId;

// htmxならHX-Locationで“準SPA遷移”する
if (isHx()) {

	// HX-Locationで次の画面を読み直させる(#REDIRECT_APPだけ差し替える)
	header('HX-Location: ' . json_encode([
		'path'   => $detailUrl,
		'target' => '#REDIRECT_APP',
		'select' => '#REDIRECT_APP',
		'swap'   => 'outerHTML',
	], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

	// 終了する
	exit;
}

// 通常ならLocationで遷移する
header('Location: ' . $detailUrl);

// 終了する
exit;

PHP(詳細:未ログインならログインへ/_redirect_auth_detail.php)

<?php
// 型を厳密に扱う
declare(strict_types=1);

// セッションを開始する
session_start();

// HTMLとして返す
header('Content-Type: text/html; charset=UTF-8');

// HTMLエスケープ関数
function h(string $s): string {
	// 特殊文字をエスケープする
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// htmxリクエストか判定する
function isHx(): bool {
	// HX-Requestヘッダを見る
	return (($_SERVER['HTTP_HX_REQUEST'] ?? '') === 'true');
}

// ログイン状態を判定する
$isLogin = (bool)($_SESSION['is_login'] ?? false);

// idを受け取る
$id = (int)($_GET['id'] ?? 0);

// 未ログインならログインへ飛ばす
if (!$isLogin) {

	// ログイン後に戻るURLを作る
	$next = '/htmx/demo/_redirect_auth_detail.php?id=' . $id;

	// htmxならHX-Redirect
	if (isHx()) {
		// ログイン画面へ誘導する
		header('HX-Redirect: /htmx/demo/_redirect_auth_login.php?next=' . rawurlencode($next));
		// 終了する
		exit;
	}

	// 通常ならLocation
	header('Location: /htmx/demo/_redirect_auth_login.php?next=' . rawurlencode($next));
	// 終了する
	exit;
}

// 一覧を取り出す
$records = (array)($_SESSION['records'] ?? []);

// 対象レコードを探す
$found = null;

// 1件ずつ探す
foreach ($records as $r) {
	// idが一致したら採用する
	if ((int)($r['id'] ?? 0) === $id) {
		$found = $r;
		break;
	}
}
?>
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>詳細 | リダイレクト</title>

	<!-- htmx -->
	<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>

	<section
		id="REDIRECT_APP"
		class="SECTION"
		hx-boost="true"
		hx-target="#REDIRECT_APP"
		hx-select="#REDIRECT_APP"
		hx-swap="outerHTML"
	>

		<h1>詳細ページ</h1>

		<?php if ($found === null): ?>
			<p class="HTMX-NOTE">対象データが見つかりません。</p>
		<?php else: ?>
			<ul class="HTMX-LIST">
				<li><strong>ID:</strong><?= h((string)$found['id']) ?></li>
				<li><strong>タイトル:</strong><?= h((string)$found['title']) ?></li>
				<li><strong>担当:</strong><?= h((string)$found['owner']) ?></li>
				<li><strong>ステータス:</strong><?= h((string)$found['status']) ?></li>
			</ul>
		<?php endif; ?>

		<div class="FORM__FOOT">
			<a class="BTN is-ghost" href="/htmx/demo/_redirect_auth.php">一覧へ戻る</a>
		</div>

	</section>

</body>
</html>

PHP(admin専用 → 権限不足なら403へ/(admin専用 → 権限不足なら403へ))

<?php
// 型を厳密に扱う
declare(strict_types=1);

// セッションを開始する
session_start();

// htmxリクエストか判定する
function isHx(): bool {
	// HX-Requestヘッダを見る
	return (($_SERVER['HTTP_HX_REQUEST'] ?? '') === 'true');
}

// ログイン状態を判定する
$isLogin = (bool)($_SESSION['is_login'] ?? false);

// ロールを取得する
$role = (string)($_SESSION['role'] ?? 'user');

// 未ログインならログインへ
if (!$isLogin) {

	// ログイン後の戻り先
	$next = '/htmx/demo/_redirect_auth_admin.php';

	// htmxならHX-Redirect
	if (isHx()) {
		// ログインへ誘導する
		header('HX-Redirect: /htmx/demo/_redirect_auth_login.php?next=' . rawurlencode($next));
		// 終了する
		exit;
	}

	// 通常ならLocation
	header('Location: /htmx/demo/_redirect_auth_login.php?next=' . rawurlencode($next));
	// 終了する
	exit;
}

// admin以外なら403へ
if ($role !== 'admin') {

	// htmxならHX-Redirectで403画面へ
	if (isHx()) {
		// 403ページへ誘導する
		header('HX-Redirect: /htmx/demo/_redirect_auth_403.php');
		// 終了する
		exit;
	}

	// 通常ならLocation
	header('Location: /htmx/demo/_redirect_auth_403.php');
	// 終了する
	exit;
}

// HTMLとして返す
header('Content-Type: text/html; charset=UTF-8');

// HTMLエスケープ関数
function h(string $s): string {
	// 特殊文字をエスケープする
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
?>
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>管理者ページ | リダイレクト</title>

	<!-- htmx -->
	<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>

	<section
		id="REDIRECT_APP"
		class="SECTION"
		hx-boost="true"
		hx-target="#REDIRECT_APP"
		hx-select="#REDIRECT_APP"
		hx-swap="outerHTML"
	>

		<h1>管理者ページ(admin専用)</h1>

		<p class="HTMX-NOTE">
			admin権限がある場合のみ表示されます。<br>
			userの場合は 403 画面へ誘導されます。
		</p>

		<ul class="HTMX-LIST">
			<li>監査ログ:OK</li>
			<li>権限設定:OK</li>
			<li>危険操作:OK</li>
		</ul>

		<div class="FORM__FOOT">
			<a class="BTN is-ghost" href="/htmx/demo/_redirect_auth.php">戻る</a>
		</div>

	</section>

</body>
</html>

PHP(403画面/_redirect_auth_403.php)

<?php
// 型を厳密に扱う
declare(strict_types=1);

// 403ステータスにする
http_response_code(403);

// HTMLとして返す
header('Content-Type: text/html; charset=UTF-8');

// HTMLエスケープ関数
function h(string $s): string {
	// 特殊文字をエスケープする
	return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
?>
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<title>403 | 権限不足</title>

	<!-- htmx -->
	<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>

	<section
		id="REDIRECT_APP"
		class="SECTION"
		hx-boost="true"
		hx-target="#REDIRECT_APP"
		hx-select="#REDIRECT_APP"
		hx-swap="outerHTML"
	>

		<h1>403:権限がありません</h1>

		<p class="HTMX-NOTE">
			この操作には権限が必要です。<br>
			管理者に権限を付与してもらうか、別のアカウントでログインしてください。
		</p>

		<div class="FORM__FOOT">
			<a class="BTN is-ghost" href="/htmx/demo/_redirect_auth.php">一覧へ戻る</a>
		</div>

	</section>

</body>
</html>

デモ

リダイレクト/ログイン誘導を扱う

別タブで開く

解説

全体の流れ

  • 未ログイン:保護ページや保存処理に来たら HX-Redirect でログインへ
  • 保存成功:保存後は HX-Location で詳細ページへ(準SPAのまま遷移)
  • 権限不足:adminページへのアクセスは HX-Redirect で403画面へ

HTMLでやっていること

  • 全体を #REDIRECT_APP で包み、hx-boost="true" でリンク/フォームをAJAX化します。
  • hx-target="#REDIRECT_APP"hx-select="#REDIRECT_APP" で、“この箱だけ”を差し替える準SPAにします。
  • hx-swap="outerHTML" で箱ごと置換し、遷移を重ねてもDOMが崩れにくい構造にします。
  • リンクは通常の href も残すので、非JS環境でもSSRとして破綻しません。

PHPでやっていること

  • session でログイン状態(is_login)と権限(role)を管理します。
  • 未ログインなら、対象ページ側で HX-Redirect を返してログインへ誘導します。
  • 保存成功後は、HX-Location で詳細ページへ遷移させます(target/select/swapも同時指定)。
  • admin専用ページでは role を検査し、不足なら 403 に誘導します。

HX-RedirectHX-Location の使い分け

  • HX-Redirect「別ページへ移動してね」(ログイン誘導/403など“強制的に飛ばしたい”場面に強い)
  • HX-Location「次の画面を読み直して表示してね」(保存→詳細など“自然な導線で遷移”したい場面に最適)

ポイント:遷移の判断はサーバー側に寄せて、フロントは“表示するだけ”にすると実装が安定します。


よくある詰まり(エラーまとめ)

  • ヘッダが効かない:PHPで echo した後に header() を呼ぶと失敗します(必ず出力前にヘッダ送信)。
  • 無限リダイレクト:ログインページ自身にも「未ログインならログインへ」判定を入れるとループします。
  • 戻り先(next)が危険next をそのまま使うとオープンリダイレクトになり得ます(同一ドメインのみ許可するのが安全)。
  • DOMが崩れる:差し替えが入れ子になりやすいので、箱単位で hx-swap="outerHTML" 推奨。
  • セッションが不安定:ログイン時は session_regenerate_id(true) を入れると安全(固定化対策)。
  • 403の扱いが曖昧:403画面は http_response_code(403) を返しておくと意図が明確になります。

このページの著者

もちもちみかん(システムエンジニア)

社内SEとしてグループ企業向けの業務アプリを要件定義〜運用まで一気通貫で担当しています。

経験:Webアプリ/業務システム

得意:PHP・JavaScript・MySQL・CSS

個人実績:フォーム生成基盤クイズ学習プラットフォーム

詳しいプロフィールはこちら!  もちもちみかんのプロフィール

もちもちみかん0系くん
TOPへ

もちもちみかん.comとは


このサイトでは、コーディングがめんどうくさい人向けのお助けツールとして、フォームやCSSをノーコードで生成できる、
 もちもちみかん.forms
 もちもちみかん.css1
 もちもちみかん.css2
と言ったジェネレーターを用意してます。

また、このサイトを通じて、「もちもちみかん」のかわいさを普及したいとかんがえてます!