seo 0.16.0 → 0.16.1
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/cli.js +85 -85
- package/dist/cli.js.map +1 -1
- package/dist/{dist-Bv1JGvEz.js → dist-BZ5-V3qL.js} +1 -1
- package/dist/{dist-CHIJxr2Y.js → dist-CqzyPI3J.js} +113 -113
- package/dist/dist-CqzyPI3J.js.map +1 -0
- package/dist/{esm-CV5s7esU.js → esm-Cvmg0qhZ.js} +2 -2
- package/dist/{esm-CV5s7esU.js.map → esm-Cvmg0qhZ.js.map} +1 -1
- package/dist/index.d.ts +60 -46
- package/dist/index.js +1009 -732
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +588 -317
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/dist-CHIJxr2Y.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { n as version, t as load } from "./esm-
|
|
2
|
+
import { n as version, t as load } from "./esm-Cvmg0qhZ.js";
|
|
3
3
|
import { SEO_PROVIDER_API_VERSION } from "./provider-sdk.js";
|
|
4
4
|
import crypto, { createHash, randomUUID, sign } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -7728,17 +7728,15 @@ function recordHostFetch(input) {
|
|
|
7728
7728
|
const cfg = input.rate.backpressure;
|
|
7729
7729
|
health.durations.push(input.durationMs);
|
|
7730
7730
|
if (health.durations.length > 20) health.durations.shift();
|
|
7731
|
-
const
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
429
|
|
7735
|
-
].includes(input.status);
|
|
7736
|
-
const errored = input.status >= 500;
|
|
7731
|
+
const rateLimited = input.status === 429 || input.status === 503 && input.retryAfterMs !== void 0;
|
|
7732
|
+
const blocked = [401, 403].includes(input.status);
|
|
7733
|
+
const errored = input.status >= 500 && !rateLimited;
|
|
7737
7734
|
const slow = input.durationMs >= cfg.slowMs;
|
|
7738
7735
|
health.consecutiveBlocked = blocked ? health.consecutiveBlocked + 1 : 0;
|
|
7739
7736
|
health.consecutiveErrors = errored ? health.consecutiveErrors + 1 : 0;
|
|
7740
7737
|
health.consecutiveSlow = slow ? health.consecutiveSlow + 1 : 0;
|
|
7741
|
-
if (!blocked && !errored && !slow) health.reason = void 0;
|
|
7738
|
+
if (!blocked && !errored && !slow && !rateLimited) health.reason = void 0;
|
|
7739
|
+
if (rateLimited) health.reason = `origin rate limited the crawler (HTTP ${input.status})`;
|
|
7742
7740
|
const retryDelay = input.retryAfterMs === void 0 ? 0 : Math.min(input.retryAfterMs, cfg.retryAfterCapMs);
|
|
7743
7741
|
if (retryDelay > 0) {
|
|
7744
7742
|
health.reason = `origin sent Retry-After (${Math.round(retryDelay / 1e3)}s)`;
|
|
@@ -11288,6 +11286,17 @@ function isAuditableHtmlPage(page) {
|
|
|
11288
11286
|
const contentAuditAllowed = page.contentAuditAllowed ?? (!page.blocked && page.robotsTxt?.allowed !== false);
|
|
11289
11287
|
return isSuccessfulResponse(page) && isHtmlPage(page) && !isRedirectedPage(page) && contentAuditAllowed;
|
|
11290
11288
|
}
|
|
11289
|
+
/**
|
|
11290
|
+
* Cloudflare serves its own endpoints under `/cdn-cgi/`, such as the email
|
|
11291
|
+
* obfuscation link `/cdn-cgi/l/email-protection`. They are not site pages.
|
|
11292
|
+
*/
|
|
11293
|
+
function isCdnUtilityUrl(value) {
|
|
11294
|
+
try {
|
|
11295
|
+
return new URL(value).pathname.startsWith("/cdn-cgi/");
|
|
11296
|
+
} catch {
|
|
11297
|
+
return false;
|
|
11298
|
+
}
|
|
11299
|
+
}
|
|
11291
11300
|
// packages/core/src/analyze/crawler/audit.ts
|
|
11292
11301
|
function sameUrl(a, b) {
|
|
11293
11302
|
if (!a || !b) return false;
|
|
@@ -11557,7 +11566,7 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
11557
11566
|
if (page.accessBlock) continue;
|
|
11558
11567
|
if (page.status >= 500) {
|
|
11559
11568
|
issues.push(issue$1("server_error", page, String(page.status), { status: page.status }));
|
|
11560
|
-
if ((page.internalInlinkCount ?? 0) > 0) issues.push(issue$1("broken_internal_link", page, String(page.status), {
|
|
11569
|
+
if ((page.internalInlinkCount ?? 0) > 0 && !isCdnUtilityUrl(page.url)) issues.push(issue$1("broken_internal_link", page, String(page.status), {
|
|
11561
11570
|
status: page.status,
|
|
11562
11571
|
internalInlinkCount: page.internalInlinkCount
|
|
11563
11572
|
}));
|
|
@@ -11565,7 +11574,7 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
11565
11574
|
}
|
|
11566
11575
|
if (page.status >= 400) {
|
|
11567
11576
|
issues.push(issue$1("client_error", page, String(page.status), { status: page.status }));
|
|
11568
|
-
if (isBrokenLinkStatus(page.status) && (page.internalInlinkCount ?? 0) > 0) issues.push(issue$1("broken_internal_link", page, String(page.status), {
|
|
11577
|
+
if (isBrokenLinkStatus(page.status) && (page.internalInlinkCount ?? 0) > 0 && !isCdnUtilityUrl(page.url)) issues.push(issue$1("broken_internal_link", page, String(page.status), {
|
|
11569
11578
|
status: page.status,
|
|
11570
11579
|
internalInlinkCount: page.internalInlinkCount
|
|
11571
11580
|
}));
|
|
@@ -11788,6 +11797,7 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
11788
11797
|
// packages/core/src/analyze/crawler/crawl-skip-reasons.ts
|
|
11789
11798
|
const CRAWL_SKIP_REASONS = [
|
|
11790
11799
|
"asset-url",
|
|
11800
|
+
"cdn-utility-url",
|
|
11791
11801
|
"configured-exclusion",
|
|
11792
11802
|
"duplicate-normalization",
|
|
11793
11803
|
"invalid-url",
|
|
@@ -11802,6 +11812,7 @@ const CRAWL_SKIP_REASONS = [
|
|
|
11802
11812
|
];
|
|
11803
11813
|
const SKIP_IMPACT = {
|
|
11804
11814
|
"asset-url": "non-impacting",
|
|
11815
|
+
"cdn-utility-url": "non-impacting",
|
|
11805
11816
|
"configured-exclusion": "non-impacting",
|
|
11806
11817
|
"duplicate-normalization": "non-impacting",
|
|
11807
11818
|
"invalid-url": "non-impacting",
|
|
@@ -12428,473 +12439,85 @@ function refreshPrioritiesCsvFiles(report) {
|
|
|
12428
12439
|
...prefixCsvFiles("diagnosis", diagnoseCsvFiles(report.output.diagnosis))
|
|
12429
12440
|
]);
|
|
12430
12441
|
}
|
|
12431
|
-
// packages/core/src/analyze/crawler/
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
medium: 1200,
|
|
12435
|
-
low: 100
|
|
12436
|
-
};
|
|
12437
|
-
const EFFORT_BY_CATEGORY = {
|
|
12438
|
-
canonical: "medium",
|
|
12439
|
-
content: "high",
|
|
12440
|
-
agents: "low",
|
|
12441
|
-
headings: "low",
|
|
12442
|
-
images: "medium",
|
|
12443
|
-
indexability: "medium",
|
|
12444
|
-
international: "low",
|
|
12445
|
-
metadata: "low",
|
|
12446
|
-
mobile: "low",
|
|
12447
|
-
response: "medium",
|
|
12448
|
-
security: "medium",
|
|
12449
|
-
social: "low",
|
|
12450
|
-
"structured-data": "medium"
|
|
12451
|
-
};
|
|
12452
|
-
const EFFORT_SCORE = {
|
|
12453
|
-
low: 60,
|
|
12454
|
-
medium: 30,
|
|
12455
|
-
high: 0
|
|
12456
|
-
};
|
|
12457
|
-
const AFFECTED_URL_WEIGHT = {
|
|
12458
|
-
high: 25,
|
|
12459
|
-
medium: 15,
|
|
12460
|
-
low: 2
|
|
12461
|
-
};
|
|
12462
|
-
const VALUE_WEIGHT = {
|
|
12463
|
-
high: 1,
|
|
12464
|
-
medium: .75,
|
|
12465
|
-
low: .15
|
|
12466
|
-
};
|
|
12467
|
-
const CRAWL_REACH_RULES = /* @__PURE__ */ new Set(["crawl_single_page_reach"]);
|
|
12468
|
-
function reachRank(ruleId) {
|
|
12469
|
-
return CRAWL_REACH_RULES.has(ruleId) ? 0 : 1;
|
|
12442
|
+
// packages/core/src/analyze/crawler/crawl-url-queue.ts
|
|
12443
|
+
function before(left, right) {
|
|
12444
|
+
return left.depth < right.depth || left.depth === right.depth && left.url < right.url;
|
|
12470
12445
|
}
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
if (!part) continue;
|
|
12477
|
-
const found = value.slice(position).indexOf(part);
|
|
12478
|
-
if (found === -1) return false;
|
|
12479
|
-
position += found + part.length;
|
|
12446
|
+
var CrawlUrlQueue = class {
|
|
12447
|
+
#items = [];
|
|
12448
|
+
#positions = /* @__PURE__ */ new Map();
|
|
12449
|
+
get size() {
|
|
12450
|
+
return this.#items.length;
|
|
12480
12451
|
}
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
function filteredIssues(report, filters) {
|
|
12484
|
-
return report.issues.filter((issue) => {
|
|
12485
|
-
if (filters.category && issue.category !== filters.category) return false;
|
|
12486
|
-
if (filters.severity && issue.severity !== filters.severity) return false;
|
|
12487
|
-
if (filters.url && !matchesPattern(filters.url, issue.url)) return false;
|
|
12488
|
-
if ((filters.recommendation ?? "fix") !== recommendationForRule(issue.ruleId)) return false;
|
|
12489
|
-
return true;
|
|
12490
|
-
});
|
|
12491
|
-
}
|
|
12492
|
-
function groupIssues(issues) {
|
|
12493
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12494
|
-
for (const issue of issues) {
|
|
12495
|
-
const existing = groups.get(issue.ruleId) ?? {
|
|
12496
|
-
ruleId: issue.ruleId,
|
|
12497
|
-
title: issue.title,
|
|
12498
|
-
category: issue.category,
|
|
12499
|
-
severity: issue.severity,
|
|
12500
|
-
count: 0,
|
|
12501
|
-
sampleUrls: []
|
|
12502
|
-
};
|
|
12503
|
-
existing.count += 1;
|
|
12504
|
-
if (existing.sampleUrls.length < 10) existing.sampleUrls.push(issue.url);
|
|
12505
|
-
groups.set(issue.ruleId, existing);
|
|
12452
|
+
has(url) {
|
|
12453
|
+
return this.#positions.has(url);
|
|
12506
12454
|
}
|
|
12507
|
-
|
|
12508
|
-
}
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
const clicks = metrics.reduce((sum, item) => sum + item.clicks, 0);
|
|
12514
|
-
const impressions = metrics.reduce((sum, item) => sum + item.impressions, 0);
|
|
12515
|
-
const sessions = pages.reduce((sum, page) => sum + (page.analytics?.sessions ?? 0), 0);
|
|
12516
|
-
const observedMetrics = new Set(report.dataSources?.analytics.observedMetrics ?? [
|
|
12517
|
-
"sessions",
|
|
12518
|
-
"totalUsers",
|
|
12519
|
-
"conversions"
|
|
12520
|
-
]);
|
|
12521
|
-
const totalUsers = observedMetrics.has("totalUsers") ? pages.reduce((sum, page) => sum + (page.analytics?.totalUsers ?? 0), 0) : void 0;
|
|
12522
|
-
const conversions = observedMetrics.has("conversions") ? pages.reduce((sum, page) => sum + (page.analytics?.conversions ?? 0), 0) : void 0;
|
|
12523
|
-
const avgPosition = metrics.length ? metrics.reduce((sum, item) => sum + item.position, 0) / metrics.length : void 0;
|
|
12524
|
-
return {
|
|
12525
|
-
searchVisibleUrls: metrics.length,
|
|
12526
|
-
clicks,
|
|
12527
|
-
impressions,
|
|
12528
|
-
sessions,
|
|
12529
|
-
totalUsers,
|
|
12530
|
-
conversions,
|
|
12531
|
-
avgPosition
|
|
12532
|
-
};
|
|
12533
|
-
}
|
|
12534
|
-
function whyThisRanks(input, recommendation, searchStatus, analyticsProvider) {
|
|
12535
|
-
const visibility = input.searchVisibleUrls ? `${input.searchVisibleUrls} affected URLs have GSC visibility (${input.clicks} clicks, ${input.impressions} impressions).` : searchStatus && !["joined", "none"].includes(searchStatus) ? `No joined GSC visibility is available for these affected URLs; Search Console evidence is ${searchStatus}.` : "No affected URL has joined GSC visibility yet.";
|
|
12536
|
-
const analyticsDetails = analyticsProvider ? analyticsProviderDetails(analyticsProvider) : analyticsProviderDetails("google");
|
|
12537
|
-
const analyticsLabel = analyticsDetails.label;
|
|
12538
|
-
const sessionLabel = analyticsDetails.landingMetricLabel;
|
|
12539
|
-
return `${visibility}${input.sessions ? ` ${analyticsLabel} adds ${input.sessions} ${sessionLabel}${input.conversions === void 0 ? "" : ` and ${input.conversions} conversions`} from affected pages.` : ""} ${recommendation === "review" ? "This is a review observation. Confirm it before scheduling implementation work." : `This ranks as a fix. Severity is ${input.severity}; ${input.affectedUrls} URL${input.affectedUrls === 1 ? " is" : "s are"} affected; estimated effort is ${input.effort}.`}`;
|
|
12540
|
-
}
|
|
12541
|
-
function verificationCommand(report, severity) {
|
|
12542
|
-
return `seo crawl ${report.config.url} --severity ${severity} --max-pages ${Math.min(report.config.maxPages, 100)}`;
|
|
12543
|
-
}
|
|
12544
|
-
function topFixes(report, filters = {}) {
|
|
12545
|
-
return groupIssues(filteredIssues(report, filters)).map((group) => {
|
|
12546
|
-
const search = searchValueForGroup(report, group.sampleUrls);
|
|
12547
|
-
const effort = EFFORT_BY_CATEGORY[group.category] ?? "medium";
|
|
12548
|
-
const scoreFactors = {
|
|
12549
|
-
severity: SEVERITY_SCORE[group.severity],
|
|
12550
|
-
affectedUrls: group.count,
|
|
12551
|
-
searchVisibleUrls: search.searchVisibleUrls,
|
|
12552
|
-
clicks: search.clicks,
|
|
12553
|
-
impressions: search.impressions,
|
|
12554
|
-
sessions: search.sessions,
|
|
12555
|
-
totalUsers: search.totalUsers,
|
|
12556
|
-
conversions: search.conversions,
|
|
12557
|
-
avgPosition: search.avgPosition,
|
|
12558
|
-
effort,
|
|
12559
|
-
effortScore: EFFORT_SCORE[effort]
|
|
12560
|
-
};
|
|
12561
|
-
const valueWeight = VALUE_WEIGHT[group.severity];
|
|
12562
|
-
const score = scoreFactors.severity + scoreFactors.affectedUrls * AFFECTED_URL_WEIGHT[group.severity] + scoreFactors.searchVisibleUrls * 100 * valueWeight + scoreFactors.clicks * 20 * valueWeight + Math.min(scoreFactors.impressions, 1e4) / 25 * valueWeight + scoreFactors.sessions * 2 * valueWeight + (scoreFactors.conversions ?? 0) * 100 * valueWeight + scoreFactors.effortScore;
|
|
12563
|
-
const rule = explainRule(group.ruleId);
|
|
12564
|
-
const recommendation = recommendationForRule(group.ruleId);
|
|
12565
|
-
return {
|
|
12566
|
-
...group,
|
|
12567
|
-
recommendation,
|
|
12568
|
-
score: Math.round(score),
|
|
12569
|
-
scoreFactors,
|
|
12570
|
-
whyThisRanks: whyThisRanks(scoreFactors, recommendation, report.dataSources?.searchConsole.status, report.dataSources?.analytics.provider),
|
|
12571
|
-
howToFix: rule?.howToFix ?? "",
|
|
12572
|
-
howToVerify: rule?.howToVerify ?? "",
|
|
12573
|
-
verification: {
|
|
12574
|
-
command: verificationCommand(report, group.severity),
|
|
12575
|
-
expected: rule?.howToVerify ?? ""
|
|
12576
|
-
}
|
|
12577
|
-
};
|
|
12578
|
-
}).sort((a, b) => reachRank(a.ruleId) - reachRank(b.ruleId) || b.score - a.score || SEVERITY_SCORE[b.severity] - SEVERITY_SCORE[a.severity] || b.count - a.count || a.ruleId.localeCompare(b.ruleId)).slice(0, filters.limit ?? 10);
|
|
12579
|
-
}
|
|
12580
|
-
function reviewObservations(report, filters = {}) {
|
|
12581
|
-
return topFixes(report, {
|
|
12582
|
-
...filters,
|
|
12583
|
-
recommendation: "review"
|
|
12584
|
-
});
|
|
12585
|
-
}
|
|
12586
|
-
// packages/core/src/analyze/crawler/export.ts
|
|
12587
|
-
const issueHeaders = [
|
|
12588
|
-
"rule_id",
|
|
12589
|
-
"title",
|
|
12590
|
-
"category",
|
|
12591
|
-
"severity",
|
|
12592
|
-
"url",
|
|
12593
|
-
"detail",
|
|
12594
|
-
"clicks",
|
|
12595
|
-
"impressions",
|
|
12596
|
-
"position"
|
|
12597
|
-
];
|
|
12598
|
-
const pageHeaders = [
|
|
12599
|
-
"url",
|
|
12600
|
-
"final_url",
|
|
12601
|
-
"status",
|
|
12602
|
-
"indexable",
|
|
12603
|
-
"title",
|
|
12604
|
-
"meta_description",
|
|
12605
|
-
"canonical",
|
|
12606
|
-
"word_count",
|
|
12607
|
-
"content_extractor",
|
|
12608
|
-
"content_extractor_type",
|
|
12609
|
-
"content_extraction_fallback",
|
|
12610
|
-
"word_count_source",
|
|
12611
|
-
"internal_links",
|
|
12612
|
-
"internal_inlinks",
|
|
12613
|
-
"internal_authority_score",
|
|
12614
|
-
"external_links",
|
|
12615
|
-
"images_total",
|
|
12616
|
-
"images_missing_alt",
|
|
12617
|
-
"oversized_image_candidates",
|
|
12618
|
-
"schema_types",
|
|
12619
|
-
"clicks",
|
|
12620
|
-
"impressions",
|
|
12621
|
-
"sessions"
|
|
12622
|
-
];
|
|
12623
|
-
function renderCrawlCsv(report) {
|
|
12624
|
-
return renderCsv(report.issues.map((issue) => ({
|
|
12625
|
-
rule_id: issue.ruleId,
|
|
12626
|
-
title: issue.title,
|
|
12627
|
-
category: issue.category,
|
|
12628
|
-
severity: issue.severity,
|
|
12629
|
-
url: issue.url,
|
|
12630
|
-
detail: issue.detail,
|
|
12631
|
-
clicks: issue.searchMetrics?.clicks,
|
|
12632
|
-
impressions: issue.searchMetrics?.impressions,
|
|
12633
|
-
position: issue.searchMetrics?.position
|
|
12634
|
-
})), issueHeaders);
|
|
12635
|
-
}
|
|
12636
|
-
function renderCrawlPagesCsv(report) {
|
|
12637
|
-
return renderCsv(report.pages.map((page) => ({
|
|
12638
|
-
url: page.url,
|
|
12639
|
-
final_url: page.finalUrl,
|
|
12640
|
-
status: page.status,
|
|
12641
|
-
indexable: page.indexable,
|
|
12642
|
-
title: page.title,
|
|
12643
|
-
meta_description: page.metaDescription,
|
|
12644
|
-
canonical: page.canonical,
|
|
12645
|
-
word_count: page.wordCount,
|
|
12646
|
-
content_extractor: page.contentExtraction?.used,
|
|
12647
|
-
content_extractor_type: page.contentExtraction?.extractorType,
|
|
12648
|
-
content_extraction_fallback: page.contentExtraction?.fallback,
|
|
12649
|
-
word_count_source: page.contentExtraction?.wordCountSource,
|
|
12650
|
-
internal_links: page.outgoingInternalCount,
|
|
12651
|
-
internal_inlinks: page.internalInlinkCount,
|
|
12652
|
-
internal_authority_score: page.internalLinkAuthorityScore,
|
|
12653
|
-
external_links: page.outgoingExternalCount,
|
|
12654
|
-
images_total: page.imagesTotal,
|
|
12655
|
-
images_missing_alt: page.imagesMissingAlt,
|
|
12656
|
-
oversized_image_candidates: page.oversizedImageCandidates?.length ?? 0,
|
|
12657
|
-
schema_types: page.schemaTypes?.join("|"),
|
|
12658
|
-
clicks: page.searchMetrics?.clicks,
|
|
12659
|
-
impressions: page.searchMetrics?.impressions,
|
|
12660
|
-
sessions: page.analytics?.sessions
|
|
12661
|
-
})), pageHeaders);
|
|
12662
|
-
}
|
|
12663
|
-
function renderCrawlPretty(report, fixes = topFixes(report)) {
|
|
12664
|
-
const reviews = reviewObservations(report);
|
|
12665
|
-
const requestSummary = report.requestEvidenceStatus === "available" ? `${report.requests.length} requests` : report.requestEvidenceStatus === "partial" ? `${report.requests.length} observed requests; some started requests were still in flight when the crawl stopped` : "request evidence unavailable for this legacy report";
|
|
12666
|
-
const lines = [
|
|
12667
|
-
`Crawl report for ${report.config.url}`,
|
|
12668
|
-
"",
|
|
12669
|
-
`Status: ${report.status}`,
|
|
12670
|
-
`Strategy: ${report.config.strategy === "health" ? "sitemap health pass" : "full audit"}`,
|
|
12671
|
-
`Crawler: ${report.access.crawler.userAgent}`,
|
|
12672
|
-
`Documents: ${report.summary.totalPages} retained; ${requestSummary}, ${report.summary.discoveredUrls} URLs discovered, ${report.summary.failedUrls} failed, ${report.summary.skippedUrls} skipped`,
|
|
12673
|
-
`Issues: ${report.issues.length} (${report.summary.highIssues} high, ${report.summary.mediumIssues} medium, ${report.summary.lowIssues} low)`
|
|
12674
|
-
];
|
|
12675
|
-
if (report.externalLinkVerification) {
|
|
12676
|
-
const external = report.externalLinkVerification;
|
|
12677
|
-
lines.push(`External links: ${external.fetchedUrls} fetched, ${external.failedUrls} request failure${external.failedUrls === 1 ? "" : "s"}, ${external.deferredUrls} deferred (${external.dataStatus})`);
|
|
12455
|
+
push(item) {
|
|
12456
|
+
if (this.has(item.url)) throw new Error(`URL is already queued: ${item.url}`);
|
|
12457
|
+
const index = this.#items.length;
|
|
12458
|
+
this.#items.push(item);
|
|
12459
|
+
this.#positions.set(item.url, index);
|
|
12460
|
+
this.#bubbleUp(index);
|
|
12678
12461
|
}
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12462
|
+
decreaseDepth(url, depth) {
|
|
12463
|
+
const index = this.#positions.get(url);
|
|
12464
|
+
if (index === void 0) return false;
|
|
12465
|
+
const item = this.#items[index];
|
|
12466
|
+
if (!item || depth >= item.depth) return false;
|
|
12467
|
+
item.depth = depth;
|
|
12468
|
+
this.#bubbleUp(index);
|
|
12469
|
+
return true;
|
|
12686
12470
|
}
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12471
|
+
take() {
|
|
12472
|
+
const first = this.#items[0];
|
|
12473
|
+
if (!first) return void 0;
|
|
12474
|
+
const last = this.#items.pop();
|
|
12475
|
+
this.#positions.delete(first.url);
|
|
12476
|
+
if (last && last !== first) {
|
|
12477
|
+
this.#items[0] = last;
|
|
12478
|
+
this.#positions.set(last.url, 0);
|
|
12479
|
+
this.#bubbleDown(0);
|
|
12692
12480
|
}
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12481
|
+
return first;
|
|
12482
|
+
}
|
|
12483
|
+
#swap(left, right) {
|
|
12484
|
+
const leftItem = this.#items[left];
|
|
12485
|
+
const rightItem = this.#items[right];
|
|
12486
|
+
if (!leftItem || !rightItem) return;
|
|
12487
|
+
this.#items[left] = rightItem;
|
|
12488
|
+
this.#items[right] = leftItem;
|
|
12489
|
+
this.#positions.set(rightItem.url, left);
|
|
12490
|
+
this.#positions.set(leftItem.url, right);
|
|
12491
|
+
}
|
|
12492
|
+
#bubbleUp(start) {
|
|
12493
|
+
let index = start;
|
|
12494
|
+
while (index > 0) {
|
|
12495
|
+
const parent = Math.floor((index - 1) / 2);
|
|
12496
|
+
const item = this.#items[index];
|
|
12497
|
+
const parentItem = this.#items[parent];
|
|
12498
|
+
if (!item || !parentItem || !before(item, parentItem)) break;
|
|
12499
|
+
this.#swap(index, parent);
|
|
12500
|
+
index = parent;
|
|
12699
12501
|
}
|
|
12700
12502
|
}
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12503
|
+
#bubbleDown(start) {
|
|
12504
|
+
let index = start;
|
|
12505
|
+
while (true) {
|
|
12506
|
+
const left = index * 2 + 1;
|
|
12507
|
+
const right = left + 1;
|
|
12508
|
+
let next = index;
|
|
12509
|
+
const item = this.#items[next];
|
|
12510
|
+
const leftItem = this.#items[left];
|
|
12511
|
+
const rightItem = this.#items[right];
|
|
12512
|
+
if (leftItem && item && before(leftItem, item)) next = left;
|
|
12513
|
+
const nextItem = this.#items[next];
|
|
12514
|
+
if (rightItem && nextItem && before(rightItem, nextItem)) next = right;
|
|
12515
|
+
if (next === index) return;
|
|
12516
|
+
this.#swap(index, next);
|
|
12517
|
+
index = next;
|
|
12518
|
+
}
|
|
12713
12519
|
}
|
|
12714
|
-
|
|
12715
|
-
return (root.documents.length ? root.documents : [{
|
|
12716
|
-
url: root.url,
|
|
12717
|
-
dataStatus: root.dataStatus,
|
|
12718
|
-
compression: "none",
|
|
12719
|
-
warning: root.warnings.join(" ") || void 0
|
|
12720
|
-
}]).map((document) => {
|
|
12721
|
-
const failures = [
|
|
12722
|
-
...document.dataStatus !== "complete" ? [document.warning ?? `Sitemap evidence is ${document.dataStatus}.`] : [],
|
|
12723
|
-
...document.redirected ? [`Sitemap redirected to ${document.finalUrl ?? "an unknown URL"}.`] : [],
|
|
12724
|
-
...document.status !== void 0 && (document.status < 200 || document.status >= 300) ? [`Sitemap returned HTTP ${document.status}.`] : []
|
|
12725
|
-
];
|
|
12726
|
-
const output = [
|
|
12727
|
-
`dataStatus=${document.dataStatus}`,
|
|
12728
|
-
...document.status !== void 0 ? [`status=${document.status}`] : [],
|
|
12729
|
-
...document.finalUrl ? [`finalUrl=${document.finalUrl}`] : [],
|
|
12730
|
-
...document.root ? [`root=${document.root}`] : [],
|
|
12731
|
-
...document.warning ? [`warning=${document.warning}`] : []
|
|
12732
|
-
].join("\n");
|
|
12733
|
-
return {
|
|
12734
|
-
xml: [
|
|
12735
|
-
` <testcase classname="seo.sitemap-document" name="${escapeXml(document.url)}" time="0.000">`,
|
|
12736
|
-
...failures.length ? [` <failure message="${escapeXml(failures[0] ?? "Sitemap check failed")}">${escapeXml(failures.join("\n"))}</failure>`] : [],
|
|
12737
|
-
` <system-out>${escapeXml(output)}</system-out>`,
|
|
12738
|
-
" </testcase>"
|
|
12739
|
-
].join("\n"),
|
|
12740
|
-
failed: failures.length > 0
|
|
12741
|
-
};
|
|
12742
|
-
});
|
|
12743
|
-
});
|
|
12744
|
-
const requestCases = report.requests.map((request) => {
|
|
12745
|
-
const failures = [
|
|
12746
|
-
...(issuesByUrl.get(request.requestedUrl) ?? []).map((issue) => `${issue.severity}: ${issue.title}${issue.detail ? `: ${issue.detail}` : ""}`),
|
|
12747
|
-
...request.outcome === "failure" ? [`${request.failureKind}: ${request.error}`] : [],
|
|
12748
|
-
...request.outcome === "skipped" ? [request.reason === "origin-backpressure" ? request.error : `${request.reason}: ${request.robotsTxt.matchedLine ?? request.robotsTxt.availability}`] : []
|
|
12749
|
-
];
|
|
12750
|
-
const output = request.outcome === "response" ? [
|
|
12751
|
-
`status=${request.status}`,
|
|
12752
|
-
`finalUrl=${request.finalUrl}`,
|
|
12753
|
-
...request.robotsTxt ? [
|
|
12754
|
-
`robotsUrl=${request.robotsTxt.url}`,
|
|
12755
|
-
`robotsAllowed=${request.robotsTxt.allowed ?? "unknown"}`,
|
|
12756
|
-
`robotsAvailability=${request.robotsTxt.availability}`,
|
|
12757
|
-
...request.robotsTxt.status !== void 0 ? [`robotsStatus=${request.robotsTxt.status}`] : [],
|
|
12758
|
-
...request.robotsTxt.matchedLine ? [`robotsMatchedLine=${request.robotsTxt.matchedLine}`] : [],
|
|
12759
|
-
...request.robotsTxt.error ? [`robotsError=${request.robotsTxt.error}`] : []
|
|
12760
|
-
] : [],
|
|
12761
|
-
...request.accessBlock ? [
|
|
12762
|
-
`accessProvider=${request.accessBlock.provider}`,
|
|
12763
|
-
`crawler=${request.accessBlock.crawler.userAgent}`,
|
|
12764
|
-
...request.accessBlock.requestId ? [`requestId=${request.accessBlock.requestId}`] : []
|
|
12765
|
-
] : []
|
|
12766
|
-
].join("\n") : request.outcome === "failure" ? request.error : request.reason === "origin-backpressure" ? `reason=${request.reason}\nerror=${request.error}` : [
|
|
12767
|
-
`reason=${request.reason}`,
|
|
12768
|
-
`robotsUrl=${request.robotsTxt.url}`,
|
|
12769
|
-
`robotsAllowed=${request.robotsTxt.allowed ?? "unknown"}`,
|
|
12770
|
-
`robotsAvailability=${request.robotsTxt.availability}`,
|
|
12771
|
-
...request.robotsTxt.status !== void 0 ? [`robotsStatus=${request.robotsTxt.status}`] : [],
|
|
12772
|
-
...request.robotsTxt.matchedLine ? [`robotsMatchedLine=${request.robotsTxt.matchedLine}`] : [],
|
|
12773
|
-
...request.robotsTxt.error ? [`robotsError=${request.robotsTxt.error}`] : []
|
|
12774
|
-
].join("\n");
|
|
12775
|
-
const duration = ((request.durationMs ?? 0) / 1e3).toFixed(3);
|
|
12776
|
-
return {
|
|
12777
|
-
xml: [
|
|
12778
|
-
` <testcase classname="seo.sitemap-health" name="${escapeXml(request.requestedUrl)}" time="${duration}">`,
|
|
12779
|
-
...failures.length ? [` <failure message="${escapeXml(failures[0] ?? "Health check failed")}">${escapeXml(failures.join("\n"))}</failure>`] : [],
|
|
12780
|
-
` <system-out>${escapeXml(output)}</system-out>`,
|
|
12781
|
-
" </testcase>"
|
|
12782
|
-
].join("\n"),
|
|
12783
|
-
failed: failures.length > 0
|
|
12784
|
-
};
|
|
12785
|
-
});
|
|
12786
|
-
const cases = [...sitemapCases, ...requestCases];
|
|
12787
|
-
const failures = cases.filter((item) => item.failed).length;
|
|
12788
|
-
const elapsed = report.requests.reduce((sum, request) => sum + (request.durationMs ?? 0), 0);
|
|
12789
|
-
return [
|
|
12790
|
-
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
12791
|
-
`<testsuite name="seo sitemap health" tests="${cases.length}" failures="${failures}" errors="0" skipped="0" time="${(elapsed / 1e3).toFixed(3)}">`,
|
|
12792
|
-
" <properties>",
|
|
12793
|
-
` <property name="report.id" value="${escapeXml(report.id)}"/>`,
|
|
12794
|
-
` <property name="report.status" value="${escapeXml(report.status)}"/>`,
|
|
12795
|
-
` <property name="crawl.strategy" value="${escapeXml(report.config.strategy)}"/>`,
|
|
12796
|
-
` <property name="crawler.userAgent" value="${escapeXml(report.access.crawler.userAgent)}"/>`,
|
|
12797
|
-
" </properties>",
|
|
12798
|
-
...cases.map((item) => item.xml),
|
|
12799
|
-
"</testsuite>",
|
|
12800
|
-
""
|
|
12801
|
-
].join("\n");
|
|
12802
|
-
}
|
|
12803
|
-
function renderCrawlHtml(report, fixes = topFixes(report)) {
|
|
12804
|
-
const reviews = reviewObservations(report);
|
|
12805
|
-
const issueRows = report.issueGroups.map((group) => {
|
|
12806
|
-
const rule = explainRule(group.ruleId);
|
|
12807
|
-
return `<tr><td>${escapeHtml(group.severity)}</td><td>${escapeHtml(group.title)}</td><td>${group.count}</td><td>${escapeHtml(rule?.howToFix ?? "")}</td><td>${escapeHtml(group.sampleUrls[0] ?? "")}</td></tr>`;
|
|
12808
|
-
}).join("\n");
|
|
12809
|
-
const fixItems = fixes.slice(0, 10).map((fix) => `<li><strong>${escapeHtml(fix.title)}</strong><br>${escapeHtml(fix.howToFix)}<br><span>Verify: ${escapeHtml(fix.howToVerify)}</span><br><code>${escapeHtml(fix.verification.command)}</code></li>`).join("\n");
|
|
12810
|
-
const reviewItems = reviews.slice(0, 10).map((observation) => `<li><strong>${escapeHtml(observation.title)}</strong><br>${escapeHtml(observation.howToFix)}<br><span>Check before scheduling work. ${escapeHtml(observation.howToVerify)}</span></li>`).join("\n");
|
|
12811
|
-
const caveatItems = report.caveats.map((caveat) => `<li>${escapeHtml(caveat)}</li>`).join("\n");
|
|
12812
|
-
const warningItems = report.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("\n");
|
|
12813
|
-
const accessItems = report.access.samples.map((sample) => `<li><strong>${escapeHtml(sample.url)}</strong><br>${escapeHtml(sample.evidence.guidance.summary)}<br>${escapeHtml(sample.evidence.guidance.recommendedAction)}<br><span>${escapeHtml(sample.evidence.guidance.securityNote)}</span></li>`).join("\n");
|
|
12814
|
-
return `<!doctype html>
|
|
12815
|
-
<html lang="en">
|
|
12816
|
-
<head>
|
|
12817
|
-
<meta charset="utf-8">
|
|
12818
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
12819
|
-
<title>Crawl report for ${escapeHtml(report.config.url)}</title>
|
|
12820
|
-
<style>
|
|
12821
|
-
body { color: #17202a; font: 15px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 32px; max-width: 1120px; }
|
|
12822
|
-
h1, h2 { line-height: 1.2; }
|
|
12823
|
-
.summary { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); margin: 24px 0; }
|
|
12824
|
-
.metric { border: 1px solid #d8dee4; border-radius: 8px; padding: 12px; }
|
|
12825
|
-
.metric strong { display: block; font-size: 24px; }
|
|
12826
|
-
table { border-collapse: collapse; width: 100%; }
|
|
12827
|
-
th, td { border-bottom: 1px solid #d8dee4; padding: 10px; text-align: left; vertical-align: top; }
|
|
12828
|
-
th { background: #f6f8fa; }
|
|
12829
|
-
li { margin: 0 0 12px; }
|
|
12830
|
-
span { color: #57606a; }
|
|
12831
|
-
</style>
|
|
12832
|
-
</head>
|
|
12833
|
-
<body>
|
|
12834
|
-
<h1>Crawl report</h1>
|
|
12835
|
-
<p>${escapeHtml(report.config.url)}</p>
|
|
12836
|
-
<p>Strategy: ${escapeHtml(report.config.strategy === "health" ? "sitemap health pass" : "full audit")}<br>Crawler: <code>${escapeHtml(report.access.crawler.userAgent)}</code></p>
|
|
12837
|
-
<section class="summary">
|
|
12838
|
-
<div class="metric"><span>Status</span><strong>${escapeHtml(report.status)}</strong></div>
|
|
12839
|
-
<div class="metric"><span>Documents</span><strong>${report.summary.totalPages}</strong></div>
|
|
12840
|
-
<div class="metric"><span>Requests</span><strong>${report.requestEvidenceStatus === "unavailable" ? "n/a" : report.requests.length}${report.requestEvidenceStatus === "partial" ? " (partial)" : ""}</strong></div>
|
|
12841
|
-
<div class="metric"><span>Discovered</span><strong>${report.summary.discoveredUrls}</strong></div>
|
|
12842
|
-
<div class="metric"><span>Queued</span><strong>${report.summary.queuedUrls}</strong></div>
|
|
12843
|
-
<div class="metric"><span>Skipped</span><strong>${report.summary.skippedUrls}</strong></div>
|
|
12844
|
-
<div class="metric"><span>Failed fetches</span><strong>${report.summary.failedUrls}</strong></div>
|
|
12845
|
-
<div class="metric"><span>Observed internal links</span><strong>${report.summary.observedInternalLinks}</strong></div>
|
|
12846
|
-
<div class="metric"><span>Issues</span><strong>${report.issues.length}</strong></div>
|
|
12847
|
-
<div class="metric"><span>High</span><strong>${report.summary.highIssues}</strong></div>
|
|
12848
|
-
</section>
|
|
12849
|
-
${accessItems ? `<h2>Access blocks</h2><ul>${accessItems}</ul>` : ""}
|
|
12850
|
-
<h2>Prioritised fixes</h2>
|
|
12851
|
-
${fixItems ? `<ol>${fixItems}</ol>` : "<p>No prioritised fixes found.</p>"}
|
|
12852
|
-
${reviewItems ? `<h2>Review observations</h2>
|
|
12853
|
-
<p>Check these before scheduling implementation work.</p>
|
|
12854
|
-
<ul>${reviewItems}</ul>` : ""}
|
|
12855
|
-
${report.caveats.length ? `<h2>Caveats</h2>
|
|
12856
|
-
<ul>${caveatItems}</ul>` : ""}
|
|
12857
|
-
${report.warnings.length ? `<h2>Warnings</h2>
|
|
12858
|
-
<ul>${warningItems}</ul>` : ""}
|
|
12859
|
-
<h2>Issue groups</h2>
|
|
12860
|
-
<table>
|
|
12861
|
-
<thead><tr><th>Severity</th><th>Issue</th><th>URLs</th><th>Fix</th><th>Sample URL</th></tr></thead>
|
|
12862
|
-
<tbody>${issueRows}</tbody>
|
|
12863
|
-
</table>
|
|
12864
|
-
</body>
|
|
12865
|
-
</html>
|
|
12866
|
-
`;
|
|
12867
|
-
}
|
|
12868
|
-
function renderCrawlMarkdownTickets(report, fixes = topFixes(report)) {
|
|
12869
|
-
const reviews = reviewObservations(report);
|
|
12870
|
-
const lines = [
|
|
12871
|
-
`# Crawl Implementation Tickets`,
|
|
12872
|
-
"",
|
|
12873
|
-
`Source: ${report.config.url}`,
|
|
12874
|
-
`Report: ${report.id}`,
|
|
12875
|
-
`Status: ${report.status}`,
|
|
12876
|
-
`Strategy: ${report.config.strategy}`,
|
|
12877
|
-
`Crawler: ${report.access.crawler.userAgent}`,
|
|
12878
|
-
""
|
|
12879
|
-
];
|
|
12880
|
-
if (report.caveats.length) lines.push("## Caveats", "", ...report.caveats.map((item) => `- ${item}`), "");
|
|
12881
|
-
if (report.warnings.length) lines.push("## Warnings", "", ...report.warnings.map((item) => `- ${item}`), "");
|
|
12882
|
-
if (report.access.blockedRequests > 0) lines.push("## Access blocks", "", ...report.access.samples.flatMap((sample) => [
|
|
12883
|
-
`- ${sample.url}: ${sample.evidence.guidance.summary}`,
|
|
12884
|
-
` - Action: ${sample.evidence.guidance.recommendedAction}`,
|
|
12885
|
-
` - Security: ${sample.evidence.guidance.securityNote}`
|
|
12886
|
-
]), "");
|
|
12887
|
-
for (const [index, fix] of fixes.entries()) lines.push(`## ${index + 1}. ${fix.title}`, "", `- [ ] Fix ${fix.count} affected URL${fix.count === 1 ? "" : "s"}`, `- Severity: ${fix.severity}`, `- Rule: ${fix.ruleId}`, `- Why this ranks: ${fix.whyThisRanks}`, `- Plain-English fix: ${fix.howToFix}`, `- Verify: ${fix.howToVerify}`, `- Command: ${fix.verification.command}`, "", `Affected URLs:`, ...fix.sampleUrls.slice(0, 10).map((url) => `- ${url}`), "");
|
|
12888
|
-
if (!fixes.length) lines.push("No implementation tickets were generated.");
|
|
12889
|
-
if (reviews.length) lines.push("## Review observations", "", "These are not implementation tickets. Check the evidence before deciding whether to work on them.", "", ...reviews.slice(0, 10).flatMap((observation) => [`- ${observation.title} (${observation.severity}, ${observation.count} URL${observation.count === 1 ? "" : "s"}): ${observation.howToFix}`, ` - First URL: ${observation.sampleUrls[0] ?? "No sample URL recorded"}`]), "");
|
|
12890
|
-
return `${lines.join("\n")}\n`;
|
|
12891
|
-
}
|
|
12892
|
-
function escapeHtml(value) {
|
|
12893
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
12894
|
-
}
|
|
12895
|
-
function escapeXml(value) {
|
|
12896
|
-
return escapeHtml(value);
|
|
12897
|
-
}
|
|
12520
|
+
};
|
|
12898
12521
|
// packages/core/src/analyze/crawler/report.ts
|
|
12899
12522
|
const MAX_CRAWL_PAGES = 1e4;
|
|
12900
12523
|
const MAX_CRAWL_DEPTH = 64;
|
|
@@ -13065,6 +12688,10 @@ function sanitizeMessages(values = []) {
|
|
|
13065
12688
|
function compareText$17(left, right) {
|
|
13066
12689
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
13067
12690
|
}
|
|
12691
|
+
function compareCaveats(left, right) {
|
|
12692
|
+
const rank = (value) => value.startsWith("Coverage incomplete:") ? 0 : 1;
|
|
12693
|
+
return rank(left) - rank(right) || compareText$17(left, right);
|
|
12694
|
+
}
|
|
13068
12695
|
function sortPages(pages) {
|
|
13069
12696
|
return [...pages].sort((left, right) => compareText$17(left.url, right.url));
|
|
13070
12697
|
}
|
|
@@ -13216,123 +12843,661 @@ function normalizeLinkUrl(value) {
|
|
|
13216
12843
|
return;
|
|
13217
12844
|
}
|
|
13218
12845
|
}
|
|
13219
|
-
function deriveInternalLinkAuthority(pages, linkGraph) {
|
|
13220
|
-
const hasLinkGraph = linkGraph && Object.keys(linkGraph).length > 0;
|
|
13221
|
-
const hasStoredScores = pages.some((page) => page.internalInlinkCount !== void 0 || page.internalLinkAuthorityScore !== void 0);
|
|
13222
|
-
if (!hasLinkGraph && hasStoredScores) {
|
|
13223
|
-
if (pages.every((page) => page.internalInlinkCount !== void 0 && page.internalLinkAuthorityScore !== void 0)) return pages;
|
|
13224
|
-
return pages.map((page) => page.internalInlinkCount !== void 0 && page.internalLinkAuthorityScore !== void 0 ? page : {
|
|
13225
|
-
...page,
|
|
13226
|
-
internalInlinkCount: page.internalInlinkCount ?? 0,
|
|
13227
|
-
internalLinkAuthorityScore: page.internalLinkAuthorityScore ?? 0
|
|
13228
|
-
});
|
|
12846
|
+
function deriveInternalLinkAuthority(pages, linkGraph) {
|
|
12847
|
+
const hasLinkGraph = linkGraph && Object.keys(linkGraph).length > 0;
|
|
12848
|
+
const hasStoredScores = pages.some((page) => page.internalInlinkCount !== void 0 || page.internalLinkAuthorityScore !== void 0);
|
|
12849
|
+
if (!hasLinkGraph && hasStoredScores) {
|
|
12850
|
+
if (pages.every((page) => page.internalInlinkCount !== void 0 && page.internalLinkAuthorityScore !== void 0)) return pages;
|
|
12851
|
+
return pages.map((page) => page.internalInlinkCount !== void 0 && page.internalLinkAuthorityScore !== void 0 ? page : {
|
|
12852
|
+
...page,
|
|
12853
|
+
internalInlinkCount: page.internalInlinkCount ?? 0,
|
|
12854
|
+
internalLinkAuthorityScore: page.internalLinkAuthorityScore ?? 0
|
|
12855
|
+
});
|
|
12856
|
+
}
|
|
12857
|
+
const pageByUrl = /* @__PURE__ */ new Map();
|
|
12858
|
+
for (const page of pages) for (const value of [page.url, page.finalUrl]) {
|
|
12859
|
+
const normalized = normalizeLinkUrl(value);
|
|
12860
|
+
if (normalized) pageByUrl.set(normalized, page.url);
|
|
12861
|
+
}
|
|
12862
|
+
const inlinks = /* @__PURE__ */ new Map();
|
|
12863
|
+
for (const page of pages) inlinks.set(page.url, 0);
|
|
12864
|
+
for (const page of pages) {
|
|
12865
|
+
const targets = hasLinkGraph ? linkGraph?.[page.url] ?? linkGraph?.[page.finalUrl] ?? [] : page.sampleInternalLinks ?? [];
|
|
12866
|
+
const linkedPages = /* @__PURE__ */ new Set();
|
|
12867
|
+
for (const target of targets) {
|
|
12868
|
+
const normalized = normalizeLinkUrl(target);
|
|
12869
|
+
const linkedPage = normalized ? pageByUrl.get(normalized) : void 0;
|
|
12870
|
+
if (!linkedPage || linkedPage === page.url) continue;
|
|
12871
|
+
linkedPages.add(linkedPage);
|
|
12872
|
+
}
|
|
12873
|
+
for (const linkedPage of linkedPages) inlinks.set(linkedPage, (inlinks.get(linkedPage) ?? 0) + 1);
|
|
12874
|
+
}
|
|
12875
|
+
let maxInlinks = 0;
|
|
12876
|
+
for (const count of inlinks.values()) maxInlinks = Math.max(maxInlinks, count);
|
|
12877
|
+
return pages.map((page) => {
|
|
12878
|
+
const internalInlinkCount = inlinks.get(page.url) ?? 0;
|
|
12879
|
+
return {
|
|
12880
|
+
...page,
|
|
12881
|
+
internalInlinkCount,
|
|
12882
|
+
internalLinkAuthorityScore: maxInlinks ? Math.round(internalInlinkCount / maxInlinks * 100) : 0
|
|
12883
|
+
};
|
|
12884
|
+
});
|
|
12885
|
+
}
|
|
12886
|
+
function createCrawlReport(input) {
|
|
12887
|
+
const config = sanitizeCrawlConfig(normalizeCrawlConfig(input.config));
|
|
12888
|
+
const definitionId = crawlDefinitionId({
|
|
12889
|
+
config,
|
|
12890
|
+
site: input.site,
|
|
12891
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
12892
|
+
analyticsConnection: input.analyticsConnection
|
|
12893
|
+
});
|
|
12894
|
+
const safePagesWithLinks = sortPages(sanitizePages(deriveInternalLinkAuthority(input.pages ?? [], input.linkGraph)));
|
|
12895
|
+
const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
|
|
12896
|
+
const requestEvidenceStatus = input.requestEvidenceStatus ?? (input.requests ? "available" : "unavailable");
|
|
12897
|
+
if (requestEvidenceStatus === "unavailable" && requests.length > 0) throw new Error("Unavailable request evidence cannot include request observations.");
|
|
12898
|
+
const safeIssues = sanitizeIssues([...input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })], ...input.additionalIssues ?? []]);
|
|
12899
|
+
const pages = safePagesWithLinks;
|
|
12900
|
+
return {
|
|
12901
|
+
id: input.id ?? crawlRunId(),
|
|
12902
|
+
definitionId,
|
|
12903
|
+
site: input.site,
|
|
12904
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
12905
|
+
analyticsConnection: input.analyticsConnection,
|
|
12906
|
+
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
12907
|
+
status: input.status ?? "completed",
|
|
12908
|
+
configHash: crawlConfigHash(config),
|
|
12909
|
+
config,
|
|
12910
|
+
access: crawlAccessSummary(requests),
|
|
12911
|
+
summary: summarizeCrawlReport({
|
|
12912
|
+
pages,
|
|
12913
|
+
requests,
|
|
12914
|
+
requestEvidenceStatus,
|
|
12915
|
+
issues: safeIssues,
|
|
12916
|
+
stats: input.stats
|
|
12917
|
+
}),
|
|
12918
|
+
requestEvidenceStatus,
|
|
12919
|
+
requests,
|
|
12920
|
+
pages,
|
|
12921
|
+
issues: safeIssues,
|
|
12922
|
+
issueGroups: groupCrawlIssues(safeIssues),
|
|
12923
|
+
...input.dataSources ? { dataSources: sanitizeTenantValue(input.dataSources) } : {},
|
|
12924
|
+
...input.ai ? { ai: sanitizeTenantValue(input.ai) } : {},
|
|
12925
|
+
...input.sitemapDiscovery ? { sitemapDiscovery: sanitizeTenantValue(input.sitemapDiscovery) } : {},
|
|
12926
|
+
...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
|
|
12927
|
+
...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
|
|
12928
|
+
warnings: sanitizeMessages(input.warnings).sort(compareText$17),
|
|
12929
|
+
caveats: sanitizeMessages(input.caveats).sort(compareCaveats)
|
|
12930
|
+
};
|
|
12931
|
+
}
|
|
12932
|
+
function normalizeLoadedCrawlReport(report) {
|
|
12933
|
+
const config = sanitizeCrawlConfig(normalizeCrawlConfig(report.config));
|
|
12934
|
+
const requestEvidenceStatus = report.requestEvidenceStatus ?? (Object.hasOwn(report, "requests") ? "available" : "unavailable");
|
|
12935
|
+
const issues = sanitizeIssues(report.issues ?? []);
|
|
12936
|
+
const requests = sortRequests(sanitizeTenantValue(report.requests ?? []) ?? []);
|
|
12937
|
+
const pages = sortPages(sanitizePages(deriveInternalLinkAuthority(report.pages ?? [])));
|
|
12938
|
+
return {
|
|
12939
|
+
...sanitizeTenantValue(report),
|
|
12940
|
+
config,
|
|
12941
|
+
access: crawlAccessSummary(requests),
|
|
12942
|
+
requestEvidenceStatus,
|
|
12943
|
+
requests,
|
|
12944
|
+
definitionId: report.definitionId ?? crawlDefinitionId({
|
|
12945
|
+
config,
|
|
12946
|
+
site: report.site,
|
|
12947
|
+
googleAnalyticsPropertyId: report.googleAnalyticsPropertyId,
|
|
12948
|
+
analyticsConnection: report.analyticsConnection
|
|
12949
|
+
}),
|
|
12950
|
+
summary: summarizeCrawlReport({
|
|
12951
|
+
pages,
|
|
12952
|
+
requests,
|
|
12953
|
+
requestEvidenceStatus,
|
|
12954
|
+
issues,
|
|
12955
|
+
stats: report.summary
|
|
12956
|
+
}),
|
|
12957
|
+
pages,
|
|
12958
|
+
issues,
|
|
12959
|
+
issueGroups: groupCrawlIssues(issues),
|
|
12960
|
+
warnings: sanitizeMessages(report.warnings).sort(compareText$17),
|
|
12961
|
+
caveats: sanitizeMessages(report.caveats).sort(compareCaveats)
|
|
12962
|
+
};
|
|
12963
|
+
}
|
|
12964
|
+
// packages/core/src/analyze/crawler/crawl-caveats.ts
|
|
12965
|
+
/** Reports rank a caveat with this prefix first and lead with it. */
|
|
12966
|
+
const CRAWL_COVERAGE_CAVEAT_PREFIX = "Coverage incomplete:";
|
|
12967
|
+
function rateLimitRerunCommand(config, intervalMs) {
|
|
12968
|
+
const scope = config.strategy === "health" ? " --health" : config.mode === "site" ? "" : ` --mode ${config.mode}`;
|
|
12969
|
+
const sitemap = config.sitemapUrl ? ` --sitemap-url ${config.sitemapUrl}` : "";
|
|
12970
|
+
const slowMs = Math.min(8e3, Math.max(2e3, intervalMs * 2));
|
|
12971
|
+
return `seo crawl ${config.url}${scope}${sitemap} --max-pages ${config.maxPages} --concurrency 1 --fetch-interval-cap 1 --fetch-interval-ms ${slowMs}`;
|
|
12972
|
+
}
|
|
12973
|
+
/** Set when the sitemaps list more URLs than the page limit let the crawl read. */
|
|
12974
|
+
function sitemapLimitCaveatInput(config, discovery, defaulted) {
|
|
12975
|
+
if (!discovery || discovery.urlsReturned < config.maxPages || !discovery.roots.some((root) => root.possiblyTruncated)) return;
|
|
12976
|
+
return {
|
|
12977
|
+
maxPages: config.maxPages,
|
|
12978
|
+
defaulted,
|
|
12979
|
+
ceiling: MAX_CRAWL_PAGES,
|
|
12980
|
+
health: config.strategy === "health",
|
|
12981
|
+
url: config.url
|
|
12982
|
+
};
|
|
12983
|
+
}
|
|
12984
|
+
function crawlCaveats(input) {
|
|
12985
|
+
const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
|
|
12986
|
+
if (input.queueSafetySkippedUrls > 0) caveats.push(input.queueSafetyInventoryCapped ? `Left at least ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. The excluded-URL inventory reached its reporting cap. Increase --max-pages to inspect more.` : `Left ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
|
|
12987
|
+
if (input.originBackpressureSkippedUrls > 0) caveats.push(`Stopped ${input.originBackpressureSkippedUrls} queued URL${input.originBackpressureSkippedUrls === 1 ? "" : "s"} because the origin stayed slow. This is incomplete crawl evidence, not a failed URL check.`);
|
|
12988
|
+
if (input.rateLimit) {
|
|
12989
|
+
const { readPages, skippedUrls, rerunCommand } = input.rateLimit;
|
|
12990
|
+
caveats.push(`${CRAWL_COVERAGE_CAVEAT_PREFIX} read ${readPages} of ${readPages + skippedUrls} pages because the origin kept rate limiting the crawl (HTTP 429) after it backed off. Rerun slower: ${rerunCommand}`);
|
|
12991
|
+
}
|
|
12992
|
+
if (input.sitemapLimit) {
|
|
12993
|
+
const { maxPages, defaulted, ceiling, health, url } = input.sitemapLimit;
|
|
12994
|
+
caveats.push(`Checked only the first ${maxPages} sitemap URLs because the page limit is ${maxPages}${defaulted ? " by default" : ""}, and the sitemaps list more. Raise --max-pages, up to ${ceiling.toLocaleString("en-US")}, to check more: seo crawl ${url}${health ? " --health" : ""} --max-pages ${ceiling}`);
|
|
12995
|
+
}
|
|
12996
|
+
if (input.memoryPressureSkippedUrls > 0) caveats.push(`Left ${input.memoryPressureSkippedUrls} eligible URL${input.memoryPressureSkippedUrls === 1 ? "" : "s"} unchecked after the local memory safety limit was reached. Start with --health or lower --max-pages.`);
|
|
12997
|
+
const renderingFallbackUrls = input.renderingFallbackUrls ?? 0;
|
|
12998
|
+
if (renderingFallbackUrls > 0) caveats.push(`JavaScript rendering could not run for ${renderingFallbackUrls} page${renderingFallbackUrls === 1 ? "" : "s"}, so ${renderingFallbackUrls === 1 ? "it was" : "they were"} audited from raw HTML. Client-rendered content, metadata and links may be missing. Install Chromium or set SEO_BROWSER_EXECUTABLE_PATH, then rerun.`);
|
|
12999
|
+
const queryVariantSkippedUrls = input.queryVariantSkippedUrls ?? 0;
|
|
13000
|
+
if (queryVariantSkippedUrls > 0) caveats.push(`Left ${queryVariantSkippedUrls} query-string variant${queryVariantSkippedUrls === 1 ? "" : "s"} unqueued after 5 variants of the same path, so tracking parameters do not fill the crawl.`);
|
|
13001
|
+
for (const issue of input.reachIssues ?? []) if (issue.detail) caveats.push(issue.detail);
|
|
13002
|
+
return caveats;
|
|
13003
|
+
}
|
|
13004
|
+
/**
|
|
13005
|
+
* A site crawl that found no internal links on the start page and no sitemap
|
|
13006
|
+
* URLs checked one page, so its other findings cannot stand for the site.
|
|
13007
|
+
*/
|
|
13008
|
+
function crawlReachIssues(input) {
|
|
13009
|
+
const [page] = input.pages;
|
|
13010
|
+
if (input.crawledPages !== 1 || input.pages.length !== 1 || !page || page.outgoingInternalCount > 0 || input.sitemapUrlsReturned > 0) return [];
|
|
13011
|
+
const rule = explainRule("crawl_single_page_reach");
|
|
13012
|
+
if (!rule) throw new Error("Missing rule guidance for crawl_single_page_reach");
|
|
13013
|
+
return [{
|
|
13014
|
+
ruleId: "crawl_single_page_reach",
|
|
13015
|
+
title: rule.title,
|
|
13016
|
+
category: rule.category,
|
|
13017
|
+
severity: rule.defaultSeverity,
|
|
13018
|
+
url: page.url,
|
|
13019
|
+
detail: page.usedJs ? "The rendered start page had 0 internal links and no sitemap returned URLs, so the crawl checked only this page. List the pages to check with --mode list." : "The start page had 0 internal links in its HTML and no sitemap returned URLs, so the crawl checked only this page. It probably builds its links with JavaScript. Rerun with --rendering on, or list the pages with --mode list.",
|
|
13020
|
+
evidence: {
|
|
13021
|
+
startUrl: input.startUrl,
|
|
13022
|
+
crawledPages: 1,
|
|
13023
|
+
outgoingInternalCount: 0,
|
|
13024
|
+
sitemapUrlsReturned: 0,
|
|
13025
|
+
rendered: Boolean(page.usedJs)
|
|
13026
|
+
}
|
|
13027
|
+
}];
|
|
13028
|
+
}
|
|
13029
|
+
// packages/core/src/analyze/crawler/top-fixes.ts
|
|
13030
|
+
const SEVERITY_SCORE = {
|
|
13031
|
+
high: 5e3,
|
|
13032
|
+
medium: 1200,
|
|
13033
|
+
low: 100
|
|
13034
|
+
};
|
|
13035
|
+
const EFFORT_BY_CATEGORY = {
|
|
13036
|
+
canonical: "medium",
|
|
13037
|
+
content: "high",
|
|
13038
|
+
agents: "low",
|
|
13039
|
+
headings: "low",
|
|
13040
|
+
images: "medium",
|
|
13041
|
+
indexability: "medium",
|
|
13042
|
+
international: "low",
|
|
13043
|
+
metadata: "low",
|
|
13044
|
+
mobile: "low",
|
|
13045
|
+
response: "medium",
|
|
13046
|
+
security: "medium",
|
|
13047
|
+
social: "low",
|
|
13048
|
+
"structured-data": "medium"
|
|
13049
|
+
};
|
|
13050
|
+
const EFFORT_SCORE = {
|
|
13051
|
+
low: 60,
|
|
13052
|
+
medium: 30,
|
|
13053
|
+
high: 0
|
|
13054
|
+
};
|
|
13055
|
+
const AFFECTED_URL_WEIGHT = {
|
|
13056
|
+
high: 25,
|
|
13057
|
+
medium: 15,
|
|
13058
|
+
low: 2
|
|
13059
|
+
};
|
|
13060
|
+
const VALUE_WEIGHT = {
|
|
13061
|
+
high: 1,
|
|
13062
|
+
medium: .75,
|
|
13063
|
+
low: .15
|
|
13064
|
+
};
|
|
13065
|
+
const CRAWL_REACH_RULES = /* @__PURE__ */ new Set(["crawl_single_page_reach"]);
|
|
13066
|
+
function reachRank(ruleId) {
|
|
13067
|
+
return CRAWL_REACH_RULES.has(ruleId) ? 0 : 1;
|
|
13068
|
+
}
|
|
13069
|
+
function matchesPattern(pattern, value) {
|
|
13070
|
+
if (!pattern.includes("*")) return value.includes(pattern);
|
|
13071
|
+
const parts = pattern.split("*");
|
|
13072
|
+
let position = 0;
|
|
13073
|
+
for (const part of parts) {
|
|
13074
|
+
if (!part) continue;
|
|
13075
|
+
const found = value.slice(position).indexOf(part);
|
|
13076
|
+
if (found === -1) return false;
|
|
13077
|
+
position += found + part.length;
|
|
13078
|
+
}
|
|
13079
|
+
return true;
|
|
13080
|
+
}
|
|
13081
|
+
function filteredIssues(report, filters) {
|
|
13082
|
+
return report.issues.filter((issue) => {
|
|
13083
|
+
if (filters.category && issue.category !== filters.category) return false;
|
|
13084
|
+
if (filters.severity && issue.severity !== filters.severity) return false;
|
|
13085
|
+
if (filters.url && !matchesPattern(filters.url, issue.url)) return false;
|
|
13086
|
+
if ((filters.recommendation ?? "fix") !== recommendationForRule(issue.ruleId)) return false;
|
|
13087
|
+
return true;
|
|
13088
|
+
});
|
|
13089
|
+
}
|
|
13090
|
+
function groupIssues(issues) {
|
|
13091
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13092
|
+
for (const issue of issues) {
|
|
13093
|
+
const existing = groups.get(issue.ruleId) ?? {
|
|
13094
|
+
ruleId: issue.ruleId,
|
|
13095
|
+
title: issue.title,
|
|
13096
|
+
category: issue.category,
|
|
13097
|
+
severity: issue.severity,
|
|
13098
|
+
count: 0,
|
|
13099
|
+
sampleUrls: []
|
|
13100
|
+
};
|
|
13101
|
+
existing.count += 1;
|
|
13102
|
+
if (existing.sampleUrls.length < 10) existing.sampleUrls.push(issue.url);
|
|
13103
|
+
groups.set(issue.ruleId, existing);
|
|
13104
|
+
}
|
|
13105
|
+
return [...groups.values()];
|
|
13106
|
+
}
|
|
13107
|
+
function searchValueForGroup(report, urls) {
|
|
13108
|
+
const wanted = new Set(urls);
|
|
13109
|
+
const pages = report.pages.filter((page) => wanted.has(page.url) || wanted.has(page.finalUrl));
|
|
13110
|
+
const metrics = pages.map((page) => page.searchMetrics).filter((item) => Boolean(item));
|
|
13111
|
+
const clicks = metrics.reduce((sum, item) => sum + item.clicks, 0);
|
|
13112
|
+
const impressions = metrics.reduce((sum, item) => sum + item.impressions, 0);
|
|
13113
|
+
const sessions = pages.reduce((sum, page) => sum + (page.analytics?.sessions ?? 0), 0);
|
|
13114
|
+
const observedMetrics = new Set(report.dataSources?.analytics.observedMetrics ?? [
|
|
13115
|
+
"sessions",
|
|
13116
|
+
"totalUsers",
|
|
13117
|
+
"conversions"
|
|
13118
|
+
]);
|
|
13119
|
+
const totalUsers = observedMetrics.has("totalUsers") ? pages.reduce((sum, page) => sum + (page.analytics?.totalUsers ?? 0), 0) : void 0;
|
|
13120
|
+
const conversions = observedMetrics.has("conversions") ? pages.reduce((sum, page) => sum + (page.analytics?.conversions ?? 0), 0) : void 0;
|
|
13121
|
+
const avgPosition = metrics.length ? metrics.reduce((sum, item) => sum + item.position, 0) / metrics.length : void 0;
|
|
13122
|
+
return {
|
|
13123
|
+
searchVisibleUrls: metrics.length,
|
|
13124
|
+
clicks,
|
|
13125
|
+
impressions,
|
|
13126
|
+
sessions,
|
|
13127
|
+
totalUsers,
|
|
13128
|
+
conversions,
|
|
13129
|
+
avgPosition
|
|
13130
|
+
};
|
|
13131
|
+
}
|
|
13132
|
+
function whyThisRanks(input, recommendation, searchStatus, analyticsProvider) {
|
|
13133
|
+
const visibility = input.searchVisibleUrls ? `${input.searchVisibleUrls} affected URLs have GSC visibility (${input.clicks} clicks, ${input.impressions} impressions).` : searchStatus && !["joined", "none"].includes(searchStatus) ? `No joined GSC visibility is available for these affected URLs; Search Console evidence is ${searchStatus}.` : "No affected URL has joined GSC visibility yet.";
|
|
13134
|
+
const analyticsDetails = analyticsProvider ? analyticsProviderDetails(analyticsProvider) : analyticsProviderDetails("google");
|
|
13135
|
+
const analyticsLabel = analyticsDetails.label;
|
|
13136
|
+
const sessionLabel = analyticsDetails.landingMetricLabel;
|
|
13137
|
+
return `${visibility}${input.sessions ? ` ${analyticsLabel} adds ${input.sessions} ${sessionLabel}${input.conversions === void 0 ? "" : ` and ${input.conversions} conversions`} from affected pages.` : ""} ${recommendation === "review" ? "This is a review observation. Confirm it before scheduling implementation work." : `This ranks as a fix. Severity is ${input.severity}; ${input.affectedUrls} URL${input.affectedUrls === 1 ? " is" : "s are"} affected; estimated effort is ${input.effort}.`}`;
|
|
13138
|
+
}
|
|
13139
|
+
function verificationCommand(report, severity) {
|
|
13140
|
+
return `seo crawl ${report.config.url} --severity ${severity} --max-pages ${Math.min(report.config.maxPages, 100)}`;
|
|
13141
|
+
}
|
|
13142
|
+
function topFixes(report, filters = {}) {
|
|
13143
|
+
return groupIssues(filteredIssues(report, filters)).map((group) => {
|
|
13144
|
+
const search = searchValueForGroup(report, group.sampleUrls);
|
|
13145
|
+
const effort = EFFORT_BY_CATEGORY[group.category] ?? "medium";
|
|
13146
|
+
const scoreFactors = {
|
|
13147
|
+
severity: SEVERITY_SCORE[group.severity],
|
|
13148
|
+
affectedUrls: group.count,
|
|
13149
|
+
searchVisibleUrls: search.searchVisibleUrls,
|
|
13150
|
+
clicks: search.clicks,
|
|
13151
|
+
impressions: search.impressions,
|
|
13152
|
+
sessions: search.sessions,
|
|
13153
|
+
totalUsers: search.totalUsers,
|
|
13154
|
+
conversions: search.conversions,
|
|
13155
|
+
avgPosition: search.avgPosition,
|
|
13156
|
+
effort,
|
|
13157
|
+
effortScore: EFFORT_SCORE[effort]
|
|
13158
|
+
};
|
|
13159
|
+
const valueWeight = VALUE_WEIGHT[group.severity];
|
|
13160
|
+
const score = scoreFactors.severity + scoreFactors.affectedUrls * AFFECTED_URL_WEIGHT[group.severity] + scoreFactors.searchVisibleUrls * 100 * valueWeight + scoreFactors.clicks * 20 * valueWeight + Math.min(scoreFactors.impressions, 1e4) / 25 * valueWeight + scoreFactors.sessions * 2 * valueWeight + (scoreFactors.conversions ?? 0) * 100 * valueWeight + scoreFactors.effortScore;
|
|
13161
|
+
const rule = explainRule(group.ruleId);
|
|
13162
|
+
const recommendation = recommendationForRule(group.ruleId);
|
|
13163
|
+
return {
|
|
13164
|
+
...group,
|
|
13165
|
+
recommendation,
|
|
13166
|
+
score: Math.round(score),
|
|
13167
|
+
scoreFactors,
|
|
13168
|
+
whyThisRanks: whyThisRanks(scoreFactors, recommendation, report.dataSources?.searchConsole.status, report.dataSources?.analytics.provider),
|
|
13169
|
+
howToFix: rule?.howToFix ?? "",
|
|
13170
|
+
howToVerify: rule?.howToVerify ?? "",
|
|
13171
|
+
verification: {
|
|
13172
|
+
command: verificationCommand(report, group.severity),
|
|
13173
|
+
expected: rule?.howToVerify ?? ""
|
|
13174
|
+
}
|
|
13175
|
+
};
|
|
13176
|
+
}).sort((a, b) => reachRank(a.ruleId) - reachRank(b.ruleId) || b.score - a.score || SEVERITY_SCORE[b.severity] - SEVERITY_SCORE[a.severity] || b.count - a.count || a.ruleId.localeCompare(b.ruleId)).slice(0, filters.limit ?? 10);
|
|
13177
|
+
}
|
|
13178
|
+
function reviewObservations(report, filters = {}) {
|
|
13179
|
+
return topFixes(report, {
|
|
13180
|
+
...filters,
|
|
13181
|
+
recommendation: "review"
|
|
13182
|
+
});
|
|
13183
|
+
}
|
|
13184
|
+
// packages/core/src/analyze/crawler/export.ts
|
|
13185
|
+
const issueHeaders = [
|
|
13186
|
+
"rule_id",
|
|
13187
|
+
"title",
|
|
13188
|
+
"category",
|
|
13189
|
+
"severity",
|
|
13190
|
+
"url",
|
|
13191
|
+
"detail",
|
|
13192
|
+
"clicks",
|
|
13193
|
+
"impressions",
|
|
13194
|
+
"position"
|
|
13195
|
+
];
|
|
13196
|
+
const pageHeaders = [
|
|
13197
|
+
"url",
|
|
13198
|
+
"final_url",
|
|
13199
|
+
"status",
|
|
13200
|
+
"indexable",
|
|
13201
|
+
"title",
|
|
13202
|
+
"meta_description",
|
|
13203
|
+
"canonical",
|
|
13204
|
+
"word_count",
|
|
13205
|
+
"content_extractor",
|
|
13206
|
+
"content_extractor_type",
|
|
13207
|
+
"content_extraction_fallback",
|
|
13208
|
+
"word_count_source",
|
|
13209
|
+
"internal_links",
|
|
13210
|
+
"internal_inlinks",
|
|
13211
|
+
"internal_authority_score",
|
|
13212
|
+
"external_links",
|
|
13213
|
+
"images_total",
|
|
13214
|
+
"images_missing_alt",
|
|
13215
|
+
"oversized_image_candidates",
|
|
13216
|
+
"schema_types",
|
|
13217
|
+
"clicks",
|
|
13218
|
+
"impressions",
|
|
13219
|
+
"sessions"
|
|
13220
|
+
];
|
|
13221
|
+
function renderCrawlCsv(report) {
|
|
13222
|
+
return renderCsv(report.issues.map((issue) => ({
|
|
13223
|
+
rule_id: issue.ruleId,
|
|
13224
|
+
title: issue.title,
|
|
13225
|
+
category: issue.category,
|
|
13226
|
+
severity: issue.severity,
|
|
13227
|
+
url: issue.url,
|
|
13228
|
+
detail: issue.detail,
|
|
13229
|
+
clicks: issue.searchMetrics?.clicks,
|
|
13230
|
+
impressions: issue.searchMetrics?.impressions,
|
|
13231
|
+
position: issue.searchMetrics?.position
|
|
13232
|
+
})), issueHeaders);
|
|
13233
|
+
}
|
|
13234
|
+
function renderCrawlPagesCsv(report) {
|
|
13235
|
+
return renderCsv(report.pages.map((page) => ({
|
|
13236
|
+
url: page.url,
|
|
13237
|
+
final_url: page.finalUrl,
|
|
13238
|
+
status: page.status,
|
|
13239
|
+
indexable: page.indexable,
|
|
13240
|
+
title: page.title,
|
|
13241
|
+
meta_description: page.metaDescription,
|
|
13242
|
+
canonical: page.canonical,
|
|
13243
|
+
word_count: page.wordCount,
|
|
13244
|
+
content_extractor: page.contentExtraction?.used,
|
|
13245
|
+
content_extractor_type: page.contentExtraction?.extractorType,
|
|
13246
|
+
content_extraction_fallback: page.contentExtraction?.fallback,
|
|
13247
|
+
word_count_source: page.contentExtraction?.wordCountSource,
|
|
13248
|
+
internal_links: page.outgoingInternalCount,
|
|
13249
|
+
internal_inlinks: page.internalInlinkCount,
|
|
13250
|
+
internal_authority_score: page.internalLinkAuthorityScore,
|
|
13251
|
+
external_links: page.outgoingExternalCount,
|
|
13252
|
+
images_total: page.imagesTotal,
|
|
13253
|
+
images_missing_alt: page.imagesMissingAlt,
|
|
13254
|
+
oversized_image_candidates: page.oversizedImageCandidates?.length ?? 0,
|
|
13255
|
+
schema_types: page.schemaTypes?.join("|"),
|
|
13256
|
+
clicks: page.searchMetrics?.clicks,
|
|
13257
|
+
impressions: page.searchMetrics?.impressions,
|
|
13258
|
+
sessions: page.analytics?.sessions
|
|
13259
|
+
})), pageHeaders);
|
|
13260
|
+
}
|
|
13261
|
+
/** The caveat a report must lead with when the crawl read too little. */
|
|
13262
|
+
function crawlCoverageCaveat(report) {
|
|
13263
|
+
return report.caveats.find((caveat) => caveat.startsWith(CRAWL_COVERAGE_CAVEAT_PREFIX));
|
|
13264
|
+
}
|
|
13265
|
+
function renderCrawlPretty(report, fixes = topFixes(report)) {
|
|
13266
|
+
const reviews = reviewObservations(report);
|
|
13267
|
+
const requestSummary = report.requestEvidenceStatus === "available" ? `${report.requests.length} requests` : report.requestEvidenceStatus === "partial" ? `${report.requests.length} observed requests; some started requests were still in flight when the crawl stopped` : "request evidence unavailable for this legacy report";
|
|
13268
|
+
const coverage = crawlCoverageCaveat(report);
|
|
13269
|
+
const lines = [
|
|
13270
|
+
...coverage ? [coverage, ""] : [],
|
|
13271
|
+
`Crawl report for ${report.config.url}`,
|
|
13272
|
+
"",
|
|
13273
|
+
`Status: ${report.status}`,
|
|
13274
|
+
`Strategy: ${report.config.strategy === "health" ? "sitemap health pass" : "full audit"}`,
|
|
13275
|
+
`Crawler: ${report.access.crawler.userAgent}`,
|
|
13276
|
+
`Documents: ${report.summary.totalPages} retained; ${requestSummary}, ${report.summary.discoveredUrls} URLs discovered, ${report.summary.failedUrls} failed, ${report.summary.skippedUrls} skipped`,
|
|
13277
|
+
`Issues: ${report.issues.length} (${report.summary.highIssues} high, ${report.summary.mediumIssues} medium, ${report.summary.lowIssues} low)`
|
|
13278
|
+
];
|
|
13279
|
+
if (report.externalLinkVerification) {
|
|
13280
|
+
const external = report.externalLinkVerification;
|
|
13281
|
+
lines.push(`External links: ${external.fetchedUrls} fetched, ${external.failedUrls} request failure${external.failedUrls === 1 ? "" : "s"}, ${external.deferredUrls} deferred (${external.dataStatus})`);
|
|
13229
13282
|
}
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
const
|
|
13233
|
-
|
|
13283
|
+
if (report.access.blockedRequests > 0) {
|
|
13284
|
+
lines.push("", `Access blocks: ${report.access.blockedRequests}`);
|
|
13285
|
+
for (const sample of report.access.samples.slice(0, 3)) {
|
|
13286
|
+
const requestId = sample.evidence.requestId ? `, request ID ${sample.evidence.requestId}` : "";
|
|
13287
|
+
lines.push(`- ${sample.url}: ${sample.evidence.guidance.summary} (${sample.evidence.provider}${requestId})`, ` Action: ${sample.evidence.guidance.recommendedAction}`, ` Security: ${sample.evidence.guidance.securityNote}`);
|
|
13288
|
+
if (sample.evidence.guidance.documentationUrl) lines.push(` Provider docs: ${sample.evidence.guidance.documentationUrl}`);
|
|
13289
|
+
}
|
|
13234
13290
|
}
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
13291
|
+
if (fixes.length) {
|
|
13292
|
+
lines.push("", "Prioritised fixes");
|
|
13293
|
+
for (const fix of fixes.slice(0, 5)) {
|
|
13294
|
+
lines.push(`- ${fix.title} (${fix.severity}, ${fix.count} URL${fix.count === 1 ? "" : "s"}): ${fix.howToFix}`);
|
|
13295
|
+
if (fix.sampleUrls[0]) lines.push(` First URL: ${fix.sampleUrls[0]}`);
|
|
13296
|
+
}
|
|
13297
|
+
} else lines.push("", "No prioritised fixes found.");
|
|
13298
|
+
if (reviews.length) {
|
|
13299
|
+
lines.push("", "Review observations (check before scheduling work)");
|
|
13300
|
+
for (const observation of reviews.slice(0, 5)) {
|
|
13301
|
+
lines.push(`- ${observation.title} (${observation.severity}, ${observation.count} URL${observation.count === 1 ? "" : "s"}): ${observation.howToFix}`);
|
|
13302
|
+
if (observation.sampleUrls[0]) lines.push(` First URL: ${observation.sampleUrls[0]}`);
|
|
13245
13303
|
}
|
|
13246
|
-
for (const linkedPage of linkedPages) inlinks.set(linkedPage, (inlinks.get(linkedPage) ?? 0) + 1);
|
|
13247
13304
|
}
|
|
13248
|
-
|
|
13249
|
-
for (const
|
|
13250
|
-
|
|
13251
|
-
|
|
13305
|
+
lines.push("", "Next commands", report.config.strategy === "health" ? `- seo crawl ${report.config.url} --max-pages ${report.config.maxPages} --json` : `- seo crawl ${report.config.url} --json`);
|
|
13306
|
+
for (const command of [...new Set(fixes.slice(0, 3).map((fix) => fix.verification.command))]) lines.push(`- ${command}`);
|
|
13307
|
+
if (report.warnings.length) lines.push("", "Warnings", ...report.warnings.map((item) => `- ${item}`));
|
|
13308
|
+
if (report.caveats.length) lines.push("", "Caveats", ...report.caveats.map((item) => `- ${item}`));
|
|
13309
|
+
return `${lines.join("\n")}\n`;
|
|
13310
|
+
}
|
|
13311
|
+
function renderCrawlJunit(report) {
|
|
13312
|
+
const issuesByUrl = /* @__PURE__ */ new Map();
|
|
13313
|
+
for (const issue of report.issues) {
|
|
13314
|
+
const existing = issuesByUrl.get(issue.url) ?? [];
|
|
13315
|
+
existing.push(issue);
|
|
13316
|
+
issuesByUrl.set(issue.url, existing);
|
|
13317
|
+
}
|
|
13318
|
+
const sitemapCases = (report.sitemapDiscovery?.roots ?? []).flatMap((root) => {
|
|
13319
|
+
return (root.documents.length ? root.documents : [{
|
|
13320
|
+
url: root.url,
|
|
13321
|
+
dataStatus: root.dataStatus,
|
|
13322
|
+
compression: "none",
|
|
13323
|
+
warning: root.warnings.join(" ") || void 0
|
|
13324
|
+
}]).map((document) => {
|
|
13325
|
+
const failures = [
|
|
13326
|
+
...document.dataStatus !== "complete" ? [document.warning ?? `Sitemap evidence is ${document.dataStatus}.`] : [],
|
|
13327
|
+
...document.redirected ? [`Sitemap redirected to ${document.finalUrl ?? "an unknown URL"}.`] : [],
|
|
13328
|
+
...document.status !== void 0 && (document.status < 200 || document.status >= 300) ? [`Sitemap returned HTTP ${document.status}.`] : []
|
|
13329
|
+
];
|
|
13330
|
+
const output = [
|
|
13331
|
+
`dataStatus=${document.dataStatus}`,
|
|
13332
|
+
...document.status !== void 0 ? [`status=${document.status}`] : [],
|
|
13333
|
+
...document.finalUrl ? [`finalUrl=${document.finalUrl}`] : [],
|
|
13334
|
+
...document.root ? [`root=${document.root}`] : [],
|
|
13335
|
+
...document.warning ? [`warning=${document.warning}`] : []
|
|
13336
|
+
].join("\n");
|
|
13337
|
+
return {
|
|
13338
|
+
xml: [
|
|
13339
|
+
` <testcase classname="seo.sitemap-document" name="${escapeXml(document.url)}" time="0.000">`,
|
|
13340
|
+
...failures.length ? [` <failure message="${escapeXml(failures[0] ?? "Sitemap check failed")}">${escapeXml(failures.join("\n"))}</failure>`] : [],
|
|
13341
|
+
` <system-out>${escapeXml(output)}</system-out>`,
|
|
13342
|
+
" </testcase>"
|
|
13343
|
+
].join("\n"),
|
|
13344
|
+
failed: failures.length > 0
|
|
13345
|
+
};
|
|
13346
|
+
});
|
|
13347
|
+
});
|
|
13348
|
+
const requestCases = report.requests.map((request) => {
|
|
13349
|
+
const failures = [
|
|
13350
|
+
...(issuesByUrl.get(request.requestedUrl) ?? []).map((issue) => `${issue.severity}: ${issue.title}${issue.detail ? `: ${issue.detail}` : ""}`),
|
|
13351
|
+
...request.outcome === "failure" ? [`${request.failureKind}: ${request.error}`] : [],
|
|
13352
|
+
...request.outcome === "skipped" ? [request.reason === "origin-backpressure" ? request.error : `${request.reason}: ${request.robotsTxt.matchedLine ?? request.robotsTxt.availability}`] : []
|
|
13353
|
+
];
|
|
13354
|
+
const output = request.outcome === "response" ? [
|
|
13355
|
+
`status=${request.status}`,
|
|
13356
|
+
`finalUrl=${request.finalUrl}`,
|
|
13357
|
+
...request.robotsTxt ? [
|
|
13358
|
+
`robotsUrl=${request.robotsTxt.url}`,
|
|
13359
|
+
`robotsAllowed=${request.robotsTxt.allowed ?? "unknown"}`,
|
|
13360
|
+
`robotsAvailability=${request.robotsTxt.availability}`,
|
|
13361
|
+
...request.robotsTxt.status !== void 0 ? [`robotsStatus=${request.robotsTxt.status}`] : [],
|
|
13362
|
+
...request.robotsTxt.matchedLine ? [`robotsMatchedLine=${request.robotsTxt.matchedLine}`] : [],
|
|
13363
|
+
...request.robotsTxt.error ? [`robotsError=${request.robotsTxt.error}`] : []
|
|
13364
|
+
] : [],
|
|
13365
|
+
...request.accessBlock ? [
|
|
13366
|
+
`accessProvider=${request.accessBlock.provider}`,
|
|
13367
|
+
`crawler=${request.accessBlock.crawler.userAgent}`,
|
|
13368
|
+
...request.accessBlock.requestId ? [`requestId=${request.accessBlock.requestId}`] : []
|
|
13369
|
+
] : []
|
|
13370
|
+
].join("\n") : request.outcome === "failure" ? request.error : request.reason === "origin-backpressure" ? `reason=${request.reason}\nerror=${request.error}` : [
|
|
13371
|
+
`reason=${request.reason}`,
|
|
13372
|
+
`robotsUrl=${request.robotsTxt.url}`,
|
|
13373
|
+
`robotsAllowed=${request.robotsTxt.allowed ?? "unknown"}`,
|
|
13374
|
+
`robotsAvailability=${request.robotsTxt.availability}`,
|
|
13375
|
+
...request.robotsTxt.status !== void 0 ? [`robotsStatus=${request.robotsTxt.status}`] : [],
|
|
13376
|
+
...request.robotsTxt.matchedLine ? [`robotsMatchedLine=${request.robotsTxt.matchedLine}`] : [],
|
|
13377
|
+
...request.robotsTxt.error ? [`robotsError=${request.robotsTxt.error}`] : []
|
|
13378
|
+
].join("\n");
|
|
13379
|
+
const duration = ((request.durationMs ?? 0) / 1e3).toFixed(3);
|
|
13252
13380
|
return {
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13381
|
+
xml: [
|
|
13382
|
+
` <testcase classname="seo.sitemap-health" name="${escapeXml(request.requestedUrl)}" time="${duration}">`,
|
|
13383
|
+
...failures.length ? [` <failure message="${escapeXml(failures[0] ?? "Health check failed")}">${escapeXml(failures.join("\n"))}</failure>`] : [],
|
|
13384
|
+
` <system-out>${escapeXml(output)}</system-out>`,
|
|
13385
|
+
" </testcase>"
|
|
13386
|
+
].join("\n"),
|
|
13387
|
+
failed: failures.length > 0
|
|
13256
13388
|
};
|
|
13257
13389
|
});
|
|
13390
|
+
const cases = [...sitemapCases, ...requestCases];
|
|
13391
|
+
const failures = cases.filter((item) => item.failed).length;
|
|
13392
|
+
const elapsed = report.requests.reduce((sum, request) => sum + (request.durationMs ?? 0), 0);
|
|
13393
|
+
return [
|
|
13394
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
13395
|
+
`<testsuite name="seo sitemap health" tests="${cases.length}" failures="${failures}" errors="0" skipped="0" time="${(elapsed / 1e3).toFixed(3)}">`,
|
|
13396
|
+
" <properties>",
|
|
13397
|
+
` <property name="report.id" value="${escapeXml(report.id)}"/>`,
|
|
13398
|
+
` <property name="report.status" value="${escapeXml(report.status)}"/>`,
|
|
13399
|
+
` <property name="crawl.strategy" value="${escapeXml(report.config.strategy)}"/>`,
|
|
13400
|
+
` <property name="crawler.userAgent" value="${escapeXml(report.access.crawler.userAgent)}"/>`,
|
|
13401
|
+
" </properties>",
|
|
13402
|
+
...cases.map((item) => item.xml),
|
|
13403
|
+
"</testsuite>",
|
|
13404
|
+
""
|
|
13405
|
+
].join("\n");
|
|
13258
13406
|
}
|
|
13259
|
-
function
|
|
13260
|
-
const
|
|
13261
|
-
const
|
|
13262
|
-
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13266
|
-
});
|
|
13267
|
-
const
|
|
13268
|
-
const
|
|
13269
|
-
const
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13407
|
+
function renderCrawlHtml(report, fixes = topFixes(report)) {
|
|
13408
|
+
const reviews = reviewObservations(report);
|
|
13409
|
+
const issueRows = report.issueGroups.map((group) => {
|
|
13410
|
+
const rule = explainRule(group.ruleId);
|
|
13411
|
+
return `<tr><td>${escapeHtml(group.severity)}</td><td>${escapeHtml(group.title)}</td><td>${group.count}</td><td>${escapeHtml(rule?.howToFix ?? "")}</td><td>${escapeHtml(group.sampleUrls[0] ?? "")}</td></tr>`;
|
|
13412
|
+
}).join("\n");
|
|
13413
|
+
const fixItems = fixes.slice(0, 10).map((fix) => `<li><strong>${escapeHtml(fix.title)}</strong><br>${escapeHtml(fix.howToFix)}<br><span>Verify: ${escapeHtml(fix.howToVerify)}</span><br><code>${escapeHtml(fix.verification.command)}</code></li>`).join("\n");
|
|
13414
|
+
const reviewItems = reviews.slice(0, 10).map((observation) => `<li><strong>${escapeHtml(observation.title)}</strong><br>${escapeHtml(observation.howToFix)}<br><span>Check before scheduling work. ${escapeHtml(observation.howToVerify)}</span></li>`).join("\n");
|
|
13415
|
+
const caveatItems = report.caveats.map((caveat) => `<li>${escapeHtml(caveat)}</li>`).join("\n");
|
|
13416
|
+
const warningItems = report.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("\n");
|
|
13417
|
+
const accessItems = report.access.samples.map((sample) => `<li><strong>${escapeHtml(sample.url)}</strong><br>${escapeHtml(sample.evidence.guidance.summary)}<br>${escapeHtml(sample.evidence.guidance.recommendedAction)}<br><span>${escapeHtml(sample.evidence.guidance.securityNote)}</span></li>`).join("\n");
|
|
13418
|
+
return `<!doctype html>
|
|
13419
|
+
<html lang="en">
|
|
13420
|
+
<head>
|
|
13421
|
+
<meta charset="utf-8">
|
|
13422
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
13423
|
+
<title>Crawl report for ${escapeHtml(report.config.url)}</title>
|
|
13424
|
+
<style>
|
|
13425
|
+
body { color: #17202a; font: 15px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 32px; max-width: 1120px; }
|
|
13426
|
+
h1, h2 { line-height: 1.2; }
|
|
13427
|
+
.summary { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); margin: 24px 0; }
|
|
13428
|
+
.metric { border: 1px solid #d8dee4; border-radius: 8px; padding: 12px; }
|
|
13429
|
+
.metric strong { display: block; font-size: 24px; }
|
|
13430
|
+
table { border-collapse: collapse; width: 100%; }
|
|
13431
|
+
th, td { border-bottom: 1px solid #d8dee4; padding: 10px; text-align: left; vertical-align: top; }
|
|
13432
|
+
th { background: #f6f8fa; }
|
|
13433
|
+
li { margin: 0 0 12px; }
|
|
13434
|
+
span { color: #57606a; }
|
|
13435
|
+
</style>
|
|
13436
|
+
</head>
|
|
13437
|
+
<body>
|
|
13438
|
+
<h1>Crawl report</h1>
|
|
13439
|
+
<p>${escapeHtml(report.config.url)}</p>
|
|
13440
|
+
<p>Strategy: ${escapeHtml(report.config.strategy === "health" ? "sitemap health pass" : "full audit")}<br>Crawler: <code>${escapeHtml(report.access.crawler.userAgent)}</code></p>
|
|
13441
|
+
<section class="summary">
|
|
13442
|
+
<div class="metric"><span>Status</span><strong>${escapeHtml(report.status)}</strong></div>
|
|
13443
|
+
<div class="metric"><span>Documents</span><strong>${report.summary.totalPages}</strong></div>
|
|
13444
|
+
<div class="metric"><span>Requests</span><strong>${report.requestEvidenceStatus === "unavailable" ? "n/a" : report.requests.length}${report.requestEvidenceStatus === "partial" ? " (partial)" : ""}</strong></div>
|
|
13445
|
+
<div class="metric"><span>Discovered</span><strong>${report.summary.discoveredUrls}</strong></div>
|
|
13446
|
+
<div class="metric"><span>Queued</span><strong>${report.summary.queuedUrls}</strong></div>
|
|
13447
|
+
<div class="metric"><span>Skipped</span><strong>${report.summary.skippedUrls}</strong></div>
|
|
13448
|
+
<div class="metric"><span>Failed fetches</span><strong>${report.summary.failedUrls}</strong></div>
|
|
13449
|
+
<div class="metric"><span>Observed internal links</span><strong>${report.summary.observedInternalLinks}</strong></div>
|
|
13450
|
+
<div class="metric"><span>Issues</span><strong>${report.issues.length}</strong></div>
|
|
13451
|
+
<div class="metric"><span>High</span><strong>${report.summary.highIssues}</strong></div>
|
|
13452
|
+
</section>
|
|
13453
|
+
${accessItems ? `<h2>Access blocks</h2><ul>${accessItems}</ul>` : ""}
|
|
13454
|
+
<h2>Prioritised fixes</h2>
|
|
13455
|
+
${fixItems ? `<ol>${fixItems}</ol>` : "<p>No prioritised fixes found.</p>"}
|
|
13456
|
+
${reviewItems ? `<h2>Review observations</h2>
|
|
13457
|
+
<p>Check these before scheduling implementation work.</p>
|
|
13458
|
+
<ul>${reviewItems}</ul>` : ""}
|
|
13459
|
+
${report.caveats.length ? `<h2>Caveats</h2>
|
|
13460
|
+
<ul>${caveatItems}</ul>` : ""}
|
|
13461
|
+
${report.warnings.length ? `<h2>Warnings</h2>
|
|
13462
|
+
<ul>${warningItems}</ul>` : ""}
|
|
13463
|
+
<h2>Issue groups</h2>
|
|
13464
|
+
<table>
|
|
13465
|
+
<thead><tr><th>Severity</th><th>Issue</th><th>URLs</th><th>Fix</th><th>Sample URL</th></tr></thead>
|
|
13466
|
+
<tbody>${issueRows}</tbody>
|
|
13467
|
+
</table>
|
|
13468
|
+
</body>
|
|
13469
|
+
</html>
|
|
13470
|
+
`;
|
|
13304
13471
|
}
|
|
13305
|
-
function
|
|
13306
|
-
const
|
|
13307
|
-
const
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
config
|
|
13314
|
-
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
}
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13329
|
-
|
|
13330
|
-
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
caveats: sanitizeMessages(report.caveats).sort(compareText$17)
|
|
13335
|
-
};
|
|
13472
|
+
function renderCrawlMarkdownTickets(report, fixes = topFixes(report)) {
|
|
13473
|
+
const reviews = reviewObservations(report);
|
|
13474
|
+
const lines = [
|
|
13475
|
+
`# Crawl Implementation Tickets`,
|
|
13476
|
+
"",
|
|
13477
|
+
`Source: ${report.config.url}`,
|
|
13478
|
+
`Report: ${report.id}`,
|
|
13479
|
+
`Status: ${report.status}`,
|
|
13480
|
+
`Strategy: ${report.config.strategy}`,
|
|
13481
|
+
`Crawler: ${report.access.crawler.userAgent}`,
|
|
13482
|
+
""
|
|
13483
|
+
];
|
|
13484
|
+
if (report.caveats.length) lines.push("## Caveats", "", ...report.caveats.map((item) => `- ${item}`), "");
|
|
13485
|
+
if (report.warnings.length) lines.push("## Warnings", "", ...report.warnings.map((item) => `- ${item}`), "");
|
|
13486
|
+
if (report.access.blockedRequests > 0) lines.push("## Access blocks", "", ...report.access.samples.flatMap((sample) => [
|
|
13487
|
+
`- ${sample.url}: ${sample.evidence.guidance.summary}`,
|
|
13488
|
+
` - Action: ${sample.evidence.guidance.recommendedAction}`,
|
|
13489
|
+
` - Security: ${sample.evidence.guidance.securityNote}`
|
|
13490
|
+
]), "");
|
|
13491
|
+
for (const [index, fix] of fixes.entries()) lines.push(`## ${index + 1}. ${fix.title}`, "", `- [ ] Fix ${fix.count} affected URL${fix.count === 1 ? "" : "s"}`, `- Severity: ${fix.severity}`, `- Rule: ${fix.ruleId}`, `- Why this ranks: ${fix.whyThisRanks}`, `- Plain-English fix: ${fix.howToFix}`, `- Verify: ${fix.howToVerify}`, `- Command: ${fix.verification.command}`, "", `Affected URLs:`, ...fix.sampleUrls.slice(0, 10).map((url) => `- ${url}`), "");
|
|
13492
|
+
if (!fixes.length) lines.push("No implementation tickets were generated.");
|
|
13493
|
+
if (reviews.length) lines.push("## Review observations", "", "These are not implementation tickets. Check the evidence before deciding whether to work on them.", "", ...reviews.slice(0, 10).flatMap((observation) => [`- ${observation.title} (${observation.severity}, ${observation.count} URL${observation.count === 1 ? "" : "s"}): ${observation.howToFix}`, ` - First URL: ${observation.sampleUrls[0] ?? "No sample URL recorded"}`]), "");
|
|
13494
|
+
return `${lines.join("\n")}\n`;
|
|
13495
|
+
}
|
|
13496
|
+
function escapeHtml(value) {
|
|
13497
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
13498
|
+
}
|
|
13499
|
+
function escapeXml(value) {
|
|
13500
|
+
return escapeHtml(value);
|
|
13336
13501
|
}
|
|
13337
13502
|
// packages/core/src/analyze/crawler/site-checks-schema.ts
|
|
13338
13503
|
const crawlSiteChecksSchema = z.object({ soft404: z.object({
|
|
@@ -14464,123 +14629,6 @@ function agentReadyIssues(input) {
|
|
|
14464
14629
|
}
|
|
14465
14630
|
return issues;
|
|
14466
14631
|
}
|
|
14467
|
-
// packages/core/src/analyze/crawler/crawl-url-queue.ts
|
|
14468
|
-
function before(left, right) {
|
|
14469
|
-
return left.depth < right.depth || left.depth === right.depth && left.url < right.url;
|
|
14470
|
-
}
|
|
14471
|
-
var CrawlUrlQueue = class {
|
|
14472
|
-
#items = [];
|
|
14473
|
-
#positions = /* @__PURE__ */ new Map();
|
|
14474
|
-
get size() {
|
|
14475
|
-
return this.#items.length;
|
|
14476
|
-
}
|
|
14477
|
-
has(url) {
|
|
14478
|
-
return this.#positions.has(url);
|
|
14479
|
-
}
|
|
14480
|
-
push(item) {
|
|
14481
|
-
if (this.has(item.url)) throw new Error(`URL is already queued: ${item.url}`);
|
|
14482
|
-
const index = this.#items.length;
|
|
14483
|
-
this.#items.push(item);
|
|
14484
|
-
this.#positions.set(item.url, index);
|
|
14485
|
-
this.#bubbleUp(index);
|
|
14486
|
-
}
|
|
14487
|
-
decreaseDepth(url, depth) {
|
|
14488
|
-
const index = this.#positions.get(url);
|
|
14489
|
-
if (index === void 0) return false;
|
|
14490
|
-
const item = this.#items[index];
|
|
14491
|
-
if (!item || depth >= item.depth) return false;
|
|
14492
|
-
item.depth = depth;
|
|
14493
|
-
this.#bubbleUp(index);
|
|
14494
|
-
return true;
|
|
14495
|
-
}
|
|
14496
|
-
take() {
|
|
14497
|
-
const first = this.#items[0];
|
|
14498
|
-
if (!first) return void 0;
|
|
14499
|
-
const last = this.#items.pop();
|
|
14500
|
-
this.#positions.delete(first.url);
|
|
14501
|
-
if (last && last !== first) {
|
|
14502
|
-
this.#items[0] = last;
|
|
14503
|
-
this.#positions.set(last.url, 0);
|
|
14504
|
-
this.#bubbleDown(0);
|
|
14505
|
-
}
|
|
14506
|
-
return first;
|
|
14507
|
-
}
|
|
14508
|
-
#swap(left, right) {
|
|
14509
|
-
const leftItem = this.#items[left];
|
|
14510
|
-
const rightItem = this.#items[right];
|
|
14511
|
-
if (!leftItem || !rightItem) return;
|
|
14512
|
-
this.#items[left] = rightItem;
|
|
14513
|
-
this.#items[right] = leftItem;
|
|
14514
|
-
this.#positions.set(rightItem.url, left);
|
|
14515
|
-
this.#positions.set(leftItem.url, right);
|
|
14516
|
-
}
|
|
14517
|
-
#bubbleUp(start) {
|
|
14518
|
-
let index = start;
|
|
14519
|
-
while (index > 0) {
|
|
14520
|
-
const parent = Math.floor((index - 1) / 2);
|
|
14521
|
-
const item = this.#items[index];
|
|
14522
|
-
const parentItem = this.#items[parent];
|
|
14523
|
-
if (!item || !parentItem || !before(item, parentItem)) break;
|
|
14524
|
-
this.#swap(index, parent);
|
|
14525
|
-
index = parent;
|
|
14526
|
-
}
|
|
14527
|
-
}
|
|
14528
|
-
#bubbleDown(start) {
|
|
14529
|
-
let index = start;
|
|
14530
|
-
while (true) {
|
|
14531
|
-
const left = index * 2 + 1;
|
|
14532
|
-
const right = left + 1;
|
|
14533
|
-
let next = index;
|
|
14534
|
-
const item = this.#items[next];
|
|
14535
|
-
const leftItem = this.#items[left];
|
|
14536
|
-
const rightItem = this.#items[right];
|
|
14537
|
-
if (leftItem && item && before(leftItem, item)) next = left;
|
|
14538
|
-
const nextItem = this.#items[next];
|
|
14539
|
-
if (rightItem && nextItem && before(rightItem, nextItem)) next = right;
|
|
14540
|
-
if (next === index) return;
|
|
14541
|
-
this.#swap(index, next);
|
|
14542
|
-
index = next;
|
|
14543
|
-
}
|
|
14544
|
-
}
|
|
14545
|
-
};
|
|
14546
|
-
// packages/core/src/analyze/crawler/crawl-caveats.ts
|
|
14547
|
-
function crawlCaveats(input) {
|
|
14548
|
-
const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
|
|
14549
|
-
if (input.queueSafetySkippedUrls > 0) caveats.push(input.queueSafetyInventoryCapped ? `Left at least ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. The excluded-URL inventory reached its reporting cap. Increase --max-pages to inspect more.` : `Left ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
|
|
14550
|
-
if (input.originBackpressureSkippedUrls > 0) caveats.push(`Stopped ${input.originBackpressureSkippedUrls} queued URL${input.originBackpressureSkippedUrls === 1 ? "" : "s"} because the origin stayed slow. This is incomplete crawl evidence, not a failed URL check.`);
|
|
14551
|
-
if (input.memoryPressureSkippedUrls > 0) caveats.push(`Left ${input.memoryPressureSkippedUrls} eligible URL${input.memoryPressureSkippedUrls === 1 ? "" : "s"} unchecked after the local memory safety limit was reached. Start with --health or lower --max-pages.`);
|
|
14552
|
-
const renderingFallbackUrls = input.renderingFallbackUrls ?? 0;
|
|
14553
|
-
if (renderingFallbackUrls > 0) caveats.push(`JavaScript rendering could not run for ${renderingFallbackUrls} page${renderingFallbackUrls === 1 ? "" : "s"}, so ${renderingFallbackUrls === 1 ? "it was" : "they were"} audited from raw HTML. Client-rendered content, metadata and links may be missing. Install Chromium or set SEO_BROWSER_EXECUTABLE_PATH, then rerun.`);
|
|
14554
|
-
const queryVariantSkippedUrls = input.queryVariantSkippedUrls ?? 0;
|
|
14555
|
-
if (queryVariantSkippedUrls > 0) caveats.push(`Left ${queryVariantSkippedUrls} query-string variant${queryVariantSkippedUrls === 1 ? "" : "s"} unqueued after 5 variants of the same path, so tracking parameters do not fill the crawl.`);
|
|
14556
|
-
for (const issue of input.reachIssues ?? []) if (issue.detail) caveats.push(issue.detail);
|
|
14557
|
-
return caveats;
|
|
14558
|
-
}
|
|
14559
|
-
/**
|
|
14560
|
-
* A site crawl that found no internal links on the start page and no sitemap
|
|
14561
|
-
* URLs checked one page, so its other findings cannot stand for the site.
|
|
14562
|
-
*/
|
|
14563
|
-
function crawlReachIssues(input) {
|
|
14564
|
-
const [page] = input.pages;
|
|
14565
|
-
if (input.crawledPages !== 1 || input.pages.length !== 1 || !page || page.outgoingInternalCount > 0 || input.sitemapUrlsReturned > 0) return [];
|
|
14566
|
-
const rule = explainRule("crawl_single_page_reach");
|
|
14567
|
-
if (!rule) throw new Error("Missing rule guidance for crawl_single_page_reach");
|
|
14568
|
-
return [{
|
|
14569
|
-
ruleId: "crawl_single_page_reach",
|
|
14570
|
-
title: rule.title,
|
|
14571
|
-
category: rule.category,
|
|
14572
|
-
severity: rule.defaultSeverity,
|
|
14573
|
-
url: page.url,
|
|
14574
|
-
detail: page.usedJs ? "The rendered start page had 0 internal links and no sitemap returned URLs, so the crawl checked only this page. List the pages to check with --mode list." : "The start page had 0 internal links in its HTML and no sitemap returned URLs, so the crawl checked only this page. It probably builds its links with JavaScript. Rerun with --rendering on, or list the pages with --mode list.",
|
|
14575
|
-
evidence: {
|
|
14576
|
-
startUrl: input.startUrl,
|
|
14577
|
-
crawledPages: 1,
|
|
14578
|
-
outgoingInternalCount: 0,
|
|
14579
|
-
sitemapUrlsReturned: 0,
|
|
14580
|
-
rendered: Boolean(page.usedJs)
|
|
14581
|
-
}
|
|
14582
|
-
}];
|
|
14583
|
-
}
|
|
14584
14632
|
// packages/core/src/analyze/crawler/crawl-control.ts
|
|
14585
14633
|
const CRAWL_CANCELLED = Symbol("crawl-cancelled");
|
|
14586
14634
|
function abortController(input) {
|
|
@@ -14622,6 +14670,96 @@ function crawlRssLimitBytes(totalMemoryBytes) {
|
|
|
14622
14670
|
function crawlMemoryPressure(input) {
|
|
14623
14671
|
return input.memoryUsage.heapUsed + input.memoryUsage.external >= crawlMemoryLimitBytes(input.totalMemoryBytes) || input.memoryUsage.rss >= crawlRssLimitBytes(input.totalMemoryBytes);
|
|
14624
14672
|
}
|
|
14673
|
+
// packages/core/src/analyze/crawler/crawl-rate-limit.ts
|
|
14674
|
+
const RETRIES_PER_URL = 3;
|
|
14675
|
+
const BASE_BACKOFF_MS = 2e3;
|
|
14676
|
+
const MAX_BACKOFF_MS = 3e4;
|
|
14677
|
+
const COOLDOWN_BUDGET_MS = 18e4;
|
|
14678
|
+
const LIMITED_INTERVAL_MS = 1e3;
|
|
14679
|
+
const LIMITED_MAX_INTERVAL_MS = 4e3;
|
|
14680
|
+
function rateLimitEvidence(result, status, nowMs) {
|
|
14681
|
+
if (status !== 429 && status !== 503) return void 0;
|
|
14682
|
+
const header = retryAfterMs(result.page?.responseHeaders?.["retry-after"]);
|
|
14683
|
+
const cooldownUntil = Date.parse(result.page?.fetchDiagnostics?.backpressure?.cooldownUntil ?? "");
|
|
14684
|
+
const fromCooldown = Number.isFinite(cooldownUntil) ? Math.max(0, cooldownUntil - nowMs) : void 0;
|
|
14685
|
+
const wait = header ?? fromCooldown;
|
|
14686
|
+
if (status === 503 && wait === void 0) return void 0;
|
|
14687
|
+
return wait === void 0 ? {} : { retryAfterMs: Math.min(wait, MAX_BACKOFF_MS) };
|
|
14688
|
+
}
|
|
14689
|
+
/**
|
|
14690
|
+
* Rate-limit state for one crawl. After the first 429 the crawl drops to one
|
|
14691
|
+
* request at a time with a per-host interval and never speeds back up.
|
|
14692
|
+
*/
|
|
14693
|
+
var CrawlRateLimit = class {
|
|
14694
|
+
rate;
|
|
14695
|
+
responses = 0;
|
|
14696
|
+
skippedUrls = 0;
|
|
14697
|
+
pendingMs = 0;
|
|
14698
|
+
spentMs = 0;
|
|
14699
|
+
#level = 0;
|
|
14700
|
+
#budgetSpent = false;
|
|
14701
|
+
#retries = /* @__PURE__ */ new Map();
|
|
14702
|
+
constructor(rate) {
|
|
14703
|
+
this.rate = rate;
|
|
14704
|
+
}
|
|
14705
|
+
get limited() {
|
|
14706
|
+
return this.responses > 0;
|
|
14707
|
+
}
|
|
14708
|
+
get intervalMs() {
|
|
14709
|
+
return this.rate.intervalMs ?? LIMITED_INTERVAL_MS;
|
|
14710
|
+
}
|
|
14711
|
+
/** The cool-down before probes that run after the crawl. */
|
|
14712
|
+
get settleMs() {
|
|
14713
|
+
return Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** this.#level);
|
|
14714
|
+
}
|
|
14715
|
+
/**
|
|
14716
|
+
* `retry` requeues the URL after `pendingMs`. `skip` gives it up, and
|
|
14717
|
+
* `stop` gives up the rest of the queue too once the budget is spent.
|
|
14718
|
+
*/
|
|
14719
|
+
observe(url, result, status, nowMs) {
|
|
14720
|
+
const evidence = rateLimitEvidence(result, status, nowMs);
|
|
14721
|
+
if (!evidence) return void 0;
|
|
14722
|
+
this.responses += 1;
|
|
14723
|
+
if (this.pendingMs === 0) this.#level += 1;
|
|
14724
|
+
const step = 2 ** Math.max(0, this.#level - 1);
|
|
14725
|
+
const waitMs = evidence.retryAfterMs ?? Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * step);
|
|
14726
|
+
this.rate = {
|
|
14727
|
+
...this.rate,
|
|
14728
|
+
concurrency: 1,
|
|
14729
|
+
intervalCap: 1,
|
|
14730
|
+
intervalMs: Math.min(LIMITED_MAX_INTERVAL_MS, Math.max(this.rate.intervalMs ?? 0, LIMITED_INTERVAL_MS * step))
|
|
14731
|
+
};
|
|
14732
|
+
const nextMs = Math.max(this.pendingMs, waitMs);
|
|
14733
|
+
if (this.spentMs + nextMs > COOLDOWN_BUDGET_MS) this.#budgetSpent = true;
|
|
14734
|
+
const retries = this.#retries.get(url) ?? 0;
|
|
14735
|
+
if (!this.#budgetSpent && retries < RETRIES_PER_URL) {
|
|
14736
|
+
this.#retries.set(url, retries + 1);
|
|
14737
|
+
this.pendingMs = nextMs;
|
|
14738
|
+
return "retry";
|
|
14739
|
+
}
|
|
14740
|
+
this.skippedUrls += 1;
|
|
14741
|
+
return this.#budgetSpent ? "stop" : "skip";
|
|
14742
|
+
}
|
|
14743
|
+
async settle(sleep, signal) {
|
|
14744
|
+
if (this.pendingMs <= 0) return;
|
|
14745
|
+
await sleep(this.pendingMs, signal);
|
|
14746
|
+
this.spentMs += this.pendingMs;
|
|
14747
|
+
this.pendingMs = 0;
|
|
14748
|
+
}
|
|
14749
|
+
caveatInput(config, readPages) {
|
|
14750
|
+
if (this.skippedUrls === 0) return void 0;
|
|
14751
|
+
return {
|
|
14752
|
+
readPages,
|
|
14753
|
+
skippedUrls: this.skippedUrls,
|
|
14754
|
+
rerunCommand: rateLimitRerunCommand(config, this.intervalMs)
|
|
14755
|
+
};
|
|
14756
|
+
}
|
|
14757
|
+
warning() {
|
|
14758
|
+
if (!this.limited) return void 0;
|
|
14759
|
+
const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
14760
|
+
return `The origin rate limited ${plural(this.responses, "request")}. The crawl slowed to one request at a time, at least ${Math.round(this.intervalMs / 1e3)}s apart, and waited ${Math.round(this.spentMs / 1e3)}s in cool-downs${this.skippedUrls > 0 ? `, then left ${plural(this.skippedUrls, "URL")} unread` : " before reading every queued URL"}.`;
|
|
14761
|
+
}
|
|
14762
|
+
};
|
|
14625
14763
|
// packages/core/src/analyze/crawler/external-link-checks.ts
|
|
14626
14764
|
const EXTERNAL_LINK_SELECTION_LIMIT = 200;
|
|
14627
14765
|
const EXTERNAL_LINK_STATES = [
|
|
@@ -14882,21 +15020,32 @@ function probeObservation(url, result) {
|
|
|
14882
15020
|
error: request.outcome === "failure" ? request.error : `Probe skipped: ${request.reason}`
|
|
14883
15021
|
};
|
|
14884
15022
|
}
|
|
15023
|
+
function isConclusive(probe) {
|
|
15024
|
+
return probe.status !== void 0 && !probe.accessBlocked && probe.status !== 429 && probe.status !== 503;
|
|
15025
|
+
}
|
|
14885
15026
|
async function collectCrawlSiteChecks(input) {
|
|
15027
|
+
const probe = async (url) => probeObservation(url, await input.fetchStatusPage(url, {
|
|
15028
|
+
js: "off",
|
|
15029
|
+
refresh: true,
|
|
15030
|
+
timeoutMs: input.timeoutMs,
|
|
15031
|
+
signal: input.signal,
|
|
15032
|
+
writeCache: false,
|
|
15033
|
+
respectRobots: input.respectRobots,
|
|
15034
|
+
...input.rate ? { rate: input.rate } : {}
|
|
15035
|
+
}));
|
|
14886
15036
|
const probes = [];
|
|
14887
15037
|
for (const url of probeUrls(input.startUrl)) {
|
|
14888
15038
|
if (input.signal?.aborted) break;
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
}
|
|
14897
|
-
probes.push(probeObservation(url, result));
|
|
15039
|
+
probes.push(await probe(url));
|
|
15040
|
+
}
|
|
15041
|
+
if (input.sleep && !input.signal?.aborted && probes.some((item) => !isConclusive(item))) {
|
|
15042
|
+
await input.sleep(input.retryDelayMs ?? 2e3, input.signal);
|
|
15043
|
+
for (const [index, item] of probes.entries()) {
|
|
15044
|
+
if (input.signal?.aborted) break;
|
|
15045
|
+
if (!isConclusive(item)) probes[index] = await probe(item.url);
|
|
15046
|
+
}
|
|
14898
15047
|
}
|
|
14899
|
-
const conclusive = probes.filter(
|
|
15048
|
+
const conclusive = probes.filter(isConclusive);
|
|
14900
15049
|
const successful = conclusive.filter((probe) => (probe.status ?? 0) >= 200 && (probe.status ?? 0) < 400);
|
|
14901
15050
|
const correctMissing = conclusive.filter((probe) => probe.status === 404 || probe.status === 410);
|
|
14902
15051
|
const status = successful.length > 0 ? "warning" : correctMissing.length === PROBE_COUNT ? "pass" : "unknown";
|
|
@@ -14904,7 +15053,7 @@ async function collectCrawlSiteChecks(input) {
|
|
|
14904
15053
|
status,
|
|
14905
15054
|
probes,
|
|
14906
15055
|
probeLimit: PROBE_COUNT,
|
|
14907
|
-
complete:
|
|
15056
|
+
complete: conclusive.length === PROBE_COUNT
|
|
14908
15057
|
} };
|
|
14909
15058
|
if (status !== "warning") return {
|
|
14910
15059
|
checks,
|
|
@@ -17460,6 +17609,18 @@ async function fetchLandingPageValues(input, dependencies = defaultDependencies$
|
|
|
17460
17609
|
}[connection.provider](connection);
|
|
17461
17610
|
}
|
|
17462
17611
|
// packages/core/src/analyze/crawler/site-crawl-dependencies.ts
|
|
17612
|
+
function sleep$2(ms, signal) {
|
|
17613
|
+
return new Promise((resolve) => {
|
|
17614
|
+
if (signal?.aborted) return resolve();
|
|
17615
|
+
const done = () => {
|
|
17616
|
+
clearTimeout(timer);
|
|
17617
|
+
signal?.removeEventListener("abort", done);
|
|
17618
|
+
resolve();
|
|
17619
|
+
};
|
|
17620
|
+
const timer = setTimeout(done, ms);
|
|
17621
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
17622
|
+
});
|
|
17623
|
+
}
|
|
17463
17624
|
function resolveCrawlSiteDependencies(dependencies = {}) {
|
|
17464
17625
|
const hasInjectedPerPageSearchProvider = Boolean(dependencies.queryPageMetrics || dependencies.queryPageTopQuery);
|
|
17465
17626
|
const hasInjectedLegacyBulkSearchProvider = Boolean(dependencies.queryPagesMetrics || dependencies.queryPagesTopQueries);
|
|
@@ -17480,6 +17641,7 @@ function resolveCrawlSiteDependencies(dependencies = {}) {
|
|
|
17480
17641
|
fetchLandingPageValues: dependencies.fetchLandingPageValues ?? fetchLandingPageValues,
|
|
17481
17642
|
landingValueForUrl: dependencies.landingValueForUrl ?? landingValueForUrl,
|
|
17482
17643
|
now: dependencies.now ?? (() => /* @__PURE__ */ new Date()),
|
|
17644
|
+
sleep: dependencies.sleep ?? sleep$2,
|
|
17483
17645
|
memoryUsage: dependencies.memoryUsage ?? (() => process.memoryUsage()),
|
|
17484
17646
|
totalMemory: dependencies.totalMemory ?? totalmem
|
|
17485
17647
|
};
|
|
@@ -17698,6 +17860,7 @@ async function crawlSite(input, dependencies) {
|
|
|
17698
17860
|
const queryVariantsByPath = /* @__PURE__ */ new Map();
|
|
17699
17861
|
const queryVariantSkipped = /* @__PURE__ */ new Set();
|
|
17700
17862
|
let renderingFallbackUrls = 0;
|
|
17863
|
+
const rateLimit = new CrawlRateLimit(config.fetchRate);
|
|
17701
17864
|
let failedUrls = 0;
|
|
17702
17865
|
let observedInternalLinks = 0;
|
|
17703
17866
|
let memoryLimited = false;
|
|
@@ -17782,6 +17945,15 @@ async function crawlSite(input, dependencies) {
|
|
|
17782
17945
|
});
|
|
17783
17946
|
return;
|
|
17784
17947
|
}
|
|
17948
|
+
if (isCdnUtilityUrl(normalized)) {
|
|
17949
|
+
recordSkip("cdn-utility-url");
|
|
17950
|
+
emitStatus("url_skipped", {
|
|
17951
|
+
url: normalized,
|
|
17952
|
+
depth,
|
|
17953
|
+
reason: "cdn_utility_url"
|
|
17954
|
+
});
|
|
17955
|
+
return;
|
|
17956
|
+
}
|
|
17785
17957
|
if (isLikelyAsset(normalized)) {
|
|
17786
17958
|
recordSkip("asset-url");
|
|
17787
17959
|
emitStatus("url_skipped", {
|
|
@@ -17886,7 +18058,7 @@ async function crawlSite(input, dependencies) {
|
|
|
17886
18058
|
js: config.js,
|
|
17887
18059
|
refresh: config.refresh,
|
|
17888
18060
|
timeoutMs: config.timeoutMs,
|
|
17889
|
-
rate:
|
|
18061
|
+
rate: rateLimit.rate,
|
|
17890
18062
|
signal,
|
|
17891
18063
|
writeCache: false,
|
|
17892
18064
|
respectRobots: config.respectRobots,
|
|
@@ -17917,7 +18089,11 @@ async function crawlSite(input, dependencies) {
|
|
|
17917
18089
|
});
|
|
17918
18090
|
};
|
|
17919
18091
|
while (!isCancelled() && (queue.size || inFlight.size) && (!memoryLimited || inFlight.size > 0) && pages.length < config.maxPages) {
|
|
17920
|
-
|
|
18092
|
+
if (rateLimit.pendingMs > 0 && inFlight.size === 0) {
|
|
18093
|
+
await rateLimit.settle(deps.sleep, signal);
|
|
18094
|
+
if (isCancelled()) break;
|
|
18095
|
+
}
|
|
18096
|
+
while (!isCancelled() && !memoryLimited && rateLimit.pendingMs === 0 && inFlight.size < activeConcurrency && pages.length + inFlight.size < config.maxPages) {
|
|
17921
18097
|
const item = nextQueueItem();
|
|
17922
18098
|
if (!item) break;
|
|
17923
18099
|
startTask(item);
|
|
@@ -17937,11 +18113,46 @@ async function crawlSite(input, dependencies) {
|
|
|
17937
18113
|
error: result.warning ?? "Crawler returned no page snapshot.",
|
|
17938
18114
|
extraction: "not-applicable"
|
|
17939
18115
|
});
|
|
18116
|
+
const limited = rateLimit.observe(task.url, result, request.outcome === "response" ? request.status : void 0, deps.now().getTime());
|
|
18117
|
+
if (limited) {
|
|
18118
|
+
activeConcurrency = 1;
|
|
18119
|
+
healthyProbeStreak = 0;
|
|
18120
|
+
}
|
|
18121
|
+
if (limited === "retry") {
|
|
18122
|
+
visited.delete(task.url);
|
|
18123
|
+
if (!queue.has(task.url)) queue.push({
|
|
18124
|
+
url: task.url,
|
|
18125
|
+
depth: task.depth
|
|
18126
|
+
});
|
|
18127
|
+
emitStatus("url_queued", {
|
|
18128
|
+
url: task.url,
|
|
18129
|
+
depth: task.depth,
|
|
18130
|
+
reason: "rate_limited_retry",
|
|
18131
|
+
message: `The origin rate limited ${task.url}. Waiting ${Math.ceil(rateLimit.pendingMs / 1e3)}s, then retrying one request at a time.`
|
|
18132
|
+
});
|
|
18133
|
+
continue;
|
|
18134
|
+
}
|
|
18135
|
+
if (limited) {
|
|
18136
|
+
requests.push(request);
|
|
18137
|
+
recordSkip("origin-backpressure");
|
|
18138
|
+
emitStatus("page_skipped", {
|
|
18139
|
+
url: task.url,
|
|
18140
|
+
depth: task.depth,
|
|
18141
|
+
reason: "origin-backpressure"
|
|
18142
|
+
});
|
|
18143
|
+
if (limited === "stop") {
|
|
18144
|
+
let left = 0;
|
|
18145
|
+
while (queue.take()) left += 1;
|
|
18146
|
+
recordSkips("origin-backpressure", left);
|
|
18147
|
+
rateLimit.skippedUrls += left;
|
|
18148
|
+
}
|
|
18149
|
+
continue;
|
|
18150
|
+
}
|
|
17940
18151
|
requests.push(request);
|
|
17941
18152
|
if (healthStrategy) {
|
|
17942
18153
|
if (request.outcome === "response" && request.status < 400 && !request.accessBlock) {
|
|
17943
18154
|
healthyProbeStreak += 1;
|
|
17944
|
-
activeConcurrency = Math.min(config.concurrency, 1 + Math.floor(healthyProbeStreak / 20));
|
|
18155
|
+
activeConcurrency = Math.min(rateLimit.limited ? 1 : config.concurrency, 1 + Math.floor(healthyProbeStreak / 20));
|
|
17945
18156
|
} else {
|
|
17946
18157
|
healthyProbeStreak = 0;
|
|
17947
18158
|
activeConcurrency = 1;
|
|
@@ -18014,7 +18225,7 @@ async function crawlSite(input, dependencies) {
|
|
|
18014
18225
|
const storeDocument = (urls) => {
|
|
18015
18226
|
const sourceRequest = normalizeUrl$1(task.url, config.url) ?? task.url;
|
|
18016
18227
|
const directlyRequested = sourceRequest === finalUrl;
|
|
18017
|
-
const normalizedLinks = urls.map((url) => normalizeUrl$1(url, finalUrl)).filter((url) => Boolean(url));
|
|
18228
|
+
const normalizedLinks = urls.map((url) => normalizeUrl$1(url, finalUrl)).filter((url) => Boolean(url) && !isCdnUtilityUrl(url ?? ""));
|
|
18018
18229
|
const syncLinkEvidence = (target, links) => {
|
|
18019
18230
|
const sortedLinks = [...links].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
18020
18231
|
target.outgoingInternalCount = sortedLinks.length;
|
|
@@ -18093,7 +18304,7 @@ async function crawlSite(input, dependencies) {
|
|
|
18093
18304
|
if (followLinks && task.depth < config.maxDepth) {
|
|
18094
18305
|
if (memoryLimited) for (const value of result.urls) {
|
|
18095
18306
|
const normalized = normalizeUrl$1(value, finalUrl);
|
|
18096
|
-
if (normalized && sameOrigin(normalized, origin) && !isLikelyAsset(normalized) && passesFilters(normalized, config.include, config.exclude) && !visited.has(normalized) && !queue.has(normalized)) {
|
|
18307
|
+
if (normalized && sameOrigin(normalized, origin) && !isCdnUtilityUrl(normalized) && !isLikelyAsset(normalized) && passesFilters(normalized, config.include, config.exclude) && !visited.has(normalized) && !queue.has(normalized)) {
|
|
18097
18308
|
discovered.add(normalized);
|
|
18098
18309
|
memoryDeferredUrls.add(normalized);
|
|
18099
18310
|
}
|
|
@@ -18118,6 +18329,8 @@ async function crawlSite(input, dependencies) {
|
|
|
18118
18329
|
cancelled = isCancelled();
|
|
18119
18330
|
const robotsDisallowed = skipReasonCounts["robots-disallowed"] ?? 0;
|
|
18120
18331
|
if (robotsDisallowed > 0) warnings.push(`${robotsDisallowed} URL${robotsDisallowed === 1 ? "" : "s"} disallowed ${SEO_CRAWLER_USER_AGENT} in robots.txt. If the audit should reach them, allow the SEO-Skill token on only those public paths.`);
|
|
18332
|
+
const rateLimitWarning = rateLimit.warning();
|
|
18333
|
+
if (rateLimitWarning) warnings.push(rateLimitWarning);
|
|
18121
18334
|
if (cancelled) {
|
|
18122
18335
|
warnings.push("Crawl cancelled; returning a partial report.");
|
|
18123
18336
|
emitStatus("cancelled", {
|
|
@@ -18129,7 +18342,7 @@ async function crawlSite(input, dependencies) {
|
|
|
18129
18342
|
const sitemapEvidencePartial = sitemapDiscovery?.dataStatus === "partial";
|
|
18130
18343
|
const accessEvidencePartial = requests.some((request) => request.outcome === "response" && request.accessBlock);
|
|
18131
18344
|
const requiredSitemapUnavailable = config.mode === "sitemap" && sitemapDiscovery?.dataStatus === "unavailable";
|
|
18132
|
-
const partial = cancelled || failedUrls > 0 || pageLimitReached || robotsDeferredUrls > 0 || originBackpressureSkippedUrls > 0 || queueSafetySkippedUrls > 0 || queue.size > 0 || inFlight.size > 0 || stoppedForMemory || sitemapEvidencePartial || accessEvidencePartial || requiredSitemapUnavailable;
|
|
18345
|
+
const partial = cancelled || failedUrls > 0 || pageLimitReached || robotsDeferredUrls > 0 || originBackpressureSkippedUrls > 0 || rateLimit.skippedUrls > 0 || queueSafetySkippedUrls > 0 || queue.size > 0 || inFlight.size > 0 || stoppedForMemory || sitemapEvidencePartial || accessEvidencePartial || requiredSitemapUnavailable;
|
|
18133
18346
|
const failed = !cancelled && pages.length === 0 && (requiredSitemapUnavailable || requests.length > 0 && requests.every((request) => request.outcome === "failure"));
|
|
18134
18347
|
const requestEvidenceStatus = cancelled && inFlight.size > 0 ? "partial" : "available";
|
|
18135
18348
|
const dataSources = await crawlDataSources({
|
|
@@ -18176,7 +18389,10 @@ async function crawlSite(input, dependencies) {
|
|
|
18176
18389
|
timeoutMs: config.timeoutMs,
|
|
18177
18390
|
fetchStatusPage: deps.fetchStatusPage,
|
|
18178
18391
|
respectRobots: config.respectRobots,
|
|
18179
|
-
signal
|
|
18392
|
+
signal,
|
|
18393
|
+
rate: rateLimit.rate,
|
|
18394
|
+
sleep: deps.sleep,
|
|
18395
|
+
retryDelayMs: rateLimit.settleMs
|
|
18180
18396
|
}) : void 0;
|
|
18181
18397
|
for (const page of pages) {
|
|
18182
18398
|
if (!page.geo || !llmsTxt) continue;
|
|
@@ -18245,6 +18461,8 @@ async function crawlSite(input, dependencies) {
|
|
|
18245
18461
|
queueSafetySkippedUrls,
|
|
18246
18462
|
queueSafetyInventoryCapped,
|
|
18247
18463
|
originBackpressureSkippedUrls,
|
|
18464
|
+
rateLimit: rateLimit.caveatInput(config, pages.length),
|
|
18465
|
+
sitemapLimit: sitemapLimitCaveatInput(config, sitemapDiscovery, input.maxPages === void 0),
|
|
18248
18466
|
memoryPressureSkippedUrls,
|
|
18249
18467
|
queryVariantSkippedUrls: queryVariantSkipped.size,
|
|
18250
18468
|
renderingFallbackUrls,
|
|
@@ -32200,6 +32418,31 @@ async function dataForSeoRows(keys, input, spent) {
|
|
|
32200
32418
|
}
|
|
32201
32419
|
const GAP_DOMAINS = 1e3;
|
|
32202
32420
|
const GAP_COMPETITORS = 5;
|
|
32421
|
+
const SHORTENERS = [
|
|
32422
|
+
"bit.ly",
|
|
32423
|
+
"tinyurl.com",
|
|
32424
|
+
"t.co",
|
|
32425
|
+
"ow.ly",
|
|
32426
|
+
"buff.ly",
|
|
32427
|
+
"is.gd",
|
|
32428
|
+
"cutt.ly",
|
|
32429
|
+
"rebrand.ly",
|
|
32430
|
+
"shorturl.at",
|
|
32431
|
+
"rb.gy",
|
|
32432
|
+
"lnkd.in"
|
|
32433
|
+
];
|
|
32434
|
+
const FREE_APP_HOSTS = [
|
|
32435
|
+
"web.app",
|
|
32436
|
+
"firebaseapp.com",
|
|
32437
|
+
"pages.dev"
|
|
32438
|
+
];
|
|
32439
|
+
const LINK_LIST_NAME = /(subdomain|whois|login|signin|coupon|promo|voucher|discount|sitelike|similarsite|sitesimilar|siteprice|sitevalue|siteworth|websiteworth|hypestat|statshow|websiteoutlook|siteinfo|domaininfo|domainstat|domaintools|ipaddress|dnslookup|linkdirectory|webdirectory|alternativeto-)/;
|
|
32440
|
+
function isLinkList(domain) {
|
|
32441
|
+
const within = (name) => domain === name || domain.endsWith(`.${name}`);
|
|
32442
|
+
if (SHORTENERS.some(within)) return true;
|
|
32443
|
+
if (FREE_APP_HOSTS.some((name) => domain.endsWith(`.${name}`))) return true;
|
|
32444
|
+
return LINK_LIST_NAME.test(domain.replace(/[-.]/g, ""));
|
|
32445
|
+
}
|
|
32203
32446
|
async function linkGap(provider, headers, input) {
|
|
32204
32447
|
const target = bareTarget(input.target).replace(/^www\./, "");
|
|
32205
32448
|
const competitors = [...new Set((input.competitors ?? []).map((site) => bareTarget(site).replace(/^www\./, "")))].filter((site) => site && site !== target);
|
|
@@ -32240,8 +32483,10 @@ async function linkGap(provider, headers, input) {
|
|
|
32240
32483
|
found.set(domain, entry);
|
|
32241
32484
|
}
|
|
32242
32485
|
});
|
|
32486
|
+
const gap = [...found.entries()].filter(([, entry]) => entry.sites.length >= 2 && (floor === null || entry.value >= floor));
|
|
32487
|
+
const linkLists = gap.map(([domain]) => domain).filter(isLinkList).sort((a, b) => a.localeCompare(b));
|
|
32243
32488
|
return {
|
|
32244
|
-
rows:
|
|
32489
|
+
rows: gap.filter(([domain]) => !isLinkList(domain)).sort(([a, left], [b, right]) => right.value - left.value || right.sites.length - left.sites.length || a.localeCompare(b)).map(([domain, entry]) => ({
|
|
32245
32490
|
domain,
|
|
32246
32491
|
[score]: entry.value,
|
|
32247
32492
|
competitors_linking: entry.sites.length,
|
|
@@ -32254,7 +32499,8 @@ async function linkGap(provider, headers, input) {
|
|
|
32254
32499
|
...floor !== null ? { floor: {
|
|
32255
32500
|
field: score,
|
|
32256
32501
|
value: floor
|
|
32257
|
-
} } : {}
|
|
32502
|
+
} } : {},
|
|
32503
|
+
...linkLists.length ? { leftOut: linkLists } : {}
|
|
32258
32504
|
};
|
|
32259
32505
|
}
|
|
32260
32506
|
function isPage(target) {
|
|
@@ -32621,6 +32867,7 @@ async function runAiAnswers(input, headers, deps, more) {
|
|
|
32621
32867
|
question,
|
|
32622
32868
|
impressions,
|
|
32623
32869
|
task: null,
|
|
32870
|
+
ref: null,
|
|
32624
32871
|
cost_usd: 0,
|
|
32625
32872
|
...empty
|
|
32626
32873
|
});
|
|
@@ -32632,7 +32879,8 @@ async function runAiAnswers(input, headers, deps, more) {
|
|
|
32632
32879
|
...blank("", null),
|
|
32633
32880
|
engine: engine.name,
|
|
32634
32881
|
result: "pending",
|
|
32635
|
-
task: value
|
|
32882
|
+
task: value,
|
|
32883
|
+
ref: value
|
|
32636
32884
|
};
|
|
32637
32885
|
rows.push(row);
|
|
32638
32886
|
waiting.push({
|
|
@@ -32685,6 +32933,7 @@ async function runAiAnswers(input, headers, deps, more) {
|
|
|
32685
32933
|
}
|
|
32686
32934
|
row.result = "pending";
|
|
32687
32935
|
row.task = `${engine.name}:${task.id}`;
|
|
32936
|
+
row.ref = row.task;
|
|
32688
32937
|
waiting.push({
|
|
32689
32938
|
row,
|
|
32690
32939
|
engine,
|
|
@@ -32725,6 +32974,7 @@ async function runAiAnswers(input, headers, deps, more) {
|
|
|
32725
32974
|
const cost = usd(rows.reduce((sum, row) => sum + row.cost_usd, 0));
|
|
32726
32975
|
const count = (result) => rows.filter((row) => row.result === result).length;
|
|
32727
32976
|
const left = rows.filter((row) => row.task).map((row) => row.task);
|
|
32977
|
+
const refs = rows.flatMap((row) => row.ref ? [row.ref] : []);
|
|
32728
32978
|
const cites = /* @__PURE__ */ new Map();
|
|
32729
32979
|
for (const row of rows) for (const host of row.sources) cites.set(host, (cites.get(host) ?? 0) + 1);
|
|
32730
32980
|
const topCited = [...cites].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10);
|
|
@@ -32758,7 +33008,7 @@ async function runAiAnswers(input, headers, deps, more) {
|
|
|
32758
33008
|
limit: 50,
|
|
32759
33009
|
detail: input.detail,
|
|
32760
33010
|
notes,
|
|
32761
|
-
more
|
|
33011
|
+
more: (offset) => more(offset, refs)
|
|
32762
33012
|
}),
|
|
32763
33013
|
data: {
|
|
32764
33014
|
provider: "dataforseo",
|
|
@@ -32772,6 +33022,7 @@ async function runAiAnswers(input, headers, deps, more) {
|
|
|
32772
33022
|
questions: [...new Set(rows.map((row) => row.question))],
|
|
32773
33023
|
costUsd: cost,
|
|
32774
33024
|
pendingTasks: left,
|
|
33025
|
+
tasks: refs,
|
|
32775
33026
|
citedSites: topCited.map(([host, answers]) => ({
|
|
32776
33027
|
host,
|
|
32777
33028
|
answers
|
|
@@ -32891,7 +33142,7 @@ function ga4MatchReason(match, site) {
|
|
|
32891
33142
|
}
|
|
32892
33143
|
// packages/core/src/commands/analytics-site.ts
|
|
32893
33144
|
const STREAM_CHECKS = 30;
|
|
32894
|
-
const AT_ONCE = 5;
|
|
33145
|
+
const AT_ONCE$1 = 5;
|
|
32895
33146
|
const TIED = 3;
|
|
32896
33147
|
const hostOf$1 = (site) => {
|
|
32897
33148
|
const value = site.replace(/^sc-domain:/, "");
|
|
@@ -32956,8 +33207,8 @@ async function lookProperty(access, site) {
|
|
|
32956
33207
|
}
|
|
32957
33208
|
const order = ranked(site, candidates).slice(0, STREAM_CHECKS);
|
|
32958
33209
|
let failures = 0;
|
|
32959
|
-
for (let start = 0; start < order.length; start += AT_ONCE) {
|
|
32960
|
-
const batch = order.slice(start, start + AT_ONCE);
|
|
33210
|
+
for (let start = 0; start < order.length; start += AT_ONCE$1) {
|
|
33211
|
+
const batch = order.slice(start, start + AT_ONCE$1);
|
|
32961
33212
|
const streams = await Promise.all(batch.map((candidate) => listAnalyticsDataStreams(candidate.google, candidate.property, { signIn: access.signIn }).catch(() => {
|
|
32962
33213
|
failures += 1;
|
|
32963
33214
|
return [];
|
|
@@ -34090,7 +34341,8 @@ async function runClickyRows(input, keys, options) {
|
|
|
34090
34341
|
// packages/core/src/commands/domain-rating.ts
|
|
34091
34342
|
const DR_ATTRIBUTION = "domain_rating: Domain Rating by Ahrefs (https://ahrefs.com/).";
|
|
34092
34343
|
const ENDPOINT = "https://api.ahrefs.com/v3/public/domain-rating-free";
|
|
34093
|
-
const MAX_DOMAINS =
|
|
34344
|
+
const MAX_DOMAINS = 50;
|
|
34345
|
+
const AT_ONCE = 25;
|
|
34094
34346
|
const TTL_SECONDS = 86400;
|
|
34095
34347
|
const DOMAIN_FIELDS = [
|
|
34096
34348
|
"domain",
|
|
@@ -34121,23 +34373,26 @@ async function lookup(domain, key) {
|
|
|
34121
34373
|
}
|
|
34122
34374
|
return body.domain_rating?.domain_rating ?? null;
|
|
34123
34375
|
}
|
|
34124
|
-
async function domainRatings(domains, key) {
|
|
34376
|
+
async function domainRatings(domains, key, max = MAX_DOMAINS) {
|
|
34125
34377
|
const found = /* @__PURE__ */ new Map();
|
|
34126
34378
|
if (!key) return found;
|
|
34127
|
-
const unique = [...new Set(domains.map(bareDomain).filter(Boolean))].slice(0,
|
|
34128
|
-
|
|
34129
|
-
|
|
34130
|
-
|
|
34131
|
-
|
|
34379
|
+
const unique = [...new Set(domains.map(bareDomain).filter(Boolean))].slice(0, max);
|
|
34380
|
+
for (let start = 0; start < unique.length; start += AT_ONCE) {
|
|
34381
|
+
const batch = unique.slice(start, start + AT_ONCE);
|
|
34382
|
+
const values = await Promise.all(batch.map((domain) => lookup(domain, key).catch(() => null)));
|
|
34383
|
+
batch.forEach((domain, index) => {
|
|
34384
|
+
found.set(domain, values[index] ?? null);
|
|
34385
|
+
});
|
|
34386
|
+
}
|
|
34132
34387
|
return found;
|
|
34133
34388
|
}
|
|
34134
|
-
async function withDomainRating(rows, key) {
|
|
34389
|
+
async function withDomainRating(rows, key, start = 0) {
|
|
34135
34390
|
const field = DOMAIN_FIELDS.find((name) => rows.some((row) => typeof row[name] === "string"));
|
|
34136
34391
|
if (!key || !field || rows.some((row) => "domain_rating" in row)) return {
|
|
34137
34392
|
rows,
|
|
34138
34393
|
added: false
|
|
34139
34394
|
};
|
|
34140
|
-
const ratings = await domainRatings(rows.map((row) => String(row[field] ?? "")), key);
|
|
34395
|
+
const ratings = await domainRatings(rows.slice(start).map((row) => String(row[field] ?? "")), key);
|
|
34141
34396
|
return {
|
|
34142
34397
|
rows: rows.map((row) => {
|
|
34143
34398
|
const next = {};
|
|
@@ -36173,7 +36428,7 @@ async function runResearch(input, keys, options) {
|
|
|
36173
36428
|
const offset = input.offset ?? 0;
|
|
36174
36429
|
const language = input.provider === "dataforseo" ? dataForSeoLanguage(country) : null;
|
|
36175
36430
|
if (input.action === "ai_answers") throw new CommandError("INVALID_INPUT", "ai_answers runs on its own.");
|
|
36176
|
-
const { rows: found, costUsd, partial, missed, noData, floor } = await research(input.provider, keys, {
|
|
36431
|
+
const { rows: found, costUsd, partial, missed, noData, floor, leftOut } = await research(input.provider, keys, {
|
|
36177
36432
|
action: input.action,
|
|
36178
36433
|
keywords: input.keywords ?? [],
|
|
36179
36434
|
target,
|
|
@@ -36187,7 +36442,8 @@ async function runResearch(input, keys, options) {
|
|
|
36187
36442
|
};
|
|
36188
36443
|
const unresolved = rows.filter((row) => row.url === null && row.url_hint !== void 0).length;
|
|
36189
36444
|
const drKey = keys.get("x-ahrefs-key") ?? options.drKey;
|
|
36190
|
-
const
|
|
36445
|
+
const byDr = input.action === "link_gap" && input.provider === "dataforseo" && drKey;
|
|
36446
|
+
const rated = byDr ? await sortedByDomainRating(rows, drKey) : await withDomainRating(rows, drKey, offset);
|
|
36191
36447
|
const targetDr = target ? (await domainRatings([target], drKey)).get(bareDomain(target)) : void 0;
|
|
36192
36448
|
const hasTargetDr = targetDr !== void 0 && targetDr !== null;
|
|
36193
36449
|
return {
|
|
@@ -36203,6 +36459,8 @@ async function runResearch(input, keys, options) {
|
|
|
36203
36459
|
...partial ? ["Partial: DataForSEO could not fetch some pages of results, so rows may be missing."] : [],
|
|
36204
36460
|
...missed.length ? [`Partial: ${input.provider} failed for ${missed.join(", ")}, so those keywords have no rows. Ask for them again.`] : [],
|
|
36205
36461
|
...noData.length ? [noDataNote(input, noData)] : [],
|
|
36462
|
+
...byDr ? [rows.length > GAP_RATED ? `Sorted by Domain Rating for the top ${GAP_RATED} rows by DataForSEO rank; the ${rows.length - GAP_RATED} rows after them follow by rank, with no Domain Rating.` : "Sorted by Domain Rating, then DataForSEO rank."] : [],
|
|
36463
|
+
...leftOut?.length ? [`Left out ${leftOut.length} link lists, URL shorteners and directories that link to most sites (a guess from the host name): ${leftOut.join(", ")}.`] : [],
|
|
36206
36464
|
...floor ? [`Cut at ${floor.field} ${floor.value}: at least one site has more than 1,000 referring domains, and only its top 1,000 were read, so domains below ${floor.field} ${floor.value} are left out rather than guessed.`] : [],
|
|
36207
36465
|
...unresolved ? [unresolvedNote(input, unresolved, rows.length)] : [],
|
|
36208
36466
|
...note ? [note] : [],
|
|
@@ -36226,7 +36484,8 @@ async function runResearch(input, keys, options) {
|
|
|
36226
36484
|
noDataKeywords: noData,
|
|
36227
36485
|
...input.action === "link_gap" ? {
|
|
36228
36486
|
competitors: input.competitors ?? [],
|
|
36229
|
-
cutAt: floor ?? null
|
|
36487
|
+
cutAt: floor ?? null,
|
|
36488
|
+
leftOut: leftOut ?? []
|
|
36230
36489
|
} : {},
|
|
36231
36490
|
total: rated.rows.length,
|
|
36232
36491
|
offset,
|
|
@@ -36242,6 +36501,24 @@ function noDataNote(input, keywords) {
|
|
|
36242
36501
|
function unresolvedNote(input, count, total) {
|
|
36243
36502
|
return `${count} of ${total} results have no exact page: ${input.provider} returned Google redirect links for them. Their domain and url_hint come from Google's breadcrumb, which is shortened.${input.provider === "dataforseo" ? " For exact pages, use provider ahrefs." : ""}`;
|
|
36244
36503
|
}
|
|
36504
|
+
const GAP_RATED = 200;
|
|
36505
|
+
async function sortedByDomainRating(rows, key) {
|
|
36506
|
+
const top = rows.slice(0, GAP_RATED);
|
|
36507
|
+
const ratings = await domainRatings(top.map((row) => String(row.domain ?? "")), key, GAP_RATED);
|
|
36508
|
+
const withDr = (row, rated) => {
|
|
36509
|
+
const { domain = null, ...rest } = row;
|
|
36510
|
+
return {
|
|
36511
|
+
domain,
|
|
36512
|
+
domain_rating: rated ? ratings.get(bareDomain(String(domain ?? ""))) ?? null : null,
|
|
36513
|
+
...rest
|
|
36514
|
+
};
|
|
36515
|
+
};
|
|
36516
|
+
const dr = (row) => Number(row.domain_rating ?? -1);
|
|
36517
|
+
return {
|
|
36518
|
+
rows: [...top.map((row) => withDr(row, true)).sort((a, b) => dr(b) - dr(a) || Number(b.rank ?? 0) - Number(a.rank ?? 0) || String(a.domain).localeCompare(String(b.domain))), ...rows.slice(GAP_RATED).map((row) => withDr(row, false))],
|
|
36519
|
+
added: true
|
|
36520
|
+
};
|
|
36521
|
+
}
|
|
36245
36522
|
// packages/core/src/commands/sitemap-urls.ts
|
|
36246
36523
|
const MAX_FILES = 20;
|
|
36247
36524
|
const MAX_URLS = 5e4;
|
|
@@ -37103,7 +37380,7 @@ const SHARED_TOOLS = [
|
|
|
37103
37380
|
offset: input.offset,
|
|
37104
37381
|
questions: input.keywords,
|
|
37105
37382
|
tasks: input.tasks
|
|
37106
|
-
}, await surface.keys(), surface.deps,
|
|
37383
|
+
}, await surface.keys(), surface.deps, (offset, tasks) => `seo_research with the same provider, action and target, tasks ${JSON.stringify(tasks)}, detail "rows", offset ${offset}. Passing the tasks reads these answers back at no cost; without them every question is asked and paid for again`);
|
|
37107
37384
|
}
|
|
37108
37385
|
return runResearch(input, await surface.keys(), {
|
|
37109
37386
|
drKey: surface.drKey,
|
|
@@ -39947,6 +40224,6 @@ function isoDate(daysAgo = 0) {
|
|
|
39947
40224
|
date.setUTCDate(date.getUTCDate() - daysAgo);
|
|
39948
40225
|
return date.toISOString().slice(0, 10);
|
|
39949
40226
|
}
|
|
39950
|
-
export { AHREFS_API_KEY_ENV, AHREFS_API_KEY_SECRET, AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, ANALYTICS_PROVIDER_IDS, AUDIT_PAGE_RULE_IDS, AhrefsClient, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_IMPRESSIONS, CANNIBAL_MINIMUM_PAGE_SHARE, CLICKY_SITEKEY_ENV, CLICKY_SITE_KEYS_SECRET, CLOUD_SIGN_IN, CLOUD_URL, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_MAX_PAGES, DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, DecisionModelError, DecisionRunner, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, JsonFileStorageAdapter, LINK_ROLES, LINT_MAX_FILES, LOCAL_BING_CONNECT, LOCAL_SIGN_IN, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MANAGED_PROVIDER_SECRET_NAMES, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, MAX_MAX_PAGES, MAX_SEARCH_CONSOLE_EXPORT_FILES, MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, PAGE_TYPES, PROVIDER_ERROR_CODES, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, SEARCH_INTENTS, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_PROVIDER_API_VERSION, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SITEMAP_FILE_URL_LIMIT, SITE_CHECK_MAX_ISSUES, SITE_CHECK_MAX_PAGES, STEP_CHANGE_RULES, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_FAILURE_CONTEXTS, TELEMETRY_FAILURE_REASONS, TELEMETRY_NOTICE, TELEMETRY_OPERATIONS, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, TYPESAFE_API_KEY_ENV, agentActionsView, aiReferralSourceForValue, aiReferralsReport, analyticsConnectionLabel, analyticsConnectionProviderId, analyticsConnectionSchema, analyticsProviderDetails, analyticsProviderSchema, analyzeAiReferralRows, analyzeCannibalImportRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, auditCrawlPages, auditCrawlRequests, auditPage, authStatus, bearerFetch, boundedSitemapInventory, buildAgentFindings, buildQueryClusterReportFromRows, buildTelemetryPayload, builtPagePath, cacheDb, cannibalDataStatus, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkBuiltSite, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, cloudWeek, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, ctrUnderperformersReportWith, decayClusterDrafts, decayComparisonRange, decayingReport, decayingReportWith, deleteAhrefsApiKey, deleteBingApiKey, deleteClickySiteKey, deleteDataForSeoCredentials, deleteManagedProviderSecrets, deleteProviderExtensionAccount, deleteProviderExtensionCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, describeInstalledProviderExtension, detectAgentName, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, finishCloudLogin, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, getAuthModeStatus, getCacheStats, getClientConfig, getDb, getPrivacySnapshot, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleJson, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoverageSignals, inferTemplateMovement, initializeTelemetry, inspectProviderPackage, inspectSearchConsoleUrl, inspectUrl, installProviderPackage, installedProviderPackageSchema, installedProviderPackagesSchema, internalLinksReport, internalLinksReportWith, interpretUpdateCorrelation, isBrandQuery, isNewerVersion, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, lintPaths, listAnalyticsAccounts, listAnalyticsDataStreams, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, localInternalLinksDependencies, localRefreshClicky, localResearchKeys, loginWithLoopback, makeRunId, matchGa4WebStreams, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, nodeCommandDeps, normalizeBrandText, normalizeClickySiteId, normalizeClickySiteKey, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, normalizeProviderExtensionAccount, normalizedResearchColumnName, notSignedIn, noteCacheWrite, openCloudLink, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, pendingCloudLogin, performanceAudit, performanceReportIsCacheable, plural, providerCapabilitySchema, providerConnectionFieldSchema, providerExactVersionSchema, providerExtensionIdSchema, providerIdSchema, providerIntegritySchema, providerKindSchema, providerPackageManifestSchema, providerPackageNameSchema, providerPreferenceSchema, providerRegistrationSchema, providerRequestJson, providerRequestText, pruneLogs, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, quickWinsReportWith, readAhrefsApiKey, readAllTokens, readBingApiKey, readBoundedResponseText, readClickySiteKey, readClickySiteKeys, readConfig, readDataForSeoCredentials, readInstalledProviderPackages, readJson, readJsonOrUndefined, readLintFiles, readOauthClient, readProviderExtensionAccount, readProviderExtensionCredentials, readProviderSecret, readTokens, recommendationForRule, reconcileExportPagesWithCrawl, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, registrableDomain, removeInstalledProviderPackage, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderWorkflowMarkdown, reportNarrative, requestHeaders, requirePageVerification, researchImportColumns, resetSeoData, resolveLocalSite, resolveSegmentRanges, resolveSeoCliPaths, reviewObservations, roundedPosition, runAnalyticsProviderLandingPages, runAnalyticsReport, runDoctor, runGa4Report, runProviderExtensionAction, runtimeForProvider, safeProviderAdapterError, saveInstalledProviderPackage, searchAnalyticsRows, searchUpdatesFromIncidents, secondPage, secondPageWith, segmentComparisonRange, segmentImpact, segmentRangeDays, seoErrorEnvelope, sessionSourceInListFilter, setActiveGoogleAccount, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteStartUrl, snapshotFetchedPage, startCloudLogin, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, typesafeDecisionModel, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updateNotice, updatePostmortemCsvFiles, updatePostmortemWorkflow, urlInspectionQuota, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, withRankingHistory, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeToCache, writeTokens, xRobotsDirectives };
|
|
40227
|
+
export { AHREFS_API_KEY_ENV, AHREFS_API_KEY_SECRET, AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, ANALYTICS_PROVIDER_IDS, AUDIT_PAGE_RULE_IDS, AhrefsClient, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_IMPRESSIONS, CANNIBAL_MINIMUM_PAGE_SHARE, CLICKY_SITEKEY_ENV, CLICKY_SITE_KEYS_SECRET, CLOUD_SIGN_IN, CLOUD_URL, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_MAX_PAGES, DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, DecisionModelError, DecisionRunner, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, JsonFileStorageAdapter, LINK_ROLES, LINT_MAX_FILES, LOCAL_BING_CONNECT, LOCAL_SIGN_IN, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MANAGED_PROVIDER_SECRET_NAMES, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, MAX_MAX_PAGES, MAX_SEARCH_CONSOLE_EXPORT_FILES, MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, PAGE_TYPES, PROVIDER_ERROR_CODES, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, SEARCH_INTENTS, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_PROVIDER_API_VERSION, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SITEMAP_FILE_URL_LIMIT, SITE_CHECK_MAX_ISSUES, SITE_CHECK_MAX_PAGES, STEP_CHANGE_RULES, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_FAILURE_CONTEXTS, TELEMETRY_FAILURE_REASONS, TELEMETRY_NOTICE, TELEMETRY_OPERATIONS, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, TYPESAFE_API_KEY_ENV, agentActionsView, aiReferralSourceForValue, aiReferralsReport, analyticsConnectionLabel, analyticsConnectionProviderId, analyticsConnectionSchema, analyticsProviderDetails, analyticsProviderSchema, analyzeAiReferralRows, analyzeCannibalImportRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, auditCrawlPages, auditCrawlRequests, auditPage, authStatus, bearerFetch, boundedSitemapInventory, buildAgentFindings, buildQueryClusterReportFromRows, buildTelemetryPayload, builtPagePath, cacheDb, cannibalDataStatus, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkBuiltSite, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, cloudWeek, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlCoverageCaveat, crawlDefinitionId, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, ctrUnderperformersReportWith, decayClusterDrafts, decayComparisonRange, decayingReport, decayingReportWith, deleteAhrefsApiKey, deleteBingApiKey, deleteClickySiteKey, deleteDataForSeoCredentials, deleteManagedProviderSecrets, deleteProviderExtensionAccount, deleteProviderExtensionCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, describeInstalledProviderExtension, detectAgentName, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, finishCloudLogin, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, getAuthModeStatus, getCacheStats, getClientConfig, getDb, getPrivacySnapshot, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleJson, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoverageSignals, inferTemplateMovement, initializeTelemetry, inspectProviderPackage, inspectSearchConsoleUrl, inspectUrl, installProviderPackage, installedProviderPackageSchema, installedProviderPackagesSchema, internalLinksReport, internalLinksReportWith, interpretUpdateCorrelation, isBrandQuery, isNewerVersion, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, lintPaths, listAnalyticsAccounts, listAnalyticsDataStreams, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, localInternalLinksDependencies, localRefreshClicky, localResearchKeys, loginWithLoopback, makeRunId, matchGa4WebStreams, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, nodeCommandDeps, normalizeBrandText, normalizeClickySiteId, normalizeClickySiteKey, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, normalizeProviderExtensionAccount, normalizedResearchColumnName, notSignedIn, noteCacheWrite, openCloudLink, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, pendingCloudLogin, performanceAudit, performanceReportIsCacheable, plural, providerCapabilitySchema, providerConnectionFieldSchema, providerExactVersionSchema, providerExtensionIdSchema, providerIdSchema, providerIntegritySchema, providerKindSchema, providerPackageManifestSchema, providerPackageNameSchema, providerPreferenceSchema, providerRegistrationSchema, providerRequestJson, providerRequestText, pruneLogs, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, quickWinsReportWith, readAhrefsApiKey, readAllTokens, readBingApiKey, readBoundedResponseText, readClickySiteKey, readClickySiteKeys, readConfig, readDataForSeoCredentials, readInstalledProviderPackages, readJson, readJsonOrUndefined, readLintFiles, readOauthClient, readProviderExtensionAccount, readProviderExtensionCredentials, readProviderSecret, readTokens, recommendationForRule, reconcileExportPagesWithCrawl, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, registrableDomain, removeInstalledProviderPackage, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderWorkflowMarkdown, reportNarrative, requestHeaders, requirePageVerification, researchImportColumns, resetSeoData, resolveLocalSite, resolveSegmentRanges, resolveSeoCliPaths, reviewObservations, roundedPosition, runAnalyticsProviderLandingPages, runAnalyticsReport, runDoctor, runGa4Report, runProviderExtensionAction, runtimeForProvider, safeProviderAdapterError, saveInstalledProviderPackage, searchAnalyticsRows, searchUpdatesFromIncidents, secondPage, secondPageWith, segmentComparisonRange, segmentImpact, segmentRangeDays, seoErrorEnvelope, sessionSourceInListFilter, setActiveGoogleAccount, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteStartUrl, snapshotFetchedPage, startCloudLogin, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, typesafeDecisionModel, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updateNotice, updatePostmortemCsvFiles, updatePostmortemWorkflow, urlInspectionQuota, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, withRankingHistory, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeToCache, writeTokens, xRobotsDirectives };
|
|
39951
40228
|
|
|
39952
40229
|
//# sourceMappingURL=index.js.map
|