Get an endpoint Sign in

Guides Astro

Contact form for an Astro site

A static Astro build has no server to handle a POST, and adding an adapter just for one form is a lot of machinery. Post the form to sendm8 instead: it works with output: "static", needs no API route, and keeps working with JavaScript off.

A component for the form

Create a component and use it on any page. Change you@example.com to your own address.

src/components/ContactForm.astro
---
// src/components/ContactForm.astro
const endpoint = "https://sendm8.com/f/you@example.com";
---
<form action={endpoint} method="POST">
  <label>Your email <input name="email" type="email" required /></label>
  <label>Message <textarea name="message" required></textarea></label>
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />
  <input type="hidden" name="_next" value={new URL("/thanks/", Astro.site).href} />
  <button>Send</button>
</form>

The _next line sends visitors to your own /thanks/ page and uses site from astro.config.mjs, so set that if you haven’t. Leave the line out and visitors see sendm8’s plain thank-you page instead.

Confirm, then you’re done

  1. The first time anyone submits, sendm8 stores the message and emails you@example.com (well, your address) one confirmation link. Click it and that message, plus anything that arrived meanwhile, is delivered. After that, submissions just arrive.
  2. Each submission arrives as an email with the visitor’s address as Reply-To.
  3. Because nothing runs on your side, it works the same on Netlify, Vercel, Cloudflare Pages or GitHub Pages, in astro dev and in production.

Show a message without leaving the page

Astro ships no JavaScript unless you ask, so add a small script to the component. It posts the form with fetch and asks for JSON back, and the form still posts normally if the script doesn’t run.

src/components/ContactForm.astro
<script>
  const form = document.querySelector<HTMLFormElement>("form[action*='sendm8.com']")!;
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const res = await fetch(form.action, {
      method: "POST",
      body: new FormData(form),
      headers: { Accept: "application/json" },
    });
    const result = await res.json();
    if (result.ok) form.replaceWith("Thanks! It’s on its way.");
    else alert(result.error.message);
  });
</script>

result.error.message is a plain-English sentence you can show as it is, such as a rate-limit or a field that isn’t an email address. The AJAX docs list every response.

Prefer not to put your address in the source?

Sign in and create a form in the dashboard. You get an id like k3x9q2m7ab to use instead of your address, plus allowed domains, Discord, Slack, Telegram and webhook delivery. Forms you’ve already made with your address move into your account when you sign in with it.

A no-account endpoint keeps up to 100 submissions a month and delivers by email. Sign in with the same address and the form moves into your account: 1,000 submissions a month, a dashboard inbox with search and CSV export, and Discord, Slack, Telegram or webhook delivery.

Questions

Do I need an Astro adapter or SSR for a contact form?
No. The form posts straight to sendm8, so it works with a fully static Astro build and no adapter, API route or server action.
Will it work with Astro view transitions?
A normal form post works either way. If you use the fetch script with the ClientRouter, run it on the astro:page-load event so it’s attached again after each navigation.
Can I use it from a React, Vue or Svelte island instead?
Yes. Any island can post to the same endpoint, either as a plain form or with fetch. For React there’s the @sendm8/react package.