seo 0.2.20 → 0.2.22
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/README.md +34 -5
- package/dist/cli.js +115 -115
- package/dist/cli.js.map +1 -1
- package/dist/{dist-CtDv7OFQ.js → dist-DNsXiggv.js} +122 -122
- package/dist/dist-DNsXiggv.js.map +1 -0
- package/dist/{dist-DXAAiZSX.js → dist-DhQXrgg2.js} +1 -1
- package/dist/{esm-DvJj2-vq.js → esm-insIkRuo.js} +2 -2
- package/dist/{esm-DvJj2-vq.js.map → esm-insIkRuo.js.map} +1 -1
- package/dist/index.d.ts +828 -158
- package/dist/index.js +2327 -640
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +2501 -662
- package/dist/mcp.js.map +1 -1
- package/evals/link-evidence.json +13 -0
- package/evals/local-search-demand.json +41 -0
- package/package.json +1 -1
- package/skills/seo/SKILL.md +4 -3
- package/dist/dist-CtDv7OFQ.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-insIkRuo.js";
|
|
3
3
|
import { Headers, fetch as fetch$1 } from "undici";
|
|
4
4
|
import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { arch, homedir, platform, totalmem } from "node:os";
|
|
@@ -2249,10 +2249,10 @@ function aiReferralSourceForValue(value) {
|
|
|
2249
2249
|
}
|
|
2250
2250
|
//#endregion
|
|
2251
2251
|
//#region packages/core/src/analyze/ai-referrals-analysis.ts
|
|
2252
|
-
function compareText$
|
|
2252
|
+
function compareText$19(left, right) {
|
|
2253
2253
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
2254
2254
|
}
|
|
2255
|
-
function metric(row, name) {
|
|
2255
|
+
function metric$1(row, name) {
|
|
2256
2256
|
const value = row[name];
|
|
2257
2257
|
const parsed = Number(value);
|
|
2258
2258
|
if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
|
|
@@ -2287,8 +2287,8 @@ function analyzeAiReferralRows(input) {
|
|
|
2287
2287
|
...emptyMetrics()
|
|
2288
2288
|
};
|
|
2289
2289
|
addMetrics(total, {
|
|
2290
|
-
sessions: metric(row, "sessions"),
|
|
2291
|
-
eventCount: metric(row, "eventCount")
|
|
2290
|
+
sessions: metric$1(row, "sessions"),
|
|
2291
|
+
eventCount: metric$1(row, "eventCount")
|
|
2292
2292
|
});
|
|
2293
2293
|
total.observedSessionSources.add(row.sessionSource ?? "");
|
|
2294
2294
|
sourceTotals.set(definition.id, total);
|
|
@@ -2300,8 +2300,8 @@ function analyzeAiReferralRows(input) {
|
|
|
2300
2300
|
const definition = aiReferralSourceForValue(row.sessionSource ?? "");
|
|
2301
2301
|
if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
|
|
2302
2302
|
const metrics = {
|
|
2303
|
-
sessions: metric(row, "sessions"),
|
|
2304
|
-
eventCount: metric(row, "eventCount")
|
|
2303
|
+
sessions: metric$1(row, "sessions"),
|
|
2304
|
+
eventCount: metric$1(row, "eventCount")
|
|
2305
2305
|
};
|
|
2306
2306
|
addMetrics(detailTotal, metrics);
|
|
2307
2307
|
const landingPage = row.landingPagePlusQueryString || "(not set)";
|
|
@@ -2338,7 +2338,7 @@ function analyzeAiReferralRows(input) {
|
|
|
2338
2338
|
const generalCaveat = "Google Analytics only attributes visits that arrive with a recognized session source; some AI visits appear as direct, unassigned, or another source.";
|
|
2339
2339
|
const possiblyTruncated = input.sourceDiscovery.truncated || input.detail.truncated || input.totalUsersEvidence.truncated;
|
|
2340
2340
|
const allLandingPages = [...pageTotals.entries()].map(([landingPage, metrics]) => {
|
|
2341
|
-
const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$
|
|
2341
|
+
const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$19(left[0], right[0]))[0]?.[0];
|
|
2342
2342
|
const topSource = topSourceId ? sourceTotals.get(topSourceId)?.definition : void 0;
|
|
2343
2343
|
if (!topSource) throw new SeoError("PROVIDER_UNAVAILABLE", "Could not resolve the top AI referral source.");
|
|
2344
2344
|
return {
|
|
@@ -2352,7 +2352,7 @@ function analyzeAiReferralRows(input) {
|
|
|
2352
2352
|
label: topSource.label
|
|
2353
2353
|
}
|
|
2354
2354
|
};
|
|
2355
|
-
}).sort((left, right) => right.sessions - left.sessions || compareText$
|
|
2355
|
+
}).sort((left, right) => right.sessions - left.sessions || compareText$19(left.landingPage, right.landingPage));
|
|
2356
2356
|
const landingPages = allLandingPages.slice(0, input.resultLimit);
|
|
2357
2357
|
const selection = {
|
|
2358
2358
|
limit: input.resultLimit,
|
|
@@ -2397,19 +2397,19 @@ function analyzeAiReferralRows(input) {
|
|
|
2397
2397
|
id: source.definition.id,
|
|
2398
2398
|
label: source.definition.label,
|
|
2399
2399
|
source: source.definition.label,
|
|
2400
|
-
observedSessionSources: [...source.observedSessionSources].sort(compareText$
|
|
2400
|
+
observedSessionSources: [...source.observedSessionSources].sort(compareText$19),
|
|
2401
2401
|
sessions: source.sessions,
|
|
2402
2402
|
eventCount: source.eventCount,
|
|
2403
2403
|
totalUsers: null,
|
|
2404
2404
|
shareOfAiSessions: total.sessions ? source.sessions / total.sessions : 0,
|
|
2405
2405
|
share: total.sessions ? source.sessions / total.sessions : 0
|
|
2406
|
-
})).sort((left, right) => right.sessions - left.sessions || compareText$
|
|
2406
|
+
})).sort((left, right) => right.sessions - left.sessions || compareText$19(left.id, right.id)),
|
|
2407
2407
|
landingPages,
|
|
2408
2408
|
daily: [...dailyTotals.entries()].map(([date, metrics]) => ({
|
|
2409
2409
|
date,
|
|
2410
2410
|
...metrics,
|
|
2411
2411
|
totalUsers: null
|
|
2412
|
-
})).sort((left, right) => compareText$
|
|
2412
|
+
})).sort((left, right) => compareText$19(left.date, right.date)),
|
|
2413
2413
|
caveats: [
|
|
2414
2414
|
generalCaveat,
|
|
2415
2415
|
"Attribution uses Google Analytics sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
|
|
@@ -2499,7 +2499,7 @@ function sessionSourceInListFilter(values) {
|
|
|
2499
2499
|
//#endregion
|
|
2500
2500
|
//#region packages/core/src/analyze/ai-referrals.ts
|
|
2501
2501
|
const DEFAULT_MAX_ROWS$1 = 1e5;
|
|
2502
|
-
const MAX_ROWS = 1e5;
|
|
2502
|
+
const MAX_ROWS$1 = 1e5;
|
|
2503
2503
|
const DEFAULT_RESULT_LIMIT = 25;
|
|
2504
2504
|
const MAX_RESULT_LIMIT = 1e3;
|
|
2505
2505
|
function validIsoDate$1(value) {
|
|
@@ -2525,7 +2525,7 @@ function reportRange(input) {
|
|
|
2525
2525
|
function maxRows(input) {
|
|
2526
2526
|
if (input.maxRows !== void 0 && input.limit !== void 0 && input.maxRows !== input.limit) throw new SeoError("INVALID_INPUT", "Use maxRows or the legacy limit option, not both.");
|
|
2527
2527
|
const value = input.maxRows ?? input.limit ?? DEFAULT_MAX_ROWS$1;
|
|
2528
|
-
if (!Number.isInteger(value) || value < 1 || value > MAX_ROWS) throw new SeoError("INVALID_INPUT", `maxRows must be a whole number between 1 and ${MAX_ROWS}.`);
|
|
2528
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_ROWS$1) throw new SeoError("INVALID_INPUT", `maxRows must be a whole number between 1 and ${MAX_ROWS$1}.`);
|
|
2529
2529
|
return value;
|
|
2530
2530
|
}
|
|
2531
2531
|
function resultLimit(input) {
|
|
@@ -5775,7 +5775,7 @@ const AUDIT_PAGE_RULE_IDS = [
|
|
|
5775
5775
|
"canonical_outside_head",
|
|
5776
5776
|
"canonical_mismatch"
|
|
5777
5777
|
];
|
|
5778
|
-
const defaultDependencies$
|
|
5778
|
+
const defaultDependencies$17 = {
|
|
5779
5779
|
fetchPage,
|
|
5780
5780
|
queryPageMetrics,
|
|
5781
5781
|
now: () => /* @__PURE__ */ new Date()
|
|
@@ -5806,7 +5806,7 @@ function buildRecommendations(report) {
|
|
|
5806
5806
|
});
|
|
5807
5807
|
return recommendations;
|
|
5808
5808
|
}
|
|
5809
|
-
async function auditPage(input, dependencies = defaultDependencies$
|
|
5809
|
+
async function auditPage(input, dependencies = defaultDependencies$17) {
|
|
5810
5810
|
const fetched = await dependencies.fetchPage(input.url, {
|
|
5811
5811
|
js: input.js,
|
|
5812
5812
|
refresh: input.refresh
|
|
@@ -5948,7 +5948,7 @@ function tokenStem(token) {
|
|
|
5948
5948
|
function tokensMatch(a, b) {
|
|
5949
5949
|
return a === b || tokenStem(a) === tokenStem(b);
|
|
5950
5950
|
}
|
|
5951
|
-
function includesTokenSequence(queryTokens, termTokens) {
|
|
5951
|
+
function includesTokenSequence$1(queryTokens, termTokens) {
|
|
5952
5952
|
if (!termTokens.length || termTokens.length > queryTokens.length) return false;
|
|
5953
5953
|
for (let index = 0; index <= queryTokens.length - termTokens.length; index++) if (termTokens.every((termToken, offset) => tokensMatch(queryTokens[index + offset] ?? "", termToken))) return true;
|
|
5954
5954
|
return false;
|
|
@@ -5977,7 +5977,7 @@ function isBrandQuery(query, brandTerms) {
|
|
|
5977
5977
|
const compactMatches = (compactTermLength >= 6 || /[^\p{ASCII}]/u.test(compactTerm) && compactTermLength >= 2) && compactQuery.includes(compactTerm);
|
|
5978
5978
|
const termTokens = normalizedTerm.split(" ");
|
|
5979
5979
|
if (termTokens.length === 1) return queryTokens.some((queryToken) => tokensMatch(queryToken, normalizedTerm)) || compactMatches;
|
|
5980
|
-
return includesTokenSequence(queryTokens, termTokens) || compactMatches;
|
|
5980
|
+
return includesTokenSequence$1(queryTokens, termTokens) || compactMatches;
|
|
5981
5981
|
});
|
|
5982
5982
|
}
|
|
5983
5983
|
function shouldExcludeBrandQuery(input) {
|
|
@@ -6289,7 +6289,7 @@ function resolveQueryOpportunityInput(input, now) {
|
|
|
6289
6289
|
}
|
|
6290
6290
|
//#endregion
|
|
6291
6291
|
//#region packages/core/src/analyze/query-opportunity-rows.ts
|
|
6292
|
-
function compareText$
|
|
6292
|
+
function compareText$18(left, right) {
|
|
6293
6293
|
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
|
|
6294
6294
|
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
|
|
6295
6295
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -6299,9 +6299,9 @@ function compareText$17(left, right) {
|
|
|
6299
6299
|
return leftPoints.length - rightPoints.length;
|
|
6300
6300
|
}
|
|
6301
6301
|
function compareQueryOpportunityRows(left, right) {
|
|
6302
|
-
return right.impressions - left.impressions || right.clicks - left.clicks || left.position - right.position || compareText$
|
|
6302
|
+
return right.impressions - left.impressions || right.clicks - left.clicks || left.position - right.position || compareText$18(left.query, right.query);
|
|
6303
6303
|
}
|
|
6304
|
-
function validRow$
|
|
6304
|
+
function validRow$4(row, query) {
|
|
6305
6305
|
return row.keys.length === 1 && Boolean(query) && Number.isFinite(row.clicks) && Number.isFinite(row.impressions) && Number.isFinite(row.ctr) && Number.isFinite(row.position) && row.clicks >= 0 && row.impressions > 0 && row.clicks <= row.impressions && row.ctr >= 0 && row.ctr <= 1 && row.position > 0;
|
|
6306
6306
|
}
|
|
6307
6307
|
function signature(row) {
|
|
@@ -6311,7 +6311,7 @@ function normalizeQueryOpportunityRows(rows, selection) {
|
|
|
6311
6311
|
const grouped = /* @__PURE__ */ new Map();
|
|
6312
6312
|
for (const row of rows) {
|
|
6313
6313
|
const query = row.keys[0]?.trim() ?? "";
|
|
6314
|
-
if (!validRow$
|
|
6314
|
+
if (!validRow$4(row, query)) {
|
|
6315
6315
|
selection.invalidRows++;
|
|
6316
6316
|
continue;
|
|
6317
6317
|
}
|
|
@@ -7323,11 +7323,11 @@ function selectBenchmarkRows$1(input) {
|
|
|
7323
7323
|
}
|
|
7324
7324
|
//#endregion
|
|
7325
7325
|
//#region packages/core/src/analyze/page-opportunities-analysis.ts
|
|
7326
|
-
function compareText$
|
|
7326
|
+
function compareText$17(left, right) {
|
|
7327
7327
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
7328
7328
|
}
|
|
7329
7329
|
function compareItems$2(left, right) {
|
|
7330
|
-
return (right.estimatedCtrClickShortfall ?? -1) - (left.estimatedCtrClickShortfall ?? -1) || right.impressions - left.impressions || left.position - right.position || compareText$
|
|
7330
|
+
return (right.estimatedCtrClickShortfall ?? -1) - (left.estimatedCtrClickShortfall ?? -1) || right.impressions - left.impressions || left.position - right.position || compareText$17(left.query, right.query) || compareText$17(left.url, right.url);
|
|
7331
7331
|
}
|
|
7332
7332
|
function analyzeRow(input) {
|
|
7333
7333
|
const query = input.row.keys[0]?.trim() ?? "";
|
|
@@ -7489,7 +7489,7 @@ function pageOpportunityRecommendations(analysis) {
|
|
|
7489
7489
|
//#endregion
|
|
7490
7490
|
//#region packages/core/src/analyze/page-opportunities.ts
|
|
7491
7491
|
const MAX_BENCHMARK_ROWS = 1e5;
|
|
7492
|
-
const defaultDependencies$
|
|
7492
|
+
const defaultDependencies$16 = {
|
|
7493
7493
|
searchAnalytics: querySearchAnalytics,
|
|
7494
7494
|
fetch: fetchPage,
|
|
7495
7495
|
extract: extractPage,
|
|
@@ -7561,7 +7561,7 @@ function reportCaveats$1(input) {
|
|
|
7561
7561
|
input.benchmarkTruncated ? `The site-wide benchmark reached its ${MAX_BENCHMARK_ROWS.toLocaleString("en-US")}-row cap, so provenance is complete but the peer sample is truncated.` : ""
|
|
7562
7562
|
].filter(Boolean);
|
|
7563
7563
|
}
|
|
7564
|
-
async function pageOpportunitiesReport(input, dependencies = defaultDependencies$
|
|
7564
|
+
async function pageOpportunitiesReport(input, dependencies = defaultDependencies$16) {
|
|
7565
7565
|
const url = assertUrlMatchesGscProperty(input.site, input.url);
|
|
7566
7566
|
const days = reportDays$1(input.days);
|
|
7567
7567
|
const range = defaultDateRange$1(days);
|
|
@@ -11358,7 +11358,7 @@ function absoluteHttpUrl(value) {
|
|
|
11358
11358
|
return false;
|
|
11359
11359
|
}
|
|
11360
11360
|
}
|
|
11361
|
-
function urlKey(value) {
|
|
11361
|
+
function urlKey$1(value) {
|
|
11362
11362
|
if (!value) return void 0;
|
|
11363
11363
|
try {
|
|
11364
11364
|
const url = new URL(value);
|
|
@@ -11557,7 +11557,7 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
11557
11557
|
for (const page of pages) {
|
|
11558
11558
|
if (!isAuditableHtmlPage(page)) continue;
|
|
11559
11559
|
for (const value of [page.url, page.finalUrl]) {
|
|
11560
|
-
const key = urlKey(value);
|
|
11560
|
+
const key = urlKey$1(value);
|
|
11561
11561
|
if (key) pageByUrl.set(key, page);
|
|
11562
11562
|
}
|
|
11563
11563
|
}
|
|
@@ -11699,7 +11699,7 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
11699
11699
|
canonicalRaw: page.canonicalRaw,
|
|
11700
11700
|
canonical: page.canonical
|
|
11701
11701
|
}));
|
|
11702
|
-
const canonicalTarget = pageByUrl.get(urlKey(page.canonical) ?? "");
|
|
11702
|
+
const canonicalTarget = pageByUrl.get(urlKey$1(page.canonical) ?? "");
|
|
11703
11703
|
if (canonicalTarget?.canonical && !sameUrl(canonicalTarget.canonical, page.canonical)) issues.push(issue("canonical_chain", page, page.canonical, {
|
|
11704
11704
|
canonical: page.canonical,
|
|
11705
11705
|
nextCanonical: canonicalTarget.canonical,
|
|
@@ -11874,7 +11874,7 @@ const SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
|
11874
11874
|
"refresh-token"
|
|
11875
11875
|
]);
|
|
11876
11876
|
const SENSITIVE_PARAM_PATTERN = /^(access_?token|auth|api_?key|client_?secret|code|jwt|key|password|refresh_?token|secret|session|sig|signature|token)$/i;
|
|
11877
|
-
function uniqueSorted(values = []) {
|
|
11877
|
+
function uniqueSorted$1(values = []) {
|
|
11878
11878
|
return [...new Set(values.filter(Boolean))].sort();
|
|
11879
11879
|
}
|
|
11880
11880
|
function normalizeFetchRate(input, strategy) {
|
|
@@ -11894,13 +11894,13 @@ function normalizeCrawlConfig(input) {
|
|
|
11894
11894
|
mode: input.mode ?? (strategy === "health" ? "sitemap" : "site"),
|
|
11895
11895
|
strategy,
|
|
11896
11896
|
...input.sitemapUrl ? { sitemapUrl: new URL(input.sitemapUrl).toString() } : {},
|
|
11897
|
-
urls: uniqueSorted(input.urls ?? []),
|
|
11897
|
+
urls: uniqueSorted$1(input.urls ?? []),
|
|
11898
11898
|
maxPages: input.maxPages ?? 500,
|
|
11899
11899
|
maxDepth: input.maxDepth ?? 16,
|
|
11900
11900
|
concurrency,
|
|
11901
11901
|
timeoutMs: input.timeoutMs ?? 2e4,
|
|
11902
|
-
include: uniqueSorted(input.include ?? []),
|
|
11903
|
-
exclude: uniqueSorted(input.exclude ?? []),
|
|
11902
|
+
include: uniqueSorted$1(input.include ?? []),
|
|
11903
|
+
exclude: uniqueSorted$1(input.exclude ?? []),
|
|
11904
11904
|
respectRobots: input.respectRobots ?? true,
|
|
11905
11905
|
useSitemap: input.useSitemap ?? true,
|
|
11906
11906
|
checkExternal: strategy === "health" ? false : input.checkExternal ?? true,
|
|
@@ -12024,19 +12024,19 @@ function sanitizeIssues(issues) {
|
|
|
12024
12024
|
function sanitizeMessages(values = []) {
|
|
12025
12025
|
return values.map((value) => sanitizeTenantString(value));
|
|
12026
12026
|
}
|
|
12027
|
-
function compareText$
|
|
12027
|
+
function compareText$16(left, right) {
|
|
12028
12028
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
12029
12029
|
}
|
|
12030
12030
|
function sortPages(pages) {
|
|
12031
|
-
return [...pages].sort((left, right) => compareText$
|
|
12031
|
+
return [...pages].sort((left, right) => compareText$16(left.url, right.url));
|
|
12032
12032
|
}
|
|
12033
12033
|
function sortRequests(requests) {
|
|
12034
12034
|
return [...requests].sort((left, right) => {
|
|
12035
|
-
const byUrl = compareText$
|
|
12035
|
+
const byUrl = compareText$16(left.requestedUrl, right.requestedUrl);
|
|
12036
12036
|
if (byUrl) return byUrl;
|
|
12037
|
-
const byOutcome = compareText$
|
|
12037
|
+
const byOutcome = compareText$16(left.outcome, right.outcome);
|
|
12038
12038
|
if (byOutcome) return byOutcome;
|
|
12039
|
-
return compareText$
|
|
12039
|
+
return compareText$16(left.outcome === "response" ? `${left.status}:${left.finalUrl}` : left.outcome === "skipped" ? left.reason : `${left.failureKind}:${left.error}`, right.outcome === "response" ? `${right.status}:${right.finalUrl}` : right.outcome === "skipped" ? right.reason : `${right.failureKind}:${right.error}`);
|
|
12040
12040
|
});
|
|
12041
12041
|
}
|
|
12042
12042
|
function summarizeCrawlReport(input) {
|
|
@@ -12258,8 +12258,8 @@ function createCrawlReport(input) {
|
|
|
12258
12258
|
...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
|
|
12259
12259
|
...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
|
|
12260
12260
|
...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
|
|
12261
|
-
warnings: sanitizeMessages(input.warnings).sort(compareText$
|
|
12262
|
-
caveats: sanitizeMessages(input.caveats).sort(compareText$
|
|
12261
|
+
warnings: sanitizeMessages(input.warnings).sort(compareText$16),
|
|
12262
|
+
caveats: sanitizeMessages(input.caveats).sort(compareText$16)
|
|
12263
12263
|
};
|
|
12264
12264
|
}
|
|
12265
12265
|
function normalizeLoadedCrawlReport(report) {
|
|
@@ -12289,8 +12289,8 @@ function normalizeLoadedCrawlReport(report) {
|
|
|
12289
12289
|
pages,
|
|
12290
12290
|
issues,
|
|
12291
12291
|
issueGroups: groupCrawlIssues(issues),
|
|
12292
|
-
warnings: sanitizeMessages(report.warnings).sort(compareText$
|
|
12293
|
-
caveats: sanitizeMessages(report.caveats).sort(compareText$
|
|
12292
|
+
warnings: sanitizeMessages(report.warnings).sort(compareText$16),
|
|
12293
|
+
caveats: sanitizeMessages(report.caveats).sort(compareText$16)
|
|
12294
12294
|
};
|
|
12295
12295
|
}
|
|
12296
12296
|
//#endregion
|
|
@@ -14154,7 +14154,7 @@ async function fetchSitemapUrls(input) {
|
|
|
14154
14154
|
}
|
|
14155
14155
|
//#endregion
|
|
14156
14156
|
//#region packages/core/src/analyze/workflows/analytics-value.ts
|
|
14157
|
-
const defaultDependencies$
|
|
14157
|
+
const defaultDependencies$15 = { runGa4Report };
|
|
14158
14158
|
function normalizePath(value) {
|
|
14159
14159
|
if (!value || value === "(not set)") return "";
|
|
14160
14160
|
const [path = ""] = value.split("?");
|
|
@@ -14167,12 +14167,12 @@ function urlPath(url) {
|
|
|
14167
14167
|
return "";
|
|
14168
14168
|
}
|
|
14169
14169
|
}
|
|
14170
|
-
function compareText$
|
|
14170
|
+
function compareText$15(left, right) {
|
|
14171
14171
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
14172
14172
|
}
|
|
14173
14173
|
function landingPageValuesFromRows(rows) {
|
|
14174
14174
|
const orderedRows = [...rows].sort((left, right) => {
|
|
14175
|
-
return compareText$
|
|
14175
|
+
return compareText$15(normalizePath(left.landingPagePlusQueryString ?? ""), normalizePath(right.landingPagePlusQueryString ?? "")) || compareText$15(left.landingPagePlusQueryString ?? "", right.landingPagePlusQueryString ?? "") || compareText$15(left.sessions ?? "", right.sessions ?? "") || compareText$15(left.totalUsers ?? "", right.totalUsers ?? "") || compareText$15(left.conversions ?? "", right.conversions ?? "");
|
|
14176
14176
|
});
|
|
14177
14177
|
const values = /* @__PURE__ */ new Map();
|
|
14178
14178
|
for (const row of orderedRows) {
|
|
@@ -14214,7 +14214,7 @@ function landingPageRankingPolicy(input) {
|
|
|
14214
14214
|
warnings
|
|
14215
14215
|
};
|
|
14216
14216
|
}
|
|
14217
|
-
async function fetchLandingPageValues(input, dependencies = defaultDependencies$
|
|
14217
|
+
async function fetchLandingPageValues(input, dependencies = defaultDependencies$15) {
|
|
14218
14218
|
const retainedRowLimit = input.limit ?? 5e3;
|
|
14219
14219
|
if (!input.propertyId) return {
|
|
14220
14220
|
values: /* @__PURE__ */ new Map(),
|
|
@@ -14984,7 +14984,7 @@ async function crawlSite(input, dependencies) {
|
|
|
14984
14984
|
const REPORT_BASELINE_MAX_PAGES = 100;
|
|
14985
14985
|
const REPORT_BASELINE_MAX_DEPTH = 4;
|
|
14986
14986
|
const REPORT_BASELINE_MAX_AGE_MS = 10080 * 60 * 1e3;
|
|
14987
|
-
const defaultDependencies$
|
|
14987
|
+
const defaultDependencies$14 = {
|
|
14988
14988
|
latestCrawlReport,
|
|
14989
14989
|
crawlSite,
|
|
14990
14990
|
saveCrawlReport: (report) => saveCrawlReport(report, crawlReportStore, { retention: "baseline" }),
|
|
@@ -15033,7 +15033,7 @@ function crawlInput$1(input) {
|
|
|
15033
15033
|
refresh: input.refresh ?? false
|
|
15034
15034
|
};
|
|
15035
15035
|
}
|
|
15036
|
-
async function resolveTechnicalBaseline(input, dependencies = defaultDependencies$
|
|
15036
|
+
async function resolveTechnicalBaseline(input, dependencies = defaultDependencies$14) {
|
|
15037
15037
|
if (input.crawl === false) return {
|
|
15038
15038
|
status: "skipped",
|
|
15039
15039
|
reason: "Technical crawl evidence was skipped with --no-crawl."
|
|
@@ -16858,7 +16858,7 @@ function countLabel(count, singular, pluralLabel = `${singular}s`) {
|
|
|
16858
16858
|
//#region packages/core/src/analyze/crawler/okf.ts
|
|
16859
16859
|
const OKF_DEFAULT_CONCEPTS = 500;
|
|
16860
16860
|
const OKF_MAX_CONCEPTS = 5e3;
|
|
16861
|
-
function compareText$
|
|
16861
|
+
function compareText$14(left, right) {
|
|
16862
16862
|
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
|
|
16863
16863
|
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
|
|
16864
16864
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -16948,7 +16948,7 @@ function okfTitle(value, report) {
|
|
|
16948
16948
|
}
|
|
16949
16949
|
function selectedOkfPages(report, requestedLimit) {
|
|
16950
16950
|
const limit = okfConceptLimit(requestedLimit);
|
|
16951
|
-
const eligible = report.pages.filter((page) => page.indexable && page.status >= 200 && page.status < 300 && !page.blocked && !page.error).sort((left, right) => (right.searchMetrics?.clicks ?? 0) - (left.searchMetrics?.clicks ?? 0) || (right.searchMetrics?.impressions ?? 0) - (left.searchMetrics?.impressions ?? 0) || (right.internalLinkAuthorityScore ?? 0) - (left.internalLinkAuthorityScore ?? 0) || (right.internalInlinkCount ?? 0) - (left.internalInlinkCount ?? 0) || compareText$
|
|
16951
|
+
const eligible = report.pages.filter((page) => page.indexable && page.status >= 200 && page.status < 300 && !page.blocked && !page.error).sort((left, right) => (right.searchMetrics?.clicks ?? 0) - (left.searchMetrics?.clicks ?? 0) || (right.searchMetrics?.impressions ?? 0) - (left.searchMetrics?.impressions ?? 0) || (right.internalLinkAuthorityScore ?? 0) - (left.internalLinkAuthorityScore ?? 0) || (right.internalInlinkCount ?? 0) - (left.internalInlinkCount ?? 0) || compareText$14(left.finalUrl, right.finalUrl));
|
|
16952
16952
|
const unique = /* @__PURE__ */ new Map();
|
|
16953
16953
|
for (const page of eligible) if (!unique.has(page.finalUrl)) unique.set(page.finalUrl, page);
|
|
16954
16954
|
const uniquePages = [...unique.values()];
|
|
@@ -18446,7 +18446,7 @@ const CTR_DEFAULT_MIN_IMPRESSIONS = 200;
|
|
|
18446
18446
|
const CTR_MAX_MIN_IMPRESSIONS = 1e9;
|
|
18447
18447
|
const CTR_DEFAULT_LIMIT = 25;
|
|
18448
18448
|
const CTR_MAX_LIMIT = 100;
|
|
18449
|
-
function compareText$
|
|
18449
|
+
function compareText$13(left, right) {
|
|
18450
18450
|
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
|
|
18451
18451
|
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
|
|
18452
18452
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -18455,7 +18455,7 @@ function compareText$12(left, right) {
|
|
|
18455
18455
|
}
|
|
18456
18456
|
return leftPoints.length - rightPoints.length;
|
|
18457
18457
|
}
|
|
18458
|
-
function boundedInteger$
|
|
18458
|
+
function boundedInteger$7(value, fallback, minimum, maximum) {
|
|
18459
18459
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
18460
18460
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
18461
18461
|
}
|
|
@@ -18494,7 +18494,7 @@ function aggregateCtrRows(rows, selection) {
|
|
|
18494
18494
|
weightedPosition: 0,
|
|
18495
18495
|
sourceRows: 0
|
|
18496
18496
|
};
|
|
18497
|
-
if (compareText$
|
|
18497
|
+
if (compareText$13(query, group.query) < 0) group.query = query;
|
|
18498
18498
|
group.clicks += row.clicks;
|
|
18499
18499
|
group.impressions += row.impressions;
|
|
18500
18500
|
group.weightedPosition += row.position * row.impressions;
|
|
@@ -18532,8 +18532,8 @@ function initialSelection(sourceRows) {
|
|
|
18532
18532
|
};
|
|
18533
18533
|
}
|
|
18534
18534
|
function analyzeCtrUnderperformersFromRows(input) {
|
|
18535
|
-
const minImpressions = boundedInteger$
|
|
18536
|
-
const limit = boundedInteger$
|
|
18535
|
+
const minImpressions = boundedInteger$7(input.minImpressions, 200, 1, CTR_MAX_MIN_IMPRESSIONS);
|
|
18536
|
+
const limit = boundedInteger$7(input.limit, 25, 1, 100);
|
|
18537
18537
|
const selection = initialSelection(input.rows.length);
|
|
18538
18538
|
const benchmarkRows = aggregateCtrRows(input.rows, selection).filter((row) => {
|
|
18539
18539
|
const query = row.keys[0] ?? "";
|
|
@@ -18598,7 +18598,7 @@ function analyzeCtrUnderperformersFromRows(input) {
|
|
|
18598
18598
|
confidence: "low"
|
|
18599
18599
|
}
|
|
18600
18600
|
};
|
|
18601
|
-
}).filter((item) => item.actualCtr < item.expectedCtr * .6).sort((left, right) => right.clickShortfall - left.clickShortfall || compareText$
|
|
18601
|
+
}).filter((item) => item.actualCtr < item.expectedCtr * .6).sort((left, right) => right.clickShortfall - left.clickShortfall || compareText$13(left.query, right.query) || compareText$13(left.url, right.url));
|
|
18602
18602
|
const items = eligibleItems.slice(0, limit);
|
|
18603
18603
|
selection.eligibleUnderperformers = eligibleItems.length;
|
|
18604
18604
|
selection.returnedUnderperformers = items.length;
|
|
@@ -18614,15 +18614,15 @@ function analyzeCtrUnderperformersFromRows(input) {
|
|
|
18614
18614
|
}
|
|
18615
18615
|
//#endregion
|
|
18616
18616
|
//#region packages/core/src/analyze/ctr-underperformers.ts
|
|
18617
|
-
const MAX_GSC_ROWS$
|
|
18617
|
+
const MAX_GSC_ROWS$5 = 1e5;
|
|
18618
18618
|
function plural$4(count, singular, pluralLabel = `${singular}s`) {
|
|
18619
18619
|
return count === 1 ? singular : pluralLabel;
|
|
18620
18620
|
}
|
|
18621
|
-
const defaultDependencies$
|
|
18621
|
+
const defaultDependencies$13 = {
|
|
18622
18622
|
searchAnalytics: querySearchAnalytics,
|
|
18623
18623
|
now: () => /* @__PURE__ */ new Date()
|
|
18624
18624
|
};
|
|
18625
|
-
async function ctrUnderperformersReport(input, dependencies = defaultDependencies$
|
|
18625
|
+
async function ctrUnderperformersReport(input, dependencies = defaultDependencies$13) {
|
|
18626
18626
|
const minImpressions = integerOption$2({
|
|
18627
18627
|
value: input.minImpressions,
|
|
18628
18628
|
fallback: 200,
|
|
@@ -18644,7 +18644,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
|
|
|
18644
18644
|
dimensions: ["query", "page"],
|
|
18645
18645
|
type: "web",
|
|
18646
18646
|
dataState: "final",
|
|
18647
|
-
maxRows: MAX_GSC_ROWS$
|
|
18647
|
+
maxRows: MAX_GSC_ROWS$5
|
|
18648
18648
|
}, { refresh: input.refresh });
|
|
18649
18649
|
const { items, totalClickShortfall, returnedClickShortfall, selection } = analyzeCtrUnderperformersFromRows({
|
|
18650
18650
|
rows: source.rows,
|
|
@@ -18655,7 +18655,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
|
|
|
18655
18655
|
includeBrand: input.includeBrand
|
|
18656
18656
|
});
|
|
18657
18657
|
const top = items[0];
|
|
18658
|
-
const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$
|
|
18658
|
+
const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$5;
|
|
18659
18659
|
const partialValidation = selection.invalidRows > 0;
|
|
18660
18660
|
const dataStatus = selection.sourceRows === 0 ? "empty" : possiblyTruncated || partialValidation ? "partial" : selection.eligibleUnderperformers === 0 ? "filtered" : "complete";
|
|
18661
18661
|
const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
|
|
@@ -18672,7 +18672,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
|
|
|
18672
18672
|
dataState: "final",
|
|
18673
18673
|
rowsFetched: source.rowsFetched,
|
|
18674
18674
|
calls: source.calls,
|
|
18675
|
-
maxRows: MAX_GSC_ROWS$
|
|
18675
|
+
maxRows: MAX_GSC_ROWS$5,
|
|
18676
18676
|
possiblyTruncated,
|
|
18677
18677
|
completeness,
|
|
18678
18678
|
validation: {
|
|
@@ -18702,7 +18702,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
|
|
|
18702
18702
|
"CTR benchmarks and calculated click shortfalls are directional heuristics, not forecasts or proof that clicks are available. Validate the search intent and live SERP before editing.",
|
|
18703
18703
|
...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$4(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
|
|
18704
18704
|
...selection.duplicateRows ? [`${selection.duplicateRows} repeated query/page ${plural$4(selection.duplicateRows, "row")} ${selection.duplicateRows === 1 ? "was" : "were"} aggregated before CTR and average position were calculated.`] : [],
|
|
18705
|
-
...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$
|
|
18705
|
+
...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$5} was reached, so lower-ranked source rows may be absent.`] : [],
|
|
18706
18706
|
...selection.limitedUnderperformers ? [`${selection.limitedUnderperformers} eligible ${plural$4(selection.limitedUnderperformers, "underperformer")} ${selection.limitedUnderperformers === 1 ? "was" : "were"} omitted by the output limit.`] : []
|
|
18707
18707
|
],
|
|
18708
18708
|
recommendations: top ? [
|
|
@@ -18829,7 +18829,7 @@ function diagnosisDataStatus(input) {
|
|
|
18829
18829
|
function fixed$1(value) {
|
|
18830
18830
|
return Number(value.toFixed(3));
|
|
18831
18831
|
}
|
|
18832
|
-
function compareText$
|
|
18832
|
+
function compareText$12(left, right) {
|
|
18833
18833
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
18834
18834
|
}
|
|
18835
18835
|
function validMetric$2(value) {
|
|
@@ -18909,8 +18909,8 @@ function compareRetainedSegmentRows(input) {
|
|
|
18909
18909
|
else unmatched.push(unmatchedRow(row, "before"));
|
|
18910
18910
|
}
|
|
18911
18911
|
for (const [key, row] of after.rows) if (!before.rows.has(key)) unmatched.push(unmatchedRow(row, "after"));
|
|
18912
|
-
matched.sort((left, right) => Math.abs(right.clickDelta) - Math.abs(left.clickDelta) || compareText$
|
|
18913
|
-
unmatched.sort((left, right) => right.clicks - left.clicks || compareText$
|
|
18912
|
+
matched.sort((left, right) => Math.abs(right.clickDelta) - Math.abs(left.clickDelta) || compareText$12(left.key, right.key));
|
|
18913
|
+
unmatched.sort((left, right) => right.clicks - left.clicks || compareText$12(left.retainedIn, right.retainedIn) || compareText$12(left.key, right.key));
|
|
18914
18914
|
return {
|
|
18915
18915
|
before,
|
|
18916
18916
|
after,
|
|
@@ -19226,7 +19226,7 @@ function unavailableSegmentImpactReport(input) {
|
|
|
19226
19226
|
//#endregion
|
|
19227
19227
|
//#region packages/core/src/analyze/segment-impact.ts
|
|
19228
19228
|
const DEFAULT_MAX_ROWS = 1e5;
|
|
19229
|
-
const defaultDependencies$
|
|
19229
|
+
const defaultDependencies$12 = {
|
|
19230
19230
|
searchAnalytics: querySearchAnalytics,
|
|
19231
19231
|
now: () => /* @__PURE__ */ new Date()
|
|
19232
19232
|
};
|
|
@@ -19266,7 +19266,7 @@ function compareSegmentRows(input) {
|
|
|
19266
19266
|
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
19267
19267
|
});
|
|
19268
19268
|
}
|
|
19269
|
-
async function segmentImpact(input, dependencies = defaultDependencies$
|
|
19269
|
+
async function segmentImpact(input, dependencies = defaultDependencies$12) {
|
|
19270
19270
|
validateSegmentSite(input.site);
|
|
19271
19271
|
const now = dependencies.now();
|
|
19272
19272
|
const ranges = resolveSegmentRanges({
|
|
@@ -19667,13 +19667,13 @@ const MINIMUM_PAGE_SHARE = .1;
|
|
|
19667
19667
|
const MINIMUM_PAGE_IMPRESSIONS = 10;
|
|
19668
19668
|
const MAXIMUM_DOMINANT_SHARE = .8;
|
|
19669
19669
|
const MAX_SUPPRESSIONS = 100;
|
|
19670
|
-
function boundedInteger$
|
|
19670
|
+
function boundedInteger$6(value, fallback, minimum, maximum) {
|
|
19671
19671
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
19672
19672
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
19673
19673
|
}
|
|
19674
19674
|
function analyzeCannibalRows(input) {
|
|
19675
|
-
const minImpressions = boundedInteger$
|
|
19676
|
-
const limit = boundedInteger$
|
|
19675
|
+
const minImpressions = boundedInteger$6(input.minImpressions, 50, 0, 1e9);
|
|
19676
|
+
const limit = boundedInteger$6(input.limit, 25, 1, 100);
|
|
19677
19677
|
const selection = {
|
|
19678
19678
|
sourceRows: input.rows.length,
|
|
19679
19679
|
invalidRows: 0,
|
|
@@ -19821,8 +19821,8 @@ const CANNIBAL_MINIMUM_PAGE_SHARE = MINIMUM_PAGE_SHARE;
|
|
|
19821
19821
|
const CANNIBAL_MAXIMUM_DOMINANT_SHARE = MAXIMUM_DOMINANT_SHARE;
|
|
19822
19822
|
//#endregion
|
|
19823
19823
|
//#region packages/core/src/analyze/site-diagnostics/cannibal.ts
|
|
19824
|
-
const MAX_GSC_ROWS$
|
|
19825
|
-
const defaultDependencies$
|
|
19824
|
+
const MAX_GSC_ROWS$4 = 1e5;
|
|
19825
|
+
const defaultDependencies$11 = {
|
|
19826
19826
|
searchAnalytics: querySearchAnalytics,
|
|
19827
19827
|
now: () => /* @__PURE__ */ new Date()
|
|
19828
19828
|
};
|
|
@@ -19835,7 +19835,7 @@ function verdict$4(input) {
|
|
|
19835
19835
|
const result = `${input.returned} of ${input.eligible} multi-URL query candidate${input.eligible === 1 ? "" : "s"} returned for intent and technical review.`;
|
|
19836
19836
|
return input.status === "partial" ? `${result} Provider evidence is partial.` : result;
|
|
19837
19837
|
}
|
|
19838
|
-
async function cannibalReport(input, dependencies = defaultDependencies$
|
|
19838
|
+
async function cannibalReport(input, dependencies = defaultDependencies$11) {
|
|
19839
19839
|
const days = integerOption$2({
|
|
19840
19840
|
value: input.days,
|
|
19841
19841
|
fallback: 28,
|
|
@@ -19865,7 +19865,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
19865
19865
|
...range,
|
|
19866
19866
|
type: "web",
|
|
19867
19867
|
dataState: "final",
|
|
19868
|
-
maxRows: MAX_GSC_ROWS$
|
|
19868
|
+
maxRows: MAX_GSC_ROWS$4
|
|
19869
19869
|
};
|
|
19870
19870
|
const [pageExposure, propertyDemand] = await Promise.all([dependencies.searchAnalytics(input.site, {
|
|
19871
19871
|
...requestBase,
|
|
@@ -19888,8 +19888,8 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
19888
19888
|
brandTerms: input.brandTerms,
|
|
19889
19889
|
includeBrand: input.includeBrand
|
|
19890
19890
|
});
|
|
19891
|
-
const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$
|
|
19892
|
-
const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$
|
|
19891
|
+
const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$4;
|
|
19892
|
+
const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$4;
|
|
19893
19893
|
const possiblyTruncated = pageTruncated || propertyTruncated;
|
|
19894
19894
|
const partialValidation = analysis.selection.invalidRows + analysis.selection.propertyInvalidRows > 0;
|
|
19895
19895
|
const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
|
|
@@ -19914,7 +19914,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
19914
19914
|
invalidRows: analysis.selection.invalidRows
|
|
19915
19915
|
},
|
|
19916
19916
|
calls: pageExposure.calls,
|
|
19917
|
-
maxRows: MAX_GSC_ROWS$
|
|
19917
|
+
maxRows: MAX_GSC_ROWS$4,
|
|
19918
19918
|
possiblyTruncated: pageTruncated
|
|
19919
19919
|
},
|
|
19920
19920
|
propertyDemand: {
|
|
@@ -19926,7 +19926,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
19926
19926
|
invalidRows: analysis.selection.propertyInvalidRows
|
|
19927
19927
|
},
|
|
19928
19928
|
calls: propertyDemand.calls,
|
|
19929
|
-
maxRows: MAX_GSC_ROWS$
|
|
19929
|
+
maxRows: MAX_GSC_ROWS$4,
|
|
19930
19930
|
possiblyTruncated: propertyTruncated
|
|
19931
19931
|
},
|
|
19932
19932
|
completeness
|
|
@@ -19999,7 +19999,7 @@ function normalizeUrl(value) {
|
|
|
19999
19999
|
return;
|
|
20000
20000
|
}
|
|
20001
20001
|
}
|
|
20002
|
-
function validRow$
|
|
20002
|
+
function validRow$3(row, query, url) {
|
|
20003
20003
|
return Boolean(query && url) && Number.isFinite(row.clicks) && row.clicks >= 0 && Number.isFinite(row.impressions) && row.impressions > 0 && row.clicks <= row.impressions && Number.isFinite(row.ctr) && row.ctr >= 0 && row.ctr <= 1 && Number.isFinite(row.position) && row.position > 0;
|
|
20004
20004
|
}
|
|
20005
20005
|
function aggregateDecayRows(rows) {
|
|
@@ -20008,7 +20008,7 @@ function aggregateDecayRows(rows) {
|
|
|
20008
20008
|
for (const row of rows) {
|
|
20009
20009
|
const query = row.keys[0]?.trim() ?? "";
|
|
20010
20010
|
const url = normalizeUrl(row.keys[1]?.trim() ?? "");
|
|
20011
|
-
if (!validRow$
|
|
20011
|
+
if (!validRow$3(row, query, url)) {
|
|
20012
20012
|
invalid++;
|
|
20013
20013
|
continue;
|
|
20014
20014
|
}
|
|
@@ -20234,8 +20234,8 @@ function analyzeDecay(input) {
|
|
|
20234
20234
|
}
|
|
20235
20235
|
//#endregion
|
|
20236
20236
|
//#region packages/core/src/analyze/site-diagnostics/decaying.ts
|
|
20237
|
-
const MAX_GSC_ROWS$
|
|
20238
|
-
const defaultDependencies$
|
|
20237
|
+
const MAX_GSC_ROWS$3 = 1e5;
|
|
20238
|
+
const defaultDependencies$10 = {
|
|
20239
20239
|
searchAnalytics: querySearchAnalytics,
|
|
20240
20240
|
now: () => /* @__PURE__ */ new Date()
|
|
20241
20241
|
};
|
|
@@ -20246,7 +20246,7 @@ function verdict$3(report) {
|
|
|
20246
20246
|
if (!report.selection.eligibleRows) return "No observed retained query/page declines matched the report filters.";
|
|
20247
20247
|
return `${report.selection.returnedRows} of ${report.selection.eligibleRows} observed retained query/page declines returned for review.`;
|
|
20248
20248
|
}
|
|
20249
|
-
async function decayingReport(input, dependencies = defaultDependencies$
|
|
20249
|
+
async function decayingReport(input, dependencies = defaultDependencies$10) {
|
|
20250
20250
|
if (input.comparison !== void 0 && input.comparison !== "previous-period" && input.comparison !== "year-over-year") throw new SeoError("INVALID_INPUT", "comparison must be previous-period or year-over-year.");
|
|
20251
20251
|
if (input.brandTerms && (input.brandTerms.length > 20 || input.brandTerms.some((term) => !term.trim() || term.trim().length > 200))) throw new SeoError("INVALID_INPUT", "brandTerms must contain at most 20 non-empty terms of 200 characters or fewer.");
|
|
20252
20252
|
const days = integerOption$2({
|
|
@@ -20299,7 +20299,7 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
|
|
|
20299
20299
|
aggregationType: "auto",
|
|
20300
20300
|
type: "web",
|
|
20301
20301
|
dataState: "final",
|
|
20302
|
-
maxRows: MAX_GSC_ROWS$
|
|
20302
|
+
maxRows: MAX_GSC_ROWS$3
|
|
20303
20303
|
};
|
|
20304
20304
|
const [current, previous] = await Promise.all([dependencies.searchAnalytics(input.site, {
|
|
20305
20305
|
...request,
|
|
@@ -20322,8 +20322,8 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
|
|
|
20322
20322
|
brandTerms: input.brandTerms,
|
|
20323
20323
|
includeBrand: input.includeBrand
|
|
20324
20324
|
});
|
|
20325
|
-
const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$
|
|
20326
|
-
const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$
|
|
20325
|
+
const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$3;
|
|
20326
|
+
const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$3;
|
|
20327
20327
|
const possiblyTruncated = currentTruncated || previousTruncated;
|
|
20328
20328
|
const dataStatus = current.rowsFetched === 0 && previous.rowsFetched === 0 ? "empty" : current.rowsFetched === 0 || previous.rowsFetched === 0 || possiblyTruncated || analysis.selection.currentInvalidRows > 0 || analysis.selection.previousInvalidRows > 0 || analysis.selection.currentRowNotRetained > 0 ? "partial" : analysis.selection.eligibleRows === 0 ? "filtered" : "complete";
|
|
20329
20329
|
const reportBase = {
|
|
@@ -20350,13 +20350,13 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
|
|
|
20350
20350
|
current: {
|
|
20351
20351
|
rowsFetched: current.rowsFetched,
|
|
20352
20352
|
calls: current.calls,
|
|
20353
|
-
maxRows: MAX_GSC_ROWS$
|
|
20353
|
+
maxRows: MAX_GSC_ROWS$3,
|
|
20354
20354
|
possiblyTruncated: currentTruncated
|
|
20355
20355
|
},
|
|
20356
20356
|
previous: {
|
|
20357
20357
|
rowsFetched: previous.rowsFetched,
|
|
20358
20358
|
calls: previous.calls,
|
|
20359
|
-
maxRows: MAX_GSC_ROWS$
|
|
20359
|
+
maxRows: MAX_GSC_ROWS$3,
|
|
20360
20360
|
possiblyTruncated: previousTruncated
|
|
20361
20361
|
},
|
|
20362
20362
|
completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-rows-only"
|
|
@@ -20415,7 +20415,7 @@ function normalizedQuery(value) {
|
|
|
20415
20415
|
function groupKey$1(item) {
|
|
20416
20416
|
return `${item.template.id}:${normalizedQuery(item.query)}`;
|
|
20417
20417
|
}
|
|
20418
|
-
function compareText$
|
|
20418
|
+
function compareText$11(left, right) {
|
|
20419
20419
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
20420
20420
|
}
|
|
20421
20421
|
function groupAction(input) {
|
|
@@ -20451,11 +20451,11 @@ function groupQuickWins(items) {
|
|
|
20451
20451
|
technicalUrls
|
|
20452
20452
|
});
|
|
20453
20453
|
return group;
|
|
20454
|
-
}).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$
|
|
20454
|
+
}).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$11(left.id, right.id));
|
|
20455
20455
|
}
|
|
20456
20456
|
//#endregion
|
|
20457
20457
|
//#region packages/core/src/analyze/site-diagnostics/quick-wins-analysis-primitives.ts
|
|
20458
|
-
function boundedInteger$
|
|
20458
|
+
function boundedInteger$5(value, fallback, minimum, maximum) {
|
|
20459
20459
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
20460
20460
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
20461
20461
|
}
|
|
@@ -20467,7 +20467,7 @@ function validHttpUrl(value) {
|
|
|
20467
20467
|
return false;
|
|
20468
20468
|
}
|
|
20469
20469
|
}
|
|
20470
|
-
function validRow$
|
|
20470
|
+
function validRow$2(row, query, url) {
|
|
20471
20471
|
return row.keys.length >= 2 && Boolean(query) && validHttpUrl(url) && Number.isFinite(row.clicks) && row.clicks >= 0 && Number.isFinite(row.impressions) && row.impressions > 0 && row.clicks <= row.impressions && Number.isFinite(row.ctr) && row.ctr >= 0 && row.ctr <= 1 && Number.isFinite(row.position) && row.position >= 1;
|
|
20472
20472
|
}
|
|
20473
20473
|
function selectBenchmarkRows(input) {
|
|
@@ -20489,7 +20489,7 @@ function selectBenchmarkRows(input) {
|
|
|
20489
20489
|
for (const row of input.rows) {
|
|
20490
20490
|
const query = row.keys[0]?.trim() ?? "";
|
|
20491
20491
|
const url = row.keys[1]?.trim() ?? "";
|
|
20492
|
-
if (!validRow$
|
|
20492
|
+
if (!validRow$2(row, query, url)) selection.invalidRows++;
|
|
20493
20493
|
else if (row.position > 10) selection.outsideBenchmarkPositionRows++;
|
|
20494
20494
|
else if (isLowActionabilityQuery(query)) selection.lowActionabilityRows++;
|
|
20495
20495
|
else if (shouldExcludeBrandQuery({
|
|
@@ -20563,11 +20563,11 @@ function quickWinItem(input) {
|
|
|
20563
20563
|
}
|
|
20564
20564
|
};
|
|
20565
20565
|
}
|
|
20566
|
-
function compareText$
|
|
20566
|
+
function compareText$10(left, right) {
|
|
20567
20567
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
20568
20568
|
}
|
|
20569
20569
|
function compareQuickWinItems(left, right) {
|
|
20570
|
-
return right.estimatedCtrClickShortfall - left.estimatedCtrClickShortfall || right.impressions - left.impressions || compareText$
|
|
20570
|
+
return right.estimatedCtrClickShortfall - left.estimatedCtrClickShortfall || right.impressions - left.impressions || compareText$10(left.query, right.query) || compareText$10(left.url, right.url);
|
|
20571
20571
|
}
|
|
20572
20572
|
function roundedSum(values) {
|
|
20573
20573
|
return Number(values.reduce((sum, value) => sum + value, 0).toFixed(2));
|
|
@@ -20580,8 +20580,8 @@ const DEFAULT_LIMIT$3 = 25;
|
|
|
20580
20580
|
const MAX_LIMIT$3 = 100;
|
|
20581
20581
|
const FALLBACK_SOURCE = "builtin_position_ctr_curve_v1";
|
|
20582
20582
|
function analyzeQuickWinsFromRows(input) {
|
|
20583
|
-
const minImpressions = boundedInteger$
|
|
20584
|
-
const limit = boundedInteger$
|
|
20583
|
+
const minImpressions = boundedInteger$5(input.minImpressions, DEFAULT_MIN_IMPRESSIONS$2, 0, MAX_MIN_IMPRESSIONS$2);
|
|
20584
|
+
const limit = boundedInteger$5(input.limit, DEFAULT_LIMIT$3, 1, MAX_LIMIT$3);
|
|
20585
20585
|
const selected = selectBenchmarkRows(input);
|
|
20586
20586
|
const benchmarkContext = createCtrBenchmarkContext(selected.rows, {
|
|
20587
20587
|
samplePopulation: "all_qualified_url_samples",
|
|
@@ -20682,7 +20682,7 @@ function analyzeQuickWinsFromRows(input) {
|
|
|
20682
20682
|
}
|
|
20683
20683
|
//#endregion
|
|
20684
20684
|
//#region packages/core/src/analyze/site-diagnostics/quick-wins-templates.ts
|
|
20685
|
-
function compareText$
|
|
20685
|
+
function compareText$9(left, right) {
|
|
20686
20686
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
20687
20687
|
}
|
|
20688
20688
|
function unique$2(values) {
|
|
@@ -20708,7 +20708,7 @@ function quickWinTemplateRecommendations(items) {
|
|
|
20708
20708
|
action: technicalUrls > 0 ? `Fix verified technical issues on ${technicalUrls} affected URLs before testing shared template changes.` : `Compare live results and verified page evidence for ${urls.length} affected URLs before testing a shared title, snippet, or heading change.`,
|
|
20709
20709
|
evidence: `${templateItems.length} eligible rows across ${urls.length} distinct URLs share this recognised template. Example queries: ${queries.join("; ")}.`
|
|
20710
20710
|
};
|
|
20711
|
-
}).filter((item) => item !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$
|
|
20711
|
+
}).filter((item) => item !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$9(left.templateId, right.templateId));
|
|
20712
20712
|
}
|
|
20713
20713
|
function quickWinTemplateSummaries(items) {
|
|
20714
20714
|
const templates = /* @__PURE__ */ new Map();
|
|
@@ -20727,17 +20727,17 @@ function quickWinTemplateSummaries(items) {
|
|
|
20727
20727
|
label: item.label,
|
|
20728
20728
|
rowCount: item.rows,
|
|
20729
20729
|
urlCount: item.urls.size,
|
|
20730
|
-
sampleUrls: [...item.urls].sort(compareText$
|
|
20731
|
-
})).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$
|
|
20730
|
+
sampleUrls: [...item.urls].sort(compareText$9).slice(0, 3)
|
|
20731
|
+
})).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$9(left.id, right.id)).slice(0, 5);
|
|
20732
20732
|
}
|
|
20733
20733
|
//#endregion
|
|
20734
20734
|
//#region packages/core/src/analyze/site-diagnostics/quick-wins.ts
|
|
20735
|
-
const DEFAULT_DAYS$
|
|
20736
|
-
const MAX_DAYS$
|
|
20735
|
+
const DEFAULT_DAYS$3 = 28;
|
|
20736
|
+
const MAX_DAYS$4 = 548;
|
|
20737
20737
|
const DEFAULT_VERIFY_LIMIT$1 = 5;
|
|
20738
20738
|
const MAX_VERIFY_LIMIT = 100;
|
|
20739
20739
|
const MAX_SOURCE_ROWS$3 = 1e5;
|
|
20740
|
-
const defaultDependencies$
|
|
20740
|
+
const defaultDependencies$9 = {
|
|
20741
20741
|
searchAnalytics: querySearchAnalytics,
|
|
20742
20742
|
fetch: fetchPage,
|
|
20743
20743
|
extract: extractPage,
|
|
@@ -20756,13 +20756,13 @@ function applyVerification$1(item, coverage) {
|
|
|
20756
20756
|
evidenceRef: `${item.recommendation.evidenceRef} ${coverage.summary}`
|
|
20757
20757
|
};
|
|
20758
20758
|
}
|
|
20759
|
-
async function quickWinsReport(input, dependencies = defaultDependencies$
|
|
20760
|
-
const explicitRange = explicitDateRange(input, MAX_DAYS$
|
|
20759
|
+
async function quickWinsReport(input, dependencies = defaultDependencies$9) {
|
|
20760
|
+
const explicitRange = explicitDateRange(input, MAX_DAYS$4);
|
|
20761
20761
|
const days = explicitRange?.days ?? integerOption$2({
|
|
20762
20762
|
value: input.days,
|
|
20763
|
-
fallback: DEFAULT_DAYS$
|
|
20763
|
+
fallback: DEFAULT_DAYS$3,
|
|
20764
20764
|
minimum: 1,
|
|
20765
|
-
maximum: MAX_DAYS$
|
|
20765
|
+
maximum: MAX_DAYS$4,
|
|
20766
20766
|
label: "Days"
|
|
20767
20767
|
});
|
|
20768
20768
|
const verifyLimit = integerOption$2({
|
|
@@ -20912,7 +20912,7 @@ async function quickWinsReport(input, dependencies = defaultDependencies$8) {
|
|
|
20912
20912
|
}
|
|
20913
20913
|
//#endregion
|
|
20914
20914
|
//#region packages/core/src/analyze/striking-distance-analysis-primitives.ts
|
|
20915
|
-
function compareText$
|
|
20915
|
+
function compareText$8(left, right) {
|
|
20916
20916
|
const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
|
|
20917
20917
|
const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
|
|
20918
20918
|
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
@@ -20978,7 +20978,7 @@ function strikingDistanceItem(row) {
|
|
|
20978
20978
|
};
|
|
20979
20979
|
}
|
|
20980
20980
|
function compareStrikingDistanceItems(left, right) {
|
|
20981
|
-
return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$
|
|
20981
|
+
return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$8(left.query, right.query) || compareText$8(left.url, right.url);
|
|
20982
20982
|
}
|
|
20983
20983
|
function groupStrikingDistanceItems(items) {
|
|
20984
20984
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -21015,7 +21015,7 @@ function groupStrikingDistanceItems(items) {
|
|
|
21015
21015
|
action: shared ? `Review the sampled ${first.template.label.toLowerCase()} URLs for a recurring technical, content, or internal-link pattern before changing the shared template.` : `Review these URLs individually; the current evidence does not support a shared-template change.`
|
|
21016
21016
|
}
|
|
21017
21017
|
};
|
|
21018
|
-
}).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$
|
|
21018
|
+
}).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$8(left.id, right.id));
|
|
21019
21019
|
}
|
|
21020
21020
|
//#endregion
|
|
21021
21021
|
//#region packages/core/src/analyze/striking-distance-analysis.ts
|
|
@@ -21129,22 +21129,22 @@ function analyzeStrikingDistanceRows(input) {
|
|
|
21129
21129
|
}
|
|
21130
21130
|
//#endregion
|
|
21131
21131
|
//#region packages/core/src/analyze/striking-distance.ts
|
|
21132
|
-
const DEFAULT_DAYS$
|
|
21133
|
-
const MAX_DAYS$
|
|
21132
|
+
const DEFAULT_DAYS$2 = 28;
|
|
21133
|
+
const MAX_DAYS$3 = 548;
|
|
21134
21134
|
const DEFAULT_VERIFY_LIMIT = 5;
|
|
21135
21135
|
const MAX_SOURCE_ROWS$2 = 1e5;
|
|
21136
|
-
const defaultDependencies$
|
|
21136
|
+
const defaultDependencies$8 = {
|
|
21137
21137
|
searchAnalytics: querySearchAnalytics,
|
|
21138
21138
|
verifyContent: verifyQueryContent,
|
|
21139
21139
|
now: () => /* @__PURE__ */ new Date()
|
|
21140
21140
|
};
|
|
21141
|
-
function boundedInteger$
|
|
21141
|
+
function boundedInteger$4(value, fallback, minimum, maximum) {
|
|
21142
21142
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
21143
21143
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
21144
21144
|
}
|
|
21145
21145
|
function reportDays(value) {
|
|
21146
|
-
if (value === void 0) return DEFAULT_DAYS$
|
|
21147
|
-
if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$
|
|
21146
|
+
if (value === void 0) return DEFAULT_DAYS$2;
|
|
21147
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$3) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS$3}.`);
|
|
21148
21148
|
return value;
|
|
21149
21149
|
}
|
|
21150
21150
|
function dateRange(days, now) {
|
|
@@ -21183,7 +21183,7 @@ function verdict$2(analysis) {
|
|
|
21183
21183
|
if (analysis.dataStatus === "filtered") return "No retained query/page rows met the report filters.";
|
|
21184
21184
|
return `${countLabel(analysis.summary.eligibleRows, "eligible query/page row")} found; ${countLabel(analysis.summary.returnedRows, "row")} returned for review.`;
|
|
21185
21185
|
}
|
|
21186
|
-
async function strikingDistance(input, dependencies = defaultDependencies$
|
|
21186
|
+
async function strikingDistance(input, dependencies = defaultDependencies$8) {
|
|
21187
21187
|
const days = reportDays(input.days);
|
|
21188
21188
|
const generatedAt = dependencies.now().toISOString();
|
|
21189
21189
|
const range = dateRange(days, new Date(generatedAt));
|
|
@@ -21202,7 +21202,7 @@ async function strikingDistance(input, dependencies = defaultDependencies$7) {
|
|
|
21202
21202
|
brandTerms: input.brandTerms,
|
|
21203
21203
|
includeBrand: input.includeBrand
|
|
21204
21204
|
});
|
|
21205
|
-
const verifyLimit = boundedInteger$
|
|
21205
|
+
const verifyLimit = boundedInteger$4(input.verifyLimit, DEFAULT_VERIFY_LIMIT, 0, analysis.limit);
|
|
21206
21206
|
const verificationRequested = input.verifyContent === true || input.verifyContent !== false && input.verifyLimit !== void 0;
|
|
21207
21207
|
const attempted = verificationRequested ? Math.min(verifyLimit, analysis.items.length) : 0;
|
|
21208
21208
|
const items = await Promise.all(analysis.items.map(async (item, index) => {
|
|
@@ -22030,7 +22030,7 @@ function emptyQuickWins(input) {
|
|
|
22030
22030
|
warnings: []
|
|
22031
22031
|
};
|
|
22032
22032
|
}
|
|
22033
|
-
const defaultDependencies$
|
|
22033
|
+
const defaultDependencies$7 = {
|
|
22034
22034
|
trafficAnomaly,
|
|
22035
22035
|
updateCorrelation,
|
|
22036
22036
|
segmentImpact,
|
|
@@ -22041,7 +22041,7 @@ const defaultDependencies$6 = {
|
|
|
22041
22041
|
};
|
|
22042
22042
|
async function diagnoseProperty(input, dependencies = {}) {
|
|
22043
22043
|
const providers = {
|
|
22044
|
-
...defaultDependencies$
|
|
22044
|
+
...defaultDependencies$7,
|
|
22045
22045
|
...dependencies
|
|
22046
22046
|
};
|
|
22047
22047
|
const limit = input.limit ?? 10;
|
|
@@ -22989,6 +22989,42 @@ async function providerRequestJson(input) {
|
|
|
22989
22989
|
return parsed.data;
|
|
22990
22990
|
}
|
|
22991
22991
|
//#endregion
|
|
22992
|
+
//#region packages/core/src/providers/dataforseo/account-pricing.ts
|
|
22993
|
+
function unitPrice(components) {
|
|
22994
|
+
const prices = components ?? [];
|
|
22995
|
+
const total = (type) => {
|
|
22996
|
+
const matching = prices.filter((item) => item.cost_type === type);
|
|
22997
|
+
return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
|
|
22998
|
+
};
|
|
22999
|
+
return {
|
|
23000
|
+
perRequestMicros: total("per_request"),
|
|
23001
|
+
perResultMicros: total("per_result")
|
|
23002
|
+
};
|
|
23003
|
+
}
|
|
23004
|
+
function dataForSeoAccountPrices(account) {
|
|
23005
|
+
return {
|
|
23006
|
+
keywordOverviewPrice: unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal),
|
|
23007
|
+
keywordDiscoveryPrices: {
|
|
23008
|
+
ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
|
|
23009
|
+
related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
|
|
23010
|
+
suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
|
|
23011
|
+
},
|
|
23012
|
+
domainResearchPrices: {
|
|
23013
|
+
domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
|
|
23014
|
+
rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
|
|
23015
|
+
rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
|
|
23016
|
+
serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
|
|
23017
|
+
},
|
|
23018
|
+
linkPrices: {
|
|
23019
|
+
summary: unitPrice(account.price?.backlinks?.summary?.live?.priority_normal),
|
|
23020
|
+
backlinks: unitPrice(account.price?.backlinks?.backlinks?.live?.priority_normal),
|
|
23021
|
+
referringDomains: unitPrice(account.price?.backlinks?.referring_domains?.live?.priority_normal)
|
|
23022
|
+
},
|
|
23023
|
+
serpLiveAdvancedPrice: unitPrice(account.price?.serp?.live?.advanced?.priority_normal),
|
|
23024
|
+
serpTaskPostPrice: unitPrice(account.price?.serp?.task_post?.priority_normal)
|
|
23025
|
+
};
|
|
23026
|
+
}
|
|
23027
|
+
//#endregion
|
|
22992
23028
|
//#region packages/core/src/providers/dataforseo/account-schema.ts
|
|
22993
23029
|
const userDataMoneySchema = z.object({
|
|
22994
23030
|
total: z.number().finite().nonnegative().optional(),
|
|
@@ -23006,6 +23042,11 @@ const priceComponentSchema = z.object({
|
|
|
23006
23042
|
const normalPriorityPriceSchema = z.object({ priority_normal: z.array(priceComponentSchema).max(20).optional() }).passthrough();
|
|
23007
23043
|
const livePriceSchema = z.object({ live: normalPriorityPriceSchema.optional() }).passthrough();
|
|
23008
23044
|
const userDataPriceSchema = z.object({
|
|
23045
|
+
backlinks: z.object({
|
|
23046
|
+
summary: livePriceSchema.optional(),
|
|
23047
|
+
backlinks: livePriceSchema.optional(),
|
|
23048
|
+
referring_domains: livePriceSchema.optional()
|
|
23049
|
+
}).passthrough().optional(),
|
|
23009
23050
|
dataforseo_labs: z.object({
|
|
23010
23051
|
keyword_overview: livePriceSchema.optional(),
|
|
23011
23052
|
keyword_ideas: livePriceSchema.optional(),
|
|
@@ -23063,7 +23104,7 @@ function discoveryNextCursor(response) {
|
|
|
23063
23104
|
//#region packages/core/src/providers/dataforseo/schema.ts
|
|
23064
23105
|
const MAX_PROVIDER_MONTHLY_SEARCH_ROWS = 120;
|
|
23065
23106
|
const nullableNumber$1 = z.number().finite().nullable().optional();
|
|
23066
|
-
const nullableCount$
|
|
23107
|
+
const nullableCount$2 = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
|
|
23067
23108
|
const monthlySearchSchema$1 = z.object({
|
|
23068
23109
|
year: z.number().int(),
|
|
23069
23110
|
month: z.number().int(),
|
|
@@ -23079,7 +23120,7 @@ const keywordOverviewItemSchema = z.object({
|
|
|
23079
23120
|
last_updated_time: z.string().trim().min(1).nullable().optional()
|
|
23080
23121
|
}).nullish(),
|
|
23081
23122
|
keyword_properties: z.object({ keyword_difficulty: nullableNumber$1 }).nullish(),
|
|
23082
|
-
serp_info: z.object({ se_results_count: nullableCount$
|
|
23123
|
+
serp_info: z.object({ se_results_count: nullableCount$2 }).nullish(),
|
|
23083
23124
|
search_intent_info: z.object({ main_intent: z.enum([
|
|
23084
23125
|
"informational",
|
|
23085
23126
|
"navigational",
|
|
@@ -23152,37 +23193,37 @@ const dataForSeoDiscoveryResponseSchema = z.object({
|
|
|
23152
23193
|
//#region packages/core/src/providers/dataforseo/domain-schema.ts
|
|
23153
23194
|
const nullableNumber = z.number().finite().nullable().optional();
|
|
23154
23195
|
const nullableNonnegativeNumber = z.number().finite().nonnegative().nullable().optional();
|
|
23155
|
-
const nullableCount = z.number().int().nonnegative().nullable().optional();
|
|
23196
|
+
const nullableCount$1 = z.number().int().nonnegative().nullable().optional();
|
|
23156
23197
|
const rankingMetricsSchema = z.object({
|
|
23157
|
-
pos_1: nullableCount,
|
|
23158
|
-
pos_2_3: nullableCount,
|
|
23159
|
-
pos_4_10: nullableCount,
|
|
23160
|
-
pos_11_20: nullableCount,
|
|
23161
|
-
pos_21_30: nullableCount,
|
|
23162
|
-
pos_31_40: nullableCount,
|
|
23163
|
-
pos_41_50: nullableCount,
|
|
23164
|
-
pos_51_60: nullableCount,
|
|
23165
|
-
pos_61_70: nullableCount,
|
|
23166
|
-
pos_71_80: nullableCount,
|
|
23167
|
-
pos_81_90: nullableCount,
|
|
23168
|
-
pos_91_100: nullableCount,
|
|
23198
|
+
pos_1: nullableCount$1,
|
|
23199
|
+
pos_2_3: nullableCount$1,
|
|
23200
|
+
pos_4_10: nullableCount$1,
|
|
23201
|
+
pos_11_20: nullableCount$1,
|
|
23202
|
+
pos_21_30: nullableCount$1,
|
|
23203
|
+
pos_31_40: nullableCount$1,
|
|
23204
|
+
pos_41_50: nullableCount$1,
|
|
23205
|
+
pos_51_60: nullableCount$1,
|
|
23206
|
+
pos_61_70: nullableCount$1,
|
|
23207
|
+
pos_71_80: nullableCount$1,
|
|
23208
|
+
pos_81_90: nullableCount$1,
|
|
23209
|
+
pos_91_100: nullableCount$1,
|
|
23169
23210
|
etv: nullableNonnegativeNumber,
|
|
23170
|
-
count: nullableCount,
|
|
23211
|
+
count: nullableCount$1,
|
|
23171
23212
|
estimated_paid_traffic_cost: nullableNonnegativeNumber,
|
|
23172
|
-
is_new: nullableCount,
|
|
23173
|
-
is_up: nullableCount,
|
|
23174
|
-
is_down: nullableCount,
|
|
23175
|
-
is_lost: nullableCount
|
|
23213
|
+
is_new: nullableCount$1,
|
|
23214
|
+
is_up: nullableCount$1,
|
|
23215
|
+
is_down: nullableCount$1,
|
|
23216
|
+
is_lost: nullableCount$1
|
|
23176
23217
|
}).passthrough();
|
|
23177
23218
|
const metricsSchema = z.object({ organic: rankingMetricsSchema.nullable().optional() }).passthrough();
|
|
23178
|
-
const paidResponseSchema = z.object({
|
|
23219
|
+
const paidResponseSchema$1 = z.object({
|
|
23179
23220
|
status_code: z.number().int(),
|
|
23180
23221
|
status_message: z.string().max(1e3),
|
|
23181
23222
|
cost: z.number().finite().nonnegative().optional(),
|
|
23182
23223
|
tasks_count: z.number().int().nonnegative(),
|
|
23183
23224
|
tasks_error: z.number().int().nonnegative()
|
|
23184
23225
|
}).passthrough();
|
|
23185
|
-
const taskFields = {
|
|
23226
|
+
const taskFields$1 = {
|
|
23186
23227
|
id: z.string().trim().min(1).max(100).optional(),
|
|
23187
23228
|
status_code: z.number().int(),
|
|
23188
23229
|
status_message: z.string().max(1e3),
|
|
@@ -23202,8 +23243,8 @@ const overviewResultSchema = z.object({
|
|
|
23202
23243
|
items_count: z.number().int().nonnegative().optional(),
|
|
23203
23244
|
items: z.array(overviewItemSchema).max(1e3).nullable().optional()
|
|
23204
23245
|
}).passthrough();
|
|
23205
|
-
const dataForSeoDomainOverviewResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
23206
|
-
...taskFields,
|
|
23246
|
+
const dataForSeoDomainOverviewResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
|
|
23247
|
+
...taskFields$1,
|
|
23207
23248
|
result: z.array(overviewResultSchema).max(10).nullable().optional()
|
|
23208
23249
|
}).passthrough()).max(10) });
|
|
23209
23250
|
const rankedSerpItemSchema = z.object({
|
|
@@ -23223,8 +23264,8 @@ const rankedKeywordsResultSchema = z.object({
|
|
|
23223
23264
|
items_count: z.number().int().nonnegative().optional(),
|
|
23224
23265
|
items: z.array(rankedKeywordItemSchema).max(1e3).nullable().optional()
|
|
23225
23266
|
}).passthrough();
|
|
23226
|
-
const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
23227
|
-
...taskFields,
|
|
23267
|
+
const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
|
|
23268
|
+
...taskFields$1,
|
|
23228
23269
|
result: z.array(rankedKeywordsResultSchema).max(10).nullable().optional()
|
|
23229
23270
|
}).passthrough()).max(10) });
|
|
23230
23271
|
const rankingPageItemSchema = z.object({
|
|
@@ -23237,8 +23278,8 @@ const rankingPagesResultSchema = z.object({
|
|
|
23237
23278
|
items_count: z.number().int().nonnegative().optional(),
|
|
23238
23279
|
items: z.array(rankingPageItemSchema).max(1e3).nullable().optional()
|
|
23239
23280
|
}).passthrough();
|
|
23240
|
-
const dataForSeoRankingPagesResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
23241
|
-
...taskFields,
|
|
23281
|
+
const dataForSeoRankingPagesResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
|
|
23282
|
+
...taskFields$1,
|
|
23242
23283
|
result: z.array(rankingPagesResultSchema).max(10).nullable().optional()
|
|
23243
23284
|
}).passthrough()).max(10) });
|
|
23244
23285
|
const competitorItemSchema = z.object({
|
|
@@ -23246,9 +23287,9 @@ const competitorItemSchema = z.object({
|
|
|
23246
23287
|
avg_position: nullableNumber,
|
|
23247
23288
|
median_position: nullableNumber,
|
|
23248
23289
|
etv: nullableNonnegativeNumber,
|
|
23249
|
-
keywords_count: nullableCount,
|
|
23290
|
+
keywords_count: nullableCount$1,
|
|
23250
23291
|
visibility: nullableNonnegativeNumber,
|
|
23251
|
-
relevant_serp_items: nullableCount,
|
|
23292
|
+
relevant_serp_items: nullableCount$1,
|
|
23252
23293
|
keywords_positions: z.record(z.string().trim().min(1).max(80), z.array(z.number().int().positive()).max(100)).nullable().optional()
|
|
23253
23294
|
}).passthrough();
|
|
23254
23295
|
const competitorsResultSchema = z.object({
|
|
@@ -23257,8 +23298,8 @@ const competitorsResultSchema = z.object({
|
|
|
23257
23298
|
items_count: z.number().int().nonnegative().optional(),
|
|
23258
23299
|
items: z.array(competitorItemSchema).max(1e3).nullable().optional()
|
|
23259
23300
|
}).passthrough();
|
|
23260
|
-
const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
23261
|
-
...taskFields,
|
|
23301
|
+
const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
|
|
23302
|
+
...taskFields$1,
|
|
23262
23303
|
result: z.array(competitorsResultSchema).max(10).nullable().optional()
|
|
23263
23304
|
}).passthrough()).max(10) });
|
|
23264
23305
|
const DOMAIN_OVERVIEW_PATH = "v3/dataforseo_labs/google/domain_rank_overview/live";
|
|
@@ -23448,6 +23489,209 @@ function serpCompetitorsPaidRequest(input, ttlMs) {
|
|
|
23448
23489
|
};
|
|
23449
23490
|
}
|
|
23450
23491
|
//#endregion
|
|
23492
|
+
//#region packages/core/src/providers/dataforseo/link-schema.ts
|
|
23493
|
+
const nullableCount = z.number().int().nonnegative().nullable().optional();
|
|
23494
|
+
const nullableScore = z.number().finite().nonnegative().nullable().optional();
|
|
23495
|
+
const nullableText = z.string().max(2e4).nullable().optional();
|
|
23496
|
+
const paidResponseSchema = z.object({
|
|
23497
|
+
status_code: z.number().int(),
|
|
23498
|
+
status_message: z.string().max(1e3),
|
|
23499
|
+
cost: z.number().finite().nonnegative().optional(),
|
|
23500
|
+
tasks_count: z.number().int().nonnegative(),
|
|
23501
|
+
tasks_error: z.number().int().nonnegative()
|
|
23502
|
+
}).passthrough();
|
|
23503
|
+
const taskFields = {
|
|
23504
|
+
id: z.string().trim().min(1).max(100).optional(),
|
|
23505
|
+
status_code: z.number().int(),
|
|
23506
|
+
status_message: z.string().max(1e3),
|
|
23507
|
+
cost: z.number().finite().nonnegative().optional(),
|
|
23508
|
+
result_count: z.number().int().nonnegative().optional()
|
|
23509
|
+
};
|
|
23510
|
+
const dataForSeoLinkSummaryItemSchema = z.object({
|
|
23511
|
+
target: z.string().trim().min(1).max(2048).optional(),
|
|
23512
|
+
rank: nullableScore,
|
|
23513
|
+
backlinks: nullableCount,
|
|
23514
|
+
referring_pages: nullableCount,
|
|
23515
|
+
referring_domains: nullableCount,
|
|
23516
|
+
broken_backlinks: nullableCount,
|
|
23517
|
+
broken_pages: nullableCount,
|
|
23518
|
+
backlinks_spam_score: nullableScore,
|
|
23519
|
+
info: z.object({ target_spam_score: nullableScore }).passthrough().nullable().optional()
|
|
23520
|
+
}).passthrough();
|
|
23521
|
+
const dataForSeoLinkSummaryResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
23522
|
+
...taskFields,
|
|
23523
|
+
result: z.array(dataForSeoLinkSummaryItemSchema).max(10).nullable().optional()
|
|
23524
|
+
}).passthrough()).max(10) });
|
|
23525
|
+
const dataForSeoBacklinkItemSchema = z.object({
|
|
23526
|
+
domain_from: nullableText,
|
|
23527
|
+
url_from: nullableText,
|
|
23528
|
+
url_to: nullableText,
|
|
23529
|
+
anchor: nullableText,
|
|
23530
|
+
item_type: z.string().max(100).nullable().optional(),
|
|
23531
|
+
dofollow: z.boolean().nullable().optional(),
|
|
23532
|
+
rank: nullableScore,
|
|
23533
|
+
domain_from_rank: nullableScore,
|
|
23534
|
+
page_from_rank: nullableScore,
|
|
23535
|
+
backlink_spam_score: nullableScore,
|
|
23536
|
+
backlinks_spam_score: nullableScore,
|
|
23537
|
+
first_seen: z.string().max(100).nullable().optional(),
|
|
23538
|
+
last_visited: z.string().max(100).nullable().optional(),
|
|
23539
|
+
lost_date: z.string().max(100).nullable().optional(),
|
|
23540
|
+
is_lost: z.boolean().nullable().optional(),
|
|
23541
|
+
is_indirect_link: z.boolean().nullable().optional(),
|
|
23542
|
+
links_count: nullableCount,
|
|
23543
|
+
group_count: nullableCount,
|
|
23544
|
+
rel_attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional(),
|
|
23545
|
+
attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional()
|
|
23546
|
+
}).passthrough();
|
|
23547
|
+
const backlinkResultSchema = z.object({
|
|
23548
|
+
target: z.string().trim().min(1).max(2048).optional(),
|
|
23549
|
+
total_count: nullableCount,
|
|
23550
|
+
items_count: nullableCount,
|
|
23551
|
+
search_after_token: z.string().max(2e4).nullable().optional(),
|
|
23552
|
+
items: z.array(dataForSeoBacklinkItemSchema).max(1e3).nullable().optional()
|
|
23553
|
+
}).passthrough();
|
|
23554
|
+
const dataForSeoBacklinksResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
23555
|
+
...taskFields,
|
|
23556
|
+
result: z.array(backlinkResultSchema).max(10).nullable().optional()
|
|
23557
|
+
}).passthrough()).max(10) });
|
|
23558
|
+
const dataForSeoReferringDomainItemSchema = z.object({
|
|
23559
|
+
domain: z.string().trim().min(1).max(2048).nullable().optional(),
|
|
23560
|
+
backlinks: nullableCount,
|
|
23561
|
+
referring_pages: nullableCount,
|
|
23562
|
+
broken_backlinks: nullableCount,
|
|
23563
|
+
broken_pages: nullableCount,
|
|
23564
|
+
rank: nullableScore,
|
|
23565
|
+
backlinks_spam_score: nullableScore,
|
|
23566
|
+
first_seen: z.string().max(100).nullable().optional()
|
|
23567
|
+
}).passthrough();
|
|
23568
|
+
const referringDomainsResultSchema = z.object({
|
|
23569
|
+
target: z.string().trim().min(1).max(2048).optional(),
|
|
23570
|
+
total_count: nullableCount,
|
|
23571
|
+
items_count: nullableCount,
|
|
23572
|
+
items: z.array(dataForSeoReferringDomainItemSchema).max(1e3).nullable().optional()
|
|
23573
|
+
}).passthrough();
|
|
23574
|
+
const dataForSeoReferringDomainsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
23575
|
+
...taskFields,
|
|
23576
|
+
result: z.array(referringDomainsResultSchema).max(10).nullable().optional()
|
|
23577
|
+
}).passthrough()).max(10) });
|
|
23578
|
+
const MAX_LINK_ROWS$1 = 1e3;
|
|
23579
|
+
const MAX_LINK_OFFSET = 2e4;
|
|
23580
|
+
const LINK_ENDPOINTS = {
|
|
23581
|
+
summary: "v3/backlinks/summary/live",
|
|
23582
|
+
backlinks: "v3/backlinks/backlinks/live",
|
|
23583
|
+
referringDomains: "v3/backlinks/referring_domains/live"
|
|
23584
|
+
};
|
|
23585
|
+
function resultRows(response) {
|
|
23586
|
+
return response.tasks.reduce((taskTotal, task) => taskTotal + (task.result ?? []).reduce((resultTotal, result) => resultTotal + (result.items?.length ?? 0), 0), 0);
|
|
23587
|
+
}
|
|
23588
|
+
function summaryRows(response) {
|
|
23589
|
+
return response.tasks.reduce((total, task) => total + (task.result?.length ?? 0), 0);
|
|
23590
|
+
}
|
|
23591
|
+
function commonRequest(input) {
|
|
23592
|
+
return {
|
|
23593
|
+
target: input.target,
|
|
23594
|
+
include_subdomains: input.scope === "domain" ? input.includeSubdomains : false,
|
|
23595
|
+
include_indirect_links: true,
|
|
23596
|
+
exclude_internal_backlinks: true,
|
|
23597
|
+
backlinks_status_type: "live",
|
|
23598
|
+
rank_scale: "one_hundred"
|
|
23599
|
+
};
|
|
23600
|
+
}
|
|
23601
|
+
function validatePage(input) {
|
|
23602
|
+
if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 1e3) throw new ProviderError({
|
|
23603
|
+
provider: "dataforseo",
|
|
23604
|
+
operation: input.operation,
|
|
23605
|
+
code: "configuration",
|
|
23606
|
+
message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
|
|
23607
|
+
});
|
|
23608
|
+
if (!Number.isSafeInteger(input.offset) || input.offset < 0 || input.offset > 2e4) throw new ProviderError({
|
|
23609
|
+
provider: "dataforseo",
|
|
23610
|
+
operation: input.operation,
|
|
23611
|
+
code: "configuration",
|
|
23612
|
+
message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
|
|
23613
|
+
});
|
|
23614
|
+
}
|
|
23615
|
+
function linkSummaryPaidRequest(input, ttlMs) {
|
|
23616
|
+
return {
|
|
23617
|
+
operation: "link-summary",
|
|
23618
|
+
capability: "link-summary",
|
|
23619
|
+
endpoint: LINK_ENDPOINTS.summary,
|
|
23620
|
+
request: commonRequest(input),
|
|
23621
|
+
schema: dataForSeoLinkSummaryResponseSchema,
|
|
23622
|
+
requestedRows: 1,
|
|
23623
|
+
price: (account) => account.linkPrices.summary,
|
|
23624
|
+
context: input.context,
|
|
23625
|
+
ttlMs,
|
|
23626
|
+
refresh: input.refresh,
|
|
23627
|
+
rowCount: summaryRows
|
|
23628
|
+
};
|
|
23629
|
+
}
|
|
23630
|
+
function backlinksPaidRequest(input, ttlMs) {
|
|
23631
|
+
validatePage({
|
|
23632
|
+
limit: input.limit,
|
|
23633
|
+
offset: input.offset,
|
|
23634
|
+
operation: "backlinks"
|
|
23635
|
+
});
|
|
23636
|
+
if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
|
|
23637
|
+
provider: "dataforseo",
|
|
23638
|
+
operation: "backlinks",
|
|
23639
|
+
code: "configuration",
|
|
23640
|
+
message: "Backlink rows need from 1 to 3 sort rules."
|
|
23641
|
+
});
|
|
23642
|
+
return {
|
|
23643
|
+
operation: "backlinks",
|
|
23644
|
+
capability: "backlinks",
|
|
23645
|
+
endpoint: LINK_ENDPOINTS.backlinks,
|
|
23646
|
+
request: {
|
|
23647
|
+
...commonRequest(input),
|
|
23648
|
+
mode: input.mode,
|
|
23649
|
+
backlinks_status_type: input.status,
|
|
23650
|
+
limit: input.limit,
|
|
23651
|
+
offset: input.offset,
|
|
23652
|
+
order_by: input.orderBy
|
|
23653
|
+
},
|
|
23654
|
+
schema: dataForSeoBacklinksResponseSchema,
|
|
23655
|
+
requestedRows: input.limit,
|
|
23656
|
+
price: (account) => account.linkPrices.backlinks,
|
|
23657
|
+
context: input.context,
|
|
23658
|
+
ttlMs,
|
|
23659
|
+
refresh: input.refresh,
|
|
23660
|
+
rowCount: resultRows
|
|
23661
|
+
};
|
|
23662
|
+
}
|
|
23663
|
+
function referringDomainsPaidRequest(input, ttlMs) {
|
|
23664
|
+
validatePage({
|
|
23665
|
+
limit: input.limit,
|
|
23666
|
+
offset: input.offset,
|
|
23667
|
+
operation: "referring-domains"
|
|
23668
|
+
});
|
|
23669
|
+
if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
|
|
23670
|
+
provider: "dataforseo",
|
|
23671
|
+
operation: "referring-domains",
|
|
23672
|
+
code: "configuration",
|
|
23673
|
+
message: "Referring-domain rows need from 1 to 3 sort rules."
|
|
23674
|
+
});
|
|
23675
|
+
return {
|
|
23676
|
+
operation: "referring-domains",
|
|
23677
|
+
capability: "referring-domains",
|
|
23678
|
+
endpoint: LINK_ENDPOINTS.referringDomains,
|
|
23679
|
+
request: {
|
|
23680
|
+
...commonRequest(input),
|
|
23681
|
+
limit: input.limit,
|
|
23682
|
+
offset: input.offset,
|
|
23683
|
+
order_by: input.orderBy
|
|
23684
|
+
},
|
|
23685
|
+
schema: dataForSeoReferringDomainsResponseSchema,
|
|
23686
|
+
requestedRows: input.limit,
|
|
23687
|
+
price: (account) => account.linkPrices.referringDomains,
|
|
23688
|
+
context: input.context,
|
|
23689
|
+
ttlMs,
|
|
23690
|
+
refresh: input.refresh,
|
|
23691
|
+
rowCount: resultRows
|
|
23692
|
+
};
|
|
23693
|
+
}
|
|
23694
|
+
//#endregion
|
|
23451
23695
|
//#region packages/core/src/providers/dataforseo/paid-request.ts
|
|
23452
23696
|
function usdToMicros(value) {
|
|
23453
23697
|
if (value === void 0) return null;
|
|
@@ -23971,41 +24215,6 @@ const MAX_KEYWORD_CHARACTERS = 80;
|
|
|
23971
24215
|
const MAX_KEYWORD_WORDS = 10;
|
|
23972
24216
|
const MAX_DISCOVERY_SEEDS$1 = 5;
|
|
23973
24217
|
const MAX_DISCOVERY_ROWS$1 = 100;
|
|
23974
|
-
function unitPrice(components) {
|
|
23975
|
-
const prices = components ?? [];
|
|
23976
|
-
const total = (type) => {
|
|
23977
|
-
const matching = prices.filter((item) => item.cost_type === type);
|
|
23978
|
-
return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
|
|
23979
|
-
};
|
|
23980
|
-
return {
|
|
23981
|
-
perRequestMicros: total("per_request"),
|
|
23982
|
-
perResultMicros: total("per_result")
|
|
23983
|
-
};
|
|
23984
|
-
}
|
|
23985
|
-
function keywordOverviewPrice(account) {
|
|
23986
|
-
return unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal);
|
|
23987
|
-
}
|
|
23988
|
-
function keywordDiscoveryPrices(account) {
|
|
23989
|
-
return {
|
|
23990
|
-
ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
|
|
23991
|
-
related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
|
|
23992
|
-
suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
|
|
23993
|
-
};
|
|
23994
|
-
}
|
|
23995
|
-
function domainResearchPrices(account) {
|
|
23996
|
-
return {
|
|
23997
|
-
domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
|
|
23998
|
-
rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
|
|
23999
|
-
rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
|
|
24000
|
-
serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
|
|
24001
|
-
};
|
|
24002
|
-
}
|
|
24003
|
-
function serpLiveAdvancedPrice(account) {
|
|
24004
|
-
return unitPrice(account.price?.serp?.live?.advanced?.priority_normal);
|
|
24005
|
-
}
|
|
24006
|
-
function serpTaskPostPrice(account) {
|
|
24007
|
-
return unitPrice(account.price?.serp?.task_post?.priority_normal);
|
|
24008
|
-
}
|
|
24009
24218
|
function responseTaskIds(response) {
|
|
24010
24219
|
return [...new Set(response.tasks.flatMap((task) => task.id ? [task.id] : []))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
24011
24220
|
}
|
|
@@ -24025,6 +24234,7 @@ var DataForSeoClient = class {
|
|
|
24025
24234
|
serpTtlMs;
|
|
24026
24235
|
accountPricingTtlMs;
|
|
24027
24236
|
domainResearchTtlMs;
|
|
24237
|
+
linkTtlMs;
|
|
24028
24238
|
spendLimits;
|
|
24029
24239
|
accountPricing;
|
|
24030
24240
|
accountPricingRequest;
|
|
@@ -24041,6 +24251,7 @@ var DataForSeoClient = class {
|
|
|
24041
24251
|
this.serpTtlMs = options.serpTtlMs ?? DEFAULT_SERP_TTL_MS;
|
|
24042
24252
|
this.accountPricingTtlMs = options.accountPricingTtlMs ?? DEFAULT_ACCOUNT_PRICING_TTL_MS;
|
|
24043
24253
|
this.domainResearchTtlMs = options.domainResearchTtlMs ?? 6048e5;
|
|
24254
|
+
this.linkTtlMs = options.linkTtlMs ?? 864e5;
|
|
24044
24255
|
this.spendLimits = options.spendLimits;
|
|
24045
24256
|
}
|
|
24046
24257
|
async getCredentials(operation) {
|
|
@@ -24123,11 +24334,7 @@ var DataForSeoClient = class {
|
|
|
24123
24334
|
accountDailySpendMicros: dataForSeoUsdToMicros(account.money?.statistics?.day?.total),
|
|
24124
24335
|
accountDailySpendPeriod: account.money?.statistics?.day?.value ?? null,
|
|
24125
24336
|
accountDailyLimitMicros: dataForSeoUsdToMicros(account.money?.limits?.day?.total),
|
|
24126
|
-
|
|
24127
|
-
keywordDiscoveryPrices: keywordDiscoveryPrices(account),
|
|
24128
|
-
domainResearchPrices: domainResearchPrices(account),
|
|
24129
|
-
serpLiveAdvancedPrice: serpLiveAdvancedPrice(account),
|
|
24130
|
-
serpTaskPostPrice: serpTaskPostPrice(account),
|
|
24337
|
+
...dataForSeoAccountPrices(account),
|
|
24131
24338
|
backlinksSubscriptionExpiresAt: account.backlinks_subscription_expiry_date ?? null,
|
|
24132
24339
|
aiMentionsSubscriptionExpiresAt: account.llm_mentions_subscription_expiry_date ?? null,
|
|
24133
24340
|
apiVersion: response.version ?? null,
|
|
@@ -24190,6 +24397,15 @@ var DataForSeoClient = class {
|
|
|
24190
24397
|
async serpCompetitors(input) {
|
|
24191
24398
|
return this.paidPost(serpCompetitorsPaidRequest(input, this.domainResearchTtlMs));
|
|
24192
24399
|
}
|
|
24400
|
+
async linkSummary(input) {
|
|
24401
|
+
return this.paidPost(linkSummaryPaidRequest(input, this.linkTtlMs));
|
|
24402
|
+
}
|
|
24403
|
+
async backlinks(input) {
|
|
24404
|
+
return this.paidPost(backlinksPaidRequest(input, this.linkTtlMs));
|
|
24405
|
+
}
|
|
24406
|
+
async referringDomains(input) {
|
|
24407
|
+
return this.paidPost(referringDomainsPaidRequest(input, this.linkTtlMs));
|
|
24408
|
+
}
|
|
24193
24409
|
async keywordOverview(input) {
|
|
24194
24410
|
const keywords = input.keywords.map((keyword) => keyword.trim());
|
|
24195
24411
|
if (keywords.length < 1 || keywords.length > MAX_KEYWORDS_PER_OVERVIEW_REQUEST || keywords.some((keyword) => keyword.length < 1 || keyword.length > MAX_KEYWORD_CHARACTERS || keyword.split(/\s+/u).length > MAX_KEYWORD_WORDS)) throw new ProviderError({
|
|
@@ -24533,20 +24749,20 @@ function compareCodepoints$3(left, right) {
|
|
|
24533
24749
|
function normalizedKeyword$2(keyword) {
|
|
24534
24750
|
return keyword.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
24535
24751
|
}
|
|
24536
|
-
function missing$
|
|
24752
|
+
function missing$2(field) {
|
|
24537
24753
|
return unavailableValue("missing", `DataForSEO omitted ${field}.`);
|
|
24538
24754
|
}
|
|
24539
24755
|
function numericValue(values, field, valid) {
|
|
24540
24756
|
const present = values.filter((value) => value !== null && value !== void 0);
|
|
24541
|
-
if (present.length === 0) return missing$
|
|
24757
|
+
if (present.length === 0) return missing$2(field);
|
|
24542
24758
|
const numbers = present.filter((value) => typeof value === "number" && valid(value));
|
|
24543
24759
|
if (numbers.length !== present.length) return unavailableValue("invalid", `DataForSEO returned an invalid ${field}.`);
|
|
24544
24760
|
const unique = [...new Set(numbers)];
|
|
24545
24761
|
return unique.length === 1 ? observedValue(unique[0]) : unavailableValue("invalid", `DataForSEO returned conflicting ${field} values.`);
|
|
24546
24762
|
}
|
|
24547
|
-
function stringValue(values, field, normalize) {
|
|
24763
|
+
function stringValue$1(values, field, normalize) {
|
|
24548
24764
|
const present = values.filter((value) => value !== null && value !== void 0);
|
|
24549
|
-
if (present.length === 0) return missing$
|
|
24765
|
+
if (present.length === 0) return missing$2(field);
|
|
24550
24766
|
const strings = present.flatMap((value) => {
|
|
24551
24767
|
if (typeof value !== "string") return [];
|
|
24552
24768
|
const normalized = normalize(value);
|
|
@@ -24558,7 +24774,7 @@ function stringValue(values, field, normalize) {
|
|
|
24558
24774
|
}
|
|
24559
24775
|
function monthlySearchesValue(rows) {
|
|
24560
24776
|
const histories = rows.map((row) => row.keyword_info?.monthly_searches).filter((value) => value !== null && value !== void 0);
|
|
24561
|
-
if (histories.length === 0) return missing$
|
|
24777
|
+
if (histories.length === 0) return missing$2("monthlySearches");
|
|
24562
24778
|
const byMonth = /* @__PURE__ */ new Map();
|
|
24563
24779
|
let invalid = false;
|
|
24564
24780
|
for (const history of histories) for (const item of history) {
|
|
@@ -24591,27 +24807,27 @@ function resultCountValues(rows) {
|
|
|
24591
24807
|
function metricForKeyword(keyword, rows) {
|
|
24592
24808
|
if (rows.length === 0) return {
|
|
24593
24809
|
keyword,
|
|
24594
|
-
monthlySearchVolume: missing$
|
|
24595
|
-
monthlySearches: missing$
|
|
24596
|
-
searchVolumeUpdatedAt: missing$
|
|
24597
|
-
cpcUsd: missing$
|
|
24598
|
-
paidCompetition: missing$
|
|
24599
|
-
keywordDifficulty: missing$
|
|
24600
|
-
intent: missing$
|
|
24601
|
-
resultCount: missing$
|
|
24810
|
+
monthlySearchVolume: missing$2("monthlySearchVolume"),
|
|
24811
|
+
monthlySearches: missing$2("monthlySearches"),
|
|
24812
|
+
searchVolumeUpdatedAt: missing$2("searchVolumeUpdatedAt"),
|
|
24813
|
+
cpcUsd: missing$2("cpcUsd"),
|
|
24814
|
+
paidCompetition: missing$2("paidCompetition"),
|
|
24815
|
+
keywordDifficulty: missing$2("keywordDifficulty"),
|
|
24816
|
+
intent: missing$2("intent"),
|
|
24817
|
+
resultCount: missing$2("resultCount")
|
|
24602
24818
|
};
|
|
24603
24819
|
return {
|
|
24604
24820
|
keyword,
|
|
24605
24821
|
monthlySearchVolume: numericValue(rows.map((row) => row.keyword_info?.search_volume), "monthlySearchVolume", (value) => Number.isSafeInteger(value) && value >= 0),
|
|
24606
24822
|
monthlySearches: monthlySearchesValue(rows),
|
|
24607
|
-
searchVolumeUpdatedAt: stringValue(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
|
|
24823
|
+
searchVolumeUpdatedAt: stringValue$1(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
|
|
24608
24824
|
const timestamp = Date.parse(value);
|
|
24609
24825
|
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
24610
24826
|
}),
|
|
24611
24827
|
cpcUsd: numericValue(rows.map((row) => row.keyword_info?.cpc), "cpcUsd", (value) => Number.isFinite(value) && value >= 0),
|
|
24612
24828
|
paidCompetition: numericValue(rows.map((row) => row.keyword_info?.competition), "paidCompetition", (value) => Number.isFinite(value) && value >= 0 && value <= 1),
|
|
24613
24829
|
keywordDifficulty: numericValue(rows.map((row) => row.keyword_properties?.keyword_difficulty), "keywordDifficulty", (value) => Number.isFinite(value) && value >= 0 && value <= 100),
|
|
24614
|
-
intent: stringValue(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
|
|
24830
|
+
intent: stringValue$1(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
|
|
24615
24831
|
resultCount: numericValue(resultCountValues(rows), "resultCount", (value) => Number.isSafeInteger(value) && value >= 0)
|
|
24616
24832
|
};
|
|
24617
24833
|
}
|
|
@@ -24664,21 +24880,21 @@ const COMPETITOR_RESULT_TYPES = /* @__PURE__ */ new Set([
|
|
|
24664
24880
|
"featured_snippet",
|
|
24665
24881
|
"local_pack"
|
|
24666
24882
|
]);
|
|
24667
|
-
function missing(field) {
|
|
24883
|
+
function missing$1(field) {
|
|
24668
24884
|
return unavailableValue("missing", `DataForSEO omitted ${field}.`);
|
|
24669
24885
|
}
|
|
24670
|
-
function numberValue(value, field) {
|
|
24671
|
-
return value === null || value === void 0 ? missing(field) : observedValue(value);
|
|
24886
|
+
function numberValue$1(value, field) {
|
|
24887
|
+
return value === null || value === void 0 ? missing$1(field) : observedValue(value);
|
|
24672
24888
|
}
|
|
24673
24889
|
function countValue(value, field) {
|
|
24674
|
-
return numberValue(value, field);
|
|
24890
|
+
return numberValue$1(value, field);
|
|
24675
24891
|
}
|
|
24676
24892
|
function sumBuckets(metrics, fields, label) {
|
|
24677
24893
|
const values = fields.map((field) => metrics[field]);
|
|
24678
|
-
return values.some((value) => value === null || value === void 0) ? missing(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
|
|
24894
|
+
return values.some((value) => value === null || value === void 0) ? missing$1(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
|
|
24679
24895
|
}
|
|
24680
24896
|
function rankingDistribution(metrics) {
|
|
24681
|
-
if (!metrics) return missing("organic ranking distribution");
|
|
24897
|
+
if (!metrics) return missing$1("organic ranking distribution");
|
|
24682
24898
|
const buckets = {
|
|
24683
24899
|
first: sumBuckets(metrics, ["pos_1"], "position 1 rankings"),
|
|
24684
24900
|
top3: sumBuckets(metrics, ["pos_1", "pos_2_3"], "top 3 rankings"),
|
|
@@ -24729,9 +24945,9 @@ function rankingDistribution(metrics) {
|
|
|
24729
24945
|
}
|
|
24730
24946
|
function organicFootprint(metrics) {
|
|
24731
24947
|
return {
|
|
24732
|
-
estimatedMonthlyTraffic: numberValue(metrics?.etv, "estimated organic monthly traffic"),
|
|
24948
|
+
estimatedMonthlyTraffic: numberValue$1(metrics?.etv, "estimated organic monthly traffic"),
|
|
24733
24949
|
rankedKeywords: countValue(metrics?.count, "ranked organic keywords"),
|
|
24734
|
-
estimatedMonthlyTrafficCostUsd: numberValue(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
|
|
24950
|
+
estimatedMonthlyTrafficCostUsd: numberValue$1(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
|
|
24735
24951
|
rankings: rankingDistribution(metrics),
|
|
24736
24952
|
newRankings: countValue(metrics?.is_new, "new rankings"),
|
|
24737
24953
|
improvedRankings: countValue(metrics?.is_up, "improved rankings"),
|
|
@@ -24786,7 +25002,7 @@ function researchMarket(input, operation) {
|
|
|
24786
25002
|
});
|
|
24787
25003
|
return parsed;
|
|
24788
25004
|
}
|
|
24789
|
-
function rowLimit(limit, offset = 0) {
|
|
25005
|
+
function rowLimit$1(limit, offset = 0) {
|
|
24790
25006
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
|
|
24791
25007
|
provider: "dataforseo",
|
|
24792
25008
|
operation: "domain-research",
|
|
@@ -24810,7 +25026,7 @@ function resultTypes(input, allowed) {
|
|
|
24810
25026
|
});
|
|
24811
25027
|
return normalized;
|
|
24812
25028
|
}
|
|
24813
|
-
function safeUrl$
|
|
25029
|
+
function safeUrl$2(value) {
|
|
24814
25030
|
if (!value) return null;
|
|
24815
25031
|
try {
|
|
24816
25032
|
const url = new URL(value);
|
|
@@ -24830,7 +25046,7 @@ function dedupeBy(rows, key) {
|
|
|
24830
25046
|
}
|
|
24831
25047
|
return [...grouped.entries()].sort(([left], [right]) => compareCodepoints$3(left, right)).map(([, matches]) => [...matches].sort((left, right) => compareCodepoints$3(JSON.stringify(left), JSON.stringify(right)))[0]);
|
|
24832
25048
|
}
|
|
24833
|
-
function observedNumber(value) {
|
|
25049
|
+
function observedNumber$1(value) {
|
|
24834
25050
|
return value.state === "observed" ? value.value : -1;
|
|
24835
25051
|
}
|
|
24836
25052
|
function duplicateWarning(available, retained, label) {
|
|
@@ -24841,7 +25057,7 @@ function duplicateWarning(available, retained, label) {
|
|
|
24841
25057
|
message: `${duplicates} duplicate ${label} row${duplicates === 1 ? "" : "s"} were collapsed deterministically.`
|
|
24842
25058
|
}] : [];
|
|
24843
25059
|
}
|
|
24844
|
-
function totalRows(values) {
|
|
25060
|
+
function totalRows$1(values) {
|
|
24845
25061
|
const present = values.filter((value) => value !== null && value !== void 0);
|
|
24846
25062
|
return present.length ? Math.max(...present) : null;
|
|
24847
25063
|
}
|
|
@@ -24857,7 +25073,7 @@ function coverage(input) {
|
|
|
24857
25073
|
nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
|
|
24858
25074
|
};
|
|
24859
25075
|
}
|
|
24860
|
-
function requestContext(reportId, supplied) {
|
|
25076
|
+
function requestContext$1(reportId, supplied) {
|
|
24861
25077
|
return supplied ?? {
|
|
24862
25078
|
reportId,
|
|
24863
25079
|
reportRunId: randomUUID()
|
|
@@ -24906,7 +25122,7 @@ async function dataForSeoDomainOverview(client, input) {
|
|
|
24906
25122
|
languageCode: market.languageCode.split("-")[0],
|
|
24907
25123
|
...location,
|
|
24908
25124
|
refresh: input.refresh,
|
|
24909
|
-
context: requestContext("domain-overview", input.context)
|
|
25125
|
+
context: requestContext$1("domain-overview", input.context)
|
|
24910
25126
|
});
|
|
24911
25127
|
const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
24912
25128
|
const matchingResult = results.find((row) => !row.target || domain(row.target) === target);
|
|
@@ -24998,7 +25214,7 @@ async function dataForSeoRankedKeywords(client, input) {
|
|
|
24998
25214
|
const market = researchMarket(input.market, "ranked-keywords");
|
|
24999
25215
|
const target = rankedTarget(input.target);
|
|
25000
25216
|
const offset = input.offset ?? 0;
|
|
25001
|
-
rowLimit(input.limit, offset);
|
|
25217
|
+
rowLimit$1(input.limit, offset);
|
|
25002
25218
|
const types = resultTypes(input.resultTypes, RANKED_RESULT_TYPES);
|
|
25003
25219
|
const excluded = excludedTerms(input);
|
|
25004
25220
|
const requestedFilters = filters$1(input, excluded);
|
|
@@ -25018,7 +25234,7 @@ async function dataForSeoRankedKeywords(client, input) {
|
|
|
25018
25234
|
limit: input.limit,
|
|
25019
25235
|
offset,
|
|
25020
25236
|
refresh: input.refresh,
|
|
25021
|
-
context: requestContext("ranked-keywords", input.context)
|
|
25237
|
+
context: requestContext$1("ranked-keywords", input.context)
|
|
25022
25238
|
});
|
|
25023
25239
|
const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
25024
25240
|
const rawRows = responseResults.flatMap((result) => result.items ?? []);
|
|
@@ -25027,7 +25243,7 @@ async function dataForSeoRankedKeywords(client, input) {
|
|
|
25027
25243
|
const keywordData = row.keyword_data;
|
|
25028
25244
|
const serp = row.ranked_serp_element?.serp_item;
|
|
25029
25245
|
const keyword = keywordData?.keyword ? normalizedKeyword$2(keywordData.keyword) : "";
|
|
25030
|
-
const url = safeUrl$
|
|
25246
|
+
const url = safeUrl$2(serp?.url);
|
|
25031
25247
|
if (!keywordData || !keyword || !url || !serp?.rank_group || !serp.rank_absolute || !serp.type) {
|
|
25032
25248
|
invalidRows += 1;
|
|
25033
25249
|
return [];
|
|
@@ -25038,11 +25254,11 @@ async function dataForSeoRankedKeywords(client, input) {
|
|
|
25038
25254
|
rankGroup: serp.rank_group,
|
|
25039
25255
|
rankAbsolute: serp.rank_absolute,
|
|
25040
25256
|
resultType: serp.type,
|
|
25041
|
-
estimatedMonthlyTraffic: numberValue(serp.etv, "estimated keyword traffic")
|
|
25257
|
+
estimatedMonthlyTraffic: numberValue$1(serp.etv, "estimated keyword traffic")
|
|
25042
25258
|
}];
|
|
25043
25259
|
});
|
|
25044
|
-
const rows = dedupeBy(mappedRows, (row) => `${row.keyword}\0${row.url}\0${row.resultType}`).sort((left, right) => observedNumber(right.monthlySearchVolume) - observedNumber(left.monthlySearchVolume) || left.rankGroup - right.rankGroup || compareCodepoints$3(left.keyword, right.keyword) || compareCodepoints$3(left.url, right.url));
|
|
25045
|
-
const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
|
|
25260
|
+
const rows = dedupeBy(mappedRows, (row) => `${row.keyword}\0${row.url}\0${row.resultType}`).sort((left, right) => observedNumber$1(right.monthlySearchVolume) - observedNumber$1(left.monthlySearchVolume) || left.rankGroup - right.rankGroup || compareCodepoints$3(left.keyword, right.keyword) || compareCodepoints$3(left.url, right.url));
|
|
25261
|
+
const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
|
|
25046
25262
|
return domainEvidence({
|
|
25047
25263
|
capability: "ranked-keywords",
|
|
25048
25264
|
data: {
|
|
@@ -25115,7 +25331,7 @@ async function dataForSeoRankingPages(client, input) {
|
|
|
25115
25331
|
const market = researchMarket(input.market, "ranking-pages");
|
|
25116
25332
|
const target = domain(input.domain);
|
|
25117
25333
|
const offset = input.offset ?? 0;
|
|
25118
|
-
rowLimit(input.limit, offset);
|
|
25334
|
+
rowLimit$1(input.limit, offset);
|
|
25119
25335
|
const requestedFilters = filters(input);
|
|
25120
25336
|
const orderBy = ["metrics.organic.etv,desc", "page_address,asc"];
|
|
25121
25337
|
const snapshot = await client.rankingPages({
|
|
@@ -25127,13 +25343,13 @@ async function dataForSeoRankingPages(client, input) {
|
|
|
25127
25343
|
limit: input.limit,
|
|
25128
25344
|
offset,
|
|
25129
25345
|
refresh: input.refresh,
|
|
25130
|
-
context: requestContext("ranking-pages", input.context)
|
|
25346
|
+
context: requestContext$1("ranking-pages", input.context)
|
|
25131
25347
|
});
|
|
25132
25348
|
const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
25133
25349
|
const rawRows = responseResults.flatMap((result) => result.items ?? []);
|
|
25134
25350
|
let invalidRows = 0;
|
|
25135
25351
|
const mappedRows = rawRows.flatMap((row) => {
|
|
25136
|
-
const url = safeUrl$
|
|
25352
|
+
const url = safeUrl$2(row.page_address);
|
|
25137
25353
|
if (!url) {
|
|
25138
25354
|
invalidRows += 1;
|
|
25139
25355
|
return [];
|
|
@@ -25143,8 +25359,8 @@ async function dataForSeoRankingPages(client, input) {
|
|
|
25143
25359
|
organic: organicFootprint(row.metrics?.organic)
|
|
25144
25360
|
}];
|
|
25145
25361
|
});
|
|
25146
|
-
const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber(right.organic.estimatedMonthlyTraffic) - observedNumber(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
|
|
25147
|
-
const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
|
|
25362
|
+
const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber$1(right.organic.estimatedMonthlyTraffic) - observedNumber$1(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
|
|
25363
|
+
const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
|
|
25148
25364
|
return domainEvidence({
|
|
25149
25365
|
capability: "relevant-pages",
|
|
25150
25366
|
data: {
|
|
@@ -25189,7 +25405,7 @@ function keywords(input) {
|
|
|
25189
25405
|
async function dataForSeoSerpCompetitors(client, input) {
|
|
25190
25406
|
const market = researchMarket(input.market, "serp-competitors");
|
|
25191
25407
|
const offset = input.offset ?? 0;
|
|
25192
|
-
rowLimit(input.limit, offset);
|
|
25408
|
+
rowLimit$1(input.limit, offset);
|
|
25193
25409
|
const requestedKeywords = keywords(input.keywords);
|
|
25194
25410
|
const types = resultTypes(input.resultTypes, COMPETITOR_RESULT_TYPES);
|
|
25195
25411
|
const orderBy = [
|
|
@@ -25207,7 +25423,7 @@ async function dataForSeoSerpCompetitors(client, input) {
|
|
|
25207
25423
|
limit: input.limit,
|
|
25208
25424
|
offset,
|
|
25209
25425
|
refresh: input.refresh,
|
|
25210
|
-
context: requestContext("serp-competitors", input.context)
|
|
25426
|
+
context: requestContext$1("serp-competitors", input.context)
|
|
25211
25427
|
});
|
|
25212
25428
|
const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
25213
25429
|
const rawRows = responseResults.flatMap((result) => result.items ?? []);
|
|
@@ -25230,16 +25446,16 @@ async function dataForSeoSerpCompetitors(client, input) {
|
|
|
25230
25446
|
return [{
|
|
25231
25447
|
domain: normalizedDomain,
|
|
25232
25448
|
matchedKeywords: row.keywords_count,
|
|
25233
|
-
averagePosition: numberValue(row.avg_position, "average position"),
|
|
25234
|
-
medianPosition: numberValue(row.median_position, "median position"),
|
|
25235
|
-
visibility: numberValue(row.visibility, "visibility"),
|
|
25236
|
-
estimatedMonthlyTraffic: numberValue(row.etv, "estimated monthly traffic"),
|
|
25449
|
+
averagePosition: numberValue$1(row.avg_position, "average position"),
|
|
25450
|
+
medianPosition: numberValue$1(row.median_position, "median position"),
|
|
25451
|
+
visibility: numberValue$1(row.visibility, "visibility"),
|
|
25452
|
+
estimatedMonthlyTraffic: numberValue$1(row.etv, "estimated monthly traffic"),
|
|
25237
25453
|
relevantResults: countValue(row.relevant_serp_items, "relevant result count"),
|
|
25238
25454
|
keywordPositions
|
|
25239
25455
|
}];
|
|
25240
25456
|
});
|
|
25241
|
-
const rows = dedupeBy(mappedRows, (row) => row.domain).sort((left, right) => observedNumber(right.visibility) - observedNumber(left.visibility) || right.matchedKeywords - left.matchedKeywords || compareCodepoints$3(left.domain, right.domain));
|
|
25242
|
-
const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
|
|
25457
|
+
const rows = dedupeBy(mappedRows, (row) => row.domain).sort((left, right) => observedNumber$1(right.visibility) - observedNumber$1(left.visibility) || right.matchedKeywords - left.matchedKeywords || compareCodepoints$3(left.domain, right.domain));
|
|
25458
|
+
const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
|
|
25243
25459
|
return domainEvidence({
|
|
25244
25460
|
capability: "serp-competitors",
|
|
25245
25461
|
data: {
|
|
@@ -25363,7 +25579,7 @@ function resolveProvider(input) {
|
|
|
25363
25579
|
//#endregion
|
|
25364
25580
|
//#region packages/core/src/analyze/domain-research/shared.ts
|
|
25365
25581
|
const MAX_GSC_DOMAIN_ROWS = 1e5;
|
|
25366
|
-
function compareText$
|
|
25582
|
+
function compareText$7(left, right) {
|
|
25367
25583
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
25368
25584
|
}
|
|
25369
25585
|
function normalizedKeyword$1(value) {
|
|
@@ -25480,8 +25696,8 @@ function aggregateGscQueries(rows) {
|
|
|
25480
25696
|
clicks: item.clicks,
|
|
25481
25697
|
impressions: item.impressions,
|
|
25482
25698
|
averagePosition: item.positionWeight ? item.weightedPosition / item.positionWeight : null,
|
|
25483
|
-
urls: [...item.urls].sort(compareText$
|
|
25484
|
-
})).sort((left, right) => right.impressions - left.impressions || compareText$
|
|
25699
|
+
urls: [...item.urls].sort(compareText$7).slice(0, 5)
|
|
25700
|
+
})).sort((left, right) => right.impressions - left.impressions || compareText$7(left.query, right.query));
|
|
25485
25701
|
}
|
|
25486
25702
|
async function acquireGscQueries(input) {
|
|
25487
25703
|
const range = finalGscDateRange(input.days, input.now);
|
|
@@ -25847,7 +26063,7 @@ function pseoQueryPatterns(rows) {
|
|
|
25847
26063
|
//#region packages/core/src/analyze/pseo/templates.ts
|
|
25848
26064
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
25849
26065
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
25850
|
-
function compareText$
|
|
26066
|
+
function compareText$6(left, right) {
|
|
25851
26067
|
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
|
|
25852
26068
|
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
|
|
25853
26069
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -25903,7 +26119,7 @@ function valueTokens(values) {
|
|
|
25903
26119
|
const tokens = value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
25904
26120
|
for (const token of tokens.filter((item) => item.length > 1 || /[^\p{ASCII}]/u.test(item))) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
25905
26121
|
}
|
|
25906
|
-
return [...counts.entries()].sort((a, b) => b[1] - a[1] || compareText$
|
|
26122
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || compareText$6(a[0], b[0])).slice(0, 8).map(([token]) => token);
|
|
25907
26123
|
}
|
|
25908
26124
|
function templateShape(signature, urls) {
|
|
25909
26125
|
const signatureParts = signature.split("/").filter(Boolean);
|
|
@@ -25952,7 +26168,7 @@ function collectClusters(urls, signatures) {
|
|
|
25952
26168
|
return bySignature;
|
|
25953
26169
|
}
|
|
25954
26170
|
function clusterPseoTemplates(urls, opts = {}) {
|
|
25955
|
-
const uniqueUrls = [...new Set(urls)].sort(compareText$
|
|
26171
|
+
const uniqueUrls = [...new Set(urls)].sort(compareText$6);
|
|
25956
26172
|
const minUrls = opts.minUrls ?? 3;
|
|
25957
26173
|
const minShare = opts.minShare ?? .01;
|
|
25958
26174
|
const sampleSize = opts.sampleSize ?? 5;
|
|
@@ -25970,15 +26186,15 @@ function clusterPseoTemplates(urls, opts = {}) {
|
|
|
25970
26186
|
shape: templateShape(signature, clusterUrls),
|
|
25971
26187
|
urlCount: clusterUrls.length,
|
|
25972
26188
|
share: uniqueUrls.length ? clusterUrls.length / uniqueUrls.length : 0,
|
|
25973
|
-
sampleUrls: clusterUrls.sort(compareText$
|
|
25974
|
-
})).filter((cluster) => cluster.shape.variableSegments.length > 0 && cluster.urlCount >= minUrls && cluster.share >= minShare).sort((a, b) => b.urlCount - a.urlCount || compareText$
|
|
26189
|
+
sampleUrls: clusterUrls.sort(compareText$6).slice(0, sampleSize)
|
|
26190
|
+
})).filter((cluster) => cluster.shape.variableSegments.length > 0 && cluster.urlCount >= minUrls && cluster.share >= minShare).sort((a, b) => b.urlCount - a.urlCount || compareText$6(a.signature, b.signature)).slice(0, opts.limit ?? 50);
|
|
25975
26191
|
}
|
|
25976
26192
|
//#endregion
|
|
25977
26193
|
//#region packages/core/src/analyze/domain-research/gap-analysis.ts
|
|
25978
26194
|
const MAX_RELEVANCE_QUERY_REFS = 3;
|
|
25979
26195
|
const MAX_FIRST_PARTY_PATTERN_URLS = 1e4;
|
|
25980
26196
|
function terms(value) {
|
|
25981
|
-
return [...new Set(pseoQueryTerms(value).map(canonicalPseoTerm).filter(Boolean))].sort(compareText$
|
|
26197
|
+
return [...new Set(pseoQueryTerms(value).map(canonicalPseoTerm).filter(Boolean))].sort(compareText$7);
|
|
25982
26198
|
}
|
|
25983
26199
|
function observedVolume$1(row) {
|
|
25984
26200
|
return value(row.monthlySearchVolume) ?? -1;
|
|
@@ -26022,7 +26238,7 @@ function relevanceForKeyword(keyword, acquisition, tokenIndex) {
|
|
|
26022
26238
|
query: row.query,
|
|
26023
26239
|
overlap: terms(row.query).filter((token) => keywordTerms.includes(token)).length,
|
|
26024
26240
|
impressions: row.impressions
|
|
26025
|
-
})).filter((row) => row.overlap > 0).sort((left, right) => right.overlap - left.overlap || right.impressions - left.impressions || compareText$
|
|
26241
|
+
})).filter((row) => row.overlap > 0).sort((left, right) => right.overlap - left.overlap || right.impressions - left.impressions || compareText$7(left.query, right.query)).slice(0, MAX_RELEVANCE_QUERY_REFS).map((row) => row.query);
|
|
26026
26242
|
return {
|
|
26027
26243
|
state: sharedTokens.length >= 2 ? "observed-overlap" : sharedTokens.length === 1 ? "weak-overlap" : "unavailable",
|
|
26028
26244
|
sharedTokens,
|
|
@@ -26044,10 +26260,10 @@ function repeatedCompetitorPatterns(rows) {
|
|
|
26044
26260
|
urlCount: pattern.urlCount,
|
|
26045
26261
|
sampleUrls: pattern.sampleUrls,
|
|
26046
26262
|
evidenceRefs: items.flatMap((item) => pattern.sampleUrls.includes(item.row.url) ? [item.evidenceRef] : [])
|
|
26047
|
-
}))).sort((left, right) => right.urlCount - left.urlCount || compareText$
|
|
26263
|
+
}))).sort((left, right) => right.urlCount - left.urlCount || compareText$7(left.domain, right.domain) || compareText$7(left.signature, right.signature));
|
|
26048
26264
|
}
|
|
26049
26265
|
function ownPatterns(acquisition) {
|
|
26050
|
-
return clusterPseoTemplates([...new Set(acquisition.sourceRows.rows.flatMap((row) => row.urls))].sort(compareText$
|
|
26266
|
+
return clusterPseoTemplates([...new Set(acquisition.sourceRows.rows.flatMap((row) => row.urls))].sort(compareText$7).slice(0, MAX_FIRST_PARTY_PATTERN_URLS), {
|
|
26051
26267
|
minUrls: 3,
|
|
26052
26268
|
minShare: 0,
|
|
26053
26269
|
limit: 50,
|
|
@@ -26088,10 +26304,10 @@ function analyzeCompetitorGap(acquisition) {
|
|
|
26088
26304
|
const byDomain = /* @__PURE__ */ new Map();
|
|
26089
26305
|
for (const item of rows) {
|
|
26090
26306
|
const current = byDomain.get(item.domain);
|
|
26091
|
-
if (!current || item.row.rankGroup < current.row.rankGroup || item.row.rankGroup === current.row.rankGroup && compareText$
|
|
26307
|
+
if (!current || item.row.rankGroup < current.row.rankGroup || item.row.rankGroup === current.row.rankGroup && compareText$7(item.row.url, current.row.url) < 0) byDomain.set(item.domain, item);
|
|
26092
26308
|
}
|
|
26093
|
-
const retainedRows = [...byDomain.values()].sort((left, right) => left.row.rankGroup - right.row.rankGroup || compareText$
|
|
26094
|
-
const best = [...retainedRows].sort((left, right) => observedVolume$1(right.row) - observedVolume$1(left.row) || left.row.rankGroup - right.row.rankGroup || compareText$
|
|
26309
|
+
const retainedRows = [...byDomain.values()].sort((left, right) => left.row.rankGroup - right.row.rankGroup || compareText$7(left.domain, right.domain));
|
|
26310
|
+
const best = [...retainedRows].sort((left, right) => observedVolume$1(right.row) - observedVolume$1(left.row) || left.row.rankGroup - right.row.rankGroup || compareText$7(left.domain, right.domain))[0];
|
|
26095
26311
|
const firstParty = firstPartyByQuery.get(keyword);
|
|
26096
26312
|
const relevance = relevanceForKeyword(keyword, acquisition, tokenIndex.index);
|
|
26097
26313
|
const ownRank = ownRanks.get(keyword) ?? null;
|
|
@@ -26135,7 +26351,7 @@ function analyzeCompetitorGap(acquisition) {
|
|
|
26135
26351
|
},
|
|
26136
26352
|
evidenceRefs: retainedRows.map((item) => item.evidenceRef)
|
|
26137
26353
|
};
|
|
26138
|
-
}).sort((left, right) => classificationOrder(left.classification) - classificationOrder(right.classification) || right.competitorCount - left.competitorCount || (value(right.monthlySearchVolume) ?? -1) - (value(left.monthlySearchVolume) ?? -1) || Math.min(...left.competitors.map((item) => item.rank)) - Math.min(...right.competitors.map((item) => item.rank)) || compareText$
|
|
26354
|
+
}).sort((left, right) => classificationOrder(left.classification) - classificationOrder(right.classification) || right.competitorCount - left.competitorCount || (value(right.monthlySearchVolume) ?? -1) - (value(left.monthlySearchVolume) ?? -1) || Math.min(...left.competitors.map((item) => item.rank)) - Math.min(...right.competitors.map((item) => item.rank)) || compareText$7(left.keyword, right.keyword));
|
|
26139
26355
|
return {
|
|
26140
26356
|
rawRows,
|
|
26141
26357
|
candidates,
|
|
@@ -26154,7 +26370,7 @@ function analyzeCompetitorGap(acquisition) {
|
|
|
26154
26370
|
}
|
|
26155
26371
|
//#endregion
|
|
26156
26372
|
//#region packages/core/src/analyze/domain-research/gap-report.ts
|
|
26157
|
-
const MAX_FINDINGS = 10;
|
|
26373
|
+
const MAX_FINDINGS$2 = 10;
|
|
26158
26374
|
const MAX_SOURCE_BRIEFS = 5;
|
|
26159
26375
|
function count(candidates, classification) {
|
|
26160
26376
|
return candidates.filter((item) => item.classification === classification).length;
|
|
@@ -26169,7 +26385,7 @@ function dataStatus(input) {
|
|
|
26169
26385
|
function findings$2(input) {
|
|
26170
26386
|
const result = [];
|
|
26171
26387
|
for (const [index, candidate] of input.candidates.entries()) {
|
|
26172
|
-
if (result.length >= MAX_FINDINGS) break;
|
|
26388
|
+
if (result.length >= MAX_FINDINGS$2) break;
|
|
26173
26389
|
if (candidate.classification === "relevant-gap-candidate") result.push({
|
|
26174
26390
|
code: "relevant-competitor-gap",
|
|
26175
26391
|
evidenceRefs: [`candidates[${index}]`, ...candidate.evidenceRefs],
|
|
@@ -26184,7 +26400,7 @@ function findings$2(input) {
|
|
|
26184
26400
|
});
|
|
26185
26401
|
}
|
|
26186
26402
|
for (const [index, pattern] of input.patterns.entries()) {
|
|
26187
|
-
if (result.length >= MAX_FINDINGS) break;
|
|
26403
|
+
if (result.length >= MAX_FINDINGS$2) break;
|
|
26188
26404
|
result.push({
|
|
26189
26405
|
code: "competitor-template-pattern",
|
|
26190
26406
|
evidenceRefs: [`repeatedCompetitorPatterns[${index}]`, ...pattern.evidenceRefs],
|
|
@@ -26607,7 +26823,7 @@ async function rankingPagesReport(input, dependencies = {}) {
|
|
|
26607
26823
|
impressions: row.impressions,
|
|
26608
26824
|
averagePosition: row.position
|
|
26609
26825
|
}] : [];
|
|
26610
|
-
}).sort((left, right) => right.impressions - left.impressions || compareText$
|
|
26826
|
+
}).sort((left, right) => right.impressions - left.impressions || compareText$7(left.url, right.url));
|
|
26611
26827
|
const firstPartyStatus = !input.site ? "not-requested" : (firstPartyResponse?.rowsFetched ?? 0) >= MAX_GSC_PAGE_ROWS ? "partial" : firstPartyResponse?.rows.length === 0 ? "empty" : "complete";
|
|
26612
26828
|
const findings = patterns.slice(0, 5).map((pattern, index) => ({
|
|
26613
26829
|
code: "repeated-ranking-page-pattern",
|
|
@@ -26688,7 +26904,7 @@ async function serpCompetitorsReport(input, dependencies = {}) {
|
|
|
26688
26904
|
const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
26689
26905
|
const market = validatedMarket(input.market);
|
|
26690
26906
|
const providerId = validatedProvider(input.provider);
|
|
26691
|
-
const keywords = [...new Set(input.keywords.map(normalizedKeyword$1))].filter(Boolean).sort(compareText$
|
|
26907
|
+
const keywords = [...new Set(input.keywords.map(normalizedKeyword$1))].filter(Boolean).sort(compareText$7);
|
|
26692
26908
|
if (keywords.length < 2 || keywords.length > 200 || keywords.some((keyword) => keyword.length > 80 || keyword.split(/\s+/u).length > 10)) throw new SeoError("INVALID_INPUT", "SERP competitor research requires 2 to 200 keywords of at most 80 characters and 10 words.");
|
|
26693
26909
|
const target = input.targetDomain ? normalizeDomain$1(input.targetDomain) : null;
|
|
26694
26910
|
const declared = declaredCompetitors(input.declaredCompetitors);
|
|
@@ -27231,7 +27447,7 @@ function measurementWindow(input) {
|
|
|
27231
27447
|
}
|
|
27232
27448
|
//#endregion
|
|
27233
27449
|
//#region packages/core/src/analyze/experiments/measure-change.ts
|
|
27234
|
-
const defaultDependencies$
|
|
27450
|
+
const defaultDependencies$6 = {
|
|
27235
27451
|
searchAnalytics: querySearchAnalytics,
|
|
27236
27452
|
googleAnalyticsReport: runGa4Report,
|
|
27237
27453
|
contentGroup: getContentGroup,
|
|
@@ -27249,7 +27465,7 @@ function counterfactualDelta(input) {
|
|
|
27249
27465
|
const expectedAfter = input.treatmentBefore * (input.controlAfter / input.controlBefore);
|
|
27250
27466
|
return fixed(input.treatmentAfter - expectedAfter);
|
|
27251
27467
|
}
|
|
27252
|
-
async function measureChange(input, dependencies = defaultDependencies$
|
|
27468
|
+
async function measureChange(input, dependencies = defaultDependencies$6) {
|
|
27253
27469
|
const now = dependencies.now();
|
|
27254
27470
|
const change = (input.id ? getChange(input.id) : void 0) ?? {
|
|
27255
27471
|
id: "adhoc",
|
|
@@ -27974,14 +28190,14 @@ async function verifyInternalLinkCandidate(input) {
|
|
|
27974
28190
|
}
|
|
27975
28191
|
//#endregion
|
|
27976
28192
|
//#region packages/core/src/analyze/internal-links.ts
|
|
27977
|
-
const MAX_GSC_ROWS$
|
|
27978
|
-
const defaultDependencies$
|
|
28193
|
+
const MAX_GSC_ROWS$2 = 1e5;
|
|
28194
|
+
const defaultDependencies$5 = {
|
|
27979
28195
|
searchAnalytics: querySearchAnalytics,
|
|
27980
28196
|
fetch: fetchPage,
|
|
27981
28197
|
extract: extractPage,
|
|
27982
28198
|
now: () => /* @__PURE__ */ new Date()
|
|
27983
28199
|
};
|
|
27984
|
-
async function internalLinksReport(input, dependencies = defaultDependencies$
|
|
28200
|
+
async function internalLinksReport(input, dependencies = defaultDependencies$5) {
|
|
27985
28201
|
const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
|
|
27986
28202
|
targetPage.hash = "";
|
|
27987
28203
|
const targetUrl = targetPage.toString();
|
|
@@ -28030,7 +28246,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
28030
28246
|
dimensions: ["query", "page"],
|
|
28031
28247
|
type: "web",
|
|
28032
28248
|
dataState: "final",
|
|
28033
|
-
maxRows: MAX_GSC_ROWS$
|
|
28249
|
+
maxRows: MAX_GSC_ROWS$2,
|
|
28034
28250
|
dimensionFilterGroups: [{
|
|
28035
28251
|
groupType: "and",
|
|
28036
28252
|
filters: [{
|
|
@@ -28062,7 +28278,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
28062
28278
|
dimensions: ["query", "page"],
|
|
28063
28279
|
type: "web",
|
|
28064
28280
|
dataState: "final",
|
|
28065
|
-
maxRows: MAX_GSC_ROWS$
|
|
28281
|
+
maxRows: MAX_GSC_ROWS$2
|
|
28066
28282
|
}, { refresh: input.refresh }) : {
|
|
28067
28283
|
rows: [],
|
|
28068
28284
|
calls: 0,
|
|
@@ -28107,8 +28323,8 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
28107
28323
|
else checkedSources += 1;
|
|
28108
28324
|
}
|
|
28109
28325
|
const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
|
|
28110
|
-
const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$
|
|
28111
|
-
const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$
|
|
28326
|
+
const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$2);
|
|
28327
|
+
const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$2;
|
|
28112
28328
|
const invalidRows = analysis.selection.targetInvalidRows + analysis.selection.sourceInvalidRows;
|
|
28113
28329
|
const structuredWarnings = uniqueInternalLinksWarnings(warnings);
|
|
28114
28330
|
const dataStatus = target.verification === "technical-issue" ? "target-technical-issue" : target.verification === "failed" ? "partial" : targetRows.length === 0 ? "empty" : targetTruncated || sourceTruncated || invalidRows > 0 ? "partial" : queryCandidates && sourceResult.rows.length === 0 ? "source-empty" : analysis.selection.targetEligibleQueries === 0 || analysis.candidates.length === 0 ? "filtered" : failedChecks > 0 || uncheckedCandidates > 0 ? "partial" : "complete";
|
|
@@ -28149,18 +28365,18 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
28149
28365
|
pageFilter: alias,
|
|
28150
28366
|
rowsFetched: result.rowsFetched,
|
|
28151
28367
|
calls: result.calls,
|
|
28152
|
-
possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$
|
|
28368
|
+
possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$2
|
|
28153
28369
|
})),
|
|
28154
28370
|
rowsFetched: targetRequests.reduce((sum, { result }) => sum + result.rowsFetched, 0),
|
|
28155
28371
|
calls: targetRequests.reduce((sum, { result }) => sum + result.calls, 0),
|
|
28156
|
-
maxRowsPerRequest: MAX_GSC_ROWS$
|
|
28372
|
+
maxRowsPerRequest: MAX_GSC_ROWS$2,
|
|
28157
28373
|
possiblyTruncated: targetTruncated
|
|
28158
28374
|
},
|
|
28159
28375
|
candidates: {
|
|
28160
28376
|
queried: queryCandidates,
|
|
28161
28377
|
rowsFetched: sourceResult.rowsFetched,
|
|
28162
28378
|
calls: sourceResult.calls,
|
|
28163
|
-
maxRows: MAX_GSC_ROWS$
|
|
28379
|
+
maxRows: MAX_GSC_ROWS$2,
|
|
28164
28380
|
possiblyTruncated: sourceTruncated
|
|
28165
28381
|
},
|
|
28166
28382
|
completeness: !queryCandidates ? "not-queried" : targetTruncated || sourceTruncated ? "possibly-truncated" : "retained-rows-only"
|
|
@@ -28179,7 +28395,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
28179
28395
|
minImpressions,
|
|
28180
28396
|
resultLimit: limit,
|
|
28181
28397
|
checkLimit,
|
|
28182
|
-
maxGscRowsPerRequest: MAX_GSC_ROWS$
|
|
28398
|
+
maxGscRowsPerRequest: MAX_GSC_ROWS$2
|
|
28183
28399
|
},
|
|
28184
28400
|
target: {
|
|
28185
28401
|
requestedUrl: target.requestedUrl,
|
|
@@ -28469,7 +28685,7 @@ function providerReportError(error) {
|
|
|
28469
28685
|
if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
|
|
28470
28686
|
throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
28471
28687
|
}
|
|
28472
|
-
function validateInput$
|
|
28688
|
+
function validateInput$4(input) {
|
|
28473
28689
|
if (input.keywords.length < 1 || input.keywords.length > MAX_REPORT_KEYWORDS || input.keywords.some((keyword) => {
|
|
28474
28690
|
const value = keyword.trim();
|
|
28475
28691
|
return value.length < 1 || value.length > 80 || value.split(/\s+/u).length > 10;
|
|
@@ -28503,7 +28719,7 @@ function findings$1(analysis) {
|
|
|
28503
28719
|
}).sort((left, right) => Number(right.code === "recent-demand-increase") - Number(left.code === "recent-demand-increase") || right.recentAverage - left.recentAverage || compareCodepoints$2(left.keyword, right.keyword)).slice(0, 10).map(({ recentAverage: _recentAverage, ...finding }) => finding);
|
|
28504
28720
|
}
|
|
28505
28721
|
async function keywordMetricsReport(input, dependencies = {}) {
|
|
28506
|
-
const validated = validateInput$
|
|
28722
|
+
const validated = validateInput$4(input);
|
|
28507
28723
|
let candidates;
|
|
28508
28724
|
try {
|
|
28509
28725
|
candidates = dependencies.candidates ?? await defaultCandidates$3();
|
|
@@ -28801,16 +29017,16 @@ function benchmarkDetails(benchmark) {
|
|
|
28801
29017
|
positiveUrlSamples: benchmark.positiveUrlSamples
|
|
28802
29018
|
};
|
|
28803
29019
|
}
|
|
28804
|
-
function boundedInteger$
|
|
29020
|
+
function boundedInteger$3(input) {
|
|
28805
29021
|
const value = Number.isFinite(input.value) ? input.value ?? 0 : input.fallback;
|
|
28806
29022
|
return Math.min(input.maximum ?? Number.POSITIVE_INFINITY, Math.max(1, Math.floor(value)));
|
|
28807
29023
|
}
|
|
28808
29024
|
function analyzeQueryClustersFromRows(input) {
|
|
28809
|
-
const minImpressions = boundedInteger$
|
|
29025
|
+
const minImpressions = boundedInteger$3({
|
|
28810
29026
|
value: input.minImpressions,
|
|
28811
29027
|
fallback: 25
|
|
28812
29028
|
});
|
|
28813
|
-
const limit = boundedInteger$
|
|
29029
|
+
const limit = boundedInteger$3({
|
|
28814
29030
|
value: input.limit,
|
|
28815
29031
|
fallback: 25,
|
|
28816
29032
|
maximum: 100
|
|
@@ -28929,7 +29145,7 @@ function integerInRange(value, fallback, minimum, maximum) {
|
|
|
28929
29145
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
28930
29146
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
28931
29147
|
}
|
|
28932
|
-
function compareText$
|
|
29148
|
+
function compareText$5(left, right) {
|
|
28933
29149
|
const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
|
|
28934
29150
|
const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
|
|
28935
29151
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -28948,7 +29164,7 @@ function normalizedHttpUrl(value) {
|
|
|
28948
29164
|
return;
|
|
28949
29165
|
}
|
|
28950
29166
|
}
|
|
28951
|
-
function validRow(row, query, url) {
|
|
29167
|
+
function validRow$1(row, query, url) {
|
|
28952
29168
|
return Boolean(query && url) && Number.isFinite(row.clicks) && Number.isFinite(row.impressions) && Number.isFinite(row.ctr) && Number.isFinite(row.position) && row.clicks >= 0 && row.impressions > 0 && row.clicks <= row.impressions && row.ctr >= 0 && row.ctr <= 1 && row.position > 0;
|
|
28953
29169
|
}
|
|
28954
29170
|
function queryFromRow(row) {
|
|
@@ -28961,7 +29177,7 @@ function queryFromRow(row) {
|
|
|
28961
29177
|
};
|
|
28962
29178
|
}
|
|
28963
29179
|
function compareQueries(left, right) {
|
|
28964
|
-
return right.impressions - left.impressions || left.position - right.position || right.clicks - left.clicks || compareText$
|
|
29180
|
+
return right.impressions - left.impressions || left.position - right.position || right.clicks - left.clicks || compareText$5(left.query, right.query);
|
|
28965
29181
|
}
|
|
28966
29182
|
function recommendation(input) {
|
|
28967
29183
|
return {
|
|
@@ -29007,7 +29223,7 @@ function pageItem(url, rows) {
|
|
|
29007
29223
|
};
|
|
29008
29224
|
}
|
|
29009
29225
|
function compareItems$1(left, right) {
|
|
29010
|
-
return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$
|
|
29226
|
+
return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$5(left.url, right.url);
|
|
29011
29227
|
}
|
|
29012
29228
|
function analyzeSecondPageRows(input) {
|
|
29013
29229
|
const minImpressions = integerInRange(input.minImpressions, DEFAULT_MIN_IMPRESSIONS, 0, MAX_MIN_IMPRESSIONS);
|
|
@@ -29028,7 +29244,7 @@ function analyzeSecondPageRows(input) {
|
|
|
29028
29244
|
for (const row of input.rows) {
|
|
29029
29245
|
const query = row.keys[0]?.trim() ?? "";
|
|
29030
29246
|
const url = normalizedHttpUrl(row.keys[1]?.trim() ?? "");
|
|
29031
|
-
if (!validRow(row, query, url)) selection.invalidRows++;
|
|
29247
|
+
if (!validRow$1(row, query, url)) selection.invalidRows++;
|
|
29032
29248
|
else if (row.position <= 10 || row.position > 20) selection.outsidePositionRows++;
|
|
29033
29249
|
else if (shouldExcludeBrandQuery({
|
|
29034
29250
|
query,
|
|
@@ -29119,8 +29335,8 @@ function analyzeSecondPageRows(input) {
|
|
|
29119
29335
|
}
|
|
29120
29336
|
//#endregion
|
|
29121
29337
|
//#region packages/core/src/analyze/keyword-opportunities.ts
|
|
29122
|
-
const DEFAULT_DAYS = 28;
|
|
29123
|
-
const MAX_DAYS = 548;
|
|
29338
|
+
const DEFAULT_DAYS$1 = 28;
|
|
29339
|
+
const MAX_DAYS$2 = 548;
|
|
29124
29340
|
const DEFAULT_REPORT_LIMIT = 10;
|
|
29125
29341
|
const MAX_REPORT_LIMIT = 25;
|
|
29126
29342
|
const DEFAULT_KEYWORD_LIMIT = 30;
|
|
@@ -29137,7 +29353,7 @@ const SOURCE_ORDER = [
|
|
|
29137
29353
|
"second-page",
|
|
29138
29354
|
"striking-distance"
|
|
29139
29355
|
];
|
|
29140
|
-
function compareText$
|
|
29356
|
+
function compareText$4(left, right) {
|
|
29141
29357
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
29142
29358
|
}
|
|
29143
29359
|
function rounded(value, precision = 3) {
|
|
@@ -29149,7 +29365,7 @@ function integerOption$1(input) {
|
|
|
29149
29365
|
if (!Number.isInteger(input.value) || input.value < input.minimum || input.value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be a whole number between ${input.minimum} and ${input.maximum}.`);
|
|
29150
29366
|
return input.value;
|
|
29151
29367
|
}
|
|
29152
|
-
function validateInput$
|
|
29368
|
+
function validateInput$3(input) {
|
|
29153
29369
|
const site = input.site.trim();
|
|
29154
29370
|
if (!site || site.length > 2048) throw new SeoError("INVALID_INPUT", "Use a valid Search Console property.");
|
|
29155
29371
|
if (input.minImpressions !== void 0 && (!Number.isInteger(input.minImpressions) || input.minImpressions < 0 || input.minImpressions > 1e9)) throw new SeoError("INVALID_INPUT", "Minimum impressions must be a whole number between 0 and 1000000000.");
|
|
@@ -29167,9 +29383,9 @@ function validateInput$2(input) {
|
|
|
29167
29383
|
site,
|
|
29168
29384
|
days: integerOption$1({
|
|
29169
29385
|
value: input.days,
|
|
29170
|
-
fallback: DEFAULT_DAYS,
|
|
29386
|
+
fallback: DEFAULT_DAYS$1,
|
|
29171
29387
|
minimum: 1,
|
|
29172
|
-
maximum: MAX_DAYS,
|
|
29388
|
+
maximum: MAX_DAYS$2,
|
|
29173
29389
|
label: "Days"
|
|
29174
29390
|
}),
|
|
29175
29391
|
limit: integerOption$1({
|
|
@@ -29281,7 +29497,7 @@ function aggregateCandidates(rows) {
|
|
|
29281
29497
|
impressions: rounded(row.impressions),
|
|
29282
29498
|
ctr: rounded(row.ctr, 4),
|
|
29283
29499
|
averagePosition: rounded(row.position, 2)
|
|
29284
|
-
})).sort((left, right) => right.impressions - left.impressions || left.averagePosition - right.averagePosition || compareText$
|
|
29500
|
+
})).sort((left, right) => right.impressions - left.impressions || left.averagePosition - right.averagePosition || compareText$4(left.url, right.url));
|
|
29285
29501
|
return [keyword, {
|
|
29286
29502
|
keyword,
|
|
29287
29503
|
clicks: rounded(clicks),
|
|
@@ -29515,7 +29731,7 @@ function nextSteps(input) {
|
|
|
29515
29731
|
return steps;
|
|
29516
29732
|
}
|
|
29517
29733
|
async function keywordOpportunitiesReport(input, dependencies = {}) {
|
|
29518
|
-
const validated = validateInput$
|
|
29734
|
+
const validated = validateInput$3(input);
|
|
29519
29735
|
const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
29520
29736
|
const generatedAt = now.toISOString();
|
|
29521
29737
|
const range = finalGscDateRange(validated.days, now);
|
|
@@ -29883,7 +30099,7 @@ function providerError$3(error) {
|
|
|
29883
30099
|
if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
|
|
29884
30100
|
throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
29885
30101
|
}
|
|
29886
|
-
function validateInput$
|
|
30102
|
+
function validateInput$2(input) {
|
|
29887
30103
|
if (input.seeds.length < 1 || input.seeds.length > MAX_RESEARCH_SEEDS || input.seeds.some((seed) => {
|
|
29888
30104
|
const value = seed.trim();
|
|
29889
30105
|
return value.length < 1 || value.length > 80 || value.split(/\s+/u).length > 10;
|
|
@@ -29936,7 +30152,7 @@ async function keywordResearchReport(input, dependencies = {}) {
|
|
|
29936
30152
|
"suggestions"
|
|
29937
30153
|
];
|
|
29938
30154
|
const limit = input.limit ?? 50;
|
|
29939
|
-
const validated = validateInput$
|
|
30155
|
+
const validated = validateInput$2({
|
|
29940
30156
|
...input,
|
|
29941
30157
|
sources,
|
|
29942
30158
|
limit
|
|
@@ -30019,6 +30235,864 @@ async function keywordResearchReport(input, dependencies = {}) {
|
|
|
30019
30235
|
};
|
|
30020
30236
|
}
|
|
30021
30237
|
//#endregion
|
|
30238
|
+
//#region packages/core/src/analyze/local-search/intent.ts
|
|
30239
|
+
const NEARBY_PHRASES = [
|
|
30240
|
+
"closest to me",
|
|
30241
|
+
"in my area",
|
|
30242
|
+
"near me",
|
|
30243
|
+
"nearby"
|
|
30244
|
+
];
|
|
30245
|
+
const UK_POSTCODE = /\b[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}\b/iu;
|
|
30246
|
+
const US_ZIP_CODE = /\b\d{5}(?:-\d{4})?\b/u;
|
|
30247
|
+
const US_ZIP_CONTEXT = /\b(?:around|near|nearby|postal(?:\s+code)?|zip(?:\s+code)?)\b/iu;
|
|
30248
|
+
function normalizeLocalSearchText(value) {
|
|
30249
|
+
return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase();
|
|
30250
|
+
}
|
|
30251
|
+
function tokens(value) {
|
|
30252
|
+
return normalizeLocalSearchText(value).match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
30253
|
+
}
|
|
30254
|
+
function includesTokenSequence(source, candidate) {
|
|
30255
|
+
if (candidate.length === 0 || candidate.length > source.length) return false;
|
|
30256
|
+
return source.some((_, start) => candidate.every((token, offset) => source[start + offset] === token));
|
|
30257
|
+
}
|
|
30258
|
+
function termMatcher(terms) {
|
|
30259
|
+
const root = {
|
|
30260
|
+
children: /* @__PURE__ */ new Map(),
|
|
30261
|
+
terms: []
|
|
30262
|
+
};
|
|
30263
|
+
for (const term of terms) {
|
|
30264
|
+
let node = root;
|
|
30265
|
+
for (const token of tokens(term)) {
|
|
30266
|
+
const child = node.children.get(token) ?? {
|
|
30267
|
+
children: /* @__PURE__ */ new Map(),
|
|
30268
|
+
terms: []
|
|
30269
|
+
};
|
|
30270
|
+
node.children.set(token, child);
|
|
30271
|
+
node = child;
|
|
30272
|
+
}
|
|
30273
|
+
node.terms.push(term);
|
|
30274
|
+
}
|
|
30275
|
+
return (source) => {
|
|
30276
|
+
const matches = /* @__PURE__ */ new Set();
|
|
30277
|
+
for (let start = 0; start < source.length; start++) {
|
|
30278
|
+
let node = root;
|
|
30279
|
+
for (let index = start; index < source.length; index++) {
|
|
30280
|
+
node = node.children.get(source[index]);
|
|
30281
|
+
if (!node) break;
|
|
30282
|
+
for (const term of node.terms) matches.add(term);
|
|
30283
|
+
}
|
|
30284
|
+
}
|
|
30285
|
+
return [...matches].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
30286
|
+
};
|
|
30287
|
+
}
|
|
30288
|
+
function normalizeLocationTerms(values = []) {
|
|
30289
|
+
return [...new Set(values.map(normalizeLocalSearchText).filter(Boolean))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
30290
|
+
}
|
|
30291
|
+
function classifyLocalIntent(query, locationTerms) {
|
|
30292
|
+
return createLocalIntentClassifier(locationTerms)(query);
|
|
30293
|
+
}
|
|
30294
|
+
function createLocalIntentClassifier(locationTerms) {
|
|
30295
|
+
const matchLocations = termMatcher(locationTerms);
|
|
30296
|
+
return (query) => {
|
|
30297
|
+
const queryTokens = tokens(normalizeLocalSearchText(query));
|
|
30298
|
+
const matchedLocations = matchLocations(queryTokens);
|
|
30299
|
+
const matchedNearby = NEARBY_PHRASES.filter((phrase) => includesTokenSequence(queryTokens, tokens(phrase)));
|
|
30300
|
+
const ukPostcode = UK_POSTCODE.exec(query)?.[0] ?? null;
|
|
30301
|
+
const usZipCode = US_ZIP_CONTEXT.test(query) && US_ZIP_CODE.test(query) ? US_ZIP_CODE.exec(query)?.[0] ?? null : null;
|
|
30302
|
+
const postalCode = ukPostcode ?? usZipCode;
|
|
30303
|
+
const classes = [];
|
|
30304
|
+
if (matchedLocations.length > 0) classes.push("named-location");
|
|
30305
|
+
if (matchedNearby.length > 0) classes.push("nearby");
|
|
30306
|
+
if (postalCode) classes.push("postal-code");
|
|
30307
|
+
if (classes.length === 0) return null;
|
|
30308
|
+
return {
|
|
30309
|
+
heuristic: true,
|
|
30310
|
+
method: "explicit-local-intent-v1",
|
|
30311
|
+
classes,
|
|
30312
|
+
matchedTerms: [
|
|
30313
|
+
...matchedLocations,
|
|
30314
|
+
...matchedNearby,
|
|
30315
|
+
...postalCode ? [normalizeLocalSearchText(postalCode)] : []
|
|
30316
|
+
]
|
|
30317
|
+
};
|
|
30318
|
+
};
|
|
30319
|
+
}
|
|
30320
|
+
//#endregion
|
|
30321
|
+
//#region packages/core/src/analyze/local-search/analysis.ts
|
|
30322
|
+
function compareText$3(left, right) {
|
|
30323
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
30324
|
+
}
|
|
30325
|
+
function validRow(row) {
|
|
30326
|
+
const query = row.keys[0]?.trim() ?? "";
|
|
30327
|
+
const page = row.keys[1]?.trim() ?? "";
|
|
30328
|
+
let validPage = false;
|
|
30329
|
+
try {
|
|
30330
|
+
validPage = ["http:", "https:"].includes(new URL(page).protocol);
|
|
30331
|
+
} catch {
|
|
30332
|
+
validPage = false;
|
|
30333
|
+
}
|
|
30334
|
+
if (row.keys.length !== 2 || !query || !validPage || !Number.isFinite(row.clicks) || !Number.isFinite(row.impressions) || !Number.isFinite(row.ctr) || !Number.isFinite(row.position) || row.clicks < 0 || row.impressions <= 0 || row.clicks > row.impressions || row.ctr < 0 || row.ctr > 1 || row.position <= 0) return null;
|
|
30335
|
+
return {
|
|
30336
|
+
query,
|
|
30337
|
+
page,
|
|
30338
|
+
clicks: row.clicks,
|
|
30339
|
+
impressions: row.impressions,
|
|
30340
|
+
ctr: row.ctr,
|
|
30341
|
+
position: row.position
|
|
30342
|
+
};
|
|
30343
|
+
}
|
|
30344
|
+
function rowSignature(row) {
|
|
30345
|
+
return `${row.clicks}\u0000${row.impressions}\u0000${row.ctr}\u0000${row.position}`;
|
|
30346
|
+
}
|
|
30347
|
+
function normalizeRows(rows, selection) {
|
|
30348
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
30349
|
+
for (const source of rows) {
|
|
30350
|
+
const row = validRow(source);
|
|
30351
|
+
if (!row) {
|
|
30352
|
+
selection.invalidRows++;
|
|
30353
|
+
continue;
|
|
30354
|
+
}
|
|
30355
|
+
const key = `${row.query}\u0000${row.page}`;
|
|
30356
|
+
grouped.set(key, [...grouped.get(key) ?? [], row]);
|
|
30357
|
+
}
|
|
30358
|
+
const result = [];
|
|
30359
|
+
for (const group of grouped.values()) {
|
|
30360
|
+
if (new Set(group.map(rowSignature)).size > 1) {
|
|
30361
|
+
selection.conflictingRows += group.length;
|
|
30362
|
+
continue;
|
|
30363
|
+
}
|
|
30364
|
+
selection.exactDuplicateRows += group.length - 1;
|
|
30365
|
+
const row = group[0];
|
|
30366
|
+
if (row) result.push(row);
|
|
30367
|
+
}
|
|
30368
|
+
return result;
|
|
30369
|
+
}
|
|
30370
|
+
function opportunityAction(position, pageCount) {
|
|
30371
|
+
if (pageCount > 1) return "review-page-overlap";
|
|
30372
|
+
if (position <= 3) return "protect-visibility";
|
|
30373
|
+
if (position <= 20) return "improve-existing-page";
|
|
30374
|
+
return "investigate-relevance";
|
|
30375
|
+
}
|
|
30376
|
+
function aggregateQueries(input) {
|
|
30377
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
30378
|
+
for (const row of input.rows) {
|
|
30379
|
+
if (isLowActionabilityQuery(row.query)) {
|
|
30380
|
+
input.selection.lowActionabilityRows++;
|
|
30381
|
+
continue;
|
|
30382
|
+
}
|
|
30383
|
+
if (shouldExcludeBrandQuery({
|
|
30384
|
+
query: row.query,
|
|
30385
|
+
siteUrl: input.site,
|
|
30386
|
+
brandTerms: input.brandTerms,
|
|
30387
|
+
includeBrand: input.includeBrand
|
|
30388
|
+
})) {
|
|
30389
|
+
input.selection.brandRows++;
|
|
30390
|
+
continue;
|
|
30391
|
+
}
|
|
30392
|
+
grouped.set(row.query, [...grouped.get(row.query) ?? [], row]);
|
|
30393
|
+
}
|
|
30394
|
+
const opportunities = [];
|
|
30395
|
+
for (const [query, rows] of grouped) {
|
|
30396
|
+
const intent = input.classify(query);
|
|
30397
|
+
if (!intent) {
|
|
30398
|
+
input.selection.nonLocalRows += rows.length;
|
|
30399
|
+
continue;
|
|
30400
|
+
}
|
|
30401
|
+
const impressions = rows.reduce((sum, row) => sum + row.impressions, 0);
|
|
30402
|
+
if (impressions < input.minImpressions) {
|
|
30403
|
+
input.selection.belowMinimumRows += rows.length;
|
|
30404
|
+
continue;
|
|
30405
|
+
}
|
|
30406
|
+
const clicks = rows.reduce((sum, row) => sum + row.clicks, 0);
|
|
30407
|
+
const averagePosition = rows.reduce((sum, row) => sum + row.position * row.impressions, 0) / impressions;
|
|
30408
|
+
const pages = rows.map((row) => ({
|
|
30409
|
+
url: row.page,
|
|
30410
|
+
clicks: row.clicks,
|
|
30411
|
+
impressions: row.impressions,
|
|
30412
|
+
ctr: row.impressions ? row.clicks / row.impressions : 0,
|
|
30413
|
+
averagePosition: row.position
|
|
30414
|
+
})).sort((left, right) => right.impressions - left.impressions || right.clicks - left.clicks || left.averagePosition - right.averagePosition || compareText$3(left.url, right.url));
|
|
30415
|
+
opportunities.push({
|
|
30416
|
+
query,
|
|
30417
|
+
clicks,
|
|
30418
|
+
impressions,
|
|
30419
|
+
ctr: impressions ? clicks / impressions : 0,
|
|
30420
|
+
averagePosition,
|
|
30421
|
+
intent,
|
|
30422
|
+
action: opportunityAction(averagePosition, pages.length),
|
|
30423
|
+
pages,
|
|
30424
|
+
pageCoverage: {
|
|
30425
|
+
available: pages.length,
|
|
30426
|
+
returned: Math.min(3, pages.length),
|
|
30427
|
+
omitted: Math.max(0, pages.length - 3)
|
|
30428
|
+
}
|
|
30429
|
+
});
|
|
30430
|
+
}
|
|
30431
|
+
return opportunities.sort((left, right) => right.impressions - left.impressions || right.clicks - left.clicks || left.averagePosition - right.averagePosition || compareText$3(left.query, right.query));
|
|
30432
|
+
}
|
|
30433
|
+
function localTemplates(opportunities) {
|
|
30434
|
+
const clusters = clusterPseoTemplates(opportunities.flatMap((item) => item.pages.map((page) => page.url)), {
|
|
30435
|
+
minUrls: 2,
|
|
30436
|
+
minShare: 0,
|
|
30437
|
+
limit: 10,
|
|
30438
|
+
sampleSize: 5
|
|
30439
|
+
});
|
|
30440
|
+
return clusters.map((cluster) => {
|
|
30441
|
+
const matched = opportunities.flatMap((item) => item.pages.filter((page) => templateForUrl(page.url, clusters) === cluster.signature).map((page) => ({
|
|
30442
|
+
query: item.query,
|
|
30443
|
+
page
|
|
30444
|
+
})));
|
|
30445
|
+
return {
|
|
30446
|
+
heuristic: true,
|
|
30447
|
+
signature: cluster.signature,
|
|
30448
|
+
urlCount: cluster.urlCount,
|
|
30449
|
+
sampleUrls: cluster.sampleUrls,
|
|
30450
|
+
queryCount: new Set(matched.map((item) => item.query)).size,
|
|
30451
|
+
clicks: matched.reduce((sum, item) => sum + item.page.clicks, 0),
|
|
30452
|
+
impressions: matched.reduce((sum, item) => sum + item.page.impressions, 0)
|
|
30453
|
+
};
|
|
30454
|
+
});
|
|
30455
|
+
}
|
|
30456
|
+
function analyzeLocalSearchRows(input) {
|
|
30457
|
+
const selection = {
|
|
30458
|
+
sourceRows: input.rows.length,
|
|
30459
|
+
invalidRows: 0,
|
|
30460
|
+
exactDuplicateRows: 0,
|
|
30461
|
+
conflictingRows: 0,
|
|
30462
|
+
lowActionabilityRows: 0,
|
|
30463
|
+
brandRows: 0,
|
|
30464
|
+
nonLocalRows: 0,
|
|
30465
|
+
belowMinimumRows: 0,
|
|
30466
|
+
eligibleQueries: 0,
|
|
30467
|
+
returnedQueries: 0,
|
|
30468
|
+
omittedQueries: 0,
|
|
30469
|
+
limit: input.limit,
|
|
30470
|
+
minImpressions: input.minImpressions
|
|
30471
|
+
};
|
|
30472
|
+
const normalized = normalizeRows(input.rows, selection);
|
|
30473
|
+
const eligible = aggregateQueries({
|
|
30474
|
+
...input,
|
|
30475
|
+
rows: normalized,
|
|
30476
|
+
selection,
|
|
30477
|
+
classify: createLocalIntentClassifier(input.locationTerms)
|
|
30478
|
+
});
|
|
30479
|
+
selection.eligibleQueries = eligible.length;
|
|
30480
|
+
selection.returnedQueries = Math.min(input.limit, eligible.length);
|
|
30481
|
+
selection.omittedQueries = Math.max(0, eligible.length - input.limit);
|
|
30482
|
+
return {
|
|
30483
|
+
selection,
|
|
30484
|
+
eligibleSummary: {
|
|
30485
|
+
clicks: eligible.reduce((sum, item) => sum + item.clicks, 0),
|
|
30486
|
+
impressions: eligible.reduce((sum, item) => sum + item.impressions, 0),
|
|
30487
|
+
namedLocationQueries: eligible.filter((item) => item.intent.classes.includes("named-location")).length,
|
|
30488
|
+
nearbyQueries: eligible.filter((item) => item.intent.classes.includes("nearby")).length,
|
|
30489
|
+
postalCodeQueries: eligible.filter((item) => item.intent.classes.includes("postal-code")).length,
|
|
30490
|
+
pageOverlapQueries: eligible.filter((item) => item.action === "review-page-overlap").length
|
|
30491
|
+
},
|
|
30492
|
+
eligiblePageCount: new Set(eligible.flatMap((item) => item.pages.map((page) => page.url))).size,
|
|
30493
|
+
opportunities: eligible.slice(0, input.limit).map((item) => ({
|
|
30494
|
+
...item,
|
|
30495
|
+
pages: item.pages.slice(0, 3)
|
|
30496
|
+
})),
|
|
30497
|
+
templates: localTemplates(eligible)
|
|
30498
|
+
};
|
|
30499
|
+
}
|
|
30500
|
+
//#endregion
|
|
30501
|
+
//#region packages/core/src/providers/dataforseo/serp-snapshot.ts
|
|
30502
|
+
const MAX_SERP_DEPTH = 100;
|
|
30503
|
+
const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
|
|
30504
|
+
function safeUrl$1(value) {
|
|
30505
|
+
if (!value) return null;
|
|
30506
|
+
try {
|
|
30507
|
+
const parsed = new URL(value);
|
|
30508
|
+
if (!["http:", "https:"].includes(parsed.protocol)) return null;
|
|
30509
|
+
parsed.username = "";
|
|
30510
|
+
parsed.password = "";
|
|
30511
|
+
return parsed.toString();
|
|
30512
|
+
} catch {
|
|
30513
|
+
return null;
|
|
30514
|
+
}
|
|
30515
|
+
}
|
|
30516
|
+
function validOrganic(item) {
|
|
30517
|
+
return item.type === "organic" && Number.isSafeInteger(item.rank_group) && (item.rank_group ?? 0) > 0 && Number.isSafeInteger(item.rank_absolute) && (item.rank_absolute ?? 0) > 0 && Number.isSafeInteger(item.page) && (item.page ?? 0) > 0 && Boolean(item.domain?.trim()) && safeUrl$1(item.url) !== null;
|
|
30518
|
+
}
|
|
30519
|
+
function organicResult(item) {
|
|
30520
|
+
if (!validOrganic(item)) return null;
|
|
30521
|
+
const url = safeUrl$1(item.url);
|
|
30522
|
+
if (!url) return null;
|
|
30523
|
+
return {
|
|
30524
|
+
rankGroup: item.rank_group,
|
|
30525
|
+
rankAbsolute: item.rank_absolute,
|
|
30526
|
+
page: item.page,
|
|
30527
|
+
domain: item.domain.trim().toLowerCase(),
|
|
30528
|
+
url,
|
|
30529
|
+
title: item.title ?? null,
|
|
30530
|
+
description: item.description ?? null,
|
|
30531
|
+
isFeaturedSnippet: item.is_featured_snippet ?? null
|
|
30532
|
+
};
|
|
30533
|
+
}
|
|
30534
|
+
function checkedAt(value, fallback) {
|
|
30535
|
+
const timestamp = Date.parse(value);
|
|
30536
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
|
|
30537
|
+
}
|
|
30538
|
+
function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
|
|
30539
|
+
const market = searchMarketSchema.parse(input.market);
|
|
30540
|
+
const keyword = normalizedKeyword$2(input.keyword);
|
|
30541
|
+
const location = locationRequest(market, "serp-snapshot");
|
|
30542
|
+
const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$2(item.keyword) === keyword);
|
|
30543
|
+
if (!result) throw new ProviderError({
|
|
30544
|
+
provider: "dataforseo",
|
|
30545
|
+
operation: "serp-snapshot",
|
|
30546
|
+
code: "invalid-response",
|
|
30547
|
+
message: "DataForSEO returned no matching SERP result."
|
|
30548
|
+
});
|
|
30549
|
+
const warnings = [...snapshot.warnings];
|
|
30550
|
+
const items = result.items ?? [];
|
|
30551
|
+
const organicItems = items.filter((item) => item.type === "organic");
|
|
30552
|
+
const mappedOrganicResults = organicItems.flatMap((item) => {
|
|
30553
|
+
const mapped = organicResult(item);
|
|
30554
|
+
return mapped ? [mapped] : [];
|
|
30555
|
+
}).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
|
|
30556
|
+
const organicResults = mappedOrganicResults.slice(0, input.depth);
|
|
30557
|
+
const invalidRows = organicItems.length - mappedOrganicResults.length;
|
|
30558
|
+
const resultCapped = mappedOrganicResults.length > input.depth;
|
|
30559
|
+
const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
|
|
30560
|
+
if (invalidRows > 0) warnings.push({
|
|
30561
|
+
code: "invalid-organic-results",
|
|
30562
|
+
field: "organicResults",
|
|
30563
|
+
message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
|
|
30564
|
+
});
|
|
30565
|
+
if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
|
|
30566
|
+
code: "invalid-serp-observation-time",
|
|
30567
|
+
field: "checkedAt",
|
|
30568
|
+
message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
|
|
30569
|
+
});
|
|
30570
|
+
const checkUrl = safeUrl$1(result.check_url);
|
|
30571
|
+
if (result.check_url && !checkUrl) warnings.push({
|
|
30572
|
+
code: "invalid-serp-check-url",
|
|
30573
|
+
field: "checkUrl",
|
|
30574
|
+
message: "DataForSEO returned an invalid SERP check URL."
|
|
30575
|
+
});
|
|
30576
|
+
if (invalidResultCount) warnings.push({
|
|
30577
|
+
code: "invalid-serp-result-count",
|
|
30578
|
+
field: "resultCount",
|
|
30579
|
+
message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
|
|
30580
|
+
});
|
|
30581
|
+
const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
|
|
30582
|
+
return {
|
|
30583
|
+
schemaVersion: 1,
|
|
30584
|
+
provider: "dataforseo",
|
|
30585
|
+
capability: "serp-snapshot",
|
|
30586
|
+
data: {
|
|
30587
|
+
keyword,
|
|
30588
|
+
effectiveKeyword: normalizedKeyword$2(result.spell?.keyword ?? result.keyword),
|
|
30589
|
+
searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
|
|
30590
|
+
checkedAt: checkedAt(result.datetime, snapshot.observedAt),
|
|
30591
|
+
checkUrl,
|
|
30592
|
+
resultCount: invalidResultCount ? null : result.se_results_count ?? null,
|
|
30593
|
+
pagesCount: result.pages_count === void 0 ? null : result.pages_count,
|
|
30594
|
+
features,
|
|
30595
|
+
organicResults
|
|
30596
|
+
},
|
|
30597
|
+
observedAt: snapshot.observedAt,
|
|
30598
|
+
market: {
|
|
30599
|
+
...market,
|
|
30600
|
+
device: market.device ?? "desktop"
|
|
30601
|
+
},
|
|
30602
|
+
coverage: {
|
|
30603
|
+
requestedRows: input.depth,
|
|
30604
|
+
returnedRows: snapshot.returnedRows,
|
|
30605
|
+
retainedRows: organicResults.length,
|
|
30606
|
+
invalidRows,
|
|
30607
|
+
providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
|
|
30608
|
+
completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
|
|
30609
|
+
nextCursor: null
|
|
30610
|
+
},
|
|
30611
|
+
cache: snapshot.cache,
|
|
30612
|
+
cost: snapshot.cost,
|
|
30613
|
+
request: {
|
|
30614
|
+
operation: "serp-snapshot",
|
|
30615
|
+
endpoint,
|
|
30616
|
+
limit: input.depth,
|
|
30617
|
+
filters: {
|
|
30618
|
+
countryCode: market.countryCode,
|
|
30619
|
+
languageCode: market.languageCode,
|
|
30620
|
+
device: market.device ?? "desktop",
|
|
30621
|
+
...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
|
|
30622
|
+
},
|
|
30623
|
+
sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
|
|
30624
|
+
},
|
|
30625
|
+
warnings
|
|
30626
|
+
};
|
|
30627
|
+
}
|
|
30628
|
+
var DataForSeoSerpSnapshotProvider = class {
|
|
30629
|
+
provider = "dataforseo";
|
|
30630
|
+
capabilitySupport = [{
|
|
30631
|
+
capability: "serp-snapshot",
|
|
30632
|
+
status: "available",
|
|
30633
|
+
markets: [{
|
|
30634
|
+
searchEngines: ["google"],
|
|
30635
|
+
devices: ["desktop", "mobile"],
|
|
30636
|
+
location: "any"
|
|
30637
|
+
}]
|
|
30638
|
+
}];
|
|
30639
|
+
client;
|
|
30640
|
+
constructor(options = {}) {
|
|
30641
|
+
this.client = options.client ?? new DataForSeoClient(options);
|
|
30642
|
+
}
|
|
30643
|
+
async serpSnapshot(input) {
|
|
30644
|
+
const market = searchMarketSchema.parse(input.market);
|
|
30645
|
+
if (market.searchEngine !== "google") throw new ProviderError({
|
|
30646
|
+
provider: "dataforseo",
|
|
30647
|
+
operation: "serp-snapshot",
|
|
30648
|
+
code: "configuration",
|
|
30649
|
+
message: "DataForSEO SERP snapshots currently support Google."
|
|
30650
|
+
});
|
|
30651
|
+
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH) throw new ProviderError({
|
|
30652
|
+
provider: "dataforseo",
|
|
30653
|
+
operation: "serp-snapshot",
|
|
30654
|
+
code: "configuration",
|
|
30655
|
+
message: "SERP depth must be from 1 to 100."
|
|
30656
|
+
});
|
|
30657
|
+
const keyword = normalizedKeyword$2(input.keyword);
|
|
30658
|
+
if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new ProviderError({
|
|
30659
|
+
provider: "dataforseo",
|
|
30660
|
+
operation: "serp-snapshot",
|
|
30661
|
+
code: "configuration",
|
|
30662
|
+
message: "SERP snapshots require a keyword of at most 80 characters and 10 words."
|
|
30663
|
+
});
|
|
30664
|
+
const context = input.context ?? {
|
|
30665
|
+
reportId: "serp-results",
|
|
30666
|
+
reportRunId: randomUUID()
|
|
30667
|
+
};
|
|
30668
|
+
const location = locationRequest(market, "serp-snapshot");
|
|
30669
|
+
return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
|
|
30670
|
+
keyword,
|
|
30671
|
+
languageCode: market.languageCode.split("-")[0],
|
|
30672
|
+
...location,
|
|
30673
|
+
device: market.device ?? "desktop",
|
|
30674
|
+
depth: input.depth,
|
|
30675
|
+
refresh: input.refresh,
|
|
30676
|
+
context
|
|
30677
|
+
}));
|
|
30678
|
+
}
|
|
30679
|
+
};
|
|
30680
|
+
//#endregion
|
|
30681
|
+
//#region packages/core/src/analyze/serp-results.ts
|
|
30682
|
+
const MAX_REPORT_DEPTH = 100;
|
|
30683
|
+
function compareCodepoints(left, right) {
|
|
30684
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
30685
|
+
}
|
|
30686
|
+
function snapshotProvider(candidate) {
|
|
30687
|
+
return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
|
|
30688
|
+
}
|
|
30689
|
+
async function defaultCandidates$1() {
|
|
30690
|
+
return [{
|
|
30691
|
+
adapter: new DataForSeoSerpSnapshotProvider(),
|
|
30692
|
+
connected: Boolean(await readDataForSeoCredentials()),
|
|
30693
|
+
priority: 10
|
|
30694
|
+
}];
|
|
30695
|
+
}
|
|
30696
|
+
function providerError$2(error) {
|
|
30697
|
+
if (!(error instanceof ProviderError)) throw error;
|
|
30698
|
+
if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
|
|
30699
|
+
if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
|
|
30700
|
+
throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
30701
|
+
}
|
|
30702
|
+
function validateInput$1(input) {
|
|
30703
|
+
const keyword = input.keyword.trim().replace(/\s+/gu, " ");
|
|
30704
|
+
if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new SeoError("INVALID_INPUT", "SERP results requires a keyword of at most 80 characters and 10 words.");
|
|
30705
|
+
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_REPORT_DEPTH) throw new SeoError("INVALID_INPUT", `SERP result depth must be from 1 to ${MAX_REPORT_DEPTH}.`);
|
|
30706
|
+
const market = searchMarketSchema.safeParse(input.market);
|
|
30707
|
+
if (!market.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
|
|
30708
|
+
const provider = input.provider ? providerIdSchema.safeParse(input.provider) : void 0;
|
|
30709
|
+
if (provider && !provider.success) throw new SeoError("INVALID_INPUT", "Use a supported SERP provider.");
|
|
30710
|
+
return {
|
|
30711
|
+
keyword,
|
|
30712
|
+
market: market.data,
|
|
30713
|
+
provider: provider?.data
|
|
30714
|
+
};
|
|
30715
|
+
}
|
|
30716
|
+
function reportStatus$1(evidence) {
|
|
30717
|
+
if (evidence.coverage.completeness === "unavailable" || evidence.coverage.completeness === "invalid") return "unavailable";
|
|
30718
|
+
return evidence.coverage.completeness === "complete" ? "complete" : "partial";
|
|
30719
|
+
}
|
|
30720
|
+
function domainSummaries(snapshot) {
|
|
30721
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
30722
|
+
for (const result of snapshot.organicResults) byDomain.set(result.domain, [...byDomain.get(result.domain) ?? [], result.rankAbsolute]);
|
|
30723
|
+
return [...byDomain.entries()].map(([domain, ranks]) => ({
|
|
30724
|
+
domain,
|
|
30725
|
+
resultCount: ranks.length,
|
|
30726
|
+
ranks: [...ranks].sort((left, right) => left - right)
|
|
30727
|
+
})).sort((left, right) => right.resultCount - left.resultCount || (left.ranks[0] ?? Number.MAX_SAFE_INTEGER) - (right.ranks[0] ?? Number.MAX_SAFE_INTEGER) || compareCodepoints(left.domain, right.domain));
|
|
30728
|
+
}
|
|
30729
|
+
function reportFindings(snapshot, domains) {
|
|
30730
|
+
const findings = [];
|
|
30731
|
+
if (snapshot.effectiveKeyword !== snapshot.keyword) findings.push({
|
|
30732
|
+
code: "query-correction",
|
|
30733
|
+
evidenceRef: "evidence.data.effectiveKeyword",
|
|
30734
|
+
principle: "A corrected query changes the observed result context and does not prove the original wording has the same intent.",
|
|
30735
|
+
detail: `The provider observed results for ${snapshot.effectiveKeyword} after the requested query ${snapshot.keyword}.`
|
|
30736
|
+
});
|
|
30737
|
+
for (const [index, domain] of domains.entries()) {
|
|
30738
|
+
if (domain.resultCount <= 1) continue;
|
|
30739
|
+
findings.push({
|
|
30740
|
+
code: "repeated-domain",
|
|
30741
|
+
evidenceRef: `domains[${index}]`,
|
|
30742
|
+
principle: "Repeated results are descriptive snapshot evidence, not a domain strength or quality score.",
|
|
30743
|
+
detail: `${domain.domain} appears ${domain.resultCount} times at absolute ranks ${domain.ranks.join(", ")}.`
|
|
30744
|
+
});
|
|
30745
|
+
}
|
|
30746
|
+
return findings.slice(0, 10);
|
|
30747
|
+
}
|
|
30748
|
+
async function serpResultsReport(input, dependencies = {}) {
|
|
30749
|
+
const depth = input.depth ?? 10;
|
|
30750
|
+
const validated = validateInput$1({
|
|
30751
|
+
...input,
|
|
30752
|
+
depth
|
|
30753
|
+
});
|
|
30754
|
+
let candidates;
|
|
30755
|
+
try {
|
|
30756
|
+
candidates = dependencies.candidates ?? await defaultCandidates$1();
|
|
30757
|
+
} catch (error) {
|
|
30758
|
+
return providerError$2(error);
|
|
30759
|
+
}
|
|
30760
|
+
const resolution = resolveProvider({
|
|
30761
|
+
capability: "serp-snapshot",
|
|
30762
|
+
market: validated.market,
|
|
30763
|
+
candidates,
|
|
30764
|
+
provider: validated.provider
|
|
30765
|
+
});
|
|
30766
|
+
if (resolution.status === "unavailable") throw new SeoError("PROVIDER_UNAVAILABLE", resolution.reason === "provider-not-connected" ? "No connected provider can return live SERP results. Run `seo providers dataforseo connect` first." : validated.provider ? `${validated.provider} cannot return live SERP results for this market.` : "No configured provider can return live SERP results for this market.");
|
|
30767
|
+
const provider = snapshotProvider(resolution.provider);
|
|
30768
|
+
if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no live SERP implementation.");
|
|
30769
|
+
let evidence;
|
|
30770
|
+
try {
|
|
30771
|
+
evidence = await provider.serpSnapshot({
|
|
30772
|
+
keyword: validated.keyword,
|
|
30773
|
+
market: validated.market,
|
|
30774
|
+
depth,
|
|
30775
|
+
refresh: input.refresh,
|
|
30776
|
+
context: {
|
|
30777
|
+
projectId: input.context?.projectId ?? input.projectId,
|
|
30778
|
+
reportId: input.context?.reportId ?? "serp-results",
|
|
30779
|
+
reportRunId: input.context?.reportRunId ?? randomUUID()
|
|
30780
|
+
}
|
|
30781
|
+
});
|
|
30782
|
+
} catch (error) {
|
|
30783
|
+
return providerError$2(error);
|
|
30784
|
+
}
|
|
30785
|
+
const domains = domainSummaries(evidence.data);
|
|
30786
|
+
const dataStatus = reportStatus$1(evidence);
|
|
30787
|
+
const generatedAt = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
30788
|
+
const correctedQuery = evidence.data.effectiveKeyword !== evidence.data.keyword;
|
|
30789
|
+
return {
|
|
30790
|
+
schemaVersion: 1,
|
|
30791
|
+
generatedAt,
|
|
30792
|
+
dataStatus,
|
|
30793
|
+
market: evidence.market,
|
|
30794
|
+
summary: {
|
|
30795
|
+
keyword: evidence.data.keyword,
|
|
30796
|
+
effectiveKeyword: evidence.data.effectiveKeyword,
|
|
30797
|
+
requestedDepth: depth,
|
|
30798
|
+
organicResults: evidence.data.organicResults.length,
|
|
30799
|
+
uniqueDomains: domains.length,
|
|
30800
|
+
observedFeatures: evidence.data.features.length,
|
|
30801
|
+
correctedQuery,
|
|
30802
|
+
verdict: `${evidence.data.organicResults.length} organic results across ${domains.length} domains were retained from the ${evidence.market.device ?? "desktop"} snapshot.`
|
|
30803
|
+
},
|
|
30804
|
+
evidence,
|
|
30805
|
+
domains,
|
|
30806
|
+
findings: reportFindings(evidence.data, domains),
|
|
30807
|
+
caveats: [
|
|
30808
|
+
"This is one market, location, language, and device-specific result snapshot; it is not rank history.",
|
|
30809
|
+
"Search results can change between checks and may differ from a person's signed-in or personalized results.",
|
|
30810
|
+
"The provider result count is an estimate and may be missing; retained organic rows are bounded by the requested depth.",
|
|
30811
|
+
"Repeated domains, result features, titles, and snippets do not establish content quality, authority, or ranking feasibility."
|
|
30812
|
+
],
|
|
30813
|
+
nextSteps: [
|
|
30814
|
+
"Review the retained pages and result types before deciding whether the query fits an existing page, a new page, or no page.",
|
|
30815
|
+
"Keep Search Console average position separate from these exact snapshot ranks when comparing first-party performance.",
|
|
30816
|
+
"Repeat the same market and device check later if the decision depends on result stability."
|
|
30817
|
+
]
|
|
30818
|
+
};
|
|
30819
|
+
}
|
|
30820
|
+
//#endregion
|
|
30821
|
+
//#region packages/core/src/analyze/local-search/report.ts
|
|
30822
|
+
const MAX_DAYS$1 = 548;
|
|
30823
|
+
const MAX_ROWS = 5e4;
|
|
30824
|
+
function boundedInteger$2(input) {
|
|
30825
|
+
const value = input.value ?? input.fallback;
|
|
30826
|
+
if (!Number.isSafeInteger(value) || value < input.minimum || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be a whole number between ${input.minimum} and ${input.maximum}.`);
|
|
30827
|
+
return value;
|
|
30828
|
+
}
|
|
30829
|
+
function validateInput(input) {
|
|
30830
|
+
const site = input.site.trim();
|
|
30831
|
+
if (!site) throw new SeoError("INVALID_INPUT", "site must not be empty.");
|
|
30832
|
+
if (input.locationTerms && (input.locationTerms.length > 100 || input.locationTerms.some((term) => !term.trim() || term.length > 100))) throw new SeoError("INVALID_INPUT", "locationTerms must contain at most 100 non-empty terms of 100 characters or fewer.");
|
|
30833
|
+
if (input.brandTerms && (input.brandTerms.length > 20 || input.brandTerms.some((term) => !term.trim() || term.length > 200))) throw new SeoError("INVALID_INPUT", "brandTerms must contain at most 20 non-empty terms of 200 characters or fewer.");
|
|
30834
|
+
const includeSerps = input.includeSerps ?? false;
|
|
30835
|
+
let market;
|
|
30836
|
+
if (input.market) {
|
|
30837
|
+
const parsed = searchMarketSchema.safeParse(input.market);
|
|
30838
|
+
if (!parsed.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
|
|
30839
|
+
market = parsed.data;
|
|
30840
|
+
}
|
|
30841
|
+
if (includeSerps && !market?.location) throw new SeoError("INVALID_INPUT", "Local SERP evidence requires a country, language, and canonical location.");
|
|
30842
|
+
if (!includeSerps && (input.market || input.provider || input.serpLimit !== void 0 || input.serpDepth !== void 0)) throw new SeoError("INVALID_INPUT", "Set includeSerps to true before passing market, provider, or SERP options.");
|
|
30843
|
+
return {
|
|
30844
|
+
site,
|
|
30845
|
+
days: boundedInteger$2({
|
|
30846
|
+
value: input.days,
|
|
30847
|
+
fallback: 28,
|
|
30848
|
+
minimum: 1,
|
|
30849
|
+
maximum: MAX_DAYS$1,
|
|
30850
|
+
label: "days"
|
|
30851
|
+
}),
|
|
30852
|
+
locationTerms: normalizeLocationTerms(input.locationTerms),
|
|
30853
|
+
minImpressions: boundedInteger$2({
|
|
30854
|
+
value: input.minImpressions,
|
|
30855
|
+
fallback: 1,
|
|
30856
|
+
minimum: 0,
|
|
30857
|
+
maximum: 1e9,
|
|
30858
|
+
label: "minImpressions"
|
|
30859
|
+
}),
|
|
30860
|
+
limit: boundedInteger$2({
|
|
30861
|
+
value: input.limit,
|
|
30862
|
+
fallback: 25,
|
|
30863
|
+
minimum: 1,
|
|
30864
|
+
maximum: 100,
|
|
30865
|
+
label: "limit"
|
|
30866
|
+
}),
|
|
30867
|
+
maxRows: boundedInteger$2({
|
|
30868
|
+
value: input.maxRows,
|
|
30869
|
+
fallback: MAX_ROWS,
|
|
30870
|
+
minimum: 1,
|
|
30871
|
+
maximum: MAX_ROWS,
|
|
30872
|
+
label: "maxRows"
|
|
30873
|
+
}),
|
|
30874
|
+
includeSerps,
|
|
30875
|
+
market,
|
|
30876
|
+
serpLimit: boundedInteger$2({
|
|
30877
|
+
value: input.serpLimit,
|
|
30878
|
+
fallback: 3,
|
|
30879
|
+
minimum: 1,
|
|
30880
|
+
maximum: 3,
|
|
30881
|
+
label: "serpLimit"
|
|
30882
|
+
}),
|
|
30883
|
+
serpDepth: boundedInteger$2({
|
|
30884
|
+
value: input.serpDepth,
|
|
30885
|
+
fallback: 10,
|
|
30886
|
+
minimum: 1,
|
|
30887
|
+
maximum: 20,
|
|
30888
|
+
label: "serpDepth"
|
|
30889
|
+
})
|
|
30890
|
+
};
|
|
30891
|
+
}
|
|
30892
|
+
function emptyCost() {
|
|
30893
|
+
return {
|
|
30894
|
+
currency: "USD",
|
|
30895
|
+
estimatedMicros: 0,
|
|
30896
|
+
actualMicros: 0,
|
|
30897
|
+
taskIds: []
|
|
30898
|
+
};
|
|
30899
|
+
}
|
|
30900
|
+
function aggregateCosts(costs) {
|
|
30901
|
+
const estimated = costs.map((cost) => cost.estimatedMicros);
|
|
30902
|
+
const actual = costs.map((cost) => cost.actualMicros);
|
|
30903
|
+
return {
|
|
30904
|
+
currency: "USD",
|
|
30905
|
+
estimatedMicros: estimated.some((value) => value === null) ? null : estimated.reduce((sum, value) => sum + (value ?? 0), 0),
|
|
30906
|
+
actualMicros: actual.some((value) => value === null) ? null : actual.reduce((sum, value) => sum + (value ?? 0), 0),
|
|
30907
|
+
taskIds: [...new Set(costs.flatMap((cost) => cost.taskIds))].sort()
|
|
30908
|
+
};
|
|
30909
|
+
}
|
|
30910
|
+
function costsWithUnknownRequest(costs) {
|
|
30911
|
+
return {
|
|
30912
|
+
currency: "USD",
|
|
30913
|
+
estimatedMicros: null,
|
|
30914
|
+
actualMicros: null,
|
|
30915
|
+
taskIds: [...new Set(costs.flatMap((cost) => cost.taskIds))].sort()
|
|
30916
|
+
};
|
|
30917
|
+
}
|
|
30918
|
+
async function acquireSerps(input) {
|
|
30919
|
+
const selected = input.queries.slice(0, input.limit);
|
|
30920
|
+
const selection = {
|
|
30921
|
+
availableQueries: input.queries.length,
|
|
30922
|
+
requestedQueries: selected.length,
|
|
30923
|
+
omittedQueries: Math.max(0, input.queries.length - selected.length),
|
|
30924
|
+
limit: input.limit,
|
|
30925
|
+
depth: input.depth,
|
|
30926
|
+
method: "highest-impression-local-queries-v1"
|
|
30927
|
+
};
|
|
30928
|
+
if (!input.requested) return {
|
|
30929
|
+
requested: false,
|
|
30930
|
+
status: "not-requested",
|
|
30931
|
+
selection,
|
|
30932
|
+
market: null,
|
|
30933
|
+
reports: [],
|
|
30934
|
+
cost: emptyCost(),
|
|
30935
|
+
reason: "Local SERP snapshots were not requested, so no paid provider work was attempted."
|
|
30936
|
+
};
|
|
30937
|
+
if (selected.length === 0) return {
|
|
30938
|
+
requested: true,
|
|
30939
|
+
status: "skipped",
|
|
30940
|
+
selection,
|
|
30941
|
+
market: input.market ?? null,
|
|
30942
|
+
reports: [],
|
|
30943
|
+
cost: emptyCost(),
|
|
30944
|
+
reason: "No retained local-intent query was available for a SERP snapshot."
|
|
30945
|
+
};
|
|
30946
|
+
if (!input.market) throw new SeoError("INVALID_INPUT", "Local SERP evidence requires a market.");
|
|
30947
|
+
const reports = [];
|
|
30948
|
+
try {
|
|
30949
|
+
for (const query of selected) reports.push(await input.run({
|
|
30950
|
+
keyword: query,
|
|
30951
|
+
market: input.market,
|
|
30952
|
+
depth: input.depth,
|
|
30953
|
+
provider: input.provider,
|
|
30954
|
+
projectId: input.projectId,
|
|
30955
|
+
context: {
|
|
30956
|
+
reportId: "local-search-demand",
|
|
30957
|
+
reportRunId: input.reportRunId
|
|
30958
|
+
},
|
|
30959
|
+
refresh: input.refresh
|
|
30960
|
+
}));
|
|
30961
|
+
} catch (error) {
|
|
30962
|
+
if (error instanceof SeoError && (error.code === "PROVIDER_UNAVAILABLE" || error.code === "RATE_LIMITED")) return {
|
|
30963
|
+
requested: true,
|
|
30964
|
+
status: reports.length > 0 ? "partial" : "unavailable",
|
|
30965
|
+
selection,
|
|
30966
|
+
market: input.market,
|
|
30967
|
+
reports,
|
|
30968
|
+
cost: costsWithUnknownRequest(reports.map((report) => report.evidence.cost)),
|
|
30969
|
+
reason: "First-party local demand remains available, but not every requested SERP snapshot could be acquired.",
|
|
30970
|
+
error: {
|
|
30971
|
+
code: error.code,
|
|
30972
|
+
message: error.message,
|
|
30973
|
+
retryable: error.retryable
|
|
30974
|
+
}
|
|
30975
|
+
};
|
|
30976
|
+
throw error;
|
|
30977
|
+
}
|
|
30978
|
+
return {
|
|
30979
|
+
requested: true,
|
|
30980
|
+
status: reports.every((report) => report.dataStatus === "complete") ? "complete" : "partial",
|
|
30981
|
+
selection,
|
|
30982
|
+
market: input.market,
|
|
30983
|
+
reports,
|
|
30984
|
+
cost: aggregateCosts(reports.map((report) => report.evidence.cost))
|
|
30985
|
+
};
|
|
30986
|
+
}
|
|
30987
|
+
async function localSearchReport(input, dependencies = {}) {
|
|
30988
|
+
const options = validateInput(input);
|
|
30989
|
+
const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
30990
|
+
const generatedAt = now.toISOString();
|
|
30991
|
+
const range = finalGscDateRange(options.days, now);
|
|
30992
|
+
const source = await (dependencies.searchAnalytics ?? querySearchAnalytics)(options.site, {
|
|
30993
|
+
...range,
|
|
30994
|
+
dimensions: ["query", "page"],
|
|
30995
|
+
type: "web",
|
|
30996
|
+
dataState: "final",
|
|
30997
|
+
maxRows: options.maxRows
|
|
30998
|
+
}, { refresh: input.refresh });
|
|
30999
|
+
const analysis = analyzeLocalSearchRows({
|
|
31000
|
+
rows: source.rows,
|
|
31001
|
+
site: options.site,
|
|
31002
|
+
locationTerms: options.locationTerms,
|
|
31003
|
+
brandTerms: input.brandTerms,
|
|
31004
|
+
includeBrand: input.includeBrand,
|
|
31005
|
+
minImpressions: options.minImpressions,
|
|
31006
|
+
limit: options.limit
|
|
31007
|
+
});
|
|
31008
|
+
const serpEvidence = await acquireSerps({
|
|
31009
|
+
requested: options.includeSerps,
|
|
31010
|
+
queries: analysis.opportunities.map((item) => item.query),
|
|
31011
|
+
market: options.market,
|
|
31012
|
+
provider: input.provider,
|
|
31013
|
+
projectId: input.projectId,
|
|
31014
|
+
refresh: input.refresh,
|
|
31015
|
+
limit: options.serpLimit,
|
|
31016
|
+
depth: options.serpDepth,
|
|
31017
|
+
reportRunId: randomUUID(),
|
|
31018
|
+
run: dependencies.serpResults ?? serpResultsReport
|
|
31019
|
+
});
|
|
31020
|
+
const possiblyTruncated = source.rowsFetched >= options.maxRows;
|
|
31021
|
+
const sourceEmpty = source.rows.length === 0;
|
|
31022
|
+
const filtered = !sourceEmpty && analysis.selection.eligibleQueries === 0;
|
|
31023
|
+
const partial = possiblyTruncated || serpEvidence.status === "partial" || serpEvidence.status === "unavailable";
|
|
31024
|
+
const opportunities = analysis.opportunities;
|
|
31025
|
+
const localPackSnapshots = serpEvidence.reports.filter((report) => report.evidence.data.features.includes("local_pack")).length;
|
|
31026
|
+
const dataStatus = sourceEmpty ? "empty" : partial ? "partial" : filtered ? "filtered" : "complete";
|
|
31027
|
+
const warnings = [];
|
|
31028
|
+
if (possiblyTruncated) warnings.push(`Search Console reached the ${options.maxRows}-row retention cap. Lower-volume local queries may be missing.`);
|
|
31029
|
+
if (analysis.selection.invalidRows > 0) warnings.push(`${analysis.selection.invalidRows} malformed Search Console rows were excluded.`);
|
|
31030
|
+
if (analysis.selection.conflictingRows > 0) warnings.push(`${analysis.selection.conflictingRows} conflicting duplicate Search Console rows were excluded.`);
|
|
31031
|
+
if (serpEvidence.error) warnings.push(serpEvidence.error.message);
|
|
31032
|
+
const nextSteps = [];
|
|
31033
|
+
if (!options.includeSerps) nextSteps.push("If the shortlisted queries justify paid verification, rerun with includeSerps true and an exact location and device to inspect current local result features and organic competitors.");
|
|
31034
|
+
else if (serpEvidence.status === "unavailable") nextSteps.push("Check the provider connection, market support, and local spend limits before retrying the missing SERP snapshots.");
|
|
31035
|
+
if (analysis.templates.length > 0) nextSteps.push("Run pseo-audit on the strongest repeated local page pattern before expanding it; verify source fields, page uniqueness, indexability, and representative output.");
|
|
31036
|
+
if (opportunities.some((item) => item.action === "review-page-overlap")) nextSteps.push("Review overlapping pages query by query before consolidating anything; multiple retained pages do not by themselves prove harmful cannibalisation.");
|
|
31037
|
+
nextSteps.push("Use location terms for the actual service area and repeat the report for materially different markets or devices instead of treating one area as representative.");
|
|
31038
|
+
return {
|
|
31039
|
+
schemaVersion: 1,
|
|
31040
|
+
methodology: "local-search-demand-v1",
|
|
31041
|
+
site: options.site,
|
|
31042
|
+
generatedAt,
|
|
31043
|
+
range,
|
|
31044
|
+
rangeDays: options.days,
|
|
31045
|
+
dataStatus,
|
|
31046
|
+
source: {
|
|
31047
|
+
provider: "google-search-console",
|
|
31048
|
+
dimensions: ["query", "page"],
|
|
31049
|
+
searchType: "web",
|
|
31050
|
+
dataState: "final",
|
|
31051
|
+
rowsFetched: source.rowsFetched,
|
|
31052
|
+
calls: source.calls,
|
|
31053
|
+
maxRows: options.maxRows,
|
|
31054
|
+
possiblyTruncated,
|
|
31055
|
+
completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-page-rows-only"
|
|
31056
|
+
},
|
|
31057
|
+
methodologyDetails: {
|
|
31058
|
+
intentMethod: "explicit-local-intent-v1",
|
|
31059
|
+
suppliedLocationTerms: options.locationTerms,
|
|
31060
|
+
automaticPatterns: [
|
|
31061
|
+
"nearby-phrases",
|
|
31062
|
+
"uk-postcodes",
|
|
31063
|
+
"contextual-us-zip-codes"
|
|
31064
|
+
],
|
|
31065
|
+
opportunityOrder: "impressions-clicks-position-query-v1",
|
|
31066
|
+
templateMethod: "pseo-url-template-clustering-v1"
|
|
31067
|
+
},
|
|
31068
|
+
selection: analysis.selection,
|
|
31069
|
+
summary: {
|
|
31070
|
+
localQueries: analysis.selection.eligibleQueries,
|
|
31071
|
+
returnedQueries: opportunities.length,
|
|
31072
|
+
pages: analysis.eligiblePageCount,
|
|
31073
|
+
...analysis.eligibleSummary,
|
|
31074
|
+
templates: analysis.templates.length,
|
|
31075
|
+
serpSnapshots: serpEvidence.reports.length,
|
|
31076
|
+
localPackSnapshots,
|
|
31077
|
+
verdict: sourceEmpty ? "Search Console returned no retained query/page rows for this date window." : filtered ? "No retained query/page rows matched the supplied terms or automatic local-intent patterns." : `${countLabel(analysis.selection.eligibleQueries, "local-intent query")} found; ${countLabel(opportunities.length, "query")} returned for review.`
|
|
31078
|
+
},
|
|
31079
|
+
opportunities,
|
|
31080
|
+
templates: analysis.templates,
|
|
31081
|
+
serpEvidence,
|
|
31082
|
+
warnings,
|
|
31083
|
+
caveats: [
|
|
31084
|
+
"Search Console exposes retained top query/page rows and does not guarantee every query. A row cap means lower-volume local demand may be missing.",
|
|
31085
|
+
"Named-place, proximity, and postal-code matching are explicit heuristics. They can miss implicit local intent and postal-looking non-location terms.",
|
|
31086
|
+
"A place in a query describes query wording, not the searcher's physical location. Search Console average position is not an exact local rank.",
|
|
31087
|
+
"A Google Analytics city or region describes measured users under Analytics rules; it is not joined here because it cannot prove which search query brought a user.",
|
|
31088
|
+
"A local_pack feature in a live snapshot proves only that the result type appeared in that market, device, and observation. It does not expose or validate every business listing.",
|
|
31089
|
+
"Repeated URL patterns and page overlap are review signals, not proof that more pages should exist or that consolidation is required.",
|
|
31090
|
+
input.includeBrand ? "Brand queries were included when they matched the local-intent rules." : "Brand-like queries were excluded using the site and supplied brand terms."
|
|
31091
|
+
],
|
|
31092
|
+
nextSteps
|
|
31093
|
+
};
|
|
31094
|
+
}
|
|
31095
|
+
//#endregion
|
|
30022
31096
|
//#region packages/core/src/analyze/monitoring/crawl-recommendations.ts
|
|
30023
31097
|
function statusMovedToError(item) {
|
|
30024
31098
|
return item.kind !== "removed" && (item.after?.status ?? 0) >= 400 && (item.before?.status ?? 200) < 400;
|
|
@@ -30707,7 +31781,7 @@ function analyzeIndexCoverageSignals(input) {
|
|
|
30707
31781
|
const MAX_SITEMAP_DOCUMENTS = 20;
|
|
30708
31782
|
const MAX_SEARCH_ROWS = 25e4;
|
|
30709
31783
|
const MAX_SITEMAP_URLS = 25e4;
|
|
30710
|
-
const defaultDependencies$
|
|
31784
|
+
const defaultDependencies$4 = {
|
|
30711
31785
|
loadCrawl: loadCrawlReport,
|
|
30712
31786
|
latestCrawl: latestCrawlReport,
|
|
30713
31787
|
searchAnalytics: querySearchAnalytics,
|
|
@@ -30739,7 +31813,7 @@ function sitemapCompleteness(input) {
|
|
|
30739
31813
|
if (input.inventoryTruncated) return "truncated";
|
|
30740
31814
|
return input.results.every((result) => result.dataStatus === "complete") ? "complete" : "partial";
|
|
30741
31815
|
}
|
|
30742
|
-
async function indexCoverageSignals(input, dependencies = defaultDependencies$
|
|
31816
|
+
async function indexCoverageSignals(input, dependencies = defaultDependencies$4) {
|
|
30743
31817
|
const days = integerOption$2({
|
|
30744
31818
|
value: input.days,
|
|
30745
31819
|
fallback: 90,
|
|
@@ -31223,7 +32297,7 @@ function pruneIndexWatchSnapshots(rootSite, url, database = getDb()) {
|
|
|
31223
32297
|
WHERE attempts.attempt_rank > ? AND latest_success.id IS NULL
|
|
31224
32298
|
)`).run(rootSite, url, rootSite, url, 20).changes;
|
|
31225
32299
|
}
|
|
31226
|
-
const defaultDependencies$
|
|
32300
|
+
const defaultDependencies$3 = {
|
|
31227
32301
|
inspectUrl,
|
|
31228
32302
|
now: () => /* @__PURE__ */ new Date(),
|
|
31229
32303
|
store: {
|
|
@@ -31268,7 +32342,7 @@ function isFatalBatchError(error) {
|
|
|
31268
32342
|
"AUTH_REQUIRED"
|
|
31269
32343
|
].includes(error.code);
|
|
31270
32344
|
}
|
|
31271
|
-
async function indexWatch(input, dependencies = defaultDependencies$
|
|
32345
|
+
async function indexWatch(input, dependencies = defaultDependencies$3) {
|
|
31272
32346
|
const languageCode = validateLanguageCode(input.languageCode);
|
|
31273
32347
|
const dailyLimit = integerOption$2({
|
|
31274
32348
|
value: input.dailyLimit,
|
|
@@ -33450,9 +34524,9 @@ function pseoSampleUrls(report) {
|
|
|
33450
34524
|
}
|
|
33451
34525
|
//#endregion
|
|
33452
34526
|
//#region packages/core/src/analyze/pseo/audit.ts
|
|
33453
|
-
const MAX_GSC_ROWS = 5e4;
|
|
34527
|
+
const MAX_GSC_ROWS$1 = 5e4;
|
|
33454
34528
|
const DEFAULT_SITEMAP_URLS = 5e4;
|
|
33455
|
-
const defaultDependencies$
|
|
34529
|
+
const defaultDependencies$2 = {
|
|
33456
34530
|
searchAnalytics: querySearchAnalytics,
|
|
33457
34531
|
fetchPage,
|
|
33458
34532
|
inspectUrl,
|
|
@@ -33654,7 +34728,7 @@ async function inspectSample(input) {
|
|
|
33654
34728
|
};
|
|
33655
34729
|
}
|
|
33656
34730
|
}
|
|
33657
|
-
async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$
|
|
34731
|
+
async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$2) {
|
|
33658
34732
|
const options = pseoAuditOptions(input);
|
|
33659
34733
|
const now = dependencies.now();
|
|
33660
34734
|
const generatedAt = now.toISOString();
|
|
@@ -33673,7 +34747,7 @@ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencie
|
|
|
33673
34747
|
type: "web",
|
|
33674
34748
|
dataState: "final",
|
|
33675
34749
|
aggregationType: "auto",
|
|
33676
|
-
maxRows: MAX_GSC_ROWS
|
|
34750
|
+
maxRows: MAX_GSC_ROWS$1
|
|
33677
34751
|
};
|
|
33678
34752
|
input.progress?.(`Fetching ${options.days} days of GSC page and query/page rows`);
|
|
33679
34753
|
const [pageResponse, queryPageResponse] = await Promise.all([dependencies.searchAnalytics(input.site, {
|
|
@@ -33724,7 +34798,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
|
|
|
33724
34798
|
minimumTemplateImpressions: options.minimumTemplateImpressions,
|
|
33725
34799
|
crawlSamplesPerTemplate: options.crawlSamples,
|
|
33726
34800
|
inspectionSamplesPerTemplate: options.inspectSamples,
|
|
33727
|
-
maxRowsPerRequest: MAX_GSC_ROWS,
|
|
34801
|
+
maxRowsPerRequest: MAX_GSC_ROWS$1,
|
|
33728
34802
|
pageRowsFetched: acquisition.pageRowsFetched,
|
|
33729
34803
|
queryPageRowsFetched: acquisition.queryPageRowsFetched,
|
|
33730
34804
|
sitemapsRequested: input.sitemaps?.length ?? 0,
|
|
@@ -33735,7 +34809,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
|
|
|
33735
34809
|
caveats: [`Data freshness: ${input.refresh ? "fresh fetch requested; caches bypassed where supported" : "local cache allowed; use --refresh to bypass cached provider and HTTP data"}.`, input.sitemaps?.length ? `Sitemaps: ${countLabel(input.sitemaps.length, "sitemap URL")} requested.` : "Sitemaps: none provided; template discovery used retained GSC page rows."]
|
|
33736
34810
|
});
|
|
33737
34811
|
}
|
|
33738
|
-
async function pseoAuditReport(input, dependencies = defaultDependencies$
|
|
34812
|
+
async function pseoAuditReport(input, dependencies = defaultDependencies$2) {
|
|
33739
34813
|
const acquisition = await acquirePseoAuditEvidence(input, dependencies);
|
|
33740
34814
|
const sampleUrls = pseoSampleUrls(buildPseoAuditFromAcquisition(input, acquisition));
|
|
33741
34815
|
const topQueries = topQueryByUrl(acquisition.queryPageRows, input);
|
|
@@ -34574,326 +35648,6 @@ function pseoOpportunityVerdict(input) {
|
|
|
34574
35648
|
return `${expansions} template-expansion candidate${expansions === 1 ? "" : "s"} and ${input.competitors.length} observed competitor domain${input.competitors.length === 1 ? "" : "s"} were retained for review.`;
|
|
34575
35649
|
}
|
|
34576
35650
|
//#endregion
|
|
34577
|
-
//#region packages/core/src/providers/dataforseo/serp-snapshot.ts
|
|
34578
|
-
const MAX_SERP_DEPTH = 100;
|
|
34579
|
-
const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
|
|
34580
|
-
function safeUrl(value) {
|
|
34581
|
-
if (!value) return null;
|
|
34582
|
-
try {
|
|
34583
|
-
const parsed = new URL(value);
|
|
34584
|
-
if (!["http:", "https:"].includes(parsed.protocol)) return null;
|
|
34585
|
-
parsed.username = "";
|
|
34586
|
-
parsed.password = "";
|
|
34587
|
-
return parsed.toString();
|
|
34588
|
-
} catch {
|
|
34589
|
-
return null;
|
|
34590
|
-
}
|
|
34591
|
-
}
|
|
34592
|
-
function validOrganic(item) {
|
|
34593
|
-
return item.type === "organic" && Number.isSafeInteger(item.rank_group) && (item.rank_group ?? 0) > 0 && Number.isSafeInteger(item.rank_absolute) && (item.rank_absolute ?? 0) > 0 && Number.isSafeInteger(item.page) && (item.page ?? 0) > 0 && Boolean(item.domain?.trim()) && safeUrl(item.url) !== null;
|
|
34594
|
-
}
|
|
34595
|
-
function organicResult(item) {
|
|
34596
|
-
if (!validOrganic(item)) return null;
|
|
34597
|
-
const url = safeUrl(item.url);
|
|
34598
|
-
if (!url) return null;
|
|
34599
|
-
return {
|
|
34600
|
-
rankGroup: item.rank_group,
|
|
34601
|
-
rankAbsolute: item.rank_absolute,
|
|
34602
|
-
page: item.page,
|
|
34603
|
-
domain: item.domain.trim().toLowerCase(),
|
|
34604
|
-
url,
|
|
34605
|
-
title: item.title ?? null,
|
|
34606
|
-
description: item.description ?? null,
|
|
34607
|
-
isFeaturedSnippet: item.is_featured_snippet ?? null
|
|
34608
|
-
};
|
|
34609
|
-
}
|
|
34610
|
-
function checkedAt(value, fallback) {
|
|
34611
|
-
const timestamp = Date.parse(value);
|
|
34612
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
|
|
34613
|
-
}
|
|
34614
|
-
function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
|
|
34615
|
-
const market = searchMarketSchema.parse(input.market);
|
|
34616
|
-
const keyword = normalizedKeyword$2(input.keyword);
|
|
34617
|
-
const location = locationRequest(market, "serp-snapshot");
|
|
34618
|
-
const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$2(item.keyword) === keyword);
|
|
34619
|
-
if (!result) throw new ProviderError({
|
|
34620
|
-
provider: "dataforseo",
|
|
34621
|
-
operation: "serp-snapshot",
|
|
34622
|
-
code: "invalid-response",
|
|
34623
|
-
message: "DataForSEO returned no matching SERP result."
|
|
34624
|
-
});
|
|
34625
|
-
const warnings = [...snapshot.warnings];
|
|
34626
|
-
const items = result.items ?? [];
|
|
34627
|
-
const organicItems = items.filter((item) => item.type === "organic");
|
|
34628
|
-
const mappedOrganicResults = organicItems.flatMap((item) => {
|
|
34629
|
-
const mapped = organicResult(item);
|
|
34630
|
-
return mapped ? [mapped] : [];
|
|
34631
|
-
}).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
|
|
34632
|
-
const organicResults = mappedOrganicResults.slice(0, input.depth);
|
|
34633
|
-
const invalidRows = organicItems.length - mappedOrganicResults.length;
|
|
34634
|
-
const resultCapped = mappedOrganicResults.length > input.depth;
|
|
34635
|
-
const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
|
|
34636
|
-
if (invalidRows > 0) warnings.push({
|
|
34637
|
-
code: "invalid-organic-results",
|
|
34638
|
-
field: "organicResults",
|
|
34639
|
-
message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
|
|
34640
|
-
});
|
|
34641
|
-
if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
|
|
34642
|
-
code: "invalid-serp-observation-time",
|
|
34643
|
-
field: "checkedAt",
|
|
34644
|
-
message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
|
|
34645
|
-
});
|
|
34646
|
-
const checkUrl = safeUrl(result.check_url);
|
|
34647
|
-
if (result.check_url && !checkUrl) warnings.push({
|
|
34648
|
-
code: "invalid-serp-check-url",
|
|
34649
|
-
field: "checkUrl",
|
|
34650
|
-
message: "DataForSEO returned an invalid SERP check URL."
|
|
34651
|
-
});
|
|
34652
|
-
if (invalidResultCount) warnings.push({
|
|
34653
|
-
code: "invalid-serp-result-count",
|
|
34654
|
-
field: "resultCount",
|
|
34655
|
-
message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
|
|
34656
|
-
});
|
|
34657
|
-
const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
|
|
34658
|
-
return {
|
|
34659
|
-
schemaVersion: 1,
|
|
34660
|
-
provider: "dataforseo",
|
|
34661
|
-
capability: "serp-snapshot",
|
|
34662
|
-
data: {
|
|
34663
|
-
keyword,
|
|
34664
|
-
effectiveKeyword: normalizedKeyword$2(result.spell?.keyword ?? result.keyword),
|
|
34665
|
-
searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
|
|
34666
|
-
checkedAt: checkedAt(result.datetime, snapshot.observedAt),
|
|
34667
|
-
checkUrl,
|
|
34668
|
-
resultCount: invalidResultCount ? null : result.se_results_count ?? null,
|
|
34669
|
-
pagesCount: result.pages_count === void 0 ? null : result.pages_count,
|
|
34670
|
-
features,
|
|
34671
|
-
organicResults
|
|
34672
|
-
},
|
|
34673
|
-
observedAt: snapshot.observedAt,
|
|
34674
|
-
market: {
|
|
34675
|
-
...market,
|
|
34676
|
-
device: market.device ?? "desktop"
|
|
34677
|
-
},
|
|
34678
|
-
coverage: {
|
|
34679
|
-
requestedRows: input.depth,
|
|
34680
|
-
returnedRows: snapshot.returnedRows,
|
|
34681
|
-
retainedRows: organicResults.length,
|
|
34682
|
-
invalidRows,
|
|
34683
|
-
providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
|
|
34684
|
-
completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
|
|
34685
|
-
nextCursor: null
|
|
34686
|
-
},
|
|
34687
|
-
cache: snapshot.cache,
|
|
34688
|
-
cost: snapshot.cost,
|
|
34689
|
-
request: {
|
|
34690
|
-
operation: "serp-snapshot",
|
|
34691
|
-
endpoint,
|
|
34692
|
-
limit: input.depth,
|
|
34693
|
-
filters: {
|
|
34694
|
-
countryCode: market.countryCode,
|
|
34695
|
-
languageCode: market.languageCode,
|
|
34696
|
-
device: market.device ?? "desktop",
|
|
34697
|
-
...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
|
|
34698
|
-
},
|
|
34699
|
-
sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
|
|
34700
|
-
},
|
|
34701
|
-
warnings
|
|
34702
|
-
};
|
|
34703
|
-
}
|
|
34704
|
-
var DataForSeoSerpSnapshotProvider = class {
|
|
34705
|
-
provider = "dataforseo";
|
|
34706
|
-
capabilitySupport = [{
|
|
34707
|
-
capability: "serp-snapshot",
|
|
34708
|
-
status: "available",
|
|
34709
|
-
markets: [{
|
|
34710
|
-
searchEngines: ["google"],
|
|
34711
|
-
devices: ["desktop", "mobile"],
|
|
34712
|
-
location: "any"
|
|
34713
|
-
}]
|
|
34714
|
-
}];
|
|
34715
|
-
client;
|
|
34716
|
-
constructor(options = {}) {
|
|
34717
|
-
this.client = options.client ?? new DataForSeoClient(options);
|
|
34718
|
-
}
|
|
34719
|
-
async serpSnapshot(input) {
|
|
34720
|
-
const market = searchMarketSchema.parse(input.market);
|
|
34721
|
-
if (market.searchEngine !== "google") throw new ProviderError({
|
|
34722
|
-
provider: "dataforseo",
|
|
34723
|
-
operation: "serp-snapshot",
|
|
34724
|
-
code: "configuration",
|
|
34725
|
-
message: "DataForSEO SERP snapshots currently support Google."
|
|
34726
|
-
});
|
|
34727
|
-
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH) throw new ProviderError({
|
|
34728
|
-
provider: "dataforseo",
|
|
34729
|
-
operation: "serp-snapshot",
|
|
34730
|
-
code: "configuration",
|
|
34731
|
-
message: "SERP depth must be from 1 to 100."
|
|
34732
|
-
});
|
|
34733
|
-
const keyword = normalizedKeyword$2(input.keyword);
|
|
34734
|
-
if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new ProviderError({
|
|
34735
|
-
provider: "dataforseo",
|
|
34736
|
-
operation: "serp-snapshot",
|
|
34737
|
-
code: "configuration",
|
|
34738
|
-
message: "SERP snapshots require a keyword of at most 80 characters and 10 words."
|
|
34739
|
-
});
|
|
34740
|
-
const context = input.context ?? {
|
|
34741
|
-
reportId: "serp-results",
|
|
34742
|
-
reportRunId: randomUUID()
|
|
34743
|
-
};
|
|
34744
|
-
const location = locationRequest(market, "serp-snapshot");
|
|
34745
|
-
return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
|
|
34746
|
-
keyword,
|
|
34747
|
-
languageCode: market.languageCode.split("-")[0],
|
|
34748
|
-
...location,
|
|
34749
|
-
device: market.device ?? "desktop",
|
|
34750
|
-
depth: input.depth,
|
|
34751
|
-
refresh: input.refresh,
|
|
34752
|
-
context
|
|
34753
|
-
}));
|
|
34754
|
-
}
|
|
34755
|
-
};
|
|
34756
|
-
//#endregion
|
|
34757
|
-
//#region packages/core/src/analyze/serp-results.ts
|
|
34758
|
-
const MAX_REPORT_DEPTH = 100;
|
|
34759
|
-
function compareCodepoints(left, right) {
|
|
34760
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
34761
|
-
}
|
|
34762
|
-
function snapshotProvider(candidate) {
|
|
34763
|
-
return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
|
|
34764
|
-
}
|
|
34765
|
-
async function defaultCandidates$1() {
|
|
34766
|
-
return [{
|
|
34767
|
-
adapter: new DataForSeoSerpSnapshotProvider(),
|
|
34768
|
-
connected: Boolean(await readDataForSeoCredentials()),
|
|
34769
|
-
priority: 10
|
|
34770
|
-
}];
|
|
34771
|
-
}
|
|
34772
|
-
function providerError$2(error) {
|
|
34773
|
-
if (!(error instanceof ProviderError)) throw error;
|
|
34774
|
-
if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
|
|
34775
|
-
if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
|
|
34776
|
-
throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
34777
|
-
}
|
|
34778
|
-
function validateInput(input) {
|
|
34779
|
-
const keyword = input.keyword.trim().replace(/\s+/gu, " ");
|
|
34780
|
-
if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new SeoError("INVALID_INPUT", "SERP results requires a keyword of at most 80 characters and 10 words.");
|
|
34781
|
-
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_REPORT_DEPTH) throw new SeoError("INVALID_INPUT", `SERP result depth must be from 1 to ${MAX_REPORT_DEPTH}.`);
|
|
34782
|
-
const market = searchMarketSchema.safeParse(input.market);
|
|
34783
|
-
if (!market.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
|
|
34784
|
-
const provider = input.provider ? providerIdSchema.safeParse(input.provider) : void 0;
|
|
34785
|
-
if (provider && !provider.success) throw new SeoError("INVALID_INPUT", "Use a supported SERP provider.");
|
|
34786
|
-
return {
|
|
34787
|
-
keyword,
|
|
34788
|
-
market: market.data,
|
|
34789
|
-
provider: provider?.data
|
|
34790
|
-
};
|
|
34791
|
-
}
|
|
34792
|
-
function reportStatus$1(evidence) {
|
|
34793
|
-
if (evidence.coverage.completeness === "unavailable" || evidence.coverage.completeness === "invalid") return "unavailable";
|
|
34794
|
-
return evidence.coverage.completeness === "complete" ? "complete" : "partial";
|
|
34795
|
-
}
|
|
34796
|
-
function domainSummaries(snapshot) {
|
|
34797
|
-
const byDomain = /* @__PURE__ */ new Map();
|
|
34798
|
-
for (const result of snapshot.organicResults) byDomain.set(result.domain, [...byDomain.get(result.domain) ?? [], result.rankAbsolute]);
|
|
34799
|
-
return [...byDomain.entries()].map(([domain, ranks]) => ({
|
|
34800
|
-
domain,
|
|
34801
|
-
resultCount: ranks.length,
|
|
34802
|
-
ranks: [...ranks].sort((left, right) => left - right)
|
|
34803
|
-
})).sort((left, right) => right.resultCount - left.resultCount || (left.ranks[0] ?? Number.MAX_SAFE_INTEGER) - (right.ranks[0] ?? Number.MAX_SAFE_INTEGER) || compareCodepoints(left.domain, right.domain));
|
|
34804
|
-
}
|
|
34805
|
-
function reportFindings(snapshot, domains) {
|
|
34806
|
-
const findings = [];
|
|
34807
|
-
if (snapshot.effectiveKeyword !== snapshot.keyword) findings.push({
|
|
34808
|
-
code: "query-correction",
|
|
34809
|
-
evidenceRef: "evidence.data.effectiveKeyword",
|
|
34810
|
-
principle: "A corrected query changes the observed result context and does not prove the original wording has the same intent.",
|
|
34811
|
-
detail: `The provider observed results for ${snapshot.effectiveKeyword} after the requested query ${snapshot.keyword}.`
|
|
34812
|
-
});
|
|
34813
|
-
for (const [index, domain] of domains.entries()) {
|
|
34814
|
-
if (domain.resultCount <= 1) continue;
|
|
34815
|
-
findings.push({
|
|
34816
|
-
code: "repeated-domain",
|
|
34817
|
-
evidenceRef: `domains[${index}]`,
|
|
34818
|
-
principle: "Repeated results are descriptive snapshot evidence, not a domain strength or quality score.",
|
|
34819
|
-
detail: `${domain.domain} appears ${domain.resultCount} times at absolute ranks ${domain.ranks.join(", ")}.`
|
|
34820
|
-
});
|
|
34821
|
-
}
|
|
34822
|
-
return findings.slice(0, 10);
|
|
34823
|
-
}
|
|
34824
|
-
async function serpResultsReport(input, dependencies = {}) {
|
|
34825
|
-
const depth = input.depth ?? 10;
|
|
34826
|
-
const validated = validateInput({
|
|
34827
|
-
...input,
|
|
34828
|
-
depth
|
|
34829
|
-
});
|
|
34830
|
-
let candidates;
|
|
34831
|
-
try {
|
|
34832
|
-
candidates = dependencies.candidates ?? await defaultCandidates$1();
|
|
34833
|
-
} catch (error) {
|
|
34834
|
-
return providerError$2(error);
|
|
34835
|
-
}
|
|
34836
|
-
const resolution = resolveProvider({
|
|
34837
|
-
capability: "serp-snapshot",
|
|
34838
|
-
market: validated.market,
|
|
34839
|
-
candidates,
|
|
34840
|
-
provider: validated.provider
|
|
34841
|
-
});
|
|
34842
|
-
if (resolution.status === "unavailable") throw new SeoError("PROVIDER_UNAVAILABLE", resolution.reason === "provider-not-connected" ? "No connected provider can return live SERP results. Run `seo providers dataforseo connect` first." : validated.provider ? `${validated.provider} cannot return live SERP results for this market.` : "No configured provider can return live SERP results for this market.");
|
|
34843
|
-
const provider = snapshotProvider(resolution.provider);
|
|
34844
|
-
if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no live SERP implementation.");
|
|
34845
|
-
let evidence;
|
|
34846
|
-
try {
|
|
34847
|
-
evidence = await provider.serpSnapshot({
|
|
34848
|
-
keyword: validated.keyword,
|
|
34849
|
-
market: validated.market,
|
|
34850
|
-
depth,
|
|
34851
|
-
refresh: input.refresh,
|
|
34852
|
-
context: {
|
|
34853
|
-
projectId: input.context?.projectId ?? input.projectId,
|
|
34854
|
-
reportId: input.context?.reportId ?? "serp-results",
|
|
34855
|
-
reportRunId: input.context?.reportRunId ?? randomUUID()
|
|
34856
|
-
}
|
|
34857
|
-
});
|
|
34858
|
-
} catch (error) {
|
|
34859
|
-
return providerError$2(error);
|
|
34860
|
-
}
|
|
34861
|
-
const domains = domainSummaries(evidence.data);
|
|
34862
|
-
const dataStatus = reportStatus$1(evidence);
|
|
34863
|
-
const generatedAt = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
34864
|
-
const correctedQuery = evidence.data.effectiveKeyword !== evidence.data.keyword;
|
|
34865
|
-
return {
|
|
34866
|
-
schemaVersion: 1,
|
|
34867
|
-
generatedAt,
|
|
34868
|
-
dataStatus,
|
|
34869
|
-
market: evidence.market,
|
|
34870
|
-
summary: {
|
|
34871
|
-
keyword: evidence.data.keyword,
|
|
34872
|
-
effectiveKeyword: evidence.data.effectiveKeyword,
|
|
34873
|
-
requestedDepth: depth,
|
|
34874
|
-
organicResults: evidence.data.organicResults.length,
|
|
34875
|
-
uniqueDomains: domains.length,
|
|
34876
|
-
observedFeatures: evidence.data.features.length,
|
|
34877
|
-
correctedQuery,
|
|
34878
|
-
verdict: `${evidence.data.organicResults.length} organic results across ${domains.length} domains were retained from the ${evidence.market.device ?? "desktop"} snapshot.`
|
|
34879
|
-
},
|
|
34880
|
-
evidence,
|
|
34881
|
-
domains,
|
|
34882
|
-
findings: reportFindings(evidence.data, domains),
|
|
34883
|
-
caveats: [
|
|
34884
|
-
"This is one market, location, language, and device-specific result snapshot; it is not rank history.",
|
|
34885
|
-
"Search results can change between checks and may differ from a person's signed-in or personalized results.",
|
|
34886
|
-
"The provider result count is an estimate and may be missing; retained organic rows are bounded by the requested depth.",
|
|
34887
|
-
"Repeated domains, result features, titles, and snippets do not establish content quality, authority, or ranking feasibility."
|
|
34888
|
-
],
|
|
34889
|
-
nextSteps: [
|
|
34890
|
-
"Review the retained pages and result types before deciding whether the query fits an existing page, a new page, or no page.",
|
|
34891
|
-
"Keep Search Console average position separate from these exact snapshot ranks when comparing first-party performance.",
|
|
34892
|
-
"Repeat the same market and device check later if the decision depends on result stability."
|
|
34893
|
-
]
|
|
34894
|
-
};
|
|
34895
|
-
}
|
|
34896
|
-
//#endregion
|
|
34897
35651
|
//#region packages/core/src/analyze/pseo-opportunities/serps.ts
|
|
34898
35652
|
function selectSerpKeywords(candidates, limit) {
|
|
34899
35653
|
const seedRefs = /* @__PURE__ */ new Set();
|
|
@@ -37191,7 +37945,7 @@ function groupedMappings(evidence) {
|
|
|
37191
37945
|
//#endregion
|
|
37192
37946
|
//#region packages/core/src/analyze/second-page.ts
|
|
37193
37947
|
const MAX_SOURCE_ROWS = 1e5;
|
|
37194
|
-
const defaultDependencies = {
|
|
37948
|
+
const defaultDependencies$1 = {
|
|
37195
37949
|
searchAnalytics: querySearchAnalytics,
|
|
37196
37950
|
fetch: fetchPage,
|
|
37197
37951
|
extract: extractPage,
|
|
@@ -37268,7 +38022,7 @@ function reportRecommendations(items) {
|
|
|
37268
38022
|
"Treat the priority score as a triage heuristic, not an estimated traffic lift."
|
|
37269
38023
|
]);
|
|
37270
38024
|
}
|
|
37271
|
-
async function secondPage(input, dependencies = defaultDependencies) {
|
|
38025
|
+
async function secondPage(input, dependencies = defaultDependencies$1) {
|
|
37272
38026
|
const days = integerOption({
|
|
37273
38027
|
value: input.range,
|
|
37274
38028
|
fallback: 28,
|
|
@@ -40434,10 +41188,10 @@ const DEFAULT_TARGET_LIMIT = 20;
|
|
|
40434
41188
|
const MAX_TARGET_LIMIT = 50;
|
|
40435
41189
|
const DEFAULT_DETAIL_PAGES = 1;
|
|
40436
41190
|
const MAX_DETAIL_PAGES = 3;
|
|
40437
|
-
const DEFAULT_ROW_LIMIT$
|
|
40438
|
-
const MAX_ROW_LIMIT$
|
|
41191
|
+
const DEFAULT_ROW_LIMIT$3 = 500;
|
|
41192
|
+
const MAX_ROW_LIMIT$3 = 1e3;
|
|
40439
41193
|
const MAX_CONCURRENCY = 3;
|
|
40440
|
-
function integer(value, fallback, max, name) {
|
|
41194
|
+
function integer$1(value, fallback, max, name) {
|
|
40441
41195
|
const resolved = value ?? fallback;
|
|
40442
41196
|
if (!Number.isInteger(resolved) || resolved < 1 || resolved > max) throw new SeoError("INVALID_INPUT", `${name} must be between 1 and ${max}.`);
|
|
40443
41197
|
return resolved;
|
|
@@ -40498,9 +41252,9 @@ async function targetDetails(input) {
|
|
|
40498
41252
|
}
|
|
40499
41253
|
async function collectBingLinkEvidence(input) {
|
|
40500
41254
|
if (!input.site.trim()) throw new SeoError("INVALID_INPUT", "Pass a Bing Webmaster site URL.");
|
|
40501
|
-
const targetLimit = integer(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
|
|
40502
|
-
const detailPages = integer(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
|
|
40503
|
-
const rowLimit = integer(input.rowLimit, DEFAULT_ROW_LIMIT$
|
|
41255
|
+
const targetLimit = integer$1(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
|
|
41256
|
+
const detailPages = integer$1(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
|
|
41257
|
+
const rowLimit = integer$1(input.rowLimit, DEFAULT_ROW_LIMIT$3, MAX_ROW_LIMIT$3, "Bing link row limit");
|
|
40504
41258
|
const client = input.client ?? (await createBingWebmasterClient()).client;
|
|
40505
41259
|
const countRows = [];
|
|
40506
41260
|
let countPagesRequested = 0;
|
|
@@ -40621,6 +41375,884 @@ async function collectBingLinkEvidence(input) {
|
|
|
40621
41375
|
};
|
|
40622
41376
|
}
|
|
40623
41377
|
//#endregion
|
|
41378
|
+
//#region packages/core/src/links/context-sources.ts
|
|
41379
|
+
function urlKey(value) {
|
|
41380
|
+
try {
|
|
41381
|
+
const url = new URL(value);
|
|
41382
|
+
if (!["http:", "https:"].includes(url.protocol)) return null;
|
|
41383
|
+
url.hash = "";
|
|
41384
|
+
return url.toString();
|
|
41385
|
+
} catch {
|
|
41386
|
+
return null;
|
|
41387
|
+
}
|
|
41388
|
+
}
|
|
41389
|
+
function compareTargets(left, right) {
|
|
41390
|
+
return right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0);
|
|
41391
|
+
}
|
|
41392
|
+
function linkTargetCounts(evidence) {
|
|
41393
|
+
if (evidence.targetCounts.length) return [...evidence.targetCounts].sort(compareTargets);
|
|
41394
|
+
const counts = /* @__PURE__ */ new Map();
|
|
41395
|
+
for (const row of evidence.rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
|
|
41396
|
+
return [...counts].map(([targetUrl, observedLinks]) => ({
|
|
41397
|
+
targetUrl,
|
|
41398
|
+
observedLinks
|
|
41399
|
+
})).sort(compareTargets);
|
|
41400
|
+
}
|
|
41401
|
+
function crawlForLinkTargets(input) {
|
|
41402
|
+
const values = /* @__PURE__ */ new Map();
|
|
41403
|
+
if (!input.report) {
|
|
41404
|
+
for (const target of input.targets) values.set(target.targetUrl, {
|
|
41405
|
+
state: "unavailable",
|
|
41406
|
+
reason: "No matching saved crawl was available."
|
|
41407
|
+
});
|
|
41408
|
+
return {
|
|
41409
|
+
values,
|
|
41410
|
+
matched: 0
|
|
41411
|
+
};
|
|
41412
|
+
}
|
|
41413
|
+
const pages = /* @__PURE__ */ new Map();
|
|
41414
|
+
for (const page of input.report.pages) for (const value of [page.url, page.finalUrl]) {
|
|
41415
|
+
const key = urlKey(value);
|
|
41416
|
+
if (key && !pages.has(key)) pages.set(key, page);
|
|
41417
|
+
}
|
|
41418
|
+
const issues = /* @__PURE__ */ new Map();
|
|
41419
|
+
for (const issue of input.report.issues) {
|
|
41420
|
+
const key = urlKey(issue.url);
|
|
41421
|
+
if (!key) continue;
|
|
41422
|
+
const ids = issues.get(key) ?? /* @__PURE__ */ new Set();
|
|
41423
|
+
ids.add(issue.ruleId);
|
|
41424
|
+
issues.set(key, ids);
|
|
41425
|
+
}
|
|
41426
|
+
let matched = 0;
|
|
41427
|
+
for (const target of input.targets) {
|
|
41428
|
+
const key = urlKey(target.targetUrl);
|
|
41429
|
+
const page = key ? pages.get(key) : void 0;
|
|
41430
|
+
if (!page) {
|
|
41431
|
+
values.set(target.targetUrl, {
|
|
41432
|
+
state: "not-observed",
|
|
41433
|
+
reason: "The target URL was not present in the retained crawl pages."
|
|
41434
|
+
});
|
|
41435
|
+
continue;
|
|
41436
|
+
}
|
|
41437
|
+
matched += 1;
|
|
41438
|
+
values.set(target.targetUrl, {
|
|
41439
|
+
state: "observed",
|
|
41440
|
+
reportId: input.report.id,
|
|
41441
|
+
observedAt: input.report.generatedAt,
|
|
41442
|
+
status: page.status,
|
|
41443
|
+
finalUrl: page.finalUrl,
|
|
41444
|
+
indexable: page.indexable,
|
|
41445
|
+
canonical: page.canonical ?? null,
|
|
41446
|
+
issueIds: [...issues.get(key) ?? []].sort()
|
|
41447
|
+
});
|
|
41448
|
+
}
|
|
41449
|
+
return {
|
|
41450
|
+
values,
|
|
41451
|
+
matched
|
|
41452
|
+
};
|
|
41453
|
+
}
|
|
41454
|
+
function aggregateLinkSearchRows(rows) {
|
|
41455
|
+
const values = /* @__PURE__ */ new Map();
|
|
41456
|
+
for (const row of rows) {
|
|
41457
|
+
const page = row.keys[0];
|
|
41458
|
+
const key = page ? urlKey(page) : null;
|
|
41459
|
+
if (!key) continue;
|
|
41460
|
+
const current = values.get(key) ?? {
|
|
41461
|
+
clicks: 0,
|
|
41462
|
+
impressions: 0,
|
|
41463
|
+
positionWeight: 0
|
|
41464
|
+
};
|
|
41465
|
+
current.clicks += row.clicks;
|
|
41466
|
+
current.impressions += row.impressions;
|
|
41467
|
+
current.positionWeight += row.position * row.impressions;
|
|
41468
|
+
values.set(key, current);
|
|
41469
|
+
}
|
|
41470
|
+
return values;
|
|
41471
|
+
}
|
|
41472
|
+
//#endregion
|
|
41473
|
+
//#region packages/core/src/links/context-findings.ts
|
|
41474
|
+
const MAX_FINDINGS$1 = 50;
|
|
41475
|
+
function findingPriority(input) {
|
|
41476
|
+
const searchValue = input.search.state === "observed" && (input.search.clicks > 0 || input.search.impressions >= 100);
|
|
41477
|
+
if (input.code === "linked-broken-target" || searchValue) return "high";
|
|
41478
|
+
if (input.observedLinks >= 5) return "medium";
|
|
41479
|
+
return "low";
|
|
41480
|
+
}
|
|
41481
|
+
function finding(input) {
|
|
41482
|
+
const crawl = input.row.crawl;
|
|
41483
|
+
if (crawl.state !== "observed") throw new Error("A link finding requires observed crawl evidence.");
|
|
41484
|
+
const searchEvidence = input.row.searchConsole.state === "observed" ? ` Search Console retained ${input.row.searchConsole.clicks.toFixed(0)} clicks and ${input.row.searchConsole.impressions.toFixed(0)} impressions for this page in the selected window.` : "";
|
|
41485
|
+
const common = {
|
|
41486
|
+
priority: findingPriority({
|
|
41487
|
+
code: input.code,
|
|
41488
|
+
observedLinks: input.row.observedLinks,
|
|
41489
|
+
crawl,
|
|
41490
|
+
search: input.row.searchConsole
|
|
41491
|
+
}),
|
|
41492
|
+
heuristic: true,
|
|
41493
|
+
targetUrl: input.row.targetUrl,
|
|
41494
|
+
observedLinks: input.row.observedLinks,
|
|
41495
|
+
evidenceRefs: [input.row.targetUrl, crawl.reportId]
|
|
41496
|
+
};
|
|
41497
|
+
if (input.code === "linked-broken-target") return {
|
|
41498
|
+
...common,
|
|
41499
|
+
code: input.code,
|
|
41500
|
+
principle: "Linked target pages should resolve intentionally.",
|
|
41501
|
+
evidence: `The saved crawl observed status ${crawl.status} for a target with ${input.row.observedLinks} retained representative links.${searchEvidence}`,
|
|
41502
|
+
action: "Confirm whether the page should exist. Restore it or add one direct redirect to a genuinely equivalent live page when the missing URL is unintended.",
|
|
41503
|
+
verify: "Run redirect-trace for the target, then recrawl it and confirm a stable intended response."
|
|
41504
|
+
};
|
|
41505
|
+
if (input.code === "linked-redirect-target") return {
|
|
41506
|
+
...common,
|
|
41507
|
+
code: input.code,
|
|
41508
|
+
principle: "External links should reach a stable final destination.",
|
|
41509
|
+
evidence: `The saved crawl observed ${input.row.targetUrl} resolving to ${crawl.finalUrl}.${searchEvidence}`,
|
|
41510
|
+
action: "Check that the redirect is intentional, direct and semantically equivalent. Update links you control to the final URL and ask important external sources to update only when the change is worth the effort.",
|
|
41511
|
+
verify: "Run redirect-trace and confirm one stable hop to the intended indexable page."
|
|
41512
|
+
};
|
|
41513
|
+
if (input.code === "linked-canonical-conflict") return {
|
|
41514
|
+
...common,
|
|
41515
|
+
code: input.code,
|
|
41516
|
+
principle: "Canonical hints should agree with the intended linked page.",
|
|
41517
|
+
evidence: `The saved crawl observed canonical ${crawl.canonical} on a linked target that resolved to ${crawl.finalUrl}.${searchEvidence}`,
|
|
41518
|
+
action: "Confirm the preferred URL. Align the canonical and redirect only if the current disagreement is unintended.",
|
|
41519
|
+
verify: "Audit the final page and inspect the declared and Google-selected canonical before changing it."
|
|
41520
|
+
};
|
|
41521
|
+
return {
|
|
41522
|
+
...common,
|
|
41523
|
+
code: input.code,
|
|
41524
|
+
principle: "A linked page should be indexable when search visibility is intended.",
|
|
41525
|
+
evidence: `The saved crawl marked a linked target as non-indexable.${searchEvidence}`,
|
|
41526
|
+
action: "Confirm whether non-indexing is intentional. Remove the blocking signal or redirect to an equivalent indexable page only when this URL should appear in search.",
|
|
41527
|
+
verify: "Audit the page, then use URL Inspection when the intended state is indexable."
|
|
41528
|
+
};
|
|
41529
|
+
}
|
|
41530
|
+
function linkTargetFindings(rows) {
|
|
41531
|
+
return rows.flatMap((row) => {
|
|
41532
|
+
const crawl = row.crawl;
|
|
41533
|
+
if (crawl.state !== "observed") return [];
|
|
41534
|
+
if (crawl.status >= 400) return [finding({
|
|
41535
|
+
row,
|
|
41536
|
+
code: "linked-broken-target"
|
|
41537
|
+
})];
|
|
41538
|
+
if (urlKey(crawl.finalUrl) !== urlKey(row.targetUrl)) return [finding({
|
|
41539
|
+
row,
|
|
41540
|
+
code: "linked-redirect-target"
|
|
41541
|
+
})];
|
|
41542
|
+
if (crawl.canonical && urlKey(crawl.canonical) !== urlKey(crawl.finalUrl)) return [finding({
|
|
41543
|
+
row,
|
|
41544
|
+
code: "linked-canonical-conflict"
|
|
41545
|
+
})];
|
|
41546
|
+
if (!crawl.indexable) return [finding({
|
|
41547
|
+
row,
|
|
41548
|
+
code: "linked-non-indexable-target"
|
|
41549
|
+
})];
|
|
41550
|
+
return [];
|
|
41551
|
+
}).sort((left, right) => ({
|
|
41552
|
+
high: 0,
|
|
41553
|
+
medium: 1,
|
|
41554
|
+
low: 2
|
|
41555
|
+
})[left.priority] - {
|
|
41556
|
+
high: 0,
|
|
41557
|
+
medium: 1,
|
|
41558
|
+
low: 2
|
|
41559
|
+
}[right.priority] || right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0)).slice(0, MAX_FINDINGS$1);
|
|
41560
|
+
}
|
|
41561
|
+
//#endregion
|
|
41562
|
+
//#region packages/core/src/links/context.ts
|
|
41563
|
+
const DEFAULT_DAYS = 90;
|
|
41564
|
+
const MAX_DAYS = 548;
|
|
41565
|
+
const DEFAULT_CONTEXT_LIMIT = 100;
|
|
41566
|
+
const MAX_CONTEXT_LIMIT = 100;
|
|
41567
|
+
const MAX_GSC_ROWS = 25e3;
|
|
41568
|
+
const defaultDependencies = {
|
|
41569
|
+
latestCrawl: latestCrawlReport,
|
|
41570
|
+
searchAnalytics: querySearchAnalytics,
|
|
41571
|
+
now: () => /* @__PURE__ */ new Date()
|
|
41572
|
+
};
|
|
41573
|
+
function integer(input) {
|
|
41574
|
+
const value = input.value ?? input.fallback;
|
|
41575
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be between 1 and ${input.maximum}.`);
|
|
41576
|
+
return value;
|
|
41577
|
+
}
|
|
41578
|
+
function searchEvidence(input) {
|
|
41579
|
+
if (!input.site) return {
|
|
41580
|
+
state: "unavailable",
|
|
41581
|
+
reason: "No Search Console property was selected for this report."
|
|
41582
|
+
};
|
|
41583
|
+
if (input.error) return {
|
|
41584
|
+
state: "unavailable",
|
|
41585
|
+
reason: input.error
|
|
41586
|
+
};
|
|
41587
|
+
if (!input.aggregate) return {
|
|
41588
|
+
state: "not-retained",
|
|
41589
|
+
reason: input.retainedLimitReached ? `The bounded Search Console response reached ${MAX_GSC_ROWS} rows and did not retain this page.` : "Search Console did not retain a page row for this URL in the selected window."
|
|
41590
|
+
};
|
|
41591
|
+
return {
|
|
41592
|
+
state: "observed",
|
|
41593
|
+
clicks: input.aggregate.clicks,
|
|
41594
|
+
impressions: input.aggregate.impressions,
|
|
41595
|
+
ctr: input.aggregate.impressions > 0 ? input.aggregate.clicks / input.aggregate.impressions : 0,
|
|
41596
|
+
position: input.aggregate.impressions > 0 ? input.aggregate.positionWeight / input.aggregate.impressions : 0
|
|
41597
|
+
};
|
|
41598
|
+
}
|
|
41599
|
+
async function linkTargetContext(input, dependencies = defaultDependencies) {
|
|
41600
|
+
const days = integer({
|
|
41601
|
+
value: input.days,
|
|
41602
|
+
fallback: DEFAULT_DAYS,
|
|
41603
|
+
maximum: MAX_DAYS,
|
|
41604
|
+
label: "Link context days"
|
|
41605
|
+
});
|
|
41606
|
+
const limit = integer({
|
|
41607
|
+
value: input.limit,
|
|
41608
|
+
fallback: DEFAULT_CONTEXT_LIMIT,
|
|
41609
|
+
maximum: MAX_CONTEXT_LIMIT,
|
|
41610
|
+
label: "Link context limit"
|
|
41611
|
+
});
|
|
41612
|
+
const available = linkTargetCounts(input.evidence);
|
|
41613
|
+
const selected = available.slice(0, limit);
|
|
41614
|
+
const crawlLookup = input.crawlSite ?? input.searchConsoleSite;
|
|
41615
|
+
const crawl = crawlLookup ? dependencies.latestCrawl(crawlLookup) : void 0;
|
|
41616
|
+
const crawlValues = crawlForLinkTargets({
|
|
41617
|
+
report: crawl,
|
|
41618
|
+
targets: selected
|
|
41619
|
+
});
|
|
41620
|
+
const warnings = [];
|
|
41621
|
+
const range = finalGscDateRange(days, dependencies.now());
|
|
41622
|
+
let searchRows = /* @__PURE__ */ new Map();
|
|
41623
|
+
let searchCalls = 0;
|
|
41624
|
+
let searchRowsFetched = 0;
|
|
41625
|
+
let searchError = null;
|
|
41626
|
+
if (input.searchConsoleSite) try {
|
|
41627
|
+
const result = await dependencies.searchAnalytics(input.searchConsoleSite, {
|
|
41628
|
+
...range,
|
|
41629
|
+
dimensions: ["page"],
|
|
41630
|
+
type: "web",
|
|
41631
|
+
dataState: "final",
|
|
41632
|
+
rowLimit: MAX_GSC_ROWS,
|
|
41633
|
+
maxRows: MAX_GSC_ROWS
|
|
41634
|
+
}, { refresh: input.refresh });
|
|
41635
|
+
searchRows = aggregateLinkSearchRows(result.rows);
|
|
41636
|
+
searchCalls = result.calls;
|
|
41637
|
+
searchRowsFetched = result.rowsFetched;
|
|
41638
|
+
} catch (error) {
|
|
41639
|
+
searchError = error instanceof Error ? error.message : "Search Console evidence could not be loaded.";
|
|
41640
|
+
warnings.push(`Search Console context was unavailable. ${searchError}`);
|
|
41641
|
+
}
|
|
41642
|
+
let searchMatches = 0;
|
|
41643
|
+
const retainedLimitReached = searchRowsFetched >= MAX_GSC_ROWS;
|
|
41644
|
+
const rows = selected.map((target) => {
|
|
41645
|
+
const aggregate = searchRows.get(urlKey(target.targetUrl) ?? "");
|
|
41646
|
+
if (aggregate) searchMatches += 1;
|
|
41647
|
+
return {
|
|
41648
|
+
...target,
|
|
41649
|
+
crawl: crawlValues.values.get(target.targetUrl) ?? {
|
|
41650
|
+
state: "not-observed",
|
|
41651
|
+
reason: "The target URL was not present in the retained crawl pages."
|
|
41652
|
+
},
|
|
41653
|
+
searchConsole: searchEvidence({
|
|
41654
|
+
site: input.searchConsoleSite,
|
|
41655
|
+
error: searchError,
|
|
41656
|
+
retainedLimitReached,
|
|
41657
|
+
aggregate
|
|
41658
|
+
})
|
|
41659
|
+
};
|
|
41660
|
+
});
|
|
41661
|
+
if (!crawl) warnings.push("No matching saved crawl was available, so target response and indexability checks were skipped.");
|
|
41662
|
+
if (retainedLimitReached) warnings.push(`Search Console reached the ${MAX_GSC_ROWS}-row acquisition limit. Missing target rows remain not retained rather than zero.`);
|
|
41663
|
+
const unavailableSources = Number(!crawl) + Number(!input.searchConsoleSite);
|
|
41664
|
+
return {
|
|
41665
|
+
schemaVersion: 1,
|
|
41666
|
+
dataStatus: unavailableSources === 2 ? "unavailable" : unavailableSources > 0 || searchError || retainedLimitReached ? "partial" : "complete",
|
|
41667
|
+
selection: {
|
|
41668
|
+
availableTargets: available.length,
|
|
41669
|
+
returnedTargets: rows.length,
|
|
41670
|
+
omittedTargets: Math.max(0, available.length - rows.length),
|
|
41671
|
+
limit
|
|
41672
|
+
},
|
|
41673
|
+
provenance: {
|
|
41674
|
+
crawl: {
|
|
41675
|
+
status: crawl ? "joined" : "unavailable",
|
|
41676
|
+
reportId: crawl?.id ?? null,
|
|
41677
|
+
observedAt: crawl?.generatedAt ?? null,
|
|
41678
|
+
availablePages: crawl?.pages.length ?? 0,
|
|
41679
|
+
matchedTargets: crawlValues.matched
|
|
41680
|
+
},
|
|
41681
|
+
searchConsole: {
|
|
41682
|
+
status: !input.searchConsoleSite || searchError ? "unavailable" : retainedLimitReached ? "partial" : "joined",
|
|
41683
|
+
site: input.searchConsoleSite ?? null,
|
|
41684
|
+
range: input.searchConsoleSite ? {
|
|
41685
|
+
...range,
|
|
41686
|
+
days
|
|
41687
|
+
} : null,
|
|
41688
|
+
calls: searchCalls,
|
|
41689
|
+
rowsFetched: searchRowsFetched,
|
|
41690
|
+
retainedRowLimit: MAX_GSC_ROWS,
|
|
41691
|
+
retainedRowLimitReached: retainedLimitReached,
|
|
41692
|
+
matchedTargets: searchMatches
|
|
41693
|
+
}
|
|
41694
|
+
},
|
|
41695
|
+
rows,
|
|
41696
|
+
findings: linkTargetFindings(rows),
|
|
41697
|
+
warnings
|
|
41698
|
+
};
|
|
41699
|
+
}
|
|
41700
|
+
//#endregion
|
|
41701
|
+
//#region packages/core/src/providers/dataforseo/link-research-shared.ts
|
|
41702
|
+
function linkTarget(value, requestedScope = "domain") {
|
|
41703
|
+
const raw = value.trim();
|
|
41704
|
+
if (!raw || raw.length > 2048) throw invalidTarget();
|
|
41705
|
+
if (requestedScope === "page") try {
|
|
41706
|
+
const url = new URL(raw);
|
|
41707
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error();
|
|
41708
|
+
url.hash = "";
|
|
41709
|
+
return {
|
|
41710
|
+
target: url.toString(),
|
|
41711
|
+
scope: "page"
|
|
41712
|
+
};
|
|
41713
|
+
} catch {
|
|
41714
|
+
throw invalidTarget();
|
|
41715
|
+
}
|
|
41716
|
+
try {
|
|
41717
|
+
const hostname = new URL(raw.includes("://") ? raw : `https://${raw}`).hostname.toLowerCase().replace(/^www\./u, "").replace(/\.$/u, "");
|
|
41718
|
+
if (!hostname || hostname.length > 253 || hostname.includes("..") || !hostname.includes(".") || !/^[a-z0-9.-]+$/u.test(hostname)) throw new Error();
|
|
41719
|
+
return {
|
|
41720
|
+
target: hostname,
|
|
41721
|
+
scope: "domain"
|
|
41722
|
+
};
|
|
41723
|
+
} catch {
|
|
41724
|
+
throw invalidTarget();
|
|
41725
|
+
}
|
|
41726
|
+
}
|
|
41727
|
+
function invalidTarget() {
|
|
41728
|
+
return new ProviderError({
|
|
41729
|
+
provider: "dataforseo",
|
|
41730
|
+
operation: "link-evidence",
|
|
41731
|
+
code: "configuration",
|
|
41732
|
+
message: "Use a valid domain, or choose page scope and pass an absolute URL."
|
|
41733
|
+
});
|
|
41734
|
+
}
|
|
41735
|
+
function linkRowLimit(limit, offset = 0) {
|
|
41736
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
|
|
41737
|
+
provider: "dataforseo",
|
|
41738
|
+
operation: "link-evidence",
|
|
41739
|
+
code: "configuration",
|
|
41740
|
+
message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
|
|
41741
|
+
});
|
|
41742
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 2e4) throw new ProviderError({
|
|
41743
|
+
provider: "dataforseo",
|
|
41744
|
+
operation: "link-evidence",
|
|
41745
|
+
code: "configuration",
|
|
41746
|
+
message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
|
|
41747
|
+
});
|
|
41748
|
+
}
|
|
41749
|
+
function requestContext(reportId, supplied) {
|
|
41750
|
+
return supplied ?? {
|
|
41751
|
+
reportId,
|
|
41752
|
+
reportRunId: randomUUID()
|
|
41753
|
+
};
|
|
41754
|
+
}
|
|
41755
|
+
function missing(field) {
|
|
41756
|
+
return unavailableValue("missing", `DataForSEO omitted ${field}.`);
|
|
41757
|
+
}
|
|
41758
|
+
function numberValue(value, field) {
|
|
41759
|
+
return value === null || value === void 0 ? missing(field) : observedValue(value);
|
|
41760
|
+
}
|
|
41761
|
+
function stringValue(value, field) {
|
|
41762
|
+
return value ? observedValue(value) : missing(field);
|
|
41763
|
+
}
|
|
41764
|
+
function metric(id, label, value) {
|
|
41765
|
+
return value === null || value === void 0 ? [] : [{
|
|
41766
|
+
provider: "dataforseo",
|
|
41767
|
+
id,
|
|
41768
|
+
label,
|
|
41769
|
+
value,
|
|
41770
|
+
scale: {
|
|
41771
|
+
minimum: 0,
|
|
41772
|
+
maximum: 100
|
|
41773
|
+
}
|
|
41774
|
+
}];
|
|
41775
|
+
}
|
|
41776
|
+
function safeUrl(value) {
|
|
41777
|
+
if (!value) return null;
|
|
41778
|
+
try {
|
|
41779
|
+
const url = new URL(value);
|
|
41780
|
+
if (!["http:", "https:"].includes(url.protocol)) return null;
|
|
41781
|
+
url.username = "";
|
|
41782
|
+
url.password = "";
|
|
41783
|
+
url.hash = "";
|
|
41784
|
+
return url.toString();
|
|
41785
|
+
} catch {
|
|
41786
|
+
return null;
|
|
41787
|
+
}
|
|
41788
|
+
}
|
|
41789
|
+
function normalizedDate(value) {
|
|
41790
|
+
if (!value) return null;
|
|
41791
|
+
const parsed = Date.parse(value);
|
|
41792
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
41793
|
+
}
|
|
41794
|
+
function uniqueSorted(values = []) {
|
|
41795
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort(compareCodepoints$3);
|
|
41796
|
+
}
|
|
41797
|
+
function totalRows(values) {
|
|
41798
|
+
const present = values.filter((value) => value !== null && value !== void 0);
|
|
41799
|
+
return present.length ? Math.max(...present) : null;
|
|
41800
|
+
}
|
|
41801
|
+
function linkCoverage(input) {
|
|
41802
|
+
const hasMore = input.providerTotalComparable !== false && input.providerTotalRows !== null ? input.offset + input.returnedRows < input.providerTotalRows : input.returnedRows >= input.requestedRows;
|
|
41803
|
+
return {
|
|
41804
|
+
requestedRows: input.requestedRows,
|
|
41805
|
+
returnedRows: input.returnedRows,
|
|
41806
|
+
retainedRows: input.retainedRows,
|
|
41807
|
+
invalidRows: input.invalidRows,
|
|
41808
|
+
providerTotalRows: input.providerTotalRows,
|
|
41809
|
+
completeness: input.invalidRows > 0 ? "partial" : hasMore ? "capped" : input.filtered ? "filtered" : "complete",
|
|
41810
|
+
nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
|
|
41811
|
+
};
|
|
41812
|
+
}
|
|
41813
|
+
function linkEvidence(input) {
|
|
41814
|
+
return {
|
|
41815
|
+
schemaVersion: 1,
|
|
41816
|
+
provider: "dataforseo",
|
|
41817
|
+
capability: input.capability,
|
|
41818
|
+
data: input.data,
|
|
41819
|
+
observedAt: input.snapshot.observedAt,
|
|
41820
|
+
market: null,
|
|
41821
|
+
coverage: input.coverage,
|
|
41822
|
+
cache: input.snapshot.cache,
|
|
41823
|
+
cost: input.snapshot.cost,
|
|
41824
|
+
request: {
|
|
41825
|
+
operation: input.capability,
|
|
41826
|
+
endpoint: input.endpoint,
|
|
41827
|
+
limit: input.limit,
|
|
41828
|
+
filters: input.filters,
|
|
41829
|
+
sort: input.sort
|
|
41830
|
+
},
|
|
41831
|
+
warnings: input.warnings
|
|
41832
|
+
};
|
|
41833
|
+
}
|
|
41834
|
+
function mappingWarnings(input) {
|
|
41835
|
+
return [
|
|
41836
|
+
...input.snapshotWarnings,
|
|
41837
|
+
...input.invalidRows ? [{
|
|
41838
|
+
code: `invalid-${input.label}-rows`,
|
|
41839
|
+
field: "data.rows",
|
|
41840
|
+
message: `${input.invalidRows} ${input.label} row${input.invalidRows === 1 ? "" : "s"} lacked required fields and were omitted.`
|
|
41841
|
+
}] : [],
|
|
41842
|
+
...input.duplicateRows ? [{
|
|
41843
|
+
code: `duplicate-${input.label}-rows`,
|
|
41844
|
+
field: "data.rows",
|
|
41845
|
+
message: `${input.duplicateRows} duplicate ${input.label} row${input.duplicateRows === 1 ? "" : "s"} were collapsed deterministically.`
|
|
41846
|
+
}] : []
|
|
41847
|
+
];
|
|
41848
|
+
}
|
|
41849
|
+
//#endregion
|
|
41850
|
+
//#region packages/core/src/providers/dataforseo/backlinks.ts
|
|
41851
|
+
function metricValue(row, id) {
|
|
41852
|
+
return row.metrics.find((item) => item.id === id)?.value ?? -1;
|
|
41853
|
+
}
|
|
41854
|
+
function compareRows(left, right) {
|
|
41855
|
+
return metricValue(right, "backlink-rank") - metricValue(left, "backlink-rank") || metricValue(right, "source-domain-rank") - metricValue(left, "source-domain-rank") || compareCodepoints$3(left.sourceDomain, right.sourceDomain) || compareCodepoints$3(left.sourceUrl, right.sourceUrl) || compareCodepoints$3(left.targetUrl, right.targetUrl) || compareCodepoints$3(left.anchorText ?? "", right.anchorText ?? "");
|
|
41856
|
+
}
|
|
41857
|
+
function dedupe(rows) {
|
|
41858
|
+
const sorted = [...rows].sort(compareRows);
|
|
41859
|
+
const seen = /* @__PURE__ */ new Set();
|
|
41860
|
+
return sorted.filter((row) => {
|
|
41861
|
+
const key = `${row.sourceUrl}\0${row.targetUrl}\0${row.anchorText ?? ""}`;
|
|
41862
|
+
if (seen.has(key)) return false;
|
|
41863
|
+
seen.add(key);
|
|
41864
|
+
return true;
|
|
41865
|
+
});
|
|
41866
|
+
}
|
|
41867
|
+
async function dataForSeoBacklinks(client, input) {
|
|
41868
|
+
const normalized = linkTarget(input.target, input.scope);
|
|
41869
|
+
const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
|
|
41870
|
+
const offset = input.offset ?? 0;
|
|
41871
|
+
linkRowLimit(input.limit, offset);
|
|
41872
|
+
const mode = input.mode ?? "representative";
|
|
41873
|
+
const status = input.status ?? "live";
|
|
41874
|
+
const providerMode = mode === "representative" ? "one_per_domain" : "as_is";
|
|
41875
|
+
const orderBy = [
|
|
41876
|
+
"rank,desc",
|
|
41877
|
+
"domain_from_rank,desc",
|
|
41878
|
+
"url_from,asc"
|
|
41879
|
+
];
|
|
41880
|
+
const snapshot = await client.backlinks({
|
|
41881
|
+
...normalized,
|
|
41882
|
+
includeSubdomains,
|
|
41883
|
+
mode: providerMode,
|
|
41884
|
+
status,
|
|
41885
|
+
limit: input.limit,
|
|
41886
|
+
offset,
|
|
41887
|
+
orderBy,
|
|
41888
|
+
refresh: input.refresh,
|
|
41889
|
+
context: requestContext("link-evidence", input.context)
|
|
41890
|
+
});
|
|
41891
|
+
const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
41892
|
+
const rawRows = results.flatMap((result) => result.items ?? []);
|
|
41893
|
+
let invalidRows = 0;
|
|
41894
|
+
const mapped = rawRows.flatMap((row) => {
|
|
41895
|
+
const sourceUrl = safeUrl(row.url_from);
|
|
41896
|
+
const targetUrl = safeUrl(row.url_to);
|
|
41897
|
+
if (!sourceUrl || !targetUrl) {
|
|
41898
|
+
invalidRows += 1;
|
|
41899
|
+
return [];
|
|
41900
|
+
}
|
|
41901
|
+
return [{
|
|
41902
|
+
sourceUrl,
|
|
41903
|
+
sourceDomain: new URL(sourceUrl).hostname.toLowerCase(),
|
|
41904
|
+
targetUrl,
|
|
41905
|
+
anchorText: row.anchor ?? null,
|
|
41906
|
+
linkType: row.item_type ?? null,
|
|
41907
|
+
dofollow: row.dofollow ?? null,
|
|
41908
|
+
attributes: uniqueSorted(row.rel_attributes ?? row.attributes ?? []),
|
|
41909
|
+
firstSeenAt: normalizedDate(row.first_seen),
|
|
41910
|
+
lastSeenAt: normalizedDate(row.lost_date ?? row.last_visited),
|
|
41911
|
+
state: row.is_lost || row.lost_date ? "lost" : "live",
|
|
41912
|
+
indirect: row.is_indirect_link ?? null,
|
|
41913
|
+
linksFromPage: row.links_count ?? null,
|
|
41914
|
+
linksFromDomain: row.group_count ?? null,
|
|
41915
|
+
metrics: [
|
|
41916
|
+
...metric("backlink-rank", "DataForSEO backlink rank", row.rank),
|
|
41917
|
+
...metric("source-domain-rank", "DataForSEO source domain rank", row.domain_from_rank),
|
|
41918
|
+
...metric("source-page-rank", "DataForSEO source page rank", row.page_from_rank),
|
|
41919
|
+
...metric("backlink-spam-score", "DataForSEO backlink spam score", row.backlink_spam_score ?? row.backlinks_spam_score)
|
|
41920
|
+
]
|
|
41921
|
+
}];
|
|
41922
|
+
});
|
|
41923
|
+
const rows = dedupe(mapped);
|
|
41924
|
+
const duplicateRows = mapped.length - rows.length;
|
|
41925
|
+
const providerTotalRows = totalRows(results.map((result) => result.total_count));
|
|
41926
|
+
return linkEvidence({
|
|
41927
|
+
capability: "backlinks",
|
|
41928
|
+
data: {
|
|
41929
|
+
target: normalized.target,
|
|
41930
|
+
mode,
|
|
41931
|
+
rows,
|
|
41932
|
+
totalRows: providerTotalRows
|
|
41933
|
+
},
|
|
41934
|
+
snapshot,
|
|
41935
|
+
coverage: linkCoverage({
|
|
41936
|
+
requestedRows: input.limit,
|
|
41937
|
+
returnedRows: rawRows.length,
|
|
41938
|
+
retainedRows: rows.length,
|
|
41939
|
+
invalidRows,
|
|
41940
|
+
providerTotalRows,
|
|
41941
|
+
offset,
|
|
41942
|
+
filtered: mode === "representative" || status !== "all",
|
|
41943
|
+
providerTotalComparable: mode !== "representative"
|
|
41944
|
+
}),
|
|
41945
|
+
endpoint: LINK_ENDPOINTS.backlinks,
|
|
41946
|
+
limit: input.limit,
|
|
41947
|
+
filters: {
|
|
41948
|
+
scope: normalized.scope,
|
|
41949
|
+
includeSubdomains,
|
|
41950
|
+
mode,
|
|
41951
|
+
status,
|
|
41952
|
+
includeIndirectLinks: true,
|
|
41953
|
+
excludeInternalBacklinks: true,
|
|
41954
|
+
offset
|
|
41955
|
+
},
|
|
41956
|
+
sort: orderBy,
|
|
41957
|
+
warnings: mappingWarnings({
|
|
41958
|
+
snapshotWarnings: snapshot.warnings,
|
|
41959
|
+
invalidRows,
|
|
41960
|
+
duplicateRows,
|
|
41961
|
+
label: "backlink"
|
|
41962
|
+
})
|
|
41963
|
+
});
|
|
41964
|
+
}
|
|
41965
|
+
//#endregion
|
|
41966
|
+
//#region packages/core/src/providers/dataforseo/link-summary.ts
|
|
41967
|
+
async function dataForSeoLinkSummary(client, input) {
|
|
41968
|
+
const normalized = linkTarget(input.target, input.scope);
|
|
41969
|
+
const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
|
|
41970
|
+
const snapshot = await client.linkSummary({
|
|
41971
|
+
...normalized,
|
|
41972
|
+
includeSubdomains,
|
|
41973
|
+
refresh: input.refresh,
|
|
41974
|
+
context: requestContext("link-evidence", input.context)
|
|
41975
|
+
});
|
|
41976
|
+
const rows = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
41977
|
+
const row = [...rows].sort((left, right) => compareCodepoints$3(left.target ?? "", right.target ?? ""))[0];
|
|
41978
|
+
const data = {
|
|
41979
|
+
target: normalized.target,
|
|
41980
|
+
scope: normalized.scope,
|
|
41981
|
+
backlinks: numberValue(row?.backlinks, "total backlinks"),
|
|
41982
|
+
referringDomains: numberValue(row?.referring_domains, "referring domains"),
|
|
41983
|
+
referringPages: numberValue(row?.referring_pages, "referring pages"),
|
|
41984
|
+
brokenBacklinks: numberValue(row?.broken_backlinks, "broken backlinks"),
|
|
41985
|
+
brokenPages: numberValue(row?.broken_pages, "broken referring pages"),
|
|
41986
|
+
metrics: [
|
|
41987
|
+
...metric("rank", "DataForSEO rank", row?.rank),
|
|
41988
|
+
...metric("backlinks-spam-score", "DataForSEO backlink spam score", row?.backlinks_spam_score),
|
|
41989
|
+
...metric("target-spam-score", "DataForSEO target spam score", row?.info?.target_spam_score)
|
|
41990
|
+
]
|
|
41991
|
+
};
|
|
41992
|
+
const retainedRows = row ? 1 : 0;
|
|
41993
|
+
return linkEvidence({
|
|
41994
|
+
capability: "link-summary",
|
|
41995
|
+
data,
|
|
41996
|
+
snapshot,
|
|
41997
|
+
coverage: {
|
|
41998
|
+
requestedRows: 1,
|
|
41999
|
+
returnedRows: rows.length,
|
|
42000
|
+
retainedRows,
|
|
42001
|
+
invalidRows: 0,
|
|
42002
|
+
providerTotalRows: rows.length,
|
|
42003
|
+
completeness: row ? "complete" : "unavailable",
|
|
42004
|
+
nextCursor: null
|
|
42005
|
+
},
|
|
42006
|
+
endpoint: LINK_ENDPOINTS.summary,
|
|
42007
|
+
limit: 1,
|
|
42008
|
+
filters: {
|
|
42009
|
+
scope: normalized.scope,
|
|
42010
|
+
includeSubdomains,
|
|
42011
|
+
status: "live",
|
|
42012
|
+
includeIndirectLinks: true,
|
|
42013
|
+
excludeInternalBacklinks: true
|
|
42014
|
+
},
|
|
42015
|
+
sort: [],
|
|
42016
|
+
warnings: [
|
|
42017
|
+
...snapshot.warnings,
|
|
42018
|
+
...rows.length > 1 ? [{
|
|
42019
|
+
code: "multiple-link-summary-rows",
|
|
42020
|
+
field: "data",
|
|
42021
|
+
message: "DataForSEO returned more than one summary row. One row was retained deterministically."
|
|
42022
|
+
}] : [],
|
|
42023
|
+
...!row ? [{
|
|
42024
|
+
code: "missing-link-summary-row",
|
|
42025
|
+
field: "data",
|
|
42026
|
+
message: "DataForSEO returned no summary row. Counts remain unavailable rather than zero."
|
|
42027
|
+
}] : []
|
|
42028
|
+
]
|
|
42029
|
+
});
|
|
42030
|
+
}
|
|
42031
|
+
//#endregion
|
|
42032
|
+
//#region packages/core/src/providers/dataforseo/referring-domains.ts
|
|
42033
|
+
function observedNumber(row) {
|
|
42034
|
+
return row.backlinks.state === "observed" ? row.backlinks.value : -1;
|
|
42035
|
+
}
|
|
42036
|
+
async function dataForSeoReferringDomains(client, input) {
|
|
42037
|
+
const normalized = linkTarget(input.target, input.scope);
|
|
42038
|
+
const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
|
|
42039
|
+
const offset = input.offset ?? 0;
|
|
42040
|
+
linkRowLimit(input.limit, offset);
|
|
42041
|
+
const orderBy = [
|
|
42042
|
+
"backlinks,desc",
|
|
42043
|
+
"rank,desc",
|
|
42044
|
+
"domain,asc"
|
|
42045
|
+
];
|
|
42046
|
+
const snapshot = await client.referringDomains({
|
|
42047
|
+
...normalized,
|
|
42048
|
+
includeSubdomains,
|
|
42049
|
+
limit: input.limit,
|
|
42050
|
+
offset,
|
|
42051
|
+
orderBy,
|
|
42052
|
+
refresh: input.refresh,
|
|
42053
|
+
context: requestContext("link-evidence", input.context)
|
|
42054
|
+
});
|
|
42055
|
+
const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
42056
|
+
const rawRows = results.flatMap((result) => result.items ?? []);
|
|
42057
|
+
let invalidRows = 0;
|
|
42058
|
+
const mapped = rawRows.flatMap((row) => {
|
|
42059
|
+
const domain = row.domain?.trim().toLowerCase();
|
|
42060
|
+
if (!domain) {
|
|
42061
|
+
invalidRows += 1;
|
|
42062
|
+
return [];
|
|
42063
|
+
}
|
|
42064
|
+
const firstSeenAt = normalizedDate(row.first_seen);
|
|
42065
|
+
return [{
|
|
42066
|
+
domain,
|
|
42067
|
+
backlinks: numberValue(row.backlinks, "domain backlinks"),
|
|
42068
|
+
referringPages: numberValue(row.referring_pages, "domain referring pages"),
|
|
42069
|
+
brokenBacklinks: numberValue(row.broken_backlinks, "domain broken backlinks"),
|
|
42070
|
+
brokenPages: numberValue(row.broken_pages, "domain broken pages"),
|
|
42071
|
+
firstSeenAt: stringValue(firstSeenAt, "domain first seen date"),
|
|
42072
|
+
metrics: [...metric("source-domain-rank", "DataForSEO source domain rank", row.rank), ...metric("domain-backlinks-spam-score", "DataForSEO domain backlink spam score", row.backlinks_spam_score)]
|
|
42073
|
+
}];
|
|
42074
|
+
});
|
|
42075
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
42076
|
+
for (const row of mapped.sort((left, right) => observedNumber(right) - observedNumber(left) || compareCodepoints$3(left.domain, right.domain))) if (!byDomain.has(row.domain)) byDomain.set(row.domain, row);
|
|
42077
|
+
const rows = [...byDomain.values()];
|
|
42078
|
+
const duplicateRows = mapped.length - rows.length;
|
|
42079
|
+
const providerTotalRows = totalRows(results.map((result) => result.total_count));
|
|
42080
|
+
return linkEvidence({
|
|
42081
|
+
capability: "referring-domains",
|
|
42082
|
+
data: {
|
|
42083
|
+
target: normalized.target,
|
|
42084
|
+
rows,
|
|
42085
|
+
totalRows: providerTotalRows
|
|
42086
|
+
},
|
|
42087
|
+
snapshot,
|
|
42088
|
+
coverage: linkCoverage({
|
|
42089
|
+
requestedRows: input.limit,
|
|
42090
|
+
returnedRows: rawRows.length,
|
|
42091
|
+
retainedRows: rows.length,
|
|
42092
|
+
invalidRows,
|
|
42093
|
+
providerTotalRows,
|
|
42094
|
+
offset,
|
|
42095
|
+
filtered: true
|
|
42096
|
+
}),
|
|
42097
|
+
endpoint: LINK_ENDPOINTS.referringDomains,
|
|
42098
|
+
limit: input.limit,
|
|
42099
|
+
filters: {
|
|
42100
|
+
scope: normalized.scope,
|
|
42101
|
+
includeSubdomains,
|
|
42102
|
+
status: "live",
|
|
42103
|
+
includeIndirectLinks: true,
|
|
42104
|
+
excludeInternalBacklinks: true,
|
|
42105
|
+
offset
|
|
42106
|
+
},
|
|
42107
|
+
sort: orderBy,
|
|
42108
|
+
warnings: mappingWarnings({
|
|
42109
|
+
snapshotWarnings: snapshot.warnings,
|
|
42110
|
+
invalidRows,
|
|
42111
|
+
duplicateRows,
|
|
42112
|
+
label: "referring-domain"
|
|
42113
|
+
})
|
|
42114
|
+
});
|
|
42115
|
+
}
|
|
42116
|
+
//#endregion
|
|
42117
|
+
//#region packages/core/src/providers/dataforseo/link-research.ts
|
|
42118
|
+
var DataForSeoLinkProvider = class {
|
|
42119
|
+
provider = "dataforseo";
|
|
42120
|
+
capabilitySupport = [
|
|
42121
|
+
"link-summary",
|
|
42122
|
+
"referring-domains",
|
|
42123
|
+
"backlinks"
|
|
42124
|
+
].map((capability) => ({
|
|
42125
|
+
capability,
|
|
42126
|
+
status: "available",
|
|
42127
|
+
markets: "all"
|
|
42128
|
+
}));
|
|
42129
|
+
client;
|
|
42130
|
+
constructor(options = {}) {
|
|
42131
|
+
this.client = options.client ?? new DataForSeoClient(options);
|
|
42132
|
+
}
|
|
42133
|
+
linkSummary(input) {
|
|
42134
|
+
return dataForSeoLinkSummary(this.client, input);
|
|
42135
|
+
}
|
|
42136
|
+
backlinks(input) {
|
|
42137
|
+
return dataForSeoBacklinks(this.client, input);
|
|
42138
|
+
}
|
|
42139
|
+
referringDomains(input) {
|
|
42140
|
+
return dataForSeoReferringDomains(this.client, input);
|
|
42141
|
+
}
|
|
42142
|
+
};
|
|
42143
|
+
//#endregion
|
|
42144
|
+
//#region packages/core/src/links/dataforseo.ts
|
|
42145
|
+
const DEFAULT_ROW_LIMIT$2 = 100;
|
|
42146
|
+
const MAX_ROW_LIMIT$2 = 500;
|
|
42147
|
+
function rowLimit(value) {
|
|
42148
|
+
const limit = value ?? DEFAULT_ROW_LIMIT$2;
|
|
42149
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_ROW_LIMIT$2) throw new SeoError("INVALID_INPUT", `DataForSEO link row limit must be between 1 and ${MAX_ROW_LIMIT$2}.`);
|
|
42150
|
+
return limit;
|
|
42151
|
+
}
|
|
42152
|
+
async function collectDataForSeoLinkEvidence(input) {
|
|
42153
|
+
const limit = rowLimit(input.rowLimit);
|
|
42154
|
+
const provider = input.provider ?? new DataForSeoLinkProvider();
|
|
42155
|
+
const context = input.context ?? {
|
|
42156
|
+
reportId: "link-evidence",
|
|
42157
|
+
reportRunId: randomUUID()
|
|
42158
|
+
};
|
|
42159
|
+
const summary = await provider.linkSummary({
|
|
42160
|
+
target: input.target,
|
|
42161
|
+
scope: input.scope,
|
|
42162
|
+
includeSubdomains: input.includeSubdomains,
|
|
42163
|
+
refresh: input.refresh,
|
|
42164
|
+
context
|
|
42165
|
+
});
|
|
42166
|
+
const backlinks = await provider.backlinks({
|
|
42167
|
+
target: summary.data.target,
|
|
42168
|
+
scope: summary.data.scope,
|
|
42169
|
+
includeSubdomains: input.includeSubdomains,
|
|
42170
|
+
mode: "representative",
|
|
42171
|
+
status: "live",
|
|
42172
|
+
limit,
|
|
42173
|
+
refresh: input.refresh,
|
|
42174
|
+
context
|
|
42175
|
+
});
|
|
42176
|
+
const rows = backlinks.data.rows.map((row) => ({
|
|
42177
|
+
sourceUrl: row.sourceUrl,
|
|
42178
|
+
sourceDomain: row.sourceDomain,
|
|
42179
|
+
targetUrl: row.targetUrl,
|
|
42180
|
+
anchorText: row.anchorText ?? void 0,
|
|
42181
|
+
firstSeenAt: row.firstSeenAt ?? void 0,
|
|
42182
|
+
lastSeenAt: row.lastSeenAt ?? void 0,
|
|
42183
|
+
nofollow: row.dofollow === null ? void 0 : !row.dofollow,
|
|
42184
|
+
linkType: row.linkType ?? void 0,
|
|
42185
|
+
attributes: row.attributes,
|
|
42186
|
+
state: row.state,
|
|
42187
|
+
indirect: row.indirect ?? void 0,
|
|
42188
|
+
linksFromPage: row.linksFromPage ?? void 0,
|
|
42189
|
+
linksFromDomain: row.linksFromDomain ?? void 0,
|
|
42190
|
+
providerMetrics: row.metrics
|
|
42191
|
+
}));
|
|
42192
|
+
const counts = /* @__PURE__ */ new Map();
|
|
42193
|
+
for (const row of rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
|
|
42194
|
+
const targetCounts = [...counts].map(([targetUrl, observedLinks]) => ({
|
|
42195
|
+
targetUrl,
|
|
42196
|
+
observedLinks
|
|
42197
|
+
})).sort((left, right) => right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0));
|
|
42198
|
+
const completeness = backlinks.coverage.completeness;
|
|
42199
|
+
const representative = backlinks.data.mode === "representative";
|
|
42200
|
+
const partial = completeness !== "complete" || representative;
|
|
42201
|
+
return {
|
|
42202
|
+
rows,
|
|
42203
|
+
targetCounts,
|
|
42204
|
+
provenance: {
|
|
42205
|
+
provider: "dataforseo",
|
|
42206
|
+
observedAt: backlinks.observedAt > summary.observedAt ? backlinks.observedAt : summary.observedAt,
|
|
42207
|
+
cached: summary.cache.status === "hit" && backlinks.cache.status === "hit",
|
|
42208
|
+
suppliedRows: backlinks.coverage.returnedRows ?? rows.length,
|
|
42209
|
+
validRows: rows.length,
|
|
42210
|
+
invalidRows: backlinks.coverage.invalidRows,
|
|
42211
|
+
duplicateRows: Math.max(0, (backlinks.coverage.returnedRows ?? rows.length) - backlinks.coverage.invalidRows - rows.length),
|
|
42212
|
+
capped: ["capped", "partial"].includes(completeness),
|
|
42213
|
+
rowLimit: limit,
|
|
42214
|
+
completeness: partial ? "partial" : "complete",
|
|
42215
|
+
providerRequests: {
|
|
42216
|
+
methods: [summary.request.endpoint, backlinks.request.endpoint],
|
|
42217
|
+
maxConcurrentRequests: 1
|
|
42218
|
+
},
|
|
42219
|
+
providerCoverage: {
|
|
42220
|
+
targetCountRows: {
|
|
42221
|
+
returnedRows: targetCounts.length,
|
|
42222
|
+
retainedRows: targetCounts.length,
|
|
42223
|
+
invalidRows: 0
|
|
42224
|
+
},
|
|
42225
|
+
detailRows: {
|
|
42226
|
+
returnedRows: backlinks.coverage.returnedRows ?? rows.length,
|
|
42227
|
+
retainedRows: rows.length,
|
|
42228
|
+
invalidRows: backlinks.coverage.invalidRows
|
|
42229
|
+
},
|
|
42230
|
+
summaryRows: {
|
|
42231
|
+
returnedRows: summary.coverage.returnedRows ?? 0,
|
|
42232
|
+
retainedRows: summary.coverage.retainedRows ?? 0,
|
|
42233
|
+
invalidRows: summary.coverage.invalidRows
|
|
42234
|
+
},
|
|
42235
|
+
backlinkRows: {
|
|
42236
|
+
returnedRows: backlinks.coverage.returnedRows ?? rows.length,
|
|
42237
|
+
retainedRows: rows.length,
|
|
42238
|
+
invalidRows: backlinks.coverage.invalidRows,
|
|
42239
|
+
providerTotalRows: backlinks.coverage.providerTotalRows
|
|
42240
|
+
}
|
|
42241
|
+
}
|
|
42242
|
+
},
|
|
42243
|
+
externalProvider: {
|
|
42244
|
+
summary,
|
|
42245
|
+
backlinks
|
|
42246
|
+
},
|
|
42247
|
+
warnings: [
|
|
42248
|
+
...summary.warnings.map((warning) => warning.message),
|
|
42249
|
+
...backlinks.warnings.map((warning) => warning.message),
|
|
42250
|
+
...representative ? ["The link list retains one representative backlink per referring domain. Provider summary counts remain separate from retained rows."] : [],
|
|
42251
|
+
...!representative && partial ? ["The link list is bounded or partial. Provider summary counts remain separate from retained rows."] : []
|
|
42252
|
+
]
|
|
42253
|
+
};
|
|
42254
|
+
}
|
|
42255
|
+
//#endregion
|
|
40624
42256
|
//#region packages/core/src/links/import.ts
|
|
40625
42257
|
const DEFAULT_ROW_LIMIT$1 = 1e4;
|
|
40626
42258
|
const MAX_ROW_LIMIT$1 = 1e5;
|
|
@@ -40809,6 +42441,10 @@ async function importLinkEvidence(input) {
|
|
|
40809
42441
|
//#region packages/core/src/links/report.ts
|
|
40810
42442
|
const DEFAULT_OUTPUT_LIMIT = 100;
|
|
40811
42443
|
const MAX_OUTPUT_LIMIT = 500;
|
|
42444
|
+
const MAX_TARGET_COUNTS = 100;
|
|
42445
|
+
const MAX_CONTEXT_ROWS = 100;
|
|
42446
|
+
const MAX_FINDINGS = 50;
|
|
42447
|
+
const MAX_STRUCTURED_DETAIL_ROWS = 750;
|
|
40812
42448
|
function boundedLimit(value) {
|
|
40813
42449
|
const limit = value ?? DEFAULT_OUTPUT_LIMIT;
|
|
40814
42450
|
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_OUTPUT_LIMIT) throw new SeoError("INVALID_INPUT", `Link report limit must be between 1 and ${MAX_OUTPUT_LIMIT}.`);
|
|
@@ -40820,7 +42456,7 @@ function targetCounts(rows) {
|
|
|
40820
42456
|
return [...counts].map(([targetUrl, observedLinks]) => ({
|
|
40821
42457
|
targetUrl,
|
|
40822
42458
|
observedLinks
|
|
40823
|
-
})).sort((a, b) => b.observedLinks - a.observedLinks || a.targetUrl.
|
|
42459
|
+
})).sort((a, b) => b.observedLinks - a.observedLinks || (a.targetUrl < b.targetUrl ? -1 : a.targetUrl > b.targetUrl ? 1 : 0));
|
|
40824
42460
|
}
|
|
40825
42461
|
function linkEvidenceReport(input) {
|
|
40826
42462
|
const limit = boundedLimit(input.limit);
|
|
@@ -40828,8 +42464,24 @@ function linkEvidenceReport(input) {
|
|
|
40828
42464
|
const domains = new Set(input.evidence.rows.map((row) => row.sourceDomain));
|
|
40829
42465
|
const observedTargets = targetCounts(input.evidence.rows);
|
|
40830
42466
|
const counts = input.evidence.targetCounts.length ? input.evidence.targetCounts : observedTargets;
|
|
42467
|
+
const providerEvidence = input.evidence.externalProvider ? {
|
|
42468
|
+
summary: input.evidence.externalProvider.summary,
|
|
42469
|
+
backlinks: {
|
|
42470
|
+
...input.evidence.externalProvider.backlinks,
|
|
42471
|
+
data: {
|
|
42472
|
+
target: input.evidence.externalProvider.backlinks.data.target,
|
|
42473
|
+
mode: input.evidence.externalProvider.backlinks.data.mode,
|
|
42474
|
+
totalRows: input.evidence.externalProvider.backlinks.data.totalRows,
|
|
42475
|
+
retainedRows: input.evidence.externalProvider.backlinks.data.rows.length
|
|
42476
|
+
}
|
|
42477
|
+
}
|
|
42478
|
+
} : null;
|
|
42479
|
+
const targetCountsOutput = counts.slice(0, MAX_TARGET_COUNTS);
|
|
42480
|
+
const contextRows = input.targetContext?.rows.slice(0, MAX_CONTEXT_ROWS) ?? [];
|
|
42481
|
+
const findings = input.targetContext?.findings.slice(0, MAX_FINDINGS) ?? [];
|
|
42482
|
+
const returnedDetailRows = rows.length + targetCountsOutput.length + contextRows.length + findings.length;
|
|
40831
42483
|
return {
|
|
40832
|
-
schemaVersion:
|
|
42484
|
+
schemaVersion: 2,
|
|
40833
42485
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
40834
42486
|
dataStatus: input.evidence.provenance.completeness === "complete" ? "complete" : "partial",
|
|
40835
42487
|
summary: {
|
|
@@ -40838,6 +42490,8 @@ function linkEvidenceReport(input) {
|
|
|
40838
42490
|
targetPages: new Set(input.evidence.rows.map((row) => row.targetUrl)).size,
|
|
40839
42491
|
providerTargetPages: counts.length
|
|
40840
42492
|
},
|
|
42493
|
+
providerSummary: providerEvidence?.summary.data ?? null,
|
|
42494
|
+
providerEvidence,
|
|
40841
42495
|
provenance: input.evidence.provenance,
|
|
40842
42496
|
selection: {
|
|
40843
42497
|
availableRows: input.evidence.rows.length,
|
|
@@ -40845,13 +42499,45 @@ function linkEvidenceReport(input) {
|
|
|
40845
42499
|
omittedRows: Math.max(0, input.evidence.rows.length - rows.length),
|
|
40846
42500
|
limit
|
|
40847
42501
|
},
|
|
40848
|
-
|
|
42502
|
+
outputBudget: {
|
|
42503
|
+
unit: "structured-detail-rows",
|
|
42504
|
+
limit: MAX_STRUCTURED_DETAIL_ROWS,
|
|
42505
|
+
returned: returnedDetailRows,
|
|
42506
|
+
omitted: Math.max(0, input.evidence.rows.length - rows.length + Math.max(0, counts.length - targetCountsOutput.length) + Math.max(0, (input.targetContext?.rows.length ?? 0) - contextRows.length) + Math.max(0, (input.targetContext?.findings.length ?? 0) - findings.length)),
|
|
42507
|
+
sections: {
|
|
42508
|
+
links: {
|
|
42509
|
+
limit: MAX_OUTPUT_LIMIT,
|
|
42510
|
+
returned: rows.length
|
|
42511
|
+
},
|
|
42512
|
+
targetCounts: {
|
|
42513
|
+
limit: MAX_TARGET_COUNTS,
|
|
42514
|
+
returned: targetCountsOutput.length
|
|
42515
|
+
},
|
|
42516
|
+
targetContext: {
|
|
42517
|
+
limit: MAX_CONTEXT_ROWS,
|
|
42518
|
+
returned: contextRows.length
|
|
42519
|
+
},
|
|
42520
|
+
findings: {
|
|
42521
|
+
limit: MAX_FINDINGS,
|
|
42522
|
+
returned: findings.length
|
|
42523
|
+
}
|
|
42524
|
+
}
|
|
42525
|
+
},
|
|
42526
|
+
targetCounts: targetCountsOutput,
|
|
40849
42527
|
links: rows,
|
|
40850
|
-
|
|
42528
|
+
targetContext: input.targetContext ? {
|
|
42529
|
+
...input.targetContext,
|
|
42530
|
+
rows: contextRows,
|
|
42531
|
+
findings
|
|
42532
|
+
} : null,
|
|
42533
|
+
findings,
|
|
42534
|
+
warnings: [...input.evidence.warnings, ...input.targetContext?.warnings ?? []],
|
|
40851
42535
|
caveats: [
|
|
40852
42536
|
"This is bounded provider or imported evidence, not a complete backlink index.",
|
|
40853
42537
|
"A missing link is not proof that no link exists, and an observed link is not a quality or ranking verdict.",
|
|
40854
|
-
"Referring-domain counts describe the retained rows only."
|
|
42538
|
+
"Referring-domain counts describe the retained rows only.",
|
|
42539
|
+
...providerEvidence ? ["Provider profile counts describe its current index. The representative link rows are a separate bounded sample.", "Provider rank and spam metrics keep their provider names and scales. They are context, not ranking factors or universal authority scores."] : [],
|
|
42540
|
+
...input.targetContext ? ["Search Console page rows are retained first-party evidence. Missing or capped rows are never treated as zero.", "Saved crawl findings describe the recorded crawl time. Verify the live target before changing it."] : []
|
|
40855
42541
|
]
|
|
40856
42542
|
};
|
|
40857
42543
|
}
|
|
@@ -41840,6 +43526,7 @@ const TELEMETRY_REPORTS = [
|
|
|
41840
43526
|
"crawl-history",
|
|
41841
43527
|
"crawler-rules",
|
|
41842
43528
|
"llms-txt-audit",
|
|
43529
|
+
"local-search-demand",
|
|
41843
43530
|
"generate-llms-txt",
|
|
41844
43531
|
"measure-change",
|
|
41845
43532
|
"monthly-report",
|
|
@@ -42183,6 +43870,6 @@ function isoDate(daysAgo = 0) {
|
|
|
42183
43870
|
return date.toISOString().slice(0, 10);
|
|
42184
43871
|
}
|
|
42185
43872
|
//#endregion
|
|
42186
|
-
export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, competitorKeywordGapReport, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, domainOverviewReport, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, rankedKeywordsReport, rankingPagesReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpCompetitorsReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
|
|
43873
|
+
export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, AUTOMATIC_BASELINE_SITE_RETENTION, AUTOMATIC_BASELINE_TOTAL_RETENTION, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, BingWebmasterClient, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_RUN_RETENTION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_PROVIDER_SPEND_LIMITS, DataForSeoClient, DataForSeoKeywordDiscoveryProvider, DataForSeoKeywordMetricsProvider, DataForSeoLinkProvider, DataForSeoProvider, DataForSeoRankTrackingCollector, DataForSeoSerpSnapshotProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INDEXNOW_ENDPOINT, INDEXNOW_KEYS_SECRET, INDEXNOW_KEY_ENV, INDEXNOW_MAX_URLS, INDEX_WATCH_ATTEMPT_RETENTION, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, KEYWORD_SET_FIELD_LIMITS, KEYWORD_SET_LIMITS, KEYWORD_SET_SCHEMA_SQL, LINK_RECOVER_RUN_RETENTION, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, PROVIDER_SPEND_MAX_BYTES, PROVIDER_SPEND_MAX_ROWS, PROVIDER_SPEND_RETENTION_DAYS, ProviderError, RANK_TRACKING_LIMITS, RANK_TRACKING_SCHEMA_SQL, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, ResponseSizeLimitError, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, acquirePseoAuditEvidence, activeRankTrackingRun, addKeywordsToSet, addStatus, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeBingCrawl, analyzeBingDimensions, analyzeBingTraffic, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzeKeywordTrend, analyzeLocalSearchRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, bingAnalysisDefaults, bingWebmasterOverview, bingWebmasterSiteUrl, boundedSitemapInventory, buildOkfBundle, buildPseoAuditFromAcquisition, buildPseoAuditReportFromRows, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, checkDatabaseReadiness, classifyCommunityIntent, classifyCrawler, classifyLocalIntent, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, collectBingLinkEvidence, collectDataForSeoLinkEvidence, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareLinkEvidence, compareSegmentRows, competitorKeywordGapReport, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createBingWebmasterClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createIndexNowKey, createIndexNowKeyRecord, createKeywordSet, createLocalIntentClassifier, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteDataForSeoCredentials, deleteKeywordSet, deleteProviderSecret, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, domainOverviewReport, effectiveRobotsDirectives, effectiveSnippetControl, emptyStatusBreakdown, enforceRankTrackingLogicalBytes, ensurePrivateDatabaseFiles, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, executeRankTracking, expectedCtrForPosition, explainOkfValidation, explainRule, failRankTrackingTask, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fetchText, fileSize, finalGscDateRange, finalizeProviderSpend, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getKeywordSet, getOrCreateRankTrackingConfiguration, getPreviousRun, getPrivacySnapshot, getProviderSpendLimits, getProviderSpendSummary, getRankTrackingRun, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, importLinkEvidence, importServerLog, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, keywordDiscoverySourceSchema, keywordMetricsReport, keywordOpportunitiesReport, keywordResearchReport, keywordSetLogicalBytes, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, latestRankTrackingRun, linkEntries, linkEvidenceKey, linkEvidenceReport, linkRecover, linkRecoverRecommendation, linkTargetContext, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listIndexNowKeys, listKeywordSets, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, localSearchReport, loginWithLoopback, makeRunId, mapDataForSeoSerpSnapshot, markRankTasksPosted, markRankTasksPosting, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLinkEvidenceRow, normalizeLoadedCrawlReport, normalizeLocalSearchText, normalizeLocationTerms, normalizeSavedKeyword, noteCacheWrite, observedValue, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseCombinedLogLine, parseIndexNowSite, parseJsonLogLine, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, priorComparableRankTrackingRun, propertyMatchesUrl, providerCapabilitySchema, providerIdSchema, providerPreferenceSchema, providerRequestJson, providerRequestText, providerSpendLimitOverridesSchema, pruneIndexWatchSnapshots, pruneLogs, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoOpportunitiesReport, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, rankObservations, rankTrackingLogicalBytes, rankTrackingReport, rankTrackingTasks, rankedKeywordsReport, rankingPagesReport, readBoundedResponseText, readConfig, readDataForSeoCredentials, readOauthClient, readProviderSecret, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, recoverRankTaskReceipt, redirectTrace, refreshAuthToken, refreshKeywordSet, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, refreshRankTrackingRun, removeIndexNowKey, removeKeywordsFromSet, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderReportHtml, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, reserveProviderSpend, resolveIndexNowKey, resolveProvider, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, safeError, saveClient, saveCrawlReport, saveIndexNowKey, saveRankObservation, saveRankObservations, savedKeywordSetReport, searchMarketSchema, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, serpCompetitorsReport, serpResultsReport, serverLogReport, sessionSourceInListFilter, setClientBingSite, setDefaultClient, setKeywordSetRefreshTime, setProviderSpendLimits, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, startRankTrackingRun, strikingDistance, submitIndexNow, summarizeCrawlReport, targetMatchesDomain, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unavailableValue, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateIndexNowKey, validateIndexNowKeyRecord, validateOkfFiles, verifyIndexNowKey, verifyQueryContent, workflowPresentation, writeConfig, writeDataForSeoCredentials, writeOauthClient, writeProviderSecret, writeTokens, xRobotsDirectives };
|
|
42187
43874
|
|
|
42188
43875
|
//# sourceMappingURL=index.js.map
|