タイトル: History API
SEOタイトル: HTML5 History API(pushState / popstate / SPA ルーティング)完全ガイド
この本文には実行可能なタグ等が含まれるため、安全のためHTMLソースとして表示しています。本文データは変更していません。
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;background:#fffbea;border:1px solid #f4c430;margin:1em 0;width:100%"><tr><td style="background:#f4c430;color:#000;font-weight:bold;padding:8px">この記事の要点</td></tr><tr><td style="padding:10px"><ul><li><strong>HTML5 History API</strong> = URL を変えずにブラウザ履歴を操作する API。SPA ルーティングの土台</li><li><code>history.pushState(state, "", url)</code> で履歴を追加(ページリロード無し)</li><li><code>history.replaceState()</code> は履歴を置き換え(戻れない)</li><li>戻る/進むで <code>popstate</code> イベントが発火 → そこで View を切替</li><li><code>location.hash</code> 方式(古典)と比べ URL がきれいだが、サーバ側で <strong>SPA フォールバック</strong>(全パスを index.html に返す)が必要</li></ul></td></tr></table>
<h2>History API とは</h2>
<p><strong>HTML5 History API</strong> は、ブラウザの戻る/進む履歴を JavaScript から操作する API です。<strong>ページをリロードせずに URL を変更</strong>できるため、SPA(Single Page Application)のルーティング機構の中核として広く使われています。Vue Router、React Router、Next.js Client Router、SvelteKit などのフレームワークも内部では History API を呼んでいます。</p>
<h2>主要メソッド一覧</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>API</th><th>動作</th><th>履歴に追加</th></tr></thead>
<tbody>
<tr><td><code>history.pushState(state, title, url)</code></td><td>新エントリ追加</td><td>○</td></tr>
<tr><td><code>history.replaceState(state, title, url)</code></td><td>現エントリを置換</td><td>×</td></tr>
<tr><td><code>history.back()</code></td><td>1 つ戻る(戻るボタン同等)</td><td>-</td></tr>
<tr><td><code>history.forward()</code></td><td>1 つ進む</td><td>-</td></tr>
<tr><td><code>history.go(n)</code></td><td>n エントリ移動(負数で戻る)</td><td>-</td></tr>
<tr><td><code>history.length</code></td><td>現セッションの履歴数</td><td>-</td></tr>
<tr><td><code>history.state</code></td><td>現エントリの state オブジェクト</td><td>-</td></tr>
<tr><td><code>history.scrollRestoration</code></td><td>戻り時のスクロール位置復元(<code>auto</code>/<code>manual</code>)</td><td>-</td></tr>
</tbody>
</table>
<h2>pushState / popstate の最小例</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><nav>
<a href="/home" data-link>Home</a>
<a href="/about" data-link>About</a>
</nav>
<main id="view">Home Page</main>
<script>
function render(path) {
document.getElementById('view').textContent = path + ' Page';
}
// リンククリックで pushState
document.querySelectorAll('[data-link]').forEach(a => {
a.addEventListener('click', (e) => {
e.preventDefault();
const url = a.getAttribute('href');
history.pushState({ path: url }, '', url);
render(url);
});
});
// 戻る/進むで popstate
window.addEventListener('popstate', (e) => {
const path = (e.state && e.state.path) || location.pathname;
render(path);
});
</script></code></pre>
<h2>pushState と replaceState の違い</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#fff3e0"><th>用途</th><th>選択</th></tr></thead>
<tbody>
<tr><td>通常のページ遷移</td><td><code>pushState</code></td></tr>
<tr><td>クエリ更新(フィルタ変更等)で戻る対象にしたくない</td><td><code>replaceState</code></td></tr>
<tr><td>ログイン後のリダイレクト(ログイン画面を履歴に残さない)</td><td><code>replaceState</code></td></tr>
<tr><td>モーダルの開閉を URL に反映</td><td>状況次第(戻るで閉じたいなら <code>pushState</code>)</td></tr>
</tbody>
</table>
<h2>location.hash 方式との比較</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// 古典: ハッシュベース(HTML5 以前)
location.hash = '#/about';
window.addEventListener('hashchange', () => {
console.log(location.hash);
});
// URL: https://example.com/#/about
// モダン: History API
history.pushState(null, '', '/about');
// URL: https://example.com/about</code></pre>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>比較</th><th>hash 方式</th><th>History API</th></tr></thead>
<tbody>
<tr><td>URL 見た目</td><td><code>#</code> 入りで不格好</td><td>通常 URL でクリーン</td></tr>
<tr><td>SEO</td><td>通常クロール対象外</td><td>SSR / プリレンダリングで対応可</td></tr>
<tr><td>サーバ設定</td><td>不要</td><td>★ 全パスを index.html へフォールバックが必要</td></tr>
<tr><td>古い IE 対応</td><td>IE6+ で動く</td><td>IE10+</td></tr>
</tbody>
</table>
<h2>SPA フォールバックの設定例</h2>
<p>History API で <code>/about</code> という URL に切り替えても、ブラウザをリロードすると<strong>サーバが本当に <code>/about</code> を返そうとして 404</strong> になります。これを防ぐためサーバで「全パスを <code>index.html</code> に返す」設定を入れます。</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-nginx"># Nginx
location / {
try_files $uri $uri/ /index.html;
}</code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-apache"># Apache (.htaccess)
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]</code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// Express
const path = require('path');
app.use(express.static('dist'));
app.get('*', (_req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});</code></pre>
<h2>URL / URLSearchParams との連携</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// 現在の URL からクエリ取得
const params = new URLSearchParams(location.search);
const page = params.get('page') || '1';
// クエリだけ書き換え(履歴に追加せず replace)
params.set('page', '2');
history.replaceState(null, '', '?' + params.toString());
// URL オブジェクトで安全に組み立て
const url = new URL(location.href);
url.pathname = '/items';
url.searchParams.set('sort', 'asc');
history.pushState(null, '', url);</code></pre>
<h2>scrollRestoration</h2>
<p>「戻る」操作で前ページのスクロール位置を復元するかをコントロールできます。SPA では自前で復元したい場合があるので <code>manual</code> にする運用が一般的です。</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
// 自前でスクロール位置を保存
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('scroll:' + location.pathname, scrollY);
});
window.addEventListener('load', () => {
const y = sessionStorage.getItem('scroll:' + location.pathname);
if (y) window.scrollTo(0, +y);
});</code></pre>
<h2>Router フレームワークの内部</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e8f5e9"><th>フレームワーク</th><th>History API の使い方</th></tr></thead>
<tbody>
<tr><td>Vue Router (createWebHistory)</td><td><code>history.pushState</code> + <code>popstate</code> リスナ</td></tr>
<tr><td>React Router (BrowserRouter)</td><td>history パッケージ経由で History API</td></tr>
<tr><td>Next.js App Router</td><td>独自 Router、Soft Navigation に History API</td></tr>
<tr><td>SvelteKit</td><td>fetch + History API + クライアントナビ</td></tr>
<tr><td>Astro View Transitions</td><td>History API + ViewTransition API</td></tr>
</tbody>
</table>
<h2>FAQ</h2>
<p><strong>Q: <code>pushState</code> の第 2 引数 title は何を入れる?</strong><br />
A: ほとんどのブラウザで無視されます。空文字 <code>""</code> で OK。<code>document.title</code> を別途代入してください。</p>
<p><strong>Q: <code>popstate</code> が pushState 直後にも発火する?</strong><br />
A: 発火しません。<code>popstate</code> は<strong>ユーザの戻る/進む操作と <code>history.back()</code> 等のメソッド呼び出し</strong>でのみ発火します。</p>
<p><strong>Q: 別オリジンに pushState できる?</strong><br />
A: できません。同一オリジン内のパス変更のみ許可。クロスオリジンは <code>location.href</code> 代入で通常遷移。</p>