SvelteKit

Svelte公式のフルスタックフレームワーク。コンパイル時最適化による高性能と小さなバンドルサイズを実現。2025年急成長中の注目株。

JavaScriptSvelteフレームワークSSRSSGルーティングメタフレームワークフルスタック

GitHub概要

sveltejs/kit

web development, streamlined

スター19,601
ウォッチ156
フォーク2,096
作成日:2020年10月15日
言語:JavaScript
ライセンス:MIT License

トピックス

hacktoberfestsvelte

スター履歴

sveltejs/kit Star History
データ取得日時: 2025/8/13 01:43

フレームワーク

SvelteKit

概要

SvelteKitは、Svelteをベースとした Web アプリケーション構築のためのフルスタックフレームワークです。ファイルベースルーティング、SSR、SSG、フォームアクション、API ルートなどの機能を提供します。

詳細

SvelteKitは、2021年にSvelteチームによって開発されたSvelteのメタフレームワークで、"Svelteアプリを構築する最速の方法"として位置付けられています。Svelteの軽量性とパフォーマンスを活かしつつ、現代的なWebアプリケーション開発に必要な機能を統合しています。主な特徴として、ファイルベースルーティングシステム(src/routes構造による自動ルート生成)、サーバーサイドレンダリング(SSR)とクライアントサイドハイドレーション、静的サイト生成(SSG)対応、プログレッシブエンハンスメント対応のフォームアクション、API エンドポイント作成機能、TypeScript完全サポート、Viteベースの高速開発環境、アダプターベースの柔軟なデプロイメントシステムがあります。SvelteKitはハイブリッドレンダリング(SSR + CSR)、完全静的サイト、SPA、サーバーレスアプリケーション、モバイルアプリ(Tauri/Capacitor)、PWAまで幅広い用途に対応し、Vercel、Netlify、Cloudflare Pages、AWS Lambda、Node.js等への柔軟なデプロイが可能です。

メリット・デメリット

メリット

  • ファイルベースルーティング: 直感的な src/routes 構造による自動ルート生成
  • 柔軟なレンダリング: SSR、SSG、SPA、ハイブリッドレンダリングを選択可能
  • 高速パフォーマンス: Svelteのコンパイル時最適化の恩恵
  • プログレッシブエンハンスメント: JavaScriptなしでも動作するフォーム
  • TypeScript完全サポート: 自動生成される型定義と型安全性
  • Viteベース: 高速なHMRと開発体験
  • アダプターシステム: 50+の展開先に対応
  • フルスタック機能: API ルート、データローディング、フォームアクション

デメリット

  • 学習コスト: Svelte + SvelteKit固有概念の理解が必要
  • エコシステム: React/Vue.jsと比較してライブラリが少ない
  • コミュニティサイズ: 他のメタフレームワークと比較して小さい
  • ツーリング成熟度: 開発ツールや統合ソリューションが発展途上
  • ドキュメント量: 日本語の学習リソースが限定的
  • 企業採用: エンタープライズでの採用事例が少ない
  • サードパーティ統合: 一部のライブラリで統合が複雑

主要リンク

書き方の例

Hello World

<!-- src/routes/+page.svelte -->
<script>
	// データの受け取り
	/** @type {import('./$types').PageData} */
	export let data;
	
	// ローカル状態
	let count = 0;
	let name = '';
	
	// リアクティブステートメント
	$: greeting = name ? `こんにちは、${name}さん!` : 'こんにちは!';
	
	function increment() {
		count += 1;
	}
</script>

