The Contact Form Setup That Stopped Landing in Spam. Resend, Next.js, DKIM.
Your contact form works. You tested it, the email arrived, you shipped it. Then a client calls and asks why you never replied to their enquiry from two...
Garvit Sharma
20 June 2026 · 6 MIN READ
The Contact Form Setup That Stopped Landing in Spam. Resend, Next.js, DKIM.
webight.comOn this page
Your contact form works. You tested it, the email arrived, you shipped it. Then a client calls and asks why you never replied to their enquiry from two weeks ago. You check spam. There it is.
This happened to us on a client build early this year. The form was fine. The API route was fine. Gmail still filed the enquiries under spam, and the client found out only because a customer followed up on WhatsApp. We sent 7 test emails after they told us; 3 landed in spam.
Here is the one idea this whole post defends: Gmail does not read your code. It reads your DNS. A contact form that lands in the inbox is a DNS problem first and a code problem second, and almost everyone spends 100% of their time on the second part.
Why your perfectly good email gets filed as junk
See, when your server sends an email claiming to be from contact@yourdomain.com, Gmail asks one question: can this sender prove the domain owner allowed this?
The proof lives in three DNS records.
- SPF says "these servers are allowed to send mail for this domain."
- DKIM is a cryptographic signature. The sending server signs each email with a private key, and the public key sits in your DNS so receivers can check the signature.
- DMARC tells receivers what to do when SPF or DKIM fails: do nothing, quarantine, or reject.
No records, no proof. And since 2024, Google and Yahoo stopped treating this as optional. Bulk senders without authentication get throttled or binned outright. Your contact form sends 4 emails a week, sure, but it sends them from a domain with zero reputation, which is worse than a bad reputation. Unknown sender, unauthenticated, transactional-looking content with a stranger's text inside. That is the exact shape of spam.
There is a psychology angle here too. Spam filters work like trust works between people. A stranger who shows up with ID, a referral, and a consistent story gets in. A stranger with a great smile and no ID does not, no matter how genuine he is. Your email content is the smile. The DNS records are the ID.
The two mistakes that cause 90% of this
Mistake one: sending from the visitor's email address. It feels natural. The enquiry came from rahul@gmail.com, so you set from: rahul@gmail.com and now you can hit reply. Except you just spoofed Gmail's own domain. You are a random server claiming to send mail as gmail.com, and Gmail's DMARC policy says reject that. This single mistake sends more contact forms to spam than everything else combined.
Mistake two: sending from the provider's shared test address, like onboarding@resend.dev. It works in development, so it ships. Now your client's enquiries arrive from a domain shared with every other developer who never finished setup. You inherit whatever reputation that pool has, and you look like a template.
The fix for both is the same pattern: from is an address on your own verified domain, replyTo is the visitor. You get authentication and you still reply with one click.
The actual stack
Everything at Webight runs on Next.js and Resend, including our own contact form on webight.com and the client forms we ship. Here is the complete API route, copy-paste ready:
// src/app/api/contact/route.ts
import { NextResponse } from "next/server";
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
let body: { name?: string; email?: string; message?: string };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid request." }, { status: 400 });
}
const name = body.name?.trim();
const email = body.email?.trim();
const message = body.message?.trim();
if (!name || !email || !message) {
return NextResponse.json(
{ error: "Name, email and message are required." },
{ status: 400 }
);
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json({ error: "Invalid email." }, { status: 400 });
}
if (message.length > 5000) {
return NextResponse.json({ error: "Message too long." }, { status: 400 });
}
const { error } = await resend.emails.send({
from: "Webight Contact <contact@webight.com>",
to: ["hello@webight.com"],
replyTo: email,
subject: `New enquiry from ${name}`,
text: `Name: ${name}\nEmail: ${email}\n\n${message}`,
});
if (error) {
console.error("Resend error:", error);
return NextResponse.json({ error: "Failed to send." }, { status: 500 });
}
return NextResponse.json({ ok: true });
}
Four things in there matter for deliverability, and none of them are clever.
The from is an address on our own domain, verified in Resend. The replyTo is the visitor, so hitting reply in Gmail goes straight to them. The body is plain text, because an enquiry notification needs zero HTML and plain text trips fewer content filters. And the subject contains the visitor's name instead of a static string, so 30 enquiries do not look like 30 identical machine blasts.
RESEND_API_KEY goes in .env.local and in your Vercel project settings. That part takes a minute.
The DNS records, exactly
This is the part people skip, and it is the part that decides everything. When you add your domain in Resend, it gives you records to create. The pattern looks like this (your DKIM key and region will differ):
Type Name Value
---- ---- -----
MX send.yourdomain.com feedback-smtp.us-east-1.amazonses.com (priority 10)
TXT send.yourdomain.com "v=spf1 include:amazonses.com ~all"
TXT resend._domainkey.yourdomain.com "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
TXT _dmarc.yourdomain.com "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com"
What each one does, in one line each:
The MX and SPF records on the send subdomain authorize Resend's infrastructure to send for you, and they keep that authorization scoped to a subdomain so your root domain's mail setup stays untouched. The DKIM record publishes the public key that matches the private key Resend signs every email with. The DMARC record tells receivers your policy and where to send aggregate reports.
Two notes on DMARC because this is where people get scared. Resend does not require it, but Gmail's sender guidelines do, and a missing DMARC record costs you trust points on every delivery. Start with p=none if you want to watch reports for a week before enforcing anything, then move to p=quarantine. And create the dmarc@yourdomain.com mailbox or alias, otherwise the reports bounce.
Add the records at your DNS provider, hit verify in Resend, wait. Propagation took 19 minutes for us on Cloudflare, but give it up to an hour elsewhere.
Then test properly. Send yourself an email through the form, open it in Gmail, click the three dots, hit "Show original". You want three lines: SPF PASS, DKIM PASS, DMARC PASS. If any of them says FAIL or NEUTRAL, your records have a typo or have not propagated. Fix it before launch, because reputation damage from failed sends sticks around longer than the typo does.
On that client build I mentioned, this exact setup, records plus own-domain sender plus plain text, took the better part of an afternoon including DNS waiting time. Every enquiry since has landed in the primary inbox. Around 40 of them so far, zero in spam.
What this costs you
Nothing. Resend's free tier covers 3,000 emails a month, which is roughly 100 enquiries a day. No contact form on a studio or small business site gets near that. The DNS records are free. The whole setup is under an hour of actual work, and the alternative is losing leads silently, which is the most expensive bug a business website can have because nobody files an issue for an email they never saw.
A slow site loses you visitors you can count in analytics. A contact form that lands in spam loses you the exact people who already decided to pay you, and it leaves no trace.
So here is the challenge. Open your own contact form right now, send yourself a test enquiry, and click "Show original" in Gmail. If you see three PASS lines, good, you are ahead of most agencies. If you do not, you now know the fix takes one afternoon, and every day you delay it there might be a lead sitting in your spam folder asking why you are ignoring them.
If you get stuck on the records, my DMs are open.
We build custom platforms, websites, and automation.
The two people who build it are the two you talk to, and every price is on the page.

Garvit Sharma
Full-stack developer and co-founder of Webight, a two-person web and AI studio in India. He writes these from real client work. More about us.
Keep reading
// RELATEDA Client Paid 3 Agencies for SEO Before Me. None of Them Opened the Website Code.
webight.comA Client Paid 3 Agencies for SEO Before Me. None of Them Opened the Website Code.
7 MIN READ
One Cloudflare Worker Saved My Client's App When Supabase Got Blocked. It Also Cut TTFB in Half.
webight.comOne Cloudflare Worker Saved My Client's App When Supabase Got Blocked. It Also Cut TTFB in Half.
6 MIN READ
Claude Code Fixed 6 SEO Bugs on My Site in 40 Minutes. The Exact Prompts.
webight.comClaude Code Fixed 6 SEO Bugs on My Site in 40 Minutes. The Exact Prompts.
8 MIN READ