How to Add a Contact Form to a Jekyll Site on GitHub Pages
Jekyll is the static site generator that powers GitHub Pages, which is part of why it's so popular: push Markdown to a repo and your site is live. The catch shows up the moment you need a contact form. GitHub Pages serves static files only — there's no server to receive a POST request, no database to store a message, and no way to send an email. And because GitHub Pages runs Jekyll in safe mode, you can't install a Jekyll plugin to do it either.
The fix is to hand form submissions off to a form backend. In this tutorial you'll add a fully working contact form to a Jekyll site on GitHub Pages using Static Forms, wrap it in a reusable Jekyll include, and have submissions land in your inbox — without writing or hosting any server code.
Why Jekyll on GitHub Pages can't process forms
A traditional contact form posts to a script on your server that validates the data, stores or emails it, and returns a response. Jekyll produces only HTML, CSS, and JavaScript, and GitHub Pages serves those files as-is. There is no request handler at the other end of your <form>.
Here's what that submission path actually looks like once you route it through a form backend:
Prerequisites
Before you start, make sure you have:
- A Jekyll site deployed to GitHub Pages (or one you can build locally)
- A Static Forms account — sign up for free
- Your API key from the Static Forms dashboard
Step 1: Build the contact form
Jekyll renders Markdown, but it passes raw HTML straight through. So you can drop a standard form into any page or layout. Create a contact.html page at the root of your site:
---
layout: default
title: Contact
permalink: /contact/
---
<h1>Get in touch</h1>
<form action="https://api.staticforms.dev/submit" method="POST">
<input type="hidden" name="apiKey" value="YOUR_API_KEY_HERE" />
<input type="hidden" name="replyTo" value="@" />
<input type="hidden" name="subject" value="New message from my Jekyll site" />
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
<!-- Spam honeypot: bots fill this, humans never see it -->
<input type="text" name="honeypot" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send message</button>
</form>That top block between the --- lines is Jekyll front matter — it tells Jekyll to wrap the page in your default layout and serve it at /contact/. Replace YOUR_API_KEY_HERE with your real key.
A few fields are doing important work:
apiKeyroutes the submission to your account.replyToset to@tells Static Forms to use the submitter's own email as the reply-to address, so you can reply straight from your inbox.honeypotis a hidden field. Real visitors never see it, but spam bots fill in every field they find — so any submission with a value here is silently rejected.
Step 2: Make it a reusable include
If you want the same form in your footer, your about page, and a dedicated contact page, copy-pasting the markup gets old fast. Jekyll includes solve this. Create _includes/contact-form.html:
<form action="https://api.staticforms.dev/submit" method="POST" class="sf-form">
<input type="hidden" name="apiKey" value="{{ site.staticforms_key }}" />
<input type="hidden" name="replyTo" value="@" />
<input type="hidden" name="subject" value="{{ include.subject | default: 'New website message' }}" />
<input type="text" name="name" placeholder="Your name" required />
<input type="email" name="email" placeholder="Your email" required />
<textarea name="message" placeholder="Your message" rows="5" required></textarea>
<input type="text" name="honeypot" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send</button>
</form>Store the key once in _config.yml so it isn't scattered across files:
staticforms_key: YOUR_API_KEY_HERENow drop the form anywhere with a single Liquid tag, optionally overriding the email subject per page:
{% include contact-form.html subject="Contact form — pricing page" %}Because the API key is published in your site's HTML either way (that's true of any client-side form), treat it as a public form key rather than a secret. It only authorizes submissions to your account's endpoint.
Step 3: Commit, push, and test
GitHub Pages rebuilds automatically on every push to your publishing branch:
git add contact.html _includes/contact-form.html _config.yml
git commit -m "Add Static Forms contact form"
git push origin mainGive the build a minute, then open /contact/ on your live site and send yourself a test message. The first time you submit, Static Forms emails you a confirmation link to verify the form — click it once and every submission after that flows straight to your inbox.
Step 4: Send visitors to a thank-you page
By default the API returns a plain success response. For a Jekyll site you almost always want to send visitors to a friendly confirmation page instead. Add a redirectTo field pointing at a thank-you page you build in Jekyll:
<input type="hidden" name="redirectTo" value="https://yourusername.github.io/thank-you/" />Then create thank-you.html with permalink: /thank-you/ and whatever message you like. The full list of supported fields is in the Static Forms documentation.
Step 5: Style the form to match your theme
The default Jekyll theme (Minima) and most others give you sensible base styles, but a contact form usually wants a little polish. Add this to your assets/css/style.scss (or a <style> block in the include) to get clean, full-width fields:
.sf-form {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 32rem;
}
.sf-form input,
.sf-form textarea {
width: 100%;
padding: 0.65rem 0.75rem;
font: inherit;
border: 1px solid #cbd5e1;
border-radius: 6px;
}
.sf-form input:focus,
.sf-form textarea:focus {
outline: 2px solid #7c3aed;
outline-offset: 1px;
border-color: transparent;
}
.sf-form button {
align-self: flex-start;
padding: 0.65rem 1.25rem;
font: inherit;
font-weight: 600;
color: #fff;
background: #7c3aed;
border: none;
border-radius: 6px;
cursor: pointer;
}
.sf-form button:hover {
background: #6d28d9;
}If you're using Minima, the file at assets/css/style.scss should start with the two --- lines (an empty front matter block) followed by @import "minima"; — add your rules after that import so they override the theme.
Step 6: Submit without leaving the page (AJAX)
The plain form works everywhere and needs no JavaScript. But if you'd rather keep visitors on the page and show an inline success message, submit with fetch() instead. Static Forms accepts JSON and returns a { success: true } response you can check:
<form id="contact-form" class="sf-form">
<input type="text" name="name" placeholder="Your name" required />
<input type="email" name="email" placeholder="Your email" required />
<textarea name="message" placeholder="Your message" rows="5" required></textarea>
<input type="text" name="honeypot" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send</button>
</form>
<p id="form-success" style="display:none">Thanks — your message is on its way!</p>
<p id="form-error" style="display:none">Something went wrong. Please try again.</p>
<script>
const form = document.getElementById('contact-form');
const successEl = document.getElementById('form-success');
const errorEl = document.getElementById('form-error');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(form).entries());
try {
const response = await fetch('https://api.staticforms.dev/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...data,
apiKey: 'YOUR_API_KEY_HERE',
replyTo: '@',
}),
});
const result = await response.json();
if (result.success) {
form.style.display = 'none';
successEl.style.display = 'block';
} else {
throw new Error(result.message);
}
} catch {
errorEl.style.display = 'block';
}
});
</script>Because Jekyll passes raw HTML and <script> blocks straight through, you can drop this directly into a page or an include. The honeypot still works in the AJAX version — its value rides along in the JSON payload.
Make the form accessible
A contact form is often the only interactive element on an otherwise static site, so it's worth getting right for keyboard and screen-reader users:
- Pair every input with a
<label>usingfor/id, as in Step 1. Placeholders are not a substitute for labels — they vanish on focus. - Keep the visible focus outline. The CSS above uses a high-contrast outline rather than removing it.
- Mark required fields with the
requiredattribute so the browser enforces them and announces them. - Announce success and errors. If you use the AJAX version, add
role="status"to the success message androle="alert"to the error message so screen readers read them out when they appear.
Going further
Once the basics work, a few upgrades are worth knowing about:
- Stop spam without a CAPTCHA. The honeypot handles most bots. If you start seeing sophisticated spam, you can layer on a CAPTCHA — see our guide to adding a contact form to GitHub Pages for the no-framework version.
- Route submissions somewhere other than email. You can forward every submission to a spreadsheet with the Google Sheets integration, or pipe it into your other tools through webhooks.
- AJAX submit. If you'd rather not navigate away, submit with
fetch()and show an inline success message — as shown in Step 6, or in our JAMstack contact form guide.
Troubleshooting
A few things trip people up on a Jekyll + GitHub Pages setup:
- The form does nothing / the page reloads with no email. Double-check the
actionURL is exactlyhttps://api.staticforms.dev/submitand that you replacedYOUR_API_KEY_HEREwith your real key. A typo in either silently fails. - No email arrives on the first try. The first submission to a new form triggers a one-time verification email. Check the inbox for the address tied to your account (and the spam folder) and click the verification link.
- Liquid is eating your markup. If your form contains literal curly braces (for example in placeholder copy), wrap that section in
{% raw %}...{% endraw %}so Jekyll's Liquid parser doesn't try to interpret it. - Changes aren't showing up. GitHub Pages builds can take a minute or two, and browsers cache aggressively. Wait for the build to finish (check the Actions tab) and hard-refresh.
- The include isn't found. Jekyll includes must live in
_includes/and be referenced by filename:{% include contact-form.html %}. A path prefix will break it.
For anything API-specific, the debugging section of the docs lists the exact response codes the endpoint returns.
Frequently asked questions
Is my API key safe to put in the HTML?
Yes. It's a public form key, not a secret — like any client-side form, the field ships in your page source. It only authorizes submissions to your account's endpoint and can't be used to read your data.
Will this work on a custom domain?
Yes. The form posts to the Static Forms API regardless of where your Jekyll site is hosted, so a custom domain on GitHub Pages works exactly the same.
Do I need a Jekyll plugin?
No — and that's the point. GitHub Pages runs Jekyll in safe mode and blocks third-party plugins, but this approach is pure HTML, so no plugin is involved.
How much does it cost?
There's a free plan that covers a typical personal or small-business contact form. If you need higher volume, file uploads, or advanced spam tools, see the pricing page.
Wrapping up
A Jekyll site on GitHub Pages can't process a form on its own — but it doesn't need to. By posting to a form backend you keep the speed and zero-maintenance hosting of static files while still giving visitors a working contact form. Build the form once as a Jekyll include, store your key in _config.yml, and reuse it across the whole site.
Ready to add yours? Create a free Static Forms account and grab your API key.
Related Articles
How to Add a Contact Form to GitHub Pages
Learn how to add a fully functional contact form to your GitHub Pages site without any backend code. Complete tutorial with working examples.
How to Add a Contact Form to a Hugo Site (No Backend Required)
Add a working contact form to any Hugo site using a reusable partial and shortcode — no server, no plugins. Includes setup for popular themes like PaperMod and Ananke.
How to Add a Contact Form to Google Sites
Learn how to add a working contact form to your Google Sites website using Static Forms — no backend or coding experience required.