# 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.

Published 2026-10-25 · https://ikiform.com/guides/contact-form-static-site/github-pages

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:

`Terminal`:

```bash
curl -i -X POST -d "message=hello" https://ada.github.io/notebook/contact.html
```

`Response`:

```http
HTTP/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 |

Diagram: What GitHub Pages runs, and where a contact form post has to go

1. You push to the branch Pages publishes from.
2. Jekyll or your Actions workflow builds the site once.
3. Pages serves the result as static files on github.io or your custom domain.
4. A visitor submits the contact form. If its action points at your own site, Pages answers 405 Not Allowed.
5. With the action set to your Ikiform endpoint, Ikiform stores the message and emails you.
6. Ikiform redirects the browser to the thank-you page, which Pages serves like any other file.

Code runs on GitHub once per push. Nothing of yours runs when a visitor presses Send, which is why the post has to leave github.io.

## 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/`:

`contact.html`:

```html
<!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 sure `form-action` and `connect-src` include `https://ikiform.com`. Pages can't send the header for you.
- The `about` select tells you why someone wrote before you open the message. Change the options to what people actually write to you about.
- The hidden `page` input records which page the form was on, which is useful when the same form sits on several docs pages.
- Keep `email` as 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:

`contact.html`:

```html
<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](https://ikiform.com/docs/guides/jekyll.md) 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](https://ikiform.com/docs/guides/hugo.md) 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.

> Turn on **Enforce HTTPS** in the repository's Pages settings. Ikiform only follows `_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](https://ikiform.com/blog/form-submissions-to-slack.md) or [Discord](https://ikiform.com/blog/form-submissions-to-discord.md) 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.

1. Add `localhost` and `username.github.io` under **Allowed domains**, plus your custom domain if you have one.
2. Commit `contact.html` and `thanks.html`, wait for the Pages deployment to finish under the repository's **Actions** tab, and open the live page.
3. Submit the form. You should land on your own `thanks.html`, with the project path in the address bar.
4. Open **Submissions** in the dashboard. The new submission is there with every field you sent.
5. 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 `email` field.
6. 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

### Can GitHub Pages process form submissions?

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.

### Is it safe to commit the form endpoint to a public repository?

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.

### Why does my thank-you redirect go to the wrong page on a project site?

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.

### Does the form still work after I add a custom domain to GitHub Pages?

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.

### Is Ikiform free?

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.

## More guides

- [Contact form on a static site](https://ikiform.com/guides/contact-form-static-site.md): Add a working contact form to a static site with no server: where the POST goes, spam, replies by email, and code for every host and framework.
- [Netlify](https://ikiform.com/guides/contact-form-static-site/netlify.md): Run a contact form on a Netlify site without Netlify Forms: no build-time detection, JSON posts from React, and a separate form for deploy previews.
- [Cloudflare Pages](https://ikiform.com/guides/contact-form-static-site/cloudflare-pages.md): Add a contact form to Cloudflare Pages without writing a Function: post to a form backend, set headers in _headers and use your own Turnstile keys.
- [Portfolio website](https://ikiform.com/guides/contact-form-static-site/portfolio-website.md): A portfolio contact form that asks the questions you'd ask on a first call: project, budget, timing and a link, with no server and replies from your inbox.
- [Vercel](https://ikiform.com/guides/contact-form-static-site/vercel.md): A static page on Vercel answers a form POST with 405 Method Not Allowed; here is why, and how to send the form to a backend instead.
- [Amazon S3](https://ikiform.com/guides/contact-form-static-site/aws-s3.md): S3 website endpoints accept only GET and HEAD over plain HTTP, so post the contact form to a form backend and get redirects right with CloudFront.
