SheetLink Forms beta

Cloudflare Pages forms to Google Sheets

A plain form action on static output, or a Pages Function relay when you want one - every submission lands as a row in Google Sheets or Excel.

Cloudflare Pages serves static assets by default and turns server-side when you add a functions/ directory - two deployment shapes, and SheetLink Forms has a clean wiring for each. Every form gets a permanent endpoint at https://sheetlinkforms.com/f/{token} that accepts urlencoded, JSON, or multipart POSTs (bodies up to 256KB) and files each accepted submission as a row in Google Sheets or an Excel Online table.

Delivery is direct: an asynchronous worker writes to the Google Sheets API or Microsoft Graph with retries at 5 minutes, 30 minutes, and 2 hours, and a delivery log records every attempt. Field names matching your column headers map automatically (case and punctuation insensitive), so input names or JSON keys named after your columns are usually the entire mapping. The full pipeline is on how it works.

The product is free during beta and invite-gated - join the waitlist - and there are no per-submission fees, ever.

The best path for Cloudflare Pages

For a static Pages project - Hugo, Astro, Eleventy, or plain HTML pushed from a repo - point the form's action at the endpoint and you are done: the 303-redirect thank-you flow needs no client code at all. If your project already uses Pages Functions, an onRequestPost handler in functions/api/ makes a natural relay, giving you server-side validation and keeping CORS out of the conversation.

One pairing worth noting for a site already on Cloudflare: SheetLink supports optional Cloudflare Turnstile as a challenge on the form, alongside the honeypot, rate limits, and content heuristics. Suspicious submissions are quarantined for one-click review rather than dropped, so a false positive costs seconds, not a lead.

<!-- Static path: works in any HTML Cloudflare Pages serves --> <form action="https://sheetlinkforms.com/f/slf_yourtoken" method="POST"> <input name="Name" placeholder="Name"> <input name="Email" type="email" placeholder="Email"> <textarea name="Message" placeholder="Message"></textarea> <!-- Honeypot: keep it in the form, leave it empty --> <input name="_slhp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px"> <button type="submit">Send</button> </form> /* Or a Pages Function relay: functions/api/contact.js export async function onRequestPost({ request }) { const fields = await request.json(); const res = await fetch("https://sheetlinkforms.com/f/slf_yourtoken", { method: "POST", headers: { "Content-Type": "application/json" }, // _slhp is the honeypot: include it, leave it empty body: JSON.stringify({ ...fields, _slhp: "" }), }); return new Response(await res.text(), { status: res.status, headers: { "Content-Type": "application/json" }, }); } */

1. Connect a sheet and get your endpoint

Connect Google Sheets with one-click OAuth (the drive.file scope means the product only sees sheets you pick or create - never the rest of your Drive) or connect Excel Online and pick a table. You get a permanent endpoint of the form https://sheetlinkforms.com/f/{token}. On an empty sheet the header row is seeded for you; rows then append under it.

2. Static projects: use the form action

Paste the HTML form from the snippet into your template or page. Name inputs after your sheet's column headers for automatic mapping, keep the honeypot input in place, and the browser follows a 303 redirect to a hosted thank-you page or a custom URL on your domain after submitting. No functions directory required.

3. Or add a Pages Function relay

Create functions/api/contact.js with the onRequestPost handler from the snippet - Pages routes it at /api/contact automatically. Your client posts JSON to your own origin, the function forwards it, and the endpoint's {"ok":true,"id":"..."} comes straight back. Validate or enrich in the function as needed.

4. Add sl.js for inline success and attribution

On the static path, the optional embed upgrades the form to AJAX submission with inline success text, auto-injects the honeypot, adds a timing signal, and captures utm_* plus gclid, wbraid, gbraid, fbclid, and msclkid, persisted in localStorage across pages. It binds forms marked data-sheetlink or posting to /f/.

5. Turn on Turnstile if you want a challenge

SheetLink supports optional Cloudflare Turnstile per form - a familiar tool if you are already in the Cloudflare ecosystem. The cf-turnstile-response field is verified at the endpoint and stripped before the row is written, like the other reserved fields. Honeypot, rate limits, and the heuristics dial keep working alongside it.

6. Test and read the delivery log

Send a test submission and watch the row land - or try the live demo first. The delivery log records every attempt, and the docs list the error responses worth handling: 404 unknown token, 410 paused form, 403 origin not allowed, 413 payload too large, 429 rate limited, 400 unreadable body.

FAQ

Do I need a Pages Function for this to work?

No. The plain form action is a complete integration on a purely static Pages project. The function relay is an option for projects that already run server code and want validation or enrichment before the row is filed - not a requirement.

How does Cloudflare Turnstile fit in?

Turnstile is supported as an optional challenge on SheetLink forms. Add the widget, and the endpoint verifies the cf-turnstile-response token before accepting the submission; the field itself never appears in your sheet. It layers on top of the honeypot, rate limits, and content heuristics rather than replacing them.

I deploy a framework preset (Astro, SvelteKit, Remix) to Pages - does that change anything?

No. The integration is one HTTP POST, so it works from any framework's output: a plain action in static HTML, a client fetch with a JSON body, or the framework's own server routes running on Pages doing the relay. Send Accept: application/json from server code to get the JSON response instead of the 303 redirect.

Can I keep ad attribution on a static Pages site?

Yes - that is what the sl.js embed is for. It captures utm_* and the ad click IDs (gclid, wbraid, gbraid, fbclid, msclkid) from landing URLs, persists them in localStorage as the visitor browses, and attaches them to the submission so they land in the sheet next to the lead.

What happens if delivery to the sheet fails?

Acceptance and delivery are decoupled. The endpoint accepts the post, then an asynchronous worker writes the row with retries at 5 minutes, 30 minutes, and 2 hours. Every attempt is visible in the dashboard's delivery log, so a transient API failure costs latency, not data.

Also on: Netlify-hosted sites · Vercel-hosted sites · Hugo

Request an invite See the live demo