How to Add a Contact Form to Hugo

Last updated: March 2026

Hugo is a Go-based static site generator known for its speed. It can build thousands of pages in seconds. But because Hugo outputs plain HTML files with no server runtime, there's no built-in way to handle form submissions. Hugo's Go templating engine is powerful for generating pages, but once those pages are served to visitors, you need an external service to process any form data.

This guide walks you through adding a fully functional contact form to a Hugo site using FormWit as the form backend. No Hugo modules, no serverless functions, no Go code. Just HTML templates and a form endpoint that handles submissions, spam protection, and email notifications.

How Hugo form handling works

Hugo generates static HTML at build time using Go templates. Your site is a collection of .html files served by a web server or CDN. There's no application server running behind it. When a visitor fills out a contact form, the browser needs somewhere to send that data. That's where a form backend comes in.

The flow works like this:

  1. You create an HTML form in a Hugo template with the action attribute pointing to your FormWit endpoint
  2. A visitor fills out the form on your Hugo site and clicks submit
  3. The browser sends the form data directly to FormWit via a POST request
  4. FormWit validates the data, filters spam, stores the submission, and sends you an email notification
  5. The visitor sees a confirmation (either a redirect or an inline success message)

This approach keeps your Hugo site fully static. No server-side logic, no Hugo modules to install, and it works on any hosting provider: GitHub Pages, Netlify, Cloudflare Pages, Vercel, S3, or a plain web server.

Set up a Hugo contact form

Step 1: Create a FormWit account

Go to app.formwit.com/auth/signup and create a free account. No credit card required. Once signed in, click Create Form and give it a name (e.g., "Hugo Contact Form"). You'll receive a unique endpoint URL that looks like https://app.formwit.com/api/s/YOUR_FORM_ID. Copy this. You'll need it in the next step.

Step 2: Create a contact page and template

Hugo uses a combination of content files and layout templates to render pages. You'll create a content file that defines the contact page and a layout template that contains the actual HTML form.

First, create the content file at content/contact.md:

---
title: "Contact Us"
description: "Get in touch with us. We'd love to hear from you."
layout: "contact"
---

Have a question or want to get in touch? Fill out the form below and we'll
get back to you as soon as possible.

The layout: "contact" front matter tells Hugo to use a specific template for this page. Now create the layout file. Depending on your Hugo setup, place this at layouts/page/contact.html or layouts/_default/contact.html:

{{ define "main" }}
<div class="contact-page">
  <h1>{{ .Title }}</h1>

  {{ .Content }}

  <form
    action="https://app.formwit.com/api/s/YOUR_FORM_ID"
    method="POST"
    class="contact-form"
  >
    <!-- Honeypot field for spam protection -->
    <input type="text" name="_gotcha"
      style="display:none" tabindex="-1" autocomplete="off" />

    <div class="form-field">
      <label for="name">Name</label>
      <input type="text" id="name" name="name" required />
    </div>

    <div class="form-field">
      <label for="email">Email</label>
      <input type="email" id="email" name="email" required />
    </div>

    <div class="form-field">
      <label for="message">Message</label>
      <textarea id="message" name="message" rows="5" required></textarea>
    </div>

    <button type="submit">Send Message</button>
  </form>
</div>
{{ end }}

Replace YOUR_FORM_ID with the endpoint ID from your FormWit dashboard. The template uses Hugo's {{ define "main" }} block, which inserts the content into your base template's {{ block "main" . }} placeholder. The {{ .Title }} and {{ .Content }} variables pull data from the front matter and body of contact.md.

If your Hugo theme uses a different block name or doesn't use blocks at all, you may need to extend your theme's base template instead. Check your theme's baseof.html to see what block names are available.

Step 3: Add honeypot spam protection

The form above already includes a honeypot field, the hidden _gotcha input. This is a simple but effective spam prevention technique. Here's how it works:

  • The field is hidden from real visitors using style="display:none" and tabindex="-1"
  • Automated spam bots crawl the page and fill in every field they find, including hidden ones
  • When FormWit receives a submission where _gotcha has a value, it silently discards it as spam
  • Real visitors never see or interact with the field, so their submissions go through normally

The autocomplete="off" attribute prevents browsers from auto-filling the hidden field, which could cause false positives. Combined with FormWit's server-side rate limiting, this catches the vast majority of spam without requiring CAPTCHAs or JavaScript. For more details on spam prevention techniques, see the spam protection guide.

