splitforms.com
ASTRO · CONTACT FORM

Contact form for Astro websites

Astro's zero-JS-by-default philosophy pairs perfectly with the splitforms endpoint. Use a pure HTML form for static pages, an Astro Action for SSR, or a React/Vue/Svelte island for client-side interactivity. Same backend, three patterns, zero hydration cost when you don't need it.

1,000 free submissions every month.·No credit card.
contact.astroastro16 lines
01---
02const ACCESS_KEY = import.meta.env.PUBLIC_SPLITFORMS_KEY;
03---
04
05<form action="https://splitforms.com/api/submit" method="POST">
06 <input type="hidden" name="access_key" value={ACCESS_KEY} />
07 <input type="hidden" name="redirect" value="https://yoursite.com/thanks" />
08
09 <input type="text" name="name" placeholder="Name" required />
10 <input type="email" name="email" placeholder="Email" required />
11 <textarea name="message" placeholder="Message" required />
12
13 <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />
14
15 <button type="submit">Send</button>
16</form>
1,000
submissions / mo, free
14ms
median latency, edge
0
lines of backend code
17+
frameworks supported
✶ Live preview

What your Astro contact form actually looks like.

Drop-in form backend with spam filtering, signed webhooks, and a real submissions dashboard. The same code in this preview is what you copy into your Astro project — no SDK, no plugin, no PHP.

  • 1,000 submissions per month, free forever
  • Honeypot + AI spam classifier on every plan
  • Signed webhooks to Slack, Discord, your server
Astro contact form on Splitforms — drop-in form backend with spam filtering and webhooks
§ 01Setup3 steps · 60 seconds · zero config

Ship a Astro contact form without a backend.

No SDK, no PHP, no plugin. Your form posts standard FormData to one URL — submissions land in your inbox.

STEP 01GENERATE

Get your free access key

Verify your email and your access key is generated instantly. Free for 1,000 submissions per month, forever.

Create your form

By signing up, you agree to our terms and privacy policy.

STEP 02EMBED

Drop in the Astro code

Copy the Astro snippet on the right and paste it into your project. Replace YOUR_ACCESS_KEY with the key from step 1.

snippetastro
---
…
STEP 03RECEIVE

Submissions land in your inbox

Hits your dashboard and email in seconds. Forward to Slack, Discord, Sheets, Notion, or any signed webhook URL.

inbox · 1 newjust now
FROM contact@yoursite.com
New Astro form submission
Maya Iyer maya@studio71.co
Loved the new pricing page — quick question about the 4-year plan. Are usage limits per project or account-wide?
§ 02Live demosandboxed · no key required · no submission sent

Try it now — no signup, no key.

This is a styled HTML preview of what your Astro form will look like. Submitting opens a confirmation, no real request is sent.

preview · astrolocalhost:3000
✦ what just happened

Your Astro form posts FormData to /api/submit. Splitforms validates the access key, runs the spam classifier, and forwards the parsed submission to your inbox plus the dashboard.

  • 14ms median round-trip from the edge.
  • Honeypot + classifier, no CAPTCHA.
  • Per-domain key locking out of the box.
REQUEST · POST /api/submit
{
  "access_key": "sk_live_4f9a_••••",
  "name":       "Maya Iyer",
  "email":      "maya@studio71.co",
  "message":    "…"
}
← 200 OK · { "success": true } · 14ms
§ 03Best practices5 rules · production-tested

How to ship this without regrets.

Five rules that make the difference between a form that works in the demo and a form that survives launch traffic.

  1. 01

    Use the pure HTML form pattern (the snippet above) for static pages — zero JS shipped, no hydration cost, works with JavaScript disabled.

  2. 02

    Set `PUBLIC_SPLITFORMS_KEY` in `.env` and reference it in the Astro frontmatter. Lock the key to your domain in the splitforms dashboard so a leaked key can't be replayed.

  3. 03

    Add `<input type="hidden" name="redirect" value={new URL('/thanks', Astro.site).href} />` so non-JS users still land on a thank-you page after submitting.

  4. 04

    If you need client-side success/error UI, hydrate a single small island (Preact is 3kb) rather than React (45kb). The form component is tiny.

  5. 05

    Cache the contact page aggressively — set `Cache-Control: public, max-age=3600` in your adapter config. The form action is server-side regardless.

§ 04Common gotchas in Astro6 edge cases worth knowing

What bites people who skip the docs.

Worth a 60-second skim before you ship to production. Each one has caused a Astro support ticket at least once.

