Next.js static export guide

Next.js static export contact form without backend

Build a real contact form for output: "export", GitHub Pages, Cloudflare Pages, Vercel static output, and other static hosts without API routes or server actions.

If you searched for next.js static export contact form without backend, you are probably at the point where the UI is done and the deployment target is the problem. The form works locally through an API route, or a tutorial told you to use a server action, but your production build is static files. There is no Node process waiting for /api/contact.

The fix is to stop treating the contact form as an internal API problem. A browser can post a normal HTML form to an external endpoint. Your Next.js app stays static, your host only serves files, and the form backend handles delivery, spam checks, redirects, webhooks, and integrations. FormsFort is one option for that endpoint; the same architecture is covered more generally in the HTML form without backend guide.

Why static export breaks API routes and server actions

Next.js can render pages in several ways. With a server deployment, API routes, route handlers, middleware, and server actions can execute when a request arrives. With static export, Next.js writes files to disk. The output is HTML, CSS, JavaScript, images, and other assets that a static host can serve directly.

That difference matters for forms. After export, a file host can serve /contact/index.html, but it cannot execute app/api/contact/route.ts. A POST to /api/contact often becomes a 404 because no file exists at that path, or a 405 because the host can only serve a static asset and does not accept POST there.

Server actions have the same issue. They are server features. They can be compiled into a server deployment, but they do not give GitHub Pages or an object storage bucket a request handler. For static export, design the form around what the browser can do by itself: submit fields to an absolute URL.

The form action is the backend boundary

A plain HTML form already knows how to make an HTTP request. The action attribute is the destination. The method controls the verb. Inputs with a name become the submitted payload. When the user clicks submit, the browser sends the form body to the action URL.

In a static Next.js site, the action should point outside your app:

<form action="https://api.formsfort.com/submit" method="POST">
  ...
</form>

That URL receives the submission and performs the work your exported site cannot perform: validation, abuse filtering, email delivery, webhook dispatch, and optional integrations. If you need a broader endpoint overview before wiring Next.js, read the form endpoint guide.

Static export configuration

Your Next.js config can stay focused on static output. You do not need an API route for the contact form, and you do not need a rewrite to proxy the request.

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "export",
  trailingSlash: true,
};

export default nextConfig;

trailingSlash is optional, but it is common for GitHub Pages and static hosts that prefer directory-style routes. The important line is output: "export". Once you choose that deployment mode, keep form processing outside the app.

A simple React contact form component

This component works in the App Router or Pages Router because it renders ordinary markup. It can live at app/contact/page.tsx, inside a shared component, or in a static page.

export default function ContactForm() {
  return (
    <form action="https://api.formsfort.com/submit" method="POST">
      <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
      <input
        type="hidden"
        name="redirect"
        value="https://example.com/contact/thanks/"
      />

      <div>
        <label htmlFor="name">Name</label>
        <input id="name" name="name" type="text" autoComplete="name" required />
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          autoComplete="email"
          required
        />
      </div>

      <div>
        <label htmlFor="message">Message</label>
        <textarea id="message" name="message" rows={6} required />
      </div>

      <input
        type="checkbox"
        name="botcheck"
        tabIndex={-1}
        autoComplete="off"
        style={{ display: "none" }}
      />

      <button type="submit">Send message</button>
    </form>
  );
}

Every submitted field needs a name. The labels and browser validation are for the user. The names are what the endpoint receives.

Fields to include

Field Purpose Static export note
access_key Routes the submission to the right form. This is public form configuration, not a private server secret.
name, email, message The actual contact form payload. Use normal inputs with stable name attributes.
botcheck Honeypot field for basic spam filtering. Hide it from people; automated spam often fills it anyway.
redirect Where the browser goes after a successful no-JS submit. Use an absolute URL or a route your static host serves.
captcha_token Optional stronger abuse protection. Generate it in the browser, then submit it with the form.

Do not put a private API key in a hidden input or in NEXT_PUBLIC_ variables. A hidden input is hidden in the page, not hidden from the internet. The access key is meant to be public. Server-only credentials are not.

Handling JavaScript and no-JavaScript paths

The most robust static form starts as plain HTML. If JavaScript fails, the user can still submit and land on the configured redirect page. That matters on static sites because there is no server fallback you control.

You can then enhance the form with JavaScript for inline feedback. In Next.js, that usually means a client component that intercepts submit, sends FormData with fetch, and updates local state. Keep the same action and method on the form so the no-JS path remains valid.

"use client";

import { useState } from "react";

