@@ -18,7 +18,7 @@ description: Full-stack meta-framework for Pyreon applications.
1818Scaffold a new project with the ` create ` command:
1919
2020``` bash
21- bun create zero my-app
21+ bun create @pyreon/ zero my-app
2222cd my-app
2323bun install
2424bun run dev
@@ -489,6 +489,108 @@ import { varyEncoding } from "@pyreon/zero"
489489varyEncoding ()
490490```
491491
492+ ## Server Actions
493+
494+ Define server-side mutations that are callable from the client. Actions receive parsed JSON or FormData and are mounted at ` /_zero/actions/* ` .
495+
496+ ``` ts title="src/features/posts.ts"
497+ import { defineAction } from " @pyreon/zero/actions"
498+
499+ export const createPost = defineAction (async (ctx ) => {
500+ const { title, body } = ctx .json as { title: string ; body: string }
501+ const post = await db .posts .create ({ title , body })
502+ return { success: true , id: post .id }
503+ })
504+
505+ export const deletePost = defineAction (async (ctx ) => {
506+ const { id } = ctx .json as { id: number }
507+ await db .posts .delete (id )
508+ return { success: true }
509+ })
510+ ```
511+
512+ Call actions from components — they're just async functions:
513+
514+ ``` tsx
515+ import { createPost } from " ../features/posts"
516+
517+ function NewPostForm() {
518+ const handleSubmit = async (e : Event ) => {
519+ e .preventDefault ()
520+ const result = await createPost ({ title: " Hello" , body: " World" })
521+ if (result .success ) window .location .href = ` /posts/${result .id } `
522+ }
523+
524+ return <form onSubmit = { handleSubmit } >...</form >
525+ }
526+ ```
527+
528+ ### ActionContext
529+
530+ | Property | Type | Description |
531+ | ----------| ------| -------------|
532+ | ` request ` | ` Request ` | The original HTTP request |
533+ | ` json ` | ` unknown ` | Parsed JSON body (for ` application/json ` ) |
534+ | ` formData ` | ` FormData \| null ` | Parsed form data (for ` multipart/form-data ` ) |
535+ | ` headers ` | ` Headers ` | Request headers |
536+
537+ ### Action Middleware
538+
539+ Mount the action handler in your server entry:
540+
541+ ``` ts title="src/entry-server.ts"
542+ import { createActionMiddleware } from " @pyreon/zero/actions"
543+
544+ export default createServer ({
545+ routes ,
546+ middleware: [
547+ createActionMiddleware (), // handles /_zero/actions/* requests
548+ securityHeaders (),
549+ cacheMiddleware (),
550+ ],
551+ })
552+ ```
553+
554+ ## Per-Route Middleware
555+
556+ Route files can export a ` middleware ` function that runs on the server before rendering. Middleware uses ` @pyreon/server ` 's signature:
557+
558+ ``` tsx title="src/routes/(admin)/dashboard.tsx"
559+ import type { MiddlewareContext } from " @pyreon/server"
560+
561+ // Runs on every request to /dashboard
562+ export const middleware = (ctx : MiddlewareContext ) => {
563+ const token = ctx .req .headers .get (" authorization" )
564+ if (! token ) {
565+ return new Response (" Unauthorized" , { status: 401 })
566+ }
567+ // Return void to continue to rendering
568+ }
569+ ```
570+
571+ Wire route middleware in your server entry:
572+
573+ ``` ts title="src/entry-server.ts"
574+ import { routes } from " virtual:zero/routes"
575+ import { routeMiddleware } from " virtual:zero/route-middleware"
576+ import { createServer } from " @pyreon/zero"
577+
578+ export default createServer ({
579+ routes ,
580+ routeMiddleware , // per-route middleware dispatched before global middleware
581+ middleware: [securityHeaders (), cacheMiddleware ()],
582+ })
583+ ```
584+
585+ Add the virtual module type to your ` env.d.ts ` :
586+
587+ ``` ts title="env.d.ts"
588+ declare module " virtual:zero/route-middleware" {
589+ import type { RouteMiddlewareEntry } from " @pyreon/zero"
590+ export const routeMiddleware: RouteMiddlewareEntry []
591+ }
592+ ```
593+
492594## SEO
493595
494596### Sitemap Generation
@@ -763,6 +865,7 @@ import { createServer } from "@pyreon/zero"
763865
764866const handler = createServer ({
765867 routes ,
868+ routeMiddleware , // Per-route middleware from virtual:zero/route-middleware
766869 config: { mode: " ssr" },
767870 middleware: [securityHeaders (), cacheMiddleware ()],
768871 template: indexHtml , // HTML template string
@@ -822,6 +925,8 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
822925| ` nodeAdapter ` | ` () => Adapter ` | Node.js adapter |
823926| ` bunAdapter ` | ` () => Adapter ` | Bun adapter |
824927| ` staticAdapter ` | ` () => Adapter ` | Static output adapter |
928+ | ` defineAction ` | ` (handler: ActionHandler) => Action ` | Define a server action |
929+ | ` createActionMiddleware ` | ` () => Middleware ` | Mount action handler at ` /_zero/actions/* ` |
825930
826931## Subpath Exports
827932
@@ -838,6 +943,7 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
838943| ` @pyreon/zero/seo ` | ` seoPlugin ` , ` seoMiddleware ` , ` generateSitemap ` , ` generateRobots ` , ` jsonLd ` |
839944| ` @pyreon/zero/theme ` | Theme signals and ` ThemeToggle ` component |
840945| ` @pyreon/zero/image-plugin ` | ` imagePlugin ` Vite plugin |
946+ | ` @pyreon/zero/actions ` | ` defineAction ` , ` createActionMiddleware ` |
841947
842948## Type Exports
843949
@@ -865,3 +971,7 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
865971| ` FormatSource ` | Per-format srcset in a ` ProcessedImage ` |
866972| ` ImageFormat ` | ` "webp" \| "avif" \| "jpeg" \| "png" ` |
867973| ` JsonLdType ` | JSON-LD structured data type |
974+ | ` ActionContext ` | Context passed to server action handlers |
975+ | ` Action ` | Client-callable action returned by ` defineAction ` |
976+ | ` ActionHandler ` | Server action handler function type |
977+ | ` RouteMiddlewareEntry ` | Maps URL pattern to route middleware |
0 commit comments