Free plan250 submissions a month, spam filtering included, no card.See pricing →
Framework · Hugo

Hugo contact form, with no server behind it

Hugo is fast because it runs at build time and never at request time, which is also why a contact form has nowhere to go. The fix fits Hugo’s own patterns: keep the endpoint in config, the markup in a partial, and a one-line shortcode for Markdown pages.

3 min readAny Hugo siteLast reviewed

On this page

At a glance

Output
Static
Server code
None
Where it goes
layouts/partials/contact.html
Endpoint
In site params
In Markdown
A shortcode

The integration

Three small files. The endpoint lives in your config, so the same partial works in every environment, and the shortcode lets writers drop the form into any Markdown page.

{{/* layouts/partials/contact.html */}}
<form action="{{ site.Params.formEndpoint }}" method="POST">
  <label>Name <input name="name" autocomplete="name" required></label>
  <label>Email <input type="email" name="email" autocomplete="email" required></label>
  <label>Message <textarea name="message" required></textarea></label>

  <input type="text" name="_mtf_honeypot" tabindex="-1" autocomplete="off"
         aria-hidden="true" style="position:absolute;left:-9999px">

  <button type="submit">Send</button>
</form>

Step by step

  1. 1

    Create the endpoint

    Create a form in MakeTheForm to get your endpoint URL and inbox.

  2. 2

    Store it in your site params

    Add formEndpoint under [params] in hugo.toml. The endpoint is an address, not a secret, so it is fine in a public repository.

  3. 3

    Add the partial and shortcode

    Create layouts/partials/contact.html with the form, and layouts/shortcodes/contact.html that renders it. Keep the hidden _mtf_honeypot input.

  4. 4

    Build, deploy and test

    Deploy as usual to Netlify, Cloudflare Pages, GitHub Pages or any host, verify your recipient address once, and submit the live form.

Common pitfalls

Hard-coding the endpoint in every template

Pasting the URL into several layouts means several places to change it later, and a staging build that posts to the production form.

Fix: Read it from site.Params.formEndpoint, and override it per environment in config.

Putting raw HTML in Markdown

Hugo’s default Markdown renderer drops raw HTML unless unsafe rendering is turned on, so a form pasted into content silently disappears.

Fix: Use the shortcode. It renders the partial and needs no unsafe setting.

Troubleshooting

SymptomWhat’s happeningFix
form missingThe form was pasted into a Markdown file and Hugo removed it.Use the contact shortcode instead of raw HTML in content.
empty actionThe partial rendered action="" because the param is missing.Check formEndpoint is under [params] in hugo.toml, spelled the same as in the partial.
403Allowed domains is on and the site’s address is not listed.Add your production and preview domains, or leave the form in public mode.

Frequently asked questions

How do I add a contact form to a Hugo site?

Put a normal HTML form in a partial, set its action to a hosted form endpoint, and render it with a shortcode or from a layout. Hugo stays static, and the endpoint handles delivery and spam.

Can Hugo send email?

No. Hugo runs only when you build the site, so it cannot react to a submission. The form has to post to something that runs at request time, such as a form endpoint.

Does this work with Hugo themes?

Yes. Override the theme’s contact partial with your own in layouts/partials, or add the shortcode to the contact page. Theme files are never edited.

Other platforms

Keep going

Ship this form for real.

Create an endpoint, paste it in and watch the first submission land in your inbox.

Create free endpoint