How to add a contact form to a static site
Short answer: A static site has no server, so the form must post to a hosted endpoint. Change the form's action to that endpoint, keep method="POST", and the hosting platform never needs to run code.
Static hosting platforms serve files. They do not execute a script when a visitor submits a form, which is why a contact form is usually the first thing that breaks when a site moves to Netlify, Vercel, Cloudflare Pages or GitHub Pages.
The fix is smaller than most tutorials suggest. Your markup stays static; the request goes somewhere else.
The two attributes that matter
<form action="https://formivo.faizyab.me/api/f/YOUR_FORM_ID" method="POST">
<label for="email">Email</label>
<input id="email" type="email" name="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
<button type="submit">Send</button>
</form>Each name attribute becomes a key in the stored submission. A form that omits method defaults to GET, which the endpoint rejects with 405, so method="POST" is not optional.
Send the visitor back to your site
Without extra configuration the visitor sees a Formivo thank-you page. That works, but it breaks the illusion that the form is part of your site. Add a hidden redirect field to keep them where they were.
<input type="hidden" name="_redirect" value="https://your-site.com/thanks" />Add a honeypot before you publish
Static forms are scraped within days. A hidden field that people never see but naive bots fill will remove most automated submissions without asking a real visitor to solve anything.
<div aria-hidden="true" style="position:absolute;left:-9999px">
<label for="website">Website</label>
<input id="website" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
</div>- Position the field off screen instead of using display:none, because some bots skip hidden inputs.
- Remove it from the tab order with tabindex="-1" and hide the wrapper from screen readers.
- Keep the field name exactly _gotcha: the endpoint treats a filled value as a bot and discards the request.
What about a framework build step?
Astro, Eleventy, Hugo and Next.js static export all produce HTML in the end. If the page ships a form element, the same two attributes work. A client-side framework only changes whether you also want an inline success message instead of a redirect.
| Host | Form works as-is? | Notes |
|---|---|---|
| Netlify | Yes | Do not enable Netlify Forms for the same form; pick one receiver. |
| Vercel | Yes | No serverless function is needed for this approach. |
| GitHub Pages | Yes | Cannot run server code at all, which is fine here. |
| Cloudflare Pages | Yes | A Pages Function works too, but is not required. |
Checklist before you ship
- 1action points at the endpoint and method is POST.
- 2Every input has a label and a meaningful name attribute.
- 3_redirect points at a real page that thanks the visitor.
- 4A honeypot field is present and reachable by bots but not by people.
- 5Submit the form once from the deployed site, not only locally.