Category: Articles

  • 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 Bannermaterial 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)


  • 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.

  • How CraftForms Stops WordPress Form Spam — Without a CAPTCHA

    How CraftForms Stops WordPress Form Spam — Without a CAPTCHA


    TL;DR

    • CraftForms never shows a CAPTCHA. Instead, every form quietly watches how it was filled in: mouse movement, focus events, keystrokes vs. paste, and how naturally the timing plays out.
    • A submission that doesn’t look human — no mouse movement, no focus, no typing, filled out far faster than a person physically could — gets blocked right there in the browser, with a plain “Spam submission detected” message. No redirect, no lecture, no “prove you’re not a robot.”
    • A real visitor who interacts with the form at all — clicks a field, types a character, even just engages with the submit button — never comes close to being flagged. The check is invisible to them by design.
    • I run this on several of my own WordPress sites, including my highest-traffic one, and haven’t had a single spam submission come through since turning it on. That’s not a lab result — it’s just what’s happened in production.
    • Being honest about the limits: this is a client-side heuristic, not a server-side firewall. It’s a very effective first line of defense against the form-spam bots that actually exist in the wild — but it isn’t the same claim as “unbeatable,” and this post says so plainly.

    If you’ve ever added a CAPTCHA to a WordPress form, you already know the trade-off nobody advertises: it stops some bots, and it also makes some real visitors leave. Google’s own reCAPTCHA v2 checkbox occasionally escalates into a grid of blurry street signs. Mobile users hate it. Screen-reader users have a genuinely bad time with it. And you’re trusting a third-party script with performance and privacy implications, just to protect a contact form.

    CraftForms takes a different bet: don’t ask the visitor anything. Watch how the form actually gets filled in, and let the behavior answer the question instead.

    Cf captchaless

    How spam bots actually fill out a form

    Almost every automated form-spam tool — from a crude script to a headless-browser bot — shares a set of tells, because they’re optimizing for volume, not for looking human:

    • They don’t move the mouse. There’s no pointer to move; the values are set directly.
    • They don’t focus fields in sequence. A human tabs or clicks through a form field by field; a bot just writes into the DOM or POSTs a payload.
    • They don’t type. Values appear instantly — either injected all at once, or dumped in via a single paste-like operation.
    • They’re fast. A form that takes a human 20–60 seconds to read and fill gets “submitted” in under a second by a script, sometimes before the page has even finished rendering.

    None of that requires a CAPTCHA to detect. It just requires paying attention.

    What CraftForms actually watches

    Every CraftForms form tracks a handful of signals for the life of that page load — all client-side, all invisible, nothing rendered on screen:

    • Mouse movement — did the pointer move at all?
    • Focus events — did any field actually receive focus?
    • Keystrokes vs. paste — did the visitor type, paste, or neither?
    • Timing — how long between the form appearing and the submit firing, and did submit happen before the form had even finished rendering?
    • Input-change pattern — a lot of fields changing with very few keystrokes typed looks like autofill or a scripted fill, not a person.

    CraftForms also accounts for legitimate edge cases a naive detector would punish — like a form with sensible defaults already filled in, where a real visitor just glances at it and clicks submit without typing anything. Genuine physical interaction with the form is always read as a human signal, however it shows up.

    A typical WordPress form with a reCAPTCHA checkbox widget sitting under the submit button, next to a CraftForms form with nothing below the fields but the submit button itself
    Cf captchaless

    The score, and what crosses the line

    Each of those signals feeds into a single behavioral score for the submission, weighted by how strongly it points to “not human.” No single signal is treated as proof on its own — it’s the combination that matters, which is exactly why a person absent-mindedly filling out a form never gets close to a false positive, while a script that never touches the page like a human does trips several signals at once, immediately.

    Cross the threshold, and the submission gets blocked right there in the browser, before it’s ever sent to the server. The visitor (or, more accurately, the bot) sees a plain “Spam submission detected.” message. Nothing about the score, the breakdown, or the mechanism is exposed anywhere in the page — the internals aren’t something we publish, for the same reason a lock manufacturer doesn’t publish pin depths.

    A real person filling out a real form essentially never triggers this. They move the mouse. They click into a field. They type something, even one character. Any one of those alone rules out an entire category of “this looks like a bot.”

    Real-world results: I run this on my own sites

    I don’t just build this — I use it. I manage several WordPress sites that run CraftForms, including my main site, which gets a meaningful amount of traffic. Every one of them uses this exact behavioral check, with no CAPTCHA anywhere in the form. Since turning it on, none of them has received a spam submission. Not “spam is down” — zero.

    I’d rather be precise than impressive here: this isn’t a controlled study, and I’m not claiming it stops literally every bot that could ever exist. It’s a report from actually running the thing, on real forms, exposed to real internet traffic, for a while now.

    Being honest about what this is — and isn’t

    This is a genuinely useful post specifically because it doesn’t oversell the mechanism, so here’s the honest version:

    • It’s a behavioral check, evaluated as the form is filled out. A basic spam bot that fills a visible <form> — which describes the overwhelming majority of what actually hits WordPress sites — never gets past it, because it never produces human-shaped behavior in the first place.
    • It’s not a substitute for edge-level protection. It’s not a WAF, and it doesn’t claim to be. If you’re running a very high-traffic, public-facing form that you expect to become a specific target — not just incidental spam traffic — pair this with protection at the edge (Cloudflare Turnstile, a security plugin’s rate limiting, or your host’s WAF). CraftForms’ behavioral check is the friction-free first layer, not the last line of defense for a form under active, targeted attack.

    For the form volume most WordPress sites actually deal with — generic spam bots crawling for any submittable form — this is the whole story, and it’s the reason you can skip the CAPTCHA entirely.

    Try it

    There’s nothing to configure. Every CraftForms form ships with this on by default — no toggle to find, no threshold to tune, no third-party script to add. Install CraftForms, build the form, publish it — the spam filtering is already running before you’ve thought about it once.


    Summary

    1. CraftForms scores form submissions using behavioral signals — mouse movement, focus, keystrokes vs. paste, and timing — entirely in the browser and entirely invisible to the visitor.
    2. Cross the threshold and the submission is blocked before it’s ever sent, with a plain message; nothing about the scoring mechanism is ever shown.
    3. Legitimate edge cases — like a form with sensible defaults where someone just clicks submit — are accounted for, so real visitors are never unfairly penalized.
    4. In real production use across several of my own sites — including my highest-traffic one — this alone has kept spam at zero.
    5. It’s a behavioral first layer, not a server-side firewall: excellent against the generic bots that make up most form spam, but pair it with edge-level protection on a form under deliberate, targeted attack.
  • 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.

  • Static Catalog, Live Prices: Stop Rebuilding 15,000 Pages Every Time a Price Changes

    Static Catalog, Live Prices: Stop Rebuilding 15,000 Pages Every Time a Price Changes

    You built a big product catalog as a static site — thousands of prebuilt pages, fast and cheap to serve. Then reality set in: prices change a few times a day. And every change means rebuilding the whole site. On a small catalog that’s an annoyance. At 15,000 pages it’s a build queue, a cache purge, and a nagging feeling you’ve picked the wrong architecture.

    You haven’t. You’ve just been treating one small dynamic thing — the price — as if it were static. The fix isn’t “go full server-side.” It’s to keep the pages static and make only the price dynamic.

    And “the price” is really shorthand for more than a number. On a real product page it’s the options a shopper picks, the live total those options add up to, maybe a custom text field or an uploaded file, and — if they’re ready to buy — the checkout step itself. A product configurator is the same dynamic island as a price display; it just does more once it’s booted. The pattern in this post scales from “show a live number” all the way to “run a full configurator with payment,” because it’s the same embed either way.

    New to the headless-plus-static-frontend idea in general? Using WordPress as a Form Backend for Static Sites and Web Apps covers the underlying pattern before this post zooms in on the pricing case.


    The insight

    A catalog page is 95% static: title, description, images, specs, related links. None of that changes when a price does. So don’t rebuild it. Let the page stay frozen and let the price and options be computed at view time, in the browser, from live data.

    CraftForms is built around exactly this split. Its product forms run a price formula on two sides: instantly in the browser as the shopper picks options, and again on the server when they submit. The browser side is what makes a static page feel live. The server side is what keeps it honest.


    What “make only the price dynamic” looks like

    Embed a CraftForms product form on each catalog page. The page ships static; the form is fetched at runtime and computes the price from current data as the shopper configures the product:

    <div data-craftforms-embed="YOUR_EMBED_KEY" data-resource-id="42"></div>
    <script src="https://your-backend.com/wp-content/plugins/craftforms/build/webcomponents/embed.js" defer></script>
    
    • The page is static — built once, served from the edge.
    • The form (and its pricing logic and current option prices) is fetched live from your WordPress backend via embed.js.
    • The price updates in the browser as options change — no round-trip per keystroke.
    • The backend is only touched on submit, when it re-computes the total and rejects any mismatch.

    Nothing here is specific to a plain price number — the same embed can carry a file upload for custom artwork, a Stripe payment step, or an “Add to Cart” button that lands the configured item in a WooCommerce cart. You’re not bolting a separate checkout widget onto the page; the form is the configurator, the price is one of its outputs, and checkout is another.

    Change a price in WordPress and the next visitor sees it. No rebuild. No 15,000-page deploy.


    Where prices actually live

    You don’t want prices hard-coded into 15,000 templates — that’s what caused the rebuild problem. Keep them in your backend:

    • A catalog item per product (data-resource-id links a form to one), so option prices are looked up live.
    • A price formula on the form for anything computed (area × rate, tiered quantities, add-ons, surcharges).

    Now a price change is a data edit, not a code deploy. The static pages never need to know.


    “Won’t the price flash in after load?”

    Only briefly, and only for the form region — the page itself (images, copy, specs) is already there instantly. If you want a number visible before the form boots, render a build-time price as a placeholder in the static HTML and let the live form replace it on load. Shoppers see a price immediately; the authoritative, current price arrives a moment later and is the one that gets submitted.


    Filtering and search at scale

    Live pricing solves the rebuild problem; it doesn’t solve search. If pure static filtering is getting clunky at your catalog size, that’s a separate concern — offload search/filtering to a client-side index or a hosted search service, and keep using static pages + live pricing for the product detail experience. The two decisions are independent; you don’t have to go fullstack to fix either.


    It’s a configurator, not a price tag

    Once the pricing logic lives in a real form engine instead of a hard-coded number, the same setup covers a lot more than “show the total”:

    • Complex pricing — formulas, tiered quantities, and lookup tables instead of a single price field, for products where the total isn’t just base × quantity. (Example for WooCommerce: Complex WooCommerce Pricing Without Hundreds of Variations; the same formula/table engine runs behind a static catalog.)
    • Stock per option combination — a dynamic SKU built from the shopper’s selections, checked against a stock table before the order goes through, so a static page never oversells a specific size/material/colour combo. (Possibly related article: Track WooCommerce Stock by Option — No Variations is the deep dive.)
    • File uploads for custom artwork or specs, validated server-side.
    • Payment — Stripe (hosted checkout or embedded card element).
    • Booking capacity — the same live-then-verified pattern applies to an appointment slot or a date range, not just a price.

    None of that changes the architecture in this post. It’s the same static page, the same embed, the same submit-only backend — the form itself is just doing more.


    Why not just go server-rendered?

    You could. But you’d give up the thing that made the catalog cheap and fast in the first place. The static-plus-live-price shape keeps the edge performance and the trivial hosting, and confines the “dynamic” surface to a single fetch and a formula. Your backend does almost nothing per pageview — it answers one embed request and, occasionally, a submission.

    One honest note: the submission endpoint is public, so rely on the domain-bound embed key (the backend checks request Origin) plus a required request header as a spam gate (CraftForms doesn’t ship a CAPTCHA yet), and the built-in server-side price re-verification so a shopper can’t submit a total they edited in devtools. Locking the rest of the WordPress install down for this is a one-click Headless backend mode toggle in CraftForms → Settings → Embed — see the locked-down backend guide for what it covers and the extra server-level steps.


    The takeaway

    Rebuilding a whole catalog to change a number is a smell, not a requirement. Keep the pages static. Make the price — or the whole configurator, with options, stock, and checkout — live. Let the browser compute it for feel and the server verify it for trust. Your build stays fast, your catalog stays current, and 15,000 pages stop caring what anything costs or whether it’s even in stock.

    It’s a shape developers tend to recognize the moment they see it work — static frontend, instant in-browser pricing, backend touched only on submit — because it removes the one thing that made big catalogs painful without giving up the speed and cheap hosting that made them appealing.

    Related reading: if your catalog lives on Astro, Add a Real Form Backend to Your Astro Site walks through the embed end to end; on Hugo, see Add Forms to a Hugo Site With a WordPress Backend.

    CraftForms embedding, external submissions, and catalog pricing 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.

  • Generate a Static Site from WordPress — With Forms That Actually Work

    Generate a Static Site from WordPress — With Forms That Actually Work

    Static sites are the easiest sites to love: they’re fast, they cost almost nothing to host, and there’s no server to patch at 2am. The catch is always the same. The moment you need a form — a registration, a booking, a product configurator with a live price — a static site has nowhere to send it.

    The usual answer is to bolt on a hosted form endpoint (Formspree, Netlify Forms, Web3Forms). That works until it doesn’t: submission caps, no real validation, no conditional logic, no pricing, and your submission data living on someone else’s infrastructure.

    There’s a cleaner option if you already work in WordPress. Build the site visually in Builderius, export it to static files, and let CraftForms — running on a locked-down WordPress backend — handle every form. You get a static frontend on Cloudflare or Netlify Pages and a real form/price engine you own. This article shows the exact wiring, including a one-line shortcode that survives the static export intact.

    New here? Two companion articles set the stage: Using WordPress as a Form Backend for Static Sites and Web Apps introduces the mechanism, and Use WordPress as a Locked-Down Form Backend for Static Sites covers securing the backend. This post is the “build it in Builderius” path.


    The stack

    Three pieces, each doing one job:

    • Builderius — the visual builder. Design pages in WordPress, then use its static-site generator to crawl the rendered pages and export clean HTML/CSS/JS with no WordPress dependency in production. Deploys straight to Netlify, Cloudflare Pages, GitHub Pages, or S3.
    • CraftForms — the form + price engine. Builds forms, validates input, runs conditional logic and price formulas (client-side for instant feedback, re-verified server-side on submit), stores submissions, sends notifications, takes payments.
    • A locked-down WordPress install — invisible to the public, serving only the CraftForms REST endpoints. As of CraftForms 1.6, a single toggle in CraftForms → Settings → Embed (“Headless backend mode”) does this natively — no mu-plugin required. See below for what it covers.

    The static site your visitors load has no WordPress in it. When someone opens a form, a tiny script fetches the current form from your backend and boots it in place. When they submit, it posts to your backend. Nothing else touches WordPress.


    The problem with freezing a form

    Here’s the subtle part most walkthroughs skip.

    When a static generator crawls a page that has a form rendered directly into it, it freezes a snapshot of that form. For a CraftForms form rendered inline, that snapshot bakes in two things that don’t survive well:

    1. A submit URL of the …/wp-json/craftforms/v1/submit/… form. On a locked-down backend, /wp-json/ is often blocked outright — so the frozen form posts to a dead URL.
    2. A one-time security nonce. Nonces expire in hours and are tied to a session. On a page frozen at build time and served for weeks, it’s stale on arrival.

    You can work around both by hand. But there’s a purpose-built path that avoids the problem entirely: don’t freeze the form — freeze a placeholder that fetches the live form at runtime.


    The shortcode

    CraftForms ships a shortcode built for exactly this:

    [craftforms_static_form key="YOUR_EMBED_KEY"]
    

    Instead of rendering the whole form, it outputs the CraftForms embed placeholder:

    <div data-craftforms-embed="YOUR_EMBED_KEY"></div>
    <script src="https://your-backend.com/wp-content/plugins/craftforms/build/webcomponents/embed.js" defer></script>
    

    That’s what gets frozen into your static export — two lines, no baked nonce, no dead submit URL. At runtime embed.js decodes your backend’s URL from the embed key, calls GET /craftforms/v1/embed/{key}, and replaces the placeholder with the live form and its current price logic. Because the form is fetched fresh every visit, you can change fields or prices in WordPress without rebuilding the static site.

    Link a form to a catalog item for catalog pricing by adding resource-id:

    [craftforms_static_form key="YOUR_EMBED_KEY" resource-id="42"]
    

    Step by step

    1. Enable external submissions on the form

    Open your form → Submission settings → turn on Allow External Submissions. This is what lets a request from your static domain (a different origin) be accepted at all.

    2. Create an embed key

    Go to CraftForms → Settings → Embed and create a key for that form and the domain your static site will be served from (e.g. camp.example.com). The key encodes your WordPress URL, so you never hard-code it on the static side. Copy it.

    3. Drop the shortcode into Builderius

    In the Builderius canvas, add a Shortcode module where you want the form and set it to:

    [craftforms_static_form key="aHR0cHM6Ly9mb3Jtcy5leGFtcGxlLmNvbQ.abc123"]
    

    Builderius renders the shortcode server-side, so the <div> + <script> land in the page’s HTML — which is exactly what its static-site generator captures.

    4. Generate and deploy the static site

    Run Builderius’ static-site generation on your published release, then deploy to Cloudflare Pages / Netlify / GitHub Pages. The generator freezes the placeholder verbatim and — because embed.js is served from your WordPress domain — bundles the script into the export automatically.

    5. Open the static page

    The form appears, live. Change an option and the price updates instantly (computed in the browser). Submit, and it posts to your locked-down backend — which independently re-computes the price and rejects any tampering before storing the submission or charging a card.

    Why this is fast: the page was already static and instant, and once the form boots, every option change recalculates the price in the browser with no backend call. The backend is touched exactly once — on submit. There’s no per-click round-trip, so a shopper can configure a product as fast as they can click, on hosting that costs almost nothing.


    Why this is safe

    External submission endpoints are public by nature, so CraftForms doesn’t trust the client:

    • Origin check — the embed key is bound to one domain; requests from anywhere else are refused.
    • Server-side price re-verification — the browser shows a live price for UX, but the server recomputes _price, stock, and booking capacity from the form’s own formulas and rejects mismatches. A user editing the price in devtools gets a 4xx, not a discount.
    • Locked-down surface — flip Headless backend mode on in CraftForms → Settings → Embed and the plugin restricts the REST API to its own namespace (anonymous requests to anything else get a 403), disables XML-RPC, and blacks out every public frontend request for logged-out visitors — leaving just the CraftForms embed and submit endpoints reachable. It’s PHP-layer, fully reversible, and ships with a CRAFTFORMS_HEADLESS_DISABLE constant for wp-config.php as a kill switch if you ever lock yourself out.

    That one toggle covers the protections that matter most day to day. It deliberately stops short of two things a plugin can’t safely do for you: hiding /wp-login.php behind a custom slug, and server-level rules (blocking xmlrpc.php at nginx/Apache, disabling PHP execution in /uploads). If you want those too, the locked-down guide still has the extra mu-plugin snippet and server config for them — otherwise the built-in toggle is now the fastest way to get most of the way there.

    One honest caveat: CraftForms does not yet ship a built-in CAPTCHA or honeypot. For public submission endpoints, add a required request header to the form (Submission settings → Required headers) as a lightweight shared-secret filter, and keep an eye on the roadmap for a first-class anti-abuse layer.


    Not using Builderius?

    The same embed placeholder works anywhere. CraftForms ships copy-paste starters for other static generators:

    • Astro — a <CraftForm embedKey="…" /> component. Find out more on how to add a form to Astro website.
    • Hugo — a {{< craftform key="…" >}} shortcode and a partial.

    They’re in examples/static-site/ in the plugin, and each emits the identical <div data-craftforms-embed> + embed.js snippet. One mechanism, every static toolchain.


    The payoff

    You keep everything static sites are good at — speed, cheap hosting, a tiny attack surface — and you stop pretending forms are someone else’s problem. One WordPress backend, locked down and invisible, powers the forms, pricing, bookings, and payments across every static site you build. And because the form is fetched at runtime, your prices are never one stale deploy behind.

    CraftForms embedding, external submissions, the shortcode, and Headless backend mode are CraftForms PRO features.

  • CraftForms 1.6 Is Here — Meet the Frames Editor, Headless Lockdown, and Bulletproof Bookings

    CraftForms 1.6 Is Here — Meet the Frames Editor, Headless Lockdown, and Bulletproof Bookings

    CraftForms 1.6 is out now, and this release is about giving customers a more tactile way to shop, and giving you a more solid platform underneath them. The headline is a brand-new interactive element — the Frames Editor — that lets shoppers preview a framed print in real time. Alongside it: a Locked WP mode for running WordPress as a pure headless form backend, a smoother path to selling a WooCommerce product from any page, and a fix that makes double bookings a thing of the past.

    Here’s everything in 1.6.


    Frames Editor — let customers preview the frame before they buy

    The star of 1.6 is the new Frames Editor interactive element. If you sell framed prints, canvases, photo gifts, or anything that goes on a wall, this is built for you.

    Add it to a form and shoppers get a live, realistic preview: they pick a frame style, a mat/mount color and width, and watch the preview update instantly around their photo or product image. There’s even a “view it in a room” scene so customers can see roughly how the finished piece will look on their own wall before they commit. The finished composite image is saved right along with the order, so you know exactly what to produce.

    It turns a flat product photo into something customers can actually configure and picture in their space — which tends to mean fewer support questions about “what will this look like” and more confident checkouts.

    Craftforms picture frame

    Locked WP mode — run WordPress as a headless form backend

    1.6 adds Locked WP mode, a new setting for anyone using CraftForms to power forms embedded on a separate site — a static site built with Astro, Hugo, or similar.

    Turn it on and your WordPress install locks down: public front-end pages, REST routes, and XML-RPC are closed off to anonymous visitors, while CraftForms’ own embed and submission endpoints stay open exactly as before. You still log in and manage everything as an admin — only the parts of WordPress you’re not using get shut off to the outside world. And if anything ever goes sideways, a single wp-config.php constant is a guaranteed way to switch it back off.

    It’s a small setting with a specific job: let WordPress be the engine for your forms without also being a second, unnecessary public website.


    Sell a product from anywhere — shortcode + WooCommerce, improved

    1.6 improves how a form connects to a WooCommerce product when embedded via shortcode. Connect a form to a product and add it with , and the full add-to-cart experience — price, options, and all — now displays cleanly wherever you drop that shortcode, not just on the product’s own page.

    Combined with better support for page builders’ product widgets, it’s now more reliable to feature a product-connected form on a landing page, a bundle page, or anywhere else you want to sell — without wrestling with where WooCommerce normally expects that markup to live.


    No more double bookings

    Booking the same slot twice used to be possible in one specific situation: two customers checking availability and confirming a booking at almost the same moment. Both could pass the availability check before either booking was actually recorded, and both would go through — leaving you overbooked.

    1.6 closes that gap by making every booking write atomic: each reservation now checks and reserves the slot in a single guarded database operation, so if two bookings race for the same slot, only one can win. Multi-night stays are protected the same way — if any night in the range can’t be secured, the whole booking rolls back rather than leaving you with a booking that’s only partially confirmed. The result: reliable capacity limits, even under real demand.


    Fixes and polish

    1.6 also ships two fixes worth calling out:

    • Add to cart no longer clears your cart. A timing issue meant that, in some cases, adding a CraftForms-connected product to the cart could wipe out whatever else the customer already had in it. That’s fixed — existing cart items now stick around.
    • Multi-file upload fields now receive every file. Fields that accept multiple files were only getting the last one through; all selected files now upload correctly.

    Get CraftForms 1.6 today

    CraftForms 1.6 is about letting customers preview what they’re buying, giving you a safer way to run headless forms, making product-connected forms easier to place anywhere, and making sure your bookings hold up under real traffic. Update now to get all of it.

    New to CraftForms? The free version gives you the full block-based form builder to start with today — the Frames Editor, Locked WP mode, and WooCommerce features arrive with Pro.