# Jekyll contact form that sends email (no plugins)

> A Jekyll contact form built as an _includes partial with the key in _config.yml, a thank-you page and baseurl-safe links. Works on GitHub Pages, no plugins.

Source: https://formsmtp.com/guides/jekyll-contact-form · Last updated November 2, 2026

Just want to paste a form into a plain HTML site on GitHub Pages? The [GitHub Pages contact form guide](https://formsmtp.com/guides/github-pages-contact-form) covers that. This guide is for Jekyll’s templating: one reusable include, settings in your config, and links that survive a `baseurl`.

## 1. Create a FormSMTP project

[Sign up free](https://app.formsmtp.com/#/signup) 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](https://formsmtp.com/guides/gmail-smtp-contact-form) or [Zoho](https://formsmtp.com/guides/zoho-smtp-contact-form) guides.

## 2. Lock 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`.

## 3. Add the key to _config.yml

```yaml
# _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`.

## 4. Write the include

```html
<!-- _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.

## 5. Use 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:

```markdown
---
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:

```liquid
{% 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.

## 6. Add the thank-you page

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

```html
<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.key` is empty. Check the indentation in `_config.yml` and restart `jekyll 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:** `_next` was written without `relative_url` on a project site, or `thanks.md` has no `permalink`.
- **“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.

## More guides

- [Next.js contact form with email (App Router)](https://formsmtp.com/guides/nextjs-contact-form)
- [Astro contact form that sends email](https://formsmtp.com/guides/astro-contact-form)
- [React contact form that sends email (Vite, CRA, any React)](https://formsmtp.com/guides/react-contact-form)
- [Vue contact form that sends email (Vue 3 & Nuxt)](https://formsmtp.com/guides/vue-contact-form)
