Astro contact form with no client JavaScript

An Astro contact form does not need an island, an adapter, or a line of client JavaScript. A static build has no server, so the form posts somewhere else, and the browser already knows how to do that:

---
const endpoint = "https://api.formroute.dev/f/site_xyz";
---

<form action={endpoint} method="POST">
  <label>
    Email
    <input name="email" type="email" required />
  </label>
  <label>
    Message
    <textarea name="message" required></textarea>
  </label>
  <button>Send</button>
</form>

That is the whole component. Drop it in src/components/ContactForm.astro, import it into any page, and the form works.

Why nothing is shipped to the browser here

Astro’s default is to send no JavaScript, and this form keeps that promise because it never needed any. The <form> element has serialised fields and sent a POST since long before frameworks existed. The browser reads action and method and makes the request itself.

This matters more on Astro than on most stacks. The reason you picked Astro is that pages arrive as HTML. A contact form that pulls in a client island, a form library and a validation package undoes that on the one page most likely to be someone’s first visit.

What Astro Actions need that a static site does not have

Astro has Actions, which let a form call a typed server function. They are a good fit for an app and a poor fit here, because they need a route rendered on demand: an adapter, a server runtime, and a host that runs it. Not a folder of files.

That is the trade, stated plainly:

Approach What it costs
External endpoint Nothing. The site stays static and deploys anywhere
Astro Actions An adapter, a server runtime, and a host that runs it
Your own API route The same, plus you now own spam filtering and delivery

If the form is the only dynamic thing on the site, adding a server runtime to support it is a large change bought for a small one.

Keeping the visitor on the page

A native POST navigates, and a plain success page is the most robust thing you can ship. If you want the visitor to stay, that is the one place a script earns its keep:

<form action={endpoint} method="POST" data-contact>
  <!-- same fields -->
</form>

<script>
  const form = document.querySelector("[data-contact]");

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const response = await fetch(form.action, {
      method: "POST",
      body: new FormData(form),
    });
    form.replaceWith(
      Object.assign(document.createElement("p"), {
        textContent: response.ok ? "Thanks, we got it." : "Something went wrong.",
      }),
    );
  });
</script>

An inline <script> in an .astro file is bundled by Astro and scoped to that page, so you do not need a framework island for this. Reaching for one means shipping React to render a form the browser renders on its own. The vanilla JavaScript contact form covers the states and the failure path this version skips.

Validation has nowhere else to run

On most stacks, “validate on the server too” is advice. On a static Astro build it is the only option, because you have no server of your own. required and type="email" are a courtesy to the honest visitor and they are gone the moment a request arrives from something that is not your page.

So the rules have to hold where the POST lands. On FormRoute they are declared once per form and applied to every request:

email    required|email
message  required|min:10

That is also the only place to check formats the browser has never heard of, like a national ID number.

What a static host does with the response

One Astro-specific detail worth knowing before you ship. On a native POST the browser follows whatever the endpoint returns, and your static host is not involved: there is no route on your side to redirect to, so the confirmation page has to be a real page in your build, or the endpoint’s own. Build a /thanks page and point the endpoint at it, or use the script above and never leave.

The wider picture of what an endpoint does with a submission before it reaches you is in what a form backend is. FormRoute keeps the static half static: no adapter, no runtime, one endpoint. It is invite-only today: ask for one.

Point your Astro form somewhere.

FormRoute is live in private access. Drop your email and we'll send your invite.

Free forever · 1,000 submissions/mo · unlimited forms