Blog
How to add a contact form to GitHub Pages
· 3 min read
GitHub Pages serves static files only - no PHP, no server code, no way to receive a form POST. So a contact form needs an external endpoint to send to. Here are the three real options, and the one-minute version.
Option 1: a form backend (recommended)
Point your form's action at a hosted form endpoint:
<form action="https://formhook.app/f/fh_your-key" method="POST">
<label>Email <input type="email" name="email" required></label>
<label>Message <textarea name="message" required></textarea></label>
<button type="submit">Send</button>
</form>Commit, push, done. Submissions land in a dashboard and ping your phone via push notification. Spam is filtered by a honeypot and rate limits before it ever reaches you. This works identically for plain HTML repos and Jekyll sites (GitHub Pages' native generator - see the Jekyll guide for a themed include, or the plain HTML guide).
Pros: one attribute, no JavaScript required, spam handled, submissions stored.
Cons: it's a third-party service - so check the free-tier limits and retention policy of whichever you pick. Formhook's free tier keeps submissions forever.
Option 2: a mailto: link
<a href="mailto:you@example.com">Email me</a>Honest but weak: it depends on the visitor having a configured mail client (increasingly untrue on desktop), exposes your address to scrapers, gives you no spam protection, and tells you nothing about drop-off. Fine for a hobby README page; costly for anything where enquiries are money. (More on that trade-off in mailto vs contact form.)
Option 3: a serverless function elsewhere
You can keep the site on GitHub Pages and receive the POST with a Cloudflare Worker or similar. It works - but now you're writing and maintaining the receiver: validation, spam filtering, storage, email delivery, and CORS. You've reintroduced exactly the backend GitHub Pages let you avoid. Reasonable if you enjoy it; unnecessary if you don't.
The AJAX upgrade (optional)
The plain HTML form navigates away on submit. To stay on the page:
<script>
document.querySelector("form").addEventListener("submit", async (e) => {
e.preventDefault();
const res = await fetch(e.target.action, {
method: "POST",
body: new FormData(e.target),
headers: { Accept: "application/json" },
});
e.target.outerHTML = res.ok
? "<p>Thanks - message sent.</p>"
: "<p>Something went wrong. Please email us directly.</p>";
});
</script>No build step, no dependencies - it works in a raw GitHub Pages repo.
Custom domain note
If your Pages site runs on a custom domain, make sure your form service's per-form settings (like a CORS allowlist) include that domain, not just username.github.io. In Formhook that's one field in the form's settings.
That's the whole story: GitHub Pages handles your site, a form endpoint handles the one thing it can't. Start free and the form is live before your next git push finishes.
Ship a working form in one line
EU-hosted, submissions kept forever, push notifications on every tier.
Start freeNo credit card · read the docs
Keep reading
- What is a form backend? (And when you need one)A form backend receives, stores, and forwards your website's form submissions so you don't run a server. Here's how they work and when you need one.
- Mailto links vs contact forms: which loses fewer leads?Mailto links fail silently without a mail client and expose your address to spam bots. When a mailto is fine, and when a contact form pays for itself.
- File uploads on a static site: your real optionsStatic hosting can't receive files - but your forms still can. How multipart uploads work through a form backend, plus limits, security, and GDPR storage.