← Blog

Astro form submissions without an SSR adapter

Collect Astro form submissions with static HTML or client fetch — no SSR adapter, exact domain allow-listing, hosted endpoint.

If your Astro site is static, the simplest way to handle form submissions is to post HTML or JSON to a hosted endpoint instead of adding an SSR adapter just for email capture. Astro renders the page; the endpoint accepts the POST, stores the email, and keeps bots out.

Astro ships fast marketing pages, docs, and launch sites as static HTML by default. That is great for performance — and awkward only if you assume the same project must also own the intake backend. HTML and small fetch-based flows both work without changing that static default.

Static Astro pages still need a form backend

A default Astro build is files on a CDN or object store. Relative form actions that assume a same-origin PHP script, a homemade Node route, or "the host will figure it out" fail unless you add that backend yourself. You still need an HTTPS endpoint that accepts form or JSON bodies, checks the caller, and keeps a list you can export.

Astro API routes vs a hosted intake endpoint

  • SSR adapter + API routes / server endpoints. Full control on your origin — and you own rate limits, domain policy, spam controls, storage, and exports for the life of the form.
  • Host form products or heavy ESP embeds. Fast until branding, data ownership, or leaving that host becomes painful.
  • Simple Signups. A campaign-scoped subscribe API. Keep the Astro site static (no SSR adapter required for the happy path). The form posts to the Simple Signups origin with your public campaign id. That is intake — not a full ESP or a replacement for app APIs you already run.

No Astro SDK. If you only need email (and optional name/tags/metadata) into a campaign you control, point the markup at the hosted endpoint and ship.

Drop-in HTML form in an .astro component

Put the form in any component or page. Set action to the Simple Signups subscribe URL, include the public campaignId (pub_...), email, and an empty honeypot hp. Optional firstName / lastName / tags work the same way as in the quickstart.

Astro component (HTML form POST)
---
// src/components/SignupForm.astro
// No client script required. Replace campaign id after dashboard create.
---
<form action="https://simple-signups.com/api/subscribe" method="POST">
<input type="hidden" name="campaignId" value="pub_your_campaign_id" />
<label>
Email
<input type="email" name="email" required placeholder="you@example.com" />
</label>
<input
type="text"
name="hp"
tabindex="-1"
autocomplete="off"
style="position:absolute;left:-9999px"
aria-hidden="true"
/>
<button type="submit">Subscribe</button>
</form>

Import the component on a page as usual (import SignupForm from '../components/SignupForm.astro'). Zero client JS required for a classic full-page POST.

Client fetch JSON variant

Prefer staying on the page and handling success/error in the UI? POST JSON from a small browser script. Same endpoint and fields; set content-type: application/json. Handy next to Astro islands or a plain module script — still no SSR adapter on the critical path.

Client fetch (JSON)
// e.g. public/signup.js — load with <script type="module" src="/signup.js">
const endpoint = 'https://simple-signups.com/api/subscribe';
export async function submitSignup(email) {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
campaignId: 'pub_your_campaign_id',
email,
// optional: firstName, lastName, tags: ['newsletter'], metadata: { source: 'astro' }
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error ?? res.statusText);
}
return res.json();
}

Field reference and more language samples live under docs examples. Caps for tags, metadata, and optional names are documented with the endpoint — keep blog copy at the pattern level.

When Astro server endpoints still make sense

Use Astro server endpoints when the form must join a workflow your own app already owns — for example, authenticated product actions, private third-party API calls, or app-specific database writes. For marketing, contact, and waitlist forms, a hosted intake endpoint is usually the smaller surface.

Domain allow-list for your production Astro URL

Each campaign stores an exact list of allowed hostnames. On subscribe, the Worker checks browser Origin (or Referer) against that list. Matching is exact after normalization — no wildcards, and apex does not automatically include www.

Add every host you actually embed from, for example:

  • Production custom domain — www.example.com and, if you serve the bare apex, example.com as a separate entry
  • Preview or staging hosts you test from (each hostname explicitly)
  • Local dev only if you truly submit from it — e.g. localhost:4321 — otherwise smoke-test with cURL against the API

Empty allow-lists deny third-party embeds. Cross-site posts from your Astro deploy always need an explicit entry. See anti-abuse & domains.

What this does not replace

Simple Signups stores subscribers per campaign with spam controls and export. It is not Mailchimp, a CRM, or your Astro SSR layer for authenticated app traffic. Keep server routes for product logic; point the marketing or contact form at a dedicated campaign.

Related reading