feat: add sveltekit, astro, hono, and pydantic-ai skills (#336)
This commit is contained in:
359
skills/astro/SKILL.md
Normal file
359
skills/astro/SKILL.md
Normal file
@@ -0,0 +1,359 @@
|
||||
---
|
||||
name: astro
|
||||
description: "Build content-focused websites with Astro — zero JS by default, islands architecture, multi-framework components, and Markdown/MDX support."
|
||||
category: frontend
|
||||
risk: safe
|
||||
source: community
|
||||
date_added: "2026-03-18"
|
||||
author: suhaibjanjua
|
||||
tags: [astro, ssg, ssr, islands, content, markdown, mdx, performance]
|
||||
tools: [claude, cursor, gemini]
|
||||
---
|
||||
|
||||
# Astro Web Framework
|
||||
|
||||
## Overview
|
||||
|
||||
Astro is a web framework designed for content-rich websites — blogs, docs, portfolios, marketing sites, and e-commerce. Its core innovation is the **Islands Architecture**: by default, Astro ships zero JavaScript to the browser. Interactive components are selectively hydrated as isolated "islands." Astro supports React, Vue, Svelte, Solid, and other UI frameworks simultaneously in the same project, letting you pick the right tool per component.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Use when building a blog, documentation site, marketing page, or portfolio
|
||||
- Use when performance and Core Web Vitals are the top priority
|
||||
- Use when the project is content-heavy with Markdown or MDX files
|
||||
- Use when you want SSG (static) output with optional SSR for dynamic routes
|
||||
- Use when the user asks about `.astro` files, `Astro.props`, content collections, or `client:` directives
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step 1: Project Setup
|
||||
|
||||
```bash
|
||||
npm create astro@latest my-site
|
||||
cd my-site
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Add integrations as needed:
|
||||
|
||||
```bash
|
||||
npx astro add tailwind # Tailwind CSS
|
||||
npx astro add react # React component support
|
||||
npx astro add mdx # MDX support
|
||||
npx astro add sitemap # Auto sitemap.xml
|
||||
npx astro add vercel # Vercel SSR adapter
|
||||
```
|
||||
|
||||
Project structure:
|
||||
|
||||
```
|
||||
src/
|
||||
pages/ ← File-based routing (.astro, .md, .mdx)
|
||||
layouts/ ← Reusable page shells
|
||||
components/ ← UI components (.astro, .tsx, .vue, etc.)
|
||||
content/ ← Type-safe content collections (Markdown/MDX)
|
||||
styles/ ← Global CSS
|
||||
public/ ← Static assets (copied as-is)
|
||||
astro.config.mjs ← Framework config
|
||||
```
|
||||
|
||||
### Step 2: Astro Component Syntax
|
||||
|
||||
`.astro` files have a code fence at the top (server-only) and a template below:
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/components/Card.astro
|
||||
// This block runs on the server ONLY — never in the browser
|
||||
interface Props {
|
||||
title: string;
|
||||
href: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { title, href, description } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="card">
|
||||
<h2><a href={href}>{title}</a></h2>
|
||||
<p>{description}</p>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
/* Scoped to this component automatically */
|
||||
.card { border: 1px solid #eee; padding: 1rem; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Step 3: File-Based Pages and Routing
|
||||
|
||||
```
|
||||
src/pages/index.astro → /
|
||||
src/pages/about.astro → /about
|
||||
src/pages/blog/[slug].astro → /blog/:slug (dynamic)
|
||||
src/pages/blog/[...path].astro → /blog/* (catch-all)
|
||||
```
|
||||
|
||||
Dynamic route with `getStaticPaths`:
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/blog/[slug].astro
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map(post => ({
|
||||
params: { slug: post.slug },
|
||||
props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<h1>{post.data.title}</h1>
|
||||
<Content />
|
||||
```
|
||||
|
||||
### Step 4: Content Collections
|
||||
|
||||
Content collections give you type-safe access to Markdown and MDX files:
|
||||
|
||||
```typescript
|
||||
// src/content/config.ts
|
||||
import { z, defineCollection } from 'astro:content';
|
||||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
date: z.coerce.date(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
draft: z.boolean().default(false),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/blog/index.astro
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter(p => !p.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
---
|
||||
|
||||
<ul>
|
||||
{posts.map(post => (
|
||||
<li>
|
||||
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
|
||||
<time>{post.data.date.toLocaleDateString()}</time>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
```
|
||||
|
||||
### Step 5: Islands — Selective Hydration
|
||||
|
||||
By default, UI framework components render to static HTML with no JS. Use `client:` directives to hydrate:
|
||||
|
||||
```astro
|
||||
---
|
||||
import Counter from '../components/Counter.tsx'; // React component
|
||||
import VideoPlayer from '../components/VideoPlayer.svelte';
|
||||
---
|
||||
|
||||
<!-- Static HTML — no JavaScript sent to browser -->
|
||||
<Counter initialCount={0} />
|
||||
|
||||
<!-- Hydrate immediately on page load -->
|
||||
<Counter initialCount={0} client:load />
|
||||
|
||||
<!-- Hydrate when the component scrolls into view -->
|
||||
<VideoPlayer src="/demo.mp4" client:visible />
|
||||
|
||||
<!-- Hydrate only when browser is idle -->
|
||||
<Analytics client:idle />
|
||||
|
||||
<!-- Hydrate only on a specific media query -->
|
||||
<MobileMenu client:media="(max-width: 768px)" />
|
||||
```
|
||||
|
||||
### Step 6: Layouts
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/layouts/BaseLayout.astro
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
const { title, description = 'My Astro Site' } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
</head>
|
||||
<body>
|
||||
<nav>...</nav>
|
||||
<main>
|
||||
<slot /> <!-- page content renders here -->
|
||||
</main>
|
||||
<footer>...</footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/about.astro
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="About Us">
|
||||
<h1>About Us</h1>
|
||||
<p>Welcome to our company...</p>
|
||||
</BaseLayout>
|
||||
```
|
||||
|
||||
### Step 7: SSR Mode (On-Demand Rendering)
|
||||
|
||||
Enable SSR for dynamic pages by setting an adapter:
|
||||
|
||||
```javascript
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import vercel from '@astrojs/vercel/serverless';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'hybrid', // 'static' | 'server' | 'hybrid'
|
||||
adapter: vercel(),
|
||||
});
|
||||
```
|
||||
|
||||
Opt individual pages into SSR with `export const prerender = false`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Blog with RSS Feed
|
||||
|
||||
```typescript
|
||||
// src/pages/rss.xml.ts
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection('blog');
|
||||
return rss({
|
||||
title: 'My Blog',
|
||||
description: 'Latest posts',
|
||||
site: context.site,
|
||||
items: posts.map(post => ({
|
||||
title: post.data.title,
|
||||
pubDate: post.data.date,
|
||||
link: `/blog/${post.slug}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: API Endpoint (SSR)
|
||||
|
||||
```typescript
|
||||
// src/pages/api/subscribe.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const { email } = await request.json();
|
||||
|
||||
if (!email) {
|
||||
return new Response(JSON.stringify({ error: 'Email required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
await addToNewsletter(email);
|
||||
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
||||
};
|
||||
```
|
||||
|
||||
### Example 3: React Component as Island
|
||||
|
||||
```tsx
|
||||
// src/components/SearchBox.tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function SearchBox() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
async function search(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
|
||||
setResults(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={search}>
|
||||
<input value={query} onChange={e => setQuery(e.target.value)} />
|
||||
<button type="submit">Search</button>
|
||||
<ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
import SearchBox from '../components/SearchBox.tsx';
|
||||
---
|
||||
<!-- Hydrated immediately — this island is interactive -->
|
||||
<SearchBox client:load />
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- ✅ Keep most components as static `.astro` files — only hydrate what must be interactive
|
||||
- ✅ Use content collections for all Markdown/MDX content — you get type safety and auto-validation
|
||||
- ✅ Prefer `client:visible` over `client:load` for below-the-fold components to reduce initial JS
|
||||
- ✅ Use `import.meta.env` for environment variables — prefix public vars with `PUBLIC_`
|
||||
- ✅ Add `<ViewTransitions />` from `astro:transitions` for smooth page navigation without a full SPA
|
||||
- ❌ Don't use `client:load` on every component — this defeats Astro's performance advantage
|
||||
- ❌ Don't put secrets in `.astro` frontmatter that gets used in client-facing templates
|
||||
- ❌ Don't skip `getStaticPaths` for dynamic routes in static mode — builds will fail
|
||||
|
||||
## Security & Safety Notes
|
||||
|
||||
- Frontmatter code in `.astro` files runs server-side only and is never exposed to the browser.
|
||||
- Use `import.meta.env.PUBLIC_*` only for non-sensitive values. Private env vars (no `PUBLIC_` prefix) are never sent to the client.
|
||||
- When using SSR mode, validate all `Astro.request` inputs before database queries or API calls.
|
||||
- Sanitize any user-supplied content before rendering with `set:html` — it bypasses auto-escaping.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Problem:** JavaScript from a React/Vue component doesn't run in the browser
|
||||
**Solution:** Add a `client:` directive (`client:load`, `client:visible`, etc.) — without it, components render as static HTML only.
|
||||
|
||||
- **Problem:** `getStaticPaths` data is stale after content updates during dev
|
||||
**Solution:** Astro's dev server watches content files — restart if changes to `content/config.ts` are not reflected.
|
||||
|
||||
- **Problem:** `Astro.props` type is `any` — no autocomplete
|
||||
**Solution:** Define a `Props` interface or type in the frontmatter and Astro will infer it automatically.
|
||||
|
||||
- **Problem:** CSS from a `.astro` component bleeds into other components
|
||||
**Solution:** Styles in `.astro` `<style>` tags are automatically scoped. Use `:global()` only when intentionally targeting children.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `@sveltekit` — When you need a full-stack framework with reactive UI (vs Astro's content focus)
|
||||
- `@nextjs-app-router-patterns` — When you need a React-first full-stack framework
|
||||
- `@tailwind-patterns` — Styling Astro sites with Tailwind CSS
|
||||
- `@progressive-web-app` — Adding PWA capabilities to an Astro site
|
||||
348
skills/hono/SKILL.md
Normal file
348
skills/hono/SKILL.md
Normal file
@@ -0,0 +1,348 @@
|
||||
---
|
||||
name: hono
|
||||
description: "Build ultra-fast web APIs and full-stack apps with Hono — runs on Cloudflare Workers, Deno, Bun, Node.js, and any WinterCG-compatible runtime."
|
||||
category: backend
|
||||
risk: safe
|
||||
source: community
|
||||
date_added: "2026-03-18"
|
||||
author: suhaibjanjua
|
||||
tags: [hono, edge, cloudflare-workers, bun, deno, api, typescript, web-standards]
|
||||
tools: [claude, cursor, gemini]
|
||||
---
|
||||
|
||||
# Hono Web Framework
|
||||
|
||||
## Overview
|
||||
|
||||
Hono (炎, "flame" in Japanese) is a small, ultrafast web framework built on Web Standards (`Request`/`Response`/`fetch`). It runs anywhere: Cloudflare Workers, Deno Deploy, Bun, Node.js, AWS Lambda, and any WinterCG-compatible runtime — with the same code. Hono's router is one of the fastest available, and its middleware system, built-in JSX support, and RPC client make it a strong choice for edge APIs, BFFs, and lightweight full-stack apps.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Use when building a REST or RPC API for edge deployment (Cloudflare Workers, Deno Deploy)
|
||||
- Use when you need a minimal but type-safe server framework for Bun or Node.js
|
||||
- Use when building a Backend for Frontend (BFF) layer with low latency requirements
|
||||
- Use when migrating from Express but wanting better TypeScript support and edge compatibility
|
||||
- Use when the user asks about Hono routing, middleware, `c.req`, `c.json`, or `hc()` RPC client
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step 1: Project Setup
|
||||
|
||||
**Cloudflare Workers (recommended for edge):**
|
||||
```bash
|
||||
npm create hono@latest my-api
|
||||
# Select: cloudflare-workers
|
||||
cd my-api
|
||||
npm install
|
||||
npm run dev # Wrangler local dev
|
||||
npm run deploy # Deploy to Cloudflare
|
||||
```
|
||||
|
||||
**Bun / Node.js:**
|
||||
```bash
|
||||
mkdir my-api && cd my-api
|
||||
bun init
|
||||
bun add hono
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/index.ts (Bun)
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.get('/', c => c.text('Hello Hono!'));
|
||||
|
||||
export default {
|
||||
port: 3000,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Routing
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Basic methods
|
||||
app.get('/posts', c => c.json({ posts: [] }));
|
||||
app.post('/posts', c => c.json({ created: true }, 201));
|
||||
app.put('/posts/:id', c => c.json({ updated: true }));
|
||||
app.delete('/posts/:id', c => c.json({ deleted: true }));
|
||||
|
||||
// Route params and query strings
|
||||
app.get('/posts/:id', async c => {
|
||||
const id = c.req.param('id');
|
||||
const format = c.req.query('format') ?? 'json';
|
||||
return c.json({ id, format });
|
||||
});
|
||||
|
||||
// Wildcard
|
||||
app.get('/static/*', c => c.text('static file'));
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
**Chained routing:**
|
||||
```typescript
|
||||
app
|
||||
.get('/users', listUsers)
|
||||
.post('/users', createUser)
|
||||
.get('/users/:id', getUser)
|
||||
.patch('/users/:id', updateUser)
|
||||
.delete('/users/:id', deleteUser);
|
||||
```
|
||||
|
||||
### Step 3: Middleware
|
||||
|
||||
Hono middleware works exactly like `fetch` interceptors — before and after handlers:
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono';
|
||||
import { logger } from 'hono/logger';
|
||||
import { cors } from 'hono/cors';
|
||||
import { bearerAuth } from 'hono/bearer-auth';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Built-in middleware
|
||||
app.use('*', logger());
|
||||
app.use('/api/*', cors({ origin: 'https://myapp.com' }));
|
||||
app.use('/api/admin/*', bearerAuth({ token: process.env.API_TOKEN! }));
|
||||
|
||||
// Custom middleware
|
||||
app.use('*', async (c, next) => {
|
||||
c.set('requestId', crypto.randomUUID());
|
||||
await next();
|
||||
c.header('X-Request-Id', c.get('requestId'));
|
||||
});
|
||||
```
|
||||
|
||||
**Available built-in middleware:** `logger`, `cors`, `csrf`, `etag`, `cache`, `basicAuth`, `bearerAuth`, `jwt`, `compress`, `bodyLimit`, `timeout`, `prettyJSON`, `secureHeaders`.
|
||||
|
||||
### Step 4: Request and Response Helpers
|
||||
|
||||
```typescript
|
||||
app.post('/submit', async c => {
|
||||
// Parse body
|
||||
const body = await c.req.json<{ name: string; email: string }>();
|
||||
const form = await c.req.formData();
|
||||
const text = await c.req.text();
|
||||
|
||||
// Headers and cookies
|
||||
const auth = c.req.header('authorization');
|
||||
const token = getCookie(c, 'session');
|
||||
|
||||
// Responses
|
||||
return c.json({ ok: true }); // JSON
|
||||
return c.text('hello'); // plain text
|
||||
return c.html('<h1>Hello</h1>'); // HTML
|
||||
return c.redirect('/dashboard', 302); // redirect
|
||||
return new Response(stream, { status: 200 }); // raw Response
|
||||
});
|
||||
```
|
||||
|
||||
### Step 5: Zod Validator Middleware
|
||||
|
||||
```typescript
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
|
||||
const createPostSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
body: z.string().min(1),
|
||||
tags: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
app.post(
|
||||
'/posts',
|
||||
zValidator('json', createPostSchema),
|
||||
async c => {
|
||||
const data = c.req.valid('json'); // fully typed
|
||||
const post = await db.post.create({ data });
|
||||
return c.json(post, 201);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Step 6: Route Groups and App Composition
|
||||
|
||||
```typescript
|
||||
// src/routes/posts.ts
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const posts = new Hono();
|
||||
|
||||
posts.get('/', async c => { /* list posts */ });
|
||||
posts.post('/', async c => { /* create post */ });
|
||||
posts.get('/:id', async c => { /* get post */ });
|
||||
|
||||
export default posts;
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
import { Hono } from 'hono';
|
||||
import posts from './routes/posts';
|
||||
import users from './routes/users';
|
||||
|
||||
const app = new Hono().basePath('/api');
|
||||
|
||||
app.route('/posts', posts);
|
||||
app.route('/users', users);
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
### Step 7: RPC Client (End-to-End Type Safety)
|
||||
|
||||
Hono's RPC mode exports route types that the `hc` client consumes — similar to tRPC but using fetch conventions:
|
||||
|
||||
```typescript
|
||||
// server: src/routes/posts.ts
|
||||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
|
||||
const posts = new Hono()
|
||||
.get('/', c => c.json({ posts: [{ id: '1', title: 'Hello' }] }))
|
||||
.post(
|
||||
'/',
|
||||
zValidator('json', z.object({ title: z.string() })),
|
||||
async c => {
|
||||
const { title } = c.req.valid('json');
|
||||
return c.json({ id: '2', title }, 201);
|
||||
}
|
||||
);
|
||||
|
||||
export default posts;
|
||||
export type PostsType = typeof posts;
|
||||
```
|
||||
|
||||
```typescript
|
||||
// client: src/client.ts
|
||||
import { hc } from 'hono/client';
|
||||
import type { PostsType } from '../server/routes/posts';
|
||||
|
||||
const client = hc<PostsType>('/api/posts');
|
||||
|
||||
// Fully typed — autocomplete on routes, params, and responses
|
||||
const { posts } = await client.$get().json();
|
||||
const newPost = await client.$post({ json: { title: 'New Post' } }).json();
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: JWT Auth Middleware
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono';
|
||||
import { jwt, sign } from 'hono/jwt';
|
||||
|
||||
const app = new Hono();
|
||||
const SECRET = process.env.JWT_SECRET!;
|
||||
|
||||
app.post('/login', async c => {
|
||||
const { email, password } = await c.req.json();
|
||||
const user = await validateUser(email, password);
|
||||
if (!user) return c.json({ error: 'Invalid credentials' }, 401);
|
||||
|
||||
const token = await sign({ sub: user.id, exp: Math.floor(Date.now() / 1000) + 3600 }, SECRET);
|
||||
return c.json({ token });
|
||||
});
|
||||
|
||||
app.use('/api/*', jwt({ secret: SECRET }));
|
||||
app.get('/api/me', async c => {
|
||||
const payload = c.get('jwtPayload');
|
||||
const user = await getUserById(payload.sub);
|
||||
return c.json(user);
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
### Example 2: Cloudflare Workers with D1 Database
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
import { Hono } from 'hono';
|
||||
|
||||
type Bindings = {
|
||||
DB: D1Database;
|
||||
API_TOKEN: string;
|
||||
};
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
app.get('/users', async c => {
|
||||
const { results } = await c.env.DB.prepare('SELECT * FROM users LIMIT 50').all();
|
||||
return c.json(results);
|
||||
});
|
||||
|
||||
app.post('/users', async c => {
|
||||
const { name, email } = await c.req.json();
|
||||
await c.env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
|
||||
.bind(name, email)
|
||||
.run();
|
||||
return c.json({ created: true }, 201);
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
### Example 3: Streaming Response
|
||||
|
||||
```typescript
|
||||
import { stream, streamText } from 'hono/streaming';
|
||||
|
||||
app.get('/stream', c =>
|
||||
streamText(c, async stream => {
|
||||
for (const chunk of ['Hello', ' ', 'World']) {
|
||||
await stream.write(chunk);
|
||||
await stream.sleep(100);
|
||||
}
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- ✅ Use route groups (sub-apps) to keep handlers in separate files — `app.route('/users', usersRouter)`
|
||||
- ✅ Use `zValidator` for all request body, query, and param validation
|
||||
- ✅ Type Cloudflare Workers bindings with the `Bindings` generic: `new Hono<{ Bindings: Env }>()`
|
||||
- ✅ Use the RPC client (`hc`) when your frontend and backend share the same repo
|
||||
- ✅ Prefer returning `c.json()`/`c.text()` over `new Response()` for cleaner code
|
||||
- ❌ Don't use Node.js-specific APIs (`fs`, `path`, `process`) if you want edge portability
|
||||
- ❌ Don't add heavy dependencies — Hono's value is its tiny footprint on edge runtimes
|
||||
- ❌ Don't skip middleware typing — use generics (`Variables`, `Bindings`) to keep `c.get()` type-safe
|
||||
|
||||
## Security & Safety Notes
|
||||
|
||||
- Always validate input with `zValidator` before using data from requests.
|
||||
- Use Hono's built-in `csrf` middleware on mutation endpoints when serving HTML/forms.
|
||||
- For Cloudflare Workers, store secrets in `wrangler.toml` `[vars]` (non-secret) or `wrangler secret put` (secret) — never hardcode them in source.
|
||||
- When using `bearerAuth` or `jwt`, ensure tokens are validated server-side — do not trust client-provided user IDs.
|
||||
- Rate-limit sensitive endpoints (auth, password reset) with Cloudflare Rate Limiting or a custom middleware.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Problem:** Handler returns `undefined` — response is empty
|
||||
**Solution:** Always `return` a response from handlers: `return c.json(...)` not just `c.json(...)`.
|
||||
|
||||
- **Problem:** Middleware runs after the response is sent
|
||||
**Solution:** Call `await next()` before post-response logic; Hono runs code after `next()` as the response travels back up the chain.
|
||||
|
||||
- **Problem:** `c.env` is undefined on Node.js
|
||||
**Solution:** Cloudflare `env` bindings only exist in Workers. Use `process.env` on Node.js.
|
||||
|
||||
- **Problem:** Route not matching — gets a 404
|
||||
**Solution:** Check that `app.route('/prefix', subRouter)` uses the same prefix your client calls. Sub-routers should **not** repeat the prefix in their own routes.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `@cloudflare-workers-expert` — Deep dive into Cloudflare Workers platform specifics
|
||||
- `@trpc-fullstack` — Alternative RPC approach for TypeScript full-stack apps
|
||||
- `@zod-validation-expert` — Detailed Zod schema patterns used with `@hono/zod-validator`
|
||||
- `@nodejs-backend-patterns` — When you need a Node.js-specific backend (not edge)
|
||||
350
skills/pydantic-ai/SKILL.md
Normal file
350
skills/pydantic-ai/SKILL.md
Normal file
@@ -0,0 +1,350 @@
|
||||
---
|
||||
name: pydantic-ai
|
||||
description: "Build production-ready AI agents with PydanticAI — type-safe tool use, structured outputs, dependency injection, and multi-model support."
|
||||
category: ai-agents
|
||||
risk: safe
|
||||
source: community
|
||||
date_added: "2026-03-18"
|
||||
author: suhaibjanjua
|
||||
tags: [pydantic-ai, ai-agents, llm, openai, anthropic, gemini, tool-use, structured-output, python]
|
||||
tools: [claude, cursor, gemini]
|
||||
---
|
||||
|
||||
# PydanticAI — Typed AI Agents in Python
|
||||
|
||||
## Overview
|
||||
|
||||
PydanticAI is a Python agent framework from the Pydantic team that brings the same type-safety and validation guarantees as Pydantic to LLM-based applications. It supports structured outputs (validated with Pydantic models), dependency injection for testability, streamed responses, multi-turn conversations, and tool use — across OpenAI, Anthropic, Google Gemini, Groq, Mistral, and Ollama. Use this skill when building production AI agents, chatbots, or LLM pipelines where correctness and testability matter.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Use when building Python AI agents that call tools and return structured data
|
||||
- Use when you need validated, typed LLM outputs (not raw strings)
|
||||
- Use when you want to write unit tests for agent logic without hitting a real LLM
|
||||
- Use when switching between LLM providers without rewriting agent code
|
||||
- Use when the user asks about `Agent`, `@agent.tool`, `RunContext`, `ModelRetry`, or `result_type`
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step 1: Installation
|
||||
|
||||
```bash
|
||||
pip install pydantic-ai
|
||||
|
||||
# Install extras for specific providers
|
||||
pip install 'pydantic-ai[openai]' # OpenAI / Azure OpenAI
|
||||
pip install 'pydantic-ai[anthropic]' # Anthropic Claude
|
||||
pip install 'pydantic-ai[gemini]' # Google Gemini
|
||||
pip install 'pydantic-ai[groq]' # Groq
|
||||
pip install 'pydantic-ai[vertexai]' # Google Vertex AI
|
||||
```
|
||||
|
||||
### Step 2: A Minimal Agent
|
||||
|
||||
```python
|
||||
from pydantic_ai import Agent
|
||||
|
||||
# Simple agent — returns a plain string
|
||||
agent = Agent(
|
||||
'anthropic:claude-sonnet-4-6',
|
||||
system_prompt='You are a helpful assistant. Be concise.',
|
||||
)
|
||||
|
||||
result = agent.run_sync('What is the capital of Japan?')
|
||||
print(result.data) # "Tokyo"
|
||||
print(result.usage()) # Usage(requests=1, request_tokens=..., response_tokens=...)
|
||||
```
|
||||
|
||||
### Step 3: Structured Output with Pydantic Models
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from pydantic_ai import Agent
|
||||
|
||||
class MovieReview(BaseModel):
|
||||
title: str
|
||||
year: int
|
||||
rating: float # 0.0 to 10.0
|
||||
summary: str
|
||||
recommended: bool
|
||||
|
||||
agent = Agent(
|
||||
'openai:gpt-4o',
|
||||
result_type=MovieReview,
|
||||
system_prompt='You are a film critic. Return structured reviews.',
|
||||
)
|
||||
|
||||
result = agent.run_sync('Review Inception (2010)')
|
||||
review = result.data # Fully typed MovieReview instance
|
||||
print(f"{review.title} ({review.year}): {review.rating}/10")
|
||||
print(f"Recommended: {review.recommended}")
|
||||
```
|
||||
|
||||
### Step 4: Tool Use
|
||||
|
||||
Register tools with `@agent.tool` — the LLM can call them during a run:
|
||||
|
||||
```python
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from pydantic import BaseModel
|
||||
import httpx
|
||||
|
||||
class WeatherReport(BaseModel):
|
||||
city: str
|
||||
temperature_c: float
|
||||
condition: str
|
||||
|
||||
weather_agent = Agent(
|
||||
'anthropic:claude-sonnet-4-6',
|
||||
result_type=WeatherReport,
|
||||
system_prompt='Get current weather for the requested city.',
|
||||
)
|
||||
|
||||
@weather_agent.tool
|
||||
async def get_temperature(ctx: RunContext, city: str) -> dict:
|
||||
"""Fetch the current temperature for a city from the weather API."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(f'https://wttr.in/{city}?format=j1')
|
||||
data = r.json()
|
||||
return {
|
||||
'temp_c': float(data['current_condition'][0]['temp_C']),
|
||||
'description': data['current_condition'][0]['weatherDesc'][0]['value'],
|
||||
}
|
||||
|
||||
import asyncio
|
||||
result = asyncio.run(weather_agent.run('What is the weather in Tokyo?'))
|
||||
print(result.data)
|
||||
```
|
||||
|
||||
### Step 5: Dependency Injection
|
||||
|
||||
Inject services (database, HTTP clients, config) into agents for testability:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from pydantic import BaseModel
|
||||
|
||||
@dataclass
|
||||
class Deps:
|
||||
db: Database
|
||||
user_id: str
|
||||
|
||||
class SupportResponse(BaseModel):
|
||||
message: str
|
||||
escalate: bool
|
||||
|
||||
support_agent = Agent(
|
||||
'openai:gpt-4o-mini',
|
||||
deps_type=Deps,
|
||||
result_type=SupportResponse,
|
||||
system_prompt='You are a support agent. Use the tools to help customers.',
|
||||
)
|
||||
|
||||
@support_agent.tool
|
||||
async def get_order_history(ctx: RunContext[Deps]) -> list[dict]:
|
||||
"""Fetch recent orders for the current user."""
|
||||
return await ctx.deps.db.get_orders(ctx.deps.user_id, limit=5)
|
||||
|
||||
@support_agent.tool
|
||||
async def create_refund(ctx: RunContext[Deps], order_id: str, reason: str) -> dict:
|
||||
"""Initiate a refund for a specific order."""
|
||||
return await ctx.deps.db.create_refund(order_id, reason, ctx.deps.user_id)
|
||||
|
||||
# Usage
|
||||
async def handle_support(user_id: str, message: str):
|
||||
deps = Deps(db=get_db(), user_id=user_id)
|
||||
result = await support_agent.run(message, deps=deps)
|
||||
return result.data
|
||||
```
|
||||
|
||||
### Step 6: Testing with TestModel
|
||||
|
||||
Write unit tests without real LLM calls:
|
||||
|
||||
```python
|
||||
from pydantic_ai.models.test import TestModel
|
||||
|
||||
def test_support_agent_escalates():
|
||||
with support_agent.override(model=TestModel()):
|
||||
# TestModel returns a minimal valid response matching result_type
|
||||
result = support_agent.run_sync(
|
||||
'I want to cancel my account',
|
||||
deps=Deps(db=FakeDb(), user_id='user-123'),
|
||||
)
|
||||
# Test the structure, not the LLM's exact words
|
||||
assert isinstance(result.data, SupportResponse)
|
||||
assert isinstance(result.data.escalate, bool)
|
||||
```
|
||||
|
||||
**FunctionModel** for deterministic test responses:
|
||||
|
||||
```python
|
||||
from pydantic_ai.models.function import FunctionModel, ModelContext
|
||||
|
||||
def my_model(messages, info):
|
||||
return ModelResponse(parts=[TextPart('Always this response')])
|
||||
|
||||
with agent.override(model=FunctionModel(my_model)):
|
||||
result = agent.run_sync('anything')
|
||||
```
|
||||
|
||||
### Step 7: Streaming Responses
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from pydantic_ai import Agent
|
||||
|
||||
agent = Agent('anthropic:claude-sonnet-4-6')
|
||||
|
||||
async def stream_response():
|
||||
async with agent.run_stream('Write a haiku about Python') as result:
|
||||
async for chunk in result.stream_text():
|
||||
print(chunk, end='', flush=True)
|
||||
print() # newline
|
||||
print(f"Total tokens: {result.usage()}")
|
||||
|
||||
asyncio.run(stream_response())
|
||||
```
|
||||
|
||||
### Step 8: Multi-Turn Conversations
|
||||
|
||||
```python
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.messages import ModelMessagesTypeAdapter
|
||||
|
||||
agent = Agent('openai:gpt-4o', system_prompt='You are a helpful assistant.')
|
||||
|
||||
# First turn
|
||||
result1 = agent.run_sync('My name is Alice.')
|
||||
history = result1.all_messages()
|
||||
|
||||
# Second turn — passes conversation history
|
||||
result2 = agent.run_sync('What is my name?', message_history=history)
|
||||
print(result2.data) # "Your name is Alice."
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Code Review Agent
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_ai import Agent
|
||||
from typing import Literal
|
||||
|
||||
class CodeReview(BaseModel):
|
||||
quality: Literal['excellent', 'good', 'needs_work', 'poor']
|
||||
issues: list[str] = Field(default_factory=list)
|
||||
suggestions: list[str] = Field(default_factory=list)
|
||||
approved: bool
|
||||
|
||||
code_review_agent = Agent(
|
||||
'anthropic:claude-sonnet-4-6',
|
||||
result_type=CodeReview,
|
||||
system_prompt="""
|
||||
You are a senior engineer performing code review.
|
||||
Evaluate code quality, identify issues, and provide actionable suggestions.
|
||||
Set approved=True only for good or excellent quality code with no security issues.
|
||||
""",
|
||||
)
|
||||
|
||||
def review_code(diff: str) -> CodeReview:
|
||||
result = code_review_agent.run_sync(f"Review this code:\n\n{diff}")
|
||||
return result.data
|
||||
```
|
||||
|
||||
### Example 2: Agent with Retry Logic
|
||||
|
||||
```python
|
||||
from pydantic_ai import Agent, ModelRetry
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
class StrictJson(BaseModel):
|
||||
value: int
|
||||
|
||||
@field_validator('value')
|
||||
def must_be_positive(cls, v):
|
||||
if v <= 0:
|
||||
raise ValueError('value must be positive')
|
||||
return v
|
||||
|
||||
agent = Agent('openai:gpt-4o-mini', result_type=StrictJson)
|
||||
|
||||
@agent.result_validator
|
||||
async def validate_result(ctx, result: StrictJson) -> StrictJson:
|
||||
if result.value > 1000:
|
||||
raise ModelRetry('Value must be under 1000. Try again with a smaller number.')
|
||||
return result
|
||||
```
|
||||
|
||||
### Example 3: Multi-Agent Pipeline
|
||||
|
||||
```python
|
||||
from pydantic_ai import Agent
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ResearchSummary(BaseModel):
|
||||
key_points: list[str]
|
||||
conclusion: str
|
||||
|
||||
class BlogPost(BaseModel):
|
||||
title: str
|
||||
body: str
|
||||
meta_description: str
|
||||
|
||||
researcher = Agent('openai:gpt-4o', result_type=ResearchSummary)
|
||||
writer = Agent('anthropic:claude-sonnet-4-6', result_type=BlogPost)
|
||||
|
||||
async def research_and_write(topic: str) -> BlogPost:
|
||||
# Stage 1: research
|
||||
research = await researcher.run(f'Research the topic: {topic}')
|
||||
|
||||
# Stage 2: write based on research
|
||||
post = await writer.run(
|
||||
f'Write a blog post about: {topic}\n\nResearch:\n' +
|
||||
'\n'.join(f'- {p}' for p in research.data.key_points) +
|
||||
f'\n\nConclusion: {research.data.conclusion}'
|
||||
)
|
||||
return post.data
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- ✅ Always define `result_type` with a Pydantic model — avoid returning raw strings in production
|
||||
- ✅ Use `deps_type` with a dataclass for dependency injection — makes agents testable
|
||||
- ✅ Use `TestModel` in unit tests — never hit a real LLM in CI
|
||||
- ✅ Add `@agent.result_validator` for business-logic checks beyond Pydantic validation
|
||||
- ✅ Use `run_stream` for long outputs in user-facing applications to show progressive results
|
||||
- ❌ Don't put secrets (API keys) in `Agent()` arguments — use environment variables
|
||||
- ❌ Don't share a single `Agent` instance across async tasks if deps differ — create per-request instances or use `agent.run()` with per-call `deps`
|
||||
- ❌ Don't catch `ValidationError` broadly — let PydanticAI retry with `ModelRetry` for recoverable LLM output errors
|
||||
|
||||
## Security & Safety Notes
|
||||
|
||||
- Set API keys via environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.) — never hardcode them.
|
||||
- Validate all tool inputs before passing to external systems — use Pydantic models or manual checks.
|
||||
- Tools that mutate data (write to DB, send emails, call payment APIs) should require explicit user confirmation before the agent invokes them in production.
|
||||
- Log `result.all_messages()` for audit trails when agents perform consequential actions.
|
||||
- Set `retries=` limits on `Agent()` to prevent runaway loops on persistent validation failures.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Problem:** `ValidationError` on every LLM response — structured output never validates
|
||||
**Solution:** Simplify `result_type` fields. Use `Optional` and `default` where appropriate. The model may struggle with overly strict schemas.
|
||||
|
||||
- **Problem:** Tool is never called by the LLM
|
||||
**Solution:** Write a clear, specific docstring for the tool function — PydanticAI sends the docstring as the tool description to the LLM.
|
||||
|
||||
- **Problem:** `RunContext` dependency is `None` inside a tool
|
||||
**Solution:** Pass `deps=` when calling `agent.run()` or `agent.run_sync()`. Dependencies are not set globally.
|
||||
|
||||
- **Problem:** `asyncio.run()` error when calling `agent.run()` inside FastAPI
|
||||
**Solution:** Use `await agent.run()` directly in async FastAPI route handlers — don't wrap in `asyncio.run()`.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `@langchain-architecture` — Alternative Python AI framework (more flexible, less type-safe)
|
||||
- `@llm-application-dev-ai-assistant` — General LLM application development patterns
|
||||
- `@fastapi-templates` — Serving PydanticAI agents via FastAPI endpoints
|
||||
- `@agent-orchestration-multi-agent-optimize` — Orchestrating multiple PydanticAI agents
|
||||
286
skills/sveltekit/SKILL.md
Normal file
286
skills/sveltekit/SKILL.md
Normal file
@@ -0,0 +1,286 @@
|
||||
---
|
||||
name: sveltekit
|
||||
description: "Build full-stack web applications with SvelteKit — file-based routing, SSR, SSG, API routes, and form actions in one framework."
|
||||
category: frontend
|
||||
risk: safe
|
||||
source: community
|
||||
date_added: "2026-03-18"
|
||||
author: suhaibjanjua
|
||||
tags: [svelte, sveltekit, fullstack, ssr, ssg, typescript]
|
||||
tools: [claude, cursor, gemini]
|
||||
---
|
||||
|
||||
# SvelteKit Full-Stack Development
|
||||
|
||||
## Overview
|
||||
|
||||
SvelteKit is the official full-stack framework built on top of Svelte. It provides file-based routing, server-side rendering (SSR), static site generation (SSG), API routes, and progressive form actions — all with Svelte's compile-time reactivity model that ships zero runtime overhead to the browser. Use this skill when building fast, modern web apps where both DX and performance matter.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Use when building a new full-stack web application with Svelte
|
||||
- Use when you need SSR or SSG with fine-grained control per route
|
||||
- Use when migrating a SPA to a framework with server capabilities
|
||||
- Use when working on a project that needs file-based routing and collocated API endpoints
|
||||
- Use when the user asks about `+page.svelte`, `+layout.svelte`, `load` functions, or form actions
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step 1: Project Setup
|
||||
|
||||
```bash
|
||||
npm create svelte@latest my-app
|
||||
cd my-app
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Choose **Skeleton project** + **TypeScript** + **ESLint/Prettier** when prompted.
|
||||
|
||||
Directory structure after scaffolding:
|
||||
|
||||
```
|
||||
src/
|
||||
routes/
|
||||
+page.svelte ← Root page component
|
||||
+layout.svelte ← Root layout (wraps all pages)
|
||||
+error.svelte ← Error boundary
|
||||
lib/
|
||||
server/ ← Server-only code (never bundled to client)
|
||||
components/ ← Shared components
|
||||
app.html ← HTML shell
|
||||
static/ ← Static assets
|
||||
```
|
||||
|
||||
### Step 2: File-Based Routing
|
||||
|
||||
Every `+page.svelte` file in `src/routes/` maps directly to a URL:
|
||||
|
||||
```
|
||||
src/routes/+page.svelte → /
|
||||
src/routes/about/+page.svelte → /about
|
||||
src/routes/blog/[slug]/+page.svelte → /blog/:slug
|
||||
src/routes/shop/[...path]/+page.svelte → /shop/* (catch-all)
|
||||
```
|
||||
|
||||
**Route groups** (no URL segment): wrap in `(group)/` folder.
|
||||
**Private routes** (not accessible as URLs): prefix with `_` or `(group)`.
|
||||
|
||||
### Step 3: Loading Data with `load` Functions
|
||||
|
||||
Use a `+page.ts` (universal) or `+page.server.ts` (server-only) file alongside the page:
|
||||
|
||||
```typescript
|
||||
// src/routes/blog/[slug]/+page.server.ts
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params, fetch }) => {
|
||||
const post = await fetch(`/api/posts/${params.slug}`).then(r => r.json());
|
||||
|
||||
if (!post) {
|
||||
error(404, 'Post not found');
|
||||
}
|
||||
|
||||
return { post };
|
||||
};
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/blog/[slug]/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<h1>{data.post.title}</h1>
|
||||
<article>{@html data.post.content}</article>
|
||||
```
|
||||
|
||||
### Step 4: API Routes (Server Endpoints)
|
||||
|
||||
Create `+server.ts` files for REST-style endpoints:
|
||||
|
||||
```typescript
|
||||
// src/routes/api/posts/+server.ts
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const limit = Number(url.searchParams.get('limit') ?? 10);
|
||||
const posts = await db.post.findMany({ take: limit });
|
||||
return json(posts);
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const body = await request.json();
|
||||
const post = await db.post.create({ data: body });
|
||||
return json(post, { status: 201 });
|
||||
};
|
||||
```
|
||||
|
||||
### Step 5: Form Actions
|
||||
|
||||
Form actions are the SvelteKit-native way to handle mutations — no client-side fetch required:
|
||||
|
||||
```typescript
|
||||
// src/routes/contact/+page.server.ts
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const email = data.get('email');
|
||||
|
||||
if (!email) {
|
||||
return fail(400, { email, missing: true });
|
||||
}
|
||||
|
||||
await sendEmail(String(email));
|
||||
redirect(303, '/thank-you');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/contact/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData } from './$types';
|
||||
export let form: ActionData;
|
||||
</script>
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<input name="email" type="email" />
|
||||
{#if form?.missing}<p class="error">Email is required</p>{/if}
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Step 6: Layouts and Nested Routes
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
export let data: LayoutData;
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<a href="/blog">Blog</a>
|
||||
{#if data.user}
|
||||
<a href="/dashboard">Dashboard</a>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<slot /> <!-- child page renders here -->
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
return { user: locals.user ?? null };
|
||||
};
|
||||
```
|
||||
|
||||
### Step 7: Rendering Modes
|
||||
|
||||
Control per-route rendering with page options:
|
||||
|
||||
```typescript
|
||||
// src/routes/docs/+page.ts
|
||||
export const prerender = true; // Static — generated at build time
|
||||
export const ssr = true; // Default — rendered on server per request
|
||||
export const csr = false; // Disable client-side hydration entirely
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Protected Dashboard Route
|
||||
|
||||
```typescript
|
||||
// src/routes/dashboard/+layout.server.ts
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
if (!locals.user) {
|
||||
redirect(303, '/login');
|
||||
}
|
||||
return { user: locals.user };
|
||||
};
|
||||
```
|
||||
|
||||
### Example 2: Hooks — Session Middleware
|
||||
|
||||
```typescript
|
||||
// src/hooks.server.ts
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { verifyToken } from '$lib/server/auth';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const token = event.cookies.get('session');
|
||||
if (token) {
|
||||
event.locals.user = await verifyToken(token);
|
||||
}
|
||||
return resolve(event);
|
||||
};
|
||||
```
|
||||
|
||||
### Example 3: Preloading and Invalidation
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
|
||||
async function refresh() {
|
||||
await invalidateAll(); // re-runs all load functions on the page
|
||||
}
|
||||
</script>
|
||||
|
||||
<button on:click={refresh}>Refresh</button>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- ✅ Use `+page.server.ts` for database/auth logic — it never ships to the client
|
||||
- ✅ Use `$lib/server/` for shared server-only modules (DB client, auth helpers)
|
||||
- ✅ Use form actions for mutations instead of client-side `fetch` — works without JS
|
||||
- ✅ Type all `load` return values with generated `$types` (`PageData`, `LayoutData`)
|
||||
- ✅ Use `event.locals` in hooks to pass server-side context to load functions
|
||||
- ❌ Don't import server-only code in `+page.svelte` or `+layout.svelte` directly
|
||||
- ❌ Don't store sensitive state in stores — use `locals` on the server
|
||||
- ❌ Don't skip `use:enhance` on forms — without it, forms lose progressive enhancement
|
||||
|
||||
## Security & Safety Notes
|
||||
|
||||
- All code in `+page.server.ts`, `+server.ts`, and `$lib/server/` runs exclusively on the server — safe for DB queries, secrets, and session validation.
|
||||
- Always validate and sanitize form data before database writes.
|
||||
- Use `error(403)` or `redirect(303)` from `@sveltejs/kit` rather than returning raw error objects.
|
||||
- Set `httpOnly: true` and `secure: true` on all auth cookies.
|
||||
- CSRF protection is built-in for form actions — do not disable `checkOrigin` in production.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Problem:** `Cannot use import statement in a module` in `+page.server.ts`
|
||||
**Solution:** The file must be `.ts` or `.js`, not `.svelte`. Server files and Svelte components are separate.
|
||||
|
||||
- **Problem:** Store value is `undefined` on first SSR render
|
||||
**Solution:** Populate the store from the `load` function return value (`data` prop), not from client-side `onMount`.
|
||||
|
||||
- **Problem:** Form action does not redirect after submit
|
||||
**Solution:** Use `redirect(303, '/path')` from `@sveltejs/kit`, not a plain `return`. 303 is required for POST redirects.
|
||||
|
||||
- **Problem:** `locals.user` is undefined inside a `+page.server.ts` load function
|
||||
**Solution:** Set `event.locals.user` in `src/hooks.server.ts` before the `resolve()` call.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `@nextjs-app-router-patterns` — When you prefer React over Svelte for SSR/SSG
|
||||
- `@trpc-fullstack` — Add end-to-end type safety to SvelteKit API routes
|
||||
- `@auth-implementation-patterns` — Authentication patterns usable with SvelteKit hooks
|
||||
- `@tailwind-patterns` — Styling SvelteKit apps with Tailwind CSS
|
||||
Reference in New Issue
Block a user