export default function AjaxContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus("sending");

    const form = event.currentTarget;
    const formData = new FormData(form);

    const response = await fetch(form.action, {
      method: "POST",
      body: formData,
      headers: {
        Accept: "application/json",
      },
    });

    if (response.ok) {
      form.reset();
      setStatus("sent");
      return;
    }

    setStatus("error");
  }

  return (
    <form action="https://api.formsfort.com/submit" method="POST" onSubmit={onSubmit}>
      <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
      <input type="text" name="name" placeholder="Name" required />
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <input type="checkbox" name="botcheck" style={{ display: "none" }} />
      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending..." : "Send"}
      </button>
      {status === "sent" && <p>Message sent.</p>}
      {status === "error" && <p>Something went wrong. Please try again.</p>}
    </form>
  );
}

The AJAX version is optional. It improves the experience, but it also introduces CORS and client-side error handling. For many production contact pages, the plain POST with a redirect is simpler and more reliable.

Using redirects on static hosts

With a no-JS form, the browser navigates away after submit. Set a redirect field to a static thank-you page you control, such as https://example.com/contact/thanks/. Create that route as a normal static Next.js page.

<input
  type="hidden"
  name="redirect"
  value="https://example.com/contact/thanks/"
/>

Use the final production URL when you test redirects. Relative paths can behave differently depending on the endpoint and referrer, while absolute URLs make the expected destination explicit.

Spam protection without a server

A static export cannot run your own server-side spam filter, but it can still collect signals for a form backend. Start with a honeypot. It is cheap, works without JavaScript, and blocks low-effort automated submissions.

<input
  type="checkbox"
  name="botcheck"
  tabindex="-1"
  autocomplete="off"
  style="display:none"
/>

If your form is exposed to heavier abuse, add captcha. With FormsFort, custom captcha belongs on Pro, along with webhooks, file uploads, Google Sheets, and scanning. Compare the current plan fit on pricing.

Where webhooks fit

Webhooks are the production escape hatch for static forms. Instead of asking your static site to process the submission, the form backend receives it and then calls a server you control, an automation platform, or another API. That keeps the public site simple while still letting your business workflow run server-side.

If the contact form should create tickets, enrich leads, or notify another system, read form backend with webhooks. For a more general Next.js setup that is not limited to export mode, see Next.js contact form.

Troubleshooting static export contact forms

POST to /api/contact returns 404 or 405

You are still posting to your own app. In static export, that route is not a running handler. Change the form action to the external endpoint, or deploy Next.js to a server/runtime that supports API routes.

The form reloads the page but nothing arrives

Check that the form has method="POST". Without it, browsers default to GET, which puts fields into the URL and does not create the expected submission. Also confirm the access key field is present and named exactly access_key.

AJAX fails with a CORS error

A normal HTML form submit does not use CORS the same way fetch does. If the plain form works but AJAX fails, inspect the endpoint response and allowed origin. Use Accept: application/json, send FormData directly, and test from the deployed domain, not only localhost.

The success redirect does not go where expected

Use an absolute redirect URL and make sure the thank-you page exists in the exported output. Static hosts commonly normalize trailing slashes, so test the exact production URL after deployment.

Spam testing is giving false confidence

Do not only test by filling the form manually. Submit once with the honeypot checked to confirm the backend rejects it. Submit quickly several times to understand rate limiting. If you enable captcha, test both a valid token and a missing token.

Environment variables are missing after export

In Next.js, client-side environment variables must be prefixed with NEXT_PUBLIC_ and are baked into the bundle at build time. That is fine for a public form access key, but not for secrets. If you changed an environment variable after building, rebuild and redeploy the static output.

Production checklist

  • Use an absolute external action URL.
  • Set method="POST".
  • Give every submitted field a name.
  • Use a public access key, not a private server secret.
  • Add a honeypot field and test that it is rejected.
  • Create a static thank-you page and use an absolute redirect URL.
  • Test the deployed domain, not only local development.
  • Use AJAX only when inline status is worth the extra CORS and state handling.

The working model is simple: Next.js exports the page, the browser submits the form, and the external endpoint processes the message. That is the right boundary for static hosting. For more copy-paste variants, browse the examples page.

FAQ

Static export form questions.

Can a Next.js static export have a contact form?

Yes. The form cannot depend on your own exported API route, but it can post directly to an external form backend.

Do I need server actions?

No. Server actions are useful on server deployments, but they are not available as request handlers on a static host after export.

Should I use fetch or a plain form submit?

Start with a plain POST because it works without JavaScript. Add fetch when you need inline success and error states.

Can this work on GitHub Pages and Cloudflare Pages?

Yes. Those hosts only need to serve the exported page. The browser sends the POST request to the external endpoint.

Ship the static contact form.

Create a FormsFort form, copy the access key, and use the endpoint from any exported Next.js page.

Open dashboard