name: fresh description: | Fresh Deno web framework. Covers islands architecture, file-based routing, handlers, signals, and plugins. Use for server-rendered Deno applications.
USE WHEN: user mentions "Fresh", "fresh", "islands architecture", "Deno SSR", "file-based routing in Deno", asks about "Preact SSR", "server-rendered Deno apps", "Deno web framework with hydration", "zero-config Deno framework"
DO NOT USE FOR: Node.js apps - use nextjs or remix instead, Pure API servers - use oak instead, Edge runtimes - use hono instead, Static sites - use SSG tools like Astro
allowed-tools: Read, Grep, Glob, Write, Edit
Fresh Core Knowledge
Full Reference: See advanced.md for Shared Signals, Middleware (Auth, CORS), Plugins, Error Handling, and Production Readiness.
Basic Setup
# Create new Fresh project
deno run -A -r https://fresh.deno.dev my-project
# Project structure
my-project/
├── deno.json
├── dev.ts
├── main.ts
├── fresh.gen.ts # Auto-generated manifest
├── routes/ # File-based routing
├── islands/ # Interactive components
├── components/ # Static components
└── static/ # Static assets
Configuration
// deno.json
{
"tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts"
},
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
"preact": "https://esm.sh/preact@10.19.6",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
File-Based Routing
// routes/index.tsx
export default function Home() {
return <h1>Welcome to Fresh</h1>;
}
// routes/users/[id].tsx - Dynamic route
import { PageProps } from "$fresh/server.ts";
export default function UserPage(props: PageProps) {
const { id } = props.params;
return <h1>User: {id}</h1>;
}
// routes/blog/[...slug].tsx - Catch-all
export default function BlogPost(props: PageProps) {
const { slug } = props.params;
return <h1>Post: {slug}</h1>;
}
// routes/(marketing)/pricing.tsx - Route groups
// URL: /pricing (group name not in URL)
Handlers
// routes/api/users.ts
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
GET(_req, _ctx) {
const users = [{ id: 1, name: "Alice" }];
return new Response(JSON.stringify(users), {
headers: { "Content-Type": "application/json" },
});
},
async POST(req, ctx) {
const body = await req.json();
// Process...
return new Response(JSON.stringify(body), { status: 201 });
},
};
Handler with Page
// routes/greet/[name].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface Data {
greeting: string;
}
export const handler: Handlers<Data> = {
GET(_req, ctx) {
return ctx.render({ greeting: `Hello, ${ctx.params.name}!` });
},
};
export default function GreetPage({ data }: PageProps<Data>) {
return <h1>{data.greeting}</h1>;
}
Islands Architecture
// islands/Counter.tsx
import { useSignal } from "@preact/signals";
export default function Counter() {
const count = useSignal(0);
return (
<div>
<p>Count: {count.value}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
// routes/index.tsx - Using islands
import Counter from "../islands/Counter.tsx";
export default function Home() {
return (
<div>
<h1>My App</h1>
{/* This component is hydrated on the client */}
<Counter />
</div>
);
}
Islands with Props
// islands/Greeting.tsx
import { useSignal } from "@preact/signals";
interface GreetingProps {
initialName: string;
}
export default function Greeting({ initialName }: GreetingProps) {
const name = useSignal(initialName);
return (
<div>
<input
type="text"
value={name.value}
onInput={(e) => name.value = (e.target as HTMLInputElement).value}
/>
<p>Hello, {name.value}!</p>
</div>
);
}
Signals (State Management)
// islands/TodoList.tsx
import { useSignal, useComputed } from "@preact/signals";
export default function TodoList() {
const todos = useSignal<{ id: number; text: string; done: boolean }[]>([]);
const newTodo = useSignal("");
const remaining = useComputed(() =>
todos.value.filter((t) => !t.done).length
);
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value = [...todos.value, {
id: Date.now(),
text: newTodo.value,
done: false,
}];
newTodo.value = "";
}
};
return (
<div>
<input
value={newTodo.value}
onInput={(e) => newTodo.value = (e.target as HTMLInputElement).value}
onKeyDown={(e) => e.key === "Enter" && addTodo()}
/>
<button onClick={addTodo}>Add</button>
<p>{remaining.value} items remaining</p>
</div>
);
}
Static Components
// components/Header.tsx
import { JSX } from "preact";
interface HeaderProps {
title: string;
children?: JSX.Element;
}
export function Header({ title, children }: HeaderProps) {
return (
<header>
<h1>{title}</h1>
<nav>{children}</nav>
</header>
);
}
Forms
// routes/contact.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface PageData {
error?: string;
success?: boolean;
}
export const handler: Handlers<PageData> = {
GET(_req, ctx) {
return ctx.render({});
},
async POST(req, ctx) {
const form = await req.formData();
const name = form.get("name")?.toString();
const email = form.get("email")?.toString();
if (!name || !email) {
return ctx.render({ error: "All fields required" });
}
return ctx.render({ success: true });
},
};
export default function ContactPage({ data }: PageProps<PageData>) {
return (
<div>
{data.error && <p style={{ color: "red" }}>{data.error}</p>}
{data.success && <p style={{ color: "green" }}>Sent!</p>}
<form method="POST">
<input type="text" name="name" required />
<input type="email" name="email" required />
<button type="submit">Send</button>
</form>
</div>
);
}
Basic Middleware
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
export async function handler(req: Request, ctx: FreshContext) {
const start = performance.now();
const resp = await ctx.next();
const duration = performance.now() - start;
resp.headers.set("X-Response-Time", `${duration.toFixed(2)}ms`);
return resp;
}
Checklist
- Islands only for interactive components
- Static components for server-rendered content
- Middleware for cross-cutting concerns
- Error pages (_404.tsx, _500.tsx)
- Health/readiness endpoints
- Environment variables via Deno.env
- Form validation on both client and server
When NOT to Use This Skill
- Node.js Projects: Use Next.js, Remix
- Pure API Servers: Use Oak for Deno API-only apps
- Edge Runtimes: Use Hono for Cloudflare Workers
- Static Sites: Use Astro or other SSG tools
Anti-Patterns
| Anti-Pattern | Why It's Bad | Correct Approach |
|---|---|---|
| Using islands for static content | Unnecessary hydration | Use static components |
| Heavy computation in islands | Slow client-side | Compute on server |
| Fetching data in islands | Waterfalls | Fetch in handlers |
| Hardcoding env variables | Not portable | Use Deno.env.get() |
Quick Troubleshooting
| Issue | Solution |
|---|---|
| Island not hydrating | Move component to islands/ folder |
| Props not passing | Ensure props are JSON-serializable |
| Handler not executing | Ensure handler is exported |
| 404 for dynamic route | Use [param].tsx naming |
| Signals not reactive | Access with .value |