Skip to content

Commit a28e1ea

Browse files
ralyodioclaude
andcommitted
feat: rule-engine roboauto parity + premium AI-Recognition section
Free tier (rule engine): - Title length: tier 30–60 = pass, <30 warn, >70 warn. Also detects error-page titles (502 Bad Gateway etc.) so a misread of a downed site doesn't silently produce a clean report. - Meta description length: 50–160 = pass, otherwise warn. - Twitter Card detection (card/title/description/image). - Content volume / word count: 300+ = pass, 100–300 warn, <100 fail. - Heading hierarchy: counts h1/h2/h3, warns if total <3. - Internal vs external link count: warns if <5 internal links. - Recommendations now include copy-pasteable code snippets for OG, Twitter Card, canonical, lang, security.txt, and Organization JSON-LD — matches roboauto's "AI Visibility Fixes" pattern. Premium tier (LLM engines): - New "AI Recognition" section in SECTIONS list. - Claude system prompt instructs the model to honestly self-report whether the target appears in its training data BEFORE any web search, with a "recognition.training_data" finding. Priority 3 (informational), not a defect. - Markdown report template reflows: AI Recognition slots in after LLM Accessibility, pushing other sections up one number. The non-Claude engines (openai/gemini/qwen/kimi/deepseek) use the same SECTIONS list via shared prompt.ts, so the section is offered to all paid engines — though prompt-side enforcement remains Anthropic-only for now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7830c3a commit a28e1ea

4 files changed

Lines changed: 243 additions & 15 deletions

File tree

‎lib/audit/checks/homepage.ts‎

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ export function checkHomepage(ctx: CrawlContext): Finding[] {
9494
});
9595
}
9696

97-
// Title
97+
// Title — flag both too-short and too-long. Search/answer engines truncate
98+
// around 60 chars and AI bots use short titles as a weak signal of intent.
9899
const title = $("title").first().text().trim();
100+
const titleLooksLikeError = /\b(502|503|504|bad gateway|service unavailable|gateway timeout|not found)\b/i.test(title);
99101
if (!title) {
100102
out.push({
101103
section: "Homepage Audit",
@@ -105,13 +107,33 @@ export function checkHomepage(ctx: CrawlContext): Finding[] {
105107
detail: "The `<title>` tag is required for search and answer engines.",
106108
priority: 1,
107109
});
108-
} else if (title.length < 15) {
110+
} else if (titleLooksLikeError) {
111+
out.push({
112+
section: "Homepage Audit",
113+
check_key: "homepage.title",
114+
status: "fail",
115+
title: "`<title>` looks like an error page",
116+
detail: `Title is "${title}" — the crawl may have hit a server error or downtime. Re-scan when the site is healthy.`,
117+
evidence: { title },
118+
priority: 1,
119+
});
120+
} else if (title.length < 30) {
121+
out.push({
122+
section: "Homepage Audit",
123+
check_key: "homepage.title",
124+
status: "warn",
125+
title: `Short \`<title>\` (${title.length} chars)`,
126+
detail: "Aim for 30–60 chars. Lead with brand or product, then value prop.",
127+
evidence: { title },
128+
priority: 3,
129+
});
130+
} else if (title.length > 70) {
109131
out.push({
110132
section: "Homepage Audit",
111133
check_key: "homepage.title",
112134
status: "warn",
113-
title: "Very short `<title>`",
114-
detail: `Title is only ${title.length} chars.`,
135+
title: `Long \`<title>\` (${title.length} chars)`,
136+
detail: "Engines and AI snippets truncate titles around 60–70 chars. Trim to keep the key phrase visible.",
115137
evidence: { title },
116138
priority: 3,
117139
});
@@ -120,13 +142,14 @@ export function checkHomepage(ctx: CrawlContext): Finding[] {
120142
section: "Homepage Audit",
121143
check_key: "homepage.title",
122144
status: "pass",
123-
title: "`<title>` present",
145+
title: `\`<title>\` present (${title.length} chars)`,
124146
evidence: { title },
125147
priority: 5,
126148
});
127149
}
128150

