@awesomate/hosting-mcp 0.20.5 → 0.20.7
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 +29 -9
- package/package.json +1 -1
- package/skill/CHANGELOG.json +14 -0
- package/skill/awesomate-app-builder/SKILL.md +1 -1
- package/skill/awesomate-app-builder/references/stack-decision.md +1 -1
- package/skill/awesomate-credentials/SKILL.md +1 -1
- package/skill/awesomate-database/SKILL.md +2 -2
- package/skill/awesomate-hosting/SKILL.md +3 -2
- package/skill/awesomate-hosting/scripts/bootstrap.mjs +2 -2
- package/skill/awesomate-hosting/scripts/deploy.sh +1 -1
- package/skill/awesomate-hosting/scripts/pull-live.sh +1 -1
- package/skill/awesomate-hosting/scripts/resolve-account.mjs +2 -2
- package/skill/awesomate-hosting/scripts/ssh-connect.sh +1 -1
- package/skill/awesomate-knowledge/SKILL.md +1 -1
- package/skill/awesomate-knowledge/references/from-your-files.md +6 -4
- package/skill/awesomate-knowledge/references/n8n-connection.md +39 -12
- package/skill/awesomate-n8n/SKILL.md +2 -2
- package/skill/awesomate-n8n/references/error-handling.md +1 -1
- package/skill/awesomate-n8n/references/platform-notes.md +1 -1
- package/skill/awesomate-n8n/references/rest-fallback.md +1 -1
- package/skill/awesomate-n8n/references/troubleshooting.md +3 -3
- 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/skill/awesomate-support/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -39754,13 +39754,13 @@ function findPinFile(startDir = process.cwd()) {
|
|
|
39754
39754
|
}
|
|
39755
39755
|
function profileList(profiles) {
|
|
39756
39756
|
const names = Object.keys(profiles);
|
|
39757
|
-
return names.length ? names.join(", ") : "(none \u2014 run Connect Claude Code from hub.awesomate.ai/
|
|
39757
|
+
return names.length ? names.join(", ") : "(none \u2014 run Connect Claude Code from hub.awesomate.ai/claude)";
|
|
39758
39758
|
}
|
|
39759
39759
|
function fromProfile(key, profile, source, credPath, pinPath, availableProfiles) {
|
|
39760
39760
|
const pat = profile.pat ?? "";
|
|
39761
39761
|
if (!pat.startsWith("amt_pat_")) {
|
|
39762
39762
|
throw new Error(
|
|
39763
|
-
`Profile "${key}" in ${credPath} has a malformed access token \u2014 re-run Connect Claude Code from hub.awesomate.ai/
|
|
39763
|
+
`Profile "${key}" in ${credPath} has a malformed access token \u2014 re-run Connect Claude Code from hub.awesomate.ai/claude for this account.`
|
|
39764
39764
|
);
|
|
39765
39765
|
}
|
|
39766
39766
|
return {
|
|
@@ -39804,7 +39804,7 @@ function loadConfig() {
|
|
|
39804
39804
|
const profile = profiles[envAccount];
|
|
39805
39805
|
if (!profile) {
|
|
39806
39806
|
throw new Error(
|
|
39807
|
-
`AWESOMATE_ACCOUNT="${envAccount}" has no matching profile in ${credPath}. Available profiles: ${profileList(profiles)}. Connect that account from hub.awesomate.ai/
|
|
39807
|
+
`AWESOMATE_ACCOUNT="${envAccount}" has no matching profile in ${credPath}. Available profiles: ${profileList(profiles)}. Connect that account from hub.awesomate.ai/claude (logged in as it) to add its profile.`
|
|
39808
39808
|
);
|
|
39809
39809
|
}
|
|
39810
39810
|
return fromProfile(envAccount, profile, "env-account", credPath, null, available);
|
|
@@ -39819,7 +39819,7 @@ function loadConfig() {
|
|
|
39819
39819
|
const profile = profiles[account];
|
|
39820
39820
|
if (!profile) {
|
|
39821
39821
|
throw new Error(
|
|
39822
|
-
`This folder is pinned to account "${account}" (${pinPath}) but no matching profile exists in ${credPath}. Available profiles: ${profileList(profiles)}. Fix the pin, or connect "${account}" from hub.awesomate.ai/
|
|
39822
|
+
`This folder is pinned to account "${account}" (${pinPath}) but no matching profile exists in ${credPath}. Available profiles: ${profileList(profiles)}. Fix the pin, or connect "${account}" from hub.awesomate.ai/claude while logged into that account.`
|
|
39823
39823
|
);
|
|
39824
39824
|
}
|
|
39825
39825
|
return fromProfile(account, profile, "pin", credPath, pinPath, available);
|
|
@@ -39833,7 +39833,7 @@ function loadConfig() {
|
|
|
39833
39833
|
}
|
|
39834
39834
|
if (available.length === 0) {
|
|
39835
39835
|
throw new Error(
|
|
39836
|
-
`No Awesomate access token found. Set AWESOMATE_PAT or run Connect Claude Code from hub.awesomate.ai/
|
|
39836
|
+
`No Awesomate access token found. Set AWESOMATE_PAT or run Connect Claude Code from hub.awesomate.ai/claude (it writes ${credPath}).`
|
|
39837
39837
|
);
|
|
39838
39838
|
}
|
|
39839
39839
|
throw new Error(
|
|
@@ -39899,9 +39899,9 @@ async function hubRequest(config3, method, path, jsonBody, opts = {}) {
|
|
|
39899
39899
|
const code = body && typeof body === "object" && "code" in body && typeof body.code === "string" ? body.code : null;
|
|
39900
39900
|
let hint = "";
|
|
39901
39901
|
if (res.status === 401) {
|
|
39902
|
-
hint = " Your access token is invalid or expired \u2014 ask the user to open the hub
|
|
39902
|
+
hint = " Your access token is invalid or expired \u2014 ask the user to open the hub Claude page (hub.awesomate.ai/claude), generate a fresh Connect prompt, and re-run the bootstrap.";
|
|
39903
39903
|
} else if (res.status === 403 && code === "consent_required") {
|
|
39904
|
-
const settingsUrl = body && typeof body === "object" && "settingsUrl" in body && typeof body.settingsUrl === "string" ? body.settingsUrl : "https://hub.awesomate.ai/
|
|
39904
|
+
const settingsUrl = body && typeof body === "object" && "settingsUrl" in body && typeof body.settingsUrl === "string" ? body.settingsUrl : "https://hub.awesomate.ai/settings?tab=privacy";
|
|
39905
39905
|
hint = ` This needs a privacy toggle the user must flip themselves \u2014 send them to ${settingsUrl}, wait for them to confirm, then retry. Never suggest a plan upgrade for a consent denial.`;
|
|
39906
39906
|
} else if (res.status === 403) {
|
|
39907
39907
|
const missing = body && typeof body === "object" && "missingScopes" in body ? ` (missing: ${JSON.stringify(body.missingScopes)})` : "";
|
|
@@ -40770,7 +40770,7 @@ server.registerTool(
|
|
|
40770
40770
|
"awesomate_get_context",
|
|
40771
40771
|
{
|
|
40772
40772
|
annotations: READ_ONLY,
|
|
40773
|
-
description: "THE session entry point \u2014 call it FIRST, before any domain context. Returns the connected account: plan, capabilities, limits, scopes, token expiry, cPanel routing, PLUS `attention` ({unreadNotifications, erroringWorkflows7d, patExpiresInDays} \u2014 null means unknown, never zero; when something is non-zero, mention it to the user in one line before starting the asked task), `latestMcpVersion` (if serverVersion still lags it after a restart, the npx cache is stale \u2014 remedy: rm -rf ~/.npm/_npx, then restart), and `skill` ({updateAvailable, staleSkills, whatsNew} \u2014 if updateAvailable, mention ONCE with a whatsNew line, offer awesomate_skill_update, never mid-task). If the token is near expiry (patExpiresInDays < 7), re-running Connect Claude Code from hub.awesomate.ai/
|
|
40773
|
+
description: "THE session entry point \u2014 call it FIRST, before any domain context. Returns the connected account: plan, capabilities, limits, scopes, token expiry, cPanel routing, PLUS `attention` ({unreadNotifications, erroringWorkflows7d, patExpiresInDays} \u2014 null means unknown, never zero; when something is non-zero, mention it to the user in one line before starting the asked task), `latestMcpVersion` (if serverVersion still lags it after a restart, the npx cache is stale \u2014 remedy: rm -rf ~/.npm/_npx, then restart), and `skill` ({updateAvailable, staleSkills, whatsNew} \u2014 if updateAvailable, mention ONCE with a whatsNew line, offer awesomate_skill_update, never mid-task). If the token is near expiry (patExpiresInDays < 7), re-running Connect Claude Code from hub.awesomate.ai/claude refreshes skills AND renews the token in one go.",
|
|
40774
40774
|
inputSchema: {}
|
|
40775
40775
|
},
|
|
40776
40776
|
async () => {
|
|
@@ -41804,7 +41804,7 @@ readTool(
|
|
|
41804
41804
|
);
|
|
41805
41805
|
readTool(
|
|
41806
41806
|
"awesomate_privacy_settings",
|
|
41807
|
-
"READ which privacy/consent toggles are on or off for this account \u2014 call it when a tool returns 403 consent_required so you can name the exact toggle instead of guessing. Toggles are changed ONLY by the user in the hub (
|
|
41807
|
+
"READ which privacy/consent toggles are on or off for this account \u2014 call it when a tool returns 403 consent_required so you can name the exact toggle instead of guessing. Toggles are changed ONLY by the user in the hub (Settings \u2192 Privacy, hub.awesomate.ai/settings?tab=privacy); there is deliberately no write here.",
|
|
41808
41808
|
"/api/client-settings/privacy"
|
|
41809
41809
|
);
|
|
41810
41810
|
server.registerTool(
|
|
@@ -41877,6 +41877,26 @@ server.registerTool(
|
|
|
41877
41877
|
}
|
|
41878
41878
|
}
|
|
41879
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
|
+
);
|
|
41880
41900
|
server.registerTool(
|
|
41881
41901
|
"awesomate_wp_media_import",
|
|
41882
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.7",
|
|
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.7",
|
|
5
|
+
"highlights": [
|
|
6
|
+
"Your Knowledge Base can now go on your website: install the Verified Content Chat Agent from the hub Library and it answers visitors from the agent you published, with sources on every reply",
|
|
7
|
+
"Fixed: a credential Awesomate creates in your n8n now shows up immediately in the list Claude reads, instead of after the weekly refresh"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.20.6",
|
|
12
|
+
"highlights": [
|
|
13
|
+
"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",
|
|
14
|
+
"Catches the faults that silently make a site invisible to ChatGPT and Perplexity, like content that only appears once the browser runs it"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
3
17
|
{
|
|
4
18
|
"version": "0.20.5",
|
|
5
19
|
"highlights": [
|
|
@@ -78,7 +78,7 @@ inventory (`awesomate_n8n_inspect {what:'credentials'}`) and say e.g. *"you alre
|
|
|
78
78
|
have Gmail connected in n8n — want the form to email you through that?"*
|
|
79
79
|
That read is **consent-gated**: it needs the **"Allow Claude Code to Build
|
|
80
80
|
n8n Workflows"** toggle (n8n → Settings → Privacy,
|
|
81
|
-
hub.awesomate.ai/
|
|
81
|
+
hub.awesomate.ai/settings?tab=privacy; also on hub.awesomate.ai/settings?tab=privacy),
|
|
82
82
|
which is **off by default** — so on a fresh account it comes back 403
|
|
83
83
|
`consent_required`. That's not a failure to work around: call
|
|
84
84
|
`awesomate_privacy_settings` to name the exact toggle, ask the user to flip
|
|
@@ -68,7 +68,7 @@ credentials are already connected there, so there's nothing new to set up.
|
|
|
68
68
|
sheet?"* That read needs the **"Allow Claude Code to Build n8n Workflows"**
|
|
69
69
|
privacy toggle, which is off by default — a 403 `consent_required` means
|
|
70
70
|
call `awesomate_privacy_settings`, name the toggle, and ask the user to flip
|
|
71
|
-
it at hub.awesomate.ai/
|
|
71
|
+
it at hub.awesomate.ai/settings?tab=privacy.
|
|
72
72
|
- Build the workflow with the **awesomate-n8n** skill (webhook trigger →
|
|
73
73
|
action), take its **webhook URL**, and wire it into the app.
|
|
74
74
|
- **Node app:** `awesomate_n8n_attach_to_app` — it stores
|
|
@@ -123,7 +123,7 @@ A secret for an n8n workflow belongs on the n8n instance (awesomate-n8n
|
|
|
123
123
|
skill), never in an app `.env`. Those tools are consent-gated: if one comes
|
|
124
124
|
back 403 `consent_required`, call `awesomate_privacy_settings` — it names
|
|
125
125
|
the exact toggle that's off, so you can send the user to the right switch at
|
|
126
|
-
hub.awesomate.ai/
|
|
126
|
+
hub.awesomate.ai/settings?tab=privacy instead of guessing. You can't flip it
|
|
127
127
|
for them.
|
|
128
128
|
|
|
129
129
|
## Fallback: the file-drop flow
|
|
@@ -28,8 +28,8 @@ automation-shaped. Inspect what exists with
|
|
|
28
28
|
`awesomate_n8n_inspect {what:'datatables'}`.
|
|
29
29
|
**Both the write AND that read are gated:** Support Plus or above, plus the
|
|
30
30
|
privacy toggle **"Allow Claude Code to Build n8n Workflows"** — n8n →
|
|
31
|
-
Settings → Privacy (hub.awesomate.ai/
|
|
32
|
-
hub.awesomate.ai/
|
|
31
|
+
Settings → Privacy (hub.awesomate.ai/settings?tab=privacy, or the same toggles at
|
|
32
|
+
hub.awesomate.ai/settings?tab=privacy, which is what the 403's
|
|
33
33
|
`settingsUrl` points at). It is **off by default**, so on a fresh account
|
|
34
34
|
both the read and the write come back 403 `consent_required`. Call
|
|
35
35
|
`awesomate_privacy_settings` to name the exact toggle that's off, send the
|
|
@@ -46,7 +46,7 @@ once (plus `awesomate_get_limits` before any create/change) and cache:
|
|
|
46
46
|
asked task, then get on with the task. `null` means unknown, never zero —
|
|
47
47
|
say nothing on null.
|
|
48
48
|
- **`patExpiresAt`** — within ~7 days, or any 401: re-run the setup prompt at
|
|
49
|
-
**hub.awesomate.ai/
|
|
49
|
+
**hub.awesomate.ai/claude → Connect Claude Code** to refresh the token.
|
|
50
50
|
- **`skill.updateAvailable`** — covers all nine skills; the local files are
|
|
51
51
|
older than the MCP server. Mention it ONCE per session, with the what's-new
|
|
52
52
|
line the context includes, and offer to run `awesomate_skill_update`. Its
|
|
@@ -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) |
|
|
@@ -114,7 +115,7 @@ proxy failures, resuming a connect, support report) ·
|
|
|
114
115
|
| `awesomate_wp_post` | Create/update/read a WP post or page — drafts by default (writes are Support Plus+) |
|
|
115
116
|
| `awesomate_wp_media_import` | Pull an image/file into the WP media library from an https URL (Support Plus+) |
|
|
116
117
|
| `awesomate_uninstall_site` | Remove a WordPress install (destructive — confirm explicitly) |
|
|
117
|
-
| `awesomate_privacy_settings` | Read which privacy/consent toggles are on — explain a 403 `consent_required` precisely (the user flips toggles themselves under
|
|
118
|
+
| `awesomate_privacy_settings` | Read which privacy/consent toggles are on — explain a 403 `consent_required` precisely (the user flips toggles themselves under Settings → Privacy at hub.awesomate.ai/settings?tab=privacy) |
|
|
118
119
|
| `awesomate_notifications` | The hub notification bell — `list` unread (quota warnings, quote ready, support-access events), `read`/`read_all` to clear |
|
|
119
120
|
| `awesomate_dashboard_metrics` | Account-wide numbers: executions by status, error rate, time saved, chat sessions, 7-day trend |
|
|
120
121
|
| `awesomate_account_report` | A monthly-report-shaped read: working / not working / engagement / commercials |
|
|
@@ -67,7 +67,7 @@ if (process.argv.includes('--update')) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (!resumePat && (!code || !code.startsWith('amt_bs_'))) {
|
|
70
|
-
console.error('Missing/invalid --code. Copy a fresh setup prompt from hub.awesomate.ai/
|
|
70
|
+
console.error('Missing/invalid --code. Copy a fresh setup prompt from hub.awesomate.ai/claude (or resume with --pat if you already hold a token).');
|
|
71
71
|
console.error('AWESOMATE CONNECT: FAILED reason=bad-code');
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
@@ -596,7 +596,7 @@ async function main() {
|
|
|
596
596
|
main().catch((err) => {
|
|
597
597
|
console.error('Bootstrap failed:', err.message);
|
|
598
598
|
if (err.isApiError && err.status === 401) {
|
|
599
|
-
console.error('Grab a fresh code from hub.awesomate.ai/
|
|
599
|
+
console.error('Grab a fresh code from hub.awesomate.ai/claude — codes last 10 minutes (and stay re-runnable within that window).');
|
|
600
600
|
}
|
|
601
601
|
console.error(SUPPORT_HINT);
|
|
602
602
|
console.error('AWESOMATE CONNECT: FAILED reason=error');
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
set -euo pipefail
|
|
26
26
|
|
|
27
27
|
CRED="$HOME/.awesomate/credentials.json"
|
|
28
|
-
[ -f "$CRED" ] || { echo "Not connected — run the Connect Claude Code setup from hub.awesomate.ai/
|
|
28
|
+
[ -f "$CRED" ] || { echo "Not connected — run the Connect Claude Code setup from hub.awesomate.ai/claude first."; exit 1; }
|
|
29
29
|
|
|
30
30
|
FROM="" DOMAIN="" DOCROOT="" LOCAL_URL="" LIVE_URL="" WITH_DB=0 ASSUME_YES=0
|
|
31
31
|
while [ "$#" -gt 0 ]; do
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
13
|
CRED="$HOME/.awesomate/credentials.json"
|
|
14
|
-
[ -f "$CRED" ] || { echo "Not connected — run the Connect Claude Code setup from hub.awesomate.ai/
|
|
14
|
+
[ -f "$CRED" ] || { echo "Not connected — run the Connect Claude Code setup from hub.awesomate.ai/claude first."; exit 1; }
|
|
15
15
|
|
|
16
16
|
DOMAIN="" TO="" DOCROOT=""
|
|
17
17
|
while [ "$#" -gt 0 ]; do
|
|
@@ -70,7 +70,7 @@ if (process.env.AWESOMATE_ACCOUNT) {
|
|
|
70
70
|
if (!profiles[key]) {
|
|
71
71
|
fail(
|
|
72
72
|
`This folder is pinned to account "${key}" (${pinPath}) but no matching profile exists in ${credPath}. ` +
|
|
73
|
-
`Available: ${names.join(', ') || '(none)'}. Connect "${key}" from hub.awesomate.ai/
|
|
73
|
+
`Available: ${names.join(', ') || '(none)'}. Connect "${key}" from hub.awesomate.ai/claude (logged in as it) or fix the pin.`,
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
} else if (names.length === 1) {
|
|
@@ -78,7 +78,7 @@ if (process.env.AWESOMATE_ACCOUNT) {
|
|
|
78
78
|
} else if (file.defaultProfile && profiles[file.defaultProfile]) {
|
|
79
79
|
key = file.defaultProfile;
|
|
80
80
|
} else if (names.length === 0) {
|
|
81
|
-
fail('Not connected. Run the Connect Claude Code setup from hub.awesomate.ai/
|
|
81
|
+
fail('Not connected. Run the Connect Claude Code setup from hub.awesomate.ai/claude.');
|
|
82
82
|
} else {
|
|
83
83
|
fail(
|
|
84
84
|
`Multiple accounts are connected (${names.join(', ')}) and this folder isn't pinned to one. ` +
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# ssh-connect.sh 'wp --info' # one-off command
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
CRED="$HOME/.awesomate/credentials.json"
|
|
9
|
-
[ -f "$CRED" ] || { echo "Not connected. Run bootstrap first (Connect Claude Code on hub.awesomate.ai/
|
|
9
|
+
[ -f "$CRED" ] || { echo "Not connected. Run bootstrap first (Connect Claude Code on hub.awesomate.ai/claude)."; exit 1; }
|
|
10
10
|
|
|
11
11
|
# Resolve WHICH account (folder pin / AWESOMATE_ACCOUNT / sole profile) and its
|
|
12
12
|
# ssh block via the shared resolver. Capture then eval so a resolver failure
|
|
@@ -31,7 +31,7 @@ platform's answer beats anything you remember.
|
|
|
31
31
|
link once, honestly, then help within what reads allow. Never retry
|
|
32
32
|
into the gate.
|
|
33
33
|
- `consent.knowledge_platform_enabled: false` → the user must turn it on
|
|
34
|
-
themselves: hub.awesomate.ai/
|
|
34
|
+
themselves: hub.awesomate.ai/settings?tab=privacy, under the heading
|
|
35
35
|
**"Knowledge Base"**, the toggle **"Send My Content to the Knowledge
|
|
36
36
|
Platform"**. Name it exactly like that — one heading and one switch, so
|
|
37
37
|
they can find it without hunting. You cannot flip it for them, and
|
|
@@ -106,10 +106,12 @@ Two destinations. Ask which they want; do not assume.
|
|
|
106
106
|
|
|
107
107
|
**A chat on their existing website — via n8n.** This is usually the right
|
|
108
108
|
answer, and the plumbing is already provisioned. See
|
|
109
|
-
`references/n8n-connection.md
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
`references/n8n-connection.md` §3: they install the **Verified Content Chat
|
|
110
|
+
Agent** card from the hub Library (`/templates`); it runs the agent they
|
|
111
|
+
published, publishes on install, and the Chat URL goes into the embed
|
|
112
|
+
snippet. Only when they want their OWN agent node (extra tools, a different
|
|
113
|
+
model) build it with the awesomate-n8n skill instead — Chat Trigger -> AI
|
|
114
|
+
Agent with the `knowledge_answer` library tool -> Respond — and read
|
|
113
115
|
`awesomate-n8n/references/ai-agents.md` before designing the agent node.
|
|
114
116
|
|
|
115
117
|
**A standalone page or app — via the app builder.** When they want a hosted
|
|
@@ -34,18 +34,45 @@ their own agent on top and accept ungated composition.
|
|
|
34
34
|
|
|
35
35
|
## 3. The bundle — "Verified Content Chat Agent"
|
|
36
36
|
|
|
37
|
-
One
|
|
38
|
-
`@n8n/chat` embed widget contract
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
One library card installs the public chat workflow that speaks the
|
|
38
|
+
`@n8n/chat` embed widget contract. There is **no AI Agent node in it**: the
|
|
39
|
+
agent the member published in the hub (Knowledge → Agents) IS the agent —
|
|
40
|
+
its system message, grounding mode, scope and no-answer message run on the
|
|
41
|
+
platform, and n8n only relays. Chat Trigger → `POST /v1/answer` with
|
|
42
|
+
`agent_id` (scope credential above) → a Code node that renders the N1
|
|
43
|
+
contract (§4) → the widget.
|
|
44
|
+
|
|
45
|
+
Variables (all read from `$vars`, all with fallbacks):
|
|
46
|
+
|
|
47
|
+
- `kb_agent_id` — **set automatically at install** when the member has
|
|
48
|
+
exactly one published, unsuspended agent. Two or more is their choice:
|
|
49
|
+
preflight lists it as manual setup and the value is the agent id shown in
|
|
50
|
+
the hub. Unset, `/v1/answer` falls back to the tenant's default agent.
|
|
51
|
+
- `kb_allowed_origins` — the website origin (CORS). Default: any.
|
|
52
|
+
- `kb_fallback_message` — shown only when the platform itself could not be
|
|
53
|
+
reached (401/429/timeout). A verified "no answer" shows the agent's own
|
|
54
|
+
no-answer message, never this.
|
|
55
|
+
|
|
56
|
+
Flow you drive: confirm the knowledge base is active with indexed content and
|
|
57
|
+
one published agent → the member installs the bundle from the hub's
|
|
58
|
+
**Library** page (`/templates`, card "Verified Content Chat Agent"; a redeem
|
|
59
|
+
token via `POST /api/client-settings/redeem {token}` also works if Awesomate
|
|
60
|
+
hands one over) → the workflow publishes on install once the scope credential
|
|
61
|
+
is verified → open "When chat message received", copy the **Chat URL**, and
|
|
62
|
+
give them the embed snippet (it is also in the workflow's HOW IT WORKS note):
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<link href="https://cdn.jsdelivr.net/npm/@n8n/chat/dist/style.css" rel="stylesheet" />
|
|
66
|
+
<script type="module">
|
|
67
|
+
import { createChat } from 'https://cdn.jsdelivr.net/npm/@n8n/chat/dist/chat.bundle.es.js';
|
|
68
|
+
createChat({ webhookUrl: '<the Chat URL>' });
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Not yet shipped (do not promise them): a "Knowledge Bot Setup" Chat Hub
|
|
73
|
+
agent that walks the member through greeting/name/origins, and the hosted
|
|
74
|
+
widget with a per-site install flow. Until then the sticky notes in the
|
|
75
|
+
workflow are the setup guide.
|
|
49
76
|
|
|
50
77
|
If the member already has their own agent workflow, attach the
|
|
51
78
|
`knowledge_answer` tool from the library to it instead — same credential,
|
|
@@ -30,11 +30,11 @@ as outcomes ("your form now emails you") with URLs.
|
|
|
30
30
|
2. `awesomate_n8n_context` — cache the result:
|
|
31
31
|
- `consented: false` → give the user the `settingsUrl` and tell them
|
|
32
32
|
where it lands: **n8n → Settings → Privacy** at
|
|
33
|
-
`hub.awesomate.ai/
|
|
33
|
+
`hub.awesomate.ai/settings?tab=privacy` → "Allow Claude Code to Build n8n
|
|
34
34
|
Workflows". Privacy moved there; the plain `/settings` page no longer
|
|
35
35
|
has it (old `?tab=privacy` links just redirect). Wait, then re-check.
|
|
36
36
|
- `403 missingScopes` → token predates n8n support — reconnect from
|
|
37
|
-
hub.awesomate.ai/
|
|
37
|
+
hub.awesomate.ai/claude (Connect Claude Code card).
|
|
38
38
|
- `capabilities.builder: false` → reads only; building is Support Plus+.
|
|
39
39
|
Be honest about it once, then help fully within reads.
|
|
40
40
|
- `capabilities.variant` → FFMPEG variants have the media community
|
|
@@ -82,7 +82,7 @@ use it to explain and prioritise, not as a substitute for your own diagnosis
|
|
|
82
82
|
of the execution data. On a 403, the plan or the toggle is the reason —
|
|
83
83
|
`awesomate_privacy_settings` (hosting skill) tells you which; the user flips
|
|
84
84
|
toggles themselves under **n8n → Settings → Privacy** at
|
|
85
|
-
hub.awesomate.ai/
|
|
85
|
+
hub.awesomate.ai/settings?tab=privacy — the one for this tool is labelled
|
|
86
86
|
**"Enable AI Error Diagnosis"**.
|
|
87
87
|
|
|
88
88
|
## 5. What to do when you inherit a workflow with no plan
|
|
@@ -74,7 +74,7 @@ trust this file.
|
|
|
74
74
|
real error. Use it instead of guessing from the summary. It is gated on
|
|
75
75
|
the client's `error_content_analysis` privacy toggle (default OFF) — on
|
|
76
76
|
a consent refusal, ask the user to enable it under **n8n → Settings →
|
|
77
|
-
Privacy** at `hub.awesomate.ai/
|
|
77
|
+
Privacy** at `hub.awesomate.ai/settings?tab=privacy` (Privacy moved off the plain
|
|
78
78
|
`/settings` page; old `?tab=privacy` links redirect there);
|
|
79
79
|
never work around the gate.
|
|
80
80
|
|
|
@@ -62,7 +62,7 @@ the conversation or logs.
|
|
|
62
62
|
- `403 consent_required` + `flag` + `settingsUrl` → relay the link, re-check
|
|
63
63
|
after the user toggles (a null `settingsUrl` means contact support).
|
|
64
64
|
- `403 missingScopes` → token predates n8n support — reconnect from
|
|
65
|
-
hub.awesomate.ai/
|
|
65
|
+
hub.awesomate.ai/claude.
|
|
66
66
|
- `429 quota_exceeded` → the plan's daily cap; stop, don't retry-loop.
|
|
67
67
|
- `503 node_catalog_unavailable` → use `references/vendor/` knowledge.
|
|
68
68
|
- `404` on a path in this table → check the path against this file
|
|
@@ -49,9 +49,9 @@ rebuilt (a rebuilt instance shows 0 workflows and invalidates every old key).
|
|
|
49
49
|
5. `awesomate_n8n_executions {executionId, debug: true}` — node-level output
|
|
50
50
|
data. Requires `error_content_analysis` consent:
|
|
51
51
|
- `403 consent_required` → give the user the `settingsUrl` from the error
|
|
52
|
-
response and stop; they enable it under **
|
|
53
|
-
`hub.awesomate.ai/
|
|
54
|
-
|
|
52
|
+
response and stop; they enable it under **Settings → Privacy** at
|
|
53
|
+
`hub.awesomate.ai/settings?tab=privacy` (old `/n8n/settings?section=privacy`
|
|
54
|
+
links redirect there).
|
|
55
55
|
- `tooLarge: true` → payload over 15MB. Use the summary in the response;
|
|
56
56
|
do NOT retry hoping for the full payload.
|
|
57
57
|
6. Node behaving strangely rather than erroring → check its schema with
|
|
@@ -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.
|
|
@@ -135,7 +135,7 @@ to the Awesomate team, who scope, build, test, and deliver it.
|
|
|
135
135
|
to do rather than opening a ticket.
|
|
136
136
|
- **A 403 that mentions consent is a toggle, not a bug.** Call
|
|
137
137
|
`awesomate_privacy_settings` to see exactly which toggle is off, name it,
|
|
138
|
-
and send them to hub.awesomate.ai/
|
|
138
|
+
and send them to hub.awesomate.ai/settings?tab=privacy. You cannot flip it
|
|
139
139
|
for them, and there is deliberately no tool that can.
|
|
140
140
|
|
|
141
141
|
## 6. Hard rules
|