Skip to content

Contact form: send through a Worker, Resend and Turnstile - #8

Merged
omercelikdev merged 1 commit into
mainfrom
contact-form
Sep 13, 2026
Merged

Contact form: send through a Worker, Resend and Turnstile#8
omercelikdev merged 1 commit into
mainfrom
contact-form

Conversation

@omercelikdev

Copy link
Copy Markdown
Owner

The contact form used to open the visitor's mail app, which does nothing for anyone on webmail. It now sends the message.

How it works

  • The form posts to /api/contact, served by a small Worker script next to the static assets (run_worker_first: ["/api/*"]); every other request is still a static file.
  • The Worker checks method, same origin, body size and fields, drops honeypot submissions quietly, verifies the Cloudflare Turnstile token (hostname and action included), and emails the message through Resend as plain text with the visitor as Reply-To.
  • Without its secrets it fails closed (503) and the form points to the email address.
  • Until a Turnstile site key is set in src/config/site.ts, the page shows a plain email link instead of the form, so merging this changes nothing visible yet.

Also: Giscus strict title matching, set before the first comment thread exists.

Verified

  • npm run check: lint, both typechecks (site + worker), format, 41 tests (24 new for the Worker).
  • Local end-to-end with wrangler dev and Cloudflare's test keys: form sent, email built with the right recipient, Reply-To and subject (dry run); "send another" gets a fresh token.
  • Build without a site key: email link instead of the form.

To activate (after deploy)

Resend domain mail.omercelik.dev, a Turnstile widget, then RESEND_API_KEY and TURNSTILE_SECRET_KEY as Worker secrets and the site key in site.ts. See README → Contact form.

🤖 Generated with Claude Code

…nstile

The form used to open the visitor's mail app, which does nothing for anyone
using webmail. It now posts to /api/contact, served by a small Worker script
next to the static assets (run_worker_first: /api/*):

- checks method, same origin, body size and fields; a honeypot drops bots
  quietly
- verifies the Cloudflare Turnstile token (hostname and action included)
- emails the message through Resend with the visitor as Reply-To, plain
  text only
- fails closed (503) without its secrets; the form then points to email

Until a Turnstile site key is set in src/config/site.ts, the page shows a
plain email link instead of the form. Local end-to-end runs use Cloudflare's
test keys and a dry run (npm run preview). Secrets stay in Cloudflare.

Also: Giscus strict title matching, set before the first thread exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@omercelikdev
omercelikdev merged commit 7256464 into main Sep 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant