Ahora con creación de páginas por IA vía el servidor MCP
Tutorial

Cómo construir un bloque de formulario de contacto

Crea un formulario de contacto en el Form Builder de Cmssy y luego rendérizalo en tu propio sitio Next.js headless con @cmssy/react. Validación, envíos y correo gestionados por Cmssy.

E
Equipo Cmssy
12 min read

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.

Last updated: 15 de marzo de 2026

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 choice

Una 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:

  1. Haz clic en el botón + para añadir un bloque
  2. Busca Contact Form en la lista de bloques
  3. Suéltalo en tu página
  4. En la barra lateral, elige el formulario creado en el Paso 1
  5. Define el encabezado y la descripción
  6. 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 preview

Form 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

Cómo crear un formulario de contacto con cmssy (Headless + Next.js)