# So stoppst du Spam über das Kontaktformular einer statischen Site

> Stopp Spam in einem Formular einer statischen Site, obwohl sie kein Geheimnis verwahren und keine Posts zählen kann, mit einem Honeypot-Partial, einem sicheren Skript und serverseitigen Prüfungen.

Veröffentlicht am 2026-10-25 · https://ikiform.com/de/guides/spam-protection/static-site

Eine statische Site hat keinen Server, nichts auf der Seite kann also einen Secret Key verwahren, ein Captcha-Token verifizieren oder Posts pro Besucher zählen. Diese Prüfungen müssen anderswo laufen: in einer Funktion bei deinem Host oder in einem Formular-Backend, an das dein Formular sendet. Setze in jedem Fall einen barrierefreien Honeypot ins Formular und sorg dafür, dass das Formular ohne JavaScript funktioniert.

## Was bei einer statischen Site anders ist

Bei einer statischen Site ist jedes Byte, das du deployst, öffentlich. Alles in deinem HTML oder JavaScript kann ein Bot lesen und überspringen, clientseitige Spamprüfungen sind also Empfehlung, keine Durchsetzung. Die drei wichtigsten Prüfungen brauchen alle einen Server:

- **Ein Captcha-Token verifizieren** braucht einen Secret Key, der nicht in einer öffentlichen Datei liegen kann.
- **Posts pro Besucher zählen** braucht einen Speicher, der zwischen Requests erhalten bleibt.
- **Einen Honeypot lesen** braucht Code, der nach dem Eintreffen des Posts läuft, außerhalb der Reichweite des Bots.

Statische Hosts nehmen außerdem keine Posts an deine Seiten an. Vercel und GitHub Pages beantworten einen Formular-`POST` an eine statische Seite mit `405 Method Not Allowed`, das Formular braucht also einen Endpunkt, bevor Spam überhaupt ein Thema ist.

Diagramm: Drei Arten, ein Formular einer statischen Site anzubinden, und wo jeweils die Spamprüfungen laufen

1. Eine statische Seite, die an sich selbst sendet, erreicht einen Host, der keinen Code ausführt, also scheitert der Post mit 405 oder geht ins Leere.
2. Mit einer Host-Funktion sendet die Seite an eine Funktion, die das Secret hält, den Honeypot liest und das Token verifiziert und die Nachricht dann weiterschickt.
3. Mit einem Formular-Backend sendet die Seite direkt an das Backend, das Honeypot, Bewertung, Rate-Limit und Menschlichkeitsprüfung ausführt, bevor es dir eine E-Mail schickt.

In beiden funktionierenden Zeilen laufen die Prüfungen, nachdem der Post den Browser verlassen hat. Nur dort kann ein Bot sie nicht überspringen.

## Deine Optionen