129-
// Meta description
151+
// Meta description — length tiers (Google truncates ~155, AI bots use it
152+
// as the default snippet).
130153
const description = $("meta[name='description']").attr("content")?.trim();
131154
if (!description) {
132155
out.push({
@@ -137,12 +160,32 @@ export function checkHomepage(ctx: CrawlContext): Finding[] {
137160
detail: "Add a `<meta name=\"description\">` to control the snippet AI/SERP show.",
138161
priority: 2,
139162
});
163+
} else if (description.length < 50) {
164+
out.push({
165+
section: "Homepage Audit",
166+
check_key: "homepage.description",
167+
status: "warn",
168+
title: `Short meta description (${description.length} chars)`,
169+
detail: "Aim for 50–160 chars. Restate your value prop in plain language.",
170+
evidence: { description },
171+
priority: 3,
172+
});
173+
} else if (description.length > 160) {
174+
out.push({
175+
section: "Homepage Audit",
176+
check_key: "homepage.description",
177+
status: "warn",
178+
title: `Long meta description (${description.length} chars)`,
179+
detail: "Snippets truncate around 160 chars. Tighten to keep the key sentence visible.",
180+
evidence: { description },
181+
priority: 3,
182+
});
140183
} else {
141184
out.push({
142185
section: "Homepage Audit",
143186
check_key: "homepage.description",
144187
status: "pass",
145-
title: "Meta description present",
188+
title: `Meta description present (${description.length} chars)`,
146189
evidence: { description },
147190
priority: 5,
148191
});
@@ -179,6 +222,32 @@ export function checkHomepage(ctx: CrawlContext): Finding[] {
179222
priority: ogMissing.length === 0 ? 5 : 3,
180223
});
181224

225+
// Twitter Card — AI link previews and social agents both consume these.
226+
const twitter = {
227+
card: $("meta[name='twitter:card']").attr("content"),
228+
title: $("meta[name='twitter:title']").attr("content"),
229+
description: $("meta[name='twitter:description']").attr("content"),
230+
image: $("meta[name='twitter:image']").attr("content"),
231+
};
232+
const twitterMissing = Object.entries(twitter).filter(([, v]) => !v).map(([k]) => k);
233+
out.push({
234+
section: "Homepage Audit",
235+
check_key: "homepage.twitter",
236+
status: twitterMissing.length === 0 ? "pass" : twitterMissing.length === 4 ? "warn" : "warn",
237+
title:
238+
twitterMissing.length === 0
239+
? "Twitter Card tags complete"
240+
: twitterMissing.length === 4
241+
? "No Twitter Card tags"
242+
: `Twitter Card: missing ${twitterMissing.join(", ")}`,
243+
detail:
244+
twitterMissing.length === 0
245+
? undefined
246+
: "Add twitter:card, twitter:title, twitter:description, twitter:image for richer previews in social and AI agent surfaces.",
247+
evidence: twitter as Record<string, unknown>,
248+
priority: twitterMissing.length === 0 ? 5 : 3,
249+
});
250+
182251
// JS-rendered content check
183252
if (home.renderedText && home.renderedBytes) {
184253
const rawText = $.root().text().replace(/\s+/g, " ").trim();
@@ -232,5 +301,73 @@ export function checkHomepage(ctx: CrawlContext): Finding[] {
232301
});
233302
}
234303

304+
// Content volume — AI models need enough substance to summarize. Count
305+
// visible text in <body>, ignoring scripts/styles/templates.
306+
const $body = $("body").clone();
307+
$body.find("script, style, noscript, template").remove();
308+
const visibleText = $body.text().replace(/\s+/g, " ").trim();
309+
const wordCount = visibleText ? visibleText.split(/\s+/).length : 0;
310+
out.push({
311+
section: "Homepage Audit",
312+
check_key: "homepage.word_count",
313+
status: wordCount >= 300 ? "pass" : wordCount >= 100 ? "warn" : "fail",
314+
title: `Content volume: ${wordCount} words`,
315+
detail:
316+
wordCount >= 300
317+
? "Substantive content — AI models have enough to summarize and recommend."
318+
: wordCount >= 100
319+
? "Thin content. Aim for 300+ words on the homepage so AI models can extract a useful description."
320+
: "Very thin content. AI models need substantive text to understand and recommend your site.",
321+
evidence: { wordCount },
322+
priority: wordCount >= 300 ? 5 : wordCount >= 100 ? 3 : 1,
323+
});
324+
325+
// Heading hierarchy — AI uses h1/h2/h3 to chunk content. A flat document
326+
// with one heading is harder to parse than one with sectioned structure.
327+
const h2Count = $("h2").length;
328+
const h3Count = $("h3").length;
329+
const totalHeadings = h1s.length + h2Count + h3Count;
330+
out.push({
331+
section: "Homepage Audit",
332+
check_key: "homepage.heading_structure",
333+
status: totalHeadings >= 3 ? "pass" : totalHeadings >= 1 ? "warn" : "fail",
334+
title: `Heading structure: ${totalHeadings} (h1:${h1s.length}, h2:${h2Count}, h3:${h3Count})`,
335+
detail:
336+
totalHeadings >= 3
337+
? "Multiple headings help AI chunk and outline your page."
338+
: "Few headings make it hard for AI to understand sectioning. Use h2/h3 to label each section.",
339+
evidence: { h1: h1s.length, h2: h2Count, h3: h3Count },
340+
priority: totalHeadings >= 3 ? 5 : 3,
341+
});
342+
343+
// Internal link count — AI crawlers follow links to discover the rest of
344+
// the site. Need at least a nav-worth.
345+
const allLinks = $("a[href]").get();
346+
let internalLinks = 0;
347+
let externalLinks = 0;
348+
for (const a of allLinks) {
349+
const href = $(a).attr("href") ?? "";
350+
if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) continue;
351+
try {
352+
const u = new URL(href, ctx.target);
353+
if (u.hostname === ctx.host) internalLinks++;
354+
else externalLinks++;
355+
} catch {
356+
// skip invalid hrefs
357+
}
358+
}
359+
out.push({
360+
section: "Homepage Audit",
361+
check_key: "homepage.internal_links",
362+
status: internalLinks >= 5 ? "pass" : internalLinks >= 1 ? "warn" : "fail",
363+
title: `Internal links: ${internalLinks}`,
364+
detail:
365+
internalLinks >= 5
366+
? `${internalLinks} internal + ${externalLinks} external links help crawlers navigate.`
367+
: "Few internal links. Add a nav/footer with links to your key pages so AI crawlers can discover them.",
368+
evidence: { internal: internalLinks, external: externalLinks },
369+
priority: internalLinks >= 5 ? 5 : 3,
370+
});
371+
235372
return out;
236373
}