<main>
	<h1>SvelteKit へようこそ!</h1>
	
	<div class="greeting-section">
		<p>{greeting}</p>
		<input 
			bind:value={name} 
			placeholder="お名前を入力してください"
		/>
	</div>
	
	<div class="counter-section">
		<p>カウント: {count}</p>
		<button on:click={increment}>+1</button>
	</div>
	
	{#if data}
		<div class="data-section">
			<h2>サーバーからのデータ</h2>
			<p>現在の時刻: {data.timestamp}</p>
			<p>環境: {data.environment}</p>
		</div>
	{/if}
</main>

<style>
	main {
		text-align: center;
		padding: 1em;
		max-width: 240px;
		margin: 0 auto;
	}

	.greeting-section, .counter-section, .data-section {
		margin: 2em 0;
		padding: 1em;
		border: 1px solid #ddd;
		border-radius: 8px;
	}

	input {
		padding: 0.5em;
		border: 1px solid #ccc;
		border-radius: 4px;
		width: 100%;
		margin-top: 0.5em;
	}

	button {
		background: #ff3e00;
		color: white;
		border: none;
		padding: 0.5em 1em;
		border-radius: 4px;
		cursor: pointer;
		font-size: 1em;
	}

	button:hover {
		background: #cc3200;
	}

	h1 {
		color: #ff3e00;
		text-transform: uppercase;
		font-size: 2em;
		font-weight: 100;
	}
</style>
// src/routes/+page.js
/** @type {import('./$types').PageLoad} */
export function load() {
	return {
		timestamp: new Date().toLocaleString('ja-JP'),
		environment: 'development'
	};
}

ファイルベースルーティングとレイアウト

<!-- src/routes/+layout.svelte -->
<script>
	import { page } from '$app/stores';
	import { navigating } from '$app/stores';
	
	// レイアウトデータの受け取り
	/** @type {import('./$types').LayoutData} */
	export let data;
</script>

<div class="app">
	<header>
		<nav>
			<a href="/" class:active={$page.url.pathname === '/'}>
				ホーム
			</a>
			<a href="/about" class:active={$page.url.pathname === '/about'}>
				このサイトについて
			</a>
			<a href="/blog" class:active={$page.url.pathname.startsWith('/blog')}>
				ブログ
			</a>
			<a href="/api/users" class:active={$page.url.pathname.startsWith('/api')}>
				API
			</a>
		</nav>
		
		{#if $navigating}
			<div class="loading">読み込み中...</div>
		{/if}
	</header>

	<main>
		<!-- 子ページのコンテンツをここにレンダリング -->
		<slot />
	</main>

	<footer>
		<p>&copy; 2024 SvelteKit アプリケーション</p>
		{#if data.user}
			<p>ログイン中: {data.user.name}</p>
		{/if}
	</footer>
</div>

<style>
	.app {
		display: flex;
		flex-direction: column;
		min-height: 100vh;
	}

	header {
		padding: 1rem;
		background: #282c34;
		color: white;
	}

	nav {
		display: flex;
		gap: 1rem;
	}

	nav a {
		color: white;
		text-decoration: none;
		padding: 0.5rem 1rem;
		border-radius: 4px;
		transition: background 0.2s;
	}

	nav a:hover {
		background: rgba(255, 255, 255, 0.1);
	}

	nav a.active {
		background: #ff3e00;
	}

	.loading {
		position: fixed;
		top: 0;
		left: 50%;
		transform: translateX(-50%);
		background: #ff3e00;
		color: white;
		padding: 0.5rem 1rem;
		border-radius: 0 0 4px 4px;
		z-index: 1000;
	}

	main {
		flex: 1;
		padding: 2rem;
	}

	footer {
		padding: 1rem;
		background: #f5f5f5;
		text-align: center;
		color: #666;
	}
</style>
// src/routes/+layout.js
/** @type {import('./$types').LayoutLoad} */
export async function load({ fetch, url }) {
	// レイアウト共通データの取得
	const user = await fetch('/api/me').then(r => r.ok ? r.json() : null);
	
	return {
		user,
		currentPath: url.pathname
	};
}
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
	/** @type {import('./$types').PageData} */
	export let data;
</script>

<svelte:head>
	<title>{data.post.title} - ブログ</title>
	<meta name="description" content={data.post.excerpt} />
</svelte:head>

<article>
	<header>
		<h1>{data.post.title}</h1>
		<p class="meta">
			投稿日: {new Date(data.post.date).toLocaleDateString('ja-JP')}
			・カテゴリ: {data.post.category}
		</p>
	</header>
	
	<div class="content">
		{@html data.post.content}
	</div>
	
	<footer>
		<a href="/blog">← ブログ一覧に戻る</a>
	</footer>
</article>

<style>
	article {
		max-width: 800px;
		margin: 0 auto;
	}

	header h1 {
		color: #333;
		margin-bottom: 0.5rem;
	}

	.meta {
		color: #666;
		font-size: 0.9rem;
		margin-bottom: 2rem;
	}

	.content {
		line-height: 1.6;
		margin-bottom: 2rem;
	}

	footer a {
		color: #ff3e00;
		text-decoration: none;
	}

	footer a:hover {
		text-decoration: underline;
	}
</style>

データローディングとサーバーサイド機能

// src/routes/blog/[slug]/+page.server.js
import { error } from '@sveltejs/kit';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params, fetch }) {
	try {
		// データベースまたはAPIからブログ記事を取得
		const response = await fetch(`/api/posts/${params.slug}`);
		
		if (!response.ok) {
			if (response.status === 404) {
				throw error(404, '記事が見つかりません');
			}
			throw error(500, 'サーバーエラーが発生しました');
		}
		
		const post = await response.json();
		
		return {
			post: {
				title: post.title,
				content: post.content,
				excerpt: post.excerpt,
				date: post.publishedAt,
				category: post.category,
				author: post.author
			}
		};
	} catch (err) {
		console.error('ブログ記事の読み込みエラー:', err);
		throw error(500, 'ブログ記事を読み込めませんでした');
	}
}
// src/routes/api/posts/+server.js
import { json } from '@sveltejs/kit';

// モックデータ(実際にはデータベースから取得)
const posts = [
	{
		id: '1',
		slug: 'sveltekit-introduction',
		title: 'SvelteKit入門',
		content: '<p>SvelteKitの基本的な使い方を学びましょう...</p>',
		excerpt: 'SvelteKitの基本的な使い方',
		publishedAt: '2024-01-15T10:00:00Z',
		category: '技術',
		author: { name: '山田太郎', email: '[email protected]' }
	},
	{
		id: '2',
		slug: 'sveltekit-routing',
		title: 'SvelteKitのルーティング',
		content: '<p>ファイルベースルーティングについて...</p>',
		excerpt: 'ファイルベースルーティングの詳細',
		publishedAt: '2024-01-20T14:30:00Z',
		category: '技術',
		author: { name: '佐藤花子', email: '[email protected]' }
	}
];

/** @type {import('./$types').RequestHandler} */
export async function GET({ url }) {
	const page = parseInt(url.searchParams.get('page') ?? '1');
	const limit = parseInt(url.searchParams.get('limit') ?? '10');
	const category = url.searchParams.get('category');
	
	let filteredPosts = posts;
	
	// カテゴリフィルタリング
	if (category) {
		filteredPosts = posts.filter(post => 
			post.category.toLowerCase() === category.toLowerCase()
		);
	}
	
	// ページネーション
	const start = (page - 1) * limit;
	const end = start + limit;
	const paginatedPosts = filteredPosts.slice(start, end);
	
	return json({
		posts: paginatedPosts,
		pagination: {
			page,
			limit,
			total: filteredPosts.length,
			totalPages: Math.ceil(filteredPosts.length / limit)
		}
	});
}

/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
	try {
		const data = await request.json();
		
		// バリデーション
		if (!data.title || !data.content) {
			return json(
				{ error: 'タイトルとコンテンツは必須です' },
				{ status: 400 }
			);
		}
		
		// 新しい記事の作成(実際にはデータベースに保存)
		const newPost = {
			id: String(posts.length + 1),
			slug: data.title.toLowerCase().replace(/\s+/g, '-'),
			title: data.title,
			content: data.content,
			excerpt: data.excerpt || data.content.substring(0, 100) + '...',
			publishedAt: new Date().toISOString(),
			category: data.category || '未分類',
			author: data.author
		};
		
		posts.push(newPost);
		
		return json(newPost, { status: 201 });
	} catch (error) {
		console.error('記事作成エラー:', error);
		return json(
			{ error: '記事の作成に失敗しました' },
			{ status: 500 }
		);
	}
}
// src/routes/api/posts/[id]/+server.js
import { json, error } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export async function GET({ params }) {
	// 実際にはデータベースから取得
	const post = posts.find(p => p.id === params.id || p.slug === params.id);
	
	if (!post) {
		throw error(404, '記事が見つかりません');
	}
	
	return json(post);
}

