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 bloc formulaire de contact : vous définissez le formulaire une fois dans le Form Builder de Cmssy, et votre bloc l'affiche dans votre propre application Next.js. Cmssy gère la validation, stocke la soumission et l'envoie par e-mail aux destinataires ; votre application possède le markup et le style.
Cette séparation compte. La définition du formulaire - champs, libellés, validation, message de succès, le tout localisé - est du contenu : elle vit dans le CMS et un rédacteur peut la modifier sans déploiement. Le rendu est du code : il vit dans votre repo.
Prérequis
- Une application Next.js (App Router) reliée à votre workspace avec
@cmssy/reactet@cmssy/next- voir le guide d'installation - Le codegen GraphQL configuré, pour que
SubmitFormDocumentsoit généré pour vous
Étape 1 : créer le formulaire dans Cmssy
Dans l'admin Cmssy, allez dans Formulaires et créez-en un :
- Ajoutez les champs nécessaires -
name(text),email(email),message(textarea) - chacun avec un libellé localisé et des règles de validation - Définissez le type d'action sur contact et ajoutez les adresses des destinataires
- Rédigez le libellé du bouton et le message de succès, par langue
- Passez le statut en published pour que le formulaire accepte les soumissions
Étape 2 : définir le bloc
Créez blocks/contact/block.ts. Le builder fields.form donne à l'éditeur un sélecteur de formulaire ; le placer sur l'onglet advanced l'écarte des retouches de texte quotidiennes :
import { defineBlock, fields } from "@cmssy/react";
import Contact from "./Contact";
export const contactProps = {
heading: fields.text({ label: "Heading" }),
description: fields.textarea({ label: "Description" }),
formId: fields.form({ label: "Form", tab: "advanced" }),
submitLoadingText: fields.text({
label: "Submit Loading Text",
defaultValue: "Sending...",
}),
successHeading: fields.text({
label: "Success Heading",
defaultValue: "Message Sent!",
}),
};
export const contactBlock = defineBlock({
type: "contact",
category: "Forms",
label: "Contact",
description:
"Contact details and/or contact form; near the end of a page or on a dedicated contact page.",
component: Contact,
props: contactProps,
});Étape 3 : construire le composant
Le bloc ne va pas chercher le formulaire. Le SDK résout chaque formulaire référencé sur la page et transmet les définitions à votre composant via context.forms, indexées par id. Comme le champ est sur l'onglet advanced, sa valeur arrive dans la prop advanced :
import type { BlockProps } from "@cmssy/react";
import type { contactProps } from "./block";
import { ContactForm } from "./ContactForm";
export default function Contact({
content,
context,
advanced = {},
}: BlockProps<typeof contactProps>) {
const { heading, description, successHeading, submitLoadingText } = content;
const { formId } = advanced as { formId?: string };
const formDef = formId ? (context?.forms?.[formId] ?? null) : null;
return (
<section className="py-24">
<div className="max-w-lg mx-auto px-6">
{heading && <h2 className="text-3xl font-bold">{heading}</h2>}
{description && (
<p className="mt-3 text-muted-foreground">{description}</p>
)}
{formDef?.fields?.length && formId ? (
<ContactForm
formDef={formDef}
formId={formId}
successHeading={successHeading}
submitLoadingText={submitLoadingText}
/>
) : null}
</div>
</section>
);
}Notez la garde : pas de formulaire sélectionné, pas de formulaire affiché. Un bloc montre ce que le CMS lui donne et rien d'autre - jamais un titre de repli en dur qui ferait fuiter de l'anglais sur une page française.
Étape 4 : soumettre via une server action
La soumission est une mutation GraphQL, et sa place est sur le serveur pour que vos identifiants de diffusion n'atteignent jamais le navigateur. Mettez l'appel dans services/forms.ts :
import { print } from "graphql";
import { createCmssyClient } from "@cmssy/react";
import { cmssy } from "@/cmssy/config";
import {
SubmitFormDocument,
type SubmitFormMutation,
} from "@/graphql/generated/graphql";
const client = createCmssyClient(cmssy);
export async function submitForm(
formId: string,
data: Record<string, string>,
) {
const res = await client.queryScoped<SubmitFormMutation>(
print(SubmitFormDocument),
{ formId, input: { data } },
);
const result = res.public.form.submit;
return { success: result.success, message: result.message };
}Enveloppez-le ensuite dans une server action, dans blocks/contact/actions.ts. Le champ website est un pot de miel : invisible pour les humains, donc tout ce qui le remplit est un bot et reçoit un faux succès :
"use server";
import { submitForm } from "@/services/forms";
import type { ContactState } from "./types";
export async function submitContact(
formId: string,
_prevState: ContactState,
formData: FormData,
): Promise<ContactState> {
if (formData.get("website")) {
return { status: "success", message: null };
}
const data: Record<string, string> = {};
for (const [key, value] of formData.entries()) {
if (key === "website") continue;
if (typeof value === "string" && value) data[key] = value;
}
try {
const result = await submitForm(formId, data);
return {
status: result.success ? "success" : "error",
message: result.message,
};
} catch {
return { status: "error", message: null };
}
}Étape 5 : afficher les champs
La moitié client affiche exactement les champs déclarés par la définition du formulaire et appelle l'action avec useActionState. Rien de la liste de champs n'est en dur : ajoutez un champ dans le Form Builder et il apparaît ici sans déploiement :
"use client";
import { useActionState } from "react";
import type { CmssyFormDefinition } from "@cmssy/react";
import { submitContact } from "./actions";
import type { ContactState } from "./types";
const INITIAL_STATE: ContactState = { status: "idle", message: null };
export function ContactForm({
formDef,
formId,
successHeading,
submitLoadingText,
}: {
formDef: CmssyFormDefinition;
formId: string;
successHeading: string;
submitLoadingText: string;
}) {
const [state, formAction, isPending] = useActionState(
submitContact.bind(null, formId),
INITIAL_STATE,
);
if (state.status === "success") {
return <p>{successHeading}</p>;
}
return (
<form action={formAction} className="mt-10 space-y-5">
<input type="text" name="website" tabIndex={-1} className="hidden" />
{formDef.fields.map((field) => (
<div key={field.id}>
<label className="block text-sm font-medium mb-1.5">
{field.label}
</label>
{field.fieldType === "textarea" ? (
<textarea
name={field.name}
rows={5}
required={field.validation?.required}
className="w-full px-4 py-2.5 border rounded-lg"
/>
) : (
<input
type={field.fieldType}
name={field.name}
required={field.validation?.required}
className="w-full px-4 py-2.5 border rounded-lg"
/>
)}
</div>
))}
<button type="submit" disabled={isPending}>
{isPending ? submitLoadingText : "Send"}
</button>
{state.status === "error" && (
<p className="text-sm text-red-500">{state.message}</p>
)}
</form>
);
}Le libellé est localisé dans le CMS : chaque langue a le sien, sans la moindre condition dans votre code.
Étape 6 : enregistrer le bloc
Ajoutez-le au tableau dans cmssy/blocks.ts - celui-là même que vous passez à createCmssyPage :
import { contactBlock } from "@/blocks/contact/block";
// ...vos autres blocs
export const blocks = [contactBlock];Étape 7 : déployer et utiliser
Il n'y a pas d'étape de déploiement séparée pour le bloc - il part avec votre application Next.js :
git push # puis déployez via Vercel, ou votre CIOuvrez ensuite une page dans l'éditeur Cmssy, déposez-y le bloc Contact, choisissez votre formulaire dans l'onglet Advanced et publiez. Les soumissions arrivent dans Formulaires côté admin et partent vers les destinataires configurés.
Motifs clés
- La définition du formulaire est du contenu - champs, libellés et validation vivent dans le CMS, les rédacteurs les changent sans déploiement
- La soumission est côté serveur - une server action garde vos identifiants de diffusion hors du navigateur
- Pot de miel plutôt que captcha - un champ caché ne coûte rien et arrête l'essentiel des bots
- Pas de vraie soumission dans l'éditeur - vérifiez
context.isPreviewavant de brancher quoi que ce soit de destructeur - Ne rien afficher plutôt qu'un défaut - pas de formulaire choisi signifie pas de formulaire, pas un placeholder
Prochaines étapes
- Lisez le guide de développement de blocs
- Consultez tous les types de champs et options de schéma
- Découvrez les formulaires et soumissions
- Suivez le guide d'installation pour mettre en place
@cmssy/reactet@cmssy/next