Gatsby contact form

Formgong team ·

A Gatsby contact form can post from the browser to Formgong. Gatsby ships static files, so it has no server to receive the POST. You do not need a Gatsby Function or an email secret. Paste the form and replace the access key. On the free plan, email is a daily digest and Telegram is instant.

React component with fetch

Gatsby is React. A contact form is a component with state and fetch, not only a static HTML tag.

Save the component as src/components/ContactForm.js and render it from src/pages/contact.js.

The handler calls preventDefault, posts FormData, and checks response.ok and result.success. The button stays disabled while the request runs.

On the free plan, email is a daily digest to one address. It goes out the next morning at 08:00 in your time zone and covers the previous day.

Telegram is instant on every plan, including Free. Pro and Business email each lead.

ContactForm.js
// Save as src/components/ContactForm.js
import { useState } from "react";

// Paste your key: replace fk_your_access_key with the key from your Formgong form.
const ACCESS_KEY = "fk_your_access_key";

export default function ContactForm() {
  const [status, setStatus] = useState("idle");
  const [notice, setNotice] = useState("");

  async function onSubmit(event) {
    event.preventDefault();
    const form = event.currentTarget;
    if (!(form instanceof HTMLFormElement) || !form.reportValidity()) return;
    const body = new FormData(form);
    body.set("access_key", ACCESS_KEY);
    body.set("_lang", "en");
    setStatus("sending");
    setNotice("");
    try {
      const response = await fetch("https://formgong.com/submit", {
        method: "POST",
        headers: { Accept: "application/json" },
        body,
      });
      const result = await response.json();
      if (!response.ok || !result.success) {
        setStatus("error");
        setNotice(result.message || "Could not send. Please try again.");
        return;
      }
      form.reset();
      setStatus("success");
      setNotice(result.message || "Sent. Thank you!");
    } catch {
      setStatus("error");
      setNotice("Connection failed. Please try again.");
    }
  }

  const sending = status === "sending";
  return (
    <form onSubmit={onSubmit}>
      <label>{"Name"} <input name="name" autoComplete="name" placeholder={"Your name"} required /></label>
      <label>{"Email"} <input name="email" type="email" autoComplete="email" placeholder={"you@example.com"} required /></label>
      <label>{"Message"} <textarea name="message" placeholder={"How can we help?"} required /></label>
      <div aria-hidden="true" style={{ position: "absolute", insetInlineStart: 0, top: 0, width: 1, height: 1, overflow: "hidden", clipPath: "inset(50%)" }}>
        <input name="botcheck" tabIndex={-1} autoComplete="off" />
      </div>
      <button type="submit" disabled={sending}>{sending ? "Sending…" : "Send"}</button>
      <p role={status === "error" ? "alert" : "status"}>{notice}</p>
    </form>
  );
}
src/pages/contact.js and the Gatsby Head API
src/pages/contact.js and the Gatsby Head API
// src/pages/contact.js
import React from "react";
import ContactForm from "../components/ContactForm";

export default function ContactPage() {
  return (
    <main>
      <h1>Contact</h1>
      <ContactForm />
    </main>
  );
}

export function Head() {
  return <title>Contact</title>;
}
Plain HTML form, if JavaScript is off
Plain HTML form, if JavaScript is off
<form action="https://formgong.com/submit" method="POST">
  <input type="hidden" name="access_key" value="fk_your_access_key">
  <input type="hidden" name="_lang" value="en">
  <label>Name <input type="text" name="name" autocomplete="name" placeholder="Your name" required></label>
  <label>Email <input type="email" name="email" autocomplete="email" placeholder="you@example.com" required></label>
  <label>Message <textarea name="message" placeholder="How can we help?" required></textarea></label>
  <div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
  <label>Leave this field empty<input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
</div>
  
  <button type="submit">Send</button>
</form>
Thanks page
Thanks page
// src/pages/thanks.js
import React from "react";

export default function ThanksPage() {
  return (
    <main>
      <h1>Thanks</h1>
      <p>Your message is in. We will reply by email.</p>
    </main>
  );
}

Moving off Netlify Forms

Netlify reads HTML at the end of the build. A form that exists only after React runs is missed unless you also ship a hidden copy.

That hidden copy uses data-netlify, a honeypot attribute and a form-name field. An AJAX submit posts URL-encoded fields to the site root.

