Hugo contact forms
without serverless glue.
Keep your Hugo site static and point the generated form at Form Plume for email notifications, dashboard storage, spam filtering, file uploads, integrations, and signed webhooks.
<form action="{{ site.Params.formplumeEndpoint }}" method="POST">
<input type="hidden" name="source" value="hugo">
<input name="name" autocomplete="name" required>
<input name="email" type="email" autocomplete="email" required>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>The full guide
A hosted backend for Hugo partials.
Use Hugo params and partials to generate a plain form that works from any static host.
- 1Create your Form Plume accountStart free and give your Hugo form a hosted endpoint for email, submissions, spam filtering, uploads, integrations, and webhooks.Start free
- 2Create the form and copy the endpointCopy the endpoint URL from Form Plume. It looks like
https://api.formplume.com/f/your-slug - 3Connect it inside HugoPut the endpoint in Hugo params, render it in a contact form partial, build the site, and inspect the generated HTML before deploying. Send one test submission from the real Hugo app or preview environment and confirm it reaches Form Plume.
You can use this AI prompt to hand the platform-specific wiring to an assistant without accidentally creating a backend.
Hugo forms belong in partials
Hugo generates static HTML, so the contact form only needs a real action, method="POST", and named fields. Put the markup in a partial such as layouts/partials/contact-form.html so every page uses the same endpoint and field names.
<form action="{{ site.Params.formplumeEndpoint }}" method="POST">
<input type="hidden" name="source" value="hugo">
<input type="hidden" name="_redirect" value="/thanks/">
<label for="contact-name">Name</label>
<input id="contact-name" name="name" autocomplete="name" required>
<label for="contact-email">Email</label>
<input id="contact-email" type="email" name="email" autocomplete="email" required>
<label for="contact-message">Message</label>
<textarea id="contact-message" name="message" rows="5" required></textarea>
<button type="submit">Send message</button>
</form>Then render the partial from a page template.
{{ partial "contact-form.html" . }}Store the endpoint in params
Keep the endpoint out of repeated templates. Put it in hugo.toml, config/_default/params.toml, or an environment-specific config file.
[params]
formplumeEndpoint = "PASTE_YOUR_FORM_PLUME_ENDPOINT_HERE"After hugo builds the site, inspect public/contact/index.html and confirm the real endpoint replaced the placeholder.
Uploads
Hugo does not need a plugin for file uploads. The generated HTML just needs multipart encoding and a named file input.
<form
action="{{ site.Params.formplumeEndpoint }}"
method="POST"
enctype="multipart/form-data"
>
<input type="file" name="attachment" accept=".pdf,.png,.jpg">
<button type="submit">Send file</button>
</form>Form Plume stores attachments separately from email delivery so large files do not break notifications.
Deploy anywhere
This works on Cloudflare Pages, Netlify, Vercel, GitHub Pages, S3, or any host that serves Hugo output. The host does not need native form handling because the browser posts to Form Plume.
Test the deployed URL, not only a local preview. Redirect paths, custom domains, and spam signals are easiest to verify on the final public page.
Primary sources
FAQ
Hugo form questions
before deploy.
One line. Zero backend.
