Tag: headless

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

  • Use WordPress as a Locked-Down Form Backend for Static Sites

    Use WordPress as a Locked-Down Form Backend for Static Sites

    Static sites are fast, cheap to host, and nearly impossible to compromise — but they can’t process a contact form. Every static site eventually hits the same wall: you need a backend.

    Most developers reach for a third-party service (Formspree, Netlify Forms, Basin) or bolt on a separate server. Both options add a dependency you don’t control, a recurring cost, and submission data stored on someone else’s infrastructure. There is a third option that gives you full ownership, unlimited forms, and a security profile close to zero: a locked-down WordPress installation used exclusively as a form backend.

    One WordPress install. Zero public pages. Every form submission from every static site you own — handled, stored, and routed — on infrastructure you control.

    This article is an evolution of Using WordPress as a Form Backend for Static Sites and Web Apps. That article introduced the idea — a single WordPress install as a submission endpoint. This one picks up where it left off: the site is locked down and invisible to visitors, improved security setup. CraftForms now supports embedded forms — the backend serves the form HTML directly to any external page, with no markup required on the static site side — and the full ecommerce and booking stack that comes with them. The same backend that took contact form submissions can now handle bookings, inventory on a site that has no server of its own.


    Part 1 — The Architecture: One Backend, Many Static Sites

    The Stack

    Three tools, each doing exactly one job:

    • WordPress — the backend. Locked down so aggressively it no longer resembles a normal WP install. No theme, no public content, no extra plugins.
    • CraftForms — the form engine. Handles form building, validation, submissions, conditional logic, file uploads, and email notifications.
    • Builderius — the optional static site builder. Design your pages visually and export clean HTML/CSS/JS files with no WordPress dependency in production.
    Static sites embed schema

    Your static sites connect to the WordPress backend over HTTPS. Static site A makes a direct fetch call on form submit. Static sites B and C use CraftForms’ embed feature — the form HTML is served from WordPress and rendered on the page automatically. Both methods hit the same craftforms/v1 REST endpoint; everything else on the WordPress install is locked down.

    What the locked-down WP install does NOT have

    • No public frontend — all page and post requests return 403
    • No theme vulnerabilities — no theme is active
    • No page builder, no WooCommerce, no third-party contact form plugin
    • No XML-RPC
    • No /wp-login.php at its default path

    A JAMstack site on Cloudflare Pages or Netlify serves your visitors. WordPress never touches a public HTTP request. It only processes form submissions.


    Part 2 — Locking Down the WordPress Installation

    Why One Plugin Changes Everything

    The most common vector for WordPress compromise is not your hosting provider — it’s outdated plugins. Every plugin in your install is a potential attack surface: a page builder you added for one client project, a contact form plugin with a stored XSS CVE published last week, a WooCommerce extension that stopped receiving updates.

    A WordPress installation with one plugin and a blocked public frontend has an attack surface close to zero. No theme vulnerabilities, no page builder vulnerabilities, no contact form plugin vulnerabilities — because none of those exist on this install.

    The steps below lock down the remaining standard entry points.


    Step 1 — Block the WordPress Frontend

    The template_redirect action fires before WordPress outputs anything. For any visitor who is not logged in, the hook returns a 403 and exits — no page, no post, no homepage is ever served. Because this runs in PHP it works on any server: Apache, nginx, or a local PHP built-in server. No .htaccess rules or server configuration required.

    template_redirect does not fire for REST API requests or wp-admin, so the CraftForms submission endpoint and the admin panel remain fully accessible to logged-in users and external form submissions.

    The implementation is in the complete mu-plugin below.


    Step 2 — Restrict the REST API to CraftForms Only

    All REST namespaces except craftforms/v1 return 403. This closes user enumeration (GET /wp-json/wp/v2/users), route discovery (GET /wp-json/), and every standard WordPress REST exploit in one filter. The filter fires after WordPress resolves the CORS OPTIONS preflight, so cross-origin submissions from your static sites continue to work correctly.

    Create wp-content/mu-plugins/craftforms-backend.php — files in mu-plugins/ load automatically on every request, no activation required. The full implementation is in the complete mu-plugin below.


    Step 3 — Hide the WordPress Login URL

    Automated brute-force scripts target /wp-login.php by default. Moving the login to an unpredictable URL removes your install from every automated scan. Pick a slug that is long, random, and only you know — and store it somewhere safe. Your login page will be at https://your-wp-backend.com/your-secret-slug. Losing the slug means you cannot log in.

    The full implementation is in the complete mu-plugin below.


    Complete mu-plugin

    Create a new PHP file craftforms-backend.php Drop this single file in wp-content/mu-plugins/ and all four measures are active immediately:

    <?php
    /**
     * CraftForms backend — security measures.
     * Place in: wp-content/mu-plugins/craftforms-backend.php
     */
    if ( ! defined( 'ABSPATH' ) ) exit;
    
    // ── 1. Restrict REST API to craftforms/v1 only ──────────────────────────────
    add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
        $route = $request->get_route();
        if ( strpos( $route, '/craftforms/' ) === 0 ) {
            return $result;
        }
        return new \WP_Error(
            'rest_restricted',
            'REST API is disabled on this installation.',
            [ 'status' => 403 ]
        );
    }, 10, 3 );
    
    // ── 2. Disable XML-RPC ───────────────────────────────────────────────────────
    add_filter( 'xmlrpc_enabled', '__return_false' );
    
    // ── 3. Custom login URL ──────────────────────────────────────────────────────
    if ( ! defined( 'CF_LOGIN_SLUG' ) ) {
        define( 'CF_LOGIN_SLUG', 'my-secret-access-8k2m9x' ); // ← CHANGE THIS
    }
    
    add_action( 'init', function () {
        global $pagenow;
        $request_path = parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH );
    
        if ( $request_path === '/' . CF_LOGIN_SLUG ) {
            // wp-login.php reads $user_login and $error before conditionally setting
            // them; initialise here to prevent PHP 8 "Undefined variable" warnings.
            global $error;
            $error      = $error ?? null;
            $user_login = '';
            require_once ABSPATH . 'wp-login.php';
            exit;
        }
    
        if ( $pagenow === 'wp-login.php' ) {
            status_header( 404 );
            nocache_headers();
            exit( 'Not found.' );
        }
    } );
    
    // Rewrite site_url( 'wp-login.php', 'login|login_post' ) calls so the login
    // form action POSTs to the custom slug instead of the blocked wp-login.php.
    add_filter( 'site_url', function ( $url, $path, $scheme ) {
        if ( 'wp-login.php' === $path && in_array( $scheme, [ 'login', 'login_post' ], true ) ) {
            return home_url( CF_LOGIN_SLUG );
        }
        return $url;
    }, 10, 3 );
    
    add_filter( 'login_url', function ( $url, $redirect, $force_reauth ) {
        $custom = home_url( CF_LOGIN_SLUG );
        if ( $redirect ) {
            $custom = add_query_arg( 'redirect_to', urlencode( $redirect ), $custom );
        }
        return $custom;
    }, 10, 3 );
    
    add_filter( 'logout_url', function ( $url ) {
        return str_replace( 'wp-login.php', CF_LOGIN_SLUG, $url );
    } );
    
    // ── 4. Block all public frontend requests ───────────────────────────────────
    add_action( 'template_redirect', function () {
        if ( is_user_logged_in() ) {
            return;
        }
        status_header( 403 );
        nocache_headers();
        exit;
    } );
    

    Verify the setup

    # Should return 403
    curl https://your-wp-backend.com/wp-json/wp/v2/
    
    # Should return form data
    curl https://your-wp-backend.com/wp-json/craftforms/v1/embed/YOUR_KEY
    

    Nathan Foley has prepared a GIST – an updated version of this my MU plugin version. The comment was published in our FB group, you can check it here.


    Security Measures Summary

    MeasureWhat it blocks
    Minimal plugin countEvery plugin not installed = zero CVEs from that plugin
    PHP frontend blockWeb scrapers, bots, and crawlers requesting WordPress pages — works on any server without .htaccess
    REST API namespace restrictionUser enumeration (/wp/v2/users), route discovery, WordPress REST exploits
    XML-RPC disabledBrute-force via XML-RPC, pingback DDoS amplification
    Hidden login URLAutomated brute-force scripts targeting /wp-login.php

    Part 3 — CraftForms: External Submissions and Embedding

    Enable External Submissions

    Open any CraftForms form in the WordPress editor. Scroll to the Advanced Settings panel at the bottom of the settings sidebar — it shows the form’s submission URL and a button to open the full configuration.

    Advanced settings
    Advanced Settings on the form edit page

    Click Configure Submission Settings. The modal opens with everything you need: the toggle, the endpoint URL, a ready-to-run cURL example, and the field validation table.

    Advanced settings modal
    Submission Settings

    Toggle Allow External Submissions on. The API Reference section below it shows the endpoint — the form’s REST name (a human-readable slug you set when creating the form, e.g. contact-form):

    POST https://your-wp-backend.com/wp-json/craftforms/v1/submit/contact-form
    

    Submitting from Your Static Site

    CraftForms accepts application/json. For most static site integrations this is the cleanest approach:

    cURL:

    curl -X POST "https://your-wp-backend.com/wp-json/craftforms/v1/submit/contact-form" \
      -H "Content-Type: application/json" \
      -d '{"name":"John Doe","email":"[email protected]","message":"Hello from curl"}'
    

    Vanilla JavaScript:

    <form id="contact-form">
      <input name="name" type="text" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message"></textarea>
      <button type="submit">Send</button>
      <p id="status"></p>
    </form>
    
    <script>
    document.getElementById('contact-form').addEventListener('submit', async (e) => {
      e.preventDefault();
      const status = document.getElementById('status');
      status.textContent = 'Sending…';
    
      const data = Object.fromEntries(new FormData(e.target));
    
      const res = await fetch(
        'https://your-wp-backend.com/wp-json/craftforms/v1/submit/contact-form',
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data),
        }
      );
    
      const json = await res.json();
      if (json.success) {
        status.textContent = json.data.successMsg || 'Sent!';
        e.target.reset();
      } else {
        status.textContent = json.data.errorMsg || 'Something went wrong.';
      }
    });
    </script>
    

    Field names must match the Name attribute of each CraftForms field block. The Field Validation table in the Submission Settings modal shows the exact field names and their validation rules:

    Advanced settings field valdiation
    Field validation schema

    Required Request Headers

    For an extra layer of spam protection, require a shared secret on every submission. Open the Submission Settings modal and click + Add required header in the Required Request Headers section. Any request that omits the header — or sends the wrong value — is rejected before the form is processed.

    The header name is entirely up to you — X-CF-Token is just one example. Pick any name and any value:

    Header nameHeader value
    X-CF-Tokenyour-secret-value

    Add the header to every fetch call from your static site:

    const res = await fetch(
      'https://your-wp-backend.com/wp-json/craftforms/v1/submit/contact-form',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CF-Token': 'your-secret-value',
        },
        body: JSON.stringify(data),
      }
    );
    

    Note: This header is visible in the browser’s DevTools Network panel, so it is not a true secret for public-facing forms. It raises the bar for automated spam — scripts that don’t know the header will be rejected — but it is not a substitute for rate limiting. For server-to-server calls (a serverless function proxying the submission) it acts as a proper shared secret.


    Embedding a Form

    With embedding you don’t build a form on the external site at all. CraftForms renders the form HTML and delivers it — along with all required styles and scripts — directly to the external page. The form submits back to the same WordPress backend automatically.

    Setup:

    1. Make sure Allow External Submissions is enabled on the form (step above).
    2. Go to CraftForms → Settings → Embed tab. Click Generate new key, select the form, and enter the external domain (e.g. mysite.com — no protocol, no trailing slash).
    Settings embed
    Add embed key
    1. Click the Snippet button on the new row.
    Settings embed modal
    Embed snippet

    Copy the snippet and paste it anywhere in your HTML page:

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

    The <div> is replaced by the live form at page load — no configuration on the static site side, no build step, no manual form markup.

    What you get for free with an embedded form:

    • Every CraftForms field type — text, email, file uploads, dropdowns, date pickers, conditional fields, multi-step flows.
    • Advanced components — star ratings, range sliders, repeater groups, catalog selectors.
    • Client-side validation built in — required fields, email format, character limits, custom error messages. Everything works out of the box; you write zero validation JavaScript.
    • Consistent UI — the form looks and behaves identically everywhere it is embedded, because it is the same rendered output from the same source.

    Compare that to building the form manually: custom markup, a validation library, wiring up field names, handling error states, writing the fetch call, testing cross-browser. With embedding, that work is already done.

    All form submissions use the ?rest_route= URL format — fully compatible with the locked-down setup in Part 2, even if /wp-json/ is blocked at the server level.

    With a catalog resource (booking, product, etc.):

    <div
      data-craftforms-embed="aHR0cHM6Ly9zYW5kYm94LnRlc3Q.oHrv6dPVreM"
      data-resource-id="42">
    </div>
    <script src="…/embed.js" defer></script>
    

    What Your Static Site Gains

    Third-party form services (Formspree, Netlify Forms, Basin) give you one thing: an email on form submit. CraftForms gives you a backend.

    Email that doesn’t land in spam

    CraftForms routes email through a real SMTP provider — not PHP’s wp_mail. Under SMTP Servers in the admin menu, add as many providers as you need — Postmark, SendGrid, Mailgun, your own mail server — each with its own credentials. Then, inside each form’s submit actions, the Send Email and Send Email Template actions each have an SMTP server selector: pick which provider handles that delivery. A contact form can send notifications via Postmark; a booking form can use a separate provider tied to your reservations mailbox. No shared infrastructure, no sender reputation you don’t control.

    Branded HTML email templates — designed in WordPress

    The email template designer is a Gutenberg editor. Add headings, images, buttons, and text blocks; insert {{field_name}} variables anywhere. CraftForms compiles the design to optimised, email-client-compatible HTML automatically. The confirmation email a customer gets after booking a stay looks like it came from a real hospitality brand — because you designed it, in the same editor you use for everything else.

    With an embedded form, your static site gets ecommerce and booking

    This is where the gap between a third-party service and CraftForms widens the most. An embedded CraftForms form isn’t just a contact form — it can be any form type the plugin supports, and Pro forms include:

    • File uploads — with server-side MIME validation and automatic import into the WordPress Media Library
    • Price calculator — Smart Variables evaluate a formula as the user selects options; the live price updates in real time before they submit, and the server recalculates on submission so the charged amount can never be manipulated client-side
    • Booking datepicker — hotel-style checkin/checkout ranges, fixed time-slot grids, or single-date selection; blocked dates and advance-notice requirements enforced visually
    • Catalog and inventory — attach a resource to a form; availability is tracked per date and per slot automatically; pre-submission stock checks prevent double-booking
    • iCal sync — paste an Airbnb or Booking.com iCal URL and those dates are marked unavailable in your datepicker automatically; a private .ics feed goes back the other direction so external platforms stay in sync

    A static site on Cloudflare Pages with a CraftForms embedded form can take bookings, calculate and charge prices, manage inventory, and send a branded confirmation email — all without a server of its own, and without stitching together five separate services.


    Part 4 — Builderius: Build the Static Frontend Without Code

    Builderius is a visual site builder that runs inside WordPress. Design your pages with drag-and-drop, then export the result as pure HTML, CSS, and JavaScript — no PHP, no database, no WordPress dependency in production.

    Workflow:

    1. Build your site in Builderius — on a local or staging WordPress install, completely separate from the locked-down form backend.
    2. Export the static build. The output is clean HTML files and assets. No server-side code, no theme files, nothing to maintain.
    3. Deploy to Cloudflare Pages (free tier, globally distributed CDN, deploys from a git push in seconds) or GitHub pages. Or download the static site as ZIP archive and deploy anywhere you want.
    4. Connect to CraftForms:
      • Option A — Embed: paste the CraftForms snippet into your exported HTML. The form renders automatically on page load. Zero custom JavaScript required.
      • Option B — Custom fetch: build your form directly in Builderius — its form builder lets you design a fully styled form with complete control over every element and its markup. Submit the form data to the endpoint URL provided by CraftForms. You own the design; CraftForms handles the processing.

    The result: a static CDN site with millisecond load times and a tiny security surface on the frontend. The WordPress backend handles form processing and storage — it never serves a single page to a visitor.


    All features described in this article — external submissions, required headers, and form embedding — are part of CraftForms PRO.

  • How to Use WordPress as a Form Backend for Static Sites And Web Apps

    How to Use WordPress as a Form Backend for Static Sites And Web Apps

    TL;DR: CraftForms Pro can receive form submissions from any website or app — not just pages on the WordPress site where it’s installed. Configure the form once in WordPress, submit to it from anywhere. Your data stays in your WordPress admin, server-side validation is always enforced on every request.


    Most WordPress form plugins do one thing well: they put a form on a WordPress page. That’s fine when your site is entirely built in WordPress. But what happens when it isn’t?

    Static sites built with Next.js, Gatsby, or Astro don’t have a PHP backend. React and Vue apps run in the browser with no server of their own. WordPress plugins and themes sometimes need to collect feedback but have no dedicated website to host a form. In each of these cases, the standard “create a form, add it to a page” workflow simply doesn’t apply.

    The usual answer is a third-party form service. Formspree, Netlify Forms, Basin, Getform — they all accept POST requests from any site and forward the submissions somewhere. They work, but they come with strings attached: per-submission limits on free plans, monthly fees once you grow, no control over validation rules, and — most importantly — your submissions living in someone else’s system, accessible from someone else’s dashboard.

    There’s a better option if you already have a WordPress site: use CraftForms as your form backend.

    Note: The feature described in this post — Allow External Submissions — is part of CraftForms Pro.


    The Idea: Separate the Form from the Backend

    A form is really two things:

    1. The interface — the HTML inputs, the layout, the labels, the button
    2. The backend — validation, sanitization and data storage, and optional email notifications

    These two things don’t have to live on the same site or even in the same technology stack. CraftForms normally handles both. But with external submissions enabled, you can take the interface anywhere and leave the backend exactly where it is — running on WordPress, handling everything it always handled.


    Craftforms architecture diagram

    How It Works

    Every CraftForms form has its own submission endpoint — a REST API URL that the form posts to when a user clicks Submit. By default, CraftForms only accepts submissions from pages on the same WordPress site. Enable Allow External Submissions and that restriction is lifted: the endpoint accepts POST requests from any origin.

    The endpoint format is:

    https://yoursite.com/index.php?rest_route=/craftforms/v1/submit/your-form-name
    

    The your-form-name part is what you set in the Form REST Name field. You can use anything descriptive — contact, support, quote-request — as long as it’s unique across your forms. If you don’t set a REST name, CraftForms uses an auto-generated unique ID instead.


    Setting It Up

    Step 1 — Build the form in CraftForms

    Create and configure the form exactly as you would for any WordPress page: add fields, set required rules, configure submit actions (Save Submission, Send Email Template, whatever you need). This is where all the logic lives — the form interface on your external site is just the visible layer on top.

    Step 2 — Open Advanced Settings

    In the form editor, open the settings panel and navigate to the Advanced tab (or open the Form Settings modal). You’ll find two settings here:

    • Form REST Name — set a short, readable slug for your endpoint (e.g., contact)
    • Allow External Submissions — toggle this on
    Image

    Step 3 — Use the generated curl example

    Once you enable external submissions, a panel appears showing your exact endpoint URL and a ready-to-use curl command. It’s not a generic example — CraftForms builds it from your actual form: the correct field names, the right data types, any custom header requirements. You can copy it directly to test the endpoint before writing a single line of frontend code.

    The panel also lists all form fields with their names, types, and validation rules — everything you need to know to replicate the form structure on your frontend.


    What You Get — Without Rebuilding Anything

    When a submission arrives from an external site, it goes through exactly the same processing as a submission from a WordPress page:

    Server-side validation is always enforced. Required fields, min/max rules, email format, file type restrictions — all of it runs on the server regardless of what the browser does. It cannot be bypassed by inspecting the page or submitting manually crafted requests.

    Submissions are saved to your WordPress database. Every entry lands in CraftForms → Submissions with all field values, timestamps, and metadata. Nothing gets lost in a forwarded email. Important: submit action “Save Submission” must be enabled for the form.

    Email Templates. Any Send Email Template actions configured on the form run as normal — it is a possibility to render nice, branded template based emails.

    The delivery log tracks every outgoing email. If a notification failed to arrive, you can check the log and know exactly what happened.


    Real-World Use Cases

    Static sites (Next.js, Gatsby, Astro, plain HTML)

    Static sites have no backend. If you want to handle form submissions, you either need an external service or you need to reach a backend somewhere. With CraftForms, that backend is your WordPress site. Build your form UI in whatever framework or plain HTML — wire the submit action to your CraftForms endpoint — and that’s it. Validation, storage, and email are all handled without writing any server-side code.

    React and Vue apps

    Single-page apps often already have an API they talk to, but that API rarely includes a full form-processing stack. Rather than building validation rules, and setting up email notifications from scratch — or paying a third-party service per submission — you can point your form’s POST request at CraftForms and let it handle all of that.

    WordPress plugin and theme developers

    This use case is closer to home for many developers. If you ship a WordPress plugin or theme, you might want to include a support form, a feedback collector, or a feature request button — without hosting a separate form service.

    The “Request a Feature” button in the CraftForms admin is exactly this pattern in practice. It opens a modal with a form built in plain HTML. When submitted, the request is sent to craftformswp.com — a standard CraftForms installation with a form configured to accept external submissions. The submission goes straight into the submissions inbox, triggers a notification email, and is stored in the WordPress database. No third-party service involved, no per-submission fees, full control over what gets stored and how notifications are worded.

    SCR 20260507 osbi

    If you build and sell a WordPress plugin, this means you can include a polished feedback or support form in your own product and receive everything centralised on your own site.

    Agencies managing multiple client sites

    If you manage several WordPress installations for clients but want submissions from all of them flowing into one place, external submissions make that possible without installing CraftForms on every client site. One CraftForms Pro install on your agency site can serve as the collection point for forms across any number of external properties.


    Why This Beats Third-Party Form Services

    The pitch for services like Formspree is convenience — you get a form backend without setting anything up. But once you already have a WordPress site, that convenience trade-off looks different:

    You own the data. Submissions are in your WordPress database. You export them whenever you want. If you stop paying for something, your data doesn’t disappear — because your data was never somewhere else to begin with.

    No per-submission pricing. Third-party services charge by volume. CraftForms Pro is a flat annual fee. A contact form that receives 5,000 submissions a month costs the same as one that receives 50.

    Validation rules are yours to configure. With a third-party service, you validate on the frontend (which can be bypassed) or accept whatever comes in. With CraftForms, you define the rules in the form editor and they’re enforced server-side on every request, automatically.

    Email templates are fully branded. You design the confirmation email in Gutenberg, using your own logo and colours, and reference any submitted field value with {{email.fieldname}} — not a plain-text forward with no formatting control.

    Submissions sit next to your other business data. Everything is in the same WordPress admin you already work in every day. No separate dashboard to check, no separate login to remember.


    Getting Started

    If you have a CraftForms Pro licence and a WordPress site, there’s nothing additional to install. Create your form, open its Advanced Settings, set a Form REST Name, toggle Allow External Submissions on, and copy the generated endpoint URL.

    For full documentation on the settings and the request format, see the Advanced Settings reference in the CraftForms knowledge base.


    Using CraftForms as a headless form backend? If you’ve built something interesting with external submissions — a plugin feedback form, a static site contact form, a multi-site collection setup — we’d be interested to hear about it. Join our FB community group!