GitHub Pages forms to Google Sheets
No servers, no functions, no build-time secrets - a plain HTML form action is the entire integration, and every submission lands as a row in Google Sheets or Excel.
GitHub Pages serves files. There is no server-side code, no functions directory, no environment variables at request time - by design. So a contact form on a GitHub Pages site needs exactly one thing it cannot provide itself: somewhere to POST. SheetLink Forms is that somewhere. Every form gets a permanent endpoint at https://sheetlinkforms.com/f/{token}; the browser posts to it, follows a 303 redirect to a thank-you page or a custom URL on your site, and the submission is filed as a row in Google Sheets or an Excel Online table.
This is the purest version of the static-hosting story: the form's action attribute is the whole answer. 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. Input name attributes that match your column headers map automatically (case and punctuation insensitive). 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 GitHub Pages
The best path is the only path, which is a kind of clarity: a plain HTML form whose action points at your endpoint. It works identically in a hand-written index.html, a Jekyll layout (Jekyll is what GitHub Pages builds natively), or the output of any generator you run in a GitHub Actions workflow before publishing. Nothing about the host is involved beyond serving the page.
If you want more than the redirect flow - inline success text instead of a page navigation, plus ad attribution capture - add the sl.js embed. It is one script tag, it binds the form automatically, and it still requires no backend of any kind.
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. Paste the form into your page
Use the snippet above. Name each input after a column header in your sheet - "Name", "Email", "Message" - and mapping is automatic; explicit per-field mapping exists in the dashboard if your markup and your columns disagree. Keep the honeypot input exactly as shown: off-screen, empty, present.
3. Place it once in Jekyll
On a Jekyll site, put the form in _includes/contact-form.html and drop {% include contact-form.html %} wherever a page needs it. Liquid passes plain HTML through untouched, so there is nothing to escape and nothing to configure. Sites built by an Actions workflow work the same way - the form is just markup in the published output.
4. Choose where the redirect lands
After a successful post, the browser follows a 303 redirect - to a hosted thank-you page by default, or to a custom URL you set per form, such as https://yourname.github.io/thanks.html. A page you control is the nicer finish: it keeps the visitor on your site and gives you a place to set expectations about response time.
5. Add sl.js if you want inline success
The optional embed upgrades the same form to AJAX submission with inline success text, so the visitor never leaves the page. It also auto-injects the honeypot, adds a timing signal, and captures utm_* plus gclid, fbclid, and the other ad click IDs, persisting them in localStorage across pages - attribution data lands in the sheet next to the submission.
6. Lock it down and test
Set the per-form origin allowlist to your github.io address and custom domain so only your pages may submit. Then send a test - or try the live demo first - and watch the row land. The docs list the responses worth knowing: 404 unknown token, 403 origin not allowed, 429 rate limited, 413 payload too large.
FAQ
Can GitHub Pages handle form submissions by itself?
No. GitHub Pages is static hosting only - it cannot run server code, so it has nowhere to receive a POST. Every working form on a GitHub Pages site posts to an external endpoint; SheetLink's happens to end in a spreadsheet you own rather than an inbox.
Does this work with a custom domain?
Yes. The form posts from whatever origin serves the page, so a custom domain works exactly like username.github.io. If you enable the origin allowlist, list both the custom domain and the github.io address if both stay reachable.
Do I need any JavaScript?
No. The plain form action with the 303-redirect flow is a complete integration with zero script. JavaScript (sl.js) is an optional layer for inline success text and ad attribution capture, not a requirement.
How is spam handled with no server on my side?
Screening happens at the endpoint: the honeypot field, per-form and per-IP rate limits, content heuristics (link stuffing, disposable email domains, empty payloads) with a per-form strictness dial, and optional Cloudflare Turnstile. Suspicious submissions are quarantined for one-click review - approve delivers the row - never silently dropped. Only the honeypot marks spam outright.
Will Jekyll or the GitHub Pages build interfere with the form?
No. The form is plain HTML, and Jekyll's build passes it through unchanged. There are no build plugins to install, no _config.yml entries, and no secrets - the endpoint token is public by design, with abuse handled by the origin allowlist, rate limits, and quarantine.
Also on: Jekyll · Plain HTML · 11ty