Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test) - Delete old documentation, Docker files, and k8s configs - Add comprehensive Svelte 5 reference library (svelte-reference/) - Update ModeSwitch orchestrator: - Fix port to 3005 (never use 3000/3001) - Add API endpoints for mode switching - Add bun-types and proper tsconfig - Clean up mode folder mappings
This commit is contained in:
@@ -0,0 +1,728 @@
|
||||
# SvelteKit Routing & Data Loading Guide
|
||||
|
||||
## Overview
|
||||
|
||||
SvelteKit is the official framework for building Svelte applications. It provides file-based routing, server-side rendering, and powerful data loading primitives.
|
||||
|
||||
---
|
||||
|
||||
## 📁 File-Based Routing
|
||||
|
||||
### Route Structure
|
||||
```
|
||||
src/routes/
|
||||
├── +page.svelte # → /
|
||||
├── +layout.svelte # Layout for all routes
|
||||
├── about/
|
||||
│ └── +page.svelte # → /about
|
||||
├── blog/
|
||||
│ ├── +page.svelte # → /blog
|
||||
│ └── [slug]/
|
||||
│ └── +page.svelte # → /blog/:slug
|
||||
├── users/
|
||||
│ ├── +page.svelte # → /users
|
||||
│ ├── [id]/
|
||||
│ │ ├── +page.svelte # → /users/:id
|
||||
│ │ └── +page.server.ts # Server-only load
|
||||
│ └── [...rest]/
|
||||
│ └── +page.svelte # → /users/* (catch-all)
|
||||
└── api/
|
||||
└── posts/
|
||||
└── +server.ts # → /api/posts (API endpoint)
|
||||
```
|
||||
|
||||
### Route Files Explained
|
||||
|
||||
| File | Purpose | Runs On |
|
||||
|------|---------|---------|
|
||||
| `+page.svelte` | Page component | Client |
|
||||
| `+page.ts` | Load data for page | Client & Server |
|
||||
| `+page.server.ts` | Load data (server only) | Server only |
|
||||
| `+layout.svelte` | Shared layout | Client |
|
||||
| `+layout.ts` | Load data for layout | Client & Server |
|
||||
| `+layout.server.ts` | Load layout data (server) | Server only |
|
||||
| `+server.ts` | API endpoint | Server only |
|
||||
| `+error.svelte` | Error page | Client |
|
||||
|
||||
---
|
||||
|
||||
## 📥 Data Loading
|
||||
|
||||
### Basic Load Function (+page.ts)
|
||||
|
||||
```typescript
|
||||
// src/routes/posts/+page.ts
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ fetch }) => {
|
||||
const response = await fetch('/api/posts');
|
||||
const posts = await response.json();
|
||||
|
||||
return {
|
||||
posts
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/posts/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<h1>Posts</h1>
|
||||
|
||||
{#each data.posts as post}
|
||||
<article>
|
||||
<h2>{post.title}</h2>
|
||||
<p>{post.excerpt}</p>
|
||||
</article>
|
||||
{/each}
|
||||
```
|
||||
|
||||
### Server-Only Load (+page.server.ts)
|
||||
|
||||
```typescript
|
||||
// src/routes/dashboard/+page.server.ts
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/database';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, cookies }) => {
|
||||
// Access server-only resources
|
||||
const session = cookies.get('session');
|
||||
|
||||
if (!session) {
|
||||
redirect(303, '/login');
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { sessionId: session }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const stats = await db.stats.findMany({
|
||||
where: { userId: user.id }
|
||||
});
|
||||
|
||||
return {
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email
|
||||
},
|
||||
stats
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Load with Parameters
|
||||
|
||||
```typescript
|
||||
// src/routes/blog/[slug]/+page.ts
|
||||
import type { PageLoad } from './$types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
const response = await fetch(`/api/posts/${params.slug}`);
|
||||
|
||||
if (!response.ok) {
|
||||
error(404, 'Post not found');
|
||||
}
|
||||
|
||||
const post = await response.json();
|
||||
|
||||
return {
|
||||
post
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Parent Data Access
|
||||
|
||||
```typescript
|
||||
// src/routes/+layout.ts
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch }) => {
|
||||
const response = await fetch('/api/user');
|
||||
const user = response.ok ? await response.json() : null;
|
||||
|
||||
return { user };
|
||||
};
|
||||
|
||||
// src/routes/dashboard/+page.ts
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
// Access data from parent layout
|
||||
const { user } = await parent();
|
||||
|
||||
if (!user) {
|
||||
// Handle unauthenticated state
|
||||
}
|
||||
|
||||
return {
|
||||
// Additional data
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Parallel Data Loading
|
||||
|
||||
```typescript
|
||||
// src/routes/dashboard/+page.ts
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ fetch }) => {
|
||||
// Load data in parallel
|
||||
const [postsRes, statsRes, notificationsRes] = await Promise.all([
|
||||
fetch('/api/posts'),
|
||||
fetch('/api/stats'),
|
||||
fetch('/api/notifications')
|
||||
]);
|
||||
|
||||
return {
|
||||
posts: postsRes.json(),
|
||||
stats: statsRes.json(),
|
||||
notifications: notificationsRes.json()
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📤 Form Actions
|
||||
|
||||
### Basic Form Action
|
||||
|
||||
```typescript
|
||||
// src/routes/login/+page.server.ts
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/database';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
if (locals.user) {
|
||||
redirect(303, '/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const email = data.get('email')?.toString();
|
||||
const password = data.get('password')?.toString();
|
||||
|
||||
if (!email || !password) {
|
||||
return fail(400, {
|
||||
email,
|
||||
error: 'Email and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({ where: { email } });
|
||||
|
||||
if (!user || !await verifyPassword(password, user.passwordHash)) {
|
||||
return fail(401, {
|
||||
email,
|
||||
error: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
const session = await createSession(user.id);
|
||||
cookies.set('session', session.id, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7 // 1 week
|
||||
});
|
||||
|
||||
redirect(303, '/dashboard');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/login/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { form }: { form: ActionData } = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
</script>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
loading = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
{#if form?.error}
|
||||
<p class="error">{form.error}</p>
|
||||
{/if}
|
||||
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={form?.email ?? ''}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Password
|
||||
<input type="password" name="password" required />
|
||||
</label>
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Named Actions
|
||||
|
||||
```typescript
|
||||
// src/routes/settings/+page.server.ts
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
updateProfile: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
// Handle profile update
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
changePassword: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
// Handle password change
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
deleteAccount: async ({ request, cookies }) => {
|
||||
// Handle account deletion
|
||||
cookies.delete('session', { path: '/' });
|
||||
redirect(303, '/');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- Use named actions -->
|
||||
<form method="POST" action="?/updateProfile" use:enhance>
|
||||
<!-- Profile form -->
|
||||
</form>
|
||||
|
||||
<form method="POST" action="?/changePassword" use:enhance>
|
||||
<!-- Password form -->
|
||||
</form>
|
||||
|
||||
<form method="POST" action="?/deleteAccount" use:enhance>
|
||||
<button type="submit">Delete Account</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API Routes (+server.ts)
|
||||
|
||||
### RESTful API Endpoint
|
||||
|
||||
```typescript
|
||||
// src/routes/api/posts/+server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/database';
|
||||
|
||||
// GET /api/posts
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const limit = Number(url.searchParams.get('limit')) || 10;
|
||||
const offset = Number(url.searchParams.get('offset')) || 0;
|
||||
|
||||
const posts = await db.post.findMany({
|
||||
take: limit,
|
||||
skip: offset,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
return json(posts);
|
||||
};
|
||||
|
||||
// POST /api/posts
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
if (!locals.user) {
|
||||
error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const post = await db.post.create({
|
||||
data: {
|
||||
title: body.title,
|
||||
content: body.content,
|
||||
authorId: locals.user.id
|
||||
}
|
||||
});
|
||||
|
||||
return json(post, { status: 201 });
|
||||
};
|
||||
|
||||
// src/routes/api/posts/[id]/+server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
|
||||
// GET /api/posts/:id
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const post = await db.post.findUnique({
|
||||
where: { id: params.id }
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
error(404, 'Post not found');
|
||||
}
|
||||
|
||||
return json(post);
|
||||
};
|
||||
|
||||
// PUT /api/posts/:id
|
||||
export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
||||
const post = await db.post.findUnique({
|
||||
where: { id: params.id }
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
error(404, 'Post not found');
|
||||
}
|
||||
|
||||
if (post.authorId !== locals.user?.id) {
|
||||
error(403, 'Forbidden');
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const updated = await db.post.update({
|
||||
where: { id: params.id },
|
||||
data: body
|
||||
});
|
||||
|
||||
return json(updated);
|
||||
};
|
||||
|
||||
// DELETE /api/posts/:id
|
||||
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||
const post = await db.post.findUnique({
|
||||
where: { id: params.id }
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
error(404, 'Post not found');
|
||||
}
|
||||
|
||||
if (post.authorId !== locals.user?.id) {
|
||||
error(403, 'Forbidden');
|
||||
}
|
||||
|
||||
await db.post.delete({
|
||||
where: { id: params.id }
|
||||
});
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Layouts
|
||||
|
||||
### Root Layout
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import '../app.css';
|
||||
|
||||
let { data, children }: { data: LayoutData; children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="app">
|
||||
<Header user={data.user} />
|
||||
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
```
|
||||
|
||||
### Nested Layouts
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/dashboard/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||
|
||||
let { data, children }: { data: LayoutData; children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="dashboard">
|
||||
<Sidebar />
|
||||
|
||||
<div class="content">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Layout Groups
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── (auth)/ # Group - no route segment
|
||||
│ ├── +layout.svelte # Shared layout for auth pages
|
||||
│ ├── login/
|
||||
│ │ └── +page.svelte
|
||||
│ └── register/
|
||||
│ └── +page.svelte
|
||||
├── (app)/ # Group - no route segment
|
||||
│ ├── +layout.svelte # Shared layout for app pages
|
||||
│ ├── dashboard/
|
||||
│ │ └── +page.svelte
|
||||
│ └── settings/
|
||||
│ └── +page.svelte
|
||||
└── +layout.svelte # Root layout
|
||||
```
|
||||
|
||||
### Breaking Out of Layout
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/special/+page@.svelte -->
|
||||
<!-- The @. means use root layout, not parent -->
|
||||
<script lang="ts">
|
||||
// This page uses +layout.svelte directly, skipping any nested layouts
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔀 Navigation
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { goto, invalidate, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
// Navigate to a page
|
||||
async function navigate() {
|
||||
await goto('/dashboard');
|
||||
}
|
||||
|
||||
// Navigate with options
|
||||
async function navigateWithState() {
|
||||
await goto('/search', {
|
||||
replaceState: true, // Replace instead of push
|
||||
keepFocus: true, // Keep focus on current element
|
||||
noScroll: true, // Don't scroll to top
|
||||
state: { query: 'test' } // Pass state
|
||||
});
|
||||
}
|
||||
|
||||
// Invalidate load functions
|
||||
async function refresh() {
|
||||
await invalidate('/api/posts'); // Rerun loads that depend on this
|
||||
// or
|
||||
await invalidateAll(); // Rerun all load functions
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Current page info -->
|
||||
<p>Current path: {$page.url.pathname}</p>
|
||||
<p>Query: {$page.url.searchParams.get('q')}</p>
|
||||
```
|
||||
|
||||
### Link Prefetching
|
||||
|
||||
```svelte
|
||||
<!-- Preload on hover (default) -->
|
||||
<a href="/about">About</a>
|
||||
|
||||
<!-- Preload on viewport entry -->
|
||||
<a href="/posts" data-sveltekit-preload-data="hover">Posts</a>
|
||||
|
||||
<!-- Disable preloading -->
|
||||
<a href="/external" data-sveltekit-preload-data="off">External</a>
|
||||
|
||||
<!-- Preload immediately when link is rendered -->
|
||||
<a href="/dashboard" data-sveltekit-preload-data="eager">Dashboard</a>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Error Handling
|
||||
|
||||
### Error Pages
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+error.svelte -->
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<div class="error">
|
||||
<h1>{$page.status}</h1>
|
||||
<p>{$page.error?.message}</p>
|
||||
<a href="/">Go home</a>
|
||||
</div>
|
||||
|
||||
<!-- src/routes/blog/+error.svelte -->
|
||||
<!-- More specific error page for /blog routes -->
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<div class="blog-error">
|
||||
<h1>Blog Post Not Found</h1>
|
||||
<p>{$page.error?.message}</p>
|
||||
<a href="/blog">View all posts</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Throwing Errors
|
||||
|
||||
```typescript
|
||||
// In load functions or actions
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async ({ params }) => {
|
||||
const post = await getPost(params.id);
|
||||
|
||||
if (!post) {
|
||||
error(404, {
|
||||
message: 'Post not found',
|
||||
code: 'POST_NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
if (post.draft) {
|
||||
redirect(303, '/blog');
|
||||
}
|
||||
|
||||
return { post };
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Hooks
|
||||
|
||||
### Server Hooks (hooks.server.ts)
|
||||
|
||||
```typescript
|
||||
// src/hooks.server.ts
|
||||
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit';
|
||||
|
||||
// Runs for every request
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Authentication
|
||||
const session = event.cookies.get('session');
|
||||
if (session) {
|
||||
event.locals.user = await getUserFromSession(session);
|
||||
}
|
||||
|
||||
// Performance timing
|
||||
const start = Date.now();
|
||||
|
||||
const response = await resolve(event, {
|
||||
// Transform HTML
|
||||
transformPageChunk: ({ html }) => html.replace('%theme%', 'dark')
|
||||
});
|
||||
|
||||
// Log request duration
|
||||
console.log(`${event.request.method} ${event.url.pathname} - ${Date.now() - start}ms`);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
// Customize server-side fetch
|
||||
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
|
||||
// Add auth header to internal API calls
|
||||
if (request.url.startsWith('https://api.internal.com')) {
|
||||
request.headers.set('Authorization', `Bearer ${API_KEY}`);
|
||||
}
|
||||
|
||||
return fetch(request);
|
||||
};
|
||||
|
||||
// Handle unexpected errors
|
||||
export const handleError: HandleServerError = async ({ error, event }) => {
|
||||
console.error('Server error:', error);
|
||||
|
||||
// Report to error tracking service
|
||||
await reportError(error, event);
|
||||
|
||||
return {
|
||||
message: 'An unexpected error occurred',
|
||||
code: 'INTERNAL_ERROR'
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Client Hooks (hooks.client.ts)
|
||||
|
||||
```typescript
|
||||
// src/hooks.client.ts
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
|
||||
export const handleError: HandleClientError = async ({ error, event }) => {
|
||||
console.error('Client error:', error);
|
||||
|
||||
// Report to error tracking
|
||||
await reportClientError(error);
|
||||
|
||||
return {
|
||||
message: 'Something went wrong',
|
||||
code: 'CLIENT_ERROR'
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Best Practices
|
||||
|
||||
### ✅ Do:
|
||||
- Use `+page.server.ts` for sensitive data
|
||||
- Return typed data from load functions
|
||||
- Use `use:enhance` for progressive enhancement
|
||||
- Handle loading and error states
|
||||
- Use route groups for layout organization
|
||||
- Validate form data server-side
|
||||
|
||||
### ❌ Don't:
|
||||
- Put secrets in `+page.ts` (runs on client)
|
||||
- Forget to await `parent()` in child loads
|
||||
- Block navigation with slow loads
|
||||
- Ignore accessibility in forms
|
||||
- Hardcode URLs (use `$page` store)
|
||||
Reference in New Issue
Block a user