特殊ファイルシステム
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>
+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;
+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');
}
};
+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>
+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';
+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 {};
};
+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 });
};
+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}
Layout vs Page の使い分け
- Layout: 複数ページで共通の処理・UI
- Page: そのページ固有の処理・コンテンツ
観点 | Layout | Page |
---|---|---|
スコープ | ディレクトリ全体 | 単一ページ |
再利用性 | 高い(継承される) | 低い(そのページのみ) |
典型的な処理 | 認証、共通UI、ナビゲーション | 個別データ取得、フォーム |
更新頻度 | 低い | 高い |
サーバーファイルの本質的な違い
サーバーサイドでのlayout
とpage
の区別は、見た目ではなく責務の範囲で決まります。
+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 # ユーザー管理処理(このページ固有)
この場合
+layout.server.ts
:/admin
以下のすべてのページで管理者チェック+page.server.ts
:各ページ固有の処理のみ
ネストされたレイアウト
SvelteKitのレイアウトは階層的に構成でき、ディレクトリ構造に応じて自動的にネストされます。
レイアウトの継承
親ディレクトリの+layout.svelte
は、子ディレクトリのページやレイアウトを自動的にラップします。
src/routes/
├── +layout.svelte # ルートレイアウト(全ページに適用)
├── blog/
│ ├── +layout.svelte # ブログレイアウト(ブログセクションのみ)
│ ├── +page.svelte # ブログトップページ
│ └── [slug]/
│ └── +page.svelte # 個別記事ページ
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()
};
};
レイアウトのリセット
レイアウトの継承をリセットしたい場合は、ルートグループのディレクトリ名に@
記号を使用します。
src/routes/
├── +layout.svelte # ルートレイアウト
├── (marketing)/ # マーケティンググループ
│ ├── +layout.svelte # マーケティング用レイアウト
│ └── about/+page.svelte
└── (app)@/ # @でルートレイアウトに戻る
├── +layout.svelte # アプリ用の新しいレイアウト
└── dashboard/+page.svelte
上記の例では、(app)@
グループ内のページは、(marketing)
のレイアウトを継承せず、ルートレイアウトから直接継承します。これにより、異なるデザインや構造を持つセクションを同じアプリケーション内で実現できます。
レイアウト適用の流れ
以下の図は、レイアウトがどのように階層的に適用されるかを示しています。
ネスト制御のベストプラクティス
- 共通要素の抽出: ヘッダーやフッターなど、複数ページで共有する要素はレイアウトに配置
- 段階的な構築: ルートレイアウトは最小限に、セクション固有の要素は子レイアウトに
- データの継承:
parent()
を使って親のデータを継承し、重複を避ける - リセットの活用: 異なるデザインが必要な場合は
@
記号でレイアウトをリセット
実行タイミングの違い
初回アクセス/リロード時
すべてのファイルが順番に実行されます。
+layout.server.ts
→+page.server.ts
(サーバー)+layout.ts
→+page.ts
(サーバー)+layout.svelte
→+page.svelte
(サーバーでHTML生成)- クライアントでハイドレーション
クライアントナビゲーション時
.server.ts
ファイルはスキップされます。
+layout.ts
(キャッシュ or 再実行)+page.ts
(必ず実行)+layout.svelte
→+page.svelte
(再レンダリング)
フォーム送信時
+page.server.ts
のactionsが実行されます。
actions.default()
または名前付きaction実行- 成功時:
redirect()
でページ遷移 - 失敗時:
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キーは含まない
};
データフローの最適化
// 親レイアウトで共通データ取得
// +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')
};
};
よくあるエラーと対処法
原因: Load関数の循環参照
解決: parent()
の呼び出しタイミングを確認
原因: サーバーとクライアントで異なる内容
解決: browser
変数を使用して条件分岐
原因: .server.ts
以外で環境変数にアクセス
解決: 環境変数へのアクセスは.server.ts
に限定
まとめ
SvelteKitの特殊ファイルシステムは、
- 規約ベース: ファイル名が機能を決定
- 型安全: TypeScriptの完全サポート
- セキュア: サーバー専用ファイルで機密情報を保護
- 効率的: 適切なファイル分割でパフォーマンス最適化
これらのファイルを適切に使い分けることで、安全で高性能なWebアプリケーションを構築できます。