Astro
Add a contact form to an Astro site and keep it fully static. A plain form post works with no JavaScript, and a small client script keeps visitors on the page.
Before you start
You need a form endpoint from the dashboard (see the Quickstart) and an Astro project. Nothing here needs an adapter or server output: the form posts to MakeTheForm directly, so your site can stay fully static and deploy anywhere.
- 1
Add the form
An Astro page renders to plain HTML, so a normal form is all you need. Set
actionto your endpoint andmethodtoPOST, and give every input aname.src/pages/contact.astro--- // src/pages/contact.astro const endpoint = 'https://mtform.co/f/your-form-key'; --- <html lang="en"> <head> <meta charset="utf-8" /> <title>Contact</title> </head> <body> <form action={endpoint} method="POST"> <label> Name <input name="name" autocomplete="name" required /> </label> <label> Email <input type="email" name="email" autocomplete="email" required /> </label> <label> Message <textarea name="message" required></textarea> </label> <!-- Spam trap: hidden from people, tempting to bots. Leave it empty. --> <input type="text" name="_mtf_honeypot" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px" /> <button type="submit">Send</button> </form> </body> </html>Keep the spam trap as it is. The_mtf_honeypotinput is positioned off-screen, not hidden withdisplay: none, because bots skip fields they can tell are hidden. Leave it empty and never mark it required. - 2
Test it
Run
astro dev, open/contactand submit. The browser follows a redirect to a plain confirmation page, and the submission appears under Submissions in the dashboard.To send visitors back to your own thank-you page instead, add the redirect field described in HTML forms. Its target must be on one of the form's allowed domains.
- 3
Optional: stay on the page
To show the result without leaving the page, add a client script to the component. The form still has its
action, so it works as a plain post if the script never runs.src/components/ContactForm.astro--- // src/components/ContactForm.astro const endpoint = 'https://mtform.co/f/your-form-key'; --- <form id="contact-form" action={endpoint} method="POST"> <label> Name <input name="name" autocomplete="name" required /> </label> <label> Email <input type="email" name="email" autocomplete="email" required /> </label> <label> Message <textarea name="message" required></textarea> </label> <input type="text" name="_mtf_honeypot" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px" /> <button type="submit">Send</button> <p id="contact-status" role="status"></p> </form> <script> const form = document.getElementById('contact-form') as HTMLFormElement; const status = document.getElementById('contact-status') as HTMLParagraphElement; const button = form.querySelector('button') as HTMLButtonElement; form.addEventListener('submit', async (event) => { event.preventDefault(); button.disabled = true; status.textContent = ''; try { const response = await fetch(form.action, { method: 'POST', headers: { Accept: 'application/json' }, body: new FormData(form), }); const result = await response.json(); if (!response.ok) { throw new Error(result.error?.message ?? 'Unable to send your message.'); } form.reset(); status.textContent = 'Thanks. We got your message.'; } catch (error) { // Entered values stay put; we only reset on success. status.textContent = error instanceof Error ? error.message : 'Unable to send your message.'; } finally { button.disabled = false; } }); </script>The script sends the form as
FormDatawithAccept: application/json, so the endpoint answers with JSON instead of a redirect. Do not set aContent-Typeyourself: the browser adds the right one, and sendingFormDataalso means a file input is sent along with the other fields.Astro processes and bundles
<script>tags in components and runs them after the page has parsed, so the form exists when the script looks it up. Keep the script in the same component as the form.