Adding a contact form to a Hugo site

July 23, 2026

Hugo builds your site in milliseconds precisely because it doesn't run a server — which is wonderful right up until you want a contact form, and the usual advice is "add a serverless function" (now you have two deploy targets) or "use mailto" (please don't). The third option: point plain HTML at a form backend and stay 100% static.

1. Get an endpoint

Create a NormForms form — you'll get a URL like https://normforms.com/f/x7Kp2mQ9RtLw.

2. Make it a partial

Create layouts/partials/contact-form.html:

<form action="https://normforms.com/f/YOUR_TOKEN" method="POST">
  <input type="text" name="name" placeholder="Your name">
  <input type="email" name="email" placeholder="Your email">
  <textarea name="message" placeholder="Your message"></textarea>
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
  <button type="submit">Send</button>
</form>

Then render it wherever you want the form:

{{ partial "contact-form.html" . }}

Prefer to keep the token out of templates? Put it in your site config (params.normformsToken in hugo.toml) and interpolate it in the partial — handy if you use different forms per environment.

3. Test locally

Submissions from hugo server on localhost are automatic test mode: delivered with a [Test] subject prefix, not counted against your quota. In production, set your domain in the form's settings so submissions from anywhere else are silently rejected.

The part you don't build

Spam filtering (honeypot, origin lock, rate limits, invisible challenge), email delivery with proper authentication, and a dashboard that stores every message with its delivery status. Your Hugo build stays exactly what it is: fast, static, and serverless in the original sense of the word.

Add your form →