seo 0.5.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/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-CsRkLZGn.js";
2
+ import { n as load, t as version } from "./package-CmplBMg7.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";
@@ -1598,7 +1598,7 @@ async function runAnalyticsReport(gfetch, propertyId, body, options) {
1598
1598
  //#endregion
1599
1599
  //#region packages/core/src/bing/api.ts
1600
1600
  const BASE = "https://ssl.bing.com/webmaster/api.svc/json";
1601
- const MAX_BYTES$1 = 8e6;
1601
+ const MAX_BYTES$2 = 8e6;
1602
1602
  function bingCall(auth, options) {
1603
1603
  return async (method, params = {}) => {
1604
1604
  const url = new URL(`${BASE}/${method}`);
@@ -1623,7 +1623,7 @@ function bingCall(auth, options) {
1623
1623
  if (response.status === 429 || /throttle/i.test(detail)) throw new SeoError("RATE_LIMITED", "Bing Webmaster is limiting requests. Try again in a few minutes.");
1624
1624
  throw new SeoError("OPTIONAL_PROVIDER_UNAVAILABLE", `Bing Webmaster answered ${response.status}. ${detail}`.trim());
1625
1625
  }
1626
- return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$1 })).d;
1626
+ return (await readJson(response, "Bing Webmaster", { maxBytes: MAX_BYTES$2 })).d;
1627
1627
  };
1628
1628
  }
1629
1629
  //#endregion
@@ -2088,13 +2088,13 @@ let credentials;
2088
2088
  function expiresSoon(value) {
2089
2089
  return [...value.google, ...value.bing].some((item) => "expiresAt" in item && item.expiresAt - EARLY < Date.now());
2090
2090
  }
