Skip to content

Commit 7830c3a

Browse files
ralyodioclaude
andcommitted
feat: rule-engine — security.txt probe, lang attr, page load grade
First slice of roboauto.ai parity for the free tier: - Probe /.well-known/security.txt and emit a finding under LLM Accessibility. Crawlers and security researchers expect it; AI systems use its presence as a trust signal. - Check <html lang="…"> — engines use it to serve localized results. Missing => warn with example. - Grade homepage load time: <1s pass, <3s warn, >=3s fail. GPTBot and similar tend to time out around 3s. More additions (title-length tiers, meta-description length, Twitter Card, word count, heading hierarchy, internal-link count, and copy- pasteable code-snippet recommendations) coming in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent efc5d95 commit 7830c3a

4 files changed

Lines changed: 51 additions & 1 deletion

File tree

‎lib/audit/checks/homepage.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,41 @@ export function checkHomepage(ctx: CrawlContext): Finding[] {
2626
priority: 5,
2727
});
2828

29+
// Page load time grading. Crawlers (including AI bots) deprioritize slow
30+
// pages — anything over ~3s tends to time out for GPTBot.
31+
const loadSec = home.fetchMs / 1000;
32+
out.push({
33+
section: "Homepage Audit",
34+
check_key: "homepage.load_time",
35+
status: loadSec < 1 ? "pass" : loadSec < 3 ? "warn" : "fail",
36+
title: `Page load time: ${loadSec.toFixed(2)}s`,
37+
detail:
38+
loadSec < 1
39+
? "Fast — well within AI crawler budgets."
40+
: loadSec < 3
41+
? "Acceptable — consider optimizing for faster crawl times."
42+
: "Too slow — AI crawlers commonly time out around 3s.",
43+
evidence: { fetchMs: home.fetchMs },
44+
priority: loadSec < 1 ? 5 : loadSec < 3 ? 3 : 2,
45+
});
46+
2947
const $ = cheerio.load(home.rawHtml);
3048

49+
// <html lang="…"> — helps engines serve localized results and confirms
50+
// content language without needing to NLP-detect it.
51+
const lang = $("html").attr("lang")?.trim();
52+
out.push({
53+
section: "Homepage Audit",
54+
check_key: "homepage.lang",
55+
status: lang ? "pass" : "warn",
56+
title: lang ? `<html lang="${lang}"> declared` : "Missing <html lang> attribute",
57+
detail: lang
58+
? undefined
59+
: 'Add a lang attribute on the root <html> tag (e.g. <html lang="en">) so AI engines serve language-appropriate results.',
60+
evidence: lang ? { lang } : undefined,
61+
priority: lang ? 5 : 3,
62+
});
63+
3164
// H1
3265
const h1s = $("h1").map((_, el) => $(el).text().trim()).get().filter(Boolean);
3366
if (h1s.length === 0) {

‎lib/audit/checks/robots.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,19 @@ export function checkRobotsAndSitemap(ctx: CrawlContext): Finding[] {
200200
});
201201
}
202202

203+
const sec = ctx.wellKnown.securityTxt;
204+
out.push({
205+
section: "LLM / AI Crawler Accessibility",
206+
check_key: "security_txt",
207+
status: sec && sec.status === 200 ? "pass" : "warn",
208+
title: sec && sec.status === 200
209+
? "/.well-known/security.txt present"
210+
: "/.well-known/security.txt missing",
211+
detail: sec && sec.status === 200
212+
? "Security contact published — builds trust with crawlers and security researchers."
213+
: "Publish a /.well-known/security.txt with at least a Contact: line. Crawlers and security researchers expect it; AI systems use it as a trust signal.",
214+
priority: sec && sec.status === 200 ? 5 : 3,
215+
});
216+
203217
return out;
204218
}

‎lib/audit/engine.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,14 @@ export async function runAudit(
9595
// 1. Fetch homepage.
9696
const homepage = await fetchPage(target);
9797
// 2. Probe well-known files in parallel.
98-
const [robots, sitemap, llmsTxt, llmsFullTxt, skillMd, aiPlugin] = await Promise.all([
98+
const [robots, sitemap, llmsTxt, llmsFullTxt, skillMd, aiPlugin, securityTxt] = await Promise.all([
9999
probeText(`${origin}/robots.txt`),
100100
probeText(`${origin}/sitemap.xml`),
101101
probeText(`${origin}/llms.txt`),
102102
probeText(`${origin}/llms-full.txt`),
103103
probeText(`${origin}/skill.md`),
104104
probeText(`${origin}/.well-known/ai-plugin.json`),
105+
probeText(`${origin}/.well-known/security.txt`),
105106
]);
106107

107108
// 3. Render homepage with Playwright (best effort).
@@ -137,6 +138,7 @@ export async function runAudit(
137138
llmsFullTxt: llmsFullTxt,
138139
skillMd: skillMd,
139140
aiPlugin: aiPlugin,
141+
securityTxt: securityTxt,
140142
},
141143
findings: [],
142144
};

‎lib/audit/types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type CrawlContext = {
3838
llmsFullTxt?: { content: string; status: number };
3939
skillMd?: { content: string; status: number };
4040
aiPlugin?: { content: string; status: number };
41+
securityTxt?: { content: string; status: number };
4142
};
4243
findings: Finding[];
4344
};

0 commit comments

Comments
 (0)