Contact forms for static sites, no functions required
A static site has no server to receive a form, and it does not need one. The visitor's browser posts straight to Formward, which filters spam, stores the message in the EU, emails you, and sends the visitor back to your thank-you page. Paste this into any template:
<form action="https://forms.formward.eu/f/YOUR_FORM_ID" method="POST">
<input type="hidden" name="_redirect" value="/thanks/">
<input type="text" name="name" required>
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px;opacity:0">
<button type="submit">Send</button>
</form>No serverless function, no build step, no third-party script. The same markup works on GitHub Pages, Cloudflare Pages, an S3 bucket or your own nginx, and moving hosts does not touch the form.
Per-generator notes
The form is identical everywhere. Only where you keep it changes.
| Hugo | Keep it as a shortcode in layouts/shortcodes/contact-form.html and read the endpoint from your site config. | Hugo guide |
| Jekyll | Add it to contact.html or a layout, with the endpoint stored in _config.yml. | Jekyll guide |
| Eleventy | Add it to a page template such as src/contact.njk. Eleventy passes the markup through unchanged. | Eleventy guide |
| Astro | Paste it into a .astro component. It needs no client-side JavaScript. | Astro example |
| Gatsby | Render it from a React component and submit with fetch() so the visitor stays on the page. | Gatsby guide |
Moving off Netlify Forms
Netlify detects forms at deploy time and ties them to the site. Switching is a small markup change: drop the Netlify attributes and the hidden form-name input, add the action, and rename the honeypot to _gotcha.
<!-- Before: Netlify Forms -->
<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
<input type="hidden" name="form-name" value="contact">
<input name="bot-field" hidden>
...
</form>
<!-- After: Formward -->
<form action="https://forms.formward.eu/f/YOUR_FORM_ID" method="POST">
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px;opacity:0">
...
</form>To bring old submissions with you, export them from Netlify as CSV and import the file into the Formward form. The original created_at dates are kept.
Redirects and CORS
- A normal POST gets a 302 redirect. The target is the
_redirectfield if it is a relative path or an allowed origin, then the redirect set in the dashboard, then Formward's default thank-you page. - A relative
_redirectsuch as/thanks/resolves against the site that sent the form, so one snippet works on staging and production. - With
fetch()andAccept: application/jsonyou get JSON instead of a redirect. Cross-origin requests are checked against the form's allowed origins, so add your domain there before going live.
The AJAX docs have a complete fetch() example.
Compared with the other options
- A serverless function works, but you now maintain code, secrets and an email provider account for something an
actionattribute can do. - Host-bundled forms tie the form to one host and its pricing; leaving the host means redoing the form.
- Another form backend plugs in the same way, through the
actionURL. If you are weighing Formspark, the Formspark comparison covers pricing model, hosting and uploads. - A mailto: link relies on every visitor having a mail app set up and gives you no record.
Where the data lives
Formward stores submissions, file attachments and backups in Sweden, on servers run by Hostup AB. Notification email goes out through Mailjet from an EU data centre. Submitter IP addresses are replaced with a keyed HMAC-SHA-256 hash on receipt, so the raw address is never written to disk. On Professional and Business, AI features run on Mistral AI in France and can be switched off per form.
Data leaves the EU only where you send it: a notification inbox hosted outside the EU, a webhook URL, a Slack channel or a Google Sheet. Cloudflare Turnstile is a US service, and it is off unless you switch it on for a form. The details are in the sub-processor register, the data residency page and the published DPA. For the wider picture, see GDPR form backend.
Questions
- Does the form need JavaScript?
- No. It is a standard HTML POST followed by a redirect. Add fetch() later if you want an in-page success message.
- Will it break if I change hosts?
- No. The form posts to Formward, not to your host, so moving from Netlify to GitHub Pages or anywhere else changes nothing.
- Do I have to rebuild the site when settings change?
- No. Recipients, spam settings, the default redirect and retention live in the Formward dashboard. Only the markup is in your repository.
- What does it cost for a small site?
- The Free plan covers 50 submissions a month, which is plenty for most personal and small business sites. Free submissions are deleted after 30 days.
Keep the site static. Add the form.
Start free with 50 submissions a month. No card needed.