Guides

A form backend for a static site

Three ways to take form submissions on a static HTML, Astro, Hugo or Eleventy site: a script tag, a fetch from the page, or a small serverless function.

Updated

A static site has no server to receive a form post. You have three ways around that, and which one fits depends on how much of the form you want to own.

Script tagFetch from the pageServerless function
Who draws the form1DBYouYou
Code to writeOne lineAbout 30 linesAbout 40 lines
Key in the pageYesYesNo
Visitor’s real IP recordedYesYesIf you pass it on

All three put the lead in the same place, run the same email alerts and webhooks, and work on any host: Netlify, Cloudflare Pages, GitHub Pages, S3.

First, add your site in 1DB under Websites. It comes with a contact form, and the site’s Integrate tab shows its key and form slug.

Option 1: the script tag

Build the form in 1DB (fields, button text, colors, success message), then put one line on your page:

<script
  src="https://gallant-goshawk-513.convex.site/v1/embed.js?key=1db_YOUR_KEY&form=contact"
  async
></script>

How the form appears is set on the form’s Embed tab:

  • Popup or slide-in: add data-1db to any button or link, and clicking it opens the form.
  • Inline: put <div data-1db-inline></div> where the form should sit.
<button data-1db>Get a quote</button>

Change the form’s fields in 1DB and every page that embeds it updates; there’s nothing to redeploy.

Option 2: fetch from the page

Keep your own HTML and CSS, and post the values with a few lines of JavaScript:

<form id="contact">
  <label>Email <input name="email" type="email" required /></label>
  <label>Message <textarea name="message" required></textarea></label>
  <p class="error" hidden></p>
  <button>Send</button>
</form>

<script type="module">
  const form = document.querySelector("#contact");
  const error = form.querySelector(".error");
  const submissionId = crypto.randomUUID();

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const res = await fetch("https://gallant-goshawk-513.convex.site/v1/leads", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": "1db_YOUR_KEY",
        "X-1DB-Contract": "2",
      },
      body: JSON.stringify({
        formSlug: "contact",
        submission_id: submissionId,
        ...Object.fromEntries(new FormData(form)),
      }),
    });

    if (res.ok) {
      form.replaceWith("Thanks, we’ll be in touch.");
    } else if (res.status === 400) {
      const { fields } = await res.json();
      error.textContent = Object.values(fields).join(" ");
      error.hidden = false;
    } else {
      error.textContent = "Your message wasn’t sent. Try again.";
      error.hidden = false;
    }
  });
</script>

Is the key safe in the page?

A site key can only send leads to that site. It can’t read, change or delete anything, and requests are rate-limited per visitor. The worst someone can do with it is send you junk, the same as filling in the form by hand. If that happens, revoke the key in the dashboard and make a new one; it doesn’t affect your other sites.

If you’d rather not have it in the page at all, use option 3.

Option 3: a serverless function

Most static hosts run small functions next to your pages. The page posts to your function, and the function adds the key and forwards the request. On Cloudflare Pages:

// functions/api/contact.js
export async function onRequestPost({ request, env }) {
  const fields = await request.json();
  const res = await fetch(`${env.DB_API_ENDPOINT}/v1/leads`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": env.DB_API_KEY,
      "X-1DB-Contract": "2",
      "X-1DB-Client-IP": request.headers.get("cf-connecting-ip") ?? "",
      "X-1DB-Client-UA": request.headers.get("user-agent") ?? "",
    },
    body: JSON.stringify({ formSlug: env.DB_FORM_SLUG, ...fields }),
  });
  return new Response(res.body, {
    status: res.status,
    headers: { "Content-Type": "application/json" },
  });
}

Then change the page’s fetch to post to /api/contact and drop the X-API-Key header. Add DB_API_ENDPOINT, DB_API_KEY and DB_FORM_SLUG in your Pages project’s settings. On Netlify, the same function goes in netlify/functions/ and reads process.env; on Vercel, in api/. Use x-forwarded-for instead of cf-connecting-ip on those.

The two X-1DB-Client-* headers pass on the visitor’s IP and browser. Without them, every lead looks like it came from your function.

After the first lead

Send a test from the live site and check the dashboard’s Leads page. Then decide where leads should go: an email to your team or a webhook, and the campaign that brought each visitor.