Contact form on a static site6 min read
Add a contact form to a GitHub Pages site
GitHub Pages can't run server code, so a form POST gets 405 Not Allowed; post to a form backend instead and get project-site redirects right.
GitHub Pages serves static files only, so a form that posts to your own site gets 405 Not Allowed and the message is lost. Set the form's action to an Ikiform endpoint, add username.github.io and any custom domain to Allowed domains, and redirect to a thank-you page that includes your project site's path. Nothing in your repository or Pages settings changes, and no secret ends up in the public repo.
What's different on GitHub Pages
GitHub Pages publishes what your repository or your Actions workflow builds, and nothing else. Jekyll runs at build time if you use it, then the result is files. There is no request handler, so PHP's mail(), a Node script or a Python CGI form won't run, whatever you commit.
You can see the failure from a terminal. A POST to any page on a Pages site gets this:
curl -i -X POST -d "message=hello" https://ada.github.io/notebook/contact.htmlHTTP/2 405
server: Varnish
content-length: 131
<html>
<head><title>405 Not Allowed</title></head>Four more things set Pages apart from other static hosts, and each one touches the contact form:
| Pages trait | What it means for the form |
|---|---|
Project sites live under a path, username.github.io/repo/ | A thank-you URL of /thanks.html points at the user site, not your project. Every URL needs the /repo/ part |
| No setting for custom response headers | A Content-Security-Policy can only come from a <meta> tag in the page |
| The repository is often public | Anything in it is public. The form id is fine there; a secret API key or an email password never is |
| GitHub says Pages sites shouldn't be used for sensitive transactions like sending passwords or credit card numbers | Keep the contact form to a contact form. Don't collect card details or account passwords through it |
- You push to the branch Pages publishes from.
- Jekyll or your Actions workflow builds the site once.
- Pages serves the result as static files on github.io or your custom domain.
- A visitor submits the contact form. If its action points at your own site, Pages answers 405 Not Allowed.
- With the action set to your Ikiform endpoint, Ikiform stores the message and emails you.
- Ikiform redirects the browser to the thank-you page, which Pages serves like any other file.
The form
For a hand-written site, add contact.html next to index.html. This one is for a project site at ada.github.io/notebook, so the redirect includes /notebook/:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
/>
<title>Contact</title>
</head>
<body>
<h1>Get in touch</h1>
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="about">What is it about?</label>
<select id="about" name="about">
<option value="question">A question about the project</option>
<option value="hire">Work together</option>
<option value="other">Something else</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" minlength="10" required></textarea>
<input type="hidden" name="page" value="docs-home" />
<input type="hidden" name="_redirect" value="https://ada.github.io/notebook/thanks.html" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button type="submit">Send</button>
</form>
</body>
</html>- The
<meta>Content-Security-Policy is optional. Keep it if you have one, and make sureform-actionandconnect-srcincludehttps://ikiform.com. Pages can't send the header for you. - The
aboutselect tells you why someone wrote before you open the message. Change the options to what people actually write to you about. - The hidden
pageinput records which page the form was on, which is useful when the same form sits on several docs pages. - Keep
emailas the field name. Notification emails use it as the reply-to address.
If the same HTML is deployed under several paths, or you're not sure what the final path will be, build the redirect in the browser from the page's own address. Put this after the form:
<script>
document.querySelectorAll('input[name="_redirect"]').forEach((input) => {
input.value = new URL("thanks.html", document.baseURI).href;
});
</script>Using Jekyll? Don't hard-code the URL at all. The Jekyll guide has an include that builds it with absolute_url, which adds your baseurl for project sites. Building with Astro, Vite or Hugo through an Actions workflow? Set the framework's base path to /repo/ so the form's assets load, and use the Hugo or framework guide for the form itself.
Allowed domains on github.io
Add ada.github.io, using your own username. It covers your user site and every project site, because project sites share the same host. Add your custom domain too if you use one; example.com also covers www.example.com.
Never add github.io on its own. Every Pages site would match, including copies of your form on other people's sites. With allowed domains set, a _redirect to https://ada.github.io/notebook/thanks.html is followed; without them, Ikiform ignores it and shows its hosted thank-you page.
_redirect values that start with https:, and a custom domain without a certificate yet serves http: pages.Replacing a mailto: link
Many Pages sites start with a mailto: link in the footer. Swapping it for the form stops new scrapers from reading your address off the page, and visitors without a mail app set up can finally reach you. Two things to know before you delete it:
- The address stays in your repository's history, and in any fork or archive of the old page. The form helps from now on; it doesn't make an address that has been public for years private again.
- Replies come from your own inbox. Notification emails set the sender as the reply-to address, so pressing Reply works the way the
mailto:link did, from the other side.
Ikiform never emails the visitor, so say on the thank-you page when they can expect an answer. If you get more messages than you can answer by email, a webhook can drop each one into Slack or Discord instead.
Check it works
Run this on the live github.io address. Pages has no preview deploys, so test locally first with a local server.
- Add
localhostandusername.github.iounder Allowed domains, plus your custom domain if you have one. - Commit
contact.htmlandthanks.html, wait for the Pages deployment to finish under the repository's Actions tab, and open the live page. - Submit the form. You should land on your own
thanks.html, with the project path in the address bar. - Open Submissions in the dashboard. The new submission is there with every field you sent.
- Check the inbox of each notification recipient. Each gets an email with the fields, and Reply goes to the submitter when the form has an
emailfield. - Nothing arrived? Switch the dashboard's status filter to Spam. Spam gets the same success answer as a real submission.
Gotchas
| Problem | What to do |
|---|---|
405 Not Allowed after pressing Send | The deployed form still posts to your own site. View the page source on github.io and check the action; an old cached build may still be live for a few minutes. |
| Ikiform's thank-you page instead of yours | The _redirect value is missing the repository path, is http:, or the form has no allowed domains. Add username.github.io and use the full https: URL. |
403 origin_not_allowed, or every post lands in spam | The page's domain isn't in the form's Allowed domains. Add it, plus localhost for local tests. Subdomains of a listed domain are allowed. |
| Form posts work locally but the console shows a CSP error on github.io | A <meta> Content-Security-Policy in your layout lacks https://ikiform.com in form-action or connect-src. |
Questions
No. GitHub Pages serves static files and runs no code when a visitor submits a form, so a POST to a Pages URL gets 405 Not Allowed. Point the form's action at a form backend instead.
Yes. The form id ends up in your public HTML anyway. Protect the form with allowed domains and the spam checks, and never commit a secret API key, because a static site has no safe place to keep one.
A path such as /thanks.html points at the root of username.github.io, not at your project. Use the full URL with the repository path, or build it from document.baseURI in the browser.
Yes. The form posts to Ikiform whatever domain serves the page. Add the custom domain under Allowed domains next to username.github.io, and update any _redirect value to use the custom domain once HTTPS is enforced.
No, there is no free plan. Testing is free: you can send 50 test submissions a month from localhost before you pay. To go live, Pro starts at $6/mo for 500 submissions, or you can pay once for 10,000 submissions for $49 or 50,000 submissions for $149, with credits that never expire.
Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.