Hugo contact forms, no runtime required.
Hugo builds HTML and gets out of the way — which is exactly what a form needs. Point a plain form at one endpoint and every submission becomes an email in your inbox and a record in your dashboard, while your site stays the pile of fast static files it already is.
A partial for layouts, a shortcode for content.
Hugo has no runtime, so the plain HTML POST isn't a fallback here — it's the integration. Build the form once as a partial, expose it to content authors as a shortcode, and keep the key in site params.
<!-- layouts/partials/contact-form.html
Hugo executes this at build time and ships plain HTML. The browser's
POST to the endpoint is the entire integration. -->
<form action="https://api.formwire.com/submit" method="POST">
<input type="hidden" name="access_key" value="{{ site.Params.formwireKey }}">
<!-- Hugo can't redirect after a POST, so FormWire does. absURL turns
thanks/ into a full URL using baseURL from hugo.toml. -->
<input type="hidden" name="_redirect" value="{{ "thanks/" | absURL }}">
<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>
<button type="submit">Send</button>
</form> Swap YOUR-ACCESS-KEY for your form's key in hugo.toml. The full field and response reference lives in the docs at /docs/.
The four things that trip up Hugo forms.
Go templates have sharp edges you only meet once. The snippets above already account for all four.
Template code runs inside HTML comments
Go template actions are parsed everywhere — a {{ partial }} call you "commented out" with <!-- --> still executes at build time, and inside the partial itself it recurses until the build dies. Disable template code with {{/* */}} comments, never HTML ones.
Everything dynamic resolves at build
Hugo has no server at request time, so the key, the redirect URL and every field name are baked into the HTML when the site builds. That's a feature: the browser POSTs straight to the endpoint, and there's nothing on your side to keep alive, patch or scale.
absURL is only as good as baseURL
Hugo's default baseURL is https://example.org/ — leave it unset and your _redirect politely sends every visitor there after they submit. Set baseURL in hugo.toml (and per environment if staging differs) before trusting any absURL output.
Keep the key in site params
Hardcoding the access key in a partial and again in a shortcode drifts the moment you rotate it. Read site.Params.formwireKey in both and the key lives in one line of hugo.toml — safe to commit, since it can only deliver to inboxes you've verified.
From nothing to a working form.
Grab your access key
Create a form in the dashboard and copy its public key into [params] in hugo.toml. It's a safe-to-commit alias for your verified email.
Add the partial, place it once
Save contact-form.html under layouts/partials and render it with one line in a layout — or expose it as a shortcode so Markdown authors can place it themselves.
Submissions land in your inbox
Every POST arrives as a formatted email with Reply-To set to the visitor, and as a searchable record in your dashboard.
Built for a site with no runtime.
Works with any theme
Partials in your project's layouts directory override the theme's without forking it, so the form drops into Ananke, PaperMod or your own theme the same way.
Spam filtered without JavaScript
The honeypot runs server-side at the endpoint, so a script-free Hugo site keeps bots out of your inbox — and a captcha is one dashboard toggle away if a form gets targeted.
Nothing added to the build
The integration is a partial and a config param. Your hugo command, your CI and your deploy stay byte-for-byte the same.
Every submission stored
Beyond the email, each POST becomes a searchable record with CSV export and a JSON read API — the paper trail a static site otherwise can't keep.
Hugo form FAQ
The questions Hugo developers actually ask.
The browser does the work: submitting the form POSTs its fields to FormWire's endpoint, which stores the submission and emails it to you. Hugo's only job is rendering the form's HTML at build time, which is why no runtime, function or plugin enters the picture.
Angle brackets. {{% %}} passes the shortcode's output through the Markdown renderer, which can wrap or escape raw form HTML in ways that break the markup. {{< >}} inserts it verbatim, which is what a form needs.
Add a hidden _redirect field pointing at your thanks page, as in the partial above. It must be an absolute URL — build it with absURL and make sure baseURL is set in hugo.toml, because Hugo's default of example.org will otherwise end up in the redirect.
Yes. The key is a public alias for your verified email, not a secret — delivery only ever reaches recipients you've verified. Keep it in site params for tidiness, not secrecy.
Yes — POST the same fields as JSON with an Accept: application/json header and show the response inline. On most Hugo sites the plain POST plus _redirect is the better trade: it works with zero script and survives every theme change.
Start on the free tier: 250 submissions a month, unlimited forms, and the endpoint and markup are identical on every plan. Paid tiers add volume, file uploads and integrations like Slack.
See pricing →Building with another framework?
See all frameworksShip your Hugo form today.
Grab an access key, paste a partial, and route submissions to your inbox — or on to Slack, Sheets and Zapier.
Free tier forever. No credit card required.