Hugo setup
Connect SendBeam to Hugo: what you need, the steps, and the code.
Via API
Hugo produces HTML and nothing else, which is why it is fast and why it cannot handle a form on its own. Two partials and one small script give a Hugo site a working signup and contact form.
Before you start
- An account with Hugo
- A SendBeam API key, or a form ID
- Somewhere to paste a snippet
Set it up
- Create the forms in SendBeam. Forms → New form. One Signup form (pick the list it feeds) and one Contact form (set the address messages go to). Copy each form ID from the form page.
- Put the IDs in your environment. The snippets read them from environment variables so the same code serves every site. Form IDs are meant to be public; protection is on the form, not in the ID.
- Add the snippets and deploy. Paste the components below, deploy to your host, then submit each form once with an address you control and check Contacts and your inbox.
Code
Site params
hugo.toml
# hugo.toml
[params.sendbeam]
signupFormId = "YOUR_SIGNUP_FORM_ID"
contactFormId = "YOUR_CONTACT_FORM_ID"
turnstileSiteKey = "" # optional
Partials
layouts/partials/*.html
{{/* layouts/partials/newsletter-form.html */}}
<form data-sendbeam data-endpoint="https://sendbeam.io/api/forms/{{ .Site.Params.sendbeam.signupFormId }}">
<label for="nl-email">Email</label>
<input id="nl-email" name="email" type="email" required autocomplete="email">
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input name="YOUR_FORM_CHECK_FIELD" type="text" tabindex="-1" autocomplete="off">
</div>
<button type="submit">Subscribe</button>
</form>
<p data-status role="status" aria-live="polite" hidden></p>
{{/* layouts/partials/contact-form.html */}}
<form data-sendbeam data-endpoint="https://sendbeam.io/api/forms/{{ .Site.Params.sendbeam.contactFormId }}">
<label for="c-name">Name</label>
<input id="c-name" name="name" type="text" required>
<label for="c-email">Email</label>
<input id="c-email" name="email" type="email" required>
<label for="c-message">Message</label>
<textarea id="c-message" name="message" rows="5" required></textarea>
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input name="YOUR_FORM_CHECK_FIELD" type="text" tabindex="-1" autocomplete="off">
</div>
{{ with .Site.Params.sendbeam.turnstileSiteKey }}
<div class="cf-turnstile" data-sitekey="{{ . }}" data-size="flexible"></div>
{{ end }}
<button type="submit">Send message</button>
</form>
<p data-status role="status" aria-live="polite" hidden></p>
{{/* In your baseof.html, before </body>: */}}
<script src="{{ "js/sendbeam-forms.js" | relURL }}" defer></script>
{{ with .Site.Params.sendbeam.turnstileSiteKey }}<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>{{ end }}
Call them with {{ partial “newsletter-form.html” . }} and {{ partial “contact-form.html” . }} from any layout or shortcode.
The shared script
static/js/sendbeam-forms.js
// sendbeam-forms.js — posts any <form data-sendbeam> as JSON to SendBeam.
document.querySelectorAll('form[data-sendbeam]').forEach((form) => {
const endpoint = form.dataset.endpoint; // https://sendbeam.io/api/forms/<form-id>
const status = form.querySelector('[data-status]');
const button = form.querySelector('button[type="submit"]');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(form).entries());
const token = form.querySelector('[name="cf-turnstile-response"]');
if (token) data.turnstile_token = token.value;
button.disabled = true;
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await res.json();
if (res.ok && json.success) {
form.hidden = true;
status.hidden = false;
status.textContent = json.message;
if (json.redirect_url) setTimeout(() => location.assign(json.redirect_url), 1500);
} else {
status.hidden = false;
status.textContent = json.error || 'Something went wrong. Please try again.';
if (window.turnstile) window.turnstile.reset();
}
} catch {
status.hidden = false;
status.textContent = 'Could not reach the server. Please try again.';
} finally {
button.disabled = false;
}
});
});
Things to know
- Hugo’s static/ directory is copied verbatim, so the script needs no pipeline step; if you use Hugo Pipes, put it under assets/ and fingerprint it instead.
- Deploy anywhere static files go: Cloudflare Pages, Netlify, GitHub Pages or an S3 bucket all work because nothing runs server-side.
- For a Content-Security-Policy, allow https://sendbeam.io in connect-src, and https://challenges.cloudflare.com in script-src, frame-src and connect-src when Turnstile is on.
- The starter below is a working Hugo site with both partials, the script and the site params already in place.
Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.