Static Generators
FormsFort setup guide for Next.js, Astro, Nuxt, Hugo, Jekyll, Gatsby, Gridsome, and Eleventy.
Static Generators
Ship form handling without adding a backend. Static site generators work best with browser-first submissions. Backend rendering, server actions, and API routes are supported when a form has a paid/manual sender IP safelist.
Supported generators
Next.js
Prefer browser submissions for public forms. Server actions and route handlers need paid/manual server IP safelists.
Astro
Use static HTML forms for content pages, or client scripts for progressive success/error rendering.
Nuxt
Post from the client for public forms. Nitro server routes should be treated as server-side traffic.
Hugo
Drop the HTML form into a partial or shortcode and keep the hidden access key in the rendered page.
Jekyll
Use includes for shared contact forms and publish from GitHub Pages, Netlify, or another static host.
Gatsby
Submit from browser components with fetch or FormData so CORS and domain restrictions see the site origin.
Gridsome
Use a Vue component or static HTML block and post to the public endpoint from the browser.
Eleventy
Use Nunjucks/Liquid includes for reusable forms and test the generated HTML before enabling restrictions.
Reusable static form include
<form action="https://api.formsfort.dev/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="hidden" name="subject" value="New static site submission" />
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<input type="checkbox" name="botcheck" style="display:none" />
<button type="submit">Send</button>
</form>Next.js client component
Keep public FormsFort submissions in a client component so domain restrictions, captcha checks, and browser-origin CORS work the same way they do for static HTML.
"use client";
import { useState } from "react";
export function ContactForm() {
const [result, setResult] = useState("");
async function handleSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
setResult("Sending...");
const response = await fetch("https://api.formsfort.dev/submit", {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
},
body: JSON.stringify({
access_key: "YOUR_ACCESS_KEY",
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
}),
});
const body = await response.json();
setResult(body.message || (response.ok ? "Submitted." : "Submission failed."));
if (response.ok) {
form.reset();
}
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Name</label>
<input id="name" name="name" required />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
<button type="submit">Submit Form</button>
<p role="status">{result}</p>
</form>
);
}Next.js file upload
Upload forms must send multipart FormData and skip the content-type header. FormsFort only accepts uploads when the form has the paid/manual upload entitlement and matching limits.
"use client";
import { useState } from "react";
export function UploadForm() {
const [result, setResult] = useState("");
async function handleSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
formData.set("access_key", "YOUR_ACCESS_KEY");
setResult("Uploading...");
const response = await fetch("https://api.formsfort.dev/submit", {
method: "POST",
headers: { accept: "application/json" },
body: formData,
});
const body = await response.json();
setResult(body.message || (response.ok ? "Uploaded." : "Upload failed."));
if (response.ok) {
form.reset();
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<input name="attachment" type="file" required />
<button type="submit">Upload</button>
<p role="status">{result}</p>
</form>
);
}Astro inline validation
Astro can start with a normal static form and add progressive JavaScript for validation and inline API responses.
<form id="contact-form" class="needs-validation" novalidate>
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send Message</button>
<p id="form-result" role="status"></p>
</form>
<script is:inline>
document.addEventListener("astro:page-load", () => {
const form = document.querySelector("#contact-form");
const result = document.querySelector("#form-result");
form.addEventListener("submit", async (event) => {
event.preventDefault();
form.classList.add("was-validated");
if (!form.checkValidity()) {
form.querySelector(":invalid")?.focus();
return;
}
result.textContent = "Sending...";
const response = await fetch("https://api.formsfort.dev/submit", {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
},
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});
const body = await response.json();
result.textContent = body.message || (response.ok ? "Submitted." : "Submission failed.");
if (response.ok) {
form.reset();
form.classList.remove("was-validated");
}
});
});
</script>Server-side submission checklist
- Use a paid/manual entitlement.
- Add the server egress IP to the form sender IP safelist.
- Keep access keys out of logs.
- Forward only expected user fields.
- Preserve X-Request-Id from the FormsFort response for support.
Frameworks
FormsFort framework guide for HTML, React, React Hook Form, Next.js, Astro, Vue, Svelte, Angular, and Alpine submission patterns.
Builders
FormsFort setup guide for Webflow, Framer, Carrd, Squarespace, Wix, Dorik, WordPress, Elementor, Oxygen, Unbounce, Instapage, Pagewiz, and Groovefunnels.