UNPKG

@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
# 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).