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
Help
button and openAPI documentation
. In theIntroduction
section, note theID
for theMessages
base — this will be referred to asAIRTABLE_BASE_ID
in a later step.

- In the
MESSAGES TABLE
section, note theID
for theMessages
table — this will be referred to asAIRTABLE_TABLE_ID
in a later step.

- Go to the
Personal access tokens
page andCreate new token
. Name the token and add the scopedata.records:write
with access to the appropriate base. Click theCreate token
button and note theTOKEN ID
— this will be referred to asAIRTABLE_ACCESS_TOKEN
in a later step.

- Optional but recommended: Return to the
Messages
base 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 theMessages
table.

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 & Pages
and click theCreate
button. - Under the
Workers
tab, click theGet started
button forStart with Hello World!
. - On the
Create an application
page, rename the project to something more meaningful and click theDeploy
button. - After deployment, click the
Continue to project
button. - On the dashboard, click the
Settings
tab. - Configure
Domains & Routes
. Disableworkers.dev
andPreview URLs
routes. Define a custom route (non-wildcard) specific to your site (e.g.,/contact/submit/
).

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

- 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 theDeploy
button.
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.