Kontaktformular auf einer statischen Site

Kontaktformular auf einer statischen Site6 Min. Lesezeit

Den 405-Fehler bei einem auf Vercel gehosteten Kontaktformular beheben

Eine statische Seite auf Vercel beantwortet einen Formular-POST mit 405 Method Not Allowed; hier steht, warum, und wie du das Formular stattdessen an ein Backend sendest.

Preet SutharGründer, Ikiform

Vercel liefert deinen statischen Build als Dateien aus, und ein POST an eine Datei bekommt 405 mit leerem Body, weil keine Funktion diesen Pfad verarbeitet. Richte die action des Formulars stattdessen auf deinen Ikiform-Endpunkt aus oder schreib eine Vercel Function, die den Post annimmt. Mit Ikiform setzt du die Formular-ID pro Vercel-Umgebung, damit Previews an ein Testformular und die Produktion an das echte senden.

Warum Vercel mit 405 antwortet

Vercel leitet jeden Request an eine statische Datei oder an eine Funktion. Eine statische Site, ein Vite-Build oder eine Next.js-App mit output: "export" hat nur Dateien. Ein GET auf /contact liefert die Datei; ein POST an denselben Pfad hat kein Ziel, also lehnt Vercel ihn ab, bevor irgendein Code von dir laufen könnte:

Terminal
curl -i -X POST -d "message=hello" https://your-project.vercel.app/contact
Antwort
HTTP/2 405
server: Vercel
content-disposition: inline
x-vercel-cache: HIT
content-length: 0

Die Antwort hat keinen Body, daher zeigt der Browser eine leere Seite oder seine eigene Fehleransicht. Drei Setups führen hierher:

Dein CodeWarum es auf Vercel 405 gibt
<form method="post"> ohne actionDas Formular sendet an die Seite, auf der es steht, und die ist eine statische Datei
action="/api/contact" in einem Vite- oder einfachen HTML-ProjektIm Projektstamm gibt es keine Datei in einem api/-Ordner, also existiert für diesen Pfad keine Funktion
Eine Next.js-Server-Action oder ein Route Handler in einem statischen Exportoutput: "export" unterstützt keine Server Actions und keine Route Handler, die den Request lesen. next dev meldet den Fehler, und der Build hat keinen Server, der sie ausführen könnte
Wie Vercel entscheidet, was mit einem Formular-POST passiertaction zeigt auf deine Vercel-Siteaction zeigt auf IkiformPOST/contactvom FormularVercel-RoutingDatei oderFunktion?Funktionhier?deineFunktionfalls du einegeschriebenhast405, leererBodyeine statischeDateiPOST anikiform.comgleichesFormularIkiformspeichert,mailt dirDankeseiteeine statischeDatei aufVerceljanein303Wie Vercel entscheidet, was mit einem Formular-POST passiertaction zeigt auf deine Vercel-Siteaction zeigt auf IkiformPOST /contactvom FormularVercel-RoutingDatei oderFunktion?Funktionhier?deine Funktionfalls du einegeschrieben hast405,leererBodyeinestatischeDateiPOST anikiform.comgleiches FormularIkiformspeichert, mailt dirDankeseiteeine statischeDatei auf Vercelja303nein
  1. Das Formular sendet an einen Pfad in deinem Vercel-Deployment.
  2. Vercel sucht eine Funktion, die diesen Pfad verarbeitet.
  3. Wenn du eine geschrieben hast, läuft sie. In einem statischen Build gibt es keine, also antwortet Vercel mit 405 und leerem Body.
  4. Zeigt die action auf Ikiform, erreicht der Post Vercel gar nicht.
  5. Ikiform speichert die Nachricht, schickt dir eine E-Mail und leitet den Browser zurück auf eine statische Seite deiner Vercel-Site.
Vercel ist hier nicht kaputt. Ein statisches Deployment hat keinen Code für einen POST, und eine Funktion ist auf Vercels Seite der einzige Weg, einen hinzuzufügen.

Du fügst also entweder eine Funktion hinzu oder schickst den Post woandershin. Eine Vercel Function in api/contact.ts funktioniert, aber dann brauchst du eine E-Mail-API, Spamfilter und Rate-Limits, und du pflegst sie. Sendest du den Post an Ikiform, bleibt das Deployment statisch. Die Lösung im HTML ist ein einziges Attribut:

Vorher
<form method="post">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button>Send</button>
</form>
Nachher
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">

Wenn du die Funktion lieber selbst schreibst

Eine Funktion ist eine faire Wahl, wenn die Nachricht irgendwohin muss, wo nur dein Code hinkommt. Rechne vorher zusammen, was dazugehört:

  • Ein Konto bei einem E-Mail-Anbieter, eine verifizierte Absenderdomain und ein API-Schlüssel in den Umgebungsvariablen von Vercel.
  • Validierung auf dem Server, weil die required-Attribute des Browsers eine Bequemlichkeit sind, keine Prüfung.
  • Spamfilter und Rate-Limits, sonst schickt dir das Kontaktformular jeden Bot im Internet.
  • Einen Ort, an dem Nachrichten liegen bleiben, wenn der E-Mail-Anbieter ausfällt, und eine Möglichkeit, sie erneut zu senden.

Ein Formular-Backend deckt diese Liste für dich ab. Wenn du trotzdem eigenen Code dazwischen haben willst, lass das Formular weiter an Ikiform senden und empfange einen signierten Webhook in einer Funktion; dein eigener Server zeigt den Handler.

Eine Formular-ID pro Vercel-Umgebung

