Contact Form
The contact form is controlled entirely from src/config/contact.ts. No component code needs to change — pick a backend, add a credential, and submissions start arriving in your inbox.
Backends at a glance
Section titled “Backends at a glance”| Backend | Cost | Setup | Best for |
|---|---|---|---|
| Netlify Forms | Free (100 submissions/mo) | Zero — just deploy | Sites on Netlify |
| Formspree | Free (50 submissions/mo) | Create account, paste ID | Any host |
| FormSubmit | Free, unlimited | Paste email address | Quick setup, any host |
| Custom API route (Resend) | Pay-as-you-go | Create endpoint + API key | Full control, any host |
Option 1 — Netlify Forms (default)
Section titled “Option 1 — Netlify Forms (default)”No code changes needed. Netlify detects the data-netlify="true" attribute at build time and wires up the submission pipeline automatically.
What to do:
- Deploy the site on Netlify — no extra config required.
- After the first deployment, go to Netlify Dashboard → your site → Forms.
- Your form (
contact) appears there automatically. - Enable email notifications: Forms → contact → Form notifications → Add notification → Email notification.
Submission limit: The free tier allows 100 submissions per month. Upgrade in your Netlify billing settings if you need more.
export const CONTACT_FORM = { backend: "netlify", formName: "contact", // matches the name shown in the Netlify dashboard // ...};Option 2 — Formspree
Section titled “Option 2 — Formspree”Formspree sends submissions directly to your email and provides a dashboard with spam filtering and export.
Setup:
- Sign up at formspree.io and create a new form.
- Copy your form ID from the endpoint URL — it looks like
xpznkrjb. - Update
src/config/contact.ts:
export const CONTACT_FORM = { backend: "formspree", formspreeId: "xpznkrjb", // ← your form ID here // ...};Formspree handles CORS and spam filtering for you. The free plan allows 50 submissions per month; paid plans start at $10/month for 1,000 submissions.
Option 3 — FormSubmit
Section titled “Option 3 — FormSubmit”FormSubmit is a free, no-account service. Submissions are emailed directly to the address you configure.
Setup:
- Update
src/config/contact.tswith your email:
export const CONTACT_FORM = { backend: "formsubmit", // ...};- Deploy and submit the form once. FormSubmit sends a confirmation email to your address — click the link to activate it.
After activation, all submissions are forwarded to your email automatically. There is no submission limit on the free tier.
Option 4 — Custom API route with Resend
Section titled “Option 4 — Custom API route with Resend”For full control — custom email templates, CC/BCC, logging — create an Astro server endpoint that calls the Resend API. This works on any host that supports server-side rendering (Vercel, Cloudflare Workers, Node.js).
1. Install Resend and add an SSR adapter
Section titled “1. Install Resend and add an SSR adapter”pnpm add resendpnpm add @astrojs/vercel # or @astrojs/cloudflare / @astrojs/nodeUpdate astro.config.mjs to add the adapter:
import vercel from "@astrojs/vercel";
export default defineConfig({ output: "hybrid", // static by default, server for API routes adapter: vercel(), // ...});2. Create the API endpoint
Section titled “2. Create the API endpoint”Create src/pages/api/contact.ts:
import type { APIRoute } from "astro";import { Resend } from "resend";
const resend = new Resend(import.meta.env.RESEND_API_KEY);
export const prerender = false;
export const POST: APIRoute = async ({ request }) => { const data = await request.formData(); const name = data.get("name") as string; const email = data.get("email") as string; const service = data.get("service") as string; const message = data.get("message") as string;
// Basic validation if (!name || !email || !message) { return new Response(JSON.stringify({ error: "Missing required fields" }), { status: 400, headers: { "Content-Type": "application/json" }, }); }
const { error } = await resend.emails.send({ replyTo: email, subject: `New enquiry from ${name}`, html: ` <p><strong>Name:</strong> ${name}</p> <p><strong>Email:</strong> ${email}</p> <p><strong>Service:</strong> ${service || "—"}</p> <p><strong>Message:</strong></p> <p>${message.replace(/\n/g, "<br>")}</p> `, });
if (error) { return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { "Content-Type": "application/json" }, }); }
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "Content-Type": "application/json" }, });};3. Add your API key
Section titled “3. Add your API key”RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxAdd the same variable in your hosting provider’s dashboard (Vercel → Project → Settings → Environment Variables).
4. Switch the form to static backend and add a fetch handler
Section titled “4. Switch the form to static backend and add a fetch handler”Set backend: "static" in src/config/contact.ts, then add a <script> to src/sections/Contact.astro (or the contact page) that intercepts the submit event:
export const CONTACT_FORM = { backend: "static", // ...};<!-- at the bottom of src/sections/Contact.astro --><script> const form = document.querySelector("form") as HTMLFormElement;
form?.addEventListener("submit", async (e) => { e.preventDefault();
const button = form.querySelector("[type=submit]") as HTMLButtonElement; button.disabled = true;
const res = await fetch("/api/contact", { method: "POST", body: new FormData(form) }); const json = await res.json();
if (json.ok) { form.reset(); // Show success UI — e.g. replace the form with a thank-you message } else { alert("Something went wrong. Please try again."); }
button.disabled = false; });</script>Customizing form fields
Section titled “Customizing form fields”All fields are toggled and labelled in src/config/contact.ts:
fields: { name: { enabled: true, label: "Name", placeholder: "Jane Doe" }, service: { enabled: true, label: "Service" }, artist: { enabled: false, label: "Preferred Team Member" }, // ← disable if not needed message: { enabled: true, label: "Tell us about your project", placeholder: "...", rows: 4 },},
showServiceDropdown: true, // shows/hides the service <select>showArtistDropdown: false, // shows/hides the team member <select>
services: [ "Brand Identity", "Web Design", "UI/UX Strategy",],The artist dropdown is populated automatically from the team array in src/config.ts — no duplication needed.
Spam protection
Section titled “Spam protection”The form includes a honeypot field — a hidden input that real users never see. Bots fill it in; submissions with a non-empty honeypot are discarded by the backend (Netlify/Formspree handle this automatically) or you can check it manually in the API route.
honeypotField: { enabled: true, name: "bot-field",},To check it in a custom API route:
const bot = data.get("bot-field");if (bot) { // Silently succeed — don't tell bots they were caught return new Response(JSON.stringify({ ok: true }), { status: 200 });}