⚠ gotcha

PUBLIC_ prefix is required for client-exposed env vars

Astro mirrors Vite's env-var convention: only variables prefixed with PUBLIC_ are exposed to client-side code (and to .astro files when output: 'static'). If you write import.meta.env.SPLITFORMS_KEY, you'll get undefined at build time. Rename to PUBLIC_SPLITFORMS_KEY.

⚠ gotcha

client:load on the form island defeats the point of Astro

If you wrap the form in <MyForm client:load />, you ship a full React/Preact runtime just for one form. Use client:visible (load when scrolled into view) or client:idle (load after main thread is free) instead. For zero-JS forms, skip the island entirely and use a plain HTML form action.

⚠ gotcha

Astro Actions need a try/catch or they crash the page

If your action throws, Astro 5's behavior is to render an error page rather than return the error to your form. Wrap the splitforms fetch in try/catch and return { success: false, message } from the action — your form component can then render the message.

⚠ gotcha

View Transitions can break form re-submit state

If you've enabled <ViewTransitions /> in your layout, navigating to /contact and back may re-mount the form mid-submission. Add data-astro-reload to the form's submit anchor or guard with if (status === 'loading') return at the top of your handler.

⚠ gotcha

Adapter mismatch: form posts to /api/submit fail in static mode

If output: 'static' in astro.config.mjs, you can't have a server endpoint at all — your form must POST directly to splitforms.com. That's the recommended setup. Switch to output: 'server' or 'hybrid' only if you genuinely need a server-side proxy.

⚠ gotcha

Frontmatter fence forgotten — form action renders as literal text

Astro requires the --- fence at the top of .astro files for any frontmatter (imports, Astro.props, env reads). If you write const accessKey = import.meta.env.PUBLIC_SPLITFORMS_KEY; without the surrounding fence, Astro treats the line as plain text in the rendered HTML — you'll see the JavaScript leak into the page above your form. The build doesn't error; it just renders nonsense. Always wrap any logic in --- and reference values via {accessKey} in the template body.

§ 04bNative Astro forms…and where they break down

How Astro handles forms without splitforms.

The shape of the problem before splitforms enters the picture — and the gap it fills for Astro specifically.

Astro's whole pitch is shipping zero JavaScript by default. A native contact form on Astro means either (a) building a form with no submission target — useless — or (b) standing up an API endpoint via output: 'server' or 'hybrid', which means writing a Node/Bun/Deno handler, picking an email provider, writing your own honeypot logic. Astro 5 added typed Actions, but they're a wrapper around the same underlying fetch — you still deliver the email yourself. The result: every Astro contact-form tutorial ends with 'now configure SendGrid'. Splitforms is the SendGrid-replacement that doesn't require an account, an API key for the email provider, or DNS records for SPF/DKIM.

§ 04cAlternative integration patterns2 ways to wire it

Two ways to ship splitforms on Astro.

Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.

PATTERN A

Pattern A — pure HTML form (zero JS shipped)

The Astro-native approach: a .astro file with frontmatter pulling the key from import.meta.env, then a static <form action> that posts directly. Zero hydration, zero island, zero KB JavaScript.

pattern-a.astroastro10 lines
01---
02const ACCESS_KEY = import.meta.env.PUBLIC_SPLITFORMS_KEY;
03---
04<form action="https://splitforms.com/api/submit" method="POST">
05 <input type="hidden" name="access_key" value={ACCESS_KEY} />
06 <input type="hidden" name="redirect" value="/thanks" />
07 <input name="email" type="email" required />
08 <textarea name="message" required />
09 <button type="submit">Send</button>
10</form>
PATTERN B

Pattern B — Astro Action for typed server-side proxying

Astro 5 Actions give you Zod-validated, type-safe form handlers. Use one to keep the access key off the client entirely — the form posts to the action, the action proxies to splitforms.

pattern-b.astroastro17 lines
01// src/actions/index.ts
02import { defineAction } from "astro:actions";
03import { z } from "astro:schema";
04export const server = {
05 contact: defineAction({
06 accept: "form",
07 input: z.object({ email: z.string().email(), message: z.string() }),
08 async handler(input) {
09 const fd = new FormData();
10 Object.entries(input).forEach(([k, v]) => fd.append(k, v));
11 fd.append("access_key", import.meta.env.SPLITFORMS_KEY);
12 const r = await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
13 if (!(await r.json()).success) throw new Error("Submission failed");
14 return { ok: true };
15 },
16 }),
17};
§ 04dDeployment notes for Astrohosting · env vars · CSP

