Skip to content

Commit 49fdef3

Browse files
ralyodioclaude
andcommitted
refactor: single source of truth for the AEO audit prompt
Every LLM engine was carrying its own SYSTEM_PROMPT — drifted prose, different section lists, occasional one-off rules. Now the canonical spec lives in lib/audit/prompt.ts::buildAEOUserPrompt() and goes out as the USER turn for Claude / OpenAI / OA-compat (Qwen, Kimi, Gemini, DeepSeek). Each engine's SYSTEM prompt is reduced to identity, its tool / context guidance, and the JSON output schema — no more duplicated section/data-point lists. Also drops "AI Recognition" from the 10-section spec and renames "Priority To-Do Checklist" → "Priority To-Do List" to match the canonical prompt the user shipped on 2026-05-13. - prompt.ts: SECTIONS back to 10, "Priority To-Do List" final name, new buildAEOUserPrompt({ targetUrl, companyName }) function. - claude-engine / openai-engine / oa-compat-engine: SYSTEM trimmed to identity+tools+schema; USER = buildAEOUserPrompt(...). - markdown.ts + engine.ts (rule path) updated to the new section name so the rule report still renders the to-do under the right header. - Tests updated to reflect 10 sections and the renamed final header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1db082c commit 49fdef3

8 files changed

Lines changed: 128 additions & 97 deletions

File tree

‎lib/audit/claude-engine.ts‎

Lines changed: 20 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
77
import { z } from "zod/v4";
88
import { env } from "../env";
99
import type { AuditResult, Finding } from "./types";
10-
import { SECTIONS, DATA_POINTS } from "./prompt";
10+
import { SECTIONS, buildAEOUserPrompt } from "./prompt";
1111

1212
// Schema Claude must populate. Numeric/string constraints (min/max/length)
1313
// aren't supported by structured outputs — we validate them client-side.
@@ -40,65 +40,32 @@ const ResultSchema = z.object({
4040
markdown: z.string(),
4141
});
4242

