Give a GitHub Pages site a contact form that actually receives

Pages ships HTML, CSS, and JS from a repository, but a pull request is the only thing it accepts. Forms need a destination, so this guide wires a Pages site to SubmitHarbor's endpoint — paste-ready markup included, with the redirect and bot controls done properly.

The gap a Pages deployment leaves

GitHub's servers will serve your index.html all day, but nothing on that infrastructure is listening for a POST with your name on it. Historically the workarounds were grim: a mailto: link that opens a desktop client, or a third-party widget that injects scripts and styling you do not control. A hosted endpoint sidesteps both — your markup stays yours, and the receiving half runs elsewhere.

The whole integration fits in one file with no build tooling, which suits the way Pages already works.

Markup you can paste into the repository

Drop this into any .html file Pages serves. Jekyll processes templates, but ordinary HTML passes through untouched — no front matter, plugins, or _config.yml edits required.

contact.html
<form
  action="https://www.submitharbor.com/api/submit/sh_your_project_key"
  method="POST"
>
  <p>
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />
  </p>
  <p>
    <label for="message">Message</label>
    <textarea id="message" name="message" required></textarea>
  </p>
  <input
    type="hidden"
    name="_redirect"
    value="https://yourname.github.io/your-repo/thanks.html"
  />
  <!-- Bots fill this; people never see it. -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" hidden />
  <button>Send</button>
</form>

Create a stub thanks.html alongside it, swap the key for your own, and the form is production-shaped. The _redirect input turns the raw JSON answer into a 303 bounce to your page — without it, a successful send leaves the visitor staring at a JSON document.

The origin entries a Pages site needs

With an allow-list configured, the endpoint refuses browser posts from unlisted sites with 403 — worth setting before strangers find your key.

Project settings, one origin per line
https://yourname.github.io
https://your-custom-domain.com
https://www.your-custom-domain.com

An Origin header carries scheme, host, and port — never a path — so one yourname.github.io entry vouches for every repository you publish there. Custom domains resolve differently with and without www, hence both lines. While iterating locally with jekyll serve or a plain Python server, temporarily add http://localhost:4000 or http://localhost:8000, and remove it afterwards.

Walking through a submission end to end

A valid send
The endpoint stores the entry, answers 303, and the browser lands on thanks.html. In the studio inbox the arrival appears with its fields, timestamp, and sending origin, flagged unread.
A mistyped address or empty message
Validation rejects with 422 and a JSON body whose single-sentence error is written to be read. Native forms cannot intercept it, so keep client-side required attributes in place to spare most visitors the trip.
Bot traffic
A crawler that dutifully fills _gotcha receives an unremarkable success while nothing is recorded — no puzzle, no challenge page, no JavaScript involved.
Hammering the send button
More than 20 posts a minute from one address draw a 429 with a Retry-After hint. Impatient clicking during testing trips this more often than real visitors ever will.

Sixty seconds of checks before you push

The action URL names your project
sh_your_project_key is a placeholder. A malformed or unknown key answers 404 regardless of how perfect the rest is.
Both origins are registered
The exact scheme-and-host strings your visitors will browse from, www variant included where applicable.
One real message has landed
Send yourself a genuine submission and open it in the inbox. An untested form on a launched site collects nothing but silence.
A deliberate failure reads well
Submit once with not-an-email and read the 422 page. If the sentence would embarrass you, style an error route — or lean harder on HTML validation attributes.

Confirm the endpoint from a terminal first

Run this now — the shared demo key is open and answers immediately
curl -i -X POST https://www.submitharbor.com/api/submit/demo_contact_7x2p \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","message":"Testing the demo endpoint."}'

The shared demo key validates a submission and echoes it back, then discards it. It never writes to the database, because a stored demo submission would leave a stranger's details in an inbox nobody owns. Create your own endpoint to keep what arrives.

What SubmitHarbor does not do

Worth reading before you build on it. These limits apply today.

  • The shared signed-out demo validates and echoes a preview; durable endpoints, storage, and notifications require a configured signed-in project.
  • SubmitHarbor does not promise an uptime SLA, attachments, CAPTCHA providers, webhooks, CRM integrations, or unlimited submissions.
  • Origin checks and honeypots reduce common abuse but do not replace a complete security and privacy review for sensitive forms.

Questions about GitHub Pages contact form

Does this work for project sites, not just username.github.io?

Yes. Project pages live under the same host, and since origins exclude the path, the single `yourname.github.io` entry covers every repository at once.

My custom domain serves both apex and www — what do I list?

List each host exactly as browsers will visit it. Redirecting one to the other server-side also works, leaving a single entry to maintain.

Can several repositories share one project key?

Technically yes, and the combined inbox will interleave everything. Separate projects per site cost nothing and keep exports clean, so most people split them.

Does the form still work if JavaScript is disabled?

It is JavaScript-free by construction — a native POST, a server-side honeypot, and a 303 redirect. Enhancing it with fetch later is optional, never required.