SheetLink Forms beta

SvelteKit forms to Google Sheets

A form action in +page.server.ts, a client fetch, or a plain HTML action on a prerendered page - every submission lands as a row in Google Sheets or Excel.

SvelteKit treats a form that works before JavaScript loads as the baseline and layers enhancement on top. SheetLink Forms is built the same way: every form gets a permanent endpoint at https://sheetlinkforms.com/f/{token} that accepts urlencoded, JSON, or multipart POSTs (bodies up to 256KB). A plain HTML post gets a 303 redirect to a thank-you page; a request that asks for JSON gets {"ok":true,"id":"..."}. Either way the submission is filed 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, retries at 5 minutes, 30 minutes, and 2 hours, and logs every attempt. Field names that match your column headers map automatically (case and punctuation insensitive), so the name attributes on your inputs are usually the whole mapping story. The pipeline is described 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 SvelteKit

With a server in play, the idiomatic path is a form action: a default action in +page.server.ts reads the submitted formData, forwards it to your SheetLink endpoint as JSON, and returns the endpoint's ok flag into the page's form prop. Add use:enhance and the same form submits without a full page reload. On a fully prerendered site (adapter-static) there is no action to run at request time - so point the form's action attribute straight at the endpoint instead.

That second path is worth pausing on: SvelteKit's progressive-enhancement philosophy fits SheetLink's plain-action support exactly. The no-JavaScript baseline - a form posting to /f/{token} and following a 303 redirect - is a fully working integration on its own, and use:enhance or a client fetch are strictly layers on top of it, not replacements for it.

// src/routes/contact/+page.server.ts export const actions = { default: async ({ request, fetch }) => { const data = await request.formData(); const fields = Object.fromEntries(data); const res = await fetch("https://sheetlinkforms.com/f/slf_yourtoken", { method: "POST", headers: { "Content-Type": "application/json", // Ask for JSON instead of the 303 redirect meant for plain HTML forms Accept: "application/json", }, // _slhp is the honeypot: include it, leave it empty body: JSON.stringify({ ...fields, _slhp: "" }), }); return await res.json(); // {"ok":true,"id":"..."} }, }; /* src/routes/contact/+page.svelte <script> import { enhance } from "$app/forms"; export let form; </script> <form method="POST" use:enhance> <input name="Name" placeholder="Name"> <input name="Email" type="email" placeholder="Email"> <textarea name="Message"></textarea> <button>Send</button> {#if form?.ok}<p>Thanks - your message is in the sheet.</p>{/if} </form> */

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. Write the default form action

Use the snippet above in src/routes/contact/+page.server.ts. The action parses formData, adds the empty _slhp honeypot key, and forwards everything as JSON. Name your inputs after the sheet's column headers so mapping is automatic; explicit per-field mapping exists in the dashboard when they differ.

3. Enhance the form

In +page.svelte, a method="POST" form with use:enhance submits through the action without a reload, and the returned object arrives in the form prop - render form?.ok for the success state. Without JavaScript, the same form still posts and SvelteKit re-renders the page with the action result. Both paths land the same row.

4. Or go fully static

On an adapter-static site, skip the action and set the form's action attribute to https://sheetlinkforms.com/f/{token} directly. Include the off-screen honeypot input (<input name="_slhp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">) and the browser follows a 303 redirect to a thank-you page or your custom URL. No server, no adapter constraints, no client code.

5. Or fetch from the client

A submit handler in a Svelte component can fetch the endpoint straight from the browser - send Content-Type: application/json to get the JSON response. The endpoint supports cross-origin browser posts, and the per-form origin allowlist can restrict them to your domains. This is the right shape when one component owns the form state and no route action is warranted.

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 in an action: 404 unknown token, 410 paused form, 403 origin not allowed, 413 payload too large, 429 rate limited, 400 unreadable body.

FAQ

Form action or a direct form action attribute - which should I use?

If your deployment has a server (Node adapter, Vercel, Netlify, Cloudflare), the form action gives you server-side validation and a tidy form prop for UI state. If the site is prerendered with adapter-static, point the HTML action straight at the endpoint - it is the same integration with fewer moving parts.

Does progressive enhancement actually work here?

Yes, in both directions. The action path degrades to a normal POST plus a server render when JavaScript is absent. The direct path degrades to the endpoint's own 303-redirect thank-you flow. SheetLink was designed around the plain HTML form as the baseline, which is the same contract use:enhance assumes.

Where does the honeypot go?

In the action relay, add "_slhp": "" to the JSON body as the snippet does. In a direct HTML form, include the off-screen input instead. The honeypot is the one signal that marks spam outright; everything else suspicious is quarantined for one-click review, never silently dropped.

Can I capture ad attribution on a SvelteKit site?

Put the sl.js embed on the page. It captures utm_* plus gclid, wbraid, gbraid, fbclid, and msclkid, persists them in localStorage across client-side navigations, and binds any form whose action points at /f/ - a MutationObserver handles forms rendered after hydration. For the action-relay path, forward those values as fields from the client instead.

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 Sheets API failure costs latency, not data.

Also on: Vue and Nuxt · Next.js · Astro

Request an invite See the live demo