Home / Guides

Add a contact form to a Jekyll site

Published 2026-07-15

Jekyll's whole appeal is that it gets out of your way — write Markdown, run jekyll build, get a folder of static files you can host anywhere. That simplicity is also exactly why a contact form doesn't "just work": there's no Ruby process running in production, no _config.yml setting that spins up a mail server. GitHub Pages, Netlify's static hosting, or a plain S3 bucket serving your _site output has nothing listening for a form POST.

The fix isn't to add a build plugin or a JavaScript framework. It's to point your <form> at an endpoint that's already built to receive a POST and take it from there — which means the form itself stays exactly what Jekyll already does best: plain HTML.

The form: an include with Liquid

Create _includes/contact-form.html:

<form action="{{ site.form_endpoint }}" method="POST">
  <label>Name
    <input type="text" name="name" required>
  </label>
  <label>Email
    <input type="email" name="email" required>
  </label>
  <label>Message
    <textarea name="message" required></textarea>
  </label>

  <!-- honeypot: real visitors never see or fill this -->
  <input type="text" name="_honeypot" style="position:absolute;left:-9999px" tabindex="-1" autocomplete="off">

  <!-- where to land after a successful submit -->
  <input type="hidden" name="_redirect" value="{{ site.url }}/thanks/">

  <button type="submit">Send</button>
</form>

Pull it into any layout or page with:

{% include contact-form.html %}

That's a complete, working form. No onsubmit, no bundler, nothing to hydrate — a browser with JavaScript disabled, a screen reader, or curl -X POST all get the same result: the browser does a normal HTML form submission, and SubmissionBuddy answers with a 303 redirect to your _redirect URL.

Wiring the endpoint through _config.yml

Rather than hardcode the endpoint URL in the include, set it once in _config.yml:

form_endpoint: "https://api.submissionbuddy.io/f/your-form"

Sign up free, create a form in the SubmissionBuddy dashboard, and you'll get a URL in that shape. Every {{ site.form_endpoint }} reference across your includes and layouts then resolves from a single source, which matters the moment you have a staging Jekyll build pointed at a different form than production.

There's nothing to register about your form's fields on the SubmissionBuddy side — no schema, no field list to keep in sync. Add a phone number input, drop the message field, rename name to full_name, whatever — the endpoint stores whatever JSON, form-encoded, or multipart fields it receives.

Spam protection

The _honeypot input above is a real trap, not decoration. It's a plain text field positioned off-screen — invisible and untabbable for a human, but a form-filling bot doesn't know that and fills it in anyway. When SubmissionBuddy sees it populated, it drops the submission entirely (nothing written to storage, no notification sent) but still returns a normal-looking success, so the bot never learns it failed.

Rate limiting — per visitor IP and per form — runs on every plan automatically, no setup. For persistent bot traffic that gets past a honeypot, paid plans add an invisible proof-of-work CAPTCHA that verifies entirely server-side with no third-party requests or cookies, so it doesn't compromise the simplicity (or the privacy story) of a static Jekyll site.

The thank-you page

_redirect points at a URL, and in Jekyll that's just another page:

---
layout: default
title: Thanks
permalink: /thanks/
---

Thanks for reaching out — we'll get back to you shortly.

Save that as thanks.md in your site root (or wherever your permalink convention lives), and it builds to /thanks/index.html — the exact path referenced in the form's _redirect field above.

What happens after the visitor hits submit

Before SubmissionBuddy sends back any response, the submission is already written to durable storage — nothing else touches it until that's done. From there it flows through independent retryable stages: a database write (so it shows up searchable in your dashboard) and an email notification to your team. If the email step fails momentarily, it retries on its own; the stored record isn't affected either way.

The honest pitch

Free plan: 100 submissions a month, 2 forms, no credit card needed. For a blog, docs site, or small brochure site built with Jekyll, that covers most real-world traffic. Paid plans start at $8/month if you outgrow it. And since there's no client library involved — just an action URL and one _config.yml line — there's nothing to keep updated as Jekyll or its plugin ecosystem evolves.

Stop losing form submissions

SubmissionBuddy stores every submission durably before anything else touches it — 100 free submissions/month, no credit card, live in five minutes.

Start free