‎lib/audit/claude-engine.ts‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ For each finding:
5858
- detail: one sentence with the WHY plus the specific evidence (e.g. the actual H1 text, the actual robots.txt line)
5959
- priority: 1 (critical, breaks AEO entirely) to 5 (polish)
6060
61+
For the "AI Recognition" section, BEFORE doing any web_fetch / web_search, answer honestly from your own training data:
62+
- 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)
63+
- check_key: "recognition.training_data"
64+
- title: "Familiar with <domain>" or "No prior knowledge of <domain>"
65+
- 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.
66+
- priority: 3 (informational signal, not a defect)
67+
68+
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.
69+
6170
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+.
6271
6372
For summary.pass/warn/fail/unknown: count each across all findings.
@@ -76,10 +85,12 @@ For markdown: produce the complete report in Markdown with these exact section h
7685
## 4. Schema / Structured Data Audit
7786
## 5. robots.txt and sitemap.xml Audit
7887
## 6. LLM / AI Crawler Accessibility
79-
## 7. Positioning Clarity
80-
## 8. Missing or Hard-to-Find Information
81-
## 9. Recommended Fixes
82-
## 10. Priority To-Do Checklist
88+
## 7. AI Recognition
89+
(One paragraph stating whether you recognize this site from training data, and what specifically you remember if so. No fluff.)
90+
## 8. Positioning Clarity
91+
## 9. Missing or Hard-to-Find Information
92+
## 10. Recommended Fixes
93+
## 11. Priority To-Do Checklist
8394
8495
Use ✅ / ⚠️ / ❌ / ❓ status emojis on each bullet. Quote actual content from the site. Section 10 must be reusable checkboxes ("- [ ] **P1** Add JSON-LD Organization schema").
8596

