Free Contact Form with Cloudflare Pages & MailChannels
11 min read • Apr 5, 2024(Edited: Jun 1, 2024)
Update (01.06.2024):
Unfortunately, MailChannels recently announced that their free API is shutting down 30th of June 2024 (thank you, @Ben for mentioning it). You can read their end of life notice.
Are you tired of expensive subscriptions for your website’s contact form? Do you host your website on Cloudflare Pages?
Look no further! In this article, I’ll show you how to create a completely free, customizable contact form with Cloudflare Workers and MailChannels API integration.
It’s the same way I built my own contact form here.
Advantages of a custom contact form
- It’s completely free and lightweight.
- Unlimited monthly submissions.
- You can customize the form’s backend logic with JS/TS to reject submissions based on some conditions or integrate with APIs, third-party tools, NPM packages, etc.
- You can easily add Cloudflare Turnstile (CAPTCHA) to prevent spam.
- Fun project to build. 😁
1. DNS records
Let’s start with adding DNS records to your website to verify that you have the right to send emails from your domain. It’s a security measure to prevent others from sending emails from your domain without your permission.
This will ensure that all emails sent using MailChannels API will be sent from your website’s form.
We have to add four records.
1.1. Domain Lockdown™
(based on/more info: Domain Lockdown™ – Help Center (mailchannels.com))
This record will verify that you have the right to send emails from your domain. To do this, add this DNS record:
Type | Name | Content |
---|---|---|
TXT | _mailchannels | v=mc1 cfid=<your Cloudflare Pages website’s URL, example: something.pages.dev> cfid=<your linked custom domain (if you have one), example: rivenintech.com> |
Not sure how to do it?
Check out this short video:
1.2. SPF
(based on/more info: Set up SPF Records – Help Center (mailchannels.com))
Create a new TXT record (just like you did with Domain Lockdown):
Type | Name | Content |
---|---|---|
TXT | @ | v=spf1 include:relay.mailchannels.net -all |
Note
If you already have an existing SPF record, just add “include:relay.mailchannels.net
”. Make sure to add it BEFORE the “-all” as “-all” always matches and typically goes at the end of the SPF record.
1.3. DKIM
(based on/more info: Adding a DKIM Signature – Help Center (mailchannels.com))
DKIM is an email authentication method that allows you to sign email messages from your domain with a digital signature to prevent others from spoofing your domain.
First, we have to create a DKIM private and public key. You can find DKIM key generators online, but it’s advised to generate them locally using OpenSSL.
Video Tutorial
Adding DKIM is a bit more complicated, so I would recommend following the video and copying the commands from article (below this video).
1.3.1. Creating a DKIM private and public key (run these two commands)
Operating System Differences
If you’re on Windows OpenSSL isn’t pre-installed so, you’ll have to install Git from here (OpenSSL comes with it, and it’s the easiest way to get it). Then, open Git Bash.
On Linux and macOS, just use your terminal, as OpenSSL is already pre-installed.
Private key as PEM file and base64-encoded text file:
Terminal window openssl genrsa 2048 | tee priv_key.pem | openssl rsa -outform der | openssl base64 -A > priv_key.txtPublic key as DNS record:
Terminal window echo -n "v=DKIM1;p=" > pub_key_record.txt && openssl rsa -in priv_key.pem -pubout -outform der | openssl base64 -A >> pub_key_record.txtAfter you’re done, you should have three files:
priv_key.pem
(private key in PEM format)priv_key.txt
(private key in base64-encoded text format)pub_key_record.txt
(public key in DNS record format)
1.3.2. Adding required DNS records (DKIM public key & DMARC)
Create two DNS records:
Type Name Content TXT
mailchannels._domainkey
content from pub_key_record.txt
TXT
_dmarc
v=DMARC1; p=reject; adkim=s; aspf=s; pct=100; fo=1;
Remember
Add_dmarc
record only if you don’t have one yet. You might have one if you have an email with your domain (e.g.[email protected]
).
And if you would like to have a custom email like that with your domain for FREE, check out my another step-by-step tutorial. 😀Create environment variables for your Cloudflare Pages project:
Variable name Value DKIM_PRIVATE_KEY
The content from priv_key.txt
DKIM_SELECTOR
mailchannels
DOMAIN
<your domain, example: rivenintech.com>
2. Create an HTML contact form
The next step is to create a contact form in HTML. Let’s go with a simple one, you can always customize it later (just make sure input
and textarea
have the name
attributes).
<form id="contact-form"> <input type="text" name="name" placeholder="Name" required /> <input type="email" name="email" placeholder="Email" required /> <textarea name="message" placeholder="Message" required></textarea> <input type="submit" value="Send" /></form>
Now, let’s add JavaScript code that will handle the form submission by sending it with a fetch
request to our Cloudflare Worker.
<form id="contact-form"> <input type="text" name="name" placeholder="Name" required /> <input type="email" name="email" placeholder="Email" required /> <textarea name="message" placeholder="Message" required></textarea> <input type="submit" value="Send" /></form>
<script> // Get the form element with the id "contact-form" const form = document.getElementById("contact-form");
// Add an event listener to the form that triggers the submitForm function when the form is submitted form.addEventListener("submit", submitForm);
// Function to handle form submission function submitForm(event) { event.preventDefault();
// Create a FormData object from the form const formData = new FormData(event.target);
// Send the form to the Cloudflare Worker that will listen to requests on the "/submit-contact-form" path fetch("/submit-contact-form", { method: "POST", body: formData, }) .then((response) => { // Parse the response text and include the response status return response.text().then((text) => ({ text, ok: response.ok, })); }) .then(({ text, ok }) => { // If the request was successful, disable the form if (ok) { // You can modify/add something else here form.removeEventListener("submit", submitForm); form.reset(); document.querySelectorAll("input, textarea").forEach((input) => (input.disabled = true)); } else { // If the request was not successful, throw an error throw new Error(); } }) // Catch any errors that occur during the fetch request .catch(() => { console.log("Error") // You can add something that should happen when the form failed }); }</script>
3. Cloudflare Worker - logic/backend of our form
Now that we have our HTML contact form, we need to create a Cloudflare Worker to handle the form submission and send the form data to our inbox.
It’s really simple to create a backend “function” (that will be executed on a Cloudflare Worker) for a site hosted with Cloudflare Pages.
Create
/functions
directory at the root of your Cloudflare Pages website.Now create a new JS file inside it -
submit-contact-form.js
. Cloudflare will automatically place your function in the root of your website, with a path based on your filename.
Example:submit-contact-form.js
=/submit-contact-form
.Now just paste this code into the file you created, read the comments, and change email addresses:
functions/submit-contact-form.js // Listen for requests to the path.export async function onRequest(context) {// Check if the request is a POST request.if (context.request.method !== "POST") {return new Response("Invalid request method.", { status: 405 });}// Get the form data.const formData = await context.request.formData();const body = Object.fromEntries(formData.entries());// Send form data to my inbox.const sent = await sendFormToMe(body, context.env.DKIM_PRIVATE_KEY);if (!sent) {return new Response("Oops! Something went wrong. Please try submitting the form again.", { status: 500 });}return new Response("Form submitted successfully!", { status: 200 });}// Send email to my inbox (email with form data)async function sendFormToMe(body, ENV) {const send_request = new Request("https://api.mailchannels.net/tx/v1/send", {method: "POST",headers: {"content-type": "application/json",},body: JSON.stringify({personalizations: [{to: [{ email: "[email protected]" }], // CHANGE THIS to your email address (emails will be sent TO this address).dkim_domain: ENV.DOMAIN,dkim_selector: ENV.DKIM_SELECTOR,dkim_private_key: ENV.DKIM_PRIVATE_KEY,},],from: {name: "Contact Form",email: "[email protected]", // CHANGE THIS to your email address (emails will be sent FROM this address).},subject: "New contact form submission",content: [{type: "text/plain",value: `Name: ${body.name}\nEmail: ${body.email}\nMessage: ${body.message}`,},],}),});// Get the response from the email APIconst send_response = await fetch(send_request);// Check if the email was sent successfully.if (!send_response.ok) {return false;}return true;}
That’s it! Now once we submit the form we should receive the submitted data in our email inbox.
4. Adding a captcha to form to avoid spam (CF Turnstile)
Spam is a common issue with online forms, but we can use a captcha to prevent automated spam submissions. Cloudflare offers a service called Turnstile that fits our needs. It’s free and fully automatic, so you won’t frustrate your users by making them select squares with traffic lights 🚦.
Go to Cloudflare Dashboard > Turnstile and click Add site. Follow the instructions, then copy the Site Key and Secret Key.
Add this
div
inside your HTML form with yoursite key
and thisscript
:index.html <form id="contact-form"><input type="text" name="name" placeholder="Name" required /><input type="email" name="email" placeholder="Email" required /><textarea name="message" placeholder="Message" required></textarea><!-- You can change between dark/light widget --><div class="cf-turnstile" data-sitekey="<YOUR_SITE_KEY>" data-theme="dark"></div><input type="submit" value="Send" /></form><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script><script>// Get the form element with the id "contact-form"const form = document.getElementById("contact-form");// Add an event listener to the form that triggers the submitForm function when the form is submittedform.addEventListener("submit", submitForm);// Function to handle form submissionfunction submitForm(event) {event.preventDefault();// Create a FormData object from the formconst formData = new FormData(event.target);// Send the form to the Cloudflare Worker that will listen to requests on the "/submit-contact-form" pathfetch("/submit-contact-form", {method: "POST",body: formData,}).then((response) => {// Parse the response text and include the response statusreturn response.text().then((text) => ({text,ok: response.ok,}));}).then(({ text, ok }) => {// If the request was successful, disable the formif (ok) {// You can modify/add something else hereform.removeEventListener("submit", submitForm);form.reset();document.querySelectorAll("input, textarea").forEach((input) => (input.disabled = true));} else {// If the request was not successful, throw an errorthrow new Error();}})// Catch any errors that occur during the fetch request.catch(() => {console.log("Error")// You can add something that should happen when the form failed});}</script>Now we have to validate the captcha on the backend (server-side validation).
functions/submit-contact-form.js // Listen for requests to the path.export async function onRequest(context) {// Check if the request is a POST request.if (context.request.method !== "POST") {return new Response("Invalid request method.", { status: 405 });}// Get the form data.const formData = await context.request.formData();const body = Object.fromEntries(formData.entries());// Validate Turnstile captcha token.const captchaResult = await captchaCheck(context.request.headers, body, context.env.TURNSTILE_SECRET_KEY);if (!captchaResult) {return new Response("Ivalid captcha. Refresh page and try submitting it again.", { status: 422 });}// Send form data to my inbox.const sent = await sendFormToMe(body, context.env.DKIM_PRIVATE_KEY);if (!sent) {return new Response("Oops! Something went wrong. Please try submitting the form again.", { status: 500 });}return new Response("Form submitted successfully!", { status: 200 });}// Send email to my inbox (email with form data)async function sendFormToMe(body, ENV) {const send_request = new Request("https://api.mailchannels.net/tx/v1/send", {method: "POST",headers: {"content-type": "application/json",},body: JSON.stringify({personalizations: [{to: [{ email: "[email protected]" }], // CHANGE THIS to your email address (emails will be sent TO this address).dkim_domain: ENV.DOMAIN,dkim_selector: ENV.DKIM_SELECTOR,dkim_private_key: ENV.DKIM_PRIVATE_KEY,},],from: {name: "Contact Form",email: "[email protected]", // CHANGE THIS to your email address (emails will be sent FROM this address).},subject: "New contact form submission",content: [{type: "text/plain",value: `Name: ${body.name}\nEmail: ${body.email}\nMessage: ${body.message}`,},],}),});// Get the response from the email APIconst send_response = await fetch(send_request);// Check if the email was sent successfully.if (!send_response.ok) {return false;}return true;}async function captchaCheck(headers, body, TURNSTILE_SECRET_KEY) {// Turnstile injects a token in "cf-turnstile-response".const token = body["cf-turnstile-response"];const ip = headers.get("CF-Connecting-IP");// Validate the token by calling the "/siteverify" API.let formData = new FormData();formData.append("secret", TURNSTILE_SECRET_KEY);formData.append("response", token);formData.append("remoteip", ip);const result = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {body: formData,method: "POST",});const outcome = await result.json();return outcome.success;}
Summary
That’s it! Hopefully, you should have a working, fully customizable, free contact form. Feel free to ask in the comments below if you have any questions.
I didn’t cover sending a confirmation email to the user who submitted the form (see demo) yet! I’ll update this article with a template pack containing different contact form designs, confirmation email templates, and all the necessary code to make it work. Stay tuned! ✨
If you found this article useful, I would appreciate a comment below (no account needed) or follow/share on Twitter. You can also support my coffee addiction on ko-fi.com/rivenintech to boost my energy for more articles! ️
Thank you for reading! 😊
Donate
This site and articles are 100% created and maintained by me. If you found it useful and would like to support my work, please consider fueling my coffee addiction. ☕
Thank you! 💖