webhouse.appwebhouse.appdocs

Embed CMS forms in your pages using a richtext shortcode, a content block, or a dedicated field type.

Overview

Once you've defined a form, you need to put it on a page. The CMS gives you three methods — pick the one that fits your content model.

MethodBest forHow it works
A. ShortcodeRichtext articlesType {{form:contact}} in any richtext field
B. BlockBlock-based pagesAdd a "Form Embed" block to any blocks field
C. Field typeDedicated form pagesAdd a type: "form" field to a collection

All three render the same semantic <form> HTML with honeypot, async submit, and JS-free fallback.

A. Richtext shortcode

Type {{form:contact}} anywhere in a richtext field. At build time, the CMS replaces it with the full <form> HTML.

markdown
## Get in touch

Fill out the form below and we'll get back to you.

{{form:contact}}

The shortcode follows the same pattern as snippet embeds (<!-- snippet not found: slug -->). The form name must match a form defined in cms.config.ts or created in the admin Form Builder.

For Next.js dynamic sites: the shortcode appears as literal text in the JSON. Your renderer should replace {{form:name}} with a React form component at render time.

B. Content block

For pages that use the block editor (hero, features, testimonials, etc.), add a Form Embed block:

  1. In the block editor, click + Add block
  2. Select Form Embed
  3. Type the form name (e.g. contact)
  4. The block renders the form inline with the other blocks

The block is built in — no configuration needed. It stores:

json
{
  "_block": "form",
  "formName": "contact"
}

Your site's block renderer should check for _block === "form" and render the form. For static builds, cms build handles this automatically.

C. Dedicated field type

For collections where a specific page always shows a form (e.g. a "Landing Pages" collection), add a form field:

typescript
defineCollection({
  name: 'landing-pages',
  label: 'Landing Pages',
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'heroText', type: 'richtext' },
    { name: 'contactForm', type: 'form', label: 'Embedded form' },
    // ...
  ],
});

The field value is the form name (a string like "contact"). In the admin editor, it renders as a dropdown of all available forms. In your site template:

typescript
// Next.js example
import { generateFormHtml } from '@webhouse/cms';

function LandingPage({ page }) {
  const formHtml = page.data.contactForm
    ? generateFormHtml(
        forms.find(f => f.name === page.data.contactForm),
        process.env.CMS_ADMIN_URL
      )
    : null;

  return (
    <main>
      <h1>{page.data.title}</h1>
      {formHtml && <div dangerouslySetInnerHTML={{ __html: formHtml }} />}
    </main>
  );
}

Which method to choose

  • You write blog posts with occasional forms → use shortcodes (A). Zero config, type it inline.
  • You build pages from blocks → use the Form Embed block (B). Drag it where you want it.
  • You have a collection where every entry has a form → use the field type (C). Structured, explicit, queryable.
  • You want to embed on a page not built by the CMS → use the embeddable widget script instead.

The generated form

All three methods produce the same HTML:

  • Semantic <form> with action pointing at the CMS admin's public endpoint
  • All fields with HTML5 types (email, tel, date, etc.) and validation attributes
  • Honeypot field (invisible to humans, catches bots)
  • Inline <script> for async submit + success/error message
  • Works without JavaScript — plain POST + redirect as fallback

Style the form with your site's CSS. The generated HTML uses minimal inline styles that are easy to override.

See also

  • Form Engine — define forms, inbox, notifications, spam protection
  • Snippet Embeds — the <!-- snippet not found: slug --> pattern this is based on
  • Blocks — how block-based content works
Tags:FormsBlocksShortcodesRichtext
Previous
Richtext Editor
Next
Content Relationships
JSON API →Edit on GitHub →