seo 0.6.0 → 0.7.0
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 +9 -10
- package/dist/cli.js +77 -78
- package/dist/cli.js.map +1 -1
- package/dist/{dist-C_Ezkcm5.js → dist-C21OaeU2.js} +1 -1
- package/dist/{dist-nmppE37y.js → dist-DVFMlaqF.js} +83 -80
- package/dist/dist-DVFMlaqF.js.map +1 -0
- package/dist/index.d.ts +66 -51
- package/dist/index.js +512 -88
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +573 -150
- package/dist/mcp.js.map +1 -1
- package/dist/{package-CwLtPH6B.js → package-CmplBMg7.js} +2 -2
- package/dist/{package-CwLtPH6B.js.map → package-CmplBMg7.js.map} +1 -1
- package/package.json +2 -1
- package/dist/dist-nmppE37y.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 { i as __toESM, n as load, r as __commonJSMin, t as version } from "./package-
|
|
2
|
+
import { i as __toESM, n as load, r as __commonJSMin, t as version } from "./package-CmplBMg7.js";
|
|
3
3
|
import { createHash, randomUUID, sign } from "node:crypto";
|
|
4
4
|
import { ZodOptional, z } from "zod";
|
|
5
5
|
import { arch, homedir, platform, totalmem } from "node:os";
|
|
@@ -16802,7 +16802,7 @@ function extractCanonicalEvidence($, headers, baseUrl) {
|
|
|
16802
16802
|
//#region packages/core/dist/extract/content-sketch.js
|
|
16803
16803
|
const SHINGLE_SIZE = 5;
|
|
16804
16804
|
const MAX_HASHES = 32;
|
|
16805
|
-
function words(value) {
|
|
16805
|
+
function words$1(value) {
|
|
16806
16806
|
return value.normalize("NFKC").toLocaleLowerCase("en").match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
16807
16807
|
}
|
|
16808
16808
|
function sampleIndexes(total, limit) {
|
|
@@ -16813,7 +16813,7 @@ function digest(value) {
|
|
|
16813
16813
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
16814
16814
|
}
|
|
16815
16815
|
function contentSketch(value) {
|
|
16816
|
-
const tokens = words(value);
|
|
16816
|
+
const tokens = words$1(value);
|
|
16817
16817
|
const hashes = sampleIndexes(Math.max(0, tokens.length - SHINGLE_SIZE + 1), MAX_HASHES).map((index) => digest(tokens.slice(index, index + SHINGLE_SIZE).join(" ")));
|
|
16818
16818
|
return {
|
|
16819
16819
|
version: 1,
|
|
@@ -16825,6 +16825,11 @@ function contentSketch(value) {
|
|
|
16825
16825
|
}
|
|
16826
16826
|
//#endregion
|
|
16827
16827
|
//#region packages/core/dist/extract/main-content.js
|
|
16828
|
+
function parseDocument$1(html) {
|
|
16829
|
+
const { document } = parseHTML(html);
|
|
16830
|
+
if (document.documentElement?.localName === "html") return document;
|
|
16831
|
+
return parseHTML(`<!doctype html><html><head></head><body>${html}</body></html>`).document;
|
|
16832
|
+
}
|
|
16828
16833
|
function readableError(error) {
|
|
16829
16834
|
return error instanceof Error ? error.message : String(error);
|
|
16830
16835
|
}
|
|
@@ -16854,10 +16859,6 @@ function resolveDocumentUrls(document, baseUrl) {
|
|
|
16854
16859
|
}
|
|
16855
16860
|
}
|
|
16856
16861
|
function readabilityResult(fetchResult, requested, fallbackReason, fallbackDetail) {
|
|
16857
|
-
const { document } = parseHTML(fetchResult.html);
|
|
16858
|
-
const bodyText = document.body.textContent ?? "";
|
|
16859
|
-
const article = new Readability(document).parse();
|
|
16860
|
-
const text = article?.textContent ?? bodyText;
|
|
16861
16862
|
const fallback = requested !== "readability";
|
|
16862
16863
|
const diagnostics = {
|
|
16863
16864
|
requested,
|
|
@@ -16870,13 +16871,27 @@ function readabilityResult(fetchResult, requested, fallbackReason, fallbackDetai
|
|
|
16870
16871
|
fallbackDetail
|
|
16871
16872
|
} : {}
|
|
16872
16873
|
};
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16874
|
+
const warnings = fallback && fallbackDetail ? [`Defuddle extraction fell back to Readability: ${fallbackDetail}`] : [];
|
|
16875
|
+
try {
|
|
16876
|
+
const document = parseDocument$1(fetchResult.html);
|
|
16877
|
+
const bodyText = document.body?.textContent ?? "";
|
|
16878
|
+
const article = new Readability(document).parse();
|
|
16879
|
+
const text = article?.textContent ?? bodyText;
|
|
16880
|
+
return {
|
|
16881
|
+
text,
|
|
16882
|
+
excerpt: normalizedText(article?.excerpt),
|
|
16883
|
+
wordCount: countCjkAwareWords(text),
|
|
16884
|
+
diagnostics,
|
|
16885
|
+
warnings
|
|
16886
|
+
};
|
|
16887
|
+
} catch (error) {
|
|
16888
|
+
return {
|
|
16889
|
+
text: "",
|
|
16890
|
+
wordCount: 0,
|
|
16891
|
+
diagnostics,
|
|
16892
|
+
warnings: [...warnings, `The page's main content could not be read: ${readableError(error)}`]
|
|
16893
|
+
};
|
|
16894
|
+
}
|
|
16880
16895
|
}
|
|
16881
16896
|
function defaultParseDefuddle(document, options) {
|
|
16882
16897
|
return new Defuddle(document, options).parse();
|
|
@@ -16884,13 +16899,13 @@ function defaultParseDefuddle(document, options) {
|
|
|
16884
16899
|
function extractMainContent(fetchResult, extractor = "defuddle", dependencies = {}) {
|
|
16885
16900
|
if (extractor === "readability") return readabilityResult(fetchResult, extractor);
|
|
16886
16901
|
try {
|
|
16887
|
-
const
|
|
16902
|
+
const document = parseDocument$1(fetchResult.html);
|
|
16888
16903
|
resolveDocumentUrls(document, fetchResult.finalUrl);
|
|
16889
16904
|
const article = (dependencies.parseDefuddle ?? defaultParseDefuddle)(document, {
|
|
16890
16905
|
url: fetchResult.finalUrl,
|
|
16891
16906
|
useAsync: false
|
|
16892
16907
|
});
|
|
16893
|
-
const text = typeof article.content === "string" ? load(article.content).text() : document.body
|
|
16908
|
+
const text = typeof article.content === "string" ? load(article.content).text() : document.body?.textContent ?? "";
|
|
16894
16909
|
if (!text.trim()) return readabilityResult(fetchResult, extractor, "defuddle_empty", "Defuddle returned no main content");
|
|
16895
16910
|
const reportedWordCount = article.wordCount;
|
|
16896
16911
|
const hasValidWordCount = typeof reportedWordCount === "number" && Number.isFinite(reportedWordCount) && reportedWordCount >= 0;
|
|
@@ -16913,6 +16928,28 @@ function extractMainContent(fetchResult, extractor = "defuddle", dependencies =
|
|
|
16913
16928
|
return readabilityResult(fetchResult, extractor, "defuddle_error", readableError(error));
|
|
16914
16929
|
}
|
|
16915
16930
|
}
|
|
16931
|
+
const MARKDOWN_LIMIT = 2e4;
|
|
16932
|
+
async function extractMainMarkdown(fetchResult) {
|
|
16933
|
+
const document = parseDocument$1(fetchResult.html);
|
|
16934
|
+
resolveDocumentUrls(document, fetchResult.finalUrl);
|
|
16935
|
+
const { Defuddle: toMarkdown } = await import("defuddle/node");
|
|
16936
|
+
const markdown = ((await toMarkdown(document, fetchResult.finalUrl, {
|
|
16937
|
+
markdown: true,
|
|
16938
|
+
useAsync: false
|
|
16939
|
+
})).content ?? "").trim();
|
|
16940
|
+
if (markdown.length <= 2e4) return {
|
|
16941
|
+
markdown,
|
|
16942
|
+
characters: markdown.length,
|
|
16943
|
+
truncated: false
|
|
16944
|
+
};
|
|
16945
|
+
const cut = markdown.slice(0, MARKDOWN_LIMIT);
|
|
16946
|
+
const end = cut.lastIndexOf("\n");
|
|
16947
|
+
return {
|
|
16948
|
+
markdown: (end > 2e4 / 2 ? cut.slice(0, end) : cut).trimEnd(),
|
|
16949
|
+
characters: markdown.length,
|
|
16950
|
+
truncated: true
|
|
16951
|
+
};
|
|
16952
|
+
}
|
|
16916
16953
|
//#endregion
|
|
16917
16954
|
//#region packages/core/dist/extract/google-rich-results.js
|
|
16918
16955
|
const SUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -20688,7 +20725,7 @@ const QUERY_FLAG_METHOD = {
|
|
|
20688
20725
|
"navigational-other": "Names a domain that is not the property's host, ends with a spoken domain suffix or a login/site/app word, starts with a large platform name and adds no topic word, or is one short ASCII word that is not a brand term. Such queries usually look for another site."
|
|
20689
20726
|
}
|
|
20690
20727
|
};
|
|
20691
|
-
const FUNCTION_WORDS = /* @__PURE__ */ new Set([
|
|
20728
|
+
const FUNCTION_WORDS$1 = /* @__PURE__ */ new Set([
|
|
20692
20729
|
"a",
|
|
20693
20730
|
"about",
|
|
20694
20731
|
"an",
|
|
@@ -20753,7 +20790,7 @@ function isLikelyAutomatedQuery(query) {
|
|
|
20753
20790
|
if (query.trim().split(/\s+/).filter(Boolean).some((token) => IDENTIFIER_TOKEN.test(token))) return true;
|
|
20754
20791
|
const tokens = tokenize(query);
|
|
20755
20792
|
if (tokens.length >= 20) return true;
|
|
20756
|
-
if (tokens.length >= 8 && !/[^\p{ASCII}]/u.test(query) && !tokens.some((token) => FUNCTION_WORDS.has(token))) return true;
|
|
20793
|
+
if (tokens.length >= 8 && !/[^\p{ASCII}]/u.test(query) && !tokens.some((token) => FUNCTION_WORDS$1.has(token))) return true;
|
|
20757
20794
|
return false;
|
|
20758
20795
|
}
|
|
20759
20796
|
const FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -27692,7 +27729,7 @@ function analyzeCtrUnderperformersFromRows(input) {
|
|
|
27692
27729
|
//#endregion
|
|
27693
27730
|
//#region packages/core/dist/analyze/ctr-underperformers-report.js
|
|
27694
27731
|
const MAX_GSC_ROWS$3 = 1e5;
|
|
27695
|
-
const DEFAULT_DAYS$
|
|
27732
|
+
const DEFAULT_DAYS$4 = 28;
|
|
27696
27733
|
const MAX_DAYS$2 = 548;
|
|
27697
27734
|
function plural$4(count, singular, pluralLabel = `${singular}s`) {
|
|
27698
27735
|
return count === 1 ? singular : pluralLabel;
|
|
@@ -27715,7 +27752,7 @@ async function ctrUnderperformersReportWith(input, dependencies) {
|
|
|
27715
27752
|
const explicitRange = explicitDateRange(input, MAX_DAYS$2);
|
|
27716
27753
|
const days = explicitRange?.days ?? integerOption$1({
|
|
27717
27754
|
value: input.days,
|
|
27718
|
-
fallback: DEFAULT_DAYS$
|
|
27755
|
+
fallback: DEFAULT_DAYS$4,
|
|
27719
27756
|
minimum: 1,
|
|
27720
27757
|
maximum: MAX_DAYS$2,
|
|
27721
27758
|
label: "Days"
|
|
@@ -30920,7 +30957,7 @@ function quickWinTemplateSummaries(items) {
|
|
|
30920
30957
|
}
|
|
30921
30958
|
//#endregion
|
|
30922
30959
|
//#region packages/core/dist/analyze/site-diagnostics/quick-wins-report.js
|
|
30923
|
-
const DEFAULT_DAYS$
|
|
30960
|
+
const DEFAULT_DAYS$3 = 28;
|
|
30924
30961
|
const MAX_DAYS$1 = 548;
|
|
30925
30962
|
const DEFAULT_VERIFY_LIMIT$1 = 5;
|
|
30926
30963
|
const MAX_VERIFY_LIMIT = 100;
|
|
@@ -30942,7 +30979,7 @@ async function quickWinsReportWith(input, dependencies) {
|
|
|
30942
30979
|
const explicitRange = explicitDateRange(input, MAX_DAYS$1);
|
|
30943
30980
|
const days = explicitRange?.days ?? integerOption$1({
|
|
30944
30981
|
value: input.days,
|
|
30945
|
-
fallback: DEFAULT_DAYS$
|
|
30982
|
+
fallback: DEFAULT_DAYS$3,
|
|
30946
30983
|
minimum: 1,
|
|
30947
30984
|
maximum: MAX_DAYS$1,
|
|
30948
30985
|
label: "Days"
|
|
@@ -31366,7 +31403,7 @@ function analyzeStrikingDistanceRows(input) {
|
|
|
31366
31403
|
}
|
|
31367
31404
|
//#endregion
|
|
31368
31405
|
//#region packages/core/dist/analyze/striking-distance-report.js
|
|
31369
|
-
const DEFAULT_DAYS$
|
|
31406
|
+
const DEFAULT_DAYS$2 = 28;
|
|
31370
31407
|
const MAX_DAYS = 548;
|
|
31371
31408
|
const DEFAULT_VERIFY_LIMIT = 5;
|
|
31372
31409
|
const MAX_SOURCE_ROWS$1 = 1e5;
|
|
@@ -31375,7 +31412,7 @@ function boundedInteger$2(value, fallback, minimum, maximum) {
|
|
|
31375
31412
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
31376
31413
|
}
|
|
31377
31414
|
function reportDays(value) {
|
|
31378
|
-
if (value === void 0) return DEFAULT_DAYS$
|
|
31415
|
+
if (value === void 0) return DEFAULT_DAYS$2;
|
|
31379
31416
|
if (!Number.isInteger(value) || value < 1 || value > MAX_DAYS) throw new SeoError("INVALID_INPUT", `Days must be a whole number between 1 and ${MAX_DAYS}.`);
|
|
31380
31417
|
return value;
|
|
31381
31418
|
}
|
|
@@ -32815,20 +32852,394 @@ function analyzeInternalLinksFromRows(input) {
|
|
|
32815
32852
|
}
|
|
32816
32853
|
};
|
|
32817
32854
|
}
|
|
32818
|
-
|
|
32819
|
-
|
|
32820
|
-
|
|
32821
|
-
|
|
32822
|
-
|
|
32823
|
-
|
|
32855
|
+
const MAX_TERMS = 100;
|
|
32856
|
+
const PLACEMENT_SENTENCES = 2;
|
|
32857
|
+
const ANCHORS_PER_SENTENCE = 3;
|
|
32858
|
+
const MIN_SENTENCE_WORDS = 6;
|
|
32859
|
+
const NAME_WEIGHT = 2;
|
|
32860
|
+
const GENERIC_ANCHORS = /^(click here|here|read more|learn more|more|this|this post|this article|this page|link|website|check it out|see more|view|continue reading)$/i;
|
|
32861
|
+
function normalise(value) {
|
|
32862
|
+
return value.normalize("NFKD").replace(/\p{Diacritic}/gu, "").normalize("NFC").replace(/([\p{L}\p{N}])[’'`]([\p{L}\p{N}])/gu, "$1$2").toLowerCase().replace(/&/g, " and ").replace(/[^\p{L}\p{N}]+/gu, " ").replace(/\s+/g, " ").trim();
|
|
32863
|
+
}
|
|
32864
|
+
const words = (value) => normalise(value).split(" ").filter(Boolean);
|
|
32865
|
+
const FUNCTION_WORDS = new Set("a an and are as at be but by for from has have how if in into is it its of on or that the their then there these they this to was we were what when where which who why will with you your our can do does not just more most also than too very about after before over under again all any each other some such no nor only own same so up out off one two both every few several many much should would could because while since although though until unless whether however".split(" "));
|
|
32866
|
+
const NOT_PLURAL = /* @__PURE__ */ new Set([
|
|
32867
|
+
"always",
|
|
32868
|
+
"atlas",
|
|
32869
|
+
"canvas",
|
|
32870
|
+
"gas",
|
|
32871
|
+
"lens",
|
|
32872
|
+
"means",
|
|
32873
|
+
"news",
|
|
32874
|
+
"perhaps",
|
|
32875
|
+
"series",
|
|
32876
|
+
"species",
|
|
32877
|
+
"sometimes",
|
|
32878
|
+
"whereas",
|
|
32879
|
+
"yes",
|
|
32880
|
+
"this",
|
|
32881
|
+
"has",
|
|
32882
|
+
"was",
|
|
32883
|
+
"does",
|
|
32884
|
+
"its"
|
|
32885
|
+
]);
|
|
32886
|
+
const CHE = /* @__PURE__ */ new Set([
|
|
32887
|
+
"caches",
|
|
32888
|
+
"niches",
|
|
32889
|
+
"headaches",
|
|
32890
|
+
"cliches",
|
|
32891
|
+
"avalanches",
|
|
32892
|
+
"moustaches"
|
|
32893
|
+
]);
|
|
32894
|
+
const NOT_AGENT = /* @__PURE__ */ new Set([
|
|
32895
|
+
"customer",
|
|
32896
|
+
"together",
|
|
32897
|
+
"however",
|
|
32898
|
+
"whatever",
|
|
32899
|
+
"whenever",
|
|
32900
|
+
"wherever",
|
|
32901
|
+
"remember",
|
|
32902
|
+
"consider",
|
|
32903
|
+
"character",
|
|
32904
|
+
"register",
|
|
32905
|
+
"november",
|
|
32906
|
+
"december",
|
|
32907
|
+
"september",
|
|
32908
|
+
"october"
|
|
32909
|
+
]);
|
|
32910
|
+
function singular(word) {
|
|
32911
|
+
if (word.length <= 3 || !/^[a-z]+$/.test(word) || NOT_PLURAL.has(word) || /(ss|us|is)$/.test(word)) return word;
|
|
32912
|
+
if (word.endsWith("ies") && word.length > 4) return `${word.slice(0, -3)}y`;
|
|
32913
|
+
if (/(xes|ches|shes|sses)$/.test(word) && !CHE.has(word)) return word.slice(0, -2);
|
|
32914
|
+
return word.endsWith("s") ? word.slice(0, -1) : word;
|
|
32915
|
+
}
|
|
32916
|
+
function stem(word) {
|
|
32917
|
+
const one = singular(word);
|
|
32918
|
+
if (!/^[a-z]+$/.test(one)) return one;
|
|
32919
|
+
if (one.length >= 8 && one.endsWith("er") && !one.endsWith("eer") && one.at(-3) !== one.at(-4) && !NOT_AGENT.has(one)) return one.slice(0, -2);
|
|
32920
|
+
if (one.length > 5 && one.endsWith("ly")) return one.slice(0, -2);
|
|
32921
|
+
if (one.length > 5 && one.endsWith("ed") && !one.endsWith("eed")) return one.slice(0, -2);
|
|
32922
|
+
if (one.length > 6 && one.endsWith("ing")) return one.slice(0, -3);
|
|
32923
|
+
return one;
|
|
32924
|
+
}
|
|
32925
|
+
const terms = (text) => words(text).filter((word) => word.length > 1 && !FUNCTION_WORDS.has(word) && !/^\d+$/.test(word)).map(stem);
|
|
32926
|
+
const slugWords = (url) => {
|
|
32927
|
+
try {
|
|
32928
|
+
return new URL(url).pathname.split(/[/._-]+/).filter(Boolean).join(" ");
|
|
32929
|
+
} catch {
|
|
32930
|
+
return "";
|
|
32931
|
+
}
|
|
32932
|
+
};
|
|
32933
|
+
function keyTerms(page) {
|
|
32934
|
+
const key = /* @__PURE__ */ new Map();
|
|
32935
|
+
const title = page.title.replace(/\s+[|\u2013\u2014-]\s+[^|\u2013\u2014]*$/u, "");
|
|
32936
|
+
for (const term of terms(page.description)) key.set(term, 1);
|
|
32937
|
+
for (const term of terms(`${title} ${page.h1} ${lastSegmentWords(page.url)}`)) key.set(term, NAME_WEIGHT);
|
|
32938
|
+
for (const term of terms(siteName(page.url))) key.delete(term);
|
|
32939
|
+
return key;
|
|
32940
|
+
}
|
|
32941
|
+
function lastSegmentWords(url) {
|
|
32942
|
+
try {
|
|
32943
|
+
return (new URL(url).pathname.split("/").filter(Boolean).at(-1) ?? "").split(/[._-]+/).join(" ");
|
|
32944
|
+
} catch {
|
|
32945
|
+
return "";
|
|
32946
|
+
}
|
|
32824
32947
|
}
|
|
32825
|
-
function
|
|
32826
|
-
|
|
32827
|
-
|
|
32828
|
-
|
|
32948
|
+
function siteName(url) {
|
|
32949
|
+
try {
|
|
32950
|
+
return new URL(url).hostname.replace(/^www\./u, "").split(".")[0] ?? "";
|
|
32951
|
+
} catch {
|
|
32952
|
+
return "";
|
|
32953
|
+
}
|
|
32829
32954
|
}
|
|
32830
|
-
function
|
|
32831
|
-
|
|
32955
|
+
function termCounts(page) {
|
|
32956
|
+
const counts = {};
|
|
32957
|
+
const add = (text, weight) => {
|
|
32958
|
+
for (const term of terms(text)) counts[term] = (counts[term] ?? 0) + weight;
|
|
32959
|
+
};
|
|
32960
|
+
add(page.title, 3);
|
|
32961
|
+
add(page.h1, 3);
|
|
32962
|
+
add(slugWords(page.url), 3);
|
|
32963
|
+
add(page.description, 2);
|
|
32964
|
+
for (const heading of page.headings) add(heading, 2);
|
|
32965
|
+
for (const sentence of page.sentences) add(sentence.text, 1);
|
|
32966
|
+
return counts;
|
|
32967
|
+
}
|
|
32968
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
32969
|
+
"script",
|
|
32970
|
+
"style",
|
|
32971
|
+
"noscript",
|
|
32972
|
+
"svg",
|
|
32973
|
+
"template",
|
|
32974
|
+
"iframe",
|
|
32975
|
+
"select"
|
|
32976
|
+
]);
|
|
32977
|
+
const CHROME = /* @__PURE__ */ new Set([
|
|
32978
|
+
"nav",
|
|
32979
|
+
"header",
|
|
32980
|
+
"footer",
|
|
32981
|
+
"aside"
|
|
32982
|
+
]);
|
|
32983
|
+
const MAIN = /* @__PURE__ */ new Set(["main", "article"]);
|
|
32984
|
+
const BLOCK = new Set("p div li h1 h2 h3 h4 h5 h6 blockquote td th dd dt pre section article main aside header footer nav figcaption summary details tr ul ol table body form".split(" "));
|
|
32985
|
+
const VOID = /* @__PURE__ */ new Set([
|
|
32986
|
+
"br",
|
|
32987
|
+
"hr",
|
|
32988
|
+
"img",
|
|
32989
|
+
"input",
|
|
32990
|
+
"meta",
|
|
32991
|
+
"link",
|
|
32992
|
+
"wbr",
|
|
32993
|
+
"source",
|
|
32994
|
+
"col",
|
|
32995
|
+
"embed"
|
|
32996
|
+
]);
|
|
32997
|
+
const ENTITIES = {
|
|
32998
|
+
amp: "&",
|
|
32999
|
+
lt: "<",
|
|
33000
|
+
gt: ">",
|
|
33001
|
+
quot: "\"",
|
|
33002
|
+
apos: "'",
|
|
33003
|
+
nbsp: " ",
|
|
33004
|
+
hellip: "…",
|
|
33005
|
+
mdash: "—",
|
|
33006
|
+
ndash: "–",
|
|
33007
|
+
rsquo: "’",
|
|
33008
|
+
lsquo: "‘",
|
|
33009
|
+
rdquo: "”",
|
|
33010
|
+
ldquo: "“"
|
|
33011
|
+
};
|
|
33012
|
+
const TAG_ATTRS = String.raw`(?:[^>"'=]|=\s*"[^"]*"|=\s*'[^']*'|=(?!\s*["'])|["'])*`;
|
|
33013
|
+
const A_OPEN = "";
|
|
33014
|
+
const UNSPACED = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
|
|
33015
|
+
function decode(text) {
|
|
33016
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, code) => {
|
|
33017
|
+
if (code[0] === "#") {
|
|
33018
|
+
const hex = code[1] === "x" || code[1] === "X";
|
|
33019
|
+
const point = Number.parseInt(code.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
33020
|
+
return point > 0 && point <= 1114111 ? String.fromCodePoint(point) : "";
|
|
33021
|
+
}
|
|
33022
|
+
return ENTITIES[code.toLowerCase()] ?? match;
|
|
33023
|
+
});
|
|
33024
|
+
}
|
|
33025
|
+
const clean$1 = (value) => value.split(A_OPEN).join("").replace(/\s+/g, " ").trim();
|
|
33026
|
+
function splitSentences(block) {
|
|
33027
|
+
return block.replace(/\s+/g, " ").split(/(?<=[.!?…]["”’)]?)\s+(?=["“‘(]?[A-Z0-9])|(?<=[。!?])/).map((sentence) => sentence.trim()).filter((sentence) => sentence.length <= 500);
|
|
33028
|
+
}
|
|
33029
|
+
function pageSentences(html) {
|
|
33030
|
+
const sentences = [];
|
|
33031
|
+
const hasMain = /<(main|article)[\s>]|role=["']main["']/i.test(html);
|
|
33032
|
+
const stack = [];
|
|
33033
|
+
let skip = 0;
|
|
33034
|
+
let chrome = 0;
|
|
33035
|
+
let mainDepth = 0;
|
|
33036
|
+
let main = hasMain ? 0 : 1;
|
|
33037
|
+
let section = "";
|
|
33038
|
+
let buffer = "";
|
|
33039
|
+
let inAnchor = false;
|
|
33040
|
+
const flushRaw = (tag) => {
|
|
33041
|
+
const raw = buffer;
|
|
33042
|
+
const text = clean$1(raw);
|
|
33043
|
+
buffer = "";
|
|
33044
|
+
if (!text || sentences.length >= 250) return;
|
|
33045
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
33046
|
+
if (tag !== "h1") section = text;
|
|
33047
|
+
return;
|
|
33048
|
+
}
|
|
33049
|
+
for (const part of tag === "li" ? [raw] : splitSentences(raw)) {
|
|
33050
|
+
const sentence = clean$1(part);
|
|
33051
|
+
if (sentence.length >= 25 && sentence.split(" ").length >= 4 || UNSPACED.test(sentence) && sentence.length >= 10) {
|
|
33052
|
+
if (sentences.length >= 250) return;
|
|
33053
|
+
sentences.push({
|
|
33054
|
+
text: sentence,
|
|
33055
|
+
section,
|
|
33056
|
+
hasLink: part.includes(A_OPEN)
|
|
33057
|
+
});
|
|
33058
|
+
}
|
|
33059
|
+
}
|
|
33060
|
+
};
|
|
33061
|
+
const closeTo = (tag) => {
|
|
33062
|
+
if (!stack.some((entry) => entry.tag === tag)) return;
|
|
33063
|
+
while (stack.length) {
|
|
33064
|
+
const entry = stack.pop();
|
|
33065
|
+
if (!entry) break;
|
|
33066
|
+
flushRaw(entry.tag);
|
|
33067
|
+
if (entry.main) {
|
|
33068
|
+
main -= 1;
|
|
33069
|
+
mainDepth -= 1;
|
|
33070
|
+
}
|
|
33071
|
+
if (entry.chrome) chrome -= 1;
|
|
33072
|
+
if (entry.tag === tag) break;
|
|
33073
|
+
}
|
|
33074
|
+
};
|
|
33075
|
+
const tokens = html.matchAll(new RegExp(String.raw`<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<(\/?)([a-zA-Z][\w:-]*)(${TAG_ATTRS})>|([^<]+)`, "g"));
|
|
33076
|
+
for (const match of tokens) {
|
|
33077
|
+
const [whole, close, rawTag, rawAttrs = "", text] = match;
|
|
33078
|
+
if (text !== void 0) {
|
|
33079
|
+
if (!skip && main > 0 && chrome === 0) buffer += decode(text);
|
|
33080
|
+
continue;
|
|
33081
|
+
}
|
|
33082
|
+
if (!rawTag) continue;
|
|
33083
|
+
const tag = rawTag.toLowerCase();
|
|
33084
|
+
if (skip) {
|
|
33085
|
+
if (close && SKIP.has(tag)) skip -= 1;
|
|
33086
|
+
continue;
|
|
33087
|
+
}
|
|
33088
|
+
if (close) {
|
|
33089
|
+
if (tag === "a") inAnchor = false;
|
|
33090
|
+
else if (BLOCK.has(tag)) closeTo(tag);
|
|
33091
|
+
continue;
|
|
33092
|
+
}
|
|
33093
|
+
if (SKIP.has(tag)) {
|
|
33094
|
+
if (!whole.endsWith("/>")) skip += 1;
|
|
33095
|
+
continue;
|
|
33096
|
+
}
|
|
33097
|
+
if (tag === "br") buffer += " ";
|
|
33098
|
+
if (tag === "a" && !inAnchor && /\bhref\s*=/i.test(rawAttrs)) {
|
|
33099
|
+
inAnchor = true;
|
|
33100
|
+
if (main > 0 && chrome === 0) buffer += A_OPEN;
|
|
33101
|
+
}
|
|
33102
|
+
if (VOID.has(tag) || !BLOCK.has(tag)) continue;
|
|
33103
|
+
const parent = stack[stack.length - 1];
|
|
33104
|
+
if (parent) flushRaw(parent.tag);
|
|
33105
|
+
const role = /\brole\s*=\s*["']?main/i.test(rawAttrs);
|
|
33106
|
+
const isMain = MAIN.has(tag) || role;
|
|
33107
|
+
const isChrome = tag === "nav" || tag === "aside" || CHROME.has(tag) && mainDepth === 0;
|
|
33108
|
+
if (isMain) {
|
|
33109
|
+
main += 1;
|
|
33110
|
+
mainDepth += 1;
|
|
33111
|
+
}
|
|
33112
|
+
if (isChrome) chrome += 1;
|
|
33113
|
+
stack.push({
|
|
33114
|
+
tag,
|
|
33115
|
+
main: isMain,
|
|
33116
|
+
chrome: isChrome
|
|
33117
|
+
});
|
|
33118
|
+
}
|
|
33119
|
+
while (stack.length) {
|
|
33120
|
+
const last = stack[stack.length - 1];
|
|
33121
|
+
if (!last) break;
|
|
33122
|
+
closeTo(last.tag);
|
|
33123
|
+
}
|
|
33124
|
+
flushRaw("");
|
|
33125
|
+
return sentences;
|
|
33126
|
+
}
|
|
33127
|
+
var LinkCorpus = class {
|
|
33128
|
+
df = /* @__PURE__ */ new Map();
|
|
33129
|
+
size;
|
|
33130
|
+
constructor(pages) {
|
|
33131
|
+
this.size = pages.length;
|
|
33132
|
+
for (const counts of pages) for (const term in counts) this.df.set(term, (this.df.get(term) ?? 0) + 1);
|
|
33133
|
+
}
|
|
33134
|
+
idf(term) {
|
|
33135
|
+
return Math.log((this.size + 1) / ((this.df.get(term) ?? 1) + 1)) + 1;
|
|
33136
|
+
}
|
|
33137
|
+
vector(counts) {
|
|
33138
|
+
let norm = 0;
|
|
33139
|
+
const shared = [];
|
|
33140
|
+
for (const term in counts) {
|
|
33141
|
+
const weight = (1 + Math.log(counts[term] ?? 1)) * this.idf(term);
|
|
33142
|
+
norm += weight * weight;
|
|
33143
|
+
if ((this.df.get(term) ?? 0) >= 2) shared.push([term, weight]);
|
|
33144
|
+
}
|
|
33145
|
+
norm = Math.sqrt(norm) || 1;
|
|
33146
|
+
shared.sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0));
|
|
33147
|
+
return new Map(shared.slice(0, MAX_TERMS).map(([term, weight]) => [term, weight / norm]));
|
|
33148
|
+
}
|
|
33149
|
+
similarity(left, right) {
|
|
33150
|
+
let sum = 0;
|
|
33151
|
+
for (const [term, weight] of left) sum += weight * (right.get(term) ?? 0);
|
|
33152
|
+
return sum;
|
|
33153
|
+
}
|
|
33154
|
+
sentenceScore(sentence, key) {
|
|
33155
|
+
if (!key.size) return 0;
|
|
33156
|
+
const seen = new Set(terms(sentence));
|
|
33157
|
+
let hit = 0;
|
|
33158
|
+
let total = 0;
|
|
33159
|
+
for (const [term, place] of key) {
|
|
33160
|
+
const weight = this.idf(term) * place;
|
|
33161
|
+
total += weight;
|
|
33162
|
+
if (seen.has(term)) hit += weight;
|
|
33163
|
+
}
|
|
33164
|
+
return total ? hit / total : 0;
|
|
33165
|
+
}
|
|
33166
|
+
};
|
|
33167
|
+
function anchorCandidates(sentence, key, corpus, limit) {
|
|
33168
|
+
const scored = /* @__PURE__ */ new Map();
|
|
33169
|
+
const parts = sentence.split(/\s+/).map((word) => word.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter(Boolean);
|
|
33170
|
+
for (let size = 2; size <= 4; size += 1) for (let start = 0; start + size <= parts.length; start += 1) {
|
|
33171
|
+
const gram = parts.slice(start, start + size);
|
|
33172
|
+
const first = (gram[0] ?? "").toLowerCase();
|
|
33173
|
+
const last = (gram[size - 1] ?? "").toLowerCase();
|
|
33174
|
+
if (FUNCTION_WORDS.has(first) || FUNCTION_WORDS.has(last) || /^\d+$/.test(first) || /^\d+$/.test(last)) continue;
|
|
33175
|
+
const stems = terms(gram.join(" "));
|
|
33176
|
+
if (stems.length < 2) continue;
|
|
33177
|
+
const hits = stems.filter((term) => key.has(term)).length;
|
|
33178
|
+
const named = stems.reduce((sum, term) => sum + (key.get(term) ?? 0) * corpus.idf(term), 0);
|
|
33179
|
+
if (!stems.some((term) => key.get(term) === NAME_WEIGHT)) continue;
|
|
33180
|
+
if (Number(key.has(terms(first)[0] ?? "")) + Number(key.has(terms(last)[0] ?? "")) < 2) continue;
|
|
33181
|
+
const text = gram.join(" ");
|
|
33182
|
+
const lower = text.toLowerCase();
|
|
33183
|
+
if (GENERIC_ANCHORS.test(lower)) continue;
|
|
33184
|
+
const rarity = stems.reduce((sum, term) => sum + corpus.idf(term), 0);
|
|
33185
|
+
const score = named * (1 + hits / key.size) + rarity * .05 - (size === 4 ? .2 : 0);
|
|
33186
|
+
const existing = scored.get(lower);
|
|
33187
|
+
if (!existing || existing.score < score) scored.set(lower, {
|
|
33188
|
+
text,
|
|
33189
|
+
score
|
|
33190
|
+
});
|
|
33191
|
+
}
|
|
33192
|
+
return [...scored.values()].sort((left, right) => right.score - left.score || (left.text < right.text ? -1 : left.text > right.text ? 1 : 0)).slice(0, limit).map((item) => item.text);
|
|
33193
|
+
}
|
|
33194
|
+
function linkPlacement(source, target, corpus) {
|
|
33195
|
+
const key = keyTerms(target);
|
|
33196
|
+
const contentSimilarity = corpus.similarity(corpus.vector(termCounts(source)), corpus.vector(termCounts(target)));
|
|
33197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33198
|
+
const sentences = source.sentences.filter((sentence) => {
|
|
33199
|
+
if (seen.has(sentence.text)) return false;
|
|
33200
|
+
seen.add(sentence.text);
|
|
33201
|
+
return !sentence.hasLink && sentence.text.split(/\s+/).length >= MIN_SENTENCE_WORDS;
|
|
33202
|
+
}).map((sentence, index) => ({
|
|
33203
|
+
sentence,
|
|
33204
|
+
index,
|
|
33205
|
+
match: corpus.sentenceScore(sentence.text, key)
|
|
33206
|
+
})).filter((item) => item.match > 0).sort((left, right) => right.match - left.match || left.index - right.index).slice(0, PLACEMENT_SENTENCES).map(({ sentence, match }) => ({
|
|
33207
|
+
text: sentence.text,
|
|
33208
|
+
section: sentence.section,
|
|
33209
|
+
match: Math.round(match * 100) / 100,
|
|
33210
|
+
anchors: anchorCandidates(sentence.text, key, corpus, ANCHORS_PER_SENTENCE)
|
|
33211
|
+
})).filter((item) => item.anchors.length > 0);
|
|
33212
|
+
return {
|
|
33213
|
+
contentSimilarity: Math.round(contentSimilarity * 100) / 100,
|
|
33214
|
+
sentences
|
|
33215
|
+
};
|
|
33216
|
+
}
|
|
33217
|
+
function placementPage(input) {
|
|
33218
|
+
return {
|
|
33219
|
+
url: input.url,
|
|
33220
|
+
title: input.title ?? "",
|
|
33221
|
+
h1: input.headings.find((heading) => heading.level === 1)?.text ?? "",
|
|
33222
|
+
description: input.metaDescription ?? "",
|
|
33223
|
+
headings: input.headings.filter((heading) => heading.level === 2 || heading.level === 3).slice(0, 8).map((heading) => heading.text),
|
|
33224
|
+
sentences: pageSentences(input.html)
|
|
33225
|
+
};
|
|
33226
|
+
}
|
|
33227
|
+
//#endregion
|
|
33228
|
+
//#region packages/core/dist/analyze/internal-links-report.js
|
|
33229
|
+
const DAY$1 = 864e5;
|
|
33230
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
33231
|
+
function internalLinksReportRange(input, fallbackDays, now) {
|
|
33232
|
+
if (!input.startDate && !input.endDate) return {
|
|
33233
|
+
...finalGscDateRange(fallbackDays, now),
|
|
33234
|
+
days: fallbackDays
|
|
33235
|
+
};
|
|
33236
|
+
const { startDate, endDate } = input;
|
|
33237
|
+
if (!startDate || !endDate || !ISO_DAY.test(startDate) || !ISO_DAY.test(endDate) || startDate > endDate) throw new SeoError("INVALID_INPUT", "Send a start date and an end date together, in order, as YYYY-MM-DD.");
|
|
33238
|
+
return {
|
|
33239
|
+
startDate,
|
|
33240
|
+
endDate,
|
|
33241
|
+
days: Math.round((Date.parse(`${endDate}T00:00:00Z`) - Date.parse(`${startDate}T00:00:00Z`)) / DAY$1) + 1
|
|
33242
|
+
};
|
|
32832
33243
|
}
|
|
32833
33244
|
function completeInternalLinksSelection(analysis, runtime) {
|
|
32834
33245
|
return {
|
|
@@ -32886,9 +33297,8 @@ async function verifyInternalLinkTarget(input) {
|
|
|
32886
33297
|
let fetched;
|
|
32887
33298
|
try {
|
|
32888
33299
|
fetched = await input.dependencies.fetch(input.targetUrl, {
|
|
32889
|
-
js:
|
|
32890
|
-
refresh: input.refresh
|
|
32891
|
-
rate: input.rate
|
|
33300
|
+
js: "auto",
|
|
33301
|
+
refresh: input.refresh
|
|
32892
33302
|
});
|
|
32893
33303
|
} catch (error) {
|
|
32894
33304
|
return {
|
|
@@ -32958,7 +33368,12 @@ async function verifyInternalLinkTarget(input) {
|
|
|
32958
33368
|
verification: fatalSignals.length ? "technical-issue" : "verified",
|
|
32959
33369
|
technicalSignals: signals,
|
|
32960
33370
|
fetchDiagnostics: fetched.diagnostics,
|
|
32961
|
-
warnings
|
|
33371
|
+
warnings,
|
|
33372
|
+
text: placementPage({
|
|
33373
|
+
...page,
|
|
33374
|
+
url: page.finalUrl,
|
|
33375
|
+
html: fetched.html
|
|
33376
|
+
})
|
|
32962
33377
|
};
|
|
32963
33378
|
}
|
|
32964
33379
|
function linksToAliases(page, aliases) {
|
|
@@ -32992,9 +33407,8 @@ async function verifyInternalLinkCandidate(input) {
|
|
|
32992
33407
|
let fetched;
|
|
32993
33408
|
try {
|
|
32994
33409
|
fetched = await input.dependencies.fetch(input.candidate.sourceUrl, {
|
|
32995
|
-
js:
|
|
32996
|
-
refresh: input.refresh
|
|
32997
|
-
rate: input.rate
|
|
33410
|
+
js: "auto",
|
|
33411
|
+
refresh: input.refresh
|
|
32998
33412
|
});
|
|
32999
33413
|
} catch (error) {
|
|
33000
33414
|
return {
|
|
@@ -33084,12 +33498,21 @@ async function verifyInternalLinkCandidate(input) {
|
|
|
33084
33498
|
confidence
|
|
33085
33499
|
}
|
|
33086
33500
|
},
|
|
33501
|
+
text: placementPage({
|
|
33502
|
+
...page,
|
|
33503
|
+
url: page.finalUrl,
|
|
33504
|
+
html: fetched.html
|
|
33505
|
+
}),
|
|
33087
33506
|
warnings
|
|
33088
33507
|
};
|
|
33089
33508
|
}
|
|
33090
33509
|
//#endregion
|
|
33091
33510
|
//#region packages/core/dist/analyze/internal-links.js
|
|
33092
33511
|
const MAX_GSC_ROWS = 1e5;
|
|
33512
|
+
const DEFAULT_DAYS$1 = 90;
|
|
33513
|
+
const RESULT_LIMIT = 20;
|
|
33514
|
+
const CHECK_LIMIT = 40;
|
|
33515
|
+
const MIN_IMPRESSIONS = 1;
|
|
33093
33516
|
const defaultDependencies$2 = {
|
|
33094
33517
|
searchAnalytics: querySearchAnalytics,
|
|
33095
33518
|
fetch: fetchPage,
|
|
@@ -33100,43 +33523,17 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
33100
33523
|
const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
|
|
33101
33524
|
targetPage.hash = "";
|
|
33102
33525
|
const targetUrl = targetPage.toString();
|
|
33103
|
-
const
|
|
33104
|
-
|
|
33105
|
-
|
|
33106
|
-
minimum: 1,
|
|
33107
|
-
maximum: 548,
|
|
33108
|
-
label: "days"
|
|
33109
|
-
});
|
|
33110
|
-
const limit = internalLinksIntegerOption({
|
|
33111
|
-
value: input.limit,
|
|
33112
|
-
fallback: 20,
|
|
33113
|
-
minimum: 1,
|
|
33114
|
-
maximum: 100,
|
|
33115
|
-
label: "limit"
|
|
33116
|
-
});
|
|
33117
|
-
const checkLimit = internalLinksIntegerOption({
|
|
33118
|
-
value: input.checkLimit,
|
|
33119
|
-
fallback: Math.max(limit, 40),
|
|
33120
|
-
minimum: 1,
|
|
33121
|
-
maximum: 200,
|
|
33122
|
-
label: "checkLimit"
|
|
33123
|
-
});
|
|
33124
|
-
const minImpressions = internalLinksNumberOption({
|
|
33125
|
-
value: input.minImpressions,
|
|
33126
|
-
fallback: 1,
|
|
33127
|
-
minimum: 0,
|
|
33128
|
-
maximum: 1e9,
|
|
33129
|
-
label: "minImpressions"
|
|
33130
|
-
});
|
|
33526
|
+
const limit = RESULT_LIMIT;
|
|
33527
|
+
const checkLimit = CHECK_LIMIT;
|
|
33528
|
+
const minImpressions = MIN_IMPRESSIONS;
|
|
33131
33529
|
const now = dependencies.now();
|
|
33132
|
-
const range = internalLinksReportRange(
|
|
33530
|
+
const { days, ...range } = internalLinksReportRange(input, DEFAULT_DAYS$1, now);
|
|
33531
|
+
const brandTerms = deriveBrandTerms({ siteUrl: input.site });
|
|
33133
33532
|
const ledger = new SessionLedger();
|
|
33134
33533
|
const target = await verifyInternalLinkTarget({
|
|
33135
33534
|
site: input.site,
|
|
33136
33535
|
targetUrl,
|
|
33137
|
-
js: input.js,
|
|
33138
33536
|
refresh: input.refresh,
|
|
33139
|
-
rate: input.rate,
|
|
33140
33537
|
dependencies
|
|
33141
33538
|
});
|
|
33142
33539
|
const targetRequests = await Promise.all(target.aliases.map(async (alias) => {
|
|
@@ -33168,8 +33565,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
33168
33565
|
site: input.site,
|
|
33169
33566
|
targetAliases: target.aliases,
|
|
33170
33567
|
minImpressions,
|
|
33171
|
-
brandTerms
|
|
33172
|
-
includeBrand: input.includeBrand
|
|
33568
|
+
brandTerms
|
|
33173
33569
|
});
|
|
33174
33570
|
const queryCandidates = target.verification === "verified" && preliminary.selection.targetEligibleQueries > 0;
|
|
33175
33571
|
const sourceResult = queryCandidates ? await dependencies.searchAnalytics(input.site, {
|
|
@@ -33190,10 +33586,9 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
33190
33586
|
site: input.site,
|
|
33191
33587
|
targetAliases: target.aliases,
|
|
33192
33588
|
minImpressions,
|
|
33193
|
-
brandTerms
|
|
33194
|
-
includeBrand: input.includeBrand
|
|
33589
|
+
brandTerms
|
|
33195
33590
|
});
|
|
33196
|
-
const
|
|
33591
|
+
const found = [];
|
|
33197
33592
|
const warnings = [...target.warnings];
|
|
33198
33593
|
let attemptedSources = 0;
|
|
33199
33594
|
let checkedSources = 0;
|
|
@@ -33202,25 +33597,27 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
33202
33597
|
let selfAliasExclusions = 0;
|
|
33203
33598
|
let failedChecks = 0;
|
|
33204
33599
|
for (const candidate of analysis.candidates) {
|
|
33205
|
-
if (attemptedSources >= checkLimit ||
|
|
33600
|
+
if (attemptedSources >= checkLimit || found.length >= limit) break;
|
|
33206
33601
|
attemptedSources += 1;
|
|
33207
33602
|
const verified = await verifyInternalLinkCandidate({
|
|
33208
33603
|
site: input.site,
|
|
33209
33604
|
candidate,
|
|
33210
33605
|
target,
|
|
33211
|
-
js: input.js,
|
|
33212
33606
|
refresh: input.refresh,
|
|
33213
|
-
rate: input.rate,
|
|
33214
33607
|
dependencies
|
|
33215
33608
|
});
|
|
33216
33609
|
warnings.push(...verified.warnings);
|
|
33217
|
-
if (verified.item)
|
|
33610
|
+
if (verified.item) found.push({
|
|
33611
|
+
item: verified.item,
|
|
33612
|
+
text: verified.text
|
|
33613
|
+
});
|
|
33218
33614
|
if (verified.exclusion === "existing-link") existingLinkExclusions += 1;
|
|
33219
33615
|
if (verified.exclusion === "technical") technicalExclusions += 1;
|
|
33220
33616
|
if (verified.exclusion === "self-alias") selfAliasExclusions += 1;
|
|
33221
33617
|
if (verified.exclusion === "failed") failedChecks += 1;
|
|
33222
33618
|
else checkedSources += 1;
|
|
33223
33619
|
}
|
|
33620
|
+
const items = placeLinks(found, target.text);
|
|
33224
33621
|
const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
|
|
33225
33622
|
const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS);
|
|
33226
33623
|
const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS;
|
|
@@ -33282,13 +33679,14 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
33282
33679
|
},
|
|
33283
33680
|
methodology: {
|
|
33284
33681
|
id: "gsc_internal_link_candidates",
|
|
33285
|
-
version:
|
|
33682
|
+
version: 4,
|
|
33286
33683
|
lexicalTargetLimit: 50,
|
|
33287
33684
|
matchedQueryEvidenceLimit: 10,
|
|
33288
33685
|
observedLinkEvidenceLimit: 20,
|
|
33289
33686
|
matching: "pairwise_exact_then_precision_lexical",
|
|
33290
33687
|
ranking: "exact_matches_then_matched_query_impressions_then_relevance",
|
|
33291
|
-
contextualPlacementVerified: true
|
|
33688
|
+
contextualPlacementVerified: true,
|
|
33689
|
+
placement: "tfidf_sentence_match_then_phrase_anchors"
|
|
33292
33690
|
},
|
|
33293
33691
|
filters: {
|
|
33294
33692
|
minImpressions,
|
|
@@ -33320,18 +33718,20 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
33320
33718
|
failedChecks,
|
|
33321
33719
|
uncheckedCandidates,
|
|
33322
33720
|
matchedQueryImpressions: items.reduce((sum, item) => sum + item.matchedQueryImpressions, 0),
|
|
33323
|
-
brandFiltering:
|
|
33721
|
+
brandFiltering: "excluded",
|
|
33324
33722
|
verdict: summaryVerdict
|
|
33325
33723
|
},
|
|
33326
33724
|
items,
|
|
33327
33725
|
warnings: structuredWarnings,
|
|
33328
33726
|
caveats: [
|
|
33329
|
-
`Date window: ${range.startDate} to ${range.endDate} (${days} days), using final GSC data where available
|
|
33727
|
+
`Date window: ${range.startDate} to ${range.endDate} (${days} days), using final GSC data where available.${input.startDate ? "" : ` Without dates the window is the last ${DEFAULT_DAYS$1} days.`}`,
|
|
33728
|
+
`Limits: up to ${checkLimit} source pages are checked live and up to ${limit} are returned. Queries need at least ${minImpressions} impression.`,
|
|
33330
33729
|
`Selection: ${analysis.selection.targetEligibleQueries} eligible target queries, ${analysis.selection.candidateUrls} matched source pages, ${attemptedSources} attempted, ${checkedSources} successfully checked, ${items.length} returned.`,
|
|
33331
|
-
`
|
|
33730
|
+
`Brand queries were left out. Brand terms found from the domain: ${brandTerms.join(", ") || "none"}.`,
|
|
33332
33731
|
"GSC query rows omit anonymized queries and lower-volume rows. A 100,000-row cap is reported separately when reached.",
|
|
33333
33732
|
"Exact query overlap is medium-confidence affinity evidence; lexical matches are low-confidence review evidence. Neither predicts traffic lift or link equity.",
|
|
33334
33733
|
"The fetch checks HTML link placement, but it cannot prove editorial relevance. Review intent and reader usefulness before adding a link.",
|
|
33734
|
+
"Placement sentences are the source sentences without a link that share the most rare words with the target title, h1, description and URL. Anchors are phrases from those sentences. Content similarity compares the words of the checked pages only. All three are text heuristics, not ranking evidence.",
|
|
33335
33735
|
uncheckedCandidates ? `${uncheckedCandidates} matched source page${uncheckedCandidates === 1 ? " was" : "s were"} not checked because the output or check limit was reached.` : "",
|
|
33336
33736
|
failedChecks ? `${failedChecks} source verification attempt${failedChecks === 1 ? "" : "s"} failed. Failed fetches or extraction attempts are not counted as checked pages and do not support a missing-link conclusion.` : ""
|
|
33337
33737
|
].filter(Boolean),
|
|
@@ -33339,6 +33739,29 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
33339
33739
|
ledgerSummary: ledger.summary()
|
|
33340
33740
|
};
|
|
33341
33741
|
}
|
|
33742
|
+
function placeLinks(found, target) {
|
|
33743
|
+
const corpus = new LinkCorpus([target, ...found.map((entry) => entry.text)].filter((page) => Boolean(page)).map(termCounts));
|
|
33744
|
+
const placed = found.map(({ item, text }) => {
|
|
33745
|
+
const placement = target && text ? linkPlacement(text, target, corpus) : {
|
|
33746
|
+
contentSimilarity: 0,
|
|
33747
|
+
sentences: []
|
|
33748
|
+
};
|
|
33749
|
+
const best = placement.sentences[0];
|
|
33750
|
+
const recommendation = best ? {
|
|
33751
|
+
...item.recommendation,
|
|
33752
|
+
action: `${item.recommendation.action} It fits in: "${best.text}" Anchor text could be ${best.anchors.map((anchor) => `"${anchor}"`).join(" or ")}.`
|
|
33753
|
+
} : {
|
|
33754
|
+
...item.recommendation,
|
|
33755
|
+
action: `${item.recommendation.action} No sentence on this page names the target, so a link here needs new text.`
|
|
33756
|
+
};
|
|
33757
|
+
return {
|
|
33758
|
+
...item,
|
|
33759
|
+
placement,
|
|
33760
|
+
recommendation
|
|
33761
|
+
};
|
|
33762
|
+
});
|
|
33763
|
+
return [...placed.filter((item) => item.placement.sentences.length > 0), ...placed.filter((item) => item.placement.sentences.length === 0)];
|
|
33764
|
+
}
|
|
33342
33765
|
//#endregion
|
|
33343
33766
|
//#region packages/core/dist/analyze/url-templates.js
|
|
33344
33767
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -37164,7 +37587,8 @@ async function runAnalyticsRows(input, google, options) {
|
|
|
37164
37587
|
//#endregion
|
|
37165
37588
|
//#region packages/core/dist/commands/bing.js
|
|
37166
37589
|
const BING_LIMITS = ["Bing and Google count clicks, impressions and positions their own way. Their numbers sit side by side here and are never added together.", "Bing lists only its top queries and pages, grouped by week. A query or page missing from Bing’s list is unknown, not zero."];
|
|
37167
|
-
const
|
|
37590
|
+
const CLI_LINK = "If you have a shell, link the CLI to the same account once: run `npx -y seo login`, show the person the link it prints, and after they approve it run `npx -y seo login` again. `npx -y seo` commands then use the same Google accounts, Bing and keys.";
|
|
37591
|
+
const LOCAL_BING = `Bing blocks the hosted server, so Bing data comes from the CLI on a machine. ${CLI_LINK} Connect Bing on the account page if it is not connected, then run \`npx -y seo report --site <site> --section bing --json\`.`;
|
|
37168
37592
|
const NO_BING_HERE = "Bing: not included here, so these totals show Google only. Some sites get most of their search visits from Bing; the CLI reads them with the same account: `npx -y seo login` once, then `npx -y seo report --site <site> --section bing --json`.";
|
|
37169
37593
|
const BING_HOME = "https://www.bing.com/webmasters";
|
|
37170
37594
|
const noBingData = (site) => `Bing has no data for ${site} yet. Newly added or newly verified sites take a few days to show data in Bing Webmaster Tools.`;
|
|
@@ -38450,11 +38874,13 @@ async function runPage(input, deps) {
|
|
|
38450
38874
|
}]
|
|
38451
38875
|
}]
|
|
38452
38876
|
});
|
|
38453
|
-
const
|
|
38877
|
+
const full = input.detail === "full";
|
|
38878
|
+
const [[facts], indexed, now, then, markdown] = await Promise.all([
|
|
38454
38879
|
deps.checkPages([input.url]),
|
|
38455
38880
|
deps.inspect(input.site, input.url),
|
|
38456
38881
|
query(range),
|
|
38457
|
-
query(before)
|
|
38882
|
+
query(before),
|
|
38883
|
+
full && deps.pageMarkdown ? deps.pageMarkdown(input.url).catch((error) => ({ error: error instanceof Error ? error.message : String(error) })) : null
|
|
38458
38884
|
]);
|
|
38459
38885
|
const old = new Map(then.rows.map((row) => [row.keys[0], row]));
|
|
38460
38886
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -38489,23 +38915,32 @@ async function runPage(input, deps) {
|
|
|
38489
38915
|
const sum = (list, key) => list.reduce((total, row) => total + row[key], 0);
|
|
38490
38916
|
const offset = input.offset ?? 0;
|
|
38491
38917
|
return {
|
|
38492
|
-
text:
|
|
38493
|
-
|
|
38494
|
-
|
|
38495
|
-
|
|
38496
|
-
|
|
38497
|
-
|
|
38498
|
-
|
|
38499
|
-
|
|
38500
|
-
|
|
38501
|
-
|
|
38502
|
-
|
|
38503
|
-
|
|
38504
|
-
|
|
38505
|
-
|
|
38506
|
-
|
|
38507
|
-
|
|
38508
|
-
|
|
38918
|
+
text: [
|
|
38919
|
+
rowsText({
|
|
38920
|
+
title: `${input.url} in ${input.site}, ${range.startDate} to ${range.endDate} against ${before.startDate} to ${before.endDate}.`,
|
|
38921
|
+
notes: [
|
|
38922
|
+
`Live page (fetched now, no redirects followed):\n${facts ? pageFactsTable([facts], null) : "Not fetched."}`,
|
|
38923
|
+
indexed ? `Google index status:\n${table([indexed], Object.keys(indexed), null)}` : "Google index status: not available for this URL.",
|
|
38924
|
+
`Query totals: clicks ${sum(now.rows, "clicks")} (was ${sum(then.rows, "clicks")}), impressions ${sum(now.rows, "impressions")} (was ${sum(then.rows, "impressions")}). Search Console hides rare queries, so these can be lower than the page total.`,
|
|
38925
|
+
deps.pageMarkdown ? `Main content as Markdown: ${deps.call("page", {
|
|
38926
|
+
url: input.url,
|
|
38927
|
+
detail: "full"
|
|
38928
|
+
})}.` : `Main content as Markdown: only in the CLI and the local MCP server, with \`npx -y seo page --url ${input.url} --detail full\`.`
|
|
38929
|
+
],
|
|
38930
|
+
rows,
|
|
38931
|
+
offset,
|
|
38932
|
+
limit: 50,
|
|
38933
|
+
detail: input.detail ?? "summary",
|
|
38934
|
+
more: (next) => deps.call("page", {
|
|
38935
|
+
url: input.url,
|
|
38936
|
+
detail: "rows",
|
|
38937
|
+
offset: next
|
|
38938
|
+
})
|
|
38939
|
+
}),
|
|
38940
|
+
...full ? [markdownText(markdown, input.url)] : [],
|
|
38941
|
+
`Next: ${deps.call("research")} serp with the top query to see who else ranks.`,
|
|
38942
|
+
FACTS_ONLY
|
|
38943
|
+
].join("\n\n"),
|
|
38509
38944
|
data: {
|
|
38510
38945
|
url: input.url,
|
|
38511
38946
|
range,
|
|
@@ -38513,10 +38948,17 @@ async function runPage(input, deps) {
|
|
|
38513
38948
|
page: facts ?? null,
|
|
38514
38949
|
index: indexed,
|
|
38515
38950
|
totalQueries: rows.length,
|
|
38516
|
-
queries: rows.slice(offset, offset + 50)
|
|
38951
|
+
queries: rows.slice(offset, offset + 50),
|
|
38952
|
+
...full ? { markdown } : {}
|
|
38517
38953
|
}
|
|
38518
38954
|
};
|
|
38519
38955
|
}
|
|
38956
|
+
function markdownText(markdown, url) {
|
|
38957
|
+
if (!markdown) return `Main content as Markdown: not on this server; it runs in the CLI and the local MCP server. ${CLI_LINK} Then run \`npx -y seo page --url ${url} --detail full\`.`;
|
|
38958
|
+
if ("error" in markdown) return `Main content as Markdown: the page could not be read (${markdown.error}).`;
|
|
38959
|
+
if (!markdown.markdown) return "Main content as Markdown: the page has no main content to read.";
|
|
38960
|
+
return `Main content as Markdown (live page${markdown.truncated ? `, cut to ${markdown.markdown.length.toLocaleString("en-US")} of ${markdown.characters.toLocaleString("en-US")} characters` : ""}):\n\n${markdown.markdown}`;
|
|
38961
|
+
}
|
|
38520
38962
|
async function runCompare(input, deps) {
|
|
38521
38963
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.date)) throw new CommandError("INVALID_INPUT", "The date must be YYYY-MM-DD.");
|
|
38522
38964
|
const last = latestFinalGscDate(deps.now?.() ?? /* @__PURE__ */ new Date());
|
|
@@ -39082,6 +39524,7 @@ function nodeCommandDeps(options) {
|
|
|
39082
39524
|
return null;
|
|
39083
39525
|
}
|
|
39084
39526
|
},
|
|
39527
|
+
pageMarkdown: async (url) => extractMainMarkdown(await fetchPage(url, { refresh: options.refresh })),
|
|
39085
39528
|
call: options.call,
|
|
39086
39529
|
bing: localBing,
|
|
39087
39530
|
bingConnect: LOCAL_BING_CONNECT
|
|
@@ -41116,46 +41559,18 @@ function registerOpportunityTools(server, dependencies = {}) {
|
|
|
41116
41559
|
server.registerTool("seo_internal_links", {
|
|
41117
41560
|
description: "Find verified internal-link review candidates for a target URL",
|
|
41118
41561
|
inputSchema: {
|
|
41119
|
-
...mcpReportInputSchema([
|
|
41120
|
-
"site",
|
|
41121
|
-
"days",
|
|
41122
|
-
"limit",
|
|
41123
|
-
"checkLimit",
|
|
41124
|
-
"minImpressions",
|
|
41125
|
-
"includeBrand",
|
|
41126
|
-
"js",
|
|
41127
|
-
"fetchConcurrency",
|
|
41128
|
-
"fetchIntervalCap",
|
|
41129
|
-
"fetchIntervalMs",
|
|
41130
|
-
"refresh"
|
|
41131
|
-
]),
|
|
41562
|
+
...mcpReportInputSchema(["site", "refresh"]),
|
|
41132
41563
|
targetUrl: z$1.string().url().refine((value) => /^https?:\/\//.test(value), "Use an HTTP(S) URL."),
|
|
41133
|
-
|
|
41134
|
-
|
|
41135
|
-
checkLimit: z$1.number().int().min(1).max(200).optional(),
|
|
41136
|
-
minImpressions: z$1.number().min(0).max(1e9).optional(),
|
|
41137
|
-
brandTerms: z$1.array(z$1.string().trim().min(1).max(200)).max(20).optional(),
|
|
41138
|
-
fetchConcurrency: z$1.number().int().min(1).max(16).optional(),
|
|
41139
|
-
fetchIntervalCap: z$1.number().int().min(1).max(60).optional(),
|
|
41140
|
-
fetchIntervalMs: z$1.number().int().min(100).max(6e4).optional()
|
|
41564
|
+
startDate: z$1.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
41565
|
+
endDate: z$1.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
|
|
41141
41566
|
}
|
|
41142
|
-
}, async ({ site, targetUrl,
|
|
41567
|
+
}, async ({ site, targetUrl, startDate, endDate, refresh }) => {
|
|
41143
41568
|
try {
|
|
41144
41569
|
const result = await (dependencies.internalLinksReport ?? internalLinksReport)({
|
|
41145
41570
|
site,
|
|
41146
41571
|
targetUrl,
|
|
41147
|
-
|
|
41148
|
-
|
|
41149
|
-
checkLimit,
|
|
41150
|
-
minImpressions,
|
|
41151
|
-
brandTerms,
|
|
41152
|
-
includeBrand,
|
|
41153
|
-
js: resolveJsOption(js, void 0),
|
|
41154
|
-
rate: fetchRateInput({
|
|
41155
|
-
fetchConcurrency,
|
|
41156
|
-
fetchIntervalCap,
|
|
41157
|
-
fetchIntervalMs
|
|
41158
|
-
}),
|
|
41572
|
+
startDate,
|
|
41573
|
+
endDate,
|
|
41159
41574
|
refresh
|
|
41160
41575
|
});
|
|
41161
41576
|
return toolSuccess(result.summary.verdict, result);
|
|
@@ -41464,12 +41879,14 @@ const REPORT_DEPTH = {
|
|
|
41464
41879
|
readOrder: [
|
|
41465
41880
|
"dataStatus, target verification, aliases, canonical, and technical signals",
|
|
41466
41881
|
"source completeness and selection attempted, checked, failed, unchecked, and exclusions",
|
|
41467
|
-
"per-item exact versus lexical matches and linkEvidence"
|
|
41882
|
+
"per-item exact versus lexical matches and linkEvidence",
|
|
41883
|
+
"per-item placement: the sentence where a link fits, its anchors, and contentSimilarity"
|
|
41468
41884
|
],
|
|
41469
41885
|
doNotClaim: [
|
|
41470
|
-
"
|
|
41886
|
+
"The live check stops at 40 source pages, so unchecked candidates can remain and absence is not proven.",
|
|
41471
41887
|
"Query overlap does not prove a link belongs or that adding one improves rankings.",
|
|
41472
|
-
"Priority is a heuristic, not estimated impact."
|
|
41888
|
+
"Priority is a heuristic, not estimated impact.",
|
|
41889
|
+
"Placement sentences, anchors and contentSimilarity are text heuristics; they do not prove the link helps a reader."
|
|
41473
41890
|
],
|
|
41474
41891
|
verify: "Read the source passage, add one contextual link if the target is genuinely useful, and re-crawl to confirm it.",
|
|
41475
41892
|
related: [
|
|
@@ -42668,14 +43085,20 @@ function registerSharedTools(server, options = {}) {
|
|
|
42668
43085
|
})));
|
|
42669
43086
|
server.registerTool("seo_internal_links", {
|
|
42670
43087
|
title: "Internal link opportunities",
|
|
42671
|
-
description: "Pages of the same site that could link to a target page, from Search Console and live page checks on this machine.",
|
|
42672
|
-
inputSchema: {
|
|
43088
|
+
description: "Pages of the same site that could link to a target page, from Search Console and live page checks on this machine. Each one names the sentence where the link fits and anchor text from it. Takes the target URL and optional dates (default: the last 90 days).",
|
|
43089
|
+
inputSchema: {
|
|
43090
|
+
url: z$1.url(),
|
|
43091
|
+
start_date: z$1.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional; send with end_date."),
|
|
43092
|
+
end_date: z$1.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD. Optional; send with start_date.")
|
|
43093
|
+
},
|
|
42673
43094
|
annotations: readOnly
|
|
42674
|
-
}, async ({ url }) => counted("internal-links", {}, options.telemetry, async () => {
|
|
43095
|
+
}, async ({ url, start_date, end_date }) => counted("internal-links", {}, options.telemetry, async () => {
|
|
42675
43096
|
try {
|
|
42676
43097
|
return await executeReport("internal-links", {
|
|
42677
43098
|
site: await resolveLocalSite(url),
|
|
42678
|
-
targetUrl: url
|
|
43099
|
+
targetUrl: url,
|
|
43100
|
+
startDate: start_date,
|
|
43101
|
+
endDate: end_date
|
|
42679
43102
|
});
|
|
42680
43103
|
} catch (error) {
|
|
42681
43104
|
return toolError(localError(error));
|