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

Measure What it blocks
Minimal plugin count Every plugin not installed = zero CVEs from that plugin
PHP frontend block Web scrapers, bots, and crawlers requesting WordPress pages — works on any server without .htaccess
REST API namespace restriction User enumeration (/wp/v2/users), route discovery, WordPress REST exploits
XML-RPC disabled Brute-force via XML-RPC, pingback DDoS amplification
Hidden login URL Automated 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 name Header value
X-CF-Token your-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.