SheetLink Forms beta

Jekyll forms to Google Sheets

An include, a _config.yml value, and a plain form action. No plugins, no backend - every submission lands as a row in your sheet.

Jekyll has been quietly powering blogs and docs sites for over a decade, very often on GitHub Pages, where the build runs in safe mode and custom plugins are off the table. That rules out most form solutions before you even start - but not this one, because this one is only HTML. You point your form's action at 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 plugin, no gem, no JavaScript requirement, nothing GitHub Pages can object to.

Behind the endpoint, an asynchronous worker writes directly to the Google Sheets API or Microsoft Graph - no Zapier, no automation tool in the path - and retries at 5 minutes, 30 minutes, and 2 hours cover transient failures. Every attempt is visible in a delivery log in the dashboard, and a formula-injection guard escapes leading =, +, -, and @ characters so pasted payloads stay inert text in your cells. There are no per-submission fees, ever.

The integration maps cleanly onto Jekyll's own conventions: the form markup lives in _includes, the token lives in _config.yml, and Liquid stitches them together at build time. You can watch a live form feed a public sheet on the demo page. SheetLink Forms is free during beta and invite-gated - join the waitlist to get an endpoint.

The best path for Jekyll

The best path is a plain form action inside an include. Store the token in _config.yml as sheetlink_token: slf_yourtoken, reference it in the include as {{ site.sheetlink_token }}, and pull the form into any layout or page with {% include contact-form.html %}. Because Liquid also runs inside markdown, the same include tag works directly in a post or page body - no shortcode machinery needed. The endpoint accepts the browser's urlencoded post (bodies capped at 256KB) and answers with a 303 redirect to a hosted thank-you page or a custom redirect URL you set per form. No JavaScript is required at all.

If you want the page to stay put on submit, add the optional sl.js embed to your default layout. It upgrades the same form to an AJAX submit with inline success text, auto-injects the honeypot, adds a timing signal used in spam screening, and captures utm_source, utm_medium, utm_campaign, utm_term, and utm_content plus the gclid, wbraid, gbraid, fbclid, and msclkid click IDs from the URL, persisting them in localStorage across pages. Add matching columns to your sheet and the attribution arrives with each lead.

<!-- _includes/contact-form.html --> <!-- _config.yml: sheetlink_token: slf_yourtoken --> <form action="https://sheetlinkforms.com/f/{{ site.sheetlink_token }}" method="POST"> <input type="text" name="Name" required> <input type="email" name="Email" required> <textarea name="Message"></textarea> <!-- Honeypot: keep it exactly as-is; visitors never see it --> <input name="_slhp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px"> <button type="submit">Send</button> </form> <!-- Optional, in _layouts/default.html before </body> --> <script src="https://sheetlinkforms.com/sl.js" data-token="{{ site.sheetlink_token }}"></script>

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. Add the token to _config.yml

One line: sheetlink_token: slf_yourtoken. Any template can then read {{ site.sheetlink_token }}. The token is fine to render into public HTML - the endpoint is designed to be public and screened by its own spam layers. One Jekyll quirk to remember: jekyll serve does not watch _config.yml, so restart the server after adding the value or the variable renders empty.

3. Create the include, honeypot included

Save the snippet as _includes/contact-form.html and place it with {% include contact-form.html %} in a layout, a page, or straight inside a markdown post. Name 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. Keep the _slhp honeypot exactly as-is: it is the only signal that marks spam outright; everything else suspicious is quarantined for one-click review, never silently dropped.

4. Optionally add sl.js to your default layout

Drop the script tag into _layouts/default.html before </body>. 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, injects the honeypot for you, and layers in the timing signal plus UTM and click-ID capture. Skip it and the plain 303-redirect flow still works exactly as before - the reserved field names it uses are listed in the docs.

5. Push, let Pages build, send a test

Commit, push, and let GitHub Pages (or wherever you host) rebuild. Send a test submission: on an empty sheet the header row is seeded for you, and after that rows append under the header, aligned to your columns. The delivery log in the dashboard shows each attempt and retry, so you never wonder whether a lead made it in. The whole pipeline is drawn out on how it works.

FAQ

Does this work on GitHub Pages' restricted build?

Yes. GitHub Pages runs Jekyll in safe mode and blocks custom plugins, but this integration needs none - it is an include, a config value, and a form action. Nothing in the build changes. There is a dedicated GitHub Pages guide if that is your host.

Can I put the form in a markdown post?

Yes. Liquid runs inside markdown files, so {% include contact-form.html %} works in a post or page body directly, and raw HTML in markdown is passed through by kramdown. No shortcode layer is needed.

Why is my form action empty after I added the token?

Almost always because jekyll serve was already running when you edited _config.yml - config is loaded once at startup and is not watched. Restart the server and {{ site.sheetlink_token }} will resolve.

What does the visitor see after submitting?

Without sl.js, the browser follows a 303 redirect to a hosted thank-you page or any custom URL you set per form. With sl.js, the page does not navigate - the form swaps to inline success text, which you can customize per form with data-success-message.

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: GitHub Pages · Hugo · Plain HTML

Request an invite See the live demo