Step 4: Add AJAX submission (optional)

The basic HTML form works with a full page redirect. After submitting, the visitor is taken to a confirmation page. If you want a smoother experience where the form submits without a page reload, add a JavaScript partial.

Create a partial at layouts/partials/form-ajax.html:

<script>
document.addEventListener('DOMContentLoaded', function() {
  var form = document.querySelector('.contact-form');
  if (!form) return;

  form.addEventListener('submit', function(e) {
    e.preventDefault();

    var submitBtn = form.querySelector('button[type="submit"]');
    var originalText = submitBtn.textContent;
    submitBtn.textContent = 'Sending...';
    submitBtn.disabled = true;

    var data = new FormData(form);

    fetch(form.action, {
      method: 'POST',
      body: data
    })
    .then(function(response) {
      if (response.ok) {
        form.reset();
        var msg = document.createElement('p');
        msg.className = 'form-success';
        msg.textContent = 'Message sent successfully!';
        form.appendChild(msg);
      } else {
        var msg = document.createElement('p');
        msg.className = 'form-error';
        msg.textContent = 'Something went wrong. Please try again.';
        form.appendChild(msg);
      }
    })
    .catch(function() {
      var msg = document.createElement('p');
      msg.className = 'form-error';
      msg.textContent = 'Something went wrong. Please try again.';
      form.appendChild(msg);
    })
    .finally(function() {
      submitBtn.textContent = originalText;
      submitBtn.disabled = false;
    });
  });
});
</script>

Then include this partial in your contact layout or in your base template's footer. In Hugo, you include partials with the partial function:

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

Add this line at the bottom of your contact template (before {{ end }}) or in your site's footer partial so the script loads on the contact page. The script finds the form by its .contact-form class, intercepts the submit event, and sends the data with fetch instead of a full page navigation.

Hugo-specific details

Using Hugo partials for reusable forms

If you want to use the same contact form on multiple pages (for example, a dedicated contact page and a footer section on your homepage), extract the form HTML into a partial. Create layouts/partials/contact-form.html:

<form
  action="https://app.formwit.com/api/s/YOUR_FORM_ID"
  method="POST"
  class="contact-form"
>
  <input type="text" name="_gotcha"
    style="display:none" tabindex="-1" autocomplete="off" />

  <div class="form-field">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" required />
  </div>

  <div class="form-field">
    <label for="email">Email</label>
    <input type="email" id="email" name="email" required />
  </div>

  <div class="form-field">
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="5" required></textarea>
  </div>

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

Then include it anywhere in your templates:

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

Hugo partials are compiled at build time, so there's zero runtime cost. The form HTML is simply inlined wherever you call the partial.

Embedding forms with Hugo shortcodes

If you want content authors to embed a contact form directly within Markdown content (without touching layout files), create a Hugo shortcode. Create layouts/shortcodes/contact-form.html:

<form
  action="https://app.formwit.com/api/s/YOUR_FORM_ID"
  method="POST"
  class="contact-form"
>
  <input type="text" name="_gotcha"
    style="display:none" tabindex="-1" autocomplete="off" />

  <div class="form-field">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" required />
  </div>

  <div class="form-field">
    <label for="email">Email</label>
    <input type="email" id="email" name="email" required />
  </div>

  <div class="form-field">
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="5" required></textarea>
  </div>

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

Now any Markdown file can embed the form with the shortcode syntax:

---
title: "About Us"
---

We'd love to hear from you. Use the form below to get in touch.

{{< contact-form >}}

This is useful when non-technical content editors manage your Hugo site through a CMS like Forestry, Decap CMS (formerly Netlify CMS), or CloudCannon. They can drop the form into any page without editing layout files.

Working with Hugo themes

Most Hugo themes define their own layout structure. When adding a contact form, you have a few options depending on how your theme is set up:

  • Override a theme template: Copy the relevant layout file from themes/your-theme/layouts/ to your project's layouts/ directory and add the form HTML. Hugo's lookup order gives your project's layouts priority over theme layouts.
  • Use the shortcode approach: Create the shortcode as shown above. This works with any theme because shortcodes are processed independently of the layout system.
  • Create a custom layout: Add layout: "contact" to your content's front matter and create the corresponding template in layouts/_default/contact.html. This doesn't require modifying any theme files.

