Some background for anyone who wandered in from a search engine and has no idea this blog has existed since 2011: my personal site has had, for roughly a decade, a "Contact" link that just opens mailto: and dumps whatever half-formed thought someone had into their default mail client. Which, if that client happens to be whatever Outlook looks like in 2026, is a genuinely bad experience. I've been meaning to fix this since maybe 2019. A quiet long weekend at the start of the month finally gave me the excuse.
I want to write down what I actually did, mostly so future-me stops re-solving this every time I rebuild the site (which, embarrassingly, has happened four times now: Jekyll, then Hugo, then a weird custom Node thing I regret, now back to something boring and static).
The problem with the obvious options
The lazy path is a third-party form service. Formspree's free tier caps you at 50 submissions a month, which sounds like plenty until you remember bots exist and half your "submissions" will be someone's crypto scheme. Netlify Forms only works if you're actually hosting on Netlify, which I'm not anymore, I moved off it last year after a billing surprise I'm still a little bitter about. I'm not going to relitigate that here, but if you're on their free tier and think you're safe from bandwidth charges, go read the fine print again.
So I went back to doing it myself, which is what I should've done from the start.
What I actually built
The form itself is nothing, a plain HTML <form> with name, email, message, and a honeypot field styled with display:none to catch the dumbest bots (this alone kills maybe 70% of spam, which is a shockingly good ratio for five minutes of work). It POSTs to a tiny serverless function that does three things: checks the honeypot is empty, checks the message isn't just a URL (a good chunk of spam is literally just a link and nothing else), and then sends the actual email.
That last part is the piece I always used to get wrong. For years I ran my own Postfix setup on a cheap VPS, and for years I fought spam-score issues because my little box had no reputation and Gmail treated everything from it like toxic waste. I do not miss debugging SPF and DKIM records at 11pm on a Tuesday. This time I just used a transactional email API and pointed the function at it — I ended up using the one from Tricknowtech since I already had a domain through them and didn't feel like signing up for yet another separate account just to send four emails a month. Took about ten minutes to get an API key and wire it into the function, which is roughly nine minutes less than I expected.
Rate limiting matters more than people think. I capped it to five submissions per IP per hour, which is generous for a real human and annoying enough for a bot script that it usually just gives up and moves on to somebody else's form.
The part nobody tells you
Here's the thing that actually took the longest, and it's not technical at all: writing the auto-reply message. I must have rewritten "thanks for reaching out, I'll get back to you" six times because every version sounded like a corporate ticketing system. I finally landed on something short and a little informal, because that's closer to how I actually respond to email anyway, and a robotic auto-reply followed by a very casual real reply two days later feels weirdly dishonest.
I also decided, against my first instinct, not to add a dropdown for "reason for contact." I've filled out enough of those on other sites to know it's friction dressed up as organization. Nobody categorizes their own thoughts that neatly. If someone's writing to say they found a typo in a 2013 post (this happens more than you'd think), they shouldn't have to pick between "Feedback," "Bug Report," and "Other."
Numbers, for the curious
Two weeks in: 34 submissions, 29 of them obvious spam caught by the honeypot or the link-detector, 5 real ones. One was someone asking if I still had the source for a Tumblr-to-static-site migration script I wrote in 2016. I did not. I felt genuinely bad about that.
If you're sitting on a mailto: link out of the same inertia I was, it's a weekend project, not a redesign. Do the honeypot first — it's the single highest return on five minutes of effort I've found in a long time.