RG Forms logoRG Forms
Start here

How to add a contact form to a static website

Static sites are wonderful right up until someone asks for a contact form. HTML, CSS and JavaScript sitting on a CDN can render anything — but the moment a visitor clicks Submit, something has to be listening on the other end, and a static host isn’t.

This page lays out every legitimate way to fix that, what each one costs you in money and maintenance, and how to decide.

Last updated August 19, 2026

The short answer

A static website can't process a form on its own, because there's no server running your code when a visitor hits Submit. You solve it by sending the submission somewhere else: a form endpoint service (RG Forms, Formspree, Formspark, FormSubmit), a form feature built into your host (Netlify Forms, Cloudflare Pages), or a small serverless function you write and maintain yourself. RG Forms is the option where the endpoint lives in your own Google account — it provisions a Google Sheet plus an Apps Script web app inside your Drive, so submissions land in a spreadsheet you own, with no server to run and no monthly fee.

Why the form doesn’t “just work”

A plain <form action="/submit" method="post"> expects a server at /submit to accept the request, validate it, store it and reply. On a static host there is no such process — your files are served from a CDN and nothing executes server-side. The POST hits a 405 or a 404, and the visitor sees a broken page.

So every solution below is really the same move: point the form at something that is running. What differs is who runs it, where the data ends up, and who pays for it.

The four approaches

All four are legitimate. The right one depends on where you want the data to live and how much you want to maintain.

ApproachWhere data livesGood when
Form endpoint serviceThe provider’s database (or, with RG Forms, your own Google Sheet)You want it working in minutes and you don’t want to run infrastructure.
Host-native formsYour host’s dashboardYou’re already on a host that includes forms and you plan to stay there.
Your own serverless functionWherever you send itYou need custom logic — payments, CRM writes, complex validation — and you’re happy owning the code.
mailto: linkNowhere; it opens the visitor’s mail clientAlmost never. It fails silently for anyone without a configured desktop mail app.

A note on mailto: — it isn’t a form backend. It hands the job to the visitor’s device, and on most phones and webmail setups nothing useful happens. Treat it as a fallback link, not a form.

What makes RG Forms different

Most form endpoint services store submissions on their servers and show them to you in their dashboard. That’s a perfectly reasonable model, and the good ones do it well.

RG Forms inverts it. When you sign in with Google, it creates three things inside your own Google account: a Drive folder, a Google Sheet, and a Google Apps Script web app deployed at a permanent HTTPS URL. That Apps Script is your form backend. Submissions travel from your visitor’s browser directly to your script and land as rows in your spreadsheet.

There’s no RG Forms server in the path — which is why it’s free, and why your endpoint keeps working even if rgforms.com disappears tomorrow. Google hosts the Sheet and the script the same way it hosts any other file in your Drive.

Setting it up

From a blank page to a live endpoint is about two minutes.

  1. Sign in with Google and describe your form

    Name the project, then set your fields — name, email, phone, message by default, or whatever you need. Turn on email notifications if you want each submission in your inbox, and spam protection if you want Cloudflare Turnstile checked server-side.

  2. Let it provision

    RG Forms creates the Drive folder, the Sheet with a header row matching your fields, and the Apps Script web app — then hands you a permanent endpoint URL like https://script.google.com/macros/s/AKfycb.../exec.

  3. Authorize the script once

    Open the endpoint URL in your browser while signed in to Google and click through the permission screen. Google shows an “unverified app” warning here — the app in question is *your own script*, created minutes ago in your own account, which is why it has no verification. This is expected and safe.

  4. Point your form at it

    Drop this into your page. It works on any static host, with or without a framework, with no build step.

    index.html
    <form id="contact-form">
      <label>Name <input name="name" required /></label>
      <label>Email <input type="email" name="email" required /></label>
      <label>Message <textarea name="message" required></textarea></label>
    
      <!-- Honeypot: humans never see it, bots fill it in -->
      <input type="text" name="_hp" tabindex="-1" autocomplete="off"
             style="position:absolute;left:-9999px" aria-hidden="true" />
    
      <button type="submit">Send</button>
      <p id="form-status" role="status"></p>
    </form>
    
    <script>
      const ENDPOINT = "https://script.google.com/macros/s/AKfycb.../exec";
      const form = document.getElementById("contact-form");
      const status = document.getElementById("form-status");
    
      form.addEventListener("submit", async (event) => {
        event.preventDefault();
        status.textContent = "Sending…";
    
        const res = await fetch(ENDPOINT, {
          method: "POST",
          // text/plain avoids the CORS preflight Apps Script can't answer
          headers: { "Content-Type": "text/plain" },
          body: JSON.stringify({
            tab: "contact",
            fields: Object.fromEntries(new FormData(form)),
          }),
        });
    
        const data = await res.json();
        status.textContent =
          data.result === "success" ? "Thanks — we'll be in touch." : "Something went wrong.";
        if (data.result === "success") form.reset();
      });
    </script>

Verify it end to end

Before you wire up the UI, prove the endpoint works:

Test the endpoint from your terminal
curl -L -X POST "https://script.google.com/macros/s/AKfycb.../exec" \
  -H "Content-Type: text/plain" \
  -d '{"tab":"contact","fields":{"name":"Ada","email":"ada@example.com","message":"Hello"}}'

# → {"result":"success"}   and a new row appears in your Google Sheet

The -L matters: Apps Script answers with a redirect to a googleusercontent.com host that serves the actual response.

What to check before you ship

  • Use `Content-Type: text/plain`application/json triggers a CORS preflight that Apps Script web apps cannot answer, so the browser blocks the request. The body is still JSON — only the header changes.

  • Add the honeypotA hidden _hp field costs nothing and stops the bulk of drive-by bot submissions. Anything with _hp filled in is silently discarded.

  • Show real statesSending, success and failure. A form that gives no feedback feels broken even when it works.

  • Know the email quotaNotifications send from your own Google account, which Google caps at roughly 100 recipients/day on free Gmail and 1,500/day on Workspace. Rows still save if you hit the cap — only the email is skipped.

Common questions

Do I need a server at all?

No. Your site stays 100% static. The only thing running is the Apps Script web app in your own Google account, and Google runs that for you at no cost.

Will this work on GitHub Pages, Netlify, Vercel, Cloudflare Pages?

Yes — it’s a plain fetch() from the browser, so the host is irrelevant. There are host-specific walkthroughs for GitHub Pages, Netlify, Cloudflare Pages and Vercel.

What happens to submissions if I stop using RG Forms?

Nothing — they’re already in your Drive. The Sheet, the folder and the script are ordinary files in your Google account. You can keep using the endpoint, edit the script yourself, or export the Sheet and walk away.

Can I have more than one form?

Yes. Each form is a tab in the same Sheet and shares the same endpoint URL — you pick which one you’re writing to with the tab value in the request body.

More answers in the full FAQ.

Your endpoint goes live in ~90 seconds