# Connect Hugo to SendBeam

How to connect Hugo to SendBeam: what you need, the setup 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

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](https://sendbeam.io) in connect-src, and [https://challenges.cloudflare.com](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.

[← Hugo email marketing integration](https://sendbeam.io/integrations/hugo)

---
Source: https://sendbeam.io/docs/integrations/hugo