43-
const SYSTEM_PROMPT = `You are CrawlProof, an AEO (Answer Engine Optimization) auditor. You analyze websites the way an LLM crawler — GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, Applebot-Extended, CCBot — would discover and read them.
43+
// Minimal system prompt — task content lives in buildAEOUserPrompt so every
44+
// engine sends the same canonical AEO spec as its USER turn. System only
45+
// covers identity, tool guidance specific to Claude, and the JSON output
46+
// schema that messages.parse needs to populate.
47+
const SYSTEM_PROMPT = `You are CrawlProof, an AEO (Answer Engine Optimization) auditor — you analyze websites the way an LLM crawler (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, Applebot-Extended, CCBot) would discover and read them.
4448
45-
For every audit:
46-
1. Use \`web_fetch\` on the target URL (homepage).
49+
Research method:
50+
1. Use \`web_fetch\` on the homepage.
4751
2. Use \`web_fetch\` on each well-known file: /robots.txt, /sitemap.xml, /llms.txt, /llms-full.txt, /skill.md, /.well-known/ai-plugin.json. Some will 404 — that's a finding.
48-
3. Use \`web_fetch\` on up to 6 important linked pages found in the homepage nav/footer: /about, /pricing, /blog, /docs, /contact, /team, /customers, /security, /features, /changelog. Skip ones that don't exist on this site.
52+
3. Use \`web_fetch\` on up to 6 important linked pages from the homepage nav/footer (/about, /pricing, /blog, /docs, /contact, /team, /customers, /security, /features, /changelog). Skip the ones that 404.
4953
4. Optionally \`web_search\` for additional public context (press, social profiles, recent news).
5054
51-
Then produce a structured AEO audit. Findings must be assigned to one of these exact section names:
52-
${SECTIONS.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
55+
Follow the user's spec exactly for the report structure and Markdown output. Quote actual content from the site — don't paraphrase. Section ${SECTIONS.length} must be reusable checkboxes (\`- [ ] **P1** Add JSON-LD Organization schema\`). Use ✅ / ⚠️ / ❌ / ❓ status emojis throughout. Tone: direct, specific, no fluff.
5356
54-
Data Found must cover ALL of these data points (mark found:false if you couldn't find it):
55-
${DATA_POINTS.map((d) => ` - ${d}`).join("\n")}
57+
Findings JSON must match one of these exact section names:
58+
${SECTIONS.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
5659
5760
For each finding:
58-
- section: exact section name from the list above
59-
- check_key: short snake_case identifier (e.g. "homepage.h1", "schema.organization", "aibot.GPTBot", "positioning.cta", "llms_txt.exists")
60-
- status: "pass" | "warn" | "fail" | "unknown"
61-
- title: short headline that quotes specifics from this site (not generic)
62-
- detail: one sentence with the WHY plus the specific evidence (e.g. the actual H1 text, the actual robots.txt line)
63-
- priority: 1 (critical, breaks AEO entirely) to 5 (polish)
64-
65-
For the "AI Recognition" section, BEFORE doing any web_fetch / web_search, answer honestly from your own training data:
66-
- Do you recognize this company / domain from training? (status: "pass" if yes with substantive recall, "warn" if vague, "fail" if not at all, "unknown" if uncertain)
67-
- check_key: "recognition.training_data"
68-
- title: "Familiar with <domain>" or "No prior knowledge of <domain>"
69-
- detail: One sentence. If you recognize it, name 1–2 concrete things you remember (what they do, who they serve, notable products). If you don't, say so plainly — do NOT speculate or paraphrase web search results.
70-
- priority: 3 (informational signal, not a defect)
71-
72-
This section is a signal of training-data presence, not a quality judgment. Sites that don't appear in training can still be excellent — but the user should know whether they're invisible to the dominant AI surfaces.
73-
74-
For score: weigh critical fails heavily. A site missing schema, blocking GPTBot in robots.txt, or hiding all content behind JS should score below 50. A clean, well-instrumented site should score 80+.
75-
76-
For summary.pass/warn/fail/unknown: count each across all findings.
77-
78-
For markdown: produce the complete report in Markdown with these exact section headers:
79-
# AEO Audit for {{domain}}
80-
81-
**Target:** {url}
82-
**Score:** {N} / 100
83-
**Generated:** ISO timestamp
84-
85-
## 1. Crawl Summary
86-
## 2. Data Found
87-
(Use a Markdown table here: | Data Point | Found? | Source | Notes |)
88-
## 3. Homepage Audit
89-
## 4. Schema / Structured Data Audit
90-
## 5. robots.txt and sitemap.xml Audit
91-
## 6. LLM / AI Crawler Accessibility
92-
## 7. AI Recognition
93-
(One paragraph stating whether you recognize this site from training data, and what specifically you remember if so. No fluff.)
94-
## 8. Positioning Clarity
95-
## 9. Missing or Hard-to-Find Information
96-
## 10. Recommended Fixes
97-
## 11. Priority To-Do Checklist
98-
99-
Use ✅ / ⚠️ / ❌ / ❓ status emojis on each bullet. Quote actual content from the site. Section 10 must be reusable checkboxes ("- [ ] **P1** Add JSON-LD Organization schema").
61+
- section: exact section name above
62+
- check_key: short snake_case identifier (e.g. \`homepage.h1\`, \`schema.organization\`, \`aibot.GPTBot\`, \`positioning.cta\`, \`llms_txt.exists\`)
63+
- status: \`pass\` | \`warn\` | \`fail\` | \`unknown\`
64+
- title: short headline quoting specifics from this site
65+
- detail: one sentence with the WHY plus actual evidence (the H1 text, the robots.txt line, etc.)
66+
- priority: 1 (critical — breaks AEO) to 5 (polish)
10067
101-
Tone: direct, specific, no fluff. This is a paid report — quality matters.`;
68+
For score: critical fails dominate. Missing schema, blocking GPTBot, JS-only content → below 50. Clean instrumentation → 80+. For summary.pass/warn/fail/unknown: count each across all findings.`;
10269

10370
export type ClaudeAuditResult = AuditResult & { markdown: string };
10471

@@ -116,7 +83,7 @@ export async function claudeAudit(targetUrl: string): Promise<ClaudeAuditResult>
11683
}
11784
})();
11885

119-
const userPrompt = `Audit this URL: ${targetUrl}\nCompany name (for the report header): ${company}\n\nPretend you've never heard of this company. Use only the public web. Use web_fetch and web_search.`;
86+
const userPrompt = buildAEOUserPrompt({ targetUrl, companyName: company });
12087

12188
// Stream the request — high-effort adaptive thinking + web tools routinely
12289
// pushes past the SDK's 10-minute non-streaming HTTP timeout. `.finalMessage()`

