Add a contact form to a Hugo site
Hugo is fast because it does one thing: it renders Markdown into HTML at build time and gets out of the way. That's also why a contact form is the first thing that stops a Hugo site cold. There's no PHP, no Node process, no server-side anything sitting behind hugo server in production — just files on a CDN. When a visitor submits a <form>, there is nothing on the other end to catch it.
You don't fix this by adding JavaScript. You fix it by pointing the form's action at a URL that already knows how to receive a POST, store it, and notify you. That's the whole trick, and it needs exactly zero client-side script.
The form: plain HTML in a partial
Drop this in layouts/partials/contact-form.html (or inline it in a page template — a contact.md content page with a matching layouts/contact/single.html works too):
<form action="{{ .Site.Params.formEndpoint }}" method="POST">
<label>Name
<input type="text" name="name" required />
</label>
<label>Email
<input type="email" name="email" required />
</label>
<label>Message
<textarea name="message" required></textarea>
</label>
<!-- honeypot: hidden from real visitors, bots that fill it get silently dropped -->
<input type="text" name="_honeypot" style="position:absolute;left:-9999px" tabindex="-1" autocomplete="off" />
<!-- send visitors to a thank-you page after a successful submit -->
<input type="hidden" name="_redirect" value="{{ .Site.BaseURL }}thanks/" />
<button type="submit">Send</button>
</form>
Include it wherever you need it:
{{ partial "contact-form.html" . }}
No fetch call, no onsubmit handler, nothing to hydrate. The browser does a normal form POST, the SubmissionBuddy endpoint replies with a 303 to your _redirect target, and the visitor lands on your thank-you page. If a visitor has JavaScript disabled, or you're testing with curl, it works exactly the same way.
Wiring the endpoint through site params
Hardcoding the form URL in a template works, but keeping it in hugo.toml means you can change forms (or point a staging build at a different form) without touching layout code:
# hugo.toml
[params]
formEndpoint = "https://api.submissionbuddy.io/f/your-form"
(If you're on an older Hugo config using config.yaml or config.toml, the [params] block works the same way — Hugo just reads .Site.Params.formEndpoint regardless of which format you use.)
Sign up free at SubmissionBuddy, create a form, and you'll get a URL that looks like https://api.submissionbuddy.io/f/your-form. Drop it into formEndpoint and every {{ .Site.Params.formEndpoint }} reference across your templates picks it up.
There's no schema to define on the SubmissionBuddy side, either. Add, rename, or remove fields in your Hugo form template whenever you want — the endpoint accepts whatever field names you send, with no config change required on the form itself.
Spam protection
The _honeypot field in the form above is doing real work: it's a plain text input that's invisible to a human (positioned off-screen, not just display:none, since some bots skip visually-hidden-via-CSS fields — either approach is accepted here) but visible to a form-filling bot. If it arrives filled in, SubmissionBuddy drops the submission silently — no database row, no S3 write — while still returning a normal-looking success response, so the bot's script has no signal that anything went wrong.
Per-IP and per-form rate limiting is on by default on every plan, no configuration needed. If you get sophisticated abuse — proof-of-work solvers, headless-browser bots — paid plans add an invisible CAPTCHA that runs entirely server-side (no third-party script, no cookies) and challenges only suspicious traffic, not every visitor.
The thank-you page
_redirect needs a real page to land on. In Hugo, that's just another content file:
<!-- content/thanks.md -->
---
title: "Thanks"
---
Thanks for reaching out — we'll get back to you shortly.
With the default archetype and a matching layouts/thanks/single.html (or reuse your single.html for pages), hugo builds this to /thanks/index.html, which is the URL you pointed _redirect at above.
What happens after the visitor hits submit
The raw submission is written to durable storage before SubmissionBuddy sends back any response at all — nothing touches the data until it's safely stored. From there it moves through independent, retryable stages: a database write that makes it searchable in your dashboard, then an email notification to your team. If the email step hiccups, it retries automatically; it doesn't touch the already-stored submission.
The honest pitch
Free plan: 100 submissions a month, 2 forms, no credit card. That's enough for most personal sites, portfolios, and small project pages built with Hugo. If you outgrow it, paid plans start at $8/month. There's no client library to install for any of this — it's an HTML action attribute and a config value.