Just want to paste a form into a plain HTML site on GitHub Pages? The GitHub Pages contact form guide covers that. This guide is for Jekyll’s templating: one reusable include, settings in your config, and links that survive a baseurl.
1Create a FormSMTP project
Sign up free and add a project. Pick your email provider so the SMTP host and port fill in, enter your login, and click Test connection. Not sure which provider? Your existing mailbox works: see the Gmail or Zoho guides.
2Lock the project to your site
In Settings → Security, set Allowed websites to https://yourname.github.io and Allowed recipients to your own address. Now the key in your page can only email you, only from your site, and only up to your hourly limit.
Allowed websites matches the origin only, never the path. https://yourname.github.io covers every project site under it, such as /my-blog/. With a custom domain, add that too. While you test with jekyll serve, also add http://127.0.0.1:4000.
3Add the key to _config.yml
# _config.yml
formsmtp:
key: "YOUR_FORM_KEY"
subject: "New message from my Jekyll site"Jekyll reads _config.yml once at startup, so restart jekyll serve after you change it. Everything under formsmtp is available in templates as site.formsmtp.
4Write the include
<!-- _includes/contact-form.html -->
{% assign subject = include.subject | default: site.formsmtp.subject %}
<form class="contact-form" action="https://app.formsmtp.com/api/v1/form/{{ site.formsmtp.key }}" method="POST">
<label>Name <input name="name" autocomplete="name" required></label>
<label>Email <input name="email" type="email" autocomplete="email" required></label>
<label>Message <textarea name="message" rows="5" required></textarea></label>
<input type="hidden" name="page" value="{{ page.url }}">
<input type="hidden" name="_subject" value="{{ subject | escape }}">
<input type="hidden" name="_next" value="{{ '/thanks/' | relative_url }}">
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
<button type="submit">{{ include.button | default: "Send message" }}</button>
</form>Values passed to an include arrive as include.something, and Liquid’s default filter falls back to your config when a page doesn’t pass one. The hidden page field shows up in the email, so you know which page the message came from.
5Use it in any page or post
Jekyll runs Liquid inside Markdown, so you don’t need a shortcode. Drop the include straight into the content:
---
layout: page
title: Contact
permalink: /contact/
---
Questions, quotes or just hello. We reply within a day or two.
{% include contact-form.html %}Need a second form with its own subject line, say on a pricing page? Pass parameters:
{% include contact-form.html subject="New quote request" button="Get a quote" %}Each form emails you with its own subject, so you can filter them in your inbox.
6Add the thank-you page
---
# thanks.md
layout: page
title: Thanks!
permalink: /thanks/
sitemap: false
---
Your message is on its way. We'll reply within a day or two.
[Back to the homepage]({{ '/' | relative_url }})sitemap: false keeps the page out of sitemap.xml if you use the jekyll-sitemap plugin. Build, push, and submit a test message: it lands in your inbox, sent from your own address, and shows up under Logs.
Why relative_url matters
A GitHub Pages project site lives under a path, such as https://yourname.github.io/my-blog/, and Jekyll knows it as baseurl: /my-blog. FormSMTP resolves _next against your site’s origin and only redirects back to that site. A hard-coded /thanks/ would send visitors to yourname.github.io/thanks/, which doesn’t exist. relative_url adds the baseurl for you, and does nothing on a site at the root of its domain.
Optional: stay on the page
To show a “Thanks!” message without leaving the page, add a small script at the end of the include. Without JavaScript, the form still posts and redirects as before.
<p class="contact-status" role="status"></p>
<script>
// The include can appear more than once on a page: bind each form only once.
document.querySelectorAll(".contact-form:not([data-bound])").forEach((form) => {
form.dataset.bound = "";
form.addEventListener("submit", async (e) => {
e.preventDefault();
const status = form.nextElementSibling;
status.textContent = "Sending…";
const res = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new URLSearchParams(new FormData(form)),
});
const data = await res.json();
status.textContent = data.success ? "Thanks! Your message was sent." : data.error;
if (data.success) form.reset();
});
});
</script>With the Accept: application/json header, FormSMTP replies with JSON instead of redirecting, and _next is ignored.
GitHub Pages or anywhere else
Includes, default and relative_url are all built into Jekyll, so this needs no gems and works with GitHub Pages’ own build. The same site builds unchanged with GitHub Actions, Netlify or Cloudflare Pages: the browser posts to FormSMTP, so there’s no server code to deploy.
Troubleshooting
- The form posts to
…/form/with no key:site.formsmtp.keyis empty. Check the indentation in_config.ymland restartjekyll serve. - “This form isn’t allowed on http://127.0.0.1:4000”: the page’s origin isn’t in Allowed websites. Add it while you test, or test on the live site.
- The thank-you page is a 404:
_nextwas written withoutrelative_urlon a project site, orthanks.mdhas nopermalink. - “File uploads are not supported”: remove
enctype="multipart/form-data"from the form. The form endpoint takes text fields only.
FAQ
Does this need a Jekyll plugin?
No. Includes, the default filter and relative_url are part of Jekyll itself, so it works with the plugins GitHub Pages allows and with any other Jekyll build.
Can I use a different key while developing?
Yes. Put the development key in a second file such as _config.dev.yml and run jekyll serve --config _config.yml,_config.dev.yml. Later files override earlier ones.
Will the key be public in my repository?
The form key will be, like anything in front-end code. That’s what it’s for: it only emails you, and you can lock it to your site under Allowed websites. Never commit the API key (fs_).
Why did my message land in spam?
Usually the From address and the SMTP account don’t match, or the domain has no SPF/DKIM records. Send from the address you log in with, and check the Deliverability panel in FormSMTP’s Settings.