Contact forms in EmDash: build, email and spam-proof
The Contact Forms plugin end to end on a live site: install, fields, the page block, the inbox and email log, delivery through the site's own provider, passive anti-spam, and where AI earns its keep.

Every site I have ever run eventually needed a contact form, and on WordPress it was always the same three-plugin stack: the form plugin, the SMTP plugin to make its mail actually arrive, and a captcha plugin to keep the bots out. On EmDash it is one plugin. It stores every message on the site, sends mail through whatever provider the site already uses, and the anti-spam is passive until you decide you want more.
This site runs it at /contact/ - what you see there is what ships. This is the full path: install, the first form, where the messages land, how email works, the spam traps, and the spots where an AI assistant genuinely saves time.
What's in the box
- Form builder
- An admin page under Plugins, Contact forms: fields, ordering, per-form settings
- Public submit route
- A same-origin form POST that answers with a 303 redirect - no fetch, no JavaScript needed
- Submissions inbox
- Every stored entry with read state, resend and CSV export
- Email log
- Each delivery attempt with recipient, timing, status and the error when one fails
- Delivery
- ctx.email.send() - the site's own provider, not a new account to set up
- Anti-spam
- Honeypot, minimum fill time and a per-IP hourly limit, with optional Turnstile
Install and turn it on
Three steps to a working form
Install the plugin
Admin, Plugins, Registry - search for Contact Forms and install. On a code-managed site, add the package and register it under sandboxed: instead. Either way it runs inside the plugin sandbox, walled off from your content and settings.
Approve what it asks for
The manifest requests email:send and, only if you switch on Turnstile, network access to Cloudflare's siteverify endpoint. That is the whole list. Read it before approving - it is short on purpose.
Set the defaults
Under Plugin settings, enter the fallback notification address and, if you want it, the Turnstile keys. Individual forms can override the notify address; the defaults only catch the forms that do not bother.

Build the first form
From empty page to working form
New form
Name it and pick a slug - the slug is how the page block finds the form, so keep it short: contact, support, press.
Add the fields
Eight types: text, email, textarea, select, checkbox, radio, number and hidden. Each gets a label, a key, an optional placeholder and a required flag. Drag to reorder; a required checkbox demands a real tick, not a guess at a default.
Per-form settings
Notification email, subject line and the success message visitors see after posting. The subject accepts field values, so 'New enquiry from {name}' works.
Save and look at it
The public form endpoint renders exactly what visitors will get - the same markup the demo on this site serves.
Put it on a page
On this site a Contact form block takes the slug and renders the whole thing server-side - the markup is in the HTML before CSS or JS loads, and it still works as a plain form post with JavaScript off. Other themes get the same result from the generic component that ships in the package's site/ directory, ready to copy in.
Where the messages land
A submission that passes the traps is written to storage first and emailed second. That order matters: if the mail provider is down or misconfigured, the message still sits in your admin inbox instead of dying inside a timeout. Failed sends are marked on the row so you can spot and resend them.


Email, the part that usually breaks
The plugin never speaks SMTP itself. It calls the site's email hook, so the provider is whatever you already configured: Cloudflare Email Sending through a Worker binding, or a hosted provider through the emdash-smtp plugin. The full setup for both paths is in this video from my EmDash course:

The written version lives at Send Email from an EmDash Site. One detail worth knowing: every submission carries an emailStatus - sent, failed or skipped - and skipped is what you get until a provider exists. The inbox fills either way, so a missing provider costs you notifications, not messages.
Anti-spam without the checkbox farm
Three passive traps run before anything else: a honeypot field humans never see, a minimum fill-time check that catches instant bot posts, and a per-IP hourly limit. A bot that trips one gets a fake success back and nothing is stored, so it never learns to retry differently. If the volume still climbs, optional Cloudflare Turnstile adds a managed check that stays invisible for most visitors.
The Turnstile write-up walks the full pipeline with curl tests you can repeat against your own form.
Where AI actually helps
Worth automating
- Field sets: describe the form - booking enquiry, bug report, press contact - and let it draft the fields, types and validation, then trim to what you need
- Email copy: notification subjects and the confirmation auto-reply are easy first drafts for a model
- Spam tuning: paste a batch of blocked submissions and ask what pattern belongs in the honeypot or the rate window
- Migrating old forms: it reads a Contact Form 7 or WPForms export and proposes the matching field list
Do not automate
- Recipient addresses: a hallucinated inbox means messages land somewhere nobody reads
- Auto-reply tone, unreviewed: that is your site's voice writing to strangers
- Privacy wording: the line about what you do with submissions has to be true, not plausible-sounding
Common questions
FAQ
Does it work with no email provider configured?
Yes. Submissions store normally and each row is marked emailStatus skipped. Add a provider later and nothing is lost - the inbox was filling the whole time.
How many forms can one site have?
As many as you need. Each carries its own slug, fields, notify address and success message; the page block picks one by slug.
Can visitors attach files?
Not in this version. The honest reason: attachments mean file scanning and storage decisions that deserve a deliberate design, not a bolt-on.
Is Turnstile required?
No. The passive traps alone stop the bulk of junk on my sites. Turnstile is there for when they stop being enough.
What does a rejected visitor see?
Their values come back with the failing fields marked. The draft is held once in KV and deleted when read, so a rejection never loses the message they typed.
Keep going
The links from this guide
- Contact Forms on the registry
Install, changelog and the full field reference
- The live demo
The working form on this site - send me a message
- Email sending in EmDash
Cloudflare Email Sending and the SMTP plugin, written up
- The Turnstile deep dive
The anti-spam pipeline with tests you can run
- Migrate from WordPress
If the contact form is part of a bigger move