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/mcp.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, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { arch, homedir, platform, totalmem } from "node:os";
|
|
@@ -2134,6 +2134,43 @@ function extractCanonicalEvidence($, headers, baseUrl) {
|
|
|
2134
2134
|
return evidenceFromCandidates([...htmlCandidates($, baseUrl), ...headerCandidates(headers, baseUrl)]);
|
|
2135
2135
|
}
|
|
2136
2136
|
//#endregion
|
|
2137
|
+
//#region packages/core/dist/extract/content-sketch.js
|
|
2138
|
+
const SHINGLE_SIZE = 5;
|
|
2139
|
+
const MAX_HASHES = 32;
|
|
2140
|
+
function words(value) {
|
|
2141
|
+
return value.normalize("NFKC").toLocaleLowerCase("en").match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
2142
|
+
}
|
|
2143
|
+
function sampleIndexes(total, limit) {
|
|
2144
|
+
if (total <= limit) return Array.from({ length: total }, (_, index) => index);
|
|
2145
|
+
return Array.from({ length: limit }, (_, index) => Math.round(index * (total - 1) / (limit - 1)));
|
|
2146
|
+
}
|
|
2147
|
+
function digest(value) {
|
|
2148
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
2149
|
+
}
|
|
2150
|
+
function contentSketch(value) {
|
|
2151
|
+
const tokens = words(value);
|
|
2152
|
+
const hashes = sampleIndexes(Math.max(0, tokens.length - SHINGLE_SIZE + 1), MAX_HASHES).map((index) => digest(tokens.slice(index, index + SHINGLE_SIZE).join(" ")));
|
|
2153
|
+
return {
|
|
2154
|
+
version: 1,
|
|
2155
|
+
wordCount: tokens.length,
|
|
2156
|
+
shingleSize: SHINGLE_SIZE,
|
|
2157
|
+
sampledShingles: hashes.length,
|
|
2158
|
+
hashes
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function contentSketchCoverage(source, candidate) {
|
|
2162
|
+
if (!source || source.hashes.length < 4) return null;
|
|
2163
|
+
const targets = new Set(source.hashes);
|
|
2164
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2165
|
+
const tokens = words(candidate);
|
|
2166
|
+
for (let index = 0; index <= tokens.length - source.shingleSize; index += 1) {
|
|
2167
|
+
const hash = digest(tokens.slice(index, index + source.shingleSize).join(" "));
|
|
2168
|
+
if (targets.has(hash)) matched.add(hash);
|
|
2169
|
+
if (matched.size === targets.size) break;
|
|
2170
|
+
}
|
|
2171
|
+
return Math.round(matched.size / targets.size * 1e3) / 1e3;
|
|
2172
|
+
}
|
|
2173
|
+
//#endregion
|
|
2137
2174
|
//#region packages/core/dist/extract/main-content.js
|
|
2138
2175
|
function readableError(error) {
|
|
2139
2176
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2751,6 +2788,48 @@ const SOCIAL_PROFILE_HOSTS = [
|
|
|
2751
2788
|
"x.com",
|
|
2752
2789
|
"youtube.com"
|
|
2753
2790
|
];
|
|
2791
|
+
const LOGIN_PATH_PATTERN = /(?:^|\/)(?:auth|login|log-in|signin|sign-in|sso)(?:\/|$)/iu;
|
|
2792
|
+
const LOGIN_TITLE_PATTERN = /\b(?:authenticate|log[ -]?in|sign[ -]?in)\b/iu;
|
|
2793
|
+
function softAuthenticationGate($, finalUrl, title) {
|
|
2794
|
+
const passwordForms = $("form").toArray().filter((form) => $(form).find("input[type=\"password\" i]").length > 0);
|
|
2795
|
+
if (passwordForms.length === 0) return void 0;
|
|
2796
|
+
const pagePath = new URL(finalUrl).pathname;
|
|
2797
|
+
const action = $(passwordForms[0]).attr("action");
|
|
2798
|
+
const actionUrl = action ? httpUrl$1(action, finalUrl) : void 0;
|
|
2799
|
+
const actionPath = actionUrl ? new URL(actionUrl).pathname : void 0;
|
|
2800
|
+
const indicators = [
|
|
2801
|
+
...LOGIN_PATH_PATTERN.test(pagePath) ? ["login-path"] : [],
|
|
2802
|
+
...LOGIN_TITLE_PATTERN.test(title ?? "") ? ["login-title"] : [],
|
|
2803
|
+
...LOGIN_PATH_PATTERN.test(actionPath ?? "") ? ["login-form-action"] : []
|
|
2804
|
+
];
|
|
2805
|
+
if (indicators.length === 0) return void 0;
|
|
2806
|
+
return {
|
|
2807
|
+
kind: "login-form",
|
|
2808
|
+
indicators,
|
|
2809
|
+
...actionPath ? { formActionPath: actionPath } : {}
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
function tabbedContent($) {
|
|
2813
|
+
const groups = $("[role=\"tablist\" i]").length;
|
|
2814
|
+
const panels = $("[role=\"tabpanel\" i]").toArray();
|
|
2815
|
+
if (groups === 0 || panels.length === 0) return void 0;
|
|
2816
|
+
const panelSketches = panels.slice(0, 12).map((panel) => {
|
|
2817
|
+
const labelledBy = $(panel).attr("aria-labelledby");
|
|
2818
|
+
const labelledElement = labelledBy ? $("[id]").toArray().find((element) => $(element).attr("id") === labelledBy) : void 0;
|
|
2819
|
+
const label = safeText$1($(panel).attr("aria-label") ?? (labelledElement ? $(labelledElement).text() : void 0));
|
|
2820
|
+
return {
|
|
2821
|
+
...label ? { label: label.slice(0, 80) } : {},
|
|
2822
|
+
sketch: contentSketch($(panel).text())
|
|
2823
|
+
};
|
|
2824
|
+
});
|
|
2825
|
+
return {
|
|
2826
|
+
groups,
|
|
2827
|
+
panels: panels.length,
|
|
2828
|
+
retainedPanels: panelSketches.length,
|
|
2829
|
+
truncated: panels.length > panelSketches.length,
|
|
2830
|
+
panelSketches
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2754
2833
|
function safeText$1(value) {
|
|
2755
2834
|
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
2756
2835
|
return trimmed ? trimmed : void 0;
|
|
@@ -2977,10 +3056,11 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
2977
3056
|
html: sanitizedContentHtml($, fetchResult.html, fetchResult.finalUrl)
|
|
2978
3057
|
}, extractor, dependencies);
|
|
2979
3058
|
const { text, excerpt } = content;
|
|
3059
|
+
const title = safeText$1($("title").first().text());
|
|
2980
3060
|
return {
|
|
2981
3061
|
url: fetchResult.url,
|
|
2982
3062
|
finalUrl: fetchResult.finalUrl,
|
|
2983
|
-
title
|
|
3063
|
+
title,
|
|
2984
3064
|
metaDescription: safeText$1($("meta[name=\"description\"]").attr("content")),
|
|
2985
3065
|
metaRobots,
|
|
2986
3066
|
xRobotsTag: safeText$1(headerValue$4(fetchResult.headers, "x-robots-tag")),
|
|
@@ -3000,7 +3080,10 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3000
3080
|
externalLinks: [...crawlerExternalLinks],
|
|
3001
3081
|
internalAnchorSamples,
|
|
3002
3082
|
externalAnchorSamples,
|
|
3003
|
-
socialProfileLinks: [...socialProfileLinks]
|
|
3083
|
+
socialProfileLinks: [...socialProfileLinks],
|
|
3084
|
+
contentSketch: contentSketch(text),
|
|
3085
|
+
softAuthenticationGate: softAuthenticationGate($, fetchResult.finalUrl, title),
|
|
3086
|
+
tabbedContent: tabbedContent($)
|
|
3004
3087
|
} } : {},
|
|
3005
3088
|
markdownAlternates,
|
|
3006
3089
|
hreflang,
|
|
@@ -7105,6 +7188,8 @@ function markdownQuality(markdown, page) {
|
|
|
7105
7188
|
const codeFences = markdown.match(/^```/gmu)?.length ?? 0;
|
|
7106
7189
|
const wordCount = bodyWordCount(markdown);
|
|
7107
7190
|
const sourceWordCount = page.wordCount ?? 0;
|
|
7191
|
+
const evaluatedPanels = (page.tabbedContent?.panelSketches ?? []).map((panel) => contentSketchCoverage(panel.sketch, markdown)).filter((coverage) => coverage !== null);
|
|
7192
|
+
const retainedPanels = evaluatedPanels.filter((coverage) => coverage >= .6).length;
|
|
7108
7193
|
return {
|
|
7109
7194
|
frontmatterTitle: /^---[\s\S]*?^title:\s*.+$/mu.test(markdown),
|
|
7110
7195
|
h1Count: markdown.match(/^#\s+\S.+$/gmu)?.length ?? 0,
|
|
@@ -7121,7 +7206,15 @@ function markdownQuality(markdown, page) {
|
|
|
7121
7206
|
sourceWordCount,
|
|
7122
7207
|
wordRetentionRatio: sourceWordCount > 0 ? Math.round(wordCount / sourceWordCount * 1e3) / 1e3 : null,
|
|
7123
7208
|
introductoryCopyRetained: introProbe ? normalizedMarkdown.includes(introProbe) : null,
|
|
7124
|
-
navigationOnly: bodyWordCount(markdown) < 25
|
|
7209
|
+
navigationOnly: bodyWordCount(markdown) < 25,
|
|
7210
|
+
contentSketchCoverage: contentSketchCoverage(page.contentSketch, markdown),
|
|
7211
|
+
tabbedContent: {
|
|
7212
|
+
detectedPanels: page.tabbedContent?.panels ?? 0,
|
|
7213
|
+
evaluatedPanels: evaluatedPanels.length,
|
|
7214
|
+
retainedPanels,
|
|
7215
|
+
missingPanels: Math.max(0, evaluatedPanels.length - retainedPanels),
|
|
7216
|
+
complete: evaluatedPanels.length === 0 ? null : retainedPanels === evaluatedPanels.length
|
|
7217
|
+
}
|
|
7125
7218
|
};
|
|
7126
7219
|
}
|
|
7127
7220
|
function combinedSignal(timeoutMs, signal) {
|
|
@@ -7175,6 +7268,8 @@ async function fetchRepresentation(input) {
|
|
|
7175
7268
|
status: response.status,
|
|
7176
7269
|
contentType: response.headers.get("content-type") ?? void 0,
|
|
7177
7270
|
bytes: Buffer.byteLength(body),
|
|
7271
|
+
characters: body.length,
|
|
7272
|
+
estimatedTokens: Math.ceil(body.length / 4),
|
|
7178
7273
|
sha256: sha256(body),
|
|
7179
7274
|
canonicalUrl: link.find((entry) => entry.rel.includes("canonical"))?.url,
|
|
7180
7275
|
alternateUrl: link.find((entry) => entry.rel.includes("alternate") && entry.type === "text/markdown")?.url,
|
|
@@ -7233,11 +7328,15 @@ async function inspectMarkdownPage(input) {
|
|
|
7233
7328
|
if (explicit) observation.explicit = explicit.observation;
|
|
7234
7329
|
observation.negotiated = negotiated.observation;
|
|
7235
7330
|
observation.repeated = repeated.observation;
|
|
7236
|
-
|
|
7237
|
-
const
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7331
|
+
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 ?? "");
|
|
7332
|
+
const explicitMarkdown = isMarkdown(explicit) ? explicit : void 0;
|
|
7333
|
+
const negotiatedMarkdown = isMarkdown(negotiated) ? negotiated : void 0;
|
|
7334
|
+
const repeatedMarkdown = isMarkdown(repeated) ? repeated : void 0;
|
|
7335
|
+
observation.explicitMatchesNegotiated = explicitMarkdown?.observation.sha256 && negotiatedMarkdown?.observation.sha256 ? explicitMarkdown.observation.sha256 === negotiatedMarkdown.observation.sha256 : null;
|
|
7336
|
+
const primary = explicitMarkdown ?? negotiatedMarkdown;
|
|
7337
|
+
observation.repeatedHashStable = primary?.observation.sha256 && repeatedMarkdown?.observation.sha256 ? primary.observation.sha256 === repeatedMarkdown.observation.sha256 : null;
|
|
7338
|
+
observation.markdownCanonicalMatchesHtml = sameDocument(primary?.observation.canonicalUrl, input.page.finalUrl);
|
|
7339
|
+
if (primary?.body) observation.quality = markdownQuality(primary.body, input.page);
|
|
7241
7340
|
return observation;
|
|
7242
7341
|
}
|
|
7243
7342
|
async function fetchText(input) {
|
|
@@ -7481,17 +7580,16 @@ async function inspectRouteManifest(input) {
|
|
|
7481
7580
|
const declared = Array.isArray(parsed.pages) ? parsed.pages : [];
|
|
7482
7581
|
const declaredHtmlRoutes = declared.map((page) => page.htmlPath).filter((path) => typeof path === "string").sort();
|
|
7483
7582
|
const declaredMarkdownRoutes = declared.map((page) => page.markdownPath).filter((path) => typeof path === "string").sort();
|
|
7484
|
-
const crawledHtmlRoutes = new Set(input.pages.map((page) => new URL(page.finalUrl).pathname.replace(/\/$/u, "") || "/"));
|
|
7485
|
-
const observedMarkdownRoutes = new Set(input.observations.flatMap((page) => page.advertisedUrls.map((value) => new URL(value).pathname)));
|
|
7486
7583
|
return {
|
|
7487
7584
|
url,
|
|
7488
7585
|
status: result.response.status,
|
|
7489
7586
|
valid: Array.isArray(parsed.pages),
|
|
7490
7587
|
declaredHtmlRoutes,
|
|
7491
7588
|
declaredMarkdownRoutes,
|
|
7492
|
-
missingHtmlRoutes:
|
|
7493
|
-
missingMarkdownRoutes:
|
|
7494
|
-
orphanMarkdownRoutes: [
|
|
7589
|
+
missingHtmlRoutes: [],
|
|
7590
|
+
missingMarkdownRoutes: [],
|
|
7591
|
+
orphanMarkdownRoutes: [],
|
|
7592
|
+
comparisonStatus: "partial"
|
|
7495
7593
|
};
|
|
7496
7594
|
} catch (error) {
|
|
7497
7595
|
return {
|
|
@@ -7502,6 +7600,7 @@ async function inspectRouteManifest(input) {
|
|
|
7502
7600
|
missingHtmlRoutes: [],
|
|
7503
7601
|
missingMarkdownRoutes: [],
|
|
7504
7602
|
orphanMarkdownRoutes: [],
|
|
7603
|
+
comparisonStatus: "not-applicable",
|
|
7505
7604
|
error: safeError(error)
|
|
7506
7605
|
};
|
|
7507
7606
|
}
|
|
@@ -7545,9 +7644,7 @@ async function collectAgentDiscovery(input) {
|
|
|
7545
7644
|
}),
|
|
7546
7645
|
inspectRouteManifest({
|
|
7547
7646
|
...input,
|
|
7548
|
-
origin
|
|
7549
|
-
pages,
|
|
7550
|
-
observations
|
|
7647
|
+
origin
|
|
7551
7648
|
}),
|
|
7552
7649
|
inspectAgentEndpoints({
|
|
7553
7650
|
...input,
|
|
@@ -7652,20 +7749,13 @@ async function collectAgentDiscovery(input) {
|
|
|
7652
7749
|
};
|
|
7653
7750
|
}
|
|
7654
7751
|
//#endregion
|
|
7655
|
-
//#region packages/core/dist/analyze/crawler/agent-readiness.js
|
|
7656
|
-
function check$
|
|
7752
|
+
//#region packages/core/dist/analyze/crawler/agent-readiness-markdown.js
|
|
7753
|
+
function check$2(section, input) {
|
|
7657
7754
|
return {
|
|
7658
7755
|
section,
|
|
7659
7756
|
...input
|
|
7660
7757
|
};
|
|
7661
7758
|
}
|
|
7662
|
-
function section$2(id, title, checks) {
|
|
7663
|
-
return {
|
|
7664
|
-
id,
|
|
7665
|
-
title,
|
|
7666
|
-
checks
|
|
7667
|
-
};
|
|
7668
|
-
}
|
|
7669
7759
|
function failedMarkdownUrls(discovery) {
|
|
7670
7760
|
return discovery.markdownAlternates.pages.filter((page) => {
|
|
7671
7761
|
const negotiatedOk = page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "") && page.negotiated.varyAccept;
|
|
@@ -7676,38 +7766,12 @@ function failedMarkdownUrls(discovery) {
|
|
|
7676
7766
|
function qualityFailureUrls(discovery) {
|
|
7677
7767
|
return discovery.markdownAlternates.pages.filter((page) => {
|
|
7678
7768
|
const quality = page.quality;
|
|
7679
|
-
if (!quality) return
|
|
7680
|
-
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;
|
|
7769
|
+
if (!quality) return false;
|
|
7770
|
+
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;
|
|
7681
7771
|
}).map((page) => page.htmlUrl);
|
|
7682
7772
|
}
|
|
7683
|
-
function
|
|
7684
|
-
return
|
|
7685
|
-
"api",
|
|
7686
|
-
"application",
|
|
7687
|
-
"commerce"
|
|
7688
|
-
].map((profile) => check$1("profile", {
|
|
7689
|
-
id: `profile-${profile}`,
|
|
7690
|
-
status: "notApplicable",
|
|
7691
|
-
title: `${profile[0]?.toUpperCase()}${profile.slice(1)} checks are not applicable`,
|
|
7692
|
-
plainEnglish: discovery.profileApplicability[profile].reason,
|
|
7693
|
-
action: "Choose this profile only when the site exposes that capability publicly.",
|
|
7694
|
-
evidence: {
|
|
7695
|
-
profile,
|
|
7696
|
-
selectedProfile: discovery.profile
|
|
7697
|
-
}
|
|
7698
|
-
}));
|
|
7699
|
-
}
|
|
7700
|
-
function summariseChecks(checks) {
|
|
7701
|
-
const count = (status) => checks.filter((item) => item.status === status).length;
|
|
7702
|
-
return {
|
|
7703
|
-
checks: checks.length,
|
|
7704
|
-
passed: count("pass"),
|
|
7705
|
-
warnings: count("warning"),
|
|
7706
|
-
failed: count("fail"),
|
|
7707
|
-
unknown: count("unknown"),
|
|
7708
|
-
information: count("info"),
|
|
7709
|
-
notApplicable: count("notApplicable")
|
|
7710
|
-
};
|
|
7773
|
+
function confirmedMarkdown(page) {
|
|
7774
|
+
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;
|
|
7711
7775
|
}
|
|
7712
7776
|
function markdownChecks(discovery) {
|
|
7713
7777
|
const markdown = discovery.markdownAlternates;
|
|
@@ -7717,12 +7781,28 @@ function markdownChecks(discovery) {
|
|
|
7717
7781
|
const explicitPages = markdown.pages.filter((page) => page.htmlAlternateUnique).length;
|
|
7718
7782
|
const coverageComplete = markdown.eligibleHtmlPages > 0 && markdown.evaluatedPages === markdown.eligibleHtmlPages;
|
|
7719
7783
|
const negotiationComplete = markdown.eligibleHtmlPages > 0 && negotiatedPages === markdown.eligibleHtmlPages && markdown.pages.every((page) => page.negotiated?.varyAccept) && markdown.pages.every((page) => !page.htmlAlternateUnique || page.explicitMatchesNegotiated === true);
|
|
7720
|
-
const stable = markdown.
|
|
7784
|
+
const stable = markdown.evaluatedPages > 0 && markdown.stableResponses === markdown.evaluatedPages;
|
|
7721
7785
|
const tokenHeaders = markdown.pages.filter((page) => {
|
|
7722
|
-
return (page
|
|
7786
|
+
return confirmedMarkdown(page)?.markdownTokens !== void 0 && (!page.explicit || !page.negotiated || page.explicit.markdownTokens === page.negotiated.markdownTokens);
|
|
7723
7787
|
}).length;
|
|
7788
|
+
const sizedPages = markdown.pages.filter((page) => {
|
|
7789
|
+
return confirmedMarkdown(page)?.characters !== void 0;
|
|
7790
|
+
});
|
|
7791
|
+
const largePages = sizedPages.filter((page) => {
|
|
7792
|
+
return (confirmedMarkdown(page)?.characters ?? 0) > 5e4;
|
|
7793
|
+
});
|
|
7794
|
+
const veryLargePages = sizedPages.filter((page) => {
|
|
7795
|
+
return (confirmedMarkdown(page)?.characters ?? 0) > 1e5;
|
|
7796
|
+
});
|
|
7797
|
+
const maxCharacters = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.characters ?? 0));
|
|
7798
|
+
const maxEstimatedTokens = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.estimatedTokens ?? 0));
|
|
7799
|
+
const parityPages = markdown.pages.filter((page) => page.quality?.contentSketchCoverage !== null && page.quality?.contentSketchCoverage !== void 0);
|
|
7800
|
+
const parityFailures = parityPages.filter((page) => (page.quality?.contentSketchCoverage ?? 0) < .6);
|
|
7801
|
+
const tabbedPages = markdown.pages.filter((page) => (page.quality?.tabbedContent?.detectedPanels ?? 0) > 0);
|
|
7802
|
+
const tabFailures = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === false);
|
|
7803
|
+
const tabUnknown = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === null);
|
|
7724
7804
|
return [
|
|
7725
|
-
check$
|
|
7805
|
+
check$2("representations", {
|
|
7726
7806
|
id: "markdown-coverage",
|
|
7727
7807
|
status: markdown.eligibleHtmlPages === 0 ? "unknown" : coverageComplete ? "pass" : "fail",
|
|
7728
7808
|
title: coverageComplete ? "Every successful HTML page has a Markdown representation" : "Markdown representation coverage is incomplete",
|
|
@@ -7736,17 +7816,62 @@ function markdownChecks(discovery) {
|
|
|
7736
7816
|
},
|
|
7737
7817
|
urls: failedUrls.slice(0, 25)
|
|
7738
7818
|
}),
|
|
7739
|
-
check$
|
|
7819
|
+
check$2("representations", {
|
|
7740
7820
|
id: "markdown-token-estimates",
|
|
7741
|
-
status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "
|
|
7821
|
+
status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "info",
|
|
7742
7822
|
title: tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
|
|
7743
7823
|
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.`,
|
|
7744
|
-
action:
|
|
7824
|
+
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.",
|
|
7745
7825
|
urls: markdown.pages.filter((page) => {
|
|
7746
|
-
return (page
|
|
7826
|
+
return confirmedMarkdown(page)?.markdownTokens === void 0 || page.explicit !== void 0 && page.negotiated !== void 0 && page.explicit.markdownTokens !== page.negotiated.markdownTokens;
|
|
7747
7827
|
}).map((page) => page.htmlUrl).slice(0, 25)
|
|
7748
7828
|
}),
|
|
7749
|
-
check$
|
|
7829
|
+
check$2("representations", {
|
|
7830
|
+
id: "markdown-size",
|
|
7831
|
+
status: sizedPages.length === 0 ? "unknown" : largePages.length === 0 ? "pass" : "warning",
|
|
7832
|
+
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",
|
|
7833
|
+
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.`,
|
|
7834
|
+
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.",
|
|
7835
|
+
evidence: {
|
|
7836
|
+
measuredPages: sizedPages.length,
|
|
7837
|
+
reviewThresholdCharacters: 5e4,
|
|
7838
|
+
highRiskThresholdCharacters: 1e5,
|
|
7839
|
+
largePages: largePages.length,
|
|
7840
|
+
veryLargePages: veryLargePages.length,
|
|
7841
|
+
maxCharacters,
|
|
7842
|
+
maxEstimatedTokens,
|
|
7843
|
+
heuristic: true
|
|
7844
|
+
},
|
|
7845
|
+
urls: largePages.map((page) => page.htmlUrl).slice(0, 25)
|
|
7846
|
+
}),
|
|
7847
|
+
check$2("representations", {
|
|
7848
|
+
id: "markdown-content-parity",
|
|
7849
|
+
status: parityPages.length === 0 ? "unknown" : parityFailures.length === 0 ? "pass" : "warning",
|
|
7850
|
+
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",
|
|
7851
|
+
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.`,
|
|
7852
|
+
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.",
|
|
7853
|
+
evidence: {
|
|
7854
|
+
evaluatedPages: parityPages.length,
|
|
7855
|
+
failedPages: parityFailures.length,
|
|
7856
|
+
minimumCoverage: .6,
|
|
7857
|
+
sampling: "bounded-content-shingles"
|
|
7858
|
+
},
|
|
7859
|
+
urls: parityFailures.map((page) => page.htmlUrl).slice(0, 25)
|
|
7860
|
+
}),
|
|
7861
|
+
check$2("representations", {
|
|
7862
|
+
id: "markdown-tab-serialization",
|
|
7863
|
+
status: tabbedPages.length === 0 ? "notApplicable" : tabFailures.length > 0 ? "warning" : tabUnknown.length > 0 ? "unknown" : "pass",
|
|
7864
|
+
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",
|
|
7865
|
+
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.`,
|
|
7866
|
+
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.",
|
|
7867
|
+
evidence: {
|
|
7868
|
+
tabbedPages: tabbedPages.length,
|
|
7869
|
+
failedPages: tabFailures.length,
|
|
7870
|
+
unknownPages: tabUnknown.length
|
|
7871
|
+
},
|
|
7872
|
+
urls: [...tabFailures, ...tabUnknown].map((page) => page.htmlUrl).slice(0, 25)
|
|
7873
|
+
}),
|
|
7874
|
+
check$2("representations", {
|
|
7750
7875
|
id: "markdown-negotiation",
|
|
7751
7876
|
status: markdown.eligibleHtmlPages === 0 ? "unknown" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "pass" : "fail",
|
|
7752
7877
|
title: negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
|
|
@@ -7760,16 +7885,16 @@ function markdownChecks(discovery) {
|
|
|
7760
7885
|
},
|
|
7761
7886
|
urls: failedUrls.slice(0, 25)
|
|
7762
7887
|
}),
|
|
7763
|
-
check$
|
|
7888
|
+
check$2("representations", {
|
|
7764
7889
|
id: "markdown-determinism",
|
|
7765
|
-
status: markdown.
|
|
7890
|
+
status: markdown.evaluatedPages === 0 ? "unknown" : stable ? "pass" : "fail",
|
|
7766
7891
|
title: stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
|
|
7767
|
-
plainEnglish: `${markdown.stableResponses} of ${markdown.
|
|
7892
|
+
plainEnglish: `${markdown.stableResponses} of ${markdown.evaluatedPages} repeated Markdown responses kept the same SHA-256 digest.`,
|
|
7768
7893
|
action: "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
|
|
7769
7894
|
evidence: { stableResponses: markdown.stableResponses },
|
|
7770
7895
|
urls: failedUrls.slice(0, 25)
|
|
7771
7896
|
}),
|
|
7772
|
-
check$
|
|
7897
|
+
check$2("representations", {
|
|
7773
7898
|
id: "markdown-quality",
|
|
7774
7899
|
status: markdown.evaluatedPages === 0 ? "unknown" : qualityUrls.length === 0 ? "pass" : "warning",
|
|
7775
7900
|
title: qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
|
|
@@ -7779,6 +7904,50 @@ function markdownChecks(discovery) {
|
|
|
7779
7904
|
})
|
|
7780
7905
|
];
|
|
7781
7906
|
}
|
|
7907
|
+
//#endregion
|
|
7908
|
+
//#region packages/core/dist/analyze/crawler/agent-readiness.js
|
|
7909
|
+
function check$1(section, input) {
|
|
7910
|
+
return {
|
|
7911
|
+
section,
|
|
7912
|
+
...input
|
|
7913
|
+
};
|
|
7914
|
+
}
|
|
7915
|
+
function section$2(id, title, checks) {
|
|
7916
|
+
return {
|
|
7917
|
+
id,
|
|
7918
|
+
title,
|
|
7919
|
+
checks
|
|
7920
|
+
};
|
|
7921
|
+
}
|
|
7922
|
+
function profileChecks(discovery) {
|
|
7923
|
+
return [
|
|
7924
|
+
"api",
|
|
7925
|
+
"application",
|
|
7926
|
+
"commerce"
|
|
7927
|
+
].map((profile) => check$1("profile", {
|
|
7928
|
+
id: `profile-${profile}`,
|
|
7929
|
+
status: "notApplicable",
|
|
7930
|
+
title: `${profile[0]?.toUpperCase()}${profile.slice(1)} checks are not applicable`,
|
|
7931
|
+
plainEnglish: discovery.profileApplicability[profile].reason,
|
|
7932
|
+
action: "Choose this profile only when the site exposes that capability publicly.",
|
|
7933
|
+
evidence: {
|
|
7934
|
+
profile,
|
|
7935
|
+
selectedProfile: discovery.profile
|
|
7936
|
+
}
|
|
7937
|
+
}));
|
|
7938
|
+
}
|
|
7939
|
+
function summariseChecks(checks) {
|
|
7940
|
+
const count = (status) => checks.filter((item) => item.status === status).length;
|
|
7941
|
+
return {
|
|
7942
|
+
checks: checks.length,
|
|
7943
|
+
passed: count("pass"),
|
|
7944
|
+
warnings: count("warning"),
|
|
7945
|
+
failed: count("fail"),
|
|
7946
|
+
unknown: count("unknown"),
|
|
7947
|
+
information: count("info"),
|
|
7948
|
+
notApplicable: count("notApplicable")
|
|
7949
|
+
};
|
|
7950
|
+
}
|
|
7782
7951
|
function contentSignalsCheck(discovery, robotsContentSignals) {
|
|
7783
7952
|
const signals = discovery.contentSignals;
|
|
7784
7953
|
const robotsDeclared = robotsContentSignals ?? [];
|
|
@@ -7879,8 +8048,8 @@ function discoveryChecks(discovery, robotsContentSignals) {
|
|
|
7879
8048
|
check$1("discovery", {
|
|
7880
8049
|
id: "route-manifest",
|
|
7881
8050
|
status: !discovery.routeManifest.valid ? "info" : discovery.routeManifest.missingHtmlRoutes.length === 0 && discovery.routeManifest.missingMarkdownRoutes.length === 0 && discovery.routeManifest.orphanMarkdownRoutes.length === 0 ? "pass" : "warning",
|
|
7882
|
-
title: discovery.routeManifest.valid ? "The public route manifest
|
|
7883
|
-
plainEnglish: discovery.routeManifest.valid ? `The manifest declared ${discovery.routeManifest.declaredHtmlRoutes.length} HTML routes and ${discovery.routeManifest.declaredMarkdownRoutes.length} Markdown routes.
|
|
8051
|
+
title: discovery.routeManifest.valid ? "The public route manifest is valid" : "No public route manifest was available",
|
|
8052
|
+
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.",
|
|
7884
8053
|
action: "Generate one deterministic route inventory from the build when exact HTML and Markdown parity matters.",
|
|
7885
8054
|
evidence: discovery.routeManifest
|
|
7886
8055
|
})
|
|
@@ -8011,25 +8180,42 @@ function accessChecks(report, discovery) {
|
|
|
8011
8180
|
const blocked = bots.filter((bot) => bot.allowed === false);
|
|
8012
8181
|
const robotsKnown = report.ai?.robotsTxt?.availability === "available" || report.ai?.robotsTxt?.availability === "absent";
|
|
8013
8182
|
const protocol = discovery.protocolVariants;
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8183
|
+
const authenticationGates = report.issues.filter((issue) => issue.ruleId === "soft_authentication_gate");
|
|
8184
|
+
const clientRenderedPages = report.issues.filter((issue) => issue.ruleId === "client_rendered_content");
|
|
8185
|
+
return [
|
|
8186
|
+
check$1("access", {
|
|
8187
|
+
id: "crawler-access",
|
|
8188
|
+
status: !robotsKnown ? "unknown" : blocked.length > 0 ? "warning" : "pass",
|
|
8189
|
+
title: blocked.length ? "robots.txt blocks selected crawler tokens" : "No selected crawler token is blocked at the start URL",
|
|
8190
|
+
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.",
|
|
8191
|
+
action: "Keep blocks intentional. A crawler token being allowed here does not prove that the service will crawl, index, or cite the site.",
|
|
8192
|
+
evidence: {
|
|
8193
|
+
availability: report.ai?.robotsTxt?.availability,
|
|
8194
|
+
blocked,
|
|
8195
|
+
botAccess: bots
|
|
8196
|
+
}
|
|
8197
|
+
}),
|
|
8198
|
+
check$1("access", {
|
|
8199
|
+
id: "public-content-access",
|
|
8200
|
+
status: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "warning" : "pass",
|
|
8201
|
+
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",
|
|
8202
|
+
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.`,
|
|
8203
|
+
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.",
|
|
8204
|
+
evidence: {
|
|
8205
|
+
authenticationGates: authenticationGates.length,
|
|
8206
|
+
clientRenderedPages: clientRenderedPages.length
|
|
8207
|
+
},
|
|
8208
|
+
urls: [...authenticationGates, ...clientRenderedPages].map((issue) => issue.url).slice(0, 25)
|
|
8209
|
+
}),
|
|
8210
|
+
check$1("access", {
|
|
8211
|
+
id: "protocol-canonicalization",
|
|
8212
|
+
status: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "pass" : protocol.http.permanentRedirectToHttps === null ? "unknown" : "warning",
|
|
8213
|
+
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",
|
|
8214
|
+
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"}.`,
|
|
8215
|
+
action: "Use one permanent HTTP-to-HTTPS redirect and send HSTS only after HTTPS is stable across the site.",
|
|
8216
|
+
evidence: protocol
|
|
8217
|
+
})
|
|
8218
|
+
];
|
|
8033
8219
|
}
|
|
8034
8220
|
function identityChecks(report) {
|
|
8035
8221
|
const types = new Set(report.pages.flatMap((page) => page.schemaTypes ?? []));
|
|
@@ -9558,6 +9744,48 @@ const RULE_DEFINITIONS = [
|
|
|
9558
9744
|
suggestedCommands: ["seo crawl --sitemap-url <url> --health --json"]
|
|
9559
9745
|
}
|
|
9560
9746
|
},
|
|
9747
|
+
{
|
|
9748
|
+
id: "soft_authentication_gate",
|
|
9749
|
+
title: "Page appears to be an authentication gate",
|
|
9750
|
+
category: "response",
|
|
9751
|
+
defaultSeverity: "medium",
|
|
9752
|
+
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.",
|
|
9753
|
+
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.",
|
|
9754
|
+
impactIfIgnored: "A login page can be mistaken for the intended content, producing misleading crawl results and weak search or agent access.",
|
|
9755
|
+
howToVerify: "Fetch the affected URL without a session and confirm it returns the intended public page, or a deliberate authentication status for protected content.",
|
|
9756
|
+
agentHints: {
|
|
9757
|
+
evidenceFields: ["page.softAuthenticationGate", "page.finalUrl"],
|
|
9758
|
+
suggestedCommands: ["seo crawl <url> --max-pages 1 --json"]
|
|
9759
|
+
}
|
|
9760
|
+
},
|
|
9761
|
+
{
|
|
9762
|
+
id: "client_rendered_content",
|
|
9763
|
+
title: "Important content requires JavaScript rendering",
|
|
9764
|
+
category: "response",
|
|
9765
|
+
defaultSeverity: "medium",
|
|
9766
|
+
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.",
|
|
9767
|
+
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.",
|
|
9768
|
+
impactIfIgnored: "Clients that do not execute JavaScript may see an empty shell or miss important content and links.",
|
|
9769
|
+
howToVerify: "Re-run the crawl and confirm the raw and rendered content observations contain the same important document content.",
|
|
9770
|
+
agentHints: {
|
|
9771
|
+
evidenceFields: ["page.fetchDiagnostics.rendering.documentDifference", "page.usedJs"],
|
|
9772
|
+
suggestedCommands: ["seo crawl <url> --json"]
|
|
9773
|
+
}
|
|
9774
|
+
},
|
|
9775
|
+
{
|
|
9776
|
+
id: "soft_404",
|
|
9777
|
+
title: "Unknown URLs return a successful response",
|
|
9778
|
+
category: "response",
|
|
9779
|
+
defaultSeverity: "high",
|
|
9780
|
+
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.",
|
|
9781
|
+
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.",
|
|
9782
|
+
impactIfIgnored: "Missing URLs can remain in crawl queues, waste crawl attention, and create confusing search or monitoring evidence.",
|
|
9783
|
+
howToVerify: "Request several known-nonexistent paths and confirm each returns 404 or 410 without redirecting to a generic successful page.",
|
|
9784
|
+
agentHints: {
|
|
9785
|
+
evidenceFields: ["report.siteChecks.soft404"],
|
|
9786
|
+
suggestedCommands: ["seo crawl <url> --json"]
|
|
9787
|
+
}
|
|
9788
|
+
},
|
|
9561
9789
|
{
|
|
9562
9790
|
id: "client_error",
|
|
9563
9791
|
title: "Client error",
|
|
@@ -10047,6 +10275,8 @@ const RULE_RECOMMENDATIONS = {
|
|
|
10047
10275
|
x_robots_noindex: "review",
|
|
10048
10276
|
robots_blocked: "review",
|
|
10049
10277
|
crawler_access_blocked: "review",
|
|
10278
|
+
soft_authentication_gate: "review",
|
|
10279
|
+
client_rendered_content: "review",
|
|
10050
10280
|
orphan_page: "review",
|
|
10051
10281
|
redirected_url: "review",
|
|
10052
10282
|
slow_response: "review",
|
|
@@ -10137,6 +10367,28 @@ const WEAK_VALUABLE_INLINKS = 1;
|
|
|
10137
10367
|
const QUERY_COVERAGE_MIN_IMPRESSIONS = 50;
|
|
10138
10368
|
const QUERY_COVERAGE_MIN = .6;
|
|
10139
10369
|
const OVERSIZED_IMAGE_CANDIDATE_LIMIT = 2e3;
|
|
10370
|
+
const EMPTY_INITIAL_HTML_CHARACTERS = 200;
|
|
10371
|
+
const SUBSTANTIVE_RENDERED_CHARACTERS = 500;
|
|
10372
|
+
function clientRenderedContentEvidence(page) {
|
|
10373
|
+
const rendering = page.fetchDiagnostics?.rendering;
|
|
10374
|
+
const difference = rendering?.documentDifference;
|
|
10375
|
+
if (rendering?.status !== "rendered" || !difference?.changed.includes("content")) return;
|
|
10376
|
+
const rawCharacters = difference.raw.content.characters;
|
|
10377
|
+
const renderedCharacters = difference.rendered.content.characters;
|
|
10378
|
+
if (rawCharacters >= EMPTY_INITIAL_HTML_CHARACTERS || renderedCharacters < SUBSTANTIVE_RENDERED_CHARACTERS || renderedCharacters < rawCharacters * 3) return;
|
|
10379
|
+
return {
|
|
10380
|
+
rawCharacters,
|
|
10381
|
+
rawWords: difference.raw.content.wordCount,
|
|
10382
|
+
renderedCharacters,
|
|
10383
|
+
renderedWords: difference.rendered.content.wordCount,
|
|
10384
|
+
changed: difference.changed,
|
|
10385
|
+
thresholds: {
|
|
10386
|
+
emptyInitialHtmlCharacters: EMPTY_INITIAL_HTML_CHARACTERS,
|
|
10387
|
+
substantiveRenderedCharacters: SUBSTANTIVE_RENDERED_CHARACTERS,
|
|
10388
|
+
minimumGrowthRatio: 3
|
|
10389
|
+
}
|
|
10390
|
+
};
|
|
10391
|
+
}
|
|
10140
10392
|
function issue(ruleId, page, detail, evidence) {
|
|
10141
10393
|
const rule = explainRule(ruleId);
|
|
10142
10394
|
if (!rule) throw new Error(`Missing rule guidance for ${ruleId}`);
|
|
@@ -10351,6 +10603,9 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
10351
10603
|
}));
|
|
10352
10604
|
if (page.auditScope === "status") continue;
|
|
10353
10605
|
if (redirected) continue;
|
|
10606
|
+
if (page.softAuthenticationGate) issues.push(issue("soft_authentication_gate", page, "A password form and login-specific page signals were observed.", page.softAuthenticationGate));
|
|
10607
|
+
const renderedContent = clientRenderedContentEvidence(page);
|
|
10608
|
+
if (renderedContent) issues.push(issue("client_rendered_content", page, "The raw response was an empty or near-empty shell.", renderedContent));
|
|
10354
10609
|
if (isHtmlPage(page) && (page.sizeBytes ?? 0) > LARGE_HTML_BYTES) issues.push(issue("large_html", page, `${Math.round((page.sizeBytes ?? 0) / 1024)} KB`, {
|
|
10355
10610
|
sizeBytes: page.sizeBytes,
|
|
10356
10611
|
thresholdBytes: LARGE_HTML_BYTES
|
|
@@ -10955,7 +11210,7 @@ function createCrawlReport(input) {
|
|
|
10955
11210
|
const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
|
|
10956
11211
|
const requestEvidenceStatus = input.requestEvidenceStatus ?? (input.requests ? "available" : "unavailable");
|
|
10957
11212
|
if (requestEvidenceStatus === "unavailable" && requests.length > 0) throw new Error("Unavailable request evidence cannot include request observations.");
|
|
10958
|
-
const safeIssues = sanitizeIssues(input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })]);
|
|
11213
|
+
const safeIssues = sanitizeIssues([...input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })], ...input.additionalIssues ?? []]);
|
|
10959
11214
|
const pages = safePagesWithLinks;
|
|
10960
11215
|
return {
|
|
10961
11216
|
id: input.id ?? crawlRunId(),
|
|
@@ -10984,6 +11239,7 @@ function createCrawlReport(input) {
|
|
|
10984
11239
|
...input.ai ? { ai: sanitizeTenantValue(input.ai) } : {},
|
|
10985
11240
|
...input.sitemapDiscovery ? { sitemapDiscovery: sanitizeTenantValue(input.sitemapDiscovery) } : {},
|
|
10986
11241
|
...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
|
|
11242
|
+
...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
|
|
10987
11243
|
...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
|
|
10988
11244
|
warnings: sanitizeMessages(input.warnings).sort(compareText$13),
|
|
10989
11245
|
caveats: sanitizeMessages(input.caveats).sort(compareText$13)
|
|
@@ -11544,6 +11800,75 @@ function observationFromPage(requestedUrl, page) {
|
|
|
11544
11800
|
};
|
|
11545
11801
|
}
|
|
11546
11802
|
//#endregion
|
|
11803
|
+
//#region packages/core/dist/analyze/crawler/site-checks.js
|
|
11804
|
+
const PROBE_COUNT = 2;
|
|
11805
|
+
function probeUrls(startUrl) {
|
|
11806
|
+
const origin = new URL(startUrl).origin;
|
|
11807
|
+
const seed = createHash("sha256").update(origin).digest("hex").slice(0, 12);
|
|
11808
|
+
return Array.from({ length: PROBE_COUNT }, (_, index) => new URL(`/.well-known/seo-audit/not-found-${seed}-${index + 1}`, origin).toString());
|
|
11809
|
+
}
|
|
11810
|
+
function probeObservation(url, result) {
|
|
11811
|
+
const request = result.request;
|
|
11812
|
+
if (!request) return {
|
|
11813
|
+
url,
|
|
11814
|
+
error: result.warning ?? "No response evidence."
|
|
11815
|
+
};
|
|
11816
|
+
if (request.outcome === "response") return {
|
|
11817
|
+
url,
|
|
11818
|
+
status: request.status,
|
|
11819
|
+
finalUrl: request.finalUrl,
|
|
11820
|
+
redirected: request.finalUrl !== url,
|
|
11821
|
+
accessBlocked: Boolean(request.accessBlock)
|
|
11822
|
+
};
|
|
11823
|
+
return {
|
|
11824
|
+
url,
|
|
11825
|
+
error: request.outcome === "failure" ? request.error : `Probe skipped: ${request.reason}`
|
|
11826
|
+
};
|
|
11827
|
+
}
|
|
11828
|
+
async function collectCrawlSiteChecks(input) {
|
|
11829
|
+
const probes = [];
|
|
11830
|
+
for (const url of probeUrls(input.startUrl)) {
|
|
11831
|
+
if (input.signal?.aborted) break;
|
|
11832
|
+
const result = await input.fetchStatusPage(url, {
|
|
11833
|
+
js: "off",
|
|
11834
|
+
refresh: true,
|
|
11835
|
+
timeoutMs: input.timeoutMs,
|
|
11836
|
+
signal: input.signal,
|
|
11837
|
+
writeCache: false,
|
|
11838
|
+
respectRobots: input.respectRobots
|
|
11839
|
+
});
|
|
11840
|
+
probes.push(probeObservation(url, result));
|
|
11841
|
+
}
|
|
11842
|
+
const conclusive = probes.filter((probe) => probe.status !== void 0 && !probe.accessBlocked);
|
|
11843
|
+
const successful = conclusive.filter((probe) => (probe.status ?? 0) >= 200 && (probe.status ?? 0) < 400);
|
|
11844
|
+
const correctMissing = conclusive.filter((probe) => probe.status === 404 || probe.status === 410);
|
|
11845
|
+
const status = conclusive.length !== PROBE_COUNT ? "unknown" : successful.length === PROBE_COUNT ? "warning" : correctMissing.length === PROBE_COUNT ? "pass" : "unknown";
|
|
11846
|
+
const checks = { soft404: {
|
|
11847
|
+
status,
|
|
11848
|
+
probes,
|
|
11849
|
+
probeLimit: PROBE_COUNT,
|
|
11850
|
+
complete: probes.length === PROBE_COUNT
|
|
11851
|
+
} };
|
|
11852
|
+
if (status !== "warning") return {
|
|
11853
|
+
checks,
|
|
11854
|
+
issues: []
|
|
11855
|
+
};
|
|
11856
|
+
const rule = explainRule("soft_404");
|
|
11857
|
+
if (!rule) throw new Error("Missing rule guidance for soft_404");
|
|
11858
|
+
return {
|
|
11859
|
+
checks,
|
|
11860
|
+
issues: [{
|
|
11861
|
+
ruleId: "soft_404",
|
|
11862
|
+
title: rule.title,
|
|
11863
|
+
category: rule.category,
|
|
11864
|
+
severity: rule.defaultSeverity,
|
|
11865
|
+
url: new URL("/", input.startUrl).toString(),
|
|
11866
|
+
detail: `${successful.length} known-nonexistent paths returned a successful response or redirect.`,
|
|
11867
|
+
evidence: checks.soft404
|
|
11868
|
+
}]
|
|
11869
|
+
};
|
|
11870
|
+
}
|
|
11871
|
+
//#endregion
|
|
11547
11872
|
//#region packages/core/dist/analyze/crawler/site-crawl-ai-signals.js
|
|
11548
11873
|
const MAX_AUXILIARY_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
11549
11874
|
const AI_BOT_USER_AGENTS = [
|
|
@@ -12263,6 +12588,9 @@ async function crawlOne(url, opts = {}) {
|
|
|
12263
12588
|
mainContentHash: hashText(extracted.contentText),
|
|
12264
12589
|
textRatio: fetched.html.length ? Math.min(1, extracted.contentText.length / fetched.html.length) : void 0,
|
|
12265
12590
|
contentSample: truncate$1(extracted.contentText, 300),
|
|
12591
|
+
contentSketch: crawlerEvidence.contentSketch,
|
|
12592
|
+
softAuthenticationGate: crawlerEvidence.softAuthenticationGate,
|
|
12593
|
+
tabbedContent: crawlerEvidence.tabbedContent,
|
|
12266
12594
|
lang: extracted.lang,
|
|
12267
12595
|
hasViewport: extracted.hasViewport,
|
|
12268
12596
|
isHttps: new URL(extracted.finalUrl).protocol === "https:",
|
|
@@ -13547,6 +13875,13 @@ async function crawlSite(input, dependencies) {
|
|
|
13547
13875
|
signal,
|
|
13548
13876
|
concurrency: Math.min(config.concurrency, 4)
|
|
13549
13877
|
}) : void 0;
|
|
13878
|
+
const siteCheckResult = !healthStrategy && !cancelled && !stoppedForMemory ? await collectCrawlSiteChecks({
|
|
13879
|
+
startUrl: config.url,
|
|
13880
|
+
timeoutMs: config.timeoutMs,
|
|
13881
|
+
fetchStatusPage: deps.fetchStatusPage,
|
|
13882
|
+
respectRobots: config.respectRobots,
|
|
13883
|
+
signal
|
|
13884
|
+
}) : void 0;
|
|
13550
13885
|
for (const page of pages) {
|
|
13551
13886
|
if (!page.geo || !llmsTxt) continue;
|
|
13552
13887
|
page.geo = {
|
|
@@ -13572,6 +13907,10 @@ async function crawlSite(input, dependencies) {
|
|
|
13572
13907
|
} } : {},
|
|
13573
13908
|
...sitemapDiscovery ? { sitemapDiscovery } : {},
|
|
13574
13909
|
...externalLinkVerification ? { externalLinkVerification } : {},
|
|
13910
|
+
...siteCheckResult ? {
|
|
13911
|
+
siteChecks: siteCheckResult.checks,
|
|
13912
|
+
additionalIssues: siteCheckResult.issues
|
|
13913
|
+
} : {},
|
|
13575
13914
|
...agentDiscovery ? { agentDiscovery } : {},
|
|
13576
13915
|
status: reportStatus,
|
|
13577
13916
|
warnings,
|
|
@@ -14452,6 +14791,8 @@ const agentRepresentationResponseSchema = z.object({
|
|
|
14452
14791
|
status: z.number().int().optional(),
|
|
14453
14792
|
contentType: z.string().optional(),
|
|
14454
14793
|
bytes: z.number().int().nonnegative().optional(),
|
|
14794
|
+
characters: z.number().int().nonnegative().optional(),
|
|
14795
|
+
estimatedTokens: z.number().int().nonnegative().optional(),
|
|
14455
14796
|
sha256: z.string().optional(),
|
|
14456
14797
|
canonicalUrl: z.string().url().optional(),
|
|
14457
14798
|
alternateUrl: z.string().url().optional(),
|
|
@@ -14509,7 +14850,15 @@ const agentDiscoverySchema = z.object({
|
|
|
14509
14850
|
sourceWordCount: z.number().int().nonnegative(),
|
|
14510
14851
|
wordRetentionRatio: z.number().nonnegative().nullable(),
|
|
14511
14852
|
introductoryCopyRetained: z.boolean().nullable(),
|
|
14512
|
-
navigationOnly: z.boolean()
|
|
14853
|
+
navigationOnly: z.boolean(),
|
|
14854
|
+
contentSketchCoverage: z.number().min(0).max(1).nullable().optional(),
|
|
14855
|
+
tabbedContent: z.object({
|
|
14856
|
+
detectedPanels: z.number().int().nonnegative(),
|
|
14857
|
+
evaluatedPanels: z.number().int().nonnegative(),
|
|
14858
|
+
retainedPanels: z.number().int().nonnegative(),
|
|
14859
|
+
missingPanels: z.number().int().nonnegative(),
|
|
14860
|
+
complete: z.boolean().nullable()
|
|
14861
|
+
}).optional()
|
|
14513
14862
|
}).optional()
|
|
14514
14863
|
}))
|
|
14515
14864
|
}),
|
|
@@ -14528,6 +14877,11 @@ const agentDiscoverySchema = z.object({
|
|
|
14528
14877
|
missingHtmlRoutes: z.array(z.string()),
|
|
14529
14878
|
missingMarkdownRoutes: z.array(z.string()),
|
|
14530
14879
|
orphanMarkdownRoutes: z.array(z.string()),
|
|
14880
|
+
comparisonStatus: z.enum([
|
|
14881
|
+
"complete",
|
|
14882
|
+
"partial",
|
|
14883
|
+
"not-applicable"
|
|
14884
|
+
]).optional(),
|
|
14531
14885
|
error: z.string().optional()
|
|
14532
14886
|
}),
|
|
14533
14887
|
agentSkills: z.object({
|
|
@@ -14633,6 +14987,25 @@ const agentDiscoverySchema = z.object({
|
|
|
14633
14987
|
warnings: z.array(z.string())
|
|
14634
14988
|
});
|
|
14635
14989
|
//#endregion
|
|
14990
|
+
//#region packages/core/dist/analyze/crawler/site-checks-schema.js
|
|
14991
|
+
const crawlSiteChecksSchema = z.object({ soft404: z.object({
|
|
14992
|
+
status: z.enum([
|
|
14993
|
+
"pass",
|
|
14994
|
+
"warning",
|
|
14995
|
+
"unknown"
|
|
14996
|
+
]),
|
|
14997
|
+
probes: z.array(z.object({
|
|
14998
|
+
url: z.string().url(),
|
|
14999
|
+
status: z.number().int().optional(),
|
|
15000
|
+
finalUrl: z.string().url().optional(),
|
|
15001
|
+
redirected: z.boolean().optional(),
|
|
15002
|
+
accessBlocked: z.boolean().optional(),
|
|
15003
|
+
error: z.string().optional()
|
|
15004
|
+
})),
|
|
15005
|
+
probeLimit: z.number().int().positive(),
|
|
15006
|
+
complete: z.boolean()
|
|
15007
|
+
}) });
|
|
15008
|
+
//#endregion
|
|
14636
15009
|
//#region packages/core/dist/analyze/crawler/schemas.js
|
|
14637
15010
|
const crawlerRuleSeveritySchema = z.enum([
|
|
14638
15011
|
"low",
|
|
@@ -15062,6 +15435,34 @@ const crawlPageSnapshotSchema = z.object({
|
|
|
15062
15435
|
mainContentHash: z.string().optional(),
|
|
15063
15436
|
textRatio: z.number().min(0).max(1).optional(),
|
|
15064
15437
|
contentSample: z.string().optional(),
|
|
15438
|
+
contentSketch: z.object({
|
|
15439
|
+
version: z.literal(1),
|
|
15440
|
+
wordCount: z.number().int().nonnegative(),
|
|
15441
|
+
shingleSize: z.number().int().positive(),
|
|
15442
|
+
sampledShingles: z.number().int().nonnegative(),
|
|
15443
|
+
hashes: z.array(z.string())
|
|
15444
|
+
}).optional(),
|
|
15445
|
+
softAuthenticationGate: z.object({
|
|
15446
|
+
kind: z.literal("login-form"),
|
|
15447
|
+
indicators: z.array(z.string()),
|
|
15448
|
+
formActionPath: z.string().optional()
|
|
15449
|
+
}).optional(),
|
|
15450
|
+
tabbedContent: z.object({
|
|
15451
|
+
groups: z.number().int().positive(),
|
|
15452
|
+
panels: z.number().int().positive(),
|
|
15453
|
+
retainedPanels: z.number().int().nonnegative(),
|
|
15454
|
+
truncated: z.boolean(),
|
|
15455
|
+
panelSketches: z.array(z.object({
|
|
15456
|
+
label: z.string().optional(),
|
|
15457
|
+
sketch: z.object({
|
|
15458
|
+
version: z.literal(1),
|
|
15459
|
+
wordCount: z.number().int().nonnegative(),
|
|
15460
|
+
shingleSize: z.number().int().positive(),
|
|
15461
|
+
sampledShingles: z.number().int().nonnegative(),
|
|
15462
|
+
hashes: z.array(z.string())
|
|
15463
|
+
})
|
|
15464
|
+
}))
|
|
15465
|
+
}).optional(),
|
|
15065
15466
|
lang: z.string().optional(),
|
|
15066
15467
|
hasViewport: z.boolean().optional(),
|
|
15067
15468
|
isHttps: z.boolean().optional(),
|
|
@@ -15553,6 +15954,7 @@ const crawlReportBaseSchema = z.object({
|
|
|
15553
15954
|
ai: crawlAiSignalsSchema.optional(),
|
|
15554
15955
|
sitemapDiscovery: crawlSitemapDiscoverySchema.optional(),
|
|
15555
15956
|
externalLinkVerification: crawlExternalLinkVerificationSchema.optional(),
|
|
15957
|
+
siteChecks: crawlSiteChecksSchema.optional(),
|
|
15556
15958
|
agentDiscovery: agentDiscoverySchema.optional(),
|
|
15557
15959
|
warnings: z.array(z.string()),
|
|
15558
15960
|
caveats: z.array(z.string())
|