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.
| Method | Best for | How it works |
|---|---|---|
| A. Shortcode | Richtext articles | Type {{form:contact}} in any richtext field |
| B. Block | Block-based pages | Add a "Form Embed" block to any blocks field |
| C. Field type | Dedicated form pages | Add 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.
## 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:
- In the block editor, click + Add block
- Select Form Embed
- Type the form name (e.g.
contact) - The block renders the form inline with the other blocks
The block is built in — no configuration needed. It stores:
{
"_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:
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:
// 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>withactionpointing 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