seo 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +138 -161
- package/dist/cli.js.map +1 -1
- package/dist/{dist-D0EI_L8C.js → dist-Bc8MJ1gT.js} +1 -1
- package/dist/{dist-DaHrxVkW.js → dist-sYeCtY3Q.js} +87 -87
- package/dist/dist-sYeCtY3Q.js.map +1 -0
- package/dist/index.d.ts +870 -13
- package/dist/index.js +575 -123
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +575 -123
- package/dist/mcp.js.map +1 -1
- package/dist/{package-B-M-4PZ_.js → package-Cp8DwqCy.js} +2 -2
- package/dist/{package-B-M-4PZ_.js.map → package-Cp8DwqCy.js.map} +1 -1
- package/package.json +1 -1
- package/dist/dist-DaHrxVkW.js.map +0 -1
package/dist/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-Cp8DwqCy.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);
|
|
@@ -2741,6 +2778,22 @@ function extractStructuredData($, baseUrl, options = {}) {
|
|
|
2741
2778
|
}
|
|
2742
2779
|
//#endregion
|
|
2743
2780
|
//#region packages/core/dist/extract/page-extractor.js
|
|
2781
|
+
function metaRefresh($, finalUrl) {
|
|
2782
|
+
for (const element of $("meta[http-equiv]").toArray()) {
|
|
2783
|
+
if ($(element).attr("http-equiv")?.trim().toLowerCase() !== "refresh") continue;
|
|
2784
|
+
const match = ($(element).attr("content")?.trim())?.match(/^([0-9]+(?:\.[0-9]+)?)\s*;\s*url\s*=\s*(?:"([^"]*)"|'([^']*)'|(.+))$/iu);
|
|
2785
|
+
if (!match) continue;
|
|
2786
|
+
try {
|
|
2787
|
+
const target = new URL((match[2] ?? match[3] ?? match[4] ?? "").trim(), finalUrl);
|
|
2788
|
+
if (!["http:", "https:"].includes(target.protocol)) continue;
|
|
2789
|
+
target.hash = "";
|
|
2790
|
+
return {
|
|
2791
|
+
targetUrl: target.toString(),
|
|
2792
|
+
delaySeconds: Number(match[1])
|
|
2793
|
+
};
|
|
2794
|
+
} catch {}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2744
2797
|
const SOCIAL_PROFILE_HOSTS = [
|
|
2745
2798
|
"facebook.com",
|
|
2746
2799
|
"instagram.com",
|
|
@@ -2751,6 +2804,48 @@ const SOCIAL_PROFILE_HOSTS = [
|
|
|
2751
2804
|
"x.com",
|
|
2752
2805
|
"youtube.com"
|
|
2753
2806
|
];
|
|
2807
|
+
const LOGIN_PATH_PATTERN = /(?:^|\/)(?:auth|login|log-in|signin|sign-in|sso)(?:\/|$)/iu;
|
|
2808
|
+
const LOGIN_TITLE_PATTERN = /\b(?:authenticate|log[ -]?in|sign[ -]?in)\b/iu;
|
|
2809
|
+
function softAuthenticationGate($, finalUrl, title) {
|
|
2810
|
+
const passwordForms = $("form").toArray().filter((form) => $(form).find("input[type=\"password\" i]").length > 0);
|
|
2811
|
+
if (passwordForms.length === 0) return void 0;
|
|
2812
|
+
const pagePath = new URL(finalUrl).pathname;
|
|
2813
|
+
const action = $(passwordForms[0]).attr("action");
|
|
2814
|
+
const actionUrl = action ? httpUrl$1(action, finalUrl) : void 0;
|
|
2815
|
+
const actionPath = actionUrl ? new URL(actionUrl).pathname : void 0;
|
|
2816
|
+
const indicators = [
|
|
2817
|
+
...LOGIN_PATH_PATTERN.test(pagePath) ? ["login-path"] : [],
|
|
2818
|
+
...LOGIN_TITLE_PATTERN.test(title ?? "") ? ["login-title"] : [],
|
|
2819
|
+
...LOGIN_PATH_PATTERN.test(actionPath ?? "") ? ["login-form-action"] : []
|
|
2820
|
+
];
|
|
2821
|
+
if (indicators.length === 0) return void 0;
|
|
2822
|
+
return {
|
|
2823
|
+
kind: "login-form",
|
|
2824
|
+
indicators,
|
|
2825
|
+
...actionPath ? { formActionPath: actionPath } : {}
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
function tabbedContent($) {
|
|
2829
|
+
const groups = $("[role=\"tablist\" i]").length;
|
|
2830
|
+
const panels = $("[role=\"tabpanel\" i]").toArray();
|
|
2831
|
+
if (groups === 0 || panels.length === 0) return void 0;
|
|
2832
|
+
const panelSketches = panels.slice(0, 12).map((panel) => {
|
|
2833
|
+
const labelledBy = $(panel).attr("aria-labelledby");
|
|
2834
|
+
const labelledElement = labelledBy ? $("[id]").toArray().find((element) => $(element).attr("id") === labelledBy) : void 0;
|
|
2835
|
+
const label = safeText$1($(panel).attr("aria-label") ?? (labelledElement ? $(labelledElement).text() : void 0));
|
|
2836
|
+
return {
|
|
2837
|
+
...label ? { label: label.slice(0, 80) } : {},
|
|
2838
|
+
sketch: contentSketch($(panel).text())
|
|
2839
|
+
};
|
|
2840
|
+
});
|
|
2841
|
+
return {
|
|
2842
|
+
groups,
|
|
2843
|
+
panels: panels.length,
|
|
2844
|
+
retainedPanels: panelSketches.length,
|
|
2845
|
+
truncated: panels.length > panelSketches.length,
|
|
2846
|
+
panelSketches
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2754
2849
|
function safeText$1(value) {
|
|
2755
2850
|
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
2756
2851
|
return trimmed ? trimmed : void 0;
|
|
@@ -2977,10 +3072,13 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
2977
3072
|
html: sanitizedContentHtml($, fetchResult.html, fetchResult.finalUrl)
|
|
2978
3073
|
}, extractor, dependencies);
|
|
2979
3074
|
const { text, excerpt } = content;
|
|
3075
|
+
const title = safeText$1($("title").first().text());
|
|
3076
|
+
const crawlerMetaRefresh = extractor === "crawler" ? metaRefresh($, fetchResult.finalUrl) : void 0;
|
|
3077
|
+
if (crawlerMetaRefresh && new URL(crawlerMetaRefresh.targetUrl).origin === url.origin) crawlerInternalLinks.add(crawlerMetaRefresh.targetUrl);
|
|
2980
3078
|
return {
|
|
2981
3079
|
url: fetchResult.url,
|
|
2982
3080
|
finalUrl: fetchResult.finalUrl,
|
|
2983
|
-
title
|
|
3081
|
+
title,
|
|
2984
3082
|
metaDescription: safeText$1($("meta[name=\"description\"]").attr("content")),
|
|
2985
3083
|
metaRobots,
|
|
2986
3084
|
xRobotsTag: safeText$1(headerValue$4(fetchResult.headers, "x-robots-tag")),
|
|
@@ -3000,7 +3098,11 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
|
|
|
3000
3098
|
externalLinks: [...crawlerExternalLinks],
|
|
3001
3099
|
internalAnchorSamples,
|
|
3002
3100
|
externalAnchorSamples,
|
|
3003
|
-
socialProfileLinks: [...socialProfileLinks]
|
|
3101
|
+
socialProfileLinks: [...socialProfileLinks],
|
|
3102
|
+
contentSketch: contentSketch(text),
|
|
3103
|
+
metaRefresh: crawlerMetaRefresh,
|
|
3104
|
+
softAuthenticationGate: softAuthenticationGate($, fetchResult.finalUrl, title),
|
|
3105
|
+
tabbedContent: tabbedContent($)
|
|
3004
3106
|
} } : {},
|
|
3005
3107
|
markdownAlternates,
|
|
3006
3108
|
hreflang,
|
|
@@ -7105,6 +7207,8 @@ function markdownQuality(markdown, page) {
|
|
|
7105
7207
|
const codeFences = markdown.match(/^```/gmu)?.length ?? 0;
|
|
7106
7208
|
const wordCount = bodyWordCount(markdown);
|
|
7107
7209
|
const sourceWordCount = page.wordCount ?? 0;
|
|
7210
|
+
const evaluatedPanels = (page.tabbedContent?.panelSketches ?? []).map((panel) => contentSketchCoverage(panel.sketch, markdown)).filter((coverage) => coverage !== null);
|
|
7211
|
+
const retainedPanels = evaluatedPanels.filter((coverage) => coverage >= .6).length;
|
|
7108
7212
|
return {
|
|
7109
7213
|
frontmatterTitle: /^---[\s\S]*?^title:\s*.+$/mu.test(markdown),
|
|
7110
7214
|
h1Count: markdown.match(/^#\s+\S.+$/gmu)?.length ?? 0,
|
|
@@ -7121,7 +7225,15 @@ function markdownQuality(markdown, page) {
|
|
|
7121
7225
|
sourceWordCount,
|
|
7122
7226
|
wordRetentionRatio: sourceWordCount > 0 ? Math.round(wordCount / sourceWordCount * 1e3) / 1e3 : null,
|
|
7123
7227
|
introductoryCopyRetained: introProbe ? normalizedMarkdown.includes(introProbe) : null,
|
|
7124
|
-
navigationOnly: bodyWordCount(markdown) < 25
|
|
7228
|
+
navigationOnly: bodyWordCount(markdown) < 25,
|
|
7229
|
+
contentSketchCoverage: contentSketchCoverage(page.contentSketch, markdown),
|
|
7230
|
+
tabbedContent: {
|
|
7231
|
+
detectedPanels: page.tabbedContent?.panels ?? 0,
|
|
7232
|
+
evaluatedPanels: evaluatedPanels.length,
|
|
7233
|
+
retainedPanels,
|
|
7234
|
+
missingPanels: Math.max(0, evaluatedPanels.length - retainedPanels),
|
|
7235
|
+
complete: evaluatedPanels.length === 0 ? null : retainedPanels === evaluatedPanels.length
|
|
7236
|
+
}
|
|
7125
7237
|
};
|
|
7126
7238
|
}
|
|
7127
7239
|
function combinedSignal(timeoutMs, signal) {
|
|
@@ -7175,6 +7287,8 @@ async function fetchRepresentation(input) {
|
|
|
7175
7287
|
status: response.status,
|
|
7176
7288
|
contentType: response.headers.get("content-type") ?? void 0,
|
|
7177
7289
|
bytes: Buffer.byteLength(body),
|
|
7290
|
+
characters: body.length,
|
|
7291
|
+
estimatedTokens: Math.ceil(body.length / 4),
|
|
7178
7292
|
sha256: sha256(body),
|
|
7179
7293
|
canonicalUrl: link.find((entry) => entry.rel.includes("canonical"))?.url,
|
|
7180
7294
|
alternateUrl: link.find((entry) => entry.rel.includes("alternate") && entry.type === "text/markdown")?.url,
|
|
@@ -7233,11 +7347,15 @@ async function inspectMarkdownPage(input) {
|
|
|
7233
7347
|
if (explicit) observation.explicit = explicit.observation;
|
|
7234
7348
|
observation.negotiated = negotiated.observation;
|
|
7235
7349
|
observation.repeated = repeated.observation;
|
|
7236
|
-
|
|
7237
|
-
const
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7350
|
+
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 ?? "");
|
|
7351
|
+
const explicitMarkdown = isMarkdown(explicit) ? explicit : void 0;
|
|
7352
|
+
const negotiatedMarkdown = isMarkdown(negotiated) ? negotiated : void 0;
|
|
7353
|
+
const repeatedMarkdown = isMarkdown(repeated) ? repeated : void 0;
|
|
7354
|
+
observation.explicitMatchesNegotiated = explicitMarkdown?.observation.sha256 && negotiatedMarkdown?.observation.sha256 ? explicitMarkdown.observation.sha256 === negotiatedMarkdown.observation.sha256 : null;
|
|
7355
|
+
const primary = explicitMarkdown ?? negotiatedMarkdown;
|
|
7356
|
+
observation.repeatedHashStable = primary?.observation.sha256 && repeatedMarkdown?.observation.sha256 ? primary.observation.sha256 === repeatedMarkdown.observation.sha256 : null;
|
|
7357
|
+
observation.markdownCanonicalMatchesHtml = sameDocument(primary?.observation.canonicalUrl, input.page.finalUrl);
|
|
7358
|
+
if (primary?.body) observation.quality = markdownQuality(primary.body, input.page);
|
|
7241
7359
|
return observation;
|
|
7242
7360
|
}
|
|
7243
7361
|
async function fetchText(input) {
|
|
@@ -7481,17 +7599,16 @@ async function inspectRouteManifest(input) {
|
|
|
7481
7599
|
const declared = Array.isArray(parsed.pages) ? parsed.pages : [];
|
|
7482
7600
|
const declaredHtmlRoutes = declared.map((page) => page.htmlPath).filter((path) => typeof path === "string").sort();
|
|
7483
7601
|
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
7602
|
return {
|
|
7487
7603
|
url,
|
|
7488
7604
|
status: result.response.status,
|
|
7489
7605
|
valid: Array.isArray(parsed.pages),
|
|
7490
7606
|
declaredHtmlRoutes,
|
|
7491
7607
|
declaredMarkdownRoutes,
|
|
7492
|
-
missingHtmlRoutes:
|
|
7493
|
-
missingMarkdownRoutes:
|
|
7494
|
-
orphanMarkdownRoutes: [
|
|
7608
|
+
missingHtmlRoutes: [],
|
|
7609
|
+
missingMarkdownRoutes: [],
|
|
7610
|
+
orphanMarkdownRoutes: [],
|
|
7611
|
+
comparisonStatus: "partial"
|
|
7495
7612
|
};
|
|
7496
7613
|
} catch (error) {
|
|
7497
7614
|
return {
|
|
@@ -7502,6 +7619,7 @@ async function inspectRouteManifest(input) {
|
|
|
7502
7619
|
missingHtmlRoutes: [],
|
|
7503
7620
|
missingMarkdownRoutes: [],
|
|
7504
7621
|
orphanMarkdownRoutes: [],
|
|
7622
|
+
comparisonStatus: "not-applicable",
|
|
7505
7623
|
error: safeError(error)
|
|
7506
7624
|
};
|
|
7507
7625
|
}
|
|
@@ -7545,9 +7663,7 @@ async function collectAgentDiscovery(input) {
|
|
|
7545
7663
|
}),
|
|
7546
7664
|
inspectRouteManifest({
|
|
7547
7665
|
...input,
|
|
7548
|
-
origin
|
|
7549
|
-
pages,
|
|
7550
|
-
observations
|
|
7666
|
+
origin
|
|
7551
7667
|
}),
|
|
7552
7668
|
inspectAgentEndpoints({
|
|
7553
7669
|
...input,
|
|
@@ -7652,20 +7768,13 @@ async function collectAgentDiscovery(input) {
|
|
|
7652
7768
|
};
|
|
7653
7769
|
}
|
|
7654
7770
|
//#endregion
|
|
7655
|
-
//#region packages/core/dist/analyze/crawler/agent-readiness.js
|
|
7656
|
-
function check$
|
|
7771
|
+
//#region packages/core/dist/analyze/crawler/agent-readiness-markdown.js
|
|
7772
|
+
function check$2(section, input) {
|
|
7657
7773
|
return {
|
|
7658
7774
|
section,
|
|
7659
7775
|
...input
|
|
7660
7776
|
};
|
|
7661
7777
|
}
|
|
7662
|
-
function section$2(id, title, checks) {
|
|
7663
|
-
return {
|
|
7664
|
-
id,
|
|
7665
|
-
title,
|
|
7666
|
-
checks
|
|
7667
|
-
};
|
|
7668
|
-
}
|
|
7669
7778
|
function failedMarkdownUrls(discovery) {
|
|
7670
7779
|
return discovery.markdownAlternates.pages.filter((page) => {
|
|
7671
7780
|
const negotiatedOk = page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "") && page.negotiated.varyAccept;
|
|
@@ -7673,112 +7782,200 @@ function failedMarkdownUrls(discovery) {
|
|
|
7673
7782
|
return !negotiatedOk || !explicitOk || page.repeatedHashStable !== true;
|
|
7674
7783
|
}).map((page) => page.htmlUrl);
|
|
7675
7784
|
}
|
|
7785
|
+
function unstableMarkdownUrls(discovery) {
|
|
7786
|
+
return discovery.markdownAlternates.pages.filter((page) => confirmedMarkdown(page) !== void 0 && page.repeatedHashStable !== true).map((page) => page.htmlUrl);
|
|
7787
|
+
}
|
|
7676
7788
|
function qualityFailureUrls(discovery) {
|
|
7677
7789
|
return discovery.markdownAlternates.pages.filter((page) => {
|
|
7678
7790
|
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;
|
|
7791
|
+
if (!quality) return false;
|
|
7792
|
+
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
7793
|
}).map((page) => page.htmlUrl);
|
|
7682
7794
|
}
|
|
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
|
-
};
|
|
7795
|
+
function confirmedMarkdown(page) {
|
|
7796
|
+
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
7797
|
}
|
|
7712
7798
|
function markdownChecks(discovery) {
|
|
7713
7799
|
const markdown = discovery.markdownAlternates;
|
|
7714
7800
|
const failedUrls = failedMarkdownUrls(discovery);
|
|
7801
|
+
const unstableUrls = unstableMarkdownUrls(discovery);
|
|
7715
7802
|
const qualityUrls = qualityFailureUrls(discovery);
|
|
7803
|
+
const hasEligibleHtml = markdown.eligibleHtmlPages > 0;
|
|
7804
|
+
const hasEvaluatedMarkdown = markdown.evaluatedPages > 0;
|
|
7716
7805
|
const negotiatedPages = markdown.pages.filter((page) => page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "")).length;
|
|
7717
7806
|
const explicitPages = markdown.pages.filter((page) => page.htmlAlternateUnique).length;
|
|
7718
7807
|
const coverageComplete = markdown.eligibleHtmlPages > 0 && markdown.evaluatedPages === markdown.eligibleHtmlPages;
|
|
7719
7808
|
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.
|
|
7809
|
+
const stable = markdown.evaluatedPages > 0 && markdown.stableResponses === markdown.evaluatedPages;
|
|
7721
7810
|
const tokenHeaders = markdown.pages.filter((page) => {
|
|
7722
|
-
return (page
|
|
7811
|
+
return confirmedMarkdown(page)?.markdownTokens !== void 0 && (!page.explicit || !page.negotiated || page.explicit.markdownTokens === page.negotiated.markdownTokens);
|
|
7723
7812
|
}).length;
|
|
7813
|
+
const sizedPages = markdown.pages.filter((page) => {
|
|
7814
|
+
return confirmedMarkdown(page)?.characters !== void 0;
|
|
7815
|
+
});
|
|
7816
|
+
const largePages = sizedPages.filter((page) => {
|
|
7817
|
+
return (confirmedMarkdown(page)?.characters ?? 0) > 5e4;
|
|
7818
|
+
});
|
|
7819
|
+
const veryLargePages = sizedPages.filter((page) => {
|
|
7820
|
+
return (confirmedMarkdown(page)?.characters ?? 0) > 1e5;
|
|
7821
|
+
});
|
|
7822
|
+
const maxCharacters = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.characters ?? 0));
|
|
7823
|
+
const maxEstimatedTokens = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.estimatedTokens ?? 0));
|
|
7824
|
+
const parityPages = markdown.pages.filter((page) => page.quality?.contentSketchCoverage !== null && page.quality?.contentSketchCoverage !== void 0);
|
|
7825
|
+
const parityFailures = parityPages.filter((page) => (page.quality?.contentSketchCoverage ?? 0) < .6);
|
|
7826
|
+
const tabbedPages = markdown.pages.filter((page) => (page.quality?.tabbedContent?.detectedPanels ?? 0) > 0);
|
|
7827
|
+
const tabFailures = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === false);
|
|
7828
|
+
const tabUnknown = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === null);
|
|
7724
7829
|
return [
|
|
7725
|
-
check$
|
|
7830
|
+
check$2("representations", {
|
|
7726
7831
|
id: "markdown-coverage",
|
|
7727
7832
|
status: markdown.eligibleHtmlPages === 0 ? "unknown" : coverageComplete ? "pass" : "fail",
|
|
7728
|
-
title: coverageComplete ? "Every successful HTML page has a Markdown representation" : "Markdown representation coverage is incomplete",
|
|
7729
|
-
plainEnglish: `${markdown.evaluatedPages} of ${markdown.eligibleHtmlPages} successful HTML pages returned Markdown. ${explicitPages} advertised an explicit alternative and ${negotiatedPages} supported content negotiation
|
|
7730
|
-
action: coverageComplete ? "Keep the negotiated or explicit Markdown response stable as routes change." : "Return text/markdown through content negotiation or advertise one working Markdown alternative for each public HTML page.",
|
|
7833
|
+
title: coverageComplete ? "Every successful HTML page has a Markdown representation" : !hasEligibleHtml ? "Markdown representation coverage was not evaluated" : "Markdown representation coverage is incomplete",
|
|
7834
|
+
plainEnglish: hasEligibleHtml ? `${markdown.evaluatedPages} of ${markdown.eligibleHtmlPages} successful HTML pages returned Markdown. ${explicitPages} advertised an explicit alternative and ${negotiatedPages} supported content negotiation.` : "No successful HTML page was available, so Markdown representation coverage could not be evaluated.",
|
|
7835
|
+
action: !hasEligibleHtml ? "Resolve the crawl or page-fetch failure, then run this check again." : coverageComplete ? "Keep the negotiated or explicit Markdown response stable as routes change." : "Return text/markdown through content negotiation or advertise one working Markdown alternative for each public HTML page.",
|
|
7731
7836
|
evidence: {
|
|
7732
7837
|
eligibleHtmlPages: markdown.eligibleHtmlPages,
|
|
7733
7838
|
advertisedPages: markdown.advertisedPages,
|
|
7734
7839
|
negotiatedPages,
|
|
7735
7840
|
evaluatedPages: markdown.evaluatedPages
|
|
7736
7841
|
},
|
|
7737
|
-
urls: failedUrls.slice(0, 25)
|
|
7842
|
+
urls: hasEligibleHtml ? failedUrls.slice(0, 25) : []
|
|
7738
7843
|
}),
|
|
7739
|
-
check$
|
|
7844
|
+
check$2("representations", {
|
|
7740
7845
|
id: "markdown-token-estimates",
|
|
7741
|
-
status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "
|
|
7742
|
-
title: tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
|
|
7743
|
-
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: "
|
|
7846
|
+
status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "info",
|
|
7847
|
+
title: !hasEvaluatedMarkdown ? "Markdown token estimates were not evaluated" : tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
|
|
7848
|
+
plainEnglish: hasEvaluatedMarkdown ? `${tokenHeaders} of ${markdown.evaluatedPages} evaluated pages returned X-Markdown-Tokens. Paired explicit and negotiated responses agreed where both existed. The value is an estimate, not an exact model-specific token count.` : "No confirmed Markdown response was available, so optional server token estimates could not be evaluated.",
|
|
7849
|
+
action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : tokenHeaders === markdown.evaluatedPages ? "Keep the estimate stable when both explicit and negotiated responses exist." : "No change is required. X-Markdown-Tokens is optional; local size estimates are reported separately.",
|
|
7745
7850
|
urls: markdown.pages.filter((page) => {
|
|
7746
|
-
|
|
7851
|
+
const primary = confirmedMarkdown(page);
|
|
7852
|
+
if (!primary) return false;
|
|
7853
|
+
const paired = [page.explicit, page.negotiated].filter((response) => response?.status !== void 0 && response.status >= 200 && response.status < 300 && /^\s*text\/markdown\b/iu.test(response.contentType ?? ""));
|
|
7854
|
+
return primary?.markdownTokens === void 0 || paired.length === 2 && paired[0]?.markdownTokens !== paired[1]?.markdownTokens;
|
|
7747
7855
|
}).map((page) => page.htmlUrl).slice(0, 25)
|
|
7748
7856
|
}),
|
|
7749
|
-
check$
|
|
7857
|
+
check$2("representations", {
|
|
7858
|
+
id: "markdown-size",
|
|
7859
|
+
status: sizedPages.length === 0 ? "unknown" : largePages.length === 0 ? "pass" : "warning",
|
|
7860
|
+
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",
|
|
7861
|
+
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.`,
|
|
7862
|
+
action: sizedPages.length === 0 ? "Confirm a successful text/markdown representation, then run this check again." : largePages.length === 0 ? "Keep important content early and retain stable section structure as pages grow." : "Split genuinely separate topics, keep navigation and repeated chrome out of Markdown, and put the most useful content early. Do not remove necessary detail just to hit a threshold.",
|
|
7863
|
+
evidence: {
|
|
7864
|
+
measuredPages: sizedPages.length,
|
|
7865
|
+
reviewThresholdCharacters: 5e4,
|
|
7866
|
+
highRiskThresholdCharacters: 1e5,
|
|
7867
|
+
largePages: largePages.length,
|
|
7868
|
+
veryLargePages: veryLargePages.length,
|
|
7869
|
+
maxCharacters,
|
|
7870
|
+
maxEstimatedTokens,
|
|
7871
|
+
heuristic: true
|
|
7872
|
+
},
|
|
7873
|
+
urls: largePages.map((page) => page.htmlUrl).slice(0, 25)
|
|
7874
|
+
}),
|
|
7875
|
+
check$2("representations", {
|
|
7876
|
+
id: "markdown-content-parity",
|
|
7877
|
+
status: parityPages.length === 0 ? "unknown" : parityFailures.length === 0 ? "pass" : "warning",
|
|
7878
|
+
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",
|
|
7879
|
+
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.`,
|
|
7880
|
+
action: parityPages.length === 0 ? "Confirm a successful text/markdown representation, then run this check again." : parityFailures.length === 0 ? "Keep the shared conversion path covered as templates and components change." : "Compare the affected HTML and Markdown around the missing sections, then fix the shared converter or component serialization.",
|
|
7881
|
+
evidence: {
|
|
7882
|
+
evaluatedPages: parityPages.length,
|
|
7883
|
+
failedPages: parityFailures.length,
|
|
7884
|
+
minimumCoverage: .6,
|
|
7885
|
+
sampling: "bounded-content-shingles"
|
|
7886
|
+
},
|
|
7887
|
+
urls: parityFailures.map((page) => page.htmlUrl).slice(0, 25)
|
|
7888
|
+
}),
|
|
7889
|
+
check$2("representations", {
|
|
7890
|
+
id: "markdown-tab-serialization",
|
|
7891
|
+
status: tabbedPages.length === 0 ? "notApplicable" : tabFailures.length > 0 ? "warning" : tabUnknown.length > 0 ? "unknown" : "pass",
|
|
7892
|
+
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",
|
|
7893
|
+
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.`,
|
|
7894
|
+
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.",
|
|
7895
|
+
evidence: {
|
|
7896
|
+
tabbedPages: tabbedPages.length,
|
|
7897
|
+
failedPages: tabFailures.length,
|
|
7898
|
+
unknownPages: tabUnknown.length
|
|
7899
|
+
},
|
|
7900
|
+
urls: [...tabFailures, ...tabUnknown].map((page) => page.htmlUrl).slice(0, 25)
|
|
7901
|
+
}),
|
|
7902
|
+
check$2("representations", {
|
|
7750
7903
|
id: "markdown-negotiation",
|
|
7751
7904
|
status: markdown.eligibleHtmlPages === 0 ? "unknown" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "pass" : "fail",
|
|
7752
|
-
title: negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
|
|
7753
|
-
plainEnglish: `${negotiatedPages} of ${markdown.eligibleHtmlPages} pages returned negotiated Markdown. ${markdown.exactByteMatches} of ${explicitPages} paired explicit responses matched byte for byte. A request that refuses Markdown ${discovery.contentNegotiation.qZeroHonoured ? "received HTML" : "did not produce confirmed HTML evidence"}
|
|
7754
|
-
action: "Honour Accept q-values, send Vary: Accept, and keep paired explicit and negotiated responses identical when both exist.",
|
|
7905
|
+
title: !hasEligibleHtml ? "Markdown content negotiation was not evaluated" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
|
|
7906
|
+
plainEnglish: hasEligibleHtml ? `${negotiatedPages} of ${markdown.eligibleHtmlPages} pages returned negotiated Markdown. ${markdown.exactByteMatches} of ${explicitPages} paired explicit responses matched byte for byte. A request that refuses Markdown ${discovery.contentNegotiation.qZeroHonoured ? "received HTML" : "did not produce confirmed HTML evidence"}.` : "No successful HTML page was available, so content negotiation could not be evaluated.",
|
|
7907
|
+
action: hasEligibleHtml ? "Honour Accept q-values, send Vary: Accept, and keep paired explicit and negotiated responses identical when both exist." : "Resolve the crawl or page-fetch failure, then run this check again.",
|
|
7755
7908
|
evidence: {
|
|
7756
7909
|
negotiatedPages,
|
|
7757
7910
|
explicitPages,
|
|
7758
7911
|
exactByteMatches: markdown.exactByteMatches,
|
|
7759
7912
|
qZero: discovery.contentNegotiation
|
|
7760
7913
|
},
|
|
7761
|
-
urls: failedUrls.slice(0, 25)
|
|
7914
|
+
urls: hasEligibleHtml ? failedUrls.slice(0, 25) : []
|
|
7762
7915
|
}),
|
|
7763
|
-
check$
|
|
7916
|
+
check$2("representations", {
|
|
7764
7917
|
id: "markdown-determinism",
|
|
7765
|
-
status: markdown.
|
|
7766
|
-
title: stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
|
|
7767
|
-
plainEnglish: `${markdown.stableResponses} of ${markdown.
|
|
7768
|
-
action: "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
|
|
7918
|
+
status: markdown.evaluatedPages === 0 ? "unknown" : stable ? "pass" : "fail",
|
|
7919
|
+
title: !hasEvaluatedMarkdown ? "Markdown response stability was not evaluated" : stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
|
|
7920
|
+
plainEnglish: hasEvaluatedMarkdown ? `${markdown.stableResponses} of ${markdown.evaluatedPages} repeated Markdown responses kept the same SHA-256 digest.` : "No confirmed Markdown response was available for a repeated stability check.",
|
|
7921
|
+
action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : stable ? "Keep repeated Markdown responses byte-stable as templates and data change." : "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
|
|
7769
7922
|
evidence: { stableResponses: markdown.stableResponses },
|
|
7770
|
-
urls:
|
|
7923
|
+
urls: unstableUrls.slice(0, 25)
|
|
7771
7924
|
}),
|
|
7772
|
-
check$
|
|
7925
|
+
check$2("representations", {
|
|
7773
7926
|
id: "markdown-quality",
|
|
7774
7927
|
status: markdown.evaluatedPages === 0 ? "unknown" : qualityUrls.length === 0 ? "pass" : "warning",
|
|
7775
|
-
title: qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
|
|
7776
|
-
plainEnglish: qualityUrls.length === 0 ? "The evaluated alternatives retained one H1, frontmatter titles, balanced code fences, useful copy, and no leaked SVG, script, style, or layout tags." : `${qualityUrls.length} Markdown alternative${qualityUrls.length === 1 ? "" : "s"} lost important structure, looked navigation-only, or leaked presentation markup.`,
|
|
7777
|
-
action: "Open the affected Markdown directly and fix the shared converter or component exclusion rules rather than patching generated files.",
|
|
7928
|
+
title: !hasEvaluatedMarkdown ? "Markdown quality was not evaluated" : qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
|
|
7929
|
+
plainEnglish: !hasEvaluatedMarkdown ? "No confirmed Markdown response was available for structure and extraction checks." : qualityUrls.length === 0 ? "The evaluated alternatives retained one H1, frontmatter titles, balanced code fences, useful copy, and no leaked SVG, script, style, or layout tags." : `${qualityUrls.length} Markdown alternative${qualityUrls.length === 1 ? "" : "s"} lost important structure, looked navigation-only, or leaked presentation markup.`,
|
|
7930
|
+
action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : qualityUrls.length === 0 ? "Keep the shared Markdown conversion path covered as templates and components change." : "Open the affected Markdown directly and fix the shared converter or component exclusion rules rather than patching generated files.",
|
|
7778
7931
|
urls: qualityUrls.slice(0, 25)
|
|
7779
7932
|
})
|
|
7780
7933
|
];
|
|
7781
7934
|
}
|
|
7935
|
+
//#endregion
|
|
7936
|
+
//#region packages/core/dist/analyze/crawler/agent-readiness.js
|
|
7937
|
+
function check$1(section, input) {
|
|
7938
|
+
return {
|
|
7939
|
+
section,
|
|
7940
|
+
...input
|
|
7941
|
+
};
|
|
7942
|
+
}
|
|
7943
|
+
function section$2(id, title, checks) {
|
|
7944
|
+
return {
|
|
7945
|
+
id,
|
|
7946
|
+
title,
|
|
7947
|
+
checks
|
|
7948
|
+
};
|
|
7949
|
+
}
|
|
7950
|
+
function profileChecks(discovery) {
|
|
7951
|
+
return [
|
|
7952
|
+
"api",
|
|
7953
|
+
"application",
|
|
7954
|
+
"commerce"
|
|
7955
|
+
].map((profile) => check$1("profile", {
|
|
7956
|
+
id: `profile-${profile}`,
|
|
7957
|
+
status: "notApplicable",
|
|
7958
|
+
title: `${profile[0]?.toUpperCase()}${profile.slice(1)} checks are not applicable`,
|
|
7959
|
+
plainEnglish: discovery.profileApplicability[profile].reason,
|
|
7960
|
+
action: "Choose this profile only when the site exposes that capability publicly.",
|
|
7961
|
+
evidence: {
|
|
7962
|
+
profile,
|
|
7963
|
+
selectedProfile: discovery.profile
|
|
7964
|
+
}
|
|
7965
|
+
}));
|
|
7966
|
+
}
|
|
7967
|
+
function summariseChecks(checks) {
|
|
7968
|
+
const count = (status) => checks.filter((item) => item.status === status).length;
|
|
7969
|
+
return {
|
|
7970
|
+
checks: checks.length,
|
|
7971
|
+
passed: count("pass"),
|
|
7972
|
+
warnings: count("warning"),
|
|
7973
|
+
failed: count("fail"),
|
|
7974
|
+
unknown: count("unknown"),
|
|
7975
|
+
information: count("info"),
|
|
7976
|
+
notApplicable: count("notApplicable")
|
|
7977
|
+
};
|
|
7978
|
+
}
|
|
7782
7979
|
function contentSignalsCheck(discovery, robotsContentSignals) {
|
|
7783
7980
|
const signals = discovery.contentSignals;
|
|
7784
7981
|
const robotsDeclared = robotsContentSignals ?? [];
|
|
@@ -7879,8 +8076,8 @@ function discoveryChecks(discovery, robotsContentSignals) {
|
|
|
7879
8076
|
check$1("discovery", {
|
|
7880
8077
|
id: "route-manifest",
|
|
7881
8078
|
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.
|
|
8079
|
+
title: discovery.routeManifest.valid ? "The public route manifest is valid" : "No public route manifest was available",
|
|
8080
|
+
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
8081
|
action: "Generate one deterministic route inventory from the build when exact HTML and Markdown parity matters.",
|
|
7885
8082
|
evidence: discovery.routeManifest
|
|
7886
8083
|
})
|
|
@@ -8011,25 +8208,42 @@ function accessChecks(report, discovery) {
|
|
|
8011
8208
|
const blocked = bots.filter((bot) => bot.allowed === false);
|
|
8012
8209
|
const robotsKnown = report.ai?.robotsTxt?.availability === "available" || report.ai?.robotsTxt?.availability === "absent";
|
|
8013
8210
|
const protocol = discovery.protocolVariants;
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8211
|
+
const authenticationGates = report.issues.filter((issue) => issue.ruleId === "soft_authentication_gate");
|
|
8212
|
+
const clientRenderedPages = report.issues.filter((issue) => issue.ruleId === "client_rendered_content");
|
|
8213
|
+
return [
|
|
8214
|
+
check$1("access", {
|
|
8215
|
+
id: "crawler-access",
|
|
8216
|
+
status: !robotsKnown ? "unknown" : blocked.length > 0 ? "warning" : "pass",
|
|
8217
|
+
title: blocked.length ? "robots.txt blocks selected crawler tokens" : "No selected crawler token is blocked at the start URL",
|
|
8218
|
+
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.",
|
|
8219
|
+
action: "Keep blocks intentional. A crawler token being allowed here does not prove that the service will crawl, index, or cite the site.",
|
|
8220
|
+
evidence: {
|
|
8221
|
+
availability: report.ai?.robotsTxt?.availability,
|
|
8222
|
+
blocked,
|
|
8223
|
+
botAccess: bots
|
|
8224
|
+
}
|
|
8225
|
+
}),
|
|
8226
|
+
check$1("access", {
|
|
8227
|
+
id: "public-content-access",
|
|
8228
|
+
status: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "warning" : "pass",
|
|
8229
|
+
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",
|
|
8230
|
+
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.`,
|
|
8231
|
+
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.",
|
|
8232
|
+
evidence: {
|
|
8233
|
+
authenticationGates: authenticationGates.length,
|
|
8234
|
+
clientRenderedPages: clientRenderedPages.length
|
|
8235
|
+
},
|
|
8236
|
+
urls: [...authenticationGates, ...clientRenderedPages].map((issue) => issue.url).slice(0, 25)
|
|
8237
|
+
}),
|
|
8238
|
+
check$1("access", {
|
|
8239
|
+
id: "protocol-canonicalization",
|
|
8240
|
+
status: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "pass" : protocol.http.permanentRedirectToHttps === null ? "unknown" : "warning",
|
|
8241
|
+
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",
|
|
8242
|
+
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"}.`,
|
|
8243
|
+
action: "Use one permanent HTTP-to-HTTPS redirect and send HSTS only after HTTPS is stable across the site.",
|
|
8244
|
+
evidence: protocol
|
|
8245
|
+
})
|
|
8246
|
+
];
|
|
8033
8247
|
}
|
|
8034
8248
|
function identityChecks(report) {
|
|
8035
8249
|
const types = new Set(report.pages.flatMap((page) => page.schemaTypes ?? []));
|
|
@@ -9558,6 +9772,48 @@ const RULE_DEFINITIONS = [
|
|
|
9558
9772
|
suggestedCommands: ["seo crawl --sitemap-url <url> --health --json"]
|
|
9559
9773
|
}
|
|
9560
9774
|
},
|
|
9775
|
+
{
|
|
9776
|
+
id: "soft_authentication_gate",
|
|
9777
|
+
title: "Page appears to be an authentication gate",
|
|
9778
|
+
category: "response",
|
|
9779
|
+
defaultSeverity: "medium",
|
|
9780
|
+
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.",
|
|
9781
|
+
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.",
|
|
9782
|
+
impactIfIgnored: "A login page can be mistaken for the intended content, producing misleading crawl results and weak search or agent access.",
|
|
9783
|
+
howToVerify: "Fetch the affected URL without a session and confirm it returns the intended public page, or a deliberate authentication status for protected content.",
|
|
9784
|
+
agentHints: {
|
|
9785
|
+
evidenceFields: ["page.softAuthenticationGate", "page.finalUrl"],
|
|
9786
|
+
suggestedCommands: ["seo crawl <url> --max-pages 1 --json"]
|
|
9787
|
+
}
|
|
9788
|
+
},
|
|
9789
|
+
{
|
|
9790
|
+
id: "client_rendered_content",
|
|
9791
|
+
title: "Important content requires JavaScript rendering",
|
|
9792
|
+
category: "response",
|
|
9793
|
+
defaultSeverity: "medium",
|
|
9794
|
+
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.",
|
|
9795
|
+
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.",
|
|
9796
|
+
impactIfIgnored: "Clients that do not execute JavaScript may see an empty shell or miss important content and links.",
|
|
9797
|
+
howToVerify: "Re-run the crawl and confirm the raw and rendered content observations contain the same important document content.",
|
|
9798
|
+
agentHints: {
|
|
9799
|
+
evidenceFields: ["page.fetchDiagnostics.rendering.documentDifference", "page.usedJs"],
|
|
9800
|
+
suggestedCommands: ["seo crawl <url> --json"]
|
|
9801
|
+
}
|
|
9802
|
+
},
|
|
9803
|
+
{
|
|
9804
|
+
id: "soft_404",
|
|
9805
|
+
title: "Unknown URLs return a successful response",
|
|
9806
|
+
category: "response",
|
|
9807
|
+
defaultSeverity: "high",
|
|
9808
|
+
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.",
|
|
9809
|
+
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.",
|
|
9810
|
+
impactIfIgnored: "Missing URLs can remain in crawl queues, waste crawl attention, and create confusing search or monitoring evidence.",
|
|
9811
|
+
howToVerify: "Request several known-nonexistent paths and confirm each returns 404 or 410 without redirecting to a generic successful page.",
|
|
9812
|
+
agentHints: {
|
|
9813
|
+
evidenceFields: ["report.siteChecks.soft404"],
|
|
9814
|
+
suggestedCommands: ["seo crawl <url> --json"]
|
|
9815
|
+
}
|
|
9816
|
+
},
|
|
9561
9817
|
{
|
|
9562
9818
|
id: "client_error",
|
|
9563
9819
|
title: "Client error",
|
|
@@ -9591,7 +9847,8 @@ const RULE_DEFINITIONS = [
|
|
|
9591
9847
|
evidenceFields: [
|
|
9592
9848
|
"page.url",
|
|
9593
9849
|
"page.finalUrl",
|
|
9594
|
-
"page.status"
|
|
9850
|
+
"page.status",
|
|
9851
|
+
"page.metaRefresh"
|
|
9595
9852
|
],
|
|
9596
9853
|
suggestedCommands: ["seo redirect-trace --url <url> --json"]
|
|
9597
9854
|
}
|
|
@@ -10047,6 +10304,8 @@ const RULE_RECOMMENDATIONS = {
|
|
|
10047
10304
|
x_robots_noindex: "review",
|
|
10048
10305
|
robots_blocked: "review",
|
|
10049
10306
|
crawler_access_blocked: "review",
|
|
10307
|
+
soft_authentication_gate: "review",
|
|
10308
|
+
client_rendered_content: "review",
|
|
10050
10309
|
orphan_page: "review",
|
|
10051
10310
|
redirected_url: "review",
|
|
10052
10311
|
slow_response: "review",
|
|
@@ -10083,6 +10342,7 @@ function comparableUrl(value) {
|
|
|
10083
10342
|
}
|
|
10084
10343
|
}
|
|
10085
10344
|
function isRedirectedPage(page) {
|
|
10345
|
+
if (page.metaRefresh) return true;
|
|
10086
10346
|
const requested = comparableUrl(page.url);
|
|
10087
10347
|
const final = comparableUrl(page.finalUrl);
|
|
10088
10348
|
return Boolean(requested && final && requested !== final);
|
|
@@ -10137,6 +10397,28 @@ const WEAK_VALUABLE_INLINKS = 1;
|
|
|
10137
10397
|
const QUERY_COVERAGE_MIN_IMPRESSIONS = 50;
|
|
10138
10398
|
const QUERY_COVERAGE_MIN = .6;
|
|
10139
10399
|
const OVERSIZED_IMAGE_CANDIDATE_LIMIT = 2e3;
|
|
10400
|
+
const EMPTY_INITIAL_HTML_CHARACTERS = 200;
|
|
10401
|
+
const SUBSTANTIVE_RENDERED_CHARACTERS = 500;
|
|
10402
|
+
function clientRenderedContentEvidence(page) {
|
|
10403
|
+
const rendering = page.fetchDiagnostics?.rendering;
|
|
10404
|
+
const difference = rendering?.documentDifference;
|
|
10405
|
+
if (rendering?.status !== "rendered" || !difference?.changed.includes("content")) return;
|
|
10406
|
+
const rawCharacters = difference.raw.content.characters;
|
|
10407
|
+
const renderedCharacters = difference.rendered.content.characters;
|
|
10408
|
+
if (rawCharacters >= EMPTY_INITIAL_HTML_CHARACTERS || renderedCharacters < SUBSTANTIVE_RENDERED_CHARACTERS || renderedCharacters < rawCharacters * 3) return;
|
|
10409
|
+
return {
|
|
10410
|
+
rawCharacters,
|
|
10411
|
+
rawWords: difference.raw.content.wordCount,
|
|
10412
|
+
renderedCharacters,
|
|
10413
|
+
renderedWords: difference.rendered.content.wordCount,
|
|
10414
|
+
changed: difference.changed,
|
|
10415
|
+
thresholds: {
|
|
10416
|
+
emptyInitialHtmlCharacters: EMPTY_INITIAL_HTML_CHARACTERS,
|
|
10417
|
+
substantiveRenderedCharacters: SUBSTANTIVE_RENDERED_CHARACTERS,
|
|
10418
|
+
minimumGrowthRatio: 3
|
|
10419
|
+
}
|
|
10420
|
+
};
|
|
10421
|
+
}
|
|
10140
10422
|
function issue(ruleId, page, detail, evidence) {
|
|
10141
10423
|
const rule = explainRule(ruleId);
|
|
10142
10424
|
if (!rule) throw new Error(`Missing rule guidance for ${ruleId}`);
|
|
@@ -10335,11 +10617,18 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
10335
10617
|
}
|
|
10336
10618
|
if (page.status < 200) continue;
|
|
10337
10619
|
const redirected = isRedirectedPage(page);
|
|
10338
|
-
if (redirected)
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10620
|
+
if (redirected) {
|
|
10621
|
+
const redirectTarget = page.metaRefresh?.targetUrl ?? page.finalUrl;
|
|
10622
|
+
issues.push(issue("redirected_url", page, `Final URL: ${redirectTarget}`, {
|
|
10623
|
+
requestedUrl: page.url,
|
|
10624
|
+
finalUrl: redirectTarget,
|
|
10625
|
+
status: page.status,
|
|
10626
|
+
...page.metaRefresh ? {
|
|
10627
|
+
redirectType: "meta-refresh",
|
|
10628
|
+
delaySeconds: page.metaRefresh.delaySeconds
|
|
10629
|
+
} : {}
|
|
10630
|
+
}));
|
|
10631
|
+
}
|
|
10343
10632
|
const redirectChain = page.fetchDiagnostics?.redirectChain ?? [];
|
|
10344
10633
|
if (redirectChain.length > 1) issues.push(issue("redirect_chain", page, `${redirectChain.length} hops`, {
|
|
10345
10634
|
redirectChain,
|
|
@@ -10351,6 +10640,9 @@ function auditCrawlPages(pages, opts = {}) {
|
|
|
10351
10640
|
}));
|
|
10352
10641
|
if (page.auditScope === "status") continue;
|
|
10353
10642
|
if (redirected) continue;
|
|
10643
|
+
if (page.softAuthenticationGate) issues.push(issue("soft_authentication_gate", page, "A password form and login-specific page signals were observed.", page.softAuthenticationGate));
|
|
10644
|
+
const renderedContent = clientRenderedContentEvidence(page);
|
|
10645
|
+
if (renderedContent) issues.push(issue("client_rendered_content", page, "The raw response was an empty or near-empty shell.", renderedContent));
|
|
10354
10646
|
if (isHtmlPage(page) && (page.sizeBytes ?? 0) > LARGE_HTML_BYTES) issues.push(issue("large_html", page, `${Math.round((page.sizeBytes ?? 0) / 1024)} KB`, {
|
|
10355
10647
|
sizeBytes: page.sizeBytes,
|
|
10356
10648
|
thresholdBytes: LARGE_HTML_BYTES
|
|
@@ -10955,7 +11247,7 @@ function createCrawlReport(input) {
|
|
|
10955
11247
|
const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
|
|
10956
11248
|
const requestEvidenceStatus = input.requestEvidenceStatus ?? (input.requests ? "available" : "unavailable");
|
|
10957
11249
|
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 })]);
|
|
11250
|
+
const safeIssues = sanitizeIssues([...input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })], ...input.additionalIssues ?? []]);
|
|
10959
11251
|
const pages = safePagesWithLinks;
|
|
10960
11252
|
return {
|
|
10961
11253
|
id: input.id ?? crawlRunId(),
|
|
@@ -10984,6 +11276,7 @@ function createCrawlReport(input) {
|
|
|
10984
11276
|
...input.ai ? { ai: sanitizeTenantValue(input.ai) } : {},
|
|
10985
11277
|
...input.sitemapDiscovery ? { sitemapDiscovery: sanitizeTenantValue(input.sitemapDiscovery) } : {},
|
|
10986
11278
|
...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
|
|
11279
|
+
...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
|
|
10987
11280
|
...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
|
|
10988
11281
|
warnings: sanitizeMessages(input.warnings).sort(compareText$13),
|
|
10989
11282
|
caveats: sanitizeMessages(input.caveats).sort(compareText$13)
|
|
@@ -11181,7 +11474,7 @@ function latestCrawlReport(site, store = crawlReportStore) {
|
|
|
11181
11474
|
//#region packages/core/dist/analyze/crawler/crawl-caveats.js
|
|
11182
11475
|
function crawlCaveats(input) {
|
|
11183
11476
|
const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
|
|
11184
|
-
if (input.queueSafetySkippedUrls > 0) caveats.push(`Left ${input.queueSafetySkippedUrls} eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
|
|
11477
|
+
if (input.queueSafetySkippedUrls > 0) caveats.push(input.queueSafetyInventoryCapped ? `Left at least ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. The excluded-URL inventory reached its reporting cap. Increase --max-pages to inspect more.` : `Left ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
|
|
11185
11478
|
if (input.originBackpressureSkippedUrls > 0) caveats.push(`Stopped ${input.originBackpressureSkippedUrls} queued URL${input.originBackpressureSkippedUrls === 1 ? "" : "s"} because the origin stayed slow. This is incomplete crawl evidence, not a failed URL check.`);
|
|
11186
11479
|
if (input.memoryPressureSkippedUrls > 0) caveats.push(`Left ${input.memoryPressureSkippedUrls} eligible URL${input.memoryPressureSkippedUrls === 1 ? "" : "s"} unchecked after the local memory safety limit was reached. Start with --health or lower --max-pages.`);
|
|
11187
11480
|
return caveats;
|
|
@@ -11544,6 +11837,75 @@ function observationFromPage(requestedUrl, page) {
|
|
|
11544
11837
|
};
|
|
11545
11838
|
}
|
|
11546
11839
|
//#endregion
|
|
11840
|
+
//#region packages/core/dist/analyze/crawler/site-checks.js
|
|
11841
|
+
const PROBE_COUNT = 2;
|
|
11842
|
+
function probeUrls(startUrl) {
|
|
11843
|
+
const origin = new URL(startUrl).origin;
|
|
11844
|
+
const seed = createHash("sha256").update(origin).digest("hex").slice(0, 12);
|
|
11845
|
+
return Array.from({ length: PROBE_COUNT }, (_, index) => new URL(`/.well-known/seo-audit/not-found-${seed}-${index + 1}`, origin).toString());
|
|
11846
|
+
}
|
|
11847
|
+
function probeObservation(url, result) {
|
|
11848
|
+
const request = result.request;
|
|
11849
|
+
if (!request) return {
|
|
11850
|
+
url,
|
|
11851
|
+
error: result.warning ?? "No response evidence."
|
|
11852
|
+
};
|
|
11853
|
+
if (request.outcome === "response") return {
|
|
11854
|
+
url,
|
|
11855
|
+
status: request.status,
|
|
11856
|
+
finalUrl: request.finalUrl,
|
|
11857
|
+
redirected: request.finalUrl !== url,
|
|
11858
|
+
accessBlocked: Boolean(request.accessBlock)
|
|
11859
|
+
};
|
|
11860
|
+
return {
|
|
11861
|
+
url,
|
|
11862
|
+
error: request.outcome === "failure" ? request.error : `Probe skipped: ${request.reason}`
|
|
11863
|
+
};
|
|
11864
|
+
}
|
|
11865
|
+
async function collectCrawlSiteChecks(input) {
|
|
11866
|
+
const probes = [];
|
|
11867
|
+
for (const url of probeUrls(input.startUrl)) {
|
|
11868
|
+
if (input.signal?.aborted) break;
|
|
11869
|
+
const result = await input.fetchStatusPage(url, {
|
|
11870
|
+
js: "off",
|
|
11871
|
+
refresh: true,
|
|
11872
|
+
timeoutMs: input.timeoutMs,
|
|
11873
|
+
signal: input.signal,
|
|
11874
|
+
writeCache: false,
|
|
11875
|
+
respectRobots: input.respectRobots
|
|
11876
|
+
});
|
|
11877
|
+
probes.push(probeObservation(url, result));
|
|
11878
|
+
}
|
|
11879
|
+
const conclusive = probes.filter((probe) => probe.status !== void 0 && !probe.accessBlocked);
|
|
11880
|
+
const successful = conclusive.filter((probe) => (probe.status ?? 0) >= 200 && (probe.status ?? 0) < 400);
|
|
11881
|
+
const correctMissing = conclusive.filter((probe) => probe.status === 404 || probe.status === 410);
|
|
11882
|
+
const status = conclusive.length !== PROBE_COUNT ? "unknown" : successful.length === PROBE_COUNT ? "warning" : correctMissing.length === PROBE_COUNT ? "pass" : "unknown";
|
|
11883
|
+
const checks = { soft404: {
|
|
11884
|
+
status,
|
|
11885
|
+
probes,
|
|
11886
|
+
probeLimit: PROBE_COUNT,
|
|
11887
|
+
complete: probes.length === PROBE_COUNT
|
|
11888
|
+
} };
|
|
11889
|
+
if (status !== "warning") return {
|
|
11890
|
+
checks,
|
|
11891
|
+
issues: []
|
|
11892
|
+
};
|
|
11893
|
+
const rule = explainRule("soft_404");
|
|
11894
|
+
if (!rule) throw new Error("Missing rule guidance for soft_404");
|
|
11895
|
+
return {
|
|
11896
|
+
checks,
|
|
11897
|
+
issues: [{
|
|
11898
|
+
ruleId: "soft_404",
|
|
11899
|
+
title: rule.title,
|
|
11900
|
+
category: rule.category,
|
|
11901
|
+
severity: rule.defaultSeverity,
|
|
11902
|
+
url: new URL("/", input.startUrl).toString(),
|
|
11903
|
+
detail: `${successful.length} known-nonexistent paths returned a successful response or redirect.`,
|
|
11904
|
+
evidence: checks.soft404
|
|
11905
|
+
}]
|
|
11906
|
+
};
|
|
11907
|
+
}
|
|
11908
|
+
//#endregion
|
|
11547
11909
|
//#region packages/core/dist/analyze/crawler/site-crawl-ai-signals.js
|
|
11548
11910
|
const MAX_AUXILIARY_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
11549
11911
|
const AI_BOT_USER_AGENTS = [
|
|
@@ -12263,6 +12625,10 @@ async function crawlOne(url, opts = {}) {
|
|
|
12263
12625
|
mainContentHash: hashText(extracted.contentText),
|
|
12264
12626
|
textRatio: fetched.html.length ? Math.min(1, extracted.contentText.length / fetched.html.length) : void 0,
|
|
12265
12627
|
contentSample: truncate$1(extracted.contentText, 300),
|
|
12628
|
+
contentSketch: crawlerEvidence.contentSketch,
|
|
12629
|
+
softAuthenticationGate: crawlerEvidence.softAuthenticationGate,
|
|
12630
|
+
metaRefresh: crawlerEvidence.metaRefresh,
|
|
12631
|
+
tabbedContent: crawlerEvidence.tabbedContent,
|
|
12266
12632
|
lang: extracted.lang,
|
|
12267
12633
|
hasViewport: extracted.hasViewport,
|
|
12268
12634
|
isHttps: new URL(extracted.finalUrl).protocol === "https:",
|
|
@@ -12312,9 +12678,9 @@ async function crawlOne(url, opts = {}) {
|
|
|
12312
12678
|
hasQapageSchema: extracted.schemaTypes.includes("QAPage")
|
|
12313
12679
|
}
|
|
12314
12680
|
};
|
|
12315
|
-
page.indexability = indexabilityReason(page);
|
|
12681
|
+
page.indexability = page.metaRefresh ? `Meta refresh to ${page.metaRefresh.targetUrl}` : indexabilityReason(page);
|
|
12316
12682
|
page.indexable = !page.indexability;
|
|
12317
|
-
page.declaredIndexability = hasMetaRobotsDirective(page.metaRobots, "noindex") || hasXRobotsDirective(page.xRobotsTag, "noindex") ? "noindex" : page.robotsTxt?.allowed === false ? "robots-blocked" : page.canonicalStatus === "conflicting" ? "canonical-conflict" : page.canonical && !sameDocumentUrl(page.canonical, page.finalUrl) ? "canonical-hint-other" : "indexable-candidate";
|
|
12683
|
+
page.declaredIndexability = page.metaRefresh ? "meta-refresh" : hasMetaRobotsDirective(page.metaRobots, "noindex") || hasXRobotsDirective(page.xRobotsTag, "noindex") ? "noindex" : page.robotsTxt?.allowed === false ? "robots-blocked" : page.canonicalStatus === "conflicting" ? "canonical-conflict" : page.canonical && !sameDocumentUrl(page.canonical, page.finalUrl) ? "canonical-hint-other" : "indexable-candidate";
|
|
12318
12684
|
return {
|
|
12319
12685
|
request: responseObservation(url, fetched, "complete"),
|
|
12320
12686
|
page,
|
|
@@ -13108,6 +13474,9 @@ async function crawlSite(input, dependencies) {
|
|
|
13108
13474
|
let robotsDeferredUrls = 0;
|
|
13109
13475
|
let originBackpressureSkippedUrls = 0;
|
|
13110
13476
|
let queueSafetySkippedUrls = 0;
|
|
13477
|
+
const queueSafetyDeferredUrls = /* @__PURE__ */ new Set();
|
|
13478
|
+
const maxTrackedQueueSafetyUrls = Math.min(5e4, Math.max(1e3, config.maxPages * 5));
|
|
13479
|
+
let queueSafetyInventoryCapped = false;
|
|
13111
13480
|
let failedUrls = 0;
|
|
13112
13481
|
let observedInternalLinks = 0;
|
|
13113
13482
|
let memoryLimited = false;
|
|
@@ -13212,13 +13581,16 @@ async function crawlSite(input, dependencies) {
|
|
|
13212
13581
|
return;
|
|
13213
13582
|
}
|
|
13214
13583
|
if (queue.size + visited.size + inFlight.size >= config.maxPages * 5) {
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13584
|
+
if (!queueSafetyDeferredUrls.has(normalized)) if (queueSafetyDeferredUrls.size < maxTrackedQueueSafetyUrls) {
|
|
13585
|
+
queueSafetyDeferredUrls.add(normalized);
|
|
13586
|
+
queueSafetySkippedUrls = queueSafetyDeferredUrls.size;
|
|
13587
|
+
recordSkip("queue-safety-limit");
|
|
13588
|
+
emitStatus("url_skipped", {
|
|
13589
|
+
url: normalized,
|
|
13590
|
+
depth,
|
|
13591
|
+
reason: "queue_safety_limit"
|
|
13592
|
+
});
|
|
13593
|
+
} else queueSafetyInventoryCapped = true;
|
|
13222
13594
|
return;
|
|
13223
13595
|
}
|
|
13224
13596
|
discovered.add(normalized);
|
|
@@ -13547,6 +13919,13 @@ async function crawlSite(input, dependencies) {
|
|
|
13547
13919
|
signal,
|
|
13548
13920
|
concurrency: Math.min(config.concurrency, 4)
|
|
13549
13921
|
}) : void 0;
|
|
13922
|
+
const siteCheckResult = !healthStrategy && !cancelled && !stoppedForMemory ? await collectCrawlSiteChecks({
|
|
13923
|
+
startUrl: config.url,
|
|
13924
|
+
timeoutMs: config.timeoutMs,
|
|
13925
|
+
fetchStatusPage: deps.fetchStatusPage,
|
|
13926
|
+
respectRobots: config.respectRobots,
|
|
13927
|
+
signal
|
|
13928
|
+
}) : void 0;
|
|
13550
13929
|
for (const page of pages) {
|
|
13551
13930
|
if (!page.geo || !llmsTxt) continue;
|
|
13552
13931
|
page.geo = {
|
|
@@ -13572,6 +13951,10 @@ async function crawlSite(input, dependencies) {
|
|
|
13572
13951
|
} } : {},
|
|
13573
13952
|
...sitemapDiscovery ? { sitemapDiscovery } : {},
|
|
13574
13953
|
...externalLinkVerification ? { externalLinkVerification } : {},
|
|
13954
|
+
...siteCheckResult ? {
|
|
13955
|
+
siteChecks: siteCheckResult.checks,
|
|
13956
|
+
additionalIssues: siteCheckResult.issues
|
|
13957
|
+
} : {},
|
|
13575
13958
|
...agentDiscovery ? { agentDiscovery } : {},
|
|
13576
13959
|
status: reportStatus,
|
|
13577
13960
|
warnings,
|
|
@@ -13580,6 +13963,7 @@ async function crawlSite(input, dependencies) {
|
|
|
13580
13963
|
pageLimitReached,
|
|
13581
13964
|
maxPages: config.maxPages,
|
|
13582
13965
|
queueSafetySkippedUrls,
|
|
13966
|
+
queueSafetyInventoryCapped,
|
|
13583
13967
|
originBackpressureSkippedUrls,
|
|
13584
13968
|
memoryPressureSkippedUrls
|
|
13585
13969
|
}),
|
|
@@ -14452,6 +14836,8 @@ const agentRepresentationResponseSchema = z.object({
|
|
|
14452
14836
|
status: z.number().int().optional(),
|
|
14453
14837
|
contentType: z.string().optional(),
|
|
14454
14838
|
bytes: z.number().int().nonnegative().optional(),
|
|
14839
|
+
characters: z.number().int().nonnegative().optional(),
|
|
14840
|
+
estimatedTokens: z.number().int().nonnegative().optional(),
|
|
14455
14841
|
sha256: z.string().optional(),
|
|
14456
14842
|
canonicalUrl: z.string().url().optional(),
|
|
14457
14843
|
alternateUrl: z.string().url().optional(),
|
|
@@ -14509,7 +14895,15 @@ const agentDiscoverySchema = z.object({
|
|
|
14509
14895
|
sourceWordCount: z.number().int().nonnegative(),
|
|
14510
14896
|
wordRetentionRatio: z.number().nonnegative().nullable(),
|
|
14511
14897
|
introductoryCopyRetained: z.boolean().nullable(),
|
|
14512
|
-
navigationOnly: z.boolean()
|
|
14898
|
+
navigationOnly: z.boolean(),
|
|
14899
|
+
contentSketchCoverage: z.number().min(0).max(1).nullable().optional(),
|
|
14900
|
+
tabbedContent: z.object({
|
|
14901
|
+
detectedPanels: z.number().int().nonnegative(),
|
|
14902
|
+
evaluatedPanels: z.number().int().nonnegative(),
|
|
14903
|
+
retainedPanels: z.number().int().nonnegative(),
|
|
14904
|
+
missingPanels: z.number().int().nonnegative(),
|
|
14905
|
+
complete: z.boolean().nullable()
|
|
14906
|
+
}).optional()
|
|
14513
14907
|
}).optional()
|
|
14514
14908
|
}))
|
|
14515
14909
|
}),
|
|
@@ -14528,6 +14922,11 @@ const agentDiscoverySchema = z.object({
|
|
|
14528
14922
|
missingHtmlRoutes: z.array(z.string()),
|
|
14529
14923
|
missingMarkdownRoutes: z.array(z.string()),
|
|
14530
14924
|
orphanMarkdownRoutes: z.array(z.string()),
|
|
14925
|
+
comparisonStatus: z.enum([
|
|
14926
|
+
"complete",
|
|
14927
|
+
"partial",
|
|
14928
|
+
"not-applicable"
|
|
14929
|
+
]).optional(),
|
|
14531
14930
|
error: z.string().optional()
|
|
14532
14931
|
}),
|
|
14533
14932
|
agentSkills: z.object({
|
|
@@ -14633,6 +15032,25 @@ const agentDiscoverySchema = z.object({
|
|
|
14633
15032
|
warnings: z.array(z.string())
|
|
14634
15033
|
});
|
|
14635
15034
|
//#endregion
|
|
15035
|
+
//#region packages/core/dist/analyze/crawler/site-checks-schema.js
|
|
15036
|
+
const crawlSiteChecksSchema = z.object({ soft404: z.object({
|
|
15037
|
+
status: z.enum([
|
|
15038
|
+
"pass",
|
|
15039
|
+
"warning",
|
|
15040
|
+
"unknown"
|
|
15041
|
+
]),
|
|
15042
|
+
probes: z.array(z.object({
|
|
15043
|
+
url: z.string().url(),
|
|
15044
|
+
status: z.number().int().optional(),
|
|
15045
|
+
finalUrl: z.string().url().optional(),
|
|
15046
|
+
redirected: z.boolean().optional(),
|
|
15047
|
+
accessBlocked: z.boolean().optional(),
|
|
15048
|
+
error: z.string().optional()
|
|
15049
|
+
})),
|
|
15050
|
+
probeLimit: z.number().int().positive(),
|
|
15051
|
+
complete: z.boolean()
|
|
15052
|
+
}) });
|
|
15053
|
+
//#endregion
|
|
14636
15054
|
//#region packages/core/dist/analyze/crawler/schemas.js
|
|
14637
15055
|
const crawlerRuleSeveritySchema = z.enum([
|
|
14638
15056
|
"low",
|
|
@@ -15028,6 +15446,7 @@ const crawlPageSnapshotSchema = z.object({
|
|
|
15028
15446
|
"robots-blocked",
|
|
15029
15447
|
"canonical-conflict",
|
|
15030
15448
|
"canonical-hint-other",
|
|
15449
|
+
"meta-refresh",
|
|
15031
15450
|
"not-html",
|
|
15032
15451
|
"unknown"
|
|
15033
15452
|
]).optional(),
|
|
@@ -15062,6 +15481,38 @@ const crawlPageSnapshotSchema = z.object({
|
|
|
15062
15481
|
mainContentHash: z.string().optional(),
|
|
15063
15482
|
textRatio: z.number().min(0).max(1).optional(),
|
|
15064
15483
|
contentSample: z.string().optional(),
|
|
15484
|
+
contentSketch: z.object({
|
|
15485
|
+
version: z.literal(1),
|
|
15486
|
+
wordCount: z.number().int().nonnegative(),
|
|
15487
|
+
shingleSize: z.number().int().positive(),
|
|
15488
|
+
sampledShingles: z.number().int().nonnegative(),
|
|
15489
|
+
hashes: z.array(z.string())
|
|
15490
|
+
}).optional(),
|
|
15491
|
+
softAuthenticationGate: z.object({
|
|
15492
|
+
kind: z.literal("login-form"),
|
|
15493
|
+
indicators: z.array(z.string()),
|
|
15494
|
+
formActionPath: z.string().optional()
|
|
15495
|
+
}).optional(),
|
|
15496
|
+
metaRefresh: z.object({
|
|
15497
|
+
targetUrl: z.string().url(),
|
|
15498
|
+
delaySeconds: z.number().nonnegative()
|
|
15499
|
+
}).optional(),
|
|
15500
|
+
tabbedContent: z.object({
|
|
15501
|
+
groups: z.number().int().positive(),
|
|
15502
|
+
panels: z.number().int().positive(),
|
|
15503
|
+
retainedPanels: z.number().int().nonnegative(),
|
|
15504
|
+
truncated: z.boolean(),
|
|
15505
|
+
panelSketches: z.array(z.object({
|
|
15506
|
+
label: z.string().optional(),
|
|
15507
|
+
sketch: z.object({
|
|
15508
|
+
version: z.literal(1),
|
|
15509
|
+
wordCount: z.number().int().nonnegative(),
|
|
15510
|
+
shingleSize: z.number().int().positive(),
|
|
15511
|
+
sampledShingles: z.number().int().nonnegative(),
|
|
15512
|
+
hashes: z.array(z.string())
|
|
15513
|
+
})
|
|
15514
|
+
}))
|
|
15515
|
+
}).optional(),
|
|
15065
15516
|
lang: z.string().optional(),
|
|
15066
15517
|
hasViewport: z.boolean().optional(),
|
|
15067
15518
|
isHttps: z.boolean().optional(),
|
|
@@ -15553,6 +16004,7 @@ const crawlReportBaseSchema = z.object({
|
|
|
15553
16004
|
ai: crawlAiSignalsSchema.optional(),
|
|
15554
16005
|
sitemapDiscovery: crawlSitemapDiscoverySchema.optional(),
|
|
15555
16006
|
externalLinkVerification: crawlExternalLinkVerificationSchema.optional(),
|
|
16007
|
+
siteChecks: crawlSiteChecksSchema.optional(),
|
|
15556
16008
|
agentDiscovery: agentDiscoverySchema.optional(),
|
|
15557
16009
|
warnings: z.array(z.string()),
|
|
15558
16010
|
caveats: z.array(z.string())
|