特殊ファイルシステム

SvelteKitのファイルシステムは、ファイル名によって機能と実行環境が決まる規約ベースの設計です。+プレフィックスを持つ特殊ファイルが、アプリケーションの骨格を構成します。

特殊ファイル一覧

ファイル名実行環境主な役割秘密情報
+page.svelteクライアントページUI❌ 不可
+page.ts両方ユニバーサルデータ取得❌ 不可
+page.server.tsサーバーのみサーバー処理・Actions✅ 可能
+layout.svelteクライアント共通レイアウト❌ 不可
+layout.ts両方共通データ取得❌ 不可
+layout.server.tsサーバーのみ認証・権限チェック✅ 可能
+server.tsサーバーのみAPIエンドポイント✅ 可能
+error.svelteクライアントエラーページ❌ 不可
セキュリティ

.server.tsファイル以外では、環境変数や秘密鍵を扱わないでください。

実行フローの理解

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

データの流れ

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

データは親から子へ、サーバーからクライアントへと一方向に流れます。

+page.svelte

ページのUIコンポーネント - ユーザーに表示される画面を定義します。

  • 実行環境: クライアント(ブラウザ)
  • 受け取るデータ: data(load関数の結果)、form(Actions結果)
  • 用途: UI表示、イベントハンドリング、ユーザーインタラクション
コード例
<script lang="ts">
  import type { PageData, ActionData } from './$types';
  
  let { data, form }: { data: PageData; form: ActionData } = $props();
</script>

<h1>{data.title}</h1>

{#if form?.error}
  <p class="error">{form.error}</p>
{/if}

<form method="POST">
  <input name="title" value={form?.title ?? ''} />
  <button>送信</button>
</form>
svelte

+page.ts

ユニバーサルなデータ取得 - サーバーとクライアント両方で実行可能なデータ取得処理。

  • 実行環境: サーバー(初回)&クライアント(ナビゲーション時)
  • アクセス可能: 公開API、親のデータ、URLパラメータ
  • 用途: 公開データの取得、データの整形
コード例
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ params, url, fetch, parent }) => {
  const parentData = await parent();
  const response = await fetch(`/api/items/${params.id}`);
  const item = await response.json();
  
  return {
    ...parentData,
    item,
    query: url.searchParams.get('q')
  };
};

// ページ設定
export const prerender = false; // SSRを使用
export const ssr = true;
export const csr = true;
typescript

+page.server.ts

サーバー専用処理とフォームActions - データベース操作や秘密情報を扱う処理。

  • 実行環境: サーバーのみ
  • アクセス可能: DB、環境変数、ファイルシステム
  • 用途: 認証、DB操作、フォーム処理、ファイルアップロード
コード例
import type { PageServerLoad, Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';

// サーバー専用のデータ取得
export const load: PageServerLoad = async ({ params, locals }) => {
  const post = await db.post.findUnique({
    where: { id: params.id }
  });
  
  if (!post) throw error(404, 'Not found');
  
  return {
    post,
    canEdit: locals.user?.id === post.authorId
  };
};

// フォームActions
export const actions: Actions = {
  default: async ({ request, locals }) => {
    const data = await request.formData();
    const title = data.get('title');
    
    if (!title) {
      return fail(400, { 
        title,
        error: 'タイトルは必須です' 
      });
    }
    
    await db.post.create({
      data: { title, authorId: locals.user.id }
    });
    
    throw redirect(303, '/posts');
  }
};
typescript

+layout.svelte

共通レイアウトコンポーネント - 複数ページで共有するUI構造。

  • 実行環境: クライアント(ブラウザ)
  • 受け取るデータ: data(layout load関数の結果)
  • 用途: ヘッダー、フッター、サイドバー、ナビゲーション
コード例
<script lang="ts">
  import type { LayoutData } from './$types';
  import type { Snippet } from 'svelte';
  
  let { data, children }: { data: LayoutData; children?: Snippet } = $props();
</script>

<header>
  <nav>{data.navigation}</nav>
  {#if data.user}
    <span>Welcome, {data.user.name}</span>
  {/if}
</header>

<main>
  {@render children?.()}  <!-- 子ページがここに挿入される -->
</main>

<footer>© 2024</footer>
svelte

+layout.ts

共通データの取得 - すべての子ページで使用するデータを取得。

  • 実行環境: サーバー(初回)&クライアント(ナビゲーション時)
  • アクセス可能: 公開API、親レイアウトのデータ
  • 用途: ナビゲーションメニュー、設定、共通マスタデータ
コード例
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = async ({ fetch }) => {
  const [config, navigation] = await Promise.all([
    fetch('/api/config').then(r => r.json()),
    fetch('/api/navigation').then(r => r.json())
  ]);
  
  return { config, navigation };
};

// レイアウトグループの設定
export const prerender = false;
export const trailingSlash = 'always';
typescript

+layout.server.ts

認証と権限チェック - セキュリティ関連の横断的処理。

  • 実行環境: サーバーのみ
  • アクセス可能: Cookie、セッション、環境変数
  • 用途: 認証、権限チェック、セッション管理
重要な特性

横断的関心事を扱います。そのディレクトリ以下のすべてのページに影響する処理を記述します。

コード例
import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';

export const load: LayoutServerLoad = async ({ cookies, url }) => {
  const token = cookies.get('session');
  
  // 認証が必要なページグループ
  if (url.pathname.startsWith('/admin')) {
    if (!token) {
      throw redirect(303, `/login?redirect=${url.pathname}`);
    }
    
    const user = await validateToken(token);
    if (user.role !== 'admin') {
      throw error(403, 'Admin access required');
    }
    
    return { user };
  }
  
  return {};
};
typescript

+server.ts

APIエンドポイント - RESTful APIを実装。

  • 実行環境: サーバーのみ
  • HTTPメソッド: GET、POST、PUT、DELETE、PATCH
  • 用途: 外部API、データ提供、Webhook受信
コード例
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ url }) => {
  const limit = Number(url.searchParams.get('limit')) || 10;
  const posts = await db.post.findMany({ take: limit });
  return json(posts);
};

