My first function on Cloudflare Pages

Cloudflare Pages logo

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>
    <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 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 class="mb-3">
        <label for="company" class="form-label">Company</label>
        <input type="text" class="form-control" name="company" id="company">
    <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 class="mb-3">
        <label for="message" class="form-label">Message</label>
        <textarea class="form-control" name="message" id="message"></textarea>
    <script src="" 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>

Basic form with two lines dedicated to the captcha:

  1. load the JS file
  2. 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:

  1. Verify the captcha (token)
  2. Get form values
  3. POST form values to an API endpoint (I don’t use emails!)
  4. Notify me on Telegram
  5. 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("", {
    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("", {
        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">
        <meta charset="utf-8">
        <title>Thank you!</title>
    res.headers.set("Content-Type", 'text/html;charset=UTF-8');
    return res


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.