Désormais avec création de pages par IA via le serveur MCP
Tutoriel

Comment créer un bloc formulaire de contact

Créez un formulaire de contact dans le Form Builder de Cmssy, puis affichez-le dans votre propre site Next.js headless avec @cmssy/react. Validation, soumissions et emails gérés par Cmssy.

É
Équipe Cmssy
12 min read

Comment créer un bloc formulaire de contact

Construisez un formulaire de contact headless : créez-le dans le Form Builder de Cmssy, puis affichez-le dans votre propre site Next.js avec le SDK.

Last updated: 15 mars 2026

Ce que nous construisons

Un formulaire de contact pour un site Cmssy headless. Vous gérez le formulaire dans le Form Builder de Cmssy (admin sur cmssy.io) et vous l'affichez dans votre propre application Next.js (App Router), que vous déployez vous-même (Vercel, etc.). L'éditeur visuel de Cmssy encadre votre site déployé pour que les éditeurs puissent ajuster le contenu en direct. Cette configuration comprend :

  • Des champs nom, email et message avec validation - définis une seule fois dans le Form Builder
  • La validation côté serveur, les notifications par email et les webhooks gérés par Cmssy
  • Les soumissions stockées dans Cmssy et consultables dans l'admin
  • Des états de succès/erreur avec animations, rendus dans votre application
  • Un design responsive avec Tailwind CSS

Prérequis

  • Node.js 18+ installé
  • Une application Next.js (App Router) avec le SDK : npm install @cmssy/next @cmssy/react
  • Un workspace Cmssy avec un token API

Étape 1 : créez le formulaire dans Cmssy

Dans l'admin Cmssy, allez dans Dashboard → Forms → Create Form. Donnez-lui un nom et un slug (par ex. contact-form), puis ajoutez des champs :

  • name - text, requis
  • email - email, requis
  • message - textarea, requis, longueur min. 10

Sous Settings, choisissez le type d'action contact, ajoutez des destinataires email et définissez le libellé du bouton d'envoi, le message de succès et le message d'erreur. Les libellés et messages sont multilingues, vous rédigez donc en et pl au même endroit. Publiez le formulaire pour qu'il accepte les soumissions.

Étape 2 : définissez le bloc

Dans votre application Next.js, créez blocks/contact-form/block.ts. Le schéma définit les champs que les éditeurs voient dans l'éditeur Cmssy - ici, un champ form leur permet de choisir le formulaire à afficher :

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" }),
  },
});

Étape 3 : construisez le composant

Créez blocks/contact-form/src/index.tsx. Il récupère la définition du formulaire depuis Cmssy avec createCmssyClient (de @cmssy/react) et soumet à la mutation publique de formulaire :

"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>
  );
}

Astuce : au lieu de coder les champs en dur, vous pouvez récupérer la définition du formulaire avec le client et afficher form.fields dynamiquement - ainsi, modifier le formulaire dans Cmssy met à jour votre UI sans changement de code.

Étape 4 : enregistrez le bloc

Ajoutez le bloc à votre registre de blocs pour que l'éditeur Cmssy sache qu'il existe. Créez ou modifiez cmssy/blocks.ts :

import { contactFormBlock } from "@/blocks/contact-form/block";

export const blocks = [contactFormBlock];

Ce tableau est l'unique source de vérité sur les blocs pris en charge par votre site.

Étape 5 : déployez votre site

Cmssy n'héberge pas votre site - c'est vous qui le faites. Déployez votre application Next.js sur Vercel (ou n'importe quel hébergeur) comme d'habitude :

git push   # then deploy via Vercel, or your CI of choice

Une fois déployé, pointez votre workspace vers l'URL en production pour que l'éditeur visuel de Cmssy puisse encadrer votre site pour l'édition.

Étape 6 : utilisez-le sur une page

Dans l'éditeur Cmssy :

  1. Cliquez sur le bouton + pour ajouter un bloc
  2. Trouvez Contact Form dans la liste des blocs
  3. Déposez-le sur votre page
  4. Dans la barre latérale, choisissez le formulaire créé à l'étape 1
  5. Définissez le titre et la description
  6. Publiez la page

Les soumissions arrivent désormais dans Dashboard → Forms, où vous pouvez les consulter, les marquer comme traitées ou les exporter.

Schémas clés

Sécurité de l'éditeur

Vérifiez toujours context.isEditor avant de soumettre, pour que le formulaire n'envoie pas de vraies données pendant qu'un éditeur prévisualise :

const isEditor = context?.isEditor ?? false;
if (isEditor) return; // Skip submission in editor preview

Form Builder = une seule source de vérité

Les champs, la validation et les libellés i18n vivent dans le Form Builder. Votre bloc ne fait que les afficher et appeler submitForm. Modifiez le formulaire dans Cmssy et chaque site qui l'affiche reste synchronisé - aucun redéploiement nécessaire pour les ajustements de contenu.

Validation côté serveur

La mutation publique submitForm valide les champs côté serveur, stocke la soumission, envoie les notifications email et appelle tout webhook configuré - votre bloc reste donc léger et sûr.

Prochaines étapes

Créer un formulaire de contact avec cmssy (Headless + Next.js)