Shipping Astro + splitforms to production.

Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.

Astro deploys cleanly to Vercel, Netlify, Cloudflare Pages, GitHub Pages, and any static host with output: 'static'. For Astro Actions (Pattern B), you need output: 'server' or 'hybrid' and a matching adapter (@astrojs/vercel, @astrojs/netlify, @astrojs/cloudflare). On Cloudflare Pages with the Cloudflare adapter, the Action runs in a Worker — keep the splitforms fetch tight (no extra proxying) to stay under the 10ms CPU budget on the free plan. The PUBLIC_ prefix is mandatory for env vars exposed to client-rendered .astro files; vars without it are silently undefined. Lock the key to your *.pages.dev and custom domain.

§ 05Comparisonvs native astro

splitforms vs native astro.

What you get for free vs what you build, pay for, or do without.

FeatureNative Astrosplitforms
JavaScript shipped0 KB (Astro Forms RFC pending)0 KB (pure HTML)
Setup timeRFC isn't merged yet60 seconds
Spam filteringDIYHoneypot + classifier
Submission storageSelf-host DBDashboard included
Astro Actions supportYes (write your own)Yes (one-line wrapper)
CostServer + email providerFree (1,000/mo)
§ 06Alternative patternastro · 41 lines
ALTERNATIVE

Astro Actions variant (server-side, hides the access key)

Astro 5+ ships a typed Actions API. Use it to keep your splitforms key entirely server-side, with full TypeScript inference on your form fields.

alternative.astroastro41 lines
01// src/actions/index.ts
02import { defineAction } from "astro:actions";
03import { z } from "astro:schema";
04
05export const server = {
06 contact: defineAction({
07 accept: "form",
08 input: z.object({
09 name: z.string().min(1),
10 email: z.string().email(),
11 message: z.string().min(1),
12 }),
13 async handler(input) {
14 const body = new FormData();
15 Object.entries(input).forEach(([k, v]) => body.append(k, v));
16 body.append("access_key", import.meta.env.SPLITFORMS_KEY);
17
18 const res = await fetch("https://splitforms.com/api/submit", {
19 method: "POST",
20 body,
21 });
22 const data = await res.json();
23 if (!data.success) throw new Error(data.message);
24 return { ok: true };
25 },
26 }),
27};
28
29// src/pages/contact.astro
30---
31import { actions } from "astro:actions";
32const result = Astro.getActionResult(actions.contact);
33---
34<form method="POST" action={actions.contact}>
35 <input name="name" required />
36 <input name="email" type="email" required />
37 <textarea name="message" required />
38 <button type="submit">Send</button>
39 {result?.data && <p>Thanks!</p>}
40 {result?.error && <p>Try again.</p>}
41</form>
§ 07Questions6 answered

Things developers ask before they integrate.

Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.

01How do I add a contact form to Astro?
Paste the snippet above into any .astro page or component. Set PUBLIC_SPLITFORMS_KEY in your .env file. That's it — no install, no astro:integrations, no adapter changes.
02Does splitforms work with Astro SSR and SSG?
Yes — both. In SSG (output: 'static') the form posts directly to splitforms.com from the browser. In SSR (output: 'server' or 'hybrid') you can additionally use Astro Actions to keep the key server-side. Pick whichever fits.
03How do I handle form errors in Astro?
If you're using Astro Actions, render result?.error?.message from Astro.getActionResult(actions.contact). If you're using the pure HTML pattern, set the redirect field to /contact?error=1 and check the URL on render.
04Can I use splitforms with Astro Islands (React, Vue, Svelte, Solid)?
Yes. Build the form as a React/Vue/Svelte component, hydrate with client:visible or client:idle, and use the framework-specific snippet from the matching /forms/<framework> page.
05How do I customize the success / redirect behavior?
Pass a hidden redirect field on the form: <input type="hidden" name="redirect" value="/thanks" />. Splitforms 302s the browser there after a successful POST. With Astro Actions, render success state from result.data instead.
06Will splitforms add 1KB of JS to my Astro site?
No. The default snippet uses zero JavaScript — it's a vanilla <form action method=POST>. JavaScript only runs if you choose the island pattern or wire up custom client-side validation.
✻ ✻ ✻

Ship your Astro contact form in 60 seconds.

1,000 free submissions per month. No credit card. Lock the access key to your domains, paste the snippet, watch submissions land in your inbox.

Get free access key →Read the docs
v0.1 · founders pricing locked in · early access open