The Journey of Building My Website (part 3): contact form, colophon, and what it feels like to ship something public
The final part. How the contact form actually sends email with Resend, what the colophon records, and why hitting publish is harder than writing the code.
This is the last one.
The first two posts covered design decisions and the technical core. This one covers what I'd call the finishing work: getting the form to actually send email, adding a colophon page, and some thoughts on what it means to put something online.
Contact form: making it real
When the site first came together, the contact form was a design prop. Submit it and you'd get an alert(): "This is a design mockup — the form is for show."
You see this on portfolio sites all the time. I'd grown used to it. But one day I thought: if someone actually wants to reach me, what do they do?
They'd see the alert, hunt around for an email address, maybe find one, send a proper email. The whole experience would be fragmented. Making the form work is the most basic form of respect for anyone who bothers to fill it in.
Why Resend
Sending email sounds simple but has a few options:
- Nodemailer + SMTP: fiddly to configure, need an SMTP service
- SendGrid / Mailgun: powerful but overbuilt for a single contact form
- Resend: email API designed for developers, free tier covers 100 emails a day, excellent Next.js documentation
Resend.
The Server Action
Server Actions (available since Next.js 13.4) make this clean. I added app/actions/contact.ts:
"use server";
export async function sendContactEmail(
_prev: ContactState,
formData: FormData
): Promise<ContactState> {
const from = String(formData.get("from") ?? "").trim();
const email = String(formData.get("email") ?? "").trim();
const msg = String(formData.get("msg") ?? "").trim();
if (!from || !email || !msg) {
return { status: "error", message: "Please fill in name, email, and message." };
}
const { error } = await resend.emails.send({
from: "Portfolio Contact <noreply@sevenishere.com>",
to: "hi@sevenishere.com",
replyTo: email,
subject: `[Portfolio] ${from}`,
text: msg,
});
if (error) return { status: "error", message: "Failed to send. Email me directly." };
return { status: "success" };
}replyTo is set to the visitor's email, so when I hit reply in my inbox, the mail client fills in their address automatically.
State management with useActionState
React 19's useActionState handles pending, success, and error states in a few lines:
const [state, action, isPending] = useActionState(sendContactEmail, INITIAL);While submitting: all fields disabled, button reads "Sending…".
On success: the whole form is replaced by a confirmation screen.
On error: an inline message appears above the button; the form stays filled so the visitor can try again.
The colophon
A colophon is a publishing tradition — the last few pages of a book, recording what typeface it was set in, where it was printed, who edited it.
I like this idea. The same applied to a website: what tools were used, why the type choices were made, what's running under the hood. Writing it down isn't just for visitors — it's for future me too.
What the colophon covers:
- Typefaces: Source Serif 4 (body), JetBrains Mono (monospaced)
- Stack: Next.js 16, React 19, TypeScript, MDX, Resend, Vercel
- Design approach: designed entirely in code, no Figma
- Colour: OKLCH, single accent
- Licensing: original writing, source code not open-sourced
The footer had a "Colophon" link from the beginning, pointing at href="#". Building the page and wiring it up properly felt satisfying in a small way — one less dead link.
Shipping it
After deploying, I stared at the live URL for about ten minutes.
I'm not sure exactly what that feeling was. Technically everything was ready — all pages working, no console errors, mobile layout fine. But there was still a "are you sure?" hesitation.
I thought about where it came from.
Part of it: the writing is now public. The blog posts, what's on the about page, what I've said about myself — anyone can read it, and Google will index it. That's completely different from notes in a private folder. Once something is public, you're accountable to it in a way you aren't when it's just for you.
Another part: this site is a snapshot of who I am right now. In six months I'll probably think the design is ugly and the writing is naive. That's fine. That's just evidence of growth. Being frozen in time is part of what a personal site is.
The main thing that was keeping me from deploying was perfectionism. There was always something that needed one more adjustment. I eventually stopped myself with a simple question: can this be fixed after it's live?
For almost everything, the answer was yes. So I shipped it.
What I took away from building this
If I had to summarise the most useful things this project taught me:
Get clear on what it needs to do before you start building. This is not a new lesson. I seem to need to relearn it every time.
For me, design happens in code, not in a design tool. Iterating directly in JSX and CSS is faster than composing in Figma and then translating it back to code.
It only counts if it's public. Something sitting on localhost can always be better. Hit deploy, and put "better" in the next version.
The site is live now. You're reading it. If there's something you want to talk about, the contact form actually works.