# Feedback-Formular zu deiner Website oder App hinzufügen

> Setze ein Feedback-Formular auf jede Seite oder in deine App: eine Bewertung, ein Kommentar, die Herkunftsseite, Spamfilter und Alerts bei niedrigen Werten.

Veröffentlicht am 2026-10-25 · https://ikiform.com/de/guides/feedback-form

Füge ein kurzes Formular mit einer Bewertung von 1 bis 5, einem optionalen Kommentar und einer optionalen E-Mail hinzu und sende es an ein Formular-Backend, damit du dafür keinen Server betreibst. Schick bei jeder Antwort die Seitenadresse mit, denn der Browser verrät dir die vollständige URL nicht von selbst. Leite niedrige Bewertungen noch am selben Tag in deinen Team-Chat und bewahre alle Antworten an einem Ort auf, um Trends zu sehen.

Die meisten Leute sagen dir nie, was mit deiner Site nicht stimmt. Sie gehen einfach. Ein kleines Feedback-Formular fängt die wenigen ab, die es sagen, in dem Moment, in dem sie es bemerken, auf der Seite, die es verursacht hat. Diese Anleitung baut eins, das zu jeder Site oder App passt, die Herkunftsseite mitsendet, Bots fernhält und unzufriedene Antworten noch am selben Tag vor dein Team bringt.

## So funktioniert ein Feedback-Formular

Das Formular sendet an Ikiform. Ikiform validiert die Bewertung, prüft den Post auf Spam, speichert ihn, schickt deinem Team eine E-Mail und sendet einen signierten Webhook. Über den Webhook gehen niedrige Bewertungen in einen Chat-Channel, und jede Antwort kann in eine Tabelle oder Datenbank wandern.

Diagramm: Wie ein Feedback von deiner Seite zu deinem Team gelangt

1. Ein Besucher wählt eine Bewertung, schreibt vielleicht einen Kommentar, und das Formular sendet beides mit der Seitenadresse.
2. Ikiform prüft, ob die Bewertung eine ganze Zahl von 1 bis 5 ist, führt die Spamprüfungen aus und speichert die Antwort.
3. Die Antwort erscheint im Dashboard, wo du sie filtern und als CSV exportieren kannst.
4. Ikiform schickt deinem Team eine E-Mail. Hat der Besucher eine E-Mail hinterlassen, geht „Antworten“ direkt an ihn.
5. Ikiform sendet einen signierten submission.created-Webhook mit den Feldern und den Browser-Metadaten.
6. Ein kleines Relay schickt nur die niedrigen Bewertungen in deinen Team-Chat, damit sie heute noch gelesen werden.

Das Relay ist optional. Starte mit der E-Mail und dem Dashboard und füge den Chat-Alert hinzu, wenn sich die Antworten häufen.

## Wege, Feedback zu sammeln

Jede Art von Tool ist für eine andere Aufgabe gebaut. Wähle danach, was du sehen musst, wenn eine Antwort ankommt, nicht nach dem Aussehen des Widgets.

