Statische Seiten sind großartig: schnell, günstig zu hosten, sicher und denkbar einfach zu deployen. Dann fragt ein Kunde “können Besucher uns über die Seite eine E-Mail schicken?” und plötzlich stehst du vor genau der einen Sache, die eine statische Seite von sich aus nicht kann: eine Formularübermittlung entgegennehmen.
Die gute Nachricht: Du brauchst dafür kein Backend und keinen Server. Am Ende dieser Anleitung hast du ein voll funktionsfähiges, barrierefreies und spam-resistentes Kontaktformular aus reinem HTML und ein wenig JavaScript, mit vollständigem Copy-Paste-Code statt Platzhaltern.
Die Optionen für Formulare ohne Backend
Es gibt im Grunde vier gängige Ansätze, und sie sind nicht gleichwertig.
1. mailto:-Links. Null Aufwand. <a href="mailto:du@example.com"> öffnet das E-Mail-Programm des Besuchers. Aber die Erfahrung ist holprig: Es setzt ein eingerichtetes Desktop-Mailprogramm voraus, stellt deine Adresse für Scraper offen ins Netz und erfasst nichts Strukturiertes. Für eine persönliche Landingpage okay, für echten Kontakt nicht.
2. Google-Forms-Einbettung. Kostenlos und ohne Code, aber der iframe sieht überhaupt nicht nach deiner Seite aus, arbeitet gegen dein Design und deine Performance, und die UX schreit “das ist ein Google-Formular”. Für ein Hobbyprojekt in Ordnung, schwach für alles, was professionell wirken soll.
3. Formular-Handling des Hosters. Netlify Forms und ein paar andere Hoster bieten eingebaute Formularerfassung. Wirklich schön, wenn du bei diesem Hoster bist. Sobald du zu GitHub Pages, Cloudflare Pages oder in deinen eigenen S3-Bucket wechselst, ist es weg. Du bist eingesperrt.
4. Eine Formular-Backend-API. Du richtest dein <form> auf einen Endpunkt; der Dienst nimmt Einsendungen entgegen, filtert Spam und schickt dir eine E-Mail (oder feuert einen Webhook). Hoster-unabhängig, funktioniert mit jedem Framework, und dein Markup bleibt zu 100 % deins. Das ist der Ansatz, der überall funktioniert, also bauen wir den.
Schritt 1: Das HTML-Formular (von Haus aus barrierefrei)
Hier ein vollständiges, barrierefreies Formular. Achte auf die echten <label>-Elemente und die for/id-Paarung, genau der Teil, den die meisten Tutorials überspringen.
<form
action="https://api.formpaste.com/submit"
method="POST"
id="contact-form"
>
<!-- Dein Access Key aus dem Dashboard -->
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY_HERE" />
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" autocomplete="name" required />
</div>
<div>
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" autocomplete="email" required />
</div>
<div>
<label for="message">Nachricht</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit">Nachricht senden</button>
</form>
Das ist bereits ein funktionierendes Formular. Ganz ohne JavaScript wird eine Einsendung an den Endpunkt geschickt und landet in deinem Postfach. Progressive Enhancement, wie es sein soll: Es funktioniert, bevor eine einzige Zeile JS läuft.
Standardmäßig antwortet der Endpunkt mit JSON. Ohne JavaScript auf der Seite zeigt der Browser diese JSON-Antwort einfach an, was nicht die Erfahrung ist, die du willst. Wie du den Besucher stattdessen auf eine Danke-Seite schickst, steht in Schritt 3.
Schritt 2: Mit JavaScript aufwerten (saubere Lade- und Fehlerzustände)
Das einfache Formular löst eine komplette Seitennavigation aus. Damit Nutzer auf der Seite bleiben und Feedback direkt sehen, werten wir es auf. Beachte: Das hier wählt das Formular tatsächlich über seine id aus, behandelt den Netzwerkfehlerfall und gibt echtes Status-Feedback. Das sind die Details, die ein funktionierendes Formular von einer Demo unterscheiden.
<form action="https://api.formpaste.com/submit" method="POST" id="contact-form">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY_HERE" />
<input type="text" id="name" name="name" required />
<input type="email" id="email" name="email" required />
<textarea id="message" name="message" required></textarea>
<button type="submit">Nachricht senden</button>
<p id="form-status" role="status" aria-live="polite"></p>
</form>
<script>
const form = document.getElementById("contact-form");
const statusEl = document.getElementById("form-status");
const button = form.querySelector('button[type="submit"]');
form.addEventListener("submit", async (e) => {
e.preventDefault();
button.disabled = true;
statusEl.textContent = "Wird gesendet…";
try {
const response = await fetch(form.action, {
method: "POST",
body: new FormData(form),
});
if (response.ok) {
statusEl.textContent = "Danke! Deine Nachricht wurde gesendet.";
form.reset();
} else {
// Die API liefert bei Fehlern { success: false, code, message }.
const data = await response.json().catch(() => null);
statusEl.textContent = data?.message ?? "Da ist etwas schiefgelaufen. Bitte versuche es erneut.";
}
} catch (error) {
statusEl.textContent = "Netzwerkfehler. Bitte versuche es erneut.";
} finally {
button.disabled = false;
}
});
</script>
Der Bereich mit aria-live="polite" sorgt dafür, dass Screenreader die Statusänderung vorlesen, ein geschenkter Gewinn an Barrierefreiheit.
Eine erfolgreiche Antwort ist JSON in dieser Form:
{ "success": true, "message": "Submission received.", "id": "..." }
Fehler nutzen denselben Umschlag mit einem maschinenlesbaren Code, du kannst also auf data.code verzweigen, wenn du für rate_limited und domain_not_allowed unterschiedliche Meldungen zeigen willst.
Schritt 3: Redirect oder AJAX, wähle deinen Erfolgsfall
Es gibt zwei Wege, eine Einsendung zu bestätigen, und die Wahl trifft der Request selbst, nicht ein Header.
JSON (der Standard). Du postest das Formular und bekommst { success, message, id } mit einem 200 zurück. Genau darauf baut das JavaScript-Beispiel oben: Submit abfangen, fetch, Meldung direkt anzeigen, die Seite nie verlassen.
Redirect. Ergänze ein redirect-Feld im Formular, und eine erfolgreiche Einsendung antwortet mit einem 303 See Other auf diese URL, sodass der Browser auf deiner Danke-Seite landet. Das ist der Weg ohne JS.
<input type="hidden" name="redirect" value="/danke" />
Zwei Regeln solltest du kennen, denn beide greifen im Zweifel restriktiv:
- Eine relative URL wie
/dankefunktioniert immer. - Eine absolute URL wie
https://example.com/dankefunktioniert nur, wenn diese Domain auf der Allowlist deines Formulars steht. Bei leerer Allowlist werden absolute Redirects grundsätzlich abgelehnt. Das ist Absicht: Es verhindert, dass ein gestohlener Key deine Besucher auf eine Phishing-Seite umleitet.
Ein ungültiger Redirect liefert 400 invalid_redirect und es wird nichts gespeichert, du merkst es also sofort, statt still Einsendungen zu verlieren.
Nimm den Redirect für maximale Einfachheit und Robustheit; nimm JSON für das flüssigere Single-Page-Gefühl.
Spam stoppen ohne CAPTCHA
CAPTCHA-Rätsel nerven echte Nutzer und kosten Conversions. Den meisten Bot-Spam stoppst du mit drei unauffälligen Techniken, ganz ohne Rätsel.
Honeypot-Feld: ein verstecktes Input, das echte Nutzer nie ausfüllen, Bots aber schon. Blende es vor Menschen und Screenreadern aus und weise jede Einsendung ab, bei der es gefüllt ist. Der Feldname muss zu dem passen, wonach dein Anbieter sucht. Formpaste nutzt botcheck:
<!-- Bots füllen das aus; Menschen sehen es nie -->
<input
type="text"
name="botcheck"
style="position:absolute; left:-9999px;"
tabindex="-1"
autocomplete="off"
aria-hidden="true"
/>
Wenn du dir einen eigenen Feldnamen ausdenkst, weiß der Server nichts von einem Honeypot und speichert das Feld einfach als ganz normales Formularfeld. Schau in der Doku deines Anbieters nach dem exakten Namen; das ist ein stiller Fehler, der sich erst später als Spam im Postfach zeigt.
Zeitprüfung: Echte Nutzer brauchen ein paar Sekunden zum Ausfüllen; Bots senden in Millisekunden ab. Halte fest, wann das Formular geladen wurde, und markiere Einsendungen, die verdächtig schnell eintreffen. Formpaste liest das aus einem _ts-Feld mit dem Zeitstempel des Seitenaufrufs:
<input type="hidden" name="_ts" id="form-ts" />
<script>
document.getElementById("form-ts").value = Date.now();
</script>
Rate Limiting: Begrenze Einsendungen pro IP und Minute, damit ein Bot deinen Endpunkt nicht überrennen kann. Das hier braucht tatsächlich die Serverseite.
Alle drei selbst zu bauen bedeutet, Serverlogik zu betreiben, und das macht das Ziel “kein Backend” zunichte. Genau hier verdient sich eine Formular-Backend-API ihr Geld: Gute Anbieter übernehmen Honeypot, Zeitprüfung, Rate Limiting, Wegwerf-E-Mail- und Dubletten-Checks für dich und halten alles Verdächtige in einer Quarantäne, die du prüfen kannst, statt echte Nachrichten stillschweigend wegzuwerfen.
Häufige Fehler (und wie du sie behebst)
“Mein Formular sendet, aber ich bekomme keine E-Mail.” Prüfe deinen Spam-Ordner, kontrolliere, ob du deine Absenderadresse bestätigt hast, und stelle sicher, dass der Wert von access_key stimmt und nicht noch der Platzhalter ist.
“Ich bekomme ein 403 domain_not_allowed.” Dein Formular hat eine Domain-Allowlist und der Request kam von einer Domain, die nicht darauf steht. Trag die Domain im Dashboard ein. Wichtig: Das ist eine normale JSON-Fehlerantwort, kein CORS-Fehler.
“CORS-Fehler in der Konsole.” Das ist ein anderes Problem als das gerade genannte, und es lohnt sich, beide auseinanderzuhalten. Ein Formular-Endpunkt für statische Seiten sollte großzügige CORS-Header senden, ein echter CORS-Fehler bedeutet also meist, dass der Request die API überhaupt nicht erreicht hat: eine vertippte Endpunkt-URL, ein Netzwerk- oder DNS-Fehler oder ein Adblocker. Schau im Netzwerk-Tab nach, ob überhaupt eine Antwort zurückkam. Außerdem gut zu wissen: CORS ist eine Browser-Richtlinie, keine Sicherheitsgrenze. Es schützt deinen Key nicht, und was tatsächlich einschränkt, wer senden darf, ist die Domain-Allowlist.
“Beim Absenden passiert nichts.” Wenn du die JS-Variante nutzt, prüfe, ob dein getElementById/Selektor wirklich das Formular trifft und ob du e.preventDefault() aufgerufen hast.
“Lokal funktioniert es, in Produktion nicht.” Fast immer die Domain-Allowlist. Trag deine Produktions-Domain ein.
Den passenden Dienst wählen
Ein kurzer, ehrlicher Überblick:
Formspree ist ausgereift und beliebt, mit einem großzügigen Ökosystem. Eine sichere Standardwahl, auch wenn der kostenlose Tarif begrenzt ist. Web3Forms kommt wirklich ohne Anmeldung aus und arbeitet mit Access Keys, super für schnelle Wegwerf-Formulare. Formpaste (was ich hier verwendet habe) setzt auf Copy-Paste-Einfachheit, Spam-Filterung ohne CAPTCHA mit einer Quarantäne, die du kontrollierst, und, ungewöhnlich, einen MCP-Server, damit ein KI-Coding-Agent das Formular für dich verkabeln kann. Netlify Forms ist ausgezeichnet, aber eben nur, wenn du ohnehin bei Netlify hostest.
Jeder davon funktioniert mit allen Codebeispielen oben. Tausche Endpunkt und Key aus und schau in der Doku des jeweiligen Anbieters nach dessen Feldnamen, denn Honeypot- und Redirect-Konventionen unterscheiden sich zwischen den Diensten.
Fazit
Ein Kontaktformular in eine einfache statische HTML-Seite einzubauen bedeutet nicht mehr, einen Server aufzusetzen. Du hast jetzt ein vollständiges, barrierefreies, progressiv aufgewertetes Formular, eine Spam-Strategie ohne CAPTCHA und eine Liste mit Lösungen für die üblichen Stolperfallen.
Fang mit dem einfachen HTML-Formular an, werte es mit JavaScript auf, wenn du Feedback direkt auf der Seite willst, und überlass Zustellung und Spam einer Formular-Backend-API, damit du zurück ans Bauen kommst.
Du nutzt ein Framework? Derselbe Endpunkt und dieselben Felder funktionieren überall. Diese Seiten zeigen die idiomatische Variante samt der jeweils spezifischen Stolperfallen: React, Next.js, Vue, Astro und Svelte.