Jetzt mit KI-gestütztem Page Building über den MCP-Server
Tutorial

So baust du einen Kontaktformular-Block

Erstelle ein Kontaktformular im Form Builder von Cmssy und rendere es dann in deiner eigenen headless Next.js-Site mit @cmssy/react. Validierung, Einsendungen und E-Mail übernimmt Cmssy.

C
Cmssy Team
12 min read

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.

Last updated: 15. März 2026

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 choice

Nach 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:

  1. Klicke auf den +-Button, um einen Block hinzuzufügen
  2. Finde Contact Form in der Blockliste
  3. Ziehe ihn auf deine Seite
  4. Wähle in der Sidebar das Formular aus Schritt 1
  5. Setze Überschrift und Beschreibung
  6. 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 preview

Form 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