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.
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 choiceUne 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 :
- Cliquez sur le bouton + pour ajouter un bloc
- Trouvez Contact Form dans la liste des blocs
- Déposez-le sur votre page
- Dans la barre latérale, choisissez le formulaire créé à l'étape 1
- Définissez le titre et la description
- 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 previewForm 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
- Explorez les types de champs et la validation dans la documentation Forms & Form Builder
- Configurez votre projet avec le guide d'installation
- Apprenez toute l'API
defineBlock/fieldsdans la documentation Blocks