How to add a contact form to a GitHub Pages site (the complete 2026 guide)
April 5, 2026 · 7 min read
GitHub Pages is one of the best deals on the internet. Free hosting, HTTPS out of the box, custom domains, Jekyll built in, and a CDN that rarely hiccups. I have shipped personal sites, documentation sites, open-source project pages, and small client landing pages on GitHub Pages for years.
There is exactly one thing GitHub Pages will not do for you, and it's the thing this post is about: it cannot run a contact form handler. There is no server. There is no PHP. There is no Node runtime. You cannot set up a /api/contact endpoint because there is no API layer — your site is literally files served from a Git repo.
Which means every "contact form tutorial for GitHub Pages" that starts with "create a serverless function" is already wrong for your situation, or it's secretly telling you to leave GitHub Pages.
Here's the setup that actually works.
The approach in one paragraph
Your HTML form lives in your GitHub Pages repo like any other page. Its action attribute points to a hosted form backend running on someone else's infrastructure. When a visitor submits, their browser POSTs directly to that backend — GitHub Pages is never in the submission path. The backend handles storage, email, spam filtering, and (optionally) a redirect to a thank-you page that lives back on your GitHub Pages site.
No build changes. No Jekyll plugins. No GitHub Actions. No serverless functions. No moving to another host.
Step 1 — Create a form endpoint
Sign up for a hosted form backend. I'll use FormTo because I work on it and know the URLs; the same pattern works with any of the tools I compared in another post.
After you create a form in the dashboard, you'll get a URL like:
https://api.formto.dev/f/your-form-slug
Copy it. This is the piece of infrastructure that will actually receive the submission.
Step 2 — Add the contact page to your repo
In your GitHub Pages repo, create a new file at contact.html (if you're using plain HTML) or contact.md with Jekyll front matter (if you're using Jekyll).
Option A: plain HTML
---
layout: default
title: Contact
---
<main class="contact">
<h1>Get in touch</h1>
<p>Questions, collaborations, or just saying hi — I read every message.</p>
<form method="POST" action="https://api.formto.dev/f/your-form-slug">
<label>
Your name
<input type="text" name="name" required>
</label>
<label>
Email
<input type="email" name="email" required>
</label>
<label>
Message
<textarea name="message" rows="5" required></textarea>
</label>
<!-- honeypot: bots fill this, humans never see it -->
<input type="text" name="website" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px" aria-hidden="true">
<input type="hidden" name="_redirect"
value="https://yourusername.github.io/thanks/">
<button type="submit">Send message</button>
</form>
</main>
Option B: Jekyll
Same file content, with your existing Jekyll layout inherited at the top:
---
layout: page
title: Contact
permalink: /contact/
---
<form method="POST" action="{{ site.contact_endpoint }}">
<label>
Your name
<input type="text" name="name" required>
</label>
<label>
Email
<input type="email" name="email" required>
</label>
<label>
Message
<textarea name="message" rows="5" required></textarea>
</label>
<input type="text" name="website" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px" aria-hidden="true">
<input type="hidden" name="_redirect" value="{{ site.url }}/thanks/">
<button type="submit">Send message</button>
</form>
And in your _config.yml:
contact_endpoint: "https://api.formto.dev/f/your-form-slug"
This way the endpoint URL lives in one place and you can swap it without touching templates.
Step 3 — Add the thank-you page
Create thanks.html (or thanks.md for Jekyll) in the repo root:
---
layout: default
title: Thanks
---
<main class="thanks">
<h1>Got it.</h1>
<p>Your message is in the inbox. I'll reply within a day or two.</p>
<a href="/">← Back home</a>
</main>
The _redirect hidden input in the form tells the backend to send the browser here after a successful submission. The user sees a proper confirmation page on your domain, not a generic vendor page.
Step 4 — Commit, push, done
git add contact.html thanks.html
git commit -m "Add working contact form"
git push
GitHub Pages rebuilds in ~30 seconds. Your contact form is live. Real submissions are captured by the backend, notifications go to your email (or Slack if you wire a webhook), spam is filtered server-side by the honeypot.
Total time: about four minutes, not counting the signup.
Things to know about GitHub Pages specifically
A few gotchas that only bite on GitHub Pages:
The site URL is not localhost. Your local Jekyll server runs at http://127.0.0.1:4000. The live site is at https://yourusername.github.io/reponame/ or your custom domain. The form's _redirect field should use the live URL, not the localhost one. Set it once to the production URL; test from a deployed branch, not locally.
Project sites vs user sites. If your repo is yourusername.github.io, the site is at the root (/contact/). If it's any other repo, the site is at a subpath (/reponame/contact/). Get this wrong and your thank-you redirect points at a 404. Use {{ site.url }}{{ site.baseurl }}/thanks/ in Jekyll to avoid hand-rolling the path.
HTTPS is mandatory. Modern browsers will refuse to submit a form from an HTTP page to an HTTPS endpoint if there's mixed content involved, and some backends (correctly) require HTTPS submissions. GitHub Pages gives you HTTPS for free on both github.io and custom domains — just make sure you have the "Enforce HTTPS" checkbox ticked in your repo settings.
No environment variables. You cannot hide the form endpoint URL in GitHub Pages. It will be in the public HTML. That's fine — form endpoints are meant to be public — but don't put any kind of secret token in the HTML and expect it to stay secret. If you need a token, you need a server, and you need a different host.
No server-side redirects. If you want fancy routing (like /contact/ → /pages/contact.html), you do it with Jekyll's permalink setting, not with a .htaccess file. GitHub Pages ignores .htaccess.
Why this beats the usual "GitHub Pages + Formspree" tutorial
Most guides you'll find online stop at "paste the Formspree URL in the action attribute and you're done." That works, and for a personal site with 5 submissions a month, it is genuinely enough.
What you will run into if your site gets even a little bit of traffic:
- Spam. GitHub Pages has no spam filter. Your backend has to handle it. Make sure you add a honeypot field, and pick a backend that filters aggressively by default.
- Submission limits. Formspree's free tier is 50/month. Web3Forms is 250. FormTo is 25. Know the cap, check it monthly, upgrade before you get surprised.
- Email deliverability. Some backends send notifications from their own domain. Those emails often get filtered. Pick a backend that lets you route notifications to Slack or a webhook instead of relying on email alone.
- Data residency. If your visitors are in the EU and your backend is in the US, you need to know where the data is stored and whether there's a DPA in place. This is not optional for commercial sites. Longer version here.
None of these are reasons not to use GitHub Pages. They're reasons to pick your form backend thoughtfully and revisit it after your first 100 submissions.
The bonus move: Slack notifications
The single biggest quality-of-life upgrade for a GitHub Pages contact form is routing submissions to a Slack channel instead of email. Email is slow, unreliable, and easy to miss. Slack is immediate and hard to ignore.
In your form backend dashboard, paste a Slack incoming webhook URL into the webhook field. Every submission now pings a channel, with the full message body, formatted readably, within a second or two of the user clicking send. If you work on a team, rotate who watches the channel. If you work solo, put the channel on your phone's notifications during business hours.
I wrote more about why this matters in the response-time teardown — the short version is that fast replies are the single biggest driver of conversion on contact forms, and slow inbox notifications are the bottleneck for most small sites.
The one-line summary
GitHub Pages cannot run a contact form handler, and it doesn't need to. Drop a <form action="https://..."> into your HTML, point it at a hosted backend, add a honeypot, add a thank-you page, commit, push, done.
Create a free form — 25 submissions a month, no card, EU-hosted by default — paste the URL into your GitHub Pages contact page, and you're live in four minutes.
If you're also running sites on other stacks, the same pattern works on Astro, Next.js, Hugo, Eleventy, Webflow, and Framer.
More posts