htmx の history 拡張で快適な SPA ライク体験を実現

htmx を使った Web アプリケーション開発で、最も感動的な瞬間の一つが「history 拡張」を実装した時です。従来のマルチページアプリケーションの制約から解放され、まるで SPA のような滑らかなユーザー体験を実現できるからです。
この記事では、htmx の history 拡張機能を徹底的に解説し、実際のプロジェクトで即座に活用できる実践的なテクニックをお伝えします。初心者の方でも理解しやすいように、段階的に進めていきましょう。
htmx の history 拡張とは
htmx の history 拡張は、ブラウザの履歴管理を自動化し、SPA のようなページ遷移体験を提供する機能です。従来の htmx では、コンテンツの更新時にブラウザの URL が変わらず、ユーザーが「戻る」ボタンを押すと予期しない動作をすることがありました。
history 拡張を導入することで、以下のような体験が実現できます:
- ページ遷移時に URL が自動的に更新される
- ブラウザの戻る・進むボタンが正常に動作する
- ページのタイトルが動的に変更される
- ブックマークや共有リンクが正常に機能する
従来の htmx と history 拡張の違い
従来の htmx では、以下のような制限がありました:
html<!-- 従来のhtmx(history拡張なし) -->
<div hx-get="/products" hx-target="#content">
商品一覧を見る
</div>
この場合、コンテンツは更新されますが、URL は変わらず、ブラウザの履歴にも記録されません。
history 拡張を使用すると:
html<!-- history拡張を使用 -->
<div
hx-get="/products"
hx-target="#content"
hx-push-url="true"
hx-history="true"
>
商品一覧を見る
</div>
これにより、URL が/products
に更新され、ブラウザの履歴にも正しく記録されます。
基本的なセットアップと設定
htmx の history 拡張を始めるには、まず適切なセットアップが必要です。最新の htmx では、history 拡張が標準で含まれているため、追加のライブラリは不要です。
HTML の基本構造
まず、基本的な HTML 構造を準備しましょう:
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>htmx History 拡張デモ</title>
<!-- htmxの読み込み -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- history拡張の読み込み -->
<script src="https://unpkg.com/htmx.org@1.9.10/ext/history.js"></script>
</head>
<body>
<!-- ナビゲーション -->
<nav>
<a
href="/"
hx-get="/"
hx-target="#main"
hx-push-url="true"
>ホーム</a
>
<a
href="/about"
hx-get="/about"
hx-target="#main"
hx-push-url="true"
>会社概要</a
>
<a
href="/products"
hx-get="/products"
hx-target="#main"
hx-push-url="true"
>商品一覧</a
>
</nav>
<!-- メインコンテンツエリア -->
<main id="main">
<h1>ようこそ</h1>
<p>htmxのhistory拡張を体験してください。</p>
</main>
</body>
</html>
サーバーサイドの準備
Node.js と Express を使用した基本的なサーバー設定例:
javascriptconst express = require('express');
const app = express();
const port = 3000;
// 静的ファイルの提供
app.use(express.static('public'));
// 各ページのルート
app.get('/', (req, res) => {
res.send(`
<h1>ホームページ</h1>
<p>これはホームページのコンテンツです。</p>
<a href="/about" hx-get="/about" hx-target="#main" hx-push-url="true">
会社概要へ
</a>
`);
});
app.get('/about', (req, res) => {
res.send(`
<h1>会社概要</h1>
<p>私たちの会社について紹介します。</p>
<a href="/products" hx-get="/products" hx-target="#main" hx-push-url="true">
商品一覧へ
</a>
`);
});
app.get('/products', (req, res) => {
res.send(`
<h1>商品一覧</h1>
<ul>
<li>商品A</li>
<li>商品B</li>
<li>商品C</li>
</ul>
<a href="/" hx-get="/" hx-target="#main" hx-push-url="true">
ホームへ戻る
</a>
`);
});
app.listen(port, () => {
console.log(
`サーバーが起動しました: http://localhost:${port}`
);
});
ページ遷移の実装方法
history 拡張を使ったページ遷移の実装には、いくつかの重要な属性があります。それぞれの役割と使い方を詳しく見ていきましょう。
基本的な属性の説明
html<!-- 基本的なhistory拡張の実装 -->
<div
hx-get="/products"
hx-target="#main"
hx-push-url="true"
hx-history="true"
>
商品一覧を表示
</div>
各属性の役割:
hx-get
: リクエストする URLhx-target
: 更新対象の要素hx-push-url
: URL を履歴に追加するかどうかhx-history
: history 拡張を有効にするかどうか
動的な URL 生成
JavaScript を使って動的に URL を生成する方法:
html<!-- 動的URL生成の例 -->
<button onclick="loadProduct(123)">商品詳細を見る</button>
<script>
function loadProduct(productId) {
htmx.ajax('GET', `/products/${productId}`, {
target: '#main',
pushUrl: true,
});
}
</script>
フォーム送信での history 管理
フォーム送信時にも history 拡張を活用できます:
html<!-- 検索フォームの例 -->
<form
hx-post="/search"
hx-target="#results"
hx-push-url="true"
hx-history="true"
>
<input
type="text"
name="query"
placeholder="検索キーワード"
/>
<button type="submit">検索</button>
</form>
<div id="results">
<!-- 検索結果がここに表示される -->
</div>
ブラウザの戻る・進むボタンへの対応
ブラウザの戻る・進むボタンが正常に動作するようにするには、適切なイベントハンドリングが必要です。
基本的な戻る・進むボタンの対応
javascript// history拡張のイベントリスナー設定
document.addEventListener(
'htmx:historyRestore',
function (evt) {
console.log('履歴が復元されました:', evt.detail);
// 必要に応じて追加の処理を実行
updatePageTitle(evt.detail.path);
highlightCurrentNav(evt.detail.path);
}
);
// ページタイトルの更新
function updatePageTitle(path) {
const titles = {
'/': 'ホーム',
'/about': '会社概要',
'/products': '商品一覧',
};
document.title = titles[path] || 'デフォルトタイトル';
}
// 現在のナビゲーションをハイライト
function highlightCurrentNav(path) {
// すべてのナビリンクからactiveクラスを削除
document.querySelectorAll('nav a').forEach((link) => {
link.classList.remove('active');
});
// 現在のパスに一致するリンクにactiveクラスを追加
const currentLink = document.querySelector(
`nav a[href="${path}"]`
);
if (currentLink) {
currentLink.classList.add('active');
}
}
カスタム履歴管理
より細かい制御が必要な場合のカスタム実装:
javascript// カスタム履歴管理の例
class CustomHistoryManager {
constructor() {
this.currentPath = window.location.pathname;
this.setupEventListeners();
}
setupEventListeners() {
// htmxの履歴復元イベント
document.addEventListener(
'htmx:historyRestore',
(evt) => {
this.handleHistoryRestore(evt.detail);
}
);
// htmxの履歴追加イベント
document.addEventListener('htmx:historyPush', (evt) => {
this.handleHistoryPush(evt.detail);
});
}
handleHistoryRestore(detail) {
console.log('履歴復元:', detail);
this.currentPath = detail.path;
this.updateUI();
}
handleHistoryPush(detail) {
console.log('履歴追加:', detail);
this.currentPath = detail.path;
this.updateUI();
}
updateUI() {
// UIの更新処理
this.updateBreadcrumb();
this.updatePageTitle();
this.updateNavigation();
}
updateBreadcrumb() {
const breadcrumb =
document.getElementById('breadcrumb');
if (breadcrumb) {
breadcrumb.innerHTML = this.generateBreadcrumbHTML();
}
}
generateBreadcrumbHTML() {
const paths = this.currentPath
.split('/')
.filter((p) => p);
let html =
'<a href="/" hx-get="/" hx-target="#main" hx-push-url="true">ホーム</a>';
let currentPath = '';
paths.forEach((path) => {
currentPath += `/${path}`;
html += ` > <a href="${currentPath}" hx-get="${currentPath}" hx-target="#main" hx-push-url="true">${path}</a>`;
});
return html;
}
}
// 初期化
const historyManager = new CustomHistoryManager();
動的なタイトル変更
ページ遷移時に動的にタイトルを変更することで、ユーザーエクスペリエンスを向上させることができます。
基本的なタイトル変更
javascript// 基本的なタイトル変更の実装
document.addEventListener(
'htmx:historyRestore',
function (evt) {
const path = evt.detail.path;
updateTitle(path);
}
);
function updateTitle(path) {
const titleMap = {
'/': 'ホーム - マイサイト',
'/about': '会社概要 - マイサイト',
'/products': '商品一覧 - マイサイト',
'/contact': 'お問い合わせ - マイサイト',
};
const newTitle = titleMap[path] || 'マイサイト';
document.title = newTitle;
}
動的コンテンツに基づくタイトル変更
サーバーから返されるデータに基づいてタイトルを動的に変更する方法:
html<!-- 商品詳細ページの例 -->
<div
hx-get="/products/123"
hx-target="#main"
hx-push-url="true"
hx-swap="innerHTML"
>
商品詳細を見る
</div>
サーバーサイド(Node.js/Express):
javascriptapp.get('/products/:id', (req, res) => {
const productId = req.params.id;
// 商品データを取得(実際の実装ではデータベースから取得)
const product = getProductById(productId);
// タイトルを含むHTMLを返す
res.send(`
<h1>${product.name}</h1>
<p>${product.description}</p>
<script>
// タイトルを動的に更新
document.title = '${product.name} - 商品詳細 - マイサイト';
</script>
`);
});
メタタグの動的更新
SEO 対策としてメタタグも動的に更新する方法:
javascript// メタタグの動的更新
function updateMetaTags(path, data) {
// タイトルの更新
document.title = data.title || 'デフォルトタイトル';
// メタディスクリプションの更新
const metaDescription = document.querySelector(
'meta[name="description"]'
);
if (metaDescription && data.description) {
metaDescription.setAttribute(
'content',
data.description
);
}
// OGPタグの更新
const ogTitle = document.querySelector(
'meta[property="og:title"]'
);
if (ogTitle && data.title) {
ogTitle.setAttribute('content', data.title);
}
const ogDescription = document.querySelector(
'meta[property="og:description"]'
);
if (ogDescription && data.description) {
ogDescription.setAttribute('content', data.description);
}
}
ローディング状態の管理
ページ遷移時のローディング状態を適切に管理することで、ユーザーに処理状況を伝えることができます。
基本的なローディング表示
html<!-- ローディングインジケーター -->
<div
id="loading"
class="loading-spinner"
style="display: none;"
>
<div class="spinner"></div>
<p>読み込み中...</p>
</div>
<style>
.loading-spinner {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
htmx イベントを使ったローディング管理
javascript// htmxのイベントを使ったローディング管理
document.addEventListener(
'htmx:beforeRequest',
function (evt) {
showLoading();
}
);
document.addEventListener(
'htmx:afterRequest',
function (evt) {
hideLoading();
}
);
document.addEventListener(
'htmx:responseError',
function (evt) {
hideLoading();
showError('リクエストに失敗しました');
}
);
function showLoading() {
const loading = document.getElementById('loading');
if (loading) {
loading.style.display = 'block';
}
}
function hideLoading() {
const loading = document.getElementById('loading');
if (loading) {
loading.style.display = 'none';
}
}
function showError(message) {
// エラーメッセージの表示
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
document.body.appendChild(errorDiv);
// 3秒後に自動削除
setTimeout(() => {
errorDiv.remove();
}, 3000);
}
スケルトンローディング
より洗練されたローディング体験を提供するスケルトンローディング:
html<!-- スケルトンローディングの例 -->
<div
id="skeleton"
class="skeleton-loading"
style="display: none;"
>
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
</div>
</div>
<style>
.skeleton-loading {
padding: 20px;
}
.skeleton-header {
height: 32px;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 4px;
margin-bottom: 20px;
}
.skeleton-line {
height: 16px;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 4px;
margin-bottom: 12px;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
エラーハンドリング
htmx の history 拡張を使用する際のエラーハンドリングは、ユーザー体験を左右する重要な要素です。
基本的なエラーハンドリング
javascript// 基本的なエラーハンドリング
document.addEventListener(
'htmx:responseError',
function (evt) {
console.error('HTMX エラー:', evt.detail);
const status = evt.detail.xhr.status;
const path = evt.detail.pathInfo.requestPath;
handleError(status, path);
}
);
function handleError(status, path) {
const errorMessages = {
404: 'ページが見つかりませんでした',
500: 'サーバーエラーが発生しました',
403: 'アクセスが拒否されました',
401: '認証が必要です',
};
const message =
errorMessages[status] ||
'予期しないエラーが発生しました';
// エラーページを表示
showErrorPage(message, status);
}
function showErrorPage(message, status) {
const main = document.getElementById('main');
if (main) {
main.innerHTML = `
<div class="error-page">
<h1>エラー ${status}</h1>
<p>${message}</p>
<button onclick="goHome()">ホームに戻る</button>
</div>
`;
}
}
function goHome() {
htmx.ajax('GET', '/', {
target: '#main',
pushUrl: true,
});
}
ネットワークエラーの処理
javascript// ネットワークエラーの処理
document.addEventListener('htmx:sendError', function (evt) {
console.error('ネットワークエラー:', evt.detail);
showNetworkError();
});
function showNetworkError() {
const errorDiv = document.createElement('div');
errorDiv.className = 'network-error';
errorDiv.innerHTML = `
<div class="error-content">
<h3>ネットワークエラー</h3>
<p>インターネット接続を確認してください</p>
<button onclick="retryLastRequest()">再試行</button>
</div>
`;
document.body.appendChild(errorDiv);
}
function retryLastRequest() {
// 最後のリクエストを再試行
const lastRequest = htmx.lastRequest;
if (lastRequest) {
htmx.ajax(lastRequest.method, lastRequest.url, {
target: lastRequest.target,
pushUrl: true,
});
}
// エラーメッセージを削除
const errorDiv = document.querySelector('.network-error');
if (errorDiv) {
errorDiv.remove();
}
}
タイムアウト処理
javascript// タイムアウト処理の設定
document.addEventListener(
'htmx:beforeRequest',
function (evt) {
// タイムアウトを設定(5秒)
evt.detail.xhr.timeout = 5000;
}
);
document.addEventListener('htmx:timeout', function (evt) {
console.error('リクエストがタイムアウトしました');
showTimeoutError();
});
function showTimeoutError() {
const main = document.getElementById('main');
if (main) {
main.innerHTML = `
<div class="timeout-error">
<h2>タイムアウトエラー</h2>
<p>リクエストが時間内に完了しませんでした</p>
<button onclick="retryRequest()">再試行</button>
</div>
`;
}
}
パフォーマンス最適化のコツ
htmx の history 拡張を使用する際のパフォーマンス最適化について、実践的なテクニックを紹介します。
キャッシュ戦略
javascript// キャッシュ戦略の実装
class HTMXCache {
constructor() {
this.cache = new Map();
this.maxSize = 50; // 最大キャッシュ数
}
set(key, data) {
// キャッシュサイズを制限
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
data: data,
timestamp: Date.now(),
});
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
// 5分間のキャッシュ有効期限
const fiveMinutes = 5 * 60 * 1000;
if (Date.now() - item.timestamp > fiveMinutes) {
this.cache.delete(key);
return null;
}
return item.data;
}
clear() {
this.cache.clear();
}
}
const htmxCache = new HTMXCache();
// キャッシュを使ったリクエスト
document.addEventListener(
'htmx:beforeRequest',
function (evt) {
const url = evt.detail.pathInfo.requestPath;
const cachedData = htmxCache.get(url);
if (cachedData) {
// キャッシュからデータを取得
evt.preventDefault();
evt.detail.target.innerHTML = cachedData;
return;
}
}
);
document.addEventListener(
'htmx:afterRequest',
function (evt) {
const url = evt.detail.pathInfo.requestPath;
const response = evt.detail.target.innerHTML;
// レスポンスをキャッシュに保存
htmxCache.set(url, response);
}
);
遅延読み込み
html<!-- 遅延読み込みの実装 -->
<div
class="lazy-content"
hx-get="/api/products"
hx-trigger="intersect once"
hx-target="this"
hx-swap="innerHTML"
>
<div class="loading-placeholder">
<div class="spinner"></div>
<p>コンテンツを読み込み中...</p>
</div>
</div>
<style>
.lazy-content {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.loading-placeholder {
text-align: center;
color: #666;
}
</style>
プリロード戦略
javascript// プリロード戦略の実装
class PreloadManager {
constructor() {
this.preloaded = new Set();
this.setupPreloadTriggers();
}
setupPreloadTriggers() {
// ホバー時にプリロード
document.addEventListener('mouseover', (evt) => {
const link = evt.target.closest('[hx-get]');
if (
link &&
!this.preloaded.has(link.getAttribute('hx-get'))
) {
this.preload(link.getAttribute('hx-get'));
}
});
}
preload(url) {
if (this.preloaded.has(url)) return;
// プリロードリクエスト
fetch(url, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
})
.then((response) => {
if (response.ok) {
this.preloaded.add(url);
console.log(`プリロード完了: ${url}`);
}
})
.catch((error) => {
console.warn(`プリロード失敗: ${url}`, error);
});
}
}
const preloadManager = new PreloadManager();
メモリリークの防止
javascript// メモリリーク防止の実装
class MemoryManager {
constructor() {
this.eventListeners = new Map();
this.setupCleanup();
}
setupCleanup() {
// ページ離脱時のクリーンアップ
window.addEventListener('beforeunload', () => {
this.cleanup();
});
// 定期的なクリーンアップ
setInterval(() => {
this.cleanup();
}, 300000); // 5分ごと
}
cleanup() {
// イベントリスナーのクリーンアップ
this.eventListeners.forEach((listener, element) => {
if (!document.contains(element)) {
element.removeEventListener('click', listener);
this.eventListeners.delete(element);
}
});
// 不要なDOM要素の削除
const orphanedElements = document.querySelectorAll(
'[data-htmx-temp]'
);
orphanedElements.forEach((element) => {
element.remove();
});
}
addEventListener(element, event, listener) {
element.addEventListener(event, listener);
this.eventListeners.set(element, listener);
}
}
const memoryManager = new MemoryManager();
実践的な使用例
実際のプロジェクトで htmx の history 拡張を活用する実践的な例を紹介します。
ブログサイトの実装例
html<!-- ブログサイトのメインレイアウト -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>htmx ブログ</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/htmx.org@1.9.10/ext/history.js"></script>
<style>
.blog-layout {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.blog-header {
background: #f8f9fa;
padding: 20px;
margin-bottom: 30px;
border-radius: 8px;
}
.blog-nav {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.blog-nav a {
text-decoration: none;
color: #333;
padding: 10px 15px;
border-radius: 4px;
transition: background-color 0.3s;
}
.blog-nav a:hover {
background-color: #e9ecef;
}
.blog-nav a.active {
background-color: #007bff;
color: white;
}
.blog-content {
min-height: 400px;
}
.article-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
transition: box-shadow 0.3s;
}
.article-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="blog-layout">
<header class="blog-header">
<h1>htmx ブログ</h1>
<nav class="blog-nav">
<a
href="/"
hx-get="/"
hx-target="#content"
hx-push-url="true"
class="active"
>ホーム</a
>
<a
href="/articles"
hx-get="/articles"
hx-target="#content"
hx-push-url="true"
>記事一覧</a
>
<a
href="/categories"
hx-get="/categories"
hx-target="#content"
hx-push-url="true"
>カテゴリー</a
>
<a
href="/about"
hx-get="/about"
hx-target="#content"
hx-push-url="true"
>このブログについて</a
>
</nav>
</header>
<main id="content" class="blog-content">
<h2>ようこそ</h2>
<p>htmxのhistory拡張を使ったブログサイトです。</p>
</main>
</div>
<script>
// ナビゲーションのアクティブ状態管理
document.addEventListener(
'htmx:historyRestore',
function (evt) {
updateNavigation(evt.detail.path);
}
);
function updateNavigation(path) {
// すべてのナビリンクからactiveクラスを削除
document
.querySelectorAll('.blog-nav a')
.forEach((link) => {
link.classList.remove('active');
});
// 現在のパスに一致するリンクにactiveクラスを追加
const currentLink = document.querySelector(
`.blog-nav a[href="${path}"]`
);
if (currentLink) {
currentLink.classList.add('active');
}
}
</script>
</body>
</html>
E コマースサイトの実装例
html<!-- 商品一覧ページ -->
<div class="products-page">
<div class="filters">
<select
hx-get="/products"
hx-target="#products-grid"
hx-push-url="true"
hx-trigger="change"
name="category"
>
<option value="">すべてのカテゴリー</option>
<option value="electronics">電子機器</option>
<option value="clothing">衣類</option>
<option value="books">書籍</option>
</select>
<select
hx-get="/products"
hx-target="#products-grid"
hx-push-url="true"
hx-trigger="change"
name="sort"
>
<option value="name">名前順</option>
<option value="price">価格順</option>
<option value="newest">新着順</option>
</select>
</div>
<div id="products-grid" class="products-grid">
<!-- 商品がここに表示される -->
</div>
<div class="pagination">
<button
hx-get="/products?page=1"
hx-target="#products-grid"
hx-push-url="true"
disabled
>
前へ
</button>
<span>ページ 1 / 10</span>
<button
hx-get="/products?page=2"
hx-target="#products-grid"
hx-push-url="true"
>
次へ
</button>
</div>
</div>
<style>
.products-page {
padding: 20px;
}
.filters {
display: flex;
gap: 15px;
margin-bottom: 30px;
}
.filters select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.products-grid {
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(250px, 1fr)
);
gap: 20px;
margin-bottom: 30px;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
text-align: center;
transition: transform 0.2s;
}
.product-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
}
.pagination button {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
管理画面の実装例
html<!-- 管理画面のレイアウト -->
<div class="admin-layout">
<aside class="sidebar">
<nav class="admin-nav">
<a
href="/admin/dashboard"
hx-get="/admin/dashboard"
hx-target="#admin-content"
hx-push-url="true"
class="nav-item active"
>
<span class="icon">📊</span>
ダッシュボード
</a>
<a
href="/admin/users"
hx-get="/admin/users"
hx-target="#admin-content"
hx-push-url="true"
class="nav-item"
>
<span class="icon">👥</span>
ユーザー管理
</a>
<a
href="/admin/products"
hx-get="/admin/products"
hx-target="#admin-content"
hx-push-url="true"
class="nav-item"
>
<span class="icon">📦</span>
商品管理
</a>
<a
href="/admin/orders"
hx-get="/admin/orders"
hx-target="#admin-content"
hx-push-url="true"
class="nav-item"
>
<span class="icon">🛒</span>
注文管理
</a>
</nav>
</aside>
<main class="admin-main">
<header class="admin-header">
<h1 id="page-title">ダッシュボード</h1>
<div class="admin-actions">
<button class="btn-primary">新規作成</button>
<button class="btn-secondary">設定</button>
</div>
</header>
<div id="admin-content" class="admin-content">
<!-- 管理画面のコンテンツがここに表示される -->
</div>
</main>
</div>
<style>
.admin-layout {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 250px;
background: #2c3e50;
color: white;
padding: 20px 0;
}
.admin-nav {
display: flex;
flex-direction: column;
}
.nav-item {
display: flex;
align-items: center;
padding: 15px 20px;
color: white;
text-decoration: none;
transition: background-color 0.3s;
}
.nav-item:hover {
background-color: #34495e;
}
.nav-item.active {
background-color: #3498db;
}
.nav-item .icon {
margin-right: 10px;
font-size: 18px;
}
.admin-main {
flex: 1;
background: #f8f9fa;
}
.admin-header {
background: white;
padding: 20px;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-content {
padding: 20px;
}
.btn-primary {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.btn-secondary {
background: #95a5a6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
</style>
<script>
// 管理画面のナビゲーション管理
document.addEventListener(
'htmx:historyRestore',
function (evt) {
updateAdminNavigation(evt.detail.path);
updatePageTitle(evt.detail.path);
}
);
function updateAdminNavigation(path) {
// ナビゲーションのアクティブ状態を更新
document
.querySelectorAll('.nav-item')
.forEach((item) => {
item.classList.remove('active');
});
const currentNav = document.querySelector(
`.nav-item[href="${path}"]`
);
if (currentNav) {
currentNav.classList.add('active');
}
}
function updatePageTitle(path) {
const titles = {
'/admin/dashboard': 'ダッシュボード',
'/admin/users': 'ユーザー管理',
'/admin/products': '商品管理',
'/admin/orders': '注文管理',
};
const title = titles[path] || '管理画面';
document.getElementById('page-title').textContent =
title;
document.title = `${title} - 管理画面`;
}
</script>
まとめ
htmx の history 拡張機能は、従来のマルチページアプリケーションに SPA のような滑らかなユーザー体験をもたらす革新的な技術です。この記事で紹介した実践的なテクニックを活用することで、ユーザーが感動する Web アプリケーションを構築できるでしょう。
重要なポイントの振り返り
- 適切なセットアップ: history 拡張の正しい読み込みと設定が成功の鍵です
- ブラウザ履歴の管理: 戻る・進むボタンが正常に動作するよう、イベントハンドリングを適切に実装しましょう
- 動的なタイトル変更: SEO とユーザビリティの両方を向上させる重要な要素です
- ローディング状態の管理: ユーザーに処理状況を適切に伝えることで、信頼性を向上させます
- エラーハンドリング: 予期しない状況でもユーザーを迷わせない配慮が大切です
- パフォーマンス最適化: キャッシュ戦略や遅延読み込みで、高速な体験を提供しましょう
次のステップ
htmx の history 拡張をマスターしたら、以下のような発展的な機能にも挑戦してみてください:
- WebSocket との連携: リアルタイム更新機能の実装
- フォームバリデーション: クライアントサイドとサーバーサイドの連携
- アニメーション: ページ遷移時のスムーズなアニメーション効果
- オフライン対応: Service Worker との連携
htmx の history 拡張は、Web 開発の新しい可能性を開く技術です。この記事で学んだ知識を活かして、ユーザーが愛用する Web アプリケーションを作り上げてください。
関連リンク
- article
Jotai を 120%活用するユーティリティライブラリ(jotai-immer, jotai-xstate, jotai-form)の世界
- article
TypeScript × Vitest:次世代テストランナーの導入から活用まで
- article
スマホでも滑らか!React × アニメーションのレスポンシブ対応術
- article
Zustand でリストデータと詳細データを効率よく管理する方法
- article
Nuxt で API 連携:fetch, useAsyncData, useFetch の違いと使い分け - 記事構成案
- article
htmx の history 拡張で快適な SPA ライク体験を実現
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来