Modules
Contact
The contact module adds a fully-featured contact form to any Stedefast site. Submissions are validated server-side with Cloudflare Turnstile (no cookies, no tracking), stored in D1 for a full inbox log in the admin panel, and forwarded to your email via Cloudflare Email Routing (free, no third-party account) or Resend.
Installation
pnpm add @stedefast/module-contact
Setup
// stedefast.config.ts
import { defineConfig } from "@stedefast/core";
import { ContactModule } from "@stedefast/module-contact";
export default defineConfig({
// ...
modules: [
ContactModule({
to: "[email protected]", // must be verified in CF Email Routing
subjectPrefix: "[My Site]", // prepended to forwarded email subjects
turnstileSiteKey: process.env.TURNSTILE_SITE_KEY,
provider: "email-routing", // or "resend"
fields: { subject: true }, // show optional Subject field
}),
],
cloudflare: {
projectName: "my-site",
d1Databases: [{ binding: "CONTACT_DB", databaseId: "YOUR_D1_DATABASE_ID" }],
},
});
Cloudflare bindings
| Binding | Type | Required | Purpose |
|---|---|---|---|
CONTACT_DB |
D1 database | Always | Stores submission log |
TURNSTILE_SECRET_KEY |
Secret | Always | Server-side Turnstile verification |
SEND_EMAIL |
Email binding | When provider = "email-routing" |
CF Email Routing delivery |
RESEND_API_KEY |
Secret | When provider = "resend" |
Resend API delivery |
Create the D1 database:
wrangler d1 create my-site-contact
Copy the database_id into your config, then declare the binding in wrangler.toml:
[[d1_databases]]
binding = "CONTACT_DB"
database_name = "my-site-contact"
database_id = "YOUR_D1_DATABASE_ID"
Running D1 migrations
Apply the bundled migration to create the contact_submissions table:
wrangler d1 migrations apply my-site-contact --local # local dev
wrangler d1 migrations apply my-site-contact # production
Or use the Stedefast deploy command, which applies D1 migrations automatically:
stedefast deploy
Setting up Cloudflare Email Routing
The to address must be a verified destination in your Cloudflare dashboard before email delivery will work:
- Go to Cloudflare Dashboard → Email → Email Routing → Destination addresses
- Click Add destination address and enter the address used for
to - Verify ownership via the confirmation email Cloudflare sends
- In your Worker settings, create the
SEND_EMAILemail binding pointing to that verified address
Using the island in a template
Add the mount point in your template where you want the form to appear:
// theme/templates/contact.tsx
<div
data-island="ContactForm"
data-props={JSON.stringify({
pageUrl: page.url,
showSubject: true,
successMessage: "Thanks! I'll be in touch.",
turnstileSiteKey: process.env.TURNSTILE_SITE_KEY,
})}
/>
The Turnstile script is injected automatically into <head> via the module's headAssets — you do not need to add it manually.
How the island works
The form has four states:
| State | Description |
|---|---|
idle |
Form is ready for input |
submitting |
Fields are disabled, button shows "Sending…" |
success |
Form replaced with the configured successMessage |
error |
Error message shown, Turnstile widget reset for retry |
On submit, the island POSTs JSON to /_modules/contact/submit:
{
"name": "Alice",
"email": "[email protected]",
"subject": "Hello",
"message": "Your site is great!",
"token": "<turnstile-token>",
"pageUrl": "https://example.com/contact"
}
The Worker validates the Turnstile token, hashes the submitter's IP (sha256(ip + TURNSTILE_SECRET_KEY)) before storing it, inserts the row into D1, and forwards the email.
Admin panel
Submissions appear in the admin panel at /admin/contact. The table shows date, name, email, subject (truncated), source page, and status. Click any row to expand the full message inline.
Available actions per row:
- Read — mark the submission as read (turns badge blue)
- Spam — mark as spam (turns badge red)
- Reply — opens a
mailto:link pre-filled with subject and quoted message; marks the submission as replied (turns badge green) - Delete — remove the submission
- Bulk delete — select multiple rows and delete at once
buildStaticExport
During stedefast build, the module writes dist/data/contact/summary.json:
{ "totalSubmissions": 42, "unread": 3 }
This file is served from the CDN and can be used in templates or islands to display a submission count badge.
Configuration reference
| Option | Type | Default | Description |
|---|---|---|---|
to |
string |
required | Recipient email address — must be a verified CF destination |
subjectPrefix |
string |
"[Contact]" |
Prefix prepended to the email subject line |
turnstileSiteKey |
string |
required | Cloudflare Turnstile public site key (safe to expose) |
provider |
"email-routing" | "resend" |
"email-routing" |
Email delivery provider |
resendApiKey |
string |
— | Required when provider = "resend" |
fields.subject |
boolean |
false |
Show an optional Subject field in the form |
successMessage |
string |
"Thanks! I'll be in touch." |
Message shown after a successful submission |