目次
- はじめに
- View Transitionsのスクリプト問題
- なぜスクリプトが動かなくなるのか
- 解決パターン
- astro:after-swap と astro:page-load の使い分け
- Pagefind 全文検索の導入
- 基本構成
- ファセット検索
- 検索モーダル
- SearchAction連携
- キャッシュ設定
- インラインonclickの排除
- 改善パターン
- コンポーネントライブラリの整備
- TypeScript 型安全性の向上
- any型の排除
- コンテンツスキーマのリテラル型
- as const アサーション
- 非推奨インポートの移行
- 定数の一元管理
- その他のUX改善
- 目次のスクロール追従
- スクロールスパイ
- ページネーション
- スティッキーヘッダーのアンカーリンク
- まとめ
- この記事が含まれるシリーズ
はじめに
AstroのView Transitions(ClientRouter)は、ページ遷移をSPAのようにスムーズにする強力な機能です。しかし導入した途端、ハンバーガーメニューが開かない、検索ボタンが反応しない、スライダーが止まる……という問題に直面します。
この記事では、View Transitionsの落とし穴とその解決策、そしてUXとコード品質を改善するための実践的な手法を紹介します。
View Transitionsのスクリプト問題
なぜスクリプトが動かなくなるのか
通常のページ遷移では、ブラウザがHTMLを再パースしてすべてのスクリプトを実行します。しかしView TransitionsはページをDOM差分で更新するため、インラインスクリプトが再実行されません。
影響を受けるのは以下のような処理です。
- ハンバーガーメニューの開閉
- 検索ボタンのクリックハンドラ
- ヒーロー画像のスライダー
- 目次のスクロール追従
- YouTube埋め込みのファサードパターン
解決パターン
すべてのスクリプトを 名前付き関数に包み、astro:after-swap で再登録 するパターンに統一します。
<script>
function initHeader() {
const menuBtn = document.querySelector('[data-menu-toggle]')
menuBtn?.addEventListener('click', () => { /* ... */ })
}
// 初回実行
initHeader()
// View Transitions後に再実行
document.addEventListener('astro:after-swap', initHeader)
</script>
astro:after-swap と astro:page-load の使い分け
astro:after-swap:DOMが差し替えられた直後に発火。初回読み込みでは発火しないため、関数を直接呼ぶ必要があるastro:page-load:初回読み込み と View Transitions後の両方で発火する。初回呼び出しの記述を省略できる
YouTube埋め込みのように初回読み込みでも確実に動作させたい場合は astro:page-load が便利です。
Pagefind 全文検索の導入
静的サイトに全文検索を実装するなら Pagefind がおすすめです。ビルド時にインデックスを生成し、ブラウザ側で検索を実行するため、サーバー不要で高速です。
基本構成
{
"scripts": {
"build": "astro build && pagefind --site dist"
}
}
Astroのビルド後に Pagefind を実行し、dist/pagefind/ にインデックスを出力します。
ファセット検索
data-pagefind-filter 属性を使えば、著者・年・タグの3軸でフィルタリングできます。
<span data-pagefind-filter="author">gui</span>
<span data-pagefind-filter="year">2026</span>
<span data-pagefind-filter="tag">Astro</span>
検索モーダル
Ctrl+K ショートカットで開く検索モーダルを実装します。ゼロ結果時には記事一覧・サービスページ・お問い合わせへのリンクを表示し、ユーザーの離脱を防ぎます。
SearchAction連携
Google の構造化データ SearchAction で ?q= パラメータを定義しておけば、検索結果からサイト内検索に直接遷移できます。URLパラメータを検知して検索モーダルを自動起動する処理を追加しましょう。
キャッシュ設定
Pagefindのインデックスファイルは変更頻度が低いため、Cloudflare Pages のヘッダー設定でキャッシュを有効にします。
/pagefind/*
Cache-Control: public, max-age=604800, stale-while-revalidate=86400
インラインonclickの排除
HTMLに直接書く onclick="..." は手軽ですが、CSP(Content Security Policy)で unsafe-inline を要求する原因になります。
改善パターン
onclick を data-* 属性 + addEventListener に置き換えます。
<!-- Before -->
<button onclick="window.openSearch?.()">検索</button>
<!-- After -->
<button data-search-trigger>検索</button>
document.querySelectorAll('[data-search-trigger]').forEach(btn => {
btn.addEventListener('click', () => window.openSearch?.())
})
コンポーネントライブラリの整備
ブログ記事を書くときに使えるコンポーネントを揃えておくと、記事の表現力が上がります。
| コンポーネント | 用途 |
|---|---|
| Callout | info / warning / tip / note の4種類の注釈 |
| Timeline | イベントの時系列表示 |
| FAQ | 構造化データ対応の質問回答 |
| Gallery | Lightbox付き画像ギャラリー |
| CompareTable | ビフォーアフターの比較表 |
| ProcessFigure | ステップバイステップのプロセス図 |
| LinkCard | OGP風の外部リンクカード |
| YouTubeEmbed | ファサードパターンで遅延読み込み |
これらはすべてMarkdownのフロントマターから呼び出せるように設計しています。記事のテンプレートで data.callout があれば <Callout> を描画する、という仕組みです。
TypeScript 型安全性の向上
any型の排除
any[] → CollectionEntry<'blog'>[] のように具体的な型を指定します。IDEの補完とコンパイル時のエラー検出が効くようになり、テンプレート内のプロパティアクセスが安全になります。
コンテンツスキーマのリテラル型
type: z.enum(['info', 'warning', 'tip', 'note']).default('info')
フロントマターの値をリテラル型ユニオンで定義すると、テンプレート側で if (callout.type === 'info') のような分岐が型安全になります。
as const アサーション
定数オブジェクトに as const を付けると、プロパティが readonly になり、型推論がリテラル型になります。SITE 定数には必ず付けましょう。
非推奨インポートの移行
Astro 7 で削除予定の import { z } from 'astro:content' を import { z } from 'astro/zod' に変更しておきます。
定数の一元管理
ハードコードされた値は変更時に見落としの原因になります。以下の値を src/data/site.ts に集約しました。
| 定数 | 集約前の箇所数 |
|---|---|
| AdSense Client ID | 4ファイル |
| GA4 Measurement ID | 2箇所 |
| 広告スロットID | 4ファイル |
| ソーシャルURL(X・GitHub・Discord・Aceserver) | 17箇所 |
| 電話番号・メール・LINE | 3ファイル |
export const SITE = {
name: 'Acecore',
url: 'https://acecore.net',
ga4Id: 'G-XXXXXXXXXX',
adsenseClientId: 'ca-pub-XXXXXXXXXXXXXXXX',
social: {
x: 'https://x.com/acecore',
github: 'https://github.com/acecore-systems',
discord: 'https://discord.gg/...',
},
} as const
その他のUX改善
目次のスクロール追従
IntersectionObserver でコンテンツの見出しを監視し、アクティブな見出しをサイドバーの目次にハイライトします。scrollIntoView({ block: 'nearest', behavior: 'smooth' }) で目次自体もスクロールさせるのがポイントです。
スクロールスパイ
サービスページのようなシングルページ構成では、IntersectionObserver でナビゲーションのアクティブ項目を自動追従させます。
ページネーション
6記事ごとの自動ページ分割、省略記号付きナビゲーション(1 2 ... 9 10)、「← 前へ」「次へ →」テキストリンクを実装します。ページネーションのロジックは src/utils/pagination.ts に共通化しましょう。
スティッキーヘッダーのアンカーリンク
スティッキーヘッダーがあると、アンカーリンクで飛んだ先がヘッダーに隠れてしまいます。UnoCSS の preflight で以下を設定して解消します。
[id] { scroll-margin-top: 5rem; }
html { scroll-behavior: smooth; }
まとめ
View Transitionsを使うなら、スクリプトの初期化パターンを統一する ことが最重要です。astro:after-swap / astro:page-load の使い分けを理解し、すべてのインタラクションをテストしましょう。
コード品質の面では、TypeScriptの型安全性と定数の一元管理が長期的な保守性に大きく貢献します。最初は面倒に感じますが、IDEの補完が効くようになる恩恵は日々の開発で実感できます。
この記事が含まれるシリーズ
この記事は「Astroサイトの品質改善ガイド」シリーズの一部です。パフォーマンス・SEO・アクセシビリティの改善についても個別の記事で紹介しています。
UX改善の進め方
問題の発見
View Transitions導入後の動作不良をリストアップ。
パターンの統一
すべてのスクリプトを統一された初期化パターンに変換。
検索の実装
Pagefindで全文検索を導入し、導線を整備。
型安全の確保
any型の排除・定数一元管理で保守性を向上。
- ページ遷移後にハンバーガーメニューが動かない
- サイト内検索なし
- any型やハードコード定数が散在
- インラインonclickでCSP違反リスク
- astro:after-swapで全スクリプトが正常動作
- Pagefindで3軸フィルタ付き全文検索
- TypeScript型安全・定数一元管理
- addEventListener + data属性でCSP準拠
View Transitionsなしでもこの改善は有効ですか?
Pagefindはどのくらいのサイト規模まで対応できますか?
TypeScriptの型エラーは無視しても動きますか?
Gui
Acecore 代表。システム開発・Web制作・インフラ運用からIT教育まで幅広く手がけるエンジニア。 技術で人と組織の課題を解きほぐすのが好き。