seo 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/dist/cli.js +77 -78
- package/dist/cli.js.map +1 -1
- package/dist/{dist-nmppE37y.js → dist-S2OSIAML.js} +90 -80
- package/dist/dist-S2OSIAML.js.map +1 -0
- package/dist/{dist-C_Ezkcm5.js → dist-aHJsXHVF.js} +1 -1
- package/dist/index.d.ts +66 -51
- package/dist/index.js +522 -88
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +599 -153
- package/dist/mcp.js.map +1 -1
- package/dist/{package-CwLtPH6B.js → package-BaQwwfSG.js} +2 -2
- package/dist/{package-CwLtPH6B.js.map → package-BaQwwfSG.js.map} +1 -1
- package/package.json +2 -1
- package/dist/dist-nmppE37y.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { n as load, t as version } from "./package-
|
|
2
|
+
import { n as load, t as version } from "./package-BaQwwfSG.js";
|
|
3
3
|
import { SEO_PROVIDER_API_VERSION } from "./provider-sdk.js";
|
|
4
4
|
import crypto, { createHash, randomUUID, sign } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -4694,7 +4694,7 @@ function extractCanonicalEvidence($, headers, baseUrl) {
|
|
|
4694
4694
|
//#region packages/core/src/extract/content-sketch.ts
|
|
4695
4695
|
const SHINGLE_SIZE = 5;
|
|
4696
4696
|
const MAX_HASHES = 32;
|
|
4697
|
-
function words(value) {
|
|
4697
|
+
function words$1(value) {
|
|
4698
4698
|
return value.normalize("NFKC").toLocaleLowerCase("en").match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
4699
4699
|
}
|
|
4700
4700
|
function sampleIndexes(total, limit) {
|
|
@@ -4705,7 +4705,7 @@ function digest(value) {
|
|
|
4705
4705
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
4706
4706
|
}
|
|
4707
4707
|
function contentSketch(value) {
|
|
4708
|
-
const tokens = words(value);
|
|
4708
|
+
const tokens = words$1(value);
|
|
4709
4709
|
const hashes = sampleIndexes(Math.max(0, tokens.length - SHINGLE_SIZE + 1), MAX_HASHES).map((index) => digest(tokens.slice(index, index + SHINGLE_SIZE).join(" ")));
|
|
4710
4710
|
return {
|
|
4711
4711
|
version: 1,
|
|
@@ -4717,6 +4717,11 @@ function contentSketch(value) {
|
|
|
4717
4717
|
}
|
|
4718
4718
|
//#endregion
|
|
4719
4719
|
//#region packages/core/src/extract/main-content.ts
|
|
4720
|
+
function parseDocument$1(html) {
|
|
4721
|
+
const { document } = parseHTML(html);
|
|
4722
|
+
if (document.documentElement?.localName === "html") return document;
|
|
4723
|
+
return parseHTML(`<!doctype html><html><head></head><body>${html}</body></html>`).document;
|
|
4724
|
+
}
|
|
4720
4725
|
function readableError(error) {
|
|
4721
4726
|
return error instanceof Error ? error.message : String(error);
|
|
4722
4727
|
}
|
|
@@ -4746,10 +4751,6 @@ function resolveDocumentUrls(document, baseUrl) {
|
|
|
4746
4751
|
}
|
|
4747
4752
|
}
|
|
4748
4753
|
function readabilityResult(fetchResult, requested, fallbackReason, fallbackDetail) {
|
|
4749
|
-
const { document } = parseHTML(fetchResult.html);
|
|
4750
|
-
const bodyText = document.body.textContent ?? "";
|
|
4751
|
-
const article = new Readability(document).parse();
|
|
4752
|
-
const text = article?.textContent ?? bodyText;
|
|
4753
4754
|
const fallback = requested !== "readability";
|
|
4754
4755
|
const diagnostics = {
|
|
4755
4756
|
requested,
|
|
@@ -4762,13 +4763,27 @@ function readabilityResult(fetchResult, requested, fallbackReason, fallbackDetai
|
|
|
4762
4763
|
fallbackDetail
|
|
4763
4764
|
} : {}
|
|
4764
4765
|
};
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4766
|
+
const warnings = fallback && fallbackDetail ? [`Defuddle extraction fell back to Readability: ${fallbackDetail}`] : [];
|
|
4767
|
+
try {
|
|
4768
|
+
const document = parseDocument$1(fetchResult.html);
|
|
4769
|
+
const bodyText = document.body?.textContent ?? "";
|
|
4770
|
+
const article = new Readability(document).parse();
|
|
4771
|
+
const text = article?.textContent ?? bodyText;
|
|
4772
|
+
return {
|
|
4773
|
+
text,
|
|
4774
|
+
excerpt: normalizedText(article?.excerpt),
|
|
4775
|
+
wordCount: countCjkAwareWords(text),
|
|
4776
|
+
diagnostics,
|
|
4777
|
+
warnings
|
|
4778
|
+
};
|
|
4779
|
+
} catch (error) {
|
|
4780
|
+
return {
|
|
4781
|
+
text: "",
|
|
4782
|
+
wordCount: 0,
|
|
4783
|
+
diagnostics,
|
|
4784
|
+
warnings: [...warnings, `The page's main content could not be read: ${readableError(error)}`]
|
|
4785
|
+
};
|
|
4786
|
+
}
|
|
4772
4787
|
}
|
|
4773
4788
|
function defaultParseDefuddle(document, options) {
|
|
4774
4789
|
return new Defuddle(document, options).parse();
|
|
@@ -4776,13 +4791,13 @@ function defaultParseDefuddle(document, options) {
|
|
|
4776
4791
|
function extractMainContent(fetchResult, extractor = "defuddle", dependencies = {}) {
|
|
4777
4792
|
if (extractor === "readability") return readabilityResult(fetchResult, extractor);
|
|
4778
4793
|
try {
|
|
4779
|
-
const
|
|
4794
|
+
const document = parseDocument$1(fetchResult.html);
|
|
4780
4795
|
resolveDocumentUrls(document, fetchResult.finalUrl);
|
|
4781
4796
|
const article = (dependencies.parseDefuddle ?? defaultParseDefuddle)(document, {
|
|
4782
4797
|
url: fetchResult.finalUrl,
|
|
4783
4798
|
useAsync: false
|
|
4784
4799
|
});
|
|
4785
|
-
const text = typeof article.content === "string" ? load(article.content).text() : document.body
|
|
4800
|
+
const text = typeof article.content === "string" ? load(article.content).text() : document.body?.textContent ?? "";
|
|
4786
4801
|
if (!text.trim()) return readabilityResult(fetchResult, extractor, "defuddle_empty", "Defuddle returned no main content");
|
|
4787
4802
|
const reportedWordCount = article.wordCount;
|
|
4788
4803
|
const hasValidWordCount = typeof reportedWordCount === "number" && Number.isFinite(reportedWordCount) && reportedWordCount >= 0;
|
|
@@ -4805,6 +4820,28 @@ function extractMainContent(fetchResult, extractor = "defuddle", dependencies =
|
|
|
4805
4820
|
return readabilityResult(fetchResult, extractor, "defuddle_error", readableError(error));
|
|
4806
4821
|
}
|
|
4807
4822
|
}
|
|
4823
|
+
const MARKDOWN_LIMIT = 2e4;
|
|
4824
|
+
async function extractMainMarkdown(fetchResult) {
|
|
4825
|
+
const document = parseDocument$1(fetchResult.html);
|
|
4826
|
+
resolveDocumentUrls(document, fetchResult.finalUrl);
|
|
4827
|
+
const { Defuddle: toMarkdown } = await import("defuddle/node");
|
|
4828
|
+
const markdown = ((await toMarkdown(document, fetchResult.finalUrl, {
|
|
4829
|
+
markdown: true,
|
|
4830
|
+
useAsync: false
|
|
4831
|
+
})).content ?? "").trim();
|
|
4832
|
+
if (markdown.length <= 2e4) return {
|
|
4833
|
+
markdown,
|
|
4834
|
+
characters: markdown.length,
|
|
4835
|
+
truncated: false
|
|
4836
|
+
};
|
|
4837
|
+
const cut = markdown.slice(0, MARKDOWN_LIMIT);
|
|
4838
|
+
const end = cut.lastIndexOf("\n");
|
|
4839
|
+
return {
|
|
4840
|
+
markdown: (end > 2e4 / 2 ? cut.slice(0, end) : cut).trimEnd(),
|
|
4841
|
+
characters: markdown.length,
|
|
4842
|
+
truncated: true
|
|
4843
|
+
};
|
|
4844
|
+
}
|
|
4808
4845
|
//#endregion
|
|
4809
4846
|
//#region packages/core/src/extract/google-rich-results.ts
|
|
4810
4847
|
const SUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -8639,7 +8676,7 @@ const QUERY_FLAG_METHOD = {
|
|
|
8639
8676
|
"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."
|
|
8640
8677
|
}
|
|
8641
8678
|
};
|
|
8642
|
-
const FUNCTION_WORDS = /* @__PURE__ */ new Set([
|
|
8679
|
+
const FUNCTION_WORDS$1 = /* @__PURE__ */ new Set([
|
|
8643
8680
|
"a",
|
|
8644
8681
|
"about",
|
|
8645
8682
|
"an",
|
|
@@ -8704,7 +8741,7 @@ function isLikelyAutomatedQuery(query) {
|
|
|
8704
8741
|
if (query.trim().split(/\s+/).filter(Boolean).some((token) => IDENTIFIER_TOKEN.test(token))) return true;
|
|
8705
8742
|
const tokens = tokenize(query);
|
|
8706
8743
|
if (tokens.length >= 20) return true;
|
|
8707
|
-
if (tokens.length >= 8 && !/[^\p{ASCII}]/u.test(query) && !tokens.some((token) => FUNCTION_WORDS.has(token))) return true;
|
|
8744
|
+
if (tokens.length >= 8 && !/[^\p{ASCII}]/u.test(query) && !tokens.some((token) => FUNCTION_WORDS$1.has(token))) return true;
|
|
8708
8745
|
return false;
|
|
8709
8746
|
}
|
|
8710
8747
|
const FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -16682,7 +16719,7 @@ function analyzeCtrUnderperformersFromRows(input) {
|
|
|
16682
16719
|
//#endregion
|
|
16683
16720
|
//#region packages/core/src/analyze/ctr-underperformers-report.ts
|
|
16684
16721
|
const MAX_GSC_ROWS$3 = 1e5;
|
|
16685
|
-
const DEFAULT_DAYS$
|
|
16722
|
+
const DEFAULT_DAYS$4 = 28;
|
|
16686
16723
|
const MAX_DAYS$2 = 548;
|
|
16687
16724
|
function plural$4(count, singular, pluralLabel = `${singular}s`) {
|
|
16688
16725
|
return count === 1 ? singular : pluralLabel;
|
|
@@ -16705,7 +16742,7 @@ async function ctrUnderperformersReportWith(input, dependencies) {
|
|
|
16705
16742
|
const explicitRange = explicitDateRange(input, MAX_DAYS$2);
|
|
16706
16743
|
const days = explicitRange?.days ?? integerOption$1({
|
|
16707
16744
|
value: input.days,
|
|
16708
|
-
fallback: DEFAULT_DAYS$
|
|
16745
|
+
fallback: DEFAULT_DAYS$4,
|
|
16709
16746
|
minimum: 1,
|
|
16710
16747
|
maximum: MAX_DAYS$2,
|
|
16711
16748
|
label: "Days"
|
|
@@ -19962,7 +19999,7 @@ function quickWinTemplateSummaries(items) {
|
|
|
19962
19999
|
}
|
|
19963
20000
|
//#endregion
|
|
19964
20001
|
//#region packages/core/src/analyze/site-diagnostics/quick-wins-report.ts
|
|
19965
|
-
const DEFAULT_DAYS$
|
|
20002
|
+
const DEFAULT_DAYS$3 = 28;
|
|
19966
20003
|
const MAX_DAYS$1 = 548;
|
|
19967
20004
|
const DEFAULT_VERIFY_LIMIT$1 = 5;
|
|
19968
20005
|
const MAX_VERIFY_LIMIT = 100;
|
|
@@ -19984,7 +20021,7 @@ async function quickWinsReportWith(input, dependencies) {
|
|
|
19984
20021
|
const explicitRange = explicitDateRange(input, MAX_DAYS$1);
|
|
19985
20022
|
const days = explicitRange?.days ?? integerOption$1({
|
|
19986
20023
|
value: input.days,
|
|
19987
|
-
fallback: DEFAULT_DAYS$
|
|
20024
|
+
fallback: DEFAULT_DAYS$3,
|
|
19988
20025
|
minimum: 1,
|
|
19989
20026
|
maximum: MAX_DAYS$1,
|
|
19990
20027
|
label: "Days"
|
|
@@ -20408,7 +20445,7 @@ function analyzeStrikingDistanceRows(input) {
|
|
|
20408
20445
|
}
|
|
20409
20446
|
//#endregion
|
|
20410
20447
|
//#region packages/core/src/analyze/striking-distance-report.ts
|
|
20411
|
-
const DEFAULT_DAYS$
|
|
20448
|
+
const DEFAULT_DAYS$2 = 28;
|
|
20412
20449
|
const MAX_DAYS = 548;
|
|
20413
20450
|
const DEFAULT_VERIFY_LIMIT = 5;
|
|
20414
20451
|
const MAX_SOURCE_ROWS$1 = 1e5;
|
|
@@ -20417,7 +20454,7 @@ function boundedInteger$2(value, fallback, minimum, maximum) {
|
|
|
20417
20454
|
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
20418
20455
|
}
|
|
20419
20456
|
function reportDays(value) {
|
|
20420
|
-
if (value === void 0) return DEFAULT_DAYS$
|
|
20457
|
+
if (value === void 0) return DEFAULT_DAYS$2;
|
|
20421
20458
|
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}.`);
|
|
20422
20459
|
return value;
|
|
20423
20460
|
}
|
|
@@ -21859,20 +21896,394 @@ function analyzeInternalLinksFromRows(input) {
|
|
|
21859
21896
|
}
|
|
21860
21897
|
};
|
|
21861
21898
|
}
|
|
21862
|
-
|
|
21863
|
-
|
|
21864
|
-
|
|
21865
|
-
|
|
21866
|
-
|
|
21867
|
-
|
|
21899
|
+
const MAX_TERMS = 100;
|
|
21900
|
+
const PLACEMENT_SENTENCES = 2;
|
|
21901
|
+
const ANCHORS_PER_SENTENCE = 3;
|
|
21902
|
+
const MIN_SENTENCE_WORDS = 6;
|
|
21903
|
+
const NAME_WEIGHT = 2;
|
|
21904
|
+
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;
|
|
21905
|
+
function normalise(value) {
|
|
21906
|
+
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();
|
|
21907
|
+
}
|
|
21908
|
+
const words = (value) => normalise(value).split(" ").filter(Boolean);
|
|
21909
|
+
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(" "));
|
|
21910
|
+
const NOT_PLURAL = /* @__PURE__ */ new Set([
|
|
21911
|
+
"always",
|
|
21912
|
+
"atlas",
|
|
21913
|
+
"canvas",
|
|
21914
|
+
"gas",
|
|
21915
|
+
"lens",
|
|
21916
|
+
"means",
|
|
21917
|
+
"news",
|
|
21918
|
+
"perhaps",
|
|
21919
|
+
"series",
|
|
21920
|
+
"species",
|
|
21921
|
+
"sometimes",
|
|
21922
|
+
"whereas",
|
|
21923
|
+
"yes",
|
|
21924
|
+
"this",
|
|
21925
|
+
"has",
|
|
21926
|
+
"was",
|
|
21927
|
+
"does",
|
|
21928
|
+
"its"
|
|
21929
|
+
]);
|
|
21930
|
+
const CHE = /* @__PURE__ */ new Set([
|
|
21931
|
+
"caches",
|
|
21932
|
+
"niches",
|
|
21933
|
+
"headaches",
|
|
21934
|
+
"cliches",
|
|
21935
|
+
"avalanches",
|
|
21936
|
+
"moustaches"
|
|
21937
|
+
]);
|
|
21938
|
+
const NOT_AGENT = /* @__PURE__ */ new Set([
|
|
21939
|
+
"customer",
|
|
21940
|
+
"together",
|
|
21941
|
+
"however",
|
|
21942
|
+
"whatever",
|
|
21943
|
+
"whenever",
|
|
21944
|
+
"wherever",
|
|
21945
|
+
"remember",
|
|
21946
|
+
"consider",
|
|
21947
|
+
"character",
|
|
21948
|
+
"register",
|
|
21949
|
+
"november",
|
|
21950
|
+
"december",
|
|
21951
|
+
"september",
|
|
21952
|
+
"october"
|
|
21953
|
+
]);
|
|
21954
|
+
function singular(word) {
|
|
21955
|
+
if (word.length <= 3 || !/^[a-z]+$/.test(word) || NOT_PLURAL.has(word) || /(ss|us|is)$/.test(word)) return word;
|
|
21956
|
+
if (word.endsWith("ies") && word.length > 4) return `${word.slice(0, -3)}y`;
|
|
21957
|
+
if (/(xes|ches|shes|sses)$/.test(word) && !CHE.has(word)) return word.slice(0, -2);
|
|
21958
|
+
return word.endsWith("s") ? word.slice(0, -1) : word;
|
|
21959
|
+
}
|
|
21960
|
+
function stem(word) {
|
|
21961
|
+
const one = singular(word);
|
|
21962
|
+
if (!/^[a-z]+$/.test(one)) return one;
|
|
21963
|
+
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);
|
|
21964
|
+
if (one.length > 5 && one.endsWith("ly")) return one.slice(0, -2);
|
|
21965
|
+
if (one.length > 5 && one.endsWith("ed") && !one.endsWith("eed")) return one.slice(0, -2);
|
|
21966
|
+
if (one.length > 6 && one.endsWith("ing")) return one.slice(0, -3);
|
|
21967
|
+
return one;
|
|
21968
|
+
}
|
|
21969
|
+
const terms = (text) => words(text).filter((word) => word.length > 1 && !FUNCTION_WORDS.has(word) && !/^\d+$/.test(word)).map(stem);
|
|
21970
|
+
const slugWords = (url) => {
|
|
21971
|
+
try {
|
|
21972
|
+
return new URL(url).pathname.split(/[/._-]+/).filter(Boolean).join(" ");
|
|
21973
|
+
} catch {
|
|
21974
|
+
return "";
|
|
21975
|
+
}
|
|
21976
|
+
};
|
|
21977
|
+
function keyTerms(page) {
|
|
21978
|
+
const key = /* @__PURE__ */ new Map();
|
|
21979
|
+
const title = page.title.replace(/\s+[|\u2013\u2014-]\s+[^|\u2013\u2014]*$/u, "");
|
|
21980
|
+
for (const term of terms(page.description)) key.set(term, 1);
|
|
21981
|
+
for (const term of terms(`${title} ${page.h1} ${lastSegmentWords(page.url)}`)) key.set(term, NAME_WEIGHT);
|
|
21982
|
+
for (const term of terms(siteName(page.url))) key.delete(term);
|
|
21983
|
+
return key;
|
|
21984
|
+
}
|
|
21985
|
+
function lastSegmentWords(url) {
|
|
21986
|
+
try {
|
|
21987
|
+
return (new URL(url).pathname.split("/").filter(Boolean).at(-1) ?? "").split(/[._-]+/).join(" ");
|
|
21988
|
+
} catch {
|
|
21989
|
+
return "";
|
|
21990
|
+
}
|
|
21868
21991
|
}
|
|
21869
|
-
function
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21992
|
+
function siteName(url) {
|
|
21993
|
+
try {
|
|
21994
|
+
return new URL(url).hostname.replace(/^www\./u, "").split(".")[0] ?? "";
|
|
21995
|
+
} catch {
|
|
21996
|
+
return "";
|
|
21997
|
+
}
|
|
21873
21998
|
}
|
|
21874
|
-
function
|
|
21875
|
-
|
|
21999
|
+
function termCounts(page) {
|
|
22000
|
+
const counts = {};
|
|
22001
|
+
const add = (text, weight) => {
|
|
22002
|
+
for (const term of terms(text)) counts[term] = (counts[term] ?? 0) + weight;
|
|
22003
|
+
};
|
|
22004
|
+
add(page.title, 3);
|
|
22005
|
+
add(page.h1, 3);
|
|
22006
|
+
add(slugWords(page.url), 3);
|
|
22007
|
+
add(page.description, 2);
|
|
22008
|
+
for (const heading of page.headings) add(heading, 2);
|
|
22009
|
+
for (const sentence of page.sentences) add(sentence.text, 1);
|
|
22010
|
+
return counts;
|
|
22011
|
+
}
|
|
22012
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
22013
|
+
"script",
|
|
22014
|
+
"style",
|
|
22015
|
+
"noscript",
|
|
22016
|
+
"svg",
|
|
22017
|
+
"template",
|
|
22018
|
+
"iframe",
|
|
22019
|
+
"select"
|
|
22020
|
+
]);
|
|
22021
|
+
const CHROME = /* @__PURE__ */ new Set([
|
|
22022
|
+
"nav",
|
|
22023
|
+
"header",
|
|
22024
|
+
"footer",
|
|
22025
|
+
"aside"
|
|
22026
|
+
]);
|
|
22027
|
+
const MAIN = /* @__PURE__ */ new Set(["main", "article"]);
|
|
22028
|
+
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(" "));
|
|
22029
|
+
const VOID = /* @__PURE__ */ new Set([
|
|
22030
|
+
"br",
|
|
22031
|
+
"hr",
|
|
22032
|
+
"img",
|
|
22033
|
+
"input",
|
|
22034
|
+
"meta",
|
|
22035
|
+
"link",
|
|
22036
|
+
"wbr",
|
|
22037
|
+
"source",
|
|
22038
|
+
"col",
|
|
22039
|
+
"embed"
|
|
22040
|
+
]);
|
|
22041
|
+
const ENTITIES = {
|
|
22042
|
+
amp: "&",
|
|
22043
|
+
lt: "<",
|
|
22044
|
+
gt: ">",
|
|
22045
|
+
quot: "\"",
|
|
22046
|
+
apos: "'",
|
|
22047
|
+
nbsp: " ",
|
|
22048
|
+
hellip: "…",
|
|
22049
|
+
mdash: "—",
|
|
22050
|
+
ndash: "–",
|
|
22051
|
+
rsquo: "’",
|
|
22052
|
+
lsquo: "‘",
|
|
22053
|
+
rdquo: "”",
|
|
22054
|
+
ldquo: "“"
|
|
22055
|
+
};
|
|
22056
|
+
const TAG_ATTRS = String.raw`(?:[^>"'=]|=\s*"[^"]*"|=\s*'[^']*'|=(?!\s*["'])|["'])*`;
|
|
22057
|
+
const A_OPEN = "";
|
|
22058
|
+
const UNSPACED = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
|
|
22059
|
+
function decode(text) {
|
|
22060
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, code) => {
|
|
22061
|
+
if (code[0] === "#") {
|
|
22062
|
+
const hex = code[1] === "x" || code[1] === "X";
|
|
22063
|
+
const point = Number.parseInt(code.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
22064
|
+
return point > 0 && point <= 1114111 ? String.fromCodePoint(point) : "";
|
|
22065
|
+
}
|
|
22066
|
+
return ENTITIES[code.toLowerCase()] ?? match;
|
|
22067
|
+
});
|
|
22068
|
+
}
|
|
22069
|
+
const clean$1 = (value) => value.split(A_OPEN).join("").replace(/\s+/g, " ").trim();
|
|
22070
|
+
function splitSentences(block) {
|
|
22071
|
+
return block.replace(/\s+/g, " ").split(/(?<=[.!?…]["”’)]?)\s+(?=["“‘(]?[A-Z0-9])|(?<=[。!?])/).map((sentence) => sentence.trim()).filter((sentence) => sentence.length <= 500);
|
|
22072
|
+
}
|
|
22073
|
+
function pageSentences(html) {
|
|
22074
|
+
const sentences = [];
|
|
22075
|
+
const hasMain = /<(main|article)[\s>]|role=["']main["']/i.test(html);
|
|
22076
|
+
const stack = [];
|
|
22077
|
+
let skip = 0;
|
|
22078
|
+
let chrome = 0;
|
|
22079
|
+
let mainDepth = 0;
|
|
22080
|
+
let main = hasMain ? 0 : 1;
|
|
22081
|
+
let section = "";
|
|
22082
|
+
let buffer = "";
|
|
22083
|
+
let inAnchor = false;
|
|
22084
|
+
const flushRaw = (tag) => {
|
|
22085
|
+
const raw = buffer;
|
|
22086
|
+
const text = clean$1(raw);
|
|
22087
|
+
buffer = "";
|
|
22088
|
+
if (!text || sentences.length >= 250) return;
|
|
22089
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
22090
|
+
if (tag !== "h1") section = text;
|
|
22091
|
+
return;
|
|
22092
|
+
}
|
|
22093
|
+
for (const part of tag === "li" ? [raw] : splitSentences(raw)) {
|
|
22094
|
+
const sentence = clean$1(part);
|
|
22095
|
+
if (sentence.length >= 25 && sentence.split(" ").length >= 4 || UNSPACED.test(sentence) && sentence.length >= 10) {
|
|
22096
|
+
if (sentences.length >= 250) return;
|
|
22097
|
+
sentences.push({
|
|
22098
|
+
text: sentence,
|
|
22099
|
+
section,
|
|
22100
|
+
hasLink: part.includes(A_OPEN)
|
|
22101
|
+
});
|
|
22102
|
+
}
|
|
22103
|
+
}
|
|
22104
|
+
};
|
|
22105
|
+
const closeTo = (tag) => {
|
|
22106
|
+
if (!stack.some((entry) => entry.tag === tag)) return;
|
|
22107
|
+
while (stack.length) {
|
|
22108
|
+
const entry = stack.pop();
|
|
22109
|
+
if (!entry) break;
|
|
22110
|
+
flushRaw(entry.tag);
|
|
22111
|
+
if (entry.main) {
|
|
22112
|
+
main -= 1;
|
|
22113
|
+
mainDepth -= 1;
|
|
22114
|
+
}
|
|
22115
|
+
if (entry.chrome) chrome -= 1;
|
|
22116
|
+
if (entry.tag === tag) break;
|
|
22117
|
+
}
|
|
22118
|
+
};
|
|
22119
|
+
const tokens = html.matchAll(new RegExp(String.raw`<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<(\/?)([a-zA-Z][\w:-]*)(${TAG_ATTRS})>|([^<]+)`, "g"));
|
|
22120
|
+
for (const match of tokens) {
|
|
22121
|
+
const [whole, close, rawTag, rawAttrs = "", text] = match;
|
|
22122
|
+
if (text !== void 0) {
|
|
22123
|
+
if (!skip && main > 0 && chrome === 0) buffer += decode(text);
|
|
22124
|
+
continue;
|
|
22125
|
+
}
|
|
22126
|
+
if (!rawTag) continue;
|
|
22127
|
+
const tag = rawTag.toLowerCase();
|
|
22128
|
+
if (skip) {
|
|
22129
|
+
if (close && SKIP.has(tag)) skip -= 1;
|
|
22130
|
+
continue;
|
|
22131
|
+
}
|
|
22132
|
+
if (close) {
|
|
22133
|
+
if (tag === "a") inAnchor = false;
|
|
22134
|
+
else if (BLOCK.has(tag)) closeTo(tag);
|
|
22135
|
+
continue;
|
|
22136
|
+
}
|
|
22137
|
+
if (SKIP.has(tag)) {
|
|
22138
|
+
if (!whole.endsWith("/>")) skip += 1;
|
|
22139
|
+
continue;
|
|
22140
|
+
}
|
|
22141
|
+
if (tag === "br") buffer += " ";
|
|
22142
|
+
if (tag === "a" && !inAnchor && /\bhref\s*=/i.test(rawAttrs)) {
|
|
22143
|
+
inAnchor = true;
|
|
22144
|
+
if (main > 0 && chrome === 0) buffer += A_OPEN;
|
|
22145
|
+
}
|
|
22146
|
+
if (VOID.has(tag) || !BLOCK.has(tag)) continue;
|
|
22147
|
+
const parent = stack[stack.length - 1];
|
|
22148
|
+
if (parent) flushRaw(parent.tag);
|
|
22149
|
+
const role = /\brole\s*=\s*["']?main/i.test(rawAttrs);
|
|
22150
|
+
const isMain = MAIN.has(tag) || role;
|
|
22151
|
+
const isChrome = tag === "nav" || tag === "aside" || CHROME.has(tag) && mainDepth === 0;
|
|
22152
|
+
if (isMain) {
|
|
22153
|
+
main += 1;
|
|
22154
|
+
mainDepth += 1;
|
|
22155
|
+
}
|
|
22156
|
+
if (isChrome) chrome += 1;
|
|
22157
|
+
stack.push({
|
|
22158
|
+
tag,
|
|
22159
|
+
main: isMain,
|
|
22160
|
+
chrome: isChrome
|
|
22161
|
+
});
|
|
22162
|
+
}
|
|
22163
|
+
while (stack.length) {
|
|
22164
|
+
const last = stack[stack.length - 1];
|
|
22165
|
+
if (!last) break;
|
|
22166
|
+
closeTo(last.tag);
|
|
22167
|
+
}
|
|
22168
|
+
flushRaw("");
|
|
22169
|
+
return sentences;
|
|
22170
|
+
}
|
|
22171
|
+
var LinkCorpus = class {
|
|
22172
|
+
df = /* @__PURE__ */ new Map();
|
|
22173
|
+
size;
|
|
22174
|
+
constructor(pages) {
|
|
22175
|
+
this.size = pages.length;
|
|
22176
|
+
for (const counts of pages) for (const term in counts) this.df.set(term, (this.df.get(term) ?? 0) + 1);
|
|
22177
|
+
}
|
|
22178
|
+
idf(term) {
|
|
22179
|
+
return Math.log((this.size + 1) / ((this.df.get(term) ?? 1) + 1)) + 1;
|
|
22180
|
+
}
|
|
22181
|
+
vector(counts) {
|
|
22182
|
+
let norm = 0;
|
|
22183
|
+
const shared = [];
|
|
22184
|
+
for (const term in counts) {
|
|
22185
|
+
const weight = (1 + Math.log(counts[term] ?? 1)) * this.idf(term);
|
|
22186
|
+
norm += weight * weight;
|
|
22187
|
+
if ((this.df.get(term) ?? 0) >= 2) shared.push([term, weight]);
|
|
22188
|
+
}
|
|
22189
|
+
norm = Math.sqrt(norm) || 1;
|
|
22190
|
+
shared.sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0));
|
|
22191
|
+
return new Map(shared.slice(0, MAX_TERMS).map(([term, weight]) => [term, weight / norm]));
|
|
22192
|
+
}
|
|
22193
|
+
similarity(left, right) {
|
|
22194
|
+
let sum = 0;
|
|
22195
|
+
for (const [term, weight] of left) sum += weight * (right.get(term) ?? 0);
|
|
22196
|
+
return sum;
|
|
22197
|
+
}
|
|
22198
|
+
sentenceScore(sentence, key) {
|
|
22199
|
+
if (!key.size) return 0;
|
|
22200
|
+
const seen = new Set(terms(sentence));
|
|
22201
|
+
let hit = 0;
|
|
22202
|
+
let total = 0;
|
|
22203
|
+
for (const [term, place] of key) {
|
|
22204
|
+
const weight = this.idf(term) * place;
|
|
22205
|
+
total += weight;
|
|
22206
|
+
if (seen.has(term)) hit += weight;
|
|
22207
|
+
}
|
|
22208
|
+
return total ? hit / total : 0;
|
|
22209
|
+
}
|
|
22210
|
+
};
|
|
22211
|
+
function anchorCandidates(sentence, key, corpus, limit) {
|
|
22212
|
+
const scored = /* @__PURE__ */ new Map();
|
|
22213
|
+
const parts = sentence.split(/\s+/).map((word) => word.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter(Boolean);
|
|
22214
|
+
for (let size = 2; size <= 4; size += 1) for (let start = 0; start + size <= parts.length; start += 1) {
|
|
22215
|
+
const gram = parts.slice(start, start + size);
|
|
22216
|
+
const first = (gram[0] ?? "").toLowerCase();
|
|
22217
|
+
const last = (gram[size - 1] ?? "").toLowerCase();
|
|
22218
|
+
if (FUNCTION_WORDS.has(first) || FUNCTION_WORDS.has(last) || /^\d+$/.test(first) || /^\d+$/.test(last)) continue;
|
|
22219
|
+
const stems = terms(gram.join(" "));
|
|
22220
|
+
if (stems.length < 2) continue;
|
|
22221
|
+
const hits = stems.filter((term) => key.has(term)).length;
|
|
22222
|
+
const named = stems.reduce((sum, term) => sum + (key.get(term) ?? 0) * corpus.idf(term), 0);
|
|
22223
|
+
if (!stems.some((term) => key.get(term) === NAME_WEIGHT)) continue;
|
|
22224
|
+
if (Number(key.has(terms(first)[0] ?? "")) + Number(key.has(terms(last)[0] ?? "")) < 2) continue;
|
|
22225
|
+
const text = gram.join(" ");
|
|
22226
|
+
const lower = text.toLowerCase();
|
|
22227
|
+
if (GENERIC_ANCHORS.test(lower)) continue;
|
|
22228
|
+
const rarity = stems.reduce((sum, term) => sum + corpus.idf(term), 0);
|
|
22229
|
+
const score = named * (1 + hits / key.size) + rarity * .05 - (size === 4 ? .2 : 0);
|
|
22230
|
+
const existing = scored.get(lower);
|
|
22231
|
+
if (!existing || existing.score < score) scored.set(lower, {
|
|
22232
|
+
text,
|
|
22233
|
+
score
|
|
22234
|
+
});
|
|
22235
|
+
}
|
|
22236
|
+
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);
|
|
22237
|
+
}
|
|
22238
|
+
function linkPlacement(source, target, corpus) {
|
|
22239
|
+
const key = keyTerms(target);
|
|
22240
|
+
const contentSimilarity = corpus.similarity(corpus.vector(termCounts(source)), corpus.vector(termCounts(target)));
|
|
22241
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22242
|
+
const sentences = source.sentences.filter((sentence) => {
|
|
22243
|
+
if (seen.has(sentence.text)) return false;
|
|
22244
|
+
seen.add(sentence.text);
|
|
22245
|
+
return !sentence.hasLink && sentence.text.split(/\s+/).length >= MIN_SENTENCE_WORDS;
|
|
22246
|
+
}).map((sentence, index) => ({
|
|
22247
|
+
sentence,
|
|
22248
|
+
index,
|
|
22249
|
+
match: corpus.sentenceScore(sentence.text, key)
|
|
22250
|
+
})).filter((item) => item.match > 0).sort((left, right) => right.match - left.match || left.index - right.index).slice(0, PLACEMENT_SENTENCES).map(({ sentence, match }) => ({
|
|
22251
|
+
text: sentence.text,
|
|
22252
|
+
section: sentence.section,
|
|
22253
|
+
match: Math.round(match * 100) / 100,
|
|
22254
|
+
anchors: anchorCandidates(sentence.text, key, corpus, ANCHORS_PER_SENTENCE)
|
|
22255
|
+
})).filter((item) => item.anchors.length > 0);
|
|
22256
|
+
return {
|
|
22257
|
+
contentSimilarity: Math.round(contentSimilarity * 100) / 100,
|
|
22258
|
+
sentences
|
|
22259
|
+
};
|
|
22260
|
+
}
|
|
22261
|
+
function placementPage(input) {
|
|
22262
|
+
return {
|
|
22263
|
+
url: input.url,
|
|
22264
|
+
title: input.title ?? "",
|
|
22265
|
+
h1: input.headings.find((heading) => heading.level === 1)?.text ?? "",
|
|
22266
|
+
description: input.metaDescription ?? "",
|
|
22267
|
+
headings: input.headings.filter((heading) => heading.level === 2 || heading.level === 3).slice(0, 8).map((heading) => heading.text),
|
|
22268
|
+
sentences: pageSentences(input.html)
|
|
22269
|
+
};
|
|
22270
|
+
}
|
|
22271
|
+
//#endregion
|
|
22272
|
+
//#region packages/core/src/analyze/internal-links-report.ts
|
|
22273
|
+
const DAY$1 = 864e5;
|
|
22274
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
22275
|
+
function internalLinksReportRange(input, fallbackDays, now) {
|
|
22276
|
+
if (!input.startDate && !input.endDate) return {
|
|
22277
|
+
...finalGscDateRange(fallbackDays, now),
|
|
22278
|
+
days: fallbackDays
|
|
22279
|
+
};
|
|
22280
|
+
const { startDate, endDate } = input;
|
|
22281
|
+
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.");
|
|
22282
|
+
return {
|
|
22283
|
+
startDate,
|
|
22284
|
+
endDate,
|
|
22285
|
+
days: Math.round((Date.parse(`${endDate}T00:00:00Z`) - Date.parse(`${startDate}T00:00:00Z`)) / DAY$1) + 1
|
|
22286
|
+
};
|
|
21876
22287
|
}
|
|
21877
22288
|
function completeInternalLinksSelection(analysis, runtime) {
|
|
21878
22289
|
return {
|
|
@@ -21930,9 +22341,8 @@ async function verifyInternalLinkTarget(input) {
|
|
|
21930
22341
|
let fetched;
|
|
21931
22342
|
try {
|
|
21932
22343
|
fetched = await input.dependencies.fetch(input.targetUrl, {
|
|
21933
|
-
js:
|
|
21934
|
-
refresh: input.refresh
|
|
21935
|
-
rate: input.rate
|
|
22344
|
+
js: "auto",
|
|
22345
|
+
refresh: input.refresh
|
|
21936
22346
|
});
|
|
21937
22347
|
} catch (error) {
|
|
21938
22348
|
return {
|
|
@@ -22002,7 +22412,12 @@ async function verifyInternalLinkTarget(input) {
|
|
|
22002
22412
|
verification: fatalSignals.length ? "technical-issue" : "verified",
|
|
22003
22413
|
technicalSignals: signals,
|
|
22004
22414
|
fetchDiagnostics: fetched.diagnostics,
|
|
22005
|
-
warnings
|
|
22415
|
+
warnings,
|
|
22416
|
+
text: placementPage({
|
|
22417
|
+
...page,
|
|
22418
|
+
url: page.finalUrl,
|
|
22419
|
+
html: fetched.html
|
|
22420
|
+
})
|
|
22006
22421
|
};
|
|
22007
22422
|
}
|
|
22008
22423
|
function linksToAliases(page, aliases) {
|
|
@@ -22036,9 +22451,8 @@ async function verifyInternalLinkCandidate(input) {
|
|
|
22036
22451
|
let fetched;
|
|
22037
22452
|
try {
|
|
22038
22453
|
fetched = await input.dependencies.fetch(input.candidate.sourceUrl, {
|
|
22039
|
-
js:
|
|
22040
|
-
refresh: input.refresh
|
|
22041
|
-
rate: input.rate
|
|
22454
|
+
js: "auto",
|
|
22455
|
+
refresh: input.refresh
|
|
22042
22456
|
});
|
|
22043
22457
|
} catch (error) {
|
|
22044
22458
|
return {
|
|
@@ -22128,12 +22542,21 @@ async function verifyInternalLinkCandidate(input) {
|
|
|
22128
22542
|
confidence
|
|
22129
22543
|
}
|
|
22130
22544
|
},
|
|
22545
|
+
text: placementPage({
|
|
22546
|
+
...page,
|
|
22547
|
+
url: page.finalUrl,
|
|
22548
|
+
html: fetched.html
|
|
22549
|
+
}),
|
|
22131
22550
|
warnings
|
|
22132
22551
|
};
|
|
22133
22552
|
}
|
|
22134
22553
|
//#endregion
|
|
22135
22554
|
//#region packages/core/src/analyze/internal-links.ts
|
|
22136
22555
|
const MAX_GSC_ROWS = 1e5;
|
|
22556
|
+
const DEFAULT_DAYS$1 = 90;
|
|
22557
|
+
const RESULT_LIMIT = 20;
|
|
22558
|
+
const CHECK_LIMIT = 40;
|
|
22559
|
+
const MIN_IMPRESSIONS = 1;
|
|
22137
22560
|
const defaultDependencies$2 = {
|
|
22138
22561
|
searchAnalytics: querySearchAnalytics,
|
|
22139
22562
|
fetch: fetchPage,
|
|
@@ -22144,43 +22567,17 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
22144
22567
|
const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
|
|
22145
22568
|
targetPage.hash = "";
|
|
22146
22569
|
const targetUrl = targetPage.toString();
|
|
22147
|
-
const
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
minimum: 1,
|
|
22151
|
-
maximum: 548,
|
|
22152
|
-
label: "days"
|
|
22153
|
-
});
|
|
22154
|
-
const limit = internalLinksIntegerOption({
|
|
22155
|
-
value: input.limit,
|
|
22156
|
-
fallback: 20,
|
|
22157
|
-
minimum: 1,
|
|
22158
|
-
maximum: 100,
|
|
22159
|
-
label: "limit"
|
|
22160
|
-
});
|
|
22161
|
-
const checkLimit = internalLinksIntegerOption({
|
|
22162
|
-
value: input.checkLimit,
|
|
22163
|
-
fallback: Math.max(limit, 40),
|
|
22164
|
-
minimum: 1,
|
|
22165
|
-
maximum: 200,
|
|
22166
|
-
label: "checkLimit"
|
|
22167
|
-
});
|
|
22168
|
-
const minImpressions = internalLinksNumberOption({
|
|
22169
|
-
value: input.minImpressions,
|
|
22170
|
-
fallback: 1,
|
|
22171
|
-
minimum: 0,
|
|
22172
|
-
maximum: 1e9,
|
|
22173
|
-
label: "minImpressions"
|
|
22174
|
-
});
|
|
22570
|
+
const limit = RESULT_LIMIT;
|
|
22571
|
+
const checkLimit = CHECK_LIMIT;
|
|
22572
|
+
const minImpressions = MIN_IMPRESSIONS;
|
|
22175
22573
|
const now = dependencies.now();
|
|
22176
|
-
const range = internalLinksReportRange(
|
|
22574
|
+
const { days, ...range } = internalLinksReportRange(input, DEFAULT_DAYS$1, now);
|
|
22575
|
+
const brandTerms = deriveBrandTerms({ siteUrl: input.site });
|
|
22177
22576
|
const ledger = new SessionLedger();
|
|
22178
22577
|
const target = await verifyInternalLinkTarget({
|
|
22179
22578
|
site: input.site,
|
|
22180
22579
|
targetUrl,
|
|
22181
|
-
js: input.js,
|
|
22182
22580
|
refresh: input.refresh,
|
|
22183
|
-
rate: input.rate,
|
|
22184
22581
|
dependencies
|
|
22185
22582
|
});
|
|
22186
22583
|
const targetRequests = await Promise.all(target.aliases.map(async (alias) => {
|
|
@@ -22212,8 +22609,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
22212
22609
|
site: input.site,
|
|
22213
22610
|
targetAliases: target.aliases,
|
|
22214
22611
|
minImpressions,
|
|
22215
|
-
brandTerms
|
|
22216
|
-
includeBrand: input.includeBrand
|
|
22612
|
+
brandTerms
|
|
22217
22613
|
});
|
|
22218
22614
|
const queryCandidates = target.verification === "verified" && preliminary.selection.targetEligibleQueries > 0;
|
|
22219
22615
|
const sourceResult = queryCandidates ? await dependencies.searchAnalytics(input.site, {
|
|
@@ -22234,10 +22630,9 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
22234
22630
|
site: input.site,
|
|
22235
22631
|
targetAliases: target.aliases,
|
|
22236
22632
|
minImpressions,
|
|
22237
|
-
brandTerms
|
|
22238
|
-
includeBrand: input.includeBrand
|
|
22633
|
+
brandTerms
|
|
22239
22634
|
});
|
|
22240
|
-
const
|
|
22635
|
+
const found = [];
|
|
22241
22636
|
const warnings = [...target.warnings];
|
|
22242
22637
|
let attemptedSources = 0;
|
|
22243
22638
|
let checkedSources = 0;
|
|
@@ -22246,25 +22641,27 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
22246
22641
|
let selfAliasExclusions = 0;
|
|
22247
22642
|
let failedChecks = 0;
|
|
22248
22643
|
for (const candidate of analysis.candidates) {
|
|
22249
|
-
if (attemptedSources >= checkLimit ||
|
|
22644
|
+
if (attemptedSources >= checkLimit || found.length >= limit) break;
|
|
22250
22645
|
attemptedSources += 1;
|
|
22251
22646
|
const verified = await verifyInternalLinkCandidate({
|
|
22252
22647
|
site: input.site,
|
|
22253
22648
|
candidate,
|
|
22254
22649
|
target,
|
|
22255
|
-
js: input.js,
|
|
22256
22650
|
refresh: input.refresh,
|
|
22257
|
-
rate: input.rate,
|
|
22258
22651
|
dependencies
|
|
22259
22652
|
});
|
|
22260
22653
|
warnings.push(...verified.warnings);
|
|
22261
|
-
if (verified.item)
|
|
22654
|
+
if (verified.item) found.push({
|
|
22655
|
+
item: verified.item,
|
|
22656
|
+
text: verified.text
|
|
22657
|
+
});
|
|
22262
22658
|
if (verified.exclusion === "existing-link") existingLinkExclusions += 1;
|
|
22263
22659
|
if (verified.exclusion === "technical") technicalExclusions += 1;
|
|
22264
22660
|
if (verified.exclusion === "self-alias") selfAliasExclusions += 1;
|
|
22265
22661
|
if (verified.exclusion === "failed") failedChecks += 1;
|
|
22266
22662
|
else checkedSources += 1;
|
|
22267
22663
|
}
|
|
22664
|
+
const items = placeLinks(found, target.text);
|
|
22268
22665
|
const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
|
|
22269
22666
|
const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS);
|
|
22270
22667
|
const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS;
|
|
@@ -22326,13 +22723,14 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
22326
22723
|
},
|
|
22327
22724
|
methodology: {
|
|
22328
22725
|
id: "gsc_internal_link_candidates",
|
|
22329
|
-
version:
|
|
22726
|
+
version: 4,
|
|
22330
22727
|
lexicalTargetLimit: 50,
|
|
22331
22728
|
matchedQueryEvidenceLimit: 10,
|
|
22332
22729
|
observedLinkEvidenceLimit: 20,
|
|
22333
22730
|
matching: "pairwise_exact_then_precision_lexical",
|
|
22334
22731
|
ranking: "exact_matches_then_matched_query_impressions_then_relevance",
|
|
22335
|
-
contextualPlacementVerified: true
|
|
22732
|
+
contextualPlacementVerified: true,
|
|
22733
|
+
placement: "tfidf_sentence_match_then_phrase_anchors"
|
|
22336
22734
|
},
|
|
22337
22735
|
filters: {
|
|
22338
22736
|
minImpressions,
|
|
@@ -22364,18 +22762,20 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
22364
22762
|
failedChecks,
|
|
22365
22763
|
uncheckedCandidates,
|
|
22366
22764
|
matchedQueryImpressions: items.reduce((sum, item) => sum + item.matchedQueryImpressions, 0),
|
|
22367
|
-
brandFiltering:
|
|
22765
|
+
brandFiltering: "excluded",
|
|
22368
22766
|
verdict: summaryVerdict
|
|
22369
22767
|
},
|
|
22370
22768
|
items,
|
|
22371
22769
|
warnings: structuredWarnings,
|
|
22372
22770
|
caveats: [
|
|
22373
|
-
`Date window: ${range.startDate} to ${range.endDate} (${days} days), using final GSC data where available
|
|
22771
|
+
`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.`}`,
|
|
22772
|
+
`Limits: up to ${checkLimit} source pages are checked live and up to ${limit} are returned. Queries need at least ${minImpressions} impression.`,
|
|
22374
22773
|
`Selection: ${analysis.selection.targetEligibleQueries} eligible target queries, ${analysis.selection.candidateUrls} matched source pages, ${attemptedSources} attempted, ${checkedSources} successfully checked, ${items.length} returned.`,
|
|
22375
|
-
`
|
|
22774
|
+
`Brand queries were left out. Brand terms found from the domain: ${brandTerms.join(", ") || "none"}.`,
|
|
22376
22775
|
"GSC query rows omit anonymized queries and lower-volume rows. A 100,000-row cap is reported separately when reached.",
|
|
22377
22776
|
"Exact query overlap is medium-confidence affinity evidence; lexical matches are low-confidence review evidence. Neither predicts traffic lift or link equity.",
|
|
22378
22777
|
"The fetch checks HTML link placement, but it cannot prove editorial relevance. Review intent and reader usefulness before adding a link.",
|
|
22778
|
+
"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.",
|
|
22379
22779
|
uncheckedCandidates ? `${uncheckedCandidates} matched source page${uncheckedCandidates === 1 ? " was" : "s were"} not checked because the output or check limit was reached.` : "",
|
|
22380
22780
|
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.` : ""
|
|
22381
22781
|
].filter(Boolean),
|
|
@@ -22383,6 +22783,29 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
|
|
|
22383
22783
|
ledgerSummary: ledger.summary()
|
|
22384
22784
|
};
|
|
22385
22785
|
}
|
|
22786
|
+
function placeLinks(found, target) {
|
|
22787
|
+
const corpus = new LinkCorpus([target, ...found.map((entry) => entry.text)].filter((page) => Boolean(page)).map(termCounts));
|
|
22788
|
+
const placed = found.map(({ item, text }) => {
|
|
22789
|
+
const placement = target && text ? linkPlacement(text, target, corpus) : {
|
|
22790
|
+
contentSimilarity: 0,
|
|
22791
|
+
sentences: []
|
|
22792
|
+
};
|
|
22793
|
+
const best = placement.sentences[0];
|
|
22794
|
+
const recommendation = best ? {
|
|
22795
|
+
...item.recommendation,
|
|
22796
|
+
action: `${item.recommendation.action} It fits in: "${best.text}" Anchor text could be ${best.anchors.map((anchor) => `"${anchor}"`).join(" or ")}.`
|
|
22797
|
+
} : {
|
|
22798
|
+
...item.recommendation,
|
|
22799
|
+
action: `${item.recommendation.action} No sentence on this page names the target, so a link here needs new text.`
|
|
22800
|
+
};
|
|
22801
|
+
return {
|
|
22802
|
+
...item,
|
|
22803
|
+
placement,
|
|
22804
|
+
recommendation
|
|
22805
|
+
};
|
|
22806
|
+
});
|
|
22807
|
+
return [...placed.filter((item) => item.placement.sentences.length > 0), ...placed.filter((item) => item.placement.sentences.length === 0)];
|
|
22808
|
+
}
|
|
22386
22809
|
//#endregion
|
|
22387
22810
|
//#region packages/core/src/analyze/url-templates.ts
|
|
22388
22811
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -26316,6 +26739,16 @@ const SECTIONS = {
|
|
|
26316
26739
|
bing: "Bing’s top queries from Bing Webmaster Tools with Google’s clicks, impressions and position for the same query beside them, and the same for pages."
|
|
26317
26740
|
};
|
|
26318
26741
|
const SECTION_IDS = Object.keys(SECTIONS);
|
|
26742
|
+
new Map(`
|
|
26743
|
+
ae:ar al:sq am:hy ao:pt ar:es at:de au:en az:az ba:bs bd:bn be:nl bf:fr
|
|
26744
|
+
bg:bg bh:ar bo:es br:pt ca:en ch:de ci:fr cl:es cm:fr co:es cr:es cy:el
|
|
26745
|
+
cz:cs de:de dk:da dz:ar ec:es ee:et eg:ar es:es fi:fi fr:fr gb:en gh:en
|
|
26746
|
+
gr:el gt:es hk:en hr:hr hu:hu id:id ie:en il:he in:en it:it jo:ar jp:ja
|
|
26747
|
+
ke:en kh:en kr:ko kz:ru lk:en lt:lt lv:lv ma:ar mc:fr md:ro mk:mk mm:en
|
|
26748
|
+
mt:en mx:es my:en ng:en ni:es nl:nl no:nb nz:en pa:es pe:es ph:en pk:en
|
|
26749
|
+
pl:pl pt:pt py:es ro:ro rs:sr sa:ar se:sv sg:en si:sl sk:sk sn:fr sv:es
|
|
26750
|
+
th:th tn:ar tr:tr tw:zh-TW ua:uk us:en uy:es ve:es vn:vi za:en
|
|
26751
|
+
`.trim().split(/\s+/).map((pair) => pair.split(":")));
|
|
26319
26752
|
//#endregion
|
|
26320
26753
|
//#region packages/core/src/commands/research.ts
|
|
26321
26754
|
const RESEARCH_ACTIONS = [
|
|
@@ -26601,6 +27034,7 @@ function nodeCommandDeps(options) {
|
|
|
26601
27034
|
return null;
|
|
26602
27035
|
}
|
|
26603
27036
|
},
|
|
27037
|
+
pageMarkdown: async (url) => extractMainMarkdown(await fetchPage(url, { refresh: options.refresh })),
|
|
26604
27038
|
call: options.call,
|
|
26605
27039
|
bing: localBing,
|
|
26606
27040
|
bingConnect: LOCAL_BING_CONNECT
|