SheetLink Forms beta

Gatsby forms to Google Sheets

Gatsby is React, so the React patterns apply: a plain form action that works before hydration, or a fetch submit that keeps the visitor in your app. Either way, rows land in your sheet.

Gatsby sites are React apps that pre-render to static HTML, and that dual nature shapes how forms behave: the markup exists before JavaScript hydrates, then React takes over navigation. SheetLink Forms fits both halves. At its simplest, you set a form's action to a permanent endpoint of the shape https://sheetlinkforms.com/f/{token} and the browser posts to it like any HTML form - working even before hydration finishes. When you want the single-page feel, the same endpoint speaks JSON: a fetch POST with Content-Type: application/json gets back {"ok":true,"id":"..."} instead of a redirect, so your component can show success state without leaving the page.

Either way, each submission lands as a row in Google Sheets or an Excel Online table. An asynchronous worker writes directly to the Google Sheets API or Microsoft Graph - no Zapier, no middleman replaying webhooks - with retries at 5 minutes, 30 minutes, and 2 hours and a delivery log in the dashboard showing every attempt. A formula-injection guard escapes leading =, +, -, and @ characters, and there are no per-submission fees, ever.

No Gatsby plugin, no serverless function, no gatsby-node.js changes - the integration lives entirely in your components. You can watch the receiving end on the live demo. SheetLink Forms is free during beta and invite-gated - join the waitlist to get an endpoint.

The best path for Gatsby

The best path depends on how much of the SPA feel you want. The zero-code option is the plain form action: because Gatsby pre-renders real HTML, a form with action and method="POST" works immediately, JavaScript or not, and the endpoint answers with a 303 redirect to a thank-you page or a custom redirect URL. The trade-off is a full-page navigation out of the app. The React-native option is a small onSubmit handler that posts JSON with fetch and flips a useState flag on success - the visitor never leaves the page, and the snippet below is the whole component. Remember the JSX spelling of the honeypot attributes: tabIndex, autoComplete, and a style object.

The third option is the sl.js embed, loaded once with Gatsby's Script component. It binds any form whose action points at /f/ (and any form marked data-sheetlink), upgrades it to an AJAX submit with inline success text, injects the honeypot, and captures utm_* parameters plus the gclid, wbraid, gbraid, fbclid, and msclkid click IDs, persisting them in localStorage. Because it binds through a MutationObserver, forms that mount after client-side route changes are picked up automatically - the SPA case is handled by design.

// src/components/contact-form.js import * as React from "react"; const ENDPOINT = "https://sheetlinkforms.com/f/slf_yourtoken"; export default function ContactForm() { const [sent, setSent] = React.useState(false); async function handleSubmit(e) { e.preventDefault(); const data = Object.fromEntries(new FormData(e.target)); const res = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (res.ok) setSent(true); } if (sent) return <p>Thanks - we got it.</p>; return ( <form onSubmit={handleSubmit}> <input type="text" name="Name" required /> <input type="email" name="Email" required /> <textarea name="Message" /> {/* Honeypot: keep it; visitors never see it */} <input name="_slhp" tabIndex={-1} autoComplete="off" style={{ position: "absolute", left: "-9999px" }} /> <button type="submit">Send</button> </form> ); }

1. Connect a sheet and get your endpoint

Sign in and connect Google Sheets with one-click OAuth. The connection uses the drive.file scope, so SheetLink Forms can only touch sheets you pick in the Google picker or create inside the product - never the rest of your Drive. Prefer Microsoft 365? Connect Excel Online and pick an Excel table; the table's columns define the row shape. You get a permanent endpoint at https://sheetlinkforms.com/f/{token}.

2. Decide: plain action or fetch

For a marketing page where a redirect to a thank-you page is fine, write the form with action={ENDPOINT} and method="POST" and skip the JavaScript entirely - it works before hydration and with JavaScript disabled. For inline success state, use the component in the snippet: serialize the form with FormData, post JSON, and render on {"ok":true}. Name fields after your column headers either way - matching is case and punctuation insensitive, so Email, email, and E-mail all land in an Email column.

3. Keep the honeypot, in JSX spelling

The _slhp input must be present and empty - it is the only signal that marks spam outright, while everything else suspicious (rate limits, content heuristics, timing) is quarantined for one-click review instead of dropped. In JSX that means tabIndex={-1}, autoComplete="off", and a style object, as in the snippet. It ships in the JSON body like any other field and is stripped before your sheet - reserved field names are listed in the docs.

4. Optionally load sl.js with the Script component

If you would rather not write the handler yourself - or you want ad attribution - render <Script src="https://sheetlinkforms.com/sl.js" data-token="slf_yourtoken" /> from gatsby in your layout and use a plain action form. The embed converts it to an AJAX submit with inline success text, injects the honeypot for you, and captures UTM parameters and click IDs, persisted in localStorage across client-side navigation. Forms mounted after a route change are bound via MutationObserver.

5. Build, deploy, send a test

Run gatsby build, deploy wherever you host, and submit a test. On an empty sheet the header row is seeded for you; after that, rows append under the header, aligned to your columns. Check the delivery log in the dashboard to watch the attempt - the full endpoint-to-sheet pipeline is on how it works.

FAQ

Does a plain action form really work in a React app?

Yes. Gatsby pre-renders your components to static HTML, so the form exists in the document before hydration. Submitting it is a normal browser POST followed by a 303 redirect - it just navigates away from the SPA. If you want the visitor to stay in the app, use the fetch pattern or sl.js instead.

Do submissions depend on Gatsby's rendering mode?

No. Whether a page is statically generated, deferred (DSG), or server-rendered, the form posts from the visitor's browser to the SheetLink Forms endpoint. Your build and hosting setup never touch the submission path, and no serverless function is involved.

Does attribution survive client-side route changes?

Yes. sl.js captures utm_* parameters and ad click IDs (gclid, wbraid, gbraid, fbclid, msclkid) from the landing URL and persists them in localStorage, so a visitor can browse through Gatsby's client-side routing for as long as they like before submitting - the attribution still arrives with the lead. Add matching columns to your sheet to receive it.

How do I show my own success UI with sl.js?

Listen for the sheetlink:success and sheetlink:error events, which bubble from the form with details in event.detail - attach a listener in a useEffect and set whatever state you like. Or skip the embed and own the whole flow with the fetch component in the snippet. The same patterns apply to any React app - the React guide goes deeper.

What does it cost?

Free during beta, invite required - join the waitlist. Planned post-beta pricing is a free tier around 50 submissions a month and a paid plan around $15-19/mo, unmetered. There are no per-submission or per-task fees, ever.

Also on: React · Next.js · Netlify-hosted sites

Request an invite See the live demo