All 30 checks Form posts to staging

Form posts to staging

Updated

A form copied from a staging build can keep its old action attribute, so the live page looks perfect while every enquiry, signup or quote request goes to a preview deployment that nobody reads.

Fails: Live page whose form posts to a preview deployment
<!-- Served at https://example.com/contact -->
<form action="https://example-git-main-team.vercel.app/api/contact" method="post">
  <input name="email" type="email" required>
  <button>Send</button>
</form>
Fix: Post to the live site
<!-- A relative path always means "this site", wherever the page is served. -->
<form action="/api/contact" method="post">

The fix for your stack

Fix for: Next.js / Vercel

// app/contact/page.tsx: post to a path on the live site,
// never to a preview deployment or staging URL.
export default function Contact() {
  return (
    <form action="/api/contact" method="post">
      {/* fields */}
    </form>
  );
}
// If the endpoint comes from an environment variable, check its value under
// Vercel > Project > Settings > Environment Variables, for Production.

Why a form outlives its staging address

The action attribute of a form is the address the browser sends the visitor's answers to. When it is a relative path, such as /contact, the browser fills in whatever domain the page was served from, so the same markup works on staging and on the live site. When it is a full URL, it keeps pointing at that exact host forever. A developer who pasted the endpoint from a preview deployment, an environment variable that was only ever set for staging, or a page exported from one host and imported on another can all leave a full staging URL behind.

Nothing on the page gives this away. The form renders, the button works, and the visitor often sees a thank-you message, because the staging copy is still running and accepts the post. The submissions land in a staging database, a test inbox, or a preview function whose logs expire after a few days. The site owner finds out weeks later, when a customer phones to ask why nobody answered, or when the staging deployment is deleted and the form starts failing outright.

How Skewcheck detects it

Skewcheck reads the action attribute of every form on the audited page and resolves it against the page URL. A form with no action, an empty action, a relative path, or a full URL on the same site, ignoring a leading www, submits to this site and passes. So do mailto: actions. When the page is on a live host and a form posts to a host Skewcheck recognizes as staging, such as a .webflow.io address, a Vercel URL containing -git-, a Netlify address with a double hyphen, or any subdomain with staging, stage, dev, preview, uat or sandbox in it, the check fails.

That failure is a launch blocker. It is high severity, removes 8 points from the score, and caps the whole audit at 49 however clean the rest of the page is, because a contact form that loses every message defeats the point of launching. A form that posts to another live site is only a low-severity warning worth 2 points, since that is usually deliberate. Common hosted form services pass outright: Formspree, Mailchimp, HubSpot forms, Typeform, Getform, Basin and Webflow's own form endpoint. Netlify Forms posts to the site itself, so it passes as same-site. On a staging page, a form posting to staging is expected and passes.

List the form targets on the live page
curl -s https://example.com/contact | grep -ioE '<form[^>]*action="[^"]*"'

Pointing the form at the live site

Prefer a relative action whenever the form handler lives on the same site, because it can never drift to another host. If the endpoint has to be a full URL, for example an API on a separate subdomain, read it from configuration that has a production value, and check that value on the production deployment rather than on your laptop. In Next.js, an endpoint from a NEXT_PUBLIC_ variable is baked in at build time, so changing the variable means redeploying.

On site builders, look where the platform stores the form target. In Webflow, select the Form Block and check the Action field in its settings; leave it empty to use Webflow's own form handling. In Framer, select the form and check where it sends submissions in the right-hand panel. In WordPress, form plugins post to the site itself, so a staging action usually means the WordPress Address and Site Address under Settings, General still name the staging domain, or a template hard-codes it.

After the fix

Publish, then send a real test submission from the live domain and confirm it arrives where the client expects it: the inbox, the CRM, or the spreadsheet. Skewcheck only reads where the form points; it never submits forms, so it cannot tell whether the handler on the other end works. Re-check the page so the blocker clears, and check every page that has a form, such as the footer newsletter signup, because each template can carry its own copy of the old address.

Before and after

Before
<form action="https://staging.example.com/contact.php" method="post">
After
<form action="/contact.php" method="post">

Check your page

Run all 30 checks on any URL. Free, no account.