高度なルーティング

プロダクションレベルのアプリケーション開発に必要な、SvelteKitの高度なルーティング機能を学びます。ルートグループによる論理的な整理、複雑なレイアウト構造、動的なナビゲーション制御などを習得します。

ルートグループ

URLに影響を与えずに、ルートを論理的に整理する機能です。異なるレイアウトや認証要件を持つページをグループ化し、コードの保守性を向上させます。例えば、管理画面、マーケティングサイト、認証ページなどを別々のグループとして管理できます。

ルートグループの仕組み

以下の図は、括弧で囲まれたディレクトリ名がURLに反映されない仕組みを示しています。

ダイアグラムを読み込み中...
レイアウトのリセット

ルートグループで@記号を使用すると、レイアウトの継承をリセットできます。 例:(app)@/は親レイアウトを無視して、ルートレイアウトから直接継承します。

詳しくは 特殊ファイルシステム - レイアウトのリセット を参照してください。

(group) - URLに影響しないグループ化

括弧()で囲まれたディレクトリは、URLパスに影響を与えずにルートを論理的にグループ化できます。

ルートグループのディレクトリ構造例です。(app)(marketing)(auth)はそれぞれ異なるレイアウトを持ちますが、URLには表れません。例えば(app)/dashboardはURL上では/dashboardとなり、グループ名が省略されます。

src/routes/
├── (app)/                    # URLパスには含まれない
│   ├── +layout.svelte        # アプリケーションレイアウト
│   ├── dashboard/            # /dashboard
│   │   └── +page.svelte
│   └── settings/             # /settings
│       └── +page.svelte
├── (marketing)/              # URLパスには含まれない
│   ├── +layout.svelte        # マーケティングサイトレイアウト
│   ├── +page.svelte          # / (ホームページ)
│   └── pricing/              # /pricing
│       └── +page.svelte
└── (auth)/                   # URLパスには含まれない
    ├── +layout.svelte        # 認証用レイアウト
    ├── login/                # /login
    └── register/             # /register
null

認証が必要なルートグループ

(protected)グループのレイアウトLoad関数で認証チェックを実装した例です。locals.getUser()でユーザー情報を取得し、未認証の場合はログインページへリダイレクトします。redirectToパラメータで元のURLを保持し、ログイン後に元のページに戻れるようにしています。

// src/routes/(protected)/+layout.server.ts
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals, url }) => {
  const user = await locals.getUser();
  
  if (!user) {
    // 未認証の場合はログインページへリダイレクト
    throw redirect(302, `/login?redirectTo=${url.pathname}`);
  }
  
  return {
    user
  };
};
typescript

認証済みユーザー向けのレイアウトコンポーネントです。Load関数から受け取ったdata.userを使って、ユーザーメニューを表示しています。このグループ内のすべてのページで、このナビゲーションとレイアウトが適用されます。

<!-- src/routes/(protected)/+layout.svelte -->
<script lang="ts">
  import type { LayoutData } from './$types';
  import UserMenu from '$lib/components/UserMenu.svelte';
  import type { Snippet } from 'svelte';
  
  let { data, children }: { data: LayoutData; children?: Snippet } = $props();
</script>

<div class="app-layout">
  <header>
    <nav>
      <a href="/dashboard">Dashboard</a>
      <a href="/profile">Profile</a>
      <a href="/settings">Settings</a>
    </nav>
    <UserMenu user={data.user} />
  </header>
  
  <main>
    {@render children?.()}
  </main>
</div>
svelte

管理者専用ルートグループ

管理者権限をチェックするグループの実装例です。user.isAdminフラグを確認し、管理者でない場合は403エラー(アクセス拒否)をスローします。このエラーは最も近い+error.svelteでキャッチされ、適切なエラーメッセージが表示されます。

// src/routes/(admin)/+layout.server.ts
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
  const user = await locals.getUser();
  
  if (!user?.isAdmin) {
    throw error(403, {
      message: 'Access denied: Admin only',
      code: 'FORBIDDEN'
    });
  }
  
  return {
    user
  };
};
typescript

