Ein Kontaktformular muss eine Frage klar beantworten: Ist die Nachricht angekommen? Verwende fetch um Felder ohne Seitenwechsel zu senden, einen Sendestatus anzuzeigen und Erfolg erst nach Annahmebestätigung des Endpunkts zu melden. Bewahre bei Fehlern den Text und erkläre nächste Schritte.
Unten findest du ein vollständiges HTML- und JavaScript-Beispiel für den Mailcontact-Formular-Endpunkt. Es braucht kein Frontend-Framework und enthält sichtbare Beschriftungen, Browservalidierung, Honeypot, Doppelklickschutz und Statusmeldung im Formular. Ersetze vor dem Test den Beispiel-Endpunkt durch die echte Projekt-URL.
Warum ein erfolgreich aufgelöstes fetch nicht reicht
Eine Anfrage kann den Server erreichen, obwohl dieser die Einsendung ablehnt. Eine ungültige E-Mail-Adresse führt zum Beispiel zu HTTP 400, ein ausgeschöpftes Monatskontingent zu HTTP 429. fetch lehnt sein Promise nicht allein wegen eines HTTP-Fehlerstatus ab. Prüfe response.ok und den erwarteten Antwortinhalt, bevor du das Formular zurücksetzt. Dieses Verhalten ist dokumentiert in MDN-Anleitung zu Fetch.
Mailcontact liefert {"ok":true} bei angenommenen Anfragen. Ein ausgefüllter Honeypot erhält bewusst dieselbe Antwort, wird aber verworfen. Teste die Falle deshalb auch auf Autofill-Fehler. Echte Besucher sollten das Feld leer lassen. Das Endpunkt-Referenz beschreibt Antwortformat und Validierungsgrenzen.
Ein vollständiges Kontaktformular mit Erfolgs- und Fehlermeldungen
Für neue Integrationen empfehlen wir das gehostetes MailContact-Skript oder eigenes SDK. Es fügt die verborgene Prüfung ein, übernimmt die automatische Verifizierung und bewahrt Entwürfe bei Fehlern. Free-Skriptintegrationen benötigen einen sichtbaren Link „Processed by mailcontact.app“. Das fetch-Beispiel unten zeigt HTTP-Rückmeldungen und enthält die Pflichtprüfung. Verdächtige Nachrichten ohne Verifizierungsunterstützung werden in Spam zur Prüfung zurückgehalten.
<form id="contact-form" action="https://mailcontact.app/api/forms/your-form-id" method="POST">
<fieldset>
<legend>Contact us</legend>
<label for="contact-name">Name (optional)</label>
<input id="contact-name" name="name" autocomplete="name" maxlength="200" />
<label for="contact-email">Your email</label>
<input id="contact-email" type="email" name="email"
autocomplete="email" maxlength="320" required />
<label for="contact-message">Message</label>
<textarea id="contact-message" name="message" maxlength="5000" required></textarea>
<div class="contact-meta" aria-hidden="true">
<label for="contact-website">Website</label>
<input id="contact-website" name="company_website" type="text"
tabindex="-1" autocomplete="off" />
</div>
<button type="submit">Send message</button>
</fieldset>
<p id="contact-status" role="status" aria-live="polite" aria-atomic="true"></p>
</form>
<style>
.contact-meta {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
</style>
<!-- Keep the script after the form so its elements exist. -->
<script>
const form = document.querySelector("#contact-form");
const fields = form.querySelector("fieldset");
const button = form.querySelector('button[type="submit"]');
const status = document.querySelector("#contact-status");
let sending = false;
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (sending || !form.reportValidity()) return;
// Collect values before disabling the fieldset.
const body = new FormData(form);
if (!String(body.get("message") || "").trim()) {
status.textContent = "Please write a message before sending.";
form.elements.namedItem("message").focus();
return;
}
sending = true;
fields.disabled = true;
form.setAttribute("aria-busy", "true");
button.textContent = "Sending…";
status.textContent = "Sending your message…";
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
try {
const response = await fetch(form.action, {
method: "POST",
body,
signal: controller.signal,
});
const data = await response.json().catch(() => null);
if (!response.ok || data?.ok !== true) {
let message = "Your message could not be sent. Your text is still here.";
if (response.status === 400) {
message = "Check your email and message length, then try again.";
} else if (response.status === 429) {
message = data?.code === "MONTHLY_QUOTA_EXCEEDED"
? "This form has reached its monthly limit. Please use another contact method on this site."
: "Too many requests. Please wait before trying again.";
} else if (response.status === 404) {
message = "This contact form is unavailable. Please use another contact method on this site.";
}
status.textContent = message;
return;
}
form.reset();
status.textContent = "Thanks! Your message has been received.";
} catch {
status.textContent = "We could not confirm delivery. Your text is still here. Check your connection before trying again.";
} finally {
clearTimeout(timeout);
sending = false;
fields.disabled = false;
form.removeAttribute("aria-busy");
button.textContent = "Send message";
}
});
</script>Verwende für dieses Markup die normalen Formularstyles deiner Website. Die kleine CSS-Regel betrifft nur den Honeypot, nicht dein Layout. Ohne JavaScript sendet das Formular weiterhin nativ. Der Mailcontact-Endpunkt zeigt dann derzeit JSON statt einer Weiterleitung zur Dankesseite.
Was passiert während des Versands?
Der Handler sammelt Felder vor Deaktivierung des fieldset. Deaktivierte Eingaben fehlen in FormData; umgekehrte Reihenfolge würde Daten verlieren. Die Sperre verhindert auch Änderungen während der Anfrage, die ein erfolgreicher Reset sonst löschen würde. Der separate sending Schutz verhindert überlappende Einsendungen.
Der Statusbereich steht von Beginn an auf der Seite und aktualisiert bei Änderungen seinen Text. So gibt es Rückmeldung ohne Dialogbox oder bloßen Farbwechsel. Das finally Block stellt das Formular nach Erfolg, Server- oder Netzwerkfehler wieder her, damit der Button nicht dauerhaft gesperrt bleibt.
Nachricht bei ungewisser Zustellung bewahren
Das 15-Sekunden-Zeitlimit verhindert endloses Warten im Browser. Eine bereits angenommene Nachricht macht es nicht rückgängig. Die Verbindung kann nach Speicherung, aber vor Bestätigung abbrechen. Deshalb sagt die Fehlermeldung, dass die Zustellung unbestätigt ist, statt zu behaupten, nichts sei gesendet worden.
Das Beispiel wiederholt ungewisse Anfragen nicht automatisch, weil dadurch Duplikate entstehen könnten. Es bewahrt den Entwurf, damit Besucher entscheiden. Garantierte Duplikatvermeidung bei Wiederholungen erfordert einen ausdrücklichen Idempotenzmechanismus im Empfangsdienst. Ein gesperrter Button reicht nicht.
Behandle kurzfristige Limits anders als Monatskontingente
Eine 429-Antwort kann eine vorübergehende Ratenbegrenzung oder ein ausgeschöpftes Monatskontingent bedeuten. Prüfe das JSON- code: MONTHLY_QUOTA_EXCEEDED kennzeichnet Letzteres. Ein erneuter Versuch in wenigen Sekunden hilft nicht, wenn das Monatskontingent voll ist.
Bei vorübergehenden Ratenlimits liefert der Endpunkt einen Retry-After Header. Du kannst ihn für einen Countdown nutzen. Bei vollem Kontingent biete einen echten alternativen Kontaktweg an. Formular und Empfangsadresse eines Pro-Projekts teilen das Kontingent. Die Projektadresse hilft daher bei ausgeschöpftem Kontingent nicht weiter. Prüfe deine Nutzung und die Tariflimits vor einem großen Launch.
Formular vor Veröffentlichung prüfen
- Erfolgreiche Einsendung: Die Bestätigung erscheint, das Formular leert sich und die Nachricht liegt im Projektpostfach.
- Ungültige Eingabe: Eine ungültige E-Mail-Adresse oder leere Nachricht zeigt hilfreiche Rückmeldungen, ohne andere Eingaben zu löschen.
- Browser offline: Der Entwurf bleibt erhalten, die Zustellung wird als ungewiss beschrieben und der Button wieder bedienbar.
- Wiederholte Klicks: Während des Sendestatus läuft nur eine Anfrage gleichzeitig.
- Benachrichtigungszustellung: Ein eingerichtetes Slack- oder Discord-Ziel erhält nach einer angenommenen Nachricht eine Benachrichtigung.
Zeige Erfolg nicht nur, weil eine Animation endet oder ein Button geklickt wurde. Verknüpfe ihn mit der Endpunkt-Antwort, ermögliche Wiederholungen bei Fehlern und prüfe den ganzen Weg bis ins Postfach. Für die Empfangsseite folge der Slack-Benachrichtigungen Schritt für Schritt oder die Discord-Anleitung.