Receive form submissions in Astro
Collect Astro form submissions without an SSR adapter — static HTML or client fetch to a hosted intake endpoint, exact domain allow-list.
Astro ships fast marketing pages, docs, and launch sites as static HTML by default. That is great for performance — and awkward the first time you need a contact or newsletter form. The page can render the inputs; something else has to accept the POST, store the email, and keep bots out.
If your Astro site is static, the easiest path is to post the form to a hosted endpoint instead of adding an SSR adapter just for email capture. HTML and small fetch-based flows both work.
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.
---// 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.
// 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.
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.comand, if you serve the bare apex,example.comas 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
- Contact forms use case — product-shaped names + email flow (also useful next to newsletter signups).
- HTML form submissions without PHP — stack-agnostic static form pattern.
- Waitlist on a static website and Collect signups from Cloudflare Pages — same intake idea on other hosts.
- Docs examples, quickstart, and anti-abuse & domains.