If your theme uses a CSS framework like Tailwind or Bootstrap, style the form fields to match by adding the appropriate classes to the form HTML in your partial or shortcode.

Using Hugo config for the form endpoint

Hardcoding the form endpoint URL in templates works fine, but if you want to make it configurable (especially useful for themes shared across multiple sites), you can store it in Hugo's site config. In config.toml:

[params]
  formwit_endpoint = "https://app.formwit.com/api/s/YOUR_FORM_ID"

Then reference it in your templates with:

<form action="{{ .Site.Params.formwit_endpoint }}" method="POST">

This lets you change the endpoint in one place without editing template files. It also makes the form endpoint easy to override per environment if you use Hugo's config directory structure (config/_default/, config/production/, etc.).

Adding a custom redirect

By default, FormWit handles the redirect after submission. If you want to send visitors to a specific thank-you page on your Hugo site, add a hidden redirect_to field:

<input type="hidden" name="redirect_to" value="https://yoursite.com/thank-you/" />

Create the thank-you page at content/thank-you.md:

---
title: "Thank You"
description: "Your message has been sent."
---

Thanks for reaching out! We'll get back to you within 24 hours.

This creates a static thank-you page at /thank-you/ that visitors are redirected to after submitting the form. If you're using the AJAX approach from Step 4, the redirect field is ignored since JavaScript handles the response inline.

Testing the form

Start the Hugo development server:

hugo server

Navigate to http://localhost:1313/contact/, fill out the form, and submit. You should see the submission appear in your FormWit dashboard and receive an email notification. Hugo's dev server supports live reload, so you can adjust the form template and see changes instantly without restarting the server.

When you're ready for production, run hugo to generate the static files in the public/ directory and deploy them to your hosting provider. The form works identically in development and production because submissions are processed entirely by the browser and FormWit. Hugo's build step doesn't affect form behavior.

Why not use a Hugo module or serverless function?

Some developers reach for serverless functions (via Netlify Functions or Cloudflare Workers) to handle form submissions from Hugo sites. While that works, it means:

  • Writing and maintaining server-side code for form validation and email sending
  • Managing API keys for an email provider like SendGrid or Mailgun
  • No built-in submission dashboard or storage
  • No built-in spam protection
  • Tying your Hugo site to a specific hosting platform

A form backend handles validation, storage, spam filtering, and email notifications out of the box. Your Hugo site stays fully static and deployment-agnostic. For a deeper comparison of these approaches, see the HTML contact form guide.

Summary

Hugo generates static HTML with no server runtime, so contact forms need an external backend to process submissions. With FormWit, you add a standard HTML form to a Hugo layout template (or partial, or shortcode), point the action attribute to your endpoint, and get email notifications, submission storage, and spam protection without any server-side code or Hugo modules. The same form works whether you use Hugo's default templates, a community theme, or a custom layout structure.

FormWit's free plan includes unlimited forms, 100 submissions per month, email notifications, and built-in spam protection. Create your free account and add a contact form to your Hugo site in minutes.

Related guides: HTML contact form · Jekyll contact form · Eleventy contact form · Spam protection · Contact form templates

Frequently asked questions

Does FormWit work with Hugo?

Yes. Hugo generates static HTML files, and FormWit receives form submissions via a standard POST request from the browser. No Hugo modules, no Go code, and no server-side runtime needed. Just add the form HTML to any Hugo template, partial, or shortcode.

Can I add forms to Hugo on GitHub Pages?

Yes. GitHub Pages serves static files, and the form submits directly to FormWit's external endpoint. There are no custom plugins or gems involved, so it works within GitHub Pages' safe mode restrictions. Push your Hugo source, let GitHub build it, and the form works immediately.

Do Hugo shortcodes work?

Yes. You can create a shortcode at layouts/shortcodes/contact-form.html containing the form HTML. Then content authors can embed the form in any Markdown file using Hugo's shortcode syntax. This is useful for non-technical editors who manage content through a CMS like Decap CMS or CloudCannon.

Want to skip the setup?

FormWit gives you a form endpoint in 60 seconds. Free plan, no credit card.

Create Free Form

Need a form fast?

Build one visually with our free HTML form generator — no coding required.

Try the Form Generator →

Add FormWit to your Hugo site

Add a contact form to your site in 30 seconds. No backend code required.

Try FormWit Free