# E-Mail-Anmeldeformular auf einer statischen Site, ohne Server

> Füge einer statischen Site mit Hugo, Jekyll, Eleventy oder Astro ein Newsletter-Formular hinzu, halte den API-Key des E-Mail-Tools aus der Seite heraus und bestätige im Tool.

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

Auf einer statischen Site sendet das Formular aus dem Browser des Lesers direkt an Ikiform, weil es keinen eigenen Server gibt, an den es senden könnte. Halte den API-Key deines E-Mail-Tools komplett aus der Site heraus: Der Browser kennt nur deinen öffentlichen Ikiform-Endpunkt, und ein woanders gehostetes Relay fügt den Abonnenten hinzu. Schreib das Formular einmal als Partial oder Include, übergib pro Platzierung einen anderen `source` und füge ein kleines Skript für die Version ohne Neuladen hinzu.

## Was hier anders ist

Eine statische Site sind Dateien auf einem Host: GitHub Pages, Netlify, Cloudflare Pages, ein S3-Bucket. Nichts, was du schreibst, läuft auf einem Server, wenn ein Leser ein Formular absendet. Das schließt die übliche Abkürzung aus, die API deines E-Mail-Tools aus der Seite aufzurufen, denn alles, was der Browser ausführt, kann jeder lesen.

API-Keys von E-Mail-Tools sind selten darauf beschränkt, einen Abonnenten hinzuzufügen. Ein Key, der einen Abonnenten hinzufügen kann, kann meist auch die ganze Liste lesen, exportieren oder löschen. Steckt er im JavaScript einer statischen Site, wird er an jeden Besucher ausgeliefert und von dort an jeden, der die Entwicklertools öffnet.

Diagramm: Wo der API-Key des E-Mail-Tools auf einer statischen Site liegt

1. Mit dem Key in der Seite lädt jeder Besucher ihn herunter, und jeder kann damit die API des E-Mail-Tools aufrufen.
2. Mit einem Relay kennt die Seite nur deinen Ikiform-Endpunkt, der Formular-Posts annimmt und sonst nichts.
3. Ikiform filtert Spam und sendet einen signierten Webhook an das Relay.
4. Das Relay, außerhalb der statischen Site gehostet, hält den API-Key und fügt den Abonnenten hinzu.

Den Formular-Endpunkt kannst du bedenkenlos veröffentlichen. Den API-Key nie.

## Das Formular einmal schreiben

Ein Newsletter-Formular taucht in mehreren Templates auf: im Footer, am Ende jedes Beitrags, auf einer Newsletter-Seite. Mach daraus ein Partial, das einen `source`-Wert nimmt, damit jede Platzierung sich selbst festhält. Das Markup ist in jedem Generator identisch; nur die Template-Syntax für `source` ändert sich.

**Hugo** (`layouts/partials/newsletter.html`)

```html
<form data-newsletter action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <input name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-label="Email" />
  <label><input name="consent" type="checkbox" value="true" required /> Email me the newsletter. I can unsubscribe at any time.</label>
  <input type="hidden" name="source" value="{{ .source | default "footer" }}" />
  <input type="hidden" name="_redirect" value="https://example.com/subscribed/" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Subscribe</button>
  <p role="status" hidden></p>
</form>
```

**Jekyll** (`_includes/newsletter.html`)

```html
<form data-newsletter action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <input name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-label="Email" />
  <label><input name="consent" type="checkbox" value="true" required /> Email me the newsletter. I can unsubscribe at any time.</label>
  <input type="hidden" name="source" value="{{ include.source | default: "footer" }}" />
  <input type="hidden" name="_redirect" value="https://example.com/subscribed/" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Subscribe</button>
  <p role="status" hidden></p>
</form>
```

**Eleventy** (`_includes/newsletter.njk`)

```html
<form data-newsletter action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <input name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-label="Email" />
  <label><input name="consent" type="checkbox" value="true" required /> Email me the newsletter. I can unsubscribe at any time.</label>
  <input type="hidden" name="source" value="{{ source or "footer" }}" />
  <input type="hidden" name="_redirect" value="https://example.com/subscribed/" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Subscribe</button>
  <p role="status" hidden></p>
</form>
```

**Astro** (`src/components/Newsletter.astro`)

```astro
---
interface Props {
  source?: string;
}
const { source = "footer" } = Astro.props;
---

<form data-newsletter action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <input name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-label="Email" />
  <label><input name="consent" type="checkbox" value="true" required /> Email me the newsletter. I can unsubscribe at any time.</label>
  <input type="hidden" name="source" value={source} />
  <input type="hidden" name="_redirect" value="https://example.com/subscribed/" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Subscribe</button>
  <p role="status" hidden></p>
</form>
```

Setze es dann mit dem Namen der Platzierung in ein Template ein:

**Hugo** (`layouts/_default/single.html`)

```html
{{ partial "newsletter.html" (dict "source" "post") }}
```

**Jekyll** (`_layouts/post.html`)

```html
{% include newsletter.html source="post" %}
```

