Jak stworzyc blok formularza kontaktowego
Zbuduj headless formularz kontaktowy: stworz go w Form Builderze Cmssy, a nastepnie wyrenderuj we wlasnej stronie Next.js przez SDK.
Co budujemy
Blok formularza kontaktowego: formularz definiujesz raz w Form Builderze Cmssy, a Twój blok renderuje go we własnej aplikacji Next.js. Cmssy zajmuje się walidacją, zapisuje zgłoszenie i wysyła maile do odbiorców; Twoja aplikacja włada markupem i stylami.
Ten podział ma znaczenie. Definicja formularza - pola, etykiety, walidacja, komunikat sukcesu, wszystko przetłumaczone - to treść, więc żyje w CMS-ie i redaktor może ją zmienić bez deployu. Renderowanie to kod, więc żyje w Twoim repo.
Wymagania wstępne
- Aplikacja Next.js (App Router) podpięta do workspace'u przez
@cmssy/reacti@cmssy/next- zobacz przewodnik instalacji - Skonfigurowany codegen GraphQL, żeby
SubmitFormDocumentwygenerował się sam
Krok 1: Utwórz formularz w Cmssy
W panelu Cmssy wejdź w Formularze i utwórz jeden:
- Dodaj potrzebne pola -
name(text),email(email),message(textarea) - każde z przetłumaczoną etykietą i regułami walidacji - Ustaw typ akcji na contact i dodaj adresy odbiorców
- Wpisz etykietę przycisku i komunikat sukcesu, per język
- Ustaw status na published, żeby formularz przyjmował zgłoszenia
Krok 2: Zdefiniuj blok
Utwórz blocks/contact/block.ts. Builder fields.form daje edytorowi wybór formularza; umieszczenie go na zakładce advanced trzyma go z dala od codziennej edycji tekstów:
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,
});Krok 3: Zbuduj komponent
Blok nie pobiera formularza. SDK rozwiązuje każdy formularz użyty na stronie i podaje definicje Twojemu komponentowi w context.forms, kluczowane po id. Ponieważ pole siedzi na zakładce advanced, jego wartość przychodzi w propie 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>
);
}Zwróć uwagę na strażnika: brak wybranego formularza oznacza brak wyrenderowanego formularza. Blok pokazuje to, co da mu CMS, i nic więcej - nigdy zahardkodowanego nagłówka, który przelewałby angielski na polską stronę.
Krok 4: Wyślij przez server action
Wysłanie to mutacja GraphQL i należy do serwera, żeby Twoje poświadczenia delivery nigdy nie trafiły do przeglądarki. Umieść wywołanie w 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 };
}Następnie opakuj to w server action w blocks/contact/actions.ts. Pole website to honeypot: jest ukryte przed ludźmi, więc cokolwiek je wypełnia jest botem i dostaje fikcyjny sukces:
"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 };
}
}Krok 5: Wyrenderuj pola
Część kliencka renderuje te pola, które deklaruje definicja formularza, i woła akcję przez useActionState. Nic w liście pól nie jest zahardkodowane - dodaj pole w Form Builderze, a pojawi się tutaj bez deployu:
"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>
);
}Etykieta jest przetłumaczona w CMS-ie, więc każdy język dostaje swoją bez żadnego ifa w Twoim kodzie.
Krok 6: Zarejestruj blok
Dodaj go do tablicy w cmssy/blocks.ts - tej samej, którą podajesz do createCmssyPage:
import { contactBlock } from "@/blocks/contact/block";
// ...pozostałe bloki
export const blocks = [contactBlock];Krok 7: Wdróż i użyj
Nie ma osobnego kroku wdrożenia bloku - blok jedzie razem z Twoją aplikacją Next.js:
git push # potem deploy przez Vercel lub Twoje CIPotem otwórz stronę w edytorze Cmssy, upuść blok Contact, wybierz swój formularz na zakładce Advanced i opublikuj. Zgłoszenia lądują w sekcji Formularze w panelu i idą do skonfigurowanych odbiorców.
Kluczowe wzorce
- Definicja formularza to treść - pola, etykiety i walidacja żyją w CMS-ie, więc redaktorzy zmieniają je bez deployu
- Wysyłka po stronie serwera - server action trzyma poświadczenia delivery poza przeglądarką
- Honeypot zamiast captchy - ukryte pole nic nie kosztuje i zatrzymuje większość botów
- Pomijaj realne wysyłki w edytorze - sprawdź
context.isPreview, zanim podepniesz cokolwiek destrukcyjnego - Renderuj nic zamiast domyślnej wartości - brak wybranego formularza to brak formularza, nie placeholder
Następne kroki
- Przeczytaj przewodnik po budowaniu bloków
- Zobacz wszystkie typy pól i opcje schematu
- Poznaj formularze i zgłoszenia
- Postępuj wg przewodnika instalacji, aby ustawić
@cmssy/reacti@cmssy/next