@rankcli/agent-runtime 0.0.35 → 0.0.37
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.d.mts +1 -6
- package/dist/index.d.ts +1 -6
- package/dist/index.js +152 -10
- package/dist/index.mjs +151 -10
- package/package.json +3 -2
package/dist/index.d.mts
CHANGED
|
@@ -3106,14 +3106,9 @@ declare function servedDir(framework: Framework | undefined, files: string[], ap
|
|
|
3106
3106
|
|
|
3107
3107
|
interface ReplacementConcern {
|
|
3108
3108
|
/** Machine-readable, for logs and the PR body. */
|
|
3109
|
-
code: 'shrunk' | 'lost_closing_tag' | 'lost_script' | 'lost_stylesheet' | 'lost_verification' | 'lost_import' | 'invalid_json' | 'invalid_xml' | 'invalid_json_ld' | 'fewer_sitemap_urls' | 'lastmod_moved_backwards';
|
|
3109
|
+
code: 'shrunk' | 'lost_closing_tag' | 'lost_script' | 'lost_stylesheet' | 'lost_verification' | 'lost_import' | 'invalid_json' | 'invalid_xml' | 'invalid_json_ld' | 'fewer_sitemap_urls' | 'lastmod_moved_backwards' | 'placeholder_content' | 'canonical_in_shared_shell';
|
|
3110
3110
|
detail: string;
|
|
3111
3111
|
}
|
|
3112
|
-
/**
|
|
3113
|
-
* Everything wrong with replacing `original` with `replacement` at `path`. Empty means safe
|
|
3114
|
-
* to commit. `original` is null for a file being created, where there is nothing to lose and
|
|
3115
|
-
* only the content's own validity matters.
|
|
3116
|
-
*/
|
|
3117
3112
|
declare function assessReplacement(path: string, original: string | null, replacement: string): ReplacementConcern[];
|
|
3118
3113
|
/** One line naming why a file was dropped, for a log line or a PR body. */
|
|
3119
3114
|
declare function describeConcerns(concerns: ReplacementConcern[]): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -3106,14 +3106,9 @@ declare function servedDir(framework: Framework | undefined, files: string[], ap
|
|
|
3106
3106
|
|
|
3107
3107
|
interface ReplacementConcern {
|
|
3108
3108
|
/** Machine-readable, for logs and the PR body. */
|
|
3109
|
-
code: 'shrunk' | 'lost_closing_tag' | 'lost_script' | 'lost_stylesheet' | 'lost_verification' | 'lost_import' | 'invalid_json' | 'invalid_xml' | 'invalid_json_ld' | 'fewer_sitemap_urls' | 'lastmod_moved_backwards';
|
|
3109
|
+
code: 'shrunk' | 'lost_closing_tag' | 'lost_script' | 'lost_stylesheet' | 'lost_verification' | 'lost_import' | 'invalid_json' | 'invalid_xml' | 'invalid_json_ld' | 'fewer_sitemap_urls' | 'lastmod_moved_backwards' | 'placeholder_content' | 'canonical_in_shared_shell';
|
|
3110
3110
|
detail: string;
|
|
3111
3111
|
}
|
|
3112
|
-
/**
|
|
3113
|
-
* Everything wrong with replacing `original` with `replacement` at `path`. Empty means safe
|
|
3114
|
-
* to commit. `original` is null for a file being created, where there is nothing to lose and
|
|
3115
|
-
* only the content's own validity matters.
|
|
3116
|
-
*/
|
|
3117
3112
|
declare function assessReplacement(path: string, original: string | null, replacement: string): ReplacementConcern[];
|
|
3118
3113
|
/** One line naming why a file was dropped, for a log line or a PR body. */
|
|
3119
3114
|
declare function describeConcerns(concerns: ReplacementConcern[]): string;
|
package/dist/index.js
CHANGED
|
@@ -10401,6 +10401,121 @@ async function checkInternalRedirects(internalLinks, batchSize = 10) {
|
|
|
10401
10401
|
return { redirectLinks };
|
|
10402
10402
|
}
|
|
10403
10403
|
|
|
10404
|
+
// src/audit/checks/crawler-access.ts
|
|
10405
|
+
init_http();
|
|
10406
|
+
var CRAWLERS = [
|
|
10407
|
+
{
|
|
10408
|
+
name: "Googlebot",
|
|
10409
|
+
userAgent: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
|
|
10410
|
+
matters: "Google Search indexing",
|
|
10411
|
+
critical: true
|
|
10412
|
+
},
|
|
10413
|
+
{
|
|
10414
|
+
name: "bingbot",
|
|
10415
|
+
userAgent: "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
|
|
10416
|
+
matters: "Bing indexing, and Copilot answers",
|
|
10417
|
+
critical: true
|
|
10418
|
+
},
|
|
10419
|
+
{
|
|
10420
|
+
name: "GPTBot",
|
|
10421
|
+
userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)",
|
|
10422
|
+
matters: "ChatGPT being able to read and cite the page",
|
|
10423
|
+
critical: false
|
|
10424
|
+
},
|
|
10425
|
+
{
|
|
10426
|
+
name: "ClaudeBot",
|
|
10427
|
+
userAgent: "Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)",
|
|
10428
|
+
matters: "Claude being able to read and cite the page",
|
|
10429
|
+
critical: false
|
|
10430
|
+
},
|
|
10431
|
+
{
|
|
10432
|
+
name: "PerplexityBot",
|
|
10433
|
+
userAgent: "Mozilla/5.0 (compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)",
|
|
10434
|
+
matters: "Perplexity being able to read and cite the page",
|
|
10435
|
+
critical: false
|
|
10436
|
+
}
|
|
10437
|
+
];
|
|
10438
|
+
var BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
10439
|
+
function isRefusal(status) {
|
|
10440
|
+
return status === 401 || status === 403 || status === 429 || status === 503;
|
|
10441
|
+
}
|
|
10442
|
+
async function statusFor(url, userAgent) {
|
|
10443
|
+
try {
|
|
10444
|
+
const response = await httpGet(url, {
|
|
10445
|
+
headers: { "User-Agent": userAgent },
|
|
10446
|
+
timeout: 15e3,
|
|
10447
|
+
// We want the status itself, not an exception on 4xx/5xx.
|
|
10448
|
+
validateStatus: () => true
|
|
10449
|
+
});
|
|
10450
|
+
return response.status;
|
|
10451
|
+
} catch {
|
|
10452
|
+
return null;
|
|
10453
|
+
}
|
|
10454
|
+
}
|
|
10455
|
+
async function analyzeCrawlerAccess(url) {
|
|
10456
|
+
const issues = [];
|
|
10457
|
+
const baselineStatus = await statusFor(url, BROWSER_UA);
|
|
10458
|
+
const data = { baselineStatus, crawlers: [] };
|
|
10459
|
+
if (baselineStatus === null || baselineStatus >= 400) {
|
|
10460
|
+
return { issues, data };
|
|
10461
|
+
}
|
|
10462
|
+
const results = await Promise.all(
|
|
10463
|
+
CRAWLERS.map(async (crawler) => ({
|
|
10464
|
+
crawler,
|
|
10465
|
+
status: await statusFor(url, crawler.userAgent)
|
|
10466
|
+
}))
|
|
10467
|
+
);
|
|
10468
|
+
const blocked = results.filter(({ status }) => status !== null && isRefusal(status));
|
|
10469
|
+
data.crawlers = results.map(({ crawler, status }) => ({
|
|
10470
|
+
name: crawler.name,
|
|
10471
|
+
status,
|
|
10472
|
+
blocked: status !== null && isRefusal(status)
|
|
10473
|
+
}));
|
|
10474
|
+
if (blocked.length === 0) return { issues, data };
|
|
10475
|
+
const search = blocked.filter(({ crawler }) => crawler.critical);
|
|
10476
|
+
const ai = blocked.filter(({ crawler }) => !crawler.critical);
|
|
10477
|
+
if (search.length > 0) {
|
|
10478
|
+
const names = search.map(({ crawler }) => crawler.name).join(", ");
|
|
10479
|
+
issues.push({
|
|
10480
|
+
code: "SEARCH_CRAWLER_BLOCKED_AT_EDGE",
|
|
10481
|
+
severity: "error",
|
|
10482
|
+
category: "crawlability",
|
|
10483
|
+
title: `${names} refused at the edge, not by robots.txt`,
|
|
10484
|
+
description: `A browser request to this URL returned ${baselineStatus}, but ${names} received ${search.map(({ status }) => status).join(", ")}. Something in front of the origin - a CDN, WAF or bot rule - is turning the crawler away, and robots.txt cannot show you this.`,
|
|
10485
|
+
impact: "Pages a search crawler cannot fetch fall out of the index. This is the failure mode behind Cloudflare\u2019s 2026-09-15 default, where blocking AI training also blocked Googlebot because it is classed as a mixed-use crawler.",
|
|
10486
|
+
howToFix: `Check your CDN or WAF bot rules rather than robots.txt.
|
|
10487
|
+
|
|
10488
|
+
On Cloudflare: the zone's Security > Bots settings, and any "Block AI Bots" or AI Crawler Control toggle. Because Googlebot crawls for both Search and AI training, blocking Training applies to all of its functions on ad-supported pages - allow Search crawlers explicitly, or opt out of the default.
|
|
10489
|
+
|
|
10490
|
+
Confirm with Search Console's URL Inspection, which fetches as the verified Googlebot.`,
|
|
10491
|
+
affectedUrls: [url],
|
|
10492
|
+
details: {
|
|
10493
|
+
baselineStatus,
|
|
10494
|
+
blocked: search.map(({ crawler, status }) => ({ crawler: crawler.name, status })),
|
|
10495
|
+
note: "Observed with the crawler\u2019s user-agent. Real crawlers are also verified by IP, so a site may treat them differently."
|
|
10496
|
+
}
|
|
10497
|
+
});
|
|
10498
|
+
}
|
|
10499
|
+
if (ai.length > 0) {
|
|
10500
|
+
const names = ai.map(({ crawler }) => crawler.name).join(", ");
|
|
10501
|
+
issues.push({
|
|
10502
|
+
code: "AI_CRAWLER_BLOCKED_AT_EDGE",
|
|
10503
|
+
severity: "warning",
|
|
10504
|
+
category: "ai-readiness",
|
|
10505
|
+
title: `${names} refused at the edge, not by robots.txt`,
|
|
10506
|
+
description: `A browser request returned ${baselineStatus}, but ${names} received ${ai.map(({ status }) => status).join(", ")}. The block is in front of the origin, so allowing these agents in robots.txt will not change it.`,
|
|
10507
|
+
impact: "An assistant that cannot fetch the page cannot quote it. This is invisible to a robots.txt check, which is where most AI-visibility tools stop.",
|
|
10508
|
+
howToFix: `If blocking them is deliberate, nothing to do. If not, the rule is in your CDN or WAF, not robots.txt - on Cloudflare see Security > Bots and any AI Crawler Control setting.`,
|
|
10509
|
+
affectedUrls: [url],
|
|
10510
|
+
details: {
|
|
10511
|
+
baselineStatus,
|
|
10512
|
+
blocked: ai.map(({ crawler, status }) => ({ crawler: crawler.name, status }))
|
|
10513
|
+
}
|
|
10514
|
+
});
|
|
10515
|
+
}
|
|
10516
|
+
return { issues, data };
|
|
10517
|
+
}
|
|
10518
|
+
|
|
10404
10519
|
// src/audit/checks/ai-readiness.ts
|
|
10405
10520
|
init_http();
|
|
10406
10521
|
var cheerio13 = __toESM(require("cheerio"));
|
|
@@ -11429,17 +11544,14 @@ var DOM_SIZE_ERROR = 3e3;
|
|
|
11429
11544
|
var DOM_DEPTH_WARNING = 32;
|
|
11430
11545
|
function countNodes($) {
|
|
11431
11546
|
let count = 0;
|
|
11432
|
-
function traverse(
|
|
11433
|
-
|
|
11434
|
-
|
|
11547
|
+
function traverse(node) {
|
|
11548
|
+
const el = node;
|
|
11549
|
+
if (el.type === "tag") count++;
|
|
11435
11550
|
if (el.children) {
|
|
11436
|
-
for (const child of el.children)
|
|
11437
|
-
traverse(child);
|
|
11438
|
-
}
|
|
11551
|
+
for (const child of el.children) traverse(child);
|
|
11439
11552
|
}
|
|
11440
11553
|
}
|
|
11441
|
-
|
|
11442
|
-
root.each((_, el) => traverse(el));
|
|
11554
|
+
$.root().each((_, el) => traverse(el));
|
|
11443
11555
|
return count;
|
|
11444
11556
|
}
|
|
11445
11557
|
function calculateMaxDepth($) {
|
|
@@ -14325,7 +14437,7 @@ function analyzeResponsiveImages(html, url) {
|
|
|
14325
14437
|
if (imagesWithoutDimensions >= 3) {
|
|
14326
14438
|
issues.push({
|
|
14327
14439
|
code: "IMAGES_MISSING_DIMENSIONS",
|
|
14328
|
-
severity: "
|
|
14440
|
+
severity: "warning",
|
|
14329
14441
|
category: "performance",
|
|
14330
14442
|
title: `${imagesWithoutDimensions} images missing width/height attributes`,
|
|
14331
14443
|
description: `Found ${imagesWithoutDimensions} images without explicit dimensions.`,
|
|
@@ -14340,7 +14452,8 @@ function analyzeResponsiveImages(html, url) {
|
|
|
14340
14452
|
} else if (imagesWithoutDimensions > 0) {
|
|
14341
14453
|
issues.push({
|
|
14342
14454
|
code: "IMAGES_MISSING_DIMENSIONS",
|
|
14343
|
-
|
|
14455
|
+
// One or two keeps a gradient below the >=3 case rather than flattening both to warning.
|
|
14456
|
+
severity: "notice",
|
|
14344
14457
|
category: "performance",
|
|
14345
14458
|
title: `${imagesWithoutDimensions} image(s) missing width/height attributes`,
|
|
14346
14459
|
description: `Found ${imagesWithoutDimensions} images without explicit dimensions.`,
|
|
@@ -17710,6 +17823,7 @@ async function runFullAudit(options) {
|
|
|
17710
17823
|
asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
|
|
17711
17824
|
asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
|
|
17712
17825
|
asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
|
|
17826
|
+
asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
|
|
17713
17827
|
asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
|
|
17714
17828
|
asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
|
|
17715
17829
|
const frameworkInfo = detectFramework({ html, headers });
|
|
@@ -33097,9 +33211,37 @@ function jsonLdConcerns(html) {
|
|
|
33097
33211
|
}
|
|
33098
33212
|
return bad;
|
|
33099
33213
|
}
|
|
33214
|
+
var PLACEHOLDERS = [
|
|
33215
|
+
{ pattern: /\+?1[-\s.]?\(?800\)?[-\s.]?555[-\s.]?\d{4}/i, what: "a reserved 555 phone number" },
|
|
33216
|
+
{ pattern: /\b555[-\s.]?01\d{2}\b/, what: "a reserved 555 phone number" },
|
|
33217
|
+
{ pattern: /\bAnytown\b/i, what: '"Anytown"' },
|
|
33218
|
+
{ pattern: /\b123 Main (Street|St)\b/i, what: '"123 Main Street"' },
|
|
33219
|
+
{ pattern: /your_[a-z_]+/i, what: 'an unfilled "your_..." placeholder' },
|
|
33220
|
+
{ pattern: /\b(yourdomain|your-domain|example)\.(com|org|net)\b/i, what: "a placeholder domain" },
|
|
33221
|
+
{ pattern: /\blorem ipsum\b/i, what: "lorem ipsum" },
|
|
33222
|
+
{ pattern: /\bXXX-XXX-XXXX\b/i, what: "an unfilled phone template" }
|
|
33223
|
+
];
|
|
33224
|
+
function isSharedShell(html) {
|
|
33225
|
+
return /<div[^>]+id=["'](root|app|__next|__nuxt)["']/i.test(html);
|
|
33226
|
+
}
|
|
33100
33227
|
function assessReplacement(path3, original, replacement) {
|
|
33101
33228
|
const concerns = [];
|
|
33102
33229
|
const add = (code, detail) => concerns.push({ code, detail });
|
|
33230
|
+
for (const { pattern, what } of PLACEHOLDERS) {
|
|
33231
|
+
const hit = replacement.match(pattern);
|
|
33232
|
+
if (hit && !(original && pattern.test(original))) {
|
|
33233
|
+
add("placeholder_content", `contains ${what} (${hit[0]})`);
|
|
33234
|
+
}
|
|
33235
|
+
}
|
|
33236
|
+
if (/\.html?$/i.test(path3) && isSharedShell(replacement)) {
|
|
33237
|
+
const addsCanonical = /<link[^>]+rel=["']canonical["']/i.test(replacement) && !(original && /<link[^>]+rel=["']canonical["']/i.test(original));
|
|
33238
|
+
if (addsCanonical) {
|
|
33239
|
+
add(
|
|
33240
|
+
"canonical_in_shared_shell",
|
|
33241
|
+
`${path3} mounts a single-page app, so a canonical here applies to every route`
|
|
33242
|
+
);
|
|
33243
|
+
}
|
|
33244
|
+
}
|
|
33103
33245
|
if (/\.json$/i.test(path3) || /\.webmanifest$/i.test(path3)) {
|
|
33104
33246
|
try {
|
|
33105
33247
|
JSON.parse(replacement);
|
package/dist/index.mjs
CHANGED
|
@@ -6862,6 +6862,120 @@ async function checkInternalRedirects(internalLinks, batchSize = 10) {
|
|
|
6862
6862
|
return { redirectLinks };
|
|
6863
6863
|
}
|
|
6864
6864
|
|
|
6865
|
+
// src/audit/checks/crawler-access.ts
|
|
6866
|
+
var CRAWLERS = [
|
|
6867
|
+
{
|
|
6868
|
+
name: "Googlebot",
|
|
6869
|
+
userAgent: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
|
|
6870
|
+
matters: "Google Search indexing",
|
|
6871
|
+
critical: true
|
|
6872
|
+
},
|
|
6873
|
+
{
|
|
6874
|
+
name: "bingbot",
|
|
6875
|
+
userAgent: "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
|
|
6876
|
+
matters: "Bing indexing, and Copilot answers",
|
|
6877
|
+
critical: true
|
|
6878
|
+
},
|
|
6879
|
+
{
|
|
6880
|
+
name: "GPTBot",
|
|
6881
|
+
userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)",
|
|
6882
|
+
matters: "ChatGPT being able to read and cite the page",
|
|
6883
|
+
critical: false
|
|
6884
|
+
},
|
|
6885
|
+
{
|
|
6886
|
+
name: "ClaudeBot",
|
|
6887
|
+
userAgent: "Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)",
|
|
6888
|
+
matters: "Claude being able to read and cite the page",
|
|
6889
|
+
critical: false
|
|
6890
|
+
},
|
|
6891
|
+
{
|
|
6892
|
+
name: "PerplexityBot",
|
|
6893
|
+
userAgent: "Mozilla/5.0 (compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)",
|
|
6894
|
+
matters: "Perplexity being able to read and cite the page",
|
|
6895
|
+
critical: false
|
|
6896
|
+
}
|
|
6897
|
+
];
|
|
6898
|
+
var BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
6899
|
+
function isRefusal(status) {
|
|
6900
|
+
return status === 401 || status === 403 || status === 429 || status === 503;
|
|
6901
|
+
}
|
|
6902
|
+
async function statusFor(url, userAgent) {
|
|
6903
|
+
try {
|
|
6904
|
+
const response = await httpGet(url, {
|
|
6905
|
+
headers: { "User-Agent": userAgent },
|
|
6906
|
+
timeout: 15e3,
|
|
6907
|
+
// We want the status itself, not an exception on 4xx/5xx.
|
|
6908
|
+
validateStatus: () => true
|
|
6909
|
+
});
|
|
6910
|
+
return response.status;
|
|
6911
|
+
} catch {
|
|
6912
|
+
return null;
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6915
|
+
async function analyzeCrawlerAccess(url) {
|
|
6916
|
+
const issues = [];
|
|
6917
|
+
const baselineStatus = await statusFor(url, BROWSER_UA);
|
|
6918
|
+
const data = { baselineStatus, crawlers: [] };
|
|
6919
|
+
if (baselineStatus === null || baselineStatus >= 400) {
|
|
6920
|
+
return { issues, data };
|
|
6921
|
+
}
|
|
6922
|
+
const results = await Promise.all(
|
|
6923
|
+
CRAWLERS.map(async (crawler) => ({
|
|
6924
|
+
crawler,
|
|
6925
|
+
status: await statusFor(url, crawler.userAgent)
|
|
6926
|
+
}))
|
|
6927
|
+
);
|
|
6928
|
+
const blocked = results.filter(({ status }) => status !== null && isRefusal(status));
|
|
6929
|
+
data.crawlers = results.map(({ crawler, status }) => ({
|
|
6930
|
+
name: crawler.name,
|
|
6931
|
+
status,
|
|
6932
|
+
blocked: status !== null && isRefusal(status)
|
|
6933
|
+
}));
|
|
6934
|
+
if (blocked.length === 0) return { issues, data };
|
|
6935
|
+
const search = blocked.filter(({ crawler }) => crawler.critical);
|
|
6936
|
+
const ai = blocked.filter(({ crawler }) => !crawler.critical);
|
|
6937
|
+
if (search.length > 0) {
|
|
6938
|
+
const names = search.map(({ crawler }) => crawler.name).join(", ");
|
|
6939
|
+
issues.push({
|
|
6940
|
+
code: "SEARCH_CRAWLER_BLOCKED_AT_EDGE",
|
|
6941
|
+
severity: "error",
|
|
6942
|
+
category: "crawlability",
|
|
6943
|
+
title: `${names} refused at the edge, not by robots.txt`,
|
|
6944
|
+
description: `A browser request to this URL returned ${baselineStatus}, but ${names} received ${search.map(({ status }) => status).join(", ")}. Something in front of the origin - a CDN, WAF or bot rule - is turning the crawler away, and robots.txt cannot show you this.`,
|
|
6945
|
+
impact: "Pages a search crawler cannot fetch fall out of the index. This is the failure mode behind Cloudflare\u2019s 2026-09-15 default, where blocking AI training also blocked Googlebot because it is classed as a mixed-use crawler.",
|
|
6946
|
+
howToFix: `Check your CDN or WAF bot rules rather than robots.txt.
|
|
6947
|
+
|
|
6948
|
+
On Cloudflare: the zone's Security > Bots settings, and any "Block AI Bots" or AI Crawler Control toggle. Because Googlebot crawls for both Search and AI training, blocking Training applies to all of its functions on ad-supported pages - allow Search crawlers explicitly, or opt out of the default.
|
|
6949
|
+
|
|
6950
|
+
Confirm with Search Console's URL Inspection, which fetches as the verified Googlebot.`,
|
|
6951
|
+
affectedUrls: [url],
|
|
6952
|
+
details: {
|
|
6953
|
+
baselineStatus,
|
|
6954
|
+
blocked: search.map(({ crawler, status }) => ({ crawler: crawler.name, status })),
|
|
6955
|
+
note: "Observed with the crawler\u2019s user-agent. Real crawlers are also verified by IP, so a site may treat them differently."
|
|
6956
|
+
}
|
|
6957
|
+
});
|
|
6958
|
+
}
|
|
6959
|
+
if (ai.length > 0) {
|
|
6960
|
+
const names = ai.map(({ crawler }) => crawler.name).join(", ");
|
|
6961
|
+
issues.push({
|
|
6962
|
+
code: "AI_CRAWLER_BLOCKED_AT_EDGE",
|
|
6963
|
+
severity: "warning",
|
|
6964
|
+
category: "ai-readiness",
|
|
6965
|
+
title: `${names} refused at the edge, not by robots.txt`,
|
|
6966
|
+
description: `A browser request returned ${baselineStatus}, but ${names} received ${ai.map(({ status }) => status).join(", ")}. The block is in front of the origin, so allowing these agents in robots.txt will not change it.`,
|
|
6967
|
+
impact: "An assistant that cannot fetch the page cannot quote it. This is invisible to a robots.txt check, which is where most AI-visibility tools stop.",
|
|
6968
|
+
howToFix: `If blocking them is deliberate, nothing to do. If not, the rule is in your CDN or WAF, not robots.txt - on Cloudflare see Security > Bots and any AI Crawler Control setting.`,
|
|
6969
|
+
affectedUrls: [url],
|
|
6970
|
+
details: {
|
|
6971
|
+
baselineStatus,
|
|
6972
|
+
blocked: ai.map(({ crawler, status }) => ({ crawler: crawler.name, status }))
|
|
6973
|
+
}
|
|
6974
|
+
});
|
|
6975
|
+
}
|
|
6976
|
+
return { issues, data };
|
|
6977
|
+
}
|
|
6978
|
+
|
|
6865
6979
|
// src/audit/checks/ai-readiness.ts
|
|
6866
6980
|
import * as cheerio13 from "cheerio";
|
|
6867
6981
|
|
|
@@ -7886,17 +8000,14 @@ var DOM_SIZE_ERROR = 3e3;
|
|
|
7886
8000
|
var DOM_DEPTH_WARNING = 32;
|
|
7887
8001
|
function countNodes($) {
|
|
7888
8002
|
let count = 0;
|
|
7889
|
-
function traverse(
|
|
7890
|
-
|
|
7891
|
-
|
|
8003
|
+
function traverse(node) {
|
|
8004
|
+
const el = node;
|
|
8005
|
+
if (el.type === "tag") count++;
|
|
7892
8006
|
if (el.children) {
|
|
7893
|
-
for (const child of el.children)
|
|
7894
|
-
traverse(child);
|
|
7895
|
-
}
|
|
8007
|
+
for (const child of el.children) traverse(child);
|
|
7896
8008
|
}
|
|
7897
8009
|
}
|
|
7898
|
-
|
|
7899
|
-
root.each((_, el) => traverse(el));
|
|
8010
|
+
$.root().each((_, el) => traverse(el));
|
|
7900
8011
|
return count;
|
|
7901
8012
|
}
|
|
7902
8013
|
function calculateMaxDepth($) {
|
|
@@ -10777,7 +10888,7 @@ function analyzeResponsiveImages(html, url) {
|
|
|
10777
10888
|
if (imagesWithoutDimensions >= 3) {
|
|
10778
10889
|
issues.push({
|
|
10779
10890
|
code: "IMAGES_MISSING_DIMENSIONS",
|
|
10780
|
-
severity: "
|
|
10891
|
+
severity: "warning",
|
|
10781
10892
|
category: "performance",
|
|
10782
10893
|
title: `${imagesWithoutDimensions} images missing width/height attributes`,
|
|
10783
10894
|
description: `Found ${imagesWithoutDimensions} images without explicit dimensions.`,
|
|
@@ -10792,7 +10903,8 @@ function analyzeResponsiveImages(html, url) {
|
|
|
10792
10903
|
} else if (imagesWithoutDimensions > 0) {
|
|
10793
10904
|
issues.push({
|
|
10794
10905
|
code: "IMAGES_MISSING_DIMENSIONS",
|
|
10795
|
-
|
|
10906
|
+
// One or two keeps a gradient below the >=3 case rather than flattening both to warning.
|
|
10907
|
+
severity: "notice",
|
|
10796
10908
|
category: "performance",
|
|
10797
10909
|
title: `${imagesWithoutDimensions} image(s) missing width/height attributes`,
|
|
10798
10910
|
description: `Found ${imagesWithoutDimensions} images without explicit dimensions.`,
|
|
@@ -14153,6 +14265,7 @@ async function runFullAudit(options) {
|
|
|
14153
14265
|
asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
|
|
14154
14266
|
asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
|
|
14155
14267
|
asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
|
|
14268
|
+
asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
|
|
14156
14269
|
asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
|
|
14157
14270
|
asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
|
|
14158
14271
|
const frameworkInfo = detectFramework({ html, headers });
|
|
@@ -29525,9 +29638,37 @@ function jsonLdConcerns(html) {
|
|
|
29525
29638
|
}
|
|
29526
29639
|
return bad;
|
|
29527
29640
|
}
|
|
29641
|
+
var PLACEHOLDERS = [
|
|
29642
|
+
{ pattern: /\+?1[-\s.]?\(?800\)?[-\s.]?555[-\s.]?\d{4}/i, what: "a reserved 555 phone number" },
|
|
29643
|
+
{ pattern: /\b555[-\s.]?01\d{2}\b/, what: "a reserved 555 phone number" },
|
|
29644
|
+
{ pattern: /\bAnytown\b/i, what: '"Anytown"' },
|
|
29645
|
+
{ pattern: /\b123 Main (Street|St)\b/i, what: '"123 Main Street"' },
|
|
29646
|
+
{ pattern: /your_[a-z_]+/i, what: 'an unfilled "your_..." placeholder' },
|
|
29647
|
+
{ pattern: /\b(yourdomain|your-domain|example)\.(com|org|net)\b/i, what: "a placeholder domain" },
|
|
29648
|
+
{ pattern: /\blorem ipsum\b/i, what: "lorem ipsum" },
|
|
29649
|
+
{ pattern: /\bXXX-XXX-XXXX\b/i, what: "an unfilled phone template" }
|
|
29650
|
+
];
|
|
29651
|
+
function isSharedShell(html) {
|
|
29652
|
+
return /<div[^>]+id=["'](root|app|__next|__nuxt)["']/i.test(html);
|
|
29653
|
+
}
|
|
29528
29654
|
function assessReplacement(path3, original, replacement) {
|
|
29529
29655
|
const concerns = [];
|
|
29530
29656
|
const add = (code, detail) => concerns.push({ code, detail });
|
|
29657
|
+
for (const { pattern, what } of PLACEHOLDERS) {
|
|
29658
|
+
const hit = replacement.match(pattern);
|
|
29659
|
+
if (hit && !(original && pattern.test(original))) {
|
|
29660
|
+
add("placeholder_content", `contains ${what} (${hit[0]})`);
|
|
29661
|
+
}
|
|
29662
|
+
}
|
|
29663
|
+
if (/\.html?$/i.test(path3) && isSharedShell(replacement)) {
|
|
29664
|
+
const addsCanonical = /<link[^>]+rel=["']canonical["']/i.test(replacement) && !(original && /<link[^>]+rel=["']canonical["']/i.test(original));
|
|
29665
|
+
if (addsCanonical) {
|
|
29666
|
+
add(
|
|
29667
|
+
"canonical_in_shared_shell",
|
|
29668
|
+
`${path3} mounts a single-page app, so a canonical here applies to every route`
|
|
29669
|
+
);
|
|
29670
|
+
}
|
|
29671
|
+
}
|
|
29531
29672
|
if (/\.json$/i.test(path3) || /\.webmanifest$/i.test(path3)) {
|
|
29532
29673
|
try {
|
|
29533
29674
|
JSON.parse(replacement);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankcli/agent-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"description": "RankCLI agent runtime - executes SEO audits and fixes with AI",
|
|
5
5
|
"homepage": "https://rankcli.dev",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"test": "vitest run",
|
|
29
29
|
"test:watch": "vitest",
|
|
30
30
|
"test:coverage": "vitest run --coverage",
|
|
31
|
-
"benchmark:detection": "node scripts/benchmark-detection.mjs"
|
|
31
|
+
"benchmark:detection": "node scripts/benchmark-detection.mjs",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@anthropic-ai/sdk": "^0.52.0",
|