Astro Actions Tutorial: Type-Safe Forms in 2026
May 23, 2026
Astro Actions are type-safe server functions you define once and call from an HTML form, client JavaScript, or other server code. This tutorial builds a feedback form that validates input with Zod, returns typed per-field errors, and works even when JavaScript fails to load.
TL;DR
This hands-on guide builds a working feedback form with Astro Actions from an empty project. You will scaffold an Astro 6.3.7 app, add the Node adapter, define a type-safe action whose input is validated by a Zod schema, and wire it to an HTML form that submits with zero client-side JavaScript. Then you will render per-field validation errors, redirect on success, reject bad input with a typed ActionError, progressively enhance the form with a typed client call, and finish by loading fresh submissions onto a static page with a server island. Every command and code block is copy-paste runnable and was verified by building and running the finished app on 23 May 2026.
What you'll learn
- Scaffold an Astro project and add the Node adapter for on-demand rendering
- Keep submissions in a typed store you can later swap for a real database
- Define a type-safe action with
defineAction()and a Zod schema - Submit an HTML form to that action with zero client-side JavaScript
- Handle results: redirect on success and render per-field validation errors
- Reject bad input from the handler with a typed
ActionError - Progressively enhance the same form with a typed client-side call
- Display fresh submissions on a static page with a server island
Why Astro Actions instead of an API route
An Astro Action is a backend function with a Zod-validated input and a type-safe client. Compared with hand-rolling an API endpoint, an action removes three classes of boilerplate: it parses the request body for you, it validates that body against a schema before your handler runs, and it generates a typed function so the client and server agree on the shape of every call.1 Actions have been part of Astro since version 4.15 and are stable today — if a tutorial tells you to set an experimental.actions flag, it is out of date.1
The contrast is concrete. A traditional API route for this form would be a POST handler that calls request.formData(), pulls each field out by hand, coerces the rating string to a number, runs validation, branches on success or failure, and serializes a JSON response — most of it untyped, so a renamed field is a runtime bug. An action collapses that into a schema and a handler. The page imports the action and the form's action attribute points straight at it; call that same action from client or server code and a renamed field becomes a compile error instead of a runtime surprise. One definition covers three call sites — an HTML form, a client-side function, and other server code.1
Prerequisites
- Node.js 22.12.0 or newer. Astro 6 sets
engines.nodeto>=22.12.0.2 Check withnode --version. - A package manager — this guide uses
npm, which ships with Node. - A terminal and a code editor. No database, no account, and no API key are required.
The finished app uses astro@6.3.7 and @astrojs/node@10.1.1, the current published releases as of 23 May 2026.23 Pinning exact versions keeps the tutorial reproducible; npm install without a version pulls whatever is newest the day you run it.
Step 1 — Scaffold the Astro project
Create a new project with the official scaffolding tool:4
npm create astro@latest astro-actions-form
The wizard asks a few questions. Choose the Empty template (the minimal starter — it has no example pages to delete later), say Yes to installing dependencies, Yes to initializing a git repository, and when it asks about TypeScript choose Strict. When it finishes you have a working Astro app:
cd astro-actions-form
npm run dev
Open http://localhost:4321 and you will see the starter page. Stop the dev server with Ctrl+C before the next step. To guarantee you are on the exact version this guide was tested against, pin Astro now:
npm install astro@6.3.7
Step 2 — Add the Node adapter for on-demand rendering
Actions run on the server, and a page that submits to one through an HTML <form> must itself be rendered on demand rather than prerendered to static HTML. On-demand rendering needs a server adapter. For a self-hosted Node server, that is @astrojs/node.3 Add it with the built-in astro add command, which installs the package and edits your config in one step:
npx astro add node
Accept the prompt. The command installs @astrojs/node and adds the adapter to your astro.config.mjs. Open that file and make sure it matches the following — in particular, confirm the adapter is called with mode: 'standalone', since astro add does not always write the mode option for you:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
adapter: node({ mode: 'standalone' }),
});
npm install @astrojs/node@10.1.1
mode: 'standalone' builds a server that starts itself — useful later when you run the production build.3 You do not need to set output: 'server'. Astro 6 keeps the default output: 'static', which prerenders every page to HTML; you opt individual pages into on-demand rendering with a one-line export, which Step 5 does.5 This way your marketing pages stay static and fast, and only the form page runs per request.
Step 3 — Create a typed feedback store
Before the action has somewhere to write, give it a tiny store. To keep the tutorial self-contained, this is an in-memory array — it lives as long as the server process does. In production you would replace the three functions below with database calls; the rest of the app would not change.
Create src/lib/store.ts:
// src/lib/store.ts
export interface Feedback {
id: string;
name: string;
email: string;
rating: number;
message: string;
createdAt: Date;
}
// In-memory only. Swap for a real database in production.
const entries: Feedback[] = [];
export function addFeedback(data: Omit<Feedback, 'id' | 'createdAt'>): Feedback {
const entry: Feedback = {
id: crypto.randomUUID(),
createdAt: new Date(),
...data,
};
entries.unshift(entry);
return entry;
}
export function listFeedback(limit = 5): Feedback[] {
return entries.slice(0, limit);
}
export function countFeedback(): number {
return entries.length;
}
crypto.randomUUID() is a Node built-in, so there is nothing to install. addFeedback returns the created record, which the action will use to build a redirect URL.
Step 4 — Define a type-safe action with Zod
All actions live in a server object exported from src/actions/index.ts.1 Each action is created by defineAction() and has three parts: an accept mode, an input schema, and a handler.
Create src/actions/index.ts:
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
import { addFeedback } from '../lib/store';
export const server = {
submitFeedback: defineAction({
accept: 'form',
input: z.object({
name: z.string().min(2, 'Name must be at least 2 characters.'),
email: z.email('Enter a valid email address.'),
rating: z
.number('Choose a rating.')
.min(1, 'Rating must be between 1 and 5.')
.max(5, 'Rating must be between 1 and 5.'),
message: z
.string()
.min(10, 'Message must be at least 10 characters.')
.max(500, 'Message must be 500 characters or fewer.'),
}),
handler: async ({ name, email, rating, message }) => {
const entry = addFeedback({ name, email, rating, message });
return { id: entry.id, name: entry.name };
},
}),
};
Three details matter here. accept: 'form' tells Astro to parse an HTML form submission: it reads each <input>'s name attribute and builds an object before validation.6 import { z } from 'astro/zod' uses the Zod build that ships inside Astro 6 — Astro bundles Zod 4, so the modern top-level validators like z.email() are available without a separate install.27 And the handler receives a fully typed, already-validated input: by the time your code runs, rating is a number between 1 and 5, not a raw form string. If validation fails, Astro returns a BAD_REQUEST error and the handler never runs.6
Astro applies some conversions for form fields automatically. An <input type="number"> is validated with z.number() even though form values arrive as strings; a checkbox is validated with z.coerce.boolean(); a file input with z.instanceof(File); and an empty text field arrives as null rather than an empty string.6 That null behavior is why name uses z.string().min(2) — an empty submission fails validation cleanly instead of slipping through as "".
accept has two modes. The default, 'json', is for actions you call only from JavaScript with a plain object argument. 'form' — used here — makes the action parse FormData, which is what both an HTML form submission and a client-side FormData call send. Choose 'form' whenever a real <form> element is involved, because that is the mode that survives a browser with no JavaScript. If you omit the input schema entirely, the handler receives the raw FormData object and you validate it yourself; supplying a schema is what buys you the typed, pre-validated input.6
Step 5 — Build the form and submit it with zero JavaScript
Now wire an HTML form to the action. Create src/pages/feedback.astro:
---
// src/pages/feedback.astro
export const prerender = false;
import { actions } from 'astro:actions';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Leave feedback</title>
</head>
<body>
<h1>Leave feedback</h1>
<form method="POST" action={actions.submitFeedback}>
<p><label>Name <input type="text" name="name" required /></label></p>
<p><label>Email <input type="email" name="email" required /></label></p>
<p>
<label>Rating
<input type="number" name="rating" min="1" max="5" required />
</label>
</p>
<p>
<label>Message
<textarea name="message" required></textarea>
</label>
</p>
<button type="submit">Send feedback</button>
</form>
</body>
</html>
Two lines do the heavy lifting. export const prerender = false opts this page into on-demand rendering, which the form-action API requires.5 Setting action={actions.submitFeedback} on a method="POST" form tells Astro to route the submission to your action — it renders as action="?_action=submitFeedback", a query string the server handles for you.6 No fetch(), no onsubmit, no client JavaScript at all.
Run npm run dev and submit the form at http://localhost:4321/feedback. The action runs and stores the entry — but the page just reloads and the user sees nothing. A form that gives no feedback is not finished, which is what the next step fixes.
Step 6 — Handle results: redirect on success, render field errors
Calling Astro.getActionResult() on the server returns the outcome of a form submission: an object with data or error, or undefined if the action was not called on this request.8 Use it to do two things — redirect after a successful submission, and render validation errors when the input was bad.
Redirecting after a successful POST is the POST/Redirect/GET pattern. It stops the browser's "confirm form resubmission?" dialog on refresh and gives the user a clean URL.6 For validation failures, the isInputError() helper narrows an error to an input error and exposes a fields object keyed by input name, with an array of messages for each field that failed.9
Replace src/pages/feedback.astro with the complete version:
---
// src/pages/feedback.astro
export const prerender = false;
import { actions, isInputError } from 'astro:actions';
const result = Astro.getActionResult(actions.submitFeedback);
// POST / Redirect / GET: leave the POST response on success.
if (result && !result.error) {
return Astro.redirect('/feedback?success=' + encodeURIComponent(result.data.name));
}
const fieldErrors = isInputError(result?.error) ? result.error.fields : {};
const formError =
result?.error && !isInputError(result.error) ? result.error.message : null;
const successName = Astro.url.searchParams.get('success');
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Leave feedback</title>
</head>
<body>
<h1>Leave feedback</h1>
{successName && <p class="success">Thanks, {successName}! Your feedback was recorded.</p>}
{formError && <p class="error">{formError}</p>}
<form method="POST" action={actions.submitFeedback}>
<p>
<label>Name <input type="text" name="name" required /></label>
{fieldErrors.name && <span class="error">{fieldErrors.name.join(' ')}</span>}
</p>
<p>
<label>Email <input type="email" name="email" required /></label>
{fieldErrors.email && <span class="error">{fieldErrors.email.join(' ')}</span>}
</p>
<p>
<label>Rating
<input type="number" name="rating" min="1" max="5" required />
</label>
{fieldErrors.rating && <span class="error">{fieldErrors.rating.join(' ')}</span>}
</p>
<p>
<label>Message
<textarea name="message" required></textarea>
</label>
{fieldErrors.message && <span class="error">{fieldErrors.message.join(' ')}</span>}
</p>
<button type="submit">Send feedback</button>
</form>
</body>
</html>
Every action call — whether from a form, from client JavaScript, or from server code — resolves to the same object: data holds the typed return value of your handler, or error holds whatever went wrong, and exactly one of the two is present.1 Validation failures and thrown errors both land on error, which is why a single if (result && !result.error) check is enough to gate the redirect. If you would rather not branch, appending .orThrow() to an action call throws on failure and returns data directly — handy inside a try/catch or while prototyping.1
Submit valid input and the browser lands on /feedback?success=Your%20Name with a thank-you message. Submit a bad email or a one-word message and the form comes back with a message under each offending field — and the handler from Step 4 never ran, because BAD_REQUEST was returned before it.6 The required and type="email" attributes on the inputs still give instant in-browser checks; the action is the authoritative server-side validation that a user cannot bypass.
The formError line catches a different category of failure, which the next step produces deliberately.
Step 7 — Reject bad input from the handler with ActionError
Zod rejects input that has the wrong shape — a malformed email, a rating outside 1–5. But some failures only surface once the handler runs: a record is missing, a user is not authorized, a submission breaks a rule. For those, throw an ActionError from inside the handler. An ActionError carries a status code such as BAD_REQUEST, UNAUTHORIZED, or NOT_FOUND, plus an optional human-readable message, and it arrives on the result's error property like any other failure.9
Suppose feedback may not contain links. Update the action's import and handler in src/actions/index.ts:
// src/actions/index.ts
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro/zod';
import { addFeedback } from '../lib/store';
export const server = {
submitFeedback: defineAction({
accept: 'form',
input: z.object({
name: z.string().min(2, 'Name must be at least 2 characters.'),
email: z.email('Enter a valid email address.'),
rating: z
.number('Choose a rating.')
.min(1, 'Rating must be between 1 and 5.')
.max(5, 'Rating must be between 1 and 5.'),
message: z
.string()
.min(10, 'Message must be at least 10 characters.')
.max(500, 'Message must be 500 characters or fewer.'),
}),
handler: async ({ name, email, rating, message }) => {
if (message.toLowerCase().includes('http://') || message.toLowerCase().includes('https://')) {
throw new ActionError({
code: 'BAD_REQUEST',
message: 'Links are not allowed in feedback.',
});
}
const entry = addFeedback({ name, email, rating, message });
return { id: entry.id, name: entry.name };
},
}),
};
You did not touch feedback.astro. The formError line you wrote in Step 6 already handles this: isInputError() is false for an ActionError you threw yourself, so the error falls through to formError and renders in the banner above the form. Submit a message containing a link and you will see "Links are not allowed in feedback." A Zod failure surfaces per field; a thrown ActionError surfaces in the banner — two paths, one result object.9
Step 8 — Progressively enhance the form with JavaScript
The form already works with no JavaScript. That is the baseline you never want to lose. Progressive enhancement means layering a nicer experience on top without removing that baseline — if the script fails to load, the plain form still submits.
Import actions in a <script> and call the action directly. Astro generates a typed client function for every action, so actions.submitFeedback(formData) is a real RPC call — no fetch(), no URL strings, no manual JSON.1 Add this block just before </body> in src/pages/feedback.astro:
<script>
import { actions } from 'astro:actions';
const form = document.querySelector('form');
form?.addEventListener('submit', async (event) => {
event.preventDefault();
const { data, error } = await actions.submitFeedback(new FormData(form));
if (error) {
// Hand off to the normal server round-trip, which re-renders field errors.
form.submit();
return;
}
const note = document.createElement('p');
note.className = 'success';
note.textContent = `Thanks, ${data.name}! Your feedback was recorded.`;
form.replaceWith(note);
});
</script>
event.preventDefault() stops the native form submission so the script can take over. The action is called with a FormData object built from the form; on success it returns the typed data from your handler, and the script swaps the form for an inline thank-you message with no navigation at all. On a validation or ActionError failure, form.submit() hands control back to the standard server round-trip from Step 6, so the field-error rendering still works through a single code path.6 Because every action is also exposed at a public RPC endpoint — /_actions/submitFeedback for this one — the client call and the form submission run the exact same validated handler.1
Because the action is imported rather than addressed by a URL, the client call is fully typed: your editor knows submitFeedback takes the four fields and that a successful data is { id, name }. There is no endpoint string to keep in sync and no response shape to hand-write — rename a field in the schema and the call site stops type-checking. To prove the progressive-enhancement claim, open your browser's developer tools, disable JavaScript, and submit the form again. It still works, because the <form action={actions.submitFeedback}> markup never depended on the script. The script is pure enhancement: when it loads, a successful submission updates the page in place instead of reloading; when it does not, the server round-trip from Step 6 takes over and nothing breaks.
Step 9 — Show fresh submissions with a server island
Your homepage can stay static and instant while still showing live data, using a server island. Marking a component with server:defer tells Astro to render the page immediately, send fallback content in the component's place, then fetch the component's real HTML separately and swap it in.10 Server islands need an adapter — you already installed one in Step 2.
This is a different trade-off from a client-rendered component. A client component ships its JavaScript and renders in the browser; a server island renders on the server and ships only a small loader script in its place, so it can read your store or a database directly without exposing an API or sending its logic to the browser. Reach for server:defer when one slice of an otherwise-cacheable page needs fresh or personalized server data — a recent-activity list, a logged-in user's avatar, a per-request price.
Create the island component, src/components/RecentFeedback.astro:
---
// src/components/RecentFeedback.astro
import { listFeedback, countFeedback } from '../lib/store';
const recent = listFeedback(5);
const total = countFeedback();
---
<section>
<h2>Recent feedback ({total})</h2>
{recent.length === 0 ? (
<p>No feedback yet. Be the first to leave some.</p>
) : (
<ul>
{recent.map((item) => (
<li>
<strong>{item.name}</strong> rated us {item.rating}/5
<p>{item.message}</p>
</li>
))}
</ul>
)}
</section>
Then use it on the homepage. Replace src/pages/index.astro:
---
// src/pages/index.astro
import RecentFeedback from '../components/RecentFeedback.astro';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Feedback demo</title>
</head>
<body>
<h1>Astro Actions feedback demo</h1>
<p><a href="/feedback">Leave feedback</a></p>
<RecentFeedback server:defer>
<p slot="fallback">Loading recent feedback…</p>
</RecentFeedback>
</body>
</html>
There is no prerender = false on this page — the homepage is still a static document. Only the RecentFeedback island runs per request, so a visitor sees the page and the "Loading recent feedback…" placeholder instantly, and the real list arrives a moment later.10 The child marked slot="fallback" is what shows while the island loads. Submit feedback, return to http://localhost:4321/, and your entry appears in the list — fresh data on a cached page.
Two constraints are worth knowing before you lean on islands. First, a server island renders in an isolated context: it is fetched as its own request, so Astro.url inside the island is the island's endpoint, not the page the visitor is on. If the island needs the page URL — for query parameters, say — read it from the Referer request header.10 Second, any props you pass to a server:defer component must be serializable (plain objects, numbers, strings, arrays, Date, and similar); functions cannot cross that boundary.10 Here the island takes no props and reads the store directly, so neither constraint bites — but they will the moment you pass data in.
Verification
Confirm the whole flow end to end. With npm run dev running:
- Visit
http://localhost:4321/feedbackand submit valid input. You land on/feedback?success=Your%20Namewith a thank-you message. - Submit a bad email and a short message. The form returns with a message under each bad field; nothing is stored.
- Submit a message containing
https://example.com. The "Links are not allowed in feedback." banner appears. - Visit
http://localhost:4321/— your valid submission shows in the server island.
You can also test the form endpoint from the command line. Astro's built-in CSRF protection requires a matching Origin header on form submissions, so pass one explicitly:
curl -s -i -X POST 'http://localhost:4321/feedback?_action=submitFeedback' \
-H 'Origin: http://localhost:4321' \
--data 'name=Jordan+Lee&email=jordan%40example.com&rating=5&message=Great+feedback+widget.'
A valid request responds with HTTP/1.1 302 Found and a location: header pointing at the success URL — the POST/Redirect/GET pattern in action.
For a production build, run npm run build and start the standalone server:
npm run build
node ./dist/server/entry.mjs
The build prints output: "static" with mode: "server" — the homepage is prerendered to a static file while /feedback is compiled into the on-demand server bundle, exactly the split you set up in Steps 2 and 5. In standalone mode the adapter emits dist/server/entry.mjs, a server that starts itself and also serves your static assets. It boots on http://localhost:4321; override the binding with the HOST and PORT environment variables if needed.3
Troubleshooting
403 Cross-site POST form submissions are forbidden. This is Astro's CSRF protection (security.checkOrigin, enabled by default), which rejects form-style POSTs whose Origin header does not match the site's origin.11 Real browsers send a matching Origin automatically, so this only bites command-line testing or a misconfigured reverse proxy. With curl, add -H 'Origin: http://localhost:4321' — and note the standalone Node adapter reports its origin as localhost, so use localhost rather than 127.0.0.1 in both the URL and the header.
The handler never runs and the page just reloads. You are missing export const prerender = false. Without it the page is prerendered to static HTML and the action cannot run.5
getActionResult always returns undefined. It only returns a value on the request that called the action — that is, the POST. On a normal GET it is undefined by design.8 Read submitted data from the result during the POST, then redirect.
415 Unsupported Media Type when calling the action. Your action is accept: 'form' but the request sent JSON. Form-mode actions take form-encoded or multipart/form-data bodies; submit a FormData object from the client (as Step 8 does), not a JSON object.6
Field errors never appear. Each <input> must have a name attribute that matches a key in the Zod schema. Astro builds the validated object from those name attributes, so a typo means the value arrives as undefined.6
Your editor flags Cannot find module 'astro:actions'. astro:actions is a virtual module generated when Astro syncs your project. Running npm run dev regenerates it; if you only see the error in your editor, run npx astro sync once to refresh the generated types in the .astro/ directory.
Before you ship to production
The app is correct, but three things need attention before real traffic reaches it.
The in-memory store is per-process. It is wiped on every restart and is not shared between multiple server instances, so a load-balanced deployment would show different lists on different requests. This is the deliberate trade-off that keeps the tutorial dependency-free; replace src/lib/store.ts with a database before you ship.
Every action is a public endpoint. The handler is reachable at /_actions/submitFeedback by anyone who knows the name, exactly like a REST route — Astro's docs are explicit that you must apply the same authorization and rate-limiting you would give an API endpoint.1 For a public feedback form that means input limits and abuse protection in the handler; for anything user-specific it means an auth check that throws ActionError with an UNAUTHORIZED code.
Server islands encrypt the props passed to them with a key that is regenerated on every build. That is fine for a single server, but on rolling deployments, multi-region hosting, or a CDN that caches island-bearing pages, the encrypting build and the decrypting build can drift apart. Generate a stable key once with astro create-key, set it as the ASTRO_KEY environment variable in your build and runtime, and the key stays consistent across deploys.10
Next steps
You now have a type-safe form with server validation, progressive enhancement, and a live server island — a pattern that scales from a contact form to a full dashboard. From here:
- Swap the in-memory store for a real database. The three functions in
src/lib/store.tsare the only code that changes; the action and pages stay the same. - Add authentication so an action can reject unauthorized users. The same
ActionErrormechanism with anUNAUTHORIZEDcode drives it. - Compare the model with React's take in Server Actions and optimistic UI in Next.js 16, and with a no-framework approach in building a live Kanban board with htmx and Express.
- Server islands are close cousins of streaming UI — see streaming and Suspense in Next.js 16 for the React equivalent.
The core lesson holds across frameworks: validate on the server, make the no-JavaScript path work first, and enhance from there.
Footnotes
-
"Actions," Astro Docs — Actions added in
astro@4.15;defineAction(), theserverobject insrc/actions/index.ts, typed client calls, and the/_actions/{name}public endpoint. https://docs.astro.build/en/guides/actions/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
astronpm package — version6.3.7(latestdist-tag, published 2026-05-21) andengines.node: ">=22.12.0", verified vianpm view astroon 2026-05-23. https://www.npmjs.com/package/astro ↩ ↩2 ↩3 -
@astrojs/nodenpm package — version10.1.1(latestdist-tag, published 2026-05-13), and the@astrojs/nodeintegration guide coveringastro add node,mode: 'standalone', thedist/server/entry.mjsentrypoint, andHOST/PORToverrides. https://docs.astro.build/en/guides/integrations-guide/node/ ↩ ↩2 ↩3 ↩4 -
"Install Astro," Astro Docs — the
npm create astro@latestscaffolding command, its wizard prompts, and the starter template options. https://docs.astro.build/en/install-and-setup/ ↩ -
"On-demand rendering," Astro Docs — the default
output: 'static'model and enabling per-page on-demand rendering withexport const prerender = false. https://docs.astro.build/en/guides/on-demand-rendering/ ↩ ↩2 ↩3 -
"Actions — Accepting form data" and "Call actions from an HTML form action," Astro Docs —
accept: 'form', form parsing by inputname, the?_action=query string, the on-demand-rendering requirement, and the POST/Redirect/GET pattern. https://docs.astro.build/en/guides/actions/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
"astro/zod," Astro Docs — Astro re-exports its bundled Zod (Zod 4 in Astro 6) from the
astro/zodmodule for use in action input schemas. https://docs.astro.build/en/reference/modules/astro-zod/ ↩ -
"Render context — getActionResult," Astro Docs —
Astro.getActionResult()returnsdata/erroron the request that called the action, orundefinedotherwise. https://docs.astro.build/en/reference/api-reference/ ↩ ↩2 -
"Actions — Handling backend errors" and "Displaying form input errors," Astro Docs — the
ActionErrorclass with statuscodeandmessage, and theisInputError()guard exposingerror.fields. https://docs.astro.build/en/reference/modules/astro-actions/ ↩ ↩2 ↩3 -
"Server islands," Astro Docs — the
server:deferdirective, theslot="fallback"placeholder, the adapter requirement, and how deferred islands are fetched after page load. https://docs.astro.build/en/guides/server-islands/ ↩ ↩2 ↩3 ↩4 ↩5 -
"Configuration Reference — security.checkOrigin," Astro Docs — cross-site form-submission origin checking, enabled by default for on-demand-rendered pages. https://docs.astro.build/en/reference/configuration-reference/ ↩