Skip to content

Forms reference

The Contact form and the career application form work in one of three ways. You choose with environment variables (copy .env.example to .env, or set them in your host’s dashboard); no code changes.

Mode Set Hosting Where submissions go
Demo (default) nothing any static host Nowhere: the form shows “Thank you” and logs the fields in the browser console.
Static PUBLIC_FORM_ENDPOINT any static host Posted from the browser to a form service or webhook.
Server PUBLIC_FORM_MODE=server + FORM_PROVIDER a host with an Astro adapter Delivered by the site’s own route to console, webhook, Mailchimp or Resend.

Every mode shows the same states on the Submit button (spinner, “Thank you”, “Something went wrong”), keeps the typed values after an error, resets the form after success and drops spam caught by the hidden honeypot field.

Set PUBLIC_FORM_ENDPOINT to the URL the service gives you. The forms post multipart/form-data with Accept: application/json, and the service must answer with a 2xx status.

Service PUBLIC_FORM_ENDPOINT Notes
Formspree https://formspree.io/f/<form id> Emails you each submission.
Web3Forms https://api.web3forms.com/submit Also set PUBLIC_FORM_ACCESS_KEY.
Getform, Basin the form’s endpoint URL
Zapier, Make, n8n, Pipedream the webhook (“catch hook”) URL Route submissions anywhere: Mailchimp, Google Sheets, Slack, a CRM.

Fields sent: every input by its name (firstName, lastName, email, phone, services — once per checked option — message; applications also position, linkedin, otherSocial, website), plus form (contact or application) and subject (e.g. “New contact enquiry”).

The endpoint is public (it is in the page), which is how these services are designed to work; they filter spam themselves.

For Mailchimp or Resend without a third-party service, run the forms on your server:

  1. Add the adapter for your host: npx astro add vercel (or netlify, node, …).
  2. Set PUBLIC_FORM_MODE=server and choose providers with FORM_PROVIDER:
FORM_PROVIDER What happens Variables
console (default) Printed to the server log.
webhook Posted as JSON to a URL. FORM_WEBHOOK_URL, optional FORM_WEBHOOK_SECRET (sent as Authorization: Bearer …)
mailchimp Sender added or updated in an audience, tagged with the form and chosen services, the rest saved as a note. MAILCHIMP_API_KEY, MAILCHIMP_AUDIENCE_ID, optional MAILCHIMP_STATUS
resend Emailed to you; replying answers the sender. RESEND_API_KEY, FORM_EMAIL_TO, FORM_EMAIL_FROM

Use several at once with a comma: FORM_PROVIDER=resend,mailchimp. The forms then post to POST /api/forms/contact and /api/forms/application (src/lib/forms/route.ts, added by src/integrations/forms.ts). Without an adapter the build stops with NoAdapterInstalled. Astro rejects cross-site posts to the route, so only your own pages can submit.

  • API key: Profile → Extras → API keys. It ends in your data center (…-us21).
  • Audience ID: Audience → Settings → Audience name and defaults.
  • Consent: new contacts are added as transactional — they can receive replies, not campaigns. Use pending to send a double opt-in email, or subscribed only if the form asks for marketing consent.
  • Merge fields: first name → FNAME, last name → LNAME, phone → PHONE. Change the map at the top of providers/mailchimp.ts if your audience uses other tags.

Create src/lib/forms/providers/<name>.ts exporting a FormProvider ({ name, send(submission) } — throw to show the error state), register it in providers/index.ts and set FORM_PROVIDER=<name>. Helpers in types.ts: valueOf(submission, name), toLines(submission), env(name).

  • Fields and labels live in the page copy: src/content/pages/contact.json and src/content/pages/career.json (name is the key services receive, label is what people read).
  • Required fields for server mode are listed in src/lib/forms/definitions.ts; keep them in step with the required flags in the copy, which the browser checks first.