Build a small notes-style page: a list route with server load, a detail route with a param, and a form action that increments a counter. Start from a scaffolded app (Quick start).
Edit src/routes.ts:
import { defineRoutes, route } from '@avedon/server'
import Home from './pages/Home.ave'
import Notes from './pages/Notes.ave'
import Note from './pages/Note.ave'
export const routes = defineRoutes([
{ path: '/', component: Home, render: 'ssr' },
{ path: '/notes', component: Notes, render: 'ssr' },
route('/notes/:id', {
component: Note,
render: 'ssr',
}),
])
export default routesPrefer route('/notes/:id', …) so TypeScript can type params in guards and you stay aligned with typed load handlers.
Create src/pages/Notes.ave:
<script server>
export async function load() {
return {
data: {
notes: [
{ id: '1', title: 'Hello' },
{ id: '2', title: 'World' },
],
},
}
}
</script>
<script>
export let data
</script>
<template>
<h1>Notes</h1>
<ul>
{#each data.notes as note}
<li><a href={'/notes/' + note.id}>{note.title}</a></li>
{/each}
</ul>
</template>
load runs on the server. Returned data is available on the client as export let data.
Create src/pages/Note.ave:
<script server>
import { type LoadEvent, notFound } from '@avedon/server'
const titles: Record<string, string> = {
'1': 'Hello',
'2': 'World',
}
export async function load({
params,
}: LoadEvent<'/notes/:id'>): Promise<{ data: { id: string; title: string; likes: number } }> {
const title = titles[params.id]
if (!title) throw notFound()
return { data: { id: params.id, title, likes: 0 } }
}
export const actions = {
async like({ params }: LoadEvent<'/notes/:id'>) {
return { ok: true, id: params.id }
},
}
</script>
<script>
export let data
</script>
<template>
<p><a href="/notes">← Notes</a></p>
<h1>{data.title}</h1>
<p>id: {data.id}</p>
<form method="POST" action="?_action=like">
<button type="submit">Like</button>
</form>
</template>
Visit /notes and /notes/1. Submit the form — avedon posts to the same route with ?_action=like and runs the named action (CSRF-protected; see Loading data).
Add a local counter on the detail page:
<script>
import { signal } from '@avedon/runtime'
export let data
const clicks = signal(0)
</script>
<template>
<!-- … -->
<button type="button" on:click={() => clicks.set(clicks.get() + 1)}>
Local clicks: {clicks}
</button>
</template>
| Piece | Where |
|---|---|
| Route table | routes.ts + route() |
| Server data | load in <script server> |
| Forms | actions + ?_action= |
| Params | LoadEvent<'/notes/:id'> |
| Client state | signal from @avedon/runtime |
Protect a route with a sealed session cookie (see Session):
// src/server-entry.ts
export const session = {
secret: process.env.SESSION_SECRET!, // 32+ chars in production
}<!-- src/pages/Login.ave (sketch) -->
<script server>
import { redirect } from '@avedon/server'
export const actions = {
login: async ({ session, formData }) => {
session!.set({ userId: String(formData.get('user')) })
return redirect('/admin')
},
}
</script>
<template>
<form method="POST" action="?_action=login">
<input name="user" />
<button type="submit">Sign in</button>
</form>
</template>
// routes.ts
import { requireSession, route } from '@avedon/server'
route('/admin', {
component: Admin,
render: 'csr',
guard: requireSession({ redirectTo: '/login' }),
})Mark a marketing page as SSG with revalidate and optional cache tags, then regenerate from an api_* handler:
{
path: '/stats',
component: Stats,
render: 'ssg',
revalidate: 60,
tags: ['stats'],
getStaticPaths: () => ['/stats'],
}<script server>
import path from 'node:path'
import { revalidatePath, revalidateTag } from '@avedon/adapter-node'
export async function load() {
return { builtAt: Date.now() }
}
export async function api_POST() {
const { routes, appHtml } = await import('../server-entry')
const ctx = { clientDir: path.join(process.cwd(), 'build/client'), routes, appHtml }
// Path: await revalidatePath('/stats', ctx, { wait: true })
const paths = await revalidateTag('stats', ctx, { wait: true })
return Response.json({ ok: paths.length > 0, paths })
}
</script>
On Cloudflare, bind a KV namespace (AVEDON_ISR), then call POST /__avedon/revalidate with { "path" } / { "tag" }, or use revalidatePath / revalidateTag from @avedon/adapter-cloudflare with { kv, routes, appHtml, assets }. See Rendering and Deployment.
- Components — full
.aveformat - Routing — layouts, guards, errors
- Rendering —
ssr/ssg/csr - Session — cookies and auth-style guards
- Editor —
.avelanguage support