Skip to content

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

  1. In the dashboard, open Email services and click Add service.
  2. 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.
  3. Tick Make default and click Verify & save. Frontmail checks the credentials without sending anything.
  1. Open Templates → From gallery and choose Contact form (category Forms).
  2. 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.

  1. Open Security and add your domain under Allowed websites (for example example.com and www.example.com; add localhost while you develop). Click Save.
  2. 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 (check www and 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 held and 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 FrontmailForm component with the Turnstile widget built in – see the SDK documentation.
All tutorials