During the weekend, I decided to change my contact form.
Objective: replacing the embed form via an iframe with a static form
What's used:
Cloudflare
Pages
Pages Function
Turnstile
Telegram bot
BotFather is the tool to create a Telegram bot.
Captcha: Turnstile
For the captcha, I decided to use Turnstile from Cloudflare.
You don't have to click on pictures or write an alphanumeric code.
It's a fully automated validation, and manual verification is a simple checkbox.
Wow, that's an improvement.
The form
Specific point: no action! Effectively, it's a static form handled automatically by Cloudflare.
<form data-static-form-name="contact">
<div class="mb-3">
<label for="tags" class="form-label">Tags</label>
<div id="tagsHelp" class="form-text">Multiple values can be selected</div>
<select class="form-select" name="tags" id="tags" multiple aria-describedby="tagsHelp">
<option value="C2C">C2C</option>
<option value="Contract">Contract</option>
<option value="Digital Nomad friendly">Digital Nomad friendly</option>
<option value="Full Remote">Full Remote</option>
<option value="Permanent">Permanent</option>
<option value="Other">Other</option>
</select>
</div>
<div class="mb-3">
<label for="first_name" class="form-label">First name</label>
<input type="text" class="form-control" name="first_name" id="first_name">
</div>
<div class="mb-3">
<label for="last_name" class="form-label">Last name</label>
<input type="text" class="form-control" name="last_name" id="last_name">
</div>
<div class="mb-3">
<label for="company" class="form-label">Company</label>
<input type="text" class="form-control" name="company" id="company">
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" name="email" id="email" aria-describedby="emailHelp">
<div id="emailHelp" class="form-text">Will be used to answer you.</div>
</div>
<div class="mb-3">
<label for="message" class="form-label">Message</label>
<textarea class="form-control" name="message" id="message"></textarea>
</div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="{{ $cloudflare.turnstile_key }}" data-callback="javascriptCallback" data-name="contact-form"></div>
<button type="submit" class="btn btn-info">Submit</button>
</form>
Basic form with two lines dedicated to the captcha:
- load the JS file
- the div where the captcha will be visible
The function
At the root for your repository, a little step:
mkdir functions
echo '{"compilerOptions":{"target":"ES2020","module":"CommonJS","lib":["ES2020"],"types":["@cloudflare/workers-types"]}}
' > functions/tsconfig.json
My function file is contact.ts
, which applies on /contact
.
Big difference between a function and a worker
You can't access environment variables/secrets, so you need to add them directly to your code. It's the critical part that I learned doing this first function.
My big function
I want a function doing some simple steps:
- Verify the captcha (token)
- Get form values
- POST form values to an API endpoint (I don't use emails!)
- Notify me on Telegram
- Thank you page
Static forms
I use the static-forms
provided by Cloudflare with a small change to have the possibility to run async code.
import staticFormsPlugin from "@cloudflare/pages-plugin-static-forms";
export const onRequest: PagesFunction = staticFormsPlugin({
respondWith: async ({formData, name}) => {
A quick override adding async
at the respondWith
.
Verify the captcha (token)
const turnstile = `secret=${MY_SECRET}&response=${formData.get("cf-turnstile-response")}`;
const result = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
body: turnstile.toString(), headers: {"Content-Type": "application/x-www-form-urlencoded"}, method: 'POST',
}).then(function (response) {
return response.json();
});
const outcome = await result;
if (outcome["success"]) {}
It's like the snippet in the documentation, but I needed to add the header, or it won't work. When the captcha is ok, I go to the next step
Parsing form values
I prepare the body for the request.
const body = {
fields: {
Tags: formData.getAll("tags"),
"First Name": formData.get("first_name"),
"Last Name": formData.get("last_name"),
Company: formData.get("company"),
"Email Address": formData.get("email"),
Message: formData.get("message")
}
};
I can have multiple tags, so I need to do the getAll.
Preparing Telegram notification body
const telegram_message = `New contact form submitted by ${formData.get("email")}`
const telegram_body = {
chat_id: 0000000000, text: telegram_message.toString(), allow_sending_without_reply: true
};
Send form values to the API
await fetch("https://MY_SUBDOMAIN/MY_ENDPOINT", {
method: "POST", body: JSON.stringify(body), headers: {
x-autho: `${MY_AUTH_TOKEN}`, "Content-type": `application/json`,
}
});
Send Telegram notification
await fetch("https://api.telegram.org/botTELEGRAM_BOT_TOKEN/sendMessage", {
body: JSON.stringify(telegram_body), method: "POST", headers: {
"Content-Type": "application/json"
},
});
Thank you page
Just creating a Response with the good Content-Type
const res = await new Response(`<html lang="en">
<head>
<meta charset="utf-8">
<title>Thank you!</title>
</head
<body>
</body>
</html>`);
res.headers.set("Content-Type", 'text/html;charset=UTF-8');
return res
Conclusion
Functions is the perfect extension of Pages without needing to use a full-featured worker.
In addition, I'm impatient to have Rust/WASM available for Functions because JS/TS isn't for me.