# Eleventy (11ty) contact form that sends email

> Build an Eleventy contact form: a Nunjucks include, the form key in global data from an environment variable, a thank-you page, and deploys to any static host.

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

## 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://yoursite.com` 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.

Testing with `npx @11ty/eleventy --serve`? Also add `http://localhost:8080` (or whichever port Eleventy prints) to Allowed websites while you work.

## 3. Expose the key as global data

Files in `_data` become global data, named after the file. A JavaScript data file can read environment variables at build time, so the key stays out of your templates and your repository:

```js
// _data/formsmtp.js
export default function () {
  const key = process.env.FORMSMTP_FORM_KEY;
  if (!key) console.warn("[formsmtp] FORMSMTP_FORM_KEY is not set");
  return {
    endpoint: "https://app.formsmtp.com/api/v1/form/" + key,
    subject: "New message from my Eleventy site",
  };
}
```

This is the ESM syntax used by Eleventy 3 projects with `"type": "module"`. On a CommonJS project, write `module.exports = function () { … }` instead. Run the dev server with the key set:

```bash
FORMSMTP_FORM_KEY=fk_your_key npx @11ty/eleventy --serve
```

On Netlify, Vercel or Cloudflare Pages, add `FORMSMTP_FORM_KEY` in the project’s environment variables instead.

## 4. Create the include

```html
{# _includes/contact-form.njk #}
<form class="contact-form" action="{{ formsmtp.endpoint }}" method="POST" data-contact>
  <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="_subject" value="{{ subject or formsmtp.subject }}">
  <input type="hidden" name="_next" value="/thanks/">
  <input type="hidden" name="_t" value="">
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">

  <button type="submit">Send message</button>
  <p role="status"></p>
</form>
```

Nunjucks includes see the variables of the page that includes them, so a page can set `subject` to change the subject line. Prefer Liquid? Save the file as `contact-form.liquid` and write `{{ subject | default: formsmtp.subject }}`; the rest is the same.

## 5. Add the contact and thank-you pages

```markdown
---
# contact.md
layout: base.njk
title: Contact
templateEngineOverride: njk,md
---

Questions, quotes or just hello.

{% set subject = "New message from the contact page" %}
{% include "contact-form.njk" %}
```

Eleventy processes Markdown files with Liquid by default, which doesn’t understand Nunjucks’ `or`. `templateEngineOverride: njk,md` runs Nunjucks first, then Markdown. In a `.njk` page you don’t need it.

```markdown
---
# thanks.md
layout: base.njk
title: Thanks!
permalink: /thanks/
eleventyExcludeFromCollections: true
---

Your message is on its way. We'll reply within a day or two.
```

`eleventyExcludeFromCollections` keeps the thank-you page out of collections, so it won’t appear in your sitemap or post lists. If your site uses a `pathPrefix`, put the prefix in `_next` too.

## 6. Optional: an inline success message

Add a small script and copy it to the output with passthrough copy. It also fills in `_t`, the page-load time, which lets FormSMTP drop bots that submit within two seconds.

```js
// js/contact-form.js
for (const form of document.querySelectorAll("form[data-contact]")) {
  const status = form.querySelector("[role=status]");
  form.elements._t.value = Date.now();

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    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();
  });
}
```

```js
// eleventy.config.js
export default function (eleventyConfig) {
  eleventyConfig.addPassthroughCopy("js");
}
```

Load it from your layout with `<script src="/js/contact-form.js" defer></script>`. Without JavaScript, the form still posts and lands on `/thanks/`.

## Deploy anywhere

Eleventy builds to `_site` by default, and the form needs nothing from the host, so any static host works: Netlify, Vercel, Cloudflare Pages, GitHub Pages through Actions, or an S3 bucket. Set `FORMSMTP_FORM_KEY` wherever the build runs, add your live domain to Allowed websites, and send a test message from production.

## Troubleshooting

- **The form posts to `…/form/undefined`:** the environment variable wasn’t set when Eleventy built the page. Environment variables are only read in JavaScript files at build time, so set it in your host’s build settings and rebuild.
- **The page shows a Liquid error near `or`:** a Markdown page ran the Nunjucks include through Liquid. Add `templateEngineOverride: njk,md` to its front matter.
- **“This form isn’t allowed on http://localhost:8080”:** add the dev server’s origin to Allowed websites, or test on the deployed site.
- **`/js/contact-form.js` returns 404:** the passthrough copy is missing, or the config file isn’t named `eleventy.config.js`.

## FAQ

### Nunjucks or Liquid?

Either works. This guide uses Nunjucks because its includes see page variables directly. With Liquid, save the include as .liquid and use the default filter for the subject.

### Does it work with Eleventy 2?

Yes. Write the data file with module.exports instead of export default and keep the config in CommonJS. The form itself is plain HTML, so it doesn’t depend on the Eleventy version.

### Is it safe to put the key in my front-end code?

Yes, the form key is made for that. It starts with `fk_`, only works on the form endpoint and always delivers to your own inbox, with per-visitor limits and a spam filter on top. A copied form key can’t email anyone else. Your API key (`fs_`) can, so keep it in server code.

### 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)
