A Self-Hosted Contact Form for Static Sites

post-thumb

Static sites are great. They’re fast, secure, easy to deploy, and scale better than just about anything else. What they don’t do well, though, is anything dynamic that requires a web service. The most ubiquitous example of this is the contact form.

Every website needs one, but for static sites, they’re not possible due to the static nature of the site. You can use third party tools like Formspree or Airform, but then you’re at their mercy, and in my experience, what most sites really want is a way to simply set-it and forget-it. No one wants to deal with a third-party service that could change their API, terms or privacy policies at any time.

This is where this project comes in.

It started because I wanted something simple: accept a POST, send an email, redirect the user. No JavaScript gymnastics, no third-party lock-in, and no need to reinvent what SMTP already does well. Formspree and Airform were great, but what happens when they change their API in 6 months or a year and I have to try to figure out why I’m not getting emails? How long will it take me to even realize that I’m not getting them in the first place? My sites don’t generate huge amounts of traffic, so I might not notice for a while. How many contacts will I have missed or potential business leads? I don’t want to take that risk.

With that in mind, I asked ChatGPT to suggest some self-hosted alternatives to Formspree or Airform, but each of the suggestions were no longer on GitHub, so I asked for a few more suggestions. The conversation was actually pretty amusing, it gave a few options, I tried looking at them, but several of them were no longer on GitHub. I asked for more suggestions, and it suggested that I should just write it myself. So I did. I had ChatGPT give me the basic boilerplate, and then I added a few features, like honeypot spam protection, CORS support and validation.

It was a fun little afternoon project. I hope you find it useful.


What It Does

This is a small Go application that:

  • Accepts POST requests to /f/contact
  • Filters spam using honeypot fields
  • Sends email via any SMTP server
  • Respects CORS settings
  • Supports redirecting users to a thank-you page
  • Emits clean, structured logs using Go’s built-in slog

That’s it. No frameworks, no dependencies, and no surprises.


How To Use It

I run it in Docker behind Caddy as a reverse proxy, listening at contact.marclewis.com. Forms on this site just post to it directly:

<form method="POST" action="https://contact.marclewis.com/f/contact">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <input type="hidden" name="_next" value="https://marclewis.com/thank-you">
  <input type="text" name="_gotcha" style="display:none">
  <button type="submit">Send</button>
</form>

After submission, the backend emails me the submission and redirects the user back to a static thank-you page. Perfect for a simple contact form.

It still has its issues—spam bots can and will get through. This could be further enhanced by adding a Captcha through Google or Cloudflare (ideally a “silent” captcha), but it will block the most basic contact form spam with the honeypot features. For me, though, I’d rather make sure that people can contact me than get caught up in anti-spam measures.


Deployment Notes

It’s containerized using a two-stage Dockerfile and supports configuration via environment variables:

  • SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD
  • RECIPIENT_EMAIL
  • CORS_ALLOW_ORIGIN
  • PORT

Everything runs in about 10MB of RAM. It’s designed to be forgettable—in a good way.

Note: If you’re running this on an Akamai or Linode VPS, you’ll need to reach out to their support to enable outbound SMTP. They block it by default to prevent spam, but it’s a quick request to get it opened up.


Source Code

You’ll find it here: github.com/caffsoft/hugo-contact

It’s licensed under MIT and written entirely with the standard library. If you’re running a static site and need a basic contact form, it’ll probably work out of the box.

No frameworks, no web UI, and no magic.


Update - May 27, 2025

After launching this, it quickly became clear that today’s spam bots are more sophisticated than expected. Within minutes of deployment, I started receiving form submissions from automated scripts that ignored the honeypot fields entirely.

To address this, I added a lightweight JavaScript-based token system:

  • On page load, a small script fetches a signed timestamp token from the contact backend.
  • The token is inserted into the form as a hidden field.
  • The backend validates the token’s age and signature before accepting the submission.

This ensures that the form was loaded in a real browser with JavaScript enabled, which cuts out nearly all the low-effort spam bots.

The solution is still self-hosted, has no external dependencies, and shouldn’t impact user experience.


If you’re curious or have feedback, reach out through the contact form that uses this service, or ping me at @gottafixthat@mastodon.social.


If this post helped you, feel free to buy me a coffee or drop me a note,
I'd love to hear about what you've built.