@rankcli/agent-runtime 0.0.36 → 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 +144 -0
- package/dist/index.mjs +143 -0
- 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"));
|
|
@@ -17708,6 +17823,7 @@ async function runFullAudit(options) {
|
|
|
17708
17823
|
asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
|
|
17709
17824
|
asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
|
|
17710
17825
|
asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
|
|
17826
|
+
asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
|
|
17711
17827
|
asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
|
|
17712
17828
|
asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
|
|
17713
17829
|
const frameworkInfo = detectFramework({ html, headers });
|
|
@@ -33095,9 +33211,37 @@ function jsonLdConcerns(html) {
|
|
|
33095
33211
|
}
|
|
33096
33212
|
return bad;
|
|
33097
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
|
+
}
|
|
33098
33227
|
function assessReplacement(path3, original, replacement) {
|
|
33099
33228
|
const concerns = [];
|
|
33100
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
|
+
}
|
|
33101
33245
|
if (/\.json$/i.test(path3) || /\.webmanifest$/i.test(path3)) {
|
|
33102
33246
|
try {
|
|
33103
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
|
|
|
@@ -14151,6 +14265,7 @@ async function runFullAudit(options) {
|
|
|
14151
14265
|
asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
|
|
14152
14266
|
asyncChecks.push(safeAsync("Redirects", () => analyzeRedirects(url)));
|
|
14153
14267
|
asyncChecks.push(safeAsync("Robots Validation", () => validateRobotsTxt(url)));
|
|
14268
|
+
asyncChecks.push(safeAsyncIssues("Crawler Access", async () => (await analyzeCrawlerAccess(url)).issues));
|
|
14154
14269
|
asyncChecks.push(safeAsync("Canonical Domain", () => analyzeCanonicalDomain(url)));
|
|
14155
14270
|
asyncChecks.push(safeAsyncIssues("Framework Detection", async () => {
|
|
14156
14271
|
const frameworkInfo = detectFramework({ html, headers });
|
|
@@ -29523,9 +29638,37 @@ function jsonLdConcerns(html) {
|
|
|
29523
29638
|
}
|
|
29524
29639
|
return bad;
|
|
29525
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
|
+
}
|
|
29526
29654
|
function assessReplacement(path3, original, replacement) {
|
|
29527
29655
|
const concerns = [];
|
|
29528
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
|
+
}
|
|
29529
29672
|
if (/\.json$/i.test(path3) || /\.webmanifest$/i.test(path3)) {
|
|
29530
29673
|
try {
|
|
29531
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",
|