Why static sites need outside help for forms
A static site is just HTML, CSS, and maybe a sprinkle of JavaScript served by a CDN. There's no Node, no PHP, no database — which is exactly why static sites are fast, cheap, and hard to attack. But it also means there is nothing on the server to receive a form POST. The browser sends the data somewhere, and that somewhere has to be a real backend.
You have three honest options: pay a form backend service to receive the POST, run a tiny serverless function in front of your static site, or fall back to mailto: and accept the UX hit. The rest of this guide walks through each, with copy-pasteable HTML you can ship today.
The three real options in 2026
Setting aside the legacy stuff (PHP mail(), Google Forms iframe hacks, Formtools self-hosted on a VPS you'll forget to patch), three patterns actually work in production:
- Hosted form backend — services like splitforms, Formspree, Web3Forms, Basin, or Netlify Forms. Drop a URL into
action, done. - Serverless function — a tiny Vercel, Netlify, or Cloudflare Worker that accepts the POST and forwards via Resend, Postmark, or Mailgun.
- mailto: link— no backend at all; opens the visitor's mail client.
Method 1: Form backend (recommended)
This is the path of least resistance. A form backend exposes a single POST endpoint. You point your form at it, you get email. Setup takes about 90 seconds.
Here's a complete, working contact form using splitforms:
<form action="https://splitforms.com/api/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="hidden" name="redirect" value="https://yoursite.com/thanks" />
<input type="hidden" name="subject" value="New lead from yoursite.com" />
<label>
Name
<input type="text" name="name" required />
</label>
<label>
Email
<input type="email" name="email" required />
</label>
<label>
Message
<textarea name="message" rows="5" required></textarea>
</label>
<!-- honeypot: bots fill it, humans don't see it -->
<input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send</button>
</form>That's the entire integration. No fetch, no JSON, no CORS headache, no DNS records. The browser POSTs the form, splitforms receives it, runs spam filtering, emails you the contents, and 302-redirects the visitor to your /thanks page.
If you want to stay on the same page (single-page-app style), use fetch() instead:
<form id="contact">
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
<script>
document.getElementById('contact').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
formData.append('access_key', 'YOUR_ACCESS_KEY');
const res = await fetch('https://splitforms.com/api/submit', {
method: 'POST',
body: formData,
});
if (res.ok) {
e.target.innerHTML = '<p>Thanks — we\'ll be in touch.</p>';
} else {
alert('Something went wrong. Please email us directly.');
}
});
</script>splitforms returns CORS headers by default, so the fetch works from any origin. The same code runs on Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3 + CloudFront, or a USB stick served from a Raspberry Pi.
Method 2: Serverless function
If you want to own the email-sending pipeline yourself — maybe to add custom logic, store submissions in a database, or trigger a Slack message — a tiny serverless function works well. The tradeoff: you now own deliverability, retries, spam filtering, and uptime.
Here's a Cloudflare Worker version using Resend:
// worker.js
export default {
async fetch(request, env) {
if (request.method !== 'POST') return new Response('Nope', { status: 405 });
const data = await request.formData();
const name = data.get('name');
const email = data.get('email');
const message = data.get('message');
// honeypot
if (data.get('botcheck')) return new Response('OK'); // silently drop
const r = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + env.RESEND_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'forms@yourdomain.com',
to: 'you@yourdomain.com',
subject: 'New contact form submission',
text: `From: ${name} <${email}>\n\n${message}`,
}),
});
if (!r.ok) return new Response('Email send failed', { status: 500 });
return Response.redirect('https://yoursite.com/thanks', 303);
},
};Then point your form at https://your-worker.workers.devinstead of splitforms. You'll need a verified sending domain in Resend (SPF + DKIM records) or your emails will land in spam — see receive form submissions by email for the deliverability checklist.
Method 3: mailto: link
The simplest option. No backend at all:
<a href="mailto:you@yoursite.com?subject=Inquiry&body=Hi%20there">
Email me
</a>You can technically do this with a <form> too:
<form action="mailto:you@yoursite.com" method="POST" enctype="text/plain">
<input name="name" />
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>Both rely on the visitor having a configured email client. Mobile users on Gmail-only setups, anyone using a shared computer, and most corporate users will get a broken experience. Industry data from form analytics providers consistently shows mailto: converts 3–10x worse than a real form. Use it as a fallback, not your primary CTA.
Side-by-side comparison
For 95% of marketing sites, portfolios, landing pages, and docs sites, the form backend wins on every axis except "I want to own everything."
Spam protection without breaking UX
Static-site contact forms get scraped fast. Within a week of going live, expect 5–50 bot submissions a day. Three layers stop ~99% of them without a CAPTCHA in front of human visitors:
- Honeypot field — a hidden input named something like
botcheckorwebsite. Bots fill every input they see; if it's populated, drop the submission silently. - Time-to-submit check — humans take 5+ seconds to fill a form; bots take milliseconds. splitforms tracks this automatically.
- Rate limiting per IP and per access key— caps obvious flood attacks. Built into splitforms; in a serverless function you'd use Cloudflare KV or Upstash Redis.
If those three aren't enough (you'll know — your inbox will tell you), bolt on Cloudflare Turnstile. It's free, GDPR-clean, and passes ~99% of humans without showing a challenge.
Deploying on Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3
The HTML above runs unchanged on every static host. A few host-specific notes:
- Netlify— has its own free Forms feature (100 submissions/mo, then $19/mo for 1,000). splitforms' free tier gives you 5x the volume at the same price.
- Vercel — no native form handling. Use a form backend or write a Server Action / Route Handler.
- Cloudflare Pages — same as Vercel. Pair with Workers if you go the serverless route.
- GitHub Pages — pure static, no functions. Hosted form backend is your only realistic option.
- S3 + CloudFront — same constraint. Hosted backend or API Gateway + Lambda.
Tech support and troubleshooting
The most common static-site form failures and the one-line fix:
- Form posts but no email arrives. Search the spam folder for the splitforms domain. If nothing is there, open the dashboard's Submissions tab — if you see the row, the failure is on the email leg (DMARC, full inbox, blocked sender). Otherwise the form HTML is wrong.
- Browser shows a raw JSON response after submit. Add
<input type="hidden" name="redirect" value="https://yoursite.com/thanks" />so the browser follows a 302 to your thank-you page instead of rendering the API response. - CORS error in the console when using fetch(). The endpoint URL is wrong. Confirm it is exactly
https://splitforms.com/api/submit. splitforms returns CORS headers for any origin from that endpoint. - Submissions are getting blocked as spam. Check the dashboard for the spam reason — usually the honeypot tripped because something on the page auto-fills hidden inputs (Bitwarden, LastPass). Rename the honeypot input from
botchecktowebsite_urlor any uncommon name. - Localhost tests get 403. You enabled the "Allowed domains" restriction in your dashboard. Either disable it during development or include
localhostin the allow-list.
The full failure-mode catalogue lives in contact form not working.
Where to go next
- Read the splitforms docs for the request contract, headers, and webhook payload.
- The API reference covers every parameter, response code, and error shape.
- Browse the FAQ for billing, deliverability, and security questions.
- Compare splitforms with the alternatives: vs Formspree, vs Web3Forms, vs Netlify Forms.
Frequently asked questions
What is the easiest way to add a contact form to a static HTML site?
Point your form's action attribute at a hosted form backend like splitforms (https://splitforms.com/api/submit) and add a hidden input with your access key. The backend receives the POST, emails you the submission, and redirects the visitor to a thank-you page. No server, no JavaScript required.
Do I need JavaScript for a static site contact form?
No. A plain HTML <form> with method="POST" and action set to a form backend URL works without any JavaScript. JavaScript is only needed if you want to submit via fetch() and stay on the same page, or to add client-side validation.
Can I use mailto: links instead of a form backend?
You can, but mailto: opens the visitor's local email client, which most users on mobile or shared computers don't have configured. Industry conversion data shows mailto links convert 3-10x worse than a real form. Use a form backend or serverless function instead.
Is it safe to put my access key in HTML?
Yes, with one caveat. Form backend access keys are designed to be public (they're like Stripe publishable keys). Lock the key to your domain in the splitforms dashboard so other sites can't use it to send spam from your account.
How do I prevent spam on a static site contact form?
Add a honeypot field (a hidden input bots fill in but humans don't) and rely on your form backend's built-in spam filtering. splitforms uses honeypot detection plus heuristic scoring out of the box. Add Cloudflare Turnstile if you want a CAPTCHA fallback.
Will a static-site form work without HTTPS?
The form will technically POST, but every modern browser flags non-HTTPS forms as insecure and most form backends (splitforms included) reject HTTP-origin requests. Static hosts like Netlify, Vercel, Cloudflare Pages, and GitHub Pages give you free HTTPS — there is no reason to skip it.
Can I send the same submission to multiple destinations from a static site?
Yes. Configure a webhook on the splitforms form pointing at Slack, Discord, Zapier, or your own endpoint, and submissions fan out automatically while still landing in your inbox. The static HTML stays a one-form-tag setup.
Where can I get help if my static site form is broken?
Read /faq for common deliverability and access-key issues, /docs for the request contract, and /api-reference for the full endpoint spec. The blog post 'contact form not working' walks through the eight failure modes splitforms support sees most often.