seo 0.2.12 → 0.2.13
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-BB0yH7qK.js} +1 -1
- package/dist/{dist-DaHrxVkW.js → dist-Dwdogl4j.js} +71 -71
- package/dist/dist-Dwdogl4j.js.map +1 -0
- package/dist/index.d.ts +800 -11
- package/dist/index.js +493 -91
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +493 -91
- package/dist/mcp.js.map +1 -1
- package/dist/{package-B-M-4PZ_.js → package-B_swz2Xd.js} +2 -2
- package/dist/{package-B-M-4PZ_.js.map → package-B_swz2Xd.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-B_swz2Xd.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);
|
|
@@ -3149,6 +3186,48 @@ const SOCIAL_PROFILE_HOSTS = [
|
|
|
3149
3186
|
"x.com",
|
|
3150
3187
|
"youtube.com"
|
|
3151
3188
|
];
|
|
3189
|
+
const LOGIN_PATH_PATTERN = /(?:^|\/)(?:auth|login|log-in|signin|sign-in|sso)(?:\/|$)/iu;
|
|
3190
|
+
const LOGIN_TITLE_PATTERN = /\b(?:authenticate|log[ -]?in|sign[ -]?in)\b/iu;
|
|
3191
|
+
function softAuthenticationGate($, finalUrl, title) {
|
|
3192
|
+
const passwordForms = $("form").toArray().filter((form) => $(form).find("input[type=\"password\" i]").length > 0);
|
|
3193
|
+
if (passwordForms.length === 0) return void 0;
|
|
3194
|
+
const pagePath = new URL(finalUrl).pathname;
|
|
3195
|
+
const action = $(passwordForms[0]).attr("action");
|
|
3196
|
+
const actionUrl = action ? httpUrl$1(action, finalUrl) : void 0;
|
|
3197
|
+
const actionPath = actionUrl ? new URL(actionUrl).pathname : void 0;
|
|
3198
|
+
const indicators = [
|
|
3199
|
+
...LOGIN_PATH_PATTERN.test(pagePath) ? ["login-path"] : [],
|
|
3200
|
+
...LOGIN_TITLE_PATTERN.test(title ?? "") ? ["login-title"] : [],
|
|
3201
|
+
...LOGIN_PATH_PATTERN.test(actionPath ?? "") ? ["login-form-action"] : []
|
|
3202
|
+
];
|
|
3203
|
+
if (indicators.length === 0) return void 0;
|
|
3204
|
+
return {
|
|
3205
|
+
kind: "login-form",
|
|
3206
|
+
indicators,
|
|
3207
|
+
...actionPath ? { formActionPath: actionPath } : {}
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
function tabbedContent($) {
|
|
3211
|
+
const groups = $("[role=\"tablist\" i]").length;
|
|
3212
|
+
const panels = $("[role=\"tabpanel\" i]").toArray();
|
|
3213
|
+
if (groups === 0 || panels.length === 0) return void 0;
|
|
3214
|
+
const panelSketches = panels.slice(0, 12).map((panel) => {
|
|
3215
|
+
const labelledBy = $(panel).attr("aria-labelledby");
|
|
3216
|
+
const labelledElement = labelledBy ? $("[id]").toArray().find((element) => $(element).attr("id") === labelledBy) : void 0;
|
|
3217
|
+
const label = safeText$1($(panel).attr("aria-label") ?? (labelledElement ? $(labelledElement).text() : void 0));
|
|
3218
|
+
return {
|
|
3219
|
+
...label ? { label: label.slice(0, 80) } : {},
|
|
3220
|
+
sketch: contentSketch($(panel).text())
|
|
3221
|
+
};
|
|
3222
|
+
});
|
|
3223
|
+
return {
|
|
3224
|
+
groups,
|
|
3225
|
+
panels: panels.length,
|
|
3226
|
+
retainedPanels: panelSketches.length,
|
|
3227
|
+
truncated: panels.length > panelSketches.length,
|
|
3228
|
+
panelSketches
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3152
3231
|
function safeText$1(value) {
|
|
3153
3232
|
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
3154
3233
|
return trimmed ? trimmed : void 0;
|
|
@@ -3375,10 +3454,11 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3375
3454
|
html: sanitizedContentHtml($, fetchResult.html, fetchResult.finalUrl)
|
|
3376
3455
|
}, extractor, dependencies);
|
|
3377
3456
|
const { text, excerpt } = content;
|
|
3457
|
+
const title = safeText$1($("title").first().text());
|
|
3378
3458
|
return {
|
|
3379
3459
|
url: fetchResult.url,
|
|
3380
3460
|
finalUrl: fetchResult.finalUrl,
|
|
3381
|
-
title
|
|
3461
|
+
title,
|
|
3382
3462
|
metaDescription: safeText$1($("meta[name=\"description\"]").attr("content")),
|
|
3383
3463
|
metaRobots,
|
|
3384
3464
|
xRobotsTag: safeText$1(headerValue$4(fetchResult.headers, "x-robots-tag")),
|
|
@@ -3398,7 +3478,10 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3398
3478
|
externalLinks: [...crawlerExternalLinks],
|
|
3399
3479
|
internalAnchorSamples,
|
|
3400
3480
|
externalAnchorSamples,
|
|
3401
|
-
socialProfileLinks: [...socialProfileLinks]
|
|
3481
|
+
socialProfileLinks: [...socialProfileLinks],
|
|
3482
|
+
contentSketch: contentSketch(text),
|
|
3483
|
+
softAuthenticationGate: softAuthenticationGate($, fetchResult.finalUrl, title),
|
|
3484
|
+
tabbedContent: tabbedContent($)
|
|
3402
3485
|
} } : {},
|
|
3403
3486
|
markdownAlternates,
|
|
3404
3487
|
hreflang,
|
|
@@ -7631,6 +7714,8 @@ function markdownQuality(markdown, page) {
|
|
|
7631
7714
|
const codeFences = markdown.match(/^```/gmu)?.length ?? 0;
|
|
7632
7715
|
const wordCount = bodyWordCount(markdown);
|
|
7633
7716
|
const sourceWordCount = page.wordCount ?? 0;
|
|
7717
|
+
const evaluatedPanels = (page.tabbedContent?.panelSketches ?? []).map((panel) => contentSketchCoverage(panel.sketch, markdown)).filter((coverage) => coverage !== null);
|
|
7718
|
+
const retainedPanels = evaluatedPanels.filter((coverage) => coverage >= .6).length;
|
|
7634
7719
|
return {
|
|
7635
7720
|
frontmatterTitle: /^---[\s\S]*?^title:\s*.+$/mu.test(markdown),
|
|
7636
7721
|
h1Count: markdown.match(/^#\s+\S.+$/gmu)?.length ?? 0,
|
|
@@ -7647,7 +7732,15 @@ function markdownQuality(markdown, page) {
|
|
|
7647
7732
|
sourceWordCount,
|
|
7648
7733
|
wordRetentionRatio: sourceWordCount > 0 ? Math.round(wordCount / sourceWordCount * 1e3) / 1e3 : null,
|
|
7649
7734
|
introductoryCopyRetained: introProbe ? normalizedMarkdown.includes(introProbe) : null,
|
|
7650
|
-
navigationOnly: bodyWordCount(markdown) < 25
|
|
7735
|
+
navigationOnly: bodyWordCount(markdown) < 25,
|
|
7736
|
+
contentSketchCoverage: contentSketchCoverage(page.contentSketch, markdown),
|
|
7737
|
+
tabbedContent: {
|
|
7738
|
+
detectedPanels: page.tabbedContent?.panels ?? 0,
|
|
7739
|
+
evaluatedPanels: evaluatedPanels.length,
|
|
7740
|
+
retainedPanels,
|
|
7741
|
+
missingPanels: Math.max(0, evaluatedPanels.length - retainedPanels),
|
|
7742
|
+
complete: evaluatedPanels.length === 0 ? null : retainedPanels === evaluatedPanels.length
|
|
7743
|
+
}
|
|
7651
7744
|
};
|
|
7652
7745
|
}
|
|
7653
7746
|
function combinedSignal(timeoutMs, signal) {
|
|
@@ -7701,6 +7794,8 @@ async function fetchRepresentation(input) {
|
|
|
7701
7794
|
status: response.status,
|
|
7702
7795
|
contentType: response.headers.get("content-type") ?? void 0,
|
|
7703
7796
|
bytes: Buffer.byteLength(body),
|
|
7797
|
+
characters: body.length,
|
|
7798
|
+
estimatedTokens: Math.ceil(body.length / 4),
|
|
7704
7799
|
sha256: sha256(body),
|
|
7705
7800
|
canonicalUrl: link.find((entry) => entry.rel.includes("canonical"))?.url,
|
|
7706
7801
|
alternateUrl: link.find((entry) => entry.rel.includes("alternate") && entry.type === "text/markdown")?.url,
|
|
@@ -7759,11 +7854,15 @@ async function inspectMarkdownPage(input) {
|
|
|
7759
7854
|
if (explicit) observation.explicit = explicit.observation;
|
|
7760
7855
|
observation.negotiated = negotiated.observation;
|
|
7761
7856
|
observation.repeated = repeated.observation;
|
|
7762
|
-
|
|
7763
|
-
const
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7857
|
+
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 ?? "");
|
|
7858
|
+
const explicitMarkdown = isMarkdown(explicit) ? explicit : void 0;
|
|
7859
|
+
const negotiatedMarkdown = isMarkdown(negotiated) ? negotiated : void 0;
|
|
7860
|
+
const repeatedMarkdown = isMarkdown(repeated) ? repeated : void 0;
|
|
7861
|
+
observation.explicitMatchesNegotiated = explicitMarkdown?.observation.sha256 && negotiatedMarkdown?.observation.sha256 ? explicitMarkdown.observation.sha256 === negotiatedMarkdown.observation.sha256 : null;
|
|
7862
|
+
const primary = explicitMarkdown ?? negotiatedMarkdown;
|
|
7863
|
+
observation.repeatedHashStable = primary?.observation.sha256 && repeatedMarkdown?.observation.sha256 ? primary.observation.sha256 === repeatedMarkdown.observation.sha256 : null;
|
|
7864
|
+
observation.markdownCanonicalMatchesHtml = sameDocument(primary?.observation.canonicalUrl, input.page.finalUrl);
|
|
7865
|
+
if (primary?.body) observation.quality = markdownQuality(primary.body, input.page);
|
|
7767
7866
|
return observation;
|
|
7768
7867
|
}
|
|
7769
7868
|
async function fetchText(input) {
|
|
@@ -8007,17 +8106,16 @@ async function inspectRouteManifest(input) {
|
|
|
8007
8106
|
const declared = Array.isArray(parsed.pages) ? parsed.pages : [];
|
|
8008
8107
|
const declaredHtmlRoutes = declared.map((page) => page.htmlPath).filter((path) => typeof path === "string").sort();
|
|
8009
8108
|
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
8109
|
return {
|
|
8013
8110
|
url,
|
|
8014
8111
|
status: result.response.status,
|
|
8015
8112
|
valid: Array.isArray(parsed.pages),
|
|
8016
8113
|
declaredHtmlRoutes,
|
|
8017
8114
|
declaredMarkdownRoutes,
|
|
8018
|
-
missingHtmlRoutes:
|
|
8019
|
-
missingMarkdownRoutes:
|
|
8020
|
-
orphanMarkdownRoutes: [
|
|
8115
|
+
missingHtmlRoutes: [],
|
|
8116
|
+
missingMarkdownRoutes: [],
|
|
8117
|
+
orphanMarkdownRoutes: [],
|
|
8118
|
+
comparisonStatus: "partial"
|
|
8021
8119
|
};
|
|
8022
8120
|
} catch (error) {
|
|
8023
8121
|
return {
|
|
@@ -8028,6 +8126,7 @@ async function inspectRouteManifest(input) {
|
|
|
8028
8126
|
missingHtmlRoutes: [],
|
|
8029
8127
|
missingMarkdownRoutes: [],
|
|
8030
8128
|
orphanMarkdownRoutes: [],
|
|
8129
|
+
comparisonStatus: "not-applicable",
|
|
8031
8130
|
error: safeError(error)
|
|
8032
8131
|
};
|
|
8033
8132
|
}
|
|
@@ -8071,9 +8170,7 @@ async function collectAgentDiscovery(input) {
|
|
|
8071
8170
|
}),
|
|
8072
8171
|
inspectRouteManifest({
|
|
8073
8172
|
...input,
|
|
8074
|
-
origin
|
|
8075
|
-
pages,
|
|
8076
|
-
observations
|
|
8173
|
+
origin
|
|
8077
8174
|
}),
|
|
8078
8175
|
inspectAgentEndpoints({
|
|
8079
8176
|
...input,
|
|
@@ -8178,20 +8275,13 @@ async function collectAgentDiscovery(input) {
|
|
|
8178
8275
|
};
|
|
8179
8276
|
}
|
|
8180
8277
|
//#endregion
|
|
8181
|
-
//#region packages/core/src/analyze/crawler/agent-readiness.ts
|
|
8182
|
-
function check$
|
|
8278
|
+
//#region packages/core/src/analyze/crawler/agent-readiness-markdown.ts
|
|
8279
|
+
function check$2(section, input) {
|
|
8183
8280
|
return {
|
|
8184
8281
|
section,
|
|
8185
8282
|
...input
|
|
8186
8283
|
};
|
|
8187
8284
|
}
|
|
8188
|
-
function section$2(id, title, checks) {
|
|
8189
|
-
return {
|
|
8190
|
-
id,
|
|
8191
|
-
title,
|
|
8192
|
-
checks
|
|
8193
|
-
};
|
|
8194
|
-
}
|
|
8195
8285
|
function failedMarkdownUrls(discovery) {
|
|
8196
8286
|
return discovery.markdownAlternates.pages.filter((page) => {
|
|
8197
8287
|
const negotiatedOk = page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "") && page.negotiated.varyAccept;
|
|
@@ -8202,38 +8292,12 @@ function failedMarkdownUrls(discovery) {
|
|
|
8202
8292
|
function qualityFailureUrls(discovery) {
|
|
8203
8293
|
return discovery.markdownAlternates.pages.filter((page) => {
|
|
8204
8294
|
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;
|
|
8295
|
+
if (!quality) return false;
|
|
8296
|
+
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
8297
|
}).map((page) => page.htmlUrl);
|
|
8208
8298
|
}
|
|
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
|
-
};
|
|
8299
|
+
function confirmedMarkdown(page) {
|
|
8300
|
+
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
8301
|
}
|
|
8238
8302
|
function markdownChecks(discovery) {
|
|
8239
8303
|
const markdown = discovery.markdownAlternates;
|
|
@@ -8243,12 +8307,28 @@ function markdownChecks(discovery) {
|
|
|
8243
8307
|
const explicitPages = markdown.pages.filter((page) => page.htmlAlternateUnique).length;
|
|
8244
8308
|
const coverageComplete = markdown.eligibleHtmlPages > 0 && markdown.evaluatedPages === markdown.eligibleHtmlPages;
|
|
8245
8309
|
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.
|
|
8310
|
+
const stable = markdown.evaluatedPages > 0 && markdown.stableResponses === markdown.evaluatedPages;
|
|
8247
8311
|
const tokenHeaders = markdown.pages.filter((page) => {
|
|
8248
|
-
return (page
|
|
8312
|
+
return confirmedMarkdown(page)?.markdownTokens !== void 0 && (!page.explicit || !page.negotiated || page.explicit.markdownTokens === page.negotiated.markdownTokens);
|
|
8249
8313
|
}).length;
|
|
8314
|
+
const sizedPages = markdown.pages.filter((page) => {
|
|
8315
|
+
return confirmedMarkdown(page)?.characters !== void 0;
|
|
8316
|
+
});
|
|
8317
|
+
const largePages = sizedPages.filter((page) => {
|
|
8318
|
+
return (confirmedMarkdown(page)?.characters ?? 0) > 5e4;
|
|
8319
|
+
});
|
|
8320
|
+
const veryLargePages = sizedPages.filter((page) => {
|
|
8321
|
+
return (confirmedMarkdown(page)?.characters ?? 0) > 1e5;
|
|
8322
|
+
});
|
|
8323
|
+
const maxCharacters = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.characters ?? 0));
|
|
8324
|
+
const maxEstimatedTokens = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.estimatedTokens ?? 0));
|
|
8325
|
+
const parityPages = markdown.pages.filter((page) => page.quality?.contentSketchCoverage !== null && page.quality?.contentSketchCoverage !== void 0);
|
|
8326
|
+
const parityFailures = parityPages.filter((page) => (page.quality?.contentSketchCoverage ?? 0) < .6);
|
|
8327
|
+
const tabbedPages = markdown.pages.filter((page) => (page.quality?.tabbedContent?.detectedPanels ?? 0) > 0);
|
|
8328
|
+
const tabFailures = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === false);
|
|
8329
|
+
const tabUnknown = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === null);
|
|
8250
8330
|
return [
|
|
8251
|
-
check$
|
|
8331
|
+
check$2("representations", {
|
|
8252
8332
|
id: "markdown-coverage",
|
|
8253
8333
|
status: markdown.eligibleHtmlPages === 0 ? "unknown" : coverageComplete ? "pass" : "fail",
|
|
8254
8334
|
title: coverageComplete ? "Every successful HTML page has a Markdown representation" : "Markdown representation coverage is incomplete",
|
|
@@ -8262,17 +8342,62 @@ function markdownChecks(discovery) {
|
|
|
8262
8342
|
},
|
|
8263
8343
|
urls: failedUrls.slice(0, 25)
|
|
8264
8344
|
}),
|
|
8265
|
-
check$
|
|
8345
|
+
check$2("representations", {
|
|
8266
8346
|
id: "markdown-token-estimates",
|
|
8267
|
-
status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "
|
|
8347
|
+
status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "info",
|
|
8268
8348
|
title: tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
|
|
8269
8349
|
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:
|
|
8350
|
+
action: 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
8351
|
urls: markdown.pages.filter((page) => {
|
|
8272
|
-
return (page
|
|
8352
|
+
return confirmedMarkdown(page)?.markdownTokens === void 0 || page.explicit !== void 0 && page.negotiated !== void 0 && page.explicit.markdownTokens !== page.negotiated.markdownTokens;
|
|
8273
8353
|
}).map((page) => page.htmlUrl).slice(0, 25)
|
|
8274
8354
|
}),
|
|
8275
|
-
check$
|
|
8355
|
+
check$2("representations", {
|
|
8356
|
+
id: "markdown-size",
|
|
8357
|
+
status: sizedPages.length === 0 ? "unknown" : largePages.length === 0 ? "pass" : "warning",
|
|
8358
|
+
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",
|
|
8359
|
+
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.`,
|
|
8360
|
+
action: 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.",
|
|
8361
|
+
evidence: {
|
|
8362
|
+
measuredPages: sizedPages.length,
|
|
8363
|
+
reviewThresholdCharacters: 5e4,
|
|
8364
|
+
highRiskThresholdCharacters: 1e5,
|
|
8365
|
+
largePages: largePages.length,
|
|
8366
|
+
veryLargePages: veryLargePages.length,
|
|
8367
|
+
maxCharacters,
|
|
8368
|
+
maxEstimatedTokens,
|
|
8369
|
+
heuristic: true
|
|
8370
|
+
},
|
|
8371
|
+
urls: largePages.map((page) => page.htmlUrl).slice(0, 25)
|
|
8372
|
+
}),
|
|
8373
|
+
check$2("representations", {
|
|
8374
|
+
id: "markdown-content-parity",
|
|
8375
|
+
status: parityPages.length === 0 ? "unknown" : parityFailures.length === 0 ? "pass" : "warning",
|
|
8376
|
+
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",
|
|
8377
|
+
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.`,
|
|
8378
|
+
action: 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.",
|
|
8379
|
+
evidence: {
|
|
8380
|
+
evaluatedPages: parityPages.length,
|
|
8381
|
+
failedPages: parityFailures.length,
|
|
8382
|
+
minimumCoverage: .6,
|
|
8383
|
+
sampling: "bounded-content-shingles"
|
|
8384
|
+
},
|
|
8385
|
+
urls: parityFailures.map((page) => page.htmlUrl).slice(0, 25)
|
|
8386
|
+
}),
|
|
8387
|
+
check$2("representations", {
|
|
8388
|
+
id: "markdown-tab-serialization",
|
|
8389
|
+
status: tabbedPages.length === 0 ? "notApplicable" : tabFailures.length > 0 ? "warning" : tabUnknown.length > 0 ? "unknown" : "pass",
|
|
8390
|
+
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",
|
|
8391
|
+
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.`,
|
|
8392
|
+
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.",
|
|
8393
|
+
evidence: {
|
|
8394
|
+
tabbedPages: tabbedPages.length,
|
|
8395
|
+
failedPages: tabFailures.length,
|
|
8396
|
+
unknownPages: tabUnknown.length
|
|
8397
|
+
},
|
|
8398
|
+
urls: [...tabFailures, ...tabUnknown].map((page) => page.htmlUrl).slice(0, 25)
|
|
8399
|
+
}),
|
|
8400
|
+
check$2("representations", {
|
|
8276
8401
|
id: "markdown-negotiation",
|
|
8277
8402
|
status: markdown.eligibleHtmlPages === 0 ? "unknown" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "pass" : "fail",
|
|
8278
8403
|
title: negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
|
|
@@ -8286,16 +8411,16 @@ function markdownChecks(discovery) {
|
|
|
8286
8411
|
},
|
|
8287
8412
|
urls: failedUrls.slice(0, 25)
|
|
8288
8413
|
}),
|
|
8289
|
-
check$
|
|
8414
|
+
check$2("representations", {
|
|
8290
8415
|
id: "markdown-determinism",
|
|
8291
|
-
status: markdown.
|
|
8416
|
+
status: markdown.evaluatedPages === 0 ? "unknown" : stable ? "pass" : "fail",
|
|
8292
8417
|
title: stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
|
|
8293
|
-
plainEnglish: `${markdown.stableResponses} of ${markdown.
|
|
8418
|
+
plainEnglish: `${markdown.stableResponses} of ${markdown.evaluatedPages} repeated Markdown responses kept the same SHA-256 digest.`,
|
|
8294
8419
|
action: "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
|
|
8295
8420
|
evidence: { stableResponses: markdown.stableResponses },
|
|
8296
8421
|
urls: failedUrls.slice(0, 25)
|
|
8297
8422
|
}),
|
|
8298
|
-
check$
|
|
8423
|
+
check$2("representations", {
|
|
8299
8424
|
id: "markdown-quality",
|
|
8300
8425
|
status: markdown.evaluatedPages === 0 ? "unknown" : qualityUrls.length === 0 ? "pass" : "warning",
|
|
8301
8426
|
title: qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
|
|
@@ -8305,6 +8430,50 @@ function markdownChecks(discovery) {
|
|
|
8305
8430
|
})
|
|
8306
8431
|
];
|
|
8307
8432
|
}
|
|
8433
|
+
//#endregion
|
|
8434
|
+
//#region packages/core/src/analyze/crawler/agent-readiness.ts
|
|
8435
|
+
function check$1(section, input) {
|
|
8436
|
+
return {
|
|
8437
|
+
section,
|
|
8438
|
+
...input
|
|
8439
|
+
};
|
|
8440
|
+
}
|
|
8441
|
+
function section$2(id, title, checks) {
|
|
8442
|
+
return {
|
|
8443
|
+
id,
|
|
8444
|
+
title,
|
|
8445
|
+
checks
|
|
8446
|
+
};
|
|
8447
|
+
}
|
|
8448
|
+
function profileChecks(discovery) {
|
|
8449
|
+
return [
|
|
8450
|
+
"api",
|
|
8451
|
+
"application",
|
|
8452
|
+
"commerce"
|
|
8453
|
+
].map((profile) => check$1("profile", {
|
|
8454
|
+
id: `profile-${profile}`,
|
|
8455
|
+
status: "notApplicable",
|
|
8456
|
+
title: `${profile[0]?.toUpperCase()}${profile.slice(1)} checks are not applicable`,
|
|
8457
|
+
plainEnglish: discovery.profileApplicability[profile].reason,
|
|
8458
|
+
action: "Choose this profile only when the site exposes that capability publicly.",
|
|
8459
|
+
evidence: {
|
|
8460
|
+
profile,
|
|
8461
|
+
selectedProfile: discovery.profile
|
|
8462
|
+
}
|
|
8463
|
+
}));
|
|
8464
|
+
}
|
|
8465
|
+
function summariseChecks(checks) {
|
|
8466
|
+
const count = (status) => checks.filter((item) => item.status === status).length;
|
|
8467
|
+
return {
|
|
8468
|
+
checks: checks.length,
|
|
8469
|
+
passed: count("pass"),
|
|
8470
|
+
warnings: count("warning"),
|
|
8471
|
+
failed: count("fail"),
|
|
8472
|
+
unknown: count("unknown"),
|
|
8473
|
+
information: count("info"),
|
|
8474
|
+
notApplicable: count("notApplicable")
|
|
8475
|
+
};
|
|
8476
|
+
}
|
|
8308
8477
|
function contentSignalsCheck(discovery, robotsContentSignals) {
|
|
8309
8478
|
const signals = discovery.contentSignals;
|
|
8310
8479
|
const robotsDeclared = robotsContentSignals ?? [];
|
|
@@ -8405,8 +8574,8 @@ function discoveryChecks(discovery, robotsContentSignals) {
|
|
|
8405
8574
|
check$1("discovery", {
|
|
8406
8575
|
id: "route-manifest",
|
|
8407
8576
|
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.
|
|
8577
|
+
title: discovery.routeManifest.valid ? "The public route manifest is valid" : "No public route manifest was available",
|
|
8578
|
+
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
8579
|
action: "Generate one deterministic route inventory from the build when exact HTML and Markdown parity matters.",
|
|
8411
8580
|
evidence: discovery.routeManifest
|
|
8412
8581
|
})
|
|
@@ -8537,25 +8706,42 @@ function accessChecks(report, discovery) {
|
|
|
8537
8706
|
const blocked = bots.filter((bot) => bot.allowed === false);
|
|
8538
8707
|
const robotsKnown = report.ai?.robotsTxt?.availability === "available" || report.ai?.robotsTxt?.availability === "absent";
|
|
8539
8708
|
const protocol = discovery.protocolVariants;
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8709
|
+
const authenticationGates = report.issues.filter((issue) => issue.ruleId === "soft_authentication_gate");
|
|
8710
|
+
const clientRenderedPages = report.issues.filter((issue) => issue.ruleId === "client_rendered_content");
|
|
8711
|
+
return [
|
|
8712
|
+
check$1("access", {
|
|
8713
|
+
id: "crawler-access",
|
|
8714
|
+
status: !robotsKnown ? "unknown" : blocked.length > 0 ? "warning" : "pass",
|
|
8715
|
+
title: blocked.length ? "robots.txt blocks selected crawler tokens" : "No selected crawler token is blocked at the start URL",
|
|
8716
|
+
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.",
|
|
8717
|
+
action: "Keep blocks intentional. A crawler token being allowed here does not prove that the service will crawl, index, or cite the site.",
|
|
8718
|
+
evidence: {
|
|
8719
|
+
availability: report.ai?.robotsTxt?.availability,
|
|
8720
|
+
blocked,
|
|
8721
|
+
botAccess: bots
|
|
8722
|
+
}
|
|
8723
|
+
}),
|
|
8724
|
+
check$1("access", {
|
|
8725
|
+
id: "public-content-access",
|
|
8726
|
+
status: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "warning" : "pass",
|
|
8727
|
+
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",
|
|
8728
|
+
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.`,
|
|
8729
|
+
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.",
|
|
8730
|
+
evidence: {
|
|
8731
|
+
authenticationGates: authenticationGates.length,
|
|
8732
|
+
clientRenderedPages: clientRenderedPages.length
|
|
8733
|
+
},
|
|
8734
|
+
urls: [...authenticationGates, ...clientRenderedPages].map((issue) => issue.url).slice(0, 25)
|
|
8735
|
+
}),
|
|
8736
|
+
check$1("access", {
|
|
8737
|
+
id: "protocol-canonicalization",
|
|
8738
|
+
status: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "pass" : protocol.http.permanentRedirectToHttps === null ? "unknown" : "warning",
|
|
8739
|
+
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",
|
|
8740
|
+
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"}.`,
|
|
8741
|
+
action: "Use one permanent HTTP-to-HTTPS redirect and send HSTS only after HTTPS is stable across the site.",
|
|
8742
|
+
evidence: protocol
|
|
8743
|
+
})
|
|
8744
|
+
];
|
|
8559
8745
|
}
|
|
8560
8746
|
function identityChecks(report) {
|
|
8561
8747
|
const types = new Set(report.pages.flatMap((page) => page.schemaTypes ?? []));
|
|
@@ -10088,6 +10274,48 @@ const RULE_DEFINITIONS = [
|
|
|
10088
10274
|
suggestedCommands: ["seo crawl --sitemap-url <url> --health --json"]
|
|
10089
10275
|
}
|
|
10090
10276
|
},
|
|
10277
|
+
{
|
|
10278
|
+
id: "soft_authentication_gate",
|
|
10279
|
+
title: "Page appears to be an authentication gate",
|
|
10280
|
+
category: "response",
|
|
10281
|
+
defaultSeverity: "medium",
|
|
10282
|
+
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.",
|
|
10283
|
+
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.",
|
|
10284
|
+
impactIfIgnored: "A login page can be mistaken for the intended content, producing misleading crawl results and weak search or agent access.",
|
|
10285
|
+
howToVerify: "Fetch the affected URL without a session and confirm it returns the intended public page, or a deliberate authentication status for protected content.",
|
|
10286
|
+
agentHints: {
|
|
10287
|
+
evidenceFields: ["page.softAuthenticationGate", "page.finalUrl"],
|
|
10288
|
+
suggestedCommands: ["seo crawl <url> --max-pages 1 --json"]
|
|
10289
|
+
}
|
|
10290
|
+
},
|
|
10291
|
+
{
|
|
10292
|
+
id: "client_rendered_content",
|
|
10293
|
+
title: "Important content requires JavaScript rendering",
|
|
10294
|
+
category: "response",
|
|
10295
|
+
defaultSeverity: "medium",
|
|
10296
|
+
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.",
|
|
10297
|
+
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.",
|
|
10298
|
+
impactIfIgnored: "Clients that do not execute JavaScript may see an empty shell or miss important content and links.",
|
|
10299
|
+
howToVerify: "Re-run the crawl and confirm the raw and rendered content observations contain the same important document content.",
|
|
10300
|
+
agentHints: {
|
|
10301
|
+
evidenceFields: ["page.fetchDiagnostics.rendering.documentDifference", "page.usedJs"],
|
|
10302
|
+
suggestedCommands: ["seo crawl <url> --json"]
|
|
10303
|
+
}
|
|
10304
|
+
},
|
|
10305
|
+
{
|
|
10306
|
+
id: "soft_404",
|
|
10307
|
+
title: "Unknown URLs return a successful response",
|
|
10308
|
+
category: "response",
|
|
10309
|
+
defaultSeverity: "high",
|
|
10310
|
+
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.",
|
|
10311
|
+
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.",
|
|
10312
|
+
impactIfIgnored: "Missing URLs can remain in crawl queues, waste crawl attention, and create confusing search or monitoring evidence.",
|
|
10313
|
+
howToVerify: "Request several known-nonexistent paths and confirm each returns 404 or 410 without redirecting to a generic successful page.",
|
|
10314
|
+
agentHints: {
|
|
10315
|
+
evidenceFields: ["report.siteChecks.soft404"],
|
|
10316
|
+
suggestedCommands: ["seo crawl <url> --json"]
|
|
10317
|
+
}
|
|
10318
|
+
},
|
|
10091
10319
|
{
|
|
10092
10320
|
id: "client_error",
|
|
10093
10321
|
title: "Client error",
|
|
@@ -10577,6 +10805,8 @@ const RULE_RECOMMENDATIONS = {
|
|
|
10577
10805
|
x_robots_noindex: "review",
|
|
10578
10806
|
robots_blocked: "review",
|
|
10579
10807
|
crawler_access_blocked: "review",
|
|
10808
|
+
soft_authentication_gate: "review",
|
|
10809
|
+
client_rendered_content: "review",
|
|
10580
10810
|
orphan_page: "review",
|
|
10581
10811
|
redirected_url: "review",
|
|
10582
10812
|
slow_response: "review",
|
|
@@ -10670,6 +10900,28 @@ const WEAK_VALUABLE_INLINKS = 1;
|
|
|
10670
10900
|
const QUERY_COVERAGE_MIN_IMPRESSIONS = 50;
|
|
10671
10901
|
const QUERY_COVERAGE_MIN = .6;
|
|
10672
10902
|
const OVERSIZED_IMAGE_CANDIDATE_LIMIT = 2e3;
|
|
10903
|
+
const EMPTY_INITIAL_HTML_CHARACTERS = 200;
|
|
10904
|
+
const SUBSTANTIVE_RENDERED_CHARACTERS = 500;
|
|
10905
|
+
function clientRenderedContentEvidence(page) {
|
|
10906
|
+
const rendering = page.fetchDiagnostics?.rendering;
|
|
10907
|
+
const difference = rendering?.documentDifference;
|
|
10908
|
+
if (rendering?.status !== "rendered" || !difference?.changed.includes("content")) return;
|
|
10909
|
+
const rawCharacters = difference.raw.content.characters;
|
|
10910
|
+
const renderedCharacters = difference.rendered.content.characters;
|
|
10911
|
+
if (rawCharacters >= EMPTY_INITIAL_HTML_CHARACTERS || renderedCharacters < SUBSTANTIVE_RENDERED_CHARACTERS || renderedCharacters < rawCharacters * 3) return;
|
|
10912
|
+
return {
|
|
10913
|
+
rawCharacters,
|
|
10914
|
+
rawWords: difference.raw.content.wordCount,
|
|
10915
|
+
renderedCharacters,
|
|
10916
|
+
renderedWords: difference.rendered.content.wordCount,
|
|
10917
|
+
changed: difference.changed,
|
|
10918
|
+
thresholds: {
|
|
10919
|
+
emptyInitialHtmlCharacters: EMPTY_INITIAL_HTML_CHARACTERS,
|
|
10920
|
+
substantiveRenderedCharacters: SUBSTANTIVE_RENDERED_CHARACTERS,
|
|
10921
|
+
minimumGrowthRatio: 3
|
|
10922
|
+
}
|
|
10923
|
+
};
|
|
10924
|
+
}
|
|
10673
10925
|
function issue(ruleId, page, detail, evidence) {
|
|
10674
10926
|
const rule = explainRule(ruleId);
|
|
10675
10927
|
if (!rule) throw new Error(`Missing rule guidance for ${ruleId}`);
|
|
@@ -10884,6 +11136,9 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
10884
11136
|
}));
|
|
10885
11137
|
if (page.auditScope === "status") continue;
|
|
10886
11138
|
if (redirected) continue;
|
|
11139
|
+
if (page.softAuthenticationGate) issues.push(issue("soft_authentication_gate", page, "A password form and login-specific page signals were observed.", page.softAuthenticationGate));
|
|
11140
|
+
const renderedContent = clientRenderedContentEvidence(page);
|
|
11141
|
+
if (renderedContent) issues.push(issue("client_rendered_content", page, "The raw response was an empty or near-empty shell.", renderedContent));
|
|
10887
11142
|
if (isHtmlPage(page) && (page.sizeBytes ?? 0) > LARGE_HTML_BYTES) issues.push(issue("large_html", page, `${Math.round((page.sizeBytes ?? 0) / 1024)} KB`, {
|
|
10888
11143
|
sizeBytes: page.sizeBytes,
|
|
10889
11144
|
thresholdBytes: LARGE_HTML_BYTES
|
|
@@ -11489,7 +11744,7 @@ function createCrawlReport(input) {
|
|
|
11489
11744
|
const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
|
|
11490
11745
|
const requestEvidenceStatus = input.requestEvidenceStatus ?? (input.requests ? "available" : "unavailable");
|
|
11491
11746
|
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 })]);
|
|
11747
|
+
const safeIssues = sanitizeIssues([...input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })], ...input.additionalIssues ?? []]);
|
|
11493
11748
|
const pages = safePagesWithLinks;
|
|
11494
11749
|
return {
|
|
11495
11750
|
id: input.id ?? crawlRunId(),
|
|
@@ -11518,6 +11773,7 @@ function createCrawlReport(input) {
|
|
|
11518
11773
|
...input.ai ? { ai: sanitizeTenantValue(input.ai) } : {},
|
|
11519
11774
|
...input.sitemapDiscovery ? { sitemapDiscovery: sanitizeTenantValue(input.sitemapDiscovery) } : {},
|
|
11520
11775
|
...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
|
|
11776
|
+
...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
|
|
11521
11777
|
...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
|
|
11522
11778
|
warnings: sanitizeMessages(input.warnings).sort(compareText$13),
|
|
11523
11779
|
caveats: sanitizeMessages(input.caveats).sort(compareText$13)
|
|
@@ -12087,6 +12343,75 @@ function observationFromPage(requestedUrl, page) {
|
|
|
12087
12343
|
};
|
|
12088
12344
|
}
|
|
12089
12345
|
//#endregion
|
|
12346
|
+
//#region packages/core/src/analyze/crawler/site-checks.ts
|
|
12347
|
+
const PROBE_COUNT = 2;
|
|
12348
|
+
function probeUrls(startUrl) {
|
|
12349
|
+
const origin = new URL(startUrl).origin;
|
|
12350
|
+
const seed = createHash("sha256").update(origin).digest("hex").slice(0, 12);
|
|
12351
|
+
return Array.from({ length: PROBE_COUNT }, (_, index) => new URL(`/.well-known/seo-audit/not-found-${seed}-${index + 1}`, origin).toString());
|
|
12352
|
+
}
|
|
12353
|
+
function probeObservation(url, result) {
|
|
12354
|
+
const request = result.request;
|
|
12355
|
+
if (!request) return {
|
|
12356
|
+
url,
|
|
12357
|
+
error: result.warning ?? "No response evidence."
|
|
12358
|
+
};
|
|
12359
|
+
if (request.outcome === "response") return {
|
|
12360
|
+
url,
|
|
12361
|
+
status: request.status,
|
|
12362
|
+
finalUrl: request.finalUrl,
|
|
12363
|
+
redirected: request.finalUrl !== url,
|
|
12364
|
+
accessBlocked: Boolean(request.accessBlock)
|
|
12365
|
+
};
|
|
12366
|
+
return {
|
|
12367
|
+
url,
|
|
12368
|
+
error: request.outcome === "failure" ? request.error : `Probe skipped: ${request.reason}`
|
|
12369
|
+
};
|
|
12370
|
+
}
|
|
12371
|
+
async function collectCrawlSiteChecks(input) {
|
|
12372
|
+
const probes = [];
|
|
12373
|
+
for (const url of probeUrls(input.startUrl)) {
|
|
12374
|
+
if (input.signal?.aborted) break;
|
|
12375
|
+
const result = await input.fetchStatusPage(url, {
|
|
12376
|
+
js: "off",
|
|
12377
|
+
refresh: true,
|
|
12378
|
+
timeoutMs: input.timeoutMs,
|
|
12379
|
+
signal: input.signal,
|
|
12380
|
+
writeCache: false,
|
|
12381
|
+
respectRobots: input.respectRobots
|
|
12382
|
+
});
|
|
12383
|
+
probes.push(probeObservation(url, result));
|
|
12384
|
+
}
|
|
12385
|
+
const conclusive = probes.filter((probe) => probe.status !== void 0 && !probe.accessBlocked);
|
|
12386
|
+
const successful = conclusive.filter((probe) => (probe.status ?? 0) >= 200 && (probe.status ?? 0) < 400);
|
|
12387
|
+
const correctMissing = conclusive.filter((probe) => probe.status === 404 || probe.status === 410);
|
|
12388
|
+
const status = conclusive.length !== PROBE_COUNT ? "unknown" : successful.length === PROBE_COUNT ? "warning" : correctMissing.length === PROBE_COUNT ? "pass" : "unknown";
|
|
12389
|
+
const checks = { soft404: {
|
|
12390
|
+
status,
|
|
12391
|
+
probes,
|
|
12392
|
+
probeLimit: PROBE_COUNT,
|
|
12393
|
+
complete: probes.length === PROBE_COUNT
|
|
12394
|
+
} };
|
|
12395
|
+
if (status !== "warning") return {
|
|
12396
|
+
checks,
|
|
12397
|
+
issues: []
|
|
12398
|
+
};
|
|
12399
|
+
const rule = explainRule("soft_404");
|
|
12400
|
+
if (!rule) throw new Error("Missing rule guidance for soft_404");
|
|
12401
|
+
return {
|
|
12402
|
+
checks,
|
|
12403
|
+
issues: [{
|
|
12404
|
+
ruleId: "soft_404",
|
|
12405
|
+
title: rule.title,
|
|
12406
|
+
category: rule.category,
|
|
12407
|
+
severity: rule.defaultSeverity,
|
|
12408
|
+
url: new URL("/", input.startUrl).toString(),
|
|
12409
|
+
detail: `${successful.length} known-nonexistent paths returned a successful response or redirect.`,
|
|
12410
|
+
evidence: checks.soft404
|
|
12411
|
+
}]
|
|
12412
|
+
};
|
|
12413
|
+
}
|
|
12414
|
+
//#endregion
|
|
12090
12415
|
//#region packages/core/src/analyze/crawler/site-crawl-ai-signals.ts
|
|
12091
12416
|
const MAX_AUXILIARY_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
12092
12417
|
const AI_BOT_USER_AGENTS = [
|
|
@@ -12806,6 +13131,9 @@ async function crawlOne(url, opts = {}) {
|
|
|
12806
13131
|
mainContentHash: hashText(extracted.contentText),
|
|
12807
13132
|
textRatio: fetched.html.length ? Math.min(1, extracted.contentText.length / fetched.html.length) : void 0,
|
|
12808
13133
|
contentSample: truncate$1(extracted.contentText, 300),
|
|
13134
|
+
contentSketch: crawlerEvidence.contentSketch,
|
|
13135
|
+
softAuthenticationGate: crawlerEvidence.softAuthenticationGate,
|
|
13136
|
+
tabbedContent: crawlerEvidence.tabbedContent,
|
|
12809
13137
|
lang: extracted.lang,
|
|
12810
13138
|
hasViewport: extracted.hasViewport,
|
|
12811
13139
|
isHttps: new URL(extracted.finalUrl).protocol === "https:",
|
|
@@ -14090,6 +14418,13 @@ async function crawlSite(input, dependencies) {
|
|
|
14090
14418
|
signal,
|
|
14091
14419
|
concurrency: Math.min(config.concurrency, 4)
|
|
14092
14420
|
}) : void 0;
|
|
14421
|
+
const siteCheckResult = !healthStrategy && !cancelled && !stoppedForMemory ? await collectCrawlSiteChecks({
|
|
14422
|
+
startUrl: config.url,
|
|
14423
|
+
timeoutMs: config.timeoutMs,
|
|
14424
|
+
fetchStatusPage: deps.fetchStatusPage,
|
|
14425
|
+
respectRobots: config.respectRobots,
|
|
14426
|
+
signal
|
|
14427
|
+
}) : void 0;
|
|
14093
14428
|
for (const page of pages) {
|
|
14094
14429
|
if (!page.geo || !llmsTxt) continue;
|
|
14095
14430
|
page.geo = {
|
|
@@ -14115,6 +14450,10 @@ async function crawlSite(input, dependencies) {
|
|
|
14115
14450
|
} } : {},
|
|
14116
14451
|
...sitemapDiscovery ? { sitemapDiscovery } : {},
|
|
14117
14452
|
...externalLinkVerification ? { externalLinkVerification } : {},
|
|
14453
|
+
...siteCheckResult ? {
|
|
14454
|
+
siteChecks: siteCheckResult.checks,
|
|
14455
|
+
additionalIssues: siteCheckResult.issues
|
|
14456
|
+
} : {},
|
|
14118
14457
|
...agentDiscovery ? { agentDiscovery } : {},
|
|
14119
14458
|
status: reportStatus,
|
|
14120
14459
|
warnings,
|
|
@@ -16407,6 +16746,8 @@ const agentRepresentationResponseSchema = z.object({
|
|
|
16407
16746
|
status: z.number().int().optional(),
|
|
16408
16747
|
contentType: z.string().optional(),
|
|
16409
16748
|
bytes: z.number().int().nonnegative().optional(),
|
|
16749
|
+
characters: z.number().int().nonnegative().optional(),
|
|
16750
|
+
estimatedTokens: z.number().int().nonnegative().optional(),
|
|
16410
16751
|
sha256: z.string().optional(),
|
|
16411
16752
|
canonicalUrl: z.string().url().optional(),
|
|
16412
16753
|
alternateUrl: z.string().url().optional(),
|
|
@@ -16464,7 +16805,15 @@ const agentDiscoverySchema = z.object({
|
|
|
16464
16805
|
sourceWordCount: z.number().int().nonnegative(),
|
|
16465
16806
|
wordRetentionRatio: z.number().nonnegative().nullable(),
|
|
16466
16807
|
introductoryCopyRetained: z.boolean().nullable(),
|
|
16467
|
-
navigationOnly: z.boolean()
|
|
16808
|
+
navigationOnly: z.boolean(),
|
|
16809
|
+
contentSketchCoverage: z.number().min(0).max(1).nullable().optional(),
|
|
16810
|
+
tabbedContent: z.object({
|
|
16811
|
+
detectedPanels: z.number().int().nonnegative(),
|
|
16812
|
+
evaluatedPanels: z.number().int().nonnegative(),
|
|
16813
|
+
retainedPanels: z.number().int().nonnegative(),
|
|
16814
|
+
missingPanels: z.number().int().nonnegative(),
|
|
16815
|
+
complete: z.boolean().nullable()
|
|
16816
|
+
}).optional()
|
|
16468
16817
|
}).optional()
|
|
16469
16818
|
}))
|
|
16470
16819
|
}),
|
|
@@ -16483,6 +16832,11 @@ const agentDiscoverySchema = z.object({
|
|
|
16483
16832
|
missingHtmlRoutes: z.array(z.string()),
|
|
16484
16833
|
missingMarkdownRoutes: z.array(z.string()),
|
|
16485
16834
|
orphanMarkdownRoutes: z.array(z.string()),
|
|
16835
|
+
comparisonStatus: z.enum([
|
|
16836
|
+
"complete",
|
|
16837
|
+
"partial",
|
|
16838
|
+
"not-applicable"
|
|
16839
|
+
]).optional(),
|
|
16486
16840
|
error: z.string().optional()
|
|
16487
16841
|
}),
|
|
16488
16842
|
agentSkills: z.object({
|
|
@@ -16588,6 +16942,25 @@ const agentDiscoverySchema = z.object({
|
|
|
16588
16942
|
warnings: z.array(z.string())
|
|
16589
16943
|
});
|
|
16590
16944
|
//#endregion
|
|
16945
|
+
//#region packages/core/src/analyze/crawler/site-checks-schema.ts
|
|
16946
|
+
const crawlSiteChecksSchema = z.object({ soft404: z.object({
|
|
16947
|
+
status: z.enum([
|
|
16948
|
+
"pass",
|
|
16949
|
+
"warning",
|
|
16950
|
+
"unknown"
|
|
16951
|
+
]),
|
|
16952
|
+
probes: z.array(z.object({
|
|
16953
|
+
url: z.string().url(),
|
|
16954
|
+
status: z.number().int().optional(),
|
|
16955
|
+
finalUrl: z.string().url().optional(),
|
|
16956
|
+
redirected: z.boolean().optional(),
|
|
16957
|
+
accessBlocked: z.boolean().optional(),
|
|
16958
|
+
error: z.string().optional()
|
|
16959
|
+
})),
|
|
16960
|
+
probeLimit: z.number().int().positive(),
|
|
16961
|
+
complete: z.boolean()
|
|
16962
|
+
}) });
|
|
16963
|
+
//#endregion
|
|
16591
16964
|
//#region packages/core/src/analyze/crawler/schemas.ts
|
|
16592
16965
|
const crawlerRuleSeveritySchema = z.enum([
|
|
16593
16966
|
"low",
|
|
@@ -17017,6 +17390,34 @@ const crawlPageSnapshotSchema = z.object({
|
|
|
17017
17390
|
mainContentHash: z.string().optional(),
|
|
17018
17391
|
textRatio: z.number().min(0).max(1).optional(),
|
|
17019
17392
|
contentSample: z.string().optional(),
|
|
17393
|
+
contentSketch: z.object({
|
|
17394
|
+
version: z.literal(1),
|
|
17395
|
+
wordCount: z.number().int().nonnegative(),
|
|
17396
|
+
shingleSize: z.number().int().positive(),
|
|
17397
|
+
sampledShingles: z.number().int().nonnegative(),
|
|
17398
|
+
hashes: z.array(z.string())
|
|
17399
|
+
}).optional(),
|
|
17400
|
+
softAuthenticationGate: z.object({
|
|
17401
|
+
kind: z.literal("login-form"),
|
|
17402
|
+
indicators: z.array(z.string()),
|
|
17403
|
+
formActionPath: z.string().optional()
|
|
17404
|
+
}).optional(),
|
|
17405
|
+
tabbedContent: z.object({
|
|
17406
|
+
groups: z.number().int().positive(),
|
|
17407
|
+
panels: z.number().int().positive(),
|
|
17408
|
+
retainedPanels: z.number().int().nonnegative(),
|
|
17409
|
+
truncated: z.boolean(),
|
|
17410
|
+
panelSketches: z.array(z.object({
|
|
17411
|
+
label: z.string().optional(),
|
|
17412
|
+
sketch: z.object({
|
|
17413
|
+
version: z.literal(1),
|
|
17414
|
+
wordCount: z.number().int().nonnegative(),
|
|
17415
|
+
shingleSize: z.number().int().positive(),
|
|
17416
|
+
sampledShingles: z.number().int().nonnegative(),
|
|
17417
|
+
hashes: z.array(z.string())
|
|
17418
|
+
})
|
|
17419
|
+
}))
|
|
17420
|
+
}).optional(),
|
|
17020
17421
|
lang: z.string().optional(),
|
|
17021
17422
|
hasViewport: z.boolean().optional(),
|
|
17022
17423
|
isHttps: z.boolean().optional(),
|
|
@@ -17508,6 +17909,7 @@ const crawlReportBaseSchema = z.object({
|
|
|
17508
17909
|
ai: crawlAiSignalsSchema.optional(),
|
|
17509
17910
|
sitemapDiscovery: crawlSitemapDiscoverySchema.optional(),
|
|
17510
17911
|
externalLinkVerification: crawlExternalLinkVerificationSchema.optional(),
|
|
17912
|
+
siteChecks: crawlSiteChecksSchema.optional(),
|
|
17511
17913
|
agentDiscovery: agentDiscoverySchema.optional(),
|
|
17512
17914
|
warnings: z.array(z.string()),
|
|
17513
17915
|
caveats: z.array(z.string())
|