splitforms.com
HUGO · CONTACT FORM

Contact form for Hugo websites

Hugo builds blazing-fast static sites — but ships zero backend. Drop a partial or shortcode in your theme, pull the access key from site params, and you have a working contact form without spinning up a Cloud Run service or wiring Netlify Functions. Pure HTML, zero JavaScript, full spam protection.

1,000 free submissions every month.·No credit card.
contact.htmlhtml20 lines
01{{/*
02 layouts/partials/contact-form.html
03 Use in any template with: {{ partial "contact-form.html" . }}
04 Set splitformsKey in your site config (hugo.toml):
05 [params]
06 splitformsKey = "YOUR_ACCESS_KEY"
07*/}}
08
09<form action="https://splitforms.com/api/submit" method="POST">
10 <input type="hidden" name="access_key" value="{{ .Site.Params.splitformsKey }}" />
11 <input type="hidden" name="redirect" value="{{ "thanks/" | absURL }}" />
12
13 <input type="text" name="name" placeholder="Name" required />
14 <input type="email" name="email" placeholder="Email" required />
15 <textarea name="message" placeholder="Message" required></textarea>
16
17 <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />
18
19 <button type="submit">Send</button>
20</form>
1,000
submissions / mo, free
14ms
median latency, edge
0
lines of backend code
17+
frameworks supported
✶ Live preview

What your Hugo contact form actually looks like.

Drop-in form backend with spam filtering, signed webhooks, and a real submissions dashboard. The same code in this preview is what you copy into your Hugo project — no SDK, no plugin, no PHP.

  • 1,000 submissions per month, free forever
  • Honeypot + AI spam classifier on every plan
  • Signed webhooks to Slack, Discord, your server
Hugo contact form on Splitforms — drop-in form backend with spam filtering and webhooks
§ 01Setup3 steps · 60 seconds · zero config

Ship a Hugo contact form without a backend.

No SDK, no PHP, no plugin. Your form posts standard FormData to one URL — submissions land in your inbox.

STEP 01GENERATE

Get your free access key

Verify your email and your access key is generated instantly. Free for 1,000 submissions per month, forever.

Create your form

By signing up, you agree to our terms and privacy policy.

STEP 02EMBED

Drop in the Hugo code

Copy the Hugo snippet on the right and paste it into your project. Replace YOUR_ACCESS_KEY with the key from step 1.

