Kontaktformular auf einer statischen Site

Kontaktformular auf einer statischen Site6 Min. Lesezeit

Ein Kontaktformular auf Netlify ohne Netlify Forms einbauen

Betreib ein Kontaktformular auf einer Netlify-Site ohne Netlify Forms: keine Erkennung beim Build, JSON-Posts aus React und ein eigenes Formular für Deploy Previews.

Preet SutharGründer, Ikiform

Lass data-netlify am Formular weg und richte seine action auf deinen Ikiform-Endpunkt aus; Netlify liefert dann die Seite aus und rührt den Post nie an. React- und Vue-Formulare brauchen keine versteckte HTML-Kopie und können JSON senden. Setze in netlify.toml eine andere Formular-ID für Deploy Previews, weil Preview-Hosts unter Allowed domains nicht zu deiner Site passen.

Was bei Netlify anders ist

Netlify ist der einzige große statische Host mit eingebauter Formularfunktion. Netlify Forms liest nach jedem Build dein deploytes HTML, findet Formulare mit data-netlify oder netlify und nimmt für diese Formulare Posts an deine eigene Site an. Jeder andere Post an eine statische Seite scheitert, und auf Netlify scheitert er mit 404 statt 405:

Terminal
curl -i -X POST -d "message=hello" https://your-site.netlify.app/contact/
Antwort
HTTP/2 404
server: Netlify
cache-status: "Netlify Edge"; fwd=method; fwd-status=404
content-length: 0

Dieser 404 schickt Leute auf die Suche nach einer fehlenden Seite, die gar nicht fehlt. Er bedeutet, dass kein erkanntes Formular zum Post gepasst hat. Ob du Netlify Forms überhaupt nutzen solltest, ist eine andere Frage, und der Vergleich Ikiform vs. Netlify Forms beantwortet sie ehrlich: Wenn deine Site immer auf Netlify bleibt und das Formular einfaches HTML ist, macht Netlify Forms weniger Arbeit. Diese Seite ist für die anderen Fälle.

Deine SituationWarum Netlify Forms im Weg ist
Das Formular wird von React, Vue oder Svelte gerendertDer Build analysiert statisches HTML, daher braucht ein clientseitig gerendertes Formular ein verstecktes Duplikat, das Netlify finden kann
Du willst JSON per fetch sendenNetlify Forms nimmt nur urlencoded- und Multipart-Bodys an
Die Site zieht vielleicht zu einem anderen Host umDas Formular sendet an deine eigene Domain, funktioniert also nur, solange Netlify diese Domain ausliefert
Nachrichten sollen einen Webhook erreichen, den du verifizierstIkiform signiert jede Zustellung mit Standard-Webhooks-Headern und wiederholt sie bei Bedarf
Netlify Forms und Ikiform nebeneinander auf derselben Netlify-Sitemit Netlify Formsmit IkiformBuildNetlify durchsucht dasHTMLdata-netlify?POST an deineSiteNetlify speichert ihn404 bei POSTFormular ignoriertBuildkein Scan nötigPOST anikiform.comHTML oder JSONIkiformspeichert, mailt,Webhooksgefundennicht gefundenNetlify Forms und Ikiform nebeneinander auf derselben Netlify-Sitemit Netlify Formsmit IkiformBuildNetlify durchsuchtdas HTMLdata-netlify?POST an deineSiteNetlify speichertihn404 beiPOSTFormularignoriertBuildkein Scan nötigPOST anikiform.comHTML oder JSONIkiformspeichert, mailt,Webhooksgefundennicht gefunden
  1. Mit Netlify Forms durchsucht Netlify das HTML jedes Deployments nach Formularen mit data-netlify.
  2. Ein gefundenes Formular nimmt Posts an deine eigene Site an, und Netlify speichert sie.
  3. Ein nicht gefundenes Formular, etwa eines, das ohne versteckte Kopie per JavaScript gerendert wird, bekommt bei jedem Post 404.
  4. Mit Ikiform muss beim Build nichts gefunden werden.
  5. Das Formular sendet direkt an ikiform.com, als HTML oder JSON, und Ikiform speichert es und schickt dir eine E-Mail.
Beides kann auf einer Site nebeneinander existieren. Ein Formular mit data-netlify geht an Netlify; ein Formular, dessen action ein Ikiform-Endpunkt ist, geht an Ikiform.

Ein Formular von Netlify Forms lösen

  • Entferne data-netlify bzw. netlify, netlify-honeypot und data-netlify-recaptcha vom Formular sowie ein eventuelles verstecktes form-name-Eingabefeld. Bleibt form-name drin, wird es als normales Feld gespeichert.
  • Lösche die versteckte statische Kopie eines JavaScript-Formulars, wie die unten. Netlify brauchte sie nur, um das Formular zu erkennen.
  • Setze die action oder die fetch-URL auf deinen Ikiform-Endpunkt. Die alte action="/thanks" wandert in die Einstellung Redirect after submit des Formulars.
  • Lade vorher die bisherigen Einsendungen auf der Seite Forms von Netlify herunter. Sie ziehen nicht von allein um.
index.html, das hier löschen
<form name="contact" netlify hidden>
  <input name="name" />
  <input name="email" />
  <textarea name="message"></textarea>
</form>

Lass form detection an, wenn andere Formulare auf der Site noch Netlify Forms nutzen. Laut Netlifys Dokumentation ist das Abschalten der Erkennung nur für Sites gedacht, die Netlify Forms nicht nutzen. Sobald nichts mehr darauf angewiesen ist, sparst du durch das Abschalten bei jedem Deployment einen Scan.