プログラマティックナビゲーション

JavaScriptコードから直接ページ遷移を制御する機能です。ユーザーの操作に応じた動的なナビゲーション、フォーム送信後の遷移、条件に基づくリダイレクトなど、プログラムロジックでページ遷移を管理できます。

ナビゲーションフローの全体像

以下の図は、beforeNavigate/afterNavigateフックがどのタイミングで実行されるかを示しています。

ダイアグラムを読み込み中...

goto関数

$app/navigationから提供される関数で、JavaScriptから直接ページ遷移を実行します。フォーム送信後の遷移、条件に基づくリダイレクト、プログラムロジックによる動的なナビゲーションなどに使用します。

フォーム送信後に作成した記事ページへ遷移する実装例です。/api/postsに記事データをPOSTし、レスポンスで返される記事IDを使って/posts/123のような動的ルートへ遷移します。replaceState: falseで履歴に残し、invalidateAll: trueですべてのデータを再取得することで、新しい記事が確実に表示されます。

<script lang="ts">
  import { goto } from '$app/navigation';
  
  async function handleSubmit(event: SubmitEvent) {
    event.preventDefault();
    
    const formData = new FormData(event.target as HTMLFormElement);
    const response = await fetch('/api/posts', {
      method: 'POST',
      body: formData
    });
    
    if (response.ok) {
      const { id } = await response.json();
      
      // 作成した記事ページへ遷移
      await goto(`/posts/${id}`, {
        replaceState: false,     // 履歴を残す
        invalidateAll: true      // 全データを再取得
      });
    }
  }
</script>

<form on:submit={handleSubmit}>
  <!-- フォーム内容 -->
</form>
svelte
SPA出身者向け:SvelteKitのナビゲーション動作

SvelteKitのプログラマティックナビゲーション(goto(), replaceState(), pushState())は、すべてSPA的なクライアントサイドルーティングとして動作します。

重要なポイント:

  • ページ全体のリロードは発生しません
  • ブラウザのURLは変化します
  • 必要な部分のみが更新されます(仮想DOMではなく、Svelteのコンパイラによる効率的な更新)

初回アクセスとナビゲーションの違い:

// ブラウザで直接アクセス(またはF5リロード)
GET /aboutSSR/SSGされた完全なHTMLが返される

// goto()でのナビゲーション(SvelteKitアプリ起動後)
await goto('/about');
GET /about/__data.jsonJSONデータのみ取得
クライアントサイドでレンダリング
必要なJSチャンクを動的にロード初めてのページの場合
typescript

3つのナビゲーション関数の動作:

// すべて同じSPA的な動作(違いは履歴管理のみ)
await goto('/about');           // 新しい履歴エントリを追加
await replaceState('/about', {}); // 現在の履歴を置き換え
await pushState('/about', {});    // 明示的に履歴を追加(goto()と同じ)
typescript

これはSvelteKitのハイブリッドアプローチの核心です。

  • 初回アクセス: SEOとパフォーマンスのためにSSR/SSG
  • その後のナビゲーション: SPAのような高速な遷移

beforeNavigate / afterNavigate

ページ遷移の前後にフック処理を実行する関数です。beforeNavigateは遷移前の確認やキャンセル、afterNavigateは遷移後のスクロール位置リセットやアナリティクス送信などに使用します。

ナビゲーションフックの実装例です。beforeNavigateで未保存の変更がある場合に確認ダイアログを表示し、ユーザーがキャンセルを選択すればcancel()で遷移を中止します。afterNavigateでは遷移完了後にページトップへスクロールし、Google Analyticsにページビューイベントを送信しています。

<script lang="ts">
  import { beforeNavigate, afterNavigate } from '$app/navigation';
  
  beforeNavigate(({ from, to, cancel }) => {
    // 未保存の変更がある場合
    if (hasUnsavedChanges && !confirm('変更を破棄しますか?')) {
      cancel();
    }
  });
  
  afterNavigate(() => {
    // ページ遷移後にスクロール位置をリセット
    window.scrollTo(0, 0);
    
    // アナリティクスにページビューを送信
    gtag('event', 'page_view');
  });
