How to add a contact form to your website without a backend
A plain HTML contact form that emails you every message – no server, no PHP, no plugin. Works on any static site, from hand-written HTML to Astro, Hugo or WordPress.
Updated:
You’ll build an ordinary HTML form that sends every submission to your inbox. The email is sent by Frontmail through the mailbox you already use (Gmail, Outlook, any SMTP server or a transactional provider), so there’s no server of your own to run.
You need: a free Frontmail account and about ten minutes.
1. Connect your mailbox
- In the dashboard, open Email services and click Add service.
- Pick your provider. Gmail and Outlook connect with your account (no password stored); SMTP, Resend, Postmark, Mailgun and others take an API key or a login.
- Tick Make default and click Verify & save. Frontmail checks the credentials without sending anything.
2. Create the template from the gallery
- Open Templates → From gallery and choose Contact form (category Forms).
- Click Use template. To is your own address and Reply-To is the visitor, so hitting Reply answers them directly.
The template expects these parameters – they become the name attributes of your form fields:
| Parameter | Required | What it is |
|---|---|---|
name |
yes | The visitor’s name |
email |
yes | The visitor’s address (validated) |
phone |
no | Phone number |
topic |
no | Topic, shown in the subject |
message |
yes | The message |
3. Allow your website
Your public key sits in the page source, which is fine – but only your site should be able to use it.
- Open Security and add your domain under Allowed websites (for example
example.comandwww.example.com; addlocalhostwhile you develop). Click Save. - Recommended: create a free Cloudflare Turnstile widget for your hostnames, paste its site key and secret key into Security → Bot protection and click Turn on CAPTCHA for all templates. Bots then can’t use your form, and it’s usually invisible to people.
4. Add the form to your page
Copy the public key (Security) and the template ID (the template’s Snippets tab), then paste this where the form should appear:
<form id="contact">
<label>Name <input name="name" required maxlength="200" /></label>
<label>Email <input name="email" type="email" required /></label>
<label>Message <textarea name="message" required></textarea></label>
<!-- Turnstile adds a hidden cf-turnstile-response field; leave it out if you skipped the CAPTCHA -->
<div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY"></div>
<button type="submit">Send</button>
<p id="form-status" role="status"></p>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script
src="https://cdn.jsdelivr.net/npm/@frontmail/browser@0.1.0/dist/frontmail.umd.js"
integrity="sha384-g2Rxznkz2yb/YYSdVTkXLxZFZ2Nh2+XdhPStxh0stydXFF3gYjDuyHimWIZLJDCc"
crossorigin="anonymous"></script>
<script>
frontmail.init({ publicKey: 'pk_…' });
const form = document.getElementById('contact');
const status = document.getElementById('form-status');
form.addEventListener('submit', async (event) => {
event.preventDefault();
status.textContent = 'Sending…';
try {
// null = your default service
await frontmail.sendForm(null, 'tpl_…', form);
status.textContent = 'Thank you! We’ll get back to you soon.';
form.reset();
} catch (err) {
status.textContent = 'Sorry, that didn’t work: ' + err.message;
} finally {
window.turnstile?.reset(); // a Turnstile token works only once
}
});
</script>
sendForm() reads the fields by their name, so the form and the template stay in sync without
any mapping code. Network errors and server hiccups are retried automatically with the same
idempotency key – a visitor never sends you the same message twice.
5. Test it
Open the page, fill in the form and send it. The email arrives in your inbox within seconds, and History in the dashboard shows the message with its full timeline.
If something doesn’t work, the error tells you why:
origin_not_allowed– the page’s domain isn’t in Allowed websites (checkwwwand the port).captcha_required– the template needs Turnstile, but the widget is missing from the form.invalid_template_params– a required field is empty or a value doesn’t fit the parameter.
Ran out of credits during a busy week? The message isn’t lost: it’s accepted with the status
heldand goes out automatically as soon as you add credits.
Next steps
- Send the visitor a confirmation: add the Auto-reply template from the gallery and select it in the contact template’s Settings → Delivery.
- Using React, Vue or Svelte? The framework packages include a ready
FrontmailFormcomponent with the Turnstile widget built in – see the SDK documentation.


