Gatsby
A Gatsby form that already works in the static HTML before hydration, enhanced with fetch after, plus a Gatsby Function for posts with a secret key.
Gatsby renders every page to HTML at build time. Give the form a real action and it posts before React hydrates, or if the bundle fails to load; once hydrated, the component posts with fetch and shows the result in place.
Post from the browser
Add the form id
.env.production GATSBY_IKIFORM_FORM_ID=frm_xxxxxxxxxxxxxxxxGatsby only exposes variables that start with
GATSBY_to browser code, and it inlines them at build time. Add the same line to.env.developmentforgatsby develop.Add the component
src/components/contact-form.js import * as React from "react"; const endpoint = `https://ikiform.com/f/${process.env.GATSBY_IKIFORM_FORM_ID}`; export default function ContactForm() { const [status, setStatus] = React.useState("idle"); const [message, setMessage] = React.useState(""); async function onSubmit(event) { event.preventDefault(); setStatus("sending"); try { const response = await fetch(endpoint, { method: "POST", headers: { Accept: "application/json" }, body: new FormData(event.currentTarget), }); const result = await response.json(); if (result.ok) return setStatus("sent"); setMessage(result.error.message); } catch { setMessage("Couldn't reach the server. Try again."); } setStatus("error"); } if (status === "sent") return <p>Thanks, we got it.</p>; return ( <form action={endpoint} method="post" onSubmit={onSubmit}> <label> Email <input name="email" type="email" autoComplete="email" required /> </label> <label> Message <textarea name="message" rows={5} /> </label> <input type="hidden" name="_redirect" value="https://example.com/thanks/" /> <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden /> {status === "error" ? <p role="alert">{message}</p> : null} <button disabled={status === "sending"}>Send</button> </form> ); }Render it from any page, for example
src/pages/contact.js, and addsrc/pages/thanks.jsfor the no-JavaScript redirect. For field errors and the SDK's human check, extend it as in the React guide.
Post from a Gatsby Function
A Gatsby Function can forward the post with a secret key, which skips allowed domains and the human check. It only runs on hosts that support Gatsby Functions, such as Netlify with Gatsby's adapter; a purely static host like GitHub Pages can't run it. Store IKIFORM_FORM_ID and a secret key with the submissions:write scope as IKIFORM_SECRET_KEY, without the GATSBY_ prefix so they never reach the browser.
export default async function handler(req, res) {
if (req.method !== "POST") return res.status(405).end();
const response = await fetch(`https://ikiform.com/f/${process.env.IKIFORM_FORM_ID}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
},
body: JSON.stringify(req.body),
});
res.status(response.status).json(await response.json());
}Gatsby parses JSON and form-encoded bodies into req.body, so the page can post to /api/contact either way. All posts then share your function's IP address for rate limits; see From a server.
Allowed domains
This applies to browser posts. Posts from the Gatsby Function use a secret key and skip the check.
| Where the form runs | Add to Allowed domains |
|---|---|
gatsby develop and gatsby serve | localhost. It matches http://localhost:8000 and http://localhost:9000: Ikiform compares the host name only, never the port. |
| Your domain | example.com, which also covers www.example.com |
| Netlify | your-site.netlify.app. Deploy previews (deploy-preview-12--your-site.netlify.app) and branch deploys (branch--your-site.netlify.app) are not subdomains of it, so add the exact preview host while you test one. |
| Cloudflare Pages | your-project.pages.dev. Preview URLs such as abc123.your-project.pages.dev are subdomains of it, so they already match. |
- Never add
netlify.apporpages.devon its own. Every site on that host would match, including other people's.
Test checklist
- Run
gatsby buildand openpublic/contact/index.html. The form'sactionshould end in your form id, notundefined. - Run
gatsby servewithlocalhostin allowed domains and submit. The thanks message should replace the form. - Turn JavaScript off, reload and submit. You should see Ikiform's thank-you page locally, or
/thanks/on yourhttps:domain. - If you use the function, post to
/api/contacton a deploy preview and confirm the submission appears in Ikiform.
Troubleshooting
| What you see | Why | Fix |
|---|---|---|
404 not_found, and the action ends in /f/undefined | The variable isn't prefixed with GATSBY_, or wasn't set at build time | Name it GATSBY_IKIFORM_FORM_ID and rebuild. |
The function answers 403 forbidden | The secret key is from another workspace, revoked, or missing submissions:write | Create a secret key with that scope. |
/api/contact returns 404 after deploying | The host serves static files only | Deploy to a host with Gatsby Functions, or use the browser version. |
Submissions arrive under the spam filter with origin_mismatch | The page that posted isn't on the form's allowed domains | Add the host from the table above. With Reject chosen, the same posts get 403 origin_not_allowed instead. |