You built a portfolio, a project page, or a documentation site on GitHub Pages. Free hosting, deploys on push — hard to beat. Then you add a contact form, push the code, and… the submit button does nothing. Or it 404s. Or the page just reloads.
GitHub Pages doesn’t handle forms. It’s the most common frustration with the platform, and the docs won’t tell you why or what to do about it.
Here’s what’s going on and how to work around it.
The Problem
GitHub Pages serves static files. HTML, CSS, JavaScript, images — anything that can be stored as a file gets served to visitors as-is.
A contact form needs more than that. When someone fills out your form and clicks “Send,” the browser makes a POST request with the form data. That request needs to hit a server that can receive the data, store it somewhere, and optionally send you an email.
GitHub Pages doesn’t have that server. It’s a CDN, not an application platform. It receives GET requests and returns files. POST requests from form submissions have nowhere to go.
This means a standard HTML form like this doesn’t work on GitHub Pages:
<form method="POST">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
No action URL means the form posts to the current page. GitHub Pages receives a POST request it doesn’t know what to do with. The result is a 405 Method Not Allowed error — or on some paths, a confusing 404.
Why This Happens
GitHub Pages was never designed to process data. It’s a static file host attached to your repository, and its entire job is: take the files in your repo (or the output of a Jekyll build), put them on a CDN, and serve them to visitors.
This is by design. GitHub Pages is free because it’s simple. No server-side runtimes, no databases, no request processing — just file serving. That’s what keeps it free for millions of repositories. Whether your site is at yourusername.github.io or using a custom domain, the constraint is the same.
But it also means GitHub Pages has no mechanism for:
- Receiving form submissions
- Storing data
- Sending email notifications
- Running any server-side code
Compared to platforms like Netlify (which has a form processing layer) or Vercel (which supports serverless functions), GitHub Pages is purely a file server. If your site needs to receive data from visitors, you need something else to handle that part.
How to Add a Contact Form to GitHub Pages
You need an external endpoint to receive the form data. There are three common approaches.
Option 1: Third-party form service
Services like Formspree, Getform, and FormSubmit provide hosted endpoints for form data. You point your form’s action to their URL:
<form action="https://formspree.io/f/your-form-id" method="POST">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
This works without JavaScript. The visitor submits the form, Formspree stores it, and you get an email.
The tradeoffs:
- Another account. You sign up for the form service separately, manage submissions in their dashboard.
- Free tier limits. Formspree allows 50 submissions per month on the free plan. FormSubmit is unlimited but adds branding.
- Third-party data storage. Your visitors’ contact info lives on someone else’s servers.
- Redirect experience. After submission, the visitor is redirected to the form service’s thank-you page unless you configure a custom redirect URL.
For a personal portfolio that gets a few messages a month, this is a reasonable choice. For anything more, you’ll hit the free tier limit quickly.
Option 2: Serverless function
If you want more control, you can write a serverless function to handle submissions. Here’s a minimal Cloudflare Worker:
export default {
async fetch(request, env) {
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "https://yourusername.github.io",
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const data = await request.json();
// Store in KV, D1, or send via email API
console.log(data);
return new Response(JSON.stringify({ success: true }), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://yourusername.github.io",
},
});
},
};
Then submit from your GitHub Pages site via JavaScript:
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form));
const response = await fetch("https://your-worker.workers.dev", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert("Message sent!");
}
});
The tradeoffs:
- CORS is required. Your GitHub Pages site and your Worker are on different domains, so you need proper CORS headers. Miss one and the browser blocks the request silently.
- Two deployments to manage. Your site is on GitHub Pages, your form handler is on Cloudflare (or AWS Lambda, or wherever). They’re separate systems.
- You build everything. Rate limiting, spam protection, data storage, email notifications — that’s all on you.
- Debugging across systems. When a form stops working, the issue could be in your frontend code, CORS config, the serverless function, or the data storage layer.
This gives you full control, but it’s a lot of infrastructure for a contact form.
Option 3: Switch to a platform with built-in forms
The cleanest solution is to use a hosting platform that handles forms natively. You move your site off GitHub Pages and get forms as part of the hosting — no external services.
On ZeroDeploy, you set the form’s action to /_forms/<name> and it works:
<form action="/_forms/contact" method="POST">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
No third-party service. No serverless function. No CORS. The form endpoint is part of your hosting — same domain, same deployment.
Forms are auto-created on the first submission, so there’s nothing to configure on the platform side. After that you can view submissions in the dashboard, export as CSV, or set up email notifications.
If you prefer JavaScript submission (no page reload):
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form));
const response = await fetch("/_forms/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
if (result.success) {
alert("Message sent!");
}
});
Both HTML and JavaScript submissions include built-in rate limiting and honeypot spam protection. For a broader comparison of form options, see How to Add a Contact Form to a Static Site.
Which Approach Should You Use?
| Formspree / Getform | Serverless Function | Built-In (ZeroDeploy) | |
|---|---|---|---|
| Works on GitHub Pages | Yes | Yes (with CORS) | Requires moving off GH Pages |
| Setup time | 5 minutes | 1-2 hours | 2 minutes |
| Extra accounts | Yes | Yes (hosting for function) | No |
| Free tier | 50 submissions/mo | Varies | 100 submissions/mo |
| Spam protection | Varies | Build your own | Built-in |
| Email notifications | Varies by plan | Build your own | Included (Pro) |
| CORS needed | No (HTML form) | Yes | No |
Stay on GitHub Pages with a form service if you just need a basic contact form and are happy with the free tier limits.
Build a serverless function if you need custom logic — validation, integrations, or complex workflows.
Move to a platform with built-in forms if you want forms, analytics, custom domains with SSL, and preview deploys in one place — without patching together multiple services. See our GitHub Pages vs ZeroDeploy comparison for a full breakdown of what each platform includes.
Frequently Asked Questions
Can you add a contact form to a GitHub Pages site?
Yes, but not with GitHub Pages alone. GitHub Pages only serves static files — it can’t process form submissions. You need an external service to receive the form data: a third-party form endpoint like Formspree, a serverless function, or a hosting platform with built-in form handling.
Why don’t forms work on GitHub Pages?
GitHub Pages is a static file server. When a visitor submits a form, the browser sends a POST request — but GitHub Pages only handles GET requests for static files. There’s no server-side code to receive the data, store it, or send you a notification. The POST request simply fails.
What is the easiest way to add a contact form to GitHub Pages?
The quickest option is a third-party form service like Formspree — point your form’s action attribute to their endpoint and you’ll have a working form in five minutes. The simplest long-term option is a hosting platform with built-in forms like ZeroDeploy, where you set the action to /_forms/contact and forms work without any external service.
Wrap Up
GitHub Pages is great for what it is — free, reliable static hosting tied to your repo. But it’s a file server, and file servers don’t process forms. If your site needs a contact form, you need something else handling the backend.
A third-party service like Formspree gets you there fastest if you want to stay on GitHub Pages. If you’re open to switching platforms, ZeroDeploy’s built-in forms handle the problem without extra services or infrastructure.
If you’re dealing with form issues on other platforms, we’ve covered Netlify Forms breaking in React and adding forms to any static site. Or jump straight to the quickstart guide to deploy a site with working forms in under 5 minutes.