@seostudio/audit-engine 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +57 -2
- package/dist/amp-validator.generated-HCTPRNDJ.js +4 -0
- package/dist/chunk-6QPF6O7D.js +197 -0
- package/dist/chunk-7SGWGESF.js +1753 -0
- package/dist/chunk-J4ZNQFJO.js +733 -0
- package/dist/chunk-XTLRDZOD.js +10238 -0
- package/dist/en-gb.generated-SZLUKHIP.js +7 -0
- package/dist/en-us.generated-H6EP75K6.js +7 -0
- package/dist/es.generated-SYEDFESZ.js +7 -0
- package/dist/facts.d.ts +1165 -0
- package/dist/facts.js +1 -0
- package/dist/fr.generated-CQZWZDYQ.js +7 -0
- package/dist/host-gate.d.ts +68 -0
- package/dist/host-gate.js +1 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +9314 -0
- package/dist/nl.generated-MW64BR3F.js +7 -0
- package/dist/reading.d.ts +517 -0
- package/dist/reading.js +3 -0
- package/dist/resource-check.d.ts +131 -0
- package/dist/resource-check.js +3 -0
- package/dist/types.d.ts +370 -0
- package/dist/types.js +1 -0
- package/package.json +69 -5
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE PACE PER HOST, whoever crawls it: every request the platform's crawler sends to a site first
|
|
3
|
+
* takes the host's next free slot, shared across every project, payer and process (Redis on the
|
|
4
|
+
* server, through a gate the host lends), so many accounts pointed at one site never multiply the rate it is asked at. A host that
|
|
5
|
+
* answered 429 or 503 is backed off for every crawl of it at once. The member's own computer paces
|
|
6
|
+
* itself and never takes a slot.
|
|
7
|
+
*
|
|
8
|
+
* A slot is a reservation: the gate answers how long the caller must wait, and moves the host's next
|
|
9
|
+
* slot one interval past it, in one atomic step.
|
|
10
|
+
*/
|
|
11
|
+
/** A host's shared pace. */
|
|
12
|
+
interface SeoHostGate {
|
|
13
|
+
/**
|
|
14
|
+
* Reserves the host's next slot.
|
|
15
|
+
*
|
|
16
|
+
* @param host - The host, `www.` dropped.
|
|
17
|
+
* @param intervalMs - The least time between two requests to it (its robots.txt `Crawl-delay` or our floor).
|
|
18
|
+
* @param now - Now, in ms.
|
|
19
|
+
* @returns How long to wait before the request, in ms (0 when it may go now).
|
|
20
|
+
*/
|
|
21
|
+
reserve: (host: string, intervalMs: number, now: number) => Promise<number>;
|
|
22
|
+
/**
|
|
23
|
+
* Leaves a host alone until a moment, for every crawl of it (never brings its next slot nearer).
|
|
24
|
+
*
|
|
25
|
+
* @param host - The host, `www.` dropped.
|
|
26
|
+
* @param untilMs - When it may be asked again, in ms.
|
|
27
|
+
* @param now - Now, in ms.
|
|
28
|
+
*/
|
|
29
|
+
backOff: (host: string, untilMs: number, now: number) => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The key a host's pace is kept under: the host lowercased, its `www.` and trailing dot dropped, so a
|
|
33
|
+
* site's two spellings share one pace.
|
|
34
|
+
*
|
|
35
|
+
* @param host - The host.
|
|
36
|
+
* @returns The key.
|
|
37
|
+
*/
|
|
38
|
+
declare function hostGateKey(host: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Whether a host is one of the shared CDN hosts ({@link SEO_SHARED_CDN_HOSTS}): its exact name or a
|
|
41
|
+
* subdomain of one listed.
|
|
42
|
+
*
|
|
43
|
+
* @param host - The host.
|
|
44
|
+
* @returns Whether it is.
|
|
45
|
+
*/
|
|
46
|
+
declare function isSharedCdnHost(host: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* A gate held in this process's memory: the same rules as the shared one, for one process (and tests).
|
|
49
|
+
*
|
|
50
|
+
* @returns The gate.
|
|
51
|
+
*/
|
|
52
|
+
declare function memoryHostGate(): SeoHostGate;
|
|
53
|
+
/**
|
|
54
|
+
* How long a 429 or 503 answer asks the crawler to stay away: its `Retry-After` (seconds or an HTTP
|
|
55
|
+
* date), else the default, never past the ceiling.
|
|
56
|
+
*
|
|
57
|
+
* @param retryAfter - The header, or null.
|
|
58
|
+
* @param now - Now, in ms.
|
|
59
|
+
* @param defaults - The default and the ceiling, in ms.
|
|
60
|
+
* @returns The pause, in ms.
|
|
61
|
+
*/
|
|
62
|
+
declare function retryAfterMs(retryAfter: string | null, now: number, defaults: {
|
|
63
|
+
defaultMs: number;
|
|
64
|
+
maxMs: number;
|
|
65
|
+
}): number;
|
|
66
|
+
|
|
67
|
+
export { hostGateKey, isSharedCdnHost, memoryHostGate, retryAfterMs };
|
|
68
|
+
export type { SeoHostGate };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { hostGateKey, isSharedCdnHost, memoryHostGate, retryAfterMs } from './chunk-6QPF6O7D.js';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { SeoCrawlFetcher, SeoPageRenderer } from './types.js';
|
|
2
|
+
import { SeoPageReading, SeoReadPageOptions } from './reading.js';
|
|
3
|
+
import './facts.js';
|
|
4
|
+
import './host-gate.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE AUDIT REPORT: what a standalone crawl ({@link crawlSite} in `./site`) hands back, as plain JSON
|
|
8
|
+
* a program can store, diff or turn into a file. Versioned by {@link AUDIT_REPORT_SCHEMA_VERSION}: a
|
|
9
|
+
* change that breaks a reader raises it; a new optional field does not.
|
|
10
|
+
*
|
|
11
|
+
* Every finding is a STABLE rule id from the catalog (`./catalog`) with its evidence as structured
|
|
12
|
+
* params, never English, so a reader can word it in any language and match on the id.
|
|
13
|
+
*/
|
|
14
|
+
/** The report format's version. */
|
|
15
|
+
declare const AUDIT_REPORT_SCHEMA_VERSION = 1;
|
|
16
|
+
/** How serious an issue is: `critical` (shown as an error), `warning` or `notice`. */
|
|
17
|
+
type AuditReportSeverity = "critical" | "warning" | "notice";
|
|
18
|
+
/**
|
|
19
|
+
* Whether a finding is a problem to fix (`issue`, counted and scored) or a small improvement that is
|
|
20
|
+
* not a problem (`tip`, listed apart, never counted as an issue).
|
|
21
|
+
*/
|
|
22
|
+
type AuditReportKind = "issue" | "tip";
|
|
23
|
+
/** One page the crawl read. */
|
|
24
|
+
interface AuditReportPage {
|
|
25
|
+
/** The page's URL, `https://` plus its key. */
|
|
26
|
+
url: string;
|
|
27
|
+
/** The page's key: host, path and query, normalized (`example.com/blog`). */
|
|
28
|
+
pageKey: string;
|
|
29
|
+
/** The status it answered (a redirect's own status), 0 when nothing answered, null when not read. */
|
|
30
|
+
status: number | null;
|
|
31
|
+
/** Why nothing usable answered (`timeout`, `network`, `status_403`...), or null. */
|
|
32
|
+
error: string | null;
|
|
33
|
+
/** Its title, or null when it has none or is not a page. */
|
|
34
|
+
title: string | null;
|
|
35
|
+
/** Clicks from the home page along followed links, or null when no such path reaches it. */
|
|
36
|
+
clickDepth: number | null;
|
|
37
|
+
/** Words in its main text, or null when it is not a page. */
|
|
38
|
+
wordCount: number | null;
|
|
39
|
+
/** How many issues (not tips) were filed on it. */
|
|
40
|
+
issues: number;
|
|
41
|
+
}
|
|
42
|
+
/** One finding on one page (a whole-site finding is filed on the home page). */
|
|
43
|
+
interface AuditReportIssue {
|
|
44
|
+
/** The check's stable id (`title_missing`, `broken_internal_links`...). */
|
|
45
|
+
ruleId: string;
|
|
46
|
+
severity: AuditReportSeverity;
|
|
47
|
+
/** The area of the site the check is about (`metadata`, `links`, `performance`...). */
|
|
48
|
+
category: string;
|
|
49
|
+
kind: AuditReportKind;
|
|
50
|
+
/** Whether the check judges the whole site rather than one page. */
|
|
51
|
+
siteWide: boolean;
|
|
52
|
+
/** The page's URL. */
|
|
53
|
+
page: string;
|
|
54
|
+
/** The page's key. */
|
|
55
|
+
pageKey: string;
|
|
56
|
+
/** What the check found, as structured params (counts, examples, measured values). */
|
|
57
|
+
evidence: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
/** The counts of a report. */
|
|
60
|
+
interface AuditReportSummary {
|
|
61
|
+
/** Pages the crawl read. */
|
|
62
|
+
pages: number;
|
|
63
|
+
/** Issues filed (tips left out). */
|
|
64
|
+
issues: number;
|
|
65
|
+
/** Tips filed. */
|
|
66
|
+
tips: number;
|
|
67
|
+
/** Issues by severity. */
|
|
68
|
+
bySeverity: Record<AuditReportSeverity, number>;
|
|
69
|
+
/** Findings (issues and tips) by rule id. */
|
|
70
|
+
byRule: Record<string, number>;
|
|
71
|
+
/** Pages with at least one critical issue. */
|
|
72
|
+
pagesWithErrors: number;
|
|
73
|
+
/** The site's health score, 0 to 100, or null when no page could be judged. */
|
|
74
|
+
score: number | null;
|
|
75
|
+
}
|
|
76
|
+
/** What a standalone crawl found. */
|
|
77
|
+
interface AuditReport {
|
|
78
|
+
schemaVersion: typeof AUDIT_REPORT_SCHEMA_VERSION;
|
|
79
|
+
/** The URL the crawl started from. */
|
|
80
|
+
startUrl: string;
|
|
81
|
+
/** When the crawl started and finished (ISO). */
|
|
82
|
+
startedAt: string;
|
|
83
|
+
finishedAt: string;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the crawl read every page it found: false when it stopped at its page cap, so the checks
|
|
86
|
+
* that judge what is MISSING from the site (an orphan page, a page left out of the sitemap) held
|
|
87
|
+
* back.
|
|
88
|
+
*/
|
|
89
|
+
complete: boolean;
|
|
90
|
+
/** Every page read, in the order it was found. */
|
|
91
|
+
pages: AuditReportPage[];
|
|
92
|
+
/** Every finding, issues before tips, the most serious first. */
|
|
93
|
+
issues: AuditReportIssue[];
|
|
94
|
+
summary: AuditReportSummary;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A report's findings as CSV (RFC 4180, CRLF line ends): one row per finding with its page, rule id,
|
|
98
|
+
* severity, category, kind, whether it is site-wide, and its evidence as JSON.
|
|
99
|
+
*
|
|
100
|
+
* @param report - The report.
|
|
101
|
+
* @returns The CSV text, the header first.
|
|
102
|
+
*/
|
|
103
|
+
declare function toCsv(report: AuditReport): string;
|
|
104
|
+
/**
|
|
105
|
+
* A report as one standalone HTML page (no script, no external file): the counts, every finding
|
|
106
|
+
* grouped by rule (issues first, the most serious first), and every page read.
|
|
107
|
+
*
|
|
108
|
+
* @param report - The report.
|
|
109
|
+
* @returns The HTML document.
|
|
110
|
+
*/
|
|
111
|
+
declare function renderHtmlReport(report: AuditReport): string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* THE CRAWLER'S ADDRESS GUARD: the transport a standalone crawl reads the web through by default, so a
|
|
115
|
+
* page that links or redirects to a private address (the machine itself, the local network, a cloud's
|
|
116
|
+
* metadata service) can never make the crawler fetch it.
|
|
117
|
+
*
|
|
118
|
+
* Every request, each redirect hop included (the crawler follows redirects itself, one request per
|
|
119
|
+
* hop), is checked before it is sent: http or https only, no credentials in the URL, the host resolved
|
|
120
|
+
* and refused when ANY address it resolves to is loopback, unspecified, private (RFC 1918), shared
|
|
121
|
+
* (CGNAT), link-local (the metadata address included), unique-local, multicast, reserved, or an IPv6
|
|
122
|
+
* address carrying such an IPv4 (mapped, NAT64, 6to4). The connection is then pinned to the very
|
|
123
|
+
* addresses checked, so a host that answers DNS with a public address and then a private one cannot
|
|
124
|
+
* be dialled on an address the guard never saw. A redirect is returned as it is, never followed.
|
|
125
|
+
*
|
|
126
|
+
* A refused request throws an error named `BlockedUrlError` whose `reason` says why (`refused`,
|
|
127
|
+
* `not_found` when the host does not exist, `dns_unavailable` when DNS could not answer), which the
|
|
128
|
+
* crawler files as a blocked, missing or unreachable URL.
|
|
129
|
+
*/
|
|
130
|
+
/** Why a URL failed the guard. */
|
|
131
|
+
type BlockedUrlReason = "refused" | "not_found" | "dns_unavailable";
|
|
132
|
+
/** Thrown when a URL fails the guard. */
|
|
133
|
+
declare class BlockedUrlError extends Error {
|
|
134
|
+
readonly reason: BlockedUrlReason;
|
|
135
|
+
/**
|
|
136
|
+
* @param message - What was refused.
|
|
137
|
+
* @param reason - Why.
|
|
138
|
+
*/
|
|
139
|
+
constructor(message: string, reason?: BlockedUrlReason);
|
|
140
|
+
}
|
|
141
|
+
/** How the guarded transport is built. */
|
|
142
|
+
interface GuardedFetchOptions {
|
|
143
|
+
/**
|
|
144
|
+
* The host names (lower-cased; an IPv6 literal with or without its brackets) whose private or local
|
|
145
|
+
* addresses may be reached: a staging site on the local network or on this machine, named one by
|
|
146
|
+
* one. Every other host stays guarded, so a link from that site to another private address (a
|
|
147
|
+
* cloud's metadata service, the local network) is still refused. Empty by default.
|
|
148
|
+
*/
|
|
149
|
+
allowPrivateHosts?: ReadonlySet<string>;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether an IP address is one the guard refuses to connect to.
|
|
153
|
+
*
|
|
154
|
+
* @param address - An IPv4 or IPv6 address.
|
|
155
|
+
* @returns Whether it is refused; anything that is not an IP address is refused.
|
|
156
|
+
*/
|
|
157
|
+
declare function isBlockedAddress(address: string): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* The guarded crawler transport ({@link guardedFetch}), the private addresses of the hosts named in
|
|
160
|
+
* `allowPrivateHosts` reachable. There is no unguarded mode: every request is still checked, looked up
|
|
161
|
+
* and pinned.
|
|
162
|
+
*
|
|
163
|
+
* @param options - The hosts whose private addresses may be reached.
|
|
164
|
+
* @returns The transport.
|
|
165
|
+
*/
|
|
166
|
+
declare function createGuardedFetch(options?: GuardedFetchOptions): SeoCrawlFetcher;
|
|
167
|
+
/**
|
|
168
|
+
* THE DEFAULT TRANSPORT of a standalone crawl (`crawlSite`) and of `readPage`: every request checked against
|
|
169
|
+
* private, local and reserved addresses, its connection pinned to the addresses checked, its redirect
|
|
170
|
+
* returned rather than followed, and cut at its timeout.
|
|
171
|
+
*/
|
|
172
|
+
declare const guardedFetch: SeoCrawlFetcher;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* THE STANDALONE CRAWL: a whole site crawled in this process with the same reading, discovery and
|
|
176
|
+
* rules the SEOStudio app uses, held in memory instead of a database, and judged by every rule that
|
|
177
|
+
* needs no paid data. And one page's reading, for a client that fetches from its own address.
|
|
178
|
+
*
|
|
179
|
+
* What it leaves out, because it needs an account or the app's history: Search Console, Analytics,
|
|
180
|
+
* lab tests and CrUX data (their rules stay quiet), the TLS read of the site's host, the members'
|
|
181
|
+
* custom checks and AI prompts, and a page's change history. A one-off crawl confirms an outbound
|
|
182
|
+
* link as gone by asking it twice; a server error on another site is never confirmed within one run
|
|
183
|
+
* (an outage passes).
|
|
184
|
+
*/
|
|
185
|
+
/** Where a standalone crawl stands, as {@link CrawlSiteOptions.onProgress} is told. */
|
|
186
|
+
interface CrawlSiteProgress {
|
|
187
|
+
/** `plan`: robots.txt and the sitemaps; `pages`: the pages; `resources`: what they load; `audit`: the rules. */
|
|
188
|
+
phase: "plan" | "pages" | "resources" | "audit" | "done";
|
|
189
|
+
/** Pages read so far. */
|
|
190
|
+
pagesRead: number;
|
|
191
|
+
/** Pages found so far, read or not (never more than the page cap). */
|
|
192
|
+
pagesFound: number;
|
|
193
|
+
/** The URL being read, or null between two reads. */
|
|
194
|
+
currentUrl: string | null;
|
|
195
|
+
}
|
|
196
|
+
/** How a standalone crawl runs. */
|
|
197
|
+
interface CrawlSiteOptions {
|
|
198
|
+
/** The site's start URL, e.g. `https://example.com/`. */
|
|
199
|
+
url: string;
|
|
200
|
+
/** The most pages known to the crawl, read or not; 500 by default. */
|
|
201
|
+
maxPages?: number;
|
|
202
|
+
/** Pages read at once; 2 by default. Each host is still asked at its polite pace. */
|
|
203
|
+
concurrency?: number;
|
|
204
|
+
/** The User-Agent every request sends; the crawler's own by default. */
|
|
205
|
+
userAgent?: string;
|
|
206
|
+
/** Told whenever the crawl moves on. */
|
|
207
|
+
onProgress?: (progress: CrawlSiteProgress) => void;
|
|
208
|
+
/**
|
|
209
|
+
* The host names (lower-cased) whose private or local addresses the crawl may reach: a staging site
|
|
210
|
+
* on the local network or on this machine, named one by one. Every other request, each redirect hop
|
|
211
|
+
* included, is refused when its host resolves to a private, local or reserved address
|
|
212
|
+
* (`guardedFetch`), so a link from the staging site to another private address is still refused.
|
|
213
|
+
*/
|
|
214
|
+
allowPrivateHosts?: ReadonlySet<string>;
|
|
215
|
+
/**
|
|
216
|
+
* The transport every request goes through, in place of the address guard (redirects are followed
|
|
217
|
+
* hop by hop, so it must not follow them itself). Tests pass a fake site.
|
|
218
|
+
*/
|
|
219
|
+
fetch?: SeoCrawlFetcher;
|
|
220
|
+
/** A browser to render each page in; without one every page is judged on its raw HTML. */
|
|
221
|
+
render?: SeoPageRenderer;
|
|
222
|
+
/** The clock; the wall clock by default. */
|
|
223
|
+
now?: () => Date;
|
|
224
|
+
/** The pause between requests; a real one by default. */
|
|
225
|
+
sleep?: (ms: number) => Promise<void>;
|
|
226
|
+
}
|
|
227
|
+
/** One page's reading, the shape a server accepts from a client that fetched the page itself. */
|
|
228
|
+
type PageReading = SeoPageReading;
|
|
229
|
+
/** How one page is read. */
|
|
230
|
+
interface ReadPageOptions extends SeoReadPageOptions {
|
|
231
|
+
/** The User-Agent the request sends; the crawler's own by default. */
|
|
232
|
+
userAgent?: string;
|
|
233
|
+
/** The host names (lower-cased) whose private or local addresses may be reached; none by default. */
|
|
234
|
+
allowPrivateHosts?: ReadonlySet<string>;
|
|
235
|
+
/** The transport, in place of the address guard (redirects followed hop by hop). */
|
|
236
|
+
fetch?: SeoCrawlFetcher;
|
|
237
|
+
/** A browser to render the page in; without one the reading is raw only. */
|
|
238
|
+
render?: SeoPageRenderer;
|
|
239
|
+
/** The clock; the wall clock by default. */
|
|
240
|
+
now?: () => Date;
|
|
241
|
+
/** The pause before a retried request; a real one by default. */
|
|
242
|
+
sleep?: (ms: number) => Promise<void>;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* READS ONE PAGE as the SEOStudio desktop app does: the fetch (redirects followed hop by hop, a server
|
|
246
|
+
* error asked again once), the extractor's read of its HTML, and the rendered read when a browser is
|
|
247
|
+
* lent. The reading is cut to the bounds a server accepts, so a client can send it as it is.
|
|
248
|
+
*
|
|
249
|
+
* @param url - The page's URL.
|
|
250
|
+
* @param options - How it is read.
|
|
251
|
+
* @returns The reading.
|
|
252
|
+
* @throws When the URL names no http(s) page.
|
|
253
|
+
*/
|
|
254
|
+
declare function readPage(url: string, options?: ReadPageOptions): Promise<PageReading>;
|
|
255
|
+
/**
|
|
256
|
+
* CRAWLS A SITE in this process and audits it: robots.txt and the sitemaps, then every page found from
|
|
257
|
+
* the home page and the sitemaps (up to the cap), what the pages load and link out to, the site's host
|
|
258
|
+
* variants, and every rule that needs no paid data. Polite as the app's crawler: robots.txt obeyed,
|
|
259
|
+
* its `Crawl-delay` kept (one second at least between two requests to a host), a 429 or 503 retried
|
|
260
|
+
* only after a pause.
|
|
261
|
+
*
|
|
262
|
+
* @param options - The site, the caps and the progress hook.
|
|
263
|
+
* @returns The report.
|
|
264
|
+
* @throws When the URL names no http(s) site.
|
|
265
|
+
*/
|
|
266
|
+
declare function crawlSite(options: CrawlSiteOptions): Promise<AuditReport>;
|
|
267
|
+
|
|
268
|
+
export { AUDIT_REPORT_SCHEMA_VERSION, BlockedUrlError, crawlSite, createGuardedFetch, guardedFetch, isBlockedAddress, readPage, renderHtmlReport, toCsv };
|
|
269
|
+
export type { AuditReport, AuditReportIssue, AuditReportKind, AuditReportPage, AuditReportSeverity, AuditReportSummary, BlockedUrlReason, CrawlSiteOptions, CrawlSiteProgress, GuardedFetchOptions, PageReading, ReadPageOptions };
|