SheetLink Forms beta

A contact form on a fully static site

Static hosting's one missing feature is somewhere to POST. Borrow that one piece and keep everything that makes static good.

Static means no server - which forms notice

A static site is HTML, CSS, and JavaScript in a folder: GitHub Pages, S3 behind CloudFront, Netlify or Vercel serving prebuilt output, or a VPS running nothing but a file server. Fast, cheap, nearly unhackable, cache-friendly - static earns its popularity. The one thing it cannot do is accept a POST, and a contact form is a POST.

The traditional outs each give up part of the win: bolt on a serverless function and you are maintaining backend code again; use a mailto: link and watch conversion die on the desktop mail client prompt; embed a hosted iframe form and inherit someone else's styling and page weight. The lighter answer is to keep your form - your markup, your CSS - and borrow only the missing piece: an endpoint to receive the POST.

The whole integration is one attribute

A SheetLink form is a permanent URL. Point your form's action at it:

<form action="https://sheetlinkforms.com/f/slf_yourtoken" method="POST">
  <input name="name" placeholder="Name" required>
  <input name="email" type="email" placeholder="Email" required>
  <textarea name="message" placeholder="What can we do?"></textarea>
  <input name="_slhp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">
  <button type="submit">Send</button>
</form>

The hidden _slhp input is the spam honeypot - ship it in every form. Submissions 303-redirect to a thank-you page (hosted by default, your own URL if you set one), and each accepted submission is appended as a row in the Google Sheet or Excel table you connected. Field names matching your column headers map automatically, case and punctuation insensitive. No build step, no JavaScript required, no API key in the page - the walkthrough with troubleshooting lives at HTML form to Google Sheets without a backend.

"But the token is right there in my source"

It is, and that is by design. A static page cannot hold a secret - anything in your HTML is public the moment you deploy. So the endpoint token is not a secret: it is an address, like the email address on your contact page has always been. Knowing it lets someone do exactly one thing: submit your form, which is what the form is for.

What keeps a public address from becoming a spam firehose is the screening behind it, not obscurity in front of it: the honeypot marks bot submissions outright, per-form and per-IP rate limits cap volume, content heuristics (link stuffing, disposable email domains, empty payloads) flag the dubious, and everything suspicious is quarantined for one-click review rather than deleted - so a false positive costs seconds, not a lead. For a form that should only ever exist on your own site, add the per-form origin allowlist and posts from other origins are refused with a 403. Optional Cloudflare Turnstile sits on top for forms under sustained attack. The layers are detailed in the spam protection guide.

It fits any generator the same way

Because the integration is plain markup, every static toolchain treats it the same: put the form in a partial, layout, or component - a Hugo partial, an Astro or Jekyll include, an Eleventy shortcode, a plain HTML fragment - and reuse it across pages. Two habits pay off:

  1. One token per form purpose, not per page. A contact form reused on five pages should share one token so all rows land in one sheet; a quote form is a different purpose, a different token, its own sheet or tab.
  2. Consider a separate form for staging. Test submissions polluting the production sheet is the static-site version of testing in prod. A second form costs nothing and keeps the ledger clean - and if you use the origin allowlist, remember to list the staging origin on the staging form.

The optional upgrade: sl.js

Everything above works with zero JavaScript. One script tag adds the dynamic parts static sites usually give up:

<script src="https://sheetlinkforms.com/sl.js" data-token="slf_yourtoken"></script>

The embed binds any form whose action points at /f/ (and any form marked data-sheetlink), converts submits to AJAX with inline success text so visitors stay on the page, injects the honeypot if you forgot it, adds a timing signal that strengthens spam screening, and captures utm_* parameters plus ad click IDs from the URL, persisting them across pages so campaign attribution arrives with each row. On a marketing site running any paid traffic, that last feature alone justifies the script tag - see attribution across multi-page journeys.

Ship checklist

  1. Create the form in the dashboard and connect Google Sheets (one-click OAuth, drive.file scope - it can only touch sheets you pick or create in the product) or an Excel Online table.
  2. Name inputs to match your column headers - name, email, message - and include the _slhp honeypot.
  3. Set the redirect URL to your own thank-you page if you have one.
  4. Deploy, submit a test, and confirm the row in the delivery log (delivery is asynchronous with retries at 5, 30, and 120 minutes - a pending retry is not a lost row).
  5. If you enabled the origin allowlist, confirm your production domain is on it - a 403 on launch day is almost always this.

Watch a live form feed a public sheet on the demo. SheetLink Forms is free during beta, invite required - the waitlist is on the homepage, and there are no per-submission fees, ever.

FAQ

Is it safe that my endpoint token is visible in the page source?

Yes - the token is an address, not a credential. It grants exactly one ability: submitting the form. Abuse is handled behind the endpoint by the honeypot, rate limits, content heuristics, quarantine, the optional origin allowlist, and optional Turnstile - not by hiding the URL, which a static site cannot do anyway.

Does this work on GitHub Pages, where I cannot run any code?

Yes. The form is plain HTML posting cross-origin to sheetlinkforms.com, which requires nothing from the host - no functions, no plugins, no configuration. GitHub Pages, S3, any CDN, or a local file during development all behave identically.

Can visitors get a confirmation without leaving my site?

Two options: set a custom redirect URL so the 303 lands on your own thank-you page, or add the sl.js embed and the submit becomes AJAX with inline success text - no navigation at all. Both are per-form choices, not site rebuilds.

What about file attachments on a static site's form?

Multipart posts are accepted but file parts are currently dropped - the text fields still deliver. Bodies are capped at 256 KB. Treat the form as a text channel and link to a shared-drive upload if you genuinely need files.

I generate my site with WordPress as a headless CMS - does this still apply?

If the output your visitors see is static files, yes - the form posts from the static page and WordPress never touches it. If you run an actual WordPress front end, the sibling plugin at sheetlinkwp.com is the tighter fit.

Related guides: Send an HTML form to Google Sheets without a backend · Form endpoint vs writing your own serverless function · CORS for form endpoints: what actually needs it

Request an invite Developer docs