plugins/guild/skills/svelte-build-deploy/SKILL.md
Pre-loaded SvelteKit knowledge for the developer-svelte agent. Covers project structure, file-based routing (+page, +layout, +server, +error), load functions (universal vs server), form actions, page options (prerender / ssr / csr), building for production, and deployment via adapters (auto, node, static, vercel, cloudflare, netlify). Load this skill before working on any SvelteKit route, server module, or build configuration. Trigger phrases include "sveltekit routing", "+page", "+server", "load function", "form actions", "sveltekit adapter", "svelte deploy".
npx skillsauth add hirogakatageri/hirokata svelte-build-deployInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Authoritative reference for working in SvelteKit projects: project layout, routing, data loading, mutations, and getting an app to production.
my-app/
├─ src/
│ ├─ lib/ # $lib alias — shared modules
│ │ └─ server/ # $lib/server — server-only code
│ ├─ params/ # custom param matchers
│ ├─ routes/ # filesystem routes (root)
│ ├─ app.html # HTML shell
│ ├─ error.html # static fallback error page
│ ├─ hooks.server.ts # server hooks
│ ├─ hooks.client.ts # client hooks
│ └─ hooks.ts # shared (universal) hooks
├─ static/ # served verbatim at /
├─ tests/
├─ svelte.config.js # Kit + adapter config
├─ vite.config.ts
├─ tsconfig.json
└─ package.json
Key path aliases:
$lib → src/lib$lib/server/* → server-only; importing from client code is a build error$app/* → SvelteKit runtime modules$env/static/private, $env/static/public, $env/dynamic/* → environment variablesInside src/routes/, each directory is a URL segment. Files prefixed with + are route files:
| File | Role |
|------|------|
| +page.svelte | Page UI |
| +page.ts | Universal load (runs server + client) |
| +page.server.ts | Server load and form actions |
| +layout.svelte | Wraps child pages; renders {@render children()} |
| +layout.ts / +layout.server.ts | Layout-level load |
| +error.svelte | Error boundary |
| +server.ts | API endpoint (HTTP method exports) |
Dynamic segments use brackets:
[slug] — required parameter[[slug]] — optional parameter[...rest] — catch-all[id=int] — parameter matcher (custom validator in src/params/int.ts)Rule of thumb:
+server.ts.+layout and +error apply to their directory and all subdirectories.+page.svelte<script lang="ts">
import type { PageProps } from './$types';
let { data, form }: PageProps = $props();
</script>
<h1>{data.title}</h1>
PageProps (since 2.16) packages data, form, and route params into one type. Use it instead of typing fields individually.
+layout.svelte<script lang="ts">
import type { LayoutProps } from './$types';
let { data, children }: LayoutProps = $props();
</script>
<nav>...</nav>
{@render children()}
The layout MUST render children somewhere, or pages won't appear.
+error.svelte<script>
import { page } from '$app/state';
</script>
<h1>{page.status}</h1>
<p>{page.error?.message}</p>
SvelteKit walks up the tree to find the closest +error.svelte. Errors thrown from the root layout's load are caught by the static src/error.html fallback.
+server.ts (API endpoints)import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = ({ url }) => {
return json({ now: Date.now() });
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
return json({ received: body });
};
// catch-all for unhandled methods
export const fallback: RequestHandler = ({ request }) =>
new Response(`No handler for ${request.method}`, { status: 405 });
+server.ts files are NOT wrapped by +layout files. To run logic before every request, use hooks.server.ts.
load FunctionsTwo flavors:
| File | Type | Runs on |
|------|------|---------|
| +page.ts, +layout.ts | Universal | Server (SSR + during prerender) AND client (after hydration) |
| +page.server.ts, +layout.server.ts | Server-only | Server only — return value is serialized to the client |
// +page.server.ts
import { error } from '@sveltejs/kit';
import * as db from '$lib/server/database';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, locals }) => {
if (!locals.user) error(401, 'login required');
const post = await db.getPost(params.slug);
if (!post) error(404, 'not found');
return { post };
};
When to use which:
load when you need a database, private env vars, secrets, or cookies / locals access.load when fetching from public APIs (avoids the proxy hop), or when you must return non-serializable values (component constructors, classes).event.data.Important load traits:
BigInt, Date, Map, Set, RegExp, repeated/cyclical refs).fetch provided in the load event — use it instead of global fetch. It inherits cookies, supports relative URLs server-side, short-circuits internal +server.ts calls, and inlines responses into the SSR HTML to avoid double-fetches on hydration..catch(() => {}) to lazily-resolving promises so an unhandled rejection doesn't crash the server.await parent() lets a child load read its parent layout's data — but call it after anything independent to avoid waterfalls.hooks.server.ts (sets locals.user) and per-page +page.server.ts guards. Avoid relying on layout loads for auth, because layouts don't always rerun on client navigation.Re-running: SvelteKit re-runs a load when:
params field it accessed changedurl property it accessed changedsearchParams key it accessed changedawait parent()-ed re-ranfetched or depends()-ed on was passed to invalidate(url)invalidateAll() was calleddepends('app:custom-key') lets you create your own invalidation tokens.
import { error, redirect } from '@sveltejs/kit';
throw_unused; // ❌ NOT needed in v2 — calling error()/redirect() throws
error(404, 'not found');
redirect(303, '/login');
In SvelteKit 2, error() and redirect() throw on your behalf — don't throw error(...). Don't call them inside a try { } catch { } block — the catch will swallow the redirect/error.
getRequestEventInside server code, import { getRequestEvent } from '$app/server' lets shared helpers (auth guards, logging) reach the current request without prop-drilling the event.
Server-side mutations triggered by <form> submissions. Defined in +page.server.ts:
// +page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => ({
user: locals.user
});
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = String(data.get('email') ?? '');
if (!email) return fail(400, { email, missing: true });
cookies.set('session', await login(email), { path: '/' });
redirect(303, '/dashboard');
},
logout: async ({ cookies }) => {
cookies.delete('session', { path: '/' });
redirect(303, '/');
}
};
<!-- +page.svelte -->
<script lang="ts">
import type { PageProps } from './$types';
import { enhance } from '$app/forms';
let { form }: PageProps = $props();
</script>
<form method="POST" use:enhance>
<input name="email" value={form?.email ?? ''} />
{#if form?.missing}<p>Email required</p>{/if}
<button>Sign in</button>
</form>
<form method="POST" action="?/logout" use:enhance>
<button>Log out</button>
</form>
fail(status, data) for validation errors — keeps user input around as form prop.redirect(303, ...) after a successful POST follows PRG.use:enhance upgrades to fetch+JS but degrades gracefully without it.action="?/name".For type-safe RPC-style mutations, also see remote functions in svelte-advanced.
Exported from +page.ts, +page.server.ts, +layout.ts, or +layout.server.ts:
export const prerender = true; // build-time HTML, served as static
export const ssr = false; // disable server rendering (SPA)
export const csr = false; // disable client hydration (no JS)
export const trailingSlash = 'never'; // 'never' | 'always' | 'ignore'
export const config = { /* adapter-specific */ };
prerender = true requires the page's data to be deterministic. For dynamic routes, also export an entries() from +page.server.ts listing the params to pre-render.prerender = 'auto' prerenders if possible, falls back to SSR otherwise.ssr = false + prerender = false produces a true SPA (page only renders on the client).csr = false ships zero JS for that page — useful for static content pages.Three files in src/:
hooks.server.ts — server lifecycle: handle, handleError, handleFetch, inithooks.client.ts — client lifecycle: handleError, inithooks.ts — universal transport (custom de/serialization for non-default types)// hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const session = event.cookies.get('session');
event.locals.user = session ? await getUser(session) : null;
const response = await resolve(event);
response.headers.set('x-frame-options', 'DENY');
return response;
};
Multiple handles compose with sequence from @sveltejs/kit/hooks.
npm run build # → vite build
npm run preview # local preview of the production build
Build output goes to .svelte-kit/output (universal) and the adapter writes the deployment artifact.
Configured in svelte.config.js:
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: { adapter: adapter() }
};
| Adapter | Use when |
|---------|----------|
| @sveltejs/adapter-auto | Default; detects Vercel / Netlify / Cloudflare Pages from env. Fine for prototyping; pin a specific adapter for production. |
| @sveltejs/adapter-node | Self-hosted Node.js server (Docker, VPS). Outputs build/index.js runnable with node build. |
| @sveltejs/adapter-static | Pure static site (SSG). Requires every route to be prerenderable; set fallback for SPA mode. |
| @sveltejs/adapter-vercel | Vercel — supports edge functions, ISR, image optimization. |
| @sveltejs/adapter-cloudflare | Cloudflare Pages / Workers — edge runtime, KV / D1 via platform.env. |
| @sveltejs/adapter-netlify | Netlify — supports functions and edge functions. |
adapter-nodeOutputs a standalone Node app:
import adapter from '@sveltejs/adapter-node';
export default { kit: { adapter: adapter({ out: 'build' }) } };
Run with node build. Configurable env vars: PORT, HOST, ORIGIN, BODY_SIZE_LIMIT. Behind a reverse proxy, set ORIGIN to the public URL so SvelteKit knows the canonical host.
adapter-staticPure static output:
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: undefined, // 'index.html' or '200.html' for SPA mode
precompress: false
})
}
};
Every page must have prerender = true (or be reachable via prerendering). For SPA mode, set fallback: '200.html' and ssr = false at the root layout.
Each platform-specific adapter wires up the platform's serverless or edge runtime. For Cloudflare, server event.platform.env exposes bindings (KV, D1, R2). For Vercel, export const config = { runtime: 'edge' } opts a route into the edge runtime. For Netlify, edge functions are similar via the edge: true option.
Four imports — pick the right one:
| Module | Static (build-time) / Dynamic | Public / Private |
|--------|-------------------------------|------------------|
| $env/static/private | static | private (server only) |
| $env/static/public | static | public (must start with PUBLIC_) |
| $env/dynamic/private | dynamic (per-request) | private |
| $env/dynamic/public | dynamic | public |
Static imports inline values at build time and tree-shake unused ones. Dynamic imports read from process.env (or platform equivalent) at runtime — required when the same build runs in multiple environments.
app.htmlEdit src/app.html to customize the HTML shell. Required placeholders:
<!DOCTYPE html>
<html lang="en">
<head>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
data-sveltekit-preload-data and data-sveltekit-preload-code attributes on <a> or any ancestor enable preloading.
$lib/server/* from a client component → build error. Move shared helpers out of $lib/server.redirect() inside a try/catch → catch swallows the redirect.throw error(...) (SvelteKit 1 syntax) → in v2, just error(...) — it throws itself.fetch in a load → use the fetch from the load event for cookie-forwarding and SSR caching.prerender = true, the route's load must be deterministic at build time.entries() for prerendered dynamic routes → SvelteKit can't know the param values without it.invalidateAll() constantly → it re-runs every load. Prefer invalidate(url) or depends() tokens.development
This skill should be used when the user reports an error, bug, or unexpected behavior and wants it diagnosed and fixed. Trigger on phrases like "check this error", "check this bug", "here's an error", "here's a bug", "I have an error", "I have a bug", "found a bug", "got an error", "debug this", "this is broken", "fix this error", "verify and fix", or any message that includes a stack trace or error output. Runs a structured workflow: gather context, investigate configured log/code sources, report root cause with ranked solutions, then apply a test-driven fix.
testing
This skill should be used when the user says "check svelte env vars", "check environment variables", "validate env vars", "check env var patterns", "audit environment variables", "audit env vars", "check SvelteKit env", "svelte env check", or any phrase asking to audit or validate SvelteKit environment variable usage patterns.
data-ai
Internal skill used by the session-tracker logger agent to append a session entry to .logs/YYYY-MM-DD-log.md, creating the file and directory if needed. Not user-invocable.
data-ai
Internal skill used by the session-tracker logger agent to query git for committed and uncommitted changes in the past 28 hours. Not user-invocable.