Eleventy setup
Connect SendBeam to Eleventy: what you need, the steps, and the code.
Via API
Eleventy builds plain HTML, so form handling has to live somewhere else. With SendBeam it lives at a public endpoint per form: an include renders it, a small script posts it, nothing is hard-coded.
Before you start
- An account with Eleventy
- 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
Global data
src/_data/sendbeam.js
// src/_data/sendbeam.js — read at build time, so the IDs are baked into the HTML.
export default function () {
return {
signupFormId: process.env.SENDBEAM_SIGNUP_FORM_ID || '',
contactFormId: process.env.SENDBEAM_CONTACT_FORM_ID || '',
turnstileSiteKey: process.env.TURNSTILE_SITE_KEY || '',
};
}
Includes
src/_includes/*.njk
{# src/_includes/newsletter-form.njk #}
<form data-sendbeam data-endpoint="https://sendbeam.io/api/forms/{{ 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>
{# src/_includes/contact-form.njk #}
<form data-sendbeam data-endpoint="https://sendbeam.io/api/forms/{{ 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>
{% if sendbeam.turnstileSiteKey %}
<div class="cf-turnstile" data-sitekey="{{ sendbeam.turnstileSiteKey }}" data-size="flexible"></div>
{% endif %}
<button type="submit">Send message</button>
</form>
<p data-status role="status" aria-live="polite" hidden></p>
{# In your layout, before </body> (copy src/js/sendbeam-forms.js through with addPassthroughCopy): #}
<script src="/js/sendbeam-forms.js" defer></script>
{% if sendbeam.turnstileSiteKey %}<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>{% endif %}
Use {% include “newsletter-form.njk” %} wherever the form should appear. Liquid or WebC templates work the same way with their own syntax.
The shared script
src/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;
}
});
});
Register it with eleventyConfig.addPassthroughCopy({ “src/js”: “js” }).
Things to know
- Set SENDBEAM_SIGNUP_FORM_ID and SENDBEAM_CONTACT_FORM_ID in your host’s build environment (Netlify: Site configuration → Environment variables). They are read once, during the build.
- Eleventy 3 uses ESM by default; for a CommonJS project write module.exports = function () { … } in the data file instead.
- The starter below is set up for Netlify with a netlify.toml that also sends a Content-Security-Policy header.
Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.