Docs
Theme
Start free
  1. Before you start
  2. The form
  3. Optional: submit with fetch
  4. Check it

Astro

Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, tell us.

Send an Astro contact form to FormParcel. A plain form posts to your address and lands on your thank-you page, with an optional script that submits with fetch.

Before you start

You need a form address from the dashboard. It looks like https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE, and your own ID replaces the last part. If you have not made one yet, start with the quickstart. The form needs no server, so it works on a fully static Astro site.

The form

Astro renders HTML, so the form is ordinary HTML that posts to FormParcel. Add a thank-you page to your site, and point _next at it.

Example form address. Use yours from the dashboard.

src/pages/contact.astro
---
// src/pages/contact.astro
const endpoint = "https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE";
---
<form action={endpoint} method="POST" onsubmit="if(!window.FormParcel){event.preventDefault();var f=this;f.setAttribute('data-fp-wait','1');setTimeout(function(){if(f.getAttribute('data-fp-wait')){f.removeAttribute('data-fp-wait');HTMLFormElement.prototype.submit.call(f)}},10000)}">
  <!-- Spam trap: people never see it, bots fill it in. Keep it hidden. -->
  <div aria-hidden="true" style="position:absolute;left:-10000px;">
    <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" />
  </div>

  <!-- Send visitors to a page on your own site after they submit. -->
  <input type="hidden" name="_next" value="https://your-site.example/thanks" />

  <label for="name">Name</label>
  <input id="name" name="name" required />
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>
  <button type="submit">Send</button>
</form>
<!-- FormParcel bot check: no cookies, no widget. Without it, submissions wait in your Spam folder. -->
<script is:inline src="https://f.formparcel.live/f/formparcel.js" async></script>
  • _next must be a page on the client’s own site, and the site’s domain must be set in FormParcel. Otherwise the visitor sees a short FormParcel confirmation page. The trust rule is on special fields.
  • Keep the _gotcha field hidden. It is the spam trap: a real visitor never fills it in.
  • Keep the script line after the form. It is FormParcel’s bot check, and is:inline makes Astro leave it as it is. Without it, leads wait in your Spam folder. See Spam protection.
  • Every field needs a name. Each name becomes a column in the Sheet.
Bot check. A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or curl, and every post from a page that does not load the script. The dashboard then shows Bot check not installed under Needs action. Fix: keep the script line on every page that has the form. Learn more

Optional: submit with fetch

If you would rather keep the visitor on the page, add a small script that submits with fetch and shows a message. The form still works without it. Keep the bot check’s script line: the script asks it for a proof before each send.

In the same file
<form id="contact" action="https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE" method="POST">
  <!-- same fields as above, including the spam trap and _next -->
</form>
<p id="contact-status" role="status"></p>
<!-- Keep the bot check's script line from above. -->

<script>
  declare global {
    interface Window {
      FormParcel?: { proof: (endpoint: string) => Promise<string> };
    }
  }
  const form = document.querySelector<HTMLFormElement>("#contact");
  const status = document.querySelector<HTMLElement>("#contact-status");

  form?.addEventListener("submit", async (event) => {
    // formparcel.js may hold a post until its proof is ready and then send the form again: let that send through here.
    if (event.defaultPrevented) return;
    event.preventDefault();
    event.stopImmediatePropagation();
    // One proof per submission. The bot check may still be loading on a slow phone, so wait for it (10 seconds at most). If it cannot load, the lead is still sent and waits in your Spam folder.
    const fp = window.FormParcel ?? (await new Promise<Window["FormParcel"]>((ok) => { addEventListener("formparcel:ready", () => ok(window.FormParcel), { once: true }); setTimeout(() => ok(undefined), 10000); }));
    const _fp_proof = await fp?.proof(form.action).catch(() => undefined);
    const response = await fetch(form.action, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify({ ...Object.fromEntries(new FormData(form)), _fp_proof }),
    });
    // 204: the post was dropped as spam (the trap field was filled). Nothing was saved.
    const result = response.status === 204 ? null : await response.json();
    if (status) status.textContent = result?.accepted ? "Thanks, we got your message." : "Your message was not sent.";
    if (result?.accepted) form.reset();
  });
</script>
  • With fetch the answer is JSON. deliveryStatus: "pending" still means the lead was accepted: do not send it again. Every status is on the fetch guide.
  • If you set allowed origins on the form, add your site’s host, and localhost while you test locally.

Check it

Submit the form once and open its receipt in the dashboard under Leads. If nothing arrives, go through Troubleshooting.

Something wrong on this page? Tell us