forked from mirror/crumbs-original
chore: add agent skills for Svelte 5, SvelteKit, TypeScript, and Drizzle
Install project-level skills via skills.sh CLI: - svelte5-best-practices (runes, snippets, events, migration) - sveltekit-structure (routing, layouts, error boundaries) - sveltekit-data-flow (load functions, form actions, serialization) - typescript-best-practices (type-first development, discriminated unions) - drizzle-migrations (SQLite schema, query patterns) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6fe2978657
commit
3ff08f9e35
@@ -0,0 +1,17 @@
|
||||
# Sveltekit Structure
|
||||
|
||||
SvelteKit structure guidance. Use for routing, layouts, error
|
||||
handling, and SSR. Covers file naming (+page vs +layout vs +server),
|
||||
nested layouts, error boundaries, and hydration.
|
||||
|
||||
## Structure
|
||||
|
||||
- `SKILL.md` - Main skill instructions
|
||||
- `references/` - Detailed documentation loaded as needed
|
||||
- `scripts/` - Executable code for deterministic operations
|
||||
- `assets/` - Templates, images, or other resources
|
||||
|
||||
## Usage
|
||||
|
||||
This skill is automatically discovered by Claude when relevant to the
|
||||
task.
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: sveltekit-structure
|
||||
# IMPORTANT: Keep description on ONE line for Claude Code compatibility
|
||||
# prettier-ignore
|
||||
description: SvelteKit structure guidance. Use for routing, layouts, error handling, SSR, or svelte:boundary. Covers file naming, nested layouts, error boundaries, pending UI, and hydration.
|
||||
---
|
||||
|
||||
# SvelteKit Structure
|
||||
|
||||
## Quick Start
|
||||
|
||||
**File types:** `+page.svelte` (page) | `+layout.svelte` (wrapper) |
|
||||
`+error.svelte` (error boundary) | `+server.ts` (API endpoint)
|
||||
|
||||
**Routes:** `src/routes/about/+page.svelte` → `/about` |
|
||||
`src/routes/posts/[id]/+page.svelte` → `/posts/123`
|
||||
|
||||
**Layouts:** Apply to all child routes. `+layout.svelte` at any level
|
||||
wraps descendants.
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +layout.svelte # Root layout (all pages)
|
||||
├── +page.svelte # Homepage /
|
||||
├── about/+page.svelte # /about
|
||||
└── dashboard/
|
||||
├── +layout.svelte # Dashboard layout (dashboard pages only)
|
||||
├── +page.svelte # /dashboard
|
||||
└── settings/+page.svelte # /dashboard/settings
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- +layout.svelte -->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<nav><!-- Navigation --></nav>
|
||||
<main>{@render children()}</main>
|
||||
<footer><!-- Footer --></footer>
|
||||
```
|
||||
|
||||
## Reference Files
|
||||
|
||||
- [file-naming.md](references/file-naming.md) - File naming
|
||||
conventions
|
||||
- [layout-patterns.md](references/layout-patterns.md) - Nested layouts
|
||||
- [error-handling.md](references/error-handling.md) - +error.svelte
|
||||
placement
|
||||
- [svelte-boundary.md](references/svelte-boundary.md) -
|
||||
Component-level error/pending
|
||||
- [ssr-hydration.md](references/ssr-hydration.md) - SSR and
|
||||
browser-only code
|
||||
|
||||
## Notes
|
||||
|
||||
- Layouts: `{@render children()}` | Errors: +error.svelte _above_
|
||||
failing route
|
||||
- Groups: `(name)` folders don't affect URL | Client-only: check
|
||||
`browser`
|
||||
- **Last verified:** 2025-01-11
|
||||
|
||||
<!--
|
||||
PROGRESSIVE DISCLOSURE GUIDELINES:
|
||||
- Keep this file ~50 lines total (max ~150 lines)
|
||||
- Use 1-2 code blocks only (recommend 1)
|
||||
- Keep description <200 chars for Level 1 efficiency
|
||||
- Move detailed docs to references/ for Level 3 loading
|
||||
- This is Level 2 - quick reference ONLY, not a manual
|
||||
|
||||
LLM WORKFLOW (when editing this file):
|
||||
1. Write/edit SKILL.md
|
||||
2. Format (if formatter available)
|
||||
3. Run: claude-skills-cli validate <path>
|
||||
4. If multi-line description warning: run claude-skills-cli doctor <path>
|
||||
5. Validate again to confirm
|
||||
-->
|
||||
@@ -0,0 +1,274 @@
|
||||
# Error Handling
|
||||
|
||||
## Error Boundary Placement
|
||||
|
||||
**Key rule:** `+error.svelte` must be _above_ the failing route in the
|
||||
hierarchy.
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +error.svelte # Catches errors in all routes below
|
||||
├── +page.svelte # If this errors → uses +error.svelte above
|
||||
└── admin/
|
||||
├── +error.svelte # Catches errors in admin routes
|
||||
└── +page.svelte # If this errors → uses admin/+error.svelte
|
||||
```
|
||||
|
||||
**Wrong:**
|
||||
|
||||
```
|
||||
src/routes/dashboard/
|
||||
├── +layout.svelte # If this errors...
|
||||
└── +error.svelte # This won't catch it (too low)
|
||||
```
|
||||
|
||||
**Right:**
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +error.svelte # Catches dashboard layout errors
|
||||
└── dashboard/
|
||||
├── +layout.svelte
|
||||
└── +error.svelte # Catches dashboard page errors
|
||||
```
|
||||
|
||||
## Error Propagation
|
||||
|
||||
Errors bubble up to the nearest `+error.svelte`:
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +error.svelte # Level 1 (root fallback)
|
||||
└── blog/
|
||||
├── +error.svelte # Level 2 (blog fallback)
|
||||
└── [slug]/
|
||||
├── +layout.server.ts # Error here → blog/+error.svelte
|
||||
├── +page.server.ts # Error here → blog/+error.svelte
|
||||
└── +page.svelte # Error here → blog/+error.svelte
|
||||
```
|
||||
|
||||
**If no error boundary exists at that level, it goes to the parent.**
|
||||
|
||||
## Basic Error Page
|
||||
|
||||
```svelte
|
||||
<!-- +error.svelte -->
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<h1>{$page.status}</h1><p>{$page.error.message}</p>
|
||||
```
|
||||
|
||||
## Custom Error Data
|
||||
|
||||
```typescript
|
||||
// +page.server.ts
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load = async ({ params }) => {
|
||||
const post = await getPost(params.id);
|
||||
|
||||
if (!post) {
|
||||
throw error(404, {
|
||||
message: 'Post not found',
|
||||
postId: params.id,
|
||||
});
|
||||
}
|
||||
|
||||
return { post };
|
||||
};
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- +error.svelte -->
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<h1>{$page.status}</h1>
|
||||
<p>{$page.error.message}</p>
|
||||
|
||||
{#if $page.error.postId}
|
||||
<p>Could not find post with ID: {$page.error.postId}</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Status Code Specific Errors
|
||||
|
||||
```svelte
|
||||
<!-- +error.svelte -->
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
{#if $page.status === 404}
|
||||
<h1>Page Not Found</h1>
|
||||
<p>The page you're looking for doesn't exist.</p>
|
||||
<a href="/">Go home</a>
|
||||
{:else if $page.status === 403}
|
||||
<h1>Access Denied</h1>
|
||||
<p>You don't have permission to view this page.</p>
|
||||
{:else if $page.status === 401}
|
||||
<h1>Unauthorized</h1>
|
||||
<p>Please log in to continue.</p>
|
||||
<a href="/login">Login</a>
|
||||
{:else if $page.status >= 500}
|
||||
<h1>Server Error</h1>
|
||||
<p>Something went wrong on our end. Please try again later.</p>
|
||||
{:else}
|
||||
<h1>Error {$page.status}</h1>
|
||||
<p>{$page.error.message}</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Common Status Codes
|
||||
|
||||
- **400** - Bad Request (malformed request)
|
||||
- **401** - Unauthorized (not logged in)
|
||||
- **403** - Forbidden (logged in but no permission)
|
||||
- **404** - Not Found (resource doesn't exist)
|
||||
- **500** - Internal Server Error (unhandled exception)
|
||||
- **503** - Service Unavailable (server overloaded/down)
|
||||
|
||||
## Error in Layout vs Page
|
||||
|
||||
**Layout error:**
|
||||
|
||||
```typescript
|
||||
// src/routes/dashboard/+layout.server.ts
|
||||
export const load = async ({ locals }) => {
|
||||
if (!locals.user) {
|
||||
throw error(401, 'Not logged in'); // Caught by +error.svelte ABOVE dashboard
|
||||
}
|
||||
return { user: locals.user };
|
||||
};
|
||||
```
|
||||
|
||||
**Page error:**
|
||||
|
||||
```typescript
|
||||
// src/routes/dashboard/settings/+page.server.ts
|
||||
export const load = async () => {
|
||||
throw error(404, 'Settings not found'); // Caught by nearest +error.svelte
|
||||
};
|
||||
```
|
||||
|
||||
## Expected Errors vs Unexpected Errors
|
||||
|
||||
**Expected (use error()):**
|
||||
|
||||
```typescript
|
||||
// User requests invalid resource
|
||||
if (!post) throw error(404, 'Post not found');
|
||||
|
||||
// User lacks permission
|
||||
if (post.authorId !== user.id) throw error(403, 'Not your post');
|
||||
```
|
||||
|
||||
**Unexpected (let it bubble):**
|
||||
|
||||
```typescript
|
||||
// Unhandled exceptions (DB connection fails, etc.)
|
||||
// Will show generic 500 error
|
||||
const posts = await db.query.posts.findMany(); // Might throw
|
||||
```
|
||||
|
||||
## Handling Errors in handleError Hook
|
||||
|
||||
```typescript
|
||||
// src/hooks.server.ts
|
||||
import type { HandleServerError } from '@sveltejs/kit';
|
||||
|
||||
export const handleError: HandleServerError = ({ error, event }) => {
|
||||
// Log to error tracking service
|
||||
console.error('Error:', error, 'Path:', event.url.pathname);
|
||||
|
||||
// Return user-friendly message (don't expose internals)
|
||||
return {
|
||||
message: 'An unexpected error occurred',
|
||||
code: error?.code ?? 'UNKNOWN',
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Error in Load vs Error in Component
|
||||
|
||||
**Load function error (recommended):**
|
||||
|
||||
```typescript
|
||||
// +page.server.ts
|
||||
export const load = async ({ params }) => {
|
||||
const user = await getUser(params.id);
|
||||
if (!user) throw error(404, 'User not found'); // Shows +error.svelte
|
||||
return { user };
|
||||
};
|
||||
```
|
||||
|
||||
**Component error (not ideal):**
|
||||
|
||||
```svelte
|
||||
<!-- +page.svelte - Don't do this -->
|
||||
<script>
|
||||
export let data;
|
||||
|
||||
// If data.user is undefined, component just shows nothing
|
||||
// No error boundary triggered
|
||||
</script>
|
||||
|
||||
<h1>{data.user.name}</h1> <!-- Might crash if user undefined -->
|
||||
```
|
||||
|
||||
**Best practice:** Validate and throw errors in `load` functions, not
|
||||
components.
|
||||
|
||||
## Fallback Error Handling
|
||||
|
||||
Always have a root `+error.svelte`:
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+error.svelte -->
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { dev } from '$app/environment';
|
||||
</script>
|
||||
|
||||
<h1>Oops! Something went wrong</h1>
|
||||
|
||||
{#if dev}
|
||||
<!-- Show details in dev mode -->
|
||||
<pre>{JSON.stringify($page.error, null, 2)}</pre>
|
||||
{:else}
|
||||
<!-- Generic message in production -->
|
||||
<p>We're sorry, but something unexpected happened.</p>
|
||||
{/if}
|
||||
|
||||
<a href="/">Go home</a>
|
||||
```
|
||||
|
||||
## Testing Error Boundaries
|
||||
|
||||
```typescript
|
||||
// +page.server.ts
|
||||
export const load = async ({ url }) => {
|
||||
// Test error page in dev
|
||||
if (url.searchParams.has('test-error')) {
|
||||
throw error(500, 'Test error');
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
```
|
||||
|
||||
Visit `http://localhost:5173/page?test-error` to see error boundary.
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. ✅ Place `+error.svelte` ABOVE failing routes
|
||||
2. ✅ Errors bubble up to nearest error boundary
|
||||
3. ✅ Always have a root `src/routes/+error.svelte`
|
||||
4. ✅ Throw errors in `load` functions, not components
|
||||
5. ✅ Use specific status codes (401, 403, 404, 500)
|
||||
6. ✅ Customize error pages per section (blog, admin, etc.)
|
||||
7. ✅ Use `handleError` hook for logging/monitoring
|
||||
8. ✅ Show detailed errors in dev, generic in production
|
||||
@@ -0,0 +1,69 @@
|
||||
# File Naming Conventions
|
||||
|
||||
## Core Files
|
||||
|
||||
| File | Purpose | Runs | Example |
|
||||
| ------------------- | --------------------- | --------------------- | --------------------------------------------- |
|
||||
| `+page.svelte` | Page component | Client & Server (SSR) | `/routes/about/+page.svelte` → `/about` |
|
||||
| `+page.ts` | Universal load | Client & Server | Data for +page.svelte |
|
||||
| `+page.server.ts` | Server load & actions | Server only | DB queries, form actions |
|
||||
| `+layout.svelte` | Layout wrapper | Client & Server | Wraps child routes |
|
||||
| `+layout.ts` | Layout universal load | Client & Server | Data for +layout.svelte |
|
||||
| `+layout.server.ts` | Layout server load | Server only | Auth, user data |
|
||||
| `+error.svelte` | Error boundary | Client & Server | Shown when error thrown |
|
||||
| `+server.ts` | API endpoint | Server only | `/routes/api/users/+server.ts` → `/api/users` |
|
||||
|
||||
## Route Parameters
|
||||
|
||||
| Pattern | Matches | Example |
|
||||
| -------------- | -------------- | ---------------------------------------------------------------- |
|
||||
| `[id]` | Single param | `/posts/[id]/+page.svelte` → `/posts/123` |
|
||||
| `[slug]` | Single param | `/blog/[slug]/+page.svelte` → `/blog/hello-world` |
|
||||
| `[[optional]]` | Optional param | `/search/[[query]]/+page.svelte` → `/search` or `/search/svelte` |
|
||||
| `[...rest]` | Rest params | `/docs/[...path]/+page.svelte` → `/docs/a/b/c` |
|
||||
|
||||
## Route Groups
|
||||
|
||||
| Pattern | Purpose | URL |
|
||||
| ------------- | ---------------------------- | ---------------------------------------------- |
|
||||
| `(group)` | Group routes (no URL impact) | `/(app)/dashboard/+page.svelte` → `/dashboard` |
|
||||
| `(marketing)` | Separate layouts | Different layout for marketing pages |
|
||||
|
||||
## Special Files
|
||||
|
||||
- `hooks.server.ts` - Server hooks (handle function, runs on every
|
||||
request)
|
||||
- `hooks.client.ts` - Client hooks (runs in browser)
|
||||
- `app.html` - HTML template
|
||||
- `service-worker.ts` - Service worker
|
||||
- `params/*.ts` - Param validators
|
||||
|
||||
## Common Patterns
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +layout.svelte # Root layout
|
||||
├── +layout.ts # Root data
|
||||
├── +page.svelte # Homepage
|
||||
├── +error.svelte # Root error boundary
|
||||
│
|
||||
├── (app)/ # App routes (grouped)
|
||||
│ ├── +layout.svelte # App layout (auth required)
|
||||
│ ├── dashboard/+page.svelte # /dashboard
|
||||
│ └── settings/+page.svelte # /settings
|
||||
│
|
||||
├── (marketing)/ # Marketing routes (grouped)
|
||||
│ ├── +layout.svelte # Marketing layout
|
||||
│ ├── about/+page.svelte # /about
|
||||
│ └── pricing/+page.svelte # /pricing
|
||||
│
|
||||
├── blog/
|
||||
│ ├── +page.svelte # /blog (list)
|
||||
│ └── [slug]/
|
||||
│ ├── +page.svelte # /blog/post-title
|
||||
│ └── +page.server.ts # Load post data
|
||||
│
|
||||
└── api/
|
||||
└── posts/
|
||||
└── +server.ts # API: GET/POST /api/posts
|
||||
```
|
||||
@@ -0,0 +1,387 @@
|
||||
# Layout Patterns
|
||||
|
||||
## Basic Layout
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<header>Header</header>
|
||||
<main>{@render children()}</main>
|
||||
<footer>Footer</footer>
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- Must declare `children` in `$props()`
|
||||
- Use `{@render children()}` to render nested content
|
||||
- Root layout wraps ALL pages
|
||||
|
||||
## Nested Layouts
|
||||
|
||||
Layouts inherit from parent layouts:
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +layout.svelte # Root layout (all pages)
|
||||
└── dashboard/
|
||||
├── +layout.svelte # Dashboard layout (dashboard pages only)
|
||||
└── +page.svelte # Uses both layouts
|
||||
```
|
||||
|
||||
**Result:** Root layout wraps dashboard layout wraps page.
|
||||
|
||||
**Rendering order:**
|
||||
|
||||
```
|
||||
Root +layout.svelte
|
||||
└─ Dashboard +layout.svelte
|
||||
└─ +page.svelte
|
||||
```
|
||||
|
||||
### Example: Nested Layout Pattern
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="app">
|
||||
<nav>Global Nav</nav>
|
||||
{@render children()}
|
||||
</div>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/dashboard/+layout.svelte -->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="dashboard">
|
||||
<aside>Dashboard Sidebar</aside>
|
||||
<main>{@render children()}</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/dashboard/+page.svelte --><h1>Dashboard Home</h1>
|
||||
```
|
||||
|
||||
**Rendered HTML structure:**
|
||||
|
||||
```html
|
||||
<div class="app">
|
||||
<nav>Global Nav</nav>
|
||||
<div class="dashboard">
|
||||
<aside>Dashboard Sidebar</aside>
|
||||
<main>
|
||||
<h1>Dashboard Home</h1>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Layout Groups
|
||||
|
||||
Use `(groups)` to organize without affecting URLs:
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── (marketing)/
|
||||
│ ├── +layout.svelte # Marketing layout
|
||||
│ ├── about/+page.svelte # /about (uses marketing layout)
|
||||
│ └── pricing/+page.svelte # /pricing (uses marketing layout)
|
||||
│
|
||||
└── (app)/
|
||||
├── +layout.svelte # App layout
|
||||
├── dashboard/+page.svelte # /dashboard (uses app layout)
|
||||
└── settings/+page.svelte # /settings (uses app layout)
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- Parentheses make groups invisible in URLs
|
||||
- `/about` route, NOT `/(marketing)/about`
|
||||
- Different layouts for different sections
|
||||
- Useful for authentication boundaries
|
||||
|
||||
### Example: Marketing vs App Layouts
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/(marketing)/+layout.svelte -->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<header class="marketing-header">
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<a href="/about">About</a>
|
||||
<a href="/login">Login</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{@render children()}
|
||||
|
||||
<footer>© 2024 Company</footer>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/(app)/+layout.svelte -->
|
||||
<script>
|
||||
let { children, data } = $props();
|
||||
</script>
|
||||
|
||||
<header class="app-header">
|
||||
<nav>
|
||||
<a href="/dashboard">Dashboard</a>
|
||||
<a href="/settings">Settings</a>
|
||||
<span>Welcome, {data.user.name}</span>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{@render children()}
|
||||
```
|
||||
|
||||
## Reset Layout
|
||||
|
||||
Use `@` in filename to break layout inheritance:
|
||||
|
||||
**NOT RECOMMENDED** - This feature is deprecated in SvelteKit 2+.
|
||||
|
||||
Instead, use layout groups to create separate hierarchies:
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +layout.svelte # Root layout (for most pages)
|
||||
├── (app)/
|
||||
│ └── +layout.svelte # App layout
|
||||
└── (public)/
|
||||
└── +layout.svelte # Public layout (completely separate)
|
||||
```
|
||||
|
||||
## Layout with Data Loading
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.server.ts -->
|
||||
export const load = async ({ locals }) => {
|
||||
// Available to all child routes
|
||||
return {
|
||||
user: locals.user,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script>
|
||||
let { children, data } = $props();
|
||||
</script>
|
||||
|
||||
<header>
|
||||
{#if data.user}
|
||||
<span>Welcome, {data.user.name}</span>
|
||||
{:else}
|
||||
<a href="/login">Login</a>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{@render children()}
|
||||
```
|
||||
|
||||
## Conditional Layout Content
|
||||
|
||||
```svelte
|
||||
<!-- +layout.svelte -->
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{#if !$page.url.pathname.startsWith('/admin')}
|
||||
<header>Public Header</header>
|
||||
{/if}
|
||||
|
||||
{@render children()}
|
||||
|
||||
{#if !$page.url.pathname.includes('/checkout')}
|
||||
<footer>Footer</footer>
|
||||
{/if}
|
||||
```
|
||||
|
||||
**Better approach:** Use layout groups instead of conditionals.
|
||||
|
||||
## Protected Layouts
|
||||
|
||||
```typescript
|
||||
// src/routes/(app)/+layout.server.ts
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async ({ locals }) => {
|
||||
if (!locals.user) {
|
||||
throw redirect(303, '/login');
|
||||
}
|
||||
|
||||
return {
|
||||
user: locals.user,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
All routes under `(app)` group now require authentication.
|
||||
|
||||
## Sharing Layout State
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script>
|
||||
import { setContext } from 'svelte';
|
||||
let { children, data } = $props();
|
||||
|
||||
// Share state with all descendant components
|
||||
setContext('user', data.user);
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- Any child component -->
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
const user = getContext('user');
|
||||
</script>
|
||||
|
||||
<p>Hello, {user.name}</p>
|
||||
```
|
||||
|
||||
## Layout Slot Props (Snippets)
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/dashboard/+layout.svelte -->
|
||||
<script>
|
||||
let { children, header } = $props();
|
||||
</script>
|
||||
|
||||
<div class="dashboard">
|
||||
<aside>Sidebar</aside>
|
||||
<div class="content">
|
||||
{#if header}
|
||||
<header>{@render header()}</header>
|
||||
{/if}
|
||||
<main>{@render children()}</main>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/dashboard/+page.svelte -->
|
||||
{#snippet header()}
|
||||
<h1>Custom Dashboard Header</h1>
|
||||
{/snippet}
|
||||
|
||||
<p>Dashboard content</p>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Auth Boundary
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +layout.svelte # Root (no auth)
|
||||
├── (public)/
|
||||
│ ├── +layout.svelte # Public layout (landing pages)
|
||||
│ ├── +page.svelte # /
|
||||
│ └── about/+page.svelte # /about
|
||||
└── (protected)/
|
||||
├── +layout.server.ts # Check auth, redirect if not logged in
|
||||
├── +layout.svelte # Protected layout (app UI)
|
||||
├── dashboard/+page.svelte # /dashboard
|
||||
└── settings/+page.svelte # /settings
|
||||
```
|
||||
|
||||
### Pattern 2: Multi-tenant with Shared Root
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── +layout.svelte # Root layout (shared)
|
||||
├── admin/
|
||||
│ ├── +layout.svelte # Admin layout
|
||||
│ └── users/+page.svelte # /admin/users
|
||||
└── client/
|
||||
├── +layout.svelte # Client layout
|
||||
└── projects/+page.svelte # /client/projects
|
||||
```
|
||||
|
||||
### Pattern 3: Progressive Enhancement
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { navigating } from '$app/stores';
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="app">
|
||||
{#if $navigating}
|
||||
<div class="loading-bar"></div>
|
||||
{/if}
|
||||
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.loading-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: blue;
|
||||
animation: loading 1s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Layout Best Practices
|
||||
|
||||
1. ✅ Keep root layout minimal (shared across ALL pages)
|
||||
2. ✅ Use layout groups for separate sections
|
||||
3. ✅ Load shared data in layout's load function
|
||||
4. ✅ Use context for sharing state with descendants
|
||||
5. ✅ Avoid too many nested layouts (max 2-3 levels)
|
||||
6. ✅ Don't put auth logic in root layout (use groups)
|
||||
7. ✅ Remember: layouts share data DOWN, not UP
|
||||
8. ❌ Avoid conditionals in layouts (use groups instead)
|
||||
|
||||
## Debugging Layouts
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
let { children } = $props();
|
||||
|
||||
$effect(() => {
|
||||
console.log('Current route:', $page.url.pathname);
|
||||
console.log('Layout data:', $page.data);
|
||||
});
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
```
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- Layouts wrap all child routes
|
||||
- Nested layouts create hierarchy (root → section → page)
|
||||
- Groups `(name)` organize without affecting URLs
|
||||
- Load data in `+layout.server.ts` to share with children
|
||||
- Use context to share reactive state
|
||||
- Keep layouts simple and focused
|
||||
@@ -0,0 +1,92 @@
|
||||
# SSR & Hydration
|
||||
|
||||
## The Problem
|
||||
|
||||
SvelteKit runs on server (SSR) then hydrates in browser. Code using
|
||||
browser APIs (`window`, `document`, `localStorage`) fails on server.
|
||||
|
||||
## Solution: Check for Browser
|
||||
|
||||
```typescript
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// In load function
|
||||
export const load = async () => {
|
||||
const theme = browser ? localStorage.getItem('theme') : 'light';
|
||||
return { theme };
|
||||
};
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- In component -->
|
||||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let data = $state(null);
|
||||
|
||||
// Option 1: Use browser check
|
||||
if (browser) {
|
||||
data = localStorage.getItem('data');
|
||||
}
|
||||
|
||||
// Option 2: Use onMount (only runs in browser)
|
||||
onMount(() => {
|
||||
data = localStorage.getItem('data');
|
||||
});
|
||||
|
||||
// Option 3: Use $effect with browser check
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
data = localStorage.getItem('data');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Using window Without Check
|
||||
|
||||
```typescript
|
||||
// WRONG - fails on server
|
||||
export const load = async () => {
|
||||
const width = window.innerWidth; // ERROR on server
|
||||
return { width };
|
||||
};
|
||||
|
||||
// RIGHT
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export const load = async () => {
|
||||
const width = browser ? window.innerWidth : 1024;
|
||||
return { width };
|
||||
};
|
||||
```
|
||||
|
||||
### ❌ Accessing DOM in Load
|
||||
|
||||
```typescript
|
||||
// WRONG
|
||||
export const load = async () => {
|
||||
const el = document.getElementById('root'); // ERROR on server
|
||||
};
|
||||
|
||||
// RIGHT - Do DOM stuff in onMount
|
||||
export const load = async () => {
|
||||
return {};
|
||||
};
|
||||
// Then in component:
|
||||
onMount(() => {
|
||||
const el = document.getElementById('root');
|
||||
});
|
||||
```
|
||||
|
||||
## Disable SSR (Not Recommended)
|
||||
|
||||
```typescript
|
||||
// +page.ts
|
||||
export const ssr = false; // Disables SSR for this page
|
||||
```
|
||||
|
||||
Only use when absolutely necessary (e.g., heavy Canvas/WebGL).
|
||||
@@ -0,0 +1,150 @@
|
||||
# svelte:boundary Component
|
||||
|
||||
> Available in Svelte 5.3+
|
||||
|
||||
## Two Purposes
|
||||
|
||||
1. **Error boundaries** - catch rendering errors
|
||||
2. **Pending UI** - show loading state while `await` resolves
|
||||
|
||||
## Basic Error Boundary
|
||||
|
||||
```svelte
|
||||
<svelte:boundary onerror={(e, reset) => console.error(e)}>
|
||||
<RiskyComponent />
|
||||
|
||||
{#snippet failed(error, reset)}
|
||||
<p>Error: {error.message}</p>
|
||||
<button onclick={reset}>Try again</button>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
## Pending UI (Loading States)
|
||||
|
||||
> **⚠️ Known Bug:** `<svelte:boundary>` + `{@const await}` causes
|
||||
> infinite navigation loops during client-side page transitions when
|
||||
> pages share async queries. See
|
||||
> [sveltejs/svelte#17717](https://github.com/sveltejs/svelte/issues/17717).
|
||||
> Use `{#await}` blocks until this is fixed.
|
||||
|
||||
With `experimental.async: true` (Svelte 5.36+), `{@const await}` is
|
||||
possible but **not safe for pages with shared queries or navigation**:
|
||||
|
||||
```svelte
|
||||
<!-- ⚠️ Causes navigation loops - use {#await} instead -->
|
||||
<svelte:boundary>
|
||||
{#snippet pending()}
|
||||
<LoadingSpinner />
|
||||
{/snippet}
|
||||
|
||||
{@const data = await loadData()}
|
||||
<DataView {data} />
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
**Safe alternative:**
|
||||
|
||||
```svelte
|
||||
{#await loadData()}
|
||||
<LoadingSpinner />
|
||||
{:then data}
|
||||
<DataView {data} />
|
||||
{:catch error}
|
||||
<p>Error: {error.message}</p>
|
||||
{/await}
|
||||
```
|
||||
|
||||
## Combined Error + Pending
|
||||
|
||||
Use `svelte:boundary` for **error catching only**, with `{#await}` for
|
||||
async data:
|
||||
|
||||
```svelte
|
||||
<svelte:boundary onerror={logError}>
|
||||
{#snippet failed(error, reset)}
|
||||
<p>Failed to load user</p>
|
||||
<button onclick={reset}>Retry</button>
|
||||
{/snippet}
|
||||
|
||||
{#await fetchUser()}
|
||||
<p>Loading user...</p>
|
||||
{:then user}
|
||||
<UserProfile {user} />
|
||||
{:catch error}
|
||||
<p>Error: {error.message}</p>
|
||||
{/await}
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
## What Gets Caught
|
||||
|
||||
**Caught:**
|
||||
|
||||
- Errors during rendering
|
||||
- Errors in `$effect`
|
||||
|
||||
**NOT Caught:**
|
||||
|
||||
- Event handler errors (`onclick`, etc.)
|
||||
- Errors after `setTimeout`
|
||||
- Async errors outside boundary's await
|
||||
|
||||
## vs +error.svelte
|
||||
|
||||
| Feature | svelte:boundary | +error.svelte |
|
||||
| -------- | ----------------------- | ------------- |
|
||||
| Scope | Component subtree | Route segment |
|
||||
| Reset | Built-in reset function | Navigate away |
|
||||
| Pending | Yes (pending snippet) | No |
|
||||
| Use case | Component-level | Page-level |
|
||||
|
||||
## Error Tracking Integration
|
||||
|
||||
```svelte
|
||||
<svelte:boundary
|
||||
onerror={(error, reset) => {
|
||||
// Send to Sentry, LogRocket, etc.
|
||||
errorTracker.captureException(error);
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
|
||||
{#snippet failed(error, reset)}
|
||||
<ErrorFallback {error} {reset} />
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
## Nested Boundaries
|
||||
|
||||
Inner boundary catches first:
|
||||
|
||||
```svelte
|
||||
<svelte:boundary>
|
||||
<!-- Outer fallback -->
|
||||
{#snippet failed(e)}
|
||||
<p>Outer caught: {e.message}</p>
|
||||
{/snippet}
|
||||
|
||||
<svelte:boundary>
|
||||
<!-- Inner fallback -->
|
||||
{#snippet failed(e)}
|
||||
<p>Inner caught: {e.message}</p>
|
||||
{/snippet}
|
||||
|
||||
<ComponentThatMightFail />
|
||||
</svelte:boundary>
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Use `svelte:boundary` for component-level error isolation
|
||||
- Use `+error.svelte` for route-level error pages
|
||||
- `pending` snippet shows only on **initial** load (not on refresh - no flicker)
|
||||
- `failed` snippet replaces content on error
|
||||
- `reset` function lets users retry
|
||||
- Errors in event handlers are NOT caught
|
||||
- Requires `experimental.async: true` in svelte.config.js for `{@const await}`
|
||||
- **⚠️ Bug:** `{@const await}` + navigation causes infinite loops ([#17717](https://github.com/sveltejs/svelte/issues/17717)) — use `{#await}` instead
|
||||
Reference in New Issue
Block a user