RG Forms logoRG Forms
Cloudflare Pages

Adding a contact form to a Cloudflare Pages site

Cloudflare Pages doesn’t ship a forms feature the way Netlify does. What it gives you instead is Pages Functions — real Workers, running on Cloudflare’s edge, that can handle a POST to your own domain.

That’s more power than a contact form needs, and it comes with more to maintain. Here are both routes, honestly.

Last updated August 19, 2026

The short answer

Cloudflare Pages serves static assets and has no built-in form handling, so you either write a Pages Function (a Worker) to receive the POST or send it to an external endpoint. A Pages Function gives you full control but leaves you owning code, storage and deployment for a contact form. RG Forms is the no-code path: your page POSTs to a Google Apps Script web app in your own Google account and submissions land in your own Google Sheet — and because RG Forms verifies Cloudflare Turnstile tokens server-side, you can pair it with the free Turnstile widget you already have access to.

Option 1: a Pages Function

Drop a file at functions/api/contact.ts and Cloudflare routes POST /api/contact to it. From there you can do anything — validate, rate-limit, write to D1 or KV, call an email API, fan out to a CRM.

The catch is that you now own a small application. You need somewhere to put the data (D1, KV, or an external store), a way to send email (Workers can’t send mail on their own — you’ll be calling a third-party email API with its own key and quota), and a plan for spam. Plus the bindings, the deploy config, and the runtime upgrades.

If your form feeds a real workflow, that investment is correct. If it emails you three times a week, it isn’t.

Option 2: an endpoint you don’t maintain

The alternative is to leave your Pages project purely static and POST from the browser to an endpoint that already handles storage, notification and spam. Your functions/ directory stays empty, your deploy stays a pure asset upload, and there’s no Worker to keep an eye on.

With RG Forms that endpoint is an Apps Script web app inside your own Google account — so “an endpoint you don’t maintain” doesn’t mean “an endpoint someone else owns.”

The integration

Framework-agnostic — works with an Astro, Hugo, SvelteKit-static or plain HTML Pages project.

index.html
<form id="contact-form">
  <label>Name <input name="name" required /></label>
  <label>Email <input type="email" name="email" required /></label>
  <label>Message <textarea name="message" required></textarea></label>

  <!-- Honeypot: humans never see it, bots fill it in -->
  <input type="text" name="_hp" tabindex="-1" autocomplete="off"
         style="position:absolute;left:-9999px" aria-hidden="true" />

  <button type="submit">Send</button>
  <p id="form-status" role="status"></p>
</form>

<script>
  const ENDPOINT = "https://script.google.com/macros/s/AKfycb.../exec";
  const form = document.getElementById("contact-form");
  const status = document.getElementById("form-status");

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    status.textContent = "Sending…";

    const res = await fetch(ENDPOINT, {
      method: "POST",
      // text/plain avoids the CORS preflight Apps Script can't answer
      headers: { "Content-Type": "text/plain" },
      body: JSON.stringify({
        tab: "contact",
        fields: Object.fromEntries(new FormData(form)),
      }),
    });

    const data = await res.json();
    status.textContent =
      data.result === "success" ? "Thanks — we'll be in touch." : "Something went wrong.";
    if (data.result === "success") form.reset();
  });
</script>

Choosing between them

If you need…Go with
A contact form that emails you and keeps a recordAn external endpoint — there’s nothing here worth a Worker
Custom validation, CRM writes, payment logic, rate limitingA Pages Function
Submissions in a spreadsheet your team can filter and annotateAn external endpoint writing to Google Sheets
Data that must stay inside Cloudflare (D1/KV/R2)A Pages Function
The same form code across several sites on different hostsAn external endpoint

Verify the endpoint first

Test the endpoint from your terminal
curl -L -X POST "https://script.google.com/macros/s/AKfycb.../exec" \
  -H "Content-Type: text/plain" \
  -d '{"tab":"contact","fields":{"name":"Ada","email":"ada@example.com","message":"Hello"}}'

# → {"result":"success"}   and a new row appears in your Google Sheet

Common questions

Does Cloudflare Pages have built-in forms like Netlify?

No. Pages serves static assets; anything dynamic goes through Pages Functions, which you write yourself.

Will an external POST be blocked by Cloudflare’s proxy or WAF?

No. The request originates in the visitor’s browser and goes directly to the endpoint’s domain — it never passes through your Cloudflare zone.

Can I use Turnstile without a Worker?

Yes — that’s exactly what this setup does. The widget runs in the browser and the token is verified inside your Apps Script, so no Worker is involved at any point.

What about Cloudflare Web Analytics on the form page?

Unaffected. Analytics is a separate script and doesn’t interact with the form submission at all.

More answers in the full FAQ.

Your endpoint goes live in ~90 seconds