Contact form on a static site

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.

Preet SutharFounder, Ikiform

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
curl -i -X POST -d "message=hello" https://ada.github.io/notebook/contact.html
Response
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 traitWhat 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 headersA Content-Security-Policy can only come from a <meta> tag in the page
The repository is often publicAnything 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 numbersKeep the contact form to a contact form. Don't collect card details or account passwords through it
What GitHub Pages runs, and where a contact form post has to gowhat GitHub Pages runswhat a visitor's post hitsgit pushto the Pages branchbuildJekyll or an Actionsworkflowstatic filesonusername.github.io/repocontact.htmlserved by PagesIkiformstores, emails yourepo/thanks.htmlserved by Pages405 Not Allowednothing storeddeployPOST303action="/contact"What GitHub Pages runs, and where a contact form post has to gowhat GitHub Pages runswhat a visitor's post hitsgit pushto the Pages branchbuildJekyll or anActions workflowstatic filesonusername.github.io/repocontact.htmlserved by PagesIkiformstores, emails yourepo/thanks.htmlserved by Pages405 NotAllowednothingstoreddeployPOST303action="/contact"
  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
<!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
<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.

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

  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

ProblemWhat to do
405 Not Allowed after pressing SendThe 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 yoursThe _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 spamThe 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.ioA <meta> Content-Security-Policy in your layout lacks https://ikiform.com in form-action or connect-src.

Questions

Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.