@awesomate/hosting-mcp
Version:
Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases
209 lines (181 loc) • 10.7 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.
**First, is this a sign-up box or a free download?** Then don't build a
forwarder: the hub's own form (`awesomate_crm_forms`, the awesomate-funnels
skill) adds the person to Contacts, emails the download and records their yes
to a list itself, with no n8n and no webhook. Use this forwarder for forms that
form doesn't cover: an enquiry with a message, a quote request, routing to a
person, or a form that has to stay exactly as it is on the site.
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).
## Where each submission goes
When `awesomate_get_context` says `features.contacts.available` is true, the
workflow puts the person into Contacts with the Awesomate node
([awesomate-node.md](awesomate-node.md)), never into a data table:
1. **Contact, Create or Update**, tagged with what the form is for
(`web-enquiry`, `asked-for-news`). One record per email address.
2. A notice to the owner: **Task, Give**, or an email to their own address
(that needs an email credential in their n8n).
3. **Contact, Record Event** (for example `enquiry_received`, with the
forwarder's `event_key` as the key) when the owner wants an automatic
reply: they make it in the hub as a service email series that starts on
that event, and approve and switch it on there. It goes last, so an
event the hub refuses (its hourly limit answers 429) still leaves the
person in Contacts and the owner told.
Never email the person from Gmail or SMTP in n8n (no welcome, no newsletter,
no offer): Contacts sends from the business's own name and knows who
unsubscribed or bounced, and n8n doesn't.
**A "send me your news" box never puts anyone on a list.** Only the person's
own Yes does, and the node has no list operation on purpose. Keep the box
unticked, beside the list's own name and promise; the workflow tags the
person (`asked-for-news`), and the owner sends them the invite from Contacts,
People (**Add to email list**), which they confirm with one click. A form
whose main job is the sign-up is better as the hub's own form
(`awesomate_crm_forms`), which records the yes by itself. Recipes:
awesomate-app-builder `references/solutions.md` (capture-leads-and-follow-up,
newsletter-signup).
Accounts without Contacts keep the data table, with a notice to the owner.
## 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 and try again.'], 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,
// The box is unticked on the page; a tick only tags the person for the owner's invite.
// Not a (bool) cast: that reads "false" or "off" as a tick.
'wants_news' => filter_var($req->get_param('wants_news'), FILTER_VALIDATE_BOOLEAN),
'page' => esc_url_raw($req->get_header('referer') ?: ''),
// Record Event's key: one per person per day, so a resubmit after a timeout is recorded once.
'event_key' => hash('sha256', strtolower($email) . '|' . gmdate('Y-m-d')),
'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, so 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, so 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>
<!-- Optional. Unticked; use the list's own name and promise as its words. -->
<label><input name="wants_news" type="checkbox" value="1"> Also send me LIST NAME: LIST PROMISE</label>
<input name="company_website" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px" aria-hidden="true">
<button type="submit">Sign up</button>
</form>
<!-- Outside the form, so the thank-you still shows once the form is hidden. -->
<p class="awm-msg" role="status"></p>
<script>
document.getElementById('awm-signup').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target, btn = form.querySelector('button'), msg = form.nextElementSibling;
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 = data.wants_news ? 'Thanks, we\'ve got it. We\'ll only add you to our list once you\'ve confirmed by email.' : 'Thanks, we\'ve got it.';
}
else { msg.textContent = body.error || 'Something went wrong. Please 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. The thank-you promises only what is sure to
happen: never "you're subscribed", because nobody is on a list until they
confirm, and never "we've emailed you", because the owner sends the invite.
## 3. End-to-end verification (both hops, in order)
1. n8n hop alone: fire the workflow (the n8n MCP server's execute tool, or a
POST to its production webhook) and read the run with
`awesomate_n8n_executions`: 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 owner's notice
arrived. With Contacts: the test person shows once on Contacts, People,
with the tag and on no list. Test with a tag and an event no live series
starts on (a series can start on either), or before such a series is
switched on, or the test starts it and sends real email.
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 (Contacts, People, by the tag), plus the invite step on
that page when the form has a news box.
## 4. When the workflow is upgraded later
The forwarder points at the webhook PATH. A change applied to the live
workflow on its own id (SKILL.md §4) keeps that path, so an 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).