SvelteKit
Svelte公式のフルスタックフレームワーク。コンパイル時最適化による高性能と小さなバンドルサイズを実現。2025年急成長中の注目株。
GitHub概要
sveltejs/kit
web development, streamlined
スター19,601
ウォッチ156
フォーク2,096
作成日:2020年10月15日
言語:JavaScript
ライセンス:MIT License
トピックス
hacktoberfestsvelte
スター履歴
データ取得日時: 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と比較してライブラリが少ない
- コミュニティサイズ: 他のメタフレームワークと比較して小さい
- ツーリング成熟度: 開発ツールや統合ソリューションが発展途上
- ドキュメント量: 日本語の学習リソースが限定的
- 企業採用: エンタープライズでの採用事例が少ない
- サードパーティ統合: 一部のライブラリで統合が複雑
主要リンク
- SvelteKit公式サイト
- SvelteKit公式ドキュメント
- SvelteKit GitHub リポジトリ
- SvelteKit アダプター
- SvelteKit コミュニティ
- SvelteKit チュートリアル
書き方の例
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>© 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"
}
}