Content framework
Astro form handling without a backend
Astro shines at static, content-heavy sites, which is exactly where you do not want to add a server just to take a contact form. You can keep the site fully static and let Formward — an EU-hosted form backend — receive the submissions. No SSR adapter, no server endpoint, no database.
The simplest version is a plain HTML <form> with an action; it works with zero JavaScript and Formward responds with a redirect or thank-you page. The example also adds a small inline <script> so you can submit via fetch() and show success or error in place when JavaScript is available.
EU-hosted · A static Astro site with an EU form endpoint is the cleanest GDPR story there is: nothing dynamic to host, and no form data leaving the EU.
The Astro example
Copy this into your project and replace <FORM_ID> with the id of a form you create in the Formward dashboard.
astro
---
// src/components/ContactForm.astro — no server code, fully static.
---
<form id="contact" action="https://forms.formward.eu/f/<FORM_ID>" method="POST">
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<!-- Honeypot: leave empty. Bots that fill it are dropped. -->
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off"
style="display:none" aria-hidden="true" />
<button type="submit">Send</button>
<p id="contact-status" role="status"></p>
</form>
<script>
const form = document.getElementById("contact");
const out = document.getElementById("contact-status");
form?.addEventListener("submit", async (e) => {
e.preventDefault();
out.textContent = "Sending…";
try {
const res = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const data = await res.json(); // { ok: true, id, files }
if (!res.ok || !data.ok) throw new Error(data.error || res.status);
form.reset();
out.textContent = "Thanks — we received your message.";
} catch {
out.textContent = "Could not send. Please try again.";
}
});
</script>Success and error handling
- Without the <script>, the form still works: a no-JS POST gets a 302 redirect (or your configured thank-you page), so the form degrades gracefully.
- With the <script>, e.preventDefault() plus the Accept: application/json header keeps the visitor on the page and gives you the { ok, id, files } JSON.
- Astro strips the inline script into its own module at build time; the form action still points at Formward either way.
Spam protection
Every example above includes the _gotcha honeypot field. It is hidden from real users and must stay empty; Formward silently drops any submission where it is filled, which stops most bots with no CAPTCHA. For a stricter gate, add a Cloudflare Turnstile widget and send its token as the cf-turnstile-response field — Formward verifies it on receipt.
The JSON response
A standard POST gets a 302 redirect (or your thank-you page). Send the Accept: application/json header — as every fetch() example above does — and Formward returns JSON instead:
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true, "id": "clxyz123...", "files": [] }On failure the body is { ok: false, error } with a 4xx status (validation, plan limit, and so on). See the AJAX docs for the full status-code table and CORS notes.
Other frameworks
Collect your first Astro submission
Create a form, paste the snippet, and keep every submission in the EU. GDPR-clean from the first POST.