seo 0.2.9 → 0.2.10
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 +1 -1
- package/dist/cli.js +114 -109
- package/dist/cli.js.map +1 -1
- package/dist/{dist-BrBR9umh.js → dist-CBY8QOnv.js} +1 -1
- package/dist/{dist-C6zXDARm.js → dist-w2XtKwjW.js} +80 -80
- package/dist/dist-w2XtKwjW.js.map +1 -0
- package/dist/index.d.ts +38 -3
- package/dist/index.js +318 -178
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +317 -177
- package/dist/mcp.js.map +1 -1
- package/dist/{package-C51oI-6o.js → package-92W1Zc-o.js} +2 -2
- package/dist/{package-C51oI-6o.js.map → package-92W1Zc-o.js.map} +1 -1
- package/package.json +1 -1
- package/dist/dist-C6zXDARm.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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-92W1Zc-o.js";
|
|
3
3
|
import { Headers, fetch as fetch$1 } from "undici";
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { arch, homedir, platform } from "node:os";
|
|
5
|
+
import { arch, homedir, platform, totalmem } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import lockfile from "proper-lockfile";
|
|
@@ -769,14 +769,14 @@ async function createGoogleAccessTokenClient() {
|
|
|
769
769
|
}
|
|
770
770
|
//#endregion
|
|
771
771
|
//#region packages/core/dist/storage/cache-maintenance.js
|
|
772
|
-
const MEBIBYTE$
|
|
772
|
+
const MEBIBYTE$2 = 1024 * 1024;
|
|
773
773
|
const CACHE_MAX_AGE_MS = 720 * 60 * 60 * 1e3;
|
|
774
774
|
const CACHE_LIMITS = {
|
|
775
|
-
gsc_cache: 48 * MEBIBYTE$
|
|
776
|
-
ga4_cache: 24 * MEBIBYTE$
|
|
777
|
-
semrush_cache: 16 * MEBIBYTE$
|
|
778
|
-
http_cache: 160 * MEBIBYTE$
|
|
779
|
-
performance_reports: 8 * MEBIBYTE$
|
|
775
|
+
gsc_cache: 48 * MEBIBYTE$2,
|
|
776
|
+
ga4_cache: 24 * MEBIBYTE$2,
|
|
777
|
+
semrush_cache: 16 * MEBIBYTE$2,
|
|
778
|
+
http_cache: 160 * MEBIBYTE$2,
|
|
779
|
+
performance_reports: 8 * MEBIBYTE$2
|
|
780
780
|
};
|
|
781
781
|
const CACHE_POLICIES = [
|
|
782
782
|
{
|
|
@@ -2571,7 +2571,7 @@ function isValidStructuredDate(value) {
|
|
|
2571
2571
|
const parsed = new Date(trimmed);
|
|
2572
2572
|
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === datePart;
|
|
2573
2573
|
}
|
|
2574
|
-
function parseJsonLd(blocks, baseUrl) {
|
|
2574
|
+
function parseJsonLd(blocks, baseUrl, retainJsonLd) {
|
|
2575
2575
|
const jsonLd = [];
|
|
2576
2576
|
const invalidJsonLdSamples = [];
|
|
2577
2577
|
const unrecognizedJsonLdTypes = [];
|
|
@@ -2643,7 +2643,7 @@ function parseJsonLd(blocks, baseUrl) {
|
|
|
2643
2643
|
blocks.forEach((block, index) => {
|
|
2644
2644
|
try {
|
|
2645
2645
|
const parsed = JSON.parse(block);
|
|
2646
|
-
jsonLd.push(parsed);
|
|
2646
|
+
if (retainJsonLd) jsonLd.push(parsed);
|
|
2647
2647
|
visit(parsed, { prefixes: /* @__PURE__ */ new Map() }, index, "$");
|
|
2648
2648
|
} catch (error) {
|
|
2649
2649
|
invalidJsonLdSamples.push({
|
|
@@ -2704,8 +2704,8 @@ function usableElementValue($, selector) {
|
|
|
2704
2704
|
return ($(element).attr("content") ?? $(element).attr("datetime") ?? $(element).text()).trim().length > 0;
|
|
2705
2705
|
});
|
|
2706
2706
|
}
|
|
2707
|
-
function extractStructuredData($, baseUrl) {
|
|
2708
|
-
const parsed = parseJsonLd($("script[type=\"application/ld+json\"]").toArray().map((element) => $(element).html() ?? ""), baseUrl);
|
|
2707
|
+
function extractStructuredData($, baseUrl, options = {}) {
|
|
2708
|
+
const parsed = parseJsonLd($("script[type=\"application/ld+json\"]").toArray().map((element) => $(element).html() ?? ""), baseUrl, options.retainJsonLd ?? true);
|
|
2709
2709
|
const microdata = microdataTypes($);
|
|
2710
2710
|
const rdfa = rdfaTypes($);
|
|
2711
2711
|
const schemaTypes = [.../* @__PURE__ */ new Set([
|
|
@@ -2739,6 +2739,16 @@ function extractStructuredData($, baseUrl) {
|
|
|
2739
2739
|
}
|
|
2740
2740
|
//#endregion
|
|
2741
2741
|
//#region packages/core/dist/extract/page-extractor.js
|
|
2742
|
+
const SOCIAL_PROFILE_HOSTS = [
|
|
2743
|
+
"facebook.com",
|
|
2744
|
+
"instagram.com",
|
|
2745
|
+
"linkedin.com",
|
|
2746
|
+
"pinterest.com",
|
|
2747
|
+
"tiktok.com",
|
|
2748
|
+
"twitter.com",
|
|
2749
|
+
"x.com",
|
|
2750
|
+
"youtube.com"
|
|
2751
|
+
];
|
|
2742
2752
|
function safeText$1(value) {
|
|
2743
2753
|
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
2744
2754
|
return trimmed ? trimmed : void 0;
|
|
@@ -2811,6 +2821,28 @@ function imageDimensionsFromUrl(src) {
|
|
|
2811
2821
|
height
|
|
2812
2822
|
};
|
|
2813
2823
|
}
|
|
2824
|
+
function addAnchorSample(samples, seen, href, text) {
|
|
2825
|
+
if (samples.length >= 25 || !text) return;
|
|
2826
|
+
const sampleText = text.length > 120 ? text.slice(0, 120).trimEnd() : text;
|
|
2827
|
+
const key = `${href}\n${sampleText}`;
|
|
2828
|
+
if (seen.has(key)) return;
|
|
2829
|
+
seen.add(key);
|
|
2830
|
+
samples.push({
|
|
2831
|
+
href,
|
|
2832
|
+
text: sampleText
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
function addSocialProfile(profiles, href) {
|
|
2836
|
+
if (profiles.size >= 50) return;
|
|
2837
|
+
try {
|
|
2838
|
+
const url = new URL(href);
|
|
2839
|
+
const host = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
2840
|
+
if (SOCIAL_PROFILE_HOSTS.some((value) => host === value || host.endsWith(`.${value}`))) {
|
|
2841
|
+
url.hash = "";
|
|
2842
|
+
profiles.add(url.toString());
|
|
2843
|
+
}
|
|
2844
|
+
} catch {}
|
|
2845
|
+
}
|
|
2814
2846
|
async function extractPage(fetchResult, extractor = "defuddle", dependencies = {}) {
|
|
2815
2847
|
const $ = extractor === "crawler" && /<html(?:\s|>)/i.test(fetchResult.html) ? load(fetchResult.html, { xml: {
|
|
2816
2848
|
xmlMode: false,
|
|
@@ -2818,32 +2850,69 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
2818
2850
|
} }) : load(fetchResult.html);
|
|
2819
2851
|
const url = new URL(fetchResult.finalUrl);
|
|
2820
2852
|
const canonicalEvidence = extractCanonicalEvidence($, fetchResult.headers, fetchResult.finalUrl);
|
|
2821
|
-
const headings =
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2853
|
+
const headings = [];
|
|
2854
|
+
let crawlerH1;
|
|
2855
|
+
let crawlerH1Count = 0;
|
|
2856
|
+
let crawlerH2Count = 0;
|
|
2857
|
+
let crawlerH3Count = 0;
|
|
2858
|
+
let questionHeadings = 0;
|
|
2859
|
+
$("h1, h2, h3, h4, h5, h6").each((_index, element) => {
|
|
2860
|
+
const level = Number.parseInt(element.tagName.slice(1), 10);
|
|
2861
|
+
const text = $(element).text().replace(/\s+/g, " ").trim();
|
|
2862
|
+
if (!text) return;
|
|
2863
|
+
if (text.trimEnd().endsWith("?")) questionHeadings += 1;
|
|
2864
|
+
if (level === 1) {
|
|
2865
|
+
crawlerH1 ??= text;
|
|
2866
|
+
crawlerH1Count += 1;
|
|
2867
|
+
} else if (level === 2) crawlerH2Count += 1;
|
|
2868
|
+
else if (level === 3) crawlerH3Count += 1;
|
|
2869
|
+
if (extractor !== "crawler") headings.push({
|
|
2870
|
+
level,
|
|
2871
|
+
text
|
|
2872
|
+
});
|
|
2873
|
+
});
|
|
2825
2874
|
const links = [];
|
|
2875
|
+
const crawlerInternalLinks = /* @__PURE__ */ new Set();
|
|
2876
|
+
const crawlerExternalLinks = /* @__PURE__ */ new Set();
|
|
2877
|
+
const internalAnchorSamples = [];
|
|
2878
|
+
const externalAnchorSamples = [];
|
|
2879
|
+
const internalAnchorKeys = /* @__PURE__ */ new Set();
|
|
2880
|
+
const externalAnchorKeys = /* @__PURE__ */ new Set();
|
|
2881
|
+
const socialProfileLinks = /* @__PURE__ */ new Set();
|
|
2826
2882
|
let invalidLinkCount = 0;
|
|
2827
2883
|
let unsupportedLinkCount = 0;
|
|
2828
|
-
|
|
2884
|
+
$("a[href]").each((_index, element) => {
|
|
2829
2885
|
const href = $(element).attr("href") ?? "";
|
|
2830
2886
|
try {
|
|
2831
2887
|
const absolute = new URL(href, fetchResult.finalUrl);
|
|
2832
2888
|
if (!["http:", "https:"].includes(absolute.protocol)) {
|
|
2833
2889
|
unsupportedLinkCount += 1;
|
|
2834
|
-
|
|
2890
|
+
return;
|
|
2835
2891
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2892
|
+
const absoluteHref = absolute.toString();
|
|
2893
|
+
const text = $(element).text().replace(/\s+/g, " ").trim();
|
|
2894
|
+
const internal = absolute.host === url.host;
|
|
2895
|
+
if (extractor === "crawler") {
|
|
2896
|
+
if (absolute.origin === url.origin) {
|
|
2897
|
+
absolute.hash = "";
|
|
2898
|
+
crawlerInternalLinks.add(absolute.toString());
|
|
2899
|
+
}
|
|
2900
|
+
if (!internal) {
|
|
2901
|
+
crawlerExternalLinks.add(absoluteHref);
|
|
2902
|
+
addSocialProfile(socialProfileLinks, absoluteHref);
|
|
2903
|
+
}
|
|
2904
|
+
addAnchorSample(internal ? internalAnchorSamples : externalAnchorSamples, internal ? internalAnchorKeys : externalAnchorKeys, absoluteHref, text);
|
|
2905
|
+
} else links.push({
|
|
2906
|
+
href: absoluteHref,
|
|
2907
|
+
text,
|
|
2839
2908
|
rel: ($(element).attr("rel") ?? "").split(/\s+/).filter(Boolean),
|
|
2840
|
-
internal
|
|
2909
|
+
internal,
|
|
2841
2910
|
location: $(element).closest("nav, header").length ? "navigation" : $(element).closest("footer").length ? "footer" : $(element).closest("main, article").length ? "main-content" : "other"
|
|
2842
2911
|
});
|
|
2843
2912
|
} catch {
|
|
2844
2913
|
invalidLinkCount += 1;
|
|
2845
2914
|
}
|
|
2846
|
-
}
|
|
2915
|
+
});
|
|
2847
2916
|
const hreflang = [];
|
|
2848
2917
|
let invalidHreflangCount = 0;
|
|
2849
2918
|
for (const element of $("link[rel~=\"alternate\"][hreflang][href]").toArray()) {
|
|
@@ -2858,25 +2927,29 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
2858
2927
|
const markdownAlternates = [...new Set($("head link[rel~=\"alternate\"][type][href]").toArray().filter((element) => $(element).attr("type")?.trim().toLowerCase() === "text/markdown").map((element) => httpUrl($(element).attr("href"), fetchResult.finalUrl)).filter((value) => Boolean(value)))].sort();
|
|
2859
2928
|
const openGraph = Object.fromEntries($("meta[property^=\"og:\"]").toArray().map((element) => [$(element).attr("property") ?? "", $(element).attr("content") ?? ""]).filter(([key, value]) => key && value));
|
|
2860
2929
|
const twitter = Object.fromEntries($("meta[name^=\"twitter:\"]").toArray().map((element) => [$(element).attr("name") ?? "", $(element).attr("content") ?? ""]).filter(([key, value]) => key && value));
|
|
2861
|
-
const structuredData = extractStructuredData($, fetchResult.finalUrl);
|
|
2930
|
+
const structuredData = extractStructuredData($, fetchResult.finalUrl, { retainJsonLd: extractor !== "crawler" });
|
|
2862
2931
|
const hasDate = structuredData.hasDate || isValidStructuredDate($("meta[property=\"article:published_time\"]").attr("content")) || isValidStructuredDate($("meta[property=\"article:modified_time\"]").attr("content")) || $("article time[datetime], main time[datetime]").toArray().some((element) => isValidStructuredDate($(element).attr("datetime")));
|
|
2863
2932
|
const author = safeText$1($("meta[name=\"author\"]").attr("content"));
|
|
2864
2933
|
const hasAuthor = Boolean(author) || structuredData.hasAuthor || $("[rel~=author], .author, .byline").toArray().some((element) => Boolean(safeText$1($(element).text())));
|
|
2865
2934
|
const semanticHtml = $("main, article").length > 0;
|
|
2866
|
-
const questionHeadings = headings.filter((heading) => heading.text.trimEnd().endsWith("?")).length;
|
|
2867
2935
|
const listCount = $("ul, ol").length;
|
|
2868
2936
|
const tableCount = $("table").length;
|
|
2869
2937
|
const structuredBlocks = listCount + tableCount;
|
|
2870
2938
|
const answerable = $("main p, article p, p").toArray().slice(0, 3).some((element) => $(element).text().replace(/\s+/g, " ").trim().split(/\s+/).filter(Boolean).length >= 25);
|
|
2871
|
-
|
|
2872
|
-
|
|
2939
|
+
let imagesTotal = 0;
|
|
2940
|
+
let imagesMissingAlt = 0;
|
|
2941
|
+
const oversizedImageCandidates = [];
|
|
2942
|
+
$("img").each((_index, element) => {
|
|
2943
|
+
imagesTotal += 1;
|
|
2944
|
+
if ($(element).attr("alt") === void 0) imagesMissingAlt += 1;
|
|
2945
|
+
if (oversizedImageCandidates.length >= 25) return;
|
|
2873
2946
|
const src = absoluteUrl$2($(element).attr("src"), fetchResult.finalUrl);
|
|
2874
|
-
if (!src) return
|
|
2947
|
+
if (!src) return;
|
|
2875
2948
|
const width = numericAttribute($(element).attr("width"));
|
|
2876
2949
|
const height = numericAttribute($(element).attr("height"));
|
|
2877
2950
|
const srcsetWidth = largestSrcsetWidth($(element).attr("srcset"));
|
|
2878
2951
|
const filenameDimensions = imageDimensionsFromUrl(src);
|
|
2879
|
-
if (Math.max(width ?? 0, height ?? 0, srcsetWidth ?? 0, filenameDimensions?.width ?? 0, filenameDimensions?.height ?? 0) < 2e3) return
|
|
2952
|
+
if (Math.max(width ?? 0, height ?? 0, srcsetWidth ?? 0, filenameDimensions?.width ?? 0, filenameDimensions?.height ?? 0) < 2e3) return;
|
|
2880
2953
|
const detectedFrom = [
|
|
2881
2954
|
width && width >= 2e3 ? "width" : void 0,
|
|
2882
2955
|
height && height >= 2e3 ? "height" : void 0,
|
|
@@ -2885,13 +2958,13 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
2885
2958
|
].filter(Boolean).join(",");
|
|
2886
2959
|
const candidateWidth = Math.max(width ?? 0, srcsetWidth ?? 0, filenameDimensions?.width ?? 0);
|
|
2887
2960
|
const candidateHeight = Math.max(height ?? 0, filenameDimensions?.height ?? 0);
|
|
2888
|
-
|
|
2961
|
+
oversizedImageCandidates.push({
|
|
2889
2962
|
src,
|
|
2890
2963
|
detectedFrom,
|
|
2891
2964
|
...candidateWidth > 0 ? { width: candidateWidth } : {},
|
|
2892
2965
|
...candidateHeight > 0 ? { height: candidateHeight } : {}
|
|
2893
|
-
};
|
|
2894
|
-
})
|
|
2966
|
+
});
|
|
2967
|
+
});
|
|
2895
2968
|
const mixedContentUrls = url.protocol === "https:" ? [...new Set($("img[src], script[src], iframe[src], source[src], video[src], audio[src], link[href]").toArray().map((element) => absoluteUrl$2($(element).attr("src") ?? $(element).attr("href"), fetchResult.finalUrl)).filter((value) => typeof value === "string" && new URL(value).protocol === "http:"))] : [];
|
|
2896
2969
|
const metaRobots = combineRobotsValues($("meta[name]").toArray().filter((element) => {
|
|
2897
2970
|
const name = $(element).attr("name")?.trim().toLowerCase();
|
|
@@ -2915,6 +2988,18 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
2915
2988
|
hasViewport: Boolean($("meta[name=\"viewport\"]").attr("content")),
|
|
2916
2989
|
headings,
|
|
2917
2990
|
links,
|
|
2991
|
+
...extractor === "crawler" ? { crawlerEvidence: {
|
|
2992
|
+
h1: crawlerH1,
|
|
2993
|
+
h1Count: crawlerH1Count,
|
|
2994
|
+
h2Count: crawlerH2Count,
|
|
2995
|
+
h3Count: crawlerH3Count,
|
|
2996
|
+
questionHeadings,
|
|
2997
|
+
internalLinks: [...crawlerInternalLinks],
|
|
2998
|
+
externalLinks: [...crawlerExternalLinks],
|
|
2999
|
+
internalAnchorSamples,
|
|
3000
|
+
externalAnchorSamples,
|
|
3001
|
+
socialProfileLinks: [...socialProfileLinks]
|
|
3002
|
+
} } : {},
|
|
2918
3003
|
markdownAlternates,
|
|
2919
3004
|
hreflang,
|
|
2920
3005
|
jsonLd: structuredData.jsonLd,
|
|
@@ -2923,19 +3008,17 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
2923
3008
|
unrecognizedJsonLdTypes: structuredData.unrecognizedJsonLdTypes.slice(0, 25),
|
|
2924
3009
|
structuredDataFormats: structuredData.formats,
|
|
2925
3010
|
googleRichResults: structuredData.googleRichResults,
|
|
2926
|
-
schemaSameAsEvidence: structuredData.sameAs,
|
|
2927
|
-
invalidSchemaSameAs: structuredData.invalidSameAs,
|
|
3011
|
+
schemaSameAsEvidence: extractor === "crawler" ? structuredData.sameAs.slice(0, 50) : structuredData.sameAs,
|
|
3012
|
+
invalidSchemaSameAs: extractor === "crawler" ? structuredData.invalidSameAs.slice(0, 25) : structuredData.invalidSameAs,
|
|
2928
3013
|
schemaTypes: structuredData.schemaTypes,
|
|
2929
3014
|
openGraph,
|
|
2930
3015
|
twitter,
|
|
2931
3016
|
author,
|
|
2932
3017
|
hasAuthor,
|
|
2933
3018
|
hasDate,
|
|
2934
|
-
imagesTotal
|
|
2935
|
-
imagesMissingAlt
|
|
2936
|
-
|
|
2937
|
-
}).length,
|
|
2938
|
-
oversizedImageCandidates: oversizedImageCandidates.slice(0, 25),
|
|
3019
|
+
imagesTotal,
|
|
3020
|
+
imagesMissingAlt,
|
|
3021
|
+
oversizedImageCandidates,
|
|
2939
3022
|
mixedContentUrls,
|
|
2940
3023
|
semanticHtml,
|
|
2941
3024
|
questionHeadings,
|
|
@@ -2943,7 +3026,7 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
2943
3026
|
tableCount,
|
|
2944
3027
|
structuredBlocks,
|
|
2945
3028
|
answerable,
|
|
2946
|
-
contentText: text.replace(/\s+/g, " ").trim(),
|
|
3029
|
+
contentText: extractor === "crawler" ? text : text.replace(/\s+/g, " ").trim(),
|
|
2947
3030
|
excerpt,
|
|
2948
3031
|
wordCount: content.wordCount,
|
|
2949
3032
|
contentExtraction: content.diagnostics,
|
|
@@ -3473,7 +3556,7 @@ function decodePageFetchCacheEvidence(value) {
|
|
|
3473
3556
|
return;
|
|
3474
3557
|
}
|
|
3475
3558
|
}
|
|
3476
|
-
async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, respectRobots = false) {
|
|
3559
|
+
async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, respectRobots = false, writeCache = true) {
|
|
3477
3560
|
const startedAt = Date.now();
|
|
3478
3561
|
const db = getDb();
|
|
3479
3562
|
const key = hashKey(["page", url]);
|
|
@@ -3585,7 +3668,7 @@ async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, r
|
|
|
3585
3668
|
warnings: [...robots.allowed === null ? [`robots.txt availability is unknown${robots.status ? ` after HTTP ${robots.status}` : ""}; this fetch does not prove crawler access is allowed.`] : [], ...accessBlock ? [`${accessBlock.guidance.summary} Identify the crawler as ${accessBlock.crawler.userAgent}.`] : []],
|
|
3586
3669
|
robotsTxt: pageRobotsEvidence(robots)
|
|
3587
3670
|
};
|
|
3588
|
-
if (!accessBlock) {
|
|
3671
|
+
if (!accessBlock && writeCache) {
|
|
3589
3672
|
db.prepare(`INSERT OR REPLACE INTO http_cache
|
|
3590
3673
|
(url_hash, url, status, headers_json, body_blob, metadata_json, etag, fetched_at, expires_at)
|
|
3591
3674
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(key, url, response.response.status, JSON.stringify(headerMap), Buffer.from(html), encodePageFetchCacheEvidence(result), response.response.headers.get("etag"), Date.now(), Date.now() + 36e5);
|
|
@@ -4153,7 +4236,7 @@ async function fetchPage(url, opts = {}, dependencies = {}) {
|
|
|
4153
4236
|
const makeRenderer = dependencies.createPageRenderer ?? createPageRenderer;
|
|
4154
4237
|
const rate = normalizeRateControls(opts.rate);
|
|
4155
4238
|
return await queueForHost(new URL(url).host, rate).add(async () => {
|
|
4156
|
-
const first = await plainFetch(url, opts.refresh, opts.timeoutMs, rate, opts.signal, opts.respectRobots);
|
|
4239
|
+
const first = await plainFetch(url, opts.refresh, opts.timeoutMs, rate, opts.signal, opts.respectRobots, opts.writeCache);
|
|
4157
4240
|
if (!(!first.diagnostics.accessBlock && (renderingMode === "on" || renderingMode === "auto" && looksLikeSpa(first.html))) || opts.signal?.aborted) return withRenderingState(first, renderingMode, opts.signal?.aborted ? "skipped" : renderingMode === "off" ? "not-requested" : "not-needed");
|
|
4158
4241
|
const ownsRenderer = !opts.renderer;
|
|
4159
4242
|
const renderer = opts.renderer ?? makeRenderer();
|
|
@@ -10431,6 +10514,7 @@ const CRAWL_SKIP_REASONS = [
|
|
|
10431
10514
|
"configured-exclusion",
|
|
10432
10515
|
"duplicate-normalization",
|
|
10433
10516
|
"invalid-url",
|
|
10517
|
+
"memory-pressure",
|
|
10434
10518
|
"off-origin",
|
|
10435
10519
|
"origin-backpressure",
|
|
10436
10520
|
"queue-safety-limit",
|
|
@@ -10443,6 +10527,7 @@ const SKIP_IMPACT = {
|
|
|
10443
10527
|
"configured-exclusion": "non-impacting",
|
|
10444
10528
|
"duplicate-normalization": "non-impacting",
|
|
10445
10529
|
"invalid-url": "non-impacting",
|
|
10530
|
+
"memory-pressure": "coverage-affecting",
|
|
10446
10531
|
"off-origin": "non-impacting",
|
|
10447
10532
|
"origin-backpressure": "coverage-affecting",
|
|
10448
10533
|
"queue-safety-limit": "coverage-affecting",
|
|
@@ -10620,17 +10705,44 @@ function sanitizeTenantString(value) {
|
|
|
10620
10705
|
return value.replace(/https?:\/\/[^\s"'<>`]+/g, (match) => sanitizeUrlString(match)).replace(/\b(access_?token|api_?key|auth|client_?secret|jwt|password|refresh_?token|secret|session|signature|token)(\s*[=:]\s*)[^\s,;&]+/gi, (_match, key, separator) => `${key}${separator}[redacted]`).replace(/\b[A-Za-z]:\\[^\s"'<>`]+/g, "[local-path]").replace(/\/(?:Users|home|tmp|var\/folders)\/[^\s"'<>`]+/g, "[local-path]");
|
|
10621
10706
|
}
|
|
10622
10707
|
function sanitizeTenantValue(value, key) {
|
|
10623
|
-
if (key && isSensitiveFieldName(key)) return "[redacted]";
|
|
10708
|
+
if (key && isSensitiveFieldName(key)) return value === "[redacted]" ? value : "[redacted]";
|
|
10624
10709
|
if (typeof value === "string") return sanitizeTenantString(value);
|
|
10625
|
-
if (Array.isArray(value))
|
|
10710
|
+
if (Array.isArray(value)) {
|
|
10711
|
+
let sanitized;
|
|
10712
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
10713
|
+
const item = sanitizeTenantValue(value[index]);
|
|
10714
|
+
if (sanitized) sanitized.push(item);
|
|
10715
|
+
else if (item !== value[index]) sanitized = [...value.slice(0, index), item];
|
|
10716
|
+
}
|
|
10717
|
+
return sanitized ?? value;
|
|
10718
|
+
}
|
|
10626
10719
|
if (!value || typeof value !== "object") return value;
|
|
10627
|
-
|
|
10720
|
+
const record = value;
|
|
10721
|
+
let sanitized;
|
|
10722
|
+
for (const [entryKey, entry] of Object.entries(record)) {
|
|
10723
|
+
const safeEntry = sanitizeTenantValue(entry, entryKey);
|
|
10724
|
+
if (safeEntry === entry) continue;
|
|
10725
|
+
sanitized ??= { ...record };
|
|
10726
|
+
sanitized[entryKey] = safeEntry;
|
|
10727
|
+
}
|
|
10728
|
+
return sanitized ?? value;
|
|
10628
10729
|
}
|
|
10629
10730
|
function sanitizeCrawlConfig(config) {
|
|
10630
10731
|
return sanitizeTenantValue(config);
|
|
10631
10732
|
}
|
|
10632
10733
|
function sanitizePages(pages) {
|
|
10633
|
-
|
|
10734
|
+
const safePages = sanitizeTenantValue(pages);
|
|
10735
|
+
let normalized;
|
|
10736
|
+
for (let index = 0; index < safePages.length; index += 1) {
|
|
10737
|
+
const page = safePages[index];
|
|
10738
|
+
if (!page) continue;
|
|
10739
|
+
if ("seoScore" in page || "geoScore" in page) {
|
|
10740
|
+
normalized ??= safePages.slice(0, index);
|
|
10741
|
+
const { seoScore: _seoScore, geoScore: _geoScore, ...current } = page;
|
|
10742
|
+
normalized.push(current);
|
|
10743
|
+
} else if (normalized) normalized.push(page);
|
|
10744
|
+
}
|
|
10745
|
+
return normalized ?? safePages;
|
|
10634
10746
|
}
|
|
10635
10747
|
function sanitizeIssues(issues) {
|
|
10636
10748
|
return sanitizeTenantValue(issues);
|
|
@@ -10659,18 +10771,28 @@ function summarizeCrawlReport(input) {
|
|
|
10659
10771
|
const byCategory = {};
|
|
10660
10772
|
let responseMs = 0;
|
|
10661
10773
|
let responseCount = 0;
|
|
10774
|
+
let statusOnlyPages = 0;
|
|
10775
|
+
let indexablePages = 0;
|
|
10776
|
+
let nonIndexablePages = 0;
|
|
10777
|
+
let statusErrors = 0;
|
|
10662
10778
|
for (const page of input.pages) {
|
|
10663
10779
|
byStatus[String(page.status)] = (byStatus[String(page.status)] ?? 0) + 1;
|
|
10664
10780
|
if (typeof page.responseTimeMs === "number") {
|
|
10665
10781
|
responseMs += page.responseTimeMs;
|
|
10666
10782
|
responseCount += 1;
|
|
10667
10783
|
}
|
|
10784
|
+
if (page.auditScope === "status") statusOnlyPages += 1;
|
|
10785
|
+
else if (page.indexable) indexablePages += 1;
|
|
10786
|
+
else nonIndexablePages += 1;
|
|
10787
|
+
if (page.status === 0 || page.status >= 400) statusErrors += 1;
|
|
10668
10788
|
}
|
|
10669
10789
|
const requests = input.requests ?? [];
|
|
10670
|
-
const failedRequests = requests.filter((request) => request.outcome === "failure" && request.failureKind !== "aborted");
|
|
10671
|
-
const abortedRequests = requests.filter((request) => request.outcome === "failure" && request.failureKind === "aborted");
|
|
10672
10790
|
let requestMs = 0;
|
|
10673
10791
|
let requestMsCount = 0;
|
|
10792
|
+
let responseRequests = 0;
|
|
10793
|
+
let failedRequests = 0;
|
|
10794
|
+
let abortedRequests = 0;
|
|
10795
|
+
let extractionFailures = 0;
|
|
10674
10796
|
for (const request of requests) {
|
|
10675
10797
|
const status = request.outcome === "response" ? String(request.status) : request.outcome === "skipped" ? request.reason : request.failureKind === "aborted" ? "aborted" : "no-response";
|
|
10676
10798
|
requestByStatus[status] = (requestByStatus[status] ?? 0) + 1;
|
|
@@ -10678,27 +10800,39 @@ function summarizeCrawlReport(input) {
|
|
|
10678
10800
|
requestMs += request.durationMs;
|
|
10679
10801
|
requestMsCount += 1;
|
|
10680
10802
|
}
|
|
10803
|
+
if (request.outcome === "response") {
|
|
10804
|
+
responseRequests += 1;
|
|
10805
|
+
if (request.extraction === "failed") extractionFailures += 1;
|
|
10806
|
+
} else if (request.outcome === "failure") if (request.failureKind === "aborted") abortedRequests += 1;
|
|
10807
|
+
else failedRequests += 1;
|
|
10808
|
+
}
|
|
10809
|
+
let highIssues = 0;
|
|
10810
|
+
let mediumIssues = 0;
|
|
10811
|
+
let lowIssues = 0;
|
|
10812
|
+
for (const issue of input.issues) {
|
|
10813
|
+
byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
|
|
10814
|
+
if (issue.severity === "high") highIssues += 1;
|
|
10815
|
+
else if (issue.severity === "medium") mediumIssues += 1;
|
|
10816
|
+
else lowIssues += 1;
|
|
10681
10817
|
}
|
|
10682
|
-
for (const issue of input.issues) byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
|
|
10683
10818
|
const runStats = crawlRunStats(input.pages, input.stats);
|
|
10684
|
-
const indexabilityPages = input.pages.filter((page) => page.auditScope !== "status");
|
|
10685
10819
|
return {
|
|
10686
10820
|
totalPages: input.pages.length,
|
|
10687
|
-
statusOnlyPages
|
|
10688
|
-
indexablePages
|
|
10689
|
-
nonIndexablePages
|
|
10690
|
-
statusErrors
|
|
10821
|
+
statusOnlyPages,
|
|
10822
|
+
indexablePages,
|
|
10823
|
+
nonIndexablePages,
|
|
10824
|
+
statusErrors,
|
|
10691
10825
|
...runStats,
|
|
10692
10826
|
attemptedRequests: input.requestEvidenceStatus === "unavailable" ? 0 : requests.length,
|
|
10693
|
-
responseRequests
|
|
10694
|
-
failedRequests
|
|
10695
|
-
abortedRequests
|
|
10696
|
-
extractionFailures
|
|
10827
|
+
responseRequests,
|
|
10828
|
+
failedRequests,
|
|
10829
|
+
abortedRequests,
|
|
10830
|
+
extractionFailures,
|
|
10697
10831
|
requestByStatus,
|
|
10698
10832
|
avgRequestMs: requestMsCount ? Math.round(requestMs / requestMsCount) : void 0,
|
|
10699
|
-
highIssues
|
|
10700
|
-
mediumIssues
|
|
10701
|
-
lowIssues
|
|
10833
|
+
highIssues,
|
|
10834
|
+
mediumIssues,
|
|
10835
|
+
lowIssues,
|
|
10702
10836
|
avgResponseMs: responseCount ? Math.round(responseMs / responseCount) : void 0,
|
|
10703
10837
|
byStatus,
|
|
10704
10838
|
byCategory
|
|
@@ -10706,42 +10840,55 @@ function summarizeCrawlReport(input) {
|
|
|
10706
10840
|
}
|
|
10707
10841
|
const ACCESS_BLOCK_SAMPLE_LIMIT = 10;
|
|
10708
10842
|
function crawlAccessSummary(requests) {
|
|
10709
|
-
const blocked = requests.flatMap((request) => request.outcome === "response" && request.accessBlock ? [{
|
|
10710
|
-
url: request.requestedUrl,
|
|
10711
|
-
evidence: request.accessBlock
|
|
10712
|
-
}] : []);
|
|
10713
10843
|
const providers = {};
|
|
10714
|
-
|
|
10844
|
+
const samples = [];
|
|
10845
|
+
let blockedRequests = 0;
|
|
10846
|
+
for (const request of requests) {
|
|
10847
|
+
if (request.outcome !== "response" || !request.accessBlock) continue;
|
|
10848
|
+
blockedRequests += 1;
|
|
10849
|
+
providers[request.accessBlock.provider] = (providers[request.accessBlock.provider] ?? 0) + 1;
|
|
10850
|
+
if (samples.length < ACCESS_BLOCK_SAMPLE_LIMIT) samples.push({
|
|
10851
|
+
url: request.requestedUrl,
|
|
10852
|
+
evidence: request.accessBlock
|
|
10853
|
+
});
|
|
10854
|
+
}
|
|
10715
10855
|
return {
|
|
10716
10856
|
crawler: SEO_CRAWLER_IDENTITY,
|
|
10717
|
-
blockedRequests
|
|
10857
|
+
blockedRequests,
|
|
10718
10858
|
providers,
|
|
10719
|
-
samples
|
|
10859
|
+
samples,
|
|
10720
10860
|
sampleLimit: ACCESS_BLOCK_SAMPLE_LIMIT,
|
|
10721
|
-
truncated:
|
|
10861
|
+
truncated: blockedRequests > ACCESS_BLOCK_SAMPLE_LIMIT
|
|
10722
10862
|
};
|
|
10723
10863
|
}
|
|
10724
10864
|
function crawlRunStats(pages, stats = {}) {
|
|
10725
|
-
const
|
|
10865
|
+
const needsDiscoveredUrls = stats.discoveredUrls === void 0;
|
|
10866
|
+
const needsObservedInternalLinks = stats.observedInternalLinks === void 0;
|
|
10867
|
+
const needsFailedUrls = stats.failedUrls === void 0;
|
|
10868
|
+
const discovered = needsDiscoveredUrls ? /* @__PURE__ */ new Set() : void 0;
|
|
10726
10869
|
let observedInternalLinks = 0;
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10870
|
+
let failedUrls = 0;
|
|
10871
|
+
if (needsDiscoveredUrls || needsObservedInternalLinks || needsFailedUrls) for (const page of pages) {
|
|
10872
|
+
if (discovered) {
|
|
10873
|
+
discovered.add(page.url);
|
|
10874
|
+
for (const url of page.sampleInternalLinks ?? []) discovered.add(url);
|
|
10875
|
+
for (const url of page.sampleExternalLinks ?? []) discovered.add(url);
|
|
10876
|
+
}
|
|
10877
|
+
if (needsObservedInternalLinks) observedInternalLinks += page.outgoingInternalCount;
|
|
10878
|
+
if (needsFailedUrls && (page.status === 0 || page.status >= 400)) failedUrls += 1;
|
|
10732
10879
|
}
|
|
10733
10880
|
const skips = normalizeCrawlSkipReasonCounts({
|
|
10734
10881
|
skippedUrls: stats.skippedUrls,
|
|
10735
10882
|
skipReasons: stats.skipReasons ?? crawlSkipReasonCountsFromRecord(stats.skipReasonCounts)
|
|
10736
10883
|
});
|
|
10737
10884
|
return {
|
|
10738
|
-
discoveredUrls: stats.discoveredUrls ?? discovered
|
|
10885
|
+
discoveredUrls: stats.discoveredUrls ?? discovered?.size ?? 0,
|
|
10739
10886
|
queuedUrls: stats.queuedUrls ?? pages.length,
|
|
10740
10887
|
crawledUrls: stats.crawledUrls ?? pages.length,
|
|
10741
10888
|
skippedUrls: skips.skippedUrls,
|
|
10742
10889
|
skipReasons: skips.skipReasons,
|
|
10743
10890
|
skippedUrlsByImpact: skips.skippedUrlsByImpact,
|
|
10744
|
-
failedUrls: stats.failedUrls ??
|
|
10891
|
+
failedUrls: stats.failedUrls ?? failedUrls,
|
|
10745
10892
|
observedInternalLinks: stats.observedInternalLinks ?? observedInternalLinks,
|
|
10746
10893
|
pageLimitReached: stats.pageLimitReached ?? false
|
|
10747
10894
|
};
|
|
@@ -10758,11 +10905,14 @@ function normalizeLinkUrl(value) {
|
|
|
10758
10905
|
function deriveInternalLinkAuthority(pages, linkGraph) {
|
|
10759
10906
|
const hasLinkGraph = linkGraph && Object.keys(linkGraph).length > 0;
|
|
10760
10907
|
const hasStoredScores = pages.some((page) => page.internalInlinkCount !== void 0 || page.internalLinkAuthorityScore !== void 0);
|
|
10761
|
-
if (!hasLinkGraph && hasStoredScores)
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10908
|
+
if (!hasLinkGraph && hasStoredScores) {
|
|
10909
|
+
if (pages.every((page) => page.internalInlinkCount !== void 0 && page.internalLinkAuthorityScore !== void 0)) return pages;
|
|
10910
|
+
return pages.map((page) => page.internalInlinkCount !== void 0 && page.internalLinkAuthorityScore !== void 0 ? page : {
|
|
10911
|
+
...page,
|
|
10912
|
+
internalInlinkCount: page.internalInlinkCount ?? 0,
|
|
10913
|
+
internalLinkAuthorityScore: page.internalLinkAuthorityScore ?? 0
|
|
10914
|
+
});
|
|
10915
|
+
}
|
|
10766
10916
|
const pageByUrl = /* @__PURE__ */ new Map();
|
|
10767
10917
|
for (const page of pages) for (const value of [page.url, page.finalUrl]) {
|
|
10768
10918
|
const normalized = normalizeLinkUrl(value);
|
|
@@ -10781,7 +10931,8 @@ function deriveInternalLinkAuthority(pages, linkGraph) {
|
|
|
10781
10931
|
}
|
|
10782
10932
|
for (const linkedPage of linkedPages) inlinks.set(linkedPage, (inlinks.get(linkedPage) ?? 0) + 1);
|
|
10783
10933
|
}
|
|
10784
|
-
|
|
10934
|
+
let maxInlinks = 0;
|
|
10935
|
+
for (const count of inlinks.values()) maxInlinks = Math.max(maxInlinks, count);
|
|
10785
10936
|
return pages.map((page) => {
|
|
10786
10937
|
const internalInlinkCount = inlinks.get(page.url) ?? 0;
|
|
10787
10938
|
return {
|
|
@@ -11030,6 +11181,7 @@ function crawlCaveats(input) {
|
|
|
11030
11181
|
const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
|
|
11031
11182
|
if (input.queueSafetySkippedUrls > 0) caveats.push(`Left ${input.queueSafetySkippedUrls} eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
|
|
11032
11183
|
if (input.originBackpressureSkippedUrls > 0) caveats.push(`Stopped ${input.originBackpressureSkippedUrls} queued URL${input.originBackpressureSkippedUrls === 1 ? "" : "s"} because the origin stayed slow. This is incomplete crawl evidence, not a failed URL check.`);
|
|
11184
|
+
if (input.memoryPressureSkippedUrls > 0) caveats.push(`Left ${input.memoryPressureSkippedUrls} eligible URL${input.memoryPressureSkippedUrls === 1 ? "" : "s"} unchecked after the local memory safety limit was reached. Start with --health or lower --max-pages.`);
|
|
11033
11185
|
return caveats;
|
|
11034
11186
|
}
|
|
11035
11187
|
//#endregion
|
|
@@ -11056,6 +11208,26 @@ function cancellationRace(signal) {
|
|
|
11056
11208
|
});
|
|
11057
11209
|
}
|
|
11058
11210
|
//#endregion
|
|
11211
|
+
//#region packages/core/dist/analyze/crawler/crawl-memory.js
|
|
11212
|
+
const MEBIBYTE$1 = 1024 * 1024;
|
|
11213
|
+
const MIN_CRAWL_MEMORY_LIMIT = 384 * MEBIBYTE$1;
|
|
11214
|
+
const MAX_CRAWL_MEMORY_LIMIT = 640 * MEBIBYTE$1;
|
|
11215
|
+
const MIN_CRAWL_RSS_LIMIT = 512 * MEBIBYTE$1;
|
|
11216
|
+
const MAX_CRAWL_RSS_LIMIT = 896 * MEBIBYTE$1;
|
|
11217
|
+
const SYSTEM_MEMORY_FRACTION = .1;
|
|
11218
|
+
const SYSTEM_RSS_FRACTION = .2;
|
|
11219
|
+
function crawlMemoryLimitBytes(totalMemoryBytes) {
|
|
11220
|
+
if (!Number.isFinite(totalMemoryBytes) || totalMemoryBytes <= 0) return MAX_CRAWL_MEMORY_LIMIT;
|
|
11221
|
+
return Math.min(MAX_CRAWL_MEMORY_LIMIT, Math.max(MIN_CRAWL_MEMORY_LIMIT, Math.floor(totalMemoryBytes * SYSTEM_MEMORY_FRACTION)));
|
|
11222
|
+
}
|
|
11223
|
+
function crawlRssLimitBytes(totalMemoryBytes) {
|
|
11224
|
+
if (!Number.isFinite(totalMemoryBytes) || totalMemoryBytes <= 0) return MAX_CRAWL_RSS_LIMIT;
|
|
11225
|
+
return Math.min(MAX_CRAWL_RSS_LIMIT, Math.max(MIN_CRAWL_RSS_LIMIT, Math.floor(totalMemoryBytes * SYSTEM_RSS_FRACTION)));
|
|
11226
|
+
}
|
|
11227
|
+
function crawlMemoryPressure(input) {
|
|
11228
|
+
return input.memoryUsage.heapUsed + input.memoryUsage.external >= crawlMemoryLimitBytes(input.totalMemoryBytes) || input.memoryUsage.rss >= crawlRssLimitBytes(input.totalMemoryBytes);
|
|
11229
|
+
}
|
|
11230
|
+
//#endregion
|
|
11059
11231
|
//#region packages/core/dist/analyze/crawler/crawl-url-queue.js
|
|
11060
11232
|
function before(left, right) {
|
|
11061
11233
|
return left.depth < right.depth || left.depth === right.depth && left.url < right.url;
|
|
@@ -11773,6 +11945,7 @@ function crawlProviderLimits(input) {
|
|
|
11773
11945
|
}
|
|
11774
11946
|
async function crawlDataSources(input) {
|
|
11775
11947
|
const window = crawlMetricsWindow(input.now());
|
|
11948
|
+
const skippedForMemory = input.skipReason === "memory-pressure";
|
|
11776
11949
|
const dataSources = {
|
|
11777
11950
|
searchConsole: {
|
|
11778
11951
|
status: "skipped",
|
|
@@ -11783,7 +11956,7 @@ async function crawlDataSources(input) {
|
|
|
11783
11956
|
pageLimit: input.searchMetricsLimit,
|
|
11784
11957
|
pageLimitReached: false,
|
|
11785
11958
|
retainedRowLimitReached: false,
|
|
11786
|
-
warning: input.cancelled ? "Search Console join skipped because the crawl was cancelled." : "Search Console join skipped because no property was selected."
|
|
11959
|
+
warning: skippedForMemory ? "Search Console join skipped after the local memory safety limit was reached." : input.skipReason === "cancelled" ? "Search Console join skipped because the crawl was cancelled." : "Search Console join skipped because no property was selected."
|
|
11787
11960
|
},
|
|
11788
11961
|
analytics: {
|
|
11789
11962
|
status: "skipped",
|
|
@@ -11792,10 +11965,10 @@ async function crawlDataSources(input) {
|
|
|
11792
11965
|
joinedPages: 0,
|
|
11793
11966
|
retainedRowLimit: input.analyticsLimit,
|
|
11794
11967
|
retainedRowLimitReached: false,
|
|
11795
|
-
warning: input.cancelled ? "Google Analytics join skipped because the crawl was cancelled." : "Google Analytics join skipped because no property was selected."
|
|
11968
|
+
warning: skippedForMemory ? "Google Analytics join skipped after the local memory safety limit was reached." : input.skipReason === "cancelled" ? "Google Analytics join skipped because the crawl was cancelled." : "Google Analytics join skipped because no property was selected."
|
|
11796
11969
|
}
|
|
11797
11970
|
};
|
|
11798
|
-
if (!input.
|
|
11971
|
+
if (!input.skipReason && input.site) dataSources.searchConsole = await joinSearchMetrics({
|
|
11799
11972
|
site: input.site,
|
|
11800
11973
|
pages: input.pages,
|
|
11801
11974
|
warnings: input.warnings,
|
|
@@ -11808,7 +11981,7 @@ async function crawlDataSources(input) {
|
|
|
11808
11981
|
queryPagesMetricsBatch: input.queryPagesMetricsBatch,
|
|
11809
11982
|
queryPagesTopQueriesBatch: input.queryPagesTopQueriesBatch
|
|
11810
11983
|
});
|
|
11811
|
-
if (!input.
|
|
11984
|
+
if (!input.skipReason && input.googleAnalyticsPropertyId) dataSources.analytics = await joinAnalytics({
|
|
11812
11985
|
propertyId: input.googleAnalyticsPropertyId,
|
|
11813
11986
|
pages: input.pages,
|
|
11814
11987
|
warnings: input.warnings,
|
|
@@ -11821,17 +11994,6 @@ async function crawlDataSources(input) {
|
|
|
11821
11994
|
}
|
|
11822
11995
|
//#endregion
|
|
11823
11996
|
//#region packages/core/dist/analyze/monitoring/crawl-page.js
|
|
11824
|
-
function sameOriginUrl(href, base) {
|
|
11825
|
-
try {
|
|
11826
|
-
const url = new URL(href, base);
|
|
11827
|
-
url.hash = "";
|
|
11828
|
-
if (url.origin !== base.origin) return void 0;
|
|
11829
|
-
if (!["http:", "https:"].includes(url.protocol)) return void 0;
|
|
11830
|
-
return url.toString();
|
|
11831
|
-
} catch {
|
|
11832
|
-
return;
|
|
11833
|
-
}
|
|
11834
|
-
}
|
|
11835
11997
|
function resolvedUrl(href, base) {
|
|
11836
11998
|
if (!href) return void 0;
|
|
11837
11999
|
try {
|
|
@@ -11861,49 +12023,6 @@ function safeResponseHeaders(headers) {
|
|
|
11861
12023
|
function truncate$1(value, max) {
|
|
11862
12024
|
return value.length > max ? value.slice(0, max).trimEnd() : value;
|
|
11863
12025
|
}
|
|
11864
|
-
function anchorSamples(links, internal) {
|
|
11865
|
-
const seen = /* @__PURE__ */ new Set();
|
|
11866
|
-
const samples = [];
|
|
11867
|
-
for (const link of links) {
|
|
11868
|
-
if (link.internal !== internal) continue;
|
|
11869
|
-
const text = truncate$1(link.text.replace(/\s+/g, " ").trim(), 120);
|
|
11870
|
-
if (!text) continue;
|
|
11871
|
-
const key = `${link.href}\n${text}`;
|
|
11872
|
-
if (seen.has(key)) continue;
|
|
11873
|
-
seen.add(key);
|
|
11874
|
-
samples.push({
|
|
11875
|
-
href: link.href,
|
|
11876
|
-
text
|
|
11877
|
-
});
|
|
11878
|
-
if (samples.length >= 25) break;
|
|
11879
|
-
}
|
|
11880
|
-
return samples;
|
|
11881
|
-
}
|
|
11882
|
-
function socialProfileLinks(links) {
|
|
11883
|
-
const hosts = [
|
|
11884
|
-
"facebook.com",
|
|
11885
|
-
"instagram.com",
|
|
11886
|
-
"linkedin.com",
|
|
11887
|
-
"pinterest.com",
|
|
11888
|
-
"tiktok.com",
|
|
11889
|
-
"twitter.com",
|
|
11890
|
-
"x.com",
|
|
11891
|
-
"youtube.com"
|
|
11892
|
-
];
|
|
11893
|
-
const profiles = /* @__PURE__ */ new Set();
|
|
11894
|
-
for (const link of links) {
|
|
11895
|
-
if (link.internal) continue;
|
|
11896
|
-
try {
|
|
11897
|
-
const url = new URL(link.href);
|
|
11898
|
-
const host = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
11899
|
-
if (hosts.some((value) => host === value || host.endsWith(`.${value}`))) {
|
|
11900
|
-
url.hash = "";
|
|
11901
|
-
profiles.add(url.toString());
|
|
11902
|
-
}
|
|
11903
|
-
} catch {}
|
|
11904
|
-
}
|
|
11905
|
-
return [...profiles].slice(0, 50);
|
|
11906
|
-
}
|
|
11907
12026
|
function sameDocumentUrl(left, right) {
|
|
11908
12027
|
if (!left || !right) return false;
|
|
11909
12028
|
try {
|
|
@@ -12003,17 +12122,9 @@ function statusOnlyPage(fetched) {
|
|
|
12003
12122
|
finalUrl: fetched.finalUrl,
|
|
12004
12123
|
status: fetched.status,
|
|
12005
12124
|
contentType,
|
|
12006
|
-
responseHeaders: safeResponseHeaders(fetched.headers),
|
|
12007
12125
|
responseTimeMs: fetched.diagnostics.durationMs,
|
|
12008
|
-
usedJs: false,
|
|
12009
|
-
fetchSource: "network",
|
|
12010
|
-
cacheState: "bypass",
|
|
12011
|
-
fetchDiagnostics: fetched.diagnostics,
|
|
12012
12126
|
auditScope: "status",
|
|
12013
|
-
accessBlock: fetched.diagnostics.accessBlock,
|
|
12014
|
-
blocked: fetched.diagnostics.blocked,
|
|
12015
12127
|
contentAuditAllowed: false,
|
|
12016
|
-
robotsTxt: fetched.robotsTxt,
|
|
12017
12128
|
indexable: false,
|
|
12018
12129
|
indexability: "Not evaluated by status-only health probe",
|
|
12019
12130
|
declaredIndexability: "unknown",
|
|
@@ -12021,8 +12132,7 @@ function statusOnlyPage(fetched) {
|
|
|
12021
12132
|
wordCount: 0,
|
|
12022
12133
|
contentHash: "",
|
|
12023
12134
|
outgoingInternalCount: 0,
|
|
12024
|
-
outgoingExternalCount: 0
|
|
12025
|
-
isHttps: new URL(fetched.finalUrl).protocol === "https:"
|
|
12135
|
+
outgoingExternalCount: 0
|
|
12026
12136
|
};
|
|
12027
12137
|
}
|
|
12028
12138
|
function indexabilityReason(page) {
|
|
@@ -12098,12 +12208,11 @@ async function crawlOne(url, opts = {}) {
|
|
|
12098
12208
|
};
|
|
12099
12209
|
try {
|
|
12100
12210
|
const extracted = await extractPage(fetched, "crawler");
|
|
12101
|
-
const
|
|
12102
|
-
|
|
12103
|
-
const
|
|
12104
|
-
const
|
|
12105
|
-
const
|
|
12106
|
-
const uniqueExternalLinks = [...new Set(externalLinks)];
|
|
12211
|
+
const crawlerEvidence = extracted.crawlerEvidence;
|
|
12212
|
+
if (!crawlerEvidence) throw new Error("Crawler extraction did not return crawl evidence.");
|
|
12213
|
+
const h1 = crawlerEvidence.h1;
|
|
12214
|
+
const uniqueInternalLinks = crawlerEvidence.internalLinks;
|
|
12215
|
+
const uniqueExternalLinks = crawlerEvidence.externalLinks;
|
|
12107
12216
|
const canonical = extracted.canonicalEvidence?.selectedUrl ?? resolvedUrl(extracted.canonical, extracted.finalUrl);
|
|
12108
12217
|
const richResults = selectGoogleRichResultAssessments(extracted.googleRichResults ?? []);
|
|
12109
12218
|
const page = {
|
|
@@ -12129,9 +12238,9 @@ async function crawlOne(url, opts = {}) {
|
|
|
12129
12238
|
metaRobots: extracted.metaRobots,
|
|
12130
12239
|
xRobotsTag: extracted.xRobotsTag,
|
|
12131
12240
|
h1,
|
|
12132
|
-
h1Count:
|
|
12133
|
-
h2Count:
|
|
12134
|
-
h3Count:
|
|
12241
|
+
h1Count: crawlerEvidence.h1Count,
|
|
12242
|
+
h2Count: crawlerEvidence.h2Count,
|
|
12243
|
+
h3Count: crawlerEvidence.h3Count,
|
|
12135
12244
|
indexable: false,
|
|
12136
12245
|
extractionStatus: "complete",
|
|
12137
12246
|
wordCount: extracted.wordCount,
|
|
@@ -12167,8 +12276,8 @@ async function crawlOne(url, opts = {}) {
|
|
|
12167
12276
|
outgoingExternalCount: uniqueExternalLinks.length,
|
|
12168
12277
|
sampleInternalLinks: uniqueInternalLinks.slice(0, 25),
|
|
12169
12278
|
sampleExternalLinks: uniqueExternalLinks.slice(0, 25),
|
|
12170
|
-
internalAnchorSamples:
|
|
12171
|
-
externalAnchorSamples:
|
|
12279
|
+
internalAnchorSamples: crawlerEvidence.internalAnchorSamples,
|
|
12280
|
+
externalAnchorSamples: crawlerEvidence.externalAnchorSamples,
|
|
12172
12281
|
markdownAlternates: extracted.markdownAlternates,
|
|
12173
12282
|
schemaTypes: extracted.schemaTypes,
|
|
12174
12283
|
structuredDataFormats: extracted.structuredDataFormats ?? [],
|
|
@@ -12177,7 +12286,7 @@ async function crawlOne(url, opts = {}) {
|
|
|
12177
12286
|
schemaSameAs: [...new Set((extracted.schemaSameAsEvidence ?? []).map((evidence) => evidence.url))].slice(0, 50),
|
|
12178
12287
|
schemaSameAsEvidence: (extracted.schemaSameAsEvidence ?? []).slice(0, 50),
|
|
12179
12288
|
invalidSchemaSameAs: (extracted.invalidSchemaSameAs ?? []).slice(0, 25),
|
|
12180
|
-
socialProfileLinks: socialProfileLinks
|
|
12289
|
+
socialProfileLinks: crawlerEvidence.socialProfileLinks,
|
|
12181
12290
|
invalidJsonLdCount: extracted.invalidJsonLdCount,
|
|
12182
12291
|
invalidJsonLdSamples: extracted.invalidJsonLdSamples,
|
|
12183
12292
|
unrecognizedJsonLdTypes: extracted.unrecognizedJsonLdTypes ?? [],
|
|
@@ -12830,7 +12939,9 @@ function resolveCrawlSiteDependencies(dependencies = {}) {
|
|
|
12830
12939
|
fetchLandingPageValues: dependencies.fetchLandingPageValues ?? fetchLandingPageValues,
|
|
12831
12940
|
landingValueForUrl: dependencies.landingValueForUrl ?? landingValueForUrl,
|
|
12832
12941
|
collectAgentDiscovery: dependencies.collectAgentDiscovery ?? collectAgentDiscovery,
|
|
12833
|
-
now: dependencies.now ?? (() => /* @__PURE__ */ new Date())
|
|
12942
|
+
now: dependencies.now ?? (() => /* @__PURE__ */ new Date()),
|
|
12943
|
+
memoryUsage: dependencies.memoryUsage ?? (() => process.memoryUsage()),
|
|
12944
|
+
totalMemory: dependencies.totalMemory ?? totalmem
|
|
12834
12945
|
};
|
|
12835
12946
|
}
|
|
12836
12947
|
//#endregion
|
|
@@ -12986,15 +13097,26 @@ async function crawlSite(input, dependencies) {
|
|
|
12986
13097
|
let queuedUrls = 0;
|
|
12987
13098
|
let skippedUrls = 0;
|
|
12988
13099
|
const skipReasonCounts = {};
|
|
12989
|
-
const
|
|
12990
|
-
|
|
12991
|
-
|
|
13100
|
+
const recordSkips = (reason, count = 1) => {
|
|
13101
|
+
if (count <= 0) return;
|
|
13102
|
+
skippedUrls += count;
|
|
13103
|
+
skipReasonCounts[reason] = (skipReasonCounts[reason] ?? 0) + count;
|
|
12992
13104
|
};
|
|
13105
|
+
const recordSkip = (reason) => recordSkips(reason);
|
|
12993
13106
|
let robotsDeferredUrls = 0;
|
|
12994
13107
|
let originBackpressureSkippedUrls = 0;
|
|
12995
13108
|
let queueSafetySkippedUrls = 0;
|
|
12996
13109
|
let failedUrls = 0;
|
|
12997
13110
|
let observedInternalLinks = 0;
|
|
13111
|
+
let memoryLimited = false;
|
|
13112
|
+
const memoryDeferredUrls = /* @__PURE__ */ new Set();
|
|
13113
|
+
const checkMemoryPressure = () => {
|
|
13114
|
+
if (memoryLimited) return;
|
|
13115
|
+
memoryLimited = crawlMemoryPressure({
|
|
13116
|
+
memoryUsage: deps.memoryUsage(),
|
|
13117
|
+
totalMemoryBytes: deps.totalMemory()
|
|
13118
|
+
});
|
|
13119
|
+
};
|
|
12998
13120
|
let sitemapDiscovery;
|
|
12999
13121
|
let externalLinkVerification;
|
|
13000
13122
|
let statusEventChain = Promise.resolve();
|
|
@@ -13124,6 +13246,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13124
13246
|
for (const url of sitemap.urls) enqueue(url, 0);
|
|
13125
13247
|
sitemapDiscovery = sitemap.discovery;
|
|
13126
13248
|
}
|
|
13249
|
+
checkMemoryPressure();
|
|
13127
13250
|
const nextQueueItem = () => {
|
|
13128
13251
|
while (queue.size) {
|
|
13129
13252
|
const item = queue.take();
|
|
@@ -13149,6 +13272,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13149
13272
|
timeoutMs: config.timeoutMs,
|
|
13150
13273
|
rate: config.fetchRate,
|
|
13151
13274
|
signal,
|
|
13275
|
+
writeCache: false,
|
|
13152
13276
|
respectRobots: config.respectRobots,
|
|
13153
13277
|
robotsResolver,
|
|
13154
13278
|
renderer
|
|
@@ -13176,8 +13300,8 @@ async function crawlSite(input, dependencies) {
|
|
|
13176
13300
|
depth: item.depth
|
|
13177
13301
|
});
|
|
13178
13302
|
};
|
|
13179
|
-
while (!isCancelled() && (queue.size || inFlight.size) && pages.length < config.maxPages) {
|
|
13180
|
-
while (!isCancelled() && inFlight.size < activeConcurrency && pages.length + inFlight.size < config.maxPages) {
|
|
13303
|
+
while (!isCancelled() && (queue.size || inFlight.size) && (!memoryLimited || inFlight.size > 0) && pages.length < config.maxPages) {
|
|
13304
|
+
while (!isCancelled() && !memoryLimited && inFlight.size < activeConcurrency && pages.length + inFlight.size < config.maxPages) {
|
|
13181
13305
|
const item = nextQueueItem();
|
|
13182
13306
|
if (!item) break;
|
|
13183
13307
|
startTask(item);
|
|
@@ -13205,6 +13329,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13205
13329
|
healthyProbeStreak = 0;
|
|
13206
13330
|
activeConcurrency = 1;
|
|
13207
13331
|
}
|
|
13332
|
+
checkMemoryPressure();
|
|
13208
13333
|
if (request.outcome === "skipped") {
|
|
13209
13334
|
recordSkip(request.reason === "robots-disallowed" ? "robots-disallowed" : request.reason === "robots-deferred" ? "robots-uncertain" : "origin-backpressure");
|
|
13210
13335
|
if (request.reason === "robots-deferred") {
|
|
@@ -13335,10 +13460,24 @@ async function crawlSite(input, dependencies) {
|
|
|
13335
13460
|
depth: task.depth,
|
|
13336
13461
|
statusCode: page.status
|
|
13337
13462
|
});
|
|
13338
|
-
if (followLinks && task.depth < config.maxDepth) for (const
|
|
13463
|
+
if (followLinks && task.depth < config.maxDepth) if (memoryLimited) for (const value of result.urls) {
|
|
13464
|
+
const normalized = normalizeUrl$1(value, finalUrl);
|
|
13465
|
+
if (normalized && sameOrigin(normalized, origin) && !isLikelyAsset(normalized) && passesFilters(normalized, config.include, config.exclude) && !visited.has(normalized) && !queue.has(normalized)) {
|
|
13466
|
+
discovered.add(normalized);
|
|
13467
|
+
memoryDeferredUrls.add(normalized);
|
|
13468
|
+
}
|
|
13469
|
+
}
|
|
13470
|
+
else for (const url of result.urls) enqueue(url, task.depth + 1);
|
|
13471
|
+
}
|
|
13472
|
+
const memoryPressureSkippedUrls = memoryLimited ? queue.size + memoryDeferredUrls.size : 0;
|
|
13473
|
+
const stoppedForMemory = memoryPressureSkippedUrls > 0;
|
|
13474
|
+
if (stoppedForMemory) {
|
|
13475
|
+
recordSkips("memory-pressure", memoryPressureSkippedUrls);
|
|
13476
|
+
warnings.push("Crawl stopped before local memory use could become unsafe. The retained report is partial.");
|
|
13339
13477
|
}
|
|
13340
13478
|
const resolvedInlinkCounts = resolveLinkCountAliases(incomingLinkCounts, fetchedAliases);
|
|
13341
|
-
|
|
13479
|
+
let maxInlinks = 0;
|
|
13480
|
+
for (const page of pages) maxInlinks = Math.max(maxInlinks, resolvedInlinkCounts.get(page.finalUrl) ?? 0);
|
|
13342
13481
|
for (const page of pages) {
|
|
13343
13482
|
const internalInlinkCount = resolvedInlinkCounts.get(page.finalUrl) ?? 0;
|
|
13344
13483
|
page.internalInlinkCount = internalInlinkCount;
|
|
@@ -13358,11 +13497,11 @@ async function crawlSite(input, dependencies) {
|
|
|
13358
13497
|
const sitemapEvidencePartial = sitemapDiscovery?.dataStatus === "partial";
|
|
13359
13498
|
const accessEvidencePartial = requests.some((request) => request.outcome === "response" && request.accessBlock);
|
|
13360
13499
|
const requiredSitemapUnavailable = config.mode === "sitemap" && sitemapDiscovery?.dataStatus === "unavailable";
|
|
13361
|
-
const partial = cancelled || failedUrls > 0 || pageLimitReached || robotsDeferredUrls > 0 || originBackpressureSkippedUrls > 0 || queueSafetySkippedUrls > 0 || queue.size > 0 || inFlight.size > 0 || sitemapEvidencePartial || accessEvidencePartial || requiredSitemapUnavailable;
|
|
13500
|
+
const partial = cancelled || failedUrls > 0 || pageLimitReached || robotsDeferredUrls > 0 || originBackpressureSkippedUrls > 0 || queueSafetySkippedUrls > 0 || queue.size > 0 || inFlight.size > 0 || stoppedForMemory || sitemapEvidencePartial || accessEvidencePartial || requiredSitemapUnavailable;
|
|
13362
13501
|
const failed = !cancelled && pages.length === 0 && (requiredSitemapUnavailable || requests.length > 0 && requests.every((request) => request.outcome === "failure"));
|
|
13363
13502
|
const requestEvidenceStatus = cancelled && inFlight.size > 0 ? "partial" : "available";
|
|
13364
13503
|
const dataSources = await crawlDataSources({
|
|
13365
|
-
cancelled,
|
|
13504
|
+
skipReason: cancelled ? "cancelled" : stoppedForMemory ? "memory-pressure" : void 0,
|
|
13366
13505
|
site: healthStrategy ? void 0 : site,
|
|
13367
13506
|
googleAnalyticsPropertyId: healthStrategy ? void 0 : input.googleAnalyticsPropertyId,
|
|
13368
13507
|
pages,
|
|
@@ -13381,7 +13520,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13381
13520
|
});
|
|
13382
13521
|
const sourceEvidencePartial = [dataSources.searchConsole.status, dataSources.analytics.status].some((status) => status === "partial" || status === "unavailable");
|
|
13383
13522
|
const reportStatus = failed ? "failed" : partial || sourceEvidencePartial ? "partial" : "completed";
|
|
13384
|
-
if (!cancelled && config.checkExternal) {
|
|
13523
|
+
if (!cancelled && !stoppedForMemory && config.checkExternal) {
|
|
13385
13524
|
emitStatus("external_links_started", {
|
|
13386
13525
|
url: config.url,
|
|
13387
13526
|
message: "Started external link checks."
|
|
@@ -13398,7 +13537,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13398
13537
|
message: "Finished external link checks."
|
|
13399
13538
|
});
|
|
13400
13539
|
}
|
|
13401
|
-
const agentDiscovery = !cancelled && config.checkAgentDiscovery ? await deps.collectAgentDiscovery({
|
|
13540
|
+
const agentDiscovery = !cancelled && !stoppedForMemory && config.checkAgentDiscovery ? await deps.collectAgentDiscovery({
|
|
13402
13541
|
startUrl: config.url,
|
|
13403
13542
|
pages,
|
|
13404
13543
|
timeoutMs: config.timeoutMs,
|
|
@@ -13439,7 +13578,8 @@ async function crawlSite(input, dependencies) {
|
|
|
13439
13578
|
pageLimitReached,
|
|
13440
13579
|
maxPages: config.maxPages,
|
|
13441
13580
|
queueSafetySkippedUrls,
|
|
13442
|
-
originBackpressureSkippedUrls
|
|
13581
|
+
originBackpressureSkippedUrls,
|
|
13582
|
+
memoryPressureSkippedUrls
|
|
13443
13583
|
}),
|
|
13444
13584
|
stats: {
|
|
13445
13585
|
discoveredUrls: discovered.size,
|