| Ansatz | Stark bei | Nachteile |
| --- | --- | --- |
| Website-Feedback-Tools wie Hotjar oder Usersnap | Screenshots, Anmerkungen und Seitenkontext, ohne Code außer einem Script-Tag. | Ein Drittanbieter-Skript auf jeder Seite, Antworten in deren Produkt gespeichert und Preise nach deren Tarifen. |
| Das Widget eines Error-Trackers wie Sentry User Feedback | Verknüpft einen Bericht mit dem Fehler und, bei aktivem Session Replay, mit einer Aufzeichnung des Geschehens. | Für Bugs gebaut, nicht für Meinungen. Du brauchst zuerst das SDK des Trackers in deiner App. |
| Formular-Baukästen wie [Typeform](https://ikiform.com/alternatives/typeform), [Tally](https://ikiform.com/alternatives/tally) oder [Google Forms](https://ikiform.com/alternatives/google-forms) | Ausgefeilte Formulare ohne Code. Gut für eine Umfrage, die du per Link verschickst. | Ein iframe oder eine separate Seite, also verliert die Antwort die Seite, um die es ging, außer du übergibst sie in der URL. |
| Feedback-Boards wie Canny | Öffentliche Abstimmungen und Statusupdates zu Feature-Wünschen. | Für Ideen gebaut, nicht für schnelle Reaktionen. Antworten sind bewusst öffentlich. |
| Deine eigene API-Route und Datenbank | Volle Kontrolle über die Daten und jede Logik. | Spamfilter, Rate-Limits, Benachrichtigungs-E-Mails und Export baust du selbst, und du brauchst einen Server. |
| Ein Formular-Backend wie Ikiform | Dein eigenes Markup auf jeder Site oder App. Validierung, Spamfilter, Speicherung, E-Mail und signierte Webhooks ohne Server. | Keine Screenshots, weil Datei-Uploads nicht unterstützt werden, und keine Abstimmungen oder Session Replay. |

Brauchst du bei jedem Bericht Screenshots, lohnt sich das Skript eines Feedback-Tools oder Error-Trackers. Soll das Formular wie deine Site aussehen, sollen die Antworten portabel bleiben und willst du selbst entscheiden, wohin sie gehen, nimm ein Formular-Backend. Der Rest dieser Anleitung macht genau das.

## Das Formular

Drei Fragen reichen: eine Bewertung, ein Kommentar und eine optionale E-Mail. Die Bewertung beantworten die Leute; im Kommentar steckt der Wert; über die E-Mail schließt du den Kreis. Wähle unten deinen Stack; die Auswahl gilt für jedes Codebeispiel in diesen Anleitungen.

**HTML**

Ein Block Markup und ein kurzes Skript. Ohne JavaScript sendet das Formular trotzdem und leitet weiter; mit JavaScript bleibt der Besucher auf der Seite und sieht den Fehler des Servers neben dem Button. Das Skript füllt `page` mit der Adresse ohne Query-String, damit Tokens in URLs nie in deinem Posteingang landen.

`feedback.html`:

```html
<form id="feedback" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <fieldset>
    <legend>How useful was this page?</legend>
    <label><input type="radio" name="rating" value="1" required /> 1</label>
    <label><input type="radio" name="rating" value="2" /> 2</label>
    <label><input type="radio" name="rating" value="3" /> 3</label>
    <label><input type="radio" name="rating" value="4" /> 4</label>
    <label><input type="radio" name="rating" value="5" /> 5</label>
  </fieldset>
  <label for="comment">What would make it better?</label>
  <textarea id="comment" name="comment" rows="4" maxlength="4000"></textarea>
  <label for="feedback-email">Email, if you'd like a reply</label>
  <input id="feedback-email" name="email" type="email" />
  <input type="hidden" name="page" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send feedback</button>
  <p id="feedback-status" role="status"></p>
</form>

<script>
  const form = document.getElementById("feedback");
  const status = document.getElementById("feedback-status");
  form.elements.page.value = location.origin + location.pathname;

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const button = form.querySelector("button");
    button.disabled = true;
    status.textContent = "Sending";
    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(Object.assign(document.createElement("p"), { textContent: "Thanks. The team reads every note." }));
        return;
      }
      const fields = result.error.fields ?? {};
      status.textContent = fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message;
    } catch {
      status.textContent = "Couldn't send it. Try again in a moment.";
    }
    button.disabled = false;
  });
</script>
```

**Next.js**

Die Server Action liest den `Referer`-Header ihres eigenen Posts. Formular und Action teilen sich einen Origin, also sendet der Browser die vollständige Seiten-URL, und die Action behält nur Origin und Pfad. Ein Hidden Input ist nicht nötig.

`app/feedback/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface FeedbackState {
  sent: boolean;
  error: string;
}

export async function sendFeedback(_previous: FeedbackState, formData: FormData): Promise<FeedbackState> {
  const rating = Number(formData.get("rating"));
  if (!Number.isInteger(rating) || rating < 1 || rating > 5) return { sent: false, error: "Pick a score from 1 to 5." };

  const incoming = await headers();
  const referer = incoming.get("referer");
  const origin = incoming.get("origin");
  const page = referer ? new URL(referer) : null;

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      rating,
      comment: String(formData.get("comment") ?? ""),
      email: String(formData.get("email") ?? ""),
      page: page ? page.origin + page.pathname : "",
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { sent: true, error: "" };
  const fields = result?.error?.fields ?? {};
  return { sent: false, error: fields.comment?.[0] ?? fields.email?.[0] ?? "Couldn't send it. Try again." };
}
```

`app/feedback/feedback-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type FeedbackState, sendFeedback } from "./actions";

const initial: FeedbackState = { sent: false, error: "" };

export function FeedbackForm() {
  const [state, action, pending] = useActionState(sendFeedback, initial);

  if (state.sent) return <p role="status">Thanks. The team reads every note.</p>;

  return (
    <form action={action}>
      <fieldset>
        <legend>How useful was this page?</legend>
        {[1, 2, 3, 4, 5].map((score) => (
          <label key={score}>
            <input type="radio" name="rating" value={score} required /> {score}
          </label>
        ))}
      </fieldset>
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} />
      <label htmlFor="feedback-email">Email, if you'd like a reply</label>
      <input id="feedback-email" name="email" type="email" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.error ? <p role="alert">{state.error}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send feedback"}</button>
    </form>
  );
}
```

Mehr zu Next.js? Lies [Feedback-Formular in der App mit Next.js bauen](https://ikiform.com/de/guides/feedback-form/nextjs.md).

**Astro**

Die Seite kennt beim Rendern ihre eigene Adresse, also füllt `Astro.url` den versteckten `page`-Input auf dem Server. Leere Inputs erreichen die Action als `null`, was Ikiform als fehlend behandelt. Die Seite muss on demand rendern, damit das Ergebnis der Action angezeigt wird.

`src/actions/index.ts`:

```ts
import { ActionError, defineAction } from "astro:actions";
import { getSecret } from "astro:env/server";
import { z } from "astro/zod";

export const server = {
  sendFeedback: defineAction({
    accept: "form",
    input: z.object({
      rating: z.coerce.number().int().min(1).max(5),
      comment: z.string().max(4000).nullish(),
      email: z.email().nullish(),
      page: z.string().nullish(),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify(input),
      });
      const result = await response.json();
      if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
      return { sent: true };
    },
  }),
};
```

`src/components/FeedbackForm.astro`:

```astro
---
import { actions, isInputError } from "astro:actions";

const result = Astro.getActionResult(actions.sendFeedback);
const page = Astro.url.origin + Astro.url.pathname;
---

{result?.data?.sent ? (
  <p role="status">Thanks. The team reads every note.</p>
) : (
  <form method="POST" action={actions.sendFeedback}>
    <fieldset>
      <legend>How useful was this page?</legend>
      {[1, 2, 3, 4, 5].map((score) => (
        <label><input type="radio" name="rating" value={score} required /> {score}</label>
      ))}
    </fieldset>
    <label for="comment">What would make it better?</label>
    <textarea id="comment" name="comment" rows="4" maxlength="4000"></textarea>
    <label for="feedback-email">Email, if you'd like a reply</label>
    <input id="feedback-email" name="email" type="email" />
    <input type="hidden" name="page" value={page} />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && (
      <p role="alert">{isInputError(result.error) ? "Pick a score and check your email address." : result.error.message}</p>
    )}
    <button>Send feedback</button>
  </form>
)}
```

Mehr zu Astro? Lies [Feedback-Formular in der App mit Astro bauen](https://ikiform.com/de/guides/feedback-form/astro.md).

**TanStack Start**

Eine POST-Server-Funktion prüft die Bewertung, bevor irgendetwas deinen Server verlässt. Die Komponente setzt `page` beim Absenden aus `window.location`, also stimmt der Wert auch nach clientseitiger Navigation.

`src/components/FeedbackForm.tsx`:

```tsx
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";

const sendFeedback = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const rating = Number(data.get("rating"));
    if (!Number.isInteger(rating) || rating < 1 || rating > 5) throw new Error("Pick a score from 1 to 5.");
    return {
      rating,
      comment: String(data.get("comment") ?? ""),
      email: String(data.get("email") ?? ""),
      page: String(data.get("page") ?? ""),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }) => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(data),
    });
    const result = await response.json();
    if (result.ok) return { sent: true, error: "" };
    const fields = result.error.fields ?? {};
    return { sent: false, error: String(fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message) };
  });

export function FeedbackForm() {
  const send = useServerFn(sendFeedback);
  const [state, setState] = useState({ sent: false, error: "" });
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    data.set("page", window.location.origin + window.location.pathname);
    setPending(true);
    try {
      setState(await send({ data }));
    } catch (error) {
      setState({ sent: false, error: error instanceof Error ? error.message : "Couldn't send it. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (state.sent) return <p role="status">Thanks. The team reads every note.</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <fieldset>
        <legend>How useful was this page?</legend>
        {[1, 2, 3, 4, 5].map((score) => (
          <label key={score}>
            <input type="radio" name="rating" value={score} required /> {score}
          </label>
        ))}
      </fieldset>
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} />
      <label htmlFor="feedback-email">Email, if you'd like a reply</label>
      <input id="feedback-email" name="email" type="email" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.error ? <p role="alert">{state.error}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send feedback"}</button>
    </form>
  );
}
```

Mehr zu TanStack Start? Lies [Feedback-Formular in der App mit TanStack Start bauen](https://ikiform.com/de/guides/feedback-form/tanstack-start.md).

**React Router**

Eine Resource Route enthält die Action, und `useFetcher` sendet von jeder Seite aus ohne Navigation an sie. Registriere die Route als `api/feedback` in `app/routes.ts`. `useLocation` liefert den Pfad, und die Action löst ihn gegen ihre eigene URL auf, die den Origin deiner Site hat.

`app/routes/api.feedback.ts`:

```ts
import { data } from "react-router";
import type { Route } from "./+types/api.feedback";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const origin = request.headers.get("origin");
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      rating: String(formData.get("rating") ?? ""),
      comment: String(formData.get("comment") ?? ""),
      email: String(formData.get("email") ?? ""),
      page: new URL(String(formData.get("path") ?? "/"), request.url).href,
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { sent: true, error: "" };
  const fields = result.error.fields ?? {};
  return data(
    { sent: false, error: String(fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message) },
    { status: response.status },
  );
}
```

`app/components/feedback-form.tsx`:

```tsx
import { useFetcher, useLocation } from "react-router";
import type { action } from "../routes/api.feedback";

export function FeedbackForm() {
  const fetcher = useFetcher<typeof action>();
  const { pathname } = useLocation();
  const pending = fetcher.state !== "idle";

  if (fetcher.data?.sent) return <p role="status">Thanks. The team reads every note.</p>;

  return (
    <fetcher.Form method="post" action="/api/feedback">
      <fieldset>
        <legend>How useful was this page?</legend>
        {[1, 2, 3, 4, 5].map((score) => (
          <label key={score}>
            <input type="radio" name="rating" value={score} required /> {score}
          </label>
        ))}
      </fieldset>
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} />
      <label htmlFor="feedback-email">Email, if you'd like a reply</label>
      <input id="feedback-email" name="email" type="email" />
      <input type="hidden" name="path" value={pathname} />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {fetcher.data?.error ? <p role="alert">{fetcher.data.error}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send feedback"}</button>
    </fetcher.Form>
  );
}
```

Mehr zu React Router? Lies [Feedback-Formular in der App mit React Router bauen](https://ikiform.com/de/guides/feedback-form/react-router.md).

**SvelteKit**

Ein `+server.ts`-Endpunkt leitet den Post weiter, sodass die Komponente in einem Layout sitzen und auf jeder Seite funktionieren kann. `page` aus `$app/state` liefert die aktuelle URL. Deklariere `IKIFORM_SECRET_KEY` in `src/env.ts` mit `defineEnvVars`, damit `$app/env/private` ihn lesen kann. Der Endpunkt gibt `Response.json()` zurück, da SvelteKit 3 den `json()`-Helper als veraltet markiert.

`src/routes/api/feedback/+server.ts`:

```ts
import { error } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { RequestHandler } from "./$types";

export const POST: RequestHandler = async ({ request, fetch, url }) => {
  const body = await request.json();
  const rating = Number(body.rating);
  if (!Number.isInteger(rating) || rating < 1 || rating > 5) error(400, "Pick a score from 1 to 5.");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
      Origin: request.headers.get("origin") ?? url.origin,
    },
    body: JSON.stringify({
      rating,
      comment: String(body.comment ?? ""),
      email: String(body.email ?? ""),
      page: String(body.page ?? ""),
      _gotcha: String(body._gotcha ?? ""),
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    const fields = result.error.fields ?? {};
    error(response.status, String(fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message));
  }
  return Response.json({ sent: true });
};
```

`src/lib/FeedbackForm.svelte`:

```svelte
<script lang="ts">
  import { page } from "$app/state";

  let rating = $state(0);
  let comment = $state("");
  let email = $state("");
  let trap = $state("");
  let status = $state<"idle" | "sending" | "sent">("idle");
  let message = $state("");

  async function send(event: SubmitEvent) {
    event.preventDefault();
    status = "sending";
    message = "";
    const response = await fetch("/api/feedback", {
      method: "POST",
      headers: { Accept: "application/json", "Content-Type": "application/json" },
      body: JSON.stringify({ rating, comment, email, page: page.url.origin + page.url.pathname, _gotcha: trap }),
    });
    if (response.ok) {
      status = "sent";
      return;
    }
    message = (await response.json().catch(() => null))?.message ?? "Couldn't send it. Try again.";
    status = "idle";
  }
</script>

{#if status === "sent"}
  <p role="status">Thanks. The team reads every note.</p>
{:else}
  <form onsubmit={send}>
    <fieldset>
      <legend>How useful was this page?</legend>
      {#each [1, 2, 3, 4, 5] as score}
        <label><input type="radio" name="rating" value={score} bind:group={rating} required /> {score}</label>
      {/each}
    </fieldset>
    <label for="comment">What would make it better?</label>
    <textarea id="comment" name="comment" rows="4" maxlength="4000" bind:value={comment}></textarea>
    <label for="feedback-email">Email, if you'd like a reply</label>
    <input id="feedback-email" name="email" type="email" bind:value={email} />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden bind:value={trap} />
    {#if message}
      <p role="alert">{message}</p>
    {/if}
    <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send feedback"}</button>
  </form>
{/if}
```

Mehr zu SvelteKit? Lies [Feedback-Formular in der App mit SvelteKit bauen](https://ikiform.com/de/guides/feedback-form/sveltekit.md).

**Nuxt**

Eine Server-Route hält den Key in `runtimeConfig.ikiformSecretKey`, befüllt aus `NUXT_IKIFORM_SECRET_KEY`. `useRoute().path` und `useRequestURL()` bauen die Seitenadresse auf dem Server wie im Browser.

`server/api/feedback.post.ts`:

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ rating?: number; comment?: string; email?: string; page?: string; _gotcha?: string }>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.ikiformSecretKey}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      rating: body.rating,
      comment: body.comment ?? "",
      email: body.email ?? "",
      page: body.page ?? "",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    const fields = result.error.fields ?? {};
    throw createError({
      status: response.status,
      message: String(fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message),
    });
  }
  return { sent: true };
});
```

`app/components/FeedbackForm.vue`:

```vue
<script setup lang="ts">
const route = useRoute();
const requestUrl = useRequestURL();
const rating = ref<number | null>(null);
const comment = ref("");
const email = ref("");
const trap = ref("");
const status = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");

async function send() {
  status.value = "sending";
  error.value = "";
  try {
    await $fetch("/api/feedback", {
      method: "POST",
      body: {
        rating: rating.value,
        comment: comment.value,
        email: email.value,
        page: requestUrl.origin + route.path,
        _gotcha: trap.value,
      },
    });
    status.value = "sent";
  } catch (failure) {
    error.value = (failure as { data?: { message?: string } }).data?.message ?? "Couldn't send it. Try again.";
    status.value = "idle";
  }
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks. The team reads every note.</p>
  <form v-else @submit.prevent="send">
    <fieldset>
      <legend>How useful was this page?</legend>
      <label v-for="score in 5" :key="score">
        <input v-model="rating" type="radio" name="rating" :value="score" required /> {{ score }}
      </label>
    </fieldset>
    <label for="comment">What would make it better?</label>
    <textarea id="comment" v-model="comment" name="comment" rows="4" maxlength="4000" />
    <label for="feedback-email">Email, if you'd like a reply</label>
    <input id="feedback-email" v-model="email" name="email" type="email" />
    <input v-model="trap" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Send feedback" }}</button>
  </form>
</template>
```

Mehr zu Nuxt? Lies [Feedback-Formular in der App mit Nuxt bauen](https://ikiform.com/de/guides/feedback-form/nuxt.md).

**React**

Ohne eigenen Server sendet der Browser direkt an Ikiform. `submit()` aus `@ikiform/sdk` sendet die `FormData` und gibt `{ ok }` oder die Feldfehler zurück. Setze hier nie einen Secret Key ein; stattdessen schützt **Allowed domains** das Formular.

`src/FeedbackForm.tsx`:

```tsx
import { submit } from "@ikiform/sdk";
import { type FormEvent, useState } from "react";

const FORM_ID = "frm_xxxxxxxxxxxxxxxx";

export function FeedbackForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    data.set("page", window.location.origin + window.location.pathname);
    setStatus("sending");
    setError("");
    const result = await submit(FORM_ID, data);
    if (result.ok) {
      setStatus("sent");
      return;
    }
    const fields = result.error.fields ?? {};
    setError(fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message);
    setStatus("idle");
  }

  if (status === "sent") return <p role="status">Thanks. The team reads every note.</p>;

  return (
    <form onSubmit={onSubmit}>
      <fieldset>
        <legend>How useful was this page?</legend>
        {[1, 2, 3, 4, 5].map((score) => (
          <label key={score}>
            <input type="radio" name="rating" value={score} required /> {score}
          </label>
        ))}
      </fieldset>
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} />
      <label htmlFor="feedback-email">Email, if you'd like a reply</label>
      <input id="feedback-email" name="email" type="email" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send feedback"}</button>
    </form>
  );
}
```

**Vue**

Derselbe browserseitige Post in einer Single-File-Komponente. Das Label des Kommentars ändert sich bei einer Bewertung von 1 oder 2, was von unzufriedenen Besuchern konkretere Antworten bringt.

`src/components/FeedbackForm.vue`:

```vue
<script setup lang="ts">
import { ref } from "vue";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

