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

138 lines (110 loc) • 7.95 kB
# Webhook pages: designed HTML pages served straight from your n8n A webhook can serve a full, beautiful web page — a branded form, a phone camera-capture mini-app the user saves to their home screen, an approval page that resumes a paused workflow, a live status page. No hosting, no CORS, no deploy pipeline: the page exists while the workflow is active. **Never guess the page's hostname.** The live address is `{instanceUrl}/webhook/<path>`, with the path from the webhook trigger you wrote and `instanceUrl` from `awesomate_n8n_context`. Most instances are `https://<slug>.awesomate.io`, but not all of them are, so a hostname built from the slug is a guess that fails silently for the clients on a different domain. Give the user the URL built from `instanceUrl`. **The contract: GET renders, POST acts.** Two webhook trigger nodes on the SAME path — one `httpMethod: GET`, one `POST`, both `responseMode: "responseNode"`. Never let a GET change anything: email scanners and link previewers click every URL they see. A webhook set to `onReceived` refuses to run (500 "Unused Respond to Webhook node") if any Respond node exists in the workflow — always use `responseNode` in page workflows. Exception: in Wait-based approval flows the signed resume URL IS the act side, so the render webhook can be GET-only; add a POST twin only when the page posts back to itself. ## Rendering a page Webhook (GET) → Set "Workflow Config" → IF key gate → Code "Render Page" → Respond to Webhook. - The Code node returns `{ status_code, html }`, escaping every interpolated value with a local `esc()` helper (`& < > "`). - Respond node: `respondWith: "text"`, `responseBody: ={{ $json.html }}`, `options.responseCode: ={{ $json.status_code }}`, header `Content-Type: text/html; charset=utf-8`. Every outcome branch (success, already-done, invalid) must converge into ONE respond node — an unwired branch silently swallows the response. - To pass config to the page's JavaScript safely, embed `<script id="__cfg" type="application/json">BASE64</script>` and read it with `JSON.parse(atob(document.getElementById("__cfg").textContent))` — immune to quote escaping. ## Brand the page from the business variables Start the workflow with a "Workflow Config" Set node (Include Other Fields ON) mapping the brand kit with fallbacks — dot notation only: ``` brand_primary ={{ $vars.brand_primary_color || '' }} brand_secondary ={{ $vars.brand_secondary_color || '' }} brand_font ={{ $vars.brand_font_primary || '' }} logo_url ={{ $vars.biz_logo_url || '' }} biz_name ={{ $vars.biz_name || '' }} ``` In the page, feed these into CSS custom properties (`:root{--brand:…}`) with a tasteful default palette in the Code node for any unset key. Every page you build this way is automatically on-brand for the business, and re-brands itself if the variables change. ## What the browser can and can't do (important) n8n serves your HTML with a security sandbox header (no iframe — your page is delivered verbatim). Measured behavior: - **Works:** JavaScript, `fetch()` back to the same webhook (relative or absolute path, no CORS setup needed), plain `<form method="POST">` submits, links, redirects, phone camera inputs (`<input type="file" accept="image/*" capture>`). - **Blocked (throws SecurityError):** localStorage, cookies, service workers. So: **state rides the URL** — query parameters, hidden form fields, and (for installed mini-apps) the key echoed into the manifest `start_url`. Never rely on localStorage. ## Getting data in — three modes | Mode | The workflow receives | Use for | |---|---|---| | Plain `<form method=POST>` | Parsed fields in `$json.body` | Zero-JS forms, approve/reject buttons | | `fetch` + JSON, files as base64 | `body.files[].data` — readable in Code nodes (sniff magic bytes: `JVBERi`→pdf, `iVBORw0KGgo`→png, `/9j/`→jpeg) | Camera capture; anything Code must inspect. Downscale photos client-side first (canvas, 1600px JPEG q0.85, white background fill) | | Multipart (native file form / FormData) | Binary properties (name/type/size; bytes NOT readable in Code nodes) | Large files; process with Extract From File or HTTP nodes | POST back to `location.pathname` — same origin, zero configuration. Respond with a small HTML page confirming what was received. ## Multi-view apps and assets from one webhook Route on a query parameter (`?view=home|form|manifest|icon`), never on `/:path` segments (those change the URL shape). A Switch node fans out to one Respond node per response type: pages (text/html), a web-app manifest (`application/manifest+json`), even a binary PNG icon (Code nodes can return binary: `{ json, binary: { data: { data: b64, mimeType: 'image/png', fileName } } }`). Redirects return **307**. n8n sets its own cache lifetimes per content type — version assets with a query param, not cache headers. ## Save-to-home-screen mini-apps (phones) - **Always ship an icon.** Serve a 512×512 PNG from an `?view=icon` route and reference it from BOTH the `apple-touch-icon` link and the manifest `icons` entry — without it iOS installs a screenshot of the page and Android shows a letter tile. Use the business logo (`$vars.biz_logo_url`) when set, else a brand-gradient placeholder. - Page head: `<link rel="manifest" href="?view=manifest&key=K">`, `<link rel="apple-touch-icon" href="?view=icon&key=K">`, `apple-mobile-web-app-capable` + `mobile-web-app-capable` + `theme-color` metas. - Manifest: `display: "standalone"`, **absolute** paths everywhere (`start_url: "/webhook/<path>?view=app&key=K"`, `scope: "/webhook/"`, absolute icon src) — a relative `start_url` 404s. **Echo the access key into `start_url`**: it is the only persistence that survives installation. - iPhone: Share → Add to Home Screen gives a standalone branded app. Android: "Add to Home screen" works as a shortcut; the full install prompt needs a service worker, which the sandbox blocks — treat Android as shortcut-only. - iOS snapshots the manifest at install time: if you fix it later, users must remove and re-add the icon. ## Approval pages that resume a paused workflow 1. Before a Wait node, capture `$execution.resumeUrl` (n8n signs it — unguessable) and deliver it: email it, or respond to the start request with a designed page linking to your approval view. 2. Wait node: `resume: "webhook"`, `httpMethod: "POST"`, a wait-time limit, and `responseMode: "responseNode"` (set it even though the UI hides it — without it the approval click gets a JSON ack instead of your page). 3. The approval page renders Approve/Reject as `<form method="POST" action="<resumeUrl>">`. 4. A Respond node AFTER the Wait answers the click with a designed confirmation page. Replaying a used resume URL returns 409 — safe by default. ## Access control - Customer one-shot links: generate a long random token, store only its SHA-256 hash (in a data table or your workflow's database, with `{token_hash, record_id, status, expires_at}`), look up by hash with an expiry, and return a vague 410 for anything unknown. Lookup errors fail CLOSED (410, never render). Sensible expiries: days for record links, hours for approval waits. - Team pages / mini-apps: a key in `$vars`, checked by an **IF node as the first step** (never the webhook's `onlyRunIf` option with `$vars` — it fails open). - Every page request is one workflow execution — designed for human-scale traffic, not public high-volume sites. Inline all CSS/JS and embed images as data URIs (response size is generous — multi-MB pages are fine). ## When NOT to use a webhook page Login sessions and multi-user accounts (no cookies), public high-traffic sites, or a real app with a database UI — those belong on hosting (awesomate-app-builder). A quick internal form with no design needs can stay on n8n's Form node.