Forms reference
The Contact form and the career application form share one pipeline:
<Form> (design system) → server action (src/lib/actions.ts) → deliver() → provider(s) chosen in FORM_PROVIDERThe forms, their states (sending, “Thank you”, “Something went wrong”) and the spam honeypot work out of the box. Where the submissions go is a setting, not code.
1. Pick a provider
Section titled “1. Pick a provider”Copy .env.example to .env.local (or add the variables in your host, e.g. Vercel → Settings → Environment Variables) and set FORM_PROVIDER.
FORM_PROVIDER |
What happens | Variables |
|---|---|---|
console (default) |
Printed to the server log. For development. | — |
webhook |
Posted as JSON to a URL — Zapier, Make, n8n, Pipedream, Formspree, Slack workflow, your own API. | 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 emails you and adds the contact to Mailchimp.
If any of them fails, the visitor sees “Something went wrong” (their input stays in the form) and the reason is logged on the server.
Mailchimp notes
Section titled “Mailchimp notes”- API key: Profile → Extras → API keys. It ends in your data center (
…-us21); nothing else to configure. - Audience ID: Audience → Settings → Audience name and defaults.
- Consent: new contacts are added as
transactional— they can receive replies, not campaigns. Usependingto send a double opt-in email, orsubscribedonly if the form asks for marketing consent. - Merge fields: first name →
FNAME, last name →LNAME, phone →PHONE(a new audience’s defaults). Change the map at the top ofproviders/mailchimp.tsif your audience uses other tags.
Webhook payload
Section titled “Webhook payload”{ "form": "contact", "title": "New contact enquiry", "submittedAt": "2026-09-16T10:48:23.256Z", "data": { "firstName": "Ana", "lastName": "Lee", "email": "ana@example.com", "phone": "", "services": ["Content Marketing"], "message": "Hi!" }, "labels": { "firstName": "First Name", "lastName": "Last Name", "email": "Email", "phone": "Phone Number", "services": "Services", "message": "Message" }}form is contact or application. Application data also carries position, linkedin, otherSocial and website.
2. Add your own provider (HubSpot, Brevo, Airtable, a database…)
Section titled “2. Add your own provider (HubSpot, Brevo, Airtable, a database…)”-
Create
src/lib/forms/providers/<name>.ts:import { env, toLines, valueOf, type FormProvider } from "../types";export const brevoProvider: FormProvider = {name: "brevo",async send(submission) {const response = await fetch("https://api.brevo.com/v3/contacts", {method: "POST",headers: { "api-key": env("BREVO_API_KEY"), "Content-Type": "application/json" },body: JSON.stringify({email: valueOf(submission, "email"),attributes: { FIRSTNAME: valueOf(submission, "firstName"), LASTNAME: valueOf(submission, "lastName") },updateEnabled: true,}),});if (!response.ok) throw new Error(`Brevo responded ${response.status}: ${await response.text()}`);},}; -
Register it in
providers/index.ts(brevo: brevoProvider). -
Set
FORM_PROVIDER=brevoand its variables in.env.example/ your host.
Helpers from types.ts: valueOf(submission, name) returns one field as text, toLines(submission) returns
"Label: value" lines for every filled field, and env(name) reads a required variable with a clear error.
3. Change a form
Section titled “3. Change a form”- Fields and labels live in the page copy:
src/content/pages/contact.tsandsrc/content/pages/career.ts(nameis the key providers receive,labelis what people read). - Required fields and multi-choice fields are listed in
src/lib/actions.ts. Keep them in step with therequiredflags in the copy, which the browser checks first. - A new form: lay it out with
<Form>,<FormField>and<CheckboxGroup>, add a server action next to the others that callsdeliver(data, { form, title, fields, required }), and add the form name toFormKindintypes.ts.