</script>
svelte

prefetch / prefetchRoutes

ページデータを事前に読み込むことで、実際の遷移時の表示を高速化する機能です。重要なページやユーザーが次に訪れる可能性の高いページを先読みすることで、体感速度を向上させます。

プリフェッチの実装例です。onMountで重要な3つのページ(ダッシュボード、プロフィール、設定)を一括で事前読み込みし、初回アクセス時の表示を高速化します。リンクのマウスオーバー時にprefetch()を呼び出すことで、クリック前にページデータの読み込みを開始し、瞬時にページ遷移できるようにしています。

<script lang="ts">
  import { prefetch, prefetchRoutes } from '$app/navigation';
  import { onMount } from 'svelte';
  
  onMount(async () => {
    // 重要なページを事前読み込み
    await prefetchRoutes([
      '/dashboard',
      '/profile',
      '/settings'
    ]);
  });
  
  async function preloadNextPage() {
    // 次のページを事前読み込み
    await prefetch('/posts/next-article');
  }
</script>

<!-- マウスオーバーで事前読み込み -->
<a 
  href="/important-page"
  on:mouseenter={() => prefetch('/important-page')}
>
  重要なページ
</a>
svelte

ルートアノテーション(ページオプション詳細)

ページやレイアウトファイルでエクスポートする特別な変数により、レンダリング方法やキャッシュ戦略を制御する高度な機能です。基本的な設定に加えて、条件付きプリレンダリング、エッジランタイム設定、継承の仕組みなど、より詳細な制御が可能です。

設定の継承と優先順位

ルートアノテーションはレイアウトツリーを通じて継承されます。

// src/routes/+layout.ts(ルートレイアウト)
export const ssr = true;
export const csr = true;
export const prerender = false;

// src/routes/blog/+layout.ts(ブログセクション)
export const prerender = true;  // ブログセクション全体を静的生成

// src/routes/blog/admin/+page.ts(管理画面)
export const prerender = false; // 管理画面は動的に
export const ssr = false;       // SPAとして動作
typescript

優先順位: ページ > 直近のレイアウト > 親レイアウト > ルートレイアウト

全設定オプション一覧

// src/routes/blog/+page.ts
import type { PageLoad } from './$types';

// レンダリング設定
export const prerender = true;      // ビルド時に静的生成
export const ssr = true;            // SSR有効
export const csr = true;            // CSR有効

// URL設定
export const trailingSlash = 'always'; // 'never' | 'always' | 'ignore'

// プラットフォーム設定(+page.server.tsのみ)
export const config = {
  runtime: 'edge',              // エッジランタイムで実行
  regions: ['us-east-1'],       // デプロイリージョン
  isr: {                        // Incremental Static Regeneration
    expiration: 60              // キャッシュ有効期限(秒)
  }
};

export const load: PageLoad = async () => {
  // データ取得
};
typescript

条件付きプリレンダリング

動的ルートで特定のページのみをプリレンダリングする設定です。entries関数で生成するページのパラメータを指定し、ビルド時に静的HTMLを作成します。

ブログ記事の動的ルートで最新10件のみをプリレンダリングする例です。entries関数で全記事から最新10件を選択し、それぞれのslug(post-1post-2など)を返すことで、ビルド時に/posts/post-1/posts/post-2などの静的HTMLが生成されます。11件目以降はアクセス時に動的に生成されます。

// src/routes/posts/[slug]/+page.ts
import type { PageLoad, EntryGenerator } from './$types';

// プリレンダリングするエントリを生成
export const entries: EntryGenerator = async () => {
  const posts = await getAllPosts();
  
  // 最新10件のみプリレンダリング
  return posts
    .slice(0, 10)
    .map(post => ({ slug: post.slug }));
};

export const prerender = 'auto'; // または true

export const load: PageLoad = async ({ params }) => {
  // 実装
};
typescript

