—here’s a drop-in setup for Cloudflare Pages that:
• validates Turnstile (anti-spam),
• emails the submission (free) using Cloudflare Email Routing → Email Workers,
• (optional) logs each submission to KV for backup,
• works with your existing static HTML form.

I’ll give you the exact files/snippets and the 3-step setup at the end.

  1. Pages Function (server code)

Create functions/api/contact.ts (or .js) in your repo:

// /functions/api/contact.ts
// Cloudflare Pages Function: validates Turnstile token, sends email via Email Workers,
// and optionally logs to KV.

import { createMimeMessage } from "mimetext";
import type { Env } from "./types"; // optional (shown below)

export const onRequestPost: PagesFunction = async (context) => {
const { request, env } = context;

// 1) Parse form data
const form = await request.formData();
const name = (form.get("name") || "").toString().trim();
const email = (form.get("email") || "").toString().trim();
const phone = (form.get("phone") || "").toString().trim();
const message = (form.get("message") || "").toString().trim();

// Honeypot (bots fill this)
if ((form.get("company") || "").toString().length > 0) {
return json({ ok: true }); // silently accept
}

// 2) Validate Turnstile (required for real protection)
const token = (form.get("cf-turnstile-response") || "").toString();
const ip = context.request.headers.get("CF-Connecting-IP") ?? "";
const verifyRes = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
body: new URLSearchParams({
secret: env.TURNSTILE_SECRET,
response: token,
remoteip: ip,
}),
});
const verify: { success: boolean } = await verifyRes.json();
if (!verify.success) {
return json({ ok: false, error: "turnstile_failed" }, 400);
}

// 3) Build the email (simple text + a tiny HTML fallback)
const msg = createMimeMessage();
msg.setSender({ name: "REISS BUILDS Website", addr: env.MAIL_FROM });
msg.setRecipient(env.MAIL_TO); // primary recipient
if (env.MAIL_BCC && env.MAIL_BCC.length) {
// You can add multiple BCCs if you like, comma-separated
env.MAIL_BCC.split(",").forEach((addr) => msg.setBcc(addr.trim()));
}
msg.setSubject(New Contact Form: ${name || "Website Lead"});

const textBody =
`New website inquiry

Name: ${name}
Email: ${email}
Phone: ${phone}
Source: ${request.headers.get("Referer") || "unknown"}

Message:
${message}

— Sent ${new Date().toISOString()}
`;

msg.addMessage({ contentType: "text/plain", data: textBody });
msg.addMessage({
contentType: "text/html",
data: <pre style="font:14px/1.4 -apple-system,Segoe UI,Roboto,Arial,sans-serif">${escapeHtml( textBody )}</pre>,
});

// 4) Send the email via Email Workers service binding
// Binding name "SEND" is configured in wrangler.toml (see below)
// @ts-ignore - global in Workers
const emailMessage = new (globalThis as any).EmailMessage(
env.MAIL_FROM,
env.MAIL_TO,
msg.asRaw()
);

try {
await env.SEND.send(emailMessage);
} catch (e: any) {
return json({ ok: false, error: "send_failed", detail: e?.message }, 500);
}

// 5) (Optional) Store to KV for backup
if (env.LEADS_KV) {
const id = lead:${Date.now()}:${cryptoRandom()};
await env.LEADS_KV.put(
id,
JSON.stringify({ name, email, phone, message, ts: new Date().toISOString() }),
{ expirationTtl: 60 * 60 * 24 * 365 } // 1 year
);
}

// 6) Return success JSON (AJAX) or redirect (form POST)
const accept = request.headers.get("accept") || "";
if (accept.includes("application/json")) {
return json({ ok: true });
}
return Response.redirect("/thank-you/", 303);
};

// Helpers
function json(data: unknown, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { "content-type": "application/json", "cache-control": "no-store" },
});
}
function escapeHtml(s: string) {
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]!));
}
function cryptoRandom() {
const a = new Uint32Array(2);
crypto.getRandomValues(a);
return ${a[0].toString(16)}${a[1].toString(16)};
}

optional TypeScript env typings (handy but not required):

// /functions/api/types.d.ts or /functions/api/types.ts
export interface Env {
TURNSTILE_SECRET: string;
MAIL_FROM: string; // e.g. no-reply@reissbuilds.com (must be authorized)
MAIL_TO: string; // where you want to receive leads
MAIL_BCC?: string; // optional comma-separated
SEND: EmailSender; // Email service binding name
LEADS_KV?: KVNamespace; // optional KV
}

  1. HTML form (frontend)

Add Turnstile + a honeypot field. Point the form action to your function URL.

  1. wrangler.toml (bindings & secrets)

In your project root (same repo), add or update wrangler.toml:

name = "reiss-builds-contact"
compatibility_date = "2025-08-01"

Email Workers binding (choose a name; we used SEND in code)

[[email]]
binding = "SEND"

Optional: KV for backups

[[kv_namespaces]]
binding = "LEADS_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # after creating KV namespace

[vars]
MAIL_FROM = "no-reply@reissbuilds.com"
MAIL_TO = "neil@agilemedia.agency"
MAIL_BCC = "jeff@example.com"

Turnstile secret is stored as a secret (not plain vars)

Add the Turnstile secret:

wrangler secret put TURNSTILE_SECRET

paste your Turnstile Secret Key when prompted

  1. Cloudflare dashboard steps (one-time)
    1. Turnstile
      • Create a Turnstile site key + secret for reissbuilds.com, add the site key to the HTML and the secret to Wrangler.
      Docs: Turnstile server-side validation is mandatory; we’re doing this in the function above. 
    2. Email Routing → Email Workers
      • In Cloudflare for reissbuilds.com, enable Email Routing.
      • Verify the destination address you’ll send to (e.g., neil@agilemedia.agency) and set up Email Workers “Send from Workers” with a binding (we used SEND).
      • Ensure MAIL_FROM uses a sender on your domain (e.g., no-reply@reissbuilds.com) per the docs example.
      Docs & sample: “Send emails from Workers” (uses EmailMessage with a binding). 
    3. Deploy

wrangler pages deploy ./ --project-name reiss-builds

or via Cloudflare Pages Git integration (push to main and it deploys)

  1. Optional: switch to Resend/Postmark later (if you prefer an ESP)

If you ever want more analytics or templates, you can swap the send block to Resend or Postmark with a single fetch/SDK call from the Worker. Cloudflare has first-party tutorials for both. 

Notes & why this stack
• No monthly SaaS just for forms: Using Email Workers means the form posts to your own function and emails you without Formspree. (MailChannels’ free Worker API was discontinued in 2024, so the modern path is Email Workers or an ESP like Resend/Postmark.)  
• Spam protection: Turnstile + honeypot stops most junk before it hits your inbox. Server-side Turnstile check is critical. 
• Pages Functions are the recommended way to handle form posts in a Pages site. 

If you want, I can tailor this to your exact folder structure (Eleventy/Pages) and wire it to your current contact page.