Skip to content

Forms

Forms are a built-in feature for collecting submissions from site visitors — contact forms, newsletter signups, surveys. Editors build forms visually in the admin, and developers render them with a single component.

Two collections ship with the CMS:

  • forms — form definitions (fields, labels, notification email, success message)
  • form-submissions — captured entries, with a new | read | archived status
  1. Open /admin/formsNew form.
  2. Give it a title (e.g., “Contact”). The slug auto-generates from the title and is used in the public component to reference this form.
  3. Optionally set:
    • Redirect URL after submit — redirect visitors to a thank-you page.
    • Success message — text shown on the form page after a successful submit (falls back to “Thanks — we got your message.” if empty).
    • Notification email — an address to email on every submission (requires RESEND_API_KEY).
  4. Add fields — each field is a block. Supported types:
TypeOptions
textname, label, placeholder, required, maxLength
emailname, label, placeholder, required
textareaname, label, placeholder, required, rows
selectname, label, required, options (array of strings)
checkboxname, label, required

name is the form data key (e.g., email) — used in the stored submission’s data object. label is what the visitor sees.

---
import CmsForm from "@/components/CmsForm.astro";
---
<CmsForm slug="contact" />

That’s it. The component fetches the form definition by slug, renders native HTML inputs (no client JS needed), submits to /api/cms/forms/submit/{slug}, and redirects back with ?submitted=1 on success so the success message appears.

To know which page a submission came from, pass a context prop:

---
import CmsForm from "@/components/CmsForm.astro";
import { cms } from "@/cms/.generated/api";
const post = await cms.posts.findOne({ slug: Astro.params.slug! });
---
<CmsForm slug="contact" context={{ pageTitle: post.title, pageUrl: Astro.url.pathname }} />

Each key-value pair becomes a hidden input prefixed with _ctx_. The server merges them into the submission’s data._context object — visible alongside submitted fields on the submission detail page.

Forms appear in the admin sidebar (between Menus and Authors) with a muted “N new” count. Open a form and click the Submissions tab to see its entries:

  • Each row: submitted date, status, preview of the data.
  • Clicking a row opens a read-only detail view with all submitted fields + _context metadata rendered as a table.
  • Bulk status change — select rows and use Mark as new/read/archived to moderate in bulk.
  • Status new entries count toward the unread badge on the sidebar.

The rendered form includes a honeypot field (_hp) hidden via CSS. Bots typically fill every visible input, so non-empty _hp submissions are silently accepted but never stored.

This is enough for most small sites. For heavier traffic, add a beforeCreate hook on form-submissions that calls Turnstile or reCAPTCHA.

PieceLocation
Form definition collectionsrc/cms/collections/forms.ts
Submission storage collectionsrc/cms/collections/form-submissions.ts
Public render componentsrc/components/CmsForm.astro
Submit endpointsrc/cms/routes/api/forms/submit/[slug].ts
Email notificationsendFormSubmissionEmail in src/cms/adapters/email.ts
Auth middleware public-route allowlistsrc/cms/middleware/auth.ts

Because everything is first-class collections and a route, removing it is straightforward:

  1. Delete src/cms/collections/forms.ts and form-submissions.ts.
  2. Unregister both from src/cms/cms.config.ts.
  3. Delete src/components/CmsForm.astro and src/cms/routes/api/forms/.
  4. Remove the isFormSubmit allowlist entry from src/cms/middleware/auth.ts.
  5. Remove forms from pinnedOrder / pinnedIcons in src/cms/admin/layouts/AdminLayout.astro.

No framework knob needed — the feature is just code.