@awesomate/hosting-mcp 0.20.4 → 0.20.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +57 -28
- package/package.json +1 -1
- package/skill/CHANGELOG.json +16 -2
- package/skill/awesomate-hosting/SKILL.md +1 -0
- package/skill/awesomate-n8n/SKILL.md +1 -0
- package/skill/awesomate-n8n/evals/webhook-page-build/graders/grader.md +36 -0
- package/skill/awesomate-n8n/evals/webhook-page-build/prompt.md +1 -0
- package/skill/awesomate-n8n/references/webhook-pages.md +137 -0
- package/skill/awesomate-seo/SKILL.md +36 -11
- package/skill/awesomate-seo/references/ai-discoverability.md +50 -7
- package/skill/awesomate-seo/references/answer-structure.md +128 -0
package/dist/index.js
CHANGED
|
@@ -3651,49 +3651,49 @@ var require_fast_uri = __commonJS({
|
|
|
3651
3651
|
schemelessOptions.skipEscape = true;
|
|
3652
3652
|
return serialize(resolved, schemelessOptions);
|
|
3653
3653
|
}
|
|
3654
|
-
function resolveComponent(base,
|
|
3654
|
+
function resolveComponent(base, relative2, options, skipNormalization) {
|
|
3655
3655
|
const target = {};
|
|
3656
3656
|
if (!skipNormalization) {
|
|
3657
3657
|
base = parse3(serialize(base, options), options);
|
|
3658
|
-
|
|
3658
|
+
relative2 = parse3(serialize(relative2, options), options);
|
|
3659
3659
|
}
|
|
3660
3660
|
options = options || {};
|
|
3661
|
-
if (!options.tolerant &&
|
|
3662
|
-
target.scheme =
|
|
3663
|
-
target.userinfo =
|
|
3664
|
-
target.host =
|
|
3665
|
-
target.port =
|
|
3666
|
-
target.path = removeDotSegments(
|
|
3667
|
-
target.query =
|
|
3661
|
+
if (!options.tolerant && relative2.scheme) {
|
|
3662
|
+
target.scheme = relative2.scheme;
|
|
3663
|
+
target.userinfo = relative2.userinfo;
|
|
3664
|
+
target.host = relative2.host;
|
|
3665
|
+
target.port = relative2.port;
|
|
3666
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
3667
|
+
target.query = relative2.query;
|
|
3668
3668
|
} else {
|
|
3669
|
-
if (
|
|
3670
|
-
target.userinfo =
|
|
3671
|
-
target.host =
|
|
3672
|
-
target.port =
|
|
3673
|
-
target.path = removeDotSegments(
|
|
3674
|
-
target.query =
|
|
3669
|
+
if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
|
|
3670
|
+
target.userinfo = relative2.userinfo;
|
|
3671
|
+
target.host = relative2.host;
|
|
3672
|
+
target.port = relative2.port;
|
|
3673
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
3674
|
+
target.query = relative2.query;
|
|
3675
3675
|
} else {
|
|
3676
|
-
if (!
|
|
3676
|
+
if (!relative2.path) {
|
|
3677
3677
|
target.path = base.path;
|
|
3678
|
-
if (
|
|
3679
|
-
target.query =
|
|
3678
|
+
if (relative2.query !== void 0) {
|
|
3679
|
+
target.query = relative2.query;
|
|
3680
3680
|
} else {
|
|
3681
3681
|
target.query = base.query;
|
|
3682
3682
|
}
|
|
3683
3683
|
} else {
|
|
3684
|
-
if (
|
|
3685
|
-
target.path = removeDotSegments(
|
|
3684
|
+
if (relative2.path[0] === "/") {
|
|
3685
|
+
target.path = removeDotSegments(relative2.path);
|
|
3686
3686
|
} else {
|
|
3687
3687
|
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
|
|
3688
|
-
target.path = "/" +
|
|
3688
|
+
target.path = "/" + relative2.path;
|
|
3689
3689
|
} else if (!base.path) {
|
|
3690
|
-
target.path =
|
|
3690
|
+
target.path = relative2.path;
|
|
3691
3691
|
} else {
|
|
3692
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3692
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
|
|
3693
3693
|
}
|
|
3694
3694
|
target.path = removeDotSegments(target.path);
|
|
3695
3695
|
}
|
|
3696
|
-
target.query =
|
|
3696
|
+
target.query = relative2.query;
|
|
3697
3697
|
}
|
|
3698
3698
|
target.userinfo = base.userinfo;
|
|
3699
3699
|
target.host = base.host;
|
|
@@ -3701,7 +3701,7 @@ var require_fast_uri = __commonJS({
|
|
|
3701
3701
|
}
|
|
3702
3702
|
target.scheme = base.scheme;
|
|
3703
3703
|
}
|
|
3704
|
-
target.fragment =
|
|
3704
|
+
target.fragment = relative2.fragment;
|
|
3705
3705
|
return target;
|
|
3706
3706
|
}
|
|
3707
3707
|
function equal(uriA, uriB, options) {
|
|
@@ -39994,7 +39994,7 @@ function hubDelete(config3, path, jsonBody) {
|
|
|
39994
39994
|
// src/skills.ts
|
|
39995
39995
|
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
39996
39996
|
import { homedir as homedir2 } from "node:os";
|
|
39997
|
-
import { join as join2, dirname as dirname2 } from "node:path";
|
|
39997
|
+
import { join as join2, dirname as dirname2, relative, sep } from "node:path";
|
|
39998
39998
|
import { fileURLToPath } from "node:url";
|
|
39999
39999
|
var PACKAGE_ROOT = join2(dirname2(fileURLToPath(import.meta.url)), "..");
|
|
40000
40000
|
var DEFAULT_SOURCE_ROOT = join2(PACKAGE_ROOT, "skill");
|
|
@@ -40015,6 +40015,9 @@ function installedSkillVersion(name) {
|
|
|
40015
40015
|
function skillVersions(sourceRoot = DEFAULT_SOURCE_ROOT) {
|
|
40016
40016
|
return bundledSkillNames(sourceRoot).map((name) => ({ name, installed: installedSkillVersion(name) }));
|
|
40017
40017
|
}
|
|
40018
|
+
function isInstallable(relPath) {
|
|
40019
|
+
return !relPath.split("/").includes("evals");
|
|
40020
|
+
}
|
|
40018
40021
|
function walkFiles(dir, prefix = "") {
|
|
40019
40022
|
const out = [];
|
|
40020
40023
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -40035,9 +40038,15 @@ function installSkills(version2, sourceRoot = DEFAULT_SOURCE_ROOT) {
|
|
|
40035
40038
|
if (!existsSync2(join2(src, "SKILL.md"))) continue;
|
|
40036
40039
|
const dest = join2(destRoot, entry.name);
|
|
40037
40040
|
const from = installedSkillVersion(entry.name);
|
|
40038
|
-
cpSync(src, dest, {
|
|
40041
|
+
cpSync(src, dest, {
|
|
40042
|
+
recursive: true,
|
|
40043
|
+
filter: (from2) => {
|
|
40044
|
+
const rel = relative(src, from2);
|
|
40045
|
+
return rel === "" || isInstallable(rel.split(sep).join("/"));
|
|
40046
|
+
}
|
|
40047
|
+
});
|
|
40039
40048
|
try {
|
|
40040
|
-
const shipped = new Set(walkFiles(src));
|
|
40049
|
+
const shipped = new Set(walkFiles(src).filter(isInstallable));
|
|
40041
40050
|
for (const rel of walkFiles(dest)) {
|
|
40042
40051
|
if (rel === ".installed-version") continue;
|
|
40043
40052
|
if (!shipped.has(rel)) {
|
|
@@ -41868,6 +41877,26 @@ server.registerTool(
|
|
|
41868
41877
|
}
|
|
41869
41878
|
}
|
|
41870
41879
|
);
|
|
41880
|
+
server.registerTool(
|
|
41881
|
+
"awesomate_site_audit",
|
|
41882
|
+
{
|
|
41883
|
+
description: "Run the SEO + AEO build gates against ONE live page the user owns, fetched AS GPTBot. Returns pass/fail per gate with specific fixes: is the content in the raw HTML at all (a React/Vue site is invisible to ChatGPT, Claude and Perplexity \u2014 only Gemini and Applebot run JavaScript), can the four retrieval bots reach it, is there an accidental noindex/nosnippet, does the canonical point at THIS domain (a leftover pointing at a Replit/Vercel/staging host silently de-indexes the real site), is the structured data valid AND mirrored in visible text, is the content structured so a single section survives being quoted, robots.txt + sitemap, and time-to-first-byte. Run it after building or changing any public page, and before telling the user their site is discoverable \u2014 reads on every plan. Two gates are reported as `skipped` because they need a headless browser or a full crawl; treat skipped as unknown, never as passing.",
|
|
41884
|
+
inputSchema: {
|
|
41885
|
+
domain: external_exports.string().min(3).max(253).describe("a domain on the user's own hosting account"),
|
|
41886
|
+
path: external_exports.string().max(400).optional().describe("page path to audit, default /")
|
|
41887
|
+
},
|
|
41888
|
+
annotations: READ_ONLY
|
|
41889
|
+
},
|
|
41890
|
+
async ({ domain, path }) => {
|
|
41891
|
+
try {
|
|
41892
|
+
const dom = encodeURIComponent(domain.toLowerCase());
|
|
41893
|
+
const q = path && path !== "/" ? `?path=${encodeURIComponent(path)}` : "";
|
|
41894
|
+
return textResult(await hubGet(requireConfig(), `/api/client-hosting/sites/${dom}/audit${q}`));
|
|
41895
|
+
} catch (err) {
|
|
41896
|
+
return errorResult(err);
|
|
41897
|
+
}
|
|
41898
|
+
}
|
|
41899
|
+
);
|
|
41871
41900
|
server.registerTool(
|
|
41872
41901
|
"awesomate_wp_media_import",
|
|
41873
41902
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awesomate/hosting-mcp",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.6",
|
|
4
4
|
"description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/skill/CHANGELOG.json
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.20.6",
|
|
5
|
+
"highlights": [
|
|
6
|
+
"Ask Claude to check any page and it now runs a full findability audit - whether AI assistants can actually read and quote it, not just whether Google can find it",
|
|
7
|
+
"Catches the faults that silently make a site invisible to ChatGPT and Perplexity, like content that only appears once the browser runs it"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.20.5",
|
|
12
|
+
"highlights": [
|
|
13
|
+
"Claude can build you a real web page served straight from your n8n - a branded form, a phone camera app your team saves to their home screen, or an approval page - with no hosting to set up",
|
|
14
|
+
"Your skills folder stays clean: our internal test files are no longer copied onto your machine"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
3
17
|
{
|
|
4
18
|
"version": "0.20.3",
|
|
5
19
|
"highlights": [
|
|
@@ -54,13 +68,13 @@
|
|
|
54
68
|
"highlights": [
|
|
55
69
|
"Search your knowledge base with filters and ask questions within a slice of your content",
|
|
56
70
|
"People & entities: your knowledge base now recognises who appears in your content, and you can name and merge them",
|
|
57
|
-
"Build AI agents from your own content
|
|
71
|
+
"Build AI agents from your own content — drafted, tested privately, and published only when you approve"
|
|
58
72
|
]
|
|
59
73
|
},
|
|
60
74
|
{
|
|
61
75
|
"version": "0.14.0",
|
|
62
76
|
"highlights": [
|
|
63
|
-
"Tools built for business owners, not developers
|
|
77
|
+
"Tools built for business owners, not developers — plain-language answers about your account, plan and limits",
|
|
64
78
|
"New support skill: get help, understand credits, or request a done-for-you build without leaving Claude",
|
|
65
79
|
"New database skill: Claude picks the right place to keep your data and sets it up for you"
|
|
66
80
|
]
|
|
@@ -105,6 +105,7 @@ proxy failures, resuming a connect, support report) ·
|
|
|
105
105
|
| `awesomate_domain_add` | Attach a custom domain to a site |
|
|
106
106
|
| `awesomate_dns_check` | Live answer to "where does this domain actually point" — before and after any DNS change |
|
|
107
107
|
| `awesomate_site_uptime` | 30-day availability, incidents, downtime per domain (cached, free) |
|
|
108
|
+
| `awesomate_site_audit` | SEO + AEO build gates for one page, fetched as GPTBot — content present without JS, retrieval bots reachable, canonical, schema, structure, speed. Not plan-gated. Run it after any public-page change, and see the awesomate-seo skill for the fixes |
|
|
108
109
|
| `awesomate_snapshot_site` | Before ANY change to a live site — files + DB restore point |
|
|
109
110
|
| `awesomate_list_snapshots` | What restore points a site has |
|
|
110
111
|
| `awesomate_rollback_site` | Restore a snapshot after something went wrong (confirm first) |
|
|
@@ -118,6 +118,7 @@ they're blocked on a restart.
|
|
|
118
118
|
| Changing a LIVE workflow | [upgrade-loop.md](references/upgrade-loop.md) |
|
|
119
119
|
| "What could I automate?" | [possibilities.md](references/possibilities.md) |
|
|
120
120
|
| WordPress form → n8n | [wp-form-handler.md](references/wp-form-handler.md) |
|
|
121
|
+
| A designed page served BY n8n — form, phone capture mini-app, approval page, status page | [webhook-pages.md](references/webhook-pages.md) |
|
|
121
122
|
| Expression syntax details | [vendor/expressions/](references/vendor/expressions/SKILL.md) |
|
|
122
123
|
| Code-node JavaScript | [vendor/code-node/](references/vendor/code-node/SKILL.md) |
|
|
123
124
|
| Node configuration patterns | [vendor/node-configuration/](references/vendor/node-configuration/SKILL.md) |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Grader: webhook-page-build
|
|
2
|
+
|
|
3
|
+
Judge the transcript (assistant text, tool calls, tool results) against every criterion.
|
|
4
|
+
Each criterion must be verifiable from the transcript alone — do not infer intent.
|
|
5
|
+
Verdict: PASS only if all numbered criteria hold and no automatic fail triggers.
|
|
6
|
+
|
|
7
|
+
## Pass criteria
|
|
8
|
+
|
|
9
|
+
1. The first `awesomate_n8n_*` tool call in the transcript is `awesomate_n8n_context`.
|
|
10
|
+
Non-n8n tool calls may precede it.
|
|
11
|
+
2. The design uses TWO webhook trigger nodes on the SAME path — one GET (renders the
|
|
12
|
+
page) and one POST (receives the photos) — both with `responseMode: "responseNode"`.
|
|
13
|
+
No Form Trigger anywhere.
|
|
14
|
+
3. Branding is grounded in the business variables: the design includes a Set/config node
|
|
15
|
+
mapping `$vars.brand_primary_color` / `$vars.brand_secondary_color` /
|
|
16
|
+
`$vars.biz_logo_url` (or explicitly checks the live variables via
|
|
17
|
+
`awesomate_n8n_inspect {what:'variables'}`), each with a fallback — never hardcoded
|
|
18
|
+
colors presented as "your brand" without checking.
|
|
19
|
+
4. Save-to-home-screen is handled correctly: a manifest served from the same webhook via
|
|
20
|
+
a query parameter, with an ABSOLUTE `start_url` under `/webhook/`, and the access key
|
|
21
|
+
echoed into `start_url`. The design does NOT rely on localStorage for the key.
|
|
22
|
+
5. Photo upload uses client-side downscale (canvas → JPEG) POSTed as base64 JSON to the
|
|
23
|
+
same webhook path — or the assistant explicitly justifies multipart binary and routes
|
|
24
|
+
processing through non-Code nodes.
|
|
25
|
+
6. Access control is an IF-node key gate (key from `$vars` or a stored value) — not the
|
|
26
|
+
webhook's `onlyRunIf` option referencing `$vars`.
|
|
27
|
+
7. The design is presented for approval before any `create_draft` call, and testing is
|
|
28
|
+
described against the LIVE published URL (curl or equivalent), not validation alone.
|
|
29
|
+
|
|
30
|
+
## Automatic fails
|
|
31
|
+
|
|
32
|
+
- Any reliance on localStorage/cookies/service workers without a stated fallback.
|
|
33
|
+
- `start_url` or manifest icon given as a relative path (".", "./", bare filename).
|
|
34
|
+
- `create_draft`, `activate`, or `awesomate_n8n_test` called before user approval.
|
|
35
|
+
- Proposes external hosting (separate site, S3, static host) for the page itself.
|
|
36
|
+
- Asks the user for their n8n API key.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Build me a page my technicians can open on their phones to snap photos of completed jobs. It should look like our brand, they should be able to save it to their home screen, and the photos should end up in my n8n so I can process them.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Webhook pages: designed HTML pages served straight from your n8n
|
|
2
|
+
|
|
3
|
+
A webhook can serve a full, beautiful web page — a branded form, a phone camera-capture
|
|
4
|
+
mini-app the user saves to their home screen, an approval page that resumes a paused
|
|
5
|
+
workflow, a live status page. No hosting, no CORS, no deploy pipeline: the page exists
|
|
6
|
+
while the workflow is active.
|
|
7
|
+
|
|
8
|
+
**Never hand-build the page's URL.** `awesomate_n8n_deploy` returns a `webhooks` array —
|
|
9
|
+
`{ node, url, method }` per trigger — and that `url` is the live address. Most instances
|
|
10
|
+
are `https://<slug>.awesomate.io`, but not all of them are, so a constructed hostname is
|
|
11
|
+
a guess that fails silently for the clients on a different domain. `awesomate_n8n_context`
|
|
12
|
+
carries the same truth as `instanceUrl`. Give the user the URL you were handed.
|
|
13
|
+
|
|
14
|
+
**The contract: GET renders, POST acts.** Two webhook trigger nodes on the SAME path —
|
|
15
|
+
one `httpMethod: GET`, one `POST`, both `responseMode: "responseNode"`. Never let a GET
|
|
16
|
+
change anything: email scanners and link previewers click every URL they see. A webhook
|
|
17
|
+
set to `onReceived` refuses to run (500 "Unused Respond to Webhook node") if any Respond
|
|
18
|
+
node exists in the workflow — always use `responseNode` in page workflows. Exception: in
|
|
19
|
+
Wait-based approval flows the signed resume URL IS the act side, so the render webhook can be
|
|
20
|
+
GET-only; add a POST twin only when the page posts back to itself.
|
|
21
|
+
|
|
22
|
+
## Rendering a page
|
|
23
|
+
|
|
24
|
+
Webhook (GET) → Set "Workflow Config" → IF key gate → Code "Render Page" → Respond to Webhook.
|
|
25
|
+
|
|
26
|
+
- The Code node returns `{ status_code, html }`, escaping every interpolated value with a
|
|
27
|
+
local `esc()` helper (`& < > "`).
|
|
28
|
+
- Respond node: `respondWith: "text"`, `responseBody: ={{ $json.html }}`,
|
|
29
|
+
`options.responseCode: ={{ $json.status_code }}`, header
|
|
30
|
+
`Content-Type: text/html; charset=utf-8`. Every outcome branch (success, already-done,
|
|
31
|
+
invalid) must converge into ONE respond node — an unwired branch silently swallows the
|
|
32
|
+
response.
|
|
33
|
+
- To pass config to the page's JavaScript safely, embed
|
|
34
|
+
`<script id="__cfg" type="application/json">BASE64</script>` and read it with
|
|
35
|
+
`JSON.parse(atob(document.getElementById("__cfg").textContent))` — immune to quote
|
|
36
|
+
escaping.
|
|
37
|
+
|
|
38
|
+
## Brand the page from the business variables
|
|
39
|
+
|
|
40
|
+
Start the workflow with a "Workflow Config" Set node (Include Other Fields ON) mapping the
|
|
41
|
+
brand kit with fallbacks — dot notation only:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
brand_primary ={{ $vars.brand_primary_color || '' }}
|
|
45
|
+
brand_secondary ={{ $vars.brand_secondary_color || '' }}
|
|
46
|
+
brand_font ={{ $vars.brand_font_primary || '' }}
|
|
47
|
+
logo_url ={{ $vars.biz_logo_url || '' }}
|
|
48
|
+
biz_name ={{ $vars.biz_name || '' }}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
In the page, feed these into CSS custom properties (`:root{--brand:…}`) with a tasteful
|
|
52
|
+
default palette in the Code node for any unset key. Every page you build this way is
|
|
53
|
+
automatically on-brand for the business, and re-brands itself if the variables change.
|
|
54
|
+
|
|
55
|
+
## What the browser can and can't do (important)
|
|
56
|
+
|
|
57
|
+
n8n serves your HTML with a security sandbox header (no iframe — your page is delivered
|
|
58
|
+
verbatim). Measured behavior:
|
|
59
|
+
|
|
60
|
+
- **Works:** JavaScript, `fetch()` back to the same webhook (relative or absolute path,
|
|
61
|
+
no CORS setup needed), plain `<form method="POST">` submits, links, redirects, phone
|
|
62
|
+
camera inputs (`<input type="file" accept="image/*" capture>`).
|
|
63
|
+
- **Blocked (throws SecurityError):** localStorage, cookies, service workers.
|
|
64
|
+
|
|
65
|
+
So: **state rides the URL** — query parameters, hidden form fields, and (for installed
|
|
66
|
+
mini-apps) the key echoed into the manifest `start_url`. Never rely on localStorage.
|
|
67
|
+
|
|
68
|
+
## Getting data in — three modes
|
|
69
|
+
|
|
70
|
+
| Mode | The workflow receives | Use for |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| Plain `<form method=POST>` | Parsed fields in `$json.body` | Zero-JS forms, approve/reject buttons |
|
|
73
|
+
| `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) |
|
|
74
|
+
| 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 |
|
|
75
|
+
|
|
76
|
+
POST back to `location.pathname` — same origin, zero configuration. Respond with a small
|
|
77
|
+
HTML page confirming what was received.
|
|
78
|
+
|
|
79
|
+
## Multi-view apps and assets from one webhook
|
|
80
|
+
|
|
81
|
+
Route on a query parameter (`?view=home|form|manifest|icon`), never on `/:path` segments
|
|
82
|
+
(those change the URL shape). A Switch node fans out to one Respond node per response
|
|
83
|
+
type: pages (text/html), a web-app manifest (`application/manifest+json`), even a binary
|
|
84
|
+
PNG icon (Code nodes can return binary:
|
|
85
|
+
`{ json, binary: { data: { data: b64, mimeType: 'image/png', fileName } } }`). Redirects
|
|
86
|
+
return **307**. n8n sets its own cache lifetimes per content type — version assets with a
|
|
87
|
+
query param, not cache headers.
|
|
88
|
+
|
|
89
|
+
## Save-to-home-screen mini-apps (phones)
|
|
90
|
+
|
|
91
|
+
- **Always ship an icon.** Serve a 512×512 PNG from an `?view=icon` route and reference it
|
|
92
|
+
from BOTH the `apple-touch-icon` link and the manifest `icons` entry — without it iOS
|
|
93
|
+
installs a screenshot of the page and Android shows a letter tile. Use the business logo
|
|
94
|
+
(`$vars.biz_logo_url`) when set, else a brand-gradient placeholder.
|
|
95
|
+
- Page head: `<link rel="manifest" href="?view=manifest&key=K">`,
|
|
96
|
+
`<link rel="apple-touch-icon" href="?view=icon&key=K">`,
|
|
97
|
+
`apple-mobile-web-app-capable` + `mobile-web-app-capable` + `theme-color` metas.
|
|
98
|
+
- Manifest: `display: "standalone"`, **absolute** paths everywhere
|
|
99
|
+
(`start_url: "/webhook/<path>?view=app&key=K"`, `scope: "/webhook/"`, absolute icon
|
|
100
|
+
src) — a relative `start_url` 404s. **Echo the access key into `start_url`**: it is the
|
|
101
|
+
only persistence that survives installation.
|
|
102
|
+
- iPhone: Share → Add to Home Screen gives a standalone branded app. Android: "Add to
|
|
103
|
+
Home screen" works as a shortcut; the full install prompt needs a service worker, which
|
|
104
|
+
the sandbox blocks — treat Android as shortcut-only.
|
|
105
|
+
- iOS snapshots the manifest at install time: if you fix it later, users must remove and
|
|
106
|
+
re-add the icon.
|
|
107
|
+
|
|
108
|
+
## Approval pages that resume a paused workflow
|
|
109
|
+
|
|
110
|
+
1. Before a Wait node, capture `$execution.resumeUrl` (n8n signs it — unguessable) and
|
|
111
|
+
deliver it: email it, or respond to the start request with a designed page linking to
|
|
112
|
+
your approval view.
|
|
113
|
+
2. Wait node: `resume: "webhook"`, `httpMethod: "POST"`, a wait-time limit, and
|
|
114
|
+
`responseMode: "responseNode"` (set it even though the UI hides it — without it the
|
|
115
|
+
approval click gets a JSON ack instead of your page).
|
|
116
|
+
3. The approval page renders Approve/Reject as `<form method="POST" action="<resumeUrl>">`.
|
|
117
|
+
4. A Respond node AFTER the Wait answers the click with a designed confirmation page.
|
|
118
|
+
Replaying a used resume URL returns 409 — safe by default.
|
|
119
|
+
|
|
120
|
+
## Access control
|
|
121
|
+
|
|
122
|
+
- Customer one-shot links: generate a long random token, store only its SHA-256 hash
|
|
123
|
+
(in a data table or your workflow's database, with `{token_hash, record_id, status,
|
|
124
|
+
expires_at}`), look up by hash with an expiry, and return a vague 410 for anything
|
|
125
|
+
unknown. Lookup errors fail CLOSED (410, never render). Sensible expiries: days for
|
|
126
|
+
record links, hours for approval waits.
|
|
127
|
+
- Team pages / mini-apps: a key in `$vars`, checked by an **IF node as the first step**
|
|
128
|
+
(never the webhook's `onlyRunIf` option with `$vars` — it fails open).
|
|
129
|
+
- Every page request is one workflow execution — designed for human-scale traffic, not
|
|
130
|
+
public high-volume sites. Inline all CSS/JS and embed images as data URIs (response
|
|
131
|
+
size is generous — multi-MB pages are fine).
|
|
132
|
+
|
|
133
|
+
## When NOT to use a webhook page
|
|
134
|
+
|
|
135
|
+
Login sessions and multi-user accounts (no cookies), public high-traffic sites, or a real
|
|
136
|
+
app with a database UI — those belong on hosting (awesomate-app-builder). A quick internal
|
|
137
|
+
form with no design needs can stay on n8n's Form node.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: awesomate-seo
|
|
3
|
-
description: Make the user's site findable
|
|
3
|
+
description: Make the user's site findable by Google AND quotable by AI assistants (SEO + AEO). Runs the build gates with awesomate_site_audit, then fixes what fails: title/meta/Open Graph, sitemap.xml, robots.txt, canonical tags, JSON-LD, answer-shaped content structure, and llms.txt. Works for static sites, Node apps, and WordPress. Use when the user says "SEO", "AEO", "GEO", "answer engine optimisation", "get found on Google", "get cited by ChatGPT", "rank higher", "make my site discoverable", "sitemap", "meta tags", "schema", or after building/changing any public page. Companion to awesomate-app-builder.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Awesomate SEO — get found on Google and AI
|
|
@@ -43,11 +43,13 @@ branch honestly.
|
|
|
43
43
|
`plugin list`) — the whole route is behind the write gate, so "just reading
|
|
44
44
|
the options" is not a way round it. The account-level reads do work
|
|
45
45
|
(`awesomate_get_context`, `awesomate_list_sites`, `awesomate_list_domains`,
|
|
46
|
-
`awesomate_dns_check`, `awesomate_site_uptime
|
|
46
|
+
`awesomate_dns_check`, `awesomate_site_uptime`, and **`awesomate_site_audit`** —
|
|
47
|
+
the full gate sweep works on every plan, because it reads the public page).
|
|
47
48
|
|
|
48
49
|
**What Essentials CAN do — which is most of the value:** audit and advise
|
|
49
|
-
from the outside. `
|
|
50
|
-
|
|
50
|
+
from the outside. Start with `awesomate_site_audit` — it runs the whole sweep
|
|
51
|
+
in one call and is NOT plan-gated, so an Essentials user gets exactly the same
|
|
52
|
+
findings as anyone else. Then produce the exact changes —
|
|
51
53
|
the titles and descriptions to paste, the JSON-LD block, the llms.txt
|
|
52
54
|
content, which plugin to install and where its settings live. They apply
|
|
53
55
|
them in wp-admin; you verify by fetching the live page again. Say that up
|
|
@@ -67,12 +69,18 @@ formats + length limits. In short, per site:
|
|
|
67
69
|
3. **`sitemap.xml`** at the root — list every real URL with `<loc>` (+ `<lastmod>`).
|
|
68
70
|
4. **JSON-LD** (`<script type="application/ld+json">`) — the right schema.org
|
|
69
71
|
type (`Organization` / `WebSite` / `Product` / `Article` / `LocalBusiness`).
|
|
70
|
-
5.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
5. **Answer-shaped content** — the part that decides whether the page is
|
|
73
|
+
actually *quoted* rather than merely found: answer-first sections of 120–180
|
|
74
|
+
words, no section opening with "it"/"this"/"also", comparisons in a real
|
|
75
|
+
`<table>`, specific numbers and dates inline, and every schema fact also
|
|
76
|
+
present in the visible copy.
|
|
77
|
+
Full discipline: [references/answer-structure.md](references/answer-structure.md).
|
|
78
|
+
This is the highest-leverage item on the list and the one most often skipped.
|
|
79
|
+
6. **`llms.txt`** at the root — cheap, so write it, but keep it in proportion:
|
|
80
|
+
Google ignores it entirely and one study of 137,210 domains found 97% of
|
|
81
|
+
published files got zero requests. Generate it, never present it as a pillar,
|
|
82
|
+
never bill for it. Crawler/rendering gotchas:
|
|
83
|
+
[references/ai-discoverability.md](references/ai-discoverability.md).
|
|
76
84
|
|
|
77
85
|
Then commit + deploy (awesomate-github). On content changes, update the
|
|
78
86
|
affected `<head>` tags, `sitemap.xml` `<lastmod>`, and `llms.txt`.
|
|
@@ -142,7 +150,24 @@ On WordPress those three come from:
|
|
|
142
150
|
the robots rules, the JSON-LD block, and tell them precisely where each goes
|
|
143
151
|
— that is the useful half, and it works on every plan.
|
|
144
152
|
|
|
145
|
-
## 3. Verify
|
|
153
|
+
## 3. Verify — run the gates, don't eyeball it
|
|
154
|
+
|
|
155
|
+
**`awesomate_site_audit { domain, path }`** runs the mechanical gates against
|
|
156
|
+
the live page, fetched *as GPTBot*, and returns pass/fail with specific fixes:
|
|
157
|
+
content present without JavaScript, all four retrieval bots reachable, no
|
|
158
|
+
accidental `noindex`/`nosnippet`, canonical pointing at this domain, valid
|
|
159
|
+
structured data whose facts also appear in the copy, section structure,
|
|
160
|
+
robots.txt + sitemap, and time-to-first-byte.
|
|
161
|
+
|
|
162
|
+
Run it after building or changing any public page, and **before telling the user
|
|
163
|
+
their site is discoverable**. Checking in a browser is not equivalent — the
|
|
164
|
+
browser runs the JavaScript that hides the biggest failure.
|
|
165
|
+
|
|
166
|
+
Two gates come back `skipped` because they need a headless browser (content
|
|
167
|
+
revealed only on click) or a full crawl (internal 404s). **Skipped means
|
|
168
|
+
unknown, not passing** — say so rather than reporting a clean sweep.
|
|
169
|
+
|
|
170
|
+
Then confirm by hand what the tool can't judge:
|
|
146
171
|
- `robots.txt` and `sitemap.xml` (or the WP sitemap) return 200 at the root.
|
|
147
172
|
- Each key page has a unique title + description and valid JSON-LD (no syntax
|
|
148
173
|
errors — a broken `application/ld+json` block is worse than none).
|
|
@@ -23,7 +23,7 @@ it, but present it as that, never as a Google requirement.
|
|
|
23
23
|
|
|
24
24
|
| Kind | Bots | Blocking means |
|
|
25
25
|
|---|---|---|
|
|
26
|
-
| Search-indexing | OAI-SearchBot, Claude-SearchBot, PerplexityBot | The site can't be cited in that assistant's answers. **Allow these.** |
|
|
26
|
+
| Search-indexing | OAI-SearchBot, Claude-SearchBot, PerplexityBot, **Bingbot** | The site can't be cited in that assistant's answers. **Allow these.** Bingbot belongs here because Copilot and several assistants ground on Bing's index. |
|
|
27
27
|
| Training | GPTBot, ClaudeBot, Google-Extended, CCBot, Applebot-Extended, Meta-ExternalAgent | Owner's policy choice about model training; doesn't remove the site from answers |
|
|
28
28
|
| User-triggered retrieval | ChatGPT-User, Perplexity-User, Claude-User | Fetches when a live user asks; may not fully respect robots.txt |
|
|
29
29
|
|
|
@@ -75,14 +75,34 @@ the edge. Where to fix it:
|
|
|
75
75
|
- **Awesomate-managed domain** (`*.awesomate.site` etc.): the zone is managed
|
|
76
76
|
by Awesomate — the user can't change it. Raise it via `awesomate_support`
|
|
77
77
|
so the zone's AI Crawl Control settings get reviewed.
|
|
78
|
-
- **Custom domain on the user's own Cloudflare account**:
|
|
79
|
-
|
|
78
|
+
- **Custom domain on the user's own Cloudflare account**: fix it with a WAF
|
|
79
|
+
custom rule, action **Skip**, expression:
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
```
|
|
82
|
+
cf.verified_bot_category in {"AI Crawler" "Search Engine Crawler"}
|
|
83
|
+
```
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
Match on the **verified category, never on user-agent strings** — a UA
|
|
86
|
+
allowlist is trivially spoofed and silently rots as bots are renamed.
|
|
87
|
+
Cloudflare's own AI-bot toggle does not separate citation bots from training
|
|
88
|
+
bots, which is why the toggle alone is the wrong instrument.
|
|
89
|
+
|
|
90
|
+
Verified 2026-09-02 across awesomate.ai, hub.awesomate.ai and eight live client
|
|
91
|
+
sites: all four retrieval bots got **200**, so this is not currently biting the
|
|
92
|
+
fleet. Re-check per site rather than assuming either way — it is a per-zone
|
|
93
|
+
setting and new zones are steered into blocking at signup.
|
|
94
|
+
|
|
95
|
+
## Most AI crawlers do not run JavaScript
|
|
96
|
+
|
|
97
|
+
**Googlebot, Gemini and Applebot render JS. GPTBot, ClaudeBot, PerplexityBot,
|
|
98
|
+
OAI-SearchBot and Claude-SearchBot do not** — they read the **raw HTML
|
|
99
|
+
response**. So a client-rendered app (React/Vue/Next SPA without SSR) is
|
|
100
|
+
invisible to ChatGPT, Claude and Perplexity regardless of its meta tags,
|
|
101
|
+
schema or llms.txt.
|
|
102
|
+
|
|
103
|
+
`awesomate_site_audit` checks this automatically (gate G1) — run it rather
|
|
104
|
+
than eyeballing the page in a browser, which renders the JS and hides the
|
|
105
|
+
problem.
|
|
86
106
|
|
|
87
107
|
Red flags — check the raw response, not the browser:
|
|
88
108
|
|
|
@@ -100,6 +120,29 @@ app projects, static marketing/landing pages in front of the app is usually
|
|
|
100
120
|
enough. Don't rebuild a working app over this, but never claim AI
|
|
101
121
|
discoverability for client-rendered content.
|
|
102
122
|
|
|
123
|
+
## The accidental opt-out — check before blaming anything else
|
|
124
|
+
|
|
125
|
+
A page can be perfectly built, reachable and fast, and still be barred from
|
|
126
|
+
every AI answer by one directive. Sweep for these before investigating
|
|
127
|
+
anything subtler:
|
|
128
|
+
|
|
129
|
+
- `<meta name="robots" content="noindex">` — out of search AND AI answers
|
|
130
|
+
- `nosnippet` or `max-snippet:0` — nothing may be quoted, which is the same as
|
|
131
|
+
being uncitable even though the page still ranks
|
|
132
|
+
- an `X-Robots-Tag` response **header** carrying any of the above (easy to miss:
|
|
133
|
+
it is not in the HTML, so viewing source shows nothing)
|
|
134
|
+
- a staging `robots.txt` that shipped to production with `Disallow: /`
|
|
135
|
+
- in Search Console, the Search generative AI control left off
|
|
136
|
+
|
|
137
|
+
The usual causes are an SEO plugin default, a staging file promoted by mistake,
|
|
138
|
+
or a "private launch" setting nobody turned back off. `awesomate_site_audit`
|
|
139
|
+
covers the meta and header cases (gate G3); the Search Console control has to be
|
|
140
|
+
checked by the owner in their own account.
|
|
141
|
+
|
|
142
|
+
Note that blocking `Google-Extended` does **not** remove a site from AI
|
|
143
|
+
Overviews or AI Mode — that control is about training, not answers. Don't
|
|
144
|
+
"fix" it expecting a visibility change.
|
|
145
|
+
|
|
103
146
|
## llms.txt rules
|
|
104
147
|
|
|
105
148
|
- Optional inventory for non-Google assistants — Google ignores it.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Writing to be quoted — structure for AI answers
|
|
2
|
+
|
|
3
|
+
SEO gets a page found. This gets a page **cited**. Answer engines retrieve
|
|
4
|
+
**passages, not pages**: they lift a section, attribute it, and drop the rest.
|
|
5
|
+
So the section — not the page — is the unit of competition, and a section that
|
|
6
|
+
cannot stand alone cannot be quoted.
|
|
7
|
+
|
|
8
|
+
Run `awesomate_site_audit` to catch the mechanical faults (gate G8). It cannot
|
|
9
|
+
judge whether an answer is any good; that part is a human read.
|
|
10
|
+
|
|
11
|
+
## The one rule that matters most
|
|
12
|
+
|
|
13
|
+
**Answer first.** Under every heading, the first 40–60 words must be a
|
|
14
|
+
complete, standalone statement that names the subject.
|
|
15
|
+
|
|
16
|
+
Roughly **55% of AI Overview citations come from the first 30% of the page**, so
|
|
17
|
+
an answer buried under three paragraphs of preamble is an answer nobody quotes.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Bad: ## Our pricing
|
|
21
|
+
It depends on a few things. Every job is different, and we always…
|
|
22
|
+
|
|
23
|
+
Good: ## Our pricing
|
|
24
|
+
Robs Plumbing charges $180 for a standard callout in metropolitan Perth,
|
|
25
|
+
including the first 30 minutes on site. Emergency after-hours callouts
|
|
26
|
+
are $290. Quotes for larger jobs are free and fixed.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The second one survives being lifted out of the page. The first one is useless
|
|
30
|
+
the moment it loses its heading.
|
|
31
|
+
|
|
32
|
+
## Never open a section with a back-reference
|
|
33
|
+
|
|
34
|
+
"It", "this", "that", "these", "they", "also", "however", "as above", "as
|
|
35
|
+
mentioned". Once a section is extracted, there is no "above" — the subject has
|
|
36
|
+
vanished. Name it again, every time, even when it reads slightly repetitively on
|
|
37
|
+
the page. Repetition is the cost of being quotable, and it is a small cost.
|
|
38
|
+
|
|
39
|
+
## Section length: 120–180 words
|
|
40
|
+
|
|
41
|
+
Long enough to answer, short enough to be one clean retrieval unit. A 900-word
|
|
42
|
+
section under one heading competes with itself: the engine has to choose which
|
|
43
|
+
part to lift, and often lifts none of it. Split it and give each part a heading.
|
|
44
|
+
|
|
45
|
+
## Say the specific thing
|
|
46
|
+
|
|
47
|
+
Vague copy loses to specific copy, measurably. Adding **quotations from credible
|
|
48
|
+
sources raised a source's share of an AI answer by roughly 41%, statistics by
|
|
49
|
+
about 31%, and citations by about 28%**.
|
|
50
|
+
|
|
51
|
+
So: real numbers, real dates, named sources, units and currency **inline in the
|
|
52
|
+
sentence** rather than implied by a table header or an image.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Bad: We serve the whole metro area and have done for years.
|
|
56
|
+
Good: We service Perth from Joondalup to Rockingham, and have since 2009.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Comparisons go in a real `<table>`
|
|
60
|
+
|
|
61
|
+
`<table>` with `<th>`, never styled divs and never an image of a table. A
|
|
62
|
+
comparison rendered as divs is a wall of unlabelled fragments to a parser; an
|
|
63
|
+
image of a price list is invisible to everything. This is the single most
|
|
64
|
+
common way a genuinely useful pricing page fails to get cited.
|
|
65
|
+
|
|
66
|
+
## Headings: descriptive, with the question phrasing in the first line
|
|
67
|
+
|
|
68
|
+
The evidence here is genuinely mixed, so hold it loosely:
|
|
69
|
+
|
|
70
|
+
- **FAQ *schema* shows a small negative correlation** with citations (one
|
|
71
|
+
analysis: 3.6 citations with it against 4.2 without). The effect is modest and
|
|
72
|
+
probably confounded — FAQ blocks tend to sit on thin support pages that would
|
|
73
|
+
earn fewer citations anyway. Google also retired FAQ rich results in May 2026.
|
|
74
|
+
- **Question-shaped *content*, on the other hand, is the extraction unit
|
|
75
|
+
everyone describes**: a question followed by a 40–80 word answer.
|
|
76
|
+
|
|
77
|
+
The safe position that satisfies both readings: **plain descriptive headings,
|
|
78
|
+
with the question phrasing carried in the first body line.** You keep the
|
|
79
|
+
question language on the page where models read it, without betting the page on
|
|
80
|
+
FAQ markup.
|
|
81
|
+
|
|
82
|
+
Keep real FAQs because users want them. Just don't expect the schema to be the
|
|
83
|
+
lever.
|
|
84
|
+
|
|
85
|
+
## Every schema fact must also be visible
|
|
86
|
+
|
|
87
|
+
The hard rule, and the one most often broken: **no fact may exist only in
|
|
88
|
+
JSON-LD.** Models read the rendered text; structured data helps machines agree
|
|
89
|
+
on entities, but a phone number, address or price that appears *only* in the
|
|
90
|
+
markup is invisible to the thing you are optimising for.
|
|
91
|
+
|
|
92
|
+
`awesomate_site_audit` cross-checks this automatically (gate G7b) and lists any
|
|
93
|
+
schema value missing from the page copy. The fix is always **add it to the
|
|
94
|
+
page** — never delete it from the schema.
|
|
95
|
+
|
|
96
|
+
## Freshness
|
|
97
|
+
|
|
98
|
+
A visible "Last updated" date, matching `dateModified` in the JSON-LD. Bump it
|
|
99
|
+
only on substantive change; a date that moves nightly while the words stay the
|
|
100
|
+
same is a stale-content signal, not a freshness one.
|
|
101
|
+
|
|
102
|
+
Sensible cadence: pricing quarterly, comparisons six-monthly, evergreen guides
|
|
103
|
+
annually.
|
|
104
|
+
|
|
105
|
+
## What not to bother with
|
|
106
|
+
|
|
107
|
+
- **llms.txt as a priority.** Generate it if it's free — it is one small file —
|
|
108
|
+
but one study of 137,210 domains found 97% of published files received zero
|
|
109
|
+
requests, and Google ignores it outright. Never present it as a pillar, and
|
|
110
|
+
never bill for it.
|
|
111
|
+
- **Speakable schema.** No engine confirms using it.
|
|
112
|
+
- **Hidden text or instructions aimed at crawlers.** Zero measured effect, and
|
|
113
|
+
classed as spam by both Google and Microsoft.
|
|
114
|
+
- **Mass AI-written listicles.** Hit hard by the August 2026 spam update, and an
|
|
115
|
+
organic penalty propagates into AI visibility.
|
|
116
|
+
|
|
117
|
+
## Local businesses: the profile usually outranks the site
|
|
118
|
+
|
|
119
|
+
For anything with a location or a service area, **Google Business Profile
|
|
120
|
+
supplies about 67% of local citations in AI Overviews** — more than the website
|
|
121
|
+
does. So a correct primary category, the full services list, real hours and
|
|
122
|
+
real photos are higher-leverage than another page of copy.
|
|
123
|
+
|
|
124
|
+
Keep the name, address and phone **identical** across the site, the profile and
|
|
125
|
+
the main directories for the vertical. Contradictions are what stop an engine
|
|
126
|
+
treating the business as one confident entity. For AU service businesses that
|
|
127
|
+
means ProductReview.com.au plus the relevant trade directories — not a blast to
|
|
128
|
+
hundreds of listing sites, which does nothing.
|