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.