All resources
Static site forms

Hugo Contact Form Without a Backend (2026)

Add a working contact form to any Hugo site — no server, no plugins. Drop in a copy-paste shortcode with honeypot spam protection and email alerts.

The ShipMyForm team

· 4 min read

To add a contact form to a Hugo site without a backend, point a normal HTML <form> at a hosted form endpoint and let it handle the submission. Hugo compiles your site to static files — there's no server runtime to receive the POST yourself — so a form backend is the piece that receives it, filters spam, and emails you. Here's the whole form:

html
<form action="https://shipmyform.com/f/YOUR_FORM_ID" method="POST">
  <label>
    Email
    <input type="email" name="email" required />
  </label>
  <label>
    Message
    <textarea name="message" required></textarea>
  </label>
  <button type="submit">Send message</button>
</form>

Replace YOUR_FORM_ID with the ID from your form backend and you have a working contact form. That's the five-minute version. The rest of this guide covers where this markup goes in a Hugo project, the thank-you page, spam, and doing it with fetch if you want to stay on the page.

Don't want to hand-write the markup? Generate it — honeypot and Turnstile included — with the free contact form generator, then paste the result into the shortcode below.

1. Create the form endpoint

You need a URL to send submissions to. With ShipMyForm, create a form and copy its endpoint — it looks like https://shipmyform.com/f/abc123. Every field's name attribute (email, message) becomes a field you receive in your inbox.

2. Drop the form in with a shortcode

Here's the Hugo-specific catch: if you paste a raw <form> straight into a markdown page, Hugo's renderer strips it — Goldmark ignores inline HTML unless you opt in. You can flip that switch in your config:

toml
# hugo.toml
[markup.goldmark.renderer]
  unsafe = true

But turning on unsafe for one form is a blunt instrument. The idiomatic Hugo move is a shortcode — a reusable snippet you can drop into any markdown page. Create the file:

html
<!-- layouts/shortcodes/contact-form.html -->
<form action="https://shipmyform.com/f/{{ .Site.Params.shipmyformID }}" method="POST">
  <label>
    Email
    <input type="email" name="email" required />
  </label>
  <label>
    Message
    <textarea name="message" required></textarea>
  </label>
  <button type="submit">Send message</button>
</form>

Put your form ID in your site config once, so it isn't hard-coded in the template:

toml
# hugo.toml
[params]
  shipmyformID = "YOUR_FORM_ID"

Now any content file can render the form with a single tag:

md
<!-- content/contact.md -->
---
title: "Contact"
---

Have a question? Send us a message.

{{< contact-form >}}

The form renders inside your normal page layout, styled by your theme — no config switch, no raw HTML in your content.

Prefer a dedicated page template?:

If your contact page isn't markdown-driven, put the same <form> straight into a layout like layouts/contact/single.html (or your theme's page template). Layout templates are plain HTML, so no shortcode or unsafe setting is needed there.

3. Show a thank-you page

By default the browser follows the response to a thank-you page. Point it wherever you like with a hidden _redirect field in the shortcode:

html
<!-- layouts/shortcodes/contact-form.html -->
<form action="https://shipmyform.com/f/{{ .Site.Params.shipmyformID }}" method="POST">
  <input type="hidden" name="_redirect" value="{{ .Site.BaseURL }}thanks/" />
  <input type="email" name="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">Send message</button>
</form>

Using {{ .Site.BaseURL }} keeps the redirect correct across local, staging, and production builds. Create content/thanks.md with a confirmation message and you're done — this works with zero JavaScript, which suits a static Hugo build.

4. Submit without leaving the page (optional)

If you'd rather show an inline success state, submit with fetch and ask for JSON back. Hugo ships no client JavaScript by default, so add a small script alongside the form in the shortcode:

html
<!-- layouts/shortcodes/contact-form.html -->
<form id="contact" action="https://shipmyform.com/f/{{ .Site.Params.shipmyformID }}" method="POST">
  <input type="email" name="email" required />
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
  <p id="status" hidden>Thanks — we'll be in touch.</p>
</form>

<script>
  const form = document.getElementById("contact");
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const res = await fetch(form.action, {
      method: "POST",
      body: new FormData(form),
      headers: { Accept: "application/json" },
    });
    if (res.ok) {
      form.reset();
      document.getElementById("status").hidden = false;
    }
  });
</script>

The Accept: application/json header is what makes the backend return { ok: true } instead of redirecting. For a page with several forms, move this into assets/js/ and pull it in with Hugo Pipes instead of inlining it.

5. Block spam

Static contact forms get scraped and hit by bots, but you can stop most of them without a CAPTCHA. The simplest, zero-friction defense is a honeypot — a hidden field real people never fill. If it's filled, the submission is a bot.

Add a honeypot field the backend knows to check:

html
<!-- visually hidden; keep it out of the tab order -->
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none" />

ShipMyForm checks the honeypot and rate-limits submissions automatically, and you can turn on Cloudflare Turnstile if a form gets targeted — no reCAPTCHA puzzles for your visitors.

Keep secrets out of the client:

Everything here is safe to ship in a static build — there are no API keys in the page. The form endpoint is public by design and only accepts submissions from your allowed domains.

6. Get notified

Submissions land in an inbox, and you can turn on an email notification so every message hits you the moment it arrives. From there, route to Slack, Google Sheets, or a webhook without touching your Hugo templates.

Why not a Hugo "backend"?

Unlike some frameworks, Hugo has no server mode to fall back on — it's a build-time generator, full stop. There's no API route to add, no adapter to install, and nothing to deploy beyond static files. That makes a hosted form backend the natural fit: your site stays a folder of HTML, and the backend owns spam filtering, storage, and email. If you're weighing the trade-off, read what a form backend is and when you need one.

Next steps

Frequently asked questions

Can you have a contact form on a Hugo site?
Yes. Hugo builds to static HTML, so you point a normal HTML form at a hosted form backend. The backend receives the submission, filters spam, and emails it to you — Hugo has no server-side runtime of its own, so this is the standard way to do it.
Where do you put a form in Hugo?
Either directly in a layout template, or — more reusably — in a shortcode you can drop into any markdown page with {{< contact-form >}}. Raw HTML in markdown is stripped unless you enable unsafe rendering, so a shortcode is the cleaner path.
How do I stop spam on a Hugo contact form?
Add a hidden honeypot field and let the backend score submissions, then optionally enable Cloudflare Turnstile. ShipMyForm checks the honeypot and rate-limits submissions automatically, with no reCAPTCHA puzzles for your visitors.

Related guides