Home / Guides

Add a contact form to an Astro site (zero JS, static output)

Published 2026-07-15

Astro's whole pitch is shipping less JavaScript, and for most content sites that means shipping none at all — output: 'static', HTML files, done. That's also exactly why a contact form is a slightly different problem in Astro than it is in a client-heavy framework: there's no server running after build time to catch a POST, and reaching for an island just to run fetch undercuts the reason you picked Astro in the first place.

The good news is you don't need one. An HTML <form> with an action attribute doesn't need JavaScript to submit — the browser does it natively. If the thing at that URL stores the submission, queues an email, and gives you somewhere to see what came in, your Astro component's job is just to render the form correctly. No .astro API endpoint, no adapter, no server.

The plain HTML version (zero JS)

---
// src/components/ContactForm.astro
---

<form action="https://api.submissionbuddy.io/f/your-form" method="POST">
  <label>
    Name
    <input type="text" name="name" required />
  </label>

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

  <label>
    Message
    <textarea name="message" required></textarea>
  </label>

  <!-- honeypot: hidden from real visitors, bots that fill every input trip it -->
  <input type="text" name="_honeypot" style="display:none" tabindex="-1" autocomplete="off" />

  <!-- send the visitor here after a successful submit -->
  <input type="hidden" name="_redirect" value="https://your-site.com/thanks/" />

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

That's a complete, working contact form with no client-side script anywhere on the page. It works with output: 'static', it works with output: 'server', it works if you delete every island on the site. The frontmatter fence at the top stays empty here — there's nothing to compute, no Astro.request to read, because the form posts straight to SubmissionBuddy's endpoint, not back to your own site.

Optional: client-side enhancement

If you'd rather keep the visitor on the page and show an inline success message instead of navigating to /thanks/, add a small script. Astro's <script> tags in .astro files are automatically scoped to the component and bundled — no framework, no island, no hydration directive needed, since this is plain DOM script rather than a UI framework component.

---
// src/components/ContactForm.astro
---

<form id="contact-form" action="https://api.submissionbuddy.io/f/your-form" method="POST">
  <label>Name <input type="text" name="name" required /></label>
  <label>Email <input type="email" name="email" required /></label>
  <label>Message <textarea name="message" required></textarea></label>
  <input type="text" name="_honeypot" style="display:none" tabindex="-1" autocomplete="off" />
  <button type="submit">Send</button>
  <p id="contact-status" role="status" hidden></p>
</form>

<script>
  const form = document.getElementById('contact-form') as HTMLFormElement
  const status = document.getElementById('contact-status')!

  form.addEventListener('submit', async (event) => {
    event.preventDefault()

    const body = Object.fromEntries(new FormData(form))

    try {
      const response = await fetch(form.action, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      })

      if (!response.ok) throw new Error(`Request failed: ${response.status}`)

      form.hidden = true
      status.hidden = false
      status.textContent = 'Thanks — we got your message.'
    } catch {
      status.hidden = false
      status.textContent = 'Something went wrong. Please try again.'
    }
  })
</script>

This ships a few hundred bytes of vanilla JS scoped to this one component — Astro doesn't pull in React or Vue to run it. Everything else about the request is identical to the zero-JS version; only the response handling changed from a full-page redirect to an inline update.

Which version to use

If the form lives on its own page, the plain HTML version with _redirect is simpler and genuinely has zero JavaScript cost — use it. If the form is embedded partway down a longer page (a footer, a product page, a landing page) where a full navigation to /thanks/ would be jarring, the fetch-enhanced version keeps the visitor in place. Both hit the same endpoint and get the same server-side handling.

Spam protection

The _honeypot field in both examples is read as a bot signal, not a real field, and is stripped before storage. Real visitors never see it or fill it in; bots that blindly fill every form field do, and their submission is silently discarded — no storage write, no email — while the response still looks like an ordinary success. It's on every plan by default. If you need more, Pro and Business plans add an invisible, self-hosted proof-of-work CAPTCHA with no third-party script and no cookies, which pairs fine with either version above.

What happens after the form posts

Whether the browser does the POST natively or your script does it via fetch, the request lands on the same endpoint and gets the same treatment: the raw submission is written to durable storage before any response comes back, then flows through independently retried queue stages — a database write that populates your dashboard, and an email notification if the form has recipients configured. A slow mail send doesn't hold up your response or risk the submission.

Cost

Free covers 100 submissions a month across 2 forms, no card required — enough for most Astro content sites and portfolios. Sign up for an endpoint and swap your-form for your real slug in whichever version you choose.

Stop losing form submissions

SubmissionBuddy stores every submission durably before anything else touches it — 100 free submissions/month, no credit card, live in five minutes.

Start free