Formular zur Lead-Erfassung6 Min. Lesezeit
Formular für Beratungsanfragen für ein Dienstleistungsunternehmen bauen
Ein Formular für Beratungsanfragen für Steuerberater, Anwälte, Coaches und Praxen, das fragt, wie und wann angerufen werden soll, und heikle Details für das Gespräch lässt.
Frag nach Name, E-Mail, wie und ungefähr wann die Person erreichbar ist und nach einem groben Thema, und heb jedes Detail ihrer Situation für das Gespräch auf. Erfasse ihre Zeitzone aus dem Browser, damit ein gewünschter Vormittag ihren Vormittag meint. Setze Regeln, die das Formularschema nicht ausdrücken kann, etwa eine Telefonnummer nur bei gewähltem Telefonanruf, in deinem eigenen Code durch.
Was an einer Beratungsanfrage anders ist
Eine Beratungsanfrage kommt von einem Menschen mit einem Problem: ein Brief vom Finanzamt, ein Mietstreit, ein Unternehmen, das gegründet werden soll. Er will es erklären, und das Formular lädt ihn dazu ein. Genau das ist die Falle. Was er eintippt, wird mit der Einsendung gespeichert, in Benachrichtigungs-E-Mails angezeigt und per Webhook dorthin gesendet, wohin du ihn richtest. Kontonummern und Falldetails gehören an keinen dieser Orte.
Die Aufgabe des Formulars ist also enger, als sie aussieht: wer die Person ist, wie und wann man sie erreicht und ungefähr worum es geht. Alles andere wartet auf das Gespräch, wo du bestimmst, was aufgeschrieben und wo es aufbewahrt wird.
- Das Formular fragt, wer die Person ist: Name und E-Mail.
- Es fragt, wie man sie erreicht: E-Mail, Telefon oder Video.
- Es fragt, wann: ein bevorzugter Tag, ein Zeitfenster und ihre Zeitzone.
- Es fragt, worum es geht, als ein grobes Thema.
- Das Gespräch behandelt ihre Situation in ihren eigenen Worten.
- Dokumente laufen über einen Kanal, den du wählst, nicht über das Formular.
- Kontonummern, Ausweisnummern und Unterlagen werden nie ins Formular getippt.
- Das Gespräch endet mit deiner Antwort zu Eignung und Honorar.
Die Felder
Vorschau
Führt den Validator von Ikiform in deinem Browser aus. Es wird nichts gesendet.
| Feld | Warum es so aufgebaut ist |
|---|---|
contact_method | Ein radio, damit der erste Kontakt so stattfindet, wie gewünscht |
phone | Im Schema optional, von deinem Code verlangt, wenn ein Telefonanruf gewählt wird |
preferred_date | Ein date-Feld speichert YYYY-MM-DD. Sein min im Schema ist fest, also setzt die Seite im Browser morgen als min |
timezone | Aus dem Browser befüllt mit Intl.DateTimeFormat().resolvedOptions().timeZone, etwa Europe/Berlin |
note | Auf 500 Zeichen begrenzt. Benachrichtigungs-E-Mails zeigen bis zu 500 Zeichen pro Wert, also passt die ganze Notiz hinein |
Bedingte Regeln in deinem eigenen Code
Ikiform validiert jedes Feld für sich. Regeln, die von einem anderen Feld abhängen, etwa Telefon nur bei Telefonanrufen, laufen in deinem Code. Server-Versionen können sie wirklich durchsetzen; Browser-Versionen führen den Besucher und senden nichts, was er nicht senden musste.
Das Skript setzt required auf den Telefon-Input, wenn Phone call gewählt wird, setzt morgen als frühesten Tag im eigenen Kalender des Besuchers und füllt die versteckte Zeitzone.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" id="consult">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<fieldset>
<legend>How should we reach you?</legend>
<label><input type="radio" name="contact_method" value="email" checked /> Email</label>
<label><input type="radio" name="contact_method" value="phone" /> Phone call</label>
<label><input type="radio" name="contact_method" value="video" /> Video call</label>
</fieldset>
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" />
<label for="topic">What is it about?</label>
<select id="topic" name="topic" required>
<option value="new_business">Starting a business</option>
<option value="tax">Tax and bookkeeping</option>
<option value="other">Something else</option>
</select>
<label for="preferred_date">Preferred day</label>
<input id="preferred_date" name="preferred_date" type="date" />
<fieldset>
<legend>Best time</legend>
<label><input type="radio" name="time_window" value="morning" /> Morning</label>
<label><input type="radio" name="time_window" value="afternoon" /> Afternoon</label>
<label><input type="radio" name="time_window" value="evening" /> Evening</label>
</fieldset>
<input type="hidden" name="timezone" />
<label for="note">Anything we should know first?</label>
<textarea id="note" name="note" rows="3" maxlength="500"></textarea>
<p>A sentence or two. Please don't include account numbers or personal records.</p>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Request a consultation</button>
</form>
<script>
const form = document.getElementById("consult");
const phone = form.elements.phone;
const tomorrow = new Date(Date.now() + 86400000);
const local = new Date(tomorrow.getTime() - tomorrow.getTimezoneOffset() * 60000);
form.elements.timezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
form.elements.preferred_date.min = local.toISOString().slice(0, 10);
form.addEventListener("change", () => {
phone.required = form.elements.contact_method.value === "phone";
});
</script>Die Server Action lehnt einen Telefonanruf ohne Nummer vor dem Senden ab, löscht eine Nummer, die eingetippt und dann abgewählt wurde, und leitet auf eine Dankeseite weiter. Rendere das Formular mit useActionState und zeig state.error an.
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
const KEYS = ["name", "email", "contact_method", "phone", "topic", "preferred_date", "time_window", "timezone", "note", "_gotcha"];
export async function requestConsultation(_previous: Record<string, string>, formData: FormData) {
const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").trim()]));
if (values.contact_method === "phone" && !values.phone) {
return { ...values, error: "Add a phone number, or choose email or video." };
}
if (values.contact_method !== "phone") values.phone = "";
const origin = (await 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(values),
});
const result = await response.json().catch(() => null);
if (!result?.ok) {
const fields: Record<string, string[]> = result?.error?.fields ?? {};
const [field, messages] = Object.entries(fields)[0] ?? [];
return { ...values, error: field ? `${field}: ${messages?.[0]}` : (result?.error?.message ?? "Couldn't send. Try again.") };
}
redirect("/consultation/thanks");
}Den Telefon-Input gibt es nur, wenn ein Telefonanruf gewählt ist, und leere Werte werden vor dem Post entfernt, also kommen optionale Felder als fehlend statt leer an.
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const request = reactive({
name: "",
email: "",
contact_method: "email",
phone: "",
topic: "new_business",
preferred_date: "",
time_window: "",
note: "",
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
const status = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");
const wantsCall = computed(() => request.contact_method === "phone");
async function send() {
status.value = "sending";
error.value = "";
const body = Object.fromEntries(Object.entries(request).filter(([, value]) => value !== ""));
if (!wantsCall.value) delete body.phone;
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const result = await response.json();
if (result.ok) {
status.value = "sent";
return;
}
const first = Object.entries(result.error.fields ?? {})[0] as [string, string[]] | undefined;
error.value = first ? `${first[0]}: ${first[1][0]}` : result.error.message;
} catch {
error.value = "Couldn't reach us. Try again, or call the office.";
}
status.value = "idle";
}
</script>
<template>
<p v-if="status === 'sent'" role="status">Thanks, {{ request.name }}. We'll contact you by {{ request.contact_method }} within one working day.</p>
<form v-else @submit.prevent="send">
<label for="name">Name</label>
<input id="name" v-model="request.name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" v-model="request.email" type="email" autocomplete="email" required />
<fieldset>
<legend>How should we reach you?</legend>
<label><input v-model="request.contact_method" type="radio" value="email" /> Email</label>
<label><input v-model="request.contact_method" type="radio" value="phone" /> Phone call</label>
<label><input v-model="request.contact_method" type="radio" value="video" /> Video call</label>
</fieldset>
<template v-if="wantsCall">
<label for="phone">Phone</label>
<input id="phone" v-model="request.phone" type="tel" autocomplete="tel" required />
</template>
<label for="topic">What is it about?</label>
<select id="topic" v-model="request.topic">
<option value="new_business">Starting a business</option>
<option value="tax">Tax and bookkeeping</option>
<option value="other">Something else</option>
</select>
<label for="preferred_date">Preferred day</label>
<input id="preferred_date" v-model="request.preferred_date" type="date" />
<label for="note">Anything we should know first?</label>
<textarea id="note" v-model="request.note" rows="3" maxlength="500" />
<p v-if="error" role="alert">{{ error }}</p>
<button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Request a consultation" }}</button>
</form>
</template>Die serverseitigen Versionen senden mit einem Secret Key, also ü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 das Rate-Limit Relaxed oder Custom für ein Formular, das du über einen Server sendest, und halte IKIFORM_SECRET_KEY in Umgebungsvariablen, die nur der Server sieht. Die Serverversionen leiten den Origin-Header des Besuchers weiter, weil ein serverseitiger Fetch keinen sendet und Ikiform daran kostenlose localhost-Tests erkennt.
Heikle Details aus dem Formular heraushalten
- Sag es im Hilfetext, wie das Formular oben. Leute folgen einer klaren Anweisung öfter, als man denkt.
- Füge keine Felder für Ausweisnummern, Kontonummern oder medizinische Details hinzu. Hat dein Berufsstand Regeln zu Mandantendaten, prüfe sie, bevor du mehr als Kontaktdaten erhebst, und erhebe den Rest über einen Kanal, der sie erfüllt.
- Auch Feldnamen zählen. Ikiform hält die Benachrichtigungs-E-Mail zur Prüfung zurück, wenn ein Feldname wie eine Zugangsdaten-Angabe aussieht, darunter Namen mit
password,ssn,cvvoderseed. Ein Feld namensseed_roundim Formular einer Startup-Beratung löst das aus. Nenne esfunding_stage. - Richte Webhooks nur dorthin, wo das ganze Team Mandantennamen lesen darf, etwa einen privaten Channel, keinen firmenweiten.
Die erste Antwort
Die Benachrichtigungs-E-Mail kommt mit Reply auf den Besucher gerichtet an, sodass die erste Antwort direkt aus deinem Posteingang kommen kann. Gib ihr eine Aufgabe: einen Termin bestätigen. Zu zwei konkreten Terminen innerhalb des Zeitfensters sagt man leichter Ja als zu der Frage, wann es passt.
- Rechne das Zeitfenster in deine Zeit um, bevor du schreibst. Ein
morninginAustralia/Sydneyist in London der Vorabend. - Biete zwei Termine in der Zeitzone der Person an und nenne die Zone in der E-Mail, damit niemand eine Stunde zu früh erscheint.
- Sag, wie du anrufst: unter der angegebenen Nummer oder per Video-Link, den du mit der Einladung schickst.
- Liegt das Thema außerhalb dessen, was du machst, sag das in der ersten Antwort und empfiehl jemanden, der es macht. Auch diese Antwort lohnt sich.
Prüfen, ob es funktioniert
- Wähle Phone call, lass das Telefon leer und sende ab. Der Browser oder dein Server stoppt es mit einer Meldung.
- Wähle Email, sende ab und öffne die Einsendung. Sie hat keinen Wert für
phoneund enthält deine Zeitzone, etwaAmerica/Chicago. - Versuch, heute als bevorzugten Tag zu wählen. Die Datumsauswahl bietet ihn nicht an.
- Prüf die Benachrichtigungs-E-Mail. Die Notiz erscheint vollständig, und Reply schreibt an den Besucher.
- Liste deine Feld-IDs auf und prüfe, dass keine
password,ssn,cvvoderseedenthält.
Stolperfallen
- Datum ohne Zeitzone.
preferred_dateist ein Kalendertag, kein Zeitpunkt. Lies es zusammen mittimezoneundtime_window, bevor du anrufst. - Radios ohne Auswahl. Eine Radio-Gruppe ohne Auswahl sendet nichts. Für
time_window, das optional ist, ist das in Ordnung, und deshalb startetcontact_methodmit vorausgewähltem Email. - Keine Buchungsbestätigung. Ikiform schreibt der Person, die das Formular ausfüllt, keine E-Mail. Deine erste Antwort bestätigt den Termin. Sollen Leute selbst einen Termin wählen, übergib an einen Terminplaner wie im Formular für Demo-Anfragen.
- Telefonformat. Das Telefonfeld akzeptiert 6 bis 15 Ziffern, wie eingetippt. Aktiviere
e164, wenn du aus einem System wählst, das+14155550123braucht; Nummern ohne Ländervorwahl werden dann abgelehnt.
Fragen
Name, E-Mail, wie die Person kontaktiert werden will, einen bevorzugten Tag und eine Uhrzeit und ein grobes Thema. Die ganze Situation, Dokumente und alle Nummern bleiben für die Beratung selbst.
Nicht in der Felddefinition, die jedes Feld für sich validiert. Setze required im Browser auf den Input, wenn Telefon gewählt ist, und prüfe es in deinem Server-Code vor dem Senden.
Befülle im Browser ein verstecktes timezone-Feld aus Intl.DateTimeFormat().resolvedOptions().timeZone. Es speichert einen IANA-Namen wie Europe/London neben dem gewünschten Tag und Zeitfenster.
Nein. Ikiform schickt E-Mails an dich und dein Team, nicht an den Absender, es gibt also keinen Autoresponder und keine Bestätigungs-E-Mail. Sende einen Webhook an dein E-Mail-Tool und verschicke die Bestätigung von dort.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.