Astro Guide

Astro Form Guide

Build a copy-pasteable Astro form that posts straight to Formigo - no server, no plugins, no fuss. Wire up an Astro contact form with client-side fetch, spam protection, and success/error handling.

Why Astro + Formigo

Static-first, zero backend

Ship a fast Astro contact form with no server to maintain

Astro ships static HTML by default - which is great for speed, but means there's no server waiting to catch your form submissions. That's exactly where Formigo fits: your Astro form posts directly to a Formigo endpoint (formigo.io/f/your-form) and we handle delivery to email, an API, or wherever you've pointed it.

  • JSON or form-encoded - Formigo accepts both, and it's CORS-enabled, so a browser fetch just works.
  • Spam protection built in - add a honeypot (_formigo_hp) and a load timestamp (_formigo_t) and bots get filtered out quietly.
  • Friendly responses - any 2xx means success; a 429 returns { retry_after } so you can tell the user when to try again.

The recommended path is the client-side fetch below - it works on a fully static Astro site with no adapter. If you're running Astro in SSR mode and want to keep the endpoint off the client, jump to the Server Endpoint variant.

The .astro Form Component

Astro contact form (recommended)

Static markup + a bundled client script doing the fetch

Drop this into src/components/ContactForm.astro and render it from any page (for example <ContactForm /> in src/pages/contact.astro). The markup is plain HTML; the <script> runs in the browser, sets the spam fields on load, and submits via fetch.

Step 1: Create the component

The frontmatter (between the --- fences) runs at build time. We use it only to configure the endpoint, then reference it in the markup.

---
// src/components/ContactForm.astro
// Replace "your-form" with your real Formigo form slug.
const FORMIGO_ENDPOINT = "https://formigo.io/f/your-form";
---

<form id="contact-form" class="contact-form">
  <div class="field">
    <label for="name">Name *</label>
    <input type="text" id="name" name="name" required />
  </div>

  <div class="field">
    <label for="email">Email *</label>
    <input type="email" id="email" name="email" required />
  </div>

  <div class="field">
    <label for="message">Message *</label>
    <textarea id="message" name="message" rows="5" required></textarea>
  </div>

  <!-- Spam protection: honeypot + load timestamp (set by the script below) -->
  <input
    type="text"
    name="_formigo_hp"
    value=""
    tabindex="-1"
    autocomplete="off"
    style="position:absolute;left:-9999px"
    aria-hidden="true"
  />
  <input type="hidden" name="_formigo_t" value="" />

  <button type="submit">Send Message</button>
  <p id="status" role="status" aria-live="polite"></p>
</form>

<script define:vars={{ endpoint: FORMIGO_ENDPOINT }}>
  const form = document.getElementById("contact-form");
  const status = form.querySelector("#status");
  const submitBtn = form.querySelector("button[type=submit]");
  const timestamp = form.querySelector("input[name=_formigo_t]");

  // Stamp the form the moment the page loads. Instant submits look like bots.
  timestamp.value = Math.floor(Date.now() / 1000);

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

    submitBtn.disabled = true;
    submitBtn.textContent = "Sending...";
    status.textContent = "";
    status.className = "";

    const data = Object.fromEntries(new FormData(form));

    try {
      const response = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });

      if (response.ok) {
        status.className = "success";
        status.textContent = "Thanks! Your message is on its way.";
        form.reset();
        timestamp.value = Math.floor(Date.now() / 1000);
      } else if (response.status === 429) {
        const { retry_after } = await response.json();
        status.className = "error";
        status.textContent = `Whoa there - try again in ${retry_after}s.`;
      } else {
        status.className = "error";
        status.textContent = "Something went wrong. Please try again.";
      }
    } catch (error) {
      status.className = "error";
      status.textContent = "Network error. Please try again.";
      console.error("Formigo submit failed:", error);
    } finally {
      submitBtn.disabled = false;
      submitBtn.textContent = "Send Message";
    }
  });
</script>

<style>
  .contact-form { max-width: 32rem; }
  .field { margin-bottom: 1rem; }
  label { display: block; margin-bottom: 0.5rem; font-weight: 600; }
  input,
  textarea {
    width: 100%;
    padding: 0.75rem;
    border: 2px solid #ddd;
    border-radius: 8px;
  }
  button {
    padding: 0.75rem 2rem;
    background: #FF6B9D;
    color: #fff;
    border: none;
    border-radius: 8px;
    font-weight: 600;
    cursor: pointer;
  }
  button:disabled { opacity: 0.5; cursor: not-allowed; }
  .success { color: #155724; }
  .error { color: #721c24; }
</style>

Step 2: Use it on a page

Import the component and render it wherever you want the form to appear.

---
// src/pages/contact.astro
import Layout from "../layouts/Layout.astro";
import ContactForm from "../components/ContactForm.astro";
---

<Layout title="Contact us">
  <main>
    <h1>Get in touch</h1>
    <ContactForm />
  </main>
</Layout>

Step 3: Swap in your form slug

Replace your-form in FORMIGO_ENDPOINT with the slug from your Formigo dashboard, deploy, and submit a test message. You're done - submissions land in your inbox (or wherever you configured delivery) without a single line of backend code.

Tip: prefer form-encoded over JSON? Send body: new URLSearchParams(new FormData(form)) and drop the Content-Type header - Formigo accepts both formats.

SSR Server Endpoint (optional)

Proxy through an Astro endpoint

SSR only - requires an adapter (Node, Vercel, Netlify, etc.)

When to use this: only if you want to keep the Formigo endpoint off the client - for example to add server-side validation or hide a future API key. It needs an SSR adapter, plus either output: "server" or a per-route export const prerender = false on a static site. For a plain static site, the component above is all you need.

A server endpoint lives at src/pages/api/contact.ts and exports a POST handler. Your Astro form posts to /api/contact, and the endpoint forwards the data to formigo.io/f/your-form server-side.

// src/pages/api/contact.ts
// Requires an SSR adapter (Node, Vercel, Netlify, etc.).
// On a default (static) site, the `prerender = false` line below opts this
// route into on-demand rendering. With `output: "server"` it is optional.
import type { APIRoute } from "astro";

export const prerender = false;

const FORMIGO_ENDPOINT = "https://formigo.io/f/your-form";

export const POST: APIRoute = async ({ request }) => {
  // The client posts JSON, so read it as JSON (this includes the
  // _formigo_hp honeypot and _formigo_t timestamp fields).
  const payload = await request.json();

  const response = await fetch(FORMIGO_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });

  if (response.ok) {
    return new Response(JSON.stringify({ ok: true }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  }

  // Pass Formigo rate-limit info back to the browser.
  if (response.status === 429) {
    const { retry_after } = await response.json();
    return new Response(JSON.stringify({ ok: false, retry_after }), {
      status: 429,
      headers: { "Content-Type": "application/json" },
    });
  }

  return new Response(JSON.stringify({ ok: false }), { status: 502 });
};

On the client, point the same script at your endpoint instead of Formigo - just change one line:

// In ContactForm.astro, swap the endpoint:
const FORMIGO_ENDPOINT = "/api/contact";

// The fetch, honeypot, timestamp, and 429 handling stay exactly the same -
// your endpoint relays Formigo's response (including retry_after) straight through.

Ready to wire up your Astro form?

Create a free Formigo form, grab your endpoint, paste the component above, and ship. No backend, no servers, no headaches.

Set up your first Astro contact form in minutes.