Jeder Pull Request auf Vercel bekommt ein Preview-Deployment. Wenn Previews an dein Produktionsformular senden, landet jede Testnachricht neben echten in deinem Postfach. Leg ein zweites Ikiform-Formular für Previews an und lass die Umgebungsvariablen von Vercel entscheiden. Füge unter Settings › Environment Variables denselben Namen zweimal hinzu:

Vercel-Umgebungsvariablen
VITE_IKIFORM_FORM_ID   Production   frm_xxxxxxxxxxxxxxxx
VITE_IKIFORM_FORM_ID   Preview      frm_yyyyyyyyyyyyyyyy

Vite ersetzt %NAME%-Platzhalter in index.html zur Build-Zeit durch VITE_-Variablen, sodass ein einfaches HTML-Formular sie ohne JavaScript nutzen kann. VITE_VERCEL_ENV setzt Vercel für Vite-Projekte, und es hält fest, aus welchem Deployment eine Nachricht kam:

index.html
<form id="contact" action="https://ikiform.com/f/%VITE_IKIFORM_FORM_ID%" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" minlength="10" required></textarea>

  <input type="hidden" name="build" value="%VITE_VERCEL_ENV%" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send message</button>
</form>
  • Vercel liest Umgebungsvariablen bei einer statischen Site zur Build-Zeit. Nach einer Änderung musst du neu deployen, sonst bleibt der alte Wert im HTML.
  • Vite lässt einen Platzhalter stehen, wenn die Variable nicht existiert, daher wird eine fehlende ID als %VITE_IKIFORM_FORM_ID% ausgeliefert, und jeder Post scheitert mit 400 Bad Request, bevor er das Formular erreicht. Die Prüfung unten fängt das ab.
  • Die Formular-ID ist so oder so öffentlich. Nur VITE_-Variablen erreichen den Browser, deshalb gehört kein Geheimnis in eine davon.
src/contact.js
const form = document.querySelector("#contact");

if (form && form.action.includes("%VITE_")) {
  console.error("VITE_IKIFORM_FORM_ID is not set for this Vercel environment");
  form.querySelector("button").disabled = true;
}

Du nutzt Next.js mit output: "export"? Lies stattdessen process.env.NEXT_PUBLIC_IKIFORM_FORM_ID in einer Client-Komponente; die Hauptanleitung hat ein Formular für den statischen Export. Wenn deine Next.js-App doch auf den Servern von Vercel läuft, kann eine Server Action mit einem Secret Key senden: siehe die Next.js-Anleitung.

Header und saubere URLs

Wenn deine Site eine Content-Security-Policy setzt, setze sie in vercel.json und nimm Ikiform auf. cleanUrls liefert thanks.html unter /thanks aus, was ein aufgeräumteres Weiterleitungsziel ergibt:

vercel.json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "cleanUrls": true,
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
        }
      ]
    }
  ]
}

Previews und erlaubte Domains

Preview-URLs wie your-project-git-branch-team.vercel.app sind keine Subdomains von your-project.vercel.app, passen also unter Allowed domains nicht dazu. Posts von dort landen mit origin_mismatch im Spam. Ein weiterer Grund für ein separates Preview-Formular: Lass seine erlaubten Domains leer oder füge den Preview-Host hinzu, den du gerade testest.

Mit aktivierter Vercel Authentication und dem Schutzumfang Standard Protection, der jedes Deployment außer den Produktionsdomains abdeckt, liegen Preview-URLs hinter einem Vercel-Login. Nur dein Team erreicht sie, ein Preview-Formular ohne erlaubte Domains ist also nicht öffentlich zugänglich. Prüfe Settings › Deployment Protection, bevor du dich darauf verlässt. Posts aus Previews sind keine localhost-Tests, sie zählen also wie jede Live-Einsendung auf deinen Tarif.

Prüfen, ob es funktioniert

  1. Führe das curl oben vor der Lösung gegen deine Produktions-URL aus und bestätige den 405. Nach der Lösung beginnt die action des Formulars im deployten HTML mit https://ikiform.com/f/.
  2. Öffne ein Preview-Deployment, sende ab und prüfe, ob die Nachricht im Preview-Formular ankommt, nicht im Produktionsformular.
  3. Sende auf deiner Produktionsdomain ab und prüfe, ob das Feld build den Wert production hat.
  4. Prüfe das Postfach jedes Benachrichtigungsempfängers. Jeder bekommt eine E-Mail mit den Feldern, und Reply geht an den Absender, wenn das Formular ein email-Feld hat.
  5. Nichts angekommen? Stell den Statusfilter im Dashboard auf Spam. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.

Stolperfallen

ProblemWas zu tun ist
Immer noch 405 nach der Änderung am FormularDas Deployment, das du testest, wurde vor der Änderung gebaut, oder ein JavaScript-Handler sendet mit fetch an deinen eigenen Pfad. Durchsuche die gebauten Dateien nach /api/contact.
400 Bad Request bei jedem Post oder 404 not_found von IkiformDie Formular-ID hat den Build nicht erreicht, oder es ist die falsche ID für diese Umgebung. Prüfe, ob die Variable für diese Umgebung existiert, und deploye neu.
Previews funktionieren, Posts aus der Produktion landen im SpamDeine Produktionsdomain fehlt unter Allowed domains des Produktionsformulars. Füge die eigene Domain sowie your-project.vercel.app hinzu.
Der Browser landet auf der Dankeseite von Ikiform statt auf deiner_redirect wird ignoriert, außer seine Domain steht unter Allowed domains und es ist eine https:-URL. Füge die Domain hinzu oder setze stattdessen Redirect after submit in den Einstellungen des Formulars.

Fragen

Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.