Astro forms to Google Sheets
Astro ships zero JavaScript by default, and so does this integration. Point your form's action at a permanent endpoint and every submission lands as a row in your sheet.
Astro's whole pitch is shipping less JavaScript: components render to plain HTML at build time, and the browser gets markup, not a bundle. A contact form is where that philosophy usually breaks down, because a form needs something on the other end - a server to receive the POST, screen it for spam, and file the lead somewhere you will actually look. SheetLink Forms is that something. You set your form's action to a permanent endpoint of the shape https://sheetlinkforms.com/f/{token}, and each submission lands as a row in Google Sheets or an Excel Online table. No API route, no server adapter, no serverless function to deploy.
Delivery is direct: an asynchronous worker writes straight to the Google Sheets API or Microsoft Graph - no Zapier, no automation middleman - with retries at 5 minutes, 30 minutes, and 2 hours, and a delivery log in the dashboard that shows every attempt. A formula-injection guard escapes leading =, +, -, and @ characters so hostile input stays inert text in your cells. There are no per-submission fees, ever.
The result fits Astro unusually well: a fully static astro build output, a form that works with JavaScript disabled, and an optional one-tag upgrade when you want AJAX submits and ad attribution. You can watch the exact flow on the live demo - a real form feeding a public sheet. SheetLink Forms is free during beta and invite-gated, so join the waitlist to get an endpoint.
The best path for Astro
The best path in Astro is the plain form action, written into a .astro component. Define the token as a frontmatter constant (or read it from import.meta.env - it ends up in the rendered HTML either way, which is fine, because the endpoint is designed to be public and protected by its own spam layers). Interpolate it into action, set method="POST", and you are done: the endpoint accepts the urlencoded post (bodies capped at 256KB) and answers with a 303 redirect to a hosted thank-you page or a custom redirect URL you configure per form. This works in output: 'static' with zero client JavaScript, which is exactly the build Astro wants you to ship.
When you want more, add the optional sl.js embed to your layout. One detail is Astro-specific: Astro normally processes and bundles <script> tags, but a tag that carries any attribute besides src - like data-token - is left untouched, exactly as if you had written is:inline. Adding the is:inline directive anyway makes the intent explicit. The embed upgrades the same form to an AJAX submit with inline success text, injects the honeypot, adds a timing signal used in spam screening, and captures utm_* parameters plus the gclid, wbraid, gbraid, fbclid, and msclkid click IDs, persisting them in localStorage across pages.
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. Either way you get a permanent endpoint at https://sheetlinkforms.com/f/{token}.
2. Build the form as an Astro component
Create src/components/ContactForm.astro, define the token in the frontmatter fence, and interpolate it into the action attribute as in the snippet above. Drop <ContactForm /> into any page or layout. Name your inputs after your column headers and mapping happens automatically - matching is case and punctuation insensitive, so Email, email, and E-mail all land in an Email column. If names and headers genuinely differ, explicit per-field mapping is available in the dashboard.
3. Keep the honeypot input
Copy the _slhp input exactly as shown. It sits off-screen where humans never see it, so anything that fills it is a bot - the honeypot is the only signal that marks spam outright. Everything else that looks suspicious (rate-limit trips, content heuristics, timing anomalies) is quarantined for one-click review instead of being dropped, so a false positive costs seconds, not a lead. The full list of reserved field names is in the docs.
4. Optionally add sl.js to your layout
Put the script tag in your base layout, before </body>, with is:inline so Astro leaves it unbundled. It binds any form whose action points at /f/ (and any form marked data-sheetlink), converts it to an AJAX submit with inline success text, and layers in attribution capture. It is also SPA-safe: forms rendered late are bound via MutationObserver, so if you use Astro's view transitions for client-side navigation, forms swapped into the page still get picked up.
5. Run astro build, submit a test, read the delivery log
Deploy the static output anywhere and send a test submission. On an empty sheet the header row is seeded for you; after that, rows append under the header, aligned to your columns. The delivery log in the dashboard shows each attempt and retry. The end-to-end path - endpoint to worker to sheet - is laid out on how it works.
FAQ
Do I need an SSR adapter or an API route?
No. The form posts from the visitor's browser directly to the SheetLink Forms endpoint, so Astro's rendering mode is irrelevant - plain output: 'static' is enough. There is no server code in your project at all.
Does the form work with zero client JavaScript?
Yes. A bare action + method="POST" form works with JavaScript disabled: the endpoint accepts the urlencoded post and answers with a 303 redirect to a hosted thank-you page or your custom redirect URL. That makes it a natural fit for Astro's zero-JS default output.
Why does the sl.js tag need is:inline?
Astro processes and bundles script tags by default, which could strip the data-token attribute the embed reads. In practice Astro already treats any script with an attribute besides src as inline, but writing is:inline makes the behavior explicit and guarantees the tag ships to the browser exactly as authored.
Does sl.js survive Astro view transitions?
Yes. The embed binds forms through a MutationObserver, so forms that appear after the initial page load - including content swapped in by client-side navigation - are bound automatically. Attribution parameters like gclid and utm_campaign are persisted in localStorage, so they survive page-to-page browsing before the visitor finally submits.
What does it cost?
SheetLink Forms is free during beta; an invite is required, so 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: 11ty · SvelteKit · Plain HTML