跳到正文
DEV Community· Alen | SmartForm AI·· 3 小時前AI 評分44

我在 Astro 網站加入無需後端的聯絡表單

I Added a Contact Form to an Astro Site Without a Backend

AI 導讀

開發者為 Astro 靜態網站加入聯絡表單,毋須在網站端部署後端。表單以純 HTML POST 至外部端點,無需 adapter 或伺服器;若用 fetch 顯示內嵌成功訊息,須設定 Accept: application/json。防垃圾表單的 honeypot 欄位則要加上 tabindex="-1" 和 autocomplete="off"。

正文

I moved a client's marketing site to Astro a few weeks ago. Astro 7 was the reason — the Rust compiler and stable route caching cut the build from ~4 minutes to about 90 seconds. Fast builds were the whole point.

Then I hit the one thing Astro can't do out of the box: accept a contact form submission.

Here's the honest version of what I tried, what worked, and the two gotchas that cost me an afternoon.

Why Astro forms trip people up

Astro is component-first and ships almost zero client-side JavaScript. Most "add a form" tutorials assume a Node runtime. Three things follow from that, and they're the reason this feels different from a Next.js or Rails form:

  1. A form has to POST somewhere. The action attribute is just a URL. There's no "submit to Astro" without an adapter.
  2. Astro Actions need a server adapter. They're stable and genuinely type-safe, but they require output: 'server' or 'hybrid' plus @astrojs/vercel / netlify / cloudflare / node. On the default output: 'static' build, they don't run.
  3. No spam filtering is included. Neither Actions nor a plain <form> ships with detection. That's on you.

What I tried (and why I backed out)

Astro Actions. Beautiful DX, end-to-end typed. But to handle one 3-field contact form I had to flip output: 'static' to 'server', add an adapter, manage secrets, and accept cold starts. My "static site" now had a server. That's a lot of surface area for a page that asks for a name, an email, and a message.

Netlify Forms. Five minutes, genuinely nice — but the site was on Vercel, so it was a non-starter. The lock-in matters more than the price tag.

Cloudflare Turnstile + Workers. Turnstile is a great CAPTCHA (free, accessible, no image grids). But to use it on Vercel I now maintain a Worker. I'd traded an adapter for a different server to maintain.

What actually shipped: a plain HTML form POSTing to an external endpoint. No adapter, no server, no secrets.

The form

This is the entire integration. src/components/ContactForm.astro:

<form
  action="https://api.usesmartform.com/api/v1/f/YOUR_FORM_ID"
  method="POST"
>
  <label for="name">Name</label>
  <input id="name" name="name" type="text" required minlength="2" />

  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" required minlength="10"></textarea>

  {/* Honeypot — bots fill it, humans never see it */}
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />

  {/* Where to send the browser after a successful submit */}
  <input type="hidden" name="_next" value="https://yoursite.com/thanks" />

  <button type="submit">Send Message</button>
</form>

Drop it in a page and you're done. Astro's whole pitch is zero JS by default, and a method="POST" form navigates to the endpoint and back — the service returns a 302 to your _next URL. No client-side state, no hydration cost, no framework.

It doesn't matter whether the surrounding page is an Astro component or a React/Vue/Svelte island. As long as the rendered DOM contains <form action="..." method="POST">, the submission lands.

Gotcha #1: the Accept header

If you want an inline success message instead of a page redirect, use fetch. The first version I wrote returned a 302, and fetch followed it cross-origin into a CORS error that looked like a server outage.

const res = await fetch("https://api.usesmartform.com/api/v1/f/YOUR_FORM_ID", {
  method: "POST",
  body: new FormData(form),
  headers: { Accept: "application/json" }, // ← required
});

if (res.ok) {
  const data = await res.json();  // includes next_url
  status.textContent = "Thanks — we'll get back to you shortly.";
  form.reset();
}

Always send Accept: application/json. You get JSON back (including next_url) instead of a redirect.

Gotcha #2: the honeypot needs three attributes

display:none alone isn't enough. I had style="display:none" and still saw bots post clean submissions, because they'd learned to skip hidden inputs. Adding tabindex="-1" (keeps it out of keyboard nav) and autocomplete="off" (keeps password managers out) closed the gap.

To actually test the filter I removed the honeypot for a day, watched bot traffic arrive, then put it back and watched those same submissions get dropped server-side — silently, with no email and no quota deduction.

Spam: layer it, don't pick one

Method Free User friction Catches
Honeypot field Yes None The dumbest bots
Cloudflare Turnstile Yes (≤20 widgets) Almost none Browser-level bots
reCAPTCHA v3 Yes (≤1M/mo) None Script bots, but flags real users sometimes
ML filter (server-side) Yes None Targeted / scripted spam

What I ended up with: honeypot as the cheap first layer, a server-side classifier behind it, and Turnstile in front only on the demo-request form where a false positive costs us a lead.

When I'd use Astro Actions instead

If you already have output: 'server' and an adapter, use Astro Actions. If you're tying submissions to a logged-in user, or the form writes to your database, Actions is the right tool. I'm not against it — I just didn't want to add a server to a static site for one contact form.

It's also fine to mix. That site uses Actions for in-product feedback and an external endpoint for the marketing contact form. They're independent.

Verdict

For a static Astro site with one marketing form, the whole thing should be ~20 lines of HTML and zero infrastructure. That's what I shipped. The hosted-service part is my own tool, so treat that section as biased — but the underlying pattern (a plain POST to an endpoint) works with any provider, including one you run yourself.

I didn't want to write this as a sales pitch, but full disclosure: the external endpoint I used is my own product, SmartForm AI. The plain POST pattern works with any provider. If you want the full comparison matrix, I put the long-form version on my blog (linked in the comments).

Happy to answer questions in the comments.

來源:DEV Community · dev.to