@rivi-software/agentic-seo-check 0.6.1 → 0.7.0
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 +45 -6
- package/dist/brand.d.ts +1 -0
- package/dist/brand.js +1 -1
- package/dist/brand.js.map +1 -1
- package/dist/core/aeo/audit.js +31 -5
- package/dist/core/aeo/audit.js.map +1 -1
- package/dist/core/aeo/probe.d.ts +2 -0
- package/dist/core/aeo/probe.js +7 -2
- package/dist/core/aeo/probe.js.map +1 -1
- package/dist/core/aeo/site-rules.d.ts +2 -0
- package/dist/core/aeo/site-rules.js +6 -0
- package/dist/core/aeo/site-rules.js.map +1 -1
- package/dist/core/audit.d.ts +4 -0
- package/dist/core/audit.js +3 -0
- package/dist/core/audit.js.map +1 -1
- package/dist/core/crawl.d.ts +11 -2
- package/dist/core/crawl.js +176 -90
- package/dist/core/crawl.js.map +1 -1
- package/dist/core/fetch.d.ts +40 -0
- package/dist/core/fetch.js +74 -15
- package/dist/core/fetch.js.map +1 -1
- package/dist/core/hosted.d.ts +54 -0
- package/dist/core/hosted.js +134 -0
- package/dist/core/hosted.js.map +1 -0
- package/dist/core/refusals.d.ts +27 -0
- package/dist/core/refusals.js +56 -0
- package/dist/core/refusals.js.map +1 -0
- package/dist/core/robots.d.ts +12 -3
- package/dist/core/robots.js +20 -15
- package/dist/core/robots.js.map +1 -1
- package/dist/core/rules/aeo.js +11 -0
- package/dist/core/rules/aeo.js.map +1 -1
- package/dist/core/rules/response.js +13 -0
- package/dist/core/rules/response.js.map +1 -1
- package/dist/core/site-probes.d.ts +3 -0
- package/dist/core/site-probes.js +10 -2
- package/dist/core/site-probes.js.map +1 -1
- package/dist/core/site-rules.d.ts +6 -0
- package/dist/core/site-rules.js +34 -12
- package/dist/core/site-rules.js.map +1 -1
- package/dist/core/sitemap.d.ts +22 -3
- package/dist/core/sitemap.js +29 -10
- package/dist/core/sitemap.js.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-hosted.d.ts +47 -0
- package/dist/mcp-hosted.js +347 -0
- package/dist/mcp-hosted.js.map +1 -0
- package/dist/mcp-shared.d.ts +135 -0
- package/dist/mcp-shared.js +122 -0
- package/dist/mcp-shared.js.map +1 -0
- package/dist/mcp.d.ts +10 -1
- package/dist/mcp.js +11 -118
- package/dist/mcp.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.js +6 -3
- package/dist/version.js.map +1 -1
- package/package.json +10 -3
- package/skills/seo-audit/SKILL.md +2 -0
package/dist/core/fetch.js
CHANGED
|
@@ -13,6 +13,26 @@ const KEPT_HEADERS = [
|
|
|
13
13
|
"vary",
|
|
14
14
|
"content-security-policy",
|
|
15
15
|
];
|
|
16
|
+
/**
|
|
17
|
+
* A request the gate refused, thrown out of `fetchPage` so that no caller takes it for an answer:
|
|
18
|
+
* a link that was not requested is not checked, never broken.
|
|
19
|
+
*/
|
|
20
|
+
export class RequestRefusedError extends Error {
|
|
21
|
+
scope;
|
|
22
|
+
/** The request that was refused: the URL asked for, or a redirect it led to. */
|
|
23
|
+
url;
|
|
24
|
+
constructor(refusal, url) {
|
|
25
|
+
super(refusal.message);
|
|
26
|
+
this.name = "RequestRefusedError";
|
|
27
|
+
this.scope = refusal.scope;
|
|
28
|
+
this.url = url;
|
|
29
|
+
}
|
|
30
|
+
get host() {
|
|
31
|
+
return new URL(this.url).hostname;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** The most of a body an audit reads unless `maxBodyBytes` says otherwise. */
|
|
35
|
+
export const MAX_BODY_BYTES = 5_000_000;
|
|
16
36
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
17
37
|
const pickHeaders = (headers) => {
|
|
18
38
|
const out = {};
|
|
@@ -55,8 +75,10 @@ const readLimited = async (res, maxBytes) => {
|
|
|
55
75
|
break;
|
|
56
76
|
}
|
|
57
77
|
if (value) {
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
// A chunk can be large: keep only what fits, so the cap holds to the byte.
|
|
79
|
+
const room = maxBytes - total;
|
|
80
|
+
chunks.push(value.byteLength > room ? value.subarray(0, room) : value);
|
|
81
|
+
total += Math.min(value.byteLength, room);
|
|
60
82
|
if (total >= maxBytes) {
|
|
61
83
|
truncated = true;
|
|
62
84
|
break;
|
|
@@ -79,8 +101,49 @@ const decodeBody = (bytes, contentType) => {
|
|
|
79
101
|
return new TextDecoder("utf-8").decode(bytes);
|
|
80
102
|
}
|
|
81
103
|
};
|
|
104
|
+
/** Asks the gate before a request, and throws its refusal. */
|
|
105
|
+
const askGate = async (gate, current) => {
|
|
106
|
+
// A URL that does not parse fails in fetch(), with an answer that says so.
|
|
107
|
+
if (!gate || !URL.canParse(current)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const refusal = await gate.beforeRequest(new URL(current));
|
|
111
|
+
if (refusal) {
|
|
112
|
+
throw new RequestRefusedError(refusal, current);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/** Shows the gate an answer, and throws its refusal once the body is let go. */
|
|
116
|
+
const showGate = async (gate, current, res) => {
|
|
117
|
+
const refusal = gate?.afterResponse?.(new URL(current), res);
|
|
118
|
+
if (refusal) {
|
|
119
|
+
await res.body?.cancel().catch(() => null);
|
|
120
|
+
throw new RequestRefusedError(refusal, current);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/** The answer with its body read, up to `maxBodyBytes`. */
|
|
124
|
+
const readAnswer = async (walk, current, res, headers) => {
|
|
125
|
+
const { url, options, started, chain } = walk;
|
|
126
|
+
const read = await readLimited(res, options.maxBodyBytes ?? MAX_BODY_BYTES);
|
|
127
|
+
const done = result(url, current, res.status, headers,
|
|
128
|
+
// The caller reads `bytes`; decoding them too would hold the body a second time.
|
|
129
|
+
options.keepBytes
|
|
130
|
+
? ""
|
|
131
|
+
: decodeBody(read.bytes, res.headers.get("content-type")), chain, started);
|
|
132
|
+
if (read.truncated) {
|
|
133
|
+
done.truncated = true;
|
|
134
|
+
}
|
|
135
|
+
if (options.keepBytes) {
|
|
136
|
+
done.bytes = read.bytes;
|
|
137
|
+
}
|
|
138
|
+
const refusal = options.gate?.afterBody?.(new URL(current), done);
|
|
139
|
+
if (refusal) {
|
|
140
|
+
throw new RequestRefusedError(refusal, current);
|
|
141
|
+
}
|
|
142
|
+
return done;
|
|
143
|
+
};
|
|
82
144
|
const fetchHop = async (walk, current) => {
|
|
83
145
|
const { url, options, readBody, timeoutMs, started, chain } = walk;
|
|
146
|
+
await askGate(options.gate, current);
|
|
84
147
|
const controller = new AbortController();
|
|
85
148
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
86
149
|
try {
|
|
@@ -95,6 +158,7 @@ const fetchHop = async (walk, current) => {
|
|
|
95
158
|
...options.headers,
|
|
96
159
|
},
|
|
97
160
|
});
|
|
161
|
+
await showGate(options.gate, current, res);
|
|
98
162
|
const headers = pickHeaders(res.headers);
|
|
99
163
|
if (REDIRECT_STATUSES.has(res.status) && headers["location"]) {
|
|
100
164
|
chain.push({ url: current, status: res.status });
|
|
@@ -121,21 +185,13 @@ const fetchHop = async (walk, current) => {
|
|
|
121
185
|
done: result(url, current, res.status, headers, null, chain, started),
|
|
122
186
|
};
|
|
123
187
|
}
|
|
124
|
-
|
|
125
|
-
const done = result(url, current, res.status, headers,
|
|
126
|
-
// The caller reads `bytes`; decoding them too would hold the body a second time.
|
|
127
|
-
options.keepBytes
|
|
128
|
-
? ""
|
|
129
|
-
: decodeBody(read.bytes, res.headers.get("content-type")), chain, started);
|
|
130
|
-
if (read.truncated) {
|
|
131
|
-
done.truncated = true;
|
|
132
|
-
}
|
|
133
|
-
if (options.keepBytes) {
|
|
134
|
-
done.bytes = read.bytes;
|
|
135
|
-
}
|
|
136
|
-
return { done };
|
|
188
|
+
return { done: await readAnswer(walk, current, res, headers) };
|
|
137
189
|
}
|
|
138
190
|
catch (error) {
|
|
191
|
+
// A refusal is no failure of the request: it goes to the caller as it is.
|
|
192
|
+
if (error instanceof RequestRefusedError) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
139
195
|
let message = String(error);
|
|
140
196
|
if (error instanceof Error) {
|
|
141
197
|
message =
|
|
@@ -187,6 +243,9 @@ export const toResponseInfo = (r) => {
|
|
|
187
243
|
if (r.error) {
|
|
188
244
|
info.error = r.error;
|
|
189
245
|
}
|
|
246
|
+
if (r.truncated) {
|
|
247
|
+
info.truncated = true;
|
|
248
|
+
}
|
|
190
249
|
return info;
|
|
191
250
|
};
|
|
192
251
|
export const isHtmlResponse = (r) => {
|
package/dist/core/fetch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/core/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAGxC,MAAM,CAAC,MAAM,kBAAkB,GAAG,8CAA8C,OAAO,wDAAwD,CAAC;AAEhJ,MAAM,YAAY,GAAG;IACnB,cAAc;IACd,kBAAkB;IAClB,cAAc;IACd,MAAM;IACN,kBAAkB;IAClB,eAAe;IACf,eAAe;IACf,UAAU;IACV,gBAAgB;IAChB,MAAM;IACN,yBAAyB;CAC1B,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/core/fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Refusal, RequestGate } from "./fetch.js";
|
|
2
|
+
/** The local server, which none of these limits apply to: the way out of every refusal. */
|
|
3
|
+
export declare const LOCAL_SERVER = "npx -y @rivi-software/agentic-seo-check mcp";
|
|
4
|
+
/**
|
|
5
|
+
* What the hosted server reads and requests at most. A Worker isolate has 128 MB of memory: one
|
|
6
|
+
* 2 MB page takes about 80 MB of it to audit, a 5 MB sitemap about 64 MB to parse.
|
|
7
|
+
*/
|
|
8
|
+
export declare const HOSTED_LIMITS: {
|
|
9
|
+
/** Bytes of a page (5 MB on the local server). Above document.html-too-large's 1.5 MB. */
|
|
10
|
+
readonly maxBodyBytes: 2000000;
|
|
11
|
+
/** Characters of inline `html`. */
|
|
12
|
+
readonly maxHtmlChars: 2000000;
|
|
13
|
+
/**
|
|
14
|
+
* Bytes of a request to the server, checked before it is parsed. 2,000,000 characters of HTML
|
|
15
|
+
* are up to 6 MB in UTF-8, and JSON escaping adds to that.
|
|
16
|
+
*/
|
|
17
|
+
readonly maxRequestBytes: 8000000;
|
|
18
|
+
readonly maxPages: 25;
|
|
19
|
+
readonly maxDepth: 3;
|
|
20
|
+
/** Requests at a time; a Worker has at most 6 connections waiting for headers. */
|
|
21
|
+
readonly concurrency: 4;
|
|
22
|
+
/** Internal links, sitemap URLs and images a crawl checks, and as many external links. */
|
|
23
|
+
readonly maxLinkChecks: 50;
|
|
24
|
+
readonly timeoutMs: 15000;
|
|
25
|
+
readonly maxSitemaps: 10;
|
|
26
|
+
readonly maxSitemapBytes: 5000000;
|
|
27
|
+
/** `includePatterns` and `excludePatterns` are the caller's regular expressions, run here. */
|
|
28
|
+
readonly maxPatterns: 20;
|
|
29
|
+
readonly maxPatternChars: 200;
|
|
30
|
+
/** `rules` and `disableRules`. */
|
|
31
|
+
readonly maxRuleIds: 500;
|
|
32
|
+
readonly maxRuleIdChars: 100;
|
|
33
|
+
/** `baseline`: the finding ids of an earlier audit. */
|
|
34
|
+
readonly maxBaselineIds: 5000;
|
|
35
|
+
readonly maxBaselineIdChars: 300;
|
|
36
|
+
readonly maxKeywordChars: 200;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Why the hosted server will not request `url`, or undefined when it may: it requests http and
|
|
40
|
+
* https URLs on public hostnames only. A Worker reaches no private network anyway; the rules give
|
|
41
|
+
* a clear answer instead of a failed request.
|
|
42
|
+
*/
|
|
43
|
+
export declare const hostedUrlProblem: (url: URL) => string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Why an answer is the site refusing the hosted server rather than its page: a challenge, a rate
|
|
46
|
+
* limit or a 403. A 429 or a challenge holds for every later request to the host; a 403 for the
|
|
47
|
+
* URL alone, since a site may forbid one path to everyone. Undefined for a page.
|
|
48
|
+
*/
|
|
49
|
+
export declare const siteRefusal: (url: URL, response: Pick<Response, "status" | "headers">) => Refusal | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* The gate of a hosted tool call: the URL rules and the site refusals, then the Worker's own gate
|
|
52
|
+
* (its request limits), which only sees requests the rules allow.
|
|
53
|
+
*/
|
|
54
|
+
export declare const hostedGate: (limits?: RequestGate) => RequestGate;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hosted server's rules (hosted/, issue #35): what it reads at most, which URLs it requests,
|
|
3
|
+
* and which answers are a site turning it away rather than the site's page. The Worker adds its
|
|
4
|
+
* limits on top, through the gate it passes to `hostedGate`.
|
|
5
|
+
*/
|
|
6
|
+
import { challengeTitle } from "./aeo/probe.js";
|
|
7
|
+
/** The local server, which none of these limits apply to: the way out of every refusal. */
|
|
8
|
+
export const LOCAL_SERVER = "npx -y @rivi-software/agentic-seo-check mcp";
|
|
9
|
+
const USE_LOCAL = `Use the local server for it, which sends the requests from your machine: ${LOCAL_SERVER}`;
|
|
10
|
+
/**
|
|
11
|
+
* What the hosted server reads and requests at most. A Worker isolate has 128 MB of memory: one
|
|
12
|
+
* 2 MB page takes about 80 MB of it to audit, a 5 MB sitemap about 64 MB to parse.
|
|
13
|
+
*/
|
|
14
|
+
export const HOSTED_LIMITS = {
|
|
15
|
+
/** Bytes of a page (5 MB on the local server). Above document.html-too-large's 1.5 MB. */
|
|
16
|
+
maxBodyBytes: 2_000_000,
|
|
17
|
+
/** Characters of inline `html`. */
|
|
18
|
+
maxHtmlChars: 2_000_000,
|
|
19
|
+
/**
|
|
20
|
+
* Bytes of a request to the server, checked before it is parsed. 2,000,000 characters of HTML
|
|
21
|
+
* are up to 6 MB in UTF-8, and JSON escaping adds to that.
|
|
22
|
+
*/
|
|
23
|
+
maxRequestBytes: 8_000_000,
|
|
24
|
+
maxPages: 25,
|
|
25
|
+
maxDepth: 3,
|
|
26
|
+
/** Requests at a time; a Worker has at most 6 connections waiting for headers. */
|
|
27
|
+
concurrency: 4,
|
|
28
|
+
/** Internal links, sitemap URLs and images a crawl checks, and as many external links. */
|
|
29
|
+
maxLinkChecks: 50,
|
|
30
|
+
timeoutMs: 15_000,
|
|
31
|
+
maxSitemaps: 10,
|
|
32
|
+
maxSitemapBytes: 5_000_000,
|
|
33
|
+
/** `includePatterns` and `excludePatterns` are the caller's regular expressions, run here. */
|
|
34
|
+
maxPatterns: 20,
|
|
35
|
+
maxPatternChars: 200,
|
|
36
|
+
/** `rules` and `disableRules`. */
|
|
37
|
+
maxRuleIds: 500,
|
|
38
|
+
maxRuleIdChars: 100,
|
|
39
|
+
/** `baseline`: the finding ids of an earlier audit. */
|
|
40
|
+
maxBaselineIds: 5000,
|
|
41
|
+
maxBaselineIdChars: 300,
|
|
42
|
+
maxKeywordChars: 200,
|
|
43
|
+
};
|
|
44
|
+
/** Names that only resolve on a local machine or network, never on the public internet. */
|
|
45
|
+
const LOCAL_SUFFIXES = [".localhost", ".local", ".internal"];
|
|
46
|
+
/** An IPv4 address as the URL parser writes it, which it does for every numeric form. */
|
|
47
|
+
const IPV4 = /^\d{1,3}(?:\.\d{1,3}){3}$/u;
|
|
48
|
+
/**
|
|
49
|
+
* Why the hosted server will not request `url`, or undefined when it may: it requests http and
|
|
50
|
+
* https URLs on public hostnames only. A Worker reaches no private network anyway; the rules give
|
|
51
|
+
* a clear answer instead of a failed request.
|
|
52
|
+
*/
|
|
53
|
+
export const hostedUrlProblem = (url) => {
|
|
54
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
55
|
+
return `The hosted server requests http and https URLs only, not ${url.protocol} ones (${url.href}).`;
|
|
56
|
+
}
|
|
57
|
+
if (url.username || url.password) {
|
|
58
|
+
return `The hosted server does not send credentials in a URL (${url.host}). A site behind a login is one for the local server, which can send your headers: ${LOCAL_SERVER}`;
|
|
59
|
+
}
|
|
60
|
+
const host = url.hostname.replace(/\.$/u, "");
|
|
61
|
+
if (IPV4.test(host) || host.startsWith("[")) {
|
|
62
|
+
return `The hosted server requests hostnames only, not IP addresses (${host}). ${USE_LOCAL}`;
|
|
63
|
+
}
|
|
64
|
+
if (host === "localhost" ||
|
|
65
|
+
!host.includes(".") ||
|
|
66
|
+
LOCAL_SUFFIXES.some((suffix) => host.endsWith(suffix))) {
|
|
67
|
+
return `The hosted server cannot reach ${host}: it runs on Cloudflare's network, not on your machine or your network. ${USE_LOCAL}`;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
const REFUSED_BY_SITE = `Many sites turn away requests from Cloudflare Workers, which the hosted server runs on, while they serve the page to everyone else. ${USE_LOCAL}`;
|
|
72
|
+
/**
|
|
73
|
+
* Why an answer is the site refusing the hosted server rather than its page: a challenge, a rate
|
|
74
|
+
* limit or a 403. A 429 or a challenge holds for every later request to the host; a 403 for the
|
|
75
|
+
* URL alone, since a site may forbid one path to everyone. Undefined for a page.
|
|
76
|
+
*/
|
|
77
|
+
export const siteRefusal = (url, response) => {
|
|
78
|
+
// Cloudflare marks every challenge page so (developers.cloudflare.com, "Detect a challenge page").
|
|
79
|
+
if (response.headers.get("cf-mitigated")?.toLowerCase() === "challenge") {
|
|
80
|
+
return {
|
|
81
|
+
scope: "host",
|
|
82
|
+
message: `${url.hostname} answered the hosted server with a challenge page (cf-mitigated: challenge). ${REFUSED_BY_SITE}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (response.status === 429) {
|
|
86
|
+
return {
|
|
87
|
+
scope: "host",
|
|
88
|
+
message: `${url.hostname} answered the hosted server with HTTP 429 (Too Many Requests). A site that counts requests per address counts every Cloudflare Worker as one. Try again later, or use the local server, which sends the requests from your machine: ${LOCAL_SERVER}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (response.status === 403) {
|
|
92
|
+
return {
|
|
93
|
+
scope: "url",
|
|
94
|
+
message: `${url.href} answered the hosted server with HTTP 403 (Forbidden). ${REFUSED_BY_SITE}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The statuses a challenge page comes with besides 403 and 429, which `siteRefusal` answers from
|
|
101
|
+
* the status alone: 503 (an "under attack" check) and 405 (AWS WAF's CAPTCHA). A 401 or 404 page
|
|
102
|
+
* titled "Access denied" is the site's own.
|
|
103
|
+
*/
|
|
104
|
+
const CHALLENGE_STATUSES = new Set([405, 503]);
|
|
105
|
+
/** An error page that is a CDN's or bot protection's interstitial rather than the site's own. */
|
|
106
|
+
const challengeRefusal = (url, result) => {
|
|
107
|
+
if (result.status === null ||
|
|
108
|
+
!CHALLENGE_STATUSES.has(result.status) ||
|
|
109
|
+
!result.body) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const title = challengeTitle(result.body.slice(0, 10_000));
|
|
113
|
+
return title === null
|
|
114
|
+
? undefined
|
|
115
|
+
: {
|
|
116
|
+
scope: "host",
|
|
117
|
+
message: `${url.hostname} answered the hosted server with a challenge page ("${title}", HTTP ${result.status}). ${REFUSED_BY_SITE}`,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The gate of a hosted tool call: the URL rules and the site refusals, then the Worker's own gate
|
|
122
|
+
* (its request limits), which only sees requests the rules allow.
|
|
123
|
+
*/
|
|
124
|
+
export const hostedGate = (limits) => ({
|
|
125
|
+
beforeRequest: (url) => {
|
|
126
|
+
const problem = hostedUrlProblem(url);
|
|
127
|
+
return problem
|
|
128
|
+
? { scope: "url", message: problem }
|
|
129
|
+
: limits?.beforeRequest(url);
|
|
130
|
+
},
|
|
131
|
+
afterResponse: (url, response) => siteRefusal(url, response) ?? limits?.afterResponse?.(url, response),
|
|
132
|
+
afterBody: (url, result) => challengeRefusal(url, result) ?? limits?.afterBody?.(url, result),
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=hosted.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted.js","sourceRoot":"","sources":["../../src/core/hosted.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { RequestRefusedError } from "./fetch.js";
|
|
2
|
+
import type { FetchOptions } from "./fetch.js";
|
|
3
|
+
/**
|
|
4
|
+
* The requests of one audit that a gate refused (the hosted server's), and what each rules out:
|
|
5
|
+
* the URL alone, its host for the rest of the audit, or every later request. A request that was
|
|
6
|
+
* not sent says nothing about its URL, so callers leave it unchecked, never broken.
|
|
7
|
+
*/
|
|
8
|
+
export interface Refusals {
|
|
9
|
+
/**
|
|
10
|
+
* Runs `request`, which fetches `url`, unless a refusal already rules it out. Null when it is
|
|
11
|
+
* refused, now or before; any other error goes to the caller.
|
|
12
|
+
*/
|
|
13
|
+
run: <T>(url: string, request: () => Promise<T>) => Promise<T | null>;
|
|
14
|
+
/** The refusal that rules out `url`, if one does. */
|
|
15
|
+
of: (url: string) => RequestRefusedError | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The refusal that ended the requests to a host `isSite` accepts, or to every host: the reason a
|
|
18
|
+
* crawl of that site stopped early.
|
|
19
|
+
*/
|
|
20
|
+
stopped: (isSite: (host: string) => boolean) => RequestRefusedError | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* `options` with a gate that also refuses the hosts already refused, so a redirect to one of them
|
|
23
|
+
* is not sent again. Without a gate nothing is ever refused, and `options` stays as it is.
|
|
24
|
+
*/
|
|
25
|
+
guard: (options: FetchOptions) => FetchOptions;
|
|
26
|
+
}
|
|
27
|
+
export declare const trackRefusals: () => Refusals;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { RequestRefusedError } from "./fetch.js";
|
|
2
|
+
import { hostOf } from "./url.js";
|
|
3
|
+
export const trackRefusals = () => {
|
|
4
|
+
const urls = new Map();
|
|
5
|
+
const hosts = new Map();
|
|
6
|
+
let all;
|
|
7
|
+
const of = (url) => all ?? urls.get(url) ?? hosts.get(hostOf(url) ?? "");
|
|
8
|
+
const record = (url, error) => {
|
|
9
|
+
// The URL asked for, refused itself or through a redirect it led to.
|
|
10
|
+
urls.set(url, error);
|
|
11
|
+
if (error.scope === "all") {
|
|
12
|
+
all ??= error;
|
|
13
|
+
}
|
|
14
|
+
else if (error.scope === "host" && !hosts.has(error.host)) {
|
|
15
|
+
hosts.set(error.host, error);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
of,
|
|
20
|
+
stopped: (isSite) => all ?? [...hosts].find(([host]) => isSite(host))?.[1],
|
|
21
|
+
guard: (options) => {
|
|
22
|
+
const { gate } = options;
|
|
23
|
+
if (!gate) {
|
|
24
|
+
return options;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
...options,
|
|
28
|
+
gate: {
|
|
29
|
+
...gate,
|
|
30
|
+
beforeRequest: (url) => {
|
|
31
|
+
const known = all ?? hosts.get(url.hostname);
|
|
32
|
+
return known
|
|
33
|
+
? { scope: known.scope, message: known.message }
|
|
34
|
+
: gate.beforeRequest(url);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
run: async (url, request) => {
|
|
40
|
+
if (of(url)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return await request();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!(error instanceof RequestRefusedError)) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
record(url, error);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=refusals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusals.js","sourceRoot":"","sources":["../../src/core/refusals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AA6BlC,MAAM,CAAC,MAAM,aAAa,GAAG,GAAa,EAAE;IAC1C,MAAM,IAAI,GAAG,IAAI,GAAG,EAA+B,CAAC;IACpD,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+B,CAAC;IACrD,IAAI,GAAoC,CAAC;IAEzC,MAAM,EAAE,GAAG,CAAC,GAAW,EAAmC,EAAE,CAC1D,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAEvD,MAAM,MAAM,GAAG,CAAC,GAAW,EAAE,KAA0B,EAAQ,EAAE;QAC/D,qEAAqE;QACrE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAErB,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;YAC1B,GAAG,KAAK,KAAK,CAAC;QAChB,CAAC;aAAM,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5D,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,EAAE;QACF,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1E,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE;YACjB,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;YAEzB,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,OAAO,CAAC;YACjB,CAAC;YAED,OAAO;gBACL,GAAG,OAAO;gBACV,IAAI,EAAE;oBACJ,GAAG,IAAI;oBACP,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE;wBACrB,MAAM,KAAK,GAAG,GAAG,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;wBAE7C,OAAO,KAAK;4BACV,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE;4BAChD,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;oBAC9B,CAAC;iBACF;aACF,CAAC;QACJ,CAAC;QACD,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;YAC1B,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;gBACZ,OAAO,IAAI,CAAC;YACd,CAAC;YAED,IAAI,CAAC;gBACH,OAAO,MAAM,OAAO,EAAE,CAAC;YACzB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,CAAC,KAAK,YAAY,mBAAmB,CAAC,EAAE,CAAC;oBAC5C,MAAM,KAAK,CAAC;gBACd,CAAC;gBAED,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBAEnB,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
|
package/dist/core/robots.d.ts
CHANGED
|
@@ -2,11 +2,22 @@ import type { FetchOptions } from "./fetch.js";
|
|
|
2
2
|
import type { RobotsTxtInfo } from "./types.js";
|
|
3
3
|
export interface RobotsResult {
|
|
4
4
|
info: RobotsTxtInfo;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Returns true when robots.txt allows the URL for the audit's User-Agent as robots-parser reads
|
|
7
|
+
* it (unknown/missing robots.txt allows everything). The site rules judge blocked pages by this.
|
|
8
|
+
*/
|
|
6
9
|
allows: (url: string) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Returns true when this crawler may request the URL: the group for its product token
|
|
12
|
+
* (`agentic-seo-check`), or `*` when there is none (RFC 9309, section 2.2.1). A site can keep
|
|
13
|
+
* the audit out without blocking anyone else.
|
|
14
|
+
*/
|
|
15
|
+
crawlerAllows: (url: string) => boolean;
|
|
7
16
|
/** The robots.txt text up to the size crawlers must read (RFC 9309: 500 KiB), or null when there is none. */
|
|
8
17
|
text: string | null;
|
|
9
18
|
}
|
|
19
|
+
/** Short bot token used for robots.txt matching, derived from the full UA string. */
|
|
20
|
+
export declare const robotsUserAgentToken: (userAgent: string) => string;
|
|
10
21
|
/**
|
|
11
22
|
* Whether robots.txt at `url` lets the crawler with product token `agent` fetch `target`. A URL
|
|
12
23
|
* on another origin is allowed: this robots.txt has no say over it.
|
|
@@ -19,5 +30,3 @@ export declare const agentMatcher: (url: string, text: string) => ((target: stri
|
|
|
19
30
|
export declare const ruleLineMatcher: (url: string, text: string) => ((target: string, agent: string) => number);
|
|
20
31
|
export declare const buildRobots: (url: string, status: number | null, error: string | undefined, text: string | null, userAgent: string) => RobotsResult;
|
|
21
32
|
export declare const loadRobotsTxt: (origin: string, userAgent: string, options?: FetchOptions) => Promise<RobotsResult>;
|
|
22
|
-
/** Short bot token used for robots.txt matching, derived from the full UA string. */
|
|
23
|
-
export declare const robotsUserAgentToken: (userAgent: string) => string;
|
package/dist/core/robots.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import robotsParserModule from "robots-parser";
|
|
2
2
|
import { fetchPage } from "./fetch.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* robots-parser is CommonJS, imported statically so a bundler (the hosted server's) takes it in.
|
|
5
|
+
* Node and bundlers hand a default import its module.exports, the parser function itself; its
|
|
6
|
+
* index.d.ts declares the parser as the module's `default`, so TypeScript's Node resolution types
|
|
7
|
+
* the import as the module and its bundler resolution as the parser. Either way, this is the parser.
|
|
8
|
+
*/
|
|
9
|
+
const parserOf = (imported) => ("default" in imported ? imported.default : imported);
|
|
10
|
+
const robotsParser = parserOf(robotsParserModule);
|
|
7
11
|
/** The part of a robots.txt file a crawler must read at least (RFC 9309 section 2.5). */
|
|
8
12
|
const MAX_ROBOTS_BYTES = 512_000;
|
|
13
|
+
/** Short bot token used for robots.txt matching, derived from the full UA string. */
|
|
14
|
+
export const robotsUserAgentToken = (userAgent) => {
|
|
15
|
+
const match = /agentic-seo-check|Googlebot|bingbot/iu.exec(userAgent);
|
|
16
|
+
return match ? match[0] : userAgent;
|
|
17
|
+
};
|
|
9
18
|
/**
|
|
10
19
|
* Whether robots.txt at `url` lets the crawler with product token `agent` fetch `target`. A URL
|
|
11
20
|
* on another origin is allowed: this robots.txt has no say over it.
|
|
@@ -35,17 +44,18 @@ export const buildRobots = (url, status, error, text, userAgent) => {
|
|
|
35
44
|
info.error = error;
|
|
36
45
|
}
|
|
37
46
|
if (text === null) {
|
|
38
|
-
return { info, allows: () => true, text: null };
|
|
47
|
+
return { info, allows: () => true, crawlerAllows: () => true, text: null };
|
|
39
48
|
}
|
|
40
49
|
const robot = robotsParser(url, text);
|
|
41
50
|
info.sitemaps = robot.getSitemaps();
|
|
51
|
+
const token = robotsUserAgentToken(userAgent);
|
|
42
52
|
return {
|
|
43
53
|
info,
|
|
44
54
|
text: text.slice(0, MAX_ROBOTS_BYTES),
|
|
45
|
-
allows: (target) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
allows: (target) => robot.isAllowed(target, userAgent) ?? true,
|
|
56
|
+
// robots-parser keeps what comes before the first "/" of a User-Agent ("Mozilla" for the
|
|
57
|
+
// audit's own), so the product token is passed on its own.
|
|
58
|
+
crawlerAllows: (target) => robot.isAllowed(target, token) ?? true,
|
|
49
59
|
};
|
|
50
60
|
};
|
|
51
61
|
export const loadRobotsTxt = async (origin, userAgent, options = {}) => {
|
|
@@ -60,9 +70,4 @@ export const loadRobotsTxt = async (origin, userAgent, options = {}) => {
|
|
|
60
70
|
!/<html/iu.test(res.body.slice(0, 500));
|
|
61
71
|
return buildRobots(url, res.status, res.error, found ? res.body : null, userAgent);
|
|
62
72
|
};
|
|
63
|
-
/** Short bot token used for robots.txt matching, derived from the full UA string. */
|
|
64
|
-
export const robotsUserAgentToken = (userAgent) => {
|
|
65
|
-
const match = /agentic-seo-check|Googlebot|bingbot/iu.exec(userAgent);
|
|
66
|
-
return match ? match[0] : userAgent;
|
|
67
|
-
};
|
|
68
73
|
//# sourceMappingURL=robots.js.map
|
package/dist/core/robots.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"robots.js","sourceRoot":"","sources":["../../src/core/robots.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"robots.js","sourceRoot":"","sources":["../../src/core/robots.ts"],"names":[],"mappings":"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"}
|
package/dist/core/rules/aeo.js
CHANGED
|
@@ -275,6 +275,17 @@ export const aeoRules = [
|
|
|
275
275
|
})
|
|
276
276
|
: null;
|
|
277
277
|
}),
|
|
278
|
+
defineRule({
|
|
279
|
+
id: "aeo.page-truncated",
|
|
280
|
+
category: "aeo",
|
|
281
|
+
severity: "info",
|
|
282
|
+
description: "The page is larger than the audit reads (5 MB; 2 MB on the hosted server), so only its first part was audited.",
|
|
283
|
+
recommendation: "Findings about the rest of the page are missing. HTML this large is slow for crawlers too: move inline scripts, data and SVG into files the page loads. On the hosted server, the local server reads up to 5 MB.",
|
|
284
|
+
}, ({ page, response }, make) => response?.truncated
|
|
285
|
+
? make(`The audit read the first ${(page.htmlBytes / 1_000_000).toFixed(1)} MB of the page and stopped there.`,
|
|
286
|
+
// About the audit, not the page.
|
|
287
|
+
{ fix: [], evidence: { bytesRead: page.htmlBytes } })
|
|
288
|
+
: null),
|
|
278
289
|
defineRule({
|
|
279
290
|
id: "aeo.noindex",
|
|
280
291
|
category: "aeo",
|