Add a contact form to a Jekyll site
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.