Astro 6 (Server Islands, Content Layer, Actions, Sessions, astro:env) Cursor plugin: 48 anti-patterns, 10 rules, 5 skills, reviewer agent, correct + anti-pattern fixtures, validator. Catches Astro 4/5 leftovers that current LLMs still produce.
10 rules
Add to Cursor # Astro Actions and Forms
Target: `astro ^6.3.3`. Actions stabilised in Astro 5.0 and replace the ad-hoc API endpoint + manual validation pattern.
## Where actions live
Actions live under `src/actions/`. A single `src/actions/index.ts` exports a `server` namespace; Astro picks them up automatically.
```ts
// src/actions/index.ts
import { defineAction } from 'astro:actions'
import { z } from 'astro:schema'
export const server = {
subscribe: defineAction({
input: z.object({
email: z.string().email(),
}),
handler: async ({ email }, ctx) => {
await db.subscribers.insert({ email })
return { ok: true }
},
}),
uploadAvatar: defineAction({
accept: 'form',
input: z.object({
avatar: z.instanceof(File),
userId: z.string(),
}),
handler: async ({ avatar, userId }) => {
const bytes = await avatar.arrayBuffer()
await storage.put(`avatars/${userId}`, bytes)
return { ok: true }
},
}),
}
```
Source: https://docs.astro.build/en/guides/actions/
## Always import zod from astro:schema
`astro:schema` re-exports the Zod instance that Astro bundles. Importing from your own `zod` dependency can produce two parallel Zod runtimes whose `instanceof` checks disagree (anti-pattern #46).
```ts
// CORRECT
import { z } from 'astro:schema'
// WRONG inside src/actions/
import { z } from 'zod'
```
## Calling actions
### From a .astro frontmatter
```astro
---
import { actions } from 'astro:actions'
const { data, error } = await Astro.callAction(actions.subscribe, { email: 'a@b.co' })
---
{error && <p class="text-red-500">{error.message}</p>}
{data && <p>Subscribed.</p>}
```
`Astro.callAction` is the canonical server-side caller: it runs validation, normalises the return to `{ data, error }`, and is type-safe.
Source: https://docs.astro.build/en/reference/api-reference/#callaction
### From a client component
```tsx
import { actions } from 'astro:actions'
async function onSubmit(e: SubmitEvent) {
e.preventDefault()
const { data, error } = await actions.subscribe({ email: 'a@b.co' })
if (error) showToast(error.message)
}
```
### From an HTML form (progressive enhancement)
```astro
---
import { actions } from 'astro:actions'
---
<form method="POST" action={actions.subscribe}>
<input name="email" type="email" required />
<button type="submit">Subscribe</button>
</form>
```
For file uploads, the `<form>` must declare `enctype="multipart/form-data"` (anti-pattern #31):
```astro
<form method="POST" enctype="multipart/form-data" action={actions.uploadAvatar}>
<input type="file" name="avatar" required />
<input type="hidden" name="userId" value={user.id} />
<button type="submit">Upload</button>
</form>
```
### Reading action results on a static page
When a static page (no `export const prerender = false`) hosts a form that posts to an action, Astro performs the action then re-requests the page with a result attached to the request. Read it with `Astro.getActionResult`, NOT `Astro.callAction` (which is a per-request server caller that cannot run during a static prerender):
```astro
---
import { actions } from 'astro:actions'
const result = Astro.getActionResult(actions.subscribe)
---
<form method="POST" action={actions.subscribe}>
<input name="email" type="email" required />
<button type="submit">Subscribe</button>
</form>
{result && !result.error && <p>Thanks, you are subscribed.</p>}
{result?.error && <p class="error">{result.error.message}</p>}
```
Use `Astro.callAction(actions.x, input)` only when the host page is on-demand-rendered (`export const prerender = false`) or in `output: 'server'`.
Source: https://docs.astro.build/en/reference/api-reference/#astrogetactionresult
## Returning errors
Throw an `ActionError` with the proper code:
```ts
import { defineAction, ActionError } from 'astro:actions'
import { z } from 'astro:schema'
export const server = {
login: defineAction({
input: z.object({ email: z.string().email(), password: z.string() }),
handler: async ({ email, password }) => {
const user = await db.users.byEmail(email)
if (!user || !user.checkPassword(password)) {
throw new ActionError({ code: 'UNAUTHORIZED', message: 'Bad credentials' })
}
return { userId: user.id }
},
}),
}
```
Source: https://docs.astro.build/en/reference/modules/astro-actions/
## Common mistakes
- No `input` schema: handler receives `unknown` (anti-pattern #3).
- Importing `z` from `zod` instead of `astro:schema` (anti-pattern #46).
- File form without `enctype="multipart/form-data"` (anti-pattern #31).
- Calling `actions.subscribe(input)` directly from frontmatter instead of `Astro.callAction(actions.subscribe, input)` (anti-pattern #30).
- Action named `apply` outside `src/actions/index.ts` (anti-pattern #44).
Add to Cursor Add to Cursor Add to Cursor Add to Cursor Add to Cursor Add to Cursor Add to Cursor Add to Cursor