Skip to content

A Cloudflare Worker that securely proxies contact form submissions from static sites (like Hugo) to Google Forms. Includes Cloudflare Turnstile CAPTCHA, nonce validation, CORS enforcement, and basic rate limiting for spam prevention.

License

Notifications You must be signed in to change notification settings

sunpech/cloudflare-contact-proxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cloudflare Contact Proxy

A Cloudflare Worker that securely proxies contact form submissions from static sites (e.g., Hugo and Cloudflare Pages) to Google Forms, while protecting against spam and abuse.

It includes:

  • Nonce-based request validation
  • Optional Cloudflare Turnstile CAPTCHA verification
  • Basic rate limiting via KV storage
  • CORS protection and form timing checks

📚 Table of Contents

🚀 Overview

Your site’s contact form does not directly post to Google Forms. Instead, it sends data to this Worker (e.g. https://example.com/api/contact), which:

  1. Issues a one-time nonce via /api/form-nonce
  2. Accepts the form POST to /api/contact
  3. Validates nonce + optional Turnstile token
  4. Submits data to Google Forms securely

This ensures no direct exposure of your Google Form endpoint or reCAPTCHA key.

🧩 Architecture

Architecture diagram showing Hugo → Cloudflare Worker → Google Forms flow

📋 Requirements

Before deploying this Worker, make sure you have:

  1. A website built with Hugo - Ideally hosted on Cloudflare Pages for easiest integration and same-origin Worker calls. (Netlify or other static hosts will work too — you’ll just need to adjust CORS_ALLOW_ORIGIN.)
  2. A Google Form configured as your backend data collector (details below).
  3. A Cloudflare Turnstile site key + secret key - Used for spam and bot protection. You can use “Managed” or “Invisible” mode. You should add all the hostnames (domains) here.

This is what a Contact form looks like with the Turnstile widget visible, just above the "Send Message" button:

Contact Form with Turnstile visible

🧱 Example Hugo Integration Files

To make it easier, you can copy a folder like examples/hugo/ to your project repo, containing ready-to-copy templates for Hugo users:

examples/hugo/
├── config/
│   └── _default/
│       └── params.toml        # Configures worker URL, Turnstile key, etc.
├── layouts/
│   ├── shortcodes/
│   │   └── contact-form.html  # Core contact form logic
│   └── partials/
│       └── extend-head.html   # CSP meta for Turnstile and worker
└── content/
    └── contact.md             # Example Hugo page embedding the form

Example files for Hugo can be found here: /examples/hugo/

🧩 Example: config/_default/params.toml

[contactForm]
workerBase = "https://example.com"
proxyAction = "/api/contact"
proxyNonce  = "/api/form-nonce"
useTurnstile = true
turnstileSiteKey = "0x4AAAAAAE37xyprqv9kTWM8"
minFillMs = 1200
buttonText = "Send Message"
successTitle = "Thank you for your message!"
successBody = "I'll get back to you as soon as possible."

🧩 Example: layouts/shortcodes/contact-form.html

See full example at: /examples/hugo/layouts/shortcodes/contact-form.html

🧩 Example: layouts/partials/extend-head.html

This file defines the Content Security Policy (CSP) to allow connections between your site, Turnstile, and the Worker. Your Hugo theme may use a different file name (e.g. custom-head.html). Adjust as needed.

{{- $cfg := .Site.Params.contactForm -}}
<meta http-equiv="Content-Security-Policy"
      content="
        default-src 'self';
        connect-src 'self' {{ $cfg.workerBase }} https://challenges.cloudflare.com;
        script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com;
        style-src 'self' 'unsafe-inline';
        frame-src https://challenges.cloudflare.com;
        form-action 'self' {{ $cfg.workerBase }};
      ">

🧩 Example: content/contact.md

---
title: "Contact"
layout: "page"
---

We’d love to hear from you!

{{< contact-form >}}

📝 Google Form Fields & Google Sheets

This Worker expects a small, consistent payload from your site. You can capture these fields in your Google Form and map them via GFORM_MAP.

Logical key Purpose Notes
name Sender’s name Text field
email Sender’s email “Short answer”
message Message body “Paragraph”
source Site or app name Optional, helpful if you have multiple sites
pageurl Page where the form lives Auto-filled by JS
useragent Browser user agent Auto-filled by JS
start_ts Timestamp when user started form Used for spam prevention
cf-turnstile-response CAPTCHA token Present only if Turnstile enabled

Only name, email, and message are strictly required, but including all helps debugging or analytics.

Mapping your Google Form fields

In wrangler.toml:

[vars]
# Google Form endpoint (your form’s "formResponse" URL)
GFORM_ACTION = "https://docs.google.com/forms/u/0/d/e/1FAIpQLSXXXXXXXXXXXXXXX/formResponse"

# Map your Google Form field IDs (replace with your real ones)
GFORM_MAP = "{\"name\":\"entry.1234567890\",\"email\":\"entry.2345678901\",\"message\":\"entry.3456789012\",\"source\":\"entry.4567890123\",\"pageurl\":\"entry.5678901234\",\"useragent\":\"entry.6789012345\"}"

Finding your entry.xxxxxxxx values

  1. Open your form in Preview.
  2. Fill it out, then check DevTools → Network → formResponse.
  3. Look under Form Data for each entry.xxxxxxxx key.
  4. Copy these into GFORM_MAP.

Saving responses to Google Sheets

You can automatically pipe every Worker-submitted response into a Google Sheet:

  1. Open your Form in edit mode.
  2. Go to Responses → Link to Sheets → Create a new spreadsheet.
  3. Submissions will now appear automatically.

If you later add fields, just create new questions → find their entry.xxxxxxxx → update GFORM_MAP.

🌐 Multi-Domain Support

This Worker supports multiple domains (e.g., example.com, example2.com) — useful if you operate more than one Hugo site that shares the same backend Worker.

1. Allow multiple domains in CORS

In wrangler.toml:

[vars]
CORS_ALLOW_ORIGIN = "https://example.com,https://www.example.com,https://example2.com,https://www.example2.com,http://localhost:1313"

2. Update your CSP in Hugo

In each site’s extend-head.html (or whatever custom head file your theme uses):

<meta http-equiv="Content-Security-Policy"
      content="
        default-src 'self';
        connect-src 'self' https://your-worker-domain.com https://challenges.cloudflare.com;
        script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com;
        style-src 'self' 'unsafe-inline';
        frame-src https://challenges.cloudflare.com;
        form-action 'self' https://your-worker-domain.com;
      ">

3. Configure per-site workerBase

See params.toml example in Hugo Integration Files section.

Multi-Site Example (Hugo)

If you host two Hugo sites (sharing one Worker):

Site A (example.com)

[contactForm]
workerBase = "https://example.com"
proxyAction = "/api/contact"
proxyNonce  = "/api/form-nonce"
useTurnstile = true
turnstileSiteKey = "0x4AAAAAAE37xyprqv9kTWM8"

Site B (example2.com)

[contactForm]
workerBase = "https://example2.com"
proxyAction = "/api/contact"
proxyNonce  = "/api/form-nonce"
useTurnstile = true
turnstileSiteKey = "0x4AAAAAAE37xyprqv9kTWM8"

Worker CORS (covers both):

[vars]
CORS_ALLOW_ORIGIN = "https://example.com,https://www.example.com,https://example2.com,https://www.example2.com"

Cloudflare Routes:

example.com/api/*
www.example.com/api/*
example2.com/api/*
www.example2.com/api/*

🛣️ Worker Routes (Cloudflare)

You must tell Cloudflare which domain paths should be handled by this Worker.

Dashboard (Recommended)

  1. Go to: Workers & Pages → Your Worker → Triggers → Routes → Add route
  2. Add one route per domain:
example.com/api/*
www.example.com/api/*
example2.com/api/*
www.example2.com/api/*

Alternatively — via wrangler.toml:

routes = [
  { pattern = "example.com/api/*", zone_name = "example.com" },
  { pattern = "www.example.com/api/*", zone_name = "example.com" },
  { pattern = "example2.com/api/*", zone_name = "example2.com" },
  { pattern = "www.example2.com/api/*", zone_name = "example2.com" }
]

Use either the Cloudflare Dashboard or routes in wrangler.toml — not both.

🔀 API Endpoints

Every domain should route these paths to your Worker:

Method Path Purpose
GET /api/health Health check
GET /api/form-nonce Get one-time nonce
POST /api/contact Validate nonce + Turnstile + forward to Google Forms

Example:

curl -i https://example.com/api/form-nonce
curl -i -X POST https://example.com/api/contact

⚙️ Setup

1. Clone or create the Worker

git clone https://github.com/sunpech/cloudflare-contact-proxy.git
cd cloudflare-contact-proxy
npm install

Copy the example file:

cp wrangler.example.toml wrangler.toml

2. Create KV namespaces

npx wrangler kv:namespace create NONCE
npx wrangler kv:namespace create RATE

Copy the IDs output from these commands into wrangler.toml:

[[kv_namespaces]]
binding = "RATE"
id = "xxxxxx"

[[kv_namespaces]]
binding = "NONCE"
id = "xxxxxx"

3. Configure environment variables

In wrangler.toml, set the following:

[vars]
# Google Form endpoint (your form’s "formResponse" URL)
GFORM_ACTION = "https://docs.google.com/forms/u/0/d/e/1FAIpQLSXXXXXXXXXXXXXXX/formResponse"

# Map your Google Form fields to internal names.
GFORM_MAP = "{\"name\":\"entry.1234567890\",\"email\":\"entry.2345678901\",\"message\":\"entry.3456789012\",\"source\":\"entry.4567890123\",\"pageurl\":\"entry.5678901234\",\"useragent\":\"entry.6789012345\"}"

# Allow-listed origins
CORS_ALLOW_ORIGIN = "https://example.com,https://www.your-other-domain.com"

# Turnstile (optional, but recommended)
TURNSTILE_REQUIRED = "true"

Ensure useTurnstile and turnstileSiteKey are configured in your Hugo site params (see Requirements).

💡 If testing locally, temporarily set:

CORS_ALLOW_ORIGIN = "http://localhost:1313,https://example.com"
DEV_ALLOW_BYPASS = "true"
TURNSTILE_REQUIRED = "false"

4. Deploy the Worker

npx wrangler deploy

If you see:

Multiple environments are defined...

You can explicitly deploy to the default environment:

npx wrangler deploy --env=""

Verify that your Worker routes are configured correctly (see API Endpoints section).

🧪 Development Tips and Testing

npx wrangler tail

🔒 Security Features

  • ✅ Nonce-based double-submit prevention
  • ✅ Timing validation (minFillMs)
  • ✅ Optional Turnstile CAPTCHA
  • ✅ CORS whitelist enforcement
  • ✅ Google Forms endpoint hidden from clients
  • ✅ No persistent user data stored (KV is short-lived)

⚠️ Disclaimer

This project was created with the assistance of ChatGPT.

While functional and tested, parts of the code were generated with AI suggestions, so you may want to review it carefully before using in production or extending it.

📄 License

This project is licensed under the MIT License.

About

A Cloudflare Worker that securely proxies contact form submissions from static sites (like Hugo) to Google Forms. Includes Cloudflare Turnstile CAPTCHA, nonce validation, CORS enforcement, and basic rate limiting for spam prevention.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published