**Eleventy** (`_includes/post.njk`)

```html
{% set source = "post" %}
{% include "newsletter.njk" %}
```

- In Astro nutzt du `<Newsletter source="post" />`. Ein statischer Astro-Build kann keine Actions ausführen, also sendet die Komponente wie die anderen direkt an Ikiform.
- Füge `source` in Ikiform als Text mit kurzer Maximallänge zur Felddefinition des Formulars hinzu. Sind Felder definiert, wird ein Input, den die Definition nicht listet, mit `unknown_field` abgelehnt.
- `_redirect` muss eine `https:`-URL auf einer der **Allowed domains** des Formulars sein. Leg die Seite an: `/subscribed/` mit einer Zeile, die Lesern sagt, dass sie in ihren Posteingang schauen sollen.

## Ein Skript für jedes Formular auf der Seite

Eine Beitragsseite kann zwei Newsletter-Formulare haben, eins nach dem Artikel und eins im Footer. Dieses Skript übernimmt jedes Formular mit `data-newsletter`, und wenn eins klappt, ersetzt es alle, damit niemand ein paar Zentimeter tiefer erneut zum Abonnieren aufgefordert wird. Lade es einmal aus deinem Basis-Layout.

`static/newsletter.js`:

```js
for (const form of document.querySelectorAll("form[data-newsletter]")) {
  const status = form.querySelector('[role="status"]');
  const button = form.querySelector("button");

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    button.disabled = true;
    status.hidden = false;
    status.textContent = "Subscribing";
    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) {
        for (const other of document.querySelectorAll("form[data-newsletter]")) {
          other.replaceChildren(Object.assign(document.createElement("p"), {
            textContent: "Almost done. Check your inbox for a confirmation link.",
          }));
        }
        return;
      }
      if (response.status === 429 && response.headers.has("Retry-After")) {
        status.textContent = "Too many tries from this network. Wait a minute and try again.";
      } else {
        const errors = result.error.fields ?? {};
        status.textContent = errors.email?.[0] ?? errors.consent?.[0] ?? result.error.message;
      }
    } catch {
      form.submit();
      return;
    }
    button.disabled = false;
  });
}
```

- Scheitert `fetch`, etwa hinter einem strengen Netzwerkfilter, fällt das Skript auf `form.submit()` zurück, das auf dem einfachen Weg sendet und auf `/subscribed/` landet.
- Ein `429` bekommt eine eigene Meldung. Leser in einem Büro- oder Campus-Netz teilen sich eine IP und können das Limit pro Besucher gemeinsam erreichen.
- Das Formular funktioniert mit ausgeschaltetem JavaScript. Das Skript ändert nur, wie die Antwort angezeigt wird.

Leser mit ausgeschaltetem JavaScript landen auf `/subscribed/`, also lass diese Seite das tun, was das Skript für alle anderen tut: Sag, dass eine Bestätigungs-E-Mail unterwegs ist, nenne den Absender, nach dem sie suchen sollen, erwähne den Spam-Ordner und verlinke zurück zum Blog. Es ist eine gewöhnliche Seite in deinem Generator, gebaut wie jede andere, und auch der Ort, auf den du danach den Bestätigungslink des E-Mail-Tools richten kannst.

## Wo das Relay lebt

Das Relay braucht eine öffentliche HTTPS-URL, die Code ausführt. Dein statischer Host bietet vielleicht eine an, vielleicht nicht:

| Statischer Host | Optionen für das Relay |
| --- | --- |
| GitHub Pages, S3 | Keine Functions. Nutze einen Cloudflare Worker auf einer eigenen Domain oder ein Automatisierungstool |
| Netlify | Eine Netlify Function auf der Domain deiner Site oder eine der Optionen oben |
| Cloudflare Pages | Ein separater Worker auf einer eigenen Domain. Ikiform lehnt `pages.dev`- und `workers.dev`-URLs ab |
| Vercel | Eine Funktion auf deiner Domain. Statische Ausgabe kann neben einer API-Route liegen |

