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/index.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, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { arch, homedir, platform } from "node:os";
|
|
5
|
+
import { arch, homedir, platform, totalmem } from "node:os";
|
|
6
6
|
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import lockfile from "proper-lockfile";
|
|
@@ -1082,15 +1082,15 @@ async function exchangeCode(input) {
|
|
|
1082
1082
|
}
|
|
1083
1083
|
//#endregion
|
|
1084
1084
|
//#region packages/core/src/storage/cache-maintenance.ts
|
|
1085
|
-
const MEBIBYTE$
|
|
1086
|
-
const CACHE_MAX_BYTES = 256 * MEBIBYTE$
|
|
1085
|
+
const MEBIBYTE$2 = 1024 * 1024;
|
|
1086
|
+
const CACHE_MAX_BYTES = 256 * MEBIBYTE$2;
|
|
1087
1087
|
const CACHE_MAX_AGE_MS = 720 * 60 * 60 * 1e3;
|
|
1088
1088
|
const CACHE_LIMITS = {
|
|
1089
|
-
gsc_cache: 48 * MEBIBYTE$
|
|
1090
|
-
ga4_cache: 24 * MEBIBYTE$
|
|
1091
|
-
semrush_cache: 16 * MEBIBYTE$
|
|
1092
|
-
http_cache: 160 * MEBIBYTE$
|
|
1093
|
-
performance_reports: 8 * MEBIBYTE$
|
|
1089
|
+
gsc_cache: 48 * MEBIBYTE$2,
|
|
1090
|
+
ga4_cache: 24 * MEBIBYTE$2,
|
|
1091
|
+
semrush_cache: 16 * MEBIBYTE$2,
|
|
1092
|
+
http_cache: 160 * MEBIBYTE$2,
|
|
1093
|
+
performance_reports: 8 * MEBIBYTE$2
|
|
1094
1094
|
};
|
|
1095
1095
|
const CACHE_POLICIES = [
|
|
1096
1096
|
{
|
|
@@ -2963,7 +2963,7 @@ function isValidStructuredDate(value) {
|
|
|
2963
2963
|
const parsed = new Date(trimmed);
|
|
2964
2964
|
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === datePart;
|
|
2965
2965
|
}
|
|
2966
|
-
function parseJsonLd(blocks, baseUrl) {
|
|
2966
|
+
function parseJsonLd(blocks, baseUrl, retainJsonLd) {
|
|
2967
2967
|
const jsonLd = [];
|
|
2968
2968
|
const invalidJsonLdSamples = [];
|
|
2969
2969
|
const unrecognizedJsonLdTypes = [];
|
|
@@ -3035,7 +3035,7 @@ function parseJsonLd(blocks, baseUrl) {
|
|
|
3035
3035
|
blocks.forEach((block, index) => {
|
|
3036
3036
|
try {
|
|
3037
3037
|
const parsed = JSON.parse(block);
|
|
3038
|
-
jsonLd.push(parsed);
|
|
3038
|
+
if (retainJsonLd) jsonLd.push(parsed);
|
|
3039
3039
|
visit(parsed, { prefixes: /* @__PURE__ */ new Map() }, index, "$");
|
|
3040
3040
|
} catch (error) {
|
|
3041
3041
|
invalidJsonLdSamples.push({
|
|
@@ -3096,8 +3096,8 @@ function usableElementValue($, selector) {
|
|
|
3096
3096
|
return ($(element).attr("content") ?? $(element).attr("datetime") ?? $(element).text()).trim().length > 0;
|
|
3097
3097
|
});
|
|
3098
3098
|
}
|
|
3099
|
-
function extractStructuredData($, baseUrl) {
|
|
3100
|
-
const parsed = parseJsonLd($("script[type=\"application/ld+json\"]").toArray().map((element) => $(element).html() ?? ""), baseUrl);
|
|
3099
|
+
function extractStructuredData($, baseUrl, options = {}) {
|
|
3100
|
+
const parsed = parseJsonLd($("script[type=\"application/ld+json\"]").toArray().map((element) => $(element).html() ?? ""), baseUrl, options.retainJsonLd ?? true);
|
|
3101
3101
|
const microdata = microdataTypes($);
|
|
3102
3102
|
const rdfa = rdfaTypes($);
|
|
3103
3103
|
const schemaTypes = [.../* @__PURE__ */ new Set([
|
|
@@ -3131,6 +3131,16 @@ function extractStructuredData($, baseUrl) {
|
|
|
3131
3131
|
}
|
|
3132
3132
|
//#endregion
|
|
3133
3133
|
//#region packages/core/src/extract/page-extractor.ts
|
|
3134
|
+
const SOCIAL_PROFILE_HOSTS = [
|
|
3135
|
+
"facebook.com",
|
|
3136
|
+
"instagram.com",
|
|
3137
|
+
"linkedin.com",
|
|
3138
|
+
"pinterest.com",
|
|
3139
|
+
"tiktok.com",
|
|
3140
|
+
"twitter.com",
|
|
3141
|
+
"x.com",
|
|
3142
|
+
"youtube.com"
|
|
3143
|
+
];
|
|
3134
3144
|
function safeText$1(value) {
|
|
3135
3145
|
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
3136
3146
|
return trimmed ? trimmed : void 0;
|
|
@@ -3203,6 +3213,28 @@ function imageDimensionsFromUrl(src) {
|
|
|
3203
3213
|
height
|
|
3204
3214
|
};
|
|
3205
3215
|
}
|
|
3216
|
+
function addAnchorSample(samples, seen, href, text) {
|
|
3217
|
+
if (samples.length >= 25 || !text) return;
|
|
3218
|
+
const sampleText = text.length > 120 ? text.slice(0, 120).trimEnd() : text;
|
|
3219
|
+
const key = `${href}\n${sampleText}`;
|
|
3220
|
+
if (seen.has(key)) return;
|
|
3221
|
+
seen.add(key);
|
|
3222
|
+
samples.push({
|
|
3223
|
+
href,
|
|
3224
|
+
text: sampleText
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
function addSocialProfile(profiles, href) {
|
|
3228
|
+
if (profiles.size >= 50) return;
|
|
3229
|
+
try {
|
|
3230
|
+
const url = new URL(href);
|
|
3231
|
+
const host = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
3232
|
+
if (SOCIAL_PROFILE_HOSTS.some((value) => host === value || host.endsWith(`.${value}`))) {
|
|
3233
|
+
url.hash = "";
|
|
3234
|
+
profiles.add(url.toString());
|
|
3235
|
+
}
|
|
3236
|
+
} catch {}
|
|
3237
|
+
}
|
|
3206
3238
|
async function extractPage(fetchResult, extractor = "defuddle", dependencies = {}) {
|
|
3207
3239
|
const $ = extractor === "crawler" && /<html(?:\s|>)/i.test(fetchResult.html) ? load(fetchResult.html, { xml: {
|
|
3208
3240
|
xmlMode: false,
|
|
@@ -3210,32 +3242,69 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3210
3242
|
} }) : load(fetchResult.html);
|
|
3211
3243
|
const url = new URL(fetchResult.finalUrl);
|
|
3212
3244
|
const canonicalEvidence = extractCanonicalEvidence($, fetchResult.headers, fetchResult.finalUrl);
|
|
3213
|
-
const headings =
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3245
|
+
const headings = [];
|
|
3246
|
+
let crawlerH1;
|
|
3247
|
+
let crawlerH1Count = 0;
|
|
3248
|
+
let crawlerH2Count = 0;
|
|
3249
|
+
let crawlerH3Count = 0;
|
|
3250
|
+
let questionHeadings = 0;
|
|
3251
|
+
$("h1, h2, h3, h4, h5, h6").each((_index, element) => {
|
|
3252
|
+
const level = Number.parseInt(element.tagName.slice(1), 10);
|
|
3253
|
+
const text = $(element).text().replace(/\s+/g, " ").trim();
|
|
3254
|
+
if (!text) return;
|
|
3255
|
+
if (text.trimEnd().endsWith("?")) questionHeadings += 1;
|
|
3256
|
+
if (level === 1) {
|
|
3257
|
+
crawlerH1 ??= text;
|
|
3258
|
+
crawlerH1Count += 1;
|
|
3259
|
+
} else if (level === 2) crawlerH2Count += 1;
|
|
3260
|
+
else if (level === 3) crawlerH3Count += 1;
|
|
3261
|
+
if (extractor !== "crawler") headings.push({
|
|
3262
|
+
level,
|
|
3263
|
+
text
|
|
3264
|
+
});
|
|
3265
|
+
});
|
|
3217
3266
|
const links = [];
|
|
3267
|
+
const crawlerInternalLinks = /* @__PURE__ */ new Set();
|
|
3268
|
+
const crawlerExternalLinks = /* @__PURE__ */ new Set();
|
|
3269
|
+
const internalAnchorSamples = [];
|
|
3270
|
+
const externalAnchorSamples = [];
|
|
3271
|
+
const internalAnchorKeys = /* @__PURE__ */ new Set();
|
|
3272
|
+
const externalAnchorKeys = /* @__PURE__ */ new Set();
|
|
3273
|
+
const socialProfileLinks = /* @__PURE__ */ new Set();
|
|
3218
3274
|
let invalidLinkCount = 0;
|
|
3219
3275
|
let unsupportedLinkCount = 0;
|
|
3220
|
-
|
|
3276
|
+
$("a[href]").each((_index, element) => {
|
|
3221
3277
|
const href = $(element).attr("href") ?? "";
|
|
3222
3278
|
try {
|
|
3223
3279
|
const absolute = new URL(href, fetchResult.finalUrl);
|
|
3224
3280
|
if (!["http:", "https:"].includes(absolute.protocol)) {
|
|
3225
3281
|
unsupportedLinkCount += 1;
|
|
3226
|
-
|
|
3282
|
+
return;
|
|
3227
3283
|
}
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3284
|
+
const absoluteHref = absolute.toString();
|
|
3285
|
+
const text = $(element).text().replace(/\s+/g, " ").trim();
|
|
3286
|
+
const internal = absolute.host === url.host;
|
|
3287
|
+
if (extractor === "crawler") {
|
|
3288
|
+
if (absolute.origin === url.origin) {
|
|
3289
|
+
absolute.hash = "";
|
|
3290
|
+
crawlerInternalLinks.add(absolute.toString());
|
|
3291
|
+
}
|
|
3292
|
+
if (!internal) {
|
|
3293
|
+
crawlerExternalLinks.add(absoluteHref);
|
|
3294
|
+
addSocialProfile(socialProfileLinks, absoluteHref);
|
|
3295
|
+
}
|
|
3296
|
+
addAnchorSample(internal ? internalAnchorSamples : externalAnchorSamples, internal ? internalAnchorKeys : externalAnchorKeys, absoluteHref, text);
|
|
3297
|
+
} else links.push({
|
|
3298
|
+
href: absoluteHref,
|
|
3299
|
+
text,
|
|
3231
3300
|
rel: ($(element).attr("rel") ?? "").split(/\s+/).filter(Boolean),
|
|
3232
|
-
internal
|
|
3301
|
+
internal,
|
|
3233
3302
|
location: $(element).closest("nav, header").length ? "navigation" : $(element).closest("footer").length ? "footer" : $(element).closest("main, article").length ? "main-content" : "other"
|
|
3234
3303
|
});
|
|
3235
3304
|
} catch {
|
|
3236
3305
|
invalidLinkCount += 1;
|
|
3237
3306
|
}
|
|
3238
|
-
}
|
|
3307
|
+
});
|
|
3239
3308
|
const hreflang = [];
|
|
3240
3309
|
let invalidHreflangCount = 0;
|
|
3241
3310
|
for (const element of $("link[rel~=\"alternate\"][hreflang][href]").toArray()) {
|
|
@@ -3250,25 +3319,29 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3250
3319
|
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();
|
|
3251
3320
|
const openGraph = Object.fromEntries($("meta[property^=\"og:\"]").toArray().map((element) => [$(element).attr("property") ?? "", $(element).attr("content") ?? ""]).filter(([key, value]) => key && value));
|
|
3252
3321
|
const twitter = Object.fromEntries($("meta[name^=\"twitter:\"]").toArray().map((element) => [$(element).attr("name") ?? "", $(element).attr("content") ?? ""]).filter(([key, value]) => key && value));
|
|
3253
|
-
const structuredData = extractStructuredData($, fetchResult.finalUrl);
|
|
3322
|
+
const structuredData = extractStructuredData($, fetchResult.finalUrl, { retainJsonLd: extractor !== "crawler" });
|
|
3254
3323
|
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")));
|
|
3255
3324
|
const author = safeText$1($("meta[name=\"author\"]").attr("content"));
|
|
3256
3325
|
const hasAuthor = Boolean(author) || structuredData.hasAuthor || $("[rel~=author], .author, .byline").toArray().some((element) => Boolean(safeText$1($(element).text())));
|
|
3257
3326
|
const semanticHtml = $("main, article").length > 0;
|
|
3258
|
-
const questionHeadings = headings.filter((heading) => heading.text.trimEnd().endsWith("?")).length;
|
|
3259
3327
|
const listCount = $("ul, ol").length;
|
|
3260
3328
|
const tableCount = $("table").length;
|
|
3261
3329
|
const structuredBlocks = listCount + tableCount;
|
|
3262
3330
|
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);
|
|
3263
|
-
|
|
3264
|
-
|
|
3331
|
+
let imagesTotal = 0;
|
|
3332
|
+
let imagesMissingAlt = 0;
|
|
3333
|
+
const oversizedImageCandidates = [];
|
|
3334
|
+
$("img").each((_index, element) => {
|
|
3335
|
+
imagesTotal += 1;
|
|
3336
|
+
if ($(element).attr("alt") === void 0) imagesMissingAlt += 1;
|
|
3337
|
+
if (oversizedImageCandidates.length >= 25) return;
|
|
3265
3338
|
const src = absoluteUrl$2($(element).attr("src"), fetchResult.finalUrl);
|
|
3266
|
-
if (!src) return
|
|
3339
|
+
if (!src) return;
|
|
3267
3340
|
const width = numericAttribute($(element).attr("width"));
|
|
3268
3341
|
const height = numericAttribute($(element).attr("height"));
|
|
3269
3342
|
const srcsetWidth = largestSrcsetWidth($(element).attr("srcset"));
|
|
3270
3343
|
const filenameDimensions = imageDimensionsFromUrl(src);
|
|
3271
|
-
if (Math.max(width ?? 0, height ?? 0, srcsetWidth ?? 0, filenameDimensions?.width ?? 0, filenameDimensions?.height ?? 0) < 2e3) return
|
|
3344
|
+
if (Math.max(width ?? 0, height ?? 0, srcsetWidth ?? 0, filenameDimensions?.width ?? 0, filenameDimensions?.height ?? 0) < 2e3) return;
|
|
3272
3345
|
const detectedFrom = [
|
|
3273
3346
|
width && width >= 2e3 ? "width" : void 0,
|
|
3274
3347
|
height && height >= 2e3 ? "height" : void 0,
|
|
@@ -3277,13 +3350,13 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3277
3350
|
].filter(Boolean).join(",");
|
|
3278
3351
|
const candidateWidth = Math.max(width ?? 0, srcsetWidth ?? 0, filenameDimensions?.width ?? 0);
|
|
3279
3352
|
const candidateHeight = Math.max(height ?? 0, filenameDimensions?.height ?? 0);
|
|
3280
|
-
|
|
3353
|
+
oversizedImageCandidates.push({
|
|
3281
3354
|
src,
|
|
3282
3355
|
detectedFrom,
|
|
3283
3356
|
...candidateWidth > 0 ? { width: candidateWidth } : {},
|
|
3284
3357
|
...candidateHeight > 0 ? { height: candidateHeight } : {}
|
|
3285
|
-
};
|
|
3286
|
-
})
|
|
3358
|
+
});
|
|
3359
|
+
});
|
|
3287
3360
|
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:"))] : [];
|
|
3288
3361
|
const metaRobots = combineRobotsValues($("meta[name]").toArray().filter((element) => {
|
|
3289
3362
|
const name = $(element).attr("name")?.trim().toLowerCase();
|
|
@@ -3307,6 +3380,18 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3307
3380
|
hasViewport: Boolean($("meta[name=\"viewport\"]").attr("content")),
|
|
3308
3381
|
headings,
|
|
3309
3382
|
links,
|
|
3383
|
+
...extractor === "crawler" ? { crawlerEvidence: {
|
|
3384
|
+
h1: crawlerH1,
|
|
3385
|
+
h1Count: crawlerH1Count,
|
|
3386
|
+
h2Count: crawlerH2Count,
|
|
3387
|
+
h3Count: crawlerH3Count,
|
|
3388
|
+
questionHeadings,
|
|
3389
|
+
internalLinks: [...crawlerInternalLinks],
|
|
3390
|
+
externalLinks: [...crawlerExternalLinks],
|
|
3391
|
+
internalAnchorSamples,
|
|
3392
|
+
externalAnchorSamples,
|
|
3393
|
+
socialProfileLinks: [...socialProfileLinks]
|
|
3394
|
+
} } : {},
|
|
3310
3395
|
markdownAlternates,
|
|
3311
3396
|
hreflang,
|
|
3312
3397
|
jsonLd: structuredData.jsonLd,
|
|
@@ -3315,19 +3400,17 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3315
3400
|
unrecognizedJsonLdTypes: structuredData.unrecognizedJsonLdTypes.slice(0, 25),
|
|
3316
3401
|
structuredDataFormats: structuredData.formats,
|
|
3317
3402
|
googleRichResults: structuredData.googleRichResults,
|
|
3318
|
-
schemaSameAsEvidence: structuredData.sameAs,
|
|
3319
|
-
invalidSchemaSameAs: structuredData.invalidSameAs,
|
|
3403
|
+
schemaSameAsEvidence: extractor === "crawler" ? structuredData.sameAs.slice(0, 50) : structuredData.sameAs,
|
|
3404
|
+
invalidSchemaSameAs: extractor === "crawler" ? structuredData.invalidSameAs.slice(0, 25) : structuredData.invalidSameAs,
|
|
3320
3405
|
schemaTypes: structuredData.schemaTypes,
|
|
3321
3406
|
openGraph,
|
|
3322
3407
|
twitter,
|
|
3323
3408
|
author,
|
|
3324
3409
|
hasAuthor,
|
|
3325
3410
|
hasDate,
|
|
3326
|
-
imagesTotal
|
|
3327
|
-
imagesMissingAlt
|
|
3328
|
-
|
|
3329
|
-
}).length,
|
|
3330
|
-
oversizedImageCandidates: oversizedImageCandidates.slice(0, 25),
|
|
3411
|
+
imagesTotal,
|
|
3412
|
+
imagesMissingAlt,
|
|
3413
|
+
oversizedImageCandidates,
|
|
3331
3414
|
mixedContentUrls,
|
|
3332
3415
|
semanticHtml,
|
|
3333
3416
|
questionHeadings,
|
|
@@ -3335,7 +3418,7 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3335
3418
|
tableCount,
|
|
3336
3419
|
structuredBlocks,
|
|
3337
3420
|
answerable,
|
|
3338
|
-
contentText: text.replace(/\s+/g, " ").trim(),
|
|
3421
|
+
contentText: extractor === "crawler" ? text : text.replace(/\s+/g, " ").trim(),
|
|
3339
3422
|
excerpt,
|
|
3340
3423
|
wordCount: content.wordCount,
|
|
3341
3424
|
contentExtraction: content.diagnostics,
|
|
@@ -3867,7 +3950,7 @@ function decodePageFetchCacheEvidence(value) {
|
|
|
3867
3950
|
return;
|
|
3868
3951
|
}
|
|
3869
3952
|
}
|
|
3870
|
-
async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, respectRobots = false) {
|
|
3953
|
+
async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, respectRobots = false, writeCache = true) {
|
|
3871
3954
|
const startedAt = Date.now();
|
|
3872
3955
|
const db = getDb();
|
|
3873
3956
|
const key = hashKey(["page", url]);
|
|
@@ -3979,7 +4062,7 @@ async function fetchPlain(url, refresh = false, timeoutMs = 2e4, rate, signal, r
|
|
|
3979
4062
|
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}.`] : []],
|
|
3980
4063
|
robotsTxt: pageRobotsEvidence(robots)
|
|
3981
4064
|
};
|
|
3982
|
-
if (!accessBlock) {
|
|
4065
|
+
if (!accessBlock && writeCache) {
|
|
3983
4066
|
db.prepare(`INSERT OR REPLACE INTO http_cache
|
|
3984
4067
|
(url_hash, url, status, headers_json, body_blob, metadata_json, etag, fetched_at, expires_at)
|
|
3985
4068
|
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);
|
|
@@ -4547,7 +4630,7 @@ async function fetchPage(url, opts = {}, dependencies = {}) {
|
|
|
4547
4630
|
const makeRenderer = dependencies.createPageRenderer ?? createPageRenderer;
|
|
4548
4631
|
const rate = normalizeRateControls(opts.rate);
|
|
4549
4632
|
return await queueForHost(new URL(url).host, rate).add(async () => {
|
|
4550
|
-
const first = await plainFetch(url, opts.refresh, opts.timeoutMs, rate, opts.signal, opts.respectRobots);
|
|
4633
|
+
const first = await plainFetch(url, opts.refresh, opts.timeoutMs, rate, opts.signal, opts.respectRobots, opts.writeCache);
|
|
4551
4634
|
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");
|
|
4552
4635
|
const ownsRenderer = !opts.renderer;
|
|
4553
4636
|
const renderer = opts.renderer ?? makeRenderer();
|
|
@@ -10958,6 +11041,7 @@ const CRAWL_SKIP_REASONS = [
|
|
|
10958
11041
|
"configured-exclusion",
|
|
10959
11042
|
"duplicate-normalization",
|
|
10960
11043
|
"invalid-url",
|
|
11044
|
+
"memory-pressure",
|
|
10961
11045
|
"off-origin",
|
|
10962
11046
|
"origin-backpressure",
|
|
10963
11047
|
"queue-safety-limit",
|
|
@@ -10970,6 +11054,7 @@ const SKIP_IMPACT = {
|
|
|
10970
11054
|
"configured-exclusion": "non-impacting",
|
|
10971
11055
|
"duplicate-normalization": "non-impacting",
|
|
10972
11056
|
"invalid-url": "non-impacting",
|
|
11057
|
+
"memory-pressure": "coverage-affecting",
|
|
10973
11058
|
"off-origin": "non-impacting",
|
|
10974
11059
|
"origin-backpressure": "coverage-affecting",
|
|
10975
11060
|
"queue-safety-limit": "coverage-affecting",
|
|
@@ -11148,17 +11233,44 @@ function sanitizeTenantString(value) {
|
|
|
11148
11233
|
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]");
|
|
11149
11234
|
}
|
|
11150
11235
|
function sanitizeTenantValue(value, key) {
|
|
11151
|
-
if (key && isSensitiveFieldName(key)) return "[redacted]";
|
|
11236
|
+
if (key && isSensitiveFieldName(key)) return value === "[redacted]" ? value : "[redacted]";
|
|
11152
11237
|
if (typeof value === "string") return sanitizeTenantString(value);
|
|
11153
|
-
if (Array.isArray(value))
|
|
11238
|
+
if (Array.isArray(value)) {
|
|
11239
|
+
let sanitized;
|
|
11240
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
11241
|
+
const item = sanitizeTenantValue(value[index]);
|
|
11242
|
+
if (sanitized) sanitized.push(item);
|
|
11243
|
+
else if (item !== value[index]) sanitized = [...value.slice(0, index), item];
|
|
11244
|
+
}
|
|
11245
|
+
return sanitized ?? value;
|
|
11246
|
+
}
|
|
11154
11247
|
if (!value || typeof value !== "object") return value;
|
|
11155
|
-
|
|
11248
|
+
const record = value;
|
|
11249
|
+
let sanitized;
|
|
11250
|
+
for (const [entryKey, entry] of Object.entries(record)) {
|
|
11251
|
+
const safeEntry = sanitizeTenantValue(entry, entryKey);
|
|
11252
|
+
if (safeEntry === entry) continue;
|
|
11253
|
+
sanitized ??= { ...record };
|
|
11254
|
+
sanitized[entryKey] = safeEntry;
|
|
11255
|
+
}
|
|
11256
|
+
return sanitized ?? value;
|
|
11156
11257
|
}
|
|
11157
11258
|
function sanitizeCrawlConfig(config) {
|
|
11158
11259
|
return sanitizeTenantValue(config);
|
|
11159
11260
|
}
|
|
11160
11261
|
function sanitizePages(pages) {
|
|
11161
|
-
|
|
11262
|
+
const safePages = sanitizeTenantValue(pages);
|
|
11263
|
+
let normalized;
|
|
11264
|
+
for (let index = 0; index < safePages.length; index += 1) {
|
|
11265
|
+
const page = safePages[index];
|
|
11266
|
+
if (!page) continue;
|
|
11267
|
+
if ("seoScore" in page || "geoScore" in page) {
|
|
11268
|
+
normalized ??= safePages.slice(0, index);
|
|
11269
|
+
const { seoScore: _seoScore, geoScore: _geoScore, ...current } = page;
|
|
11270
|
+
normalized.push(current);
|
|
11271
|
+
} else if (normalized) normalized.push(page);
|
|
11272
|
+
}
|
|
11273
|
+
return normalized ?? safePages;
|
|
11162
11274
|
}
|
|
11163
11275
|
function sanitizeIssues(issues) {
|
|
11164
11276
|
return sanitizeTenantValue(issues);
|
|
@@ -11187,18 +11299,28 @@ function summarizeCrawlReport(input) {
|
|
|
11187
11299
|
const byCategory = {};
|
|
11188
11300
|
let responseMs = 0;
|
|
11189
11301
|
let responseCount = 0;
|
|
11302
|
+
let statusOnlyPages = 0;
|
|
11303
|
+
let indexablePages = 0;
|
|
11304
|
+
let nonIndexablePages = 0;
|
|
11305
|
+
let statusErrors = 0;
|
|
11190
11306
|
for (const page of input.pages) {
|
|
11191
11307
|
byStatus[String(page.status)] = (byStatus[String(page.status)] ?? 0) + 1;
|
|
11192
11308
|
if (typeof page.responseTimeMs === "number") {
|
|
11193
11309
|
responseMs += page.responseTimeMs;
|
|
11194
11310
|
responseCount += 1;
|
|
11195
11311
|
}
|
|
11312
|
+
if (page.auditScope === "status") statusOnlyPages += 1;
|
|
11313
|
+
else if (page.indexable) indexablePages += 1;
|
|
11314
|
+
else nonIndexablePages += 1;
|
|
11315
|
+
if (page.status === 0 || page.status >= 400) statusErrors += 1;
|
|
11196
11316
|
}
|
|
11197
11317
|
const requests = input.requests ?? [];
|
|
11198
|
-
const failedRequests = requests.filter((request) => request.outcome === "failure" && request.failureKind !== "aborted");
|
|
11199
|
-
const abortedRequests = requests.filter((request) => request.outcome === "failure" && request.failureKind === "aborted");
|
|
11200
11318
|
let requestMs = 0;
|
|
11201
11319
|
let requestMsCount = 0;
|
|
11320
|
+
let responseRequests = 0;
|
|
11321
|
+
let failedRequests = 0;
|
|
11322
|
+
let abortedRequests = 0;
|
|
11323
|
+
let extractionFailures = 0;
|
|
11202
11324
|
for (const request of requests) {
|
|
11203
11325
|
const status = request.outcome === "response" ? String(request.status) : request.outcome === "skipped" ? request.reason : request.failureKind === "aborted" ? "aborted" : "no-response";
|
|
11204
11326
|
requestByStatus[status] = (requestByStatus[status] ?? 0) + 1;
|
|
@@ -11206,27 +11328,39 @@ function summarizeCrawlReport(input) {
|
|
|
11206
11328
|
requestMs += request.durationMs;
|
|
11207
11329
|
requestMsCount += 1;
|
|
11208
11330
|
}
|
|
11331
|
+
if (request.outcome === "response") {
|
|
11332
|
+
responseRequests += 1;
|
|
11333
|
+
if (request.extraction === "failed") extractionFailures += 1;
|
|
11334
|
+
} else if (request.outcome === "failure") if (request.failureKind === "aborted") abortedRequests += 1;
|
|
11335
|
+
else failedRequests += 1;
|
|
11336
|
+
}
|
|
11337
|
+
let highIssues = 0;
|
|
11338
|
+
let mediumIssues = 0;
|
|
11339
|
+
let lowIssues = 0;
|
|
11340
|
+
for (const issue of input.issues) {
|
|
11341
|
+
byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
|
|
11342
|
+
if (issue.severity === "high") highIssues += 1;
|
|
11343
|
+
else if (issue.severity === "medium") mediumIssues += 1;
|
|
11344
|
+
else lowIssues += 1;
|
|
11209
11345
|
}
|
|
11210
|
-
for (const issue of input.issues) byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
|
|
11211
11346
|
const runStats = crawlRunStats(input.pages, input.stats);
|
|
11212
|
-
const indexabilityPages = input.pages.filter((page) => page.auditScope !== "status");
|
|
11213
11347
|
return {
|
|
11214
11348
|
totalPages: input.pages.length,
|
|
11215
|
-
statusOnlyPages
|
|
11216
|
-
indexablePages
|
|
11217
|
-
nonIndexablePages
|
|
11218
|
-
statusErrors
|
|
11349
|
+
statusOnlyPages,
|
|
11350
|
+
indexablePages,
|
|
11351
|
+
nonIndexablePages,
|
|
11352
|
+
statusErrors,
|
|
11219
11353
|
...runStats,
|
|
11220
11354
|
attemptedRequests: input.requestEvidenceStatus === "unavailable" ? 0 : requests.length,
|
|
11221
|
-
responseRequests
|
|
11222
|
-
failedRequests
|
|
11223
|
-
abortedRequests
|
|
11224
|
-
extractionFailures
|
|
11355
|
+
responseRequests,
|
|
11356
|
+
failedRequests,
|
|
11357
|
+
abortedRequests,
|
|
11358
|
+
extractionFailures,
|
|
11225
11359
|
requestByStatus,
|
|
11226
11360
|
avgRequestMs: requestMsCount ? Math.round(requestMs / requestMsCount) : void 0,
|
|
11227
|
-
highIssues
|
|
11228
|
-
mediumIssues
|
|
11229
|
-
lowIssues
|
|
11361
|
+
highIssues,
|
|
11362
|
+
mediumIssues,
|
|
11363
|
+
lowIssues,
|
|
11230
11364
|
avgResponseMs: responseCount ? Math.round(responseMs / responseCount) : void 0,
|
|
11231
11365
|
byStatus,
|
|
11232
11366
|
byCategory
|
|
@@ -11234,42 +11368,55 @@ function summarizeCrawlReport(input) {
|
|
|
11234
11368
|
}
|
|
11235
11369
|
const ACCESS_BLOCK_SAMPLE_LIMIT = 10;
|
|
11236
11370
|
function crawlAccessSummary(requests) {
|
|
11237
|
-
const blocked = requests.flatMap((request) => request.outcome === "response" && request.accessBlock ? [{
|
|
11238
|
-
url: request.requestedUrl,
|
|
11239
|
-
evidence: request.accessBlock
|
|
11240
|
-
}] : []);
|
|
11241
11371
|
const providers = {};
|
|
11242
|
-
|
|
11372
|
+
const samples = [];
|
|
11373
|
+
let blockedRequests = 0;
|
|
11374
|
+
for (const request of requests) {
|
|
11375
|
+
if (request.outcome !== "response" || !request.accessBlock) continue;
|
|
11376
|
+
blockedRequests += 1;
|
|
11377
|
+
providers[request.accessBlock.provider] = (providers[request.accessBlock.provider] ?? 0) + 1;
|
|
11378
|
+
if (samples.length < ACCESS_BLOCK_SAMPLE_LIMIT) samples.push({
|
|
11379
|
+
url: request.requestedUrl,
|
|
11380
|
+
evidence: request.accessBlock
|
|
11381
|
+
});
|
|
11382
|
+
}
|
|
11243
11383
|
return {
|
|
11244
11384
|
crawler: SEO_CRAWLER_IDENTITY,
|
|
11245
|
-
blockedRequests
|
|
11385
|
+
blockedRequests,
|
|
11246
11386
|
providers,
|
|
11247
|
-
samples
|
|
11387
|
+
samples,
|
|
11248
11388
|
sampleLimit: ACCESS_BLOCK_SAMPLE_LIMIT,
|
|
11249
|
-
truncated:
|
|
11389
|
+
truncated: blockedRequests > ACCESS_BLOCK_SAMPLE_LIMIT
|
|
11250
11390
|
};
|
|
11251
11391
|
}
|
|
11252
11392
|
function crawlRunStats(pages, stats = {}) {
|
|
11253
|
-
const
|
|
11393
|
+
const needsDiscoveredUrls = stats.discoveredUrls === void 0;
|
|
11394
|
+
const needsObservedInternalLinks = stats.observedInternalLinks === void 0;
|
|
11395
|
+
const needsFailedUrls = stats.failedUrls === void 0;
|
|
11396
|
+
const discovered = needsDiscoveredUrls ? /* @__PURE__ */ new Set() : void 0;
|
|
11254
11397
|
let observedInternalLinks = 0;
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
11398
|
+
let failedUrls = 0;
|
|
11399
|
+
if (needsDiscoveredUrls || needsObservedInternalLinks || needsFailedUrls) for (const page of pages) {
|
|
11400
|
+
if (discovered) {
|
|
11401
|
+
discovered.add(page.url);
|
|
11402
|
+
for (const url of page.sampleInternalLinks ?? []) discovered.add(url);
|
|
11403
|
+
for (const url of page.sampleExternalLinks ?? []) discovered.add(url);
|
|
11404
|
+
}
|
|
11405
|
+
if (needsObservedInternalLinks) observedInternalLinks += page.outgoingInternalCount;
|
|
11406
|
+
if (needsFailedUrls && (page.status === 0 || page.status >= 400)) failedUrls += 1;
|
|
11260
11407
|
}
|
|
11261
11408
|
const skips = normalizeCrawlSkipReasonCounts({
|
|
11262
11409
|
skippedUrls: stats.skippedUrls,
|
|
11263
11410
|
skipReasons: stats.skipReasons ?? crawlSkipReasonCountsFromRecord(stats.skipReasonCounts)
|
|
11264
11411
|
});
|
|
11265
11412
|
return {
|
|
11266
|
-
discoveredUrls: stats.discoveredUrls ?? discovered
|
|
11413
|
+
discoveredUrls: stats.discoveredUrls ?? discovered?.size ?? 0,
|
|
11267
11414
|
queuedUrls: stats.queuedUrls ?? pages.length,
|
|
11268
11415
|
crawledUrls: stats.crawledUrls ?? pages.length,
|
|
11269
11416
|
skippedUrls: skips.skippedUrls,
|
|
11270
11417
|
skipReasons: skips.skipReasons,
|
|
11271
11418
|
skippedUrlsByImpact: skips.skippedUrlsByImpact,
|
|
11272
|
-
failedUrls: stats.failedUrls ??
|
|
11419
|
+
failedUrls: stats.failedUrls ?? failedUrls,
|
|
11273
11420
|
observedInternalLinks: stats.observedInternalLinks ?? observedInternalLinks,
|
|
11274
11421
|
pageLimitReached: stats.pageLimitReached ?? false
|
|
11275
11422
|
};
|
|
@@ -11286,11 +11433,14 @@ function normalizeLinkUrl(value) {
|
|
|
11286
11433
|
function deriveInternalLinkAuthority(pages, linkGraph) {
|
|
11287
11434
|
const hasLinkGraph = linkGraph && Object.keys(linkGraph).length > 0;
|
|
11288
11435
|
const hasStoredScores = pages.some((page) => page.internalInlinkCount !== void 0 || page.internalLinkAuthorityScore !== void 0);
|
|
11289
|
-
if (!hasLinkGraph && hasStoredScores)
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
11436
|
+
if (!hasLinkGraph && hasStoredScores) {
|
|
11437
|
+
if (pages.every((page) => page.internalInlinkCount !== void 0 && page.internalLinkAuthorityScore !== void 0)) return pages;
|
|
11438
|
+
return pages.map((page) => page.internalInlinkCount !== void 0 && page.internalLinkAuthorityScore !== void 0 ? page : {
|
|
11439
|
+
...page,
|
|
11440
|
+
internalInlinkCount: page.internalInlinkCount ?? 0,
|
|
11441
|
+
internalLinkAuthorityScore: page.internalLinkAuthorityScore ?? 0
|
|
11442
|
+
});
|
|
11443
|
+
}
|
|
11294
11444
|
const pageByUrl = /* @__PURE__ */ new Map();
|
|
11295
11445
|
for (const page of pages) for (const value of [page.url, page.finalUrl]) {
|
|
11296
11446
|
const normalized = normalizeLinkUrl(value);
|
|
@@ -11309,7 +11459,8 @@ function deriveInternalLinkAuthority(pages, linkGraph) {
|
|
|
11309
11459
|
}
|
|
11310
11460
|
for (const linkedPage of linkedPages) inlinks.set(linkedPage, (inlinks.get(linkedPage) ?? 0) + 1);
|
|
11311
11461
|
}
|
|
11312
|
-
|
|
11462
|
+
let maxInlinks = 0;
|
|
11463
|
+
for (const count of inlinks.values()) maxInlinks = Math.max(maxInlinks, count);
|
|
11313
11464
|
return pages.map((page) => {
|
|
11314
11465
|
const internalInlinkCount = inlinks.get(page.url) ?? 0;
|
|
11315
11466
|
return {
|
|
@@ -11567,6 +11718,7 @@ function crawlCaveats(input) {
|
|
|
11567
11718
|
const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
|
|
11568
11719
|
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.`);
|
|
11569
11720
|
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.`);
|
|
11721
|
+
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.`);
|
|
11570
11722
|
return caveats;
|
|
11571
11723
|
}
|
|
11572
11724
|
//#endregion
|
|
@@ -11593,6 +11745,26 @@ function cancellationRace(signal) {
|
|
|
11593
11745
|
});
|
|
11594
11746
|
}
|
|
11595
11747
|
//#endregion
|
|
11748
|
+
//#region packages/core/src/analyze/crawler/crawl-memory.ts
|
|
11749
|
+
const MEBIBYTE$1 = 1024 * 1024;
|
|
11750
|
+
const MIN_CRAWL_MEMORY_LIMIT = 384 * MEBIBYTE$1;
|
|
11751
|
+
const MAX_CRAWL_MEMORY_LIMIT = 640 * MEBIBYTE$1;
|
|
11752
|
+
const MIN_CRAWL_RSS_LIMIT = 512 * MEBIBYTE$1;
|
|
11753
|
+
const MAX_CRAWL_RSS_LIMIT = 896 * MEBIBYTE$1;
|
|
11754
|
+
const SYSTEM_MEMORY_FRACTION = .1;
|
|
11755
|
+
const SYSTEM_RSS_FRACTION = .2;
|
|
11756
|
+
function crawlMemoryLimitBytes(totalMemoryBytes) {
|
|
11757
|
+
if (!Number.isFinite(totalMemoryBytes) || totalMemoryBytes <= 0) return MAX_CRAWL_MEMORY_LIMIT;
|
|
11758
|
+
return Math.min(MAX_CRAWL_MEMORY_LIMIT, Math.max(MIN_CRAWL_MEMORY_LIMIT, Math.floor(totalMemoryBytes * SYSTEM_MEMORY_FRACTION)));
|
|
11759
|
+
}
|
|
11760
|
+
function crawlRssLimitBytes(totalMemoryBytes) {
|
|
11761
|
+
if (!Number.isFinite(totalMemoryBytes) || totalMemoryBytes <= 0) return MAX_CRAWL_RSS_LIMIT;
|
|
11762
|
+
return Math.min(MAX_CRAWL_RSS_LIMIT, Math.max(MIN_CRAWL_RSS_LIMIT, Math.floor(totalMemoryBytes * SYSTEM_RSS_FRACTION)));
|
|
11763
|
+
}
|
|
11764
|
+
function crawlMemoryPressure(input) {
|
|
11765
|
+
return input.memoryUsage.heapUsed + input.memoryUsage.external >= crawlMemoryLimitBytes(input.totalMemoryBytes) || input.memoryUsage.rss >= crawlRssLimitBytes(input.totalMemoryBytes);
|
|
11766
|
+
}
|
|
11767
|
+
//#endregion
|
|
11596
11768
|
//#region packages/core/src/analyze/crawler/crawl-url-queue.ts
|
|
11597
11769
|
function before(left, right) {
|
|
11598
11770
|
return left.depth < right.depth || left.depth === right.depth && left.url < right.url;
|
|
@@ -12310,6 +12482,7 @@ function crawlProviderLimits(input) {
|
|
|
12310
12482
|
}
|
|
12311
12483
|
async function crawlDataSources(input) {
|
|
12312
12484
|
const window = crawlMetricsWindow(input.now());
|
|
12485
|
+
const skippedForMemory = input.skipReason === "memory-pressure";
|
|
12313
12486
|
const dataSources = {
|
|
12314
12487
|
searchConsole: {
|
|
12315
12488
|
status: "skipped",
|
|
@@ -12320,7 +12493,7 @@ async function crawlDataSources(input) {
|
|
|
12320
12493
|
pageLimit: input.searchMetricsLimit,
|
|
12321
12494
|
pageLimitReached: false,
|
|
12322
12495
|
retainedRowLimitReached: false,
|
|
12323
|
-
warning: input.cancelled ? "Search Console join skipped because the crawl was cancelled." : "Search Console join skipped because no property was selected."
|
|
12496
|
+
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."
|
|
12324
12497
|
},
|
|
12325
12498
|
analytics: {
|
|
12326
12499
|
status: "skipped",
|
|
@@ -12329,10 +12502,10 @@ async function crawlDataSources(input) {
|
|
|
12329
12502
|
joinedPages: 0,
|
|
12330
12503
|
retainedRowLimit: input.analyticsLimit,
|
|
12331
12504
|
retainedRowLimitReached: false,
|
|
12332
|
-
warning: input.cancelled ? "Google Analytics join skipped because the crawl was cancelled." : "Google Analytics join skipped because no property was selected."
|
|
12505
|
+
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."
|
|
12333
12506
|
}
|
|
12334
12507
|
};
|
|
12335
|
-
if (!input.
|
|
12508
|
+
if (!input.skipReason && input.site) dataSources.searchConsole = await joinSearchMetrics({
|
|
12336
12509
|
site: input.site,
|
|
12337
12510
|
pages: input.pages,
|
|
12338
12511
|
warnings: input.warnings,
|
|
@@ -12345,7 +12518,7 @@ async function crawlDataSources(input) {
|
|
|
12345
12518
|
queryPagesMetricsBatch: input.queryPagesMetricsBatch,
|
|
12346
12519
|
queryPagesTopQueriesBatch: input.queryPagesTopQueriesBatch
|
|
12347
12520
|
});
|
|
12348
|
-
if (!input.
|
|
12521
|
+
if (!input.skipReason && input.googleAnalyticsPropertyId) dataSources.analytics = await joinAnalytics({
|
|
12349
12522
|
propertyId: input.googleAnalyticsPropertyId,
|
|
12350
12523
|
pages: input.pages,
|
|
12351
12524
|
warnings: input.warnings,
|
|
@@ -12358,17 +12531,6 @@ async function crawlDataSources(input) {
|
|
|
12358
12531
|
}
|
|
12359
12532
|
//#endregion
|
|
12360
12533
|
//#region packages/core/src/analyze/monitoring/crawl-page.ts
|
|
12361
|
-
function sameOriginUrl(href, base) {
|
|
12362
|
-
try {
|
|
12363
|
-
const url = new URL(href, base);
|
|
12364
|
-
url.hash = "";
|
|
12365
|
-
if (url.origin !== base.origin) return void 0;
|
|
12366
|
-
if (!["http:", "https:"].includes(url.protocol)) return void 0;
|
|
12367
|
-
return url.toString();
|
|
12368
|
-
} catch {
|
|
12369
|
-
return;
|
|
12370
|
-
}
|
|
12371
|
-
}
|
|
12372
12534
|
function resolvedUrl(href, base) {
|
|
12373
12535
|
if (!href) return void 0;
|
|
12374
12536
|
try {
|
|
@@ -12398,49 +12560,6 @@ function safeResponseHeaders(headers) {
|
|
|
12398
12560
|
function truncate$1(value, max) {
|
|
12399
12561
|
return value.length > max ? value.slice(0, max).trimEnd() : value;
|
|
12400
12562
|
}
|
|
12401
|
-
function anchorSamples(links, internal) {
|
|
12402
|
-
const seen = /* @__PURE__ */ new Set();
|
|
12403
|
-
const samples = [];
|
|
12404
|
-
for (const link of links) {
|
|
12405
|
-
if (link.internal !== internal) continue;
|
|
12406
|
-
const text = truncate$1(link.text.replace(/\s+/g, " ").trim(), 120);
|
|
12407
|
-
if (!text) continue;
|
|
12408
|
-
const key = `${link.href}\n${text}`;
|
|
12409
|
-
if (seen.has(key)) continue;
|
|
12410
|
-
seen.add(key);
|
|
12411
|
-
samples.push({
|
|
12412
|
-
href: link.href,
|
|
12413
|
-
text
|
|
12414
|
-
});
|
|
12415
|
-
if (samples.length >= 25) break;
|
|
12416
|
-
}
|
|
12417
|
-
return samples;
|
|
12418
|
-
}
|
|
12419
|
-
function socialProfileLinks(links) {
|
|
12420
|
-
const hosts = [
|
|
12421
|
-
"facebook.com",
|
|
12422
|
-
"instagram.com",
|
|
12423
|
-
"linkedin.com",
|
|
12424
|
-
"pinterest.com",
|
|
12425
|
-
"tiktok.com",
|
|
12426
|
-
"twitter.com",
|
|
12427
|
-
"x.com",
|
|
12428
|
-
"youtube.com"
|
|
12429
|
-
];
|
|
12430
|
-
const profiles = /* @__PURE__ */ new Set();
|
|
12431
|
-
for (const link of links) {
|
|
12432
|
-
if (link.internal) continue;
|
|
12433
|
-
try {
|
|
12434
|
-
const url = new URL(link.href);
|
|
12435
|
-
const host = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
12436
|
-
if (hosts.some((value) => host === value || host.endsWith(`.${value}`))) {
|
|
12437
|
-
url.hash = "";
|
|
12438
|
-
profiles.add(url.toString());
|
|
12439
|
-
}
|
|
12440
|
-
} catch {}
|
|
12441
|
-
}
|
|
12442
|
-
return [...profiles].slice(0, 50);
|
|
12443
|
-
}
|
|
12444
12563
|
function sameDocumentUrl(left, right) {
|
|
12445
12564
|
if (!left || !right) return false;
|
|
12446
12565
|
try {
|
|
@@ -12540,17 +12659,9 @@ function statusOnlyPage(fetched) {
|
|
|
12540
12659
|
finalUrl: fetched.finalUrl,
|
|
12541
12660
|
status: fetched.status,
|
|
12542
12661
|
contentType,
|
|
12543
|
-
responseHeaders: safeResponseHeaders(fetched.headers),
|
|
12544
12662
|
responseTimeMs: fetched.diagnostics.durationMs,
|
|
12545
|
-
usedJs: false,
|
|
12546
|
-
fetchSource: "network",
|
|
12547
|
-
cacheState: "bypass",
|
|
12548
|
-
fetchDiagnostics: fetched.diagnostics,
|
|
12549
12663
|
auditScope: "status",
|
|
12550
|
-
accessBlock: fetched.diagnostics.accessBlock,
|
|
12551
|
-
blocked: fetched.diagnostics.blocked,
|
|
12552
12664
|
contentAuditAllowed: false,
|
|
12553
|
-
robotsTxt: fetched.robotsTxt,
|
|
12554
12665
|
indexable: false,
|
|
12555
12666
|
indexability: "Not evaluated by status-only health probe",
|
|
12556
12667
|
declaredIndexability: "unknown",
|
|
@@ -12558,8 +12669,7 @@ function statusOnlyPage(fetched) {
|
|
|
12558
12669
|
wordCount: 0,
|
|
12559
12670
|
contentHash: "",
|
|
12560
12671
|
outgoingInternalCount: 0,
|
|
12561
|
-
outgoingExternalCount: 0
|
|
12562
|
-
isHttps: new URL(fetched.finalUrl).protocol === "https:"
|
|
12672
|
+
outgoingExternalCount: 0
|
|
12563
12673
|
};
|
|
12564
12674
|
}
|
|
12565
12675
|
function indexabilityReason(page) {
|
|
@@ -12635,12 +12745,11 @@ async function crawlOne(url, opts = {}) {
|
|
|
12635
12745
|
};
|
|
12636
12746
|
try {
|
|
12637
12747
|
const extracted = await extractPage(fetched, "crawler");
|
|
12638
|
-
const
|
|
12639
|
-
|
|
12640
|
-
const
|
|
12641
|
-
const
|
|
12642
|
-
const
|
|
12643
|
-
const uniqueExternalLinks = [...new Set(externalLinks)];
|
|
12748
|
+
const crawlerEvidence = extracted.crawlerEvidence;
|
|
12749
|
+
if (!crawlerEvidence) throw new Error("Crawler extraction did not return crawl evidence.");
|
|
12750
|
+
const h1 = crawlerEvidence.h1;
|
|
12751
|
+
const uniqueInternalLinks = crawlerEvidence.internalLinks;
|
|
12752
|
+
const uniqueExternalLinks = crawlerEvidence.externalLinks;
|
|
12644
12753
|
const canonical = extracted.canonicalEvidence?.selectedUrl ?? resolvedUrl(extracted.canonical, extracted.finalUrl);
|
|
12645
12754
|
const richResults = selectGoogleRichResultAssessments(extracted.googleRichResults ?? []);
|
|
12646
12755
|
const page = {
|
|
@@ -12666,9 +12775,9 @@ async function crawlOne(url, opts = {}) {
|
|
|
12666
12775
|
metaRobots: extracted.metaRobots,
|
|
12667
12776
|
xRobotsTag: extracted.xRobotsTag,
|
|
12668
12777
|
h1,
|
|
12669
|
-
h1Count:
|
|
12670
|
-
h2Count:
|
|
12671
|
-
h3Count:
|
|
12778
|
+
h1Count: crawlerEvidence.h1Count,
|
|
12779
|
+
h2Count: crawlerEvidence.h2Count,
|
|
12780
|
+
h3Count: crawlerEvidence.h3Count,
|
|
12672
12781
|
indexable: false,
|
|
12673
12782
|
extractionStatus: "complete",
|
|
12674
12783
|
wordCount: extracted.wordCount,
|
|
@@ -12704,8 +12813,8 @@ async function crawlOne(url, opts = {}) {
|
|
|
12704
12813
|
outgoingExternalCount: uniqueExternalLinks.length,
|
|
12705
12814
|
sampleInternalLinks: uniqueInternalLinks.slice(0, 25),
|
|
12706
12815
|
sampleExternalLinks: uniqueExternalLinks.slice(0, 25),
|
|
12707
|
-
internalAnchorSamples:
|
|
12708
|
-
externalAnchorSamples:
|
|
12816
|
+
internalAnchorSamples: crawlerEvidence.internalAnchorSamples,
|
|
12817
|
+
externalAnchorSamples: crawlerEvidence.externalAnchorSamples,
|
|
12709
12818
|
markdownAlternates: extracted.markdownAlternates,
|
|
12710
12819
|
schemaTypes: extracted.schemaTypes,
|
|
12711
12820
|
structuredDataFormats: extracted.structuredDataFormats ?? [],
|
|
@@ -12714,7 +12823,7 @@ async function crawlOne(url, opts = {}) {
|
|
|
12714
12823
|
schemaSameAs: [...new Set((extracted.schemaSameAsEvidence ?? []).map((evidence) => evidence.url))].slice(0, 50),
|
|
12715
12824
|
schemaSameAsEvidence: (extracted.schemaSameAsEvidence ?? []).slice(0, 50),
|
|
12716
12825
|
invalidSchemaSameAs: (extracted.invalidSchemaSameAs ?? []).slice(0, 25),
|
|
12717
|
-
socialProfileLinks: socialProfileLinks
|
|
12826
|
+
socialProfileLinks: crawlerEvidence.socialProfileLinks,
|
|
12718
12827
|
invalidJsonLdCount: extracted.invalidJsonLdCount,
|
|
12719
12828
|
invalidJsonLdSamples: extracted.invalidJsonLdSamples,
|
|
12720
12829
|
unrecognizedJsonLdTypes: extracted.unrecognizedJsonLdTypes ?? [],
|
|
@@ -13367,7 +13476,9 @@ function resolveCrawlSiteDependencies(dependencies = {}) {
|
|
|
13367
13476
|
fetchLandingPageValues: dependencies.fetchLandingPageValues ?? fetchLandingPageValues,
|
|
13368
13477
|
landingValueForUrl: dependencies.landingValueForUrl ?? landingValueForUrl,
|
|
13369
13478
|
collectAgentDiscovery: dependencies.collectAgentDiscovery ?? collectAgentDiscovery,
|
|
13370
|
-
now: dependencies.now ?? (() => /* @__PURE__ */ new Date())
|
|
13479
|
+
now: dependencies.now ?? (() => /* @__PURE__ */ new Date()),
|
|
13480
|
+
memoryUsage: dependencies.memoryUsage ?? (() => process.memoryUsage()),
|
|
13481
|
+
totalMemory: dependencies.totalMemory ?? totalmem
|
|
13371
13482
|
};
|
|
13372
13483
|
}
|
|
13373
13484
|
//#endregion
|
|
@@ -13523,15 +13634,26 @@ async function crawlSite(input, dependencies) {
|
|
|
13523
13634
|
let queuedUrls = 0;
|
|
13524
13635
|
let skippedUrls = 0;
|
|
13525
13636
|
const skipReasonCounts = {};
|
|
13526
|
-
const
|
|
13527
|
-
|
|
13528
|
-
|
|
13637
|
+
const recordSkips = (reason, count = 1) => {
|
|
13638
|
+
if (count <= 0) return;
|
|
13639
|
+
skippedUrls += count;
|
|
13640
|
+
skipReasonCounts[reason] = (skipReasonCounts[reason] ?? 0) + count;
|
|
13529
13641
|
};
|
|
13642
|
+
const recordSkip = (reason) => recordSkips(reason);
|
|
13530
13643
|
let robotsDeferredUrls = 0;
|
|
13531
13644
|
let originBackpressureSkippedUrls = 0;
|
|
13532
13645
|
let queueSafetySkippedUrls = 0;
|
|
13533
13646
|
let failedUrls = 0;
|
|
13534
13647
|
let observedInternalLinks = 0;
|
|
13648
|
+
let memoryLimited = false;
|
|
13649
|
+
const memoryDeferredUrls = /* @__PURE__ */ new Set();
|
|
13650
|
+
const checkMemoryPressure = () => {
|
|
13651
|
+
if (memoryLimited) return;
|
|
13652
|
+
memoryLimited = crawlMemoryPressure({
|
|
13653
|
+
memoryUsage: deps.memoryUsage(),
|
|
13654
|
+
totalMemoryBytes: deps.totalMemory()
|
|
13655
|
+
});
|
|
13656
|
+
};
|
|
13535
13657
|
let sitemapDiscovery;
|
|
13536
13658
|
let externalLinkVerification;
|
|
13537
13659
|
let statusEventChain = Promise.resolve();
|
|
@@ -13661,6 +13783,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13661
13783
|
for (const url of sitemap.urls) enqueue(url, 0);
|
|
13662
13784
|
sitemapDiscovery = sitemap.discovery;
|
|
13663
13785
|
}
|
|
13786
|
+
checkMemoryPressure();
|
|
13664
13787
|
const nextQueueItem = () => {
|
|
13665
13788
|
while (queue.size) {
|
|
13666
13789
|
const item = queue.take();
|
|
@@ -13686,6 +13809,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13686
13809
|
timeoutMs: config.timeoutMs,
|
|
13687
13810
|
rate: config.fetchRate,
|
|
13688
13811
|
signal,
|
|
13812
|
+
writeCache: false,
|
|
13689
13813
|
respectRobots: config.respectRobots,
|
|
13690
13814
|
robotsResolver,
|
|
13691
13815
|
renderer
|
|
@@ -13713,8 +13837,8 @@ async function crawlSite(input, dependencies) {
|
|
|
13713
13837
|
depth: item.depth
|
|
13714
13838
|
});
|
|
13715
13839
|
};
|
|
13716
|
-
while (!isCancelled() && (queue.size || inFlight.size) && pages.length < config.maxPages) {
|
|
13717
|
-
while (!isCancelled() && inFlight.size < activeConcurrency && pages.length + inFlight.size < config.maxPages) {
|
|
13840
|
+
while (!isCancelled() && (queue.size || inFlight.size) && (!memoryLimited || inFlight.size > 0) && pages.length < config.maxPages) {
|
|
13841
|
+
while (!isCancelled() && !memoryLimited && inFlight.size < activeConcurrency && pages.length + inFlight.size < config.maxPages) {
|
|
13718
13842
|
const item = nextQueueItem();
|
|
13719
13843
|
if (!item) break;
|
|
13720
13844
|
startTask(item);
|
|
@@ -13742,6 +13866,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13742
13866
|
healthyProbeStreak = 0;
|
|
13743
13867
|
activeConcurrency = 1;
|
|
13744
13868
|
}
|
|
13869
|
+
checkMemoryPressure();
|
|
13745
13870
|
if (request.outcome === "skipped") {
|
|
13746
13871
|
recordSkip(request.reason === "robots-disallowed" ? "robots-disallowed" : request.reason === "robots-deferred" ? "robots-uncertain" : "origin-backpressure");
|
|
13747
13872
|
if (request.reason === "robots-deferred") {
|
|
@@ -13872,10 +13997,24 @@ async function crawlSite(input, dependencies) {
|
|
|
13872
13997
|
depth: task.depth,
|
|
13873
13998
|
statusCode: page.status
|
|
13874
13999
|
});
|
|
13875
|
-
if (followLinks && task.depth < config.maxDepth) for (const
|
|
14000
|
+
if (followLinks && task.depth < config.maxDepth) if (memoryLimited) for (const value of result.urls) {
|
|
14001
|
+
const normalized = normalizeUrl$1(value, finalUrl);
|
|
14002
|
+
if (normalized && sameOrigin(normalized, origin) && !isLikelyAsset(normalized) && passesFilters(normalized, config.include, config.exclude) && !visited.has(normalized) && !queue.has(normalized)) {
|
|
14003
|
+
discovered.add(normalized);
|
|
14004
|
+
memoryDeferredUrls.add(normalized);
|
|
14005
|
+
}
|
|
14006
|
+
}
|
|
14007
|
+
else for (const url of result.urls) enqueue(url, task.depth + 1);
|
|
14008
|
+
}
|
|
14009
|
+
const memoryPressureSkippedUrls = memoryLimited ? queue.size + memoryDeferredUrls.size : 0;
|
|
14010
|
+
const stoppedForMemory = memoryPressureSkippedUrls > 0;
|
|
14011
|
+
if (stoppedForMemory) {
|
|
14012
|
+
recordSkips("memory-pressure", memoryPressureSkippedUrls);
|
|
14013
|
+
warnings.push("Crawl stopped before local memory use could become unsafe. The retained report is partial.");
|
|
13876
14014
|
}
|
|
13877
14015
|
const resolvedInlinkCounts = resolveLinkCountAliases(incomingLinkCounts, fetchedAliases);
|
|
13878
|
-
|
|
14016
|
+
let maxInlinks = 0;
|
|
14017
|
+
for (const page of pages) maxInlinks = Math.max(maxInlinks, resolvedInlinkCounts.get(page.finalUrl) ?? 0);
|
|
13879
14018
|
for (const page of pages) {
|
|
13880
14019
|
const internalInlinkCount = resolvedInlinkCounts.get(page.finalUrl) ?? 0;
|
|
13881
14020
|
page.internalInlinkCount = internalInlinkCount;
|
|
@@ -13895,11 +14034,11 @@ async function crawlSite(input, dependencies) {
|
|
|
13895
14034
|
const sitemapEvidencePartial = sitemapDiscovery?.dataStatus === "partial";
|
|
13896
14035
|
const accessEvidencePartial = requests.some((request) => request.outcome === "response" && request.accessBlock);
|
|
13897
14036
|
const requiredSitemapUnavailable = config.mode === "sitemap" && sitemapDiscovery?.dataStatus === "unavailable";
|
|
13898
|
-
const partial = cancelled || failedUrls > 0 || pageLimitReached || robotsDeferredUrls > 0 || originBackpressureSkippedUrls > 0 || queueSafetySkippedUrls > 0 || queue.size > 0 || inFlight.size > 0 || sitemapEvidencePartial || accessEvidencePartial || requiredSitemapUnavailable;
|
|
14037
|
+
const partial = cancelled || failedUrls > 0 || pageLimitReached || robotsDeferredUrls > 0 || originBackpressureSkippedUrls > 0 || queueSafetySkippedUrls > 0 || queue.size > 0 || inFlight.size > 0 || stoppedForMemory || sitemapEvidencePartial || accessEvidencePartial || requiredSitemapUnavailable;
|
|
13899
14038
|
const failed = !cancelled && pages.length === 0 && (requiredSitemapUnavailable || requests.length > 0 && requests.every((request) => request.outcome === "failure"));
|
|
13900
14039
|
const requestEvidenceStatus = cancelled && inFlight.size > 0 ? "partial" : "available";
|
|
13901
14040
|
const dataSources = await crawlDataSources({
|
|
13902
|
-
cancelled,
|
|
14041
|
+
skipReason: cancelled ? "cancelled" : stoppedForMemory ? "memory-pressure" : void 0,
|
|
13903
14042
|
site: healthStrategy ? void 0 : site,
|
|
13904
14043
|
googleAnalyticsPropertyId: healthStrategy ? void 0 : input.googleAnalyticsPropertyId,
|
|
13905
14044
|
pages,
|
|
@@ -13918,7 +14057,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13918
14057
|
});
|
|
13919
14058
|
const sourceEvidencePartial = [dataSources.searchConsole.status, dataSources.analytics.status].some((status) => status === "partial" || status === "unavailable");
|
|
13920
14059
|
const reportStatus = failed ? "failed" : partial || sourceEvidencePartial ? "partial" : "completed";
|
|
13921
|
-
if (!cancelled && config.checkExternal) {
|
|
14060
|
+
if (!cancelled && !stoppedForMemory && config.checkExternal) {
|
|
13922
14061
|
emitStatus("external_links_started", {
|
|
13923
14062
|
url: config.url,
|
|
13924
14063
|
message: "Started external link checks."
|
|
@@ -13935,7 +14074,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13935
14074
|
message: "Finished external link checks."
|
|
13936
14075
|
});
|
|
13937
14076
|
}
|
|
13938
|
-
const agentDiscovery = !cancelled && config.checkAgentDiscovery ? await deps.collectAgentDiscovery({
|
|
14077
|
+
const agentDiscovery = !cancelled && !stoppedForMemory && config.checkAgentDiscovery ? await deps.collectAgentDiscovery({
|
|
13939
14078
|
startUrl: config.url,
|
|
13940
14079
|
pages,
|
|
13941
14080
|
timeoutMs: config.timeoutMs,
|
|
@@ -13976,7 +14115,8 @@ async function crawlSite(input, dependencies) {
|
|
|
13976
14115
|
pageLimitReached,
|
|
13977
14116
|
maxPages: config.maxPages,
|
|
13978
14117
|
queueSafetySkippedUrls,
|
|
13979
|
-
originBackpressureSkippedUrls
|
|
14118
|
+
originBackpressureSkippedUrls,
|
|
14119
|
+
memoryPressureSkippedUrls
|
|
13980
14120
|
}),
|
|
13981
14121
|
stats: {
|
|
13982
14122
|
discoveredUrls: discovered.size,
|