SendBeam

Hugo setup

Connect SendBeam to Hugo: what you need, the steps, and the code.

View as Markdown

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

  1. 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.
  2. 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.
  3. 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.

← Back to the Hugo integration

Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.