seo 0.2.12 → 0.2.14
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 +138 -161
- package/dist/cli.js.map +1 -1
- package/dist/{dist-D0EI_L8C.js → dist-Bc8MJ1gT.js} +1 -1
- package/dist/{dist-DaHrxVkW.js → dist-sYeCtY3Q.js} +87 -87
- package/dist/dist-sYeCtY3Q.js.map +1 -0
- package/dist/index.d.ts +870 -13
- package/dist/index.js +575 -123
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +575 -123
- package/dist/mcp.js.map +1 -1
- package/dist/{package-B-M-4PZ_.js → package-Cp8DwqCy.js} +2 -2
- package/dist/{package-B-M-4PZ_.js.map → package-Cp8DwqCy.js.map} +1 -1
- package/package.json +1 -1
- package/dist/dist-DaHrxVkW.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { n as load, t as version } from "./package-
|
|
2
|
+
import { n as load, t as version } from "./package-Cp8DwqCy.js";
|
|
3
3
|
import { Headers, fetch as fetch$1 } from "undici";
|
|
4
4
|
import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { arch, homedir, platform, totalmem } from "node:os";
|
|
@@ -2532,6 +2532,43 @@ function extractCanonicalEvidence($, headers, baseUrl) {
|
|
|
2532
2532
|
return evidenceFromCandidates([...htmlCandidates($, baseUrl), ...headerCandidates(headers, baseUrl)]);
|
|
2533
2533
|
}
|
|
2534
2534
|
//#endregion
|
|
2535
|
+
//#region packages/core/src/extract/content-sketch.ts
|
|
2536
|
+
const SHINGLE_SIZE = 5;
|
|
2537
|
+
const MAX_HASHES = 32;
|
|
2538
|
+
function words(value) {
|
|
2539
|
+
return value.normalize("NFKC").toLocaleLowerCase("en").match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
2540
|
+
}
|
|
2541
|
+
function sampleIndexes(total, limit) {
|
|
2542
|
+
if (total <= limit) return Array.from({ length: total }, (_, index) => index);
|
|
2543
|
+
return Array.from({ length: limit }, (_, index) => Math.round(index * (total - 1) / (limit - 1)));
|
|
2544
|
+
}
|
|
2545
|
+
function digest(value) {
|
|
2546
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
2547
|
+
}
|
|
2548
|
+
function contentSketch(value) {
|
|
2549
|
+
const tokens = words(value);
|
|
2550
|
+
const hashes = sampleIndexes(Math.max(0, tokens.length - SHINGLE_SIZE + 1), MAX_HASHES).map((index) => digest(tokens.slice(index, index + SHINGLE_SIZE).join(" ")));
|
|
2551
|
+
return {
|
|
2552
|
+
version: 1,
|
|
2553
|
+
wordCount: tokens.length,
|
|
2554
|
+
shingleSize: SHINGLE_SIZE,
|
|
2555
|
+
sampledShingles: hashes.length,
|
|
2556
|
+
hashes
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
function contentSketchCoverage(source, candidate) {
|
|
2560
|
+
if (!source || source.hashes.length < 4) return null;
|
|
2561
|
+
const targets = new Set(source.hashes);
|
|
2562
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2563
|
+
const tokens = words(candidate);
|
|
2564
|
+
for (let index = 0; index <= tokens.length - source.shingleSize; index += 1) {
|
|
2565
|
+
const hash = digest(tokens.slice(index, index + source.shingleSize).join(" "));
|
|
2566
|
+
if (targets.has(hash)) matched.add(hash);
|
|
2567
|
+
if (matched.size === targets.size) break;
|
|
2568
|
+
}
|
|
2569
|
+
return Math.round(matched.size / targets.size * 1e3) / 1e3;
|
|
2570
|
+
}
|
|
2571
|
+
//#endregion
|
|
2535
2572
|
//#region packages/core/src/extract/main-content.ts
|
|
2536
2573
|
function readableError(error) {
|
|
2537
2574
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -3139,6 +3176,22 @@ function extractStructuredData($, baseUrl, options = {}) {
|
|
|
3139
3176
|
}
|
|
3140
3177
|
//#endregion
|
|
3141
3178
|
//#region packages/core/src/extract/page-extractor.ts
|
|
3179
|
+
function metaRefresh($, finalUrl) {
|
|
3180
|
+
for (const element of $("meta[http-equiv]").toArray()) {
|
|
3181
|
+
if ($(element).attr("http-equiv")?.trim().toLowerCase() !== "refresh") continue;
|
|
3182
|
+
const match = ($(element).attr("content")?.trim())?.match(/^([0-9]+(?:\.[0-9]+)?)\s*;\s*url\s*=\s*(?:"([^"]*)"|'([^']*)'|(.+))$/iu);
|
|
3183
|
+
if (!match) continue;
|
|
3184
|
+
try {
|
|
3185
|
+
const target = new URL((match[2] ?? match[3] ?? match[4] ?? "").trim(), finalUrl);
|
|
3186
|
+
if (!["http:", "https:"].includes(target.protocol)) continue;
|
|
3187
|
+
target.hash = "";
|
|
3188
|
+
return {
|
|
3189
|
+
targetUrl: target.toString(),
|
|
3190
|
+
delaySeconds: Number(match[1])
|
|
3191
|
+
};
|
|
3192
|
+
} catch {}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3142
3195
|
const SOCIAL_PROFILE_HOSTS = [
|
|
3143
3196
|
"facebook.com",
|
|
3144
3197
|
"instagram.com",
|
|
@@ -3149,6 +3202,48 @@ const SOCIAL_PROFILE_HOSTS = [
|
|
|
3149
3202
|
"x.com",
|
|
3150
3203
|
"youtube.com"
|
|
3151
3204
|
];
|
|
3205
|
+
const LOGIN_PATH_PATTERN = /(?:^|\/)(?:auth|login|log-in|signin|sign-in|sso)(?:\/|$)/iu;
|
|
3206
|
+
const LOGIN_TITLE_PATTERN = /\b(?:authenticate|log[ -]?in|sign[ -]?in)\b/iu;
|
|
3207
|
+
function softAuthenticationGate($, finalUrl, title) {
|
|
3208
|
+
const passwordForms = $("form").toArray().filter((form) => $(form).find("input[type=\"password\" i]").length > 0);
|
|
3209
|
+
if (passwordForms.length === 0) return void 0;
|
|
3210
|
+
const pagePath = new URL(finalUrl).pathname;
|
|
3211
|
+
const action = $(passwordForms[0]).attr("action");
|
|
3212
|
+
const actionUrl = action ? httpUrl$1(action, finalUrl) : void 0;
|
|
3213
|
+
const actionPath = actionUrl ? new URL(actionUrl).pathname : void 0;
|
|
3214
|
+
const indicators = [
|
|
3215
|
+
...LOGIN_PATH_PATTERN.test(pagePath) ? ["login-path"] : [],
|
|
3216
|
+
...LOGIN_TITLE_PATTERN.test(title ?? "") ? ["login-title"] : [],
|
|
3217
|
+
...LOGIN_PATH_PATTERN.test(actionPath ?? "") ? ["login-form-action"] : []
|
|
3218
|
+
];
|
|
3219
|
+
if (indicators.length === 0) return void 0;
|
|
3220
|
+
return {
|
|
3221
|
+
kind: "login-form",
|
|
3222
|
+
indicators,
|
|
3223
|
+
...actionPath ? { formActionPath: actionPath } : {}
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
function tabbedContent($) {
|
|
3227
|
+
const groups = $("[role=\"tablist\" i]").length;
|
|
3228
|
+
const panels = $("[role=\"tabpanel\" i]").toArray();
|
|
3229
|
+
if (groups === 0 || panels.length === 0) return void 0;
|
|
3230
|
+
const panelSketches = panels.slice(0, 12).map((panel) => {
|
|
3231
|
+
const labelledBy = $(panel).attr("aria-labelledby");
|
|
3232
|
+
const labelledElement = labelledBy ? $("[id]").toArray().find((element) => $(element).attr("id") === labelledBy) : void 0;
|
|
3233
|
+
const label = safeText$1($(panel).attr("aria-label") ?? (labelledElement ? $(labelledElement).text() : void 0));
|
|
3234
|
+
return {
|
|
3235
|
+
...label ? { label: label.slice(0, 80) } : {},
|
|
3236
|
+
sketch: contentSketch($(panel).text())
|
|
3237
|
+
};
|
|
3238
|
+
});
|
|
3239
|
+
return {
|
|
3240
|
+
groups,
|
|
3241
|
+
panels: panels.length,
|
|
3242
|
+
retainedPanels: panelSketches.length,
|
|
3243
|
+
truncated: panels.length > panelSketches.length,
|
|
3244
|
+
panelSketches
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3152
3247
|
function safeText$1(value) {
|
|
3153
3248
|
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
3154
3249
|
return trimmed ? trimmed : void 0;
|
|
@@ -3375,10 +3470,13 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3375
3470
|
html: sanitizedContentHtml($, fetchResult.html, fetchResult.finalUrl)
|
|
3376
3471
|
}, extractor, dependencies);
|
|
3377
3472
|
const { text, excerpt } = content;
|
|
3473
|
+
const title = safeText$1($("title").first().text());
|
|
3474
|
+
const crawlerMetaRefresh = extractor === "crawler" ? metaRefresh($, fetchResult.finalUrl) : void 0;
|
|
3475
|
+
if (crawlerMetaRefresh && new URL(crawlerMetaRefresh.targetUrl).origin === url.origin) crawlerInternalLinks.add(crawlerMetaRefresh.targetUrl);
|
|
3378
3476
|
return {
|
|
3379
3477
|
url: fetchResult.url,
|
|
3380
3478
|
finalUrl: fetchResult.finalUrl,
|
|
3381
|
-
title
|
|
3479
|
+
title,
|
|
3382
3480
|
metaDescription: safeText$1($("meta[name=\"description\"]").attr("content")),
|
|
3383
3481
|
metaRobots,
|
|
3384
3482
|
xRobotsTag: safeText$1(headerValue$4(fetchResult.headers, "x-robots-tag")),
|
|
@@ -3398,7 +3496,11 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3398
3496
|
externalLinks: [...crawlerExternalLinks],
|
|
3399
3497
|
internalAnchorSamples,
|
|
3400
3498
|
externalAnchorSamples,
|
|
3401
|
-
socialProfileLinks: [...socialProfileLinks]
|
|
3499
|
+
socialProfileLinks: [...socialProfileLinks],
|
|
3500
|
+
contentSketch: contentSketch(text),
|
|
3501
|
+
metaRefresh: crawlerMetaRefresh,
|
|
3502
|
+
softAuthenticationGate: softAuthenticationGate($, fetchResult.finalUrl, title),
|
|
3503
|
+
tabbedContent: tabbedContent($)
|
|
3402
3504
|
} } : {},
|
|
3403
3505
|
markdownAlternates,
|
|
3404
3506
|
hreflang,
|
|
@@ -7631,6 +7733,8 @@ function markdownQuality(markdown, page) {
|
|
|
7631
7733
|
const codeFences = markdown.match(/^```/gmu)?.length ?? 0;
|
|
7632
7734
|
const wordCount = bodyWordCount(markdown);
|
|
7633
7735
|
const sourceWordCount = page.wordCount ?? 0;
|
|
7736
|
+
const evaluatedPanels = (page.tabbedContent?.panelSketches ?? []).map((panel) => contentSketchCoverage(panel.sketch, markdown)).filter((coverage) => coverage !== null);
|
|
7737
|
+
const retainedPanels = evaluatedPanels.filter((coverage) => coverage >= .6).length;
|
|
7634
7738
|
return {
|
|
7635
7739
|
frontmatterTitle: /^---[\s\S]*?^title:\s*.+$/mu.test(markdown),
|
|
7636
7740
|
h1Count: markdown.match(/^#\s+\S.+$/gmu)?.length ?? 0,
|
|
@@ -7647,7 +7751,15 @@ function markdownQuality(markdown, page) {
|
|
|
7647
7751
|
sourceWordCount,
|
|
7648
7752
|
wordRetentionRatio: sourceWordCount > 0 ? Math.round(wordCount / sourceWordCount * 1e3) / 1e3 : null,
|
|
7649
7753
|
introductoryCopyRetained: introProbe ? normalizedMarkdown.includes(introProbe) : null,
|
|
7650
|
-
navigationOnly: bodyWordCount(markdown) < 25
|
|
7754
|
+
navigationOnly: bodyWordCount(markdown) < 25,
|
|
7755
|
+
contentSketchCoverage: contentSketchCoverage(page.contentSketch, markdown),
|
|
7756
|
+
tabbedContent: {
|
|
7757
|
+
detectedPanels: page.tabbedContent?.panels ?? 0,
|
|
7758
|
+
evaluatedPanels: evaluatedPanels.length,
|
|
7759
|
+
retainedPanels,
|
|
7760
|
+
missingPanels: Math.max(0, evaluatedPanels.length - retainedPanels),
|
|
7761
|
+
complete: evaluatedPanels.length === 0 ? null : retainedPanels === evaluatedPanels.length
|
|
7762
|
+
}
|
|
7651
7763
|
};
|
|
7652
7764
|
}
|
|
7653
7765
|
function combinedSignal(timeoutMs, signal) {
|
|
@@ -7701,6 +7813,8 @@ async function fetchRepresentation(input) {
|
|
|
7701
7813
|
status: response.status,
|
|
7702
7814
|
contentType: response.headers.get("content-type") ?? void 0,
|
|
7703
7815
|
bytes: Buffer.byteLength(body),
|
|
7816
|
+
characters: body.length,
|
|
7817
|
+
estimatedTokens: Math.ceil(body.length / 4),
|
|
7704
7818
|
sha256: sha256(body),
|
|
7705
7819
|
canonicalUrl: link.find((entry) => entry.rel.includes("canonical"))?.url,
|
|
7706
7820
|
alternateUrl: link.find((entry) => entry.rel.includes("alternate") && entry.type === "text/markdown")?.url,
|
|
@@ -7759,11 +7873,15 @@ async function inspectMarkdownPage(input) {
|
|
|
7759
7873
|
if (explicit) observation.explicit = explicit.observation;
|
|
7760
7874
|
observation.negotiated = negotiated.observation;
|
|
7761
7875
|
observation.repeated = repeated.observation;
|
|
7762
|
-
|
|
7763
|
-
const
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7876
|
+
const isMarkdown = (result) => result?.observation.status !== void 0 && result.observation.status >= 200 && result.observation.status < 300 && /^\s*text\/markdown\b/iu.test(result.observation.contentType ?? "");
|
|
7877
|
+
const explicitMarkdown = isMarkdown(explicit) ? explicit : void 0;
|
|
7878
|
+
const negotiatedMarkdown = isMarkdown(negotiated) ? negotiated : void 0;
|
|
7879
|
+
const repeatedMarkdown = isMarkdown(repeated) ? repeated : void 0;
|
|
7880
|
+
observation.explicitMatchesNegotiated = explicitMarkdown?.observation.sha256 && negotiatedMarkdown?.observation.sha256 ? explicitMarkdown.observation.sha256 === negotiatedMarkdown.observation.sha256 : null;
|
|
7881
|
+
const primary = explicitMarkdown ?? negotiatedMarkdown;
|
|
7882
|
+
observation.repeatedHashStable = primary?.observation.sha256 && repeatedMarkdown?.observation.sha256 ? primary.observation.sha256 === repeatedMarkdown.observation.sha256 : null;
|
|
7883
|
+
observation.markdownCanonicalMatchesHtml = sameDocument(primary?.observation.canonicalUrl, input.page.finalUrl);
|
|
7884
|
+
if (primary?.body) observation.quality = markdownQuality(primary.body, input.page);
|
|
7767
7885
|
return observation;
|
|
7768
7886
|
}
|
|
7769
7887
|
async function fetchText(input) {
|
|
@@ -8007,17 +8125,16 @@ async function inspectRouteManifest(input) {
|
|
|
8007
8125
|
const declared = Array.isArray(parsed.pages) ? parsed.pages : [];
|
|
8008
8126
|
const declaredHtmlRoutes = declared.map((page) => page.htmlPath).filter((path) => typeof path === "string").sort();
|
|
8009
8127
|
const declaredMarkdownRoutes = declared.map((page) => page.markdownPath).filter((path) => typeof path === "string").sort();
|
|
8010
|
-
const crawledHtmlRoutes = new Set(input.pages.map((page) => new URL(page.finalUrl).pathname.replace(/\/$/u, "") || "/"));
|
|
8011
|
-
const observedMarkdownRoutes = new Set(input.observations.flatMap((page) => page.advertisedUrls.map((value) => new URL(value).pathname)));
|
|
8012
8128
|
return {
|
|
8013
8129
|
url,
|
|
8014
8130
|
status: result.response.status,
|
|
8015
8131
|
valid: Array.isArray(parsed.pages),
|
|
8016
8132
|
declaredHtmlRoutes,
|
|
8017
8133
|
declaredMarkdownRoutes,
|
|
8018
|
-
missingHtmlRoutes:
|
|
8019
|
-
missingMarkdownRoutes:
|
|
8020
|
-
orphanMarkdownRoutes: [
|
|
8134
|
+
missingHtmlRoutes: [],
|
|
8135
|
+
missingMarkdownRoutes: [],
|
|
8136
|
+
orphanMarkdownRoutes: [],
|
|
8137
|
+
comparisonStatus: "partial"
|
|
8021
8138
|
};
|
|
8022
8139
|
} catch (error) {
|
|
8023
8140
|
return {
|
|
@@ -8028,6 +8145,7 @@ async function inspectRouteManifest(input) {
|
|
|
8028
8145
|
missingHtmlRoutes: [],
|
|
8029
8146
|
missingMarkdownRoutes: [],
|
|
8030
8147
|
orphanMarkdownRoutes: [],
|
|
8148
|
+
comparisonStatus: "not-applicable",
|
|
8031
8149
|
error: safeError(error)
|
|
8032
8150
|
};
|
|
8033
8151
|
}
|
|
@@ -8071,9 +8189,7 @@ async function collectAgentDiscovery(input) {
|
|
|
8071
8189
|
}),
|
|
8072
8190
|
inspectRouteManifest({
|
|
8073
8191
|
...input,
|
|
8074
|
-
origin
|
|
8075
|
-
pages,
|
|
8076
|
-
observations
|
|
8192
|
+
origin
|
|
8077
8193
|
}),
|
|
8078
8194
|
inspectAgentEndpoints({
|
|
8079
8195
|
...input,
|
|
@@ -8178,20 +8294,13 @@ async function collectAgentDiscovery(input) {
|
|
|
8178
8294
|
};
|
|
8179
8295
|
}
|
|
8180
8296
|
//#endregion
|
|
8181
|
-
//#region packages/core/src/analyze/crawler/agent-readiness.ts
|
|
8182
|
-
function check$
|
|
8297
|
+
//#region packages/core/src/analyze/crawler/agent-readiness-markdown.ts
|
|
8298
|
+
function check$2(section, input) {
|
|
8183
8299
|
return {
|
|
8184
8300
|
section,
|
|
8185
8301
|
...input
|
|
8186
8302
|
};
|
|
8187
8303
|
}
|
|
8188
|
-
function section$2(id, title, checks) {
|
|
8189
|
-
return {
|
|
8190
|
-
id,
|
|
8191
|
-
title,
|
|
8192
|
-
checks
|
|
8193
|
-
};
|
|
8194
|
-
}
|
|
8195
8304
|
function failedMarkdownUrls(discovery) {
|
|
8196
8305
|
return discovery.markdownAlternates.pages.filter((page) => {
|
|
8197
8306
|
const negotiatedOk = page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "") && page.negotiated.varyAccept;
|
|
@@ -8199,112 +8308,200 @@ function failedMarkdownUrls(discovery) {
|
|
|
8199
8308
|
return !negotiatedOk || !explicitOk || page.repeatedHashStable !== true;
|
|
8200
8309
|
}).map((page) => page.htmlUrl);
|
|
8201
8310
|
}
|
|
8311
|
+
function unstableMarkdownUrls(discovery) {
|
|
8312
|
+
return discovery.markdownAlternates.pages.filter((page) => confirmedMarkdown(page) !== void 0 && page.repeatedHashStable !== true).map((page) => page.htmlUrl);
|
|
8313
|
+
}
|
|
8202
8314
|
function qualityFailureUrls(discovery) {
|
|
8203
8315
|
return discovery.markdownAlternates.pages.filter((page) => {
|
|
8204
8316
|
const quality = page.quality;
|
|
8205
|
-
if (!quality) return
|
|
8206
|
-
return !quality.frontmatterTitle || quality.h1Count !== 1 || !quality.codeFenceBalanced || quality.rawHtmlTags > 0 || quality.rawSvgTags > 0 || quality.rawScriptTags > 0 || quality.rawStyleTags > 0 || quality.navigationOnly || quality.repeatedLines > 0 || quality.wordRetentionRatio !== null && quality.wordRetentionRatio < .4;
|
|
8317
|
+
if (!quality) return false;
|
|
8318
|
+
return !quality.frontmatterTitle || quality.h1Count !== 1 || !quality.codeFenceBalanced || quality.rawHtmlTags > 0 || quality.rawSvgTags > 0 || quality.rawScriptTags > 0 || quality.rawStyleTags > 0 || quality.navigationOnly || quality.repeatedLines > 0 || quality.contentSketchCoverage !== null && quality.contentSketchCoverage !== void 0 && quality.contentSketchCoverage < .6 || quality.tabbedContent?.complete === false || quality.wordRetentionRatio !== null && quality.wordRetentionRatio < .4;
|
|
8207
8319
|
}).map((page) => page.htmlUrl);
|
|
8208
8320
|
}
|
|
8209
|
-
function
|
|
8210
|
-
return
|
|
8211
|
-
"api",
|
|
8212
|
-
"application",
|
|
8213
|
-
"commerce"
|
|
8214
|
-
].map((profile) => check$1("profile", {
|
|
8215
|
-
id: `profile-${profile}`,
|
|
8216
|
-
status: "notApplicable",
|
|
8217
|
-
title: `${profile[0]?.toUpperCase()}${profile.slice(1)} checks are not applicable`,
|
|
8218
|
-
plainEnglish: discovery.profileApplicability[profile].reason,
|
|
8219
|
-
action: "Choose this profile only when the site exposes that capability publicly.",
|
|
8220
|
-
evidence: {
|
|
8221
|
-
profile,
|
|
8222
|
-
selectedProfile: discovery.profile
|
|
8223
|
-
}
|
|
8224
|
-
}));
|
|
8225
|
-
}
|
|
8226
|
-
function summariseChecks(checks) {
|
|
8227
|
-
const count = (status) => checks.filter((item) => item.status === status).length;
|
|
8228
|
-
return {
|
|
8229
|
-
checks: checks.length,
|
|
8230
|
-
passed: count("pass"),
|
|
8231
|
-
warnings: count("warning"),
|
|
8232
|
-
failed: count("fail"),
|
|
8233
|
-
unknown: count("unknown"),
|
|
8234
|
-
information: count("info"),
|
|
8235
|
-
notApplicable: count("notApplicable")
|
|
8236
|
-
};
|
|
8321
|
+
function confirmedMarkdown(page) {
|
|
8322
|
+
for (const response of [page.explicit, page.negotiated]) if (response?.status !== void 0 && response.status >= 200 && response.status < 300 && /^\s*text\/markdown\b/iu.test(response.contentType ?? "")) return response;
|
|
8237
8323
|
}
|
|
8238
8324
|
function markdownChecks(discovery) {
|
|
8239
8325
|
const markdown = discovery.markdownAlternates;
|
|
8240
8326
|
const failedUrls = failedMarkdownUrls(discovery);
|
|
8327
|
+
const unstableUrls = unstableMarkdownUrls(discovery);
|
|
8241
8328
|
const qualityUrls = qualityFailureUrls(discovery);
|
|
8329
|
+
const hasEligibleHtml = markdown.eligibleHtmlPages > 0;
|
|
8330
|
+
const hasEvaluatedMarkdown = markdown.evaluatedPages > 0;
|
|
8242
8331
|
const negotiatedPages = markdown.pages.filter((page) => page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "")).length;
|
|
8243
8332
|
const explicitPages = markdown.pages.filter((page) => page.htmlAlternateUnique).length;
|
|
8244
8333
|
const coverageComplete = markdown.eligibleHtmlPages > 0 && markdown.evaluatedPages === markdown.eligibleHtmlPages;
|
|
8245
8334
|
const negotiationComplete = markdown.eligibleHtmlPages > 0 && negotiatedPages === markdown.eligibleHtmlPages && markdown.pages.every((page) => page.negotiated?.varyAccept) && markdown.pages.every((page) => !page.htmlAlternateUnique || page.explicitMatchesNegotiated === true);
|
|
8246
|
-
const stable = markdown.
|
|
8335
|
+
const stable = markdown.evaluatedPages > 0 && markdown.stableResponses === markdown.evaluatedPages;
|
|
8247
8336
|
const tokenHeaders = markdown.pages.filter((page) => {
|
|
8248
|
-
return (page
|
|
8337
|
+
return confirmedMarkdown(page)?.markdownTokens !== void 0 && (!page.explicit || !page.negotiated || page.explicit.markdownTokens === page.negotiated.markdownTokens);
|
|
8249
8338
|
}).length;
|
|
8339
|
+
const sizedPages = markdown.pages.filter((page) => {
|
|
8340
|
+
return confirmedMarkdown(page)?.characters !== void 0;
|
|
8341
|
+
});
|
|
8342
|
+
const largePages = sizedPages.filter((page) => {
|
|
8343
|
+
return (confirmedMarkdown(page)?.characters ?? 0) > 5e4;
|
|
8344
|
+
});
|
|
8345
|
+
const veryLargePages = sizedPages.filter((page) => {
|
|
8346
|
+
return (confirmedMarkdown(page)?.characters ?? 0) > 1e5;
|
|
8347
|
+
});
|
|
8348
|
+
const maxCharacters = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.characters ?? 0));
|
|
8349
|
+
const maxEstimatedTokens = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.estimatedTokens ?? 0));
|
|
8350
|
+
const parityPages = markdown.pages.filter((page) => page.quality?.contentSketchCoverage !== null && page.quality?.contentSketchCoverage !== void 0);
|
|
8351
|
+
const parityFailures = parityPages.filter((page) => (page.quality?.contentSketchCoverage ?? 0) < .6);
|
|
8352
|
+
const tabbedPages = markdown.pages.filter((page) => (page.quality?.tabbedContent?.detectedPanels ?? 0) > 0);
|
|
8353
|
+
const tabFailures = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === false);
|
|
8354
|
+
const tabUnknown = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === null);
|
|
8250
8355
|
return [
|
|
8251
|
-
check$
|
|
8356
|
+
check$2("representations", {
|
|
8252
8357
|
id: "markdown-coverage",
|
|
8253
8358
|
status: markdown.eligibleHtmlPages === 0 ? "unknown" : coverageComplete ? "pass" : "fail",
|
|
8254
|
-
title: coverageComplete ? "Every successful HTML page has a Markdown representation" : "Markdown representation coverage is incomplete",
|
|
8255
|
-
plainEnglish: `${markdown.evaluatedPages} of ${markdown.eligibleHtmlPages} successful HTML pages returned Markdown. ${explicitPages} advertised an explicit alternative and ${negotiatedPages} supported content negotiation
|
|
8256
|
-
action: coverageComplete ? "Keep the negotiated or explicit Markdown response stable as routes change." : "Return text/markdown through content negotiation or advertise one working Markdown alternative for each public HTML page.",
|
|
8359
|
+
title: coverageComplete ? "Every successful HTML page has a Markdown representation" : !hasEligibleHtml ? "Markdown representation coverage was not evaluated" : "Markdown representation coverage is incomplete",
|
|
8360
|
+
plainEnglish: hasEligibleHtml ? `${markdown.evaluatedPages} of ${markdown.eligibleHtmlPages} successful HTML pages returned Markdown. ${explicitPages} advertised an explicit alternative and ${negotiatedPages} supported content negotiation.` : "No successful HTML page was available, so Markdown representation coverage could not be evaluated.",
|
|
8361
|
+
action: !hasEligibleHtml ? "Resolve the crawl or page-fetch failure, then run this check again." : coverageComplete ? "Keep the negotiated or explicit Markdown response stable as routes change." : "Return text/markdown through content negotiation or advertise one working Markdown alternative for each public HTML page.",
|
|
8257
8362
|
evidence: {
|
|
8258
8363
|
eligibleHtmlPages: markdown.eligibleHtmlPages,
|
|
8259
8364
|
advertisedPages: markdown.advertisedPages,
|
|
8260
8365
|
negotiatedPages,
|
|
8261
8366
|
evaluatedPages: markdown.evaluatedPages
|
|
8262
8367
|
},
|
|
8263
|
-
urls: failedUrls.slice(0, 25)
|
|
8368
|
+
urls: hasEligibleHtml ? failedUrls.slice(0, 25) : []
|
|
8264
8369
|
}),
|
|
8265
|
-
check$
|
|
8370
|
+
check$2("representations", {
|
|
8266
8371
|
id: "markdown-token-estimates",
|
|
8267
|
-
status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "
|
|
8268
|
-
title: tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
|
|
8269
|
-
plainEnglish: `${tokenHeaders} of ${markdown.evaluatedPages} evaluated pages returned X-Markdown-Tokens. Paired explicit and negotiated responses agreed where both existed. The value is an estimate, not an exact model-specific token count
|
|
8270
|
-
action: "
|
|
8372
|
+
status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "info",
|
|
8373
|
+
title: !hasEvaluatedMarkdown ? "Markdown token estimates were not evaluated" : tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
|
|
8374
|
+
plainEnglish: hasEvaluatedMarkdown ? `${tokenHeaders} of ${markdown.evaluatedPages} evaluated pages returned X-Markdown-Tokens. Paired explicit and negotiated responses agreed where both existed. The value is an estimate, not an exact model-specific token count.` : "No confirmed Markdown response was available, so optional server token estimates could not be evaluated.",
|
|
8375
|
+
action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : tokenHeaders === markdown.evaluatedPages ? "Keep the estimate stable when both explicit and negotiated responses exist." : "No change is required. X-Markdown-Tokens is optional; local size estimates are reported separately.",
|
|
8271
8376
|
urls: markdown.pages.filter((page) => {
|
|
8272
|
-
|
|
8377
|
+
const primary = confirmedMarkdown(page);
|
|
8378
|
+
if (!primary) return false;
|
|
8379
|
+
const paired = [page.explicit, page.negotiated].filter((response) => response?.status !== void 0 && response.status >= 200 && response.status < 300 && /^\s*text\/markdown\b/iu.test(response.contentType ?? ""));
|
|
8380
|
+
return primary?.markdownTokens === void 0 || paired.length === 2 && paired[0]?.markdownTokens !== paired[1]?.markdownTokens;
|
|
8273
8381
|
}).map((page) => page.htmlUrl).slice(0, 25)
|
|
8274
8382
|
}),
|
|
8275
|
-
check$
|
|
8383
|
+
check$2("representations", {
|
|
8384
|
+
id: "markdown-size",
|
|
8385
|
+
status: sizedPages.length === 0 ? "unknown" : largePages.length === 0 ? "pass" : "warning",
|
|
8386
|
+
title: sizedPages.length === 0 ? "Markdown size was not measured" : largePages.length === 0 ? "Markdown representations stay within the review threshold" : "Some Markdown representations are large enough to review",
|
|
8387
|
+
plainEnglish: `${sizedPages.length} Markdown responses were measured locally. ${largePages.length} exceeded 50,000 characters and ${veryLargePages.length} exceeded 100,000 characters. These are processing-risk thresholds, not universal model limits.`,
|
|
8388
|
+
action: sizedPages.length === 0 ? "Confirm a successful text/markdown representation, then run this check again." : largePages.length === 0 ? "Keep important content early and retain stable section structure as pages grow." : "Split genuinely separate topics, keep navigation and repeated chrome out of Markdown, and put the most useful content early. Do not remove necessary detail just to hit a threshold.",
|
|
8389
|
+
evidence: {
|
|
8390
|
+
measuredPages: sizedPages.length,
|
|
8391
|
+
reviewThresholdCharacters: 5e4,
|
|
8392
|
+
highRiskThresholdCharacters: 1e5,
|
|
8393
|
+
largePages: largePages.length,
|
|
8394
|
+
veryLargePages: veryLargePages.length,
|
|
8395
|
+
maxCharacters,
|
|
8396
|
+
maxEstimatedTokens,
|
|
8397
|
+
heuristic: true
|
|
8398
|
+
},
|
|
8399
|
+
urls: largePages.map((page) => page.htmlUrl).slice(0, 25)
|
|
8400
|
+
}),
|
|
8401
|
+
check$2("representations", {
|
|
8402
|
+
id: "markdown-content-parity",
|
|
8403
|
+
status: parityPages.length === 0 ? "unknown" : parityFailures.length === 0 ? "pass" : "warning",
|
|
8404
|
+
title: parityPages.length === 0 ? "HTML and Markdown content parity was not measured" : parityFailures.length === 0 ? "Markdown retains sampled content from the HTML document" : "Some Markdown responses may omit important HTML content",
|
|
8405
|
+
plainEnglish: `${parityPages.length} pages were compared using bounded hashes sampled across the main HTML content. ${parityFailures.length} retained less than 60% of that sample. This detects likely omissions without storing full page bodies.`,
|
|
8406
|
+
action: parityPages.length === 0 ? "Confirm a successful text/markdown representation, then run this check again." : parityFailures.length === 0 ? "Keep the shared conversion path covered as templates and components change." : "Compare the affected HTML and Markdown around the missing sections, then fix the shared converter or component serialization.",
|
|
8407
|
+
evidence: {
|
|
8408
|
+
evaluatedPages: parityPages.length,
|
|
8409
|
+
failedPages: parityFailures.length,
|
|
8410
|
+
minimumCoverage: .6,
|
|
8411
|
+
sampling: "bounded-content-shingles"
|
|
8412
|
+
},
|
|
8413
|
+
urls: parityFailures.map((page) => page.htmlUrl).slice(0, 25)
|
|
8414
|
+
}),
|
|
8415
|
+
check$2("representations", {
|
|
8416
|
+
id: "markdown-tab-serialization",
|
|
8417
|
+
status: tabbedPages.length === 0 ? "notApplicable" : tabFailures.length > 0 ? "warning" : tabUnknown.length > 0 ? "unknown" : "pass",
|
|
8418
|
+
title: tabbedPages.length === 0 ? "No accessible tabbed content was detected" : tabFailures.length > 0 ? "Some tab panels are missing from Markdown" : tabUnknown.length > 0 ? "Some tab panels were too small to compare reliably" : "Markdown retains detected tab panels",
|
|
8419
|
+
plainEnglish: tabbedPages.length === 0 ? "The crawled HTML did not contain ARIA tablists with tab panels, so this check does not apply." : `${tabbedPages.length} pages contained accessible tab panels. ${tabFailures.length} did not retain every evaluated panel in Markdown, and ${tabUnknown.length} had no panel large enough for a reliable comparison.`,
|
|
8420
|
+
action: tabFailures.length === 0 ? tabUnknown.length > 0 ? "Review the affected page directly if the short tab labels or values are important to the document." : "Keep every panel serialized in document order even when only one tab is visible by default." : "Serialize all tab panels into the Markdown document with headings instead of exporting only the initially active panel.",
|
|
8421
|
+
evidence: {
|
|
8422
|
+
tabbedPages: tabbedPages.length,
|
|
8423
|
+
failedPages: tabFailures.length,
|
|
8424
|
+
unknownPages: tabUnknown.length
|
|
8425
|
+
},
|
|
8426
|
+
urls: [...tabFailures, ...tabUnknown].map((page) => page.htmlUrl).slice(0, 25)
|
|
8427
|
+
}),
|
|
8428
|
+
check$2("representations", {
|
|
8276
8429
|
id: "markdown-negotiation",
|
|
8277
8430
|
status: markdown.eligibleHtmlPages === 0 ? "unknown" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "pass" : "fail",
|
|
8278
|
-
title: negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
|
|
8279
|
-
plainEnglish: `${negotiatedPages} of ${markdown.eligibleHtmlPages} pages returned negotiated Markdown. ${markdown.exactByteMatches} of ${explicitPages} paired explicit responses matched byte for byte. A request that refuses Markdown ${discovery.contentNegotiation.qZeroHonoured ? "received HTML" : "did not produce confirmed HTML evidence"}
|
|
8280
|
-
action: "Honour Accept q-values, send Vary: Accept, and keep paired explicit and negotiated responses identical when both exist.",
|
|
8431
|
+
title: !hasEligibleHtml ? "Markdown content negotiation was not evaluated" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
|
|
8432
|
+
plainEnglish: hasEligibleHtml ? `${negotiatedPages} of ${markdown.eligibleHtmlPages} pages returned negotiated Markdown. ${markdown.exactByteMatches} of ${explicitPages} paired explicit responses matched byte for byte. A request that refuses Markdown ${discovery.contentNegotiation.qZeroHonoured ? "received HTML" : "did not produce confirmed HTML evidence"}.` : "No successful HTML page was available, so content negotiation could not be evaluated.",
|
|
8433
|
+
action: hasEligibleHtml ? "Honour Accept q-values, send Vary: Accept, and keep paired explicit and negotiated responses identical when both exist." : "Resolve the crawl or page-fetch failure, then run this check again.",
|
|
8281
8434
|
evidence: {
|
|
8282
8435
|
negotiatedPages,
|
|
8283
8436
|
explicitPages,
|
|
8284
8437
|
exactByteMatches: markdown.exactByteMatches,
|
|
8285
8438
|
qZero: discovery.contentNegotiation
|
|
8286
8439
|
},
|
|
8287
|
-
urls: failedUrls.slice(0, 25)
|
|
8440
|
+
urls: hasEligibleHtml ? failedUrls.slice(0, 25) : []
|
|
8288
8441
|
}),
|
|
8289
|
-
check$
|
|
8442
|
+
check$2("representations", {
|
|
8290
8443
|
id: "markdown-determinism",
|
|
8291
|
-
status: markdown.
|
|
8292
|
-
title: stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
|
|
8293
|
-
plainEnglish: `${markdown.stableResponses} of ${markdown.
|
|
8294
|
-
action: "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
|
|
8444
|
+
status: markdown.evaluatedPages === 0 ? "unknown" : stable ? "pass" : "fail",
|
|
8445
|
+
title: !hasEvaluatedMarkdown ? "Markdown response stability was not evaluated" : stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
|
|
8446
|
+
plainEnglish: hasEvaluatedMarkdown ? `${markdown.stableResponses} of ${markdown.evaluatedPages} repeated Markdown responses kept the same SHA-256 digest.` : "No confirmed Markdown response was available for a repeated stability check.",
|
|
8447
|
+
action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : stable ? "Keep repeated Markdown responses byte-stable as templates and data change." : "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
|
|
8295
8448
|
evidence: { stableResponses: markdown.stableResponses },
|
|
8296
|
-
urls:
|
|
8449
|
+
urls: unstableUrls.slice(0, 25)
|
|
8297
8450
|
}),
|
|
8298
|
-
check$
|
|
8451
|
+
check$2("representations", {
|
|
8299
8452
|
id: "markdown-quality",
|
|
8300
8453
|
status: markdown.evaluatedPages === 0 ? "unknown" : qualityUrls.length === 0 ? "pass" : "warning",
|
|
8301
|
-
title: qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
|
|
8302
|
-
plainEnglish: qualityUrls.length === 0 ? "The evaluated alternatives retained one H1, frontmatter titles, balanced code fences, useful copy, and no leaked SVG, script, style, or layout tags." : `${qualityUrls.length} Markdown alternative${qualityUrls.length === 1 ? "" : "s"} lost important structure, looked navigation-only, or leaked presentation markup.`,
|
|
8303
|
-
action: "Open the affected Markdown directly and fix the shared converter or component exclusion rules rather than patching generated files.",
|
|
8454
|
+
title: !hasEvaluatedMarkdown ? "Markdown quality was not evaluated" : qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
|
|
8455
|
+
plainEnglish: !hasEvaluatedMarkdown ? "No confirmed Markdown response was available for structure and extraction checks." : qualityUrls.length === 0 ? "The evaluated alternatives retained one H1, frontmatter titles, balanced code fences, useful copy, and no leaked SVG, script, style, or layout tags." : `${qualityUrls.length} Markdown alternative${qualityUrls.length === 1 ? "" : "s"} lost important structure, looked navigation-only, or leaked presentation markup.`,
|
|
8456
|
+
action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : qualityUrls.length === 0 ? "Keep the shared Markdown conversion path covered as templates and components change." : "Open the affected Markdown directly and fix the shared converter or component exclusion rules rather than patching generated files.",
|
|
8304
8457
|
urls: qualityUrls.slice(0, 25)
|
|
8305
8458
|
})
|
|
8306
8459
|
];
|
|
8307
8460
|
}
|
|
8461
|
+
//#endregion
|
|
8462
|
+
//#region packages/core/src/analyze/crawler/agent-readiness.ts
|
|
8463
|
+
function check$1(section, input) {
|
|
8464
|
+
return {
|
|
8465
|
+
section,
|
|
8466
|
+
...input
|
|
8467
|
+
};
|
|
8468
|
+
}
|
|
8469
|
+
function section$2(id, title, checks) {
|
|
8470
|
+
return {
|
|
8471
|
+
id,
|
|
8472
|
+
title,
|
|
8473
|
+
checks
|
|
8474
|
+
};
|
|
8475
|
+
}
|
|
8476
|
+
function profileChecks(discovery) {
|
|
8477
|
+
return [
|
|
8478
|
+
"api",
|
|
8479
|
+
"application",
|
|
8480
|
+
"commerce"
|
|
8481
|
+
].map((profile) => check$1("profile", {
|
|
8482
|
+
id: `profile-${profile}`,
|
|
8483
|
+
status: "notApplicable",
|
|
8484
|
+
title: `${profile[0]?.toUpperCase()}${profile.slice(1)} checks are not applicable`,
|
|
8485
|
+
plainEnglish: discovery.profileApplicability[profile].reason,
|
|
8486
|
+
action: "Choose this profile only when the site exposes that capability publicly.",
|
|
8487
|
+
evidence: {
|
|
8488
|
+
profile,
|
|
8489
|
+
selectedProfile: discovery.profile
|
|
8490
|
+
}
|
|
8491
|
+
}));
|
|
8492
|
+
}
|
|
8493
|
+
function summariseChecks(checks) {
|
|
8494
|
+
const count = (status) => checks.filter((item) => item.status === status).length;
|
|
8495
|
+
return {
|
|
8496
|
+
checks: checks.length,
|
|
8497
|
+
passed: count("pass"),
|
|
8498
|
+
warnings: count("warning"),
|
|
8499
|
+
failed: count("fail"),
|
|
8500
|
+
unknown: count("unknown"),
|
|
8501
|
+
information: count("info"),
|
|
8502
|
+
notApplicable: count("notApplicable")
|
|
8503
|
+
};
|
|
8504
|
+
}
|
|
8308
8505
|
function contentSignalsCheck(discovery, robotsContentSignals) {
|
|
8309
8506
|
const signals = discovery.contentSignals;
|
|
8310
8507
|
const robotsDeclared = robotsContentSignals ?? [];
|
|
@@ -8405,8 +8602,8 @@ function discoveryChecks(discovery, robotsContentSignals) {
|
|
|
8405
8602
|
check$1("discovery", {
|
|
8406
8603
|
id: "route-manifest",
|
|
8407
8604
|
status: !discovery.routeManifest.valid ? "info" : discovery.routeManifest.missingHtmlRoutes.length === 0 && discovery.routeManifest.missingMarkdownRoutes.length === 0 && discovery.routeManifest.orphanMarkdownRoutes.length === 0 ? "pass" : "warning",
|
|
8408
|
-
title: discovery.routeManifest.valid ? "The public route manifest
|
|
8409
|
-
plainEnglish: discovery.routeManifest.valid ? `The manifest declared ${discovery.routeManifest.declaredHtmlRoutes.length} HTML routes and ${discovery.routeManifest.declaredMarkdownRoutes.length} Markdown routes.
|
|
8605
|
+
title: discovery.routeManifest.valid ? "The public route manifest is valid" : "No public route manifest was available",
|
|
8606
|
+
plainEnglish: discovery.routeManifest.valid ? `The manifest declared ${discovery.routeManifest.declaredHtmlRoutes.length} HTML routes and ${discovery.routeManifest.declaredMarkdownRoutes.length} Markdown routes. This bounded crawl did not treat unvisited routes as missing.` : "A public route manifest is optional. Without one, this crawl cannot prove that no orphan Markdown files were deployed.",
|
|
8410
8607
|
action: "Generate one deterministic route inventory from the build when exact HTML and Markdown parity matters.",
|
|
8411
8608
|
evidence: discovery.routeManifest
|
|
8412
8609
|
})
|
|
@@ -8537,25 +8734,42 @@ function accessChecks(report, discovery) {
|
|
|
8537
8734
|
const blocked = bots.filter((bot) => bot.allowed === false);
|
|
8538
8735
|
const robotsKnown = report.ai?.robotsTxt?.availability === "available" || report.ai?.robotsTxt?.availability === "absent";
|
|
8539
8736
|
const protocol = discovery.protocolVariants;
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8737
|
+
const authenticationGates = report.issues.filter((issue) => issue.ruleId === "soft_authentication_gate");
|
|
8738
|
+
const clientRenderedPages = report.issues.filter((issue) => issue.ruleId === "client_rendered_content");
|
|
8739
|
+
return [
|
|
8740
|
+
check$1("access", {
|
|
8741
|
+
id: "crawler-access",
|
|
8742
|
+
status: !robotsKnown ? "unknown" : blocked.length > 0 ? "warning" : "pass",
|
|
8743
|
+
title: blocked.length ? "robots.txt blocks selected crawler tokens" : "No selected crawler token is blocked at the start URL",
|
|
8744
|
+
plainEnglish: robotsKnown ? `${blocked.length} of ${bots.length} selected crawler tokens were blocked by the start URL robots.txt policy.` : "The robots.txt response was not stable enough to make a crawler-access claim.",
|
|
8745
|
+
action: "Keep blocks intentional. A crawler token being allowed here does not prove that the service will crawl, index, or cite the site.",
|
|
8746
|
+
evidence: {
|
|
8747
|
+
availability: report.ai?.robotsTxt?.availability,
|
|
8748
|
+
blocked,
|
|
8749
|
+
botAccess: bots
|
|
8750
|
+
}
|
|
8751
|
+
}),
|
|
8752
|
+
check$1("access", {
|
|
8753
|
+
id: "public-content-access",
|
|
8754
|
+
status: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "warning" : "pass",
|
|
8755
|
+
title: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "Some public content may be difficult for constrained clients to reach" : "No login gate or empty initial document was detected",
|
|
8756
|
+
plainEnglish: `${authenticationGates.length} crawled pages looked like login gates and ${clientRenderedPages.length} relied on JavaScript to replace an empty or near-empty initial document. These are general site-health observations reused here because they affect agents that do not maintain sessions or run JavaScript.`,
|
|
8757
|
+
action: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "Make intended public content available without a session and include its important document content in the initial HTML or a complete server-generated representation." : "Keep important public content available without a session or client-side rendering requirement.",
|
|
8758
|
+
evidence: {
|
|
8759
|
+
authenticationGates: authenticationGates.length,
|
|
8760
|
+
clientRenderedPages: clientRenderedPages.length
|
|
8761
|
+
},
|
|
8762
|
+
urls: [...authenticationGates, ...clientRenderedPages].map((issue) => issue.url).slice(0, 25)
|
|
8763
|
+
}),
|
|
8764
|
+
check$1("access", {
|
|
8765
|
+
id: "protocol-canonicalization",
|
|
8766
|
+
status: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "pass" : protocol.http.permanentRedirectToHttps === null ? "unknown" : "warning",
|
|
8767
|
+
title: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "HTTP redirects permanently to HTTPS and HTTPS sends HSTS" : "Protocol canonicalization needs attention or could not be proven",
|
|
8768
|
+
plainEnglish: `The HTTP variant ${protocol.http.permanentRedirectToHttps === true ? "redirected permanently to HTTPS" : "did not produce a confirmed permanent HTTPS redirect"}. The HTTPS start page ${protocol.hstsOnStartPage ? "sent HSTS" : "did not provide confirmed HSTS evidence"}.`,
|
|
8769
|
+
action: "Use one permanent HTTP-to-HTTPS redirect and send HSTS only after HTTPS is stable across the site.",
|
|
8770
|
+
evidence: protocol
|
|
8771
|
+
})
|
|
8772
|
+
];
|
|
8559
8773
|
}
|
|
8560
8774
|
function identityChecks(report) {
|
|
8561
8775
|
const types = new Set(report.pages.flatMap((page) => page.schemaTypes ?? []));
|
|
@@ -10088,6 +10302,48 @@ const RULE_DEFINITIONS = [
|
|
|
10088
10302
|
suggestedCommands: ["seo crawl --sitemap-url <url> --health --json"]
|
|
10089
10303
|
}
|
|
10090
10304
|
},
|
|
10305
|
+
{
|
|
10306
|
+
id: "soft_authentication_gate",
|
|
10307
|
+
title: "Page appears to be an authentication gate",
|
|
10308
|
+
category: "response",
|
|
10309
|
+
defaultSeverity: "medium",
|
|
10310
|
+
whyItMatters: "The URL returned a successful HTML response, but the visible document looks like a login gate rather than the intended public page. Users and crawlers may receive a 200 response without the content they expected.",
|
|
10311
|
+
howToFix: "Confirm whether authentication is intentional. Public URLs should return their public content without a login form. Protected URLs should use a clear 401 or 403 response where appropriate and should not appear in public sitemaps or internal crawl paths.",
|
|
10312
|
+
impactIfIgnored: "A login page can be mistaken for the intended content, producing misleading crawl results and weak search or agent access.",
|
|
10313
|
+
howToVerify: "Fetch the affected URL without a session and confirm it returns the intended public page, or a deliberate authentication status for protected content.",
|
|
10314
|
+
agentHints: {
|
|
10315
|
+
evidenceFields: ["page.softAuthenticationGate", "page.finalUrl"],
|
|
10316
|
+
suggestedCommands: ["seo crawl <url> --max-pages 1 --json"]
|
|
10317
|
+
}
|
|
10318
|
+
},
|
|
10319
|
+
{
|
|
10320
|
+
id: "client_rendered_content",
|
|
10321
|
+
title: "Important content requires JavaScript rendering",
|
|
10322
|
+
category: "response",
|
|
10323
|
+
defaultSeverity: "medium",
|
|
10324
|
+
whyItMatters: "The raw HTML contained very little content and the useful document appeared only after JavaScript rendering. Some crawlers, agents, previews, and constrained clients do not run the page JavaScript.",
|
|
10325
|
+
howToFix: "Render the important page content into the initial HTML, or publish a complete server-generated representation such as negotiated Markdown. Keep navigation, headings, canonicals, and indexability signals available without client execution.",
|
|
10326
|
+
impactIfIgnored: "Clients that do not execute JavaScript may see an empty shell or miss important content and links.",
|
|
10327
|
+
howToVerify: "Re-run the crawl and confirm the raw and rendered content observations contain the same important document content.",
|
|
10328
|
+
agentHints: {
|
|
10329
|
+
evidenceFields: ["page.fetchDiagnostics.rendering.documentDifference", "page.usedJs"],
|
|
10330
|
+
suggestedCommands: ["seo crawl <url> --json"]
|
|
10331
|
+
}
|
|
10332
|
+
},
|
|
10333
|
+
{
|
|
10334
|
+
id: "soft_404",
|
|
10335
|
+
title: "Unknown URLs return a successful response",
|
|
10336
|
+
category: "response",
|
|
10337
|
+
defaultSeverity: "high",
|
|
10338
|
+
whyItMatters: "Known-nonexistent URLs returned a successful response or redirected to another successful page. Search engines and users cannot reliably distinguish missing content from a real page.",
|
|
10339
|
+
howToFix: "Return 404 or 410 for missing URLs. If a replacement exists, redirect only the specific retired URL to its closest relevant replacement rather than sending every unknown path to a generic page.",
|
|
10340
|
+
impactIfIgnored: "Missing URLs can remain in crawl queues, waste crawl attention, and create confusing search or monitoring evidence.",
|
|
10341
|
+
howToVerify: "Request several known-nonexistent paths and confirm each returns 404 or 410 without redirecting to a generic successful page.",
|
|
10342
|
+
agentHints: {
|
|
10343
|
+
evidenceFields: ["report.siteChecks.soft404"],
|
|
10344
|
+
suggestedCommands: ["seo crawl <url> --json"]
|
|
10345
|
+
}
|
|
10346
|
+
},
|
|
10091
10347
|
{
|
|
10092
10348
|
id: "client_error",
|
|
10093
10349
|
title: "Client error",
|
|
@@ -10121,7 +10377,8 @@ const RULE_DEFINITIONS = [
|
|
|
10121
10377
|
evidenceFields: [
|
|
10122
10378
|
"page.url",
|
|
10123
10379
|
"page.finalUrl",
|
|
10124
|
-
"page.status"
|
|
10380
|
+
"page.status",
|
|
10381
|
+
"page.metaRefresh"
|
|
10125
10382
|
],
|
|
10126
10383
|
suggestedCommands: ["seo redirect-trace --url <url> --json"]
|
|
10127
10384
|
}
|
|
@@ -10577,6 +10834,8 @@ const RULE_RECOMMENDATIONS = {
|
|
|
10577
10834
|
x_robots_noindex: "review",
|
|
10578
10835
|
robots_blocked: "review",
|
|
10579
10836
|
crawler_access_blocked: "review",
|
|
10837
|
+
soft_authentication_gate: "review",
|
|
10838
|
+
client_rendered_content: "review",
|
|
10580
10839
|
orphan_page: "review",
|
|
10581
10840
|
redirected_url: "review",
|
|
10582
10841
|
slow_response: "review",
|
|
@@ -10616,6 +10875,7 @@ function comparableUrl(value) {
|
|
|
10616
10875
|
}
|
|
10617
10876
|
}
|
|
10618
10877
|
function isRedirectedPage(page) {
|
|
10878
|
+
if (page.metaRefresh) return true;
|
|
10619
10879
|
const requested = comparableUrl(page.url);
|
|
10620
10880
|
const final = comparableUrl(page.finalUrl);
|
|
10621
10881
|
return Boolean(requested && final && requested !== final);
|
|
@@ -10670,6 +10930,28 @@ const WEAK_VALUABLE_INLINKS = 1;
|
|
|
10670
10930
|
const QUERY_COVERAGE_MIN_IMPRESSIONS = 50;
|
|
10671
10931
|
const QUERY_COVERAGE_MIN = .6;
|
|
10672
10932
|
const OVERSIZED_IMAGE_CANDIDATE_LIMIT = 2e3;
|
|
10933
|
+
const EMPTY_INITIAL_HTML_CHARACTERS = 200;
|
|
10934
|
+
const SUBSTANTIVE_RENDERED_CHARACTERS = 500;
|
|
10935
|
+
function clientRenderedContentEvidence(page) {
|
|
10936
|
+
const rendering = page.fetchDiagnostics?.rendering;
|
|
10937
|
+
const difference = rendering?.documentDifference;
|
|
10938
|
+
if (rendering?.status !== "rendered" || !difference?.changed.includes("content")) return;
|
|
10939
|
+
const rawCharacters = difference.raw.content.characters;
|
|
10940
|
+
const renderedCharacters = difference.rendered.content.characters;
|
|
10941
|
+
if (rawCharacters >= EMPTY_INITIAL_HTML_CHARACTERS || renderedCharacters < SUBSTANTIVE_RENDERED_CHARACTERS || renderedCharacters < rawCharacters * 3) return;
|
|
10942
|
+
return {
|
|
10943
|
+
rawCharacters,
|
|
10944
|
+
rawWords: difference.raw.content.wordCount,
|
|
10945
|
+
renderedCharacters,
|
|
10946
|
+
renderedWords: difference.rendered.content.wordCount,
|
|
10947
|
+
changed: difference.changed,
|
|
10948
|
+
thresholds: {
|
|
10949
|
+
emptyInitialHtmlCharacters: EMPTY_INITIAL_HTML_CHARACTERS,
|
|
10950
|
+
substantiveRenderedCharacters: SUBSTANTIVE_RENDERED_CHARACTERS,
|
|
10951
|
+
minimumGrowthRatio: 3
|
|
10952
|
+
}
|
|
10953
|
+
};
|
|
10954
|
+
}
|
|
10673
10955
|
function issue(ruleId, page, detail, evidence) {
|
|
10674
10956
|
const rule = explainRule(ruleId);
|
|
10675
10957
|
if (!rule) throw new Error(`Missing rule guidance for ${ruleId}`);
|
|
@@ -10868,11 +11150,18 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
10868
11150
|
}
|
|
10869
11151
|
if (page.status < 200) continue;
|
|
10870
11152
|
const redirected = isRedirectedPage(page);
|
|
10871
|
-
if (redirected)
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
11153
|
+
if (redirected) {
|
|
11154
|
+
const redirectTarget = page.metaRefresh?.targetUrl ?? page.finalUrl;
|
|
11155
|
+
issues.push(issue("redirected_url", page, `Final URL: ${redirectTarget}`, {
|
|
11156
|
+
requestedUrl: page.url,
|
|
11157
|
+
finalUrl: redirectTarget,
|
|
11158
|
+
status: page.status,
|
|
11159
|
+
...page.metaRefresh ? {
|
|
11160
|
+
redirectType: "meta-refresh",
|
|
11161
|
+
delaySeconds: page.metaRefresh.delaySeconds
|
|
11162
|
+
} : {}
|
|
11163
|
+
}));
|
|
11164
|
+
}
|
|
10876
11165
|
const redirectChain = page.fetchDiagnostics?.redirectChain ?? [];
|
|
10877
11166
|
if (redirectChain.length > 1) issues.push(issue("redirect_chain", page, `${redirectChain.length} hops`, {
|
|
10878
11167
|
redirectChain,
|
|
@@ -10884,6 +11173,9 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
10884
11173
|
}));
|
|
10885
11174
|
if (page.auditScope === "status") continue;
|
|
10886
11175
|
if (redirected) continue;
|
|
11176
|
+
if (page.softAuthenticationGate) issues.push(issue("soft_authentication_gate", page, "A password form and login-specific page signals were observed.", page.softAuthenticationGate));
|
|
11177
|
+
const renderedContent = clientRenderedContentEvidence(page);
|
|
11178
|
+
if (renderedContent) issues.push(issue("client_rendered_content", page, "The raw response was an empty or near-empty shell.", renderedContent));
|
|
10887
11179
|
if (isHtmlPage(page) && (page.sizeBytes ?? 0) > LARGE_HTML_BYTES) issues.push(issue("large_html", page, `${Math.round((page.sizeBytes ?? 0) / 1024)} KB`, {
|
|
10888
11180
|
sizeBytes: page.sizeBytes,
|
|
10889
11181
|
thresholdBytes: LARGE_HTML_BYTES
|
|
@@ -11489,7 +11781,7 @@ function createCrawlReport(input) {
|
|
|
11489
11781
|
const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
|
|
11490
11782
|
const requestEvidenceStatus = input.requestEvidenceStatus ?? (input.requests ? "available" : "unavailable");
|
|
11491
11783
|
if (requestEvidenceStatus === "unavailable" && requests.length > 0) throw new Error("Unavailable request evidence cannot include request observations.");
|
|
11492
|
-
const safeIssues = sanitizeIssues(input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })]);
|
|
11784
|
+
const safeIssues = sanitizeIssues([...input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })], ...input.additionalIssues ?? []]);
|
|
11493
11785
|
const pages = safePagesWithLinks;
|
|
11494
11786
|
return {
|
|
11495
11787
|
id: input.id ?? crawlRunId(),
|
|
@@ -11518,6 +11810,7 @@ function createCrawlReport(input) {
|
|
|
11518
11810
|
...input.ai ? { ai: sanitizeTenantValue(input.ai) } : {},
|
|
11519
11811
|
...input.sitemapDiscovery ? { sitemapDiscovery: sanitizeTenantValue(input.sitemapDiscovery) } : {},
|
|
11520
11812
|
...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
|
|
11813
|
+
...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
|
|
11521
11814
|
...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
|
|
11522
11815
|
warnings: sanitizeMessages(input.warnings).sort(compareText$13),
|
|
11523
11816
|
caveats: sanitizeMessages(input.caveats).sort(compareText$13)
|
|
@@ -11724,7 +12017,7 @@ function latestCrawlReport(site, store = crawlReportStore) {
|
|
|
11724
12017
|
//#region packages/core/src/analyze/crawler/crawl-caveats.ts
|
|
11725
12018
|
function crawlCaveats(input) {
|
|
11726
12019
|
const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
|
|
11727
|
-
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.`);
|
|
12020
|
+
if (input.queueSafetySkippedUrls > 0) caveats.push(input.queueSafetyInventoryCapped ? `Left at least ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. The excluded-URL inventory reached its reporting cap. Increase --max-pages to inspect more.` : `Left ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
|
|
11728
12021
|
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.`);
|
|
11729
12022
|
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.`);
|
|
11730
12023
|
return caveats;
|
|
@@ -12087,6 +12380,75 @@ function observationFromPage(requestedUrl, page) {
|
|
|
12087
12380
|
};
|
|
12088
12381
|
}
|
|
12089
12382
|
//#endregion
|
|
12383
|
+
//#region packages/core/src/analyze/crawler/site-checks.ts
|
|
12384
|
+
const PROBE_COUNT = 2;
|
|
12385
|
+
function probeUrls(startUrl) {
|
|
12386
|
+
const origin = new URL(startUrl).origin;
|
|
12387
|
+
const seed = createHash("sha256").update(origin).digest("hex").slice(0, 12);
|
|
12388
|
+
return Array.from({ length: PROBE_COUNT }, (_, index) => new URL(`/.well-known/seo-audit/not-found-${seed}-${index + 1}`, origin).toString());
|
|
12389
|
+
}
|
|
12390
|
+
function probeObservation(url, result) {
|
|
12391
|
+
const request = result.request;
|
|
12392
|
+
if (!request) return {
|
|
12393
|
+
url,
|
|
12394
|
+
error: result.warning ?? "No response evidence."
|
|
12395
|
+
};
|
|
12396
|
+
if (request.outcome === "response") return {
|
|
12397
|
+
url,
|
|
12398
|
+
status: request.status,
|
|
12399
|
+
finalUrl: request.finalUrl,
|
|
12400
|
+
redirected: request.finalUrl !== url,
|
|
12401
|
+
accessBlocked: Boolean(request.accessBlock)
|
|
12402
|
+
};
|
|
12403
|
+
return {
|
|
12404
|
+
url,
|
|
12405
|
+
error: request.outcome === "failure" ? request.error : `Probe skipped: ${request.reason}`
|
|
12406
|
+
};
|
|
12407
|
+
}
|
|
12408
|
+
async function collectCrawlSiteChecks(input) {
|
|
12409
|
+
const probes = [];
|
|
12410
|
+
for (const url of probeUrls(input.startUrl)) {
|
|
12411
|
+
if (input.signal?.aborted) break;
|
|
12412
|
+
const result = await input.fetchStatusPage(url, {
|
|
12413
|
+
js: "off",
|
|
12414
|
+
refresh: true,
|
|
12415
|
+
timeoutMs: input.timeoutMs,
|
|
12416
|
+
signal: input.signal,
|
|
12417
|
+
writeCache: false,
|
|
12418
|
+
respectRobots: input.respectRobots
|
|
12419
|
+
});
|
|
12420
|
+
probes.push(probeObservation(url, result));
|
|
12421
|
+
}
|
|
12422
|
+
const conclusive = probes.filter((probe) => probe.status !== void 0 && !probe.accessBlocked);
|
|
12423
|
+
const successful = conclusive.filter((probe) => (probe.status ?? 0) >= 200 && (probe.status ?? 0) < 400);
|
|
12424
|
+
const correctMissing = conclusive.filter((probe) => probe.status === 404 || probe.status === 410);
|
|
12425
|
+
const status = conclusive.length !== PROBE_COUNT ? "unknown" : successful.length === PROBE_COUNT ? "warning" : correctMissing.length === PROBE_COUNT ? "pass" : "unknown";
|
|
12426
|
+
const checks = { soft404: {
|
|
12427
|
+
status,
|
|
12428
|
+
probes,
|
|
12429
|
+
probeLimit: PROBE_COUNT,
|
|
12430
|
+
complete: probes.length === PROBE_COUNT
|
|
12431
|
+
} };
|
|
12432
|
+
if (status !== "warning") return {
|
|
12433
|
+
checks,
|
|
12434
|
+
issues: []
|
|
12435
|
+
};
|
|
12436
|
+
const rule = explainRule("soft_404");
|
|
12437
|
+
if (!rule) throw new Error("Missing rule guidance for soft_404");
|
|
12438
|
+
return {
|
|
12439
|
+
checks,
|
|
12440
|
+
issues: [{
|
|
12441
|
+
ruleId: "soft_404",
|
|
12442
|
+
title: rule.title,
|
|
12443
|
+
category: rule.category,
|
|
12444
|
+
severity: rule.defaultSeverity,
|
|
12445
|
+
url: new URL("/", input.startUrl).toString(),
|
|
12446
|
+
detail: `${successful.length} known-nonexistent paths returned a successful response or redirect.`,
|
|
12447
|
+
evidence: checks.soft404
|
|
12448
|
+
}]
|
|
12449
|
+
};
|
|
12450
|
+
}
|
|
12451
|
+
//#endregion
|
|
12090
12452
|
//#region packages/core/src/analyze/crawler/site-crawl-ai-signals.ts
|
|
12091
12453
|
const MAX_AUXILIARY_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
12092
12454
|
const AI_BOT_USER_AGENTS = [
|
|
@@ -12806,6 +13168,10 @@ async function crawlOne(url, opts = {}) {
|
|
|
12806
13168
|
mainContentHash: hashText(extracted.contentText),
|
|
12807
13169
|
textRatio: fetched.html.length ? Math.min(1, extracted.contentText.length / fetched.html.length) : void 0,
|
|
12808
13170
|
contentSample: truncate$1(extracted.contentText, 300),
|
|
13171
|
+
contentSketch: crawlerEvidence.contentSketch,
|
|
13172
|
+
softAuthenticationGate: crawlerEvidence.softAuthenticationGate,
|
|
13173
|
+
metaRefresh: crawlerEvidence.metaRefresh,
|
|
13174
|
+
tabbedContent: crawlerEvidence.tabbedContent,
|
|
12809
13175
|
lang: extracted.lang,
|
|
12810
13176
|
hasViewport: extracted.hasViewport,
|
|
12811
13177
|
isHttps: new URL(extracted.finalUrl).protocol === "https:",
|
|
@@ -12855,9 +13221,9 @@ async function crawlOne(url, opts = {}) {
|
|
|
12855
13221
|
hasQapageSchema: extracted.schemaTypes.includes("QAPage")
|
|
12856
13222
|
}
|
|
12857
13223
|
};
|
|
12858
|
-
page.indexability = indexabilityReason(page);
|
|
13224
|
+
page.indexability = page.metaRefresh ? `Meta refresh to ${page.metaRefresh.targetUrl}` : indexabilityReason(page);
|
|
12859
13225
|
page.indexable = !page.indexability;
|
|
12860
|
-
page.declaredIndexability = hasMetaRobotsDirective(page.metaRobots, "noindex") || hasXRobotsDirective(page.xRobotsTag, "noindex") ? "noindex" : page.robotsTxt?.allowed === false ? "robots-blocked" : page.canonicalStatus === "conflicting" ? "canonical-conflict" : page.canonical && !sameDocumentUrl(page.canonical, page.finalUrl) ? "canonical-hint-other" : "indexable-candidate";
|
|
13226
|
+
page.declaredIndexability = page.metaRefresh ? "meta-refresh" : hasMetaRobotsDirective(page.metaRobots, "noindex") || hasXRobotsDirective(page.xRobotsTag, "noindex") ? "noindex" : page.robotsTxt?.allowed === false ? "robots-blocked" : page.canonicalStatus === "conflicting" ? "canonical-conflict" : page.canonical && !sameDocumentUrl(page.canonical, page.finalUrl) ? "canonical-hint-other" : "indexable-candidate";
|
|
12861
13227
|
return {
|
|
12862
13228
|
request: responseObservation(url, fetched, "complete"),
|
|
12863
13229
|
page,
|
|
@@ -13651,6 +14017,9 @@ async function crawlSite(input, dependencies) {
|
|
|
13651
14017
|
let robotsDeferredUrls = 0;
|
|
13652
14018
|
let originBackpressureSkippedUrls = 0;
|
|
13653
14019
|
let queueSafetySkippedUrls = 0;
|
|
14020
|
+
const queueSafetyDeferredUrls = /* @__PURE__ */ new Set();
|
|
14021
|
+
const maxTrackedQueueSafetyUrls = Math.min(5e4, Math.max(1e3, config.maxPages * 5));
|
|
14022
|
+
let queueSafetyInventoryCapped = false;
|
|
13654
14023
|
let failedUrls = 0;
|
|
13655
14024
|
let observedInternalLinks = 0;
|
|
13656
14025
|
let memoryLimited = false;
|
|
@@ -13755,13 +14124,16 @@ async function crawlSite(input, dependencies) {
|
|
|
13755
14124
|
return;
|
|
13756
14125
|
}
|
|
13757
14126
|
if (queue.size + visited.size + inFlight.size >= config.maxPages * 5) {
|
|
13758
|
-
|
|
13759
|
-
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
14127
|
+
if (!queueSafetyDeferredUrls.has(normalized)) if (queueSafetyDeferredUrls.size < maxTrackedQueueSafetyUrls) {
|
|
14128
|
+
queueSafetyDeferredUrls.add(normalized);
|
|
14129
|
+
queueSafetySkippedUrls = queueSafetyDeferredUrls.size;
|
|
14130
|
+
recordSkip("queue-safety-limit");
|
|
14131
|
+
emitStatus("url_skipped", {
|
|
14132
|
+
url: normalized,
|
|
14133
|
+
depth,
|
|
14134
|
+
reason: "queue_safety_limit"
|
|
14135
|
+
});
|
|
14136
|
+
} else queueSafetyInventoryCapped = true;
|
|
13765
14137
|
return;
|
|
13766
14138
|
}
|
|
13767
14139
|
discovered.add(normalized);
|
|
@@ -14090,6 +14462,13 @@ async function crawlSite(input, dependencies) {
|
|
|
14090
14462
|
signal,
|
|
14091
14463
|
concurrency: Math.min(config.concurrency, 4)
|
|
14092
14464
|
}) : void 0;
|
|
14465
|
+
const siteCheckResult = !healthStrategy && !cancelled && !stoppedForMemory ? await collectCrawlSiteChecks({
|
|
14466
|
+
startUrl: config.url,
|
|
14467
|
+
timeoutMs: config.timeoutMs,
|
|
14468
|
+
fetchStatusPage: deps.fetchStatusPage,
|
|
14469
|
+
respectRobots: config.respectRobots,
|
|
14470
|
+
signal
|
|
14471
|
+
}) : void 0;
|
|
14093
14472
|
for (const page of pages) {
|
|
14094
14473
|
if (!page.geo || !llmsTxt) continue;
|
|
14095
14474
|
page.geo = {
|
|
@@ -14115,6 +14494,10 @@ async function crawlSite(input, dependencies) {
|
|
|
14115
14494
|
} } : {},
|
|
14116
14495
|
...sitemapDiscovery ? { sitemapDiscovery } : {},
|
|
14117
14496
|
...externalLinkVerification ? { externalLinkVerification } : {},
|
|
14497
|
+
...siteCheckResult ? {
|
|
14498
|
+
siteChecks: siteCheckResult.checks,
|
|
14499
|
+
additionalIssues: siteCheckResult.issues
|
|
14500
|
+
} : {},
|
|
14118
14501
|
...agentDiscovery ? { agentDiscovery } : {},
|
|
14119
14502
|
status: reportStatus,
|
|
14120
14503
|
warnings,
|
|
@@ -14123,6 +14506,7 @@ async function crawlSite(input, dependencies) {
|
|
|
14123
14506
|
pageLimitReached,
|
|
14124
14507
|
maxPages: config.maxPages,
|
|
14125
14508
|
queueSafetySkippedUrls,
|
|
14509
|
+
queueSafetyInventoryCapped,
|
|
14126
14510
|
originBackpressureSkippedUrls,
|
|
14127
14511
|
memoryPressureSkippedUrls
|
|
14128
14512
|
}),
|
|
@@ -16407,6 +16791,8 @@ const agentRepresentationResponseSchema = z.object({
|
|
|
16407
16791
|
status: z.number().int().optional(),
|
|
16408
16792
|
contentType: z.string().optional(),
|
|
16409
16793
|
bytes: z.number().int().nonnegative().optional(),
|
|
16794
|
+
characters: z.number().int().nonnegative().optional(),
|
|
16795
|
+
estimatedTokens: z.number().int().nonnegative().optional(),
|
|
16410
16796
|
sha256: z.string().optional(),
|
|
16411
16797
|
canonicalUrl: z.string().url().optional(),
|
|
16412
16798
|
alternateUrl: z.string().url().optional(),
|
|
@@ -16464,7 +16850,15 @@ const agentDiscoverySchema = z.object({
|
|
|
16464
16850
|
sourceWordCount: z.number().int().nonnegative(),
|
|
16465
16851
|
wordRetentionRatio: z.number().nonnegative().nullable(),
|
|
16466
16852
|
introductoryCopyRetained: z.boolean().nullable(),
|
|
16467
|
-
navigationOnly: z.boolean()
|
|
16853
|
+
navigationOnly: z.boolean(),
|
|
16854
|
+
contentSketchCoverage: z.number().min(0).max(1).nullable().optional(),
|
|
16855
|
+
tabbedContent: z.object({
|
|
16856
|
+
detectedPanels: z.number().int().nonnegative(),
|
|
16857
|
+
evaluatedPanels: z.number().int().nonnegative(),
|
|
16858
|
+
retainedPanels: z.number().int().nonnegative(),
|
|
16859
|
+
missingPanels: z.number().int().nonnegative(),
|
|
16860
|
+
complete: z.boolean().nullable()
|
|
16861
|
+
}).optional()
|
|
16468
16862
|
}).optional()
|
|
16469
16863
|
}))
|
|
16470
16864
|
}),
|
|
@@ -16483,6 +16877,11 @@ const agentDiscoverySchema = z.object({
|
|
|
16483
16877
|
missingHtmlRoutes: z.array(z.string()),
|
|
16484
16878
|
missingMarkdownRoutes: z.array(z.string()),
|
|
16485
16879
|
orphanMarkdownRoutes: z.array(z.string()),
|
|
16880
|
+
comparisonStatus: z.enum([
|
|
16881
|
+
"complete",
|
|
16882
|
+
"partial",
|
|
16883
|
+
"not-applicable"
|
|
16884
|
+
]).optional(),
|
|
16486
16885
|
error: z.string().optional()
|
|
16487
16886
|
}),
|
|
16488
16887
|
agentSkills: z.object({
|
|
@@ -16588,6 +16987,25 @@ const agentDiscoverySchema = z.object({
|
|
|
16588
16987
|
warnings: z.array(z.string())
|
|
16589
16988
|
});
|
|
16590
16989
|
//#endregion
|
|
16990
|
+
//#region packages/core/src/analyze/crawler/site-checks-schema.ts
|
|
16991
|
+
const crawlSiteChecksSchema = z.object({ soft404: z.object({
|
|
16992
|
+
status: z.enum([
|
|
16993
|
+
"pass",
|
|
16994
|
+
"warning",
|
|
16995
|
+
"unknown"
|
|
16996
|
+
]),
|
|
16997
|
+
probes: z.array(z.object({
|
|
16998
|
+
url: z.string().url(),
|
|
16999
|
+
status: z.number().int().optional(),
|
|
17000
|
+
finalUrl: z.string().url().optional(),
|
|
17001
|
+
redirected: z.boolean().optional(),
|
|
17002
|
+
accessBlocked: z.boolean().optional(),
|
|
17003
|
+
error: z.string().optional()
|
|
17004
|
+
})),
|
|
17005
|
+
probeLimit: z.number().int().positive(),
|
|
17006
|
+
complete: z.boolean()
|
|
17007
|
+
}) });
|
|
17008
|
+
//#endregion
|
|
16591
17009
|
//#region packages/core/src/analyze/crawler/schemas.ts
|
|
16592
17010
|
const crawlerRuleSeveritySchema = z.enum([
|
|
16593
17011
|
"low",
|
|
@@ -16983,6 +17401,7 @@ const crawlPageSnapshotSchema = z.object({
|
|
|
16983
17401
|
"robots-blocked",
|
|
16984
17402
|
"canonical-conflict",
|
|
16985
17403
|
"canonical-hint-other",
|
|
17404
|
+
"meta-refresh",
|
|
16986
17405
|
"not-html",
|
|
16987
17406
|
"unknown"
|
|
16988
17407
|
]).optional(),
|
|
@@ -17017,6 +17436,38 @@ const crawlPageSnapshotSchema = z.object({
|
|
|
17017
17436
|
mainContentHash: z.string().optional(),
|
|
17018
17437
|
textRatio: z.number().min(0).max(1).optional(),
|
|
17019
17438
|
contentSample: z.string().optional(),
|
|
17439
|
+
contentSketch: z.object({
|
|
17440
|
+
version: z.literal(1),
|
|
17441
|
+
wordCount: z.number().int().nonnegative(),
|
|
17442
|
+
shingleSize: z.number().int().positive(),
|
|
17443
|
+
sampledShingles: z.number().int().nonnegative(),
|
|
17444
|
+
hashes: z.array(z.string())
|
|
17445
|
+
}).optional(),
|
|
17446
|
+
softAuthenticationGate: z.object({
|
|
17447
|
+
kind: z.literal("login-form"),
|
|
17448
|
+
indicators: z.array(z.string()),
|
|
17449
|
+
formActionPath: z.string().optional()
|
|
17450
|
+
}).optional(),
|
|
17451
|
+
metaRefresh: z.object({
|
|
17452
|
+
targetUrl: z.string().url(),
|
|
17453
|
+
delaySeconds: z.number().nonnegative()
|
|
17454
|
+
}).optional(),
|
|
17455
|
+
tabbedContent: z.object({
|
|
17456
|
+
groups: z.number().int().positive(),
|
|
17457
|
+
panels: z.number().int().positive(),
|
|
17458
|
+
retainedPanels: z.number().int().nonnegative(),
|
|
17459
|
+
truncated: z.boolean(),
|
|
17460
|
+
panelSketches: z.array(z.object({
|
|
17461
|
+
label: z.string().optional(),
|
|
17462
|
+
sketch: z.object({
|
|
17463
|
+
version: z.literal(1),
|
|
17464
|
+
wordCount: z.number().int().nonnegative(),
|
|
17465
|
+
shingleSize: z.number().int().positive(),
|
|
17466
|
+
sampledShingles: z.number().int().nonnegative(),
|
|
17467
|
+
hashes: z.array(z.string())
|
|
17468
|
+
})
|
|
17469
|
+
}))
|
|
17470
|
+
}).optional(),
|
|
17020
17471
|
lang: z.string().optional(),
|
|
17021
17472
|
hasViewport: z.boolean().optional(),
|
|
17022
17473
|
isHttps: z.boolean().optional(),
|
|
@@ -17508,6 +17959,7 @@ const crawlReportBaseSchema = z.object({
|
|
|
17508
17959
|
ai: crawlAiSignalsSchema.optional(),
|
|
17509
17960
|
sitemapDiscovery: crawlSitemapDiscoverySchema.optional(),
|
|
17510
17961
|
externalLinkVerification: crawlExternalLinkVerificationSchema.optional(),
|
|
17962
|
+
siteChecks: crawlSiteChecksSchema.optional(),
|
|
17511
17963
|
agentDiscovery: agentDiscoverySchema.optional(),
|
|
17512
17964
|
warnings: z.array(z.string()),
|
|
17513
17965
|
caveats: z.array(z.string())
|