How to Add a Contact Form to Jekyll
Last updated: March 2026
Jekyll is a Ruby-based static site generator and one of the most popular tools for building sites on GitHub Pages. It takes your Markdown content and Liquid templates, runs them through a build process, and produces a folder of plain HTML files ready to deploy. Because the output is entirely static, there's no server-side code running on your site, so there's no built-in way to handle form submissions.
If you add a <form> tag to a Jekyll page, it renders as HTML but has nowhere to send the data. You need an external form backend to receive submissions, store them, and send you email notifications. This guide walks you through adding a fully working contact form to your Jekyll site using FormWit. No Ruby gems, no plugins, no server code. Just HTML that works on GitHub Pages and every other static host.
How form submissions work on a static Jekyll site
When Jekyll builds your site, it outputs static HTML, CSS, and JavaScript files. There's no Ruby process running in production, no database, and no server-side request handling. When a visitor fills out a form on your Jekyll site, the browser sends a POST request to whatever URL you specify in the form's action attribute. If that URL points to a form backend like FormWit, the backend receives the data, validates it, checks for spam, stores the submission, and sends you an email notification.
The complete flow looks like this:
- A visitor fills out the contact form on your Jekyll site
- The browser sends the form data to your FormWit endpoint via a standard POST request
- FormWit validates the submission and filters out spam
- The submission is saved in your dashboard and you receive an email alert
- The visitor sees a confirmation, either a redirect to a thank-you page or an inline success message if you use AJAX
This works because the form submission happens entirely in the browser at runtime. It has nothing to do with Jekyll's build process, Ruby, or Bundler. Your site stays fully static.
Step 1: Create a FormWit account
Go to app.formwit.com/auth/signup and create a free account. No credit card required. Once signed in, click Create Form and give it a name (e.g., "Jekyll Contact Form"). You'll get a unique endpoint URL that looks like https://app.formwit.com/api/s/YOUR_FORM_ID. Copy this endpoint. You'll use it in the next step.
Step 2: Create the contact page with the HTML form
In your Jekyll project, create a file called contact.html (or contact.md) in the root directory. Add YAML front matter at the top so Jekyll processes it with your site layout. Here's a complete contact page:
---
layout: default
title: Contact Us
permalink: /contact/
---
<h1>Contact Us</h1>
<p>Have a question or want to get in touch? Fill out the form below and we'll get back to you.</p>
<form action="https://app.formwit.com/api/s/YOUR_FORM_ID" method="POST">
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit">Send Message</button>
</form> Replace YOUR_FORM_ID with the endpoint ID from your FormWit dashboard. The layout: default front matter tells Jekyll to wrap the page in your site's default layout, so it picks up your header, footer, navigation, and styles automatically. The permalink: /contact/ creates a clean URL at yoursite.com/contact/.
If you prefer to write your contact page in Markdown, use contact.md instead. The HTML form block will pass through Markdown processing unchanged since Markdown allows inline HTML. For a deeper dive on building HTML forms, see the HTML contact form guide.
Step 3: Add honeypot spam protection
Bots crawl the web looking for forms to spam. A honeypot is a hidden field that real visitors never see or fill in, but automated bots fill it out because they don't know it's invisible. FormWit checks for the _gotcha field. If it contains a value, the submission is flagged as spam and discarded.
Add this hidden input inside your form, just before the submit button:
<!-- Honeypot field — invisible to real visitors, catches bots -->
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" /> The style="display:none" hides the field visually, tabindex="-1" prevents keyboard users from accidentally tabbing into it, and autocomplete="off" stops browsers from auto-filling it. This is a simple, JavaScript-free layer of spam protection that works on every static site. For additional spam protection techniques, see the spam protection guide.
Here's the complete form with the honeypot included:
<form action="https://app.formwit.com/api/s/YOUR_FORM_ID" method="POST">
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<!-- Honeypot -->
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send Message</button>
</form> Step 4: Optional AJAX submission with vanilla JavaScript
By default, the form performs a standard browser POST, which navigates the visitor away from the page. If you want to submit the form without a page reload and show a success message inline, add a small script to your contact page. This uses vanilla JavaScript with no libraries or dependencies:
<div id="form-status"></div>
<script>
const form = document.querySelector('form');
const status = document.getElementById('form-status');
form.addEventListener('submit', async function (e) {
e.preventDefault();
status.textContent = 'Sending...';
const data = new FormData(form);
try {
const response = await fetch(form.action, {
method: 'POST',
body: data,
});
if (response.ok) {
form.reset();
status.textContent = 'Message sent successfully!';
status.style.color = 'green';
} else {
status.textContent = 'Something went wrong. Please try again.';
status.style.color = 'red';
}
} catch (error) {
status.textContent = 'Something went wrong. Please try again.';
status.style.color = 'red';
}
});
</script> Place the <div id="form-status"> element wherever you want the status message to appear, typically right below the submit button. The script intercepts the normal form submission, sends the data via fetch, and updates the status text based on the response. No jQuery, no external libraries, and it works in all modern browsers.
Jekyll-specific tips
Use Liquid variables for the form endpoint
Instead of hardcoding the FormWit endpoint URL in every page that uses the form, you can store it in your site configuration. Add the endpoint to _config.yml:
# _config.yml
formwit_endpoint: "https://app.formwit.com/api/s/YOUR_FORM_ID" Then reference it in your form using Liquid's double-brace syntax:
<form action="{{ site.formwit_endpoint }}" method="POST">
<!-- form fields here -->
</form> This makes it easy to update the endpoint in one place if you ever change it, and it keeps your templates clean.
Create a reusable form partial with _includes
If you want to use the same contact form on multiple pages (your contact page, a sidebar, a footer), extract the form into a Jekyll include. Create a file at _includes/contact-form.html:
<!-- _includes/contact-form.html -->
<form action="{{ site.formwit_endpoint }}" method="POST" class="contact-form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send Message</button>
</form> Then include it in any page or layout with one line:
{% include contact-form.html %} Jekyll's include system works in any .html or .md file that has front matter. You can place the include in your contact page, embed it in a layout, or add it to a sidebar partial. Changes to the form only need to happen in one file.
GitHub Pages deployment: no gems needed
One of Jekyll's biggest advantages is its native support on GitHub Pages. When you push your Jekyll source to a GitHub repository, GitHub builds and deploys your site automatically. The contact form approach in this guide is fully compatible with GitHub Pages because it relies only on standard HTML. No custom gems, no plugins, no server-side processing.
GitHub Pages runs Jekyll in safe mode, which disables custom plugins. Many form-handling approaches that depend on Ruby gems or custom generators won't work on GitHub Pages at all. Since FormWit handles everything via a standard HTML form POST to an external endpoint, there are no gems to install and nothing that conflicts with GitHub Pages' safe mode restrictions.
Your deployment workflow stays the same:
git add contact.html
git commit -m "Add contact form"
git push origin main GitHub Pages builds your site, the contact page is included in the output, and the form works immediately.
Works with any Jekyll theme
The form uses standard HTML elements, so it works with any Jekyll theme: Minima, Minimal Mistakes, Just the Docs, Chirpy, or any custom theme. The form inherits your theme's default styles. If you want to customize the appearance further, add CSS to your site's stylesheet or use a <style> block in the contact page itself.
Using collections and data files
Jekyll's collections and data files (_data/) let you manage structured content. You could store your form configuration in a YAML data file and reference it with Liquid, keeping your templates even cleaner. For example, create _data/forms.yml:
contact:
endpoint: "https://app.formwit.com/api/s/YOUR_FORM_ID"
fields:
- { name: "name", type: "text", label: "Name", required: true }
- { name: "email", type: "email", label: "Email", required: true }
- { name: "message", type: "textarea", label: "Message", required: true } Then you could loop over the fields in your include with Liquid:
<form action="{{ site.data.forms.contact.endpoint }}" method="POST">
{% for field in site.data.forms.contact.fields %}
<div class="form-group">
<label for="{{ field.name }}">{{ field.label }}</label>
{% if field.type == "textarea" %}
<textarea id="{{ field.name }}" name="{{ field.name }}" rows="5"
{% if field.required %}required{% endif %}></textarea>
{% else %}
<input type="{{ field.type }}" id="{{ field.name }}" name="{{ field.name }}"
{% if field.required %}required{% endif %} />
{% endif %}
</div>
{% endfor %}
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send Message</button>
</form> This is entirely optional but shows how Jekyll's data-driven approach can make form management more flexible, especially if you have multiple forms across your site.
Adding a custom redirect after submission
By default, after a standard (non-AJAX) form submission, FormWit can redirect the visitor to a page of your choice. Add a hidden field with the redirect URL:
<input type="hidden" name="redirect_to" value="https://yoursite.com/thank-you/" /> Create a thank-you.html page in your Jekyll project with front matter and a confirmation message:
---
layout: default
title: Thank You
permalink: /thank-you/
---
<h1>Thank you!</h1>
<p>Your message has been received. We'll get back to you as soon as possible.</p>
<p><a href="/">Return to the homepage</a></p> This keeps visitors on your site after submission and gives you full control over the confirmation page's design and content.
Why not use a Jekyll plugin or gem?
There are a handful of Jekyll plugins and Ruby gems that attempt to add form handling, but they come with significant trade-offs:
- Most require a separate server or serverless function to process submissions
- Custom plugins don't work on GitHub Pages (safe mode blocks them)
- You need to manage API keys, environment variables, and email provider configuration
- No built-in submission dashboard or spam protection
- More dependencies in your Gemfile means more maintenance and potential version conflicts
Using a form backend like FormWit avoids all of these issues. Your Jekyll site stays fully static with zero additional dependencies. The form is plain HTML, it works on GitHub Pages, Netlify, Vercel, Cloudflare Pages, S3, or any static host.
What you get with FormWit
- Email notifications for every submission
- A dashboard to view, search, and export submissions
- Built-in spam protection (honeypot + rate limiting)
- File uploads, webhooks, and CSV export on paid plans
- 100 free submissions per month on the free plan
- Works with any static host, no server required
Summary
Jekyll generates static sites with no server-side form handling, but adding a working contact form is straightforward with a form backend. Create your FormWit endpoint, add a standard HTML form to a Jekyll page or include, and you get email notifications, spam protection, and a submission dashboard without any gems, plugins, or server code. The approach works on GitHub Pages, is compatible with any Jekyll theme, and can be reused across your site with Liquid includes and data files.
FormWit's free plan includes unlimited forms, 100 submissions per month, email notifications, and built-in spam protection. Create your free account and add a contact form to your Jekyll site in minutes.
Related guides: HTML contact form · Hugo contact form · Eleventy contact form · Spam protection · Contact form templates
Frequently asked questions
Can I add forms to Jekyll on GitHub Pages?
Yes. GitHub Pages runs Jekyll in safe mode (no custom plugins), but FormWit uses a standard HTML form POST to an external URL. No gems or plugins are involved, so it works on GitHub Pages without any workarounds. Just push your HTML and the form works immediately.
Does FormWit work without a server?
Yes. That is the entire point. FormWit is the server. Your Jekyll site is static HTML served from a CDN. When a visitor submits the form, the browser sends a POST request directly to FormWit's endpoint. No Ruby process, no Node.js server, and no serverless functions needed on your end.
How do I test the form locally?
Run bundle exec jekyll serve and visit your contact page at localhost:4000. Submit the form with test data. The submission goes to FormWit's live endpoint even from localhost, so you will see it in your dashboard and receive an email notification. No special local configuration needed.
Want to skip the setup?
FormWit gives you a form endpoint in 60 seconds. Free plan, no credit card.
Need a form fast?
Build one visually with our free HTML form generator — no coding required.
Try the Form Generator →Add FormWit to your Jekyll site
Add a contact form to your site in 30 seconds. No backend code required.
Try FormWit Free