高度なエラーハンドリング

エラー発生時の処理をカスタマイズする機能です。HTTPステータスコードに応じた表示の切り替え、エラー情報の伝播制御、開発環境と本番環境での表示の差別化など、きめ細かなエラー処理を実装できます。

エラー伝播の仕組み

以下の図は、エラーがどのように階層を遡って処理されるかを示しています。

ダイアグラムを読み込み中...

カスタムエラーページ

HTTPステータスコードに応じて異なるエラー画面を表示する実装例です。404エラーでは「ページが見つかりません」、403エラーでは「アクセス拒否」、その他のエラーでは「サーバーエラー」を表示します。開発環境(devtrue)では詳細なエラー情報を表示し、本番環境では隠すことでセキュリティを確保しています。

<!-- src/routes/(app)/+error.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
  import { dev } from '$app/environment';
  
  let error = $derived($page.error);
  let status = $derived($page.status);
</script>

{#if status === 404}
  <div class="error-404">
    <h1>ページが見つかりません</h1>
    <p>お探しのページは移動または削除された可能性があります。</p>
    <a href="/">ホームに戻る</a>
  </div>
{:else if status === 403}
  <div class="error-403">
    <h1>アクセス拒否</h1>
    <p>{error?.message || 'このページへのアクセス権限がありません。'}</p>
    <a href="/login">ログイン</a>
  </div>
{:else}
  <div class="error-500">
    <h1>サーバーエラー</h1>
    <p>申し訳ございません。エラーが発生しました。</p>
    {#if dev && error}
      <pre>{JSON.stringify(error, null, 2)}</pre>
    {/if}
  </div>
{/if}
svelte

エラーの伝播制御

Load関数でエラーを適切にスローし、最も近い+error.svelteで処理する仕組みです。

ブログ記事を取得するLoad関数のエラー処理例です。記事が見つからない場合は404エラーをスロー、未公開記事に管理者以外がアクセスした場合は403エラーをスローします。データベースエラーなどの予期しないエラーは500エラーとして処理します。これらのエラーは階層内の最も近い+error.svelteでキャッチされ、適切なエラー画面が表示されます。

// src/routes/(app)/posts/[id]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params, locals }) => {
  try {
    const post = await getPost(params.id);
    
    if (!post) {
      // 最も近い+error.svelteで処理される
      throw error(404, {
        message: 'Post not found',
        id: params.id
      });
    }
    
    if (!post.published && !locals.user?.isAdmin) {
      throw error(403, 'This post is not published');
    }
    
    return { post };
  } catch (e) {
    // データベースエラーなど
    console.error('Failed to load post:', e);
    
    throw error(500, {
      message: 'Failed to load post',
      code: 'DB_ERROR'
    });
  }
};
typescript

条件付きコンポーネント表示

同じURLパスで、サーバーサイドのデータに基づいて異なるコンポーネントを表示する手法です。認証状態やユーザーの権限、デバイスタイプなどに応じて、適切なUIを動的に選択できます。

「並列ルート」ではない理由

このパターンは複数のルートが並列に存在するわけではなく、単一のルートで条件に応じてコンポーネントを切り替えているだけです。Next.jsの「Parallel Routes」のような機能とは異なります。

サーバーサイドでのコンポーネント選択

プロフィールページで認証状態に応じて異なるコンポーネントを表示する実装例です。Load関数でユーザーの認証状態を確認し、認証済みなら'authenticated'、未認証なら'guest'をcomponentプロパティに設定します。ページコンポーネントではこの値に基づいて、認証済みユーザーにはUserProfileを、未認証ユーザーにはGuestProfileを表示します。

// src/routes/profile/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
  const user = await locals.getUser();
  
  return {
    component: user ? 'authenticated' : 'guest',
    user
  };
};
typescript
<!-- src/routes/profile/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
  import GuestProfile from './GuestProfile.svelte';
  import UserProfile from './UserProfile.svelte';
  
  let { data }: { data: PageData } = $props();
</script>