| Option | Spamprüfungen | Nachteile |
| --- | --- | --- |
| Netlify Forms | Akismet bei jedem Post, plus ein Feld `netlify-honeypot` und reCAPTCHA v2, wenn du sie hinzufügst | Nur auf Netlify. Ein Hostwechsel bedeutet, das Formular neu zu bauen. Siehe [Netlify-Forms-Alternativen](https://ikiform.com/alternatives/netlify-forms) |
| Eine Funktion bei deinem Host | Was immer du schreibst: Honeypot, Token-Prüfungen, ein Rate-Limit mit Speicher | Du schreibst und pflegst sie und brauchst dahinter einen E-Mail- oder Speicherdienst |
| Ein Formular-Backend wie Ikiform | Honeypot, Bewertung, Rate-Limits, erlaubte Domains und eine Menschlichkeitsprüfung, ohne Code | Ein weiterer Dienst, abgerechnet pro Einsendung |

## Das Formular als wiederverwendbares Partial

Egal welchen Endpunkt du wählst, schreib das Formular einmal als Partial oder Include, damit jede Seite denselben Honeypot nutzt. Die Version unten sendet an Ikiform. Um stattdessen eine Funktion zu nutzen, ändere `action` auf den Pfad der Funktion und lass `_redirect` weg.

**Hugo** (`layouts/partials/contact-form.html`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <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="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ "thanks/" | absURL }}" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

**Jekyll** (`_includes/contact-form.html`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <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="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ '/thanks/' | absolute_url }}" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

**Eleventy** (`_includes/contact-form.njk`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <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="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ site.url }}/thanks/" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

`assets/contact.css`:

```html
<style>
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>
```

- Der Honeypot liegt außerhalb des sichtbaren Bereichs, außerhalb der Tab-Reihenfolge und vor Screenreadern verborgen. Die [Honeypot-Anleitung](https://ikiform.com/de/guides/spam-protection/honeypot.md) erklärt jedes Attribut.
- `_redirect` schickt die Leute auf deine Dankeseite. Ikiform folgt ihm nur bei einer `https:`-Adresse auf einer der **Allowed domains** des Formulars.
- Die Anleitungen zu [Hugo](https://ikiform.com/de/docs/guides/hugo.md), [Jekyll](https://ikiform.com/de/docs/guides/jekyll.md) und [Eleventy](https://ikiform.com/de/docs/guides/eleventy.md) behandeln Einrichtung, Shortcodes und GitHub Pages.

## Verbessern, ohne es kaputtzumachen

Ein Skript kann Besucher auf der Seite halten, darf aber nie der einzige Weg sein, wie das Formular funktioniert. Dieses sendet mit `fetch` und fällt in zwei Fällen auf einen normalen Browser-Post zurück: wenn der Netzwerkaufruf scheitert und wenn die Menschlichkeitsprüfung von Ikiform mit `challenge_required` nach einem Token fragt. Ein normaler Post bekommt stattdessen die Prüfseite von Ikiform, du kannst die Prüfung also einschalten, ohne das Skript zu ändern.

`contact-form.html`:

```html
<script>
  document.querySelectorAll("form.contact").forEach((form) => {
    const status = form.querySelector("[role=status]");
    const button = form.querySelector("button");

    form.addEventListener("submit", async (event) => {
      event.preventDefault();
      button.disabled = true;
      try {
        const response = await fetch(form.action, {
          method: "POST",
          headers: { Accept: "application/json" },
          body: new FormData(form),
        });
        const result = await response.json();
        if (result.ok) {
          form.replaceChildren("Thanks. We'll reply by email.");
          return;
        }
        if (result.error.code === "challenge_required") {
          form.submit();
          return;
        }
        const wait = response.headers.get("Retry-After");
        if (response.status === 429 && wait) {
          status.textContent = `Too many messages. Try again in ${wait} seconds.`;
        } else {
          status.textContent = result.error.message;
        }
      } catch {
        form.submit();
        return;
      }
      button.disabled = false;
    });
  });
</script>
```

- `form.submit()` sendet das Formular, ohne das `submit`-Event erneut auszulösen, der Fallback kann also nicht in eine Schleife geraten.
- Ein `429` zeigt aus `Retry-After`, wie lange zu warten ist, was Ikiform für Browser-Code freigibt.
- Die Erfolgszeile ersetzt das Formular, sodass es niemand versehentlich zweimal sendet.

## Wenn du eine Host-Funktion schreibst

Netlify, Cloudflare Pages und Vercel führen alle kleine Funktionen neben statischen Dateien aus. Eine Funktion ist dein eigenes Backend, ihr gehören also die Spamprüfungen. Diese Netlify Function liest den Honeypot, verwirft mit Links vollgestopfte Nachrichten, verifiziert ein Turnstile-Token und schickt dir über Resend eine E-Mail. Jeder abgefangene Post bekommt dieselbe Danke-Weiterleitung wie ein echter.

`netlify/functions/contact.mts`:

```ts
import type { Config, Context } from "@netlify/functions";

const LINK = /https?:\/\/|www\./gi;

export default async (req: Request, context: Context) => {
  if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
  const form = await req.formData();
  const email = String(form.get("email") ?? "");
  const message = String(form.get("message") ?? "");
  const thanks = Response.redirect(new URL("/thanks/", req.url), 303);

  if (String(form.get("_gotcha") ?? "").trim()) return thanks;
  if ((message.match(LINK)?.length ?? 0) >= 3) return thanks;

  const check = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
    method: "POST",
    body: new URLSearchParams({
      secret: process.env.TURNSTILE_SECRET_KEY ?? "",
      response: String(form.get("cf-turnstile-response") ?? ""),
      remoteip: context.ip,
    }),
  });
  const verdict = (await check.json()) as { success: boolean };
  if (!verdict.success) return Response.redirect(new URL("/contact/?error=check", req.url), 303);

  await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      from: "Contact form <form@example.com>",
      to: "you@example.com",
      reply_to: email,
      subject: "New message from the website",
      text: message,
    }),
  });
  return thanks;
};

export const config: Config = { path: "/api/contact" };
```

- `context.ip` ist die IP des Besuchers, die siteverify als `remoteip` nutzen kann.
- Setze `TURNSTILE_SECRET_KEY` und `RESEND_API_KEY` in den Umgebungsvariablen der Site, nie im Repository.
- Sie verwirft abgefangene Posts, statt sie zu speichern, eine versehentlich abgefangene echte Nachricht ist also weg. Logge sie zumindest.
- Sie hat kein Rate-Limit, da eine Funktion zwischen Aufrufen kein Gedächtnis hat. Die [Anleitung zu Rate-Limits](https://ikiform.com/de/guides/spam-protection/rate-limiting.md) fügt eines mit Redis hinzu.

## Was Ikiform für ein statisches Formular tut

Wenn das Formular direkt an Ikiform sendet, passiert die serverseitige Arbeit dort. Der Honeypot heißt `_gotcha`, Inhaltsbewertung und Rate-Limits pro Besucher laufen bei jedem Post, und **Allowed domains** markiert Posts von anderen Sites als Spam. Stell **Spam check** auf **Ikiform** für Cloudflare Turnstile ohne Schlüssel: Der einfache HTML-Post führt es auf einer Prüfseite aus, und das Skript oben fällt auf diese Seite zurück. Ein als Spam eingestufter Post bekommt dieselbe Erfolgsantwort wie ein echter, ein Bot lernt also nichts. Er wird mit dem Status `spam` gespeichert, löst weder Benachrichtigungs-E-Mail noch Webhook aus, zählt nicht zu deiner Nutzung und wird nach 30 Tagen gelöscht. Jeden Montag bekommt ein Workspace, der Spam blockiert hat, eine Zusammenfassung per E-Mail mit der Gesamtzahl, den meistbetroffenen Formularen und den häufigsten Gründen.

> Trag jeden Host, auf dem das Formular veröffentlicht ist, unter **Allowed domains** ein: deine Domain plus, während du testest, die eigene Adresse des Hosts, etwa `username.github.io`. Deploy Previews von Netlify und Preview-URLs von Vercel sind keine Subdomains der Adresse deiner Hauptsite; füge den genauen Preview-Host hinzu, während du eine testest.

## Prüfen, ob es funktioniert

1. Schalte JavaScript aus und sende ab. Das Formular sendet trotzdem, und du landest auf deiner Dankeseite.
2. Schalte JavaScript ein und sende ab. Das Formular wird durch die Dankeszeile ersetzt.
3. 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.
4. Setze **Spam check** auf **Ikiform** und sende mit eingeschaltetem Skript ab. Die Seite fällt auf einen normalen Post zurück und zeigt die Prüfseite.
5. Nichts angekommen? Stell den Statusfilter im Dashboard auf **Spam**. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.

## Stolperfallen

- **`403 origin_not_allowed`, oder jeder Post landet im Spam.** Die 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.
- **Minifiziertes HTML.** Manche Build-Schritte entfernen Attribute, die sie für leer oder ungenutzt halten. Prüfe, ob die gebaute Seite am Honeypot noch `aria-hidden` und `tabindex` hat.
- **Gecachte Dankeseiten.** Wenn die Dankeseite für Spam und echte Posts dieselbe URL hat, was sie sollte, stell nie etwas darauf, das davon abhängt, dass der Post echt ist.
- **Schlüssel im Repository.** Ein Secret, das ins Repository einer öffentlichen statischen Site committet wurde, ist öffentlich. Rotiere es, falls es je dort war.

## Fragen

### Kann ich einer statischen Site ein Captcha hinzufügen?

Du kannst das Widget anzeigen, aber sein Token muss auf einem Server mit einem Secret Key verifiziert werden. Nutze eine Funktion bei deinem Host, ein Formular-Backend, das deine Schlüssel verifiziert, oder die verwaltete Prüfung von Ikiform, die keine Schlüssel braucht.

### Reicht ein Honeypot für eine statische Site?

Er stoppt die häufigsten Bots, aber nur, wenn etwas auf einem Server ihn liest. Kombiniere ihn mit einem Rate-Limit und einer Inhaltsbewertung, die ebenfalls einen Server oder ein Formular-Backend brauchen.

### Warum zeigt mein statisches Formular 405 Method Not Allowed?

Das Formular sendet an eine Seite auf einem Host, der nur Dateien ausliefert. Richte die action des Formulars stattdessen auf eine Funktion oder einen Formular-Backend-Endpunkt aus.

### Funktioniert das Formular, wenn JavaScript aus ist?

Das sollte es. Sende zuerst mit einem einfachen HTML-Formular und füge ein Skript hinzu, das es verbessert und auf einen normalen Post zurückfällt, wenn etwas schiefgeht.

## Weitere Anleitungen

- [Spam im Kontaktformular stoppen](https://ikiform.com/de/guides/spam-protection.md): Stopp Kontaktformular-Spam mit Schichten, die echte Menschen nichts kosten: Honeypot, Timing, Inhaltsbewertung, Rate-Limits und eine Menschlichkeitsprüfung nur bei Bedarf.
- [Cloudflare Turnstile](https://ikiform.com/de/guides/spam-protection/cloudflare-turnstile.md): Füge Cloudflare Turnstile zu einem einfachen HTML-Formular hinzu: das Widget, serverseitiges siteverify, Testschlüssel und ein Weg ganz ohne eigene Schlüssel oder Server.
- [Honeypot-Feld](https://ikiform.com/de/guides/spam-protection/honeypot.md): Füge jedem Formular einen Honeypot hinzu, der Bots abfängt, ohne Screenreader, Tastatur oder Autofill zu stören, mit der Serverprüfung für jeden Stack.
- [Rate-Limits](https://ikiform.com/de/guides/spam-protection/rate-limiting.md): Begrenze Formulareinsendungen pro Besucher mit einem gemeinsamen Speicher, antworte mit 429 und Retry-After und sperre keine Büros aus, die sich eine IP teilen.
- [Ohne Captcha](https://ikiform.com/de/guides/spam-protection/without-captcha.md): Stopp den meisten Formular-Spam ganz ohne Captcha: Honeypot, signierte Render-Zeit, Inhaltsbewertung und Rate-Limit, zusammen statt einzeln eingesetzt.
- [Alternativen zu reCAPTCHA](https://ikiform.com/de/guides/spam-protection/recaptcha-alternatives.md): Vergleiche reCAPTCHA-Alternativen für Formulare, von Turnstile und hCaptcha bis Proof-of-Work und ganz ohne Captcha, und wechsle mit einer kleinen, sicheren Änderung.