/** @type {import('./$types').RequestHandler} */
export async function PUT({ params, request }) {
	const postIndex = posts.findIndex(p => p.id === params.id);
	
	if (postIndex === -1) {
		throw error(404, '記事が見つかりません');
	}
	
	const updates = await request.json();
	posts[postIndex] = { ...posts[postIndex], ...updates };
	
	return json(posts[postIndex]);
}

/** @type {import('./$types').RequestHandler} */
export async function DELETE({ params }) {
	const postIndex = posts.findIndex(p => p.id === params.id);
	
	if (postIndex === -1) {
		throw error(404, '記事が見つかりません');
	}
	
	posts.splice(postIndex, 1);
	
	return new Response(null, { status: 204 });
}

フォームアクションとプログレッシブエンハンスメント

<!-- src/routes/contact/+page.svelte -->
<script>
	import { enhance } from '$app/forms';
	import { page } from '$app/stores';
	
	/** @type {import('./$types').PageData} */
	export let data;
	
	/** @type {import('./$types').ActionData} */
	export let form;
	
	let loading = false;
</script>

<svelte:head>
	<title>お問い合わせ</title>
</svelte:head>

<div class="contact-page">
	<h1>お問い合わせ</h1>
	
	{#if form?.success}
		<div class="success-message">
			<p>お問い合わせありがとうございます!</p>
			<p>24時間以内にご返信いたします。</p>
		</div>
	{:else}
		<form 
			method="POST" 
			action="?/send"
			use:enhance={({ formData, cancel }) => {
				loading = true;
				
				// フォーム送信前の処理
				const name = formData.get('name');
				const email = formData.get('email');
				const message = formData.get('message');
				
				// バリデーション
				if (!name || !email || !message) {
					alert('すべての項目を入力してください');
					cancel();
					loading = false;
					return;
				}
				
				return async ({ result, update }) => {
					loading = false;
					
					if (result.type === 'success') {
						// 成功時の処理
						console.log('お問い合わせが送信されました');
					} else if (result.type === 'failure') {
						// エラー時の処理
						console.error('送信エラー:', result.data);
					}
					
					await update();
				};
			}}
		>
			<div class="form-group">
				<label for="name">お名前 *</label>
				<input
					id="name"
					name="name"
					type="text"
					required
					value={form?.name ?? ''}
					class:error={form?.errors?.name}
				/>
				{#if form?.errors?.name}
					<span class="error">{form.errors.name}</span>
				{/if}
			</div>
			
			<div class="form-group">
				<label for="email">メールアドレス *</label>
				<input
					id="email"
					name="email"
					type="email"
					required
					value={form?.email ?? ''}
					class:error={form?.errors?.email}
				/>
				{#if form?.errors?.email}
					<span class="error">{form.errors.email}</span>
				{/if}
			</div>
			
			<div class="form-group">
				<label for="category">カテゴリ</label>
				<select id="category" name="category">
					<option value="general">一般的なお問い合わせ</option>
					<option value="support">サポート</option>
					<option value="business">ビジネス</option>
					<option value="bug">バグ報告</option>
				</select>
			</div>
			
			<div class="form-group">
				<label for="message">メッセージ *</label>
				<textarea
					id="message"
					name="message"
					rows="5"
					required
					value={form?.message ?? ''}
					class:error={form?.errors?.message}
				></textarea>
				{#if form?.errors?.message}
					<span class="error">{form.errors.message}</span>
				{/if}
			</div>
			
			<button type="submit" disabled={loading}>
				{loading ? '送信中...' : '送信'}
			</button>
			
			{#if form?.errors?.general}
				<div class="general-error">
					{form.errors.general}
				</div>
			{/if}
		</form>
	{/if}
</div>

<style>
	.contact-page {
		max-width: 600px;
		margin: 0 auto;
		padding: 2rem;
	}

	.success-message {
		background: #d4edda;
		color: #155724;
		padding: 1rem;
		border-radius: 4px;
		border: 1px solid #c3e6cb;
	}

	.form-group {
		margin-bottom: 1rem;
	}

	label {
		display: block;
		margin-bottom: 0.5rem;
		font-weight: bold;
	}

	input, select, textarea {
		width: 100%;
		padding: 0.75rem;
		border: 1px solid #ddd;
		border-radius: 4px;
		font-size: 1rem;
	}

	input.error, textarea.error {
		border-color: #dc3545;
	}

	.error {
		color: #dc3545;
		font-size: 0.875rem;
		margin-top: 0.25rem;
		display: block;
	}

	button {
		background: #ff3e00;
		color: white;
		padding: 0.75rem 2rem;
		border: none;
		border-radius: 4px;
		cursor: pointer;
		font-size: 1rem;
	}

	button:disabled {
		background: #ccc;
		cursor: not-allowed;
	}

	.general-error {
		background: #f8d7da;
		color: #721c24;
		padding: 0.5rem;
		border-radius: 4px;
		margin-top: 1rem;
	}
</style>
// src/routes/contact/+page.server.js
import { fail } from '@sveltejs/kit';

/** @type {import('./$types').PageServerLoad} */
export function load() {
	return {
		title: 'お問い合わせ'
	};
}

/** @type {import('./$types').Actions} */
export const actions = {
	send: async ({ request }) => {
		const data = await request.formData();
		const name = data.get('name');
		const email = data.get('email');
		const category = data.get('category');
		const message = data.get('message');
		
		// バリデーション
		const errors = {};
		
		if (!name || name.length < 2) {
			errors.name = 'お名前は2文字以上で入力してください';
		}
		
		if (!email || !email.includes('@')) {
			errors.email = '有効なメールアドレスを入力してください';
		}
		
		if (!message || message.length < 10) {
			errors.message = 'メッセージは10文字以上で入力してください';
		}
		
		if (Object.keys(errors).length > 0) {
			return fail(400, {
				errors,
				name,
				email,
				message
			});
		}
		
		try {
			// 実際にはメール送信やデータベース保存を行う
			console.log('お問い合わせ受信:', {
				name,
				email,
				category,
				message,
				timestamp: new Date().toISOString()
			});
			
			// メール送信のシミュレーション
			await simulateEmailSend({ name, email, category, message });
			
			return {
				success: true
			};
		} catch (error) {
			console.error('お問い合わせ送信エラー:', error);
			return fail(500, {
				errors: {
					general: 'サーバーエラーが発生しました。しばらく後でお試しください。'
				},
				name,
				email,
				message
			});
		}
	}
};

async function simulateEmailSend(data) {
	// 実際のメール送信ロジック
	// 例: SendGrid, NodeMailer, 等を使用
	return new Promise((resolve) => {
		setTimeout(resolve, 1000); // 1秒の遅延をシミュレート
	});
}

デプロイと設定

// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Svelte オプション
	preprocess: vitePreprocess(),

	kit: {
		// アダプター設定(自動検出)
		adapter: adapter(),
		
		// エイリアス設定
		alias: {
			$components: 'src/components',
			$utils: 'src/utils',
			$stores: 'src/stores'
		},
		
		// CSP(Content Security Policy)設定
		csp: {
			mode: 'auto',
			directives: {
				'script-src': ['self']
			}
		},
		
		// プリロード設定
		preload: {
			// プリロードする条件
			default: 'tap'
		},
		
		// サービスワーカー設定
		serviceWorker: {
			register: false
		},
		
		// 環境変数設定
		env: {
			dir: process.cwd(),
			publicPrefix: 'PUBLIC_'
		}
	}
};

export default config;
// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [sveltekit()],
	server: {
		port: 5173,
		strictPort: false,
	},
	preview: {
		port: 4173,
		strictPort: false,
	},
	test: {
		include: ['src/**/*.{test,spec}.{js,ts}']
	}
});
{
  "name": "my-sveltekit-app",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview",
    "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
    "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
    "test": "vitest",
    "lint": "prettier --plugin-search-dir . --check . && eslint .",
    "format": "prettier --plugin-search-dir . --write ."
  },
  "devDependencies": {
    "@sveltejs/adapter-auto": "^2.0.0",
    "@sveltejs/kit": "^1.20.4",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.28.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-svelte": "^2.30.0",
    "prettier": "^2.8.0",
    "prettier-plugin-svelte": "^2.10.1",
    "svelte": "^4.0.5",
    "svelte-check": "^3.4.3",
    "tslib": "^2.4.1",
    "typescript": "^5.0.0",
    "vite": "^4.4.2",
    "vitest": "^0.34.0"
  },
  "type": "module",
  "dependencies": {
    "@sveltejs/adapter-netlify": "^2.0.8",
    "@sveltejs/adapter-vercel": "^3.0.3"
  }
}