The short answer
Jekyll builds static HTML with no server behind it, so a contact form must POST to an external endpoint. Put the endpoint URL in _config.yml, build the form as an include in _includes/contact-form.html, and reference it with {% include contact-form.html %}. With RG Forms the endpoint is a Google Apps Script web app in your own Google account and submissions land in your own Google Sheet — no plugins are involved, so it works on GitHub Pages' restricted Jekyll build exactly as it does locally.
The setup
Add the endpoint to _config.yml
Remember that changes to
_config.ymlaren’t picked up byjekyll serve— restart it after editing._config.yml form_endpoint: "https://script.google.com/macros/s/AKfycb.../exec" contact_fallback_email: "hello@example.com"Create the include
The
{% raw %}wrapper around the script isn’t strictly required for this snippet, but it’s the habit worth having: the moment you add a template literal with${...}or an object literal spanning{{, Liquid will try to parse it and the build will fail with a confusing error._includes/contact-form.html <form id="contact-form" class="contact-form" data-endpoint="{{ site.form_endpoint }}" data-fallback="{{ site.contact_fallback_email }}"> <label>Name <input name="name" required></label> <label>Email <input type="email" name="email" required></label> <label>Message <textarea name="message" rows="5" required></textarea></label> <input type="text" name="_hp" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px"> <button type="submit">Send message</button> <p id="form-status" role="status" aria-live="polite"></p> </form> {% raw %} <script> (function () { var form = document.getElementById('contact-form'); var status = document.getElementById('form-status'); var button = form.querySelector('button'); form.addEventListener('submit', function (event) { event.preventDefault(); button.disabled = true; status.textContent = 'Sending...'; fetch(form.dataset.endpoint, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify({ tab: 'contact', fields: Object.fromEntries(new FormData(form)) }) }) .then(function (res) { return res.json(); }) .then(function (data) { if (data.result === 'success') { form.reset(); status.textContent = 'Thanks - we will be in touch.'; } else { status.textContent = 'Something went wrong. Please try again.'; } }) .catch(function () { status.textContent = 'Network error. Email us at ' + form.dataset.fallback + '.'; }) .finally(function () { button.disabled = false; }); }); })(); </script> {% endraw %}Include it on your contact page
Works in a layout, a page, or a Markdown file with front matter.
contact.md --- layout: page title: Contact permalink: /contact/ --- Have a question? Send a message and we'll get back to you. {% include contact-form.html %}
GitHub Pages notes
Nothing here needs a plugin, so the restricted GitHub Pages build handles it fine. Custom domains and HTTPS are unaffected — both sides are HTTPS, so no mixed-content warnings.
If you’re also using a custom _config.yml for local development, remember that Pages builds only ever read the committed one. A missing form_endpoint there produces an empty data-endpoint and a form that silently does nothing, which is an unpleasant five minutes to debug.
Confirm the endpoint works
curl -L -X POST "https://script.google.com/macros/s/AKfycb.../exec" \
-H "Content-Type: text/plain" \
-d '{"tab":"contact","fields":{"name":"Ada","email":"ada@example.com","message":"Hello"}}'
# → {"result":"success"} and a new row appears in your Google SheetCommon questions
Do I need a Jekyll plugin?
No. It’s an include and a config value — both core Jekyll, both allowed on GitHub Pages.
Why did my build break after I edited the script?
Almost certainly Liquid parsing something in your JavaScript — {{ in an object literal or a ${...} template literal. Keep the script inside {% raw %} and pass dynamic values in through data- attributes.
Can I use this with a Jekyll theme gem?
Yes. Create _includes/contact-form.html in your own repo and it takes precedence over anything the theme ships.
Is there a no-JavaScript version?
Not with this endpoint — it expects a fetch with a JSON body. Services that accept a plain action= POST and redirect are the ones to look at if no-JS support is a hard requirement.
More answers in the full FAQ.