snippethtml
{{/*
…
STEP 03RECEIVE

Submissions land in your inbox

Hits your dashboard and email in seconds. Forward to Slack, Discord, Sheets, Notion, or any signed webhook URL.

inbox · 1 newjust now
FROM contact@yoursite.com
New Hugo form submission
Maya Iyer maya@studio71.co
Loved the new pricing page — quick question about the 4-year plan. Are usage limits per project or account-wide?
§ 02Live demosandboxed · no key required · no submission sent

Try it now — no signup, no key.

This is a styled HTML preview of what your Hugo form will look like. Submitting opens a confirmation, no real request is sent.

preview · hugolocalhost:3000
✦ what just happened

Your Hugo form posts FormData to /api/submit. Splitforms validates the access key, runs the spam classifier, and forwards the parsed submission to your inbox plus the dashboard.

  • 14ms median round-trip from the edge.
  • Honeypot + classifier, no CAPTCHA.
  • Per-domain key locking out of the box.
REQUEST · POST /api/submit
{
  "access_key": "sk_live_4f9a_••••",
  "name":       "Maya Iyer",
  "email":      "maya@studio71.co",
  "message":    "…"
}
← 200 OK · { "success": true } · 14ms
§ 03Best practices5 rules · production-tested

How to ship this without regrets.

Five rules that make the difference between a form that works in the demo and a form that survives launch traffic.

  1. 01

    Define the access key in `hugo.toml` under `[params]` — don't commit it to a public repo. For private repos, this is fine. For public repos, override via env var: `hugo --param splitformsKey=$SPLITFORMS_KEY`.

  2. 02

    Build the form as a shortcode (`{{< contact-form >}}`) rather than a partial. Shortcodes are usable from Markdown content; partials only work in templates.

  3. 03

    Add a /thanks page (`content/thanks/_index.md`) with your normal layout — splitforms's redirect target needs to exist and serve a real page.

  4. 04

    Lock the access key to your production domain in the splitforms dashboard. Hugo's `--baseURL` flag during local dev means the form posts from `localhost:1313` — add that to allowed domains for testing or use a separate dev key.

  5. 05

    If your theme uses Hugo Pipes for asset processing, add `<noscript>` styling to the form so it looks reasonable when JS is disabled — splitforms's pure-HTML pattern works without JS, but your theme might style buttons via JS-loaded CSS.

§ 04Common gotchas in Hugo6 edge cases worth knowing

What bites people who skip the docs.

Worth a 60-second skim before you ship to production. Each one has caused a Hugo support ticket at least once.

⚠ gotcha

Site.Params lookup is case-sensitive in some Hugo versions

If you set splitformsKey in hugo.toml under [params] but reference it as {{ .Site.Params.SplitformsKey }} in the template, recent Hugo versions still resolve it — but Hugo 0.110 and earlier don't. Use the exact case from your config file.

⚠ gotcha

Markdown content stripping eats inline form HTML

If you put a <form> directly in a Markdown content file, Goldmark's HTML sanitizer strips it. Either set markup.goldmark.renderer.unsafe = true in your config, or wrap the form in a shortcode (recommended): {{< contact-form >}}.

⚠ gotcha

absURL filter on the redirect URL adds a trailing slash you don't want

{{ "thanks/" | absURL }} produces https://yoursite.com/thanks/. Hugo's URL filter normalizes trailing slashes per your uglyURLs config — if your /thanks page lives at /thanks.html (uglyURLs=true), the redirect 404s. Hardcode the URL or set relURL consistently.

⚠ gotcha

Hugo modules / theme overrides require partial in your project, not the theme

If you copy contact-form.html into a vendored theme's layouts/partials/, your changes get overwritten on the next theme update. Always put custom partials in your project's own layouts/partials/ — Hugo's lookup chain prefers project over theme automatically.

⚠ gotcha

Hugo's `safeHTML` is needed when rendering the access key as raw output

If your splitforms key contains an underscore (it does — keys are sk_live_...), Hugo's HTML escaping is fine for input values, but if you echo it into JS contexts you may need safeJS. Sticking to <input value="…"> works as-is.

⚠ gotcha

Hugo's --minify collapses whitespace inside the form's textarea default

If you build with hugo --minify, the HTML minifier strips whitespace and newlines aggressively — including the contents between <textarea>…</textarea> tags. A textarea pre-filled with \n\nWrite your message here… collapses to a single line, breaking the visual hint. The minifier respects <pre> but not <textarea> by default. Either avoid pre-filled textarea content (use a placeholder attribute instead — that's what placeholders are for) or disable minification of HTML via minify.tdewolff.html.keepWhitespace = true in your config.

§ 04bNative Hugo forms…and where they break down

How Hugo handles forms without splitforms.

The shape of the problem before splitforms enters the picture — and the gap it fills for Hugo specifically.

Hugo is a static site generator — there's no runtime, no /server/api, no way to handle a form POST without an external service. The native paths are: (a) a Cloudflare Worker / Lambda / Cloud Run service handling POST /contact and emailing you (~4 hours of setup, ongoing operation), (b) Netlify Forms (Hugo-on-Netlify only, 100/mo free), or (c) a third-party form backend like Formspree, Basin, or Web3Forms. Hugo's templating shines for the form's HTML — {{ partial }}, {{ .Site.Params }} for the access key, {{< shortcode >}} for Markdown reuse — but the delivery layer is always external. Splitforms is the simplest external option that doesn't require a Cloud Run service.

§ 04cAlternative integration patterns2 ways to wire it

Two ways to ship splitforms on Hugo.

Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.

PATTERN A

Pattern A — partial in `layouts/partials/contact-form.html`

Reusable partial called from any template with {{ partial "contact-form.html" . }}. Pulls the access key from Site.Params.splitformsKey. Project-level partials override theme partials automatically.

pattern-a.htmlhtml8 lines
01{{/* layouts/partials/contact-form.html */}}
02<form action="https://splitforms.com/api/submit" method="POST">
03 <input type="hidden" name="access_key" value="{{ .Site.Params.splitformsKey }}" />
04 <input type="hidden" name="redirect" value="{{ "thanks/" | absURL }}" />
05 <input name="email" type="email" required />
06 <textarea name="message" required></textarea>
07 <button type="submit">Send</button>
08</form>
PATTERN B

Pattern B — shortcode usable from Markdown content

Save as layouts/shortcodes/contact-form.html. Now writers can drop {{< contact-form >}} (with optional redirect="…" arg) directly into any .md file. Eliminates the need to switch to template editing for one-off forms.

pattern-b.htmlhtml9 lines
01{{/* layouts/shortcodes/contact-form.html */}}
02{{- $redirect := .Get "redirect" | default "/thanks/" -}}
03<form action="https://splitforms.com/api/submit" method="POST">
04 <input type="hidden" name="access_key" value="{{ .Site.Params.splitformsKey }}" />
05 <input type="hidden" name="redirect" value="{{ $redirect | absURL }}" />
06 <input name="email" type="email" required />
07 <textarea name="message" required></textarea>
08 <button type="submit">Send</button>
09</form>
§ 04dDeployment notes for Hugohosting · env vars · CSP