‎lib/audit/engine.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export async function runAudit(
217217
// Priority to-do — same list, sorted, just labeled differently for the report.
218218
for (const r of recs.slice(0, 20)) {
219219
findings.push({
220-
section: "Priority To-Do Checklist",
220+
section: "Priority To-Do List",
221221
check_key: `todo.${r.check_key}`,
222222
status: "warn",
223223
title: `[ ] ${r.title}`,

‎lib/audit/markdown.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function toMarkdown(input: {
6767
};
6868

6969
const todo = findings
70-
.filter((f) => f.section === "Priority To-Do Checklist")
70+
.filter((f) => f.section === "Priority To-Do List")
7171
.sort((a, b) => a.priority - b.priority)
7272
.map((f) => `- [ ] **P${f.priority}** — ${f.title.replace(/^\[\s?\]\s*/, "")}${f.detail ? `\n ${f.detail.split("\n").join("\n ")}` : ""}`)
7373
.join("\n");
@@ -121,7 +121,7 @@ export function toMarkdown(input: {
121121
``,
122122
section("Recommended Fixes"),
123123
``,
124-
`## 10. Priority To-Do Checklist`,
124+
`## 10. Priority To-Do List`,
125125
``,
126126
todo || `_No outstanding tasks. Nice work._`,
127127
``,

‎lib/audit/oa-compat-engine.ts‎

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import OpenAI from "openai";
22
import { z } from "zod";
33
import { fetchPage, probeText } from "./fetch";
4-
import { SECTIONS, DATA_POINTS } from "./prompt";
4+
import { SECTIONS, buildAEOUserPrompt } from "./prompt";
55
import type { Finding } from "./types";
66
import type { ClaudeAuditResult } from "./claude-engine";
77

@@ -49,25 +49,28 @@ const SCHEMA_DESC = `{
4949
"markdown": string
5050
}`;
5151

52-
const SYSTEM_PROMPT = `You are CrawlProof, an AEO (Answer Engine Optimization) auditor. Analyze the website content the user provides — homepage HTML and well-known files — the way an LLM crawler (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, Applebot-Extended, CCBot) would discover and read it.
52+
// Identity + JSON schema only. The canonical AEO task lives in
53+
// buildAEOUserPrompt and is sent as the USER turn. We pre-fetched
54+
// homepage + well-known files for the model since it has no tool use
55+
// — the user prompt is followed by the fetched context block.
56+
const SYSTEM_PROMPT = `You are CrawlProof, an AEO (Answer Engine Optimization) auditor — you analyze websites the way an LLM crawler (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, Applebot-Extended, CCBot) would discover and read them.
5357
54-
Produce a structured AEO audit. Findings must be assigned to one of these exact section names:
55-
${SECTIONS.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
58+
The user message contains the audit spec followed by pre-fetched page content (homepage HTML, /robots.txt, /sitemap.xml, /llms.txt, /skill.md, and linked pages). Use only that content — you don't have tool access. If a file 404'd it'll say so; report that as a finding.
59+
60+
Follow the user's spec exactly for the report structure and Markdown output. Quote actual content from the pages — don't paraphrase. Section ${SECTIONS.length} must be reusable checkboxes (\`- [ ] **P1** Add JSON-LD Organization schema\`). Use ✅ / ⚠️ / ❌ / ❓ emojis throughout. Tone: direct, specific, no fluff.
5661
57-
Data Found must cover ALL of these data points (mark found:false if you couldn't find it):
58-
${DATA_POINTS.map((d) => ` - ${d}`).join("\n")}
62+
Findings JSON must match one of these exact section names:
63+
${SECTIONS.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
5964
6065
For each finding:
61-
- section: exact section name from the list above
62-
- check_key: short snake_case (e.g. "homepage.h1", "schema.organization", "aibot.GPTBot")
63-
- status: "pass" | "warn" | "fail" | "unknown"
64-
- title: short headline that quotes specifics from this site
66+
- section: exact section name above
67+
- check_key: short snake_case (e.g. \`homepage.h1\`, \`schema.organization\`, \`aibot.GPTBot\`)
68+
- status: \`pass\` | \`warn\` | \`fail\` | \`unknown\`
69+
- title: short headline quoting specifics from this site
6570
- detail: one sentence with the WHY plus actual evidence
6671
- priority: 1 (critical) to 5 (polish)
6772
68-
For score: weigh critical fails heavily. Missing schema, blocked AI bots, JS-only content → below 50. Clean instrumentation → 80+.
69-
70-
For markdown: produce the complete report in Markdown with the 10 numbered headers exactly. Use ✅ / ⚠️ / ❌ / ❓ emojis on each bullet. Include a Markdown "Data Found" table. Section 10 must be reusable checkboxes ("- [ ] **P1** Add JSON-LD Organization schema").
73+
For score: critical fails dominate. Missing schema, blocked AI bots, JS-only content → below 50. Clean instrumentation → 80+.
7174
7275
OUTPUT FORMAT: Return ONLY a single JSON object matching this schema (no prose, no fences):
7376
${SCHEMA_DESC}`;
@@ -169,6 +172,10 @@ export async function oaCompatAudit(
169172
}
170173
const started = Date.now();
171174
const context = await buildContext(targetUrl);
175+
const company = (() => {
176+
try { return new URL(targetUrl).hostname.replace(/^www\./, ""); } catch { return targetUrl; }
177+
})();
178+
const aeoTask = buildAEOUserPrompt({ targetUrl, companyName: company });
172179
// DashScope / Moonshot occasionally stall mid-completion. The OpenAI SDK
173180
// default would let a job sit for ~10 minutes, blocking the credit and
174181
// confusing the user — cap each request and fail fast instead.
@@ -190,7 +197,10 @@ export async function oaCompatAudit(
190197
model: cfg.model,
191198
messages: [
192199
{ role: "system", content: SYSTEM_PROMPT },
193-
{ role: "user", content: context },
200+
// Canonical AEO task followed by pre-fetched site context. The system
201+
// prompt tells the model the second block is read-only context, not
202+
// a separate task.
203+
{ role: "user", content: `${aeoTask}\n\n---\n\nPre-fetched site content:\n\n${context}` },
194204
],
195205
response_format: { type: "json_object" },
196206
temperature: 0.2,

‎lib/audit/openai-engine.ts‎

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { z } from "zod";
33
import { zodTextFormat } from "openai/helpers/zod";
44
import { env } from "../env";
55
import type { AuditResult, Finding } from "./types";
6-
import { SECTIONS, DATA_POINTS } from "./prompt";
6+
import { SECTIONS, buildAEOUserPrompt } from "./prompt";
77
import type { ClaudeAuditResult } from "./claude-engine";
88

99
// OpenAI scan — same JSON shape as the Claude engine so the worker stores
@@ -42,29 +42,27 @@ const ResultSchema = z.object({
4242
markdown: z.string(),
4343
});
4444

45-
const SYSTEM_PROMPT = `You are CrawlProof, an AEO (Answer Engine Optimization) auditor. Analyze websites the way an LLM crawler — GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, Applebot-Extended, CCBot — would discover and read them.
45+
// Identity + tool guidance + JSON schema only. The canonical AEO task lives
46+
// in buildAEOUserPrompt and is sent as the USER turn, identical across
47+
// every LLM engine.
48+
const SYSTEM_PROMPT = `You are CrawlProof, an AEO (Answer Engine Optimization) auditor — you analyze websites the way an LLM crawler (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, Applebot-Extended, CCBot) would discover and read them.
4649
4750
Use the web_search tool to research the target. Pull the homepage, /robots.txt, /sitemap.xml, /llms.txt, /skill.md, and any About/Pricing/Blog/Contact pages you can find. Search for press, social profiles, and recent news as needed.
4851
49-
Produce a structured AEO audit. Findings must be assigned to one of these exact section names:
50-
${SECTIONS.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
52+
Follow the user's spec exactly for the report structure and Markdown output. Quote actual content from the site — don't paraphrase. Section ${SECTIONS.length} must be reusable checkboxes (\`- [ ] **P1** Add JSON-LD Organization schema\`). Use ✅ / ⚠️ / ❌ / ❓ emojis throughout. Tone: direct, specific, no fluff.
5153
52-
Data Found must cover ALL of these data points (mark found:false if you couldn't find it):
53-
${DATA_POINTS.map((d) => ` - ${d}`).join("\n")}
54+
Findings JSON must match one of these exact section names:
55+
${SECTIONS.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
5456
5557
For each finding:
56-
- section: exact section name from the list above
57-
- check_key: short snake_case (e.g. "homepage.h1", "schema.organization", "aibot.GPTBot")
58-
- status: "pass" | "warn" | "fail" | "unknown"
59-
- title: short headline that quotes specifics from this site
60-
- detail: one sentence with the WHY plus actual evidence (the H1 text, the robots.txt line, etc.)
58+
- section: exact section name above
59+
- check_key: short snake_case (e.g. \`homepage.h1\`, \`schema.organization\`, \`aibot.GPTBot\`)
60+
- status: \`pass\` | \`warn\` | \`fail\` | \`unknown\`
61+
- title: short headline quoting specifics from this site
62+
- detail: one sentence with the WHY plus actual evidence
6163
- priority: 1 (critical) to 5 (polish)
6264
63-
For score: weigh critical fails heavily. Missing schema, blocking GPTBot, hiding content behind JS = score below 50. Clean instrumentation = 80+.
64-
65-
For markdown: produce the complete report following the section structure exactly. Use ✅ / ⚠️ / ❌ / ❓ status emojis. Include a "Data Found" markdown table in section 2. Section 10 must be reusable checkboxes ("- [ ] **P1** Add JSON-LD Organization schema").
66-
67-
Tone: direct, specific, no fluff. This is a paid report.`;
65+
For score: critical fails dominate. Missing schema, blocking GPTBot, JS-only content → below 50. Clean instrumentation → 80+.`;
6866

6967
export async function openaiAudit(targetUrl: string): Promise<ClaudeAuditResult> {
7068
if (!env.openaiApiKey) {
@@ -80,7 +78,7 @@ export async function openaiAudit(targetUrl: string): Promise<ClaudeAuditResult>
8078
}
8179
})();
8280

83-
const userPrompt = `Audit this URL: ${targetUrl}\nCompany name (for the report header): ${company}\n\nPretend you've never heard of this company. Use only the public web. Use web_search to fetch pages.`;
81+
const userPrompt = buildAEOUserPrompt({ targetUrl, companyName: company });
8482

8583
const response = await client.responses.parse({
8684
model: "gpt-5",

‎lib/audit/prompt.ts‎

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,69 @@ export const SECTIONS = [
137137
"Schema / Structured Data Audit",
138138
"robots.txt and sitemap.xml Audit",
139139
"LLM / AI Crawler Accessibility",
140-
"AI Recognition",
141140
"Positioning Clarity",
142141
"Missing or Hard-to-Find Information",
143142
"Recommended Fixes",
144-
"Priority To-Do Checklist",
143+
"Priority To-Do List",
145144
] as const;
146145

147146
export type SectionName = (typeof SECTIONS)[number];
147+
148+
// The single source of truth for the AEO audit task. Every LLM engine
149+
// renders this exact prompt as its USER turn; engine-specific guidance
150+
// (tool hints, JSON output schema) stays in each engine's SYSTEM prompt.
151+
// Keep this in sync with the spec the user posted in 2026-05-13.
152+
export function buildAEOUserPrompt(input: {
153+
targetUrl: string;
154+
companyName?: string;
155+
}): string {
156+
const name = input.companyName ?? input.targetUrl;
157+
return `Pretend you've never heard of my company: ${input.targetUrl}
158+
159+
Only use what you can find on the public web. Act like an LLM crawler discovering the site for the first time.
160+
161+
Research and extract anything you can find about the company, including:
162+
163+
${DATA_POINTS.map((d) => `- ${d}`).join("\n")}
164+
165+
For each data point, tell me exactly how you found it:
166+
167+
${SOURCES.map((s) => `- ${s}`).join("\n")}
168+
169+
If you cannot find something easily, stop and explain what the challenge is.
170+
171+
Also audit the AEO basics:
172+
173+
1. Fetch the homepage.
174+
2. Flag whether important content appears to be JavaScript-rendered.
175+
3. Check for schema/structured data.
176+
4. Read robots.txt.
177+
5. Check sitemap.xml.
178+
6. Look for LLM crawler rules or AI bot access rules.
179+
7. Identify whether the site clearly explains:
180+
- Who the company is
181+
- What it does
182+
- Who it serves
183+
- Why it is different
184+
- How to buy, sign up, or contact sales
185+
186+
Summarize:
187+
188+
- What was easy to find
189+
- What was hard to find
190+
- What was impossible to find
191+
- What methods you used
192+
- What the company should fix
193+
- What pages or metadata should be added
194+
- What would improve discoverability for AI search, LLM crawlers, and answer engines
195+
196+
Output format:
197+
198+
# AEO Audit for ${name}
199+
200+
${SECTIONS.map((s, i) => `## ${i + 1}. ${s}`).join("\n\n")}
201+
202+
(For section 2, render a Markdown table: | Data Point | Found? | Source | Notes |)
203+
204+
Create the section ${SECTIONS.length} to-do list as actionable checklist items I can reuse after every major website change.`;
205+
}

‎tests/contract/markdown.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const sampleResult: AuditResult = {
3131
priority: 1,
3232
},
3333
{
34-
section: "Priority To-Do Checklist",
34+
section: "Priority To-Do List",
3535
check_key: "todo.homepage.title",
3636
status: "warn",
3737
title: "[ ] Set a meaningful <title>",
@@ -77,7 +77,7 @@ describe("toMarkdown", () => {
7777
"## 7. Positioning Clarity",
7878
"## 8. Missing or Hard-to-Find Information",
7979
"## 9. Recommended Fixes",
80-
"## 10. Priority To-Do Checklist",
80+
"## 10. Priority To-Do List",
8181
].map((h) => md.indexOf(h));
8282
expect(positions.every((p) => p >= 0)).toBe(true);
8383
// strictly ascending

0 commit comments

Comments
 (0)