Cómo construir un bloque de formulario de contacto
Construye un formulario de contacto headless: créalo en el Form Builder de Cmssy y luego rendérizalo en tu propio sitio Next.js con el SDK.
Qué vamos a construir
Un bloque de formulario de contacto: defines el formulario una vez en el Form Builder de Cmssy y tu bloque lo renderiza en tu propia aplicación Next.js. Cmssy se encarga de la validación, guarda el envío y lo manda por correo a los destinatarios; tu aplicación es dueña del markup y los estilos.
Esa separación importa. La definición del formulario - campos, etiquetas, validación, mensaje de éxito, todo localizado - es contenido, así que vive en el CMS y un editor puede cambiarla sin desplegar. El renderizado es código, así que vive en tu repositorio.
Requisitos previos
- Una aplicación Next.js (App Router) conectada a tu workspace con
@cmssy/reacty@cmssy/next- consulta la guía de instalación - El codegen de GraphQL configurado, para que
SubmitFormDocumentse genere solo
Paso 1: crear el formulario en Cmssy
En el panel de Cmssy, ve a Formularios y crea uno:
- Añade los campos que necesites -
name(text),email(email),message(textarea) - cada uno con etiqueta localizada y reglas de validación - Pon el tipo de acción en contact y añade las direcciones de los destinatarios
- Escribe la etiqueta del botón y el mensaje de éxito, por idioma
- Cambia el estado a published para que el formulario acepte envíos
Paso 2: definir el bloque
Crea blocks/contact/block.ts. El builder fields.form le da al editor un selector de formulario; ponerlo en la pestaña advanced lo mantiene fuera del camino de la edición diaria de textos:
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,
});Paso 3: construir el componente
El bloque no obtiene el formulario. El SDK resuelve todos los formularios referenciados en la página y entrega las definiciones a tu componente en context.forms, indexadas por id. Como el campo está en la pestaña advanced, su valor llega en 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>
);
}Fíjate en la guarda: sin formulario seleccionado no se renderiza formulario. Un bloque muestra lo que le da el CMS y nada más - nunca un titular de reserva escrito a fuego que colaría inglés en una página en español.
Paso 4: enviar mediante una server action
El envío es una mutación GraphQL y su sitio es el servidor, para que tus credenciales de entrega no lleguen nunca al navegador. Pon la llamada en 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 };
}Luego envúelvela en una server action en blocks/contact/actions.ts. El campo website es un señuelo: está oculto a las personas, así que cualquier cosa que lo rellene es un bot y recibe un éxito fingido:
"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 };
}
}Paso 5: renderizar los campos
La mitad cliente renderiza exactamente los campos que declara la definición del formulario y llama a la acción con useActionState. Nada de la lista de campos está escrito a fuego: añade un campo en el Form Builder y aparece aquí sin desplegar:
"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>
);
}La etiqueta está localizada en el CMS, así que cada idioma recibe la suya sin un solo condicional en tu código.
Paso 6: registrar el bloque
Añádelo al array de cmssy/blocks.ts, el mismo que pasas a createCmssyPage:
import { contactBlock } from "@/blocks/contact/block";
// ...tus otros bloques
export const blocks = [contactBlock];Paso 7: desplegar y usarlo
No hay un paso de despliegue aparte para el bloque: viaja con tu aplicación Next.js:
git push # luego despliega vía Vercel, o tu CIDespués abre una página en el editor de Cmssy, suelta el bloque Contact, elige tu formulario en la pestaña Advanced y publica. Los envíos aparecen en Formularios dentro del panel y salen hacia los destinatarios que configuraste.
Patrones clave
- La definición del formulario es contenido - campos, etiquetas y validación viven en el CMS, y los editores los cambian sin desplegar
- El envío es del lado del servidor - una server action mantiene tus credenciales de entrega fuera del navegador
- Señuelo mejor que captcha - un campo oculto no cuesta nada y frena a la mayoría de los bots
- Nada de envíos reales en el editor - comprueba
context.isPreviewantes de conectar algo destructivo - Mejor no renderizar que poner un valor por defecto - sin formulario elegido no hay formulario, ni placeholder
Siguientes pasos
- Lee la guía de desarrollo de bloques
- Consulta todos los tipos de campo y opciones de esquema
- Aprende sobre formularios y envíos
- Sigue la guía de instalación para configurar
@cmssy/reacty@cmssy/next