Shipping Hugo + splitforms to production.

Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.

Hugo deploys to any static host: Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3, Surge, plain Apache/nginx. The form posts cross-origin so the host is irrelevant. Set splitformsKey in hugo.toml under [params] for private repos; for public repos, build with hugo --param splitformsKey=$SPLITFORMS_KEY and read it from CI env. Hugo's local dev server runs on localhost:1313 — add that to splitforms allowed-domains for testing or use a separate dev key. hugo --minify compresses HTML but doesn't touch attribute values — the form works under minification.

§ 05Comparisonvs native hugo

splitforms vs native hugo.

What you get for free vs what you build, pay for, or do without.

FeatureNative Hugosplitforms
Setup timeSpin up Cloud Run / Lambda + email60 seconds, one partial
JavaScript shipped0 KB0 KB (pure HTML)
Spam filterDIYBuilt-in honeypot + classifier
Hosting costServer (~$5/mo always-on)Free (CDN + splitforms)
Submissions storageDatabase + authDashboard included
Theme compatibilityTheme-specific configsWorks with every Hugo theme
§ 06Alternative patternhtml · 30 lines
ALTERNATIVE

Shortcode variant — usable from Markdown content

If you want to drop the form into individual blog posts or content pages (not just template files), build it as a shortcode. Then write `{{< contact-form >}}` in any `.md` file.

alternative.htmlhtml30 lines
01{{/*
02 layouts/shortcodes/contact-form.html
03 Use in any Markdown content file: {{< contact-form >}}
04 Or with a custom redirect: {{< contact-form redirect="/sales-thanks/" >}}
05*/}}
06
07{{- $redirect := .Get "redirect" | default "/thanks/" -}}
08
09<form action="https://splitforms.com/api/submit" method="POST" class="contact-form">
10 <input type="hidden" name="access_key" value="{{ .Site.Params.splitformsKey }}" />
11 <input type="hidden" name="redirect" value="{{ $redirect | absURL }}" />
12 <input type="hidden" name="form-name" value="{{ .Page.Title | default "contact" }}" />
13
14 <label>
15 Name
16 <input type="text" name="name" required />
17 </label>
18 <label>
19 Email
20 <input type="email" name="email" required />
21 </label>
22 <label>
23 Message
24 <textarea name="message" required></textarea>
25 </label>
26
27 <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />
28
29 <button type="submit">Send</button>
30</form>
§ 07Questions6 answered

Things developers ask before they integrate.

Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.

01How do I add a contact form to a Hugo site?
Save the snippet above as layouts/partials/contact-form.html in your project (NOT in the vendored theme). Add splitformsKey = "sk_live_…" to [params] in hugo.toml. Then call {{ partial "contact-form.html" . }} from any template, or use the shortcode variant for Markdown content.
02Does splitforms work with Hugo's static-site output?
Yes — that's the default Hugo mode. The form is pure HTML, posts directly to splitforms.com from the browser, and doesn't need any Hugo server feature. Works with hugo --minify and any deployment target.
03How do I handle form errors in Hugo?
Hugo doesn't render dynamic responses, so error UI is split between two paths. (1) Pure-HTML pattern: splitforms returns a generic error page on failure — customize it via splitforms dashboard → Settings → Error Page. (2) Hybrid: add a small inline <script> that intercepts submit, calls fetch, and renders inline errors (see the AJAX page).
04Can I use splitforms with Hugo's PaperMod, Ananke, Doks, or other themes?
Yes — every Hugo theme. The partial lives in your project's layouts directory, which Hugo's lookup order prefers over the theme. No theme modifications required.
05How do I customize the success / redirect behavior?
The hidden redirect field controls it. Default is /thanks/. Use the shortcode variant to override per-page: {{< contact-form redirect="/demo-thanks/" >}}. Build the matching content pages so they don't 404.
06Does the form survive Hugo's build pipeline (minify, fingerprint, etc.)?
Yes. The form is plain HTML — Hugo's HTML minifier compresses whitespace but doesn't touch attributes. Asset fingerprinting only affects linked CSS/JS files, not inline form markup.
✻ ✻ ✻

Ship your Hugo contact form in 60 seconds.

1,000 free submissions per month. No credit card. Lock the access key to your domains, paste the snippet, watch submissions land in your inbox.

Get free access key →Read the docs
v0.1 · founders pricing locked in · early access open