So baust du einen Kontaktformular-Block
Baue ein headless Kontaktformular: Erstelle es im Form Builder von Cmssy und rendere es dann mit dem SDK in deiner eigenen Next.js-Site.
Was wir bauen
Ein Kontaktformular für eine headless Cmssy-Site. Du verwaltest das Formular im Form Builder von Cmssy (Admin auf cmssy.io) und renderst es in deiner eigenen Next.js-App (App Router), die du selbst deployst (Vercel usw.). Der visuelle Editor von Cmssy rahmt deine deployte Site ein, damit Redakteure Inhalte live anpassen können. Dieses Setup umfasst:
- Felder für Name, E-Mail und Nachricht mit Validierung - einmal im Form Builder definiert
- Serverseitige Validierung, E-Mail-Benachrichtigungen und Webhooks, die Cmssy übernimmt
- Einsendungen, die in Cmssy gespeichert und im Admin einsehbar sind
- Erfolgs-/Fehlerzustände mit Animationen, gerendert in deiner App
- Responsives Design mit Tailwind CSS
Voraussetzungen
- Node.js 18+ installiert
- Eine Next.js-App (App Router) mit dem SDK:
npm install @cmssy/next @cmssy/react - Ein Cmssy-Workspace mit einem API-Token
Schritt 1: Erstelle das Formular in Cmssy
Gehe im Cmssy-Admin zu Dashboard → Forms → Create Form. Gib ihm einen Namen und Slug (z. B. contact-form) und füge dann Felder hinzu:
- name - text, erforderlich
- email - email, erforderlich
- message - textarea, erforderlich, Mindestlänge 10
Wähle unter Settings den Aktionstyp contact, füge E-Mail-Empfänger hinzu und setze Submit-Label, Erfolgsmeldung und Fehlermeldung. Labels und Meldungen sind mehrsprachig, du pflegst also en und pl an derselben Stelle. Veröffentliche das Formular, damit es Einsendungen annimmt.
Schritt 2: Definiere den Block
Erstelle in deiner Next.js-App blocks/contact-form/block.ts. Das Schema definiert die Felder, die Redakteure im Cmssy-Editor sehen - hier lässt ein form-Feld sie wählen, welches Formular gerendert wird:
import { defineBlock, fields } from "@cmssy/react";
import Component from "./src";
export const contactFormBlock = defineBlock({
type: "contact-form", // unique block type id, stored on each instance
label: "Contact Form",
component: Component,
schema: {
form: fields.form({ label: "Form" }),
heading: fields.singleLine({ label: "Heading", defaultValue: "Get in Touch" }),
description: fields.richText({ label: "Description" }),
},
});Schritt 3: Baue die Komponente
Erstelle blocks/contact-form/src/index.tsx. Sie holt die Formulardefinition mit createCmssyClient (aus @cmssy/react) von Cmssy und sendet an die öffentliche Formular-Mutation:
"use client";
import { useState } from "react";
import { createCmssyClient } from "@cmssy/react";
import type { CmssyBlockContext } from "@cmssy/react";
const client = createCmssyClient();
interface Props {
content: { form?: string; heading?: string; description?: string };
context?: CmssyBlockContext;
}
export default function ContactForm({ content, context }: Props) {
const { form: formId, heading = "Get in Touch", description } = content;
const [status, setStatus] = useState<"idle" | "sending" | "success" | "error">("idle");
const [values, setValues] = useState<Record<string, string>>({});
const isEditor = context?.isEditor ?? false;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isEditor || !formId) return; // Don't submit in editor preview
setStatus("sending");
try {
// Submits to cmssy: validates, stores the submission, emails recipients
const res = await client.submitForm({ formId, data: values });
setStatus(res.success ? "success" : "error");
} catch {
setStatus("error");
}
};
if (status === "success") {
return (
<section className="py-24">
<div className="max-w-lg mx-auto text-center px-6">
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-green-100
flex items-center justify-center">
<svg width={32} height={32} fill="none" stroke="#22c55e"
viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 className="text-2xl font-bold mb-2">Thanks! We'll get back to you soon.</h3>
</div>
</section>
);
}
return (
<section className="py-24">
<div className="max-w-lg mx-auto px-6">
<div className="text-center mb-10">
<h2 className="text-3xl font-bold mb-3">{heading}</h2>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-medium mb-1.5">Name</label>
<input
type="text"
required
value={values.name ?? ""}
onChange={(e) => setValues({ ...values, name: e.target.value })}
className="w-full px-4 py-2.5 border rounded-lg bg-background
focus:outline-none focus:ring-2 focus:ring-violet-500/20
focus:border-violet-500"
placeholder="Your name"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">Email</label>
<input
type="email"
required
value={values.email ?? ""}
onChange={(e) => setValues({ ...values, email: e.target.value })}
className="w-full px-4 py-2.5 border rounded-lg bg-background
focus:outline-none focus:ring-2 focus:ring-violet-500/20
focus:border-violet-500"
placeholder="you@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">Message</label>
<textarea
required
rows={5}
value={values.message ?? ""}
onChange={(e) => setValues({ ...values, message: e.target.value })}
className="w-full px-4 py-2.5 border rounded-lg bg-background
resize-none focus:outline-none focus:ring-2
focus:ring-violet-500/20 focus:border-violet-500"
placeholder="How can we help?"
/>
</div>
<button
type="submit"
disabled={status === "sending"}
className="w-full py-3 rounded-lg bg-violet-600 hover:bg-violet-700
text-white font-medium transition-colors
disabled:opacity-50 disabled:cursor-not-allowed"
>
{status === "sending" ? "Sending..." : "Send Message"}
</button>
{status === "error" && (
<p className="text-sm text-red-500 text-center">
Something went wrong. Please try again.
</p>
)}
</form>
</div>
</section>
);
}Tipp: Statt Felder hart zu codieren, kannst du die Formulardefinition über den Client holen und form.fields dynamisch rendern - dann aktualisiert eine Änderung des Formulars in Cmssy deine UI ohne Code-Änderung.
Schritt 4: Registriere den Block
Füge den Block zu deiner Block-Registry hinzu, damit der Cmssy-Editor weiß, dass es ihn gibt. Erstelle oder bearbeite cmssy/blocks.ts:
import { contactFormBlock } from "@/blocks/contact-form/block";
export const blocks = [contactFormBlock];Dieses Array ist die einzige Quelle der Wahrheit dafür, welche Blöcke deine Site unterstützt.
Schritt 5: Deploye deine Site
Cmssy hostet deine Site nicht - das machst du. Deploye deine Next.js-App wie gewohnt auf Vercel (oder einen beliebigen Host):
git push # then deploy via Vercel, or your CI of choiceNach dem Deployment verknüpfst du deinen Workspace mit der Live-URL, damit der visuelle Editor von Cmssy deine Site zum Bearbeiten einrahmen kann.
Schritt 6: Auf einer Seite verwenden
Im Cmssy-Editor:
- Klicke auf den +-Button, um einen Block hinzuzufügen
- Finde Contact Form in der Blockliste
- Ziehe ihn auf deine Seite
- Wähle in der Sidebar das Formular aus Schritt 1
- Setze Überschrift und Beschreibung
- Veröffentliche die Seite
Einsendungen landen jetzt unter Dashboard → Forms, wo du sie prüfen, als bearbeitet markieren oder exportieren kannst.
Wichtige Muster
Editor-Sicherheit
Prüfe vor dem Absenden immer context.isEditor, damit das Formular keine echten Daten sendet, während ein Redakteur die Vorschau nutzt:
const isEditor = context?.isEditor ?? false;
if (isEditor) return; // Skip submission in editor previewForm Builder = eine Quelle der Wahrheit
Felder, Validierung und i18n-Labels leben im Form Builder. Dein Block rendert sie nur und ruft submitForm auf. Ändere das Formular in Cmssy, und jede Site, die es rendert, bleibt synchron - kein Redeploy für Content-Anpassungen nötig.
Serverseitige Validierung
Die öffentliche submitForm-Mutation validiert Felder serverseitig, speichert die Einsendung, sendet E-Mail-Benachrichtigungen und ruft jeden konfigurierten Webhook auf - so bleibt dein Block schlank und sicher.
Nächste Schritte
- Erkunde Feldtypen und Validierung in den Forms-&-Form-Builder-Docs
- Richte dein Projekt mit dem Installations-Guide ein
- Lerne die volle
defineBlock-/fields-API in den Blocks-Docs