Contact Form for Static Sites Using Cloudflare Workers and Airtable
If you’re running a static site, adding a contact form can be challenging since there’s no backend to handle form submissions. Sure, you can list your e-mail address or link to social profiles, but a contact form offers a better user experience. My goal was to set up a simple, spam-protected contact form at no cost for a low traffic static site. In this guide, I’ll show you how to build one using:
- A static HTML form
- A Cloudflare Worker to process submissions
- Airtable to store messages and send e-mail notifications
Instructions
Step 1: Create Contact Form
Start by building a basic HTML form with the following fields (all required): Name, Email, Subject, and Message. Add a honeypot field, which can be hidden with CSS, to help detect bots (which often fill in all available fields, including hidden ones).
For simplicity, no styling is applied to the form.
<form id="contactForm">
<label for="name">Name</label>
<input type="text" id="name" name="name" placeholder="" value="" required>
<label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="" value="" required>
<label for="subject">Subject</label>
<input type="text" id="subject" name="subject" placeholder="" value="" required>
<label for="message">Message</label>
<textarea id="message" name="message" placeholder="" required></textarea>
<div>
<label for="honeypot">Do Not Use</label>
<textarea id="honeypot" name="honeypot"></textarea>
</div>
<button id="submit-button" type="submit">Send Message</button>
</form>
<div id="response"></div>
The <form> element does not include action or method attributes. Instead, use JavaScript to:
- Block many spambots that can’t process JavaScript
- Submit the form without reloading the page
- Prevent multiple submissions
let isSubmitted = false;
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('contactForm').addEventListener('submit', async (e) => {
e.preventDefault();
if (!isSubmitted) {
isSubmitted = true;
document.getElementById('submit-button').disabled = true;
document.getElementById('submit-button').classList.add('disabled');
const formData = new FormData(document.getElementById('contactForm'));
try {
const response = await fetch('/contact/submit/', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(response.status);
}
const responseText = await response.text();
document.getElementById('response').textContent = responseText;
} catch (error) {
document.getElementById('response').textContent = `ERROR: ${error.message}`;
}
}
return;
});
return;
});
Step 2: Set Up Airtable (Workspace, Base, Table, and API Keys)
We’ll use Airtable to collect form data and send e-mail notifications.
- Sign in or create an Airtable account.
- Create a new workspace (e.g.,
Cloudflare Workers).

- Add a base named
Messages, and create a table also calledMessages.

- Add these fields to the table:
Name,Email,Subject, andMessage.

- Click the
Helpbutton and openAPI documentation. In theIntroductionsection, note theIDfor theMessagesbase — this will be referred to asAIRTABLE_BASE_IDin a later step.

- In the
MESSAGES TABLEsection, note theIDfor theMessagestable — this will be referred to asAIRTABLE_TABLE_IDin a later step.

- Go to the
Personal access tokenspage andCreate new token. Name the token and add the scopedata.records:writewith access to the appropriate base. Click theCreate tokenbutton and note theTOKEN ID— this will be referred to asAIRTABLE_ACCESS_TOKENin a later step.

- Optional but recommended: Return to the
Messagesbase and clickAutomations. Create an automation to send a notification e-mail to a verified e-mail address each time a new entry is created in theMessagestable.

In the Send an email action, configure the notification to include the appropriate subject line and fields from the Messages table as well as automation timing.

Step 3: Create a Cloudflare Worker
Now we’ll create the backend service that connects your HTML form to Airtable.
- Log in to Cloudflare.
- Click
Compute (Workers)and go toWorkers & Pagesand click theCreatebutton. - Under the
Workerstab, click theGet startedbutton forStart with Hello World!. - On the
Create an applicationpage, rename the project to something more meaningful and click theDeploybutton. - After deployment, click the
Continue to projectbutton. - On the dashboard, click the
Settingstab. - Configure
Domains & Routes. Disableworkers.devandPreview URLsroutes. Define a custom route (non-wildcard) specific to your site (e.g.,/contact/submit/).

- Under
Variables and Secrets, define three new environment variables of typeTextusing the three API key names and their associated values noted from Airtable:AIRTABLE_ACCESS_TOKEN,AIRTABLE_BASE_ID, andAIRTABLE_TABLE_ID. Click theDeploybutton.

- Returning to the dashboard and open the code editor.
- Replace the template code with the following code. Adjust the conditions for
requestURL(which should match the enabled route defined inDomains & Routes) andrefererURL(which should match the URL of the HTML form that will post to this worker). These two conditions are included for basic spam detection and can be removed if you enjoy receiving spam. Click theDeploybutton.
async function createRecord(env, body) {
try {
const result = fetch(
`https://api.airtable.com/v0/${env.AIRTABLE_BASE_ID}/${encodeURIComponent(env.AIRTABLE_TABLE_ID)}`,
{
method: 'POST',
body: JSON.stringify(body),
headers: {
Authorization: `Bearer ${env.AIRTABLE_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
},
);
return result;
} catch (error) {
}
}
export default {
async fetch(request, env) {
if (request.method !== 'POST') {
return new Response('Message Failed: Method Not Allowed', { status: 200 });
}
const requestURL = new URL(request.url);
if (requestURL.toString() !== 'https://johndalesandro.com/contact/submit/') {
return new Response('Message Failed: Endpoint Not Found', { status: 200 });
}
const refererURL = new URL(request.headers.get('referer'));
if (refererURL.toString() !== 'https://johndalesandro.com/contact/') {
return new Response('Message Failed: Flagged As Spam (Invalid Referer)', { status: 200 });
}
try {
const body = await request.formData();
const { name, email, subject, message, honeypot } = Object.fromEntries(body);
if (honeypot) {
return new Response('Message Failed: Flagged As Spam (Honeypot)', { status: 200 });
}
if (!name || !email || !subject || !message) {
return new Response('Message Failed: Invalid Input', { status: 200 });
}
const reqBody = {
fields: {
Name: name,
Email: email,
Subject: subject,
Message: message,
},
};
const response = await createRecord(env, reqBody);
if (!response.ok) {
return new Response(`Message Failed: ${response.statusText}`, { status: 200 });
}
return new Response('Message Sent Successfully', { status: 200 });
} catch (error) {
return new Response(`Message Failed: ${error.message}`, { status: 200 });
}
}
};
Results
Test the form by submitting a message.

A confirmation response like Message Sent Successfully appears under the form.

Check your Airtable base — a new record is created in the Messages table.

If automation is enabled, you will receive an e-mail notification with the submitted details.

Summary
This guide walks you through setting up a free, spam-protected contact form for a static site using HTML, JavaScript, Cloudflare Workers, and Airtable. It’s a lightweight and reliable solution that lets you collect messages and receive e-mail notifications — all without a dedicated server.