Guides

Gatsby contact form that sends email (no functions)

Gatsby renders every page to static HTML, so a contact form can post straight from the browser. One page component, one GATSBY_ environment variable, and a thank-you page.

Last updated November 3, 2026

1Create a FormSMTP project

Sign up free 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 or Zoho guides.

2Lock 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.

gatsby develop serves on http://localhost:8000 by default. Add it to Allowed websites while you test, alongside your live domain.

3Add the key as a GATSBY_ variable

Gatsby only exposes environment variables to browser code when their names start with GATSBY_. It loads .env.development for gatsby develop and .env.production for gatsby build:

bash
# .env.development and .env.production
GATSBY_FORMSMTP_FORM_KEY=fk_your_key

The value is compiled into your JavaScript, so restart gatsby develop or rebuild after changing it. On Netlify or another host, set the same variable in the site’s build settings. The form key is meant to be public; never give the GATSBY_ prefix to your API key (fs_).

4Create the contact page

jsx
// src/pages/contact.js
import * as React from "react";
import { navigate } from "gatsby";

const ENDPOINT = "https://app.formsmtp.com/api/v1/form/" + process.env.GATSBY_FORMSMTP_FORM_KEY;

export default function ContactPage() {
  const [status, setStatus] = React.useState("idle"); // idle | sending | error
  const [error, setError] = React.useState("");
  const loadedAt = React.useRef(Date.now());

  async function handleSubmit(event) {
    event.preventDefault();
    setStatus("sending");
    const fields = Object.fromEntries(new FormData(event.currentTarget));
    try {
      const res = await fetch(ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json", Accept: "application/json" },
        body: JSON.stringify({ ...fields, _subject: "New message from my Gatsby site", _t: loadedAt.current }),
      });
      const data = await res.json();
      if (!data.success) throw new Error(data.error);
      navigate("/thanks/");
    } catch (err) {
      setError(err.message);
      setStatus("error");
    }
  }

  return (
    <main>
      <h1>Contact</h1>
      <form method="POST" action={ENDPOINT} onSubmit={handleSubmit}>
        <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 /></label>
        <input type="hidden" name="_next" value="/thanks/" />
        <input name="_gotcha" style={{ display: "none" }} tabIndex={-1} autoComplete="off" />
        <button disabled={status === "sending"}>{status === "sending" ? "Sending…" : "Send"}</button>
        {status === "error" && <p role="alert">{error}</p>}
      </form>
    </main>
  );
}

export const Head = () => <title>Contact</title>;

Because Gatsby renders this page to HTML at build time, the action and method attributes make the form work before React has loaded, or with JavaScript off: it posts normally and FormSMTP redirects to _next. Once React is running, handleSubmit takes over, sends JSON and uses Gatsby’s navigate. _t is the time the page loaded; FormSMTP drops submissions that arrive within two seconds of it.

5Add the thank-you page

jsx
// src/pages/thanks.js
import * as React from "react";
import { Link } from "gatsby";

export default function ThanksPage() {
  return (
    <main>
      <h1>Thanks!</h1>
      <p>Your message is on its way. We'll reply within a day or two.</p>
      <Link to="/">Back to the homepage</Link>
    </main>
  );
}

export const Head = () => (
  <>
    <title>Thanks</title>
    <meta name="robots" content="noindex" />
  </>
);

Run gatsby build, deploy, and send a test message from your live site. It arrives from your own address and appears under Logs.

What about Gatsby Functions?

If your host runs Gatsby Functions, you can keep a key on the server instead: files in src/api become endpoints, and environment variables without the GATSBY_ prefix never reach the browser. That route uses the secret API key and the full send API:

js
// src/api/contact.js
export default async function handler(req, res) {
  if (req.method !== "POST") return res.status(405).end();
  const { name, email, message } = req.body;
  const r = await fetch("https://app.formsmtp.com/api/v1/send", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.FORMSMTP_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      to: "you@yourdomain.com",
      replyTo: email,
      subject: "New message from " + name,
      text: String(message),
    }),
  });
  const data = await r.json();
  if (!data.success) {
    console.error("FormSMTP:", data.error); // the details stay in your logs
    return res.status(502).json({ ok: false, error: "Sorry, your message couldn't be sent. Please try again later." });
  }
  res.json({ ok: true });
}

Functions need a host that runs them. On a purely static host such as GitHub Pages or an S3 bucket, use the page component above: it needs nothing but static files.

Troubleshooting

  • The request goes to …/form/undefined: the variable is missing the GATSBY_ prefix, isn’t in the .env file for the command you ran, or you didn’t restart after adding it.
  • “This form isn’t allowed on http://localhost:8000”: add the dev origin to Allowed websites, or test the production build on your domain.
  • “The form has no fields”: the inputs have no name attributes, so FormData is empty.
  • Works locally, fails after deploy: the host’s build didn’t have GATSBY_FORMSMTP_FORM_KEY set, or your live domain isn’t in Allowed websites.

FAQ

Why does the variable need the GATSBY_ prefix?

Gatsby keeps other environment variables out of browser code so secrets don’t leak by accident. The form key is safe to expose; your API key is not, so never prefix that one.

Can I use Formik or React Hook Form?

Yes. Keep your form library for state and validation, and make the same fetch() call in its submit handler.

Does it work on GitHub Pages, Netlify or S3?

Yes. The page posts directly to FormSMTP from the browser, so any host that serves Gatsby’s build output works.

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

Get a Founding Pass

10,000 emails a month, free for your first year. Pass No. 0008 is next: 1,993 of 2,000 left, no card.