# Newsletter signup on an Astro site

An Astro component that adds a subscriber to a SendBeam list straight from the browser, with the form ID in an environment variable and no server-side code.

Astro is built for content sites, and content sites want a newsletter. The friction is that a
statically built Astro site has no place to put a subscriber, so people either bolt on a
third-party embed with its own styles and script, or wire up an SSR endpoint and an API key to
talk to a mailing service. This guide takes a third route: a small Astro component whose
client script posts the signup as JSON to SendBeam's public form endpoint. The form ID is the only
configuration, it lives in an environment variable, and the same list can later be mailed with
campaigns or automations from SendBeam.

## Prerequisites

- An Astro project. Static output is fine; nothing here needs an adapter or SSR.
- A SendBeam workspace with a [list](https://sendbeam.io/docs/lists/creating) to add subscribers to. On the Free plan every list is double opt-in; on paid plans it is a per-list setting.
- A signup form in SendBeam pointing at that list (created in the first step below).
- Optional: a Cloudflare Turnstile widget if the form will sit on a busy page.

## Steps

1. **Create the signup form in SendBeam.** Under **Forms**, add a form
  with type *Signup* and choose the list under **Add to List**. Set the
  thank-you message; if the list is double opt-in, make it say "Check your inbox to confirm",
  because that message is exactly what the component will show. On the form's page, tick
  *First name* under Form Fields if you want to collect it, then copy the form ID from the
  **API Endpoint** line.
2. **Put the form ID in the environment.** Astro only exposes variables prefixed
  `PUBLIC_` to client code, and that is what you want here: the form ID is not a
  secret (it is visible to anyone who submits the form), it only identifies which form receives
  the post. Add the same variable in your host's dashboard for production builds.
  `# .env (PUBLIC_ variables are inlined into the client bundle by Astro)
  PUBLIC_SENDBEAM_SIGNUP_FORM_ID=00000000-0000-0000-0000-000000000000
  # Optional: only if the form has Turnstile enabled in SendBeam
  PUBLIC_TURNSTILE_SITE_KEY=`
3. **Create the component.** The frontmatter reads the form ID at build time and
  writes the endpoint onto the form as a `data-endpoint` attribute, so the client
  script stays generic. The script is an ordinary Astro ``: it is
  bundled, type-checked and de-duplicated per page. It records the render time for the timing
  check, posts the fields, and swaps the inputs for the message SendBeam returns. The Turnstile
  widget and its script are only emitted when a site key is configured.
  `---
  // src/components/NewsletterForm.astro
  const formId = import.meta.env.PUBLIC_SENDBEAM_SIGNUP_FORM_ID;
  const siteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
  const endpoint = `https://sendbeam.io/api/forms/${formId}`;
  ---
  
  
  
  Email
  
  
  
  First name (optional)
  
  
  
  
  
  Leave this empty
  
  
  {siteKey && }
  
  Subscribe
  
  
  
  {siteKey && }
  
  
  // Astro bundles this once per page, however many forms are on it.
  document.querySelectorAll('form.sb-signup').forEach((form) => {
  const status = form.querySelector('.sb-status')!;
  const button = form.querySelector('button[type="submit"]')!;
  
  form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const data = new FormData(form);
  const payload = {
  email: String(data.get('email') || ''),
  first_name: String(data.get('first_name') || ''),
  YOUR_FORM_CHECK_FIELD: String(data.get('YOUR_FORM_CHECK_FIELD') || ''),
  turnstile_token: String(data.get('cf-turnstile-response') || ''),
  };
  
  button.disabled = true;
  status.textContent = 'Subscribing…';
  try {
  const res = await fetch(form.dataset.endpoint!, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
  });
  const json = await res.json();
  if (res.ok && json.success) {
  form.querySelectorAll('label, button, .cf-turnstile').forEach((el) => ((el as HTMLElement).hidden = true));
  status.textContent = json.message;
  if (json.redirect_url) setTimeout(() => location.assign(json.redirect_url), 1500);
  } else {
  status.textContent = json.error || 'Something went wrong. Please try again.';
  (window as any).turnstile?.reset();
  }
  } catch {
  status.textContent = 'Could not reach the server. Please try again.';
  } finally {
  button.disabled = false;
  }
  });
  });
  
  
  
  .sb-signup { display: grid; gap: .75rem; max-width: 28rem; position: relative; }
  .sb-signup label { display: grid; gap: .25rem; font-size: .9rem; }
  .sb-signup input { padding: .6rem .75rem; border: 1px solid #cbd5e1; border-radius: .5rem; font: inherit; }
  .sb-signup button { padding: .65rem 1rem; border: 0; border-radius: .5rem; background: #0b1020; color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
  .sb-signup button:disabled { opacity: .6; cursor: default; }
  .sb-status:empty { display: none; }
  `
4. **Use it on a page.** Drop the component wherever the signup belongs. If you place
  it in the layout footer, every page gets it and the script still runs once.
  `---
  // src/pages/index.astro
  import Layout from '../layouts/Layout.astro';
  import NewsletterForm from '../components/NewsletterForm.astro';
  ---
  
  My site
  
  Get the newsletter
  
  
  `
5. **Allow the requests in your CSP, if you have one.** The component makes one
  request to `sendbeam.io` and, with Turnstile, loads a script and an iframe from
  `challenges.cloudflare.com`. On Cloudflare Pages that is a `_headers`
  file; on Netlify a `[[headers]]` block in `netlify.toml`; on Vercel the
  `headers` key in `vercel.json`.
  `# public/_headers (Cloudflare Pages) — only needed if you send a CSP header
  /*
  Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; connect-src 'self' https://sendbeam.io https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:`
6. **Set the protection on the form.** In SendBeam, open the form's
  **Protection** section, add your site's origin under *Allowed sites*, and
  paste the Turnstile keys if you are using it. Then deploy.

> The
> [astro-cloudflare-pages starter](https://github.com/sendbeam-io/sendbeam-starters/tree/main/astro-cloudflare-pages)
> is this component plus a contact form, a `.env.example` and the Pages headers file,
> ready to clone. Framework notes are on the [Astro integration page](https://sendbeam.io/integrations/astro).
> 

## Test it

Run `npm run dev` and submit the form with an address you control. If you have already
set allowed sites, the local origin will be refused with a 403 and the component will display
SendBeam's error text, which is the allow-list doing its job: either add
`http://localhost:4321` temporarily or test on the deployed site. On success the inputs
disappear and the thank-you message appears. Then check three things in SendBeam: the address is
under **Contacts** with source *form*; it is a member of the list (pending,
if the list is double opt-in, until the confirmation link is clicked); and, if you enabled
*Email me about new subscribers* on the form, you received a note.

To exercise the endpoint without the browser:

```
curl -s -X POST https://sendbeam.io/api/forms/YOUR_FORM_ID \
  -H "Content-Type: application/json" \
  -H "Origin: https://www.example.com" \
  -d '{"email":"you@example.com","first_name":"Test"}'
# → {"success":true,"message":"Thanks for subscribing!","redirect_url":null}
```

Submitting the same address twice is safe. The contact is updated rather than duplicated, and if
the list is double opt-in, at most one confirmation email goes to an address every ten minutes,
however many times the form is sent. Each confirmation email counts towards the workspace's
monthly allowance.

## Dealing with spam

Signup forms attract a different kind of abuse from contact forms: address-stuffing, where a bot
subscribes strangers, and list-bombing, where one address is submitted to thousands of forms.
Double opt-in is the real defence against both, because nothing is mailed to an address that
never confirmed, and SendBeam applies its normal layers on top: per-visitor rate limits and a
daily cap per form, the *Allowed sites* origin check, the automated-submission checks,
and Turnstile when a secret is set.
Addresses that previously bounced or complained are
accepted with the normal thank-you and then ignored, so a public form can never be used to
re-subscribe someone who left. For an Astro site with real traffic the recommendation is simple:
allowed sites, Turnstile, and a double opt-in list.

## Why not a Formspree or Web3Forms endpoint?

Both are good at what they do. **Web3Forms** in particular is generous and works the
same way as this guide, with an access key in the page instead of a form ID. The difference is
what happens after the POST. A forms service emails you the submission and stops. You then need
somewhere to keep the subscribers, a way to confirm them, and a way to send the newsletter, which
means a second product and a way to move addresses between the two. With SendBeam the POST
creates the contact, joins the list, runs the confirmation flow and fires any automation you
have set up, and the campaign editor is in the same place.

The trade-off is that SendBeam is priced by contacts across your account (see
[plans](https://sendbeam.io/docs/admin/billing)), and on the Free plan every list is double opt-in whether
you want it or not. If you only want a message in your inbox and nothing else, a forms-only
service is the smaller tool.

## Next steps

- Understand what the visitor sees after subscribing, and what to say in the thank-you message: [Double opt-in on a static site](https://sendbeam.io/docs/guides/double-opt-in-static-site).
- Add a contact form to the same site: [Contact form backend for Cloudflare Pages](https://sendbeam.io/docs/guides/contact-form-cloudflare-pages).
- Send a welcome email automatically: create an [automation](https://sendbeam.io/docs/automations/triggers) on the *list joined* trigger.
- Capture more than a name with [custom fields](https://sendbeam.io/docs/forms/creating#choosing-fields), which the component can send as extra keys once they are declared on the form.

## Starter kits

This component, wired up in a site you can deploy, is in the starter kits. The Astro one is
this guide as a running project; the others do the same thing in their own templating:

- Astro [Astro on Cloudflare Pages No API key needed · Cloudflare Pages Signup and contact components, form IDs in PUBLIC_ environment variables, and a _headers file that lets Turnstile load. sendbeam-starters/astro-cloudflare-pages](https://github.com/sendbeam-io/sendbeam-starters/tree/main/astro-cloudflare-pages)
- HTML and vanilla JS [Plain HTML No API key needed · Any static host One page, one script, no build step: paste the files onto any host and both forms work. sendbeam-starters/plain-html](https://github.com/sendbeam-io/sendbeam-starters/tree/main/plain-html)
- Next.js (App Router) [Next.js on Vercel No API key needed · Vercel Client components that post straight to the form endpoint, so no route handler and no secret sit in the middle. sendbeam-starters/nextjs-vercel](https://github.com/sendbeam-io/sendbeam-starters/tree/main/nextjs-vercel)
- Eleventy [Eleventy on Netlify No API key needed · Netlify Nunjucks includes and a global data file, with a netlify.toml that sets the Content-Security-Policy for you. sendbeam-starters/eleventy-netlify](https://github.com/sendbeam-io/sendbeam-starters/tree/main/eleventy-netlify)
- Hugo [Hugo No API key needed · Any static host Two partials and one script in static/, with the form IDs read from site params in hugo.toml. sendbeam-starters/hugo](https://github.com/sendbeam-io/sendbeam-starters/tree/main/hugo)

Every starter covers the same five things — newsletter signup, contact form, double opt-in,
unsubscribe and spam protection — reads its form IDs from environment variables or site config,
and is MIT licensed. Browse them all in
[sendbeam-starters](https://github.com/sendbeam-io/sendbeam-starters).

---
Source: https://sendbeam.io/docs/guides/newsletter-signup-astro