const rating = ref(0);
const status = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");

async function send(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  body.set("page", window.location.origin + window.location.pathname);
  status.value = "sending";
  error.value = "";
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      status.value = "sent";
      return;
    }
    const fields = result.error.fields ?? {};
    error.value = fields.rating?.[0] ?? fields.comment?.[0] ?? fields.email?.[0] ?? result.error.message;
  } catch {
    error.value = "Couldn't send it. Try again in a moment.";
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks. The team reads every note.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
    <fieldset>
      <legend>How useful was this page?</legend>
      <label v-for="score in 5" :key="score">
        <input v-model="rating" type="radio" name="rating" :value="score" required /> {{ score }}
      </label>
    </fieldset>
    <label for="comment">{{ rating && rating <= 2 ? "What went wrong?" : "What would make it better?" }}</label>
    <textarea id="comment" name="comment" rows="4" maxlength="4000" />
    <label for="feedback-email">Email, if you'd like a reply</label>
    <input id="feedback-email" name="email" type="email" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Send feedback" }}</button>
  </form>
</template>
```

- Ersetze `frm_xxxxxxxxxxxxxxxx` durch die ID deines Formulars aus dem Dashboard.
- `rating` ist eine Radio-Gruppe mit fünf Werten. Ikiform speichert sie als Zahl, also brauchen Durchschnitte kein Parsing.
- `email` ist optional. Leute schreiben ehrlicher, wenn sie sich nicht zu erkennen geben müssen, und wer eine Antwort will, hinterlässt sie.
- Der versteckte `_gotcha`-Input ist der Honeypot. Menschen sehen ihn nie; Bots, die jeden Input ausfüllen, markieren sich selbst als Spam.
- `page` hält fest, um welche Seite es in der Antwort geht. Der nächste Abschnitt erklärt, warum du sie selbst sendest.

> Die serverseitigen Versionen senden mit einem Secret Key, daher überspringt Ikiform **Allowed domains** und seine Menschlichkeitsprüfung. Jeder Post kommt dann von der IP deines Servers, und das Standard-Limit von 5 pro Minute und Besucher zählt alle deine Besucher zusammen. Wähle für ein Formular, das du über einen Server sendest, das [Rate-Limit](https://ikiform.com/de/docs/rate-limits.md) Relaxed oder Custom, und halte `IKIFORM_SECRET_KEY` in reinen Server-Umgebungsvariablen.

Die Framework-Anleitungen in der Doku behandeln das Setup ausführlicher: [Next.js](https://ikiform.com/de/docs/guides/nextjs.md), [Astro](https://ikiform.com/de/docs/guides/astro.md), [SvelteKit](https://ikiform.com/de/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/de/docs/guides/nuxt.md), [React](https://ikiform.com/de/docs/guides/react.md), [Vue](https://ikiform.com/de/docs/guides/vue.md) und [reines HTML](https://ikiform.com/de/docs/html.md). Die [Feedback-Formular-Vorlage](https://ikiform.com/forms/feedback) erzeugt dieselben Felder als HTML, React und Server Action; die Vorlagen für [Bug-Reports](https://ikiform.com/forms/bug-report) und [Umfragen](https://ikiform.com/forms/survey) decken die längeren Formulare ab.

## Felder definieren

Füge eine Definition im Tab **Fields** des Formulars ein, und Ikiform setzt sie bei jedem Post durch, von jeder Seite. Probier es unten aus: Die Vorschau führt den Validator von Ikiform in deinem Browser aus, sodass eine Bewertung von `0` oder eine fehlerhafte E-Mail den `422` zeigt, den du zurückbekommen würdest.

Formular: Page feedback

- **Wie hilfreich war diese Seite?** (`rating`, rating, Pflichtfeld)
- **Was würde sie besser machen?** (`comment`, textarea)
- **E-Mail, falls du eine Antwort möchtest** (`email`, email)
- **Seite** (`page`, url)

Ein `rating` nimmt eine ganze Zahl von 1 bis zu seinem `max`, hier 5. Es geht bis 10, aber fünf Optionen sind schneller beantwortet.

| Feld | Typ | Warum |
| --- | --- | --- |
| `rating` | `rating`, max. 5 | Ein Klick. Als Zahl gespeichert, sodass du pro Seite einen Durchschnitt bilden kannst |
| `comment` | `textarea`, bis zu 4.000 Zeichen | Der Grund hinter der Bewertung |
| `email` | `email`, optional | Antworte Leuten, die eine Frage stellen. Wird zur Reply-To-Adresse der Benachrichtigung |
| `page` | `url`, optional | Um welche Seite es in der Antwort geht |

Sind Felder definiert, wird jeder andere Name im Post als `Unknown field` abgelehnt. Wenn du später mehr Kontext sendest, etwa eine App-Version oder einen Tarifnamen, füge zuerst ein Feld dafür hinzu. Feld-IDs sind in Kleinbuchstaben und snake_case.

## Wissen, um welche Seite es geht

Eine Bewertung ohne Seite ist schwer umzusetzen. Ikiform speichert bei jeder Einsendung die Metadaten des Browsers, aber einer dieser Werte ist weniger nützlich, als er aussieht:

| Wert | Woher er kommt | Was du bekommst |
| --- | --- | --- |
| `meta.user_agent` | Von Ikiform erfasst | Browser und Betriebssystem, bei einem Post aus dem Browser |
| `meta.origin` | Von Ikiform erfasst | Der Origin deiner Site, etwa `https://example.com` |
| `meta.referrer` | Von Ikiform erfasst | Meist nur der Origin. Browser nutzen standardmäßig die Policy `strict-origin-when-cross-origin`, die den Pfad beim Senden an eine andere Site entfernt |
| `meta.country` | Von Ikiform erfasst | Ein Ländercode aus zwei Buchstaben |
| `page` | Von deinem Formular gesendet | Der vollständige Pfad, damit du weißt, um welche Seite es in der Antwort geht |

Deshalb sendet jedes Beispiel oben `page` selbst. Die HTML- und SPA-Versionen lesen `location`; die Server-Versionen lesen die Adresse, die ihr Framework schon kennt. Alle entfernen den Query-String, der Suchbegriffe, Tracking-IDs oder Einmal-Tokens enthalten kann, die du nicht speichern willst.

> Die Server-Versionen senden von deinem Server, also ist `meta.user_agent` die Runtime deines Servers, nicht der Browser des Besuchers. Für Seiten-Feedback spielt das selten eine Rolle. Für Bug-Reports schon, und die [Anleitung für Bug-Reports](https://ikiform.com/de/guides/feedback-form/bug-report.md) leitet den User-Agent des Browsers als Feld weiter.

## Wo du fragst

Wo das Formular sitzt, entscheidet, was du hörst. Ein Formular im Footer jeder Seite bringt ein Rinnsal starker Meinungen. Eine Frage am Ende einer Aufgabe bringt stetige, konkrete Antworten.

| Platzierung | Gut für | Achte auf |
| --- | --- | --- |
| Ende einer Doku-Seite oder eines Artikels | Seiten finden, die die Frage nicht beantworten | Wenig Antworten pro Seite. Werte pro Abschnitt aus, nicht pro Seite |
| Nach einer Aufgabe in deiner App, etwa einem Export oder einem ersten Projekt | Wie sich ein bestimmter Ablauf anfühlt, solange er frisch ist | Frag einmal pro Person, nicht jedes Mal, wenn sie die Aufgabe abschließt |
| Ein schwebender Button auf jeder Ansicht | Alles, jederzeit. Siehe das [React-Widget](https://ikiform.com/de/guides/feedback-form/react-feedback-widget.md) | Er darf deine eigenen Buttons, Cookie-Banner oder Chat-Bubbles nicht verdecken |
| Ein Link in einer E-Mail | Werte, die du über die Zeit verfolgen willst, wie [NPS](https://ikiform.com/de/guides/feedback-form/nps-survey.md) | Der E-Mail-Client ist nicht deine Seite, also braucht das Formular eine Landingpage auf deiner Site |

Halte das Formular klein, egal wo es steht. Frag zuerst nach der Bewertung und zeig das Kommentarfeld gleich mit, nicht in einem zweiten Schritt. Ein zweiter Schritt verliert die meisten Leute, und eine Bewertung ohne Grund ist trotzdem etwas wert.

## Niedrige Bewertungen vor die richtigen Leute bringen

Diagramm: Was mit einer Antwort abhängig von ihrer Bewertung passiert

1. Jede Antwort wird gespeichert, unabhängig von ihrer Bewertung und der Seite, von der sie kam.
2. Ikiform sendet einen signierten submission.created-Webhook an dein Relay.
3. Das Relay liest die Bewertung. Werte von 3 bis 5 brauchen vorerst nichts weiter.
4. Eine Bewertung von 1 oder 2 geht mit Seite und Kommentar in deinen Team-Chat.
5. Hat der Besucher eine E-Mail hinterlassen, antwortet jemand noch am selben Tag.
6. Einmal pro Woche zeigt der Export Durchschnitte pro Seite, und dort werden Trends sichtbar.

Alarmiere nur bei schlechten Bewertungen. Ein Channel, der bei jeder 5 von 5 pingt, wird innerhalb einer Woche stummgeschaltet.

Benachrichtigungs-E-Mails gehen an bis zu 10 bestätigte Empfänger pro Formular und kommen bei jeder Antwort. Bei ein paar Antworten am Tag ist das in Ordnung. Darüber hinaus schick die niedrigen Bewertungen mit einem Relay wie dieser Next.js-Route in den Chat; derselbe Code läuft in einem Cloudflare Worker:

`app/api/feedback-alerts/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    if (event.type !== "submission.created") return new Response(null, { status: 204 });
    const { rating, comment, page, email } = event.data.fields;
    if (Number(rating) > 2) return new Response(null, { status: 204 });

    const lines = [
      `*${rating}/5* on ${page ?? event.data.meta.origin ?? "an unknown page"}`,
      comment ? `> ${String(comment).slice(0, 500)}` : "_No comment._",
      email ? `Reply to ${email}` : "Anonymous",
    ];
    const response = await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ text: lines.join("\n") }),
    });
    if (!response.ok) return new Response(`Slack answered ${response.status}`, { status: 502 });
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

- `verifyWebhook` prüft Signatur und Zeitstempel, damit niemand sonst über diese URL in deinen Channel posten kann.
- Bewertungen über 2 antworten mit `204` und hören auf. Ikiform wertet jeden 2xx als zugestellt.
- Ein fehlgeschlagener Slack-Aufruf antwortet mit `502`, und Ikiform versucht es mit derselben `webhook-id` erneut, sodass ein kurzer Ausfall keinen Alert verliert.
- Feldwerte sind das, was der Besucher eingetippt hat. Das Relay kürzt den Kommentar auf 500 Zeichen, bevor er im Chat landet.

Füge den Endpunkt unter **Webhooks** hinzu und kopiere sein `whsec_`-Secret in `IKIFORM_WEBHOOK_SECRET`. Ein Workspace kann 3 Endpunkte haben. Für No-Code-Wege siehe [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Discord](https://ikiform.com/blog/form-submissions-to-discord.md) und [Google Sheets](https://ikiform.com/blog/form-submissions-to-google-sheets.md). Für Feedback, das zu einem Ticket werden soll, hat [Linear und GitHub Issues](https://ikiform.com/blog/form-submissions-to-linear-and-github-issues.md) ein Relay, das pro Einsendung eins anlegt.

> Es gibt keine Ein-Klick-Integrationen. Jedes Tool wird über einen signierten [Webhook](https://ikiform.com/de/docs/webhooks.md) erreicht, direkt oder über ein Automatisierungstool wie [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) oder [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).

## Bots fernhalten

Ein Feedback-Formular auf jeder Seite ist ein öffentliches Formular auf jeder Seite. Bots finden es, und das meiste, was sie senden, sind Links. Die Standardeinstellungen von Ikiform erledigen den Großteil:

| Abwehr | Was sie stoppt | Einrichtung |
| --- | --- | --- |
| Honeypot | Bots, die jedes Eingabefeld ausfüllen | Der versteckte `_gotcha`-Input in den Beispielen |
| Spam-Bewertung | Drei oder mehr Links (+2), bekannte Spam-Phrasen (+2), Wegwerf-E-Mails (+2), identische Wiederholungen (+3). Ab 5 wird es als Spam gespeichert | Keine |
| Rate-Limits | Skripte, die in einer Schleife senden | Standardmäßig aktiv: Standard, 5 pro Minute pro Besucher, und 300 pro Minute für das ganze Formular |
| Erlaubte Domains | Posts von anderen Sites | Füge deine Domain in den Einstellungen des Formulars hinzu |
| Menschlichkeitsprüfung | Bots, die einen echten Browser ausführen | Setze **Spam check** auf **Ikiform**. Posts per `fetch` aus dem Browser brauchen dann ein Token von `challenge()` |

Spam bekommt dieselbe Erfolgsantwort wie ein echter Post, löst weder E-Mail noch Webhook aus und zählt nicht zur Nutzung. Ein echter Besucher, der drei Links in einen Kommentar einfügt, kann zusammen mit einer Wiederholung die Link-Regel auslösen, also schau ab und zu in den Filter **Spam**. Markierst du eine Antwort als kein Spam, wird ihr Webhook dann gesendet. Die [Spam-Doku](https://ikiform.com/de/docs/spam.md) erklärt jede Prüfung.

## Die Antworten lesen

Lies in den ersten Wochen jeden Kommentar. Danach ist die nützliche Ansicht die Durchschnittsbewertung pro Seite, die niedrigste zuerst, mit den Kommentaren daneben.

### Exportieren

Öffne **Submissions**, wähle das Feedback-Formular, einen Zeitraum und **Export CSV**. Die Spalten sind `id`, `created_at`, `form` und `status`, dann eine pro Feld, mit den Feldern oben ist `rating` also Spalte E und `page` Spalte H. Ein Export umfasst bis zu 100.000 Zeilen; die [Export-Anleitung](https://ikiform.com/de/docs/guides/export.md) behandelt die API für mehr.

Importiere die Datei in Google Sheets und füge das hier in ein leeres Tabellenblatt ein. Es listet jede Seite mit ihrer Durchschnittsbewertung und der Zahl der Antworten auf, die schlechteste zuerst:

`Google Sheets`:

```text
=QUERY(A2:H, "select H, avg(E), count(E) where D = 'received' and E is not null group by H order by avg(E) asc label avg(E) 'average', count(E) 'answers'", 0)
```

1. Ignoriere Seiten mit nur einer oder zwei Antworten. Ein verärgerter Besucher ist kein Trend.
2. Lies die Kommentare zu den drei schlechtesten Seiten. Die Lösung steckt meist in den ersten paar.
3. Wenn du eine Seite änderst, vergleiche ihren Durchschnitt der Wochen davor und danach, nicht den Gesamtwert.

> Feedback ist für Ikiform auch ohne E-Mail nicht anonym: Jede Antwort speichert User-Agent, Origin, Referrer und Land sowie einen Hash der IP, der nie zurückgegeben wird. Nenne in deiner Datenschutzerklärung, was du erhebst.

## Wähle deinen Fall

Das Formular oben deckt schnelle Reaktionen auf jeder Seite ab. Diese Seiten behandeln Feedback mit einer anderen Aufgabe: einen Bug, der reproduziert werden soll, einen Wunsch, der abgewogen werden soll, einen Wert, der verfolgt werden soll, oder einen Kunden, der geht.

- [Formular für Bug-Reports](https://ikiform.com/de/guides/feedback-form/bug-report.md): Ein Bug-Report-Formular, das nach Schritten und Schweregrad fragt und URL, Browser, Viewport und letzte Fehler von der Seite selbst ergänzen lässt.
- [Formular für Feature-Wünsche](https://ikiform.com/de/guides/feedback-form/feature-request.md): Ein Formular für Feature-Wünsche, das nach dem Problem statt nach der Lösung fragt, Wünsche nach Thema speichert, Personen statt Zeilen zählt und sie beim Release anschreibt.
- [Feedback-Widget für React](https://ikiform.com/de/guides/feedback-form/react-feedback-widget.md): Ein schwebender Feedback-Button für eine React-App, der ein kleines Panel öffnet, Ansicht, App-Version und Konto mit jeder Antwort sendet und nie einen Entwurf verliert.
- [Feedback von Beta-Testern](https://ikiform.com/de/guides/feedback-form/beta-tester-feedback.md): Ein Beta-Feedback-Formular mit einem persönlichen Link pro Tester und Build, zwei Bewertungsskalen und einer Wochenübersicht, die zeigt, ob jeder Build besser wurde.
- [NPS-Umfrage](https://ikiform.com/de/guides/feedback-form/nps-survey.md): Führe eine NPS-Umfrage mit einem Wert von 0 bis 10 in der E-Mail durch, mit einer Folgefrage auf deiner eigenen Seite, die sich mit dem Wert ändert, und einem Skript, das den NPS berechnet.
- [Kündigungsumfrage](https://ikiform.com/de/guides/feedback-form/cancellation-survey.md): Frag im Kündigungsablauf deines SaaS, warum Kunden kündigen, halte Überspringen einen Klick entfernt, speichere den Grund in Stripe und Ikiform und verzögere die Kündigung nie.
- [Feedback-Formular mit Next.js](https://ikiform.com/de/guides/feedback-form/nextjs.md): Ein Feedback-Formular in einer Next.js-App: eine Server Action, die die Session liest, die Seite aus dem Request nimmt und eine Bewertung mit dem Nutzer weiterleitet.
- [Feedback-Formular mit Astro](https://ikiform.com/de/guides/feedback-form/astro.md): Ein Feedback-Formular in einer Astro-App: eine Action, die den Nutzer aus den Middleware-Locals, die Seite aus dem Request und die Ansicht aus dem Routenmuster nimmt.
- [Feedback-Formular mit TanStack Start](https://ikiform.com/de/guides/feedback-form/tanstack-start.md): Ein Feedback-Formular in einer TanStack-Start-App: eine POST-Server-Funktion mit Session-Middleware, die Routen-ID als Kontext und die Seite aus den Request-Headern.
- [Feedback-Formular mit React Router](https://ikiform.com/de/guides/feedback-form/react-router.md): Ein Feedback-Formular mit React Router 8: Die Action einer Resource Route liest die Cookie-Session, ein Fetcher sendet aus dem Layout, und ein Post ohne JS leitet zurück.
- [Feedback-Formular mit SvelteKit](https://ikiform.com/de/guides/feedback-form/sveltekit.md): Ein Feedback-Formular in einer SvelteKit-App: Ein Layout-Formular sendet an eine Form Action, die auf dem Server den Nutzer aus locals und die Seite aus dem Request ergänzt.
- [Feedback-Formular mit Nuxt](https://ikiform.com/de/guides/feedback-form/nuxt.md): Ein Feedback-Formular in einer Nuxt-App: eine Server-Route, die die Nutzer-Session liest, die Seite aus dem Referer nimmt und auch ohne geladenes JavaScript funktioniert.

## Prüfen, ob es funktioniert

Geh das durch, bevor du das Formular auf jede Seite setzt. Ikiform hat keinen kostenlosen Tarif. Testen ist kostenlos: Ein Workspace ohne Tarif nimmt 50 Testeinsendungen pro Monat von `localhost` an. Für den Livebetrieb wähle Pro ab $6/Monat für 500 Einsendungen oder zahle einmalig: 10.000 Einsendungen für $49 oder 50.000 Einsendungen für $149, mit Guthaben, das nie verfällt. Siehe [Preise](https://ikiform.com/de/pricing).

1. Füge unter **Allowed domains** des Formulars die Domain deiner Site hinzu, plus `localhost`, während du entwickelst. Lass das Feld leer, um Posts von überall anzunehmen.
2. Wähle eine Bewertung, schreib einen Kommentar und sende ab. Die Dankeszeile ersetzt das Formular, oder der Browser landet bei der reinen HTML-Version auf deiner Weiterleitungsseite.
3. Öffne **Submissions** im Dashboard. Die Antwort zeigt `rating` als Zahl und `page` mit dem vollständigen Pfad.
4. Sende eine Bewertung von `6` mit `curl` oder aus der Browser-Konsole. Ikiform antwortet mit `422` und nennt `rating`.
5. 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.
6. Sende eine Bewertung von 1. Dein Relay postet sie in den Team-Chat, und die Zustellung unter **Webhooks** zeigt `succeeded`.
7. 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.
8. Nichts angekommen? Stell den Statusfilter im Dashboard auf **Spam**. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.

## Fehlerbehebung

| Problem | Was zu tun ist |
| --- | --- |
| `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. |
| `422` mit `Rating must be at most 5` | Das Formular sendet eine höhere Bewertung als das `max` des Felds. Ändere die Radio-Werte oder erhöhe `max` in der Definition, bis maximal 10. |
| `422` mit einem Unknown field | Das Formular sendet einen Namen, der nicht in der Definition steht, oft ein neuer Hidden Input. Füge ein Feld mit dieser ID hinzu oder entferne den Input. |
| `page` ist leer | Das Skript, das ihn füllt, lief nicht oder lief, bevor das Formular existierte. Setze es hinter das Formular oder setze `page` beim Absenden, wie es die SPA-Beispiele tun. |
| `meta.referrer` zeigt nur deine Domain | Erwartet. Browser senden bei Cross-Site-Posts standardmäßig nur den Origin. Nutze stattdessen das Feld `page`. |
| Die Einsendung ist im Dashboard, aber keine E-Mail und kein Webhook kam an | Sie wurde als Spam gespeichert. Spam löst nichts aus. Öffne sie unter dem Filter **Spam**, um die Gründe zu sehen, und markiere sie als kein Spam, um ihre E-Mail und Webhooks zu senden. |
| `429 rate_limited` | Ein Besucher hat mehr gesendet, als das Rate-Limit des Formulars erlaubt. Standard erlaubt 5 pro Minute und Besucher. Warte `Retry-After` ab oder wähle Relaxed oder Custom für Formulare, die von einer gemeinsamen IP aus senden. |
| `402 test_limit_reached` | Der Workspace hat keinen Tarif und hat seine 50 kostenlosen `localhost`-Tests diesen Monat aufgebraucht. Wähle einen Tarif oder nutze **Send test** im Dashboard. |

## Fragen

### Wie füge ich einer Website ohne Backend ein Feedback-Formular hinzu?

Richte die action des Formulars auf den Endpunkt eines Formular-Backends aus. Das Formular kann reines HTML auf einer statischen Site sein; das Backend validiert, speichert und mailt jede Antwort und sendet einen Webhook, wenn du sie woanders haben willst.

### Woher weiß ich, von welcher Seite das Feedback kam?

Sende die Seitenadresse als Feld. Ikiform speichert auch den Referrer, aber Browser senden bei einem Cross-Site-Post meist nur deine Domain, also ist ein Seitenfeld, das aus location befüllt wird, der verlässliche Weg.

### Sollte Feedback anonym sein?

Mach die E-Mail optional. Ohne sie sind Leute offener, und wer eine Antwort will, hinterlässt sie. Ikiform speichert trotzdem bei jeder Antwort Browser, Origin, Referrer und Land.

### Können Besucher einen Screenshot anhängen?

Nicht als Datei. Ikiform lehnt Posts mit Dateien mit 422 files_not_supported ab. Bitte stattdessen um einen Link zu einem Screenshot oder nutze ein Feedback-Tool, das auf Screenshots ausgelegt ist, wenn du bei jedem Bericht einen brauchst.

### Kann ich statt Sternen eine Skala von 0 bis 10 verwenden?

Nicht mit einem rating-Feld, das von 1 bis höchstens 10 reicht. Ein Wert von 0 bis 10 nutzt ein number-Feld mit min 0, max 10 und integer true. Die NPS-Anleitung zeigt das vollständige Setup.

### Wie werde ich bei schlechtem Feedback alarmiert?

Füge einen Webhook zu einem kleinen Relay hinzu, das die Bewertung prüft und nur niedrige Werte an Slack oder Discord postet. Benachrichtigungs-E-Mails decken jede Antwort ab; das Relay deckt die ab, die heute eine Reaktion brauchen.

### Ist Ikiform kostenlos?

Nein, es gibt keinen kostenlosen Tarif. Testen ist kostenlos: Du kannst 50 Testeinsendungen pro Monat von localhost senden, bevor du zahlst. Für den Livebetrieb startet Pro bei $6/Monat für 500 Einsendungen, oder du zahlst einmalig 10.000 Einsendungen für $49 oder 50.000 Einsendungen für $149, mit Guthaben, das nie verfällt.

## Weitere Anleitungen

- [Formular für Bug-Reports](https://ikiform.com/de/guides/feedback-form/bug-report.md): Ein Bug-Report-Formular, das nach Schritten und Schweregrad fragt und URL, Browser, Viewport und letzte Fehler von der Seite selbst ergänzen lässt.
- [Formular für Feature-Wünsche](https://ikiform.com/de/guides/feedback-form/feature-request.md): Ein Formular für Feature-Wünsche, das nach dem Problem statt nach der Lösung fragt, Wünsche nach Thema speichert, Personen statt Zeilen zählt und sie beim Release anschreibt.
- [Feedback-Widget für React](https://ikiform.com/de/guides/feedback-form/react-feedback-widget.md): Ein schwebender Feedback-Button für eine React-App, der ein kleines Panel öffnet, Ansicht, App-Version und Konto mit jeder Antwort sendet und nie einen Entwurf verliert.
- [Feedback von Beta-Testern](https://ikiform.com/de/guides/feedback-form/beta-tester-feedback.md): Ein Beta-Feedback-Formular mit einem persönlichen Link pro Tester und Build, zwei Bewertungsskalen und einer Wochenübersicht, die zeigt, ob jeder Build besser wurde.
- [NPS-Umfrage](https://ikiform.com/de/guides/feedback-form/nps-survey.md): Führe eine NPS-Umfrage mit einem Wert von 0 bis 10 in der E-Mail durch, mit einer Folgefrage auf deiner eigenen Seite, die sich mit dem Wert ändert, und einem Skript, das den NPS berechnet.
- [Kündigungsumfrage](https://ikiform.com/de/guides/feedback-form/cancellation-survey.md): Frag im Kündigungsablauf deines SaaS, warum Kunden kündigen, halte Überspringen einen Klick entfernt, speichere den Grund in Stripe und Ikiform und verzögere die Kündigung nie.
- [Feedback-Formular mit Next.js](https://ikiform.com/de/guides/feedback-form/nextjs.md): Ein Feedback-Formular in einer Next.js-App: eine Server Action, die die Session liest, die Seite aus dem Request nimmt und eine Bewertung mit dem Nutzer weiterleitet.
- [Feedback-Formular mit Astro](https://ikiform.com/de/guides/feedback-form/astro.md): Ein Feedback-Formular in einer Astro-App: eine Action, die den Nutzer aus den Middleware-Locals, die Seite aus dem Request und die Ansicht aus dem Routenmuster nimmt.
- [Feedback-Formular mit TanStack Start](https://ikiform.com/de/guides/feedback-form/tanstack-start.md): Ein Feedback-Formular in einer TanStack-Start-App: eine POST-Server-Funktion mit Session-Middleware, die Routen-ID als Kontext und die Seite aus den Request-Headern.
- [Feedback-Formular mit React Router](https://ikiform.com/de/guides/feedback-form/react-router.md): Ein Feedback-Formular mit React Router 8: Die Action einer Resource Route liest die Cookie-Session, ein Fetcher sendet aus dem Layout, und ein Post ohne JS leitet zurück.
- [Feedback-Formular mit SvelteKit](https://ikiform.com/de/guides/feedback-form/sveltekit.md): Ein Feedback-Formular in einer SvelteKit-App: Ein Layout-Formular sendet an eine Form Action, die auf dem Server den Nutzer aus locals und die Seite aus dem Request ergänzt.
- [Feedback-Formular mit Nuxt](https://ikiform.com/de/guides/feedback-form/nuxt.md): Ein Feedback-Formular in einer Nuxt-App: eine Server-Route, die die Nutzer-Session liest, die Seite aus dem Referer nimmt und auch ohne geladenes JavaScript funktioniert.
- [Formular zur Lead-Erfassung](https://ikiform.com/de/guides/lead-capture.md): Ein Lead-Formular auf deiner eigenen Site, das dich per E-Mail mit Reply-To benachrichtigt, den Team-Chat anpingt und dein CRM füttert, damit du antwortest, solange der Lead noch da ist.
- [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.
- [Absenden von HTML-Formularen](https://ikiform.com/de/guides/html-form-submission.md): Was ein Browser beim Absenden eines Formulars tut: welche Felder er sendet, wie er sie kodiert, was der Server antworten sollte und wie Ikiform antwortet.
