Tag: forms

  • Flower Delivery Order Form With a Branded Confirmation Email

    Flower Delivery Order Form With a Branded Confirmation Email

    Short answer: in WordPress you can build a flower delivery order form (bouquet, size, delivery date, card message) and send a confirmation email in your shop’s own design, with no email-marketing tool and no HTML. CraftForms has an email template builder that runs in the WordPress block editor. You design the email with blocks, or generate a design with AI and paste it in. The customer’s bouquet, delivery date and card message fill in automatically when they order.

    This guide builds both for an example flower shop, Bloom & Co.: the order form, a branded confirmation for the customer, and a work order for the shop with everything the florist needs to make the bouquet.


    Why a florist needs a designed confirmation email

    Flowers are bought for someone else, usually against a deadline: a birthday, an anniversary, a funeral. The customer’s worry after paying is always the same: will it arrive on the right day, and did they get my message right?

    A plain email that just lists the fields (“field_7: Peonies, field_9: 2026-10-14”) answers that question, but it doesn’t look like it came from a florist. A confirmation that repeats the delivery date and card message, in the shop’s colours and with a photo of the bouquet style, removes the worry and looks like the shop the customer thought they were buying from.

    This is where most WordPress form plugins are weak. Their notification emails are a text box with merge tags. To send a designed email you need an email-marketing tool, an email-template plugin, or someone who writes table-based HTML by hand.


    What we’re building

    PieceWhere it livesPlan
    Order form: bouquet style, size, colour palette, delivery date, delivery zone, recipient, card messageCraftForms → FormsFree, except the delivery-date picker (Pro)
    Customer email: “Your flowers are booked for {date}”CraftForms → Email TemplatesFree
    Shop email: work order with recipient address and card messageCraftForms → Email TemplatesFree
    Payment + live price by size and zoneCatalog item / Payment blockPro

    Step 1: Build the order form

    Create a form under CraftForms → Forms → Add New. The fields, with the names we’ll use as merge tags later:

    FieldBlockName (merge tag)
    Bouquet style (Romantic / Wildflower / Seasonal / Sympathy)Radio, Card option style (a photo and a short description per option)bouquet
    Size (Petite / Classic / Grand)Radio, Text option style, with a price on each optionsize
    Colour palette (Blush / Sunset / White & green)Radio with the Color option style (swatches)palette
    Delivery dateBooking datepicker (Pro), whole days only (no time slots), with Sundays and past dates closed by its Catalog itemdelivery_date
    Delivery zone (City centre / Suburbs / Out of town)Select, with a price on each optionzone
    Recipient name, phone, addressText inputsrecipient_name, recipient_phone, recipient_address
    Card messageTextarea (limit to about 200 characters so it fits the card)card_message
    Your name, emailText inputsname, email

    Tip: name your fields before building the email. The email’s Dynamic Data panel reads the form’s field names, so good names now mean readable tags later ({{email.card_message}}, not {{email.textarea_3}}).

    Pro: live price. Give each size and zone option a price (e.g. Petite 35 / Classic 55 / Grand 85, plus 0 / 7 / 15 for delivery), add two Linked smart variables (size_price, zone_price) and set the price formula to size_price + zone_price. The customer sees the total update live, and the email gets {{email.price}} (the number, in your number format) and {{email.currency}} (the symbol).

    Readable dates: a delivery date reaches the email as 2026-10-14. To print Wednesday, 14 October 2026, add an exposed smart variable delivery_day with the expression is_empty(delivery_date) ? '' : date('l, j F Y', delivery_date.date) and use {{email.delivery_day}} in the email. Leave its submission label empty so it doesn’t appear as an extra row in All Form Data.

    Craftforms flowers order
    Flowers ordering form

    Step 2: Design the confirmation email in the block editor

    Go to CraftForms → Email Templates → Add New. This is the regular WordPress block editor, limited to blocks that render reliably in email clients: headings, paragraphs, images, buttons, lists, separators, spacers, groups (with Row/Stack layouts for side-by-side content), quotes, and CraftForms’ All Form Data block.

    Side-by-side layouts: there’s no Columns block in email templates, but a Group block set to the Row layout puts blocks next to each other (e.g. a bouquet photo left, the order details right), and Stack puts them in a column. Check the result in Email Template Preview. Most modern mail apps show a Row side by side; older desktop Outlook stacks it, which still reads fine.

    You have three ways to start:

    1. Starter Templates panel → Choose email template. Pick Florist Order Confirmation (the design in this guide) or another starter, and change it.
    2. Design from scratch with blocks.
    3. Generate a design with AI and paste it in (next section). This is the fastest way to get something that doesn’t look like a template.

    A layout that works for flower orders

    • Header group: brand colour background, shop name ({{site.name}}) or a logo image, one line: “Your flowers are booked.”
    • Hero image: a photo of the bouquet style. Upload one to the Media Library. Email clients need an absolute image URL, and Media Library images already have one.
    • The two answers the customer wants, in large type:
      Delivering on {{email.delivery_day}} to {{email.recipient_name}}
    • Their card message, quoted: use a Quote block containing {{email.card_message}}. Seeing the message in print is what reassures people.
    • Order details: bouquet, size, palette and zone as a short list, or drop in the All Form Data block to print every answer as a table. It skips empty fields automatically.
    • Total (Pro): {{email.currency}}{{email.price}}. {{email.price}} on its own is only the number.
    • Button: “Questions? Reply or call us”. Link to tel: or your contact page.
    • Footer: address, opening hours, a note about the delivery window.

    The email’s background colour is set in the Styles panel. Use Email Template Preview → Open the preview at any point to see the compiled, CSS-inlined version exactly as it will be sent.

    Subject and recipient live on the template

    Open Email settings in the sidebar:

    • Email Subject: Your {{email.bouquet}} bouquet is booked for {{email.delivery_day}}
    • Email Recipient: {{email.email}} (the customer’s email field)

    Because subject and recipient are part of the template, the customer email and the shop email are two templates. That’s the next step.

    Craftforms flowers order
    Fancy email template built in the visual email templates builder

    Step 3 (the AI part): generate the design, then edit it visually

    The editor stores every template as block markup, the HTML-with-comments format WordPress saves posts in. An AI model can write block markup, and you can paste it into the editor.

    1. In the email template editor, open the ⋮ menu (top right) → Code editor.
    2. Paste the AI’s output, then Exit code editor. The design is now normal blocks you can click and edit.
    3. Save. CraftForms compiles it to email-safe HTML.

    A prompt that works (fill in your brand):

    You are designing a transactional email for a WordPress block editor (Gutenberg).
    Output ONLY WordPress block markup (<!-- wp:... --> comments + HTML), no <html>/<body>, no <style> tags.
    Allowed blocks ONLY: core/group, core/heading, core/paragraph, core/image, core/buttons,
    core/button, core/list, core/list-item, core/separator, core/spacer, core/quote.
    No core/columns. For side-by-side content use core/group with layout {"type":"flex","flexWrap":"nowrap"} (Row).
    No CSS grid. All styling via block attributes that WordPress
    serializes to inline style="" (colors as hex, font sizes in px, padding in px).
    Max content width 600px.
    
    Brand: "Bloom & Co.", a neighbourhood florist. Mood: soft, editorial, romantic.
    Palette: blush #F6E3E0, deep green #2F4A3A, cream #FBF7F2, accent terracotta #C9785D.
    Headings in a serif (Playfair Display, fallback Georgia), body in a sans (Inter, fallback Arial).
    
    Content (keep these merge tags exactly, including the double braces):
    - Header: {{site.name}} and the line "Your flowers are booked."
    - Big line: "Delivering on {{email.delivery_day}} to {{email.recipient_name}}"
    - Card message in a quote block: {{email.card_message}}
    - Order details list: Bouquet {{email.bouquet}}, Size {{email.size}}, Palette {{email.palette}}, Zone {{email.zone}}
    - Total: {{email.currency}}{{email.price}}
    - Button "Questions? Call us" linking to tel:+10000000000
    - Footer with address placeholder and "We deliver between 9:00 and 17:00."
    

    To compare against what your AI produces, the same design ships as the Florist Order Confirmation email starter.

    What to check after pasting:

    • If a block shows “This block contains unexpected or invalid content”, click Attempt recovery. AI output sometimes gets the comment attributes and the HTML slightly out of sync. Recovery rebuilds the HTML from the attributes.
    • Every {{tag}} is still intact (some models “helpfully” remove the braces).
    • Images point to your own Media Library URLs, not placeholders.
    • Web fonts: many email clients (Outlook desktop, some webmail) ignore custom fonts and use the fallback. Always give a safe fallback (Georgia, Arial) and design so the fallback still looks right.

    Step 4: The shop’s work order (second template)

    Duplicate the idea with a plainer design aimed at the florist, not the customer. This one should be quick to read at the workbench:

    • Email Subject: 🌸 New order: {{email.size}} {{email.bouquet}} for {{email.delivery_day}} ({{email.zone}})
    • Email Recipient: your shop inbox
    • Body: the All Form Data block (everything, as a table), then the recipient address and phone large, and the card message in a bordered box so it’s easy to copy onto the card.

    Next step for the shop: instead of copying the message onto a card by hand, attach a printable card PDF to this email with the action’s Attach PDF(s) setting. It’s a PDF template with a Custom page size of 105 × 148 mm (A6), with the message in a script font, generated for each order. PDF templates are built in the same block editor as email templates.


    Step 5: Connect both templates to the form

    In the form editor, open the submit actions and add:

    1. Save Submission, so every order is kept under CraftForms → Submissions.
    2. Send Email (Template) → Email Template: Bloom: Order confirmation (goes to the customer, because the template’s recipient is {{email.email}}).
    3. Send Email (Template) → Email Template: Bloom: Work order (goes to the shop).

    Optionally tick Use custom SMTP server on each and choose a real sending service. Florists’ confirmations often land in spam when sent from a shared host’s PHP mail, and a proper SMTP server fixes that (see Best free SMTP services for WordPress). While testing, point both at a Mailtrap sandbox so no real customer gets a half-finished email (how).

    Place an order. Then check CraftForms → Email Logs. Each send appears with the transport used, success or failure, and the full body.


    Limits worth knowing

    • No “if empty, hide this” logic inside templates. A tag whose field was left empty simply disappears. Write labels so an empty value still reads fine (the All Form Data block already skips empty fields). If you need text that depends on an answer, create an exposed Conditional smart variable in the form (e.g. delivery_note: when zone == 'out-of-town', the value is “Deliveries outside the city arrive after 14:00.”) and put {{email.delivery_note}} in the email.
    • Send different emails for different answers with “Run only if”. Every submit action has a Run only if rule based on a choice field (dropdown, radio, checkboxes, colour). For example, send an extra “out-of-town delivery” template only when zone is Out of town.
    • No scheduled sends. The confirmation goes out at submission time. A “remind the customer the day before” email isn’t built in yet.
    • Templates compile on save. Edit → Update → then test. The preview shows the last saved version.

    FAQ

    Can I design a WordPress form email without writing HTML? Yes. CraftForms’ email templates are built in the WordPress block editor. You arrange blocks visually, and CraftForms compiles them into email-safe HTML with inline CSS when you save.

    Can I use AI to design my WordPress email template? Yes. Ask an AI model for WordPress block markup using only email-safe blocks, paste it into the template’s Code editor, and switch back to the visual editor to adjust it. Keep the {{merge.tags}} intact.

    How do I show the delivery date and card message in the confirmation email? Use merge tags built from your field names, e.g. {{email.delivery_date}} and {{email.card_message}} (or {{email.delivery_day}} for the readable date, see Step 1). The template’s Dynamic Data panel lists every tag for the form you select.

    Can the customer and the shop get different emails? Yes. Create two email templates (each has its own subject and recipient) and add two Send Email (Template) actions to the form.

    Do I need a paid plan? The email template builder and both emails are free, and so are most of the form’s fields. The delivery-date picker, the live price and taking payment by card are Pro.


    Try it

    1. CraftForms → Forms → Add New: build the order form with the field names above.
    2. CraftForms → Email Templates → Add New: paste the AI design (Code editor), adjust, set subject and recipient under Email settings, save.
    3. Repeat for the shop’s work order.
    4. On the form: Save Submission → Send Email (Template) ×2.
    5. Place a test order, then check your inbox and Email Logs.

    Starter included: in CraftForms → Getting Started, choose Take bookings and pick Flower Delivery Order. It creates the form, the shop’s work-order email and a Flower Delivery Days catalog item (Monday to Saturday from the next day, Sundays closed). The customer confirmation is in the email template starters as Florist Order Confirmation.

    Craftforms flowers order

    Related: Branded email templates + Mailtrap · Online order form for small business

  • One Form, Many Products: How to Reuse a CraftForms Form

    One Form, Many Products: How to Reuse a CraftForms Form


    TL;DR

    • The most common CraftForms modelling mistake is assuming one form = one thing you sell. It doesn’t. A form is a question set; a catalog item is the thing being sold. The relationship is one form to many items.
    • A two-room B&B needs one booking form and two booking catalog items. Both rooms ask the same questions (dates, guests, extras) — and because availability is tracked per catalog item, Room 1 selling out has zero effect on Room 2.
    • Same story for products: one “size / colour / quantity” form can back a dozen catalog items that each override the choices, the prices, and the formula — via Field Options, Smart Variables and Item Pricing.
    • Same story for WooCommerce: the form is selected per product, so any number of products can point at one form, each with its own option set, pricing, dynamic SKU and stock table.
    • The non-obvious part is how the link is made. It’s per placement, not per form: insert the form’s synced pattern, then pick Linked Catalog Item in the sidebar (or the block toolbar). Two placements of the same form on one page can link two different items.
    • The trap: the form selector inside a catalog item is labelled “Inspect a form for reference” — it is a schema helper, not a saved binding. A catalog item does not own a form.
    • Catalog is a Pro feature (CraftForms → Catalog).

    Cf one form many catalog items

    If you have built one CraftForms form per product, and you sell fifteen products, you have fourteen forms too many — and fifteen places to make the same edit when you want to add a “Gift wrapping?” checkbox.

    This is the single most useful structural idea in the plugin, and it is easy to miss because nothing forces you into it: a form is not a product. A form is a set of questions and the layout that asks them. What you actually sell — the room, the boat, the banner, the meeting room, the SKU — is a catalog item. One form can serve as the front end for as many catalog items as you like, and each item quietly rewrites the form’s options, variables and pricing when it’s the one being booked or bought.

    CraftForms says this in its own words, in the info panel at the top of every catalog item:

    As the store owner, you create catalog items here and link them to specific pages or form instances on your site. When a linked form is submitted, CraftForms automatically extends it — applying the item’s field options to matching form fields, injecting its variables into calculations, and overriding the form-level pricing. One form, any number of product or booking variations.

    This post is the practical version of that sentence: the model, three setups that come up constantly, and the linkage details that fail silently when you get them wrong.


    The model: question set vs. sellable thing

    Keep these two jobs separate in your head and the rest follows.

    The form owns the questions. Fields, labels, layout, validation, conditional logic, the submit actions (emails, PDFs, orders, webhooks), and a default price formula. Built once, in the block editor, like any other CraftForms form.

    The catalog item owns what makes this one different. Under CraftForms → Catalog each item carries its own:

    • Item Details — Title, Type (Product or Booking), Status (Active / Draft / Archived), calendar colour, description
    • Field Options — per-field choice overrides for select, radio and checkbox fields
    • Smart Variables — extra variables injected into this item’s calculations
    • Item Pricing — a price or formula that applies to this item only
    • Inventory — stock for product items
    • Booking + iCal Import / Export — availability, capacity and calendar sync, for booking items

    Nothing about that list duplicates the form. It is purely the delta between “the questions” and “this particular thing.”


    What a catalog item actually changes at submit

    Three overrides do nearly all the work.

    Field Options override or extend the choices on a select, radio or checkbox field. Each option group targets one field and picks a Strategy:

    • Override — the form’s original choices are discarded and replaced entirely.
    • Merge — choices with the same value are updated; new choices are appended.

    Each group also sets an Option Style (Default, Cards, Color, Image), and every value can carry its own Price, Annotation, Default flag, Description, Color and Image. There’s a Pre-fill choices helper to seed values either from the form field’s own choices or from a WooCommerce attribute.

    Smart Variables defined on the item are injected into the calculation alongside the form’s own.

    Item Pricing is the blunt instrument, and the panel describes itself precisely:

    Define a price or formula that applies to this catalog item only. When set, it overrides the form-level pricing for this item — a local override that leaves the form’s own formula untouched.

    That last clause is the point of the whole feature. The item’s price is a local override. The shared form keeps its own formula, so the other twelve items are unaffected.

    Field names are matched exactly, and a mismatch is silent. Option groups target a field by name; CraftForms warns, in the editor, “Enter the exact field name — non-matching names are silently skipped.” Rename a form field later and every catalog item referencing the old name stops overriding it — with no error, no notice, just the form’s original choices quietly showing through. Grep your catalog items after any field rename.

    Craftforms catalog item override options

    Case 1 — a two-room B&B: one form, two bookings that never collide

    This is the example that makes the model click, because the instinct is so strongly wrong. Two rooms feel like two booking systems. They aren’t. They’re two catalog items.

    Both rooms ask identical questions: check-in and check-out, how many guests, breakfast, airport pickup. So build one form — say “Room booking” — with a booking datepicker field named stay_dates, a guest-count number field, and your extras.

    Then, under CraftForms → Catalog, create two items of type Booking:

    Garden RoomAttic Suite
    Booking TypeDates Range (Hotel / B&B)Dates Range (Hotel / B&B)
    Occupancy ModelExclusiveExclusive
    Capacity11
    Min Duration (nights)21
    Date Field Variablestay_datesstay_dates
    Item Pricing€95/night€140/night

    Two things here surprise people.

    Both items use the same Date Field Variable — and that’s correct. stay_dates is the form’s datepicker field name, and both items track that same field. Its help text explains why the setting exists at all: “Name of the form datepicker field this catalog item should track for date/time availability. When a form has multiple datepickers, this tells the catalog item which one to use.” It disambiguates fields, not items. It is not an identity, so there is nothing to keep unique.

    The two rooms book completely independently. This isn’t a convention you have to maintain — it’s structural. Every availability ledger CraftForms keeps is keyed by the catalog item:

    • day-based bookings are unique per (resource_id, date)
    • slot bookings are unique per (resource_id, date, time_slot)
    • continuous-duration bookings are indexed per (resource_id, date) and overlap-checked within the item

    And the orders table stores the catalog item and the form as two separate columns. Fill the Garden Room for all of July and the Attic Suite’s calendar doesn’t change by a single day, because nothing in that chain is keyed by the form.

    You also get per-item iCal Import / Export, which matters more than it sounds: each room gets its own export feed and its own import URL. That’s exactly what you need to keep the Garden Room in sync with its Airbnb listing while the Attic Suite syncs with its own — one shared form, two independent calendars. (See the Google Calendar sync post for the feed mechanics.)

    Adding a third room is then: open the Garden Room, hit Duplicate, change the title, price and capacity. The form is never touched.

    Keep the booking type consistent across items that share one form. The form has one datepicker field with one shape. Two items that share it should use the same Booking Type — two “Dates Range” rooms, or two “Single Date With Time Slots” services. Mixing a day-range item and a time-slot item on a single shared datepicker field is not a tested path; give that second shape its own form.


    Case 2 — the same product in different sizes, colours and prices

    Now the product side. You sell wall banners. The questions never change: size, material, finish, quantity, artwork upload. What changes per product line is which options exist and what they cost.

    One form — “Banner configurator” — with size, material, finish, qty and a file upload. Then one catalog item per product line, each with Field Options that reshape the choices:

    • Indoor Banner — an Override group on material offering only Paper and Fabric, each with its own per-value price; finish overridden to Matte / Satin.
    • Outdoor Banner — material overridden to Vinyl and Mesh at higher per-value prices; a Merge group on finish that appends UV-laminated to the existing choices instead of replacing them.
    • Premium Banner — same choices as Outdoor, but an Item Pricing formula that applies a different rate, plus an extra Smart Variable for a rush-production surcharge.

    Use Override when the item’s list is authoritative and the form’s defaults are irrelevant. Use Merge when the form holds the master list and the item only adjusts a price or adds one extra choice — that keeps a global change (a new finish for everything) a one-place edit on the form.

    Set Option Style to Color or Image on the colour field and each item can ship its own swatches, so the same form renders a genuinely different-looking configurator per product line.


    Case 3 — many WooCommerce products, one form

    WooCommerce works the same way, and the connection lives on the product rather than in the Catalog screen. On a product’s CraftForms panel you get a toggle — “Connect a CraftForms form to this product” — and then a CraftForms Form selector, described as “The CraftForms form that replaces the default add-to-cart button on this product page.”

    Because that selection is per product, there is nothing stopping thirty products from selecting the same form. Each product then keeps its own:

    • Field Options — this product’s colours and sizes
    • Smart Variables and Item Pricing — this product’s rates
    • Inventory, plus a Dynamic SKU Expression and Custom Stock Management stock table — so stock is tracked per resolved option combination, per product

    That’s the WooCommerce payoff of the whole idea: one configurator form, thirty products, thirty independent price sheets and stock tables, and no variation matrix. (See the inventory-by-option and formula-pricing posts for those two panels in depth.)

    You can also render a product’s add-to-cart form anywhere with . The product must be simple, CraftForms-enabled and connected to that same form; otherwise the form still renders, just without add-to-cart, and logged-in editors get an HTML comment explaining why.


    How the link is actually made

    Here’s the part that isn’t obvious, and where most “my catalog item isn’t doing anything” confusion comes from. The link is per placement of the form, not a property of the form. There are three ways to make it.

    1. Per placement in the editor (the default)

    Insert the form on the page as its synced pattern — CraftForms auto-creates one per form, titled “CraftForms: “, and it appears in the editor’s Patterns tab. Select that pattern block and you get two equivalent controls:

    • a document sidebar panel, CraftForms Catalog, with a Linked Catalog Item dropdown (“Select a catalog item to extend this form with its pricing, options, and smart variables”)
    • a Catalog Item button right in the block toolbar, showing the currently linked item’s name so you can see and change it without opening the sidebar
    Craftforms catalog item booking appointment form connect synced pattern

    So: /rooms/garden-room gets the Room booking pattern linked to Garden Room; /rooms/attic-suite gets the same pattern linked to Attic Suite. One form, two pages, two items.

    The link is stored per placement, keyed by a stable per-placement ID written into the block comment — which means it survives reordering, and it means two placements of the same form on one page can link to two different catalog items. A single “Our rooms” page can list both rooms, each with its own working booking form. That’s the capability that makes the one-form model scale past one-item-per-page.

    This panel only appears for the synced-pattern placement. The per-placement link is read off the pattern wrapper, so a raw craftforms/form block dropped straight onto a page has nowhere to hang it — no sidebar panel, no toolbar button. Insert the pattern, don’t paste the form’s block markup into the page. (Pasting a copy also forks the form: later edits to the real form won’t reach it.) If you must use a bare block, fall back to the page-meta or shortcode routes below.

    2. The shortcode

    For classic templates, page builders and anywhere blocks aren’t practical:

    [craftforms form="42" catalog-item="7"]
    

    Note the hyphen — the attribute is catalog-item, not catalog_item, and form takes the form’s post ID. For the static/headless embed the equivalent is:

    [craftforms_static_form key="…" resource-id="7"]

    which also accepts catalog-item as an alias.


    The trap: “Inspect a form for reference” is not a binding

    Craftforms catalog item form by reference

    Open a catalog item and there’s a form dropdown near the top. It is labelled “Inspect a form for reference” — and that label is doing real work. Selecting a form there loads its schema so the editor can show you the field names, datepicker sub-variables and smart variables available for copy-paste while you write formulas and option groups.

    It is not a saved link. A catalog item does not own a form, and nothing at render time reads that selection. If you pick a form there and expect the item to start applying itself, nothing will happen. The binding is always one of the three mechanisms above.

    This is the right design for the one-to-many model — an item that owned a form couldn’t be reused across placements — but the dropdown’s position at the top of the panel makes it look like step one of a wizard. It isn’t.


    Already built one of these? Add items, not forms

    Every tutorial on this blog builds a single sellable thing, because one thing is the clearest way to teach a form. That’s a teaching choice, not a limit — and each of those builds scales the same way: keep the form exactly as it is, and add catalog items.

    • Salon, spa or massage appointments — that walkthrough sets up one service on one schedule. A real salon has several. Add a catalog item per treatment room, per stylist, or per service, all pointing at the same “Book an appointment” form: each item carries its own slot interval, buffer, notice period, capacity and price, and two of them can never be booked into the same slot. One form for the whole salon, however many chairs you add.
    • B&B and vacation rental bookings — builds one rental. Add one booking item per room, exactly as in Case 1 above: its own nightly rate, its own minimum stay, its own iCal feed.
    • Seasonal boat rental — one boat becomes a fleet. Duplicate the item per hull and adjust capacity and the month-keyed price table; the seasonal datepicker is built once and shared.
    • Online order form for a small business — one product becomes one catalog item per product line, each with its own Field Options and Item Pricing.
    • Inventory by option and pricing without variations — both panels are per product, so an entire WooCommerce catalog can share one configurator form while keeping separate stock tables and price rules.

    The rule of thumb: if the questions are the same, you need another catalog item, not another form.


    When you should build a second form

    Reuse isn’t free, and the honest boundary is about questions, not things:

    • Different questions → different form. A room booking and a bike rental don’t share a question set. Forcing both through one form with conditional logic is worse than two forms. Second thought: maybe if it is a one question (form field) that differs and you need just 2-3 products based on this form – try one form and conditional logic. Yet, always better to avoid this scenario. Make forms scalable from day one!
    • Different booking shape → different form. Per the note above, keep one datepicker shape per form.
    • Different submit actions → probably a different form. Submit actions live on the form. If one product needs a PDF contract and a CRM webhook while the other needs a simple confirmation email, that’s a form-level difference. (PDFs are the exception — catalog items have their own Attach PDF(s) panel.)
    • Same questions, different options/prices/availability → one form, many catalog items. This is the case the feature exists for, and it covers far more situations than people expect.

    The shape of it

    One form per question set, one catalog item per thing you sell, and the link made per placement.

    A two-room B&B: one form, two items, two pages — or one page with two placements. Fifteen banner products: one configurator, fifteen items. Thirty WooCommerce products: one form selected thirty times, each with its own prices and stock. When you add a field, you add it once.


  • How to Build a WordPress Form with AI — Locally, Then Ship It to Production

    How to Build a WordPress Form with AI — Locally, Then Ship It to Production

    There’s a version of “AI form building” that gets demoed a lot and used very little. You paste a key into a settings screen, type a sentence, and a form appears on your production site. It’s impressive for thirty seconds. Then you need a date picker, or a price that depends on three fields, or a repeater — and the textbox has nothing to offer, because the vendor only wired up the easy cases.

    There’s a second version that’s less flashy and considerably more useful: give a coding agent the full authoring specification for your form plugin, let it build the form on your local install, verify it, and export the result as a single file you import on production.

    That’s the workflow this article covers. It’s slower to set up and dramatically more capable, and it has a property the textbox version can never have: the agent never touches your live site. What reaches production is a file you reviewed.

    The idea in one paragraph

    01 local then ship

    CraftForms publishes its complete authoring specification — the AI Form Builder Guide. It’s about 12,000 words covering every block pattern, the form meta schema, conditional logic, template variables, and the WP-CLI commands to wire it all up. Hand that URL to an agent with shell access to a local WordPress install, describe the form you want, and it has everything it needs. When the form is built and verified, one button exports it to a portable file. Import that on production. Done.

    No API key. No AI feature in the plugin. The “AI” is whichever agent you already use.

    Why not just use the MCP abilities?

    Fair question, and if you’ve read our MCP guide you’ve seen the other path: WordPress 6.9’s Abilities API, exposed as MCP tools, letting an agent call craftforms/create-form and craftforms/add-field directly.

    That path is genuinely elegant, and it has a hard ceiling:

    MCP abilitiesGuide + local build
    Field types7 basic (text, email, number, textarea, select, radio, checkboxes)all of them — datepicker, file upload, repeater, range, color, payment, WooCommerce
    Editing an existing formno — append-onlyyes
    Pricing / smart variablesat create time onlyany time
    Conditional logicnoyes
    RequiresWP 6.9+, MCP Adapter pluginnothing beyond WP-CLI
    Touches productionyes, if that’s the site you connect it tono — local only
    02 mcp vs guide

    Use the abilities when you want an agent scaffolding simple intake forms in place. Use this workflow when you want a real form. In practice the two combine well: let the agent scaffold with create-form (which gets the post type and meta right with zero chance of error), then have it author the remaining blocks by hand from the guide.

    Step 1 — Give the agent the guide

    Whatever agent you’re using, the setup is the same: point it at the guide and give it shell access to a local install.

    In Claude Code, that’s one sentence:

    Read https://kb.craftformswp.com/documentation/for-developers/ai-form-builder-guide and follow it to build a CraftForms form on my local site. Ask me anything you need about the form before you start.

    Two things make this work better than it has any right to:

    The guide is written for machines. It’s not marketing documentation with a code sample at the end. It’s block-markup patterns, a meta schema reference, and a WP-CLI cheatsheet — the shape of thing an agent consumes well.

    It leads with the failure modes. The guide opens with sections marked CRITICAL, covering the ways to produce a form that looks fine and isn’t. An agent that reads them doesn’t make those mistakes. We’ll come back to these, because they’re the whole reason this workflow is reliable.

    Step 2 — Describe the form, not the markup

    The point of handing over a specification is that you get to talk about the form, not the implementation. A useful prompt is closer to a brief than a spec:

    A quote request form for a made-to-measure blinds company. Customer picks width and height in cm, a fabric from four options with different per-square-metre prices, and optional motorisation for a flat £85. Show the running total live as they change anything. Name and email required, phone optional. Email me the quote on submit.

    That’s a form with a price formula, a lookup table keyed on fabric, a conditional add-on, and a live-updating total — four things that are entirely outside what any prompt-to-form textbox will give you, and all of which are documented in the guide.

    The agent will do roughly this:

    1. wp post create --post_type=craftforms_form for the form post.
    2. Set _craftforms_form_meta with the formula, the fabric price table as a table transformation, and the email submit action.
    3. Author the field blocks into post_content following the guide’s patterns.
    4. Add an Infoblock bound to the form for the live total.

    Step 3 — The three CRITICAL rules (read these even if the agent did)

    This is the part worth understanding yourself, because it’s where a form silently breaks. All three are in the guide, and all three exist because WP-CLI bypasses the safety nets the editor and importer provide.

    Rule 1 — Forms are craftforms_form posts, never pages

    All form logic lives on the craftforms_form custom post type. A page never holds form markup directly. Saving a form auto-generates a synced pattern (CraftForms: <form title>), and the page holds only a reference to it — <!-- wp:block {"ref":PATTERN_ID} /--> — or the shortcode.

    Writing form block HTML straight into a page with --post_content produces something that renders and does nothing: the meta is detached from the block, so no formula, no validation, no submit actions.

    A related trap sits on the form post itself: its craftforms/form block needs "ref" set to the form’s own post ID. The editor adds it on first edit, so a form built entirely by CLI never gets one — and the form stays hidden on the page with no console error. The agent has to create the post, learn its ID, then write the ID back into the block. The guide covers this; it’s worth knowing why a CLI-built form can silently fail to appear.

    Rule 2 — Form meta must be an array, not a JSON string

    This is the one that catches everybody:

    # WRONG — stores a string. Frontend works. Editor shows no formula, no smart variables.
    wp post meta update 42 _craftforms_form_meta '{"formula":"width * height * price"}'
    
    # RIGHT
    wp post meta update 42 _craftforms_form_meta '{"formula":"width * height * price"}' --format=json
    

    Without --format=json, WordPress stores the raw string. The frontend happens to survive it — the submission controller decodes strings defensively — so the form appears to work. But the block editor reads the meta as an array and finds nothing, so the Price Formula and Smart Variables panels come up empty. You’d conclude the build failed when the data is right there, in the wrong shape.

    Rule 3 — Block HTML and block-comment attributes must agree

    Gutenberg validates blocks by regenerating the saved markup from the block comment’s JSON attributes and comparing. Any mismatch produces “This block contains unexpected or invalid content.”

    The canonical trap is a required message on a radio or checkbox group, which lives in two places at once:

    <!-- wp:craftforms/radio-field {"name":"fabric","required":true,"requiredMessage":"This field is required"} -->
    <div class="wp-block-craftforms-radio-field"
         data-validate-minselected="1"
         data-validate-minselected-message="This field is required">
    

    Set one and not the other, or set them to different strings, and the block fails validation. Any hand-written data-validate-*-message needs its matching block-comment attribute at the same value.

    Why this workflow is safe anyway: all three rules are checked by simply opening the form in the editor. A validation notice means rule 3. Empty formula panels mean rule 2. That’s a ten-second check, and it’s the reason building locally matters — you find these on your machine, not on a client’s site.

    03 export file anatomy

    Step 4 — Verify, then export

    Open the form in the block editor. No validation notice, formula and variables visible in the panels, fields render — that’s the build confirmed.

    Now, in the form editor sidebar, open the Export panel and click Export Form. You get form-{id}.craftform.html.

    Do not let the agent hand-write this file. Export it. The distinction is not stylistic — the export pipeline is a normalisation pass that fixes things:

    • It repairs string meta. If rule 2 was violated, export decodes the string and writes clean JSON into the file. The imported copy comes back as a proper array. Export literally heals the most common mistake in this workflow.
    • It strips the ref attribute from the form block. ref is the form’s own post ID and is meaningless on another install; import re-assigns it.
    • It embeds images as base64 in a <!--craftforms-assets--> block, so the file is self-contained and images sideload (deduplicated) on import.
    • It re-serialises from the database, so the file reflects what the editor actually read — not what you hoped you wrote.

    The result is one readable file:

    <!--
    Name: Blinds Quote Request
    Description: Exported from CraftForms
    Type: form
    Version: pro
    UUID: form_6a95425107d97
    CraftForms-Version: 1
    -->
    <!--craftforms-meta
    {
        "sendEmails": true,
        "createEntries": true,
        "formula": "width * height / 10000 * fabric_price + motorised",
        "transformations": [ ... ],
        "submitActions": [ ... ]
    }
    -->
    <!-- wp:craftforms/form {"layout":{"type":"constrained"}} -->
    ...
    
    04 symptom check

    Treat a clean export as your conformance check. If it exports and the file looks right, it will import.

    One portability gotcha: the Version line

    Version: is set by feature detection, not your licence. A form is tagged pro if it has a price formula, transformations, conditional-logic rules, a user-registration or create-post submit action, or a file-upload field. Otherwise free.

    A pro file is refused on import into a free install. So the blinds form above — which has a formula — is not portable to a free site no matter where it was exported from. Worth knowing before you promise a client a file.

    Step 5 — Import on production

    On the target site: CraftForms → Add New Form, then Import from file in the starter modal. Same UI that installs the bundled starters, because it’s the same format.

    The importer loads the blocks and the <!--craftforms-meta--> settings into the new form and sideloads any embedded images into the media library. Publish it, then place it on a page by inserting its synced pattern (CraftForms: Blinds Quote Request) or with the shortcode, and you’re live.

    The new form takes its name from the file, so there’s nothing to retype.

    To ship a new version later, don’t create another form. Open the existing form on production, find Starters & Import in the sidebar, click Choose starter or import, and import the new file. It replaces that form’s blocks and settings in place and keeps the same form, so every page that embeds it picks up the change as soon as you click Update.

    That’s what makes this a repeatable deployment path rather than a one-shot migration: iterate locally, re-export, import over the form on production.

    Why this beats the textbox

    Nothing proprietary in the chain. The guide is a public document. The agent is whichever one you already pay for. Swap models freely; the specification doesn’t care.

    The deliverable is a file. You can read it, diff it, commit it, code-review it, and roll it back. Compare that with an AI feature that mutates your production database and leaves you an undo button.

    No ceiling. Anything documented in the guide is in scope, which is everything the plugin does.

    It scales sideways. Ten department contact forms for one client, or the same intake form across fifteen sites, is one build and fifteen imports. Drop the file into the plugin’s starters/forms/ folder and it shows up as a starter in the UI — but that folder lives inside the plugin, so a plugin update replaces it. Keep your copies in version control.

    Production stays clean. The agent works on localhost. A human reviews a file. That’s a review gate no in-plugin AI feature gives you.

    The honest limits

    • You need a local WordPress install with WP-CLI, and an agent that can run shell commands. That sounds like more than it is. Claude Code, Cursor, and similar coding agents run shell commands out of the box, and most local dev tools (Local, DDEV, wp-env) include WP-CLI. If you already have a coding agent and a local site, you have the whole setup. If you don’t, the MCP path or the editor is a lower bar.
    • The agent still needs supervision. It won’t produce invalid markup if it follows the guide, but it can absolutely build the wrong form correctly. Read the brief back before you accept it.
    • Complex pricing deserves a real test. Submit the form once on local and confirm the total. CraftForms recalculates price server-side rather than trusting the browser, so a formula wrong in both places is still wrong.
    • Free/Pro portability is feature-detected, per the Version note above.
    • This is a build workflow, not a sync workflow. Importing over an existing form updates it in place, but there’s no continuous two-way sync between environments.

    Try it

    1. Local WordPress with CraftForms and WP-CLI.
    2. Tell your agent: read the AI Form Builder Guide and build me a form that does X.
    3. Open the form in the editor — no validation notice, formula visible.
    4. Export Form in the sidebar.
    5. Import the file on production.

    The interesting thing isn’t that an AI built a form. It’s that the output is a file you own, produced by an agent you chose, from a specification anyone can read.


    Related: How to Use MCP to Build and Fill WordPress Forms · AI Form Builder Guide (docs)


  • How to Use MCP to Build and Fill WordPress Forms (2026 Guide)

    How to Use MCP to Build and Fill WordPress Forms (2026 Guide)

    Most “AI form builder” features shipping in 2026 work the same way: you paste an API key into a settings screen, type a sentence into a textbox, and the plugin’s own server round-trips your prompt to a model and hands back a form. It works, sometimes. But the AI is a feature inside the plugin. It can’t be swapped, it can’t be scripted, and it can only do what the vendor exposed in that one textbox.

    WordPress 6.9 opened a different path. It shipped the Abilities API — a core registry where any plugin can declare typed, permission-checked operations. And in February 2026 the WordPress AI team shipped the MCP Adapter, which takes those registered abilities and exposes them as Model Context Protocol tools that any MCP client can discover and call.

    Put those two together and the picture inverts. Instead of an AI feature living inside your form plugin, your form plugin publishes capabilities that any agent — Claude, Cursor, a CI script, your own code — can pick up and use. No vendor key. No textbox.

    This guide walks the whole thing end to end: install the adapter, register a server, connect a client, and have an agent build a working form. Then the other half nobody covers — having an agent fill one.

    What each piece actually is

    Three layers, and it’s worth being precise about which does what, because the names get used interchangeably and they shouldn’t be.

    The Abilities API is WordPress core (6.9+). A plugin calls wp_register_ability() with a name, a JSON Schema for its input and output, a permission callback, and an execute callback. That’s it. It’s a registry — it knows nothing about AI.

    The MCP Adapter is a separate plugin from the WordPress project. It reads the abilities registry and republishes abilities as MCP tools over a real MCP transport. It is the bridge, and it’s the piece people mean when they say “WordPress has MCP now.”

    Your form plugin registers the abilities. CraftForms registers two:

    AbilityWhat it does
    craftforms/create-formCreates a form post with optional price formula, transformations, and submit actions
    craftforms/add-fieldAppends a field to an existing form as correctly structured Gutenberg blocks

    Both ship in CraftForms free. There’s no AI tier and no key to buy — the abilities register on any install running WordPress 6.9 or later, and are simply absent on older versions (the registration hooks never fire).

    The distinction that matters: CraftForms doesn’t talk to a model. It declares what it can do, in a schema any model can read. Which model you point at it is entirely your business.

    Step 1 — Install the MCP Adapter

    The adapter isn’t on the plugin repository yet; it’s distributed from its GitHub releases.

    wp plugin install https://github.com/WordPress/mcp-adapter/releases/latest/download/mcp-adapter.zip --activate
    

    Requirements are WordPress 6.9+ and PHP 7.4+. Activating it gives you a default server and a WP-CLI command. Check what it found:

    wp mcp-adapter list
    
    ID                            Name                        Version  Tools  Resources  Prompts
    mcp-adapter-default-server    MCP Adapter Default Server  v1.0.0   3      0          0
    

    Three tools — those are core’s own abilities (site info, user info, environment info). CraftForms’ abilities are registered, but the default server doesn’t expose them. That’s deliberate: the adapter makes you opt in to what an agent can reach.

    Step 2 — Register a server that exposes the form abilities

    Drop this in an mu-plugin (wp-content/mu-plugins/craftforms-mcp-server.php):

    <?php
    /**
     * Plugin Name: CraftForms MCP Server
     * Description: Exposes the CraftForms abilities as MCP tools via the WordPress MCP Adapter.
     */
    
    add_action( 'mcp_adapter_init', function ( $adapter ) {
        $adapter->create_server(
            'craftforms',                       // server id
            'craftforms-mcp',                   // REST namespace
            'mcp',                              // REST route
            'CraftForms MCP Server',            // name
            'Build and configure CraftForms forms from an AI agent.',
            '1.0.0',
            [ \WP\MCP\Transport\HttpTransport::class ],
            \WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
            null,
            [
                'craftforms/create-form',
                'craftforms/add-field',
            ]
        );
    } );
    

    The last argument is the allowlist. Only abilities named there become tools on this server — everything else on the site stays invisible to it. If you want an agent that can create forms but never add fields, remove a line.

    The namespace and route arguments determine the endpoint: craftforms-mcp + mcp gives you https://yoursite.com/wp-json/craftforms-mcp/mcp.

    Confirm it registered:

    wp mcp-adapter list
    
    ID           Name                    Version  Tools  Resources  Prompts
    craftforms   CraftForms MCP Server   1.0.0    2      0          0
    

    Two tools. That’s the pair.

    Step 3 — Look at what the agent will see

    Before wiring up a client, it’s worth seeing the tool definitions the way a model receives them. The adapter includes a STDIO transport you can drive from the command line:

    echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
      | wp mcp-adapter serve --user=admin --server=craftforms
    

    The response includes both tools with their full input schemas. Trimmed to the essentials:

    {
      "name": "craftforms-add-field",
      "title": "Add a Field to a Form",
      "description": "Appends a new input field block to an existing CraftForms form.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "form_id":    { "type": "integer" },
          "field_type": { "type": "string",
                          "enum": ["text","email","number","textarea",
                                   "select","radio","checkboxes"] },
          "name":       { "type": "string" },
          "label":      { "type": "string" },
          "required":   { "type": "boolean" },
          "options":    { "type": "array" }
        },
        "required": ["form_id","field_type","name","label"]
      }
    }
    

    Two details worth catching here, because both will bite you otherwise:

    Tool names are slugified. The ability craftforms/create-form becomes the MCP tool craftforms-create-form. Slashes aren’t legal in MCP tool names, so the adapter converts them. When you call a tool, use the hyphenated form.

    The enum is the contract. field_type accepts exactly seven values. A model that hallucinates "date" gets a validation error back rather than a broken form — which is the entire point of schema-typed abilities over prompt-and-hope generation.

    Step 4 — Connect a client over HTTP

    STDIO is good for testing. For a real client you want the HTTP transport, which is the endpoint the server registration created.

    Authentication is standard WordPress — an application password is the practical choice. Create one under Users → Profile → Application Passwords, or:

    wp user application-password create admin mcp-client --porcelain
    

    MCP over HTTP is a session protocol, so the first call is a handshake:

    curl -i -X POST "https://yoursite.com/wp-json/craftforms-mcp/mcp" \
      -u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
           "params":{"protocolVersion":"2025-06-18","capabilities":{},
                     "clientInfo":{"name":"curl","version":"1.0"}}}'
    

    The response headers carry the session:

    HTTP/2 200
    mcp-session-id: 3d75124b-3f55-49be-a28b-cadb7b0eb633
    
    {"jsonrpc":"2.0","id":1,"result":{
      "protocolVersion":"2025-06-18",
      "serverInfo":{"name":"CraftForms MCP Server","version":"1.0.0"},
      "instructions":"Build and configure CraftForms forms from an AI agent."}}
    

    Every subsequent request must carry Mcp-Session-Id. Miss it and you get -32600 Invalid Request: Missing Mcp-Session-Id header, which is the single most common first-run stumble.

    A real MCP client handles all of this for you. In Claude Code, for example, you’d add the server once and never think about sessions again:

    claude mcp add --transport http craftforms \
      https://yoursite.com/wp-json/craftforms-mcp/mcp \
      --header "Authorization: Basic $(printf 'admin:xxxx xxxx xxxx' | base64)"
    

    Step 5 — Let the agent build the form

    Now the payoff. With the server connected, you ask in plain language:

    “Build me a quote request form with the customer’s name, email, a service dropdown for Basic / Pro / Enterprise, and a project details box. Name and email required.”

    The agent calls craftforms-create-form first:

    {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
      "name":"craftforms-create-form",
      "arguments":{"title":"Quote Request","description":"Service quote request"}}}
    
    {"jsonrpc":"2.0","id":1,"result":{
      "structuredContent":{
        "form_id":932,
        "uuid":"form_6a953ab5dc2c6",
        "edit_url":"https://yoursite.com/wp-admin/post.php?post=932&action=edit"},
      "isError":false}}
    

    Then one craftforms-add-field call per field, threading form_id: 932 through each. Four calls later the form exists, is published, and opens in the block editor at that edit_url.

    Note the output schema is doing real work here. The agent gets back a structured form_id and edit_url, not prose it has to parse. That’s what lets it chain calls reliably instead of guessing.

    Why this beats “paste a prompt into a textbox”

    Here’s what the agent actually wrote into the post, for the email field:

    <!-- wp:craftforms/text-input-field {"name":"email","label":"Email Address","required":true,"type":"email"} -->
    <!-- wp:craftforms/label {"content":"Email Address"} /-->
    
    <!-- wp:craftforms/text-input {"name":"email","required":true,"type":"email","fontSize":"medium","style":{"spacing":{"padding":{"top":"var:preset|spacing|20","bottom":"var:preset|spacing|20","left":"var:preset|spacing|20","right":"var:preset|spacing|20"}}}} /-->
    
    <!-- wp:craftforms/form-error {"content":"{{error.email}}","style":{"typography":{"fontSize":"0.8em"}}} /-->
    <!-- /wp:craftforms/text-input-field -->
    

    That’s correct Gutenberg block markup, with matched attributes, the right nesting, a bound error block, and theme spacing presets. The model didn’t generate it — the ability did. The model supplied four values (name, label, required, field_type); PHP built the markup.

    This is the structural difference, and it’s the whole argument:

    • A prompt-to-HTML AI builder asks a language model to produce block markup. Sometimes it’s valid. Sometimes you get “This block contains unexpected or invalid content” in the editor, because a single mismatched attribute between the comment JSON and the saved HTML fails Gutenberg’s block validation.
    • An ability takes typed arguments and generates the markup deterministically, in code that was tested. The model can only get the intent wrong, never the syntax.

    The same holds for everything the ability handles on your behalf: required-field validation, aria-describedby wiring, role="group" on option sets, unique field names. You get accessibility and validation correct by default because a human wrote that part once, not because a model remembered to.

    And because permission callbacks are part of the ability definition, the agent inherits WordPress’s own access rules. craftforms/create-form checks the CraftForms access capability; craftforms/add-field checks edit_post against the specific form. An agent authenticated as a Subscriber can enumerate the tools and accomplish nothing with them.

    The other half: filling a form with an agent

    “Build a form” is where every competitor article stops. But the more interesting automation is the reverse — an agent that submits to your form. Order intake from an email parser, a nightly job that pushes leads from another system, an internal assistant that files a request on someone’s behalf.

    CraftForms exposes submission as a plain REST endpoint:

    POST /wp-json/craftforms/v1/submit/{form_uuid}
    

    The {form_uuid} is the value create-form handed back (form_6a953ab5dc2c6 above). Field values go at the top level of the JSON body — not nested under a data or formData key, which is the mistake everyone makes first:

    curl -X POST "https://yoursite.com/wp-json/craftforms/v1/submit/form_6a953ab5dc2c6" \
      -H "Content-Type: application/json" \
      -d '{
            "full_name": "Ada Lovelace",
            "email":     "[email protected]",
            "service":   "pro",
            "details":   "Need a quote for Q4"
          }'
    
    {"success":true,"data":{"successMsg":"We have received your message. Thank you!"}}
    

    A real submission: it runs server-side validation, recalculates any price formula rather than trusting the payload, saves the entry, and fires the form’s submit actions — emails, webhooks, PDF generation, whatever’s configured. It’s the same code path a browser submit takes.

    The security model you need to understand first

    That request only succeeded because the form was explicitly configured to allow it. The endpoint has a gate, and it’s worth walking through because it’s the difference between “agent-fillable” and “open spam relay.”

    CraftForms classifies every submission by Origin/Referer:

    Same-origin submissions (a browser on your own site) require a valid nonce. An agent driving a real browser gets this for free — the nonce is in the rendered page. An agent making a bare HTTP call from your own domain without one gets Invalid security token.

    Off-site submissions — which is what an agent, a script, or a static site is making — are rejected by default. To allow them you must:

    1. Be on CraftForms Pro. Without a licence: External submissions require a PRO licence.
    2. Enable external submissions on that specific form. It’s per-form, not global. Without it: External submissions are not enabled for this form.
    3. Optionally, add required header rules — key/value pairs the caller must send. This is a shared secret, so an agent can submit and a random script that discovered the UUID cannot.

    That third step is the one to actually use. A form UUID isn’t a secret — it’s in the page markup. If you’re going to let machines post to a form, put a header behind it:

    curl -X POST "https://yoursite.com/wp-json/craftforms/v1/submit/form_6a953ab5dc2c6" \
      -H "Content-Type: application/json" \
      -H "X-Agent-Token: your-shared-secret" \
      -d '{"full_name":"Ada Lovelace","email":"[email protected]","service":"pro"}'
    

    Be honest with yourself about what this is: shared-secret authentication plus origin checks. It’s appropriate for an agent you control calling a form you control. It is not a substitute for a captcha on a public contact form, and CraftForms doesn’t pretend otherwise.

    Where the ceiling is

    Two abilities is a deliberate starting point, not a finished surface. Worth knowing what falls outside it before you plan around it:

    • Seven field types. text, email, number, textarea, select, radio, checkboxes. The agent can’t add a date picker, file upload, payment block, or booking calendar. Those exist in CraftForms — you add them in the editor.
    • No editing or deleting. add-field appends. There’s no ability to reorder, modify, or remove a field. An agent can build up; it can’t refactor.
    • Pricing is create-time only. create-form accepts a formula and transformations, so an agent can scaffold a calculating form — but only in the initial call. There’s no ability to adjust pricing afterwards.
    • The adapter is young. MCP Adapter 0.6.1 at the time of writing, distributed from GitHub rather than the plugin repository. Treat it as a capable pre-1.0 tool, not a set-and-forget production dependency.

    The honest framing: this gets an agent from nothing to a real, valid, publishable form in a handful of tool calls. Then you open the editor and finish it. For scaffolding — a dozen similar intake forms, a client’s ten department contact forms — that’s a genuinely large chunk of the work gone. For a complex booking or product configurator, it’s the first five minutes.

    If you hit that ceiling, there’s a second path. CraftForms publishes its full authoring specification as the AI Form Builder Guide. Hand that to a coding agent with shell access to a local install and it can build anything the plugin does — date pickers, repeaters, conditional logic, WooCommerce — then export the result as a single file you import on production. Slower to set up, no ceiling, and the agent never touches your live site.

    Running WordPress older than 6.9?

    The abilities simply don’t register — the core hooks they attach to don’t exist, and CraftForms checks for wp_register_ability() before doing anything. No error, no MCP.

    The fallback is WP-CLI. Forms are a custom post type with block content and a meta array, so an agent with shell access can scaffold one with wp post create and wp post meta update. It’s a lot more finicky — you’re hand-writing the block markup the ability would have generated, and getting a block-comment attribute out of step with the saved HTML is exactly the validation failure the ability exists to prevent. Upgrade to 6.9+ if you can.

    Try it

    Everything above runs on a stock install:

    1. WordPress 6.9+ with CraftForms (free) — the abilities register themselves.
    2. wp plugin install https://github.com/WordPress/mcp-adapter/releases/latest/download/mcp-adapter.zip --activate
    3. Drop in the mu-plugin from Step 2.
    4. wp mcp-adapter list to confirm two tools.
    5. Point your client at https://yoursite.com/wp-json/craftforms-mcp/mcp with an application password.
    6. Ask for a form.

    The interesting part isn’t that an AI made a form. It’s that nothing in that chain is proprietary to CraftForms — core registry, core-project adapter, open protocol. Any plugin can register abilities. Any client can call them. That’s a considerably better bet than an API key field in a settings screen.


    Related: Answer-Engine Optimization for Form & Quote Pages · How to Use WordPress as a Form Backend for Static Sites and Web Apps · Use WordPress as a Locked-Down Form Backend


  • AEO for Form & Quote Pages: How to Get Your Prices Into AI Answers

    AEO for Form & Quote Pages: How to Get Your Prices Into AI Answers

    More and more buyers never reach your pricing page the old way. They ask ChatGPT, Perplexity, Google’s AI Overviews, Claude, or Gemini a question — “how much does a custom roller banner cost?”, “who does made-to-measure shutters and what’s the price range?” — and act on the answer they’re given. If your price lives only inside an interactive calculator or behind a “request a quote” form, none of those engines can see it. You’ve optimised for humans who arrive at the page, and made yourself invisible to the layer that increasingly decides whether they arrive at all.

    This is what Answer Engine Optimization (AEO) fixes. And pages built around a form, calculator, or quote request are exactly the pages that get it wrong most often.

    SEO, GEO, AEO — the one-line version

    • SEO gets you ranked in a list of blue links.
    • GEO (Generative Engine Optimization) is about being used as a source by generative search.
    • AEO is the practical core of both: structuring your content so an engine can extract a clean, correct, self-contained answer and attribute it to you.

    For a pricing or quote page, the “answer” the engine wants is simple: what does this cost, and how do I get it? Your job is to make that answer readable by a machine that will never click a dropdown.

    The problem: your calculator is a black box to AI

    A live price calculator is a genuinely great experience for a human. They pick a size, choose a material, set a quantity, and watch the total update. But think about what an answer engine’s crawler actually receives when it reads that page:

    • It cannot select “2m × 1m”, so it never triggers the price.
    • It often cannot run the JavaScript that computes the number at all.
    • It sees a form, some empty fields, and — if you’re not careful — no price anywhere in the text.

    So when someone asks an AI “how much is a 2-metre banner from [you]?”, the engine has nothing to quote. It either skips you or, worse, guesses. The interactive tool that converts so well for on-page visitors is a dead end for the audience deciding which businesses to even mention.

    The fix is not to remove the calculator. It’s to stop making the calculator the only place the answer exists.

    The core principle: one page, two audiences

    The interactive form is for the human. A crawlable, plain-language version of the same answer is for the engine. A well-optimised page serves both, on the same URL.

    Everything below is a way to add the machine-readable answer around your form without hurting the human experience.

    Tactic 1 — Put representative prices in actual text

    Even if the exact price is configurable, you almost always have a defensible, crawlable version of it:

    • “Starting from” pricing — “Roller banners start at £45 for the 850mm × 2000mm size.”
    • Tiers or ranges — “Most orders fall between £45 and £180 depending on size, finish, and quantity.”
    • Worked examples — “A popular configuration — 1m × 2m, premium film, qty 1 — comes to about £89.”

    Put this as real body text near the form, in a heading-led section like “How much does it cost?”. This single change is the highest-leverage thing you can do: it gives the engine a number to attribute to you, and it reassures the human before they start configuring.

    Because a CraftForms form is built from native Gutenberg blocks, you add this pricing summary the same way you’d add any paragraph or heading — right above or beside the form, in the same editor. There’s no separate “form app” boxing the content out of the page.

    Tactic 2 — Add a real FAQ next to the form

    Answer engines love self-contained question/answer pairs, because that’s the shape of the thing they’re trying to return. A short FAQ under your quote form does double duty — it deflects the same questions your sales inbox gets, and it feeds the engine clean Q&A:

    • “How is the price calculated?”
    • “Do you offer bulk / trade pricing?”
    • “What’s the turnaround time?”
    • “Can I get a fixed quote before ordering?”

    Write the answer in the first sentence, plainly, then elaborate. Don’t bury the answer in a paragraph of preamble — the engine (and the reader) wants it up front.

    To make that FAQ machine-readable as an answer, pair it with FAQPage structured data. CraftForms doesn’t emit schema itself — you generate it with your SEO plugin (Yoast, Rank Math, SEOPress all have an FAQ block/feature) or the core FAQ pattern. The point is that it sits on the same page as the form, and because the form is native content, nothing about the form stops the schema from applying to the page.

    Tactic 3 — Describe the offer with Product / Offer / HowTo schema

    Beyond FAQ, two more schema types help a pricing/quote page get quoted:

    • Product + Offer — for a configurable product, mark up the base offer (lowPrice / priceRange, currency, availability). This gives generative engines a structured price to cite.
    • HowTo — if your page walks the buyer through configuring and ordering, a HowTo outline mirrors the steps an engine likes to summarise.

    Again: this markup comes from your SEO/schema tooling, not from CraftForms. CraftForms’ contribution is that the page is normal WordPress content — so any schema block or SEO plugin treats it like any other post, with none of the “the form is a separate embedded app” problems that break markup on other builders.

    Tactic 4 — Keep the page fast and server-rendered

    Answer engines favour pages that are quick to fetch and whose meaning is present in the delivered HTML, not assembled later by heavy scripts. This is where the architecture of your form matters:

    • Native Gutenberg, not a bolted-on widget. CraftForms forms are built from core block infrastructure that’s already on the page, so a form/quote page doesn’t drag in a multi-hundred-kilobyte third-party configurator bundle. Lighter page, better Core Web Vitals, faster crawl.
    • Don’t gate the answer behind JS-only interaction. The live total is computed in the browser for responsiveness (and re-validated server-side at submit, so the charged price is always correct) — but the representative pricing, the FAQ, and the description from the tactics above are plain server-rendered content that any crawler gets on first load.
    • The Info block for on-page context. Use it to show the live figure to humans; use body text (Tactic 1) to state the crawlable figure for engines. Same page, both covered.

    The recurring complaint about configurators — “the one I tried completely killed my page speed” — is an AEO problem too: a slow, script-heavy page is both a worse human experience and a worse candidate for AI answers.

    Tactic 5 — Structure the page so it’s easy to extract

    • One clear <h1> stating what the page is (“Custom Roller Banner Pricing & Instant Quote”).
    • Descriptive sub-headings that match real questions (“How much does it cost?”, “Get an instant quote”, “Bulk & trade pricing”).
    • Short paragraphs, one idea each. Lists for tiers and options.
    • The plain-language answer above the interactive tool, not below it.

    An engine skims structure first. If your headings are the questions people ask, you’ve done half its work for it.

    Putting it together in CraftForms

    A worked flow for a “custom product quote” page, entirely in the block editor:

    1. Heading + intro — <h1> and a two-line summary of what you make and roughly what it costs.
    2. “How much does it cost?” section — representative “from / range” prices in text (Tactic 1).
    3. The CraftForms quote form — options, quantity, and an Info block showing the live total as the visitor configures. Server-side price validation keeps the real charge honest.
    4. A short FAQ — 4–6 real questions, answer-first (Tactic 2).
    5. Schema — add FAQPage (and Product/Offer if relevant) via your SEO plugin.
    6. Ship it fast — because it’s all native blocks, the page stays light and fully crawlable.

    If you run a “Request a Quote” style page (no price shown, enquiry only), Tactic 1 matters even more: give the engine a range even when the exact figure is quote-only, or you’re invisible for every “how much does X cost” question in your category.

    How to tell if it’s working

    • Ask the engines directly. Query ChatGPT, Perplexity, Google AI Overviews, and Gemini with your real buyer questions and see whether you’re mentioned — and whether the price they quote is yours and correct.
    • Watch referral traffic from AI sources in analytics (they show up as their own referrers and are growing).
    • Re-test after each change; AEO is iterative, and engines re-crawl on their own schedule.

    The honest caveats

    • AEO doesn’t replace SEO — it’s the layer on top. You still need the page to be indexable and useful.
    • Schema is a signal, not a guarantee. Marking up a price doesn’t force an engine to quote it, and engines can still paraphrase or get it wrong. Keep the plain-text answer accurate as your first line of defence.
    • CraftForms is the form + speed + crawlable-content half of this. The structured-data half is your SEO plugin’s job — this article is deliberately clear about which is which.
    • Don’t fake precision. If pricing genuinely varies, publish an honest range rather than a fake exact number an engine might quote back at a customer.

  • Headless WordPress + a Static Frontend, Explained (and Where Forms & Prices Fit)

    Headless WordPress + a Static Frontend, Explained (and Where Forms & Prices Fit)

    If you’ve ever described your setup as “headless WordPress with a static frontend” and watched another developer’s eyes narrow, you know this conversation:

    “So you’re headless? Or is it just a frontend CMS? How does your static site actually get the data — is it prebuilt, or is it making API requests? I’m not sure what ‘the data comes from a CMS and I prebuild everything’ even means.”

    Those are good questions, and the confusion is common. The terms get used loosely, and “headless” hides a couple of very different data-flow choices underneath it. This post untangles them — and then shows where forms and live prices fit, which is the part most explainers skip.


    Coupled vs headless: two jobs, one tool or two

    A traditional WordPress site does two jobs at once:

    1. Stores and manages content — posts, pages, products, media, users.
    2. Renders the pages — a theme + PHP turn that content into HTML on every request.

    That’s the coupled model. It’s what people sometimes loosely call a “frontend CMS”: WordPress is both the database and the thing your visitors’ browsers talk to.

    Headless splits those two jobs apart. WordPress keeps job #1 — it’s your content and data store, reachable over its REST or GraphQL API — and something else entirely does job #2. A separate frontend (Astro, Hugo, Next.js, plain HTML) pulls the content and renders the pages. Visitors never touch WordPress directly; they load a fast static site, and WordPress sits quietly in the back as the source of truth.

    So “headless” just means: WordPress is the backend, a separate frontend is the face.


    How does the frontend get the data? Two models — usually a hybrid

    This is the part that trips people up, because “headless” doesn’t answer it by itself. There are two ways a decoupled frontend can get its data, and most real sites use both.

    1. Prebuilt at build time (static generation). Your generator (Astro, Hugo, Next in SSG mode) calls the WordPress API once, at build time, pulls all the content, and bakes it into finished HTML pages. Those pages are then served as flat files from a CDN — no WordPress, no database, no PHP in the request path. This is what “I’m prebuilding pages for everything” means. It’s blisteringly fast and dirt cheap to host. The trade-off: the pages are a snapshot. Content changes in WordPress don’t appear until you rebuild.

    You don’t need a separate framework to land in this same model, either — WordPress can do its own building. Builderius builds the site visually, inside Gutenberg, and exports it straight to static HTML/CSS/JS, so WordPress is the CMS and the generator at once (that’s the stack behind the Builderius + CraftForms walkthrough linked below). Simply Static comes at it from the other direction: point it at a WordPress site you already have, running whatever theme, and it crawls every page and writes out the flat-file equivalent. Both land in exactly the same place as Astro/Hugo/Next in SSG mode — content baked in at build/export time — just with WordPress doing the rendering itself instead of an external generator calling its API.

    2. Runtime API requests. The page loads, and then JavaScript in the browser fetches live data from an API and fills part of the page in. This is for the bits that must be current — the parts you can’t afford to have go stale between builds.

    For a real-world example of this model taken to its full extent — not a dynamic island on an otherwise static page, but an entire frontend built this way — see setting up WordPress as a scalable headless CMS for a knowledge-base project: one WordPress backend exposes custom REST endpoints (a /sitemap/ listing and a /get_item/ content lookup) that five separately branded React frontends call live, rather than baking content into each at build time. It’s a good illustration of why you’d reach for pure runtime fetching instead of the hybrid below: one CMS, several frontends, content that has to stay current across all of them without rebuilding five sites every time something changes.

    Almost every good headless site is a hybrid: the bulk of the page is prebuilt and static (fast, cheap, cached at the edge), with a few dynamic islands that fetch live data at runtime. You don’t choose one model for the whole site — you choose per piece of content, based on how fresh it needs to be.

    That reframing is the whole game: what actually has to be live, and what can be frozen? For most sites, the answer is “95% can be frozen.” The interesting question is what to do with the other 5%.


    Where forms and prices fit

    A form — especially a product configurator with a live price — is the textbook dynamic island. It’s the exact 5% that can’t be baked into a static file:

    • A contact or booking form needs to submit somewhere and store the result. A flat HTML page has no server to receive it.
    • A product price that depends on the options a customer picks has to be computed as they pick, and it has to reflect current pricing — not whatever the price was the last time you rebuilt.
    • A checkout or payment step needs a server that can be trusted with the final number — the browser can display a total, but it can’t be the thing that charges a card or confirms a booking slot is still free.

    “Form” ends up covering a wide range here — a two-field contact form and a multi-step product configurator with live pricing and payment are the same shape of dynamic island, just with more happening inside it.

    This is precisely what CraftForms is built for in a headless setup. The shape looks like this:

    • The page is prebuilt and static. Title, images, copy, specs — all frozen, all served from the edge, instant.
    • The form is a runtime island. You drop a tiny placeholder into the static page. On load, a small script (embed.js) fetches the live form from your WordPress backend (GET /craftforms/v1/embed/{key}) and boots it in place. Because it’s fetched fresh every visit, editing fields or prices in WordPress needs no rebuild.
    • The price computes in the browser. As the customer changes options, the price recalculates instantly — locally, with no network call.
    • The backend is touched only on submit. When they finally submit, the request goes to WordPress, which re-verifies everything and stores it.

    Nothing about the form lives in the static build, so the frontend stays a pile of flat files and the form still behaves like a real, live application.


    Why it’s always fast

    Here’s the framing that makes this click — and it’s the reason developers keep arriving at exactly this setup once they see it work:

    There are no backend calls while the customer is using the form. They open a product page (already instant — it’s static). They toggle options, change quantities, add extras — and the price updates on every change with zero round-trips, because the math runs in their browser. The only time the backend is hit is the final submit.

    Compare that to a server-rendered configurator, where every option change is a request to the server, a database read, a recompute, and a response — a little spinner on every click, and a page that’s only as fast as your slowest query and the user’s latency. In the static + runtime-island shape, the slow part (the server) is out of the interaction loop entirely. It does almost nothing per pageview: it answers one embed request when the form loads, and one submission when the customer is done.

    Static page + client-side price + submit-only backend = a configurator that feels instant, on hosting that costs almost nothing.


    The dual price engine (how the price stays honest)

    “Compute the price in the browser” should set off an alarm: anything in the browser can be edited. If the price is only calculated client-side, a user can open devtools and submit whatever total they like.

    CraftForms runs the price formula on both sides:

    • Client-side, in JavaScript (a port of Symfony’s Expression Language), for the instant feedback that makes a static page feel live.
    • Server-side, in a PHP mirror of the same pipeline (class-formula-evaluator.php), which re-computes the total on submit from the form’s own formulas and current option prices. If the submitted price doesn’t match, the submission is rejected.

    So the browser side is for feel and the server side is for trust. The customer gets an instant price; you get a total you can actually charge, because the number that matters is recomputed where the user can’t touch it.


    An honest word on abuse

    Because the submission endpoint is public (that’s what lets a static site reach it), it’s worth being straight about the current protections:

    • Embed keys are bound to a domain — the backend checks the request Origin and refuses others.
    • Server-side re-verification (above) means price, stock, and booking capacity can’t be tampered with.
    • You can require a custom request header on the form as a lightweight shared-secret spam gate.
    • On the WordPress side, a one-click Headless backend mode toggle (CraftForms → Settings → Embed) closes everything but the CraftForms endpoints — public pages, the REST API, XML-RPC — so the install behaves like an API, not a website.

    (One small implementation note if you go build this: the embed key encodes your WordPress URL so embed.js can find your backend on its own, but the framework starters still pass the WP URL explicitly in config — so you’ll set it in one obvious place, not dig it out of the key.)


    Where to go next

    That’s the concept. If you want the stack-specific walkthroughs:

    The pattern is the same across all of them: keep the frontend static and fast, treat the form as a live island, compute prices in the browser for feel and on the server for trust, and touch the backend only when there’s actually something to submit.

    CraftForms embedding and external submissions are CraftForms PRO features.

  • When You Outgrow Hosted Form Endpoints (Formspree, Netlify Forms, Web3Forms, and Friends)

    When You Outgrow Hosted Form Endpoints (Formspree, Netlify Forms, Web3Forms, and Friends)

    If you build static sites, you’ve used one: a hosted form endpoint. You paste a form action, the service catches the submission and emails it to you, and you move on. Formspree, Netlify Forms, Basin, Getform, Web3Forms, staticforms.dev — they’re all variations on the same convenient idea, and for a single contact form they’re genuinely great.

    Then the site grows up. You add a second form, then a fifth. You need a field that only shows when another is checked. You need a file upload. You need a confirmation email that looks like your brand. You hit a monthly submission cap. You realize your submission data — sometimes including personal details — lives on a service you don’t control. Or the ask stops being “collect some fields” entirely: you need a product configurator with a live price, a booking form that checks availability, or a form that actually takes payment. A hosted endpoint was never built for any of that — it can catch a submission, not run a product. At some point “convenient” quietly became “limiting.”

    This post is about recognizing that moment, and what to do when you reach it.


    What hosted endpoints are great at

    Let’s be fair. For the right job they’re excellent:

    • Zero backend. Nothing to host, patch, or monitor.
    • Minutes to set up. Paste a URL, done.
    • Cheap or free at low volume.

    If you have one contact form and light traffic, stop reading and use one. Really.


    Where they start to hurt

    The limits are structural, not bugs — they come from being a generic catch-all:

    NeedTypical hosted endpointA CraftForms backend
    Many formsEach is a separate config / projectAny number, one install
    Submission volumeMonthly caps (e.g. free tiers around 50–250/mo)No caps — it’s your database
    Spam protectionBasic; some tiers have no CAPTCHA at allOrigin-bound + header gate (no CAPTCHA yet either — see below)
    Conditional logicNone — it’s a dumb receiverField-to-field conditions, AND/OR
    Pricing / calculationsNoneFormulas, tiered lookups, price matrices
    Product configuratorsNoneOptions, live price, file uploads, add-to-cart
    BookingsNoneAvailability, capacity, iCal sync
    File uploadsLimited or paid-tier onlyBuilt in, server-validated
    Branded emailsMinimal templatingVisual template builder
    PaymentsNot their jobStripe (hosted or embedded), PayPal
    Data ownershipStored on their infrastructureYour WordPress database

    None of these matter for a contact form. All of them matter for registrations, bookings, quotes, orders, and configurators — and that last group is the point: the same engine that replaces a hosted endpoint also replaces a separate “product configurator” plugin or a payment page, because it was never just a form tool.


    The alternative: a backend you own

    You don’t have to choose between “hosted endpoint” and “rewrite everything as a fullstack app.” There’s a middle path that keeps your static frontend exactly as it is: run one locked-down WordPress install as a private form backend, with CraftForms as the engine.

    • One backend, unlimited forms, all in one place.
    • No submission caps — it’s your database.
    • Conditional logic, calculations, file uploads, branded email, Stripe payments — built in.
    • Product configurators and bookings, not just forms — live pricing, stock per option, availability calendars, WooCommerce add-to-cart — the same engine, not a bolt-on plugin.
    • Your data on your server — nothing sits with a third party.
    • A near-zero attack surface — the WordPress install has one plugin, no public pages, no XML-RPC, and a hidden login. It isn’t a website; it’s an API.

    Your static site connects one of two ways: a direct fetch on submit, or CraftForms’ embed — a <div> + one script that pulls the live form from the backend and renders it in place. Both hit the same REST endpoint; everything else on WordPress is dark. It’s worth internalizing that “form” here covers a lot of ground: the exact same mechanism runs a two-field contact form, a multi-step booking with a deposit, and a product configurator with live pricing and a checkout.


    Isn’t running WordPress the thing we were trying to avoid?

    The objection is fair, so be precise about what’s involved. This is not a normal WordPress site you babysit. It has no theme, no public frontend, no page builder, no pile of plugins — the usual sources of WordPress pain and CVEs simply aren’t installed. What remains is a submission API. Locking it down to that is a single toggle — Headless backend mode in CraftForms → Settings → Embed blacks out the public front-end, closes the REST API to everything but the CraftForms endpoints, and disables XML-RPC, all in one click, reversible any time. For the pieces a plugin can’t do from inside WordPress — hiding the login page, server-level rules — the locked-down backend guide has the extra mu-plugin snippet and server config. Set it once.

    Craftforms headless mode setting

    And be honest about the trade-off in both directions: CraftForms doesn’t yet ship a built-in CAPTCHA or honeypot either. What it gives you today is origin-bound submissions (the form only accepts posts from your domain), server-side re-verification of anything that matters (prices, stock, capacity — a tampered value is rejected), and required request headers as a shared-secret spam gate. For most sites that’s a stronger position than an emailed endpoint with no logic at all; if you need heavy bot mitigation right now, weigh that.


    A rule of thumb

    • One contact form, low volume, no logic? Use a hosted endpoint. It’s the right tool.
    • Many forms, real logic, file uploads, pricing, payments, or data you must own? You’ve outgrown the endpoint. Move the backend in-house — a locked-down WordPress + CraftForms — and keep your static frontend exactly as fast as it is today.

    The static site was never the problem. The backend was. Pick one you don’t outgrow.

    See it built: Add a Real Form Backend to Your Astro Site and Add Forms to a Hugo Site With a WordPress Backend walk through this exact move for those two stacks. If what you actually need is a configurator rather than a contact form, Complex WooCommerce Pricing Without Hundreds of Variations and Track WooCommerce Stock by Option — No Variations show the same formula/stock engine at work.

    CraftForms embedding, external submissions, calculations, and payments are CraftForms PRO features.

  • Add Forms to a Hugo Site With a WordPress Backend

    Add Forms to a Hugo Site With a WordPress Backend

    Hugo builds sites at a speed that makes everything else feel slow. It also, by design, has no idea what to do with a form submission — it’s a static generator, and there’s no server on the other end. The moment your Hugo site needs a contact form, a booking, or a quote request, you’re shopping for a backend.

    You can point that form at a hosted endpoint and hope you don’t outgrow its limits. Or you can use a backend you fully control: a locked-down WordPress install running CraftForms, serving forms to your Hugo site and handling every submission. Same fast Hugo frontend, a real form engine behind it.

    For the full architecture and how to lock the WordPress side down, see Use WordPress as a Locked-Down Form Backend for Static Sites. This post is the Hugo integration.


    How it fits together

    Build the form once in WordPress. On your Hugo pages, use a shortcode (in content) or a partial (in templates) that outputs a small placeholder. At runtime embed.js fetches the live form from your backend and renders it. Submissions post back to WordPress, which validates, stores, and routes them.

    The form isn’t baked into your Hugo build, so editing it in WordPress needs no rebuild.


    Setup

    1. Build the form in CraftForms

    A normal CraftForms form — fields, conditional logic, validation, email notifications, and payments if you need them.

    2. Enable external submissions + create an embed key

    Submission settings → Allow External Submissions, then CraftForms → Settings → Embed → create a key bound to your Hugo site’s domain. Copy it.

    3. Install the Hugo helpers

    Copy these from the plugin’s examples/static-site/hugo/ into your Hugo project, preserving structure:

    layouts/shortcodes/craftform.html   → use from Markdown content
    layouts/partials/craftform.html     → use from templates
    

    Set your backend URL once in hugo.toml:

    [params.craftforms]
      wpUrl = "https://forms.example.com"
    

    4. Use it

    From a content file:

    ## Get in touch
    
    {{< craftform key="aHR0cHM6Ly9mb3Jtcy5leGFtcGxlLmNvbQ.abc123" >}}
    

    With a catalog item for pricing:

    {{< craftform key="…" resourceId="42" >}}
    

    From a template (e.g. a booking layout):

    {{ partial "craftform.html" (dict "key" .Params.formKey "resourceId" .Params.catalogItem) }}
    

    Both emit the same snippet:

    <div data-craftforms-embed="…"></div>
    <script src="https://forms.example.com/wp-content/plugins/craftforms/build/webcomponents/embed.js" defer></script>
    

    What this buys you over a hosted endpoint

    • No submission caps — it’s your database.
    • Conditional logic, file uploads, real validation — enforced server-side, not just hinted in the browser.
    • Branded confirmation and notification emails, with attachments.
    • Payments via Stripe inside the form.
    • Data ownership — submissions never leave infrastructure you control.
    • Multiple forms, one backend — every Hugo site you run can share it.

    Security notes

    • The embed key is domain-bound; the backend validates request Origin.
    • Prices, stock, and capacity are re-verified server-side on submit.
    • The WordPress install is locked down to a near-zero attack surface.
    • No built-in CAPTCHA yet — add a required request header to the form as a shared-secret spam gate on public endpoints.

    Same idea, other stacks

    If you also build with Astro or Builderius, the mechanism is identical — an Astro <CraftForm /> component and a Builderius shortcode both emit the same embed placeholder. One backend, one embed model, every static toolchain.

    CraftForms embedding and external submissions are CraftForms PRO features.

  • Add a Real Form Backend to Your Astro Site (Not Just Email)

    Add a Real Form Backend to Your Astro Site (Not Just Email)

    Astro is a joy to build with, right up until you need a form. Then you hit the wall every static-site developer hits: there’s no server to receive the submission. The quick fix is a hosted endpoint that emails you the result — fine for a single contact form, painful the moment you need many forms with real logic.

    Take a summer camp site. It needs a registration form, a medical form, a photo-release form, a liability waiver, maybe a payment. Some fields are required only if an earlier answer was “yes.” Files get uploaded. Confirmation emails go out. A mailto-style endpoint can’t do any of that, and wiring five of them up — each with its own dashboard and submission cap — is its own small nightmare.

    There’s a better shape: keep Astro static and fast, and point every form at one WordPress backend running CraftForms. WordPress does what it’s good at (storing, validating, routing, emailing); Astro does what it’s good at (serving fast static pages). Here’s how to connect them.

    This is the Astro-specific walkthrough. New to the whole idea? Headless WordPress + a Static Frontend, Explained covers why this shape works before you wire it up. And to lock the WordPress backend down to near-zero attack surface, see Use WordPress as a Locked-Down Form Backend for Static Sites.


    How it works

    You build the form once in WordPress with CraftForms. On your Astro page you drop a small component that renders a placeholder. At runtime a tiny script (embed.js) fetches the live form from your backend and boots it in place. Submissions post back to the backend, which validates and stores them.

    Nothing about your form lives in the Astro build — so you can edit fields, validation, or pricing in WordPress and the change appears without rebuilding the site.


    Why it stays fast

    The Astro page was already instant — it’s static. The form doesn’t change that. Once it boots, everything the visitor does happens in the browser: conditional fields show and hide, and if it’s a product form, the price recalculates on every option change with no backend call. The only time the backend is touched is the final submit. So a camper’s parent can work through a multi-field registration, toggling options and watching the total update, without a single round-trip — and your WordPress backend does almost nothing per visit. Fast page, fast form, quiet server.


    Setup

    1. Build the forms in CraftForms

    Registration, medical, release, waiver — each is a normal CraftForms form. Add conditional logic (show the guardian-signature field only for under-18s), file upload fields, and email notifications. This is standard CraftForms; nothing Astro-specific yet.

    2. Enable external submissions + create an embed key

    For each form: Submission settings → Allow External Submissions. Then CraftForms → Settings → Embed → create an embed key bound to your Astro domain (e.g. camp.example.com). Copy each key. The key encodes your WordPress URL, so the Astro side never hard-codes it.

    3. Add the component

    Copy CraftForm.astro from the plugin’s examples/static-site/astro/ into src/components/. Set your backend URL once in .env:

    PUBLIC_CRAFTFORMS_WP_URL=https://forms.example.com
    

    Then use it anywhere:

    ---
    import CraftForm from "../components/CraftForm.astro";
    ---
    
    <h1>Camp registration</h1>
    <CraftForm embedKey="aHR0cHM6Ly9mb3Jtcy5leGFtcGxlLmNvbQ.abc123" />
    
    <h2>Photo release</h2>
    <CraftForm embedKey="aHR0cHM6Ly9mb3Jtcy5leGFtcGxlLmNvbQ.def456" />
    

    Multiple forms on one page are fine — the loader upgrades every placeholder and injects each shared asset only once.

    Under the hood the component emits:

    <div data-craftforms-embed="…"></div>
    <script is:inline defer src="https://forms.example.com/wp-content/plugins/craftforms/build/webcomponents/embed.js"></script>
    

    is:inline tells Astro to leave the external loader alone rather than bundling it.


    What you get that a hosted endpoint can’t do

    • Conditional logic — fields that appear based on earlier answers, all evaluated in the browser.
    • File uploads — waivers, medical documents, ID photos, stored on your backend.
    • Validation that means something — required fields, formats, min/max, custom rules, enforced server-side.
    • Real email — branded confirmations to the parent, notifications to staff, with attachments.
    • Payments — take a deposit with Stripe inside the same form.
    • Your data, your server — submissions live in your WordPress database, not a third party’s, with no monthly submission cap.

    Is it secure?

    The submission endpoint is public — that’s the point — but it isn’t naive:

    • The embed key is bound to your domain; the backend checks the request Origin and refuses others.
    • Anything money- or capacity-related (price, stock, booking slots) is re-computed server-side and rejected if tampered.
    • The WordPress install itself is locked down: one plugin, no public pages, no XML-RPC, hidden login.

    On spam: a public endpoint attracts bots, but CraftForms runs a built-in anti-spam check on every submission, so you’re covered out of the box — no CAPTCHA to bolt on. If you want an extra layer for a high-traffic public form, require a request header (Submission settings → Required headers) that only your embed sends — a lightweight gate most drive-by bots won’t clear.


    The result

    Your camp site stays a static Astro build — instant to load, trivial to host. But every form on it is a real form: logic, files, email, payments, and submissions you own. One backend serves them all, and editing a form never means redeploying the site.

    Embedding and external submissions are CraftForms PRO features.