Stedefast

Modules

Contact

4 min read

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: "hello@example.com",           // 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:

  1. Go to Cloudflare Dashboard → Email → Email Routing → Destination addresses
  2. Click Add destination address and enter the address used for to
  3. Verify ownership via the confirmation email Cloudflare sends
  4. In your Worker settings, create the SEND_EMAIL email 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": "alice@example.com",
  "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