
Sveltekit Structure
- 22 installs
- 217 repo stars
- Updated August 3, 2026
- spences10/svelte-claude-skills
Helps with ai & agent building tasks.
About
sveltekit-structure is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- sveltekit-structure
- AI & Agent Building
- AI-coding skill
Sveltekit Structure by the numbers
- 22 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #10,169 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/svelte-claude-skills --skill sveltekit-structureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 22 |
|---|---|
| repo stars | ★ 217 |
| Last updated | August 3, 2026 |
| Repository | spences10/svelte-claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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<!-- +layout.svelte -->
<script>
let { children } = $props();
</script>
<nav><!-- Navigation --></nav>
<main>{@render children()}</main>
<footer><!-- Footer --></footer>Reference Files
- file-naming.md - File naming
conventions
- layout-patterns.md - Nested layouts
- error-handling.md - +error.svelte
placement
- svelte-boundary.md -
Component-level error/pending
- 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 -->
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 instructionsreferences/- Detailed documentation loaded as neededscripts/- Executable code for deterministic operationsassets/- Templates, images, or other resources
Usage
This skill is automatically discovered by Claude when relevant to the task.
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.svelteWrong:
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 errorsError 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.svelteIf no error boundary exists at that level, it goes to the parent.
Basic Error Page
<!-- +error.svelte -->
<script>
import { page } from '$app/stores';
</script>
<h1>{$page.status}</h1><p>{$page.error.message}</p>Custom Error Data
// +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 };
};<!-- +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
<!-- +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:
// 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:
// 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()):
// 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):
// Unhandled exceptions (DB connection fails, etc.)
// Will show generic 500 error
const posts = await db.query.posts.findMany(); // Might throwHandling Errors in handleError Hook
// 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):
// +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):
<!-- +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:
<!-- 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
// +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
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 templateservice-worker.ts- Service workerparams/*.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/postsLayout Patterns
Basic Layout
<!-- src/routes/+layout.svelte -->
<script>
let { children } = $props();
</script>
<header>Header</header>
<main>{@render children()}</main>
<footer>Footer</footer>Key points:
- Must declare
childrenin$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 layoutsResult: Root layout wraps dashboard layout wraps page.
Rendering order:
Root +layout.svelte
└─ Dashboard +layout.svelte
└─ +page.svelteExample: Nested Layout Pattern
<!-- src/routes/+layout.svelte -->
<script>
let { children } = $props();
</script>
<div class="app">
<nav>Global Nav</nav>
{@render children()}
</div><!-- src/routes/dashboard/+layout.svelte -->
<script>
let { children } = $props();
</script>
<div class="dashboard">
<aside>Dashboard Sidebar</aside>
<main>{@render children()}</main>
</div><!-- src/routes/dashboard/+page.svelte --><h1>Dashboard Home</h1>Rendered HTML structure:
<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
/aboutroute, NOT/(marketing)/about- Different layouts for different sections
- Useful for authentication boundaries
Example: Marketing vs App Layouts
<!-- 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><!-- 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
<!-- src/routes/+layout.server.ts -->
export const load = async ({ locals }) => {
// Available to all child routes
return {
user: locals.user,
};
};<!-- 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
<!-- +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
// 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
<!-- 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()}<!-- Any child component -->
<script>
import { getContext } from 'svelte';
const user = getContext('user');
</script>
<p>Hello, {user.name}</p>Layout Slot Props (Snippets)
<!-- 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><!-- 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 # /settingsPattern 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/projectsPattern 3: Progressive Enhancement
<!-- 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
<!-- 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.tsto share with children - Use context to share reactive state
- Keep layouts simple and focused
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
import { browser } from '$app/environment';
// In load function
export const load = async () => {
const theme = browser ? localStorage.getItem('theme') : 'light';
return { theme };
};<!-- 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
// 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
// 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)
// +page.ts
export const ssr = false; // Disables SSR for this pageOnly use when absolutely necessary (e.g., heavy Canvas/WebGL).
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: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)
<svelte:boundary>
{#await loadData()}
<!-- This shows while loading -->
{:then data}
<DataView {data} />
{/await}
{#snippet pending()}
<LoadingSpinner />
{/snippet}
</svelte:boundary>Combined Error + Pending
<svelte:boundary onerror={logError}>
{#await fetchUser()}
<!-- Will show pending snippet -->
{:then user}
<UserProfile {user} />
{/await}
{#snippet pending()}
<p>Loading user...</p>
{/snippet}
{#snippet failed(error, reset)}
<p>Failed to load user</p>
<button onclick={reset}>Retry</button>
{/snippet}
</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: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: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:boundaryfor component-level error isolation - Use
+error.sveltefor route-level error pages pendingsnippet shows during initialawaitresolutionfailedsnippet replaces content on errorresetfunction lets users retry- Errors in event handlers are NOT caught