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 formulario de contacto para un sitio headless de Cmssy. Gestionas el formulario en el Form Builder de Cmssy (admin en cmssy.io) y lo renderizas en tu propia app Next.js (App Router), que despliegas tú mismo (Vercel, etc.). El editor visual de Cmssy enmarca tu sitio desplegado para que los editores ajusten el contenido en vivo. Esta configuración incluye:
- Campos de nombre, email y mensaje con validación - definidos una sola vez en el Form Builder
- Validación en el servidor, notificaciones por email y webhooks gestionados por Cmssy
- Envíos almacenados en Cmssy y visibles en el admin
- Estados de éxito/error con animaciones, renderizados en tu app
- Diseño responsive con Tailwind CSS
Requisitos previos
- Node.js 18+ instalado
- Una app Next.js (App Router) con el SDK:
npm install @cmssy/next @cmssy/react - Un workspace de Cmssy con un token de API
Paso 1: crea el formulario en Cmssy
En el admin de Cmssy, ve a Dashboard → Forms → Create Form. Dale un nombre y un slug (p. ej. contact-form) y añade los campos:
- name - text, obligatorio
- email - email, obligatorio
- message - textarea, obligatorio, longitud mínima 10
En Settings, elige el tipo de acción contact, añade destinatarios de email y define el texto del botón de envío, el mensaje de éxito y el mensaje de error. Las etiquetas y mensajes son multilingües, así que redactas en y pl en el mismo sitio. Publica el formulario para que acepte envíos.
Paso 2: define el bloque
En tu app Next.js, crea blocks/contact-form/block.ts. El esquema define los campos que los editores ven en el editor de Cmssy - aquí, un campo form les permite elegir qué formulario renderizar:
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" }),
},
});Paso 3: construye el componente
Crea blocks/contact-form/src/index.tsx. Obtiene la definición del formulario desde Cmssy con createCmssyClient (de @cmssy/react) y envía a la mutación pública de formularios:
"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>
);
}Consejo: en lugar de codificar los campos a mano, puedes obtener la definición del formulario con el cliente y renderizar form.fields dinámicamente - así, editar el formulario en Cmssy actualiza tu UI sin cambiar código.
Paso 4: registra el bloque
Añade el bloque a tu registro de bloques para que el editor de Cmssy sepa que existe. Crea o edita cmssy/blocks.ts:
import { contactFormBlock } from "@/blocks/contact-form/block";
export const blocks = [contactFormBlock];Ese array es la única fuente de verdad sobre qué bloques soporta tu sitio.
Paso 5: despliega tu sitio
Cmssy no aloja tu sitio - lo haces tú. Despliega tu app Next.js en Vercel (o cualquier hosting) como siempre:
git push # then deploy via Vercel, or your CI of choiceUna vez desplegado, apunta tu workspace a la URL en producción para que el editor visual de Cmssy pueda enmarcar tu sitio para editarlo.
Paso 6: úsalo en una página
En el editor de Cmssy:
- Haz clic en el botón + para añadir un bloque
- Busca Contact Form en la lista de bloques
- Suéltalo en tu página
- En la barra lateral, elige el formulario creado en el Paso 1
- Define el encabezado y la descripción
- Publica la página
Los envíos llegan ahora a Dashboard → Forms, donde puedes revisarlos, marcarlos como procesados o exportarlos.
Patrones clave
Seguridad en el editor
Comprueba siempre context.isEditor antes de enviar, para que el formulario no envíe datos reales mientras un editor previsualiza:
const isEditor = context?.isEditor ?? false;
if (isEditor) return; // Skip submission in editor previewForm Builder = una sola fuente de verdad
Los campos, la validación y las etiquetas i18n viven en el Form Builder. Tu bloque solo los renderiza y llama a submitForm. Cambia el formulario en Cmssy y cada sitio que lo renderiza se mantiene sincronizado - sin redeploy para ajustes de contenido.
Validación en el servidor
La mutación pública submitForm valida los campos en el servidor, guarda el envío, manda notificaciones por email y llama a cualquier webhook configurado - así tu bloque se mantiene ligero y seguro.
Siguientes pasos
- Explora los tipos de campo y la validación en la documentación de Forms & Form Builder
- Configura tu proyecto con la guía de instalación
- Aprende toda la API de
defineBlock/fieldsen la documentación de Blocks