export const POST: RequestHandler = async ({ request, locals }) => {
  if (!locals.user) {
    throw error(401, 'Authentication required');
  }
  
  const data = await request.json();
  const post = await db.post.create({ data });
  
  return json(post, { status: 201 });
};
typescript

+error.svelte

エラーページ - エラー発生時のカスタム表示。

  • 実行環境: クライアント(ブラウザ)
  • アクセス可能: $page.status$page.error
  • 用途: 404、500などのエラー表示
コード例
<script lang="ts">
  import { page } from '$app/stores';
</script>

<h1>{$page.status}</h1>
<p>{$page.error?.message}</p>

{#if $page.status === 404}
  <p>ページが見つかりません</p>
  <a href="/">ホームに戻る</a>
{:else if $page.status === 500}
  <p>サーバーエラーが発生しました</p>
{/if}
svelte

Layout vs Page の使い分け

使い分けの原則
  • Layout: 複数ページで共通の処理・UI
  • Page: そのページ固有の処理・コンテンツ
観点LayoutPage
スコープディレクトリ全体単一ページ
再利用性高い(継承される)低い(そのページのみ)
典型的な処理認証、共通UI、ナビゲーション個別データ取得、フォーム
更新頻度低い高い

サーバーファイルの本質的な違い

重要:UIレイアウトとは異なる概念

サーバーサイドでのlayoutpageの区別は、見た目ではなく責務の範囲で決まります。

+layout.server.ts vs +page.server.ts

観点+layout.server.ts+page.server.ts
責務横断的関心事
(Cross-cutting concerns)
ページ固有のビジネスロジック
影響範囲ディレクトリ以下すべてそのページのみ
典型的な処理• セッション管理
• ユーザー認証
• アクセス権限チェック
• 共通セキュリティ
• CRUD操作
• フォーム処理(Actions)
• ファイルアップロード
• ページ固有の処理
実行タイミング子ページアクセス時に必ず実行そのページアクセス時のみ

具体例:ブログの管理画面

/admin/
├── +layout.server.ts    # 管理者権限チェック(全ページ共通)
├── posts/
│   └── +page.server.ts  # 記事のCRUD処理(このページ固有)
└── users/
    └── +page.server.ts  # ユーザー管理処理(このページ固有)
null
この場合
  • +layout.server.ts/admin以下のすべてのページで管理者チェック
  • +page.server.ts:各ページ固有の処理のみ

ネストされたレイアウト

SvelteKitのレイアウトは階層的に構成でき、ディレクトリ構造に応じて自動的にネストされます。

レイアウトの継承

親ディレクトリの+layout.svelteは、子ディレクトリのページやレイアウトを自動的にラップします。

src/routes/
├── +layout.svelte        # ルートレイアウト(全ページに適用)
├── blog/
│   ├── +layout.svelte   # ブログレイアウト(ブログセクションのみ)
│   ├── +page.svelte     # ブログトップページ
│   └── [slug]/
│       └── +page.svelte # 個別記事ページ
null

Mermaidで見る継承構造

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

レイアウトのデータフロー

各レイアウトは親のデータを継承し、追加のデータを提供できます。

// src/routes/+layout.ts
export const load = async () => {
  return {
    user: await getUser(),
    theme: 'dark'
  };
};

// src/routes/blog/+layout.ts
export const load = async ({ parent }) => {
  const parentData = await parent(); // 親のデータを取得
  return {
    ...parentData,
    categories: await getCategories()
  };
};
typescript

レイアウトのリセット

レイアウトの継承をリセットしたい場合は、ルートグループのディレクトリ名に@記号を使用します。

src/routes/
├── +layout.svelte           # ルートレイアウト
├── (marketing)/             # マーケティンググループ
│   ├── +layout.svelte      # マーケティング用レイアウト
│   └── about/+page.svelte
└── (app)@/                  # @でルートレイアウトに戻る
    ├── +layout.svelte      # アプリ用の新しいレイアウト
    └── dashboard/+page.svelte
null

上記の例では、(app)@グループ内のページは、(marketing)のレイアウトを継承せず、ルートレイアウトから直接継承します。これにより、異なるデザインや構造を持つセクションを同じアプリケーション内で実現できます。

レイアウト適用の流れ

以下の図は、レイアウトがどのように階層的に適用されるかを示しています。

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

ネスト制御のベストプラクティス

  1. 共通要素の抽出: ヘッダーやフッターなど、複数ページで共有する要素はレイアウトに配置
  2. 段階的な構築: ルートレイアウトは最小限に、セクション固有の要素は子レイアウトに
  3. データの継承: parent()を使って親のデータを継承し、重複を避ける
  4. リセットの活用: 異なるデザインが必要な場合は@記号でレイアウトをリセット

実行タイミングの違い

初回アクセス/リロード時

すべてのファイルが順番に実行されます。

  1. +layout.server.ts+page.server.ts(サーバー)
  2. +layout.ts+page.ts(サーバー)
  3. +layout.svelte+page.svelte(サーバーでHTML生成)
  4. クライアントでハイドレーション

クライアントナビゲーション時

.server.tsファイルはスキップされます。

  1. +layout.ts(キャッシュ or 再実行)
  2. +page.ts(必ず実行)
  3. +layout.svelte+page.svelte(再レンダリング)

フォーム送信時

+page.server.tsのactionsが実行されます。

  1. actions.default()または名前付きaction実行
  2. 成功時:redirect()でページ遷移
  3. 失敗時:fail()でformプロパティ更新

ベストプラクティス

セキュリティ

// ❌ 悪い例:+page.tsで秘密情報
export const load = async () => {
  const apiKey = process.env.SECRET_API_KEY; // 露出する!
};

// ✅ 良い例:+page.server.tsで秘密情報
export const load = async () => {
  const apiKey = process.env.SECRET_API_KEY; // サーバーのみ
  const data = await fetchWithApiKey(apiKey);
  return { data }; // APIキーは含まない
};
typescript

データフローの最適化

// 親レイアウトで共通データ取得
// +layout.server.ts
export const load = async ({ locals }) => {
  const user = await locals.getUser();
  return { user };
};

// 子ページで親のデータを活用
// +page.ts
export const load = async ({ parent }) => {
  const { user } = await parent();
  return { 
    content: user?.premium 
      ? await fetch('/api/premium-content')
      : await fetch('/api/free-content')
  };
};
typescript

よくあるエラーと対処法

TypeError: Cannot access 'X' before initialization

原因: Load関数の循環参照
解決: parent()の呼び出しタイミングを確認

Hydration mismatch

原因: サーバーとクライアントで異なる内容
解決: browser変数を使用して条件分岐

500 Internal Server Error in production

原因: .server.ts以外で環境変数にアクセス
解決: 環境変数へのアクセスは.server.tsに限定

まとめ

SvelteKitの特殊ファイルシステムは、

  • 規約ベース: ファイル名が機能を決定
  • 型安全: TypeScriptの完全サポート
  • セキュア: サーバー専用ファイルで機密情報を保護
  • 効率的: 適切なファイル分割でパフォーマンス最適化

これらのファイルを適切に使い分けることで、安全で高性能なWebアプリケーションを構築できます。

次のステップ

Last update at: 2025/09/09 21:04:37