@awesomate/hosting-mcp
Version:
Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases
150 lines (129 loc) • 6.92 kB
Markdown
# WordPress form → n8n webhook: the forwarder pattern
The reliable way to wire a WordPress form to an n8n workflow on this
platform. The form NEVER posts to the n8n webhook from the browser — it
posts to a small server-side forwarder on the WP site, which relays to the
webhook.
Why the forwarder (not a direct browser POST):
- **No CORS** — same-origin from the browser; WP→n8n is server-to-server.
- **The webhook URL stays hidden** from page source and bots.
- **Spam filtering** (honeypot + nonce) happens before n8n ever sees a hit.
- **One place to update** if the webhook path ever changes.
Prerequisites: the n8n workflow exists and is ACTIVE (build it with the
awesomate-n8n build loop first — webhook trigger with explicit path, e.g.
`signup-v1`, `httpMethod: POST`, respond node returning `{"ok": true}`).
WP access comes from the awesomate-hosting skill (SSH + wp-cli).
## 1. The forwarder (mu-plugin — survives theme changes)
Install via SSH as `wp-content/mu-plugins/awm-form-forwarder.php`. Snapshot
the site first (`awesomate_snapshot_site`). Template — replace the webhook
URL and the expected fields:
```php
<?php
/**
* Plugin Name: Awesomate Form Forwarder
* Description: Relays site form submissions server-side to an n8n webhook.
*/
add_action('rest_api_init', function () {
register_rest_route('awm/v1', '/form/signup', [
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => function (WP_REST_Request $req) {
// Honeypot: bots fill every field; humans never see this one.
if (!empty($req->get_param('company_website'))) {
return new WP_REST_Response(['ok' => true], 200); // silent drop
}
// Nonce from the page keeps external scripts out.
if (!wp_verify_nonce($req->get_param('awm_nonce'), 'awm_form_signup')) {
return new WP_REST_Response(['error' => 'Session expired — reload the page'], 403);
}
$email = sanitize_email($req->get_param('email'));
$name = sanitize_text_field($req->get_param('name'));
if (!is_email($email) || $name === '') {
return new WP_REST_Response(['error' => 'Please fill in all fields'], 422);
}
$resp = wp_remote_post('https://SLUG.awesomate.io/webhook/signup-v1', [
'timeout' => 15,
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode([
'name' => $name, 'email' => $email,
'page' => esc_url_raw($req->get_header('referer') ?: ''),
'submitted_at' => gmdate('c'),
]),
]);
if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) >= 400) {
return new WP_REST_Response(['error' => 'Could not submit right now — please email us directly'], 502);
}
return new WP_REST_Response(['ok' => true], 200);
},
]);
});
```
Endpoint: `POST {site}/wp-json/awm/v1/form/signup`. One route per form —
duplicate the `register_rest_route` block with a new path + webhook.
The nonce must be printed into the page. Add alongside the form (per-builder
below) or via `wp_footer`:
```php
add_action('wp_footer', function () {
echo '<script>window.AWM_FORM_NONCE = "' . wp_create_nonce('awm_form_signup') . '";</script>';
});
```
## 2. The form, per builder
Detect the builder first: `wp plugin list` / `wp theme list` (via the
hosting skill). Style with the site's existing design tokens — inspect an
existing page's buttons/inputs and match.
**Bricks**: build a native Bricks form element (fields: name, email; plus a
hidden `company_website` honeypot field). Bricks' built-in submit actions
don't reliably POST custom JSON — attach a small custom JS snippet that
intercepts submit and `fetch()`es the forwarder.
**Elementor Pro**: Form widget → Actions After Submit → **Webhook** → the
FORWARDER URL (not the n8n URL). Add a hidden honeypot field. Nonce: Elementor's
webhook action can't send it — either relax the nonce check to same-origin
referer validation for Elementor sites, or use the custom-JS submit instead.
**Fallback (no pro form plugin)**: HTML block with a hand-built form +
`fetch()`:
```html
<form id="awm-signup" novalidate>
<label>Name <input name="name" required></label>
<label>Email <input name="email" type="email" required></label>
<input name="company_website" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px" aria-hidden="true">
<button type="submit">Sign up</button>
<p class="awm-msg" role="status"></p>
</form>
<script>
document.getElementById('awm-signup').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target, btn = form.querySelector('button'), msg = form.querySelector('.awm-msg');
btn.disabled = true; btn.textContent = 'Sending…';
const data = Object.fromEntries(new FormData(form));
data.awm_nonce = window.AWM_FORM_NONCE || '';
try {
const r = await fetch('/wp-json/awm/v1/form/signup', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
const body = await r.json();
if (r.ok) { form.style.display = 'none'; msg.textContent = 'Thanks — you\'re signed up!'; }
else { msg.textContent = body.error || 'Something went wrong — try again.'; btn.disabled = false; btn.textContent = 'Sign up'; }
} catch { msg.textContent = 'Network error — please try again.'; btn.disabled = false; btn.textContent = 'Sign up'; }
});
</script>
```
UX bar for every variant: disabled button + label change while submitting,
inline thank-you state (swap the form out), and an error state that gives a
fallback contact method.
## 3. End-to-end verification (both hops, in order)
1. n8n hop alone: `awesomate_n8n_test` against the workflow — execution
success, node-by-node inspected.
2. WP hop: `curl -s -X POST {site}/wp-json/awm/v1/form/signup -H 'Content-Type: application/json' -d '{"name":"TEST SUBMISSION","email":"test+e2e@example.com","awm_nonce":"..."}'`
(mint a nonce via wp-cli: `wp eval 'echo wp_create_nonce("awm_form_signup");'`).
3. Confirm a NEW execution appeared for the workflow and the email arrived
(`[TEST]` subject, user's own inbox).
4. Honeypot check: same curl + `"company_website":"http://spam.example"` →
200 ok but NO new execution.
5. Hand the user: page URL, forwarder endpoint, workflow URL, and where the
submissions go.
## 4. When the workflow is upgraded later
The forwarder points at the webhook PATH, and promote preserves it — a
promoted upgrade needs NO WordPress change. Only if a workflow is rebuilt
under a new path does the forwarder's `wp_remote_post` URL need updating
(one line, then re-run step 2-3 above).