Der Relay-Code für jedes E-Mail-Tool steht auf den Seiten zu [Kit](https://ikiform.com/de/guides/newsletter-signup/kit.md), [MailerLite](https://ikiform.com/de/guides/newsletter-signup/mailerlite.md) und [beehiiv](https://ikiform.com/de/guides/newsletter-signup/beehiiv.md). Ganz ohne Code richtest du den Webhook auf [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) oder [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md) aus und nutzt deren Aktion für dein E-Mail-Tool. Die [Webhook-Doku](https://ikiform.com/de/docs/webhooks.md) listet die URL-Regeln.

> Statische Sites haben oft Deploy-Previews auf einer separaten Domain. Posts von einer Domain, die nicht in **Allowed domains** steht, werden als Spam markiert oder abgelehnt, also füge die Preview-Domain während des Testens hinzu oder teste auf der Live-Domain und `localhost`.

## Wenn deine Site eine Content Security Policy sendet

Viele statische Hosts lassen dich Header aus einer Datei setzen, etwa `_headers` auf Netlify und Cloudflare Pages. Sendet deiner eine Content-Security-Policy, blockiert der Browser das Formular, außer die Policy erlaubt Ikiform. `form-action` deckt den einfachen Post ab und `connect-src` das `fetch` des Skripts:

`_headers`:

```text
Content-Security-Policy: default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com
```

Ein blockierter Post erscheint als CSP-Fehler in der Browser-Konsole und erreicht Ikiform nie, also erscheint auch im Dashboard nichts.

## Prüfen, ob es funktioniert

1. Füge deine Live-Domain und `localhost` unter **Allowed domains** hinzu.
2. Baue die Site und öffne eine Beitragsseite. Beide Formulare sind da, jedes mit seinem eigenen `source`-Wert im Seitenquelltext.
3. Abonniere über das Beitragsformular mit einer Adresse, die dir gehört. Beide Formulare wechseln zur Zeile „Schau in deinen Posteingang“.
4. Schalte JavaScript aus und abonniere über den Footer. Du landest auf `/subscribed/`.
5. Öffne **Submissions** im Dashboard. Die beiden Anmeldungen zeigen `post` und `footer` als `source`.
6. Durchsuche die gebaute Site nach dem API-Key deines E-Mail-Tools. Er ist nicht da.

## Stolperfallen

| Problem | Was zu tun ist |
| --- | --- |
| Nichts im Dashboard und ein CSP-Fehler in der Konsole | Füge `https://ikiform.com` zu `form-action` und `connect-src` in deiner Content-Security-Policy hinzu. |
| `422` mit `unknown_field` für `source` | Füge `source` zur Felddefinition des Formulars hinzu oder entferne den Hidden Input. |
| `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. |
| 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

### Kann ich einer statischen Site ohne Backend ein Newsletter-Formular hinzufügen?

Ja. Das Formular sendet aus dem Browser an ein Formular-Backend wie Ikiform, das jede Anmeldung speichert und filtert. Ein woanders gehostetes Relay oder Automatisierungstool reicht sie an dein E-Mail-Tool weiter, sodass die statische Site selbst keinen Server-Code ausführt.

### Kann ich die API meines E-Mail-Tools direkt aus der statischen Seite aufrufen?

Lass es. Jeder Key im JavaScript der Seite ist für jeden Besucher sichtbar, und Keys von E-Mail-Tools können meist die ganze Liste lesen oder ändern. Halte den Key in einem Relay, das außerhalb der Site läuft.

### Funktioniert das auf GitHub Pages?

Ja. Das Formular ist reines HTML und sendet an Ikiform. GitHub Pages kann das Relay nicht ausführen, also hoste es auf einem Cloudflare Worker mit eigener Domain oder nutze ein Automatisierungstool wie Zapier oder n8n.

### Wer sendet die Bestätigungs-E-Mail?

Dein E-Mail-Tool. Ikiform schreibt der Person, die sich anmeldet, nie eine E-Mail. Das Relay fügt den Leser als unbestätigt hinzu, und das Tool sendet seinen eigenen Bestätigungslink.

## Weitere Anleitungen

- [Newsletter-Anmeldeformular](https://ikiform.com/de/guides/newsletter-signup.md): Baue ein Newsletter-Anmeldeformular mit eigenem Markup, speichere die Einwilligung, blocke Bot-Anmeldungen und gib jeden Abonnenten an Kit, MailerLite, beehiiv oder ein anderes Tool weiter.
- [Double-Opt-in-Verfahren](https://ikiform.com/de/guides/newsletter-signup/double-opt-in.md): Ikiform sendet keine Bestätigungs-E-Mails, also läuft Double-Opt-in in deinem E-Mail-Tool: Ein Relay legt jede Anmeldung als unbestätigt an, und das Tool sendet den Link.
- [Kit (ConvertKit)](https://ikiform.com/de/guides/newsletter-signup/kit.md): Verbinde dein eigenes Newsletter-Formular mit Kit, über ein Relay, das den Abonnenten anlegt, ihn für die Incentive-E-Mail einem Kit-Formular hinzufügt und die Platzierung taggt.
- [beehiiv](https://ikiform.com/de/guides/newsletter-signup/beehiiv.md): Verbinde dein eigenes Anmeldeformular mit beehiiv: Ein Relay legt jedes Abo mit aktivem Double Opt-in an, mit der Platzierung als UTM-Quelle und der Seite als Referrer.
- [MailerLite](https://ikiform.com/de/guides/newsletter-signup/mailerlite.md): Verbinde dein eigenes Anmeldeformular mit MailerLite: Ein Relay fügt neue Leser für das Double Opt-in unbestätigt hinzu und bittet einen Abonnenten nie zweimal um Bestätigung.
- [Newsletter-Formular mit Next.js](https://ikiform.com/de/guides/newsletter-signup/nextjs.md): Ein Newsletter-Formular im Footer mit Next.js: eine Server Action mit Einwilligungsprüfung und Turnstile, die an Ikiform sendet, plus ein Route Handler, der Leser zu Kit hinzufügt.
