GitHub Pages contact form
Add a working contact form to a GitHub Pages site: point a plain HTML form at Formlane. No server, no Actions. Submissions stored in Frankfurt.
GitHub Pages only serves static files, so a form with action="/contact" or a contact.php has nothing to post to. Formlane is a hosted form endpoint: it receives the POST, filters spam, stores each submission and emails it to your account email. You don't need a serverless function, a GitHub Action or an SMTP password in your repository.
Steps
- Sign in to Formlane, create a form and copy its access key (
fl_…). - Paste the form below into your page, a Jekyll include or a plain
contact.html, and replace the access key and both URLs. - Add a
thanks.htmlpage to your repository. For a project site, the redirect URL includes the repository path. - In the form settings, restrict the form to your GitHub Pages host (for example
username.github.io) or your custom domain. - Commit and push, wait for the Pages deployment, then submit the live form once and check your inbox and dashboard.
Code
<form action="https://smallwerk.com/formlane/api/submit" method="POST" accept-charset="UTF-8"> <input type="hidden" name="access_key" value="fl_YOUR_ACCESS_KEY"> <input type="hidden" name="subject" value="New message from username.github.io"> <input type="hidden" name="redirect" value="https://username.github.io/repo-name/thanks.html"> <!-- Honeypot: hidden from people, leave it unchecked --> <input type="checkbox" name="botcheck" style="display:none" tabindex="-1"> <label for="cf-name">Name</label> <input id="cf-name" type="text" name="name" autocomplete="name" required> <label for="cf-email">Email</label> <input id="cf-email" type="email" name="email" autocomplete="email" required> <label for="cf-message">Message</label> <textarea id="cf-message" name="message" rows="5" required></textarea> <button type="submit">Send</button> </form>
The key is public by design: it only allows sending submissions to this one form. To be safe, restrict the form to your domain in its settings. In Jekyll, the same markup works as _includes/contact-form.html with {% include contact-form.html %} on your contact page.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Thanks</title> </head> <body> <h1>Thanks!</h1> <p>Your message was sent. We'll get back to you soon.</p> <p><a href="./">Back to the site</a></p> </body> </html>
For a user site (username.github.io without a repository path) or a custom domain, the redirect is https://username.github.io/thanks.html or https://example.com/thanks.html. Without a redirect field, Formlane shows its own hosted thank-you page.
Optional: stay on the page
If you'd rather show a message without a page change, send the form with fetch(). With Accept: application/json, Formlane answers {"success": true, "message": "Form submitted successfully", "id": "…"} or {"success": false, "error": "limit_reached", "message": "…"} with a 4xx/5xx status. message is always English, so the snippet below shows its own text per error code.
<script>
const MESSAGES = {
limit_reached: "This form can't take more messages this month. Please email us directly.",
rate_limited: "Too many messages in a short time. Please try again in a few minutes.",
origin_not_allowed: "This form only accepts messages from its own website.",
};
const form = document.querySelector('form[action*="formlane"]');
if (form) form.addEventListener("submit", async (e) => {
e.preventDefault();
const res = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const json = await res.json().catch(() => ({}));
// Spam caught by the honeypot or the content rules also gets success: true.
form.outerHTML = json.success
? "<p>Thanks! We'll get back to you.</p>"
: "<p>" + (MESSAGES[json.error] || "Sorry, that didn't work. Please try again later.") + "</p>";
});
</script>Allowed domains on GitHub Pages
| Your site | Enter as allowed domain |
|---|---|
User or organization site https://username.github.io/ | username.github.io |
Project site https://username.github.io/repo-name/ | username.github.io (the path is not part of the origin) |
Custom domain https://www.example.com/ | example.com (subdomains such as www match) |
Don't enter github.io on its own: subdomains match, so every GitHub Pages site could post to your form. Formlane checks the browser's Origin header and falls back to Referer; a blocked submission gets the error origin_not_allowed.
Spam protection without a captcha
- Honeypot: the hidden
botcheckcheckbox (_gotchaand_honeyalso work). If a bot ticks it, Formlane returns a normal success response and stores nothing. - Rate limit: 10 submissions per 10 minutes per visitor and form.
- Content rules: messages with 4 or more links, link markup or typical spam phrases go to a separate spam tab. Spam doesn't count toward your monthly limit.
- Allowed domains: see the table above.
Where the data is stored
Submissions are stored in Frankfurt, in a Supabase database in the eu-central-1 region. The app runs on Vercel with compute in Frankfurt; static assets and the edge network are global. Notification emails are sent through Resend, a US company. All providers are listed in the privacy policy. Formlane is run by Mindcaptain Services OÜ in Tallinn, Estonia. A data processing agreement is included in the Terms (Art. 28 GDPR, smallwerk.com/dpa), and IP hashes are deleted after 30 days.
Related guides
FAQ
Can GitHub Pages handle a form on its own?
No. GitHub Pages only serves static files, so a form needs an external endpoint to receive the POST. Your site stays on GitHub Pages; only the form action points to Formlane.
Does this work with Jekyll and a project site under /repo-name/?
Yes. The form is plain HTML, so it works in a Jekyll layout, an include or a plain .html file. For a project site, use the full URL including the repository path as the redirect, for example https://username.github.io/repo-name/thanks.html.
What do I enter as allowed domain?
Enter the host your visitors see, for example username.github.io or your custom domain. Don't enter github.io alone: subdomains match, so that would allow every GitHub Pages site.
Will I get an email for each message?
Yes. Formlane sends an email notification for each submission to your account email, with reply-to set to the sender's address. Every submission is also in your Formlane dashboard, where you can export it as CSV. Suspected spam and test submissions from the dashboard are not emailed.
Is the access key safe in a public repository?
Yes. The key only allows sending submissions to this one form; it can't read submissions or change settings. To be safe, restrict the form to your domain in its settings.
Can visitors upload files?
Not yet. Formlane accepts text fields only.
Get your access key
Free for 250 submissions a month. Submissions stored in Frankfurt.
Get your access key