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/mcp.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, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { arch, homedir, platform, totalmem } from "node:os";
|
|
@@ -1811,10 +1811,10 @@ function aiReferralSourceForValue(value) {
|
|
|
1811
1811
|
}
|
|
1812
1812
|
//#endregion
|
|
1813
1813
|
//#region packages/core/dist/analyze/ai-referrals-analysis.js
|
|
1814
|
-
function compareText$
|
|
1814
|
+
function compareText$19(left, right) {
|
|
1815
1815
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
1816
1816
|
}
|
|
1817
|
-
function metric(row, name) {
|
|
1817
|
+
function metric$1(row, name) {
|
|
1818
1818
|
const value = row[name];
|
|
1819
1819
|
const parsed = Number(value);
|
|
1820
1820
|
if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
|
|
@@ -1849,8 +1849,8 @@ function analyzeAiReferralRows(input) {
|
|
|
1849
1849
|
...emptyMetrics()
|
|
1850
1850
|
};
|
|
1851
1851
|
addMetrics(total, {
|
|
1852
|
-
sessions: metric(row, "sessions"),
|
|
1853
|
-
eventCount: metric(row, "eventCount")
|
|
1852
|
+
sessions: metric$1(row, "sessions"),
|
|
1853
|
+
eventCount: metric$1(row, "eventCount")
|
|
1854
1854
|
});
|
|
1855
1855
|
total.observedSessionSources.add(row.sessionSource ?? "");
|
|
1856
1856
|
sourceTotals.set(definition.id, total);
|
|
@@ -1862,8 +1862,8 @@ function analyzeAiReferralRows(input) {
|
|
|
1862
1862
|
const definition = aiReferralSourceForValue(row.sessionSource ?? "");
|
|
1863
1863
|
if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
|
|
1864
1864
|
const metrics = {
|
|
1865
|
-
sessions: metric(row, "sessions"),
|
|
1866
|
-
eventCount: metric(row, "eventCount")
|
|
1865
|
+
sessions: metric$1(row, "sessions"),
|
|
1866
|
+
eventCount: metric$1(row, "eventCount")
|
|
1867
1867
|
};
|
|
1868
1868
|
addMetrics(detailTotal, metrics);
|
|
1869
1869
|
const landingPage = row.landingPagePlusQueryString || "(not set)";
|
|
@@ -1900,7 +1900,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1900
1900
|
const generalCaveat = "Google Analytics only attributes visits that arrive with a recognized session source; some AI visits appear as direct, unassigned, or another source.";
|
|
1901
1901
|
const possiblyTruncated = input.sourceDiscovery.truncated || input.detail.truncated || input.totalUsersEvidence.truncated;
|
|
1902
1902
|
const allLandingPages = [...pageTotals.entries()].map(([landingPage, metrics]) => {
|
|
1903
|
-
const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$
|
|
1903
|
+
const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$19(left[0], right[0]))[0]?.[0];
|
|
1904
1904
|
const topSource = topSourceId ? sourceTotals.get(topSourceId)?.definition : void 0;
|
|
1905
1905
|
if (!topSource) throw new SeoError("PROVIDER_UNAVAILABLE", "Could not resolve the top AI referral source.");
|
|
1906
1906
|
return {
|
|
@@ -1914,7 +1914,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1914
1914
|
label: topSource.label
|
|
1915
1915
|
}
|
|
1916
1916
|
};
|
|
1917
|
-
}).sort((left, right) => right.sessions - left.sessions || compareText$
|
|
1917
|
+
}).sort((left, right) => right.sessions - left.sessions || compareText$19(left.landingPage, right.landingPage));
|
|
1918
1918
|
const landingPages = allLandingPages.slice(0, input.resultLimit);
|
|
1919
1919
|
const selection = {
|
|
1920
1920
|
limit: input.resultLimit,
|
|
@@ -1959,19 +1959,19 @@ function analyzeAiReferralRows(input) {
|
|
|
1959
1959
|
id: source.definition.id,
|
|
1960
1960
|
label: source.definition.label,
|
|
1961
1961
|
source: source.definition.label,
|
|
1962
|
-
observedSessionSources: [...source.observedSessionSources].sort(compareText$
|
|
1962
|
+
observedSessionSources: [...source.observedSessionSources].sort(compareText$19),
|
|
1963
1963
|
sessions: source.sessions,
|
|
1964
1964
|
eventCount: source.eventCount,
|
|
1965
1965
|
totalUsers: null,
|
|
1966
1966
|
shareOfAiSessions: total.sessions ? source.sessions / total.sessions : 0,
|
|
1967
1967
|
share: total.sessions ? source.sessions / total.sessions : 0
|
|
1968
|
-
})).sort((left, right) => right.sessions - left.sessions || compareText$
|
|
1968
|
+
})).sort((left, right) => right.sessions - left.sessions || compareText$19(left.id, right.id)),
|
|
1969
1969
|
landingPages,
|
|
1970
1970
|
daily: [...dailyTotals.entries()].map(([date, metrics]) => ({
|
|
1971
1971
|
date,
|
|
1972
1972
|
...metrics,
|
|
1973
1973
|
totalUsers: null
|
|
1974
|
-
})).sort((left, right) => compareText$
|
|
1974
|
+
})).sort((left, right) => compareText$19(left.date, right.date)),
|
|
1975
1975
|
caveats: [
|
|
1976
1976
|
generalCaveat,
|
|
1977
1977
|
"Attribution uses Google Analytics sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
|
|
@@ -2061,7 +2061,7 @@ function sessionSourceInListFilter(values) {
|
|
|
2061
2061
|
//#endregion
|
|
2062
2062
|
//#region packages/core/dist/analyze/ai-referrals.js
|
|
2063
2063
|
const DEFAULT_MAX_ROWS$1 = 1e5;
|
|
2064
|
-
const MAX_ROWS = 1e5;
|
|
2064
|
+
const MAX_ROWS$1 = 1e5;
|
|
2065
2065
|
const DEFAULT_RESULT_LIMIT = 25;
|
|
2066
2066
|
const MAX_RESULT_LIMIT = 1e3;
|
|
2067
2067
|
function validIsoDate$1(value) {
|
|
@@ -2087,7 +2087,7 @@ function reportRange(input) {
|
|
|
2087
2087
|
function maxRows(input) {
|
|
2088
2088
|
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.");
|
|
2089
2089
|
const value = input.maxRows ?? input.limit ?? DEFAULT_MAX_ROWS$1;
|
|
2090
|
-
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}.`);
|
|
2090
|
+
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}.`);
|
|
2091
2091
|
return value;
|
|
2092
2092
|
}
|
|
2093
2093
|
function resultLimit(input) {
|
|
@@ -5323,7 +5323,7 @@ function estimateSerpTitleWidth(title) {
|
|
|
5323
5323
|
}
|
|
5324
5324
|
//#endregion
|
|
5325
5325
|
//#region packages/core/dist/analyze/audit-page.js
|
|
5326
|
-
const defaultDependencies$
|
|
5326
|
+
const defaultDependencies$16 = {
|
|
5327
5327
|
fetchPage,
|
|
5328
5328
|
queryPageMetrics,
|
|
5329
5329
|
now: () => /* @__PURE__ */ new Date()
|
|
@@ -5354,7 +5354,7 @@ function buildRecommendations(report) {
|
|
|
5354
5354
|
});
|
|
5355
5355
|
return recommendations;
|
|
5356
5356
|
}
|
|
5357
|
-
async function auditPage(input, dependencies = defaultDependencies$
|
|
5357
|
+
async function auditPage(input, dependencies = defaultDependencies$16) {
|
|
5358
5358
|
const fetched = await dependencies.fetchPage(input.url, {
|
|
5359
5359
|
js: input.js,
|
|
5360
5360
|
refresh: input.refresh
|
|
@@ -5496,7 +5496,7 @@ function tokenStem(token) {
|
|
|
5496
5496
|
function tokensMatch(a, b) {
|
|
5497
5497
|
return a === b || tokenStem(a) === tokenStem(b);
|
|
5498
5498
|
}
|
|
5499
|
-
function includesTokenSequence(queryTokens, termTokens) {
|
|
5499
|
+
function includesTokenSequence$1(queryTokens, termTokens) {
|
|
5500
5500
|
if (!termTokens.length || termTokens.length > queryTokens.length) return false;
|
|
5501
5501
|
for (let index = 0; index <= queryTokens.length - termTokens.length; index++) if (termTokens.every((termToken, offset) => tokensMatch(queryTokens[index + offset] ?? "", termToken))) return true;
|
|
5502
5502
|
return false;
|
|
@@ -5525,7 +5525,7 @@ function isBrandQuery(query, brandTerms) {
|
|
|
5525
5525
|
const compactMatches = (compactTermLength >= 6 || /[^\p{ASCII}]/u.test(compactTerm) && compactTermLength >= 2) && compactQuery.includes(compactTerm);
|
|
5526
5526
|
const termTokens = normalizedTerm.split(" ");
|
|
5527
5527
|
if (termTokens.length === 1) return queryTokens.some((queryToken) => tokensMatch(queryToken, normalizedTerm)) || compactMatches;
|
|
5528
|
-
return includesTokenSequence(queryTokens, termTokens) || compactMatches;
|
|
5528
|
+
return includesTokenSequence$1(queryTokens, termTokens) || compactMatches;
|
|
5529
5529
|
});
|
|
5530
5530
|
}
|
|
5531
5531
|
function shouldExcludeBrandQuery(input) {
|
|
@@ -5771,7 +5771,7 @@ function resolveQueryOpportunityInput(input, now) {
|
|
|
5771
5771
|
}
|
|
5772
5772
|
//#endregion
|
|
5773
5773
|
//#region packages/core/dist/analyze/query-opportunity-rows.js
|
|
5774
|
-
function compareText$
|
|
5774
|
+
function compareText$18(left, right) {
|
|
5775
5775
|
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
|
|
5776
5776
|
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
|
|
5777
5777
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -5781,9 +5781,9 @@ function compareText$17(left, right) {
|
|
|
5781
5781
|
return leftPoints.length - rightPoints.length;
|
|
5782
5782
|
}
|
|
5783
5783
|
function compareQueryOpportunityRows(left, right) {
|
|
5784
|
-
return right.impressions - left.impressions || right.clicks - left.clicks || left.position - right.position || compareText$
|
|
5784
|
+
return right.impressions - left.impressions || right.clicks - left.clicks || left.position - right.position || compareText$18(left.query, right.query);
|
|
5785
5785
|
}
|
|
5786
|
-
function validRow$
|
|
5786
|
+
function validRow$4(row, query) {
|
|
5787
5787
|
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;
|
|
5788
5788
|
}
|
|
5789
5789
|
function signature(row) {
|
|
@@ -5793,7 +5793,7 @@ function normalizeQueryOpportunityRows(rows, selection) {
|
|
|
5793
5793
|
const grouped = /* @__PURE__ */ new Map();
|
|
5794
5794
|
for (const row of rows) {
|
|
5795
5795
|
const query = row.keys[0]?.trim() ?? "";
|
|
5796
|
-
if (!validRow$
|
|
5796
|
+
if (!validRow$4(row, query)) {
|
|
5797
5797
|
selection.invalidRows++;
|
|
5798
5798
|
continue;
|
|
5799
5799
|
}
|
|
@@ -6762,11 +6762,11 @@ function selectBenchmarkRows$1(input) {
|
|
|
6762
6762
|
}
|
|
6763
6763
|
//#endregion
|
|
6764
6764
|
//#region packages/core/dist/analyze/page-opportunities-analysis.js
|
|
6765
|
-
function compareText$
|
|
6765
|
+
function compareText$17(left, right) {
|
|
6766
6766
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
6767
6767
|
}
|
|
6768
6768
|
function compareItems$2(left, right) {
|
|
6769
|
-
return (right.estimatedCtrClickShortfall ?? -1) - (left.estimatedCtrClickShortfall ?? -1) || right.impressions - left.impressions || left.position - right.position || compareText$
|
|
6769
|
+
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);
|
|
6770
6770
|
}
|
|
6771
6771
|
function analyzeRow(input) {
|
|
6772
6772
|
const query = input.row.keys[0]?.trim() ?? "";
|
|
@@ -6928,7 +6928,7 @@ function pageOpportunityRecommendations(analysis) {
|
|
|
6928
6928
|
//#endregion
|
|
6929
6929
|
//#region packages/core/dist/analyze/page-opportunities.js
|
|
6930
6930
|
const MAX_BENCHMARK_ROWS = 1e5;
|
|
6931
|
-
const defaultDependencies$
|
|
6931
|
+
const defaultDependencies$15 = {
|
|
6932
6932
|
searchAnalytics: querySearchAnalytics,
|
|
6933
6933
|
fetch: fetchPage,
|
|
6934
6934
|
extract: extractPage,
|
|
@@ -7000,7 +7000,7 @@ function reportCaveats$1(input) {
|
|
|
7000
7000
|
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.` : ""
|
|
7001
7001
|
].filter(Boolean);
|
|
7002
7002
|
}
|
|
7003
|
-
async function pageOpportunitiesReport(input, dependencies = defaultDependencies$
|
|
7003
|
+
async function pageOpportunitiesReport(input, dependencies = defaultDependencies$15) {
|
|
7004
7004
|
const url = assertUrlMatchesGscProperty(input.site, input.url);
|
|
7005
7005
|
const days = reportDays$1(input.days);
|
|
7006
7006
|
const range = defaultDateRange$1(days);
|
|
@@ -10787,7 +10787,7 @@ function absoluteHttpUrl(value) {
|
|
|
10787
10787
|
return false;
|
|
10788
10788
|
}
|
|
10789
10789
|
}
|
|
10790
|
-
function urlKey(value) {
|
|
10790
|
+
function urlKey$1(value) {
|
|
10791
10791
|
if (!value) return void 0;
|
|
10792
10792
|
try {
|
|
10793
10793
|
const url = new URL(value);
|
|
@@ -10986,7 +10986,7 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
10986
10986
|
for (const page of pages) {
|
|
10987
10987
|
if (!isAuditableHtmlPage(page)) continue;
|
|
10988
10988
|
for (const value of [page.url, page.finalUrl]) {
|
|
10989
|
-
const key = urlKey(value);
|
|
10989
|
+
const key = urlKey$1(value);
|
|
10990
10990
|
if (key) pageByUrl.set(key, page);
|
|
10991
10991
|
}
|
|
10992
10992
|
}
|
|
@@ -11128,7 +11128,7 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
11128
11128
|
canonicalRaw: page.canonicalRaw,
|
|
11129
11129
|
canonical: page.canonical
|
|
11130
11130
|
}));
|
|
11131
|
-
const canonicalTarget = pageByUrl.get(urlKey(page.canonical) ?? "");
|
|
11131
|
+
const canonicalTarget = pageByUrl.get(urlKey$1(page.canonical) ?? "");
|
|
11132
11132
|
if (canonicalTarget?.canonical && !sameUrl(canonicalTarget.canonical, page.canonical)) issues.push(issue("canonical_chain", page, page.canonical, {
|
|
11133
11133
|
canonical: page.canonical,
|
|
11134
11134
|
nextCanonical: canonicalTarget.canonical,
|
|
@@ -11302,7 +11302,7 @@ const SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
|
11302
11302
|
"refresh-token"
|
|
11303
11303
|
]);
|
|
11304
11304
|
const SENSITIVE_PARAM_PATTERN = /^(access_?token|auth|api_?key|client_?secret|code|jwt|key|password|refresh_?token|secret|session|sig|signature|token)$/i;
|
|
11305
|
-
function uniqueSorted(values = []) {
|
|
11305
|
+
function uniqueSorted$1(values = []) {
|
|
11306
11306
|
return [...new Set(values.filter(Boolean))].sort();
|
|
11307
11307
|
}
|
|
11308
11308
|
function normalizeFetchRate(input, strategy) {
|
|
@@ -11322,13 +11322,13 @@ function normalizeCrawlConfig(input) {
|
|
|
11322
11322
|
mode: input.mode ?? (strategy === "health" ? "sitemap" : "site"),
|
|
11323
11323
|
strategy,
|
|
11324
11324
|
...input.sitemapUrl ? { sitemapUrl: new URL(input.sitemapUrl).toString() } : {},
|
|
11325
|
-
urls: uniqueSorted(input.urls ?? []),
|
|
11325
|
+
urls: uniqueSorted$1(input.urls ?? []),
|
|
11326
11326
|
maxPages: input.maxPages ?? 500,
|
|
11327
11327
|
maxDepth: input.maxDepth ?? 16,
|
|
11328
11328
|
concurrency,
|
|
11329
11329
|
timeoutMs: input.timeoutMs ?? 2e4,
|
|
11330
|
-
include: uniqueSorted(input.include ?? []),
|
|
11331
|
-
exclude: uniqueSorted(input.exclude ?? []),
|
|
11330
|
+
include: uniqueSorted$1(input.include ?? []),
|
|
11331
|
+
exclude: uniqueSorted$1(input.exclude ?? []),
|
|
11332
11332
|
respectRobots: input.respectRobots ?? true,
|
|
11333
11333
|
useSitemap: input.useSitemap ?? true,
|
|
11334
11334
|
checkExternal: strategy === "health" ? false : input.checkExternal ?? true,
|
|
@@ -11452,19 +11452,19 @@ function sanitizeIssues(issues) {
|
|
|
11452
11452
|
function sanitizeMessages(values = []) {
|
|
11453
11453
|
return values.map((value) => sanitizeTenantString(value));
|
|
11454
11454
|
}
|
|
11455
|
-
function compareText$
|
|
11455
|
+
function compareText$16(left, right) {
|
|
11456
11456
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
11457
11457
|
}
|
|
11458
11458
|
function sortPages(pages) {
|
|
11459
|
-
return [...pages].sort((left, right) => compareText$
|
|
11459
|
+
return [...pages].sort((left, right) => compareText$16(left.url, right.url));
|
|
11460
11460
|
}
|
|
11461
11461
|
function sortRequests(requests) {
|
|
11462
11462
|
return [...requests].sort((left, right) => {
|
|
11463
|
-
const byUrl = compareText$
|
|
11463
|
+
const byUrl = compareText$16(left.requestedUrl, right.requestedUrl);
|
|
11464
11464
|
if (byUrl) return byUrl;
|
|
11465
|
-
const byOutcome = compareText$
|
|
11465
|
+
const byOutcome = compareText$16(left.outcome, right.outcome);
|
|
11466
11466
|
if (byOutcome) return byOutcome;
|
|
11467
|
-
return compareText$
|
|
11467
|
+
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}`);
|
|
11468
11468
|
});
|
|
11469
11469
|
}
|
|
11470
11470
|
function summarizeCrawlReport(input) {
|
|
@@ -11686,8 +11686,8 @@ function createCrawlReport(input) {
|
|
|
11686
11686
|
...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
|
|
11687
11687
|
...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
|
|
11688
11688
|
...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
|
|
11689
|
-
warnings: sanitizeMessages(input.warnings).sort(compareText$
|
|
11690
|
-
caveats: sanitizeMessages(input.caveats).sort(compareText$
|
|
11689
|
+
warnings: sanitizeMessages(input.warnings).sort(compareText$16),
|
|
11690
|
+
caveats: sanitizeMessages(input.caveats).sort(compareText$16)
|
|
11691
11691
|
};
|
|
11692
11692
|
}
|
|
11693
11693
|
function normalizeLoadedCrawlReport(report) {
|
|
@@ -11717,8 +11717,8 @@ function normalizeLoadedCrawlReport(report) {
|
|
|
11717
11717
|
pages,
|
|
11718
11718
|
issues,
|
|
11719
11719
|
issueGroups: groupCrawlIssues(issues),
|
|
11720
|
-
warnings: sanitizeMessages(report.warnings).sort(compareText$
|
|
11721
|
-
caveats: sanitizeMessages(report.caveats).sort(compareText$
|
|
11720
|
+
warnings: sanitizeMessages(report.warnings).sort(compareText$16),
|
|
11721
|
+
caveats: sanitizeMessages(report.caveats).sort(compareText$16)
|
|
11722
11722
|
};
|
|
11723
11723
|
}
|
|
11724
11724
|
function toMeta(row) {
|
|
@@ -13573,7 +13573,7 @@ async function fetchSitemapUrls(input) {
|
|
|
13573
13573
|
}
|
|
13574
13574
|
//#endregion
|
|
13575
13575
|
//#region packages/core/dist/analyze/workflows/analytics-value.js
|
|
13576
|
-
const defaultDependencies$
|
|
13576
|
+
const defaultDependencies$14 = { runGa4Report };
|
|
13577
13577
|
function normalizePath(value) {
|
|
13578
13578
|
if (!value || value === "(not set)") return "";
|
|
13579
13579
|
const [path = ""] = value.split("?");
|
|
@@ -13586,12 +13586,12 @@ function urlPath(url) {
|
|
|
13586
13586
|
return "";
|
|
13587
13587
|
}
|
|
13588
13588
|
}
|
|
13589
|
-
function compareText$
|
|
13589
|
+
function compareText$15(left, right) {
|
|
13590
13590
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
13591
13591
|
}
|
|
13592
13592
|
function landingPageValuesFromRows(rows) {
|
|
13593
13593
|
const orderedRows = [...rows].sort((left, right) => {
|
|
13594
|
-
return compareText$
|
|
13594
|
+
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 ?? "");
|
|
13595
13595
|
});
|
|
13596
13596
|
const values = /* @__PURE__ */ new Map();
|
|
13597
13597
|
for (const row of orderedRows) {
|
|
@@ -13633,7 +13633,7 @@ function landingPageRankingPolicy(input) {
|
|
|
13633
13633
|
warnings
|
|
13634
13634
|
};
|
|
13635
13635
|
}
|
|
13636
|
-
async function fetchLandingPageValues(input, dependencies = defaultDependencies$
|
|
13636
|
+
async function fetchLandingPageValues(input, dependencies = defaultDependencies$14) {
|
|
13637
13637
|
const retainedRowLimit = input.limit ?? 5e3;
|
|
13638
13638
|
if (!input.propertyId) return {
|
|
13639
13639
|
values: /* @__PURE__ */ new Map(),
|
|
@@ -14865,7 +14865,7 @@ function countLabel(count, singular, pluralLabel = `${singular}s`) {
|
|
|
14865
14865
|
return `${count.toLocaleString("en-GB")} ${plural$4(count, singular, pluralLabel)}`;
|
|
14866
14866
|
}
|
|
14867
14867
|
const OKF_MAX_CONCEPTS = 5e3;
|
|
14868
|
-
function compareText$
|
|
14868
|
+
function compareText$14(left, right) {
|
|
14869
14869
|
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
|
|
14870
14870
|
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
|
|
14871
14871
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -14955,7 +14955,7 @@ function okfTitle(value, report) {
|
|
|
14955
14955
|
}
|
|
14956
14956
|
function selectedOkfPages(report, requestedLimit) {
|
|
14957
14957
|
const limit = okfConceptLimit(requestedLimit);
|
|
14958
|
-
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$
|
|
14958
|
+
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));
|
|
14959
14959
|
const unique = /* @__PURE__ */ new Map();
|
|
14960
14960
|
for (const page of eligible) if (!unique.has(page.finalUrl)) unique.set(page.finalUrl, page);
|
|
14961
14961
|
const uniquePages = [...unique.values()];
|
|
@@ -16433,7 +16433,7 @@ const crawlReportSchema = z.discriminatedUnion("requestEvidenceStatus", [
|
|
|
16433
16433
|
]);
|
|
16434
16434
|
z.toJSONSchema(crawlReportSchema), z.toJSONSchema(crawlIssueGroupSchema), z.toJSONSchema(crawlTopFixSchema), z.toJSONSchema(crawlerRuleInfoSchema), z.toJSONSchema(crawlRequestObservationSchema), z.toJSONSchema(crawlPageSnapshotSchema);
|
|
16435
16435
|
const CTR_MAX_MIN_IMPRESSIONS = 1e9;
|
|
16436
|
-
function compareText$
|
|
16436
|
+
function compareText$13(left, right) {
|
|
16437
16437
|
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
|
|
16438
16438
|
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
|
|
16439
16439
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -16442,7 +16442,7 @@ function compareText$12(left, right) {
|
|
|
16442
16442
|
}
|
|
16443
16443
|
return leftPoints.length - rightPoints.length;
|
|
16444
16444
|
}
|
|
16445
|
-
function boundedInteger$
|
|
16445
|
+
function boundedInteger$7(value, fallback, minimum, maximum) {
|
|
16446
16446
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
16447
16447
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
16448
16448
|
}
|
|
@@ -16481,7 +16481,7 @@ function aggregateCtrRows(rows, selection) {
|
|
|
16481
16481
|
weightedPosition: 0,
|
|
16482
16482
|
sourceRows: 0
|
|
16483
16483
|
};
|
|
16484
|
-
if (compareText$
|
|
16484
|
+
if (compareText$13(query, group.query) < 0) group.query = query;
|
|
16485
16485
|
group.clicks += row.clicks;
|
|
16486
16486
|
group.impressions += row.impressions;
|
|
16487
16487
|
group.weightedPosition += row.position * row.impressions;
|
|
@@ -16519,8 +16519,8 @@ function initialSelection(sourceRows) {
|
|
|
16519
16519
|
};
|
|
16520
16520
|
}
|
|
16521
16521
|
function analyzeCtrUnderperformersFromRows(input) {
|
|
16522
|
-
const minImpressions = boundedInteger$
|
|
16523
|
-
const limit = boundedInteger$
|
|
16522
|
+
const minImpressions = boundedInteger$7(input.minImpressions, 200, 1, CTR_MAX_MIN_IMPRESSIONS);
|
|
16523
|
+
const limit = boundedInteger$7(input.limit, 25, 1, 100);
|
|
16524
16524
|
const selection = initialSelection(input.rows.length);
|
|
16525
16525
|
const benchmarkRows = aggregateCtrRows(input.rows, selection).filter((row) => {
|
|
16526
16526
|
const query = row.keys[0] ?? "";
|
|
@@ -16585,7 +16585,7 @@ function analyzeCtrUnderperformersFromRows(input) {
|
|
|
16585
16585
|
confidence: "low"
|
|
16586
16586
|
}
|
|
16587
16587
|
};
|
|
16588
|
-
}).filter((item) => item.actualCtr < item.expectedCtr * .6).sort((left, right) => right.clickShortfall - left.clickShortfall || compareText$
|
|
16588
|
+
}).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));
|
|
16589
16589
|
const items = eligibleItems.slice(0, limit);
|
|
16590
16590
|
selection.eligibleUnderperformers = eligibleItems.length;
|
|
16591
16591
|
selection.returnedUnderperformers = items.length;
|
|
@@ -16601,15 +16601,15 @@ function analyzeCtrUnderperformersFromRows(input) {
|
|
|
16601
16601
|
}
|
|
16602
16602
|
//#endregion
|
|
16603
16603
|
//#region packages/core/dist/analyze/ctr-underperformers.js
|
|
16604
|
-
const MAX_GSC_ROWS$
|
|
16604
|
+
const MAX_GSC_ROWS$5 = 1e5;
|
|
16605
16605
|
function plural$3(count, singular, pluralLabel = `${singular}s`) {
|
|
16606
16606
|
return count === 1 ? singular : pluralLabel;
|
|
16607
16607
|
}
|
|
16608
|
-
const defaultDependencies$
|
|
16608
|
+
const defaultDependencies$13 = {
|
|
16609
16609
|
searchAnalytics: querySearchAnalytics,
|
|
16610
16610
|
now: () => /* @__PURE__ */ new Date()
|
|
16611
16611
|
};
|
|
16612
|
-
async function ctrUnderperformersReport(input, dependencies = defaultDependencies$
|
|
16612
|
+
async function ctrUnderperformersReport(input, dependencies = defaultDependencies$13) {
|
|
16613
16613
|
const minImpressions = integerOption$2({
|
|
16614
16614
|
value: input.minImpressions,
|
|
16615
16615
|
fallback: 200,
|
|
@@ -16631,7 +16631,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
|
|
|
16631
16631
|
dimensions: ["query", "page"],
|
|
16632
16632
|
type: "web",
|
|
16633
16633
|
dataState: "final",
|
|
16634
|
-
maxRows: MAX_GSC_ROWS$
|
|
16634
|
+
maxRows: MAX_GSC_ROWS$5
|
|
16635
16635
|
}, { refresh: input.refresh });
|
|
16636
16636
|
const { items, totalClickShortfall, returnedClickShortfall, selection } = analyzeCtrUnderperformersFromRows({
|
|
16637
16637
|
rows: source.rows,
|
|
@@ -16642,7 +16642,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
|
|
|
16642
16642
|
includeBrand: input.includeBrand
|
|
16643
16643
|
});
|
|
16644
16644
|
const top = items[0];
|
|
16645
|
-
const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$
|
|
16645
|
+
const possiblyTruncated = source.rowsFetched >= MAX_GSC_ROWS$5;
|
|
16646
16646
|
const partialValidation = selection.invalidRows > 0;
|
|
16647
16647
|
const dataStatus = selection.sourceRows === 0 ? "empty" : possiblyTruncated || partialValidation ? "partial" : selection.eligibleUnderperformers === 0 ? "filtered" : "complete";
|
|
16648
16648
|
const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
|
|
@@ -16659,7 +16659,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
|
|
|
16659
16659
|
dataState: "final",
|
|
16660
16660
|
rowsFetched: source.rowsFetched,
|
|
16661
16661
|
calls: source.calls,
|
|
16662
|
-
maxRows: MAX_GSC_ROWS$
|
|
16662
|
+
maxRows: MAX_GSC_ROWS$5,
|
|
16663
16663
|
possiblyTruncated,
|
|
16664
16664
|
completeness,
|
|
16665
16665
|
validation: {
|
|
@@ -16689,7 +16689,7 @@ async function ctrUnderperformersReport(input, dependencies = defaultDependencie
|
|
|
16689
16689
|
"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.",
|
|
16690
16690
|
...selection.invalidRows ? [`${selection.invalidRows} invalid provider ${plural$3(selection.invalidRows, "row")} ${selection.invalidRows === 1 ? "was" : "were"} excluded, so this report is partial.`] : [],
|
|
16691
16691
|
...selection.duplicateRows ? [`${selection.duplicateRows} repeated query/page ${plural$3(selection.duplicateRows, "row")} ${selection.duplicateRows === 1 ? "was" : "were"} aggregated before CTR and average position were calculated.`] : [],
|
|
16692
|
-
...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$
|
|
16692
|
+
...possiblyTruncated ? [`The retained-row limit of ${MAX_GSC_ROWS$5} was reached, so lower-ranked source rows may be absent.`] : [],
|
|
16693
16693
|
...selection.limitedUnderperformers ? [`${selection.limitedUnderperformers} eligible ${plural$3(selection.limitedUnderperformers, "underperformer")} ${selection.limitedUnderperformers === 1 ? "was" : "were"} omitted by the output limit.`] : []
|
|
16694
16694
|
],
|
|
16695
16695
|
recommendations: top ? [
|
|
@@ -16816,7 +16816,7 @@ function diagnosisDataStatus(input) {
|
|
|
16816
16816
|
function fixed$1(value) {
|
|
16817
16817
|
return Number(value.toFixed(3));
|
|
16818
16818
|
}
|
|
16819
|
-
function compareText$
|
|
16819
|
+
function compareText$12(left, right) {
|
|
16820
16820
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
16821
16821
|
}
|
|
16822
16822
|
function validMetric$2(value) {
|
|
@@ -16896,8 +16896,8 @@ function compareRetainedSegmentRows(input) {
|
|
|
16896
16896
|
else unmatched.push(unmatchedRow(row, "before"));
|
|
16897
16897
|
}
|
|
16898
16898
|
for (const [key, row] of after.rows) if (!before.rows.has(key)) unmatched.push(unmatchedRow(row, "after"));
|
|
16899
|
-
matched.sort((left, right) => Math.abs(right.clickDelta) - Math.abs(left.clickDelta) || compareText$
|
|
16900
|
-
unmatched.sort((left, right) => right.clicks - left.clicks || compareText$
|
|
16899
|
+
matched.sort((left, right) => Math.abs(right.clickDelta) - Math.abs(left.clickDelta) || compareText$12(left.key, right.key));
|
|
16900
|
+
unmatched.sort((left, right) => right.clicks - left.clicks || compareText$12(left.retainedIn, right.retainedIn) || compareText$12(left.key, right.key));
|
|
16901
16901
|
return {
|
|
16902
16902
|
before,
|
|
16903
16903
|
after,
|
|
@@ -17202,11 +17202,11 @@ function unavailableSegmentImpactReport(input) {
|
|
|
17202
17202
|
//#endregion
|
|
17203
17203
|
//#region packages/core/dist/analyze/segment-impact.js
|
|
17204
17204
|
const DEFAULT_MAX_ROWS = 1e5;
|
|
17205
|
-
const defaultDependencies$
|
|
17205
|
+
const defaultDependencies$12 = {
|
|
17206
17206
|
searchAnalytics: querySearchAnalytics,
|
|
17207
17207
|
now: () => /* @__PURE__ */ new Date()
|
|
17208
17208
|
};
|
|
17209
|
-
async function segmentImpact(input, dependencies = defaultDependencies$
|
|
17209
|
+
async function segmentImpact(input, dependencies = defaultDependencies$12) {
|
|
17210
17210
|
validateSegmentSite(input.site);
|
|
17211
17211
|
const now = dependencies.now();
|
|
17212
17212
|
const ranges = resolveSegmentRanges({
|
|
@@ -17607,13 +17607,13 @@ const MINIMUM_PAGE_SHARE = .1;
|
|
|
17607
17607
|
const MINIMUM_PAGE_IMPRESSIONS = 10;
|
|
17608
17608
|
const MAXIMUM_DOMINANT_SHARE = .8;
|
|
17609
17609
|
const MAX_SUPPRESSIONS = 100;
|
|
17610
|
-
function boundedInteger$
|
|
17610
|
+
function boundedInteger$6(value, fallback, minimum, maximum) {
|
|
17611
17611
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
17612
17612
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
17613
17613
|
}
|
|
17614
17614
|
function analyzeCannibalRows(input) {
|
|
17615
|
-
const minImpressions = boundedInteger$
|
|
17616
|
-
const limit = boundedInteger$
|
|
17615
|
+
const minImpressions = boundedInteger$6(input.minImpressions, 50, 0, 1e9);
|
|
17616
|
+
const limit = boundedInteger$6(input.limit, 25, 1, 100);
|
|
17617
17617
|
const selection = {
|
|
17618
17618
|
sourceRows: input.rows.length,
|
|
17619
17619
|
invalidRows: 0,
|
|
@@ -17759,8 +17759,8 @@ function analyzeCannibalRows(input) {
|
|
|
17759
17759
|
}
|
|
17760
17760
|
//#endregion
|
|
17761
17761
|
//#region packages/core/dist/analyze/site-diagnostics/cannibal.js
|
|
17762
|
-
const MAX_GSC_ROWS$
|
|
17763
|
-
const defaultDependencies$
|
|
17762
|
+
const MAX_GSC_ROWS$4 = 1e5;
|
|
17763
|
+
const defaultDependencies$11 = {
|
|
17764
17764
|
searchAnalytics: querySearchAnalytics,
|
|
17765
17765
|
now: () => /* @__PURE__ */ new Date()
|
|
17766
17766
|
};
|
|
@@ -17773,7 +17773,7 @@ function verdict$4(input) {
|
|
|
17773
17773
|
const result = `${input.returned} of ${input.eligible} multi-URL query candidate${input.eligible === 1 ? "" : "s"} returned for intent and technical review.`;
|
|
17774
17774
|
return input.status === "partial" ? `${result} Provider evidence is partial.` : result;
|
|
17775
17775
|
}
|
|
17776
|
-
async function cannibalReport(input, dependencies = defaultDependencies$
|
|
17776
|
+
async function cannibalReport(input, dependencies = defaultDependencies$11) {
|
|
17777
17777
|
const days = integerOption$2({
|
|
17778
17778
|
value: input.days,
|
|
17779
17779
|
fallback: 28,
|
|
@@ -17803,7 +17803,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
17803
17803
|
...range,
|
|
17804
17804
|
type: "web",
|
|
17805
17805
|
dataState: "final",
|
|
17806
|
-
maxRows: MAX_GSC_ROWS$
|
|
17806
|
+
maxRows: MAX_GSC_ROWS$4
|
|
17807
17807
|
};
|
|
17808
17808
|
const [pageExposure, propertyDemand] = await Promise.all([dependencies.searchAnalytics(input.site, {
|
|
17809
17809
|
...requestBase,
|
|
@@ -17826,8 +17826,8 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
17826
17826
|
brandTerms: input.brandTerms,
|
|
17827
17827
|
includeBrand: input.includeBrand
|
|
17828
17828
|
});
|
|
17829
|
-
const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$
|
|
17830
|
-
const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$
|
|
17829
|
+
const pageTruncated = pageExposure.rowsFetched >= MAX_GSC_ROWS$4;
|
|
17830
|
+
const propertyTruncated = propertyDemand.rowsFetched >= MAX_GSC_ROWS$4;
|
|
17831
17831
|
const possiblyTruncated = pageTruncated || propertyTruncated;
|
|
17832
17832
|
const partialValidation = analysis.selection.invalidRows + analysis.selection.propertyInvalidRows > 0;
|
|
17833
17833
|
const completeness = possiblyTruncated && partialValidation ? "partial-and-possibly-truncated" : possiblyTruncated ? "possibly-truncated" : partialValidation ? "partial" : "complete";
|
|
@@ -17852,7 +17852,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
17852
17852
|
invalidRows: analysis.selection.invalidRows
|
|
17853
17853
|
},
|
|
17854
17854
|
calls: pageExposure.calls,
|
|
17855
|
-
maxRows: MAX_GSC_ROWS$
|
|
17855
|
+
maxRows: MAX_GSC_ROWS$4,
|
|
17856
17856
|
possiblyTruncated: pageTruncated
|
|
17857
17857
|
},
|
|
17858
17858
|
propertyDemand: {
|
|
@@ -17864,7 +17864,7 @@ async function cannibalReport(input, dependencies = defaultDependencies$10) {
|
|
|
17864
17864
|
invalidRows: analysis.selection.propertyInvalidRows
|
|
17865
17865
|
},
|
|
17866
17866
|
calls: propertyDemand.calls,
|
|
17867
|
-
maxRows: MAX_GSC_ROWS$
|
|
17867
|
+
maxRows: MAX_GSC_ROWS$4,
|
|
17868
17868
|
possiblyTruncated: propertyTruncated
|
|
17869
17869
|
},
|
|
17870
17870
|
completeness
|
|
@@ -17937,7 +17937,7 @@ function normalizeUrl(value) {
|
|
|
17937
17937
|
return;
|
|
17938
17938
|
}
|
|
17939
17939
|
}
|
|
17940
|
-
function validRow$
|
|
17940
|
+
function validRow$3(row, query, url) {
|
|
17941
17941
|
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;
|
|
17942
17942
|
}
|
|
17943
17943
|
function aggregateDecayRows(rows) {
|
|
@@ -17946,7 +17946,7 @@ function aggregateDecayRows(rows) {
|
|
|
17946
17946
|
for (const row of rows) {
|
|
17947
17947
|
const query = row.keys[0]?.trim() ?? "";
|
|
17948
17948
|
const url = normalizeUrl(row.keys[1]?.trim() ?? "");
|
|
17949
|
-
if (!validRow$
|
|
17949
|
+
if (!validRow$3(row, query, url)) {
|
|
17950
17950
|
invalid++;
|
|
17951
17951
|
continue;
|
|
17952
17952
|
}
|
|
@@ -18172,8 +18172,8 @@ function analyzeDecay(input) {
|
|
|
18172
18172
|
}
|
|
18173
18173
|
//#endregion
|
|
18174
18174
|
//#region packages/core/dist/analyze/site-diagnostics/decaying.js
|
|
18175
|
-
const MAX_GSC_ROWS$
|
|
18176
|
-
const defaultDependencies$
|
|
18175
|
+
const MAX_GSC_ROWS$3 = 1e5;
|
|
18176
|
+
const defaultDependencies$10 = {
|
|
18177
18177
|
searchAnalytics: querySearchAnalytics,
|
|
18178
18178
|
now: () => /* @__PURE__ */ new Date()
|
|
18179
18179
|
};
|
|
@@ -18184,7 +18184,7 @@ function verdict$3(report) {
|
|
|
18184
18184
|
if (!report.selection.eligibleRows) return "No observed retained query/page declines matched the report filters.";
|
|
18185
18185
|
return `${report.selection.returnedRows} of ${report.selection.eligibleRows} observed retained query/page declines returned for review.`;
|
|
18186
18186
|
}
|
|
18187
|
-
async function decayingReport(input, dependencies = defaultDependencies$
|
|
18187
|
+
async function decayingReport(input, dependencies = defaultDependencies$10) {
|
|
18188
18188
|
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.");
|
|
18189
18189
|
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.");
|
|
18190
18190
|
const days = integerOption$2({
|
|
@@ -18237,7 +18237,7 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
|
|
|
18237
18237
|
aggregationType: "auto",
|
|
18238
18238
|
type: "web",
|
|
18239
18239
|
dataState: "final",
|
|
18240
|
-
maxRows: MAX_GSC_ROWS$
|
|
18240
|
+
maxRows: MAX_GSC_ROWS$3
|
|
18241
18241
|
};
|
|
18242
18242
|
const [current, previous] = await Promise.all([dependencies.searchAnalytics(input.site, {
|
|
18243
18243
|
...request,
|
|
@@ -18260,8 +18260,8 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
|
|
|
18260
18260
|
brandTerms: input.brandTerms,
|
|
18261
18261
|
includeBrand: input.includeBrand
|
|
18262
18262
|
});
|
|
18263
|
-
const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$
|
|
18264
|
-
const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$
|
|
18263
|
+
const currentTruncated = current.rowsFetched >= MAX_GSC_ROWS$3;
|
|
18264
|
+
const previousTruncated = previous.rowsFetched >= MAX_GSC_ROWS$3;
|
|
18265
18265
|
const possiblyTruncated = currentTruncated || previousTruncated;
|
|
18266
18266
|
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";
|
|
18267
18267
|
const reportBase = {
|
|
@@ -18288,13 +18288,13 @@ async function decayingReport(input, dependencies = defaultDependencies$9) {
|
|
|
18288
18288
|
current: {
|
|
18289
18289
|
rowsFetched: current.rowsFetched,
|
|
18290
18290
|
calls: current.calls,
|
|
18291
|
-
maxRows: MAX_GSC_ROWS$
|
|
18291
|
+
maxRows: MAX_GSC_ROWS$3,
|
|
18292
18292
|
possiblyTruncated: currentTruncated
|
|
18293
18293
|
},
|
|
18294
18294
|
previous: {
|
|
18295
18295
|
rowsFetched: previous.rowsFetched,
|
|
18296
18296
|
calls: previous.calls,
|
|
18297
|
-
maxRows: MAX_GSC_ROWS$
|
|
18297
|
+
maxRows: MAX_GSC_ROWS$3,
|
|
18298
18298
|
possiblyTruncated: previousTruncated
|
|
18299
18299
|
},
|
|
18300
18300
|
completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-rows-only"
|
|
@@ -18353,7 +18353,7 @@ function normalizedQuery(value) {
|
|
|
18353
18353
|
function groupKey$1(item) {
|
|
18354
18354
|
return `${item.template.id}:${normalizedQuery(item.query)}`;
|
|
18355
18355
|
}
|
|
18356
|
-
function compareText$
|
|
18356
|
+
function compareText$11(left, right) {
|
|
18357
18357
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
18358
18358
|
}
|
|
18359
18359
|
function groupAction(input) {
|
|
@@ -18389,11 +18389,11 @@ function groupQuickWins(items) {
|
|
|
18389
18389
|
technicalUrls
|
|
18390
18390
|
});
|
|
18391
18391
|
return group;
|
|
18392
|
-
}).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$
|
|
18392
|
+
}).filter((group) => group !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$11(left.id, right.id));
|
|
18393
18393
|
}
|
|
18394
18394
|
//#endregion
|
|
18395
18395
|
//#region packages/core/dist/analyze/site-diagnostics/quick-wins-analysis-primitives.js
|
|
18396
|
-
function boundedInteger$
|
|
18396
|
+
function boundedInteger$5(value, fallback, minimum, maximum) {
|
|
18397
18397
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
18398
18398
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
18399
18399
|
}
|
|
@@ -18405,7 +18405,7 @@ function validHttpUrl(value) {
|
|
|
18405
18405
|
return false;
|
|
18406
18406
|
}
|
|
18407
18407
|
}
|
|
18408
|
-
function validRow$
|
|
18408
|
+
function validRow$2(row, query, url) {
|
|
18409
18409
|
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;
|
|
18410
18410
|
}
|
|
18411
18411
|
function selectBenchmarkRows(input) {
|
|
@@ -18427,7 +18427,7 @@ function selectBenchmarkRows(input) {
|
|
|
18427
18427
|
for (const row of input.rows) {
|
|
18428
18428
|
const query = row.keys[0]?.trim() ?? "";
|
|
18429
18429
|
const url = row.keys[1]?.trim() ?? "";
|
|
18430
|
-
if (!validRow$
|
|
18430
|
+
if (!validRow$2(row, query, url)) selection.invalidRows++;
|
|
18431
18431
|
else if (row.position > 10) selection.outsideBenchmarkPositionRows++;
|
|
18432
18432
|
else if (isLowActionabilityQuery(query)) selection.lowActionabilityRows++;
|
|
18433
18433
|
else if (shouldExcludeBrandQuery({
|
|
@@ -18501,11 +18501,11 @@ function quickWinItem(input) {
|
|
|
18501
18501
|
}
|
|
18502
18502
|
};
|
|
18503
18503
|
}
|
|
18504
|
-
function compareText$
|
|
18504
|
+
function compareText$10(left, right) {
|
|
18505
18505
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
18506
18506
|
}
|
|
18507
18507
|
function compareQuickWinItems(left, right) {
|
|
18508
|
-
return right.estimatedCtrClickShortfall - left.estimatedCtrClickShortfall || right.impressions - left.impressions || compareText$
|
|
18508
|
+
return right.estimatedCtrClickShortfall - left.estimatedCtrClickShortfall || right.impressions - left.impressions || compareText$10(left.query, right.query) || compareText$10(left.url, right.url);
|
|
18509
18509
|
}
|
|
18510
18510
|
function roundedSum(values) {
|
|
18511
18511
|
return Number(values.reduce((sum, value) => sum + value, 0).toFixed(2));
|
|
@@ -18518,8 +18518,8 @@ const DEFAULT_LIMIT$3 = 25;
|
|
|
18518
18518
|
const MAX_LIMIT$3 = 100;
|
|
18519
18519
|
const FALLBACK_SOURCE = "builtin_position_ctr_curve_v1";
|
|
18520
18520
|
function analyzeQuickWinsFromRows(input) {
|
|
18521
|
-
const minImpressions = boundedInteger$
|
|
18522
|
-
const limit = boundedInteger$
|
|
18521
|
+
const minImpressions = boundedInteger$5(input.minImpressions, DEFAULT_MIN_IMPRESSIONS$2, 0, MAX_MIN_IMPRESSIONS$2);
|
|
18522
|
+
const limit = boundedInteger$5(input.limit, DEFAULT_LIMIT$3, 1, MAX_LIMIT$3);
|
|
18523
18523
|
const selected = selectBenchmarkRows(input);
|
|
18524
18524
|
const benchmarkContext = createCtrBenchmarkContext(selected.rows, {
|
|
18525
18525
|
samplePopulation: "all_qualified_url_samples",
|
|
@@ -18620,7 +18620,7 @@ function analyzeQuickWinsFromRows(input) {
|
|
|
18620
18620
|
}
|
|
18621
18621
|
//#endregion
|
|
18622
18622
|
//#region packages/core/dist/analyze/site-diagnostics/quick-wins-templates.js
|
|
18623
|
-
function compareText$
|
|
18623
|
+
function compareText$9(left, right) {
|
|
18624
18624
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
18625
18625
|
}
|
|
18626
18626
|
function unique$2(values) {
|
|
@@ -18646,7 +18646,7 @@ function quickWinTemplateRecommendations(items) {
|
|
|
18646
18646
|
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.`,
|
|
18647
18647
|
evidence: `${templateItems.length} eligible rows across ${urls.length} distinct URLs share this recognised template. Example queries: ${queries.join("; ")}.`
|
|
18648
18648
|
};
|
|
18649
|
-
}).filter((item) => item !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$
|
|
18649
|
+
}).filter((item) => item !== void 0).sort((left, right) => right.totalEstimatedCtrClickShortfall - left.totalEstimatedCtrClickShortfall || right.totalImpressions - left.totalImpressions || compareText$9(left.templateId, right.templateId));
|
|
18650
18650
|
}
|
|
18651
18651
|
function quickWinTemplateSummaries(items) {
|
|
18652
18652
|
const templates = /* @__PURE__ */ new Map();
|
|
@@ -18665,17 +18665,17 @@ function quickWinTemplateSummaries(items) {
|
|
|
18665
18665
|
label: item.label,
|
|
18666
18666
|
rowCount: item.rows,
|
|
18667
18667
|
urlCount: item.urls.size,
|
|
18668
|
-
sampleUrls: [...item.urls].sort(compareText$
|
|
18669
|
-
})).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$
|
|
18668
|
+
sampleUrls: [...item.urls].sort(compareText$9).slice(0, 3)
|
|
18669
|
+
})).sort((left, right) => right.urlCount - left.urlCount || right.rowCount - left.rowCount || compareText$9(left.id, right.id)).slice(0, 5);
|
|
18670
18670
|
}
|
|
18671
18671
|
//#endregion
|
|
18672
18672
|
//#region packages/core/dist/analyze/site-diagnostics/quick-wins.js
|
|
18673
|
-
const DEFAULT_DAYS$
|
|
18674
|
-
const MAX_DAYS$
|
|
18673
|
+
const DEFAULT_DAYS$3 = 28;
|
|
18674
|
+
const MAX_DAYS$4 = 548;
|
|
18675
18675
|
const DEFAULT_VERIFY_LIMIT$1 = 5;
|
|
18676
18676
|
const MAX_VERIFY_LIMIT = 100;
|
|
18677
18677
|
const MAX_SOURCE_ROWS$3 = 1e5;
|
|
18678
|
-
const defaultDependencies$
|
|
18678
|
+
const defaultDependencies$9 = {
|
|
18679
18679
|
searchAnalytics: querySearchAnalytics,
|
|
18680
18680
|
fetch: fetchPage,
|
|
18681
18681
|
extract: extractPage,
|
|
@@ -18694,13 +18694,13 @@ function applyVerification$1(item, coverage) {
|
|
|
18694
18694
|
evidenceRef: `${item.recommendation.evidenceRef} ${coverage.summary}`
|
|
18695
18695
|
};
|
|
18696
18696
|
}
|
|
18697
|
-
async function quickWinsReport(input, dependencies = defaultDependencies$
|
|
18698
|
-
const explicitRange = explicitDateRange(input, MAX_DAYS$
|
|
18697
|
+
async function quickWinsReport(input, dependencies = defaultDependencies$9) {
|
|
18698
|
+
const explicitRange = explicitDateRange(input, MAX_DAYS$4);
|
|
18699
18699
|
const days = explicitRange?.days ?? integerOption$2({
|
|
18700
18700
|
value: input.days,
|
|
18701
|
-
fallback: DEFAULT_DAYS$
|
|
18701
|
+
fallback: DEFAULT_DAYS$3,
|
|
18702
18702
|
minimum: 1,
|
|
18703
|
-
maximum: MAX_DAYS$
|
|
18703
|
+
maximum: MAX_DAYS$4,
|
|
18704
18704
|
label: "Days"
|
|
18705
18705
|
});
|
|
18706
18706
|
const verifyLimit = integerOption$2({
|
|
@@ -18850,7 +18850,7 @@ async function quickWinsReport(input, dependencies = defaultDependencies$8) {
|
|
|
18850
18850
|
}
|
|
18851
18851
|
//#endregion
|
|
18852
18852
|
//#region packages/core/dist/analyze/striking-distance-analysis-primitives.js
|
|
18853
|
-
function compareText$
|
|
18853
|
+
function compareText$8(left, right) {
|
|
18854
18854
|
const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
|
|
18855
18855
|
const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
|
|
18856
18856
|
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
@@ -18916,7 +18916,7 @@ function strikingDistanceItem(row) {
|
|
|
18916
18916
|
};
|
|
18917
18917
|
}
|
|
18918
18918
|
function compareStrikingDistanceItems(left, right) {
|
|
18919
|
-
return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$
|
|
18919
|
+
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);
|
|
18920
18920
|
}
|
|
18921
18921
|
function groupStrikingDistanceItems(items) {
|
|
18922
18922
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -18953,7 +18953,7 @@ function groupStrikingDistanceItems(items) {
|
|
|
18953
18953
|
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.`
|
|
18954
18954
|
}
|
|
18955
18955
|
};
|
|
18956
|
-
}).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$
|
|
18956
|
+
}).filter((group) => group !== void 0).sort((left, right) => right.totalImpressions - left.totalImpressions || right.uniqueUrls - left.uniqueUrls || compareText$8(left.id, right.id));
|
|
18957
18957
|
}
|
|
18958
18958
|
//#endregion
|
|
18959
18959
|
//#region packages/core/dist/analyze/striking-distance-analysis.js
|
|
@@ -19067,22 +19067,22 @@ function analyzeStrikingDistanceRows(input) {
|
|
|
19067
19067
|
}
|
|
19068
19068
|
//#endregion
|
|
19069
19069
|
//#region packages/core/dist/analyze/striking-distance.js
|
|
19070
|
-
const DEFAULT_DAYS$
|
|
19071
|
-
const MAX_DAYS$
|
|
19070
|
+
const DEFAULT_DAYS$2 = 28;
|
|
19071
|
+
const MAX_DAYS$3 = 548;
|
|
19072
19072
|
const DEFAULT_VERIFY_LIMIT = 5;
|
|
19073
19073
|
const MAX_SOURCE_ROWS$2 = 1e5;
|
|
19074
|
-
const defaultDependencies$
|
|
19074
|
+
const defaultDependencies$8 = {
|
|
19075
19075
|
searchAnalytics: querySearchAnalytics,
|
|
19076
19076
|
verifyContent: verifyQueryContent,
|
|
19077
19077
|
now: () => /* @__PURE__ */ new Date()
|
|
19078
19078
|
};
|
|
19079
|
-
function boundedInteger$
|
|
19079
|
+
function boundedInteger$4(value, fallback, minimum, maximum) {
|
|
19080
19080
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
19081
19081
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
19082
19082
|
}
|
|
19083
19083
|
function reportDays(value) {
|
|
19084
|
-
if (value === void 0) return DEFAULT_DAYS$
|
|
19085
|
-
if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS$
|
|
19084
|
+
if (value === void 0) return DEFAULT_DAYS$2;
|
|
19085
|
+
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}.`);
|
|
19086
19086
|
return value;
|
|
19087
19087
|
}
|
|
19088
19088
|
function dateRange(days, now) {
|
|
@@ -19121,7 +19121,7 @@ function verdict$2(analysis) {
|
|
|
19121
19121
|
if (analysis.dataStatus === "filtered") return "No retained query/page rows met the report filters.";
|
|
19122
19122
|
return `${countLabel(analysis.summary.eligibleRows, "eligible query/page row")} found; ${countLabel(analysis.summary.returnedRows, "row")} returned for review.`;
|
|
19123
19123
|
}
|
|
19124
|
-
async function strikingDistance(input, dependencies = defaultDependencies$
|
|
19124
|
+
async function strikingDistance(input, dependencies = defaultDependencies$8) {
|
|
19125
19125
|
const days = reportDays(input.days);
|
|
19126
19126
|
const generatedAt = dependencies.now().toISOString();
|
|
19127
19127
|
const range = dateRange(days, new Date(generatedAt));
|
|
@@ -19140,7 +19140,7 @@ async function strikingDistance(input, dependencies = defaultDependencies$7) {
|
|
|
19140
19140
|
brandTerms: input.brandTerms,
|
|
19141
19141
|
includeBrand: input.includeBrand
|
|
19142
19142
|
});
|
|
19143
|
-
const verifyLimit = boundedInteger$
|
|
19143
|
+
const verifyLimit = boundedInteger$4(input.verifyLimit, DEFAULT_VERIFY_LIMIT, 0, analysis.limit);
|
|
19144
19144
|
const verificationRequested = input.verifyContent === true || input.verifyContent !== false && input.verifyLimit !== void 0;
|
|
19145
19145
|
const attempted = verificationRequested ? Math.min(verifyLimit, analysis.items.length) : 0;
|
|
19146
19146
|
const items = await Promise.all(analysis.items.map(async (item, index) => {
|
|
@@ -19968,7 +19968,7 @@ function emptyQuickWins(input) {
|
|
|
19968
19968
|
warnings: []
|
|
19969
19969
|
};
|
|
19970
19970
|
}
|
|
19971
|
-
const defaultDependencies$
|
|
19971
|
+
const defaultDependencies$7 = {
|
|
19972
19972
|
trafficAnomaly,
|
|
19973
19973
|
updateCorrelation,
|
|
19974
19974
|
segmentImpact,
|
|
@@ -19979,7 +19979,7 @@ const defaultDependencies$6 = {
|
|
|
19979
19979
|
};
|
|
19980
19980
|
async function diagnoseProperty(input, dependencies = {}) {
|
|
19981
19981
|
const providers = {
|
|
19982
|
-
...defaultDependencies$
|
|
19982
|
+
...defaultDependencies$7,
|
|
19983
19983
|
...dependencies
|
|
19984
19984
|
};
|
|
19985
19985
|
const limit = input.limit ?? 10;
|
|
@@ -20843,6 +20843,42 @@ async function providerRequestJson(input) {
|
|
|
20843
20843
|
return parsed.data;
|
|
20844
20844
|
}
|
|
20845
20845
|
//#endregion
|
|
20846
|
+
//#region packages/core/dist/providers/dataforseo/account-pricing.js
|
|
20847
|
+
function unitPrice(components) {
|
|
20848
|
+
const prices = components ?? [];
|
|
20849
|
+
const total = (type) => {
|
|
20850
|
+
const matching = prices.filter((item) => item.cost_type === type);
|
|
20851
|
+
return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
|
|
20852
|
+
};
|
|
20853
|
+
return {
|
|
20854
|
+
perRequestMicros: total("per_request"),
|
|
20855
|
+
perResultMicros: total("per_result")
|
|
20856
|
+
};
|
|
20857
|
+
}
|
|
20858
|
+
function dataForSeoAccountPrices(account) {
|
|
20859
|
+
return {
|
|
20860
|
+
keywordOverviewPrice: unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal),
|
|
20861
|
+
keywordDiscoveryPrices: {
|
|
20862
|
+
ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
|
|
20863
|
+
related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
|
|
20864
|
+
suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
|
|
20865
|
+
},
|
|
20866
|
+
domainResearchPrices: {
|
|
20867
|
+
domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
|
|
20868
|
+
rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
|
|
20869
|
+
rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
|
|
20870
|
+
serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
|
|
20871
|
+
},
|
|
20872
|
+
linkPrices: {
|
|
20873
|
+
summary: unitPrice(account.price?.backlinks?.summary?.live?.priority_normal),
|
|
20874
|
+
backlinks: unitPrice(account.price?.backlinks?.backlinks?.live?.priority_normal),
|
|
20875
|
+
referringDomains: unitPrice(account.price?.backlinks?.referring_domains?.live?.priority_normal)
|
|
20876
|
+
},
|
|
20877
|
+
serpLiveAdvancedPrice: unitPrice(account.price?.serp?.live?.advanced?.priority_normal),
|
|
20878
|
+
serpTaskPostPrice: unitPrice(account.price?.serp?.task_post?.priority_normal)
|
|
20879
|
+
};
|
|
20880
|
+
}
|
|
20881
|
+
//#endregion
|
|
20846
20882
|
//#region packages/core/dist/providers/dataforseo/account-schema.js
|
|
20847
20883
|
const userDataMoneySchema = z.object({
|
|
20848
20884
|
total: z.number().finite().nonnegative().optional(),
|
|
@@ -20860,6 +20896,11 @@ const priceComponentSchema = z.object({
|
|
|
20860
20896
|
const normalPriorityPriceSchema = z.object({ priority_normal: z.array(priceComponentSchema).max(20).optional() }).passthrough();
|
|
20861
20897
|
const livePriceSchema = z.object({ live: normalPriorityPriceSchema.optional() }).passthrough();
|
|
20862
20898
|
const userDataPriceSchema = z.object({
|
|
20899
|
+
backlinks: z.object({
|
|
20900
|
+
summary: livePriceSchema.optional(),
|
|
20901
|
+
backlinks: livePriceSchema.optional(),
|
|
20902
|
+
referring_domains: livePriceSchema.optional()
|
|
20903
|
+
}).passthrough().optional(),
|
|
20863
20904
|
dataforseo_labs: z.object({
|
|
20864
20905
|
keyword_overview: livePriceSchema.optional(),
|
|
20865
20906
|
keyword_ideas: livePriceSchema.optional(),
|
|
@@ -20917,7 +20958,7 @@ function discoveryNextCursor(response) {
|
|
|
20917
20958
|
//#region packages/core/dist/providers/dataforseo/schema.js
|
|
20918
20959
|
const MAX_PROVIDER_MONTHLY_SEARCH_ROWS = 120;
|
|
20919
20960
|
const nullableNumber$1 = z.number().finite().nullable().optional();
|
|
20920
|
-
const nullableCount$
|
|
20961
|
+
const nullableCount$2 = z.union([z.number().finite(), z.string().trim().min(1)]).nullable().optional();
|
|
20921
20962
|
const monthlySearchSchema$1 = z.object({
|
|
20922
20963
|
year: z.number().int(),
|
|
20923
20964
|
month: z.number().int(),
|
|
@@ -20933,7 +20974,7 @@ const keywordOverviewItemSchema = z.object({
|
|
|
20933
20974
|
last_updated_time: z.string().trim().min(1).nullable().optional()
|
|
20934
20975
|
}).nullish(),
|
|
20935
20976
|
keyword_properties: z.object({ keyword_difficulty: nullableNumber$1 }).nullish(),
|
|
20936
|
-
serp_info: z.object({ se_results_count: nullableCount$
|
|
20977
|
+
serp_info: z.object({ se_results_count: nullableCount$2 }).nullish(),
|
|
20937
20978
|
search_intent_info: z.object({ main_intent: z.enum([
|
|
20938
20979
|
"informational",
|
|
20939
20980
|
"navigational",
|
|
@@ -21006,37 +21047,37 @@ const dataForSeoDiscoveryResponseSchema = z.object({
|
|
|
21006
21047
|
//#region packages/core/dist/providers/dataforseo/domain-schema.js
|
|
21007
21048
|
const nullableNumber = z.number().finite().nullable().optional();
|
|
21008
21049
|
const nullableNonnegativeNumber = z.number().finite().nonnegative().nullable().optional();
|
|
21009
|
-
const nullableCount = z.number().int().nonnegative().nullable().optional();
|
|
21050
|
+
const nullableCount$1 = z.number().int().nonnegative().nullable().optional();
|
|
21010
21051
|
const rankingMetricsSchema = z.object({
|
|
21011
|
-
pos_1: nullableCount,
|
|
21012
|
-
pos_2_3: nullableCount,
|
|
21013
|
-
pos_4_10: nullableCount,
|
|
21014
|
-
pos_11_20: nullableCount,
|
|
21015
|
-
pos_21_30: nullableCount,
|
|
21016
|
-
pos_31_40: nullableCount,
|
|
21017
|
-
pos_41_50: nullableCount,
|
|
21018
|
-
pos_51_60: nullableCount,
|
|
21019
|
-
pos_61_70: nullableCount,
|
|
21020
|
-
pos_71_80: nullableCount,
|
|
21021
|
-
pos_81_90: nullableCount,
|
|
21022
|
-
pos_91_100: nullableCount,
|
|
21052
|
+
pos_1: nullableCount$1,
|
|
21053
|
+
pos_2_3: nullableCount$1,
|
|
21054
|
+
pos_4_10: nullableCount$1,
|
|
21055
|
+
pos_11_20: nullableCount$1,
|
|
21056
|
+
pos_21_30: nullableCount$1,
|
|
21057
|
+
pos_31_40: nullableCount$1,
|
|
21058
|
+
pos_41_50: nullableCount$1,
|
|
21059
|
+
pos_51_60: nullableCount$1,
|
|
21060
|
+
pos_61_70: nullableCount$1,
|
|
21061
|
+
pos_71_80: nullableCount$1,
|
|
21062
|
+
pos_81_90: nullableCount$1,
|
|
21063
|
+
pos_91_100: nullableCount$1,
|
|
21023
21064
|
etv: nullableNonnegativeNumber,
|
|
21024
|
-
count: nullableCount,
|
|
21065
|
+
count: nullableCount$1,
|
|
21025
21066
|
estimated_paid_traffic_cost: nullableNonnegativeNumber,
|
|
21026
|
-
is_new: nullableCount,
|
|
21027
|
-
is_up: nullableCount,
|
|
21028
|
-
is_down: nullableCount,
|
|
21029
|
-
is_lost: nullableCount
|
|
21067
|
+
is_new: nullableCount$1,
|
|
21068
|
+
is_up: nullableCount$1,
|
|
21069
|
+
is_down: nullableCount$1,
|
|
21070
|
+
is_lost: nullableCount$1
|
|
21030
21071
|
}).passthrough();
|
|
21031
21072
|
const metricsSchema = z.object({ organic: rankingMetricsSchema.nullable().optional() }).passthrough();
|
|
21032
|
-
const paidResponseSchema = z.object({
|
|
21073
|
+
const paidResponseSchema$1 = z.object({
|
|
21033
21074
|
status_code: z.number().int(),
|
|
21034
21075
|
status_message: z.string().max(1e3),
|
|
21035
21076
|
cost: z.number().finite().nonnegative().optional(),
|
|
21036
21077
|
tasks_count: z.number().int().nonnegative(),
|
|
21037
21078
|
tasks_error: z.number().int().nonnegative()
|
|
21038
21079
|
}).passthrough();
|
|
21039
|
-
const taskFields = {
|
|
21080
|
+
const taskFields$1 = {
|
|
21040
21081
|
id: z.string().trim().min(1).max(100).optional(),
|
|
21041
21082
|
status_code: z.number().int(),
|
|
21042
21083
|
status_message: z.string().max(1e3),
|
|
@@ -21056,8 +21097,8 @@ const overviewResultSchema = z.object({
|
|
|
21056
21097
|
items_count: z.number().int().nonnegative().optional(),
|
|
21057
21098
|
items: z.array(overviewItemSchema).max(1e3).nullable().optional()
|
|
21058
21099
|
}).passthrough();
|
|
21059
|
-
const dataForSeoDomainOverviewResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
21060
|
-
...taskFields,
|
|
21100
|
+
const dataForSeoDomainOverviewResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
|
|
21101
|
+
...taskFields$1,
|
|
21061
21102
|
result: z.array(overviewResultSchema).max(10).nullable().optional()
|
|
21062
21103
|
}).passthrough()).max(10) });
|
|
21063
21104
|
const rankedSerpItemSchema = z.object({
|
|
@@ -21077,8 +21118,8 @@ const rankedKeywordsResultSchema = z.object({
|
|
|
21077
21118
|
items_count: z.number().int().nonnegative().optional(),
|
|
21078
21119
|
items: z.array(rankedKeywordItemSchema).max(1e3).nullable().optional()
|
|
21079
21120
|
}).passthrough();
|
|
21080
|
-
const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
21081
|
-
...taskFields,
|
|
21121
|
+
const dataForSeoRankedKeywordsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
|
|
21122
|
+
...taskFields$1,
|
|
21082
21123
|
result: z.array(rankedKeywordsResultSchema).max(10).nullable().optional()
|
|
21083
21124
|
}).passthrough()).max(10) });
|
|
21084
21125
|
const rankingPageItemSchema = z.object({
|
|
@@ -21091,8 +21132,8 @@ const rankingPagesResultSchema = z.object({
|
|
|
21091
21132
|
items_count: z.number().int().nonnegative().optional(),
|
|
21092
21133
|
items: z.array(rankingPageItemSchema).max(1e3).nullable().optional()
|
|
21093
21134
|
}).passthrough();
|
|
21094
|
-
const dataForSeoRankingPagesResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
21095
|
-
...taskFields,
|
|
21135
|
+
const dataForSeoRankingPagesResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
|
|
21136
|
+
...taskFields$1,
|
|
21096
21137
|
result: z.array(rankingPagesResultSchema).max(10).nullable().optional()
|
|
21097
21138
|
}).passthrough()).max(10) });
|
|
21098
21139
|
const competitorItemSchema = z.object({
|
|
@@ -21100,9 +21141,9 @@ const competitorItemSchema = z.object({
|
|
|
21100
21141
|
avg_position: nullableNumber,
|
|
21101
21142
|
median_position: nullableNumber,
|
|
21102
21143
|
etv: nullableNonnegativeNumber,
|
|
21103
|
-
keywords_count: nullableCount,
|
|
21144
|
+
keywords_count: nullableCount$1,
|
|
21104
21145
|
visibility: nullableNonnegativeNumber,
|
|
21105
|
-
relevant_serp_items: nullableCount,
|
|
21146
|
+
relevant_serp_items: nullableCount$1,
|
|
21106
21147
|
keywords_positions: z.record(z.string().trim().min(1).max(80), z.array(z.number().int().positive()).max(100)).nullable().optional()
|
|
21107
21148
|
}).passthrough();
|
|
21108
21149
|
const competitorsResultSchema = z.object({
|
|
@@ -21111,8 +21152,8 @@ const competitorsResultSchema = z.object({
|
|
|
21111
21152
|
items_count: z.number().int().nonnegative().optional(),
|
|
21112
21153
|
items: z.array(competitorItemSchema).max(1e3).nullable().optional()
|
|
21113
21154
|
}).passthrough();
|
|
21114
|
-
const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
21115
|
-
...taskFields,
|
|
21155
|
+
const dataForSeoSerpCompetitorsResponseSchema = paidResponseSchema$1.extend({ tasks: z.array(z.object({
|
|
21156
|
+
...taskFields$1,
|
|
21116
21157
|
result: z.array(competitorsResultSchema).max(10).nullable().optional()
|
|
21117
21158
|
}).passthrough()).max(10) });
|
|
21118
21159
|
const DOMAIN_OVERVIEW_PATH = "v3/dataforseo_labs/google/domain_rank_overview/live";
|
|
@@ -21302,6 +21343,209 @@ function serpCompetitorsPaidRequest(input, ttlMs) {
|
|
|
21302
21343
|
};
|
|
21303
21344
|
}
|
|
21304
21345
|
//#endregion
|
|
21346
|
+
//#region packages/core/dist/providers/dataforseo/link-schema.js
|
|
21347
|
+
const nullableCount = z.number().int().nonnegative().nullable().optional();
|
|
21348
|
+
const nullableScore = z.number().finite().nonnegative().nullable().optional();
|
|
21349
|
+
const nullableText = z.string().max(2e4).nullable().optional();
|
|
21350
|
+
const paidResponseSchema = z.object({
|
|
21351
|
+
status_code: z.number().int(),
|
|
21352
|
+
status_message: z.string().max(1e3),
|
|
21353
|
+
cost: z.number().finite().nonnegative().optional(),
|
|
21354
|
+
tasks_count: z.number().int().nonnegative(),
|
|
21355
|
+
tasks_error: z.number().int().nonnegative()
|
|
21356
|
+
}).passthrough();
|
|
21357
|
+
const taskFields = {
|
|
21358
|
+
id: z.string().trim().min(1).max(100).optional(),
|
|
21359
|
+
status_code: z.number().int(),
|
|
21360
|
+
status_message: z.string().max(1e3),
|
|
21361
|
+
cost: z.number().finite().nonnegative().optional(),
|
|
21362
|
+
result_count: z.number().int().nonnegative().optional()
|
|
21363
|
+
};
|
|
21364
|
+
const dataForSeoLinkSummaryItemSchema = z.object({
|
|
21365
|
+
target: z.string().trim().min(1).max(2048).optional(),
|
|
21366
|
+
rank: nullableScore,
|
|
21367
|
+
backlinks: nullableCount,
|
|
21368
|
+
referring_pages: nullableCount,
|
|
21369
|
+
referring_domains: nullableCount,
|
|
21370
|
+
broken_backlinks: nullableCount,
|
|
21371
|
+
broken_pages: nullableCount,
|
|
21372
|
+
backlinks_spam_score: nullableScore,
|
|
21373
|
+
info: z.object({ target_spam_score: nullableScore }).passthrough().nullable().optional()
|
|
21374
|
+
}).passthrough();
|
|
21375
|
+
const dataForSeoLinkSummaryResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
21376
|
+
...taskFields,
|
|
21377
|
+
result: z.array(dataForSeoLinkSummaryItemSchema).max(10).nullable().optional()
|
|
21378
|
+
}).passthrough()).max(10) });
|
|
21379
|
+
const dataForSeoBacklinkItemSchema = z.object({
|
|
21380
|
+
domain_from: nullableText,
|
|
21381
|
+
url_from: nullableText,
|
|
21382
|
+
url_to: nullableText,
|
|
21383
|
+
anchor: nullableText,
|
|
21384
|
+
item_type: z.string().max(100).nullable().optional(),
|
|
21385
|
+
dofollow: z.boolean().nullable().optional(),
|
|
21386
|
+
rank: nullableScore,
|
|
21387
|
+
domain_from_rank: nullableScore,
|
|
21388
|
+
page_from_rank: nullableScore,
|
|
21389
|
+
backlink_spam_score: nullableScore,
|
|
21390
|
+
backlinks_spam_score: nullableScore,
|
|
21391
|
+
first_seen: z.string().max(100).nullable().optional(),
|
|
21392
|
+
last_visited: z.string().max(100).nullable().optional(),
|
|
21393
|
+
lost_date: z.string().max(100).nullable().optional(),
|
|
21394
|
+
is_lost: z.boolean().nullable().optional(),
|
|
21395
|
+
is_indirect_link: z.boolean().nullable().optional(),
|
|
21396
|
+
links_count: nullableCount,
|
|
21397
|
+
group_count: nullableCount,
|
|
21398
|
+
rel_attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional(),
|
|
21399
|
+
attributes: z.array(z.string().trim().min(1).max(100)).max(100).nullable().optional()
|
|
21400
|
+
}).passthrough();
|
|
21401
|
+
const backlinkResultSchema = z.object({
|
|
21402
|
+
target: z.string().trim().min(1).max(2048).optional(),
|
|
21403
|
+
total_count: nullableCount,
|
|
21404
|
+
items_count: nullableCount,
|
|
21405
|
+
search_after_token: z.string().max(2e4).nullable().optional(),
|
|
21406
|
+
items: z.array(dataForSeoBacklinkItemSchema).max(1e3).nullable().optional()
|
|
21407
|
+
}).passthrough();
|
|
21408
|
+
const dataForSeoBacklinksResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
21409
|
+
...taskFields,
|
|
21410
|
+
result: z.array(backlinkResultSchema).max(10).nullable().optional()
|
|
21411
|
+
}).passthrough()).max(10) });
|
|
21412
|
+
const dataForSeoReferringDomainItemSchema = z.object({
|
|
21413
|
+
domain: z.string().trim().min(1).max(2048).nullable().optional(),
|
|
21414
|
+
backlinks: nullableCount,
|
|
21415
|
+
referring_pages: nullableCount,
|
|
21416
|
+
broken_backlinks: nullableCount,
|
|
21417
|
+
broken_pages: nullableCount,
|
|
21418
|
+
rank: nullableScore,
|
|
21419
|
+
backlinks_spam_score: nullableScore,
|
|
21420
|
+
first_seen: z.string().max(100).nullable().optional()
|
|
21421
|
+
}).passthrough();
|
|
21422
|
+
const referringDomainsResultSchema = z.object({
|
|
21423
|
+
target: z.string().trim().min(1).max(2048).optional(),
|
|
21424
|
+
total_count: nullableCount,
|
|
21425
|
+
items_count: nullableCount,
|
|
21426
|
+
items: z.array(dataForSeoReferringDomainItemSchema).max(1e3).nullable().optional()
|
|
21427
|
+
}).passthrough();
|
|
21428
|
+
const dataForSeoReferringDomainsResponseSchema = paidResponseSchema.extend({ tasks: z.array(z.object({
|
|
21429
|
+
...taskFields,
|
|
21430
|
+
result: z.array(referringDomainsResultSchema).max(10).nullable().optional()
|
|
21431
|
+
}).passthrough()).max(10) });
|
|
21432
|
+
const MAX_LINK_ROWS$1 = 1e3;
|
|
21433
|
+
const MAX_LINK_OFFSET = 2e4;
|
|
21434
|
+
const LINK_ENDPOINTS = {
|
|
21435
|
+
summary: "v3/backlinks/summary/live",
|
|
21436
|
+
backlinks: "v3/backlinks/backlinks/live",
|
|
21437
|
+
referringDomains: "v3/backlinks/referring_domains/live"
|
|
21438
|
+
};
|
|
21439
|
+
function resultRows(response) {
|
|
21440
|
+
return response.tasks.reduce((taskTotal, task) => taskTotal + (task.result ?? []).reduce((resultTotal, result) => resultTotal + (result.items?.length ?? 0), 0), 0);
|
|
21441
|
+
}
|
|
21442
|
+
function summaryRows(response) {
|
|
21443
|
+
return response.tasks.reduce((total, task) => total + (task.result?.length ?? 0), 0);
|
|
21444
|
+
}
|
|
21445
|
+
function commonRequest(input) {
|
|
21446
|
+
return {
|
|
21447
|
+
target: input.target,
|
|
21448
|
+
include_subdomains: input.scope === "domain" ? input.includeSubdomains : false,
|
|
21449
|
+
include_indirect_links: true,
|
|
21450
|
+
exclude_internal_backlinks: true,
|
|
21451
|
+
backlinks_status_type: "live",
|
|
21452
|
+
rank_scale: "one_hundred"
|
|
21453
|
+
};
|
|
21454
|
+
}
|
|
21455
|
+
function validatePage(input) {
|
|
21456
|
+
if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 1e3) throw new ProviderError({
|
|
21457
|
+
provider: "dataforseo",
|
|
21458
|
+
operation: input.operation,
|
|
21459
|
+
code: "configuration",
|
|
21460
|
+
message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
|
|
21461
|
+
});
|
|
21462
|
+
if (!Number.isSafeInteger(input.offset) || input.offset < 0 || input.offset > 2e4) throw new ProviderError({
|
|
21463
|
+
provider: "dataforseo",
|
|
21464
|
+
operation: input.operation,
|
|
21465
|
+
code: "configuration",
|
|
21466
|
+
message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
|
|
21467
|
+
});
|
|
21468
|
+
}
|
|
21469
|
+
function linkSummaryPaidRequest(input, ttlMs) {
|
|
21470
|
+
return {
|
|
21471
|
+
operation: "link-summary",
|
|
21472
|
+
capability: "link-summary",
|
|
21473
|
+
endpoint: LINK_ENDPOINTS.summary,
|
|
21474
|
+
request: commonRequest(input),
|
|
21475
|
+
schema: dataForSeoLinkSummaryResponseSchema,
|
|
21476
|
+
requestedRows: 1,
|
|
21477
|
+
price: (account) => account.linkPrices.summary,
|
|
21478
|
+
context: input.context,
|
|
21479
|
+
ttlMs,
|
|
21480
|
+
refresh: input.refresh,
|
|
21481
|
+
rowCount: summaryRows
|
|
21482
|
+
};
|
|
21483
|
+
}
|
|
21484
|
+
function backlinksPaidRequest(input, ttlMs) {
|
|
21485
|
+
validatePage({
|
|
21486
|
+
limit: input.limit,
|
|
21487
|
+
offset: input.offset,
|
|
21488
|
+
operation: "backlinks"
|
|
21489
|
+
});
|
|
21490
|
+
if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
|
|
21491
|
+
provider: "dataforseo",
|
|
21492
|
+
operation: "backlinks",
|
|
21493
|
+
code: "configuration",
|
|
21494
|
+
message: "Backlink rows need from 1 to 3 sort rules."
|
|
21495
|
+
});
|
|
21496
|
+
return {
|
|
21497
|
+
operation: "backlinks",
|
|
21498
|
+
capability: "backlinks",
|
|
21499
|
+
endpoint: LINK_ENDPOINTS.backlinks,
|
|
21500
|
+
request: {
|
|
21501
|
+
...commonRequest(input),
|
|
21502
|
+
mode: input.mode,
|
|
21503
|
+
backlinks_status_type: input.status,
|
|
21504
|
+
limit: input.limit,
|
|
21505
|
+
offset: input.offset,
|
|
21506
|
+
order_by: input.orderBy
|
|
21507
|
+
},
|
|
21508
|
+
schema: dataForSeoBacklinksResponseSchema,
|
|
21509
|
+
requestedRows: input.limit,
|
|
21510
|
+
price: (account) => account.linkPrices.backlinks,
|
|
21511
|
+
context: input.context,
|
|
21512
|
+
ttlMs,
|
|
21513
|
+
refresh: input.refresh,
|
|
21514
|
+
rowCount: resultRows
|
|
21515
|
+
};
|
|
21516
|
+
}
|
|
21517
|
+
function referringDomainsPaidRequest(input, ttlMs) {
|
|
21518
|
+
validatePage({
|
|
21519
|
+
limit: input.limit,
|
|
21520
|
+
offset: input.offset,
|
|
21521
|
+
operation: "referring-domains"
|
|
21522
|
+
});
|
|
21523
|
+
if (input.orderBy.length < 1 || input.orderBy.length > 3) throw new ProviderError({
|
|
21524
|
+
provider: "dataforseo",
|
|
21525
|
+
operation: "referring-domains",
|
|
21526
|
+
code: "configuration",
|
|
21527
|
+
message: "Referring-domain rows need from 1 to 3 sort rules."
|
|
21528
|
+
});
|
|
21529
|
+
return {
|
|
21530
|
+
operation: "referring-domains",
|
|
21531
|
+
capability: "referring-domains",
|
|
21532
|
+
endpoint: LINK_ENDPOINTS.referringDomains,
|
|
21533
|
+
request: {
|
|
21534
|
+
...commonRequest(input),
|
|
21535
|
+
limit: input.limit,
|
|
21536
|
+
offset: input.offset,
|
|
21537
|
+
order_by: input.orderBy
|
|
21538
|
+
},
|
|
21539
|
+
schema: dataForSeoReferringDomainsResponseSchema,
|
|
21540
|
+
requestedRows: input.limit,
|
|
21541
|
+
price: (account) => account.linkPrices.referringDomains,
|
|
21542
|
+
context: input.context,
|
|
21543
|
+
ttlMs,
|
|
21544
|
+
refresh: input.refresh,
|
|
21545
|
+
rowCount: resultRows
|
|
21546
|
+
};
|
|
21547
|
+
}
|
|
21548
|
+
//#endregion
|
|
21305
21549
|
//#region packages/core/dist/providers/dataforseo/paid-request.js
|
|
21306
21550
|
function usdToMicros(value) {
|
|
21307
21551
|
if (value === void 0) return null;
|
|
@@ -21825,41 +22069,6 @@ const MAX_KEYWORD_CHARACTERS = 80;
|
|
|
21825
22069
|
const MAX_KEYWORD_WORDS = 10;
|
|
21826
22070
|
const MAX_DISCOVERY_SEEDS$1 = 5;
|
|
21827
22071
|
const MAX_DISCOVERY_ROWS$1 = 100;
|
|
21828
|
-
function unitPrice(components) {
|
|
21829
|
-
const prices = components ?? [];
|
|
21830
|
-
const total = (type) => {
|
|
21831
|
-
const matching = prices.filter((item) => item.cost_type === type);
|
|
21832
|
-
return matching.length ? matching.reduce((sum, item) => sum + Math.round(item.cost * 1e6), 0) : prices.length ? 0 : null;
|
|
21833
|
-
};
|
|
21834
|
-
return {
|
|
21835
|
-
perRequestMicros: total("per_request"),
|
|
21836
|
-
perResultMicros: total("per_result")
|
|
21837
|
-
};
|
|
21838
|
-
}
|
|
21839
|
-
function keywordOverviewPrice(account) {
|
|
21840
|
-
return unitPrice(account.price?.dataforseo_labs?.keyword_overview?.live?.priority_normal);
|
|
21841
|
-
}
|
|
21842
|
-
function keywordDiscoveryPrices(account) {
|
|
21843
|
-
return {
|
|
21844
|
-
ideas: unitPrice(account.price?.dataforseo_labs?.keyword_ideas?.live?.priority_normal),
|
|
21845
|
-
related: unitPrice(account.price?.dataforseo_labs?.related_keywords?.live?.priority_normal),
|
|
21846
|
-
suggestions: unitPrice(account.price?.dataforseo_labs?.keyword_suggestions?.live?.priority_normal)
|
|
21847
|
-
};
|
|
21848
|
-
}
|
|
21849
|
-
function domainResearchPrices(account) {
|
|
21850
|
-
return {
|
|
21851
|
-
domainOverview: unitPrice(account.price?.dataforseo_labs?.domain_rank_overview?.live?.priority_normal),
|
|
21852
|
-
rankedKeywords: unitPrice(account.price?.dataforseo_labs?.ranked_keywords?.live?.priority_normal),
|
|
21853
|
-
rankingPages: unitPrice(account.price?.dataforseo_labs?.relevant_pages?.live?.priority_normal),
|
|
21854
|
-
serpCompetitors: unitPrice(account.price?.dataforseo_labs?.serp_competitors?.live?.priority_normal)
|
|
21855
|
-
};
|
|
21856
|
-
}
|
|
21857
|
-
function serpLiveAdvancedPrice(account) {
|
|
21858
|
-
return unitPrice(account.price?.serp?.live?.advanced?.priority_normal);
|
|
21859
|
-
}
|
|
21860
|
-
function serpTaskPostPrice(account) {
|
|
21861
|
-
return unitPrice(account.price?.serp?.task_post?.priority_normal);
|
|
21862
|
-
}
|
|
21863
22072
|
function responseTaskIds(response) {
|
|
21864
22073
|
return [...new Set(response.tasks.flatMap((task) => task.id ? [task.id] : []))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
21865
22074
|
}
|
|
@@ -21879,6 +22088,7 @@ var DataForSeoClient = class {
|
|
|
21879
22088
|
serpTtlMs;
|
|
21880
22089
|
accountPricingTtlMs;
|
|
21881
22090
|
domainResearchTtlMs;
|
|
22091
|
+
linkTtlMs;
|
|
21882
22092
|
spendLimits;
|
|
21883
22093
|
accountPricing;
|
|
21884
22094
|
accountPricingRequest;
|
|
@@ -21895,6 +22105,7 @@ var DataForSeoClient = class {
|
|
|
21895
22105
|
this.serpTtlMs = options.serpTtlMs ?? DEFAULT_SERP_TTL_MS;
|
|
21896
22106
|
this.accountPricingTtlMs = options.accountPricingTtlMs ?? DEFAULT_ACCOUNT_PRICING_TTL_MS;
|
|
21897
22107
|
this.domainResearchTtlMs = options.domainResearchTtlMs ?? 6048e5;
|
|
22108
|
+
this.linkTtlMs = options.linkTtlMs ?? 864e5;
|
|
21898
22109
|
this.spendLimits = options.spendLimits;
|
|
21899
22110
|
}
|
|
21900
22111
|
async getCredentials(operation) {
|
|
@@ -21977,11 +22188,7 @@ var DataForSeoClient = class {
|
|
|
21977
22188
|
accountDailySpendMicros: dataForSeoUsdToMicros(account.money?.statistics?.day?.total),
|
|
21978
22189
|
accountDailySpendPeriod: account.money?.statistics?.day?.value ?? null,
|
|
21979
22190
|
accountDailyLimitMicros: dataForSeoUsdToMicros(account.money?.limits?.day?.total),
|
|
21980
|
-
|
|
21981
|
-
keywordDiscoveryPrices: keywordDiscoveryPrices(account),
|
|
21982
|
-
domainResearchPrices: domainResearchPrices(account),
|
|
21983
|
-
serpLiveAdvancedPrice: serpLiveAdvancedPrice(account),
|
|
21984
|
-
serpTaskPostPrice: serpTaskPostPrice(account),
|
|
22191
|
+
...dataForSeoAccountPrices(account),
|
|
21985
22192
|
backlinksSubscriptionExpiresAt: account.backlinks_subscription_expiry_date ?? null,
|
|
21986
22193
|
aiMentionsSubscriptionExpiresAt: account.llm_mentions_subscription_expiry_date ?? null,
|
|
21987
22194
|
apiVersion: response.version ?? null,
|
|
@@ -22044,6 +22251,15 @@ var DataForSeoClient = class {
|
|
|
22044
22251
|
async serpCompetitors(input) {
|
|
22045
22252
|
return this.paidPost(serpCompetitorsPaidRequest(input, this.domainResearchTtlMs));
|
|
22046
22253
|
}
|
|
22254
|
+
async linkSummary(input) {
|
|
22255
|
+
return this.paidPost(linkSummaryPaidRequest(input, this.linkTtlMs));
|
|
22256
|
+
}
|
|
22257
|
+
async backlinks(input) {
|
|
22258
|
+
return this.paidPost(backlinksPaidRequest(input, this.linkTtlMs));
|
|
22259
|
+
}
|
|
22260
|
+
async referringDomains(input) {
|
|
22261
|
+
return this.paidPost(referringDomainsPaidRequest(input, this.linkTtlMs));
|
|
22262
|
+
}
|
|
22047
22263
|
async keywordOverview(input) {
|
|
22048
22264
|
const keywords = input.keywords.map((keyword) => keyword.trim());
|
|
22049
22265
|
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({
|
|
@@ -22387,20 +22603,20 @@ function compareCodepoints$3(left, right) {
|
|
|
22387
22603
|
function normalizedKeyword$2(keyword) {
|
|
22388
22604
|
return keyword.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
22389
22605
|
}
|
|
22390
|
-
function missing$
|
|
22606
|
+
function missing$2(field) {
|
|
22391
22607
|
return unavailableValue("missing", `DataForSEO omitted ${field}.`);
|
|
22392
22608
|
}
|
|
22393
22609
|
function numericValue(values, field, valid) {
|
|
22394
22610
|
const present = values.filter((value) => value !== null && value !== void 0);
|
|
22395
|
-
if (present.length === 0) return missing$
|
|
22611
|
+
if (present.length === 0) return missing$2(field);
|
|
22396
22612
|
const numbers = present.filter((value) => typeof value === "number" && valid(value));
|
|
22397
22613
|
if (numbers.length !== present.length) return unavailableValue("invalid", `DataForSEO returned an invalid ${field}.`);
|
|
22398
22614
|
const unique = [...new Set(numbers)];
|
|
22399
22615
|
return unique.length === 1 ? observedValue(unique[0]) : unavailableValue("invalid", `DataForSEO returned conflicting ${field} values.`);
|
|
22400
22616
|
}
|
|
22401
|
-
function stringValue(values, field, normalize) {
|
|
22617
|
+
function stringValue$1(values, field, normalize) {
|
|
22402
22618
|
const present = values.filter((value) => value !== null && value !== void 0);
|
|
22403
|
-
if (present.length === 0) return missing$
|
|
22619
|
+
if (present.length === 0) return missing$2(field);
|
|
22404
22620
|
const strings = present.flatMap((value) => {
|
|
22405
22621
|
if (typeof value !== "string") return [];
|
|
22406
22622
|
const normalized = normalize(value);
|
|
@@ -22412,7 +22628,7 @@ function stringValue(values, field, normalize) {
|
|
|
22412
22628
|
}
|
|
22413
22629
|
function monthlySearchesValue(rows) {
|
|
22414
22630
|
const histories = rows.map((row) => row.keyword_info?.monthly_searches).filter((value) => value !== null && value !== void 0);
|
|
22415
|
-
if (histories.length === 0) return missing$
|
|
22631
|
+
if (histories.length === 0) return missing$2("monthlySearches");
|
|
22416
22632
|
const byMonth = /* @__PURE__ */ new Map();
|
|
22417
22633
|
let invalid = false;
|
|
22418
22634
|
for (const history of histories) for (const item of history) {
|
|
@@ -22445,27 +22661,27 @@ function resultCountValues(rows) {
|
|
|
22445
22661
|
function metricForKeyword(keyword, rows) {
|
|
22446
22662
|
if (rows.length === 0) return {
|
|
22447
22663
|
keyword,
|
|
22448
|
-
monthlySearchVolume: missing$
|
|
22449
|
-
monthlySearches: missing$
|
|
22450
|
-
searchVolumeUpdatedAt: missing$
|
|
22451
|
-
cpcUsd: missing$
|
|
22452
|
-
paidCompetition: missing$
|
|
22453
|
-
keywordDifficulty: missing$
|
|
22454
|
-
intent: missing$
|
|
22455
|
-
resultCount: missing$
|
|
22664
|
+
monthlySearchVolume: missing$2("monthlySearchVolume"),
|
|
22665
|
+
monthlySearches: missing$2("monthlySearches"),
|
|
22666
|
+
searchVolumeUpdatedAt: missing$2("searchVolumeUpdatedAt"),
|
|
22667
|
+
cpcUsd: missing$2("cpcUsd"),
|
|
22668
|
+
paidCompetition: missing$2("paidCompetition"),
|
|
22669
|
+
keywordDifficulty: missing$2("keywordDifficulty"),
|
|
22670
|
+
intent: missing$2("intent"),
|
|
22671
|
+
resultCount: missing$2("resultCount")
|
|
22456
22672
|
};
|
|
22457
22673
|
return {
|
|
22458
22674
|
keyword,
|
|
22459
22675
|
monthlySearchVolume: numericValue(rows.map((row) => row.keyword_info?.search_volume), "monthlySearchVolume", (value) => Number.isSafeInteger(value) && value >= 0),
|
|
22460
22676
|
monthlySearches: monthlySearchesValue(rows),
|
|
22461
|
-
searchVolumeUpdatedAt: stringValue(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
|
|
22677
|
+
searchVolumeUpdatedAt: stringValue$1(rows.map((row) => row.keyword_info?.last_updated_time), "searchVolumeUpdatedAt", (value) => {
|
|
22462
22678
|
const timestamp = Date.parse(value);
|
|
22463
22679
|
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
22464
22680
|
}),
|
|
22465
22681
|
cpcUsd: numericValue(rows.map((row) => row.keyword_info?.cpc), "cpcUsd", (value) => Number.isFinite(value) && value >= 0),
|
|
22466
22682
|
paidCompetition: numericValue(rows.map((row) => row.keyword_info?.competition), "paidCompetition", (value) => Number.isFinite(value) && value >= 0 && value <= 1),
|
|
22467
22683
|
keywordDifficulty: numericValue(rows.map((row) => row.keyword_properties?.keyword_difficulty), "keywordDifficulty", (value) => Number.isFinite(value) && value >= 0 && value <= 100),
|
|
22468
|
-
intent: stringValue(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
|
|
22684
|
+
intent: stringValue$1(rows.map((row) => row.search_intent_info?.main_intent), "intent", (value) => value.trim().toLowerCase() || null),
|
|
22469
22685
|
resultCount: numericValue(resultCountValues(rows), "resultCount", (value) => Number.isSafeInteger(value) && value >= 0)
|
|
22470
22686
|
};
|
|
22471
22687
|
}
|
|
@@ -22518,21 +22734,21 @@ const COMPETITOR_RESULT_TYPES = /* @__PURE__ */ new Set([
|
|
|
22518
22734
|
"featured_snippet",
|
|
22519
22735
|
"local_pack"
|
|
22520
22736
|
]);
|
|
22521
|
-
function missing(field) {
|
|
22737
|
+
function missing$1(field) {
|
|
22522
22738
|
return unavailableValue("missing", `DataForSEO omitted ${field}.`);
|
|
22523
22739
|
}
|
|
22524
|
-
function numberValue(value, field) {
|
|
22525
|
-
return value === null || value === void 0 ? missing(field) : observedValue(value);
|
|
22740
|
+
function numberValue$1(value, field) {
|
|
22741
|
+
return value === null || value === void 0 ? missing$1(field) : observedValue(value);
|
|
22526
22742
|
}
|
|
22527
22743
|
function countValue(value, field) {
|
|
22528
|
-
return numberValue(value, field);
|
|
22744
|
+
return numberValue$1(value, field);
|
|
22529
22745
|
}
|
|
22530
22746
|
function sumBuckets(metrics, fields, label) {
|
|
22531
22747
|
const values = fields.map((field) => metrics[field]);
|
|
22532
|
-
return values.some((value) => value === null || value === void 0) ? missing(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
|
|
22748
|
+
return values.some((value) => value === null || value === void 0) ? missing$1(label) : observedValue(values.reduce((sum, value) => sum + (typeof value === "number" ? value : 0), 0));
|
|
22533
22749
|
}
|
|
22534
22750
|
function rankingDistribution(metrics) {
|
|
22535
|
-
if (!metrics) return missing("organic ranking distribution");
|
|
22751
|
+
if (!metrics) return missing$1("organic ranking distribution");
|
|
22536
22752
|
const buckets = {
|
|
22537
22753
|
first: sumBuckets(metrics, ["pos_1"], "position 1 rankings"),
|
|
22538
22754
|
top3: sumBuckets(metrics, ["pos_1", "pos_2_3"], "top 3 rankings"),
|
|
@@ -22583,9 +22799,9 @@ function rankingDistribution(metrics) {
|
|
|
22583
22799
|
}
|
|
22584
22800
|
function organicFootprint(metrics) {
|
|
22585
22801
|
return {
|
|
22586
|
-
estimatedMonthlyTraffic: numberValue(metrics?.etv, "estimated organic monthly traffic"),
|
|
22802
|
+
estimatedMonthlyTraffic: numberValue$1(metrics?.etv, "estimated organic monthly traffic"),
|
|
22587
22803
|
rankedKeywords: countValue(metrics?.count, "ranked organic keywords"),
|
|
22588
|
-
estimatedMonthlyTrafficCostUsd: numberValue(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
|
|
22804
|
+
estimatedMonthlyTrafficCostUsd: numberValue$1(metrics?.estimated_paid_traffic_cost, "estimated organic traffic cost"),
|
|
22589
22805
|
rankings: rankingDistribution(metrics),
|
|
22590
22806
|
newRankings: countValue(metrics?.is_new, "new rankings"),
|
|
22591
22807
|
improvedRankings: countValue(metrics?.is_up, "improved rankings"),
|
|
@@ -22640,7 +22856,7 @@ function researchMarket(input, operation) {
|
|
|
22640
22856
|
});
|
|
22641
22857
|
return parsed;
|
|
22642
22858
|
}
|
|
22643
|
-
function rowLimit(limit, offset = 0) {
|
|
22859
|
+
function rowLimit$1(limit, offset = 0) {
|
|
22644
22860
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
|
|
22645
22861
|
provider: "dataforseo",
|
|
22646
22862
|
operation: "domain-research",
|
|
@@ -22664,7 +22880,7 @@ function resultTypes(input, allowed) {
|
|
|
22664
22880
|
});
|
|
22665
22881
|
return normalized;
|
|
22666
22882
|
}
|
|
22667
|
-
function safeUrl$
|
|
22883
|
+
function safeUrl$2(value) {
|
|
22668
22884
|
if (!value) return null;
|
|
22669
22885
|
try {
|
|
22670
22886
|
const url = new URL(value);
|
|
@@ -22684,7 +22900,7 @@ function dedupeBy(rows, key) {
|
|
|
22684
22900
|
}
|
|
22685
22901
|
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]);
|
|
22686
22902
|
}
|
|
22687
|
-
function observedNumber(value) {
|
|
22903
|
+
function observedNumber$1(value) {
|
|
22688
22904
|
return value.state === "observed" ? value.value : -1;
|
|
22689
22905
|
}
|
|
22690
22906
|
function duplicateWarning(available, retained, label) {
|
|
@@ -22695,7 +22911,7 @@ function duplicateWarning(available, retained, label) {
|
|
|
22695
22911
|
message: `${duplicates} duplicate ${label} row${duplicates === 1 ? "" : "s"} were collapsed deterministically.`
|
|
22696
22912
|
}] : [];
|
|
22697
22913
|
}
|
|
22698
|
-
function totalRows(values) {
|
|
22914
|
+
function totalRows$1(values) {
|
|
22699
22915
|
const present = values.filter((value) => value !== null && value !== void 0);
|
|
22700
22916
|
return present.length ? Math.max(...present) : null;
|
|
22701
22917
|
}
|
|
@@ -22711,7 +22927,7 @@ function coverage(input) {
|
|
|
22711
22927
|
nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
|
|
22712
22928
|
};
|
|
22713
22929
|
}
|
|
22714
|
-
function requestContext(reportId, supplied) {
|
|
22930
|
+
function requestContext$1(reportId, supplied) {
|
|
22715
22931
|
return supplied ?? {
|
|
22716
22932
|
reportId,
|
|
22717
22933
|
reportRunId: randomUUID()
|
|
@@ -22760,7 +22976,7 @@ async function dataForSeoDomainOverview(client, input) {
|
|
|
22760
22976
|
languageCode: market.languageCode.split("-")[0],
|
|
22761
22977
|
...location,
|
|
22762
22978
|
refresh: input.refresh,
|
|
22763
|
-
context: requestContext("domain-overview", input.context)
|
|
22979
|
+
context: requestContext$1("domain-overview", input.context)
|
|
22764
22980
|
});
|
|
22765
22981
|
const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
22766
22982
|
const matchingResult = results.find((row) => !row.target || domain(row.target) === target);
|
|
@@ -22852,7 +23068,7 @@ async function dataForSeoRankedKeywords(client, input) {
|
|
|
22852
23068
|
const market = researchMarket(input.market, "ranked-keywords");
|
|
22853
23069
|
const target = rankedTarget(input.target);
|
|
22854
23070
|
const offset = input.offset ?? 0;
|
|
22855
|
-
rowLimit(input.limit, offset);
|
|
23071
|
+
rowLimit$1(input.limit, offset);
|
|
22856
23072
|
const types = resultTypes(input.resultTypes, RANKED_RESULT_TYPES);
|
|
22857
23073
|
const excluded = excludedTerms(input);
|
|
22858
23074
|
const requestedFilters = filters$1(input, excluded);
|
|
@@ -22872,7 +23088,7 @@ async function dataForSeoRankedKeywords(client, input) {
|
|
|
22872
23088
|
limit: input.limit,
|
|
22873
23089
|
offset,
|
|
22874
23090
|
refresh: input.refresh,
|
|
22875
|
-
context: requestContext("ranked-keywords", input.context)
|
|
23091
|
+
context: requestContext$1("ranked-keywords", input.context)
|
|
22876
23092
|
});
|
|
22877
23093
|
const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
22878
23094
|
const rawRows = responseResults.flatMap((result) => result.items ?? []);
|
|
@@ -22881,7 +23097,7 @@ async function dataForSeoRankedKeywords(client, input) {
|
|
|
22881
23097
|
const keywordData = row.keyword_data;
|
|
22882
23098
|
const serp = row.ranked_serp_element?.serp_item;
|
|
22883
23099
|
const keyword = keywordData?.keyword ? normalizedKeyword$2(keywordData.keyword) : "";
|
|
22884
|
-
const url = safeUrl$
|
|
23100
|
+
const url = safeUrl$2(serp?.url);
|
|
22885
23101
|
if (!keywordData || !keyword || !url || !serp?.rank_group || !serp.rank_absolute || !serp.type) {
|
|
22886
23102
|
invalidRows += 1;
|
|
22887
23103
|
return [];
|
|
@@ -22892,11 +23108,11 @@ async function dataForSeoRankedKeywords(client, input) {
|
|
|
22892
23108
|
rankGroup: serp.rank_group,
|
|
22893
23109
|
rankAbsolute: serp.rank_absolute,
|
|
22894
23110
|
resultType: serp.type,
|
|
22895
|
-
estimatedMonthlyTraffic: numberValue(serp.etv, "estimated keyword traffic")
|
|
23111
|
+
estimatedMonthlyTraffic: numberValue$1(serp.etv, "estimated keyword traffic")
|
|
22896
23112
|
}];
|
|
22897
23113
|
});
|
|
22898
|
-
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));
|
|
22899
|
-
const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
|
|
23114
|
+
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));
|
|
23115
|
+
const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
|
|
22900
23116
|
return domainEvidence({
|
|
22901
23117
|
capability: "ranked-keywords",
|
|
22902
23118
|
data: {
|
|
@@ -22969,7 +23185,7 @@ async function dataForSeoRankingPages(client, input) {
|
|
|
22969
23185
|
const market = researchMarket(input.market, "ranking-pages");
|
|
22970
23186
|
const target = domain(input.domain);
|
|
22971
23187
|
const offset = input.offset ?? 0;
|
|
22972
|
-
rowLimit(input.limit, offset);
|
|
23188
|
+
rowLimit$1(input.limit, offset);
|
|
22973
23189
|
const requestedFilters = filters(input);
|
|
22974
23190
|
const orderBy = ["metrics.organic.etv,desc", "page_address,asc"];
|
|
22975
23191
|
const snapshot = await client.rankingPages({
|
|
@@ -22981,13 +23197,13 @@ async function dataForSeoRankingPages(client, input) {
|
|
|
22981
23197
|
limit: input.limit,
|
|
22982
23198
|
offset,
|
|
22983
23199
|
refresh: input.refresh,
|
|
22984
|
-
context: requestContext("ranking-pages", input.context)
|
|
23200
|
+
context: requestContext$1("ranking-pages", input.context)
|
|
22985
23201
|
});
|
|
22986
23202
|
const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
22987
23203
|
const rawRows = responseResults.flatMap((result) => result.items ?? []);
|
|
22988
23204
|
let invalidRows = 0;
|
|
22989
23205
|
const mappedRows = rawRows.flatMap((row) => {
|
|
22990
|
-
const url = safeUrl$
|
|
23206
|
+
const url = safeUrl$2(row.page_address);
|
|
22991
23207
|
if (!url) {
|
|
22992
23208
|
invalidRows += 1;
|
|
22993
23209
|
return [];
|
|
@@ -22997,8 +23213,8 @@ async function dataForSeoRankingPages(client, input) {
|
|
|
22997
23213
|
organic: organicFootprint(row.metrics?.organic)
|
|
22998
23214
|
}];
|
|
22999
23215
|
});
|
|
23000
|
-
const rows = dedupeBy(mappedRows, (row) => row.url).sort((left, right) => observedNumber(right.organic.estimatedMonthlyTraffic) - observedNumber(left.organic.estimatedMonthlyTraffic) || compareCodepoints$3(left.url, right.url));
|
|
23001
|
-
const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
|
|
23216
|
+
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));
|
|
23217
|
+
const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
|
|
23002
23218
|
return domainEvidence({
|
|
23003
23219
|
capability: "relevant-pages",
|
|
23004
23220
|
data: {
|
|
@@ -23043,7 +23259,7 @@ function keywords(input) {
|
|
|
23043
23259
|
async function dataForSeoSerpCompetitors(client, input) {
|
|
23044
23260
|
const market = researchMarket(input.market, "serp-competitors");
|
|
23045
23261
|
const offset = input.offset ?? 0;
|
|
23046
|
-
rowLimit(input.limit, offset);
|
|
23262
|
+
rowLimit$1(input.limit, offset);
|
|
23047
23263
|
const requestedKeywords = keywords(input.keywords);
|
|
23048
23264
|
const types = resultTypes(input.resultTypes, COMPETITOR_RESULT_TYPES);
|
|
23049
23265
|
const orderBy = [
|
|
@@ -23061,7 +23277,7 @@ async function dataForSeoSerpCompetitors(client, input) {
|
|
|
23061
23277
|
limit: input.limit,
|
|
23062
23278
|
offset,
|
|
23063
23279
|
refresh: input.refresh,
|
|
23064
|
-
context: requestContext("serp-competitors", input.context)
|
|
23280
|
+
context: requestContext$1("serp-competitors", input.context)
|
|
23065
23281
|
});
|
|
23066
23282
|
const responseResults = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
23067
23283
|
const rawRows = responseResults.flatMap((result) => result.items ?? []);
|
|
@@ -23084,16 +23300,16 @@ async function dataForSeoSerpCompetitors(client, input) {
|
|
|
23084
23300
|
return [{
|
|
23085
23301
|
domain: normalizedDomain,
|
|
23086
23302
|
matchedKeywords: row.keywords_count,
|
|
23087
|
-
averagePosition: numberValue(row.avg_position, "average position"),
|
|
23088
|
-
medianPosition: numberValue(row.median_position, "median position"),
|
|
23089
|
-
visibility: numberValue(row.visibility, "visibility"),
|
|
23090
|
-
estimatedMonthlyTraffic: numberValue(row.etv, "estimated monthly traffic"),
|
|
23303
|
+
averagePosition: numberValue$1(row.avg_position, "average position"),
|
|
23304
|
+
medianPosition: numberValue$1(row.median_position, "median position"),
|
|
23305
|
+
visibility: numberValue$1(row.visibility, "visibility"),
|
|
23306
|
+
estimatedMonthlyTraffic: numberValue$1(row.etv, "estimated monthly traffic"),
|
|
23091
23307
|
relevantResults: countValue(row.relevant_serp_items, "relevant result count"),
|
|
23092
23308
|
keywordPositions
|
|
23093
23309
|
}];
|
|
23094
23310
|
});
|
|
23095
|
-
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));
|
|
23096
|
-
const providerTotalRows = totalRows(responseResults.map((result) => result.total_count));
|
|
23311
|
+
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));
|
|
23312
|
+
const providerTotalRows = totalRows$1(responseResults.map((result) => result.total_count));
|
|
23097
23313
|
return domainEvidence({
|
|
23098
23314
|
capability: "serp-competitors",
|
|
23099
23315
|
data: {
|
|
@@ -23217,7 +23433,7 @@ function resolveProvider(input) {
|
|
|
23217
23433
|
//#endregion
|
|
23218
23434
|
//#region packages/core/dist/analyze/domain-research/shared.js
|
|
23219
23435
|
const MAX_GSC_DOMAIN_ROWS = 1e5;
|
|
23220
|
-
function compareText$
|
|
23436
|
+
function compareText$7(left, right) {
|
|
23221
23437
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
23222
23438
|
}
|
|
23223
23439
|
function normalizedKeyword$1(value) {
|
|
@@ -23334,8 +23550,8 @@ function aggregateGscQueries(rows) {
|
|
|
23334
23550
|
clicks: item.clicks,
|
|
23335
23551
|
impressions: item.impressions,
|
|
23336
23552
|
averagePosition: item.positionWeight ? item.weightedPosition / item.positionWeight : null,
|
|
23337
|
-
urls: [...item.urls].sort(compareText$
|
|
23338
|
-
})).sort((left, right) => right.impressions - left.impressions || compareText$
|
|
23553
|
+
urls: [...item.urls].sort(compareText$7).slice(0, 5)
|
|
23554
|
+
})).sort((left, right) => right.impressions - left.impressions || compareText$7(left.query, right.query));
|
|
23339
23555
|
}
|
|
23340
23556
|
async function acquireGscQueries(input) {
|
|
23341
23557
|
const range = finalGscDateRange(input.days, input.now);
|
|
@@ -23701,7 +23917,7 @@ function pseoQueryPatterns(rows) {
|
|
|
23701
23917
|
//#region packages/core/dist/analyze/pseo/templates.js
|
|
23702
23918
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
23703
23919
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
23704
|
-
function compareText$
|
|
23920
|
+
function compareText$6(left, right) {
|
|
23705
23921
|
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
|
|
23706
23922
|
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
|
|
23707
23923
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -23757,7 +23973,7 @@ function valueTokens(values) {
|
|
|
23757
23973
|
const tokens = value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
23758
23974
|
for (const token of tokens.filter((item) => item.length > 1 || /[^\p{ASCII}]/u.test(item))) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
23759
23975
|
}
|
|
23760
|
-
return [...counts.entries()].sort((a, b) => b[1] - a[1] || compareText$
|
|
23976
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || compareText$6(a[0], b[0])).slice(0, 8).map(([token]) => token);
|
|
23761
23977
|
}
|
|
23762
23978
|
function templateShape(signature, urls) {
|
|
23763
23979
|
const signatureParts = signature.split("/").filter(Boolean);
|
|
@@ -23806,7 +24022,7 @@ function collectClusters(urls, signatures) {
|
|
|
23806
24022
|
return bySignature;
|
|
23807
24023
|
}
|
|
23808
24024
|
function clusterPseoTemplates(urls, opts = {}) {
|
|
23809
|
-
const uniqueUrls = [...new Set(urls)].sort(compareText$
|
|
24025
|
+
const uniqueUrls = [...new Set(urls)].sort(compareText$6);
|
|
23810
24026
|
const minUrls = opts.minUrls ?? 3;
|
|
23811
24027
|
const minShare = opts.minShare ?? .01;
|
|
23812
24028
|
const sampleSize = opts.sampleSize ?? 5;
|
|
@@ -23824,15 +24040,15 @@ function clusterPseoTemplates(urls, opts = {}) {
|
|
|
23824
24040
|
shape: templateShape(signature, clusterUrls),
|
|
23825
24041
|
urlCount: clusterUrls.length,
|
|
23826
24042
|
share: uniqueUrls.length ? clusterUrls.length / uniqueUrls.length : 0,
|
|
23827
|
-
sampleUrls: clusterUrls.sort(compareText$
|
|
23828
|
-
})).filter((cluster) => cluster.shape.variableSegments.length > 0 && cluster.urlCount >= minUrls && cluster.share >= minShare).sort((a, b) => b.urlCount - a.urlCount || compareText$
|
|
24043
|
+
sampleUrls: clusterUrls.sort(compareText$6).slice(0, sampleSize)
|
|
24044
|
+
})).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);
|
|
23829
24045
|
}
|
|
23830
24046
|
//#endregion
|
|
23831
24047
|
//#region packages/core/dist/analyze/domain-research/gap-analysis.js
|
|
23832
24048
|
const MAX_RELEVANCE_QUERY_REFS = 3;
|
|
23833
24049
|
const MAX_FIRST_PARTY_PATTERN_URLS = 1e4;
|
|
23834
24050
|
function terms(value) {
|
|
23835
|
-
return [...new Set(pseoQueryTerms(value).map(canonicalPseoTerm).filter(Boolean))].sort(compareText$
|
|
24051
|
+
return [...new Set(pseoQueryTerms(value).map(canonicalPseoTerm).filter(Boolean))].sort(compareText$7);
|
|
23836
24052
|
}
|
|
23837
24053
|
function observedVolume$1(row) {
|
|
23838
24054
|
return value(row.monthlySearchVolume) ?? -1;
|
|
@@ -23876,7 +24092,7 @@ function relevanceForKeyword(keyword, acquisition, tokenIndex) {
|
|
|
23876
24092
|
query: row.query,
|
|
23877
24093
|
overlap: terms(row.query).filter((token) => keywordTerms.includes(token)).length,
|
|
23878
24094
|
impressions: row.impressions
|
|
23879
|
-
})).filter((row) => row.overlap > 0).sort((left, right) => right.overlap - left.overlap || right.impressions - left.impressions || compareText$
|
|
24095
|
+
})).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);
|
|
23880
24096
|
return {
|
|
23881
24097
|
state: sharedTokens.length >= 2 ? "observed-overlap" : sharedTokens.length === 1 ? "weak-overlap" : "unavailable",
|
|
23882
24098
|
sharedTokens,
|
|
@@ -23898,10 +24114,10 @@ function repeatedCompetitorPatterns(rows) {
|
|
|
23898
24114
|
urlCount: pattern.urlCount,
|
|
23899
24115
|
sampleUrls: pattern.sampleUrls,
|
|
23900
24116
|
evidenceRefs: items.flatMap((item) => pattern.sampleUrls.includes(item.row.url) ? [item.evidenceRef] : [])
|
|
23901
|
-
}))).sort((left, right) => right.urlCount - left.urlCount || compareText$
|
|
24117
|
+
}))).sort((left, right) => right.urlCount - left.urlCount || compareText$7(left.domain, right.domain) || compareText$7(left.signature, right.signature));
|
|
23902
24118
|
}
|
|
23903
24119
|
function ownPatterns(acquisition) {
|
|
23904
|
-
return clusterPseoTemplates([...new Set(acquisition.sourceRows.rows.flatMap((row) => row.urls))].sort(compareText$
|
|
24120
|
+
return clusterPseoTemplates([...new Set(acquisition.sourceRows.rows.flatMap((row) => row.urls))].sort(compareText$7).slice(0, MAX_FIRST_PARTY_PATTERN_URLS), {
|
|
23905
24121
|
minUrls: 3,
|
|
23906
24122
|
minShare: 0,
|
|
23907
24123
|
limit: 50,
|
|
@@ -23942,10 +24158,10 @@ function analyzeCompetitorGap(acquisition) {
|
|
|
23942
24158
|
const byDomain = /* @__PURE__ */ new Map();
|
|
23943
24159
|
for (const item of rows) {
|
|
23944
24160
|
const current = byDomain.get(item.domain);
|
|
23945
|
-
if (!current || item.row.rankGroup < current.row.rankGroup || item.row.rankGroup === current.row.rankGroup && compareText$
|
|
24161
|
+
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);
|
|
23946
24162
|
}
|
|
23947
|
-
const retainedRows = [...byDomain.values()].sort((left, right) => left.row.rankGroup - right.row.rankGroup || compareText$
|
|
23948
|
-
const best = [...retainedRows].sort((left, right) => observedVolume$1(right.row) - observedVolume$1(left.row) || left.row.rankGroup - right.row.rankGroup || compareText$
|
|
24163
|
+
const retainedRows = [...byDomain.values()].sort((left, right) => left.row.rankGroup - right.row.rankGroup || compareText$7(left.domain, right.domain));
|
|
24164
|
+
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];
|
|
23949
24165
|
const firstParty = firstPartyByQuery.get(keyword);
|
|
23950
24166
|
const relevance = relevanceForKeyword(keyword, acquisition, tokenIndex.index);
|
|
23951
24167
|
const ownRank = ownRanks.get(keyword) ?? null;
|
|
@@ -23989,7 +24205,7 @@ function analyzeCompetitorGap(acquisition) {
|
|
|
23989
24205
|
},
|
|
23990
24206
|
evidenceRefs: retainedRows.map((item) => item.evidenceRef)
|
|
23991
24207
|
};
|
|
23992
|
-
}).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$
|
|
24208
|
+
}).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));
|
|
23993
24209
|
return {
|
|
23994
24210
|
rawRows,
|
|
23995
24211
|
candidates,
|
|
@@ -24008,7 +24224,7 @@ function analyzeCompetitorGap(acquisition) {
|
|
|
24008
24224
|
}
|
|
24009
24225
|
//#endregion
|
|
24010
24226
|
//#region packages/core/dist/analyze/domain-research/gap-report.js
|
|
24011
|
-
const MAX_FINDINGS = 10;
|
|
24227
|
+
const MAX_FINDINGS$2 = 10;
|
|
24012
24228
|
const MAX_SOURCE_BRIEFS = 5;
|
|
24013
24229
|
function count(candidates, classification) {
|
|
24014
24230
|
return candidates.filter((item) => item.classification === classification).length;
|
|
@@ -24023,7 +24239,7 @@ function dataStatus(input) {
|
|
|
24023
24239
|
function findings$2(input) {
|
|
24024
24240
|
const result = [];
|
|
24025
24241
|
for (const [index, candidate] of input.candidates.entries()) {
|
|
24026
|
-
if (result.length >= MAX_FINDINGS) break;
|
|
24242
|
+
if (result.length >= MAX_FINDINGS$2) break;
|
|
24027
24243
|
if (candidate.classification === "relevant-gap-candidate") result.push({
|
|
24028
24244
|
code: "relevant-competitor-gap",
|
|
24029
24245
|
evidenceRefs: [`candidates[${index}]`, ...candidate.evidenceRefs],
|
|
@@ -24038,7 +24254,7 @@ function findings$2(input) {
|
|
|
24038
24254
|
});
|
|
24039
24255
|
}
|
|
24040
24256
|
for (const [index, pattern] of input.patterns.entries()) {
|
|
24041
|
-
if (result.length >= MAX_FINDINGS) break;
|
|
24257
|
+
if (result.length >= MAX_FINDINGS$2) break;
|
|
24042
24258
|
result.push({
|
|
24043
24259
|
code: "competitor-template-pattern",
|
|
24044
24260
|
evidenceRefs: [`repeatedCompetitorPatterns[${index}]`, ...pattern.evidenceRefs],
|
|
@@ -24461,7 +24677,7 @@ async function rankingPagesReport(input, dependencies = {}) {
|
|
|
24461
24677
|
impressions: row.impressions,
|
|
24462
24678
|
averagePosition: row.position
|
|
24463
24679
|
}] : [];
|
|
24464
|
-
}).sort((left, right) => right.impressions - left.impressions || compareText$
|
|
24680
|
+
}).sort((left, right) => right.impressions - left.impressions || compareText$7(left.url, right.url));
|
|
24465
24681
|
const firstPartyStatus = !input.site ? "not-requested" : (firstPartyResponse?.rowsFetched ?? 0) >= MAX_GSC_PAGE_ROWS ? "partial" : firstPartyResponse?.rows.length === 0 ? "empty" : "complete";
|
|
24466
24682
|
const findings = patterns.slice(0, 5).map((pattern, index) => ({
|
|
24467
24683
|
code: "repeated-ranking-page-pattern",
|
|
@@ -24542,7 +24758,7 @@ async function serpCompetitorsReport(input, dependencies = {}) {
|
|
|
24542
24758
|
const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
24543
24759
|
const market = validatedMarket(input.market);
|
|
24544
24760
|
const providerId = validatedProvider(input.provider);
|
|
24545
|
-
const keywords = [...new Set(input.keywords.map(normalizedKeyword$1))].filter(Boolean).sort(compareText$
|
|
24761
|
+
const keywords = [...new Set(input.keywords.map(normalizedKeyword$1))].filter(Boolean).sort(compareText$7);
|
|
24546
24762
|
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.");
|
|
24547
24763
|
const target = input.targetDomain ? normalizeDomain$1(input.targetDomain) : null;
|
|
24548
24764
|
const declared = declaredCompetitors(input.declaredCompetitors);
|
|
@@ -25085,7 +25301,7 @@ function measurementWindow(input) {
|
|
|
25085
25301
|
}
|
|
25086
25302
|
//#endregion
|
|
25087
25303
|
//#region packages/core/dist/analyze/experiments/measure-change.js
|
|
25088
|
-
const defaultDependencies$
|
|
25304
|
+
const defaultDependencies$6 = {
|
|
25089
25305
|
searchAnalytics: querySearchAnalytics,
|
|
25090
25306
|
googleAnalyticsReport: runGa4Report,
|
|
25091
25307
|
contentGroup: getContentGroup,
|
|
@@ -25103,7 +25319,7 @@ function counterfactualDelta(input) {
|
|
|
25103
25319
|
const expectedAfter = input.treatmentBefore * (input.controlAfter / input.controlBefore);
|
|
25104
25320
|
return fixed(input.treatmentAfter - expectedAfter);
|
|
25105
25321
|
}
|
|
25106
|
-
async function measureChange(input, dependencies = defaultDependencies$
|
|
25322
|
+
async function measureChange(input, dependencies = defaultDependencies$6) {
|
|
25107
25323
|
const now = dependencies.now();
|
|
25108
25324
|
const change = (input.id ? getChange(input.id) : void 0) ?? {
|
|
25109
25325
|
id: "adhoc",
|
|
@@ -25826,14 +26042,14 @@ async function verifyInternalLinkCandidate(input) {
|
|
|
25826
26042
|
}
|
|
25827
26043
|
//#endregion
|
|
25828
26044
|
//#region packages/core/dist/analyze/internal-links.js
|
|
25829
|
-
const MAX_GSC_ROWS$
|
|
25830
|
-
const defaultDependencies$
|
|
26045
|
+
const MAX_GSC_ROWS$2 = 1e5;
|
|
26046
|
+
const defaultDependencies$5 = {
|
|
25831
26047
|
searchAnalytics: querySearchAnalytics,
|
|
25832
26048
|
fetch: fetchPage,
|
|
25833
26049
|
extract: extractPage,
|
|
25834
26050
|
now: () => /* @__PURE__ */ new Date()
|
|
25835
26051
|
};
|
|
25836
|
-
async function internalLinksReport(input, dependencies = defaultDependencies$
|
|
26052
|
+
async function internalLinksReport(input, dependencies = defaultDependencies$5) {
|
|
25837
26053
|
const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
|
|
25838
26054
|
targetPage.hash = "";
|
|
25839
26055
|
const targetUrl = targetPage.toString();
|
|
@@ -25882,7 +26098,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
25882
26098
|
dimensions: ["query", "page"],
|
|
25883
26099
|
type: "web",
|
|
25884
26100
|
dataState: "final",
|
|
25885
|
-
maxRows: MAX_GSC_ROWS$
|
|
26101
|
+
maxRows: MAX_GSC_ROWS$2,
|
|
25886
26102
|
dimensionFilterGroups: [{
|
|
25887
26103
|
groupType: "and",
|
|
25888
26104
|
filters: [{
|
|
@@ -25914,7 +26130,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
25914
26130
|
dimensions: ["query", "page"],
|
|
25915
26131
|
type: "web",
|
|
25916
26132
|
dataState: "final",
|
|
25917
|
-
maxRows: MAX_GSC_ROWS$
|
|
26133
|
+
maxRows: MAX_GSC_ROWS$2
|
|
25918
26134
|
}, { refresh: input.refresh }) : {
|
|
25919
26135
|
rows: [],
|
|
25920
26136
|
calls: 0,
|
|
@@ -25959,8 +26175,8 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
25959
26175
|
else checkedSources += 1;
|
|
25960
26176
|
}
|
|
25961
26177
|
const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
|
|
25962
|
-
const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$
|
|
25963
|
-
const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$
|
|
26178
|
+
const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS$2);
|
|
26179
|
+
const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS$2;
|
|
25964
26180
|
const invalidRows = analysis.selection.targetInvalidRows + analysis.selection.sourceInvalidRows;
|
|
25965
26181
|
const structuredWarnings = uniqueInternalLinksWarnings(warnings);
|
|
25966
26182
|
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";
|
|
@@ -26001,18 +26217,18 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
26001
26217
|
pageFilter: alias,
|
|
26002
26218
|
rowsFetched: result.rowsFetched,
|
|
26003
26219
|
calls: result.calls,
|
|
26004
|
-
possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$
|
|
26220
|
+
possiblyTruncated: result.rowsFetched >= MAX_GSC_ROWS$2
|
|
26005
26221
|
})),
|
|
26006
26222
|
rowsFetched: targetRequests.reduce((sum, { result }) => sum + result.rowsFetched, 0),
|
|
26007
26223
|
calls: targetRequests.reduce((sum, { result }) => sum + result.calls, 0),
|
|
26008
|
-
maxRowsPerRequest: MAX_GSC_ROWS$
|
|
26224
|
+
maxRowsPerRequest: MAX_GSC_ROWS$2,
|
|
26009
26225
|
possiblyTruncated: targetTruncated
|
|
26010
26226
|
},
|
|
26011
26227
|
candidates: {
|
|
26012
26228
|
queried: queryCandidates,
|
|
26013
26229
|
rowsFetched: sourceResult.rowsFetched,
|
|
26014
26230
|
calls: sourceResult.calls,
|
|
26015
|
-
maxRows: MAX_GSC_ROWS$
|
|
26231
|
+
maxRows: MAX_GSC_ROWS$2,
|
|
26016
26232
|
possiblyTruncated: sourceTruncated
|
|
26017
26233
|
},
|
|
26018
26234
|
completeness: !queryCandidates ? "not-queried" : targetTruncated || sourceTruncated ? "possibly-truncated" : "retained-rows-only"
|
|
@@ -26031,7 +26247,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$4)
|
|
|
26031
26247
|
minImpressions,
|
|
26032
26248
|
resultLimit: limit,
|
|
26033
26249
|
checkLimit,
|
|
26034
|
-
maxGscRowsPerRequest: MAX_GSC_ROWS$
|
|
26250
|
+
maxGscRowsPerRequest: MAX_GSC_ROWS$2
|
|
26035
26251
|
},
|
|
26036
26252
|
target: {
|
|
26037
26253
|
requestedUrl: target.requestedUrl,
|
|
@@ -26321,7 +26537,7 @@ function providerReportError(error) {
|
|
|
26321
26537
|
if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
|
|
26322
26538
|
throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
26323
26539
|
}
|
|
26324
|
-
function validateInput$
|
|
26540
|
+
function validateInput$4(input) {
|
|
26325
26541
|
if (input.keywords.length < 1 || input.keywords.length > MAX_REPORT_KEYWORDS || input.keywords.some((keyword) => {
|
|
26326
26542
|
const value = keyword.trim();
|
|
26327
26543
|
return value.length < 1 || value.length > 80 || value.split(/\s+/u).length > 10;
|
|
@@ -26355,7 +26571,7 @@ function findings$1(analysis) {
|
|
|
26355
26571
|
}).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);
|
|
26356
26572
|
}
|
|
26357
26573
|
async function keywordMetricsReport(input, dependencies = {}) {
|
|
26358
|
-
const validated = validateInput$
|
|
26574
|
+
const validated = validateInput$4(input);
|
|
26359
26575
|
let candidates;
|
|
26360
26576
|
try {
|
|
26361
26577
|
candidates = dependencies.candidates ?? await defaultCandidates$2();
|
|
@@ -26650,16 +26866,16 @@ function benchmarkDetails(benchmark) {
|
|
|
26650
26866
|
positiveUrlSamples: benchmark.positiveUrlSamples
|
|
26651
26867
|
};
|
|
26652
26868
|
}
|
|
26653
|
-
function boundedInteger$
|
|
26869
|
+
function boundedInteger$3(input) {
|
|
26654
26870
|
const value = Number.isFinite(input.value) ? input.value ?? 0 : input.fallback;
|
|
26655
26871
|
return Math.min(input.maximum ?? Number.POSITIVE_INFINITY, Math.max(1, Math.floor(value)));
|
|
26656
26872
|
}
|
|
26657
26873
|
function analyzeQueryClustersFromRows(input) {
|
|
26658
|
-
const minImpressions = boundedInteger$
|
|
26874
|
+
const minImpressions = boundedInteger$3({
|
|
26659
26875
|
value: input.minImpressions,
|
|
26660
26876
|
fallback: 25
|
|
26661
26877
|
});
|
|
26662
|
-
const limit = boundedInteger$
|
|
26878
|
+
const limit = boundedInteger$3({
|
|
26663
26879
|
value: input.limit,
|
|
26664
26880
|
fallback: 25,
|
|
26665
26881
|
maximum: 100
|
|
@@ -26778,7 +26994,7 @@ function integerInRange(value, fallback, minimum, maximum) {
|
|
|
26778
26994
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
26779
26995
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
26780
26996
|
}
|
|
26781
|
-
function compareText$
|
|
26997
|
+
function compareText$5(left, right) {
|
|
26782
26998
|
const leftPoints = [...left].map((character) => character.codePointAt(0) ?? 0);
|
|
26783
26999
|
const rightPoints = [...right].map((character) => character.codePointAt(0) ?? 0);
|
|
26784
27000
|
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
@@ -26797,7 +27013,7 @@ function normalizedHttpUrl(value) {
|
|
|
26797
27013
|
return;
|
|
26798
27014
|
}
|
|
26799
27015
|
}
|
|
26800
|
-
function validRow(row, query, url) {
|
|
27016
|
+
function validRow$1(row, query, url) {
|
|
26801
27017
|
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;
|
|
26802
27018
|
}
|
|
26803
27019
|
function queryFromRow(row) {
|
|
@@ -26810,7 +27026,7 @@ function queryFromRow(row) {
|
|
|
26810
27026
|
};
|
|
26811
27027
|
}
|
|
26812
27028
|
function compareQueries(left, right) {
|
|
26813
|
-
return right.impressions - left.impressions || left.position - right.position || right.clicks - left.clicks || compareText$
|
|
27029
|
+
return right.impressions - left.impressions || left.position - right.position || right.clicks - left.clicks || compareText$5(left.query, right.query);
|
|
26814
27030
|
}
|
|
26815
27031
|
function recommendation(input) {
|
|
26816
27032
|
return {
|
|
@@ -26856,7 +27072,7 @@ function pageItem(url, rows) {
|
|
|
26856
27072
|
};
|
|
26857
27073
|
}
|
|
26858
27074
|
function compareItems$1(left, right) {
|
|
26859
|
-
return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$
|
|
27075
|
+
return right.priority.score - left.priority.score || right.impressions - left.impressions || left.position - right.position || compareText$5(left.url, right.url);
|
|
26860
27076
|
}
|
|
26861
27077
|
function analyzeSecondPageRows(input) {
|
|
26862
27078
|
const minImpressions = integerInRange(input.minImpressions, DEFAULT_MIN_IMPRESSIONS, 0, MAX_MIN_IMPRESSIONS);
|
|
@@ -26877,7 +27093,7 @@ function analyzeSecondPageRows(input) {
|
|
|
26877
27093
|
for (const row of input.rows) {
|
|
26878
27094
|
const query = row.keys[0]?.trim() ?? "";
|
|
26879
27095
|
const url = normalizedHttpUrl(row.keys[1]?.trim() ?? "");
|
|
26880
|
-
if (!validRow(row, query, url)) selection.invalidRows++;
|
|
27096
|
+
if (!validRow$1(row, query, url)) selection.invalidRows++;
|
|
26881
27097
|
else if (row.position <= 10 || row.position > 20) selection.outsidePositionRows++;
|
|
26882
27098
|
else if (shouldExcludeBrandQuery({
|
|
26883
27099
|
query,
|
|
@@ -26968,8 +27184,8 @@ function analyzeSecondPageRows(input) {
|
|
|
26968
27184
|
}
|
|
26969
27185
|
//#endregion
|
|
26970
27186
|
//#region packages/core/dist/analyze/keyword-opportunities.js
|
|
26971
|
-
const DEFAULT_DAYS = 28;
|
|
26972
|
-
const MAX_DAYS = 548;
|
|
27187
|
+
const DEFAULT_DAYS$1 = 28;
|
|
27188
|
+
const MAX_DAYS$2 = 548;
|
|
26973
27189
|
const DEFAULT_REPORT_LIMIT = 10;
|
|
26974
27190
|
const MAX_REPORT_LIMIT = 25;
|
|
26975
27191
|
const DEFAULT_KEYWORD_LIMIT = 30;
|
|
@@ -26986,7 +27202,7 @@ const SOURCE_ORDER = [
|
|
|
26986
27202
|
"second-page",
|
|
26987
27203
|
"striking-distance"
|
|
26988
27204
|
];
|
|
26989
|
-
function compareText$
|
|
27205
|
+
function compareText$4(left, right) {
|
|
26990
27206
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
26991
27207
|
}
|
|
26992
27208
|
function rounded(value, precision = 3) {
|
|
@@ -26998,7 +27214,7 @@ function integerOption$1(input) {
|
|
|
26998
27214
|
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}.`);
|
|
26999
27215
|
return input.value;
|
|
27000
27216
|
}
|
|
27001
|
-
function validateInput$
|
|
27217
|
+
function validateInput$3(input) {
|
|
27002
27218
|
const site = input.site.trim();
|
|
27003
27219
|
if (!site || site.length > 2048) throw new SeoError("INVALID_INPUT", "Use a valid Search Console property.");
|
|
27004
27220
|
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.");
|
|
@@ -27016,9 +27232,9 @@ function validateInput$2(input) {
|
|
|
27016
27232
|
site,
|
|
27017
27233
|
days: integerOption$1({
|
|
27018
27234
|
value: input.days,
|
|
27019
|
-
fallback: DEFAULT_DAYS,
|
|
27235
|
+
fallback: DEFAULT_DAYS$1,
|
|
27020
27236
|
minimum: 1,
|
|
27021
|
-
maximum: MAX_DAYS,
|
|
27237
|
+
maximum: MAX_DAYS$2,
|
|
27022
27238
|
label: "Days"
|
|
27023
27239
|
}),
|
|
27024
27240
|
limit: integerOption$1({
|
|
@@ -27130,7 +27346,7 @@ function aggregateCandidates(rows) {
|
|
|
27130
27346
|
impressions: rounded(row.impressions),
|
|
27131
27347
|
ctr: rounded(row.ctr, 4),
|
|
27132
27348
|
averagePosition: rounded(row.position, 2)
|
|
27133
|
-
})).sort((left, right) => right.impressions - left.impressions || left.averagePosition - right.averagePosition || compareText$
|
|
27349
|
+
})).sort((left, right) => right.impressions - left.impressions || left.averagePosition - right.averagePosition || compareText$4(left.url, right.url));
|
|
27134
27350
|
return [keyword, {
|
|
27135
27351
|
keyword,
|
|
27136
27352
|
clicks: rounded(clicks),
|
|
@@ -27364,7 +27580,7 @@ function nextSteps(input) {
|
|
|
27364
27580
|
return steps;
|
|
27365
27581
|
}
|
|
27366
27582
|
async function keywordOpportunitiesReport(input, dependencies = {}) {
|
|
27367
|
-
const validated = validateInput$
|
|
27583
|
+
const validated = validateInput$3(input);
|
|
27368
27584
|
const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
27369
27585
|
const generatedAt = now.toISOString();
|
|
27370
27586
|
const range = finalGscDateRange(validated.days, now);
|
|
@@ -27732,7 +27948,7 @@ function providerError$2(error) {
|
|
|
27732
27948
|
if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
|
|
27733
27949
|
throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
27734
27950
|
}
|
|
27735
|
-
function validateInput$
|
|
27951
|
+
function validateInput$2(input) {
|
|
27736
27952
|
if (input.seeds.length < 1 || input.seeds.length > MAX_RESEARCH_SEEDS || input.seeds.some((seed) => {
|
|
27737
27953
|
const value = seed.trim();
|
|
27738
27954
|
return value.length < 1 || value.length > 80 || value.split(/\s+/u).length > 10;
|
|
@@ -27785,7 +28001,7 @@ async function keywordResearchReport(input, dependencies = {}) {
|
|
|
27785
28001
|
"suggestions"
|
|
27786
28002
|
];
|
|
27787
28003
|
const limit = input.limit ?? 50;
|
|
27788
|
-
const validated = validateInput$
|
|
28004
|
+
const validated = validateInput$2({
|
|
27789
28005
|
...input,
|
|
27790
28006
|
sources,
|
|
27791
28007
|
limit
|
|
@@ -27868,6 +28084,861 @@ async function keywordResearchReport(input, dependencies = {}) {
|
|
|
27868
28084
|
};
|
|
27869
28085
|
}
|
|
27870
28086
|
//#endregion
|
|
28087
|
+
//#region packages/core/dist/analyze/local-search/intent.js
|
|
28088
|
+
const NEARBY_PHRASES = [
|
|
28089
|
+
"closest to me",
|
|
28090
|
+
"in my area",
|
|
28091
|
+
"near me",
|
|
28092
|
+
"nearby"
|
|
28093
|
+
];
|
|
28094
|
+
const UK_POSTCODE = /\b[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}\b/iu;
|
|
28095
|
+
const US_ZIP_CODE = /\b\d{5}(?:-\d{4})?\b/u;
|
|
28096
|
+
const US_ZIP_CONTEXT = /\b(?:around|near|nearby|postal(?:\s+code)?|zip(?:\s+code)?)\b/iu;
|
|
28097
|
+
function normalizeLocalSearchText(value) {
|
|
28098
|
+
return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase();
|
|
28099
|
+
}
|
|
28100
|
+
function tokens(value) {
|
|
28101
|
+
return normalizeLocalSearchText(value).match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
28102
|
+
}
|
|
28103
|
+
function includesTokenSequence(source, candidate) {
|
|
28104
|
+
if (candidate.length === 0 || candidate.length > source.length) return false;
|
|
28105
|
+
return source.some((_, start) => candidate.every((token, offset) => source[start + offset] === token));
|
|
28106
|
+
}
|
|
28107
|
+
function termMatcher(terms) {
|
|
28108
|
+
const root = {
|
|
28109
|
+
children: /* @__PURE__ */ new Map(),
|
|
28110
|
+
terms: []
|
|
28111
|
+
};
|
|
28112
|
+
for (const term of terms) {
|
|
28113
|
+
let node = root;
|
|
28114
|
+
for (const token of tokens(term)) {
|
|
28115
|
+
const child = node.children.get(token) ?? {
|
|
28116
|
+
children: /* @__PURE__ */ new Map(),
|
|
28117
|
+
terms: []
|
|
28118
|
+
};
|
|
28119
|
+
node.children.set(token, child);
|
|
28120
|
+
node = child;
|
|
28121
|
+
}
|
|
28122
|
+
node.terms.push(term);
|
|
28123
|
+
}
|
|
28124
|
+
return (source) => {
|
|
28125
|
+
const matches = /* @__PURE__ */ new Set();
|
|
28126
|
+
for (let start = 0; start < source.length; start++) {
|
|
28127
|
+
let node = root;
|
|
28128
|
+
for (let index = start; index < source.length; index++) {
|
|
28129
|
+
node = node.children.get(source[index]);
|
|
28130
|
+
if (!node) break;
|
|
28131
|
+
for (const term of node.terms) matches.add(term);
|
|
28132
|
+
}
|
|
28133
|
+
}
|
|
28134
|
+
return [...matches].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
28135
|
+
};
|
|
28136
|
+
}
|
|
28137
|
+
function normalizeLocationTerms(values = []) {
|
|
28138
|
+
return [...new Set(values.map(normalizeLocalSearchText).filter(Boolean))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
28139
|
+
}
|
|
28140
|
+
function createLocalIntentClassifier(locationTerms) {
|
|
28141
|
+
const matchLocations = termMatcher(locationTerms);
|
|
28142
|
+
return (query) => {
|
|
28143
|
+
const queryTokens = tokens(normalizeLocalSearchText(query));
|
|
28144
|
+
const matchedLocations = matchLocations(queryTokens);
|
|
28145
|
+
const matchedNearby = NEARBY_PHRASES.filter((phrase) => includesTokenSequence(queryTokens, tokens(phrase)));
|
|
28146
|
+
const ukPostcode = UK_POSTCODE.exec(query)?.[0] ?? null;
|
|
28147
|
+
const usZipCode = US_ZIP_CONTEXT.test(query) && US_ZIP_CODE.test(query) ? US_ZIP_CODE.exec(query)?.[0] ?? null : null;
|
|
28148
|
+
const postalCode = ukPostcode ?? usZipCode;
|
|
28149
|
+
const classes = [];
|
|
28150
|
+
if (matchedLocations.length > 0) classes.push("named-location");
|
|
28151
|
+
if (matchedNearby.length > 0) classes.push("nearby");
|
|
28152
|
+
if (postalCode) classes.push("postal-code");
|
|
28153
|
+
if (classes.length === 0) return null;
|
|
28154
|
+
return {
|
|
28155
|
+
heuristic: true,
|
|
28156
|
+
method: "explicit-local-intent-v1",
|
|
28157
|
+
classes,
|
|
28158
|
+
matchedTerms: [
|
|
28159
|
+
...matchedLocations,
|
|
28160
|
+
...matchedNearby,
|
|
28161
|
+
...postalCode ? [normalizeLocalSearchText(postalCode)] : []
|
|
28162
|
+
]
|
|
28163
|
+
};
|
|
28164
|
+
};
|
|
28165
|
+
}
|
|
28166
|
+
//#endregion
|
|
28167
|
+
//#region packages/core/dist/analyze/local-search/analysis.js
|
|
28168
|
+
function compareText$3(left, right) {
|
|
28169
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
28170
|
+
}
|
|
28171
|
+
function validRow(row) {
|
|
28172
|
+
const query = row.keys[0]?.trim() ?? "";
|
|
28173
|
+
const page = row.keys[1]?.trim() ?? "";
|
|
28174
|
+
let validPage = false;
|
|
28175
|
+
try {
|
|
28176
|
+
validPage = ["http:", "https:"].includes(new URL(page).protocol);
|
|
28177
|
+
} catch {
|
|
28178
|
+
validPage = false;
|
|
28179
|
+
}
|
|
28180
|
+
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;
|
|
28181
|
+
return {
|
|
28182
|
+
query,
|
|
28183
|
+
page,
|
|
28184
|
+
clicks: row.clicks,
|
|
28185
|
+
impressions: row.impressions,
|
|
28186
|
+
ctr: row.ctr,
|
|
28187
|
+
position: row.position
|
|
28188
|
+
};
|
|
28189
|
+
}
|
|
28190
|
+
function rowSignature(row) {
|
|
28191
|
+
return `${row.clicks}\u0000${row.impressions}\u0000${row.ctr}\u0000${row.position}`;
|
|
28192
|
+
}
|
|
28193
|
+
function normalizeRows(rows, selection) {
|
|
28194
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
28195
|
+
for (const source of rows) {
|
|
28196
|
+
const row = validRow(source);
|
|
28197
|
+
if (!row) {
|
|
28198
|
+
selection.invalidRows++;
|
|
28199
|
+
continue;
|
|
28200
|
+
}
|
|
28201
|
+
const key = `${row.query}\u0000${row.page}`;
|
|
28202
|
+
grouped.set(key, [...grouped.get(key) ?? [], row]);
|
|
28203
|
+
}
|
|
28204
|
+
const result = [];
|
|
28205
|
+
for (const group of grouped.values()) {
|
|
28206
|
+
if (new Set(group.map(rowSignature)).size > 1) {
|
|
28207
|
+
selection.conflictingRows += group.length;
|
|
28208
|
+
continue;
|
|
28209
|
+
}
|
|
28210
|
+
selection.exactDuplicateRows += group.length - 1;
|
|
28211
|
+
const row = group[0];
|
|
28212
|
+
if (row) result.push(row);
|
|
28213
|
+
}
|
|
28214
|
+
return result;
|
|
28215
|
+
}
|
|
28216
|
+
function opportunityAction(position, pageCount) {
|
|
28217
|
+
if (pageCount > 1) return "review-page-overlap";
|
|
28218
|
+
if (position <= 3) return "protect-visibility";
|
|
28219
|
+
if (position <= 20) return "improve-existing-page";
|
|
28220
|
+
return "investigate-relevance";
|
|
28221
|
+
}
|
|
28222
|
+
function aggregateQueries(input) {
|
|
28223
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
28224
|
+
for (const row of input.rows) {
|
|
28225
|
+
if (isLowActionabilityQuery(row.query)) {
|
|
28226
|
+
input.selection.lowActionabilityRows++;
|
|
28227
|
+
continue;
|
|
28228
|
+
}
|
|
28229
|
+
if (shouldExcludeBrandQuery({
|
|
28230
|
+
query: row.query,
|
|
28231
|
+
siteUrl: input.site,
|
|
28232
|
+
brandTerms: input.brandTerms,
|
|
28233
|
+
includeBrand: input.includeBrand
|
|
28234
|
+
})) {
|
|
28235
|
+
input.selection.brandRows++;
|
|
28236
|
+
continue;
|
|
28237
|
+
}
|
|
28238
|
+
grouped.set(row.query, [...grouped.get(row.query) ?? [], row]);
|
|
28239
|
+
}
|
|
28240
|
+
const opportunities = [];
|
|
28241
|
+
for (const [query, rows] of grouped) {
|
|
28242
|
+
const intent = input.classify(query);
|
|
28243
|
+
if (!intent) {
|
|
28244
|
+
input.selection.nonLocalRows += rows.length;
|
|
28245
|
+
continue;
|
|
28246
|
+
}
|
|
28247
|
+
const impressions = rows.reduce((sum, row) => sum + row.impressions, 0);
|
|
28248
|
+
if (impressions < input.minImpressions) {
|
|
28249
|
+
input.selection.belowMinimumRows += rows.length;
|
|
28250
|
+
continue;
|
|
28251
|
+
}
|
|
28252
|
+
const clicks = rows.reduce((sum, row) => sum + row.clicks, 0);
|
|
28253
|
+
const averagePosition = rows.reduce((sum, row) => sum + row.position * row.impressions, 0) / impressions;
|
|
28254
|
+
const pages = rows.map((row) => ({
|
|
28255
|
+
url: row.page,
|
|
28256
|
+
clicks: row.clicks,
|
|
28257
|
+
impressions: row.impressions,
|
|
28258
|
+
ctr: row.impressions ? row.clicks / row.impressions : 0,
|
|
28259
|
+
averagePosition: row.position
|
|
28260
|
+
})).sort((left, right) => right.impressions - left.impressions || right.clicks - left.clicks || left.averagePosition - right.averagePosition || compareText$3(left.url, right.url));
|
|
28261
|
+
opportunities.push({
|
|
28262
|
+
query,
|
|
28263
|
+
clicks,
|
|
28264
|
+
impressions,
|
|
28265
|
+
ctr: impressions ? clicks / impressions : 0,
|
|
28266
|
+
averagePosition,
|
|
28267
|
+
intent,
|
|
28268
|
+
action: opportunityAction(averagePosition, pages.length),
|
|
28269
|
+
pages,
|
|
28270
|
+
pageCoverage: {
|
|
28271
|
+
available: pages.length,
|
|
28272
|
+
returned: Math.min(3, pages.length),
|
|
28273
|
+
omitted: Math.max(0, pages.length - 3)
|
|
28274
|
+
}
|
|
28275
|
+
});
|
|
28276
|
+
}
|
|
28277
|
+
return opportunities.sort((left, right) => right.impressions - left.impressions || right.clicks - left.clicks || left.averagePosition - right.averagePosition || compareText$3(left.query, right.query));
|
|
28278
|
+
}
|
|
28279
|
+
function localTemplates(opportunities) {
|
|
28280
|
+
const clusters = clusterPseoTemplates(opportunities.flatMap((item) => item.pages.map((page) => page.url)), {
|
|
28281
|
+
minUrls: 2,
|
|
28282
|
+
minShare: 0,
|
|
28283
|
+
limit: 10,
|
|
28284
|
+
sampleSize: 5
|
|
28285
|
+
});
|
|
28286
|
+
return clusters.map((cluster) => {
|
|
28287
|
+
const matched = opportunities.flatMap((item) => item.pages.filter((page) => templateForUrl(page.url, clusters) === cluster.signature).map((page) => ({
|
|
28288
|
+
query: item.query,
|
|
28289
|
+
page
|
|
28290
|
+
})));
|
|
28291
|
+
return {
|
|
28292
|
+
heuristic: true,
|
|
28293
|
+
signature: cluster.signature,
|
|
28294
|
+
urlCount: cluster.urlCount,
|
|
28295
|
+
sampleUrls: cluster.sampleUrls,
|
|
28296
|
+
queryCount: new Set(matched.map((item) => item.query)).size,
|
|
28297
|
+
clicks: matched.reduce((sum, item) => sum + item.page.clicks, 0),
|
|
28298
|
+
impressions: matched.reduce((sum, item) => sum + item.page.impressions, 0)
|
|
28299
|
+
};
|
|
28300
|
+
});
|
|
28301
|
+
}
|
|
28302
|
+
function analyzeLocalSearchRows(input) {
|
|
28303
|
+
const selection = {
|
|
28304
|
+
sourceRows: input.rows.length,
|
|
28305
|
+
invalidRows: 0,
|
|
28306
|
+
exactDuplicateRows: 0,
|
|
28307
|
+
conflictingRows: 0,
|
|
28308
|
+
lowActionabilityRows: 0,
|
|
28309
|
+
brandRows: 0,
|
|
28310
|
+
nonLocalRows: 0,
|
|
28311
|
+
belowMinimumRows: 0,
|
|
28312
|
+
eligibleQueries: 0,
|
|
28313
|
+
returnedQueries: 0,
|
|
28314
|
+
omittedQueries: 0,
|
|
28315
|
+
limit: input.limit,
|
|
28316
|
+
minImpressions: input.minImpressions
|
|
28317
|
+
};
|
|
28318
|
+
const normalized = normalizeRows(input.rows, selection);
|
|
28319
|
+
const eligible = aggregateQueries({
|
|
28320
|
+
...input,
|
|
28321
|
+
rows: normalized,
|
|
28322
|
+
selection,
|
|
28323
|
+
classify: createLocalIntentClassifier(input.locationTerms)
|
|
28324
|
+
});
|
|
28325
|
+
selection.eligibleQueries = eligible.length;
|
|
28326
|
+
selection.returnedQueries = Math.min(input.limit, eligible.length);
|
|
28327
|
+
selection.omittedQueries = Math.max(0, eligible.length - input.limit);
|
|
28328
|
+
return {
|
|
28329
|
+
selection,
|
|
28330
|
+
eligibleSummary: {
|
|
28331
|
+
clicks: eligible.reduce((sum, item) => sum + item.clicks, 0),
|
|
28332
|
+
impressions: eligible.reduce((sum, item) => sum + item.impressions, 0),
|
|
28333
|
+
namedLocationQueries: eligible.filter((item) => item.intent.classes.includes("named-location")).length,
|
|
28334
|
+
nearbyQueries: eligible.filter((item) => item.intent.classes.includes("nearby")).length,
|
|
28335
|
+
postalCodeQueries: eligible.filter((item) => item.intent.classes.includes("postal-code")).length,
|
|
28336
|
+
pageOverlapQueries: eligible.filter((item) => item.action === "review-page-overlap").length
|
|
28337
|
+
},
|
|
28338
|
+
eligiblePageCount: new Set(eligible.flatMap((item) => item.pages.map((page) => page.url))).size,
|
|
28339
|
+
opportunities: eligible.slice(0, input.limit).map((item) => ({
|
|
28340
|
+
...item,
|
|
28341
|
+
pages: item.pages.slice(0, 3)
|
|
28342
|
+
})),
|
|
28343
|
+
templates: localTemplates(eligible)
|
|
28344
|
+
};
|
|
28345
|
+
}
|
|
28346
|
+
//#endregion
|
|
28347
|
+
//#region packages/core/dist/providers/dataforseo/serp-snapshot.js
|
|
28348
|
+
const MAX_SERP_DEPTH = 100;
|
|
28349
|
+
const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
|
|
28350
|
+
function safeUrl$1(value) {
|
|
28351
|
+
if (!value) return null;
|
|
28352
|
+
try {
|
|
28353
|
+
const parsed = new URL(value);
|
|
28354
|
+
if (!["http:", "https:"].includes(parsed.protocol)) return null;
|
|
28355
|
+
parsed.username = "";
|
|
28356
|
+
parsed.password = "";
|
|
28357
|
+
return parsed.toString();
|
|
28358
|
+
} catch {
|
|
28359
|
+
return null;
|
|
28360
|
+
}
|
|
28361
|
+
}
|
|
28362
|
+
function validOrganic(item) {
|
|
28363
|
+
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;
|
|
28364
|
+
}
|
|
28365
|
+
function organicResult(item) {
|
|
28366
|
+
if (!validOrganic(item)) return null;
|
|
28367
|
+
const url = safeUrl$1(item.url);
|
|
28368
|
+
if (!url) return null;
|
|
28369
|
+
return {
|
|
28370
|
+
rankGroup: item.rank_group,
|
|
28371
|
+
rankAbsolute: item.rank_absolute,
|
|
28372
|
+
page: item.page,
|
|
28373
|
+
domain: item.domain.trim().toLowerCase(),
|
|
28374
|
+
url,
|
|
28375
|
+
title: item.title ?? null,
|
|
28376
|
+
description: item.description ?? null,
|
|
28377
|
+
isFeaturedSnippet: item.is_featured_snippet ?? null
|
|
28378
|
+
};
|
|
28379
|
+
}
|
|
28380
|
+
function checkedAt(value, fallback) {
|
|
28381
|
+
const timestamp = Date.parse(value);
|
|
28382
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
|
|
28383
|
+
}
|
|
28384
|
+
function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
|
|
28385
|
+
const market = searchMarketSchema.parse(input.market);
|
|
28386
|
+
const keyword = normalizedKeyword$2(input.keyword);
|
|
28387
|
+
const location = locationRequest(market, "serp-snapshot");
|
|
28388
|
+
const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$2(item.keyword) === keyword);
|
|
28389
|
+
if (!result) throw new ProviderError({
|
|
28390
|
+
provider: "dataforseo",
|
|
28391
|
+
operation: "serp-snapshot",
|
|
28392
|
+
code: "invalid-response",
|
|
28393
|
+
message: "DataForSEO returned no matching SERP result."
|
|
28394
|
+
});
|
|
28395
|
+
const warnings = [...snapshot.warnings];
|
|
28396
|
+
const items = result.items ?? [];
|
|
28397
|
+
const organicItems = items.filter((item) => item.type === "organic");
|
|
28398
|
+
const mappedOrganicResults = organicItems.flatMap((item) => {
|
|
28399
|
+
const mapped = organicResult(item);
|
|
28400
|
+
return mapped ? [mapped] : [];
|
|
28401
|
+
}).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
|
|
28402
|
+
const organicResults = mappedOrganicResults.slice(0, input.depth);
|
|
28403
|
+
const invalidRows = organicItems.length - mappedOrganicResults.length;
|
|
28404
|
+
const resultCapped = mappedOrganicResults.length > input.depth;
|
|
28405
|
+
const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
|
|
28406
|
+
if (invalidRows > 0) warnings.push({
|
|
28407
|
+
code: "invalid-organic-results",
|
|
28408
|
+
field: "organicResults",
|
|
28409
|
+
message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
|
|
28410
|
+
});
|
|
28411
|
+
if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
|
|
28412
|
+
code: "invalid-serp-observation-time",
|
|
28413
|
+
field: "checkedAt",
|
|
28414
|
+
message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
|
|
28415
|
+
});
|
|
28416
|
+
const checkUrl = safeUrl$1(result.check_url);
|
|
28417
|
+
if (result.check_url && !checkUrl) warnings.push({
|
|
28418
|
+
code: "invalid-serp-check-url",
|
|
28419
|
+
field: "checkUrl",
|
|
28420
|
+
message: "DataForSEO returned an invalid SERP check URL."
|
|
28421
|
+
});
|
|
28422
|
+
if (invalidResultCount) warnings.push({
|
|
28423
|
+
code: "invalid-serp-result-count",
|
|
28424
|
+
field: "resultCount",
|
|
28425
|
+
message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
|
|
28426
|
+
});
|
|
28427
|
+
const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
|
|
28428
|
+
return {
|
|
28429
|
+
schemaVersion: 1,
|
|
28430
|
+
provider: "dataforseo",
|
|
28431
|
+
capability: "serp-snapshot",
|
|
28432
|
+
data: {
|
|
28433
|
+
keyword,
|
|
28434
|
+
effectiveKeyword: normalizedKeyword$2(result.spell?.keyword ?? result.keyword),
|
|
28435
|
+
searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
|
|
28436
|
+
checkedAt: checkedAt(result.datetime, snapshot.observedAt),
|
|
28437
|
+
checkUrl,
|
|
28438
|
+
resultCount: invalidResultCount ? null : result.se_results_count ?? null,
|
|
28439
|
+
pagesCount: result.pages_count === void 0 ? null : result.pages_count,
|
|
28440
|
+
features,
|
|
28441
|
+
organicResults
|
|
28442
|
+
},
|
|
28443
|
+
observedAt: snapshot.observedAt,
|
|
28444
|
+
market: {
|
|
28445
|
+
...market,
|
|
28446
|
+
device: market.device ?? "desktop"
|
|
28447
|
+
},
|
|
28448
|
+
coverage: {
|
|
28449
|
+
requestedRows: input.depth,
|
|
28450
|
+
returnedRows: snapshot.returnedRows,
|
|
28451
|
+
retainedRows: organicResults.length,
|
|
28452
|
+
invalidRows,
|
|
28453
|
+
providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
|
|
28454
|
+
completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
|
|
28455
|
+
nextCursor: null
|
|
28456
|
+
},
|
|
28457
|
+
cache: snapshot.cache,
|
|
28458
|
+
cost: snapshot.cost,
|
|
28459
|
+
request: {
|
|
28460
|
+
operation: "serp-snapshot",
|
|
28461
|
+
endpoint,
|
|
28462
|
+
limit: input.depth,
|
|
28463
|
+
filters: {
|
|
28464
|
+
countryCode: market.countryCode,
|
|
28465
|
+
languageCode: market.languageCode,
|
|
28466
|
+
device: market.device ?? "desktop",
|
|
28467
|
+
...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
|
|
28468
|
+
},
|
|
28469
|
+
sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
|
|
28470
|
+
},
|
|
28471
|
+
warnings
|
|
28472
|
+
};
|
|
28473
|
+
}
|
|
28474
|
+
var DataForSeoSerpSnapshotProvider = class {
|
|
28475
|
+
provider = "dataforseo";
|
|
28476
|
+
capabilitySupport = [{
|
|
28477
|
+
capability: "serp-snapshot",
|
|
28478
|
+
status: "available",
|
|
28479
|
+
markets: [{
|
|
28480
|
+
searchEngines: ["google"],
|
|
28481
|
+
devices: ["desktop", "mobile"],
|
|
28482
|
+
location: "any"
|
|
28483
|
+
}]
|
|
28484
|
+
}];
|
|
28485
|
+
client;
|
|
28486
|
+
constructor(options = {}) {
|
|
28487
|
+
this.client = options.client ?? new DataForSeoClient(options);
|
|
28488
|
+
}
|
|
28489
|
+
async serpSnapshot(input) {
|
|
28490
|
+
const market = searchMarketSchema.parse(input.market);
|
|
28491
|
+
if (market.searchEngine !== "google") throw new ProviderError({
|
|
28492
|
+
provider: "dataforseo",
|
|
28493
|
+
operation: "serp-snapshot",
|
|
28494
|
+
code: "configuration",
|
|
28495
|
+
message: "DataForSEO SERP snapshots currently support Google."
|
|
28496
|
+
});
|
|
28497
|
+
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH) throw new ProviderError({
|
|
28498
|
+
provider: "dataforseo",
|
|
28499
|
+
operation: "serp-snapshot",
|
|
28500
|
+
code: "configuration",
|
|
28501
|
+
message: "SERP depth must be from 1 to 100."
|
|
28502
|
+
});
|
|
28503
|
+
const keyword = normalizedKeyword$2(input.keyword);
|
|
28504
|
+
if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new ProviderError({
|
|
28505
|
+
provider: "dataforseo",
|
|
28506
|
+
operation: "serp-snapshot",
|
|
28507
|
+
code: "configuration",
|
|
28508
|
+
message: "SERP snapshots require a keyword of at most 80 characters and 10 words."
|
|
28509
|
+
});
|
|
28510
|
+
const context = input.context ?? {
|
|
28511
|
+
reportId: "serp-results",
|
|
28512
|
+
reportRunId: randomUUID()
|
|
28513
|
+
};
|
|
28514
|
+
const location = locationRequest(market, "serp-snapshot");
|
|
28515
|
+
return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
|
|
28516
|
+
keyword,
|
|
28517
|
+
languageCode: market.languageCode.split("-")[0],
|
|
28518
|
+
...location,
|
|
28519
|
+
device: market.device ?? "desktop",
|
|
28520
|
+
depth: input.depth,
|
|
28521
|
+
refresh: input.refresh,
|
|
28522
|
+
context
|
|
28523
|
+
}));
|
|
28524
|
+
}
|
|
28525
|
+
};
|
|
28526
|
+
//#endregion
|
|
28527
|
+
//#region packages/core/dist/analyze/serp-results.js
|
|
28528
|
+
const MAX_REPORT_DEPTH = 100;
|
|
28529
|
+
function compareCodepoints(left, right) {
|
|
28530
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
28531
|
+
}
|
|
28532
|
+
function snapshotProvider(candidate) {
|
|
28533
|
+
return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
|
|
28534
|
+
}
|
|
28535
|
+
async function defaultCandidates() {
|
|
28536
|
+
return [{
|
|
28537
|
+
adapter: new DataForSeoSerpSnapshotProvider(),
|
|
28538
|
+
connected: Boolean(await readDataForSeoCredentials()),
|
|
28539
|
+
priority: 10
|
|
28540
|
+
}];
|
|
28541
|
+
}
|
|
28542
|
+
function providerError$1(error) {
|
|
28543
|
+
if (!(error instanceof ProviderError)) throw error;
|
|
28544
|
+
if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
|
|
28545
|
+
if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
|
|
28546
|
+
throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
28547
|
+
}
|
|
28548
|
+
function validateInput$1(input) {
|
|
28549
|
+
const keyword = input.keyword.trim().replace(/\s+/gu, " ");
|
|
28550
|
+
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.");
|
|
28551
|
+
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}.`);
|
|
28552
|
+
const market = searchMarketSchema.safeParse(input.market);
|
|
28553
|
+
if (!market.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
|
|
28554
|
+
const provider = input.provider ? providerIdSchema.safeParse(input.provider) : void 0;
|
|
28555
|
+
if (provider && !provider.success) throw new SeoError("INVALID_INPUT", "Use a supported SERP provider.");
|
|
28556
|
+
return {
|
|
28557
|
+
keyword,
|
|
28558
|
+
market: market.data,
|
|
28559
|
+
provider: provider?.data
|
|
28560
|
+
};
|
|
28561
|
+
}
|
|
28562
|
+
function reportStatus$1(evidence) {
|
|
28563
|
+
if (evidence.coverage.completeness === "unavailable" || evidence.coverage.completeness === "invalid") return "unavailable";
|
|
28564
|
+
return evidence.coverage.completeness === "complete" ? "complete" : "partial";
|
|
28565
|
+
}
|
|
28566
|
+
function domainSummaries(snapshot) {
|
|
28567
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
28568
|
+
for (const result of snapshot.organicResults) byDomain.set(result.domain, [...byDomain.get(result.domain) ?? [], result.rankAbsolute]);
|
|
28569
|
+
return [...byDomain.entries()].map(([domain, ranks]) => ({
|
|
28570
|
+
domain,
|
|
28571
|
+
resultCount: ranks.length,
|
|
28572
|
+
ranks: [...ranks].sort((left, right) => left - right)
|
|
28573
|
+
})).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));
|
|
28574
|
+
}
|
|
28575
|
+
function reportFindings(snapshot, domains) {
|
|
28576
|
+
const findings = [];
|
|
28577
|
+
if (snapshot.effectiveKeyword !== snapshot.keyword) findings.push({
|
|
28578
|
+
code: "query-correction",
|
|
28579
|
+
evidenceRef: "evidence.data.effectiveKeyword",
|
|
28580
|
+
principle: "A corrected query changes the observed result context and does not prove the original wording has the same intent.",
|
|
28581
|
+
detail: `The provider observed results for ${snapshot.effectiveKeyword} after the requested query ${snapshot.keyword}.`
|
|
28582
|
+
});
|
|
28583
|
+
for (const [index, domain] of domains.entries()) {
|
|
28584
|
+
if (domain.resultCount <= 1) continue;
|
|
28585
|
+
findings.push({
|
|
28586
|
+
code: "repeated-domain",
|
|
28587
|
+
evidenceRef: `domains[${index}]`,
|
|
28588
|
+
principle: "Repeated results are descriptive snapshot evidence, not a domain strength or quality score.",
|
|
28589
|
+
detail: `${domain.domain} appears ${domain.resultCount} times at absolute ranks ${domain.ranks.join(", ")}.`
|
|
28590
|
+
});
|
|
28591
|
+
}
|
|
28592
|
+
return findings.slice(0, 10);
|
|
28593
|
+
}
|
|
28594
|
+
async function serpResultsReport(input, dependencies = {}) {
|
|
28595
|
+
const depth = input.depth ?? 10;
|
|
28596
|
+
const validated = validateInput$1({
|
|
28597
|
+
...input,
|
|
28598
|
+
depth
|
|
28599
|
+
});
|
|
28600
|
+
let candidates;
|
|
28601
|
+
try {
|
|
28602
|
+
candidates = dependencies.candidates ?? await defaultCandidates();
|
|
28603
|
+
} catch (error) {
|
|
28604
|
+
return providerError$1(error);
|
|
28605
|
+
}
|
|
28606
|
+
const resolution = resolveProvider({
|
|
28607
|
+
capability: "serp-snapshot",
|
|
28608
|
+
market: validated.market,
|
|
28609
|
+
candidates,
|
|
28610
|
+
provider: validated.provider
|
|
28611
|
+
});
|
|
28612
|
+
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.");
|
|
28613
|
+
const provider = snapshotProvider(resolution.provider);
|
|
28614
|
+
if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no live SERP implementation.");
|
|
28615
|
+
let evidence;
|
|
28616
|
+
try {
|
|
28617
|
+
evidence = await provider.serpSnapshot({
|
|
28618
|
+
keyword: validated.keyword,
|
|
28619
|
+
market: validated.market,
|
|
28620
|
+
depth,
|
|
28621
|
+
refresh: input.refresh,
|
|
28622
|
+
context: {
|
|
28623
|
+
projectId: input.context?.projectId ?? input.projectId,
|
|
28624
|
+
reportId: input.context?.reportId ?? "serp-results",
|
|
28625
|
+
reportRunId: input.context?.reportRunId ?? randomUUID()
|
|
28626
|
+
}
|
|
28627
|
+
});
|
|
28628
|
+
} catch (error) {
|
|
28629
|
+
return providerError$1(error);
|
|
28630
|
+
}
|
|
28631
|
+
const domains = domainSummaries(evidence.data);
|
|
28632
|
+
const dataStatus = reportStatus$1(evidence);
|
|
28633
|
+
const generatedAt = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
28634
|
+
const correctedQuery = evidence.data.effectiveKeyword !== evidence.data.keyword;
|
|
28635
|
+
return {
|
|
28636
|
+
schemaVersion: 1,
|
|
28637
|
+
generatedAt,
|
|
28638
|
+
dataStatus,
|
|
28639
|
+
market: evidence.market,
|
|
28640
|
+
summary: {
|
|
28641
|
+
keyword: evidence.data.keyword,
|
|
28642
|
+
effectiveKeyword: evidence.data.effectiveKeyword,
|
|
28643
|
+
requestedDepth: depth,
|
|
28644
|
+
organicResults: evidence.data.organicResults.length,
|
|
28645
|
+
uniqueDomains: domains.length,
|
|
28646
|
+
observedFeatures: evidence.data.features.length,
|
|
28647
|
+
correctedQuery,
|
|
28648
|
+
verdict: `${evidence.data.organicResults.length} organic results across ${domains.length} domains were retained from the ${evidence.market.device ?? "desktop"} snapshot.`
|
|
28649
|
+
},
|
|
28650
|
+
evidence,
|
|
28651
|
+
domains,
|
|
28652
|
+
findings: reportFindings(evidence.data, domains),
|
|
28653
|
+
caveats: [
|
|
28654
|
+
"This is one market, location, language, and device-specific result snapshot; it is not rank history.",
|
|
28655
|
+
"Search results can change between checks and may differ from a person's signed-in or personalized results.",
|
|
28656
|
+
"The provider result count is an estimate and may be missing; retained organic rows are bounded by the requested depth.",
|
|
28657
|
+
"Repeated domains, result features, titles, and snippets do not establish content quality, authority, or ranking feasibility."
|
|
28658
|
+
],
|
|
28659
|
+
nextSteps: [
|
|
28660
|
+
"Review the retained pages and result types before deciding whether the query fits an existing page, a new page, or no page.",
|
|
28661
|
+
"Keep Search Console average position separate from these exact snapshot ranks when comparing first-party performance.",
|
|
28662
|
+
"Repeat the same market and device check later if the decision depends on result stability."
|
|
28663
|
+
]
|
|
28664
|
+
};
|
|
28665
|
+
}
|
|
28666
|
+
//#endregion
|
|
28667
|
+
//#region packages/core/dist/analyze/local-search/report.js
|
|
28668
|
+
const MAX_DAYS$1 = 548;
|
|
28669
|
+
const MAX_ROWS = 5e4;
|
|
28670
|
+
function boundedInteger$2(input) {
|
|
28671
|
+
const value = input.value ?? input.fallback;
|
|
28672
|
+
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}.`);
|
|
28673
|
+
return value;
|
|
28674
|
+
}
|
|
28675
|
+
function validateInput(input) {
|
|
28676
|
+
const site = input.site.trim();
|
|
28677
|
+
if (!site) throw new SeoError("INVALID_INPUT", "site must not be empty.");
|
|
28678
|
+
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.");
|
|
28679
|
+
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.");
|
|
28680
|
+
const includeSerps = input.includeSerps ?? false;
|
|
28681
|
+
let market;
|
|
28682
|
+
if (input.market) {
|
|
28683
|
+
const parsed = searchMarketSchema.safeParse(input.market);
|
|
28684
|
+
if (!parsed.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
|
|
28685
|
+
market = parsed.data;
|
|
28686
|
+
}
|
|
28687
|
+
if (includeSerps && !market?.location) throw new SeoError("INVALID_INPUT", "Local SERP evidence requires a country, language, and canonical location.");
|
|
28688
|
+
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.");
|
|
28689
|
+
return {
|
|
28690
|
+
site,
|
|
28691
|
+
days: boundedInteger$2({
|
|
28692
|
+
value: input.days,
|
|
28693
|
+
fallback: 28,
|
|
28694
|
+
minimum: 1,
|
|
28695
|
+
maximum: MAX_DAYS$1,
|
|
28696
|
+
label: "days"
|
|
28697
|
+
}),
|
|
28698
|
+
locationTerms: normalizeLocationTerms(input.locationTerms),
|
|
28699
|
+
minImpressions: boundedInteger$2({
|
|
28700
|
+
value: input.minImpressions,
|
|
28701
|
+
fallback: 1,
|
|
28702
|
+
minimum: 0,
|
|
28703
|
+
maximum: 1e9,
|
|
28704
|
+
label: "minImpressions"
|
|
28705
|
+
}),
|
|
28706
|
+
limit: boundedInteger$2({
|
|
28707
|
+
value: input.limit,
|
|
28708
|
+
fallback: 25,
|
|
28709
|
+
minimum: 1,
|
|
28710
|
+
maximum: 100,
|
|
28711
|
+
label: "limit"
|
|
28712
|
+
}),
|
|
28713
|
+
maxRows: boundedInteger$2({
|
|
28714
|
+
value: input.maxRows,
|
|
28715
|
+
fallback: MAX_ROWS,
|
|
28716
|
+
minimum: 1,
|
|
28717
|
+
maximum: MAX_ROWS,
|
|
28718
|
+
label: "maxRows"
|
|
28719
|
+
}),
|
|
28720
|
+
includeSerps,
|
|
28721
|
+
market,
|
|
28722
|
+
serpLimit: boundedInteger$2({
|
|
28723
|
+
value: input.serpLimit,
|
|
28724
|
+
fallback: 3,
|
|
28725
|
+
minimum: 1,
|
|
28726
|
+
maximum: 3,
|
|
28727
|
+
label: "serpLimit"
|
|
28728
|
+
}),
|
|
28729
|
+
serpDepth: boundedInteger$2({
|
|
28730
|
+
value: input.serpDepth,
|
|
28731
|
+
fallback: 10,
|
|
28732
|
+
minimum: 1,
|
|
28733
|
+
maximum: 20,
|
|
28734
|
+
label: "serpDepth"
|
|
28735
|
+
})
|
|
28736
|
+
};
|
|
28737
|
+
}
|
|
28738
|
+
function emptyCost() {
|
|
28739
|
+
return {
|
|
28740
|
+
currency: "USD",
|
|
28741
|
+
estimatedMicros: 0,
|
|
28742
|
+
actualMicros: 0,
|
|
28743
|
+
taskIds: []
|
|
28744
|
+
};
|
|
28745
|
+
}
|
|
28746
|
+
function aggregateCosts(costs) {
|
|
28747
|
+
const estimated = costs.map((cost) => cost.estimatedMicros);
|
|
28748
|
+
const actual = costs.map((cost) => cost.actualMicros);
|
|
28749
|
+
return {
|
|
28750
|
+
currency: "USD",
|
|
28751
|
+
estimatedMicros: estimated.some((value) => value === null) ? null : estimated.reduce((sum, value) => sum + (value ?? 0), 0),
|
|
28752
|
+
actualMicros: actual.some((value) => value === null) ? null : actual.reduce((sum, value) => sum + (value ?? 0), 0),
|
|
28753
|
+
taskIds: [...new Set(costs.flatMap((cost) => cost.taskIds))].sort()
|
|
28754
|
+
};
|
|
28755
|
+
}
|
|
28756
|
+
function costsWithUnknownRequest(costs) {
|
|
28757
|
+
return {
|
|
28758
|
+
currency: "USD",
|
|
28759
|
+
estimatedMicros: null,
|
|
28760
|
+
actualMicros: null,
|
|
28761
|
+
taskIds: [...new Set(costs.flatMap((cost) => cost.taskIds))].sort()
|
|
28762
|
+
};
|
|
28763
|
+
}
|
|
28764
|
+
async function acquireSerps(input) {
|
|
28765
|
+
const selected = input.queries.slice(0, input.limit);
|
|
28766
|
+
const selection = {
|
|
28767
|
+
availableQueries: input.queries.length,
|
|
28768
|
+
requestedQueries: selected.length,
|
|
28769
|
+
omittedQueries: Math.max(0, input.queries.length - selected.length),
|
|
28770
|
+
limit: input.limit,
|
|
28771
|
+
depth: input.depth,
|
|
28772
|
+
method: "highest-impression-local-queries-v1"
|
|
28773
|
+
};
|
|
28774
|
+
if (!input.requested) return {
|
|
28775
|
+
requested: false,
|
|
28776
|
+
status: "not-requested",
|
|
28777
|
+
selection,
|
|
28778
|
+
market: null,
|
|
28779
|
+
reports: [],
|
|
28780
|
+
cost: emptyCost(),
|
|
28781
|
+
reason: "Local SERP snapshots were not requested, so no paid provider work was attempted."
|
|
28782
|
+
};
|
|
28783
|
+
if (selected.length === 0) return {
|
|
28784
|
+
requested: true,
|
|
28785
|
+
status: "skipped",
|
|
28786
|
+
selection,
|
|
28787
|
+
market: input.market ?? null,
|
|
28788
|
+
reports: [],
|
|
28789
|
+
cost: emptyCost(),
|
|
28790
|
+
reason: "No retained local-intent query was available for a SERP snapshot."
|
|
28791
|
+
};
|
|
28792
|
+
if (!input.market) throw new SeoError("INVALID_INPUT", "Local SERP evidence requires a market.");
|
|
28793
|
+
const reports = [];
|
|
28794
|
+
try {
|
|
28795
|
+
for (const query of selected) reports.push(await input.run({
|
|
28796
|
+
keyword: query,
|
|
28797
|
+
market: input.market,
|
|
28798
|
+
depth: input.depth,
|
|
28799
|
+
provider: input.provider,
|
|
28800
|
+
projectId: input.projectId,
|
|
28801
|
+
context: {
|
|
28802
|
+
reportId: "local-search-demand",
|
|
28803
|
+
reportRunId: input.reportRunId
|
|
28804
|
+
},
|
|
28805
|
+
refresh: input.refresh
|
|
28806
|
+
}));
|
|
28807
|
+
} catch (error) {
|
|
28808
|
+
if (error instanceof SeoError && (error.code === "PROVIDER_UNAVAILABLE" || error.code === "RATE_LIMITED")) return {
|
|
28809
|
+
requested: true,
|
|
28810
|
+
status: reports.length > 0 ? "partial" : "unavailable",
|
|
28811
|
+
selection,
|
|
28812
|
+
market: input.market,
|
|
28813
|
+
reports,
|
|
28814
|
+
cost: costsWithUnknownRequest(reports.map((report) => report.evidence.cost)),
|
|
28815
|
+
reason: "First-party local demand remains available, but not every requested SERP snapshot could be acquired.",
|
|
28816
|
+
error: {
|
|
28817
|
+
code: error.code,
|
|
28818
|
+
message: error.message,
|
|
28819
|
+
retryable: error.retryable
|
|
28820
|
+
}
|
|
28821
|
+
};
|
|
28822
|
+
throw error;
|
|
28823
|
+
}
|
|
28824
|
+
return {
|
|
28825
|
+
requested: true,
|
|
28826
|
+
status: reports.every((report) => report.dataStatus === "complete") ? "complete" : "partial",
|
|
28827
|
+
selection,
|
|
28828
|
+
market: input.market,
|
|
28829
|
+
reports,
|
|
28830
|
+
cost: aggregateCosts(reports.map((report) => report.evidence.cost))
|
|
28831
|
+
};
|
|
28832
|
+
}
|
|
28833
|
+
async function localSearchReport(input, dependencies = {}) {
|
|
28834
|
+
const options = validateInput(input);
|
|
28835
|
+
const now = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
28836
|
+
const generatedAt = now.toISOString();
|
|
28837
|
+
const range = finalGscDateRange(options.days, now);
|
|
28838
|
+
const source = await (dependencies.searchAnalytics ?? querySearchAnalytics)(options.site, {
|
|
28839
|
+
...range,
|
|
28840
|
+
dimensions: ["query", "page"],
|
|
28841
|
+
type: "web",
|
|
28842
|
+
dataState: "final",
|
|
28843
|
+
maxRows: options.maxRows
|
|
28844
|
+
}, { refresh: input.refresh });
|
|
28845
|
+
const analysis = analyzeLocalSearchRows({
|
|
28846
|
+
rows: source.rows,
|
|
28847
|
+
site: options.site,
|
|
28848
|
+
locationTerms: options.locationTerms,
|
|
28849
|
+
brandTerms: input.brandTerms,
|
|
28850
|
+
includeBrand: input.includeBrand,
|
|
28851
|
+
minImpressions: options.minImpressions,
|
|
28852
|
+
limit: options.limit
|
|
28853
|
+
});
|
|
28854
|
+
const serpEvidence = await acquireSerps({
|
|
28855
|
+
requested: options.includeSerps,
|
|
28856
|
+
queries: analysis.opportunities.map((item) => item.query),
|
|
28857
|
+
market: options.market,
|
|
28858
|
+
provider: input.provider,
|
|
28859
|
+
projectId: input.projectId,
|
|
28860
|
+
refresh: input.refresh,
|
|
28861
|
+
limit: options.serpLimit,
|
|
28862
|
+
depth: options.serpDepth,
|
|
28863
|
+
reportRunId: randomUUID(),
|
|
28864
|
+
run: dependencies.serpResults ?? serpResultsReport
|
|
28865
|
+
});
|
|
28866
|
+
const possiblyTruncated = source.rowsFetched >= options.maxRows;
|
|
28867
|
+
const sourceEmpty = source.rows.length === 0;
|
|
28868
|
+
const filtered = !sourceEmpty && analysis.selection.eligibleQueries === 0;
|
|
28869
|
+
const partial = possiblyTruncated || serpEvidence.status === "partial" || serpEvidence.status === "unavailable";
|
|
28870
|
+
const opportunities = analysis.opportunities;
|
|
28871
|
+
const localPackSnapshots = serpEvidence.reports.filter((report) => report.evidence.data.features.includes("local_pack")).length;
|
|
28872
|
+
const dataStatus = sourceEmpty ? "empty" : partial ? "partial" : filtered ? "filtered" : "complete";
|
|
28873
|
+
const warnings = [];
|
|
28874
|
+
if (possiblyTruncated) warnings.push(`Search Console reached the ${options.maxRows}-row retention cap. Lower-volume local queries may be missing.`);
|
|
28875
|
+
if (analysis.selection.invalidRows > 0) warnings.push(`${analysis.selection.invalidRows} malformed Search Console rows were excluded.`);
|
|
28876
|
+
if (analysis.selection.conflictingRows > 0) warnings.push(`${analysis.selection.conflictingRows} conflicting duplicate Search Console rows were excluded.`);
|
|
28877
|
+
if (serpEvidence.error) warnings.push(serpEvidence.error.message);
|
|
28878
|
+
const nextSteps = [];
|
|
28879
|
+
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.");
|
|
28880
|
+
else if (serpEvidence.status === "unavailable") nextSteps.push("Check the provider connection, market support, and local spend limits before retrying the missing SERP snapshots.");
|
|
28881
|
+
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.");
|
|
28882
|
+
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.");
|
|
28883
|
+
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.");
|
|
28884
|
+
return {
|
|
28885
|
+
schemaVersion: 1,
|
|
28886
|
+
methodology: "local-search-demand-v1",
|
|
28887
|
+
site: options.site,
|
|
28888
|
+
generatedAt,
|
|
28889
|
+
range,
|
|
28890
|
+
rangeDays: options.days,
|
|
28891
|
+
dataStatus,
|
|
28892
|
+
source: {
|
|
28893
|
+
provider: "google-search-console",
|
|
28894
|
+
dimensions: ["query", "page"],
|
|
28895
|
+
searchType: "web",
|
|
28896
|
+
dataState: "final",
|
|
28897
|
+
rowsFetched: source.rowsFetched,
|
|
28898
|
+
calls: source.calls,
|
|
28899
|
+
maxRows: options.maxRows,
|
|
28900
|
+
possiblyTruncated,
|
|
28901
|
+
completeness: possiblyTruncated ? "possibly-truncated" : "retained-query-page-rows-only"
|
|
28902
|
+
},
|
|
28903
|
+
methodologyDetails: {
|
|
28904
|
+
intentMethod: "explicit-local-intent-v1",
|
|
28905
|
+
suppliedLocationTerms: options.locationTerms,
|
|
28906
|
+
automaticPatterns: [
|
|
28907
|
+
"nearby-phrases",
|
|
28908
|
+
"uk-postcodes",
|
|
28909
|
+
"contextual-us-zip-codes"
|
|
28910
|
+
],
|
|
28911
|
+
opportunityOrder: "impressions-clicks-position-query-v1",
|
|
28912
|
+
templateMethod: "pseo-url-template-clustering-v1"
|
|
28913
|
+
},
|
|
28914
|
+
selection: analysis.selection,
|
|
28915
|
+
summary: {
|
|
28916
|
+
localQueries: analysis.selection.eligibleQueries,
|
|
28917
|
+
returnedQueries: opportunities.length,
|
|
28918
|
+
pages: analysis.eligiblePageCount,
|
|
28919
|
+
...analysis.eligibleSummary,
|
|
28920
|
+
templates: analysis.templates.length,
|
|
28921
|
+
serpSnapshots: serpEvidence.reports.length,
|
|
28922
|
+
localPackSnapshots,
|
|
28923
|
+
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.`
|
|
28924
|
+
},
|
|
28925
|
+
opportunities,
|
|
28926
|
+
templates: analysis.templates,
|
|
28927
|
+
serpEvidence,
|
|
28928
|
+
warnings,
|
|
28929
|
+
caveats: [
|
|
28930
|
+
"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.",
|
|
28931
|
+
"Named-place, proximity, and postal-code matching are explicit heuristics. They can miss implicit local intent and postal-looking non-location terms.",
|
|
28932
|
+
"A place in a query describes query wording, not the searcher's physical location. Search Console average position is not an exact local rank.",
|
|
28933
|
+
"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.",
|
|
28934
|
+
"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.",
|
|
28935
|
+
"Repeated URL patterns and page overlap are review signals, not proof that more pages should exist or that consolidation is required.",
|
|
28936
|
+
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."
|
|
28937
|
+
],
|
|
28938
|
+
nextSteps
|
|
28939
|
+
};
|
|
28940
|
+
}
|
|
28941
|
+
//#endregion
|
|
27871
28942
|
//#region packages/core/dist/analyze/monitoring/crawl-recommendations.js
|
|
27872
28943
|
function statusMovedToError(item) {
|
|
27873
28944
|
return item.kind !== "removed" && (item.after?.status ?? 0) >= 400 && (item.before?.status ?? 200) < 400;
|
|
@@ -28553,7 +29624,7 @@ function analyzeIndexCoverageSignals(input) {
|
|
|
28553
29624
|
const MAX_SITEMAP_DOCUMENTS = 20;
|
|
28554
29625
|
const MAX_SEARCH_ROWS = 25e4;
|
|
28555
29626
|
const MAX_SITEMAP_URLS = 25e4;
|
|
28556
|
-
const defaultDependencies$
|
|
29627
|
+
const defaultDependencies$4 = {
|
|
28557
29628
|
loadCrawl: loadCrawlReport,
|
|
28558
29629
|
latestCrawl: latestCrawlReport,
|
|
28559
29630
|
searchAnalytics: querySearchAnalytics,
|
|
@@ -28585,7 +29656,7 @@ function sitemapCompleteness(input) {
|
|
|
28585
29656
|
if (input.inventoryTruncated) return "truncated";
|
|
28586
29657
|
return input.results.every((result) => result.dataStatus === "complete") ? "complete" : "partial";
|
|
28587
29658
|
}
|
|
28588
|
-
async function indexCoverageSignals(input, dependencies = defaultDependencies$
|
|
29659
|
+
async function indexCoverageSignals(input, dependencies = defaultDependencies$4) {
|
|
28589
29660
|
const days = integerOption$2({
|
|
28590
29661
|
value: input.days,
|
|
28591
29662
|
fallback: 90,
|
|
@@ -29068,7 +30139,7 @@ function pruneIndexWatchSnapshots(rootSite, url, database = getDb()) {
|
|
|
29068
30139
|
WHERE attempts.attempt_rank > ? AND latest_success.id IS NULL
|
|
29069
30140
|
)`).run(rootSite, url, rootSite, url, 20).changes;
|
|
29070
30141
|
}
|
|
29071
|
-
const defaultDependencies$
|
|
30142
|
+
const defaultDependencies$3 = {
|
|
29072
30143
|
inspectUrl,
|
|
29073
30144
|
now: () => /* @__PURE__ */ new Date(),
|
|
29074
30145
|
store: {
|
|
@@ -29113,7 +30184,7 @@ function isFatalBatchError(error) {
|
|
|
29113
30184
|
"AUTH_REQUIRED"
|
|
29114
30185
|
].includes(error.code);
|
|
29115
30186
|
}
|
|
29116
|
-
async function indexWatch(input, dependencies = defaultDependencies$
|
|
30187
|
+
async function indexWatch(input, dependencies = defaultDependencies$3) {
|
|
29117
30188
|
const languageCode = validateLanguageCode(input.languageCode);
|
|
29118
30189
|
const dailyLimit = integerOption$2({
|
|
29119
30190
|
value: input.dailyLimit,
|
|
@@ -31208,9 +32279,9 @@ function pseoSampleUrls(report) {
|
|
|
31208
32279
|
}
|
|
31209
32280
|
//#endregion
|
|
31210
32281
|
//#region packages/core/dist/analyze/pseo/audit.js
|
|
31211
|
-
const MAX_GSC_ROWS = 5e4;
|
|
32282
|
+
const MAX_GSC_ROWS$1 = 5e4;
|
|
31212
32283
|
const DEFAULT_SITEMAP_URLS = 5e4;
|
|
31213
|
-
const defaultDependencies$
|
|
32284
|
+
const defaultDependencies$2 = {
|
|
31214
32285
|
searchAnalytics: querySearchAnalytics,
|
|
31215
32286
|
fetchPage,
|
|
31216
32287
|
inspectUrl,
|
|
@@ -31412,7 +32483,7 @@ async function inspectSample(input) {
|
|
|
31412
32483
|
};
|
|
31413
32484
|
}
|
|
31414
32485
|
}
|
|
31415
|
-
async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$
|
|
32486
|
+
async function acquirePseoAuditEvidence(input, dependencies = defaultDependencies$2) {
|
|
31416
32487
|
const options = pseoAuditOptions(input);
|
|
31417
32488
|
const now = dependencies.now();
|
|
31418
32489
|
const generatedAt = now.toISOString();
|
|
@@ -31431,7 +32502,7 @@ async function acquirePseoAuditEvidence(input, dependencies = defaultDependencie
|
|
|
31431
32502
|
type: "web",
|
|
31432
32503
|
dataState: "final",
|
|
31433
32504
|
aggregationType: "auto",
|
|
31434
|
-
maxRows: MAX_GSC_ROWS
|
|
32505
|
+
maxRows: MAX_GSC_ROWS$1
|
|
31435
32506
|
};
|
|
31436
32507
|
input.progress?.(`Fetching ${options.days} days of GSC page and query/page rows`);
|
|
31437
32508
|
const [pageResponse, queryPageResponse] = await Promise.all([dependencies.searchAnalytics(input.site, {
|
|
@@ -31482,7 +32553,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
|
|
|
31482
32553
|
minimumTemplateImpressions: options.minimumTemplateImpressions,
|
|
31483
32554
|
crawlSamplesPerTemplate: options.crawlSamples,
|
|
31484
32555
|
inspectionSamplesPerTemplate: options.inspectSamples,
|
|
31485
|
-
maxRowsPerRequest: MAX_GSC_ROWS,
|
|
32556
|
+
maxRowsPerRequest: MAX_GSC_ROWS$1,
|
|
31486
32557
|
pageRowsFetched: acquisition.pageRowsFetched,
|
|
31487
32558
|
queryPageRowsFetched: acquisition.queryPageRowsFetched,
|
|
31488
32559
|
sitemapsRequested: input.sitemaps?.length ?? 0,
|
|
@@ -31493,7 +32564,7 @@ function buildPseoAuditFromAcquisition(input, acquisition, evidence) {
|
|
|
31493
32564
|
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."]
|
|
31494
32565
|
});
|
|
31495
32566
|
}
|
|
31496
|
-
async function pseoAuditReport(input, dependencies = defaultDependencies$
|
|
32567
|
+
async function pseoAuditReport(input, dependencies = defaultDependencies$2) {
|
|
31497
32568
|
const acquisition = await acquirePseoAuditEvidence(input, dependencies);
|
|
31498
32569
|
const sampleUrls = pseoSampleUrls(buildPseoAuditFromAcquisition(input, acquisition));
|
|
31499
32570
|
const topQueries = topQueryByUrl(acquisition.queryPageRows, input);
|
|
@@ -32332,326 +33403,6 @@ function pseoOpportunityVerdict(input) {
|
|
|
32332
33403
|
return `${expansions} template-expansion candidate${expansions === 1 ? "" : "s"} and ${input.competitors.length} observed competitor domain${input.competitors.length === 1 ? "" : "s"} were retained for review.`;
|
|
32333
33404
|
}
|
|
32334
33405
|
//#endregion
|
|
32335
|
-
//#region packages/core/dist/providers/dataforseo/serp-snapshot.js
|
|
32336
|
-
const MAX_SERP_DEPTH = 100;
|
|
32337
|
-
const SERP_ENDPOINT = "v3/serp/google/organic/live/advanced";
|
|
32338
|
-
function safeUrl(value) {
|
|
32339
|
-
if (!value) return null;
|
|
32340
|
-
try {
|
|
32341
|
-
const parsed = new URL(value);
|
|
32342
|
-
if (!["http:", "https:"].includes(parsed.protocol)) return null;
|
|
32343
|
-
parsed.username = "";
|
|
32344
|
-
parsed.password = "";
|
|
32345
|
-
return parsed.toString();
|
|
32346
|
-
} catch {
|
|
32347
|
-
return null;
|
|
32348
|
-
}
|
|
32349
|
-
}
|
|
32350
|
-
function validOrganic(item) {
|
|
32351
|
-
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;
|
|
32352
|
-
}
|
|
32353
|
-
function organicResult(item) {
|
|
32354
|
-
if (!validOrganic(item)) return null;
|
|
32355
|
-
const url = safeUrl(item.url);
|
|
32356
|
-
if (!url) return null;
|
|
32357
|
-
return {
|
|
32358
|
-
rankGroup: item.rank_group,
|
|
32359
|
-
rankAbsolute: item.rank_absolute,
|
|
32360
|
-
page: item.page,
|
|
32361
|
-
domain: item.domain.trim().toLowerCase(),
|
|
32362
|
-
url,
|
|
32363
|
-
title: item.title ?? null,
|
|
32364
|
-
description: item.description ?? null,
|
|
32365
|
-
isFeaturedSnippet: item.is_featured_snippet ?? null
|
|
32366
|
-
};
|
|
32367
|
-
}
|
|
32368
|
-
function checkedAt(value, fallback) {
|
|
32369
|
-
const timestamp = Date.parse(value);
|
|
32370
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
|
|
32371
|
-
}
|
|
32372
|
-
function mapDataForSeoSerpSnapshot(input, snapshot, endpoint = SERP_ENDPOINT) {
|
|
32373
|
-
const market = searchMarketSchema.parse(input.market);
|
|
32374
|
-
const keyword = normalizedKeyword$2(input.keyword);
|
|
32375
|
-
const location = locationRequest(market, "serp-snapshot");
|
|
32376
|
-
const result = snapshot.response.tasks.flatMap((task) => task.result ?? []).find((item) => normalizedKeyword$2(item.keyword) === keyword);
|
|
32377
|
-
if (!result) throw new ProviderError({
|
|
32378
|
-
provider: "dataforseo",
|
|
32379
|
-
operation: "serp-snapshot",
|
|
32380
|
-
code: "invalid-response",
|
|
32381
|
-
message: "DataForSEO returned no matching SERP result."
|
|
32382
|
-
});
|
|
32383
|
-
const warnings = [...snapshot.warnings];
|
|
32384
|
-
const items = result.items ?? [];
|
|
32385
|
-
const organicItems = items.filter((item) => item.type === "organic");
|
|
32386
|
-
const mappedOrganicResults = organicItems.flatMap((item) => {
|
|
32387
|
-
const mapped = organicResult(item);
|
|
32388
|
-
return mapped ? [mapped] : [];
|
|
32389
|
-
}).sort((left, right) => left.rankAbsolute - right.rankAbsolute || left.rankGroup - right.rankGroup || compareCodepoints$3(left.url, right.url));
|
|
32390
|
-
const organicResults = mappedOrganicResults.slice(0, input.depth);
|
|
32391
|
-
const invalidRows = organicItems.length - mappedOrganicResults.length;
|
|
32392
|
-
const resultCapped = mappedOrganicResults.length > input.depth;
|
|
32393
|
-
const invalidResultCount = result.se_results_count !== null && result.se_results_count !== void 0 && result.se_results_count < organicResults.length;
|
|
32394
|
-
if (invalidRows > 0) warnings.push({
|
|
32395
|
-
code: "invalid-organic-results",
|
|
32396
|
-
field: "organicResults",
|
|
32397
|
-
message: `DataForSEO returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank, domain, or URL.`
|
|
32398
|
-
});
|
|
32399
|
-
if (!Number.isFinite(Date.parse(result.datetime))) warnings.push({
|
|
32400
|
-
code: "invalid-serp-observation-time",
|
|
32401
|
-
field: "checkedAt",
|
|
32402
|
-
message: "DataForSEO returned an invalid SERP observation time; the local response time was retained."
|
|
32403
|
-
});
|
|
32404
|
-
const checkUrl = safeUrl(result.check_url);
|
|
32405
|
-
if (result.check_url && !checkUrl) warnings.push({
|
|
32406
|
-
code: "invalid-serp-check-url",
|
|
32407
|
-
field: "checkUrl",
|
|
32408
|
-
message: "DataForSEO returned an invalid SERP check URL."
|
|
32409
|
-
});
|
|
32410
|
-
if (invalidResultCount) warnings.push({
|
|
32411
|
-
code: "invalid-serp-result-count",
|
|
32412
|
-
field: "resultCount",
|
|
32413
|
-
message: "DataForSEO returned an estimated result count below the retained organic result count; the estimate was discarded."
|
|
32414
|
-
});
|
|
32415
|
-
const features = [.../* @__PURE__ */ new Set([...result.item_types ?? [], ...items.map((item) => item.type)])].sort(compareCodepoints$3);
|
|
32416
|
-
return {
|
|
32417
|
-
schemaVersion: 1,
|
|
32418
|
-
provider: "dataforseo",
|
|
32419
|
-
capability: "serp-snapshot",
|
|
32420
|
-
data: {
|
|
32421
|
-
keyword,
|
|
32422
|
-
effectiveKeyword: normalizedKeyword$2(result.spell?.keyword ?? result.keyword),
|
|
32423
|
-
searchEngineDomain: result.se_domain?.trim().toLowerCase() ?? null,
|
|
32424
|
-
checkedAt: checkedAt(result.datetime, snapshot.observedAt),
|
|
32425
|
-
checkUrl,
|
|
32426
|
-
resultCount: invalidResultCount ? null : result.se_results_count ?? null,
|
|
32427
|
-
pagesCount: result.pages_count === void 0 ? null : result.pages_count,
|
|
32428
|
-
features,
|
|
32429
|
-
organicResults
|
|
32430
|
-
},
|
|
32431
|
-
observedAt: snapshot.observedAt,
|
|
32432
|
-
market: {
|
|
32433
|
-
...market,
|
|
32434
|
-
device: market.device ?? "desktop"
|
|
32435
|
-
},
|
|
32436
|
-
coverage: {
|
|
32437
|
-
requestedRows: input.depth,
|
|
32438
|
-
returnedRows: snapshot.returnedRows,
|
|
32439
|
-
retainedRows: organicResults.length,
|
|
32440
|
-
invalidRows,
|
|
32441
|
-
providerTotalRows: invalidResultCount ? null : result.se_results_count ?? null,
|
|
32442
|
-
completeness: invalidRows > 0 || invalidResultCount ? "partial" : resultCapped ? "capped" : "complete",
|
|
32443
|
-
nextCursor: null
|
|
32444
|
-
},
|
|
32445
|
-
cache: snapshot.cache,
|
|
32446
|
-
cost: snapshot.cost,
|
|
32447
|
-
request: {
|
|
32448
|
-
operation: "serp-snapshot",
|
|
32449
|
-
endpoint,
|
|
32450
|
-
limit: input.depth,
|
|
32451
|
-
filters: {
|
|
32452
|
-
countryCode: market.countryCode,
|
|
32453
|
-
languageCode: market.languageCode,
|
|
32454
|
-
device: market.device ?? "desktop",
|
|
32455
|
-
...location.locationCode !== void 0 ? { locationCode: location.locationCode } : { locationName: location.locationName }
|
|
32456
|
-
},
|
|
32457
|
-
sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
|
|
32458
|
-
},
|
|
32459
|
-
warnings
|
|
32460
|
-
};
|
|
32461
|
-
}
|
|
32462
|
-
var DataForSeoSerpSnapshotProvider = class {
|
|
32463
|
-
provider = "dataforseo";
|
|
32464
|
-
capabilitySupport = [{
|
|
32465
|
-
capability: "serp-snapshot",
|
|
32466
|
-
status: "available",
|
|
32467
|
-
markets: [{
|
|
32468
|
-
searchEngines: ["google"],
|
|
32469
|
-
devices: ["desktop", "mobile"],
|
|
32470
|
-
location: "any"
|
|
32471
|
-
}]
|
|
32472
|
-
}];
|
|
32473
|
-
client;
|
|
32474
|
-
constructor(options = {}) {
|
|
32475
|
-
this.client = options.client ?? new DataForSeoClient(options);
|
|
32476
|
-
}
|
|
32477
|
-
async serpSnapshot(input) {
|
|
32478
|
-
const market = searchMarketSchema.parse(input.market);
|
|
32479
|
-
if (market.searchEngine !== "google") throw new ProviderError({
|
|
32480
|
-
provider: "dataforseo",
|
|
32481
|
-
operation: "serp-snapshot",
|
|
32482
|
-
code: "configuration",
|
|
32483
|
-
message: "DataForSEO SERP snapshots currently support Google."
|
|
32484
|
-
});
|
|
32485
|
-
if (!Number.isSafeInteger(input.depth) || input.depth < 1 || input.depth > MAX_SERP_DEPTH) throw new ProviderError({
|
|
32486
|
-
provider: "dataforseo",
|
|
32487
|
-
operation: "serp-snapshot",
|
|
32488
|
-
code: "configuration",
|
|
32489
|
-
message: "SERP depth must be from 1 to 100."
|
|
32490
|
-
});
|
|
32491
|
-
const keyword = normalizedKeyword$2(input.keyword);
|
|
32492
|
-
if (!keyword || keyword.length > 80 || keyword.split(/\s+/u).length > 10) throw new ProviderError({
|
|
32493
|
-
provider: "dataforseo",
|
|
32494
|
-
operation: "serp-snapshot",
|
|
32495
|
-
code: "configuration",
|
|
32496
|
-
message: "SERP snapshots require a keyword of at most 80 characters and 10 words."
|
|
32497
|
-
});
|
|
32498
|
-
const context = input.context ?? {
|
|
32499
|
-
reportId: "serp-results",
|
|
32500
|
-
reportRunId: randomUUID()
|
|
32501
|
-
};
|
|
32502
|
-
const location = locationRequest(market, "serp-snapshot");
|
|
32503
|
-
return mapDataForSeoSerpSnapshot(input, await this.client.serpLive({
|
|
32504
|
-
keyword,
|
|
32505
|
-
languageCode: market.languageCode.split("-")[0],
|
|
32506
|
-
...location,
|
|
32507
|
-
device: market.device ?? "desktop",
|
|
32508
|
-
depth: input.depth,
|
|
32509
|
-
refresh: input.refresh,
|
|
32510
|
-
context
|
|
32511
|
-
}));
|
|
32512
|
-
}
|
|
32513
|
-
};
|
|
32514
|
-
//#endregion
|
|
32515
|
-
//#region packages/core/dist/analyze/serp-results.js
|
|
32516
|
-
const MAX_REPORT_DEPTH = 100;
|
|
32517
|
-
function compareCodepoints(left, right) {
|
|
32518
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
32519
|
-
}
|
|
32520
|
-
function snapshotProvider(candidate) {
|
|
32521
|
-
return "serpSnapshot" in candidate && typeof candidate.serpSnapshot === "function" ? candidate : null;
|
|
32522
|
-
}
|
|
32523
|
-
async function defaultCandidates() {
|
|
32524
|
-
return [{
|
|
32525
|
-
adapter: new DataForSeoSerpSnapshotProvider(),
|
|
32526
|
-
connected: Boolean(await readDataForSeoCredentials()),
|
|
32527
|
-
priority: 10
|
|
32528
|
-
}];
|
|
32529
|
-
}
|
|
32530
|
-
function providerError$1(error) {
|
|
32531
|
-
if (!(error instanceof ProviderError)) throw error;
|
|
32532
|
-
if (error.code === "rate-limit") throw new SeoError("RATE_LIMITED", error.message);
|
|
32533
|
-
if (error.code === "configuration") throw new SeoError("INVALID_INPUT", error.message);
|
|
32534
|
-
throw new SeoError("PROVIDER_UNAVAILABLE", error.message);
|
|
32535
|
-
}
|
|
32536
|
-
function validateInput(input) {
|
|
32537
|
-
const keyword = input.keyword.trim().replace(/\s+/gu, " ");
|
|
32538
|
-
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.");
|
|
32539
|
-
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}.`);
|
|
32540
|
-
const market = searchMarketSchema.safeParse(input.market);
|
|
32541
|
-
if (!market.success) throw new SeoError("INVALID_INPUT", "Use a valid search market.");
|
|
32542
|
-
const provider = input.provider ? providerIdSchema.safeParse(input.provider) : void 0;
|
|
32543
|
-
if (provider && !provider.success) throw new SeoError("INVALID_INPUT", "Use a supported SERP provider.");
|
|
32544
|
-
return {
|
|
32545
|
-
keyword,
|
|
32546
|
-
market: market.data,
|
|
32547
|
-
provider: provider?.data
|
|
32548
|
-
};
|
|
32549
|
-
}
|
|
32550
|
-
function reportStatus$1(evidence) {
|
|
32551
|
-
if (evidence.coverage.completeness === "unavailable" || evidence.coverage.completeness === "invalid") return "unavailable";
|
|
32552
|
-
return evidence.coverage.completeness === "complete" ? "complete" : "partial";
|
|
32553
|
-
}
|
|
32554
|
-
function domainSummaries(snapshot) {
|
|
32555
|
-
const byDomain = /* @__PURE__ */ new Map();
|
|
32556
|
-
for (const result of snapshot.organicResults) byDomain.set(result.domain, [...byDomain.get(result.domain) ?? [], result.rankAbsolute]);
|
|
32557
|
-
return [...byDomain.entries()].map(([domain, ranks]) => ({
|
|
32558
|
-
domain,
|
|
32559
|
-
resultCount: ranks.length,
|
|
32560
|
-
ranks: [...ranks].sort((left, right) => left - right)
|
|
32561
|
-
})).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));
|
|
32562
|
-
}
|
|
32563
|
-
function reportFindings(snapshot, domains) {
|
|
32564
|
-
const findings = [];
|
|
32565
|
-
if (snapshot.effectiveKeyword !== snapshot.keyword) findings.push({
|
|
32566
|
-
code: "query-correction",
|
|
32567
|
-
evidenceRef: "evidence.data.effectiveKeyword",
|
|
32568
|
-
principle: "A corrected query changes the observed result context and does not prove the original wording has the same intent.",
|
|
32569
|
-
detail: `The provider observed results for ${snapshot.effectiveKeyword} after the requested query ${snapshot.keyword}.`
|
|
32570
|
-
});
|
|
32571
|
-
for (const [index, domain] of domains.entries()) {
|
|
32572
|
-
if (domain.resultCount <= 1) continue;
|
|
32573
|
-
findings.push({
|
|
32574
|
-
code: "repeated-domain",
|
|
32575
|
-
evidenceRef: `domains[${index}]`,
|
|
32576
|
-
principle: "Repeated results are descriptive snapshot evidence, not a domain strength or quality score.",
|
|
32577
|
-
detail: `${domain.domain} appears ${domain.resultCount} times at absolute ranks ${domain.ranks.join(", ")}.`
|
|
32578
|
-
});
|
|
32579
|
-
}
|
|
32580
|
-
return findings.slice(0, 10);
|
|
32581
|
-
}
|
|
32582
|
-
async function serpResultsReport(input, dependencies = {}) {
|
|
32583
|
-
const depth = input.depth ?? 10;
|
|
32584
|
-
const validated = validateInput({
|
|
32585
|
-
...input,
|
|
32586
|
-
depth
|
|
32587
|
-
});
|
|
32588
|
-
let candidates;
|
|
32589
|
-
try {
|
|
32590
|
-
candidates = dependencies.candidates ?? await defaultCandidates();
|
|
32591
|
-
} catch (error) {
|
|
32592
|
-
return providerError$1(error);
|
|
32593
|
-
}
|
|
32594
|
-
const resolution = resolveProvider({
|
|
32595
|
-
capability: "serp-snapshot",
|
|
32596
|
-
market: validated.market,
|
|
32597
|
-
candidates,
|
|
32598
|
-
provider: validated.provider
|
|
32599
|
-
});
|
|
32600
|
-
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.");
|
|
32601
|
-
const provider = snapshotProvider(resolution.provider);
|
|
32602
|
-
if (!provider) throw new SeoError("PROVIDER_UNAVAILABLE", "The selected provider has no live SERP implementation.");
|
|
32603
|
-
let evidence;
|
|
32604
|
-
try {
|
|
32605
|
-
evidence = await provider.serpSnapshot({
|
|
32606
|
-
keyword: validated.keyword,
|
|
32607
|
-
market: validated.market,
|
|
32608
|
-
depth,
|
|
32609
|
-
refresh: input.refresh,
|
|
32610
|
-
context: {
|
|
32611
|
-
projectId: input.context?.projectId ?? input.projectId,
|
|
32612
|
-
reportId: input.context?.reportId ?? "serp-results",
|
|
32613
|
-
reportRunId: input.context?.reportRunId ?? randomUUID()
|
|
32614
|
-
}
|
|
32615
|
-
});
|
|
32616
|
-
} catch (error) {
|
|
32617
|
-
return providerError$1(error);
|
|
32618
|
-
}
|
|
32619
|
-
const domains = domainSummaries(evidence.data);
|
|
32620
|
-
const dataStatus = reportStatus$1(evidence);
|
|
32621
|
-
const generatedAt = (dependencies.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
32622
|
-
const correctedQuery = evidence.data.effectiveKeyword !== evidence.data.keyword;
|
|
32623
|
-
return {
|
|
32624
|
-
schemaVersion: 1,
|
|
32625
|
-
generatedAt,
|
|
32626
|
-
dataStatus,
|
|
32627
|
-
market: evidence.market,
|
|
32628
|
-
summary: {
|
|
32629
|
-
keyword: evidence.data.keyword,
|
|
32630
|
-
effectiveKeyword: evidence.data.effectiveKeyword,
|
|
32631
|
-
requestedDepth: depth,
|
|
32632
|
-
organicResults: evidence.data.organicResults.length,
|
|
32633
|
-
uniqueDomains: domains.length,
|
|
32634
|
-
observedFeatures: evidence.data.features.length,
|
|
32635
|
-
correctedQuery,
|
|
32636
|
-
verdict: `${evidence.data.organicResults.length} organic results across ${domains.length} domains were retained from the ${evidence.market.device ?? "desktop"} snapshot.`
|
|
32637
|
-
},
|
|
32638
|
-
evidence,
|
|
32639
|
-
domains,
|
|
32640
|
-
findings: reportFindings(evidence.data, domains),
|
|
32641
|
-
caveats: [
|
|
32642
|
-
"This is one market, location, language, and device-specific result snapshot; it is not rank history.",
|
|
32643
|
-
"Search results can change between checks and may differ from a person's signed-in or personalized results.",
|
|
32644
|
-
"The provider result count is an estimate and may be missing; retained organic rows are bounded by the requested depth.",
|
|
32645
|
-
"Repeated domains, result features, titles, and snippets do not establish content quality, authority, or ranking feasibility."
|
|
32646
|
-
],
|
|
32647
|
-
nextSteps: [
|
|
32648
|
-
"Review the retained pages and result types before deciding whether the query fits an existing page, a new page, or no page.",
|
|
32649
|
-
"Keep Search Console average position separate from these exact snapshot ranks when comparing first-party performance.",
|
|
32650
|
-
"Repeat the same market and device check later if the decision depends on result stability."
|
|
32651
|
-
]
|
|
32652
|
-
};
|
|
32653
|
-
}
|
|
32654
|
-
//#endregion
|
|
32655
33406
|
//#region packages/core/dist/analyze/pseo-opportunities/serps.js
|
|
32656
33407
|
function selectSerpKeywords(candidates, limit) {
|
|
32657
33408
|
const seedRefs = /* @__PURE__ */ new Set();
|
|
@@ -34544,7 +35295,7 @@ function groupedMappings(evidence) {
|
|
|
34544
35295
|
//#endregion
|
|
34545
35296
|
//#region packages/core/dist/analyze/second-page.js
|
|
34546
35297
|
const MAX_SOURCE_ROWS = 1e5;
|
|
34547
|
-
const defaultDependencies = {
|
|
35298
|
+
const defaultDependencies$1 = {
|
|
34548
35299
|
searchAnalytics: querySearchAnalytics,
|
|
34549
35300
|
fetch: fetchPage,
|
|
34550
35301
|
extract: extractPage,
|
|
@@ -34621,7 +35372,7 @@ function reportRecommendations(items) {
|
|
|
34621
35372
|
"Treat the priority score as a triage heuristic, not an estimated traffic lift."
|
|
34622
35373
|
]);
|
|
34623
35374
|
}
|
|
34624
|
-
async function secondPage(input, dependencies = defaultDependencies) {
|
|
35375
|
+
async function secondPage(input, dependencies = defaultDependencies$1) {
|
|
34625
35376
|
const days = integerOption({
|
|
34626
35377
|
value: input.range,
|
|
34627
35378
|
fallback: 28,
|
|
@@ -36812,10 +37563,10 @@ const DEFAULT_TARGET_LIMIT = 20;
|
|
|
36812
37563
|
const MAX_TARGET_LIMIT = 50;
|
|
36813
37564
|
const DEFAULT_DETAIL_PAGES = 1;
|
|
36814
37565
|
const MAX_DETAIL_PAGES = 3;
|
|
36815
|
-
const DEFAULT_ROW_LIMIT$
|
|
36816
|
-
const MAX_ROW_LIMIT$
|
|
37566
|
+
const DEFAULT_ROW_LIMIT$3 = 500;
|
|
37567
|
+
const MAX_ROW_LIMIT$3 = 1e3;
|
|
36817
37568
|
const MAX_CONCURRENCY = 3;
|
|
36818
|
-
function integer(value, fallback, max, name) {
|
|
37569
|
+
function integer$1(value, fallback, max, name) {
|
|
36819
37570
|
const resolved = value ?? fallback;
|
|
36820
37571
|
if (!Number.isInteger(resolved) || resolved < 1 || resolved > max) throw new SeoError("INVALID_INPUT", `${name} must be between 1 and ${max}.`);
|
|
36821
37572
|
return resolved;
|
|
@@ -36876,9 +37627,9 @@ async function targetDetails(input) {
|
|
|
36876
37627
|
}
|
|
36877
37628
|
async function collectBingLinkEvidence(input) {
|
|
36878
37629
|
if (!input.site.trim()) throw new SeoError("INVALID_INPUT", "Pass a Bing Webmaster site URL.");
|
|
36879
|
-
const targetLimit = integer(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
|
|
36880
|
-
const detailPages = integer(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
|
|
36881
|
-
const rowLimit = integer(input.rowLimit, DEFAULT_ROW_LIMIT$
|
|
37630
|
+
const targetLimit = integer$1(input.targetLimit, DEFAULT_TARGET_LIMIT, MAX_TARGET_LIMIT, "Bing link target limit");
|
|
37631
|
+
const detailPages = integer$1(input.detailPagesPerTarget, DEFAULT_DETAIL_PAGES, MAX_DETAIL_PAGES, "Bing detail page limit");
|
|
37632
|
+
const rowLimit = integer$1(input.rowLimit, DEFAULT_ROW_LIMIT$3, MAX_ROW_LIMIT$3, "Bing link row limit");
|
|
36882
37633
|
const client = input.client ?? (await createBingWebmasterClient()).client;
|
|
36883
37634
|
const countRows = [];
|
|
36884
37635
|
let countPagesRequested = 0;
|
|
@@ -36999,6 +37750,884 @@ async function collectBingLinkEvidence(input) {
|
|
|
36999
37750
|
};
|
|
37000
37751
|
}
|
|
37001
37752
|
//#endregion
|
|
37753
|
+
//#region packages/core/dist/links/context-sources.js
|
|
37754
|
+
function urlKey(value) {
|
|
37755
|
+
try {
|
|
37756
|
+
const url = new URL(value);
|
|
37757
|
+
if (!["http:", "https:"].includes(url.protocol)) return null;
|
|
37758
|
+
url.hash = "";
|
|
37759
|
+
return url.toString();
|
|
37760
|
+
} catch {
|
|
37761
|
+
return null;
|
|
37762
|
+
}
|
|
37763
|
+
}
|
|
37764
|
+
function compareTargets(left, right) {
|
|
37765
|
+
return right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0);
|
|
37766
|
+
}
|
|
37767
|
+
function linkTargetCounts(evidence) {
|
|
37768
|
+
if (evidence.targetCounts.length) return [...evidence.targetCounts].sort(compareTargets);
|
|
37769
|
+
const counts = /* @__PURE__ */ new Map();
|
|
37770
|
+
for (const row of evidence.rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
|
|
37771
|
+
return [...counts].map(([targetUrl, observedLinks]) => ({
|
|
37772
|
+
targetUrl,
|
|
37773
|
+
observedLinks
|
|
37774
|
+
})).sort(compareTargets);
|
|
37775
|
+
}
|
|
37776
|
+
function crawlForLinkTargets(input) {
|
|
37777
|
+
const values = /* @__PURE__ */ new Map();
|
|
37778
|
+
if (!input.report) {
|
|
37779
|
+
for (const target of input.targets) values.set(target.targetUrl, {
|
|
37780
|
+
state: "unavailable",
|
|
37781
|
+
reason: "No matching saved crawl was available."
|
|
37782
|
+
});
|
|
37783
|
+
return {
|
|
37784
|
+
values,
|
|
37785
|
+
matched: 0
|
|
37786
|
+
};
|
|
37787
|
+
}
|
|
37788
|
+
const pages = /* @__PURE__ */ new Map();
|
|
37789
|
+
for (const page of input.report.pages) for (const value of [page.url, page.finalUrl]) {
|
|
37790
|
+
const key = urlKey(value);
|
|
37791
|
+
if (key && !pages.has(key)) pages.set(key, page);
|
|
37792
|
+
}
|
|
37793
|
+
const issues = /* @__PURE__ */ new Map();
|
|
37794
|
+
for (const issue of input.report.issues) {
|
|
37795
|
+
const key = urlKey(issue.url);
|
|
37796
|
+
if (!key) continue;
|
|
37797
|
+
const ids = issues.get(key) ?? /* @__PURE__ */ new Set();
|
|
37798
|
+
ids.add(issue.ruleId);
|
|
37799
|
+
issues.set(key, ids);
|
|
37800
|
+
}
|
|
37801
|
+
let matched = 0;
|
|
37802
|
+
for (const target of input.targets) {
|
|
37803
|
+
const key = urlKey(target.targetUrl);
|
|
37804
|
+
const page = key ? pages.get(key) : void 0;
|
|
37805
|
+
if (!page) {
|
|
37806
|
+
values.set(target.targetUrl, {
|
|
37807
|
+
state: "not-observed",
|
|
37808
|
+
reason: "The target URL was not present in the retained crawl pages."
|
|
37809
|
+
});
|
|
37810
|
+
continue;
|
|
37811
|
+
}
|
|
37812
|
+
matched += 1;
|
|
37813
|
+
values.set(target.targetUrl, {
|
|
37814
|
+
state: "observed",
|
|
37815
|
+
reportId: input.report.id,
|
|
37816
|
+
observedAt: input.report.generatedAt,
|
|
37817
|
+
status: page.status,
|
|
37818
|
+
finalUrl: page.finalUrl,
|
|
37819
|
+
indexable: page.indexable,
|
|
37820
|
+
canonical: page.canonical ?? null,
|
|
37821
|
+
issueIds: [...issues.get(key) ?? []].sort()
|
|
37822
|
+
});
|
|
37823
|
+
}
|
|
37824
|
+
return {
|
|
37825
|
+
values,
|
|
37826
|
+
matched
|
|
37827
|
+
};
|
|
37828
|
+
}
|
|
37829
|
+
function aggregateLinkSearchRows(rows) {
|
|
37830
|
+
const values = /* @__PURE__ */ new Map();
|
|
37831
|
+
for (const row of rows) {
|
|
37832
|
+
const page = row.keys[0];
|
|
37833
|
+
const key = page ? urlKey(page) : null;
|
|
37834
|
+
if (!key) continue;
|
|
37835
|
+
const current = values.get(key) ?? {
|
|
37836
|
+
clicks: 0,
|
|
37837
|
+
impressions: 0,
|
|
37838
|
+
positionWeight: 0
|
|
37839
|
+
};
|
|
37840
|
+
current.clicks += row.clicks;
|
|
37841
|
+
current.impressions += row.impressions;
|
|
37842
|
+
current.positionWeight += row.position * row.impressions;
|
|
37843
|
+
values.set(key, current);
|
|
37844
|
+
}
|
|
37845
|
+
return values;
|
|
37846
|
+
}
|
|
37847
|
+
//#endregion
|
|
37848
|
+
//#region packages/core/dist/links/context-findings.js
|
|
37849
|
+
const MAX_FINDINGS$1 = 50;
|
|
37850
|
+
function findingPriority(input) {
|
|
37851
|
+
const searchValue = input.search.state === "observed" && (input.search.clicks > 0 || input.search.impressions >= 100);
|
|
37852
|
+
if (input.code === "linked-broken-target" || searchValue) return "high";
|
|
37853
|
+
if (input.observedLinks >= 5) return "medium";
|
|
37854
|
+
return "low";
|
|
37855
|
+
}
|
|
37856
|
+
function finding(input) {
|
|
37857
|
+
const crawl = input.row.crawl;
|
|
37858
|
+
if (crawl.state !== "observed") throw new Error("A link finding requires observed crawl evidence.");
|
|
37859
|
+
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.` : "";
|
|
37860
|
+
const common = {
|
|
37861
|
+
priority: findingPriority({
|
|
37862
|
+
code: input.code,
|
|
37863
|
+
observedLinks: input.row.observedLinks,
|
|
37864
|
+
crawl,
|
|
37865
|
+
search: input.row.searchConsole
|
|
37866
|
+
}),
|
|
37867
|
+
heuristic: true,
|
|
37868
|
+
targetUrl: input.row.targetUrl,
|
|
37869
|
+
observedLinks: input.row.observedLinks,
|
|
37870
|
+
evidenceRefs: [input.row.targetUrl, crawl.reportId]
|
|
37871
|
+
};
|
|
37872
|
+
if (input.code === "linked-broken-target") return {
|
|
37873
|
+
...common,
|
|
37874
|
+
code: input.code,
|
|
37875
|
+
principle: "Linked target pages should resolve intentionally.",
|
|
37876
|
+
evidence: `The saved crawl observed status ${crawl.status} for a target with ${input.row.observedLinks} retained representative links.${searchEvidence}`,
|
|
37877
|
+
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.",
|
|
37878
|
+
verify: "Run redirect-trace for the target, then recrawl it and confirm a stable intended response."
|
|
37879
|
+
};
|
|
37880
|
+
if (input.code === "linked-redirect-target") return {
|
|
37881
|
+
...common,
|
|
37882
|
+
code: input.code,
|
|
37883
|
+
principle: "External links should reach a stable final destination.",
|
|
37884
|
+
evidence: `The saved crawl observed ${input.row.targetUrl} resolving to ${crawl.finalUrl}.${searchEvidence}`,
|
|
37885
|
+
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.",
|
|
37886
|
+
verify: "Run redirect-trace and confirm one stable hop to the intended indexable page."
|
|
37887
|
+
};
|
|
37888
|
+
if (input.code === "linked-canonical-conflict") return {
|
|
37889
|
+
...common,
|
|
37890
|
+
code: input.code,
|
|
37891
|
+
principle: "Canonical hints should agree with the intended linked page.",
|
|
37892
|
+
evidence: `The saved crawl observed canonical ${crawl.canonical} on a linked target that resolved to ${crawl.finalUrl}.${searchEvidence}`,
|
|
37893
|
+
action: "Confirm the preferred URL. Align the canonical and redirect only if the current disagreement is unintended.",
|
|
37894
|
+
verify: "Audit the final page and inspect the declared and Google-selected canonical before changing it."
|
|
37895
|
+
};
|
|
37896
|
+
return {
|
|
37897
|
+
...common,
|
|
37898
|
+
code: input.code,
|
|
37899
|
+
principle: "A linked page should be indexable when search visibility is intended.",
|
|
37900
|
+
evidence: `The saved crawl marked a linked target as non-indexable.${searchEvidence}`,
|
|
37901
|
+
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.",
|
|
37902
|
+
verify: "Audit the page, then use URL Inspection when the intended state is indexable."
|
|
37903
|
+
};
|
|
37904
|
+
}
|
|
37905
|
+
function linkTargetFindings(rows) {
|
|
37906
|
+
return rows.flatMap((row) => {
|
|
37907
|
+
const crawl = row.crawl;
|
|
37908
|
+
if (crawl.state !== "observed") return [];
|
|
37909
|
+
if (crawl.status >= 400) return [finding({
|
|
37910
|
+
row,
|
|
37911
|
+
code: "linked-broken-target"
|
|
37912
|
+
})];
|
|
37913
|
+
if (urlKey(crawl.finalUrl) !== urlKey(row.targetUrl)) return [finding({
|
|
37914
|
+
row,
|
|
37915
|
+
code: "linked-redirect-target"
|
|
37916
|
+
})];
|
|
37917
|
+
if (crawl.canonical && urlKey(crawl.canonical) !== urlKey(crawl.finalUrl)) return [finding({
|
|
37918
|
+
row,
|
|
37919
|
+
code: "linked-canonical-conflict"
|
|
37920
|
+
})];
|
|
37921
|
+
if (!crawl.indexable) return [finding({
|
|
37922
|
+
row,
|
|
37923
|
+
code: "linked-non-indexable-target"
|
|
37924
|
+
})];
|
|
37925
|
+
return [];
|
|
37926
|
+
}).sort((left, right) => ({
|
|
37927
|
+
high: 0,
|
|
37928
|
+
medium: 1,
|
|
37929
|
+
low: 2
|
|
37930
|
+
})[left.priority] - {
|
|
37931
|
+
high: 0,
|
|
37932
|
+
medium: 1,
|
|
37933
|
+
low: 2
|
|
37934
|
+
}[right.priority] || right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0)).slice(0, MAX_FINDINGS$1);
|
|
37935
|
+
}
|
|
37936
|
+
//#endregion
|
|
37937
|
+
//#region packages/core/dist/links/context.js
|
|
37938
|
+
const DEFAULT_DAYS = 90;
|
|
37939
|
+
const MAX_DAYS = 548;
|
|
37940
|
+
const DEFAULT_CONTEXT_LIMIT = 100;
|
|
37941
|
+
const MAX_CONTEXT_LIMIT = 100;
|
|
37942
|
+
const MAX_GSC_ROWS = 25e3;
|
|
37943
|
+
const defaultDependencies = {
|
|
37944
|
+
latestCrawl: latestCrawlReport,
|
|
37945
|
+
searchAnalytics: querySearchAnalytics,
|
|
37946
|
+
now: () => /* @__PURE__ */ new Date()
|
|
37947
|
+
};
|
|
37948
|
+
function integer(input) {
|
|
37949
|
+
const value = input.value ?? input.fallback;
|
|
37950
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be between 1 and ${input.maximum}.`);
|
|
37951
|
+
return value;
|
|
37952
|
+
}
|
|
37953
|
+
function searchEvidence(input) {
|
|
37954
|
+
if (!input.site) return {
|
|
37955
|
+
state: "unavailable",
|
|
37956
|
+
reason: "No Search Console property was selected for this report."
|
|
37957
|
+
};
|
|
37958
|
+
if (input.error) return {
|
|
37959
|
+
state: "unavailable",
|
|
37960
|
+
reason: input.error
|
|
37961
|
+
};
|
|
37962
|
+
if (!input.aggregate) return {
|
|
37963
|
+
state: "not-retained",
|
|
37964
|
+
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."
|
|
37965
|
+
};
|
|
37966
|
+
return {
|
|
37967
|
+
state: "observed",
|
|
37968
|
+
clicks: input.aggregate.clicks,
|
|
37969
|
+
impressions: input.aggregate.impressions,
|
|
37970
|
+
ctr: input.aggregate.impressions > 0 ? input.aggregate.clicks / input.aggregate.impressions : 0,
|
|
37971
|
+
position: input.aggregate.impressions > 0 ? input.aggregate.positionWeight / input.aggregate.impressions : 0
|
|
37972
|
+
};
|
|
37973
|
+
}
|
|
37974
|
+
async function linkTargetContext(input, dependencies = defaultDependencies) {
|
|
37975
|
+
const days = integer({
|
|
37976
|
+
value: input.days,
|
|
37977
|
+
fallback: DEFAULT_DAYS,
|
|
37978
|
+
maximum: MAX_DAYS,
|
|
37979
|
+
label: "Link context days"
|
|
37980
|
+
});
|
|
37981
|
+
const limit = integer({
|
|
37982
|
+
value: input.limit,
|
|
37983
|
+
fallback: DEFAULT_CONTEXT_LIMIT,
|
|
37984
|
+
maximum: MAX_CONTEXT_LIMIT,
|
|
37985
|
+
label: "Link context limit"
|
|
37986
|
+
});
|
|
37987
|
+
const available = linkTargetCounts(input.evidence);
|
|
37988
|
+
const selected = available.slice(0, limit);
|
|
37989
|
+
const crawlLookup = input.crawlSite ?? input.searchConsoleSite;
|
|
37990
|
+
const crawl = crawlLookup ? dependencies.latestCrawl(crawlLookup) : void 0;
|
|
37991
|
+
const crawlValues = crawlForLinkTargets({
|
|
37992
|
+
report: crawl,
|
|
37993
|
+
targets: selected
|
|
37994
|
+
});
|
|
37995
|
+
const warnings = [];
|
|
37996
|
+
const range = finalGscDateRange(days, dependencies.now());
|
|
37997
|
+
let searchRows = /* @__PURE__ */ new Map();
|
|
37998
|
+
let searchCalls = 0;
|
|
37999
|
+
let searchRowsFetched = 0;
|
|
38000
|
+
let searchError = null;
|
|
38001
|
+
if (input.searchConsoleSite) try {
|
|
38002
|
+
const result = await dependencies.searchAnalytics(input.searchConsoleSite, {
|
|
38003
|
+
...range,
|
|
38004
|
+
dimensions: ["page"],
|
|
38005
|
+
type: "web",
|
|
38006
|
+
dataState: "final",
|
|
38007
|
+
rowLimit: MAX_GSC_ROWS,
|
|
38008
|
+
maxRows: MAX_GSC_ROWS
|
|
38009
|
+
}, { refresh: input.refresh });
|
|
38010
|
+
searchRows = aggregateLinkSearchRows(result.rows);
|
|
38011
|
+
searchCalls = result.calls;
|
|
38012
|
+
searchRowsFetched = result.rowsFetched;
|
|
38013
|
+
} catch (error) {
|
|
38014
|
+
searchError = error instanceof Error ? error.message : "Search Console evidence could not be loaded.";
|
|
38015
|
+
warnings.push(`Search Console context was unavailable. ${searchError}`);
|
|
38016
|
+
}
|
|
38017
|
+
let searchMatches = 0;
|
|
38018
|
+
const retainedLimitReached = searchRowsFetched >= MAX_GSC_ROWS;
|
|
38019
|
+
const rows = selected.map((target) => {
|
|
38020
|
+
const aggregate = searchRows.get(urlKey(target.targetUrl) ?? "");
|
|
38021
|
+
if (aggregate) searchMatches += 1;
|
|
38022
|
+
return {
|
|
38023
|
+
...target,
|
|
38024
|
+
crawl: crawlValues.values.get(target.targetUrl) ?? {
|
|
38025
|
+
state: "not-observed",
|
|
38026
|
+
reason: "The target URL was not present in the retained crawl pages."
|
|
38027
|
+
},
|
|
38028
|
+
searchConsole: searchEvidence({
|
|
38029
|
+
site: input.searchConsoleSite,
|
|
38030
|
+
error: searchError,
|
|
38031
|
+
retainedLimitReached,
|
|
38032
|
+
aggregate
|
|
38033
|
+
})
|
|
38034
|
+
};
|
|
38035
|
+
});
|
|
38036
|
+
if (!crawl) warnings.push("No matching saved crawl was available, so target response and indexability checks were skipped.");
|
|
38037
|
+
if (retainedLimitReached) warnings.push(`Search Console reached the ${MAX_GSC_ROWS}-row acquisition limit. Missing target rows remain not retained rather than zero.`);
|
|
38038
|
+
const unavailableSources = Number(!crawl) + Number(!input.searchConsoleSite);
|
|
38039
|
+
return {
|
|
38040
|
+
schemaVersion: 1,
|
|
38041
|
+
dataStatus: unavailableSources === 2 ? "unavailable" : unavailableSources > 0 || searchError || retainedLimitReached ? "partial" : "complete",
|
|
38042
|
+
selection: {
|
|
38043
|
+
availableTargets: available.length,
|
|
38044
|
+
returnedTargets: rows.length,
|
|
38045
|
+
omittedTargets: Math.max(0, available.length - rows.length),
|
|
38046
|
+
limit
|
|
38047
|
+
},
|
|
38048
|
+
provenance: {
|
|
38049
|
+
crawl: {
|
|
38050
|
+
status: crawl ? "joined" : "unavailable",
|
|
38051
|
+
reportId: crawl?.id ?? null,
|
|
38052
|
+
observedAt: crawl?.generatedAt ?? null,
|
|
38053
|
+
availablePages: crawl?.pages.length ?? 0,
|
|
38054
|
+
matchedTargets: crawlValues.matched
|
|
38055
|
+
},
|
|
38056
|
+
searchConsole: {
|
|
38057
|
+
status: !input.searchConsoleSite || searchError ? "unavailable" : retainedLimitReached ? "partial" : "joined",
|
|
38058
|
+
site: input.searchConsoleSite ?? null,
|
|
38059
|
+
range: input.searchConsoleSite ? {
|
|
38060
|
+
...range,
|
|
38061
|
+
days
|
|
38062
|
+
} : null,
|
|
38063
|
+
calls: searchCalls,
|
|
38064
|
+
rowsFetched: searchRowsFetched,
|
|
38065
|
+
retainedRowLimit: MAX_GSC_ROWS,
|
|
38066
|
+
retainedRowLimitReached: retainedLimitReached,
|
|
38067
|
+
matchedTargets: searchMatches
|
|
38068
|
+
}
|
|
38069
|
+
},
|
|
38070
|
+
rows,
|
|
38071
|
+
findings: linkTargetFindings(rows),
|
|
38072
|
+
warnings
|
|
38073
|
+
};
|
|
38074
|
+
}
|
|
38075
|
+
//#endregion
|
|
38076
|
+
//#region packages/core/dist/providers/dataforseo/link-research-shared.js
|
|
38077
|
+
function linkTarget(value, requestedScope = "domain") {
|
|
38078
|
+
const raw = value.trim();
|
|
38079
|
+
if (!raw || raw.length > 2048) throw invalidTarget();
|
|
38080
|
+
if (requestedScope === "page") try {
|
|
38081
|
+
const url = new URL(raw);
|
|
38082
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error();
|
|
38083
|
+
url.hash = "";
|
|
38084
|
+
return {
|
|
38085
|
+
target: url.toString(),
|
|
38086
|
+
scope: "page"
|
|
38087
|
+
};
|
|
38088
|
+
} catch {
|
|
38089
|
+
throw invalidTarget();
|
|
38090
|
+
}
|
|
38091
|
+
try {
|
|
38092
|
+
const hostname = new URL(raw.includes("://") ? raw : `https://${raw}`).hostname.toLowerCase().replace(/^www\./u, "").replace(/\.$/u, "");
|
|
38093
|
+
if (!hostname || hostname.length > 253 || hostname.includes("..") || !hostname.includes(".") || !/^[a-z0-9.-]+$/u.test(hostname)) throw new Error();
|
|
38094
|
+
return {
|
|
38095
|
+
target: hostname,
|
|
38096
|
+
scope: "domain"
|
|
38097
|
+
};
|
|
38098
|
+
} catch {
|
|
38099
|
+
throw invalidTarget();
|
|
38100
|
+
}
|
|
38101
|
+
}
|
|
38102
|
+
function invalidTarget() {
|
|
38103
|
+
return new ProviderError({
|
|
38104
|
+
provider: "dataforseo",
|
|
38105
|
+
operation: "link-evidence",
|
|
38106
|
+
code: "configuration",
|
|
38107
|
+
message: "Use a valid domain, or choose page scope and pass an absolute URL."
|
|
38108
|
+
});
|
|
38109
|
+
}
|
|
38110
|
+
function linkRowLimit(limit, offset = 0) {
|
|
38111
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ProviderError({
|
|
38112
|
+
provider: "dataforseo",
|
|
38113
|
+
operation: "link-evidence",
|
|
38114
|
+
code: "configuration",
|
|
38115
|
+
message: `Link row limit must be from 1 to ${MAX_LINK_ROWS$1}.`
|
|
38116
|
+
});
|
|
38117
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 2e4) throw new ProviderError({
|
|
38118
|
+
provider: "dataforseo",
|
|
38119
|
+
operation: "link-evidence",
|
|
38120
|
+
code: "configuration",
|
|
38121
|
+
message: `Link row offset must be from 0 to ${MAX_LINK_OFFSET}.`
|
|
38122
|
+
});
|
|
38123
|
+
}
|
|
38124
|
+
function requestContext(reportId, supplied) {
|
|
38125
|
+
return supplied ?? {
|
|
38126
|
+
reportId,
|
|
38127
|
+
reportRunId: randomUUID()
|
|
38128
|
+
};
|
|
38129
|
+
}
|
|
38130
|
+
function missing(field) {
|
|
38131
|
+
return unavailableValue("missing", `DataForSEO omitted ${field}.`);
|
|
38132
|
+
}
|
|
38133
|
+
function numberValue(value, field) {
|
|
38134
|
+
return value === null || value === void 0 ? missing(field) : observedValue(value);
|
|
38135
|
+
}
|
|
38136
|
+
function stringValue(value, field) {
|
|
38137
|
+
return value ? observedValue(value) : missing(field);
|
|
38138
|
+
}
|
|
38139
|
+
function metric(id, label, value) {
|
|
38140
|
+
return value === null || value === void 0 ? [] : [{
|
|
38141
|
+
provider: "dataforseo",
|
|
38142
|
+
id,
|
|
38143
|
+
label,
|
|
38144
|
+
value,
|
|
38145
|
+
scale: {
|
|
38146
|
+
minimum: 0,
|
|
38147
|
+
maximum: 100
|
|
38148
|
+
}
|
|
38149
|
+
}];
|
|
38150
|
+
}
|
|
38151
|
+
function safeUrl(value) {
|
|
38152
|
+
if (!value) return null;
|
|
38153
|
+
try {
|
|
38154
|
+
const url = new URL(value);
|
|
38155
|
+
if (!["http:", "https:"].includes(url.protocol)) return null;
|
|
38156
|
+
url.username = "";
|
|
38157
|
+
url.password = "";
|
|
38158
|
+
url.hash = "";
|
|
38159
|
+
return url.toString();
|
|
38160
|
+
} catch {
|
|
38161
|
+
return null;
|
|
38162
|
+
}
|
|
38163
|
+
}
|
|
38164
|
+
function normalizedDate(value) {
|
|
38165
|
+
if (!value) return null;
|
|
38166
|
+
const parsed = Date.parse(value);
|
|
38167
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
38168
|
+
}
|
|
38169
|
+
function uniqueSorted(values = []) {
|
|
38170
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort(compareCodepoints$3);
|
|
38171
|
+
}
|
|
38172
|
+
function totalRows(values) {
|
|
38173
|
+
const present = values.filter((value) => value !== null && value !== void 0);
|
|
38174
|
+
return present.length ? Math.max(...present) : null;
|
|
38175
|
+
}
|
|
38176
|
+
function linkCoverage(input) {
|
|
38177
|
+
const hasMore = input.providerTotalComparable !== false && input.providerTotalRows !== null ? input.offset + input.returnedRows < input.providerTotalRows : input.returnedRows >= input.requestedRows;
|
|
38178
|
+
return {
|
|
38179
|
+
requestedRows: input.requestedRows,
|
|
38180
|
+
returnedRows: input.returnedRows,
|
|
38181
|
+
retainedRows: input.retainedRows,
|
|
38182
|
+
invalidRows: input.invalidRows,
|
|
38183
|
+
providerTotalRows: input.providerTotalRows,
|
|
38184
|
+
completeness: input.invalidRows > 0 ? "partial" : hasMore ? "capped" : input.filtered ? "filtered" : "complete",
|
|
38185
|
+
nextCursor: hasMore ? String(input.offset + input.returnedRows) : null
|
|
38186
|
+
};
|
|
38187
|
+
}
|
|
38188
|
+
function linkEvidence(input) {
|
|
38189
|
+
return {
|
|
38190
|
+
schemaVersion: 1,
|
|
38191
|
+
provider: "dataforseo",
|
|
38192
|
+
capability: input.capability,
|
|
38193
|
+
data: input.data,
|
|
38194
|
+
observedAt: input.snapshot.observedAt,
|
|
38195
|
+
market: null,
|
|
38196
|
+
coverage: input.coverage,
|
|
38197
|
+
cache: input.snapshot.cache,
|
|
38198
|
+
cost: input.snapshot.cost,
|
|
38199
|
+
request: {
|
|
38200
|
+
operation: input.capability,
|
|
38201
|
+
endpoint: input.endpoint,
|
|
38202
|
+
limit: input.limit,
|
|
38203
|
+
filters: input.filters,
|
|
38204
|
+
sort: input.sort
|
|
38205
|
+
},
|
|
38206
|
+
warnings: input.warnings
|
|
38207
|
+
};
|
|
38208
|
+
}
|
|
38209
|
+
function mappingWarnings(input) {
|
|
38210
|
+
return [
|
|
38211
|
+
...input.snapshotWarnings,
|
|
38212
|
+
...input.invalidRows ? [{
|
|
38213
|
+
code: `invalid-${input.label}-rows`,
|
|
38214
|
+
field: "data.rows",
|
|
38215
|
+
message: `${input.invalidRows} ${input.label} row${input.invalidRows === 1 ? "" : "s"} lacked required fields and were omitted.`
|
|
38216
|
+
}] : [],
|
|
38217
|
+
...input.duplicateRows ? [{
|
|
38218
|
+
code: `duplicate-${input.label}-rows`,
|
|
38219
|
+
field: "data.rows",
|
|
38220
|
+
message: `${input.duplicateRows} duplicate ${input.label} row${input.duplicateRows === 1 ? "" : "s"} were collapsed deterministically.`
|
|
38221
|
+
}] : []
|
|
38222
|
+
];
|
|
38223
|
+
}
|
|
38224
|
+
//#endregion
|
|
38225
|
+
//#region packages/core/dist/providers/dataforseo/backlinks.js
|
|
38226
|
+
function metricValue(row, id) {
|
|
38227
|
+
return row.metrics.find((item) => item.id === id)?.value ?? -1;
|
|
38228
|
+
}
|
|
38229
|
+
function compareRows(left, right) {
|
|
38230
|
+
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 ?? "");
|
|
38231
|
+
}
|
|
38232
|
+
function dedupe(rows) {
|
|
38233
|
+
const sorted = [...rows].sort(compareRows);
|
|
38234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
38235
|
+
return sorted.filter((row) => {
|
|
38236
|
+
const key = `${row.sourceUrl}\0${row.targetUrl}\0${row.anchorText ?? ""}`;
|
|
38237
|
+
if (seen.has(key)) return false;
|
|
38238
|
+
seen.add(key);
|
|
38239
|
+
return true;
|
|
38240
|
+
});
|
|
38241
|
+
}
|
|
38242
|
+
async function dataForSeoBacklinks(client, input) {
|
|
38243
|
+
const normalized = linkTarget(input.target, input.scope);
|
|
38244
|
+
const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
|
|
38245
|
+
const offset = input.offset ?? 0;
|
|
38246
|
+
linkRowLimit(input.limit, offset);
|
|
38247
|
+
const mode = input.mode ?? "representative";
|
|
38248
|
+
const status = input.status ?? "live";
|
|
38249
|
+
const providerMode = mode === "representative" ? "one_per_domain" : "as_is";
|
|
38250
|
+
const orderBy = [
|
|
38251
|
+
"rank,desc",
|
|
38252
|
+
"domain_from_rank,desc",
|
|
38253
|
+
"url_from,asc"
|
|
38254
|
+
];
|
|
38255
|
+
const snapshot = await client.backlinks({
|
|
38256
|
+
...normalized,
|
|
38257
|
+
includeSubdomains,
|
|
38258
|
+
mode: providerMode,
|
|
38259
|
+
status,
|
|
38260
|
+
limit: input.limit,
|
|
38261
|
+
offset,
|
|
38262
|
+
orderBy,
|
|
38263
|
+
refresh: input.refresh,
|
|
38264
|
+
context: requestContext("link-evidence", input.context)
|
|
38265
|
+
});
|
|
38266
|
+
const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
38267
|
+
const rawRows = results.flatMap((result) => result.items ?? []);
|
|
38268
|
+
let invalidRows = 0;
|
|
38269
|
+
const mapped = rawRows.flatMap((row) => {
|
|
38270
|
+
const sourceUrl = safeUrl(row.url_from);
|
|
38271
|
+
const targetUrl = safeUrl(row.url_to);
|
|
38272
|
+
if (!sourceUrl || !targetUrl) {
|
|
38273
|
+
invalidRows += 1;
|
|
38274
|
+
return [];
|
|
38275
|
+
}
|
|
38276
|
+
return [{
|
|
38277
|
+
sourceUrl,
|
|
38278
|
+
sourceDomain: new URL(sourceUrl).hostname.toLowerCase(),
|
|
38279
|
+
targetUrl,
|
|
38280
|
+
anchorText: row.anchor ?? null,
|
|
38281
|
+
linkType: row.item_type ?? null,
|
|
38282
|
+
dofollow: row.dofollow ?? null,
|
|
38283
|
+
attributes: uniqueSorted(row.rel_attributes ?? row.attributes ?? []),
|
|
38284
|
+
firstSeenAt: normalizedDate(row.first_seen),
|
|
38285
|
+
lastSeenAt: normalizedDate(row.lost_date ?? row.last_visited),
|
|
38286
|
+
state: row.is_lost || row.lost_date ? "lost" : "live",
|
|
38287
|
+
indirect: row.is_indirect_link ?? null,
|
|
38288
|
+
linksFromPage: row.links_count ?? null,
|
|
38289
|
+
linksFromDomain: row.group_count ?? null,
|
|
38290
|
+
metrics: [
|
|
38291
|
+
...metric("backlink-rank", "DataForSEO backlink rank", row.rank),
|
|
38292
|
+
...metric("source-domain-rank", "DataForSEO source domain rank", row.domain_from_rank),
|
|
38293
|
+
...metric("source-page-rank", "DataForSEO source page rank", row.page_from_rank),
|
|
38294
|
+
...metric("backlink-spam-score", "DataForSEO backlink spam score", row.backlink_spam_score ?? row.backlinks_spam_score)
|
|
38295
|
+
]
|
|
38296
|
+
}];
|
|
38297
|
+
});
|
|
38298
|
+
const rows = dedupe(mapped);
|
|
38299
|
+
const duplicateRows = mapped.length - rows.length;
|
|
38300
|
+
const providerTotalRows = totalRows(results.map((result) => result.total_count));
|
|
38301
|
+
return linkEvidence({
|
|
38302
|
+
capability: "backlinks",
|
|
38303
|
+
data: {
|
|
38304
|
+
target: normalized.target,
|
|
38305
|
+
mode,
|
|
38306
|
+
rows,
|
|
38307
|
+
totalRows: providerTotalRows
|
|
38308
|
+
},
|
|
38309
|
+
snapshot,
|
|
38310
|
+
coverage: linkCoverage({
|
|
38311
|
+
requestedRows: input.limit,
|
|
38312
|
+
returnedRows: rawRows.length,
|
|
38313
|
+
retainedRows: rows.length,
|
|
38314
|
+
invalidRows,
|
|
38315
|
+
providerTotalRows,
|
|
38316
|
+
offset,
|
|
38317
|
+
filtered: mode === "representative" || status !== "all",
|
|
38318
|
+
providerTotalComparable: mode !== "representative"
|
|
38319
|
+
}),
|
|
38320
|
+
endpoint: LINK_ENDPOINTS.backlinks,
|
|
38321
|
+
limit: input.limit,
|
|
38322
|
+
filters: {
|
|
38323
|
+
scope: normalized.scope,
|
|
38324
|
+
includeSubdomains,
|
|
38325
|
+
mode,
|
|
38326
|
+
status,
|
|
38327
|
+
includeIndirectLinks: true,
|
|
38328
|
+
excludeInternalBacklinks: true,
|
|
38329
|
+
offset
|
|
38330
|
+
},
|
|
38331
|
+
sort: orderBy,
|
|
38332
|
+
warnings: mappingWarnings({
|
|
38333
|
+
snapshotWarnings: snapshot.warnings,
|
|
38334
|
+
invalidRows,
|
|
38335
|
+
duplicateRows,
|
|
38336
|
+
label: "backlink"
|
|
38337
|
+
})
|
|
38338
|
+
});
|
|
38339
|
+
}
|
|
38340
|
+
//#endregion
|
|
38341
|
+
//#region packages/core/dist/providers/dataforseo/link-summary.js
|
|
38342
|
+
async function dataForSeoLinkSummary(client, input) {
|
|
38343
|
+
const normalized = linkTarget(input.target, input.scope);
|
|
38344
|
+
const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
|
|
38345
|
+
const snapshot = await client.linkSummary({
|
|
38346
|
+
...normalized,
|
|
38347
|
+
includeSubdomains,
|
|
38348
|
+
refresh: input.refresh,
|
|
38349
|
+
context: requestContext("link-evidence", input.context)
|
|
38350
|
+
});
|
|
38351
|
+
const rows = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
38352
|
+
const row = [...rows].sort((left, right) => compareCodepoints$3(left.target ?? "", right.target ?? ""))[0];
|
|
38353
|
+
const data = {
|
|
38354
|
+
target: normalized.target,
|
|
38355
|
+
scope: normalized.scope,
|
|
38356
|
+
backlinks: numberValue(row?.backlinks, "total backlinks"),
|
|
38357
|
+
referringDomains: numberValue(row?.referring_domains, "referring domains"),
|
|
38358
|
+
referringPages: numberValue(row?.referring_pages, "referring pages"),
|
|
38359
|
+
brokenBacklinks: numberValue(row?.broken_backlinks, "broken backlinks"),
|
|
38360
|
+
brokenPages: numberValue(row?.broken_pages, "broken referring pages"),
|
|
38361
|
+
metrics: [
|
|
38362
|
+
...metric("rank", "DataForSEO rank", row?.rank),
|
|
38363
|
+
...metric("backlinks-spam-score", "DataForSEO backlink spam score", row?.backlinks_spam_score),
|
|
38364
|
+
...metric("target-spam-score", "DataForSEO target spam score", row?.info?.target_spam_score)
|
|
38365
|
+
]
|
|
38366
|
+
};
|
|
38367
|
+
const retainedRows = row ? 1 : 0;
|
|
38368
|
+
return linkEvidence({
|
|
38369
|
+
capability: "link-summary",
|
|
38370
|
+
data,
|
|
38371
|
+
snapshot,
|
|
38372
|
+
coverage: {
|
|
38373
|
+
requestedRows: 1,
|
|
38374
|
+
returnedRows: rows.length,
|
|
38375
|
+
retainedRows,
|
|
38376
|
+
invalidRows: 0,
|
|
38377
|
+
providerTotalRows: rows.length,
|
|
38378
|
+
completeness: row ? "complete" : "unavailable",
|
|
38379
|
+
nextCursor: null
|
|
38380
|
+
},
|
|
38381
|
+
endpoint: LINK_ENDPOINTS.summary,
|
|
38382
|
+
limit: 1,
|
|
38383
|
+
filters: {
|
|
38384
|
+
scope: normalized.scope,
|
|
38385
|
+
includeSubdomains,
|
|
38386
|
+
status: "live",
|
|
38387
|
+
includeIndirectLinks: true,
|
|
38388
|
+
excludeInternalBacklinks: true
|
|
38389
|
+
},
|
|
38390
|
+
sort: [],
|
|
38391
|
+
warnings: [
|
|
38392
|
+
...snapshot.warnings,
|
|
38393
|
+
...rows.length > 1 ? [{
|
|
38394
|
+
code: "multiple-link-summary-rows",
|
|
38395
|
+
field: "data",
|
|
38396
|
+
message: "DataForSEO returned more than one summary row. One row was retained deterministically."
|
|
38397
|
+
}] : [],
|
|
38398
|
+
...!row ? [{
|
|
38399
|
+
code: "missing-link-summary-row",
|
|
38400
|
+
field: "data",
|
|
38401
|
+
message: "DataForSEO returned no summary row. Counts remain unavailable rather than zero."
|
|
38402
|
+
}] : []
|
|
38403
|
+
]
|
|
38404
|
+
});
|
|
38405
|
+
}
|
|
38406
|
+
//#endregion
|
|
38407
|
+
//#region packages/core/dist/providers/dataforseo/referring-domains.js
|
|
38408
|
+
function observedNumber(row) {
|
|
38409
|
+
return row.backlinks.state === "observed" ? row.backlinks.value : -1;
|
|
38410
|
+
}
|
|
38411
|
+
async function dataForSeoReferringDomains(client, input) {
|
|
38412
|
+
const normalized = linkTarget(input.target, input.scope);
|
|
38413
|
+
const includeSubdomains = normalized.scope === "domain" ? input.includeSubdomains ?? true : false;
|
|
38414
|
+
const offset = input.offset ?? 0;
|
|
38415
|
+
linkRowLimit(input.limit, offset);
|
|
38416
|
+
const orderBy = [
|
|
38417
|
+
"backlinks,desc",
|
|
38418
|
+
"rank,desc",
|
|
38419
|
+
"domain,asc"
|
|
38420
|
+
];
|
|
38421
|
+
const snapshot = await client.referringDomains({
|
|
38422
|
+
...normalized,
|
|
38423
|
+
includeSubdomains,
|
|
38424
|
+
limit: input.limit,
|
|
38425
|
+
offset,
|
|
38426
|
+
orderBy,
|
|
38427
|
+
refresh: input.refresh,
|
|
38428
|
+
context: requestContext("link-evidence", input.context)
|
|
38429
|
+
});
|
|
38430
|
+
const results = snapshot.response.tasks.flatMap((task) => task.result ?? []);
|
|
38431
|
+
const rawRows = results.flatMap((result) => result.items ?? []);
|
|
38432
|
+
let invalidRows = 0;
|
|
38433
|
+
const mapped = rawRows.flatMap((row) => {
|
|
38434
|
+
const domain = row.domain?.trim().toLowerCase();
|
|
38435
|
+
if (!domain) {
|
|
38436
|
+
invalidRows += 1;
|
|
38437
|
+
return [];
|
|
38438
|
+
}
|
|
38439
|
+
const firstSeenAt = normalizedDate(row.first_seen);
|
|
38440
|
+
return [{
|
|
38441
|
+
domain,
|
|
38442
|
+
backlinks: numberValue(row.backlinks, "domain backlinks"),
|
|
38443
|
+
referringPages: numberValue(row.referring_pages, "domain referring pages"),
|
|
38444
|
+
brokenBacklinks: numberValue(row.broken_backlinks, "domain broken backlinks"),
|
|
38445
|
+
brokenPages: numberValue(row.broken_pages, "domain broken pages"),
|
|
38446
|
+
firstSeenAt: stringValue(firstSeenAt, "domain first seen date"),
|
|
38447
|
+
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)]
|
|
38448
|
+
}];
|
|
38449
|
+
});
|
|
38450
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
38451
|
+
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);
|
|
38452
|
+
const rows = [...byDomain.values()];
|
|
38453
|
+
const duplicateRows = mapped.length - rows.length;
|
|
38454
|
+
const providerTotalRows = totalRows(results.map((result) => result.total_count));
|
|
38455
|
+
return linkEvidence({
|
|
38456
|
+
capability: "referring-domains",
|
|
38457
|
+
data: {
|
|
38458
|
+
target: normalized.target,
|
|
38459
|
+
rows,
|
|
38460
|
+
totalRows: providerTotalRows
|
|
38461
|
+
},
|
|
38462
|
+
snapshot,
|
|
38463
|
+
coverage: linkCoverage({
|
|
38464
|
+
requestedRows: input.limit,
|
|
38465
|
+
returnedRows: rawRows.length,
|
|
38466
|
+
retainedRows: rows.length,
|
|
38467
|
+
invalidRows,
|
|
38468
|
+
providerTotalRows,
|
|
38469
|
+
offset,
|
|
38470
|
+
filtered: true
|
|
38471
|
+
}),
|
|
38472
|
+
endpoint: LINK_ENDPOINTS.referringDomains,
|
|
38473
|
+
limit: input.limit,
|
|
38474
|
+
filters: {
|
|
38475
|
+
scope: normalized.scope,
|
|
38476
|
+
includeSubdomains,
|
|
38477
|
+
status: "live",
|
|
38478
|
+
includeIndirectLinks: true,
|
|
38479
|
+
excludeInternalBacklinks: true,
|
|
38480
|
+
offset
|
|
38481
|
+
},
|
|
38482
|
+
sort: orderBy,
|
|
38483
|
+
warnings: mappingWarnings({
|
|
38484
|
+
snapshotWarnings: snapshot.warnings,
|
|
38485
|
+
invalidRows,
|
|
38486
|
+
duplicateRows,
|
|
38487
|
+
label: "referring-domain"
|
|
38488
|
+
})
|
|
38489
|
+
});
|
|
38490
|
+
}
|
|
38491
|
+
//#endregion
|
|
38492
|
+
//#region packages/core/dist/providers/dataforseo/link-research.js
|
|
38493
|
+
var DataForSeoLinkProvider = class {
|
|
38494
|
+
provider = "dataforseo";
|
|
38495
|
+
capabilitySupport = [
|
|
38496
|
+
"link-summary",
|
|
38497
|
+
"referring-domains",
|
|
38498
|
+
"backlinks"
|
|
38499
|
+
].map((capability) => ({
|
|
38500
|
+
capability,
|
|
38501
|
+
status: "available",
|
|
38502
|
+
markets: "all"
|
|
38503
|
+
}));
|
|
38504
|
+
client;
|
|
38505
|
+
constructor(options = {}) {
|
|
38506
|
+
this.client = options.client ?? new DataForSeoClient(options);
|
|
38507
|
+
}
|
|
38508
|
+
linkSummary(input) {
|
|
38509
|
+
return dataForSeoLinkSummary(this.client, input);
|
|
38510
|
+
}
|
|
38511
|
+
backlinks(input) {
|
|
38512
|
+
return dataForSeoBacklinks(this.client, input);
|
|
38513
|
+
}
|
|
38514
|
+
referringDomains(input) {
|
|
38515
|
+
return dataForSeoReferringDomains(this.client, input);
|
|
38516
|
+
}
|
|
38517
|
+
};
|
|
38518
|
+
//#endregion
|
|
38519
|
+
//#region packages/core/dist/links/dataforseo.js
|
|
38520
|
+
const DEFAULT_ROW_LIMIT$2 = 100;
|
|
38521
|
+
const MAX_ROW_LIMIT$2 = 500;
|
|
38522
|
+
function rowLimit(value) {
|
|
38523
|
+
const limit = value ?? DEFAULT_ROW_LIMIT$2;
|
|
38524
|
+
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}.`);
|
|
38525
|
+
return limit;
|
|
38526
|
+
}
|
|
38527
|
+
async function collectDataForSeoLinkEvidence(input) {
|
|
38528
|
+
const limit = rowLimit(input.rowLimit);
|
|
38529
|
+
const provider = input.provider ?? new DataForSeoLinkProvider();
|
|
38530
|
+
const context = input.context ?? {
|
|
38531
|
+
reportId: "link-evidence",
|
|
38532
|
+
reportRunId: randomUUID()
|
|
38533
|
+
};
|
|
38534
|
+
const summary = await provider.linkSummary({
|
|
38535
|
+
target: input.target,
|
|
38536
|
+
scope: input.scope,
|
|
38537
|
+
includeSubdomains: input.includeSubdomains,
|
|
38538
|
+
refresh: input.refresh,
|
|
38539
|
+
context
|
|
38540
|
+
});
|
|
38541
|
+
const backlinks = await provider.backlinks({
|
|
38542
|
+
target: summary.data.target,
|
|
38543
|
+
scope: summary.data.scope,
|
|
38544
|
+
includeSubdomains: input.includeSubdomains,
|
|
38545
|
+
mode: "representative",
|
|
38546
|
+
status: "live",
|
|
38547
|
+
limit,
|
|
38548
|
+
refresh: input.refresh,
|
|
38549
|
+
context
|
|
38550
|
+
});
|
|
38551
|
+
const rows = backlinks.data.rows.map((row) => ({
|
|
38552
|
+
sourceUrl: row.sourceUrl,
|
|
38553
|
+
sourceDomain: row.sourceDomain,
|
|
38554
|
+
targetUrl: row.targetUrl,
|
|
38555
|
+
anchorText: row.anchorText ?? void 0,
|
|
38556
|
+
firstSeenAt: row.firstSeenAt ?? void 0,
|
|
38557
|
+
lastSeenAt: row.lastSeenAt ?? void 0,
|
|
38558
|
+
nofollow: row.dofollow === null ? void 0 : !row.dofollow,
|
|
38559
|
+
linkType: row.linkType ?? void 0,
|
|
38560
|
+
attributes: row.attributes,
|
|
38561
|
+
state: row.state,
|
|
38562
|
+
indirect: row.indirect ?? void 0,
|
|
38563
|
+
linksFromPage: row.linksFromPage ?? void 0,
|
|
38564
|
+
linksFromDomain: row.linksFromDomain ?? void 0,
|
|
38565
|
+
providerMetrics: row.metrics
|
|
38566
|
+
}));
|
|
38567
|
+
const counts = /* @__PURE__ */ new Map();
|
|
38568
|
+
for (const row of rows) counts.set(row.targetUrl, (counts.get(row.targetUrl) ?? 0) + 1);
|
|
38569
|
+
const targetCounts = [...counts].map(([targetUrl, observedLinks]) => ({
|
|
38570
|
+
targetUrl,
|
|
38571
|
+
observedLinks
|
|
38572
|
+
})).sort((left, right) => right.observedLinks - left.observedLinks || (left.targetUrl < right.targetUrl ? -1 : left.targetUrl > right.targetUrl ? 1 : 0));
|
|
38573
|
+
const completeness = backlinks.coverage.completeness;
|
|
38574
|
+
const representative = backlinks.data.mode === "representative";
|
|
38575
|
+
const partial = completeness !== "complete" || representative;
|
|
38576
|
+
return {
|
|
38577
|
+
rows,
|
|
38578
|
+
targetCounts,
|
|
38579
|
+
provenance: {
|
|
38580
|
+
provider: "dataforseo",
|
|
38581
|
+
observedAt: backlinks.observedAt > summary.observedAt ? backlinks.observedAt : summary.observedAt,
|
|
38582
|
+
cached: summary.cache.status === "hit" && backlinks.cache.status === "hit",
|
|
38583
|
+
suppliedRows: backlinks.coverage.returnedRows ?? rows.length,
|
|
38584
|
+
validRows: rows.length,
|
|
38585
|
+
invalidRows: backlinks.coverage.invalidRows,
|
|
38586
|
+
duplicateRows: Math.max(0, (backlinks.coverage.returnedRows ?? rows.length) - backlinks.coverage.invalidRows - rows.length),
|
|
38587
|
+
capped: ["capped", "partial"].includes(completeness),
|
|
38588
|
+
rowLimit: limit,
|
|
38589
|
+
completeness: partial ? "partial" : "complete",
|
|
38590
|
+
providerRequests: {
|
|
38591
|
+
methods: [summary.request.endpoint, backlinks.request.endpoint],
|
|
38592
|
+
maxConcurrentRequests: 1
|
|
38593
|
+
},
|
|
38594
|
+
providerCoverage: {
|
|
38595
|
+
targetCountRows: {
|
|
38596
|
+
returnedRows: targetCounts.length,
|
|
38597
|
+
retainedRows: targetCounts.length,
|
|
38598
|
+
invalidRows: 0
|
|
38599
|
+
},
|
|
38600
|
+
detailRows: {
|
|
38601
|
+
returnedRows: backlinks.coverage.returnedRows ?? rows.length,
|
|
38602
|
+
retainedRows: rows.length,
|
|
38603
|
+
invalidRows: backlinks.coverage.invalidRows
|
|
38604
|
+
},
|
|
38605
|
+
summaryRows: {
|
|
38606
|
+
returnedRows: summary.coverage.returnedRows ?? 0,
|
|
38607
|
+
retainedRows: summary.coverage.retainedRows ?? 0,
|
|
38608
|
+
invalidRows: summary.coverage.invalidRows
|
|
38609
|
+
},
|
|
38610
|
+
backlinkRows: {
|
|
38611
|
+
returnedRows: backlinks.coverage.returnedRows ?? rows.length,
|
|
38612
|
+
retainedRows: rows.length,
|
|
38613
|
+
invalidRows: backlinks.coverage.invalidRows,
|
|
38614
|
+
providerTotalRows: backlinks.coverage.providerTotalRows
|
|
38615
|
+
}
|
|
38616
|
+
}
|
|
38617
|
+
},
|
|
38618
|
+
externalProvider: {
|
|
38619
|
+
summary,
|
|
38620
|
+
backlinks
|
|
38621
|
+
},
|
|
38622
|
+
warnings: [
|
|
38623
|
+
...summary.warnings.map((warning) => warning.message),
|
|
38624
|
+
...backlinks.warnings.map((warning) => warning.message),
|
|
38625
|
+
...representative ? ["The link list retains one representative backlink per referring domain. Provider summary counts remain separate from retained rows."] : [],
|
|
38626
|
+
...!representative && partial ? ["The link list is bounded or partial. Provider summary counts remain separate from retained rows."] : []
|
|
38627
|
+
]
|
|
38628
|
+
};
|
|
38629
|
+
}
|
|
38630
|
+
//#endregion
|
|
37002
38631
|
//#region packages/core/dist/links/import.js
|
|
37003
38632
|
const DEFAULT_ROW_LIMIT$1 = 1e4;
|
|
37004
38633
|
const MAX_ROW_LIMIT$1 = 1e5;
|
|
@@ -37187,6 +38816,10 @@ async function importLinkEvidence(input) {
|
|
|
37187
38816
|
//#region packages/core/dist/links/report.js
|
|
37188
38817
|
const DEFAULT_OUTPUT_LIMIT = 100;
|
|
37189
38818
|
const MAX_OUTPUT_LIMIT = 500;
|
|
38819
|
+
const MAX_TARGET_COUNTS = 100;
|
|
38820
|
+
const MAX_CONTEXT_ROWS = 100;
|
|
38821
|
+
const MAX_FINDINGS = 50;
|
|
38822
|
+
const MAX_STRUCTURED_DETAIL_ROWS = 750;
|
|
37190
38823
|
function boundedLimit(value) {
|
|
37191
38824
|
const limit = value ?? DEFAULT_OUTPUT_LIMIT;
|
|
37192
38825
|
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}.`);
|
|
@@ -37198,7 +38831,7 @@ function targetCounts(rows) {
|
|
|
37198
38831
|
return [...counts].map(([targetUrl, observedLinks]) => ({
|
|
37199
38832
|
targetUrl,
|
|
37200
38833
|
observedLinks
|
|
37201
|
-
})).sort((a, b) => b.observedLinks - a.observedLinks || a.targetUrl.
|
|
38834
|
+
})).sort((a, b) => b.observedLinks - a.observedLinks || (a.targetUrl < b.targetUrl ? -1 : a.targetUrl > b.targetUrl ? 1 : 0));
|
|
37202
38835
|
}
|
|
37203
38836
|
function linkEvidenceReport(input) {
|
|
37204
38837
|
const limit = boundedLimit(input.limit);
|
|
@@ -37206,8 +38839,24 @@ function linkEvidenceReport(input) {
|
|
|
37206
38839
|
const domains = new Set(input.evidence.rows.map((row) => row.sourceDomain));
|
|
37207
38840
|
const observedTargets = targetCounts(input.evidence.rows);
|
|
37208
38841
|
const counts = input.evidence.targetCounts.length ? input.evidence.targetCounts : observedTargets;
|
|
38842
|
+
const providerEvidence = input.evidence.externalProvider ? {
|
|
38843
|
+
summary: input.evidence.externalProvider.summary,
|
|
38844
|
+
backlinks: {
|
|
38845
|
+
...input.evidence.externalProvider.backlinks,
|
|
38846
|
+
data: {
|
|
38847
|
+
target: input.evidence.externalProvider.backlinks.data.target,
|
|
38848
|
+
mode: input.evidence.externalProvider.backlinks.data.mode,
|
|
38849
|
+
totalRows: input.evidence.externalProvider.backlinks.data.totalRows,
|
|
38850
|
+
retainedRows: input.evidence.externalProvider.backlinks.data.rows.length
|
|
38851
|
+
}
|
|
38852
|
+
}
|
|
38853
|
+
} : null;
|
|
38854
|
+
const targetCountsOutput = counts.slice(0, MAX_TARGET_COUNTS);
|
|
38855
|
+
const contextRows = input.targetContext?.rows.slice(0, MAX_CONTEXT_ROWS) ?? [];
|
|
38856
|
+
const findings = input.targetContext?.findings.slice(0, MAX_FINDINGS) ?? [];
|
|
38857
|
+
const returnedDetailRows = rows.length + targetCountsOutput.length + contextRows.length + findings.length;
|
|
37209
38858
|
return {
|
|
37210
|
-
schemaVersion:
|
|
38859
|
+
schemaVersion: 2,
|
|
37211
38860
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
37212
38861
|
dataStatus: input.evidence.provenance.completeness === "complete" ? "complete" : "partial",
|
|
37213
38862
|
summary: {
|
|
@@ -37216,6 +38865,8 @@ function linkEvidenceReport(input) {
|
|
|
37216
38865
|
targetPages: new Set(input.evidence.rows.map((row) => row.targetUrl)).size,
|
|
37217
38866
|
providerTargetPages: counts.length
|
|
37218
38867
|
},
|
|
38868
|
+
providerSummary: providerEvidence?.summary.data ?? null,
|
|
38869
|
+
providerEvidence,
|
|
37219
38870
|
provenance: input.evidence.provenance,
|
|
37220
38871
|
selection: {
|
|
37221
38872
|
availableRows: input.evidence.rows.length,
|
|
@@ -37223,13 +38874,45 @@ function linkEvidenceReport(input) {
|
|
|
37223
38874
|
omittedRows: Math.max(0, input.evidence.rows.length - rows.length),
|
|
37224
38875
|
limit
|
|
37225
38876
|
},
|
|
37226
|
-
|
|
38877
|
+
outputBudget: {
|
|
38878
|
+
unit: "structured-detail-rows",
|
|
38879
|
+
limit: MAX_STRUCTURED_DETAIL_ROWS,
|
|
38880
|
+
returned: returnedDetailRows,
|
|
38881
|
+
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)),
|
|
38882
|
+
sections: {
|
|
38883
|
+
links: {
|
|
38884
|
+
limit: MAX_OUTPUT_LIMIT,
|
|
38885
|
+
returned: rows.length
|
|
38886
|
+
},
|
|
38887
|
+
targetCounts: {
|
|
38888
|
+
limit: MAX_TARGET_COUNTS,
|
|
38889
|
+
returned: targetCountsOutput.length
|
|
38890
|
+
},
|
|
38891
|
+
targetContext: {
|
|
38892
|
+
limit: MAX_CONTEXT_ROWS,
|
|
38893
|
+
returned: contextRows.length
|
|
38894
|
+
},
|
|
38895
|
+
findings: {
|
|
38896
|
+
limit: MAX_FINDINGS,
|
|
38897
|
+
returned: findings.length
|
|
38898
|
+
}
|
|
38899
|
+
}
|
|
38900
|
+
},
|
|
38901
|
+
targetCounts: targetCountsOutput,
|
|
37227
38902
|
links: rows,
|
|
37228
|
-
|
|
38903
|
+
targetContext: input.targetContext ? {
|
|
38904
|
+
...input.targetContext,
|
|
38905
|
+
rows: contextRows,
|
|
38906
|
+
findings
|
|
38907
|
+
} : null,
|
|
38908
|
+
findings,
|
|
38909
|
+
warnings: [...input.evidence.warnings, ...input.targetContext?.warnings ?? []],
|
|
37229
38910
|
caveats: [
|
|
37230
38911
|
"This is bounded provider or imported evidence, not a complete backlink index.",
|
|
37231
38912
|
"A missing link is not proof that no link exists, and an observed link is not a quality or ranking verdict.",
|
|
37232
|
-
"Referring-domain counts describe the retained rows only."
|
|
38913
|
+
"Referring-domain counts describe the retained rows only.",
|
|
38914
|
+
...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."] : [],
|
|
38915
|
+
...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."] : []
|
|
37233
38916
|
]
|
|
37234
38917
|
};
|
|
37235
38918
|
}
|
|
@@ -38070,6 +39753,7 @@ const TELEMETRY_REPORTS = [
|
|
|
38070
39753
|
"crawl-history",
|
|
38071
39754
|
"crawler-rules",
|
|
38072
39755
|
"llms-txt-audit",
|
|
39756
|
+
"local-search-demand",
|
|
38073
39757
|
"generate-llms-txt",
|
|
38074
39758
|
"measure-change",
|
|
38075
39759
|
"monthly-report",
|
|
@@ -40167,6 +41851,74 @@ function registerKeywordTools(server, dependencies = {}) {
|
|
|
40167
41851
|
});
|
|
40168
41852
|
}
|
|
40169
41853
|
//#endregion
|
|
41854
|
+
//#region packages/mcp/src/local-search-tools.ts
|
|
41855
|
+
const localSearchInput = z$1.strictObject({
|
|
41856
|
+
site: z$1.string().trim().min(1).max(2048),
|
|
41857
|
+
days: z$1.number().int().min(1).max(548).optional(),
|
|
41858
|
+
locationTerms: z$1.array(z$1.string().trim().min(1).max(100)).max(100).optional(),
|
|
41859
|
+
minImpressions: z$1.number().int().min(0).max(1e9).optional(),
|
|
41860
|
+
limit: z$1.number().int().min(1).max(100).optional(),
|
|
41861
|
+
maxRows: z$1.number().int().min(1).max(5e4).optional(),
|
|
41862
|
+
brandTerms: z$1.array(z$1.string().trim().min(1).max(200)).max(20).optional(),
|
|
41863
|
+
includeBrand: z$1.boolean().optional(),
|
|
41864
|
+
includeSerps: z$1.boolean().default(false),
|
|
41865
|
+
countryCode: providerCountryCodeInput.optional(),
|
|
41866
|
+
languageCode: providerLanguageCodeInput.optional(),
|
|
41867
|
+
searchEngine: providerSearchEngineInput,
|
|
41868
|
+
location: providerLocationInput.optional(),
|
|
41869
|
+
device: providerDeviceInput.optional(),
|
|
41870
|
+
provider: providerIdInput.optional(),
|
|
41871
|
+
serpLimit: z$1.number().int().min(1).max(3).optional(),
|
|
41872
|
+
serpDepth: z$1.number().int().min(1).max(20).optional(),
|
|
41873
|
+
refresh: z$1.boolean().optional()
|
|
41874
|
+
}).superRefine((input, context) => {
|
|
41875
|
+
const hasSerpOptions = Boolean(input.countryCode || input.languageCode || input.location || input.device || input.provider || input.serpLimit || input.serpDepth);
|
|
41876
|
+
if (!input.includeSerps && hasSerpOptions) context.addIssue({
|
|
41877
|
+
code: "custom",
|
|
41878
|
+
path: ["includeSerps"],
|
|
41879
|
+
message: "Set includeSerps to true before passing market, provider, or SERP options."
|
|
41880
|
+
});
|
|
41881
|
+
if (input.includeSerps && (!input.countryCode || !input.languageCode || !input.location)) context.addIssue({
|
|
41882
|
+
code: "custom",
|
|
41883
|
+
path: ["includeSerps"],
|
|
41884
|
+
message: "Local SERP evidence requires countryCode, languageCode, and a canonical location."
|
|
41885
|
+
});
|
|
41886
|
+
});
|
|
41887
|
+
function registerLocalSearchTools(server, dependencies = {}) {
|
|
41888
|
+
server.registerTool("seo_local_search_demand", {
|
|
41889
|
+
description: "Find retained local-intent demand, landing pages, repeated local templates, and optional location-specific SERP evidence",
|
|
41890
|
+
inputSchema: localSearchInput
|
|
41891
|
+
}, async ({ site, days, locationTerms, minImpressions, limit, maxRows, brandTerms, includeBrand, includeSerps, countryCode, languageCode, searchEngine, location, device, provider, serpLimit, serpDepth, refresh }) => {
|
|
41892
|
+
try {
|
|
41893
|
+
const report = await (dependencies.localSearchReport ?? localSearchReport)({
|
|
41894
|
+
site,
|
|
41895
|
+
days,
|
|
41896
|
+
locationTerms,
|
|
41897
|
+
minImpressions,
|
|
41898
|
+
limit,
|
|
41899
|
+
maxRows,
|
|
41900
|
+
brandTerms,
|
|
41901
|
+
includeBrand,
|
|
41902
|
+
includeSerps,
|
|
41903
|
+
...includeSerps && countryCode && languageCode && location ? { market: {
|
|
41904
|
+
countryCode,
|
|
41905
|
+
languageCode,
|
|
41906
|
+
searchEngine,
|
|
41907
|
+
location,
|
|
41908
|
+
device
|
|
41909
|
+
} } : {},
|
|
41910
|
+
provider,
|
|
41911
|
+
serpLimit,
|
|
41912
|
+
serpDepth,
|
|
41913
|
+
refresh
|
|
41914
|
+
});
|
|
41915
|
+
return toolSuccess(report.summary.verdict, compactAgentWorkflowOutput(report));
|
|
41916
|
+
} catch (error) {
|
|
41917
|
+
return toolError(error);
|
|
41918
|
+
}
|
|
41919
|
+
});
|
|
41920
|
+
}
|
|
41921
|
+
//#endregion
|
|
40170
41922
|
//#region packages/mcp/src/monitoring-tools.ts
|
|
40171
41923
|
function registerMonitoringTools(server) {
|
|
40172
41924
|
server.registerTool("seo_crawl_diff", {
|
|
@@ -40651,9 +42403,14 @@ function registerProviderTools(server) {
|
|
|
40651
42403
|
}
|
|
40652
42404
|
});
|
|
40653
42405
|
server.registerTool("seo_link_evidence", {
|
|
40654
|
-
description: "Review bounded referring-link evidence from Bing Webmaster or a local export",
|
|
42406
|
+
description: "Review bounded referring-link evidence from DataForSEO, Bing Webmaster or a local export",
|
|
40655
42407
|
inputSchema: {
|
|
40656
42408
|
site: z$1.string().url().max(2e3).optional(),
|
|
42409
|
+
provider: z$1.enum(["dataforseo", "bing"]).optional(),
|
|
42410
|
+
target: z$1.string().trim().min(1).max(2048).optional(),
|
|
42411
|
+
scope: z$1.enum(["domain", "page"]).optional(),
|
|
42412
|
+
includeSubdomains: z$1.boolean().optional(),
|
|
42413
|
+
searchConsoleSite: z$1.string().trim().min(1).max(2048).optional(),
|
|
40657
42414
|
file: z$1.string().min(1).max(4096).optional(),
|
|
40658
42415
|
format: z$1.enum([
|
|
40659
42416
|
"csv",
|
|
@@ -40663,23 +42420,43 @@ function registerProviderTools(server) {
|
|
|
40663
42420
|
rowLimit: z$1.number().int().min(1).max(1e5).optional(),
|
|
40664
42421
|
targetLimit: z$1.number().int().min(1).max(50).optional(),
|
|
40665
42422
|
detailPagesPerTarget: z$1.number().int().min(1).max(3).optional(),
|
|
40666
|
-
limit: z$1.number().int().min(1).max(500).optional()
|
|
42423
|
+
limit: z$1.number().int().min(1).max(500).optional(),
|
|
42424
|
+
days: z$1.number().int().min(1).max(548).optional(),
|
|
42425
|
+
refresh: z$1.boolean().optional()
|
|
40667
42426
|
}
|
|
40668
|
-
}, async ({ site, file, format, rowLimit, targetLimit, detailPagesPerTarget, limit }) => {
|
|
42427
|
+
}, async ({ site, provider, target, scope, includeSubdomains, searchConsoleSite, file, format, rowLimit, targetLimit, detailPagesPerTarget, limit, days, refresh }) => {
|
|
40669
42428
|
try {
|
|
40670
|
-
|
|
42429
|
+
const liveProvider = provider ?? (target ? "dataforseo" : "bing");
|
|
42430
|
+
if (Number(Boolean(file)) + Number(Boolean(site)) + Number(Boolean(target)) !== 1) throw new SeoError("INVALID_INPUT", "Pass one link source: file, site for Bing, or target for DataForSEO.");
|
|
42431
|
+
if (file && provider) throw new SeoError("INVALID_INPUT", "Do not pass provider with a local link file.");
|
|
42432
|
+
if (liveProvider === "dataforseo" && !target) throw new SeoError("INVALID_INPUT", "Pass target for DataForSEO link evidence.");
|
|
42433
|
+
if (liveProvider === "bing" && !site && !file) throw new SeoError("INVALID_INPUT", "Pass site for Bing link evidence.");
|
|
42434
|
+
const evidence = file ? await importLinkEvidence({
|
|
42435
|
+
file,
|
|
42436
|
+
format,
|
|
42437
|
+
rowLimit
|
|
42438
|
+
}) : liveProvider === "dataforseo" ? await collectDataForSeoLinkEvidence({
|
|
42439
|
+
target: target ?? "",
|
|
42440
|
+
scope,
|
|
42441
|
+
includeSubdomains,
|
|
42442
|
+
rowLimit,
|
|
42443
|
+
refresh
|
|
42444
|
+
}) : await collectBingLinkEvidence({
|
|
42445
|
+
site: site ?? "",
|
|
42446
|
+
rowLimit,
|
|
42447
|
+
targetLimit,
|
|
42448
|
+
detailPagesPerTarget
|
|
42449
|
+
});
|
|
40671
42450
|
const report = linkEvidenceReport({
|
|
40672
|
-
evidence
|
|
40673
|
-
|
|
40674
|
-
|
|
40675
|
-
|
|
40676
|
-
|
|
40677
|
-
|
|
40678
|
-
|
|
40679
|
-
|
|
40680
|
-
|
|
40681
|
-
}),
|
|
40682
|
-
limit
|
|
42451
|
+
evidence,
|
|
42452
|
+
limit,
|
|
42453
|
+
targetContext: searchConsoleSite || evidence.provenance.provider === "dataforseo" ? await linkTargetContext({
|
|
42454
|
+
evidence,
|
|
42455
|
+
searchConsoleSite,
|
|
42456
|
+
crawlSite: searchConsoleSite ?? target,
|
|
42457
|
+
days,
|
|
42458
|
+
refresh
|
|
42459
|
+
}) : void 0
|
|
40683
42460
|
});
|
|
40684
42461
|
return toolSuccess(`${report.summary.observedLinks} referring links were retained from ${report.provenance.provider}.`, report);
|
|
40685
42462
|
} catch (error) {
|
|
@@ -41094,15 +42871,18 @@ const REPORT_DEPTH = {
|
|
|
41094
42871
|
"link-evidence": {
|
|
41095
42872
|
readOrder: [
|
|
41096
42873
|
"dataStatus, provenance, warnings, and caveats",
|
|
41097
|
-
"
|
|
41098
|
-
"
|
|
42874
|
+
"providerSummary and providerEvidence coverage, cache, cost, filters, and observation time when present",
|
|
42875
|
+
"selection, outputBudget, and retained summary counts",
|
|
42876
|
+
"findings and targetContext with crawl and Search Console evidence states",
|
|
42877
|
+
"targetCounts, then the bounded representative links list and provider-native metrics"
|
|
41099
42878
|
],
|
|
41100
42879
|
doNotClaim: [
|
|
41101
|
-
"The retained rows are not a complete backlink index.",
|
|
41102
|
-
"
|
|
41103
|
-
"
|
|
42880
|
+
"The retained rows are not a complete backlink index, and missing rows do not prove a link does not exist.",
|
|
42881
|
+
"Observed links do not establish quality, authority, value, or ranking impact.",
|
|
42882
|
+
"Provider rank and spam metrics keep their provider-specific scales. They are not search-engine ranking factors or interchangeable authority scores.",
|
|
42883
|
+
"A missing Search Console row is not zero, and a saved crawl finding does not prove the target still has that state."
|
|
41104
42884
|
],
|
|
41105
|
-
verify: "Open a selected referring URL
|
|
42885
|
+
verify: "Open a selected referring URL to confirm the link, then check any flagged target live with redirect-trace or audit-page before changing it.",
|
|
41106
42886
|
related: [
|
|
41107
42887
|
{
|
|
41108
42888
|
id: "link-recovery",
|
|
@@ -41115,6 +42895,10 @@ const REPORT_DEPTH = {
|
|
|
41115
42895
|
{
|
|
41116
42896
|
id: "site-crawl",
|
|
41117
42897
|
reason: "Checks internal links and live pages."
|
|
42898
|
+
},
|
|
42899
|
+
{
|
|
42900
|
+
id: "search-performance-overview",
|
|
42901
|
+
reason: "Adds broader first-party search context."
|
|
41118
42902
|
}
|
|
41119
42903
|
]
|
|
41120
42904
|
},
|
|
@@ -42214,6 +43998,41 @@ const REPORT_DEPTH = {
|
|
|
42214
43998
|
}
|
|
42215
43999
|
]
|
|
42216
44000
|
},
|
|
44001
|
+
"local-search-demand": {
|
|
44002
|
+
readOrder: [
|
|
44003
|
+
"dataStatus, source row cap, possiblyTruncated, and selection exclusions",
|
|
44004
|
+
"methodologyDetails supplied terms and automatic heuristic patterns",
|
|
44005
|
+
"opportunities intent evidence, metrics, page coverage, and action",
|
|
44006
|
+
"templates as repeated page review signals",
|
|
44007
|
+
"serpEvidence requested status, exact market, coverage, cache, cost, features, and organic rows",
|
|
44008
|
+
"caveats and nextSteps"
|
|
44009
|
+
],
|
|
44010
|
+
doNotClaim: [
|
|
44011
|
+
"A place in a query is not evidence of the searcher location, and Google Analytics geography cannot prove the query.",
|
|
44012
|
+
"Search Console average position is not an exact city rank, and one live snapshot is not rank history.",
|
|
44013
|
+
"A local pack feature does not expose every listing, prove business visibility, or replace Google Business Profile evidence.",
|
|
44014
|
+
"A repeated local URL pattern does not prove shared intent, page quality, unique source data, or permission to scale."
|
|
44015
|
+
],
|
|
44016
|
+
verify: "Inspect representative landing pages and the retained live results in the same location, language, and device before changing or expanding local pages.",
|
|
44017
|
+
related: [
|
|
44018
|
+
{
|
|
44019
|
+
id: "pseo-audit",
|
|
44020
|
+
reason: "Validates repeated local page templates."
|
|
44021
|
+
},
|
|
44022
|
+
{
|
|
44023
|
+
id: "serp-results",
|
|
44024
|
+
reason: "Opens one local result snapshot."
|
|
44025
|
+
},
|
|
44026
|
+
{
|
|
44027
|
+
id: "rank-tracking",
|
|
44028
|
+
reason: "Measures exact local ranks over time."
|
|
44029
|
+
},
|
|
44030
|
+
{
|
|
44031
|
+
id: "keyword-research",
|
|
44032
|
+
reason: "Finds candidate terms beyond retained Search Console queries."
|
|
44033
|
+
}
|
|
44034
|
+
]
|
|
44035
|
+
},
|
|
42217
44036
|
"saved-keywords": {
|
|
42218
44037
|
readOrder: [
|
|
42219
44038
|
"dataStatus, summary, evidence pagination, and any tag filter",
|
|
@@ -42843,10 +44662,14 @@ const REPORT_GUIDANCE = {
|
|
|
42843
44662
|
},
|
|
42844
44663
|
"link-evidence": {
|
|
42845
44664
|
name: "Referring link evidence",
|
|
42846
|
-
description: "Review
|
|
42847
|
-
useWhen: [
|
|
42848
|
-
|
|
42849
|
-
|
|
44665
|
+
description: "Review bounded referring-link evidence from a live provider, Bing Webmaster or a local export.",
|
|
44666
|
+
useWhen: [
|
|
44667
|
+
"You want concrete referring URLs and anchor text where available.",
|
|
44668
|
+
"You want to compare a live provider summary with saved crawl and Search Console evidence for linked target pages.",
|
|
44669
|
+
"You need one normalized view of DataForSEO, Bing or an exported CSV, JSON, or JSONL file."
|
|
44670
|
+
],
|
|
44671
|
+
avoidWhen: ["You need a complete backlink index or a universal authority score.", "You want to find broken internal links during a crawl."],
|
|
44672
|
+
outcome: "Provider summary counts, representative links, target-page search and crawl context, and bounded technical findings with explicit source coverage."
|
|
42850
44673
|
},
|
|
42851
44674
|
"server-log-analysis": {
|
|
42852
44675
|
name: "Server log crawler analysis",
|
|
@@ -43101,6 +44924,17 @@ const REPORT_GUIDANCE = {
|
|
|
43101
44924
|
avoidWhen: ["You need exact current rankings or pages in the live results.", "You plan to turn every discovered term into a page without checking intent or fit."],
|
|
43102
44925
|
outcome: "A bounded keyword set with discovery sources, provider estimates, evidence states, and research follow-ups."
|
|
43103
44926
|
},
|
|
44927
|
+
"local-search-demand": {
|
|
44928
|
+
name: "Local search demand",
|
|
44929
|
+
description: "Find location-specific Search Console demand and enrich a small shortlist with exact local result snapshots.",
|
|
44930
|
+
useWhen: [
|
|
44931
|
+
"You want to find retained queries that name a service area, use a proximity phrase, or contain a postcode or ZIP code.",
|
|
44932
|
+
"You need to connect local demand with current landing pages and repeated programmatic page patterns.",
|
|
44933
|
+
"You want optional city and device-specific result evidence without replacing first-party performance data."
|
|
44934
|
+
],
|
|
44935
|
+
avoidWhen: ["You need Google Business Profile performance or a complete business listing audit.", "You want the searcher location inferred from a place name in the query."],
|
|
44936
|
+
outcome: "A bounded local-intent query and page queue, repeated local template signals, and optional local SERP feature and competitor evidence."
|
|
44937
|
+
},
|
|
43104
44938
|
"saved-keywords": {
|
|
43105
44939
|
name: "Saved keyword set",
|
|
43106
44940
|
description: "Review one local keyword set with metric freshness, tags, page mappings, and explicit view limits.",
|
|
@@ -43679,6 +45513,11 @@ const reportGroups = [
|
|
|
43679
45513
|
"seo_serp_results"
|
|
43680
45514
|
]
|
|
43681
45515
|
},
|
|
45516
|
+
{
|
|
45517
|
+
category: "opportunities",
|
|
45518
|
+
register: registerLocalSearchTools,
|
|
45519
|
+
names: ["seo_local_search_demand"]
|
|
45520
|
+
},
|
|
43682
45521
|
{
|
|
43683
45522
|
category: "opportunities",
|
|
43684
45523
|
register: registerDomainResearchTools,
|