2091
- async function cloudCredentials() {
2091
+ async function cloudCredentials(options = {}) {
2092
2092
  const current = credentials ? await credentials.catch(() => null) : null;
2093
- if (current && !expiresSoon(current)) return current;
2093
+ if (current && !expiresSoon(current) && !options.refreshClicky) return current;
2094
2094
  credentials = (async () => {
2095
2095
  const token = cloudToken();
2096
2096
  if (!token) throw new SeoError("AUTH_REQUIRED", notSignedIn());
2097
- const result = await call$1("/account/cli/credentials", { token });
2097
+ const result = await call$1(`/account/cli/credentials${options.refreshClicky ? "?clicky=refresh" : ""}`, { token });
2098
2098
  if (result.status === 401) throw new SeoError("AUTH_EXPIRED", `This machine is not linked to a seoskill.dev account any more. ${CLOUD_SIGN_IN}`);
2099
2099
  if (!result.ok) throw new SeoError("PROVIDER_UNAVAILABLE", `seoskill.dev returned ${result.status}. Try again in a minute.`);
2100
2100
  return result.body;
@@ -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
- return {
4766
- text,
4767
- excerpt: normalizedText(article?.excerpt),
4768
- wordCount: countCjkAwareWords(text),
4769
- diagnostics,
4770
- warnings: fallback && fallbackDetail ? [`Defuddle extraction fell back to Readability: ${fallbackDetail}`] : []
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 { document } = parseHTML(fetchResult.html);
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.textContent ?? "";
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([
@@ -14518,6 +14555,9 @@ async function storedSiteKeys() {
14518
14555
  source: credential.source
14519
14556
  };
14520
14557
  }
14558
+ async function readClickySiteKeys() {
14559
+ return (await storedSiteKeys()).siteKeys;
14560
+ }
14521
14561
  async function readClickySiteKey(siteIdValue, input = {}) {
14522
14562
  const siteId = normalizeClickySiteId(siteIdValue);
14523
14563
  const environmentKey = (input.env ?? process.env)[CLICKY_SITEKEY_ENV]?.trim();
@@ -16679,7 +16719,7 @@ function analyzeCtrUnderperformersFromRows(input) {
16679
16719
  //#endregion
16680
16720
  //#region packages/core/src/analyze/ctr-underperformers-report.ts
16681
16721
  const MAX_GSC_ROWS$3 = 1e5;
16682
- const DEFAULT_DAYS$3 = 28;
16722
+ const DEFAULT_DAYS$4 = 28;
16683
16723
  const MAX_DAYS$2 = 548;
16684
16724
  function plural$4(count, singular, pluralLabel = `${singular}s`) {
16685
16725
  return count === 1 ? singular : pluralLabel;
@@ -16702,7 +16742,7 @@ async function ctrUnderperformersReportWith(input, dependencies) {
16702
16742
  const explicitRange = explicitDateRange(input, MAX_DAYS$2);
16703
16743
  const days = explicitRange?.days ?? integerOption$1({
16704
16744
  value: input.days,
16705
- fallback: DEFAULT_DAYS$3,
16745
+ fallback: DEFAULT_DAYS$4,
16706
16746
  minimum: 1,
16707
16747
  maximum: MAX_DAYS$2,
16708
16748
  label: "Days"
@@ -19959,7 +19999,7 @@ function quickWinTemplateSummaries(items) {
19959
19999
  }
19960
20000
  //#endregion
19961
20001
  //#region packages/core/src/analyze/site-diagnostics/quick-wins-report.ts
19962
- const DEFAULT_DAYS$2 = 28;
20002
+ const DEFAULT_DAYS$3 = 28;
19963
20003
  const MAX_DAYS$1 = 548;
19964
20004
  const DEFAULT_VERIFY_LIMIT$1 = 5;
19965
20005
  const MAX_VERIFY_LIMIT = 100;
@@ -19981,7 +20021,7 @@ async function quickWinsReportWith(input, dependencies) {
19981
20021
  const explicitRange = explicitDateRange(input, MAX_DAYS$1);
19982
20022
  const days = explicitRange?.days ?? integerOption$1({
19983
20023
  value: input.days,
19984
- fallback: DEFAULT_DAYS$2,
20024
+ fallback: DEFAULT_DAYS$3,
19985
20025
  minimum: 1,
19986
20026
  maximum: MAX_DAYS$1,
19987
20027
  label: "Days"
@@ -20405,7 +20445,7 @@ function analyzeStrikingDistanceRows(input) {
20405
20445
  }
20406
20446
  //#endregion
20407
20447
  //#region packages/core/src/analyze/striking-distance-report.ts
20408
- const DEFAULT_DAYS$1 = 28;
20448
+ const DEFAULT_DAYS$2 = 28;
20409
20449
  const MAX_DAYS = 548;
20410
20450
  const DEFAULT_VERIFY_LIMIT = 5;
20411
20451
  const MAX_SOURCE_ROWS$1 = 1e5;
@@ -20414,7 +20454,7 @@ function boundedInteger$2(value, fallback, minimum, maximum) {
20414
20454
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
20415
20455
  }
20416
20456
  function reportDays(value) {
20417
- if (value === void 0) return DEFAULT_DAYS$1;
20457
+ if (value === void 0) return DEFAULT_DAYS$2;
20418
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}.`);
20419
20459
  return value;
20420
20460
  }
@@ -21856,20 +21896,394 @@ function analyzeInternalLinksFromRows(input) {
21856
21896
  }
21857
21897
  };
21858
21898
  }
21859
- //#endregion
21860
- //#region packages/core/src/analyze/internal-links-report.ts
21861
- function internalLinksIntegerOption(input) {
21862
- const value = input.value ?? input.fallback;
21863
- if (!Number.isInteger(value) || value < input.minimum || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be a whole number between ${input.minimum} and ${input.maximum}.`);
21864
- return value;
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
+ }
21865
21991
  }
21866
- function internalLinksNumberOption(input) {
21867
- const value = input.value ?? input.fallback;
21868
- if (!Number.isFinite(value) || value < input.minimum || value > input.maximum) throw new SeoError("INVALID_INPUT", `${input.label} must be between ${input.minimum} and ${input.maximum}.`);
21869
- return value;
21992
+ function siteName(url) {
21993
+ try {
21994
+ return new URL(url).hostname.replace(/^www\./u, "").split(".")[0] ?? "";
21995
+ } catch {
21996
+ return "";
21997
+ }
21870
21998
  }
21871
- function internalLinksReportRange(days, now) {
21872
- return finalGscDateRange(days, now);
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
+ };
21873
22287
  }
21874
22288
  function completeInternalLinksSelection(analysis, runtime) {
21875
22289
  return {
@@ -21927,9 +22341,8 @@ async function verifyInternalLinkTarget(input) {
21927
22341
  let fetched;
21928
22342
  try {
21929
22343
  fetched = await input.dependencies.fetch(input.targetUrl, {
21930
- js: input.js ?? "auto",
21931
- refresh: input.refresh,
21932
- rate: input.rate
22344
+ js: "auto",
22345
+ refresh: input.refresh
21933
22346
  });
21934
22347
  } catch (error) {
21935
22348
  return {
@@ -21999,7 +22412,12 @@ async function verifyInternalLinkTarget(input) {
21999
22412
  verification: fatalSignals.length ? "technical-issue" : "verified",
22000
22413
  technicalSignals: signals,
22001
22414
  fetchDiagnostics: fetched.diagnostics,
22002
- warnings
22415
+ warnings,
22416
+ text: placementPage({
22417
+ ...page,
22418
+ url: page.finalUrl,
22419
+ html: fetched.html
22420
+ })
22003
22421
  };
22004
22422
  }
22005
22423
  function linksToAliases(page, aliases) {
@@ -22033,9 +22451,8 @@ async function verifyInternalLinkCandidate(input) {
22033
22451
  let fetched;
22034
22452
  try {
22035
22453
  fetched = await input.dependencies.fetch(input.candidate.sourceUrl, {
22036
- js: input.js ?? "auto",
22037
- refresh: input.refresh,
22038
- rate: input.rate
22454
+ js: "auto",
22455
+ refresh: input.refresh
22039
22456
  });
22040
22457
  } catch (error) {
22041
22458
  return {
@@ -22125,12 +22542,21 @@ async function verifyInternalLinkCandidate(input) {
22125
22542
  confidence
22126
22543
  }
22127
22544
  },
22545
+ text: placementPage({
22546
+ ...page,
22547
+ url: page.finalUrl,
22548
+ html: fetched.html
22549
+ }),
22128
22550
  warnings
22129
22551
  };
22130
22552
  }
22131
22553
  //#endregion
22132
22554
  //#region packages/core/src/analyze/internal-links.ts
22133
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;
22134
22560
  const defaultDependencies$2 = {
22135
22561
  searchAnalytics: querySearchAnalytics,
22136
22562
  fetch: fetchPage,
@@ -22141,43 +22567,17 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
22141
22567
  const targetPage = new URL(assertUrlMatchesGscProperty(input.site, input.targetUrl));
22142
22568
  targetPage.hash = "";
22143
22569
  const targetUrl = targetPage.toString();
22144
- const days = internalLinksIntegerOption({
22145
- value: input.days,
22146
- fallback: 28,
22147
- minimum: 1,
22148
- maximum: 548,
22149
- label: "days"
22150
- });
22151
- const limit = internalLinksIntegerOption({
22152
- value: input.limit,
22153
- fallback: 20,
22154
- minimum: 1,
22155
- maximum: 100,
22156
- label: "limit"
22157
- });
22158
- const checkLimit = internalLinksIntegerOption({
22159
- value: input.checkLimit,
22160
- fallback: Math.max(limit, 40),
22161
- minimum: 1,
22162
- maximum: 200,
22163
- label: "checkLimit"
22164
- });
22165
- const minImpressions = internalLinksNumberOption({
22166
- value: input.minImpressions,
22167
- fallback: 1,
22168
- minimum: 0,
22169
- maximum: 1e9,
22170
- label: "minImpressions"
22171
- });
22570
+ const limit = RESULT_LIMIT;
22571
+ const checkLimit = CHECK_LIMIT;
22572
+ const minImpressions = MIN_IMPRESSIONS;
22172
22573
  const now = dependencies.now();
22173
- const range = internalLinksReportRange(days, now);
22574
+ const { days, ...range } = internalLinksReportRange(input, DEFAULT_DAYS$1, now);
22575
+ const brandTerms = deriveBrandTerms({ siteUrl: input.site });
22174
22576
  const ledger = new SessionLedger();
22175
22577
  const target = await verifyInternalLinkTarget({
22176
22578
  site: input.site,
22177
22579
  targetUrl,
22178
- js: input.js,
22179
22580
  refresh: input.refresh,
22180
- rate: input.rate,
22181
22581
  dependencies
22182
22582
  });
22183
22583
  const targetRequests = await Promise.all(target.aliases.map(async (alias) => {
@@ -22209,8 +22609,7 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
22209
22609
  site: input.site,
22210
22610
  targetAliases: target.aliases,
22211
22611
  minImpressions,
22212
- brandTerms: input.brandTerms,
22213
- includeBrand: input.includeBrand
22612
+ brandTerms
22214
22613
  });
22215
22614
  const queryCandidates = target.verification === "verified" && preliminary.selection.targetEligibleQueries > 0;
22216
22615
  const sourceResult = queryCandidates ? await dependencies.searchAnalytics(input.site, {
@@ -22231,10 +22630,9 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
22231
22630
  site: input.site,
22232
22631
  targetAliases: target.aliases,
22233
22632
  minImpressions,
22234
- brandTerms: input.brandTerms,
22235
- includeBrand: input.includeBrand
22633
+ brandTerms
22236
22634
  });
22237
- const items = [];
22635
+ const found = [];
22238
22636
  const warnings = [...target.warnings];
22239
22637
  let attemptedSources = 0;
22240
22638
  let checkedSources = 0;
@@ -22243,25 +22641,27 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
22243
22641
  let selfAliasExclusions = 0;
22244
22642
  let failedChecks = 0;
22245
22643
  for (const candidate of analysis.candidates) {
22246
- if (attemptedSources >= checkLimit || items.length >= limit) break;
22644
+ if (attemptedSources >= checkLimit || found.length >= limit) break;
22247
22645
  attemptedSources += 1;
22248
22646
  const verified = await verifyInternalLinkCandidate({
22249
22647
  site: input.site,
22250
22648
  candidate,
22251
22649
  target,
22252
- js: input.js,
22253
22650
  refresh: input.refresh,
22254
- rate: input.rate,
22255
22651
  dependencies
22256
22652
  });
22257
22653
  warnings.push(...verified.warnings);
22258
- if (verified.item) items.push(verified.item);
22654
+ if (verified.item) found.push({
22655
+ item: verified.item,
22656
+ text: verified.text
22657
+ });
22259
22658
  if (verified.exclusion === "existing-link") existingLinkExclusions += 1;
22260
22659
  if (verified.exclusion === "technical") technicalExclusions += 1;
22261
22660
  if (verified.exclusion === "self-alias") selfAliasExclusions += 1;
22262
22661
  if (verified.exclusion === "failed") failedChecks += 1;
22263
22662
  else checkedSources += 1;
22264
22663
  }
22664
+ const items = placeLinks(found, target.text);
22265
22665
  const uncheckedCandidates = Math.max(0, analysis.candidates.length - attemptedSources);
22266
22666
  const targetTruncated = targetRequests.some(({ result }) => result.rowsFetched >= MAX_GSC_ROWS);
22267
22667
  const sourceTruncated = sourceResult.rowsFetched >= MAX_GSC_ROWS;
@@ -22323,13 +22723,14 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
22323
22723
  },
22324
22724
  methodology: {
22325
22725
  id: "gsc_internal_link_candidates",
22326
- version: 3,
22726
+ version: 4,
22327
22727
  lexicalTargetLimit: 50,
22328
22728
  matchedQueryEvidenceLimit: 10,
22329
22729
  observedLinkEvidenceLimit: 20,
22330
22730
  matching: "pairwise_exact_then_precision_lexical",
22331
22731
  ranking: "exact_matches_then_matched_query_impressions_then_relevance",
22332
- contextualPlacementVerified: true
22732
+ contextualPlacementVerified: true,
22733
+ placement: "tfidf_sentence_match_then_phrase_anchors"
22333
22734
  },
22334
22735
  filters: {
22335
22736
  minImpressions,
@@ -22361,18 +22762,20 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
22361
22762
  failedChecks,
22362
22763
  uncheckedCandidates,
22363
22764
  matchedQueryImpressions: items.reduce((sum, item) => sum + item.matchedQueryImpressions, 0),
22364
- brandFiltering: input.includeBrand ? "included" : "excluded",
22765
+ brandFiltering: "excluded",
22365
22766
  verdict: summaryVerdict
22366
22767
  },
22367
22768
  items,
22368
22769
  warnings: structuredWarnings,
22369
22770
  caveats: [
22370
- `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.`,
22371
22773
  `Selection: ${analysis.selection.targetEligibleQueries} eligible target queries, ${analysis.selection.candidateUrls} matched source pages, ${attemptedSources} attempted, ${checkedSources} successfully checked, ${items.length} returned.`,
22372
- `Minimum retained query impressions: ${minImpressions}. Brand queries were ${input.includeBrand ? "included" : "excluded when detected or configured"}.`,
22774
+ `Brand queries were left out. Brand terms found from the domain: ${brandTerms.join(", ") || "none"}.`,
22373
22775
  "GSC query rows omit anonymized queries and lower-volume rows. A 100,000-row cap is reported separately when reached.",
22374
22776
  "Exact query overlap is medium-confidence affinity evidence; lexical matches are low-confidence review evidence. Neither predicts traffic lift or link equity.",
22375
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.",
22376
22779
  uncheckedCandidates ? `${uncheckedCandidates} matched source page${uncheckedCandidates === 1 ? " was" : "s were"} not checked because the output or check limit was reached.` : "",
22377
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.` : ""
22378
22781
  ].filter(Boolean),
@@ -22380,6 +22783,29 @@ async function internalLinksReport(input, dependencies = defaultDependencies$2)
22380
22783
  ledgerSummary: ledger.summary()
22381
22784
  };
22382
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
+ }
22383
22809
  //#endregion
22384
22810
  //#region packages/core/src/analyze/url-templates.ts
22385
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;
@@ -26183,6 +26609,9 @@ const DIMENSIONS = [
26183
26609
  "searchAppearance"
26184
26610
  ];
26185
26611
  //#endregion
26612
+ //#region packages/core/src/commands/clicky.ts
26613
+ const CLICKY_KEY_PREFIX = "x-clicky-";
26614
+ //#endregion
26186
26615
  //#region packages/core/src/commands/page-facts.ts
26187
26616
  const MAX_PAGES = 20;
26188
26617
  const TIMEOUT_MS = 6e3;
@@ -26334,7 +26763,7 @@ function indexStatusFrom(result) {
26334
26763
  user_canonical: text(status.userCanonical)
26335
26764
  };
26336
26765
  }
26337
- const LOST_PAGES_RULE = `Pages that had at least 3 clicks in the previous 28 days and kept ${.2 * 100}% or less of them in the last 28 days, with a live check of the top 20 and Google’s index status for the top 10.`;
26766
+ const LOST_PAGES_RULE = `Pages that had at least 3 clicks in the previous 28 days and kept ${.2 * 100}% or less of them in the last 28 days, with a live check of the top 20 and Google’s index status for the top 10 and the home page.`;
26338
26767
  //#endregion
26339
26768
  //#region packages/core/src/commands/tools.ts
26340
26769
  const site = z.string().min(1).describe("Search Console property or site, such as sc-domain:example.com, https://example.com/ or example.com.");
@@ -26353,7 +26782,8 @@ const detail = {
26353
26782
  const DATA_SOURCES = [
26354
26783
  "search_console",
26355
26784
  "google_analytics",
26356
- "bing"
26785
+ "bing",
26786
+ "clicky"
26357
26787
  ];
26358
26788
  z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`).join("\n")}`, z.object({
26359
26789
  site,
@@ -26380,7 +26810,7 @@ z.object({}), `${Object.entries(SECTIONS).map(([id, text]) => `- ${id}: ${text}`
26380
26810
  ...detail
26381
26811
  }), z.object({
26382
26812
  source: z.enum(DATA_SOURCES).default("search_console"),
26383
- site: z.string().min(1).describe("search_console and bing: the property or site. google_analytics: numeric property id from seo_sites."),
26813
+ site: z.string().min(1).describe("search_console and bing: the property or site. google_analytics: numeric property id from seo_sites. clicky: the site or its Clicky site ID."),
26384
26814
  dimensions: z.array(z.enum(DIMENSIONS)).max(4).default(["query"]).describe("search_console; bing uses the first, query or page."),
26385
26815
  page: z.string().min(1).optional().describe("search_console only: URL text, such as /blog/."),
26386
26816
  query: z.string().min(1).optional().describe("search_console only: query text."),
@@ -26594,6 +27024,7 @@ function nodeCommandDeps(options) {
26594
27024
  return null;
26595
27025
  }
26596
27026
  },
27027
+ pageMarkdown: async (url) => extractMainMarkdown(await fetchPage(url, { refresh: options.refresh })),
26597
27028
  call: options.call,
26598
27029
  bing: localBing,
26599
27030
  bingConnect: LOCAL_BING_CONNECT
@@ -26616,12 +27047,17 @@ async function localAccounts() {
26616
27047
  }));
26617
27048
  }
26618
27049
  async function localResearchKeys() {
26619
- const [dataForSeo, ahrefs, saved] = await Promise.all([
27050
+ const [dataForSeo, ahrefs, clicky, saved] = await Promise.all([
26620
27051
  readDataForSeoCredentials().catch(() => void 0),
26621
27052
  readAhrefsApiKey().catch(() => void 0),
27053
+ readClickySiteKeys().catch(() => ({})),
26622
27054
  cloudLinked() ? cloudKeys().catch(() => ({})) : {}
26623
27055
  ]);
26624
27056
  const keys = new Headers(saved);
27057
+ for (const [siteId, sitekey] of Object.entries(clicky)) {
27058
+ const name = `${CLICKY_KEY_PREFIX}${siteId}`;
27059
+ if (!keys.has(name)) keys.set(name, sitekey);
27060
+ }
26625
27061
  if (dataForSeo) {
26626
27062
  keys.set("x-dataforseo-login", dataForSeo.login);
26627
27063
  keys.set("x-dataforseo-password", dataForSeo.password);
@@ -26629,6 +27065,11 @@ async function localResearchKeys() {
26629
27065
  if (ahrefs?.apiKey) keys.set("x-ahrefs-key", ahrefs.apiKey);
26630
27066
  return keys;
26631
27067
  }
27068
+ async function localRefreshClicky() {
27069
+ if (!cloudLinked()) return null;
27070
+ await cloudCredentials({ refreshClicky: true });
27071
+ return localResearchKeys();
27072
+ }
26632
27073
  async function resolveLocalSite(site, options = {}) {
26633
27074
  const found = matchProperty((await listSites(options.refresh, options.account)).map((entry) => entry.siteUrl), site);
26634
27075
  if (!found) throw new CommandError("NO_ACCESS", `This Google login has no Search Console property for ${site}. Run \`seo sites\` to list the properties it can read.`);
@@ -28934,6 +29375,6 @@ function isoDate(daysAgo = 0) {
28934
29375
  return date.toISOString().slice(0, 10);
28935
29376
  }
28936
29377
  //#endregion
28937
- export { AHREFS_API_KEY_ENV, AHREFS_API_KEY_SECRET, AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, ANALYTICS_PROVIDER_IDS, AUDIT_PAGE_RULE_IDS, AhrefsClient, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CLICKY_SITEKEY_ENV, CLICKY_SITE_KEYS_SECRET, CLOUD_SIGN_IN, CLOUD_URL, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, LOCAL_BING_CONNECT, LOCAL_SIGN_IN, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MANAGED_PROVIDER_SECRET_NAMES, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, MAX_SEARCH_CONSOLE_EXPORT_FILES, MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_PROVIDER_API_VERSION, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, STEP_CHANGE_RULES, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_FAILURE_CONTEXTS, TELEMETRY_FAILURE_REASONS, TELEMETRY_NOTICE, TELEMETRY_OPERATIONS, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, agentActionsView, aiReferralSourceForValue, aiReferralsReport, analyticsConnectionLabel, analyticsConnectionProviderId, analyticsConnectionSchema, analyticsProviderDetails, analyticsProviderSchema, analyzeAiReferralRows, analyzeCannibalImportRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, auditCrawlPages, auditCrawlRequests, auditPage, authStatus, bearerFetch, boundedSitemapInventory, buildAgentFindings, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, ctrUnderperformersReportWith, decayClusterDrafts, decayComparisonRange, decayingReport, decayingReportWith, deleteAhrefsApiKey, deleteBingApiKey, deleteClickySiteKey, deleteDataForSeoCredentials, deleteManagedProviderSecrets, deleteProviderExtensionAccount, deleteProviderExtensionCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, describeInstalledProviderExtension, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, finishCloudLogin, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, getAuthModeStatus, getCacheStats, getClientConfig, getDb, getPrivacySnapshot, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleJson, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoverageSignals, inferTemplateMovement, initializeTelemetry, inspectProviderPackage, inspectSearchConsoleUrl, inspectUrl, installProviderPackage, installedProviderPackageSchema, installedProviderPackagesSchema, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, listAnalyticsAccounts, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, localResearchKeys, loginWithLoopback, makeRunId, matchGa4WebStreams, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, nodeCommandDeps, normalizeBrandText, normalizeClickySiteId, normalizeClickySiteKey, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, normalizeProviderExtensionAccount, normalizedResearchColumnName, notSignedIn, noteCacheWrite, openCloudLink, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, pendingCloudLogin, performanceAudit, performanceReportIsCacheable, plural, providerCapabilitySchema, providerConnectionFieldSchema, providerExactVersionSchema, providerExtensionIdSchema, providerIdSchema, providerIntegritySchema, providerKindSchema, providerPackageManifestSchema, providerPackageNameSchema, providerPreferenceSchema, providerRegistrationSchema, providerRequestJson, providerRequestText, pruneLogs, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, quickWinsReportWith, readAhrefsApiKey, readAllTokens, readBingApiKey, readBoundedResponseText, readClickySiteKey, readConfig, readDataForSeoCredentials, readInstalledProviderPackages, readJson, readJsonOrUndefined, readOauthClient, readProviderExtensionAccount, readProviderExtensionCredentials, readProviderSecret, readTokens, recommendationForRule, reconcileExportPagesWithCrawl, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, registrableDomain, removeInstalledProviderPackage, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderWorkflowMarkdown, reportNarrative, requestHeaders, requirePageVerification, researchImportColumns, resetSeoData, resolveLocalSite, resolveSegmentRanges, resolveSeoCliPaths, reviewObservations, roundedPosition, runAnalyticsProviderLandingPages, runAnalyticsReport, runDoctor, runGa4Report, runProviderExtensionAction, runtimeForProvider, safeProviderAdapterError, saveInstalledProviderPackage, searchAnalyticsRows, searchUpdatesFromIncidents, secondPage, secondPageWith, segmentComparisonRange, segmentImpact, segmentRangeDays, seoErrorEnvelope, sessionSourceInListFilter, setActiveGoogleAccount, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, startCloudLogin, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeTokens, xRobotsDirectives };
29378
+ export { AHREFS_API_KEY_ENV, AHREFS_API_KEY_SECRET, AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, ANALYTICS_PROVIDER_IDS, AUDIT_PAGE_RULE_IDS, AhrefsClient, BING_API_KEY_ENV, BING_API_KEY_SECRET, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CLICKY_SITEKEY_ENV, CLICKY_SITE_KEYS_SECRET, CLOUD_SIGN_IN, CLOUD_URL, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DATAFORSEO_CREDENTIAL_SECRET, DATAFORSEO_LOGIN_ENV, DATAFORSEO_PASSWORD_ENV, DEFAULT_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, DEFAULT_UNREACHED_PAGE_LIMIT, DataForSeoClient, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, GoogleApiError, INCIDENTS_URL, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, LOCAL_BING_CONNECT, LOCAL_SIGN_IN, LOG_MAX_AGE_MS, LOG_MAX_FILE_BYTES, LOG_MAX_TOTAL_BYTES, LocalCredentialsProvider, MANAGED_PROVIDER_SECRET_NAMES, MAX_CRAWL_CONCURRENCY, MAX_CRAWL_DEPTH, MAX_CRAWL_PAGES, MAX_CRAWL_TIMEOUT_MS, MAX_SEARCH_CONSOLE_EXPORT_FILES, MAX_SEARCH_CONSOLE_EXPORT_ROW_LIMIT, PROVIDER_SECRET_NAMES, ProviderError, ProviderExtensionRegistry, RESEARCH_IMPORT_COLUMN_KEYS, ResponseSizeLimitError, SEO_CRAWLER_IDENTITY, SEO_CRAWLER_TOKEN, SEO_CRAWLER_USER_AGENT, SEO_PROVIDER_API_VERSION, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, STEP_CHANGE_RULES, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_FAILURE_CONTEXTS, TELEMETRY_FAILURE_REASONS, TELEMETRY_NOTICE, TELEMETRY_OPERATIONS, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, agentActionsView, aiReferralSourceForValue, aiReferralsReport, analyticsConnectionLabel, analyticsConnectionProviderId, analyticsConnectionSchema, analyticsProviderDetails, analyticsProviderSchema, analyzeAiReferralRows, analyzeCannibalImportRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertCrawlConfigLimits, assertUrlMatchesGscProperty, auditCrawlPages, auditCrawlRequests, auditPage, authStatus, bearerFetch, boundedSitemapInventory, buildAgentFindings, buildQueryClusterReportFromRows, buildTelemetryPayload, cannibalImportReport, cannibalReport, cannibalReportWith, changeWindow, checkDatabaseReadiness, classifyTelemetryFailure, clearCache, cloudAccountFor, cloudBing, cloudCredentials, cloudGoogleEmails, cloudGoogleToken, cloudKeys, cloudLinked, cloudLogout, clusterQueryRows, combineRobotsValues, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlStatusOnly, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, ctrUnderperformersReportWith, decayClusterDrafts, decayComparisonRange, decayingReport, decayingReportWith, deleteAhrefsApiKey, deleteBingApiKey, deleteClickySiteKey, deleteDataForSeoCredentials, deleteManagedProviderSecrets, deleteProviderExtensionAccount, deleteProviderExtensionCredentials, deleteProviderSecret, deleteTokens, deriveBrandTerms, describeInstalledProviderExtension, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensurePrivateDatabaseFiles, ensureSeoCliDirs, estimateSerpTitleWidth, expectedCtrForPosition, explainRule, fetchCruxFieldData, fetchPage, fetchPageStatus, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, finishCloudLogin, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, getAuthModeStatus, getCacheStats, getClientConfig, getDb, getPrivacySnapshot, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleJson, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoverageSignals, inferTemplateMovement, initializeTelemetry, inspectProviderPackage, inspectSearchConsoleUrl, inspectUrl, installProviderPackage, installedProviderPackageSchema, installedProviderPackagesSchema, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isSkippableReportError, isTelemetryCi, isTelemetryOperation, isTelemetryReport, isoDate, isoWeek, latestFinalGscDate, linkRecover, linkRecoverRecommendation, listAnalyticsAccounts, listGa4AccountSummaries, listGa4DataStreams, listGoogleAccounts, listInstalledProviderExtensions, listRules, listSearchConsoleSites, listSearchUpdates, listSites, loadInstalledProviderExtensions, loadProviderExtensions, loadProviderPackage, loadSearchConsoleExport, localAccounts, localBing, localCredentialsProvider, localError, localGoogleFetch, localRefreshClicky, localResearchKeys, loginWithLoopback, makeRunId, matchGa4WebStreams, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, nodeCommandDeps, normalizeBrandText, normalizeClickySiteId, normalizeClickySiteKey, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, normalizeProviderExtensionAccount, normalizedResearchColumnName, notSignedIn, noteCacheWrite, openCloudLink, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, pendingCloudLogin, performanceAudit, performanceReportIsCacheable, plural, providerCapabilitySchema, providerConnectionFieldSchema, providerExactVersionSchema, providerExtensionIdSchema, providerIdSchema, providerIntegritySchema, providerKindSchema, providerPackageManifestSchema, providerPackageNameSchema, providerPreferenceSchema, providerRegistrationSchema, providerRequestJson, providerRequestText, pruneLogs, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, quickWinsReportWith, readAhrefsApiKey, readAllTokens, readBingApiKey, readBoundedResponseText, readClickySiteKey, readClickySiteKeys, readConfig, readDataForSeoCredentials, readInstalledProviderPackages, readJson, readJsonOrUndefined, readOauthClient, readProviderExtensionAccount, readProviderExtensionCredentials, readProviderSecret, readTokens, recommendationForRule, reconcileExportPagesWithCrawl, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, registrableDomain, removeInstalledProviderPackage, renderCrawlCsv, renderCrawlHtml, renderCrawlJunit, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderWorkflowMarkdown, reportNarrative, requestHeaders, requirePageVerification, researchImportColumns, resetSeoData, resolveLocalSite, resolveSegmentRanges, resolveSeoCliPaths, reviewObservations, roundedPosition, runAnalyticsProviderLandingPages, runAnalyticsReport, runDoctor, runGa4Report, runProviderExtensionAction, runtimeForProvider, safeProviderAdapterError, saveInstalledProviderPackage, searchAnalyticsRows, searchUpdatesFromIncidents, secondPage, secondPageWith, segmentComparisonRange, segmentImpact, segmentRangeDays, seoErrorEnvelope, sessionSourceInListFilter, setActiveGoogleAccount, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, startCloudLogin, strikingDistance, strikingDistanceWith, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, telemetryErrorCategory, toSeoError, tokenSchema, tokenStoreSchema, topFixes, trackTelemetryCommandFailed, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, trafficAnomalyWith, unavailableSegmentImpactReport, unicodeTokens, uninstallProviderExtension, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateResearchImportColumnSources, verifyAnalyticsProvider, verifyProviderExtension, verifyQueryContent, verifyQueryContentWith, withAgentReportContract, workflowPresentation, writeAhrefsApiKey, writeBingApiKey, writeClickySiteKey, writeConfig, writeDataForSeoCredentials, writeInstalledProviderPackages, writeOauthClient, writeProviderExtensionAccount, writeProviderExtensionCredentials, writeProviderSecret, writeTokens, xRobotsDirectives };
28938
29379
 
28939
29380
  //# sourceMappingURL=index.js.map