To leave, delete those Netlify attributes and the hidden copy. Point the action, or the fetch URL, at Formgong.

Formgong accepts FormData and JSON. You do not post to "/" and you do not set a form-name field.

Download the submissions you already have from the Netlify forms screen before you switch. Formgong starts with an empty inbox.

Remove data-netlify and point the form at Formgong
Remove data-netlify and point the form at Formgong
// Before: Netlify Forms. Remove data-netlify, netlify-honeypot and form-name.
<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
  <input type="hidden" name="form-name" value="contact" />
  <p hidden><label>Do not fill this<input name="bot-field" /></label></p>
  <label>Email <input type="email" name="email" required /></label>
  <button type="submit">Send</button>
</form>

// After: the React component above posts FormData to Formgong.
// A no-JavaScript form can post here instead. Do not add data-netlify.
<form action="https://formgong.com/submit" method="POST">
  <input type="hidden" name="access_key" value="fk_your_access_key">
  <input type="hidden" name="_lang" value="en">
  <label>Email <input type="email" name="email" required></label>
  <div aria-hidden="true" style="position:absolute;left:-9999px">
    <input name="botcheck" tabindex="-1" autocomplete="off">
  </div>
  <button type="submit">Send</button>
</form>

Gatsby gotchas

These two trips catch the mistakes that show up only after a production build.

SSR and hydration

Gatsby renders the page to HTML at build time, then React hydrates it in the browser.

The first render must match that HTML. Do not read window, and do not render a different tree when the component mounts.

useState is fine when the initial state is the same on the server and in the browser. An empty notice is the right start.

A form that returns null until the client mounts is missing from the built HTML. Visitors then see a flash, and Netlify's scan sees nothing.

gatsby develop and gatsby build

gatsby develop serves the app from a dev server. gatsby build writes static files and the POST still leaves the browser.

Formgong behaves the same in both, because the host never receives the form.

Netlify's form scan runs on the production HTML, not on the develop server. A form that looks fine in develop can be invisible after deploy.

Send one test with gatsby develop, then run gatsby build and open the built page. Both should land in the Formgong inbox.

GATSBY_ names are public

Only environment names that start with GATSBY_ are inlined into the browser bundle. The access key is public, so that prefix is fine.

Never put an SMTP password or a private API key in a GATSBY_ name. It ships in the JavaScript anyone can read.

How forms work in Gatsby

Gatsby turns React pages into static HTML. Its contact-form guide offers a form service, Netlify Forms, or a server you run. Gatsby Functions are the documented server path. That is a function you deploy and keep.

A file in src/pages can hold a plain HTML form. It posts to https://formgong.com/submit with access_key, _lang and botcheck. Only names that start with GATSBY_ reach the browser, and this key is public, so that prefix is fine.

Set it up in Gatsby

  1. Create a form in Formgong and copy the access key. It starts with fk_.
  2. Add src/pages/contact.js and paste the form below. Set the action to https://formgong.com/submit.
  3. Replace fk_your_access_key. An env name such as GATSBY_FORMGONG_KEY is optional. Never put an SMTP password in a GATSBY_ name.
  4. Run gatsby develop and send a test. Then run gatsby build. The POST runs in the browser, so the live build behaves the same.
  5. If the site is on Netlify, do not add data-netlify. A React-only form is invisible to the HTML scan unless you also ship a hidden form with form-name.

Formgong or a Gatsby Function?

Use a Gatsby Function when the submit must change your own database or call a private API.

Use Formgong for contact and lead forms. You skip SMTP, a function to host, and the hidden HTML Netlify needs for React.

Questions and answers

Does Gatsby include a contact form?

No. The official guide lists a form backend, Netlify Forms if you host there, or your own server. The page itself stays static.

Why does Netlify miss my Gatsby form?

Netlify reads the HTML after the build. A form that appears only after React runs is missed. Post to Formgong, or add a hidden static copy.

Can I use fetch and stay on the page?

Yes. Call fetch on https://formgong.com/submit and send Accept: application/json. The plain HTML form still works if JavaScript is off.

Is the access key safe in the Gatsby bundle?

Yes. It only accepts submissions for your form. On Pro you can limit the allowed domains. Do not put private API keys in a GATSBY_ name.

Start free