‎lib/audit/prompt.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export const SECTIONS = [
137137
"Schema / Structured Data Audit",
138138
"robots.txt and sitemap.xml Audit",
139139
"LLM / AI Crawler Accessibility",
140+
"AI Recognition",
140141
"Positioning Clarity",
141142
"Missing or Hard-to-Find Information",
142143
"Recommended Fixes",

‎lib/audit/recommendations.ts‎

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,67 @@ const RECS: Record<string, { title: string; how: string }> = {
88
},
99
"homepage.title": {
1010
title: "Set a meaningful `<title>`",
11-
how: "50–60 chars. Lead with the brand or product, then the value prop: 'CrawlProof — AEO audits for AI crawlers.'",
11+
how:
12+
"30–60 chars. Lead with the brand or product, then the value prop.\n\n" +
13+
"```html\n<title>CrawlProof — AEO audits for AI crawlers</title>\n```",
1214
},
1315
"homepage.description": {
1416
title: "Add a meta description",
15-
how: "150–160 chars. Repeat your core value prop in plain language; this often becomes the AI snippet.",
17+
how:
18+
"50–160 chars. Repeat your core value prop in plain language; this often becomes the AI snippet.\n\n" +
19+
"```html\n<meta name=\"description\" content=\"CrawlProof shows you exactly how AI crawlers see your site, then tells you what to fix.\" />\n```",
1620
},
1721
"homepage.canonical": {
1822
title: "Add a canonical link",
19-
how: "`<link rel=\"canonical\" href=\"https://yoursite.com/\">` on every page to prevent dup-content drift.",
23+
how:
24+
"Prevents dup-content drift and tells AI crawlers which URL is authoritative.\n\n" +
25+
"```html\n<link rel=\"canonical\" href=\"https://yoursite.com/\" />\n```",
2026
},
2127
"homepage.og": {
2228
title: "Complete Open Graph tags",
23-
how: "Set og:title, og:description, og:image. AI bots also use OG for fast disambiguation.",
29+
how:
30+
"AI bots use OG for fast disambiguation. Add all four:\n\n" +
31+
"```html\n" +
32+
"<meta property=\"og:title\" content=\"Your Page Title\" />\n" +
33+
"<meta property=\"og:description\" content=\"50–160 char description of this page.\" />\n" +
34+
"<meta property=\"og:image\" content=\"https://yoursite.com/og-image.jpg\" />\n" +
35+
"<meta property=\"og:url\" content=\"https://yoursite.com/\" />\n" +
36+
"<meta property=\"og:type\" content=\"website\" />\n" +
37+
"<meta property=\"og:site_name\" content=\"YourSite\" />\n" +
38+
"```",
39+
},
40+
"homepage.twitter": {
41+
title: "Add Twitter Card meta tags",
42+
how:
43+
"Used by social platforms and AI agents for richer previews.\n\n" +
44+
"```html\n" +
45+
"<meta name=\"twitter:card\" content=\"summary_large_image\" />\n" +
46+
"<meta name=\"twitter:title\" content=\"Your Page Title\" />\n" +
47+
"<meta name=\"twitter:description\" content=\"50–160 char description.\" />\n" +
48+
"<meta name=\"twitter:image\" content=\"https://yoursite.com/og-image.jpg\" />\n" +
49+
"```",
50+
},
51+
"homepage.lang": {
52+
title: "Declare a language on the <html> tag",
53+
how:
54+
"Helps AI engines serve localized results and confirms content language.\n\n" +
55+
"```html\n<html lang=\"en\">\n```",
56+
},
57+
"homepage.load_time": {
58+
title: "Speed up homepage rendering",
59+
how: "AI crawlers commonly time out around 3s. Cache the HTML, ship less JS for the first paint, and pre-render the hero section server-side.",
60+
},
61+
"homepage.word_count": {
62+
title: "Add more substantive homepage content",
63+
how: "AI models need 300+ words of visible body text to summarize and recommend a site. Add a value-prop paragraph, a short FAQ, and a 'how it works' section.",
64+
},
65+
"homepage.heading_structure": {
66+
title: "Add structured headings",
67+
how: "Use h2 for each section and h3 for sub-points. AI uses these to outline and chunk the page.",
68+
},
69+
"homepage.internal_links": {
70+
title: "Add internal navigation links",
71+
how: "Top nav + footer with links to /pricing, /docs, /about, /contact gives AI crawlers an entry point to the rest of the site.",
2472
},
2573
"homepage.js_rendered": {
2674
title: "Server-render critical homepage content",
@@ -78,6 +126,37 @@ const RECS: Record<string, { title: string; how: string }> = {
78126
title: "Add /skill.md",
79127
how: "Describe what an agent can do with your site (e.g., 'Search docs', 'Look up pricing'). Useful for agentic flows.",
80128
},
129+
"security_txt": {
130+
title: "Publish /.well-known/security.txt",
131+
how:
132+
"A security contact builds trust with crawlers and researchers. Minimal example:\n\n" +
133+
"```\n" +
134+
"Contact: mailto:security@yourdomain.com\n" +
135+
"Expires: 2027-01-01T00:00:00.000Z\n" +
136+
"Preferred-Languages: en\n" +
137+
"```",
138+
},
139+
"schema.json_ld_organization": {
140+
title: "Add Schema.org Organization JSON-LD",
141+
how:
142+
"Helps AI engines resolve your brand entity:\n\n" +
143+
"```html\n" +
144+
"<script type=\"application/ld+json\">\n" +
145+
"{\n" +
146+
" \"@context\": \"https://schema.org\",\n" +
147+
" \"@type\": \"Organization\",\n" +
148+
" \"name\": \"Your Company\",\n" +
149+
" \"url\": \"https://yoursite.com\",\n" +
150+
" \"logo\": \"https://yoursite.com/logo.png\",\n" +
151+
" \"description\": \"What you do, in one sentence.\",\n" +
152+
" \"sameAs\": [\n" +
153+
" \"https://twitter.com/yourhandle\",\n" +
154+
" \"https://linkedin.com/company/yourcompany\"\n" +
155+
" ]\n" +
156+
"}\n" +
157+
"</script>\n" +
158+
"```",
159+
},
81160
"positioning.who": {
82161
title: "Make your About/Team page reachable",
83162
how: "Add a top-nav or footer link to /about or /team so LLMs can identify the entity behind the site.",

0 commit comments

Comments
 (0)