Was die Spam-Einstellungen von Netlify ersetzt

Netlify lässt Akismet über die Einsendungen laufen und bietet netlify-honeypot und reCAPTCHA. Bei Ikiform funktioniert der Honeypot weiter, wenn du Honeypot field des Formulars auf den Namen setzt, den du schon verwendest, etwa bot-field. Spam-Bewertung läuft ohne Einrichtung bei jedem Post, und Spam check auf Ikiform ersetzt reCAPTCHA durch Cloudflare Turnstile, ganz ohne Schlüssel. Entferne beim Wechsel das reCAPTCHA-div.

Ein React-Formular, das JSON sendet

Ohne Netlify Forms im Weg braucht ein clientseitig gerendertes Formular kein Duplikat und kann JSON senden, was Typen erhält und dich Werte vorher trimmen lässt. Dieses hier hält außerdem fest, welcher Netlify-Kontext die Seite gebaut hat:

src/ContactForm.tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = `https://ikiform.com/f/${import.meta.env.VITE_IKIFORM_FORM_ID}`;

interface Message {
  name: string;
  email: string;
  message: string;
  context: string;
}

export function ContactForm() {
  const [sent, setSent] = useState(false);
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const body: Message & Record<string, string> = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      context: import.meta.env.VITE_CONTEXT ?? "local",
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
    setError("");
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify(body),
    }).catch(() => null);
    const result = await response?.json().catch(() => null);
    if (result?.ok) return setSent(true);
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    setError(Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.");
  }

  if (sent) return <p role="status">Got it. I'll answer from my own inbox, usually the same day.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} minLength={10} required />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button>Send</button>
    </form>
  );
}
  • Content-Type: application/json sagt Ikiform, dass es JSON parsen soll; Accept: application/json bittet um eine JSON-Antwort.
  • VITE_CONTEXT ist ein Beispielname. Netlify setzt CONTEXT während des Builds; gib es in deinem Build-Befehl an Vite weiter oder lass das Feld weg.
  • Der Honeypot wird als JSON-Schlüssel gesendet. Behalte ihn im Body, auch wenn das Eingabefeld versteckt ist, damit Bots, die ihn ausfüllen, sich weiterhin selbst verraten.

Deploy Previews bekommen ihr eigenes Formular

Deploy Previews liegen auf Hosts wie deploy-preview-12--your-site.netlify.app. Das ist keine Subdomain von your-site.netlify.app, passt also unter Allowed domains nicht, und Posts aus Previews landen im Spam. Gib Previews und Branch-Deployments in netlify.toml ein eigenes Formular und setze den Content-Security-Policy-Header in derselben Datei:

netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[context.production.environment]
  VITE_IKIFORM_FORM_ID = "frm_xxxxxxxxxxxxxxxx"

[context.deploy-preview.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[context.branch-deploy.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"

Lass Allowed domains des Preview-Formulars leer oder füge beim Testen den genauen Preview-Host hinzu. Das Produktionsformular bekommt deine Domain und your-site.netlify.app. Header in netlify.toml gelten für jeden Deploy-Kontext; wenn du eine _headers-Datei bevorzugst, muss sie im Publish-Verzeichnis landen.

Füge niemals netlify.app allein zu Allowed domains hinzu. Jede Netlify-Site würde passen, auch Kopien deines Formulars auf den Sites anderer Leute.

Prüfen, ob es funktioniert

  1. Öffne ein Deploy Preview, sende ab und prüfe, ob die Nachricht im Preview-Formular landet.
  2. Merge, dann sende in der Produktion ab. Prüfe, ob die Nachricht im Produktionsformular landet und die Seite Forms von Netlify nichts Neues zeigt.
  3. 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.
  4. Sende einen Post mit ausgefülltem Honeypot, _gotcha=bot. Er bekommt trotzdem eine Erfolgsantwort, aber die Einsendung wird als Spam gespeichert und löst weder E-Mail noch Webhook aus.
  5. Nichts angekommen? Stell den Statusfilter im Dashboard auf Spam. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.

Stolperfallen

ProblemWas zu tun ist
404 beim AbsendenDer Post ging an deine eigene Site. Ein gecachter Build hat vielleicht noch die alte action, oder ein fetch-Aufruf sendet noch an /. Durchsuche das Publish-Verzeichnis nach form-name.
Einsendungen tauchen noch in Netlify aufIrgendein Formular auf der Seite sendet noch mit data-netlify oder netlify an deine eigene Site, also erkennt und speichert Netlify es weiterhin. Entferne das Attribut, setze die action auf Ikiform und deploye neu.
404 not_found von Ikiform nur bei PreviewsDer Deploy-Preview-Kontext hat keine VITE_IKIFORM_FORM_ID, daher endet die URL auf undefined. Füge sie unter [context.deploy-preview.environment] hinzu.
403 origin_not_allowed, oder jeder Post landet im SpamDie Domain der Seite steht nicht unter Allowed domains des Formulars. Füge sie hinzu, plus localhost für lokale Tests. Subdomains einer eingetragenen Domain sind erlaubt.
422 files_not_supportedDas Formular hat ein <input type="file"> mit ausgewählter Datei. Datei-Uploads werden noch nicht unterstützt; entferne das Eingabefeld oder bitte stattdessen um einen Link.

Fragen

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