{#if data.component === 'authenticated'}
  <UserProfile user={data.user} />
{:else}
  <GuestProfile />
{/if}
svelte

パフォーマンス最適化

ページ遷移やデータ読み込みを高速化するための最適化手法です。プリロード戦略の選択、コード分割の制御、リンクごとの最適化設定により、ユーザー体験を向上させます。

リンクの最適化

リンクごとにプリロード戦略を設定する例です。eagerは表示と同時に即座にデータを読み込み(重要なページ向け)、hoverはマウスオーバー時に読み込み(デフォルト動作)、tapはタップ時に読み込み(モバイルのデータ通信量を節約)、offはプリロードを無効化(重いページや外部リンク向け)します。

<!-- プリロード戦略 -->
<nav>
  <!-- 表示時に即座にプリロード -->
  <a href="/critical" data-sveltekit-preload-data="eager">
    重要なページ
  </a>
  
  <!-- ホバー時にプリロード(デフォルト) -->
  <a href="/normal" data-sveltekit-preload-data="hover">
    通常のページ
  </a>
  
  <!-- タップ時にプリロード(モバイル向け) -->
  <a href="/mobile" data-sveltekit-preload-data="tap">
    モバイル向けページ
  </a>
  
  <!-- プリロードしない -->
  <a href="/heavy" data-sveltekit-preload-data="off">
    重いページ
  </a>
</nav>
svelte

コード分割の制御

動的インポートを使用して、必要な時だけ重いコンポーネントを読み込む手法です。

ダッシュボードページで重いコンポーネントを動的に読み込む例です。import()を使用することで、このコンポーネントのコードは別のJavaScriptファイルに分割され、ダッシュボードページにアクセスした時だけ読み込まれます。初期バンドルサイズを削減し、アプリケーション全体の起動を高速化します。

// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async () => {
  // 動的インポートで必要な時だけロード
  const { HeavyComponent } = await import('$lib/components/HeavyComponent.svelte');
  
  return {
    component: HeavyComponent
  };
};
typescript

実践例:マルチテナントアプリケーション

複数のテナント(組織)をサポートするSaaSアプリケーションの実装例です。URLパスからテナントを識別し、テナントごとに異なるテーマや設定を適用する、実践的なルーティング構造を紹介します。

マルチテナントアプリケーションのディレクトリ構造です。[tenant]動的パラメータでテナントを識別し、(public)グループには認証不要なページ、(app)グループには認証が必要なページを配置しています。例えば/acme/dashboardではacme社のダッシュボードが表示されます。

src/routes/
└── [tenant]/
    ├── +layout.server.ts    # テナント検証
    ├── +layout.svelte       # テナント別レイアウト
    ├── (public)/
    │   ├── +page.svelte     # /{tenant}
    │   └── about/
    │       └── +page.svelte # /{tenant}/about
    └── (app)/
        ├── +layout.server.ts # 認証チェック
        ├── dashboard/
        │   └── +page.svelte  # /{tenant}/dashboard
        └── settings/
            └── +page.svelte  # /{tenant}/settings
null

テナント検証を行うレイアウトLoad関数です。URLパスから取得したテナント名(params.tenant)でデータベースを検索し、存在しない場合は404エラーを返します。有効なテナントの場合は、そのテナント固有のテーマ設定やコンフィグを返し、すべての子ページで利用可能にします。

// src/routes/[tenant]/+layout.server.ts
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ params, locals }) => {
  const tenant = await getTenantBySlug(params.tenant);
  
  if (!tenant) {
    throw error(404, 'Tenant not found');
  }
  
  // テナント情報を設定
  locals.tenant = tenant;
  
  return {
    tenant,
    theme: tenant.theme,
    config: tenant.config
  };
};
typescript

まとめ

高度なルーティング機能により

  • 整理された構造: ルートグループで論理的な整理
  • 柔軟なレイアウト: ネストと継承による再利用
  • 動的な制御: プログラマティックナビゲーション
  • 最適化: プリフェッチとコード分割

次のステップ

Last update at: 2025/09/16 01:10:33