Skip to content

Latest commit

 

History

History
218 lines (176 loc) · 5.35 KB

File metadata and controls

218 lines (176 loc) · 5.35 KB

Tutorial

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).

1. Add routes

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 routes

Prefer route('/notes/:id', …) so TypeScript can type params in guards and you stay aligned with typed load handlers.

2. List page with load

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.

3. Detail page with params and an action

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).

4. Optional: client signal

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>

What you learned

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

5. Optional: session guard

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' }),
})

6. Optional: SSG + on-demand ISR (Node / Bun / Cloudflare)

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.

Next steps