@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,517 @@
|
|
|
1
|
+
import { SeoPageRenderOutcomeValue, SeoPageRenderResourceFailure, SeoPageDomFacts, SeoPageA11yFacts, SeoPageDesktopView, SeoPageCheckFacts, SeoCustomCheck, SeoRobotsFetchOutcomeValue, SeoSoft404Probe, SeoSitemapFileFacts, SeoAgentFilesFacts, SeoRobotsFetchFacts } from './facts.js';
|
|
2
|
+
import { SeoHostGate } from './host-gate.js';
|
|
3
|
+
import { FetchedResource, ExtractedPage, SeoRenderScript, SeoCrawlFetcher, SeoPageRenderer, SeoBrowserRender } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The bounds a reading carried to the server keeps. Far above any real page: they exist so one upload
|
|
7
|
+
* can never be unbounded, and a page past them is read the same way minus the overflow. In a file of
|
|
8
|
+
* its own so the route's validator loads none of the extractor.
|
|
9
|
+
*/
|
|
10
|
+
declare const SEO_READING_LIMITS: {
|
|
11
|
+
/** Characters of a page's main text. */
|
|
12
|
+
readonly mainTextChars: 2000000;
|
|
13
|
+
/** Links of one page. */
|
|
14
|
+
readonly links: 10000;
|
|
15
|
+
/** Characters of a URL, an anchor or a header value. */
|
|
16
|
+
readonly textChars: 4000;
|
|
17
|
+
/** Entries of a list the audit only counts or samples (mixed content, unsized images, canonicals). */
|
|
18
|
+
readonly listItems: 500;
|
|
19
|
+
/** JSON-LD blocks, and their characters together. */
|
|
20
|
+
readonly jsonLdBlocks: 200;
|
|
21
|
+
readonly jsonLdChars: 1000000;
|
|
22
|
+
/** hreflang alternates. */
|
|
23
|
+
readonly hreflang: 1000;
|
|
24
|
+
/** Schema types. */
|
|
25
|
+
readonly schemaTypes: 200;
|
|
26
|
+
/** Hops of a redirect chain. */
|
|
27
|
+
readonly chainHops: 20;
|
|
28
|
+
/** Readings in one batch. */
|
|
29
|
+
readonly batchPages: 50;
|
|
30
|
+
/** Sitemap URLs in a plan reading, the characters of each, and sitemap files. */
|
|
31
|
+
readonly sitemapUrls: number;
|
|
32
|
+
readonly sitemapUrlChars: 2048;
|
|
33
|
+
readonly sitemapFiles: number;
|
|
34
|
+
/** Characters of a robots.txt. */
|
|
35
|
+
readonly robotsChars: number;
|
|
36
|
+
/** Console lines of a render, the characters of each, and the failed resources it names. */
|
|
37
|
+
readonly renderConsoleLines: 20;
|
|
38
|
+
readonly renderConsoleChars: 300;
|
|
39
|
+
readonly renderFailures: 50;
|
|
40
|
+
/** Resource check readings in one upload: one hand-out's worth. */
|
|
41
|
+
readonly resourceReadings: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* THE GRAMMAR CHECK OF A PAGE, on a member's computer only: the host lends a checker (the desktop app's
|
|
46
|
+
* Harper, an English grammar checker compiled to WebAssembly that runs on the computer, no server and no
|
|
47
|
+
* Java), and this file decides what it reads and what is kept. Only English pages are read (a page
|
|
48
|
+
* whose language says English, or says none), the title, the H1 and the main text, bounded; only the
|
|
49
|
+
* problem's kind, its text and the first suggestion are kept, never an English message.
|
|
50
|
+
*
|
|
51
|
+
* PRECISION FIRST: only the kinds that name a mistake in a sentence are kept ({@link GRAMMAR_KINDS}),
|
|
52
|
+
* and never one that spans two lines (two table cells or list items read as one sentence).
|
|
53
|
+
* The checker's other kinds misfire on web text, where headings, menus, list items, table cells and code
|
|
54
|
+
* are joined into one text: a heading or a list item reads as a sentence without a capital or a full
|
|
55
|
+
* stop, every straight quote as unbalanced, a table as one 150-word sentence, and style advice ("config"
|
|
56
|
+
* to "configuration", an Oxford comma) is not a mistake. Spelling is left to the spelling check, which
|
|
57
|
+
* judges a word against the site's own vocabulary. Pure apart from the checker.
|
|
58
|
+
*/
|
|
59
|
+
/** One problem the checker found. */
|
|
60
|
+
interface SeoGrammarLint {
|
|
61
|
+
/** The kind of problem, as the checker names it (`Grammar`, `Repetition`, ...). */
|
|
62
|
+
kind: string;
|
|
63
|
+
/** The text it is about. */
|
|
64
|
+
text: string;
|
|
65
|
+
/** The first replacement it suggests, or null. */
|
|
66
|
+
suggestion: string | null;
|
|
67
|
+
}
|
|
68
|
+
/** Checks a text's grammar; the host's checker (Harper on the desktop app). Rejects when it cannot. */
|
|
69
|
+
type SeoGrammarChecker = (text: string) => Promise<SeoGrammarLint[]>;
|
|
70
|
+
|
|
71
|
+
/** One rule, split once for matching. */
|
|
72
|
+
interface CompiledRule {
|
|
73
|
+
allow: boolean;
|
|
74
|
+
/** The rule's length, which decides the longest match. */
|
|
75
|
+
length: number;
|
|
76
|
+
/** The rule split on `*`. */
|
|
77
|
+
parts: string[];
|
|
78
|
+
/** Whether the rule ends in `$`. */
|
|
79
|
+
anchored: boolean;
|
|
80
|
+
/** The rule as the file writes it, e.g. `Disallow: /private`. */
|
|
81
|
+
line: string;
|
|
82
|
+
/** The `User-agent` name of the group it came from (`*` for the catch-all group). */
|
|
83
|
+
userAgent: string;
|
|
84
|
+
}
|
|
85
|
+
/** The rules that govern one agent, ready to match. */
|
|
86
|
+
interface CompiledRobots {
|
|
87
|
+
rules: CompiledRule[];
|
|
88
|
+
/** The pause the file asks between requests, in ms, or null. */
|
|
89
|
+
crawlDelayMs: number | null;
|
|
90
|
+
sitemaps: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Which languages a reading checks: those light enough for the server, or every one (a member's computer). */
|
|
94
|
+
type SeoSpellingScope = "server" | "all";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* WHAT THE CRAWL SENDS A SITE THAT ASKS TO SIGN IN: the members' extra request headers, HTTP basic
|
|
98
|
+
* auth and (on the desktop app) the cookies of a recorded login, as one set of headers - sent ONLY to
|
|
99
|
+
* the project's own host and its `www.` twin, never to another site a page links to or loads from,
|
|
100
|
+
* and checked again at every redirect hop, so a redirect off the site drops them.
|
|
101
|
+
*
|
|
102
|
+
* PURE: no database, no network; a desktop main process can run it.
|
|
103
|
+
*/
|
|
104
|
+
/** The headers a request to one URL carries for the site, or null when the URL is not on the site. */
|
|
105
|
+
type SeoSiteHeaders = (url: string) => Record<string, string> | null;
|
|
106
|
+
|
|
107
|
+
/** What a sitemap's hreflang alternates (`<xhtml:link rel="alternate" hreflang>`) showed. */
|
|
108
|
+
interface SitemapHreflang {
|
|
109
|
+
/** Page entries that carry alternates. */
|
|
110
|
+
urls: number;
|
|
111
|
+
/** Whether an alternate was written without the XHTML namespace declared for it. */
|
|
112
|
+
namespaceMissing: boolean;
|
|
113
|
+
/** Entries whose set leaves out the entry itself, or differs from the set a listed alternate carries. */
|
|
114
|
+
incomplete: number;
|
|
115
|
+
/** A few of those entries' URLs. */
|
|
116
|
+
examples: string[];
|
|
117
|
+
}
|
|
118
|
+
/** What one `<url>` entry of a sitemap states beyond its URL. */
|
|
119
|
+
interface ParsedSitemapEntry {
|
|
120
|
+
/** The `<lastmod>` as written, or null. */
|
|
121
|
+
lastmod: string | null;
|
|
122
|
+
/** Whether it carries a tag Google ignores (`priority`, `changefreq`, a retired image tag). */
|
|
123
|
+
deprecated: boolean;
|
|
124
|
+
/** How many `<image:image>` it lists. */
|
|
125
|
+
images: number;
|
|
126
|
+
/** How many `<video:video>` lack a required tag (a thumbnail, a title, a description, a content or player URL). */
|
|
127
|
+
videosIncomplete: number;
|
|
128
|
+
/** Each `<news:publication_date>` as written. */
|
|
129
|
+
newsDates: string[];
|
|
130
|
+
}
|
|
131
|
+
/** One parsed sitemap file. */
|
|
132
|
+
interface ParsedSitemap {
|
|
133
|
+
/** Page URLs (`<url><loc>`), the first {@link SITEMAP_KEPT_URLS}. */
|
|
134
|
+
urls: string[];
|
|
135
|
+
/** Every page URL the file lists, kept or not. */
|
|
136
|
+
listed: number;
|
|
137
|
+
/** Each page URL's `<lastmod>` day (`YYYY-MM-DD`), or null when it has none; aligned with `urls`. */
|
|
138
|
+
lastmods: (string | null)[];
|
|
139
|
+
/** Each page URL's extra facts; aligned with `urls`. */
|
|
140
|
+
entries: ParsedSitemapEntry[];
|
|
141
|
+
/** Nested sitemaps (`<sitemap><loc>`). */
|
|
142
|
+
sitemaps: string[];
|
|
143
|
+
/**
|
|
144
|
+
* The root element's local name (`urlset`, `sitemapindex`, or a feed's `rss` or `feed`), `text`
|
|
145
|
+
* for a plain list of URLs, or null when it is none of them.
|
|
146
|
+
*/
|
|
147
|
+
root: string | null;
|
|
148
|
+
/** Whether the root declares a sitemap namespace (a feed or a text list needs none). */
|
|
149
|
+
namespaced: boolean;
|
|
150
|
+
/** Whether the XML is well formed: every element closed, in order (a cut or broken file is not). */
|
|
151
|
+
wellFormed: boolean;
|
|
152
|
+
/** The hreflang alternates its entries carry, or undefined when none does. */
|
|
153
|
+
hreflang?: SitemapHreflang;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The `Accept` header of a page fetch. */
|
|
157
|
+
declare const SEO_ACCEPT_HTML = "text/html,application/xhtml+xml;q=0.9,*/*;q=0.5";
|
|
158
|
+
/** Statuses that say "not now" rather than anything about the page. */
|
|
159
|
+
declare const SEO_FAILURE_STATUSES: ReadonlySet<number>;
|
|
160
|
+
/** What a reading reaches outside itself: the guarded transport, the clock, a pause and the browser. */
|
|
161
|
+
interface SeoReadDeps {
|
|
162
|
+
fetch: SeoCrawlFetcher;
|
|
163
|
+
now: () => Date;
|
|
164
|
+
sleep: (ms: number) => Promise<void>;
|
|
165
|
+
/** The browser a page is rendered in; a reading made without one is raw only. */
|
|
166
|
+
render?: SeoPageRenderer;
|
|
167
|
+
/** The pace every crawl of a host shares (the server's); without one, a reading only pauses itself. */
|
|
168
|
+
hostGate?: SeoHostGate;
|
|
169
|
+
/**
|
|
170
|
+
* The site's sign-in headers (`./site-access`), for the requests the rendering browser makes to the
|
|
171
|
+
* site; `fetch` already adds them to the crawl's own requests (`withSiteAccess`).
|
|
172
|
+
*/
|
|
173
|
+
siteHeaders?: SeoSiteHeaders;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Waits before a request to a site: for the host's shared slot when a gate is at hand (every crawl of
|
|
177
|
+
* the host takes turns), else the plain pause.
|
|
178
|
+
*
|
|
179
|
+
* @param deps - The clock, the pause and the gate.
|
|
180
|
+
* @param url - The URL about to be asked for.
|
|
181
|
+
* @param delayMs - The least time between two requests to the host.
|
|
182
|
+
* @param first - Whether this is the reading's first request (no plain pause before it).
|
|
183
|
+
*/
|
|
184
|
+
declare function paceRequest(deps: Pick<SeoReadDeps, "now" | "sleep" | "hostGate">, url: string, delayMs: number, first?: boolean): Promise<void>;
|
|
185
|
+
/**
|
|
186
|
+
* The pause between two requests to the site: the robots.txt `Crawl-delay`, never below the configured
|
|
187
|
+
* floor nor above its ceiling.
|
|
188
|
+
*
|
|
189
|
+
* @param robots - The site's robots.txt.
|
|
190
|
+
* @returns The pause in ms.
|
|
191
|
+
*/
|
|
192
|
+
declare function politeDelay(robots: CompiledRobots): number;
|
|
193
|
+
/**
|
|
194
|
+
* A robots.txt compiled for the group the crawl obeys: the crawler's own token by default.
|
|
195
|
+
*
|
|
196
|
+
* @param text - The file, or null when none was read.
|
|
197
|
+
* @param agent - The product token whose group is obeyed (`robotsAgentOf` in `./discovery`).
|
|
198
|
+
* @returns The compiled rules.
|
|
199
|
+
*/
|
|
200
|
+
declare function crawlerRobots(text: string | null, agent?: string): CompiledRobots;
|
|
201
|
+
/** What a plan reading needs to know about the site, from the server. */
|
|
202
|
+
interface SeoSitePlanInput {
|
|
203
|
+
/** The project's website. */
|
|
204
|
+
siteUrl: string;
|
|
205
|
+
/** The User-Agent every request sends. */
|
|
206
|
+
userAgent: string;
|
|
207
|
+
/** Whether the project holds a verified property for its site (every site is crawled whole). */
|
|
208
|
+
verified: boolean;
|
|
209
|
+
/** The robots.txt group the crawl obeys; the crawler's own when absent (an older server). */
|
|
210
|
+
robotsAgent?: string;
|
|
211
|
+
/** Whether the soft-404 probe is due. */
|
|
212
|
+
probeSoft404: boolean;
|
|
213
|
+
/** The robots.txt read last time, used when the site does not answer for it now. */
|
|
214
|
+
fallbackRobotsTxt: string | null;
|
|
215
|
+
/** The last soft-404 probe, for a plan that does not probe again; absent from an older server. */
|
|
216
|
+
soft404?: SeoSoft404Probe | null;
|
|
217
|
+
}
|
|
218
|
+
/** What the plan read from the site. */
|
|
219
|
+
interface SeoSitePlanReading {
|
|
220
|
+
/**
|
|
221
|
+
* The robots.txt the crawl obeys: the file the site served, empty when it answered 4xx (no file
|
|
222
|
+
* allows everything), the last one read when it did not answer, or null when there is none at all.
|
|
223
|
+
*/
|
|
224
|
+
robotsTxt: string | null;
|
|
225
|
+
sitemap: {
|
|
226
|
+
/** Whether any sitemap could be read. */
|
|
227
|
+
read: boolean;
|
|
228
|
+
/** Each listed page on the site's own hosts, its URL as written (one per page key). */
|
|
229
|
+
urls: string[];
|
|
230
|
+
/** Each url set file's facts. */
|
|
231
|
+
files: SeoSitemapFileFacts[];
|
|
232
|
+
};
|
|
233
|
+
/** The probe for a page that cannot exist, or null when it was not made (or nothing usable answered). */
|
|
234
|
+
soft404: SeoSoft404Probe | null;
|
|
235
|
+
/**
|
|
236
|
+
* How robots.txt answered; absent on readings from an app made before it was read. Stored only from
|
|
237
|
+
* the server's own read: the server replaces whatever a member's computer sends with it.
|
|
238
|
+
*/
|
|
239
|
+
robotsFetch?: SeoRobotsFetch | null;
|
|
240
|
+
/** The site's files for AI agents; absent on readings from an app made before they were read. */
|
|
241
|
+
agentFiles?: SeoAgentFilesFacts | null;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* What one url set file states for the sitemap file rules: its URLs out of scope, its invalid and
|
|
245
|
+
* future dates, the tags Google ignores, the extensions' limits, and URLs an earlier file listed.
|
|
246
|
+
*
|
|
247
|
+
* @param parsed - The parsed file.
|
|
248
|
+
* @param fileUrl - Where the file answered.
|
|
249
|
+
* @param declared - Whether robots.txt named the file (or the index that named it): it may then list
|
|
250
|
+
* any URL of the site, not only its own folder's.
|
|
251
|
+
* @param onSite - Whether a host is one of the site's own.
|
|
252
|
+
* @param listed - The URLs earlier files listed; this file's are added.
|
|
253
|
+
* @param now - The read's time.
|
|
254
|
+
* @returns The facts beyond the counts.
|
|
255
|
+
*/
|
|
256
|
+
declare function sitemapFileFacts(parsed: ParsedSitemap, fileUrl: string, declared: boolean, onSite: (host: string) => boolean, listed: Set<string>, now: Date): Omit<SeoSitemapFileFacts, "url" | "urls" | "withLastmod" | "distinctLastmod" | "source" | "status">;
|
|
257
|
+
/** How robots.txt answered one read. */
|
|
258
|
+
interface SeoRobotsFetch {
|
|
259
|
+
/** The last answer's status, or null when nothing answered. */
|
|
260
|
+
status: number | null;
|
|
261
|
+
outcome: SeoRobotsFetchOutcomeValue;
|
|
262
|
+
}
|
|
263
|
+
/** One read of robots.txt: the file the crawl obeys, and how the site answered for it. */
|
|
264
|
+
interface SeoRobotsRead {
|
|
265
|
+
/** The file, "" for a 4xx, the last one read when the site did not answer, or null when none. */
|
|
266
|
+
text: string | null;
|
|
267
|
+
/** How the site answered, or null when the website is not a crawlable URL. */
|
|
268
|
+
fetch: SeoRobotsFetch | null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Reads the robots.txt a crawl obeys from the site's own origin: the file it served, empty when it
|
|
272
|
+
* answered 4xx (no file allows everything), the last one read when it did not answer, or null when
|
|
273
|
+
* there is none at all; beside it, how the site answered.
|
|
274
|
+
*
|
|
275
|
+
* @param input - The site, the User-Agent and the last robots.txt read.
|
|
276
|
+
* @param deps - The transport and the clock.
|
|
277
|
+
* @returns The robots.txt and how the site answered.
|
|
278
|
+
*/
|
|
279
|
+
declare function readSiteRobots(input: Pick<SeoSitePlanInput, "siteUrl" | "userAgent" | "fallbackRobotsTxt">, deps: Pick<SeoReadDeps, "fetch" | "now"> & Partial<Pick<SeoReadDeps, "sleep" | "hostGate">>): Promise<SeoRobotsRead>;
|
|
280
|
+
/**
|
|
281
|
+
* How robots.txt answered at a plan, with the run of plans in a row it was unreachable at.
|
|
282
|
+
*
|
|
283
|
+
* @param fetch - How it answered now.
|
|
284
|
+
* @param before - The last plan's facts, or null.
|
|
285
|
+
* @param now - Now.
|
|
286
|
+
* @returns The facts to store.
|
|
287
|
+
*/
|
|
288
|
+
declare function robotsFetchFacts(fetch: SeoRobotsFetch, before: SeoRobotsFetchFacts | null, now: Date): SeoRobotsFetchFacts;
|
|
289
|
+
/**
|
|
290
|
+
* READS WHAT A CRAWL PLAN NEEDS FROM THE SITE: robots.txt, every sitemap file and, when it is due, the
|
|
291
|
+
* soft-404 probe. When there is no robots.txt to obey at all, nothing else is fetched.
|
|
292
|
+
*
|
|
293
|
+
* @param input - The site and what the server decided about it.
|
|
294
|
+
* @param deps - The transport, the clock and the pause.
|
|
295
|
+
* @returns The reading, or null when the website is not a crawlable URL.
|
|
296
|
+
*/
|
|
297
|
+
declare function readSitePlan(input: SeoSitePlanInput, deps: SeoReadDeps): Promise<SeoSitePlanReading | null>;
|
|
298
|
+
/** What one fetch answered, as a reading carries it: the body left behind once it was read. */
|
|
299
|
+
type SeoReadResource = Omit<FetchedResource, "body" | "requestedUrl" | "truncated">;
|
|
300
|
+
/** The page as a browser rendered it, read by the same extractor as its raw HTML. */
|
|
301
|
+
interface SeoRenderedReading {
|
|
302
|
+
outcome: SeoPageRenderOutcomeValue;
|
|
303
|
+
/** Why the render failed, or null. */
|
|
304
|
+
error: string | null;
|
|
305
|
+
/** The URL of the document the browser ended on. */
|
|
306
|
+
finalUrl: string;
|
|
307
|
+
renderMs: number;
|
|
308
|
+
/** The extractor's read of the rendered DOM; null when the render failed. */
|
|
309
|
+
page: ExtractedPage | null;
|
|
310
|
+
/** Uncaught script errors and console errors, cut short. */
|
|
311
|
+
consoleErrors: string[];
|
|
312
|
+
/** Scripts, stylesheets and data requests the page could not load. */
|
|
313
|
+
failedResources: SeoPageRenderResourceFailure[];
|
|
314
|
+
/** Requests the render refused to send (a POST to another host, a private address); absent from an older app. */
|
|
315
|
+
blockedRequests?: number;
|
|
316
|
+
/** Requests a host refused for the crawl's rate limit; absent from an older app. */
|
|
317
|
+
rateLimitedRequests?: number;
|
|
318
|
+
/** What the in-page probe measured; absent from an app from before it ran, null when it could not. */
|
|
319
|
+
dom?: SeoPageDomFacts | null;
|
|
320
|
+
/** What the accessibility checks found; absent or null when they did not run. */
|
|
321
|
+
a11y?: SeoPageA11yFacts | null;
|
|
322
|
+
/** The page as a desktop browser rendered it (the desktop app's second render), when there was one. */
|
|
323
|
+
desktop?: SeoPageDesktopView | null;
|
|
324
|
+
}
|
|
325
|
+
/** One page's reading: what answered, the extractor's read of its raw HTML, and of its render. */
|
|
326
|
+
interface SeoPageReading {
|
|
327
|
+
/** The page fetched. */
|
|
328
|
+
pageKey: string;
|
|
329
|
+
resource: SeoReadResource;
|
|
330
|
+
/**
|
|
331
|
+
* The page read from the RAW answer: only for a 2xx HTML answer that did not redirect to another
|
|
332
|
+
* page, null otherwise (a redirect, an error status, a failure, a file that is not HTML).
|
|
333
|
+
*/
|
|
334
|
+
page: ExtractedPage | null;
|
|
335
|
+
/**
|
|
336
|
+
* The page as a browser rendered it, whenever the raw answer was a page; null when it was not (or
|
|
337
|
+
* the reading was made without a browser, by an older desktop app).
|
|
338
|
+
*/
|
|
339
|
+
rendered: SeoRenderedReading | null;
|
|
340
|
+
/** What the optional checks found on the page (spelling, AMP, custom patterns); absent or null when none ran. */
|
|
341
|
+
checks?: SeoPageCheckFacts | null;
|
|
342
|
+
/**
|
|
343
|
+
* A JPEG of the rendered page (base64), only when the reading asked for one (a single page
|
|
344
|
+
* analysis, or a crawl on a member's computer that keeps screenshots); never carried to the server
|
|
345
|
+
* with a crawl's readings.
|
|
346
|
+
*/
|
|
347
|
+
screenshot?: string | null;
|
|
348
|
+
/**
|
|
349
|
+
* The page's raw and rendered HTML, only when the reading asked for them (a crawl on a member's
|
|
350
|
+
* computer that keeps them for itself); never carried to the server.
|
|
351
|
+
*/
|
|
352
|
+
html?: {
|
|
353
|
+
raw: string;
|
|
354
|
+
rendered: string | null;
|
|
355
|
+
} | null;
|
|
356
|
+
}
|
|
357
|
+
/** Where a fetch answered from, as the crawl judges it. */
|
|
358
|
+
interface SeoReadVerdict {
|
|
359
|
+
/** The page the fetch landed on, when it is a crawlable URL. */
|
|
360
|
+
finalKey: string | null;
|
|
361
|
+
/** Whether the chain moved to another page. */
|
|
362
|
+
redirected: boolean;
|
|
363
|
+
/** The status the page is filed under: a redirecting page's own redirect status, else the last hop's. */
|
|
364
|
+
status: number;
|
|
365
|
+
/** Whether nothing usable answered (a transport failure, nothing at all). */
|
|
366
|
+
failed: boolean;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* How the crawl reads a fetch: failed or not, redirected or not, and the status it files.
|
|
370
|
+
*
|
|
371
|
+
* @param pageKey - The page fetched.
|
|
372
|
+
* @param resource - What answered.
|
|
373
|
+
* @returns The verdict.
|
|
374
|
+
*/
|
|
375
|
+
declare function readVerdict(pageKey: string, resource: SeoReadResource): SeoReadVerdict;
|
|
376
|
+
/**
|
|
377
|
+
* Whether a reading is about the page it names: its first hop (or, with no hop, the URL it failed on)
|
|
378
|
+
* is that page, and a fetch that answered ends on the last hop of its own chain. A reading made
|
|
379
|
+
* somewhere else carries its URLs, so a reading that names one page and describes another is refused.
|
|
380
|
+
*
|
|
381
|
+
* @param pageKey - The page the reading names.
|
|
382
|
+
* @param resource - What answered.
|
|
383
|
+
* @returns Whether the reading describes that page.
|
|
384
|
+
*/
|
|
385
|
+
declare function readingFitsPage(pageKey: string, resource: SeoReadResource): boolean;
|
|
386
|
+
/**
|
|
387
|
+
* Whether a fetch's answer is a page the extractor reads: a 2xx HTML answer that stayed on the page.
|
|
388
|
+
*
|
|
389
|
+
* @param pageKey - The page fetched.
|
|
390
|
+
* @param resource - What answered.
|
|
391
|
+
* @returns Whether its body is read as a page.
|
|
392
|
+
*/
|
|
393
|
+
declare function readsAsPage(pageKey: string, resource: SeoReadResource): boolean;
|
|
394
|
+
/**
|
|
395
|
+
* Whether a 2xx answer that stayed on the page holds an HTML document its `Content-Type` does not
|
|
396
|
+
* name as HTML (missing, `text/plain`, ...): browsers may still show it, but search engines read it
|
|
397
|
+
* by its type.
|
|
398
|
+
*
|
|
399
|
+
* @param pageKey - The page fetched.
|
|
400
|
+
* @param resource - What answered.
|
|
401
|
+
* @param body - Its body, or null.
|
|
402
|
+
* @returns Whether it does.
|
|
403
|
+
*/
|
|
404
|
+
declare function htmlUnlabelled(pageKey: string, resource: SeoReadResource, body: Uint8Array | null): boolean;
|
|
405
|
+
/**
|
|
406
|
+
* WHAT GOOGLEBOT'S CUT LEAVES OUT of an HTML file over what it reads ({@link GOOGLEBOT_FILE_BYTES}):
|
|
407
|
+
* the page read again from only that first part, against the whole read. The head is cut when the
|
|
408
|
+
* part reads a head element search engines use otherwise (the title, the description, the robots meta,
|
|
409
|
+
* the canonical, hreflang); the main text is cut when the part holds fewer than
|
|
410
|
+
* {@link CUT_MAIN_TEXT_KEPT} of its words.
|
|
411
|
+
*
|
|
412
|
+
* @param html - The page's HTML as served, decoded.
|
|
413
|
+
* @param page - The whole read.
|
|
414
|
+
* @param finalUrl - The URL it was read at.
|
|
415
|
+
* @returns What the cut leaves out, or null for a file within the limit.
|
|
416
|
+
*/
|
|
417
|
+
declare function htmlCutOf(html: string, page: ExtractedPage, finalUrl: string): {
|
|
418
|
+
head: boolean;
|
|
419
|
+
mainText: boolean;
|
|
420
|
+
} | null;
|
|
421
|
+
/**
|
|
422
|
+
* What the extractor reads from a browser's render, judged against the page it was asked for: a render
|
|
423
|
+
* that ended on another document (a script navigated away) or on a bot wall reads as failed.
|
|
424
|
+
*
|
|
425
|
+
* @param pageKey - The page.
|
|
426
|
+
* @param render - What the browser answered.
|
|
427
|
+
* @returns The rendered reading.
|
|
428
|
+
*/
|
|
429
|
+
declare function renderedReadingOf(pageKey: string, render: SeoBrowserRender): SeoRenderedReading;
|
|
430
|
+
/**
|
|
431
|
+
* Whether a subresource may be fetched by the rendering browser: anything off the site, and on the
|
|
432
|
+
* site's own hosts what its robots.txt lets the crawler fetch (Google obeys robots.txt for a page's
|
|
433
|
+
* scripts, styles and data too).
|
|
434
|
+
*
|
|
435
|
+
* @param pageUrl - The page rendered.
|
|
436
|
+
* @param robots - The site's robots.txt, or null when none is known (everything allowed).
|
|
437
|
+
* @returns The check, or null when nothing is checked.
|
|
438
|
+
*/
|
|
439
|
+
declare function renderResourceCheck(pageUrl: string, robots: CompiledRobots | null): ((url: string) => boolean) | null;
|
|
440
|
+
/** How a page reading is made beyond its transport. */
|
|
441
|
+
interface SeoReadPageOptions {
|
|
442
|
+
/** The robots.txt the crawl obeys, for the rendering browser's own requests to the site. */
|
|
443
|
+
robots?: CompiledRobots | null;
|
|
444
|
+
/** Told just before the page is rendered. */
|
|
445
|
+
onRender?: () => void;
|
|
446
|
+
/** Which accessibility rules run on the rendered page: the search-relevant subset, all, or none. */
|
|
447
|
+
accessibility?: "seo" | "all" | null;
|
|
448
|
+
/** Whether the page is also rendered as a desktop browser sees it, for the parity check. */
|
|
449
|
+
desktopRender?: boolean;
|
|
450
|
+
/** Which languages the spelling check reads (the server's light ones, or all), or null for none. */
|
|
451
|
+
spelling?: SeoSpellingScope | null;
|
|
452
|
+
/** The project's custom search and extraction patterns. */
|
|
453
|
+
customChecks?: readonly SeoCustomCheck[];
|
|
454
|
+
/**
|
|
455
|
+
* A second robots.txt the browser also obeys for the site's own files (a desktop run's test file):
|
|
456
|
+
* a file either one forbids is not loaded.
|
|
457
|
+
*/
|
|
458
|
+
extraRobots?: CompiledRobots | null;
|
|
459
|
+
/** Whether the rendered page is also captured as a screenshot (a single page analysis). */
|
|
460
|
+
screenshot?: boolean;
|
|
461
|
+
/** The device the page is rendered as: a phone (the default, as Google renders) or a desktop. */
|
|
462
|
+
device?: "mobile" | "desktop";
|
|
463
|
+
/** How long a rendered page is left to run once it settled, in ms (the members' render wait). */
|
|
464
|
+
renderWaitMs?: number;
|
|
465
|
+
/** The project's custom JavaScript snippets, run on the rendered page (a member's computer only). */
|
|
466
|
+
scripts?: readonly SeoRenderScript[];
|
|
467
|
+
/** The grammar checker the host lends (a member's computer only), or null for no grammar check. */
|
|
468
|
+
grammar?: SeoGrammarChecker | null;
|
|
469
|
+
/**
|
|
470
|
+
* Whether the reading keeps the page's raw and rendered HTML (`html`), for the member's computer to
|
|
471
|
+
* keep for itself; never carried to the server.
|
|
472
|
+
*/
|
|
473
|
+
keepHtml?: boolean;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* A desktop render's view of the page: what the parity check compares with the phone render.
|
|
477
|
+
*
|
|
478
|
+
* @param pageKey - The page.
|
|
479
|
+
* @param render - What the desktop browser answered.
|
|
480
|
+
* @returns The view.
|
|
481
|
+
*/
|
|
482
|
+
declare function desktopViewOf(pageKey: string, render: SeoBrowserRender): SeoPageDesktopView;
|
|
483
|
+
/**
|
|
484
|
+
* READS ONE PAGE: one fetch through the guarded transport (redirects followed hop by hop, the body
|
|
485
|
+
* capped; a server error asked for once more after the polite pause, {@link RETRIED_SERVER_ERRORS};
|
|
486
|
+
* a platform's mobile copy read as the page itself, {@link readMobileCopyAsPage}),
|
|
487
|
+
* the extractor's read of it when it is a page, and - when a browser is at hand - the same
|
|
488
|
+
* extractor's read of the page as the browser rendered that very HTML. With a browser at hand the page
|
|
489
|
+
* is asked for with the browser's own User-Agent (`renderedDocumentUserAgent`).
|
|
490
|
+
*
|
|
491
|
+
* @param pageKey - The page (a normalized key; fetched over https).
|
|
492
|
+
* @param userAgent - The User-Agent.
|
|
493
|
+
* @param deps - The transport, the clock, the browser and (on the server) the per-host pace its
|
|
494
|
+
* requests to the site's own host share with every crawl of it.
|
|
495
|
+
* @param options - The robots.txt the browser obeys and a hook before it renders.
|
|
496
|
+
* @returns The reading.
|
|
497
|
+
*/
|
|
498
|
+
declare function readSitePage(pageKey: string, userAgent: string, deps: Pick<SeoReadDeps, "fetch" | "now" | "render"> & Partial<Pick<SeoReadDeps, "sleep" | "hostGate" | "siteHeaders">>, options?: SeoReadPageOptions): Promise<SeoPageReading>;
|
|
499
|
+
/**
|
|
500
|
+
* A plan reading cut to {@link SEO_READING_LIMITS}: a sitemap URL too long to be a page is left out,
|
|
501
|
+
* as the server's own crawl would never fetch it.
|
|
502
|
+
*
|
|
503
|
+
* @param reading - The plan reading.
|
|
504
|
+
* @returns The bounded reading.
|
|
505
|
+
*/
|
|
506
|
+
declare function boundPlanReading(reading: SeoSitePlanReading): SeoSitePlanReading;
|
|
507
|
+
/**
|
|
508
|
+
* A reading cut to {@link SEO_READING_LIMITS}, so it can be carried to the server whole. A page
|
|
509
|
+
* within the limits (every real page) comes back unchanged.
|
|
510
|
+
*
|
|
511
|
+
* @param reading - The reading.
|
|
512
|
+
* @returns The bounded reading.
|
|
513
|
+
*/
|
|
514
|
+
declare function boundReading(reading: SeoPageReading): SeoPageReading;
|
|
515
|
+
|
|
516
|
+
export { SEO_ACCEPT_HTML, SEO_FAILURE_STATUSES, SEO_READING_LIMITS, boundPlanReading, boundReading, crawlerRobots, desktopViewOf, htmlCutOf, htmlUnlabelled, paceRequest, politeDelay, readSitePage, readSitePlan, readSiteRobots, readVerdict, readingFitsPage, readsAsPage, renderResourceCheck, renderedReadingOf, robotsFetchFacts, sitemapFileFacts };
|
|
517
|
+
export type { SeoPageReading, SeoReadDeps, SeoReadPageOptions, SeoReadResource, SeoReadVerdict, SeoRenderedReading, SeoRobotsFetch, SeoRobotsRead, SeoSitePlanInput, SeoSitePlanReading };
|
package/dist/reading.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { SEO_ACCEPT_HTML, SEO_FAILURE_STATUSES, boundPlanReading, boundReading, crawlerRobots, desktopViewOf, htmlCutOf, htmlUnlabelled, paceRequest, politeDelay, readSitePage, readSitePlan, readSiteRobots, readVerdict, readingFitsPage, readsAsPage, renderResourceCheck, renderedReadingOf, robotsFetchFacts, sitemapFileFacts } from './chunk-XTLRDZOD.js';
|
|
2
|
+
export { SEO_READING_LIMITS } from './chunk-7SGWGESF.js';
|
|
3
|
+
import './chunk-6QPF6O7D.js';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { SeoResourceKindValue, SeoResourceCheckMethodValue, SeoPageRedirectHop, SeoResourceHeaders, SeoResourceCheckErrorValue } from './facts.js';
|
|
2
|
+
import { SeoHostGate } from './host-gate.js';
|
|
3
|
+
import { SeoCrawlFetcher } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* THE RESOURCE CHECK'S NETWORK HALF: one URL a site's pages load or link out to, asked about through
|
|
7
|
+
* the crawler's guarded transport (`safeFetch` in production: every hop SSRF-checked again, redirects
|
|
8
|
+
* followed here by hand), and a list of them read politely: each host one request at a time with a pause
|
|
9
|
+
* between two requests to it, a few hosts at once. Plain JSON out and NO database, so the server's crawl job and the
|
|
10
|
+
* desktop app's main process run the very same check.
|
|
11
|
+
*
|
|
12
|
+
* A file a page loads gets a HEAD, then a ranged GET (`Range: bytes=0-0`) when the HEAD was refused or
|
|
13
|
+
* did not say how big the file is; a server that ignores the range has its body counted up to a cap and
|
|
14
|
+
* cut. An outbound link gets a HEAD alone, and a ranged GET only where the HEAD was refused or answered
|
|
15
|
+
* any failure but a 429 (some servers fail a HEAD they would answer by GET): its status is all its
|
|
16
|
+
* check needs. An image is asked for with a browser's `Accept` header (a server that sends WebP or
|
|
17
|
+
* AVIF to browsers answers as it does for them), and one of some weight gets one more ranged GET of its
|
|
18
|
+
* first bytes, where its header states its pixel size (an image much larger than the page shows it is
|
|
19
|
+
* wasted bytes) and a GIF shows whether it is animated. A text file a page
|
|
20
|
+
* loads (a script, a stylesheet, an SVG, JSON) whose answer named no compression is asked once more by
|
|
21
|
+
* a plain GET offering gzip and brotli, its body never read: many servers compress a GET but not a
|
|
22
|
+
* HEAD or a ranged answer. A compressed script or stylesheet of some weight is read once by a plain GET,
|
|
23
|
+
* up to just past what Googlebot reads of a file, for its unpacked size. A linked PDF gets two ranged
|
|
24
|
+
* GETs more, of its first and its last bytes, where its title is written. A link's spelling of a page of
|
|
25
|
+
* the site is asked once, its redirect never followed: what that one answer says is all its check needs.
|
|
26
|
+
*/
|
|
27
|
+
/** Images lighter than this are not asked for their pixel size: shown too large, they waste little. */
|
|
28
|
+
declare const SEO_IMAGE_SIZE_MIN_BYTES: number;
|
|
29
|
+
/** Text under this many bytes gains too little from compression to be judged. */
|
|
30
|
+
declare const SEO_COMPRESS_MIN_BYTES: number;
|
|
31
|
+
/**
|
|
32
|
+
* Whether a file is text worth compressing: a text content type, and for an image, an icon, a font or
|
|
33
|
+
* a media file, an SVG only (a binary file a server labels as text is not).
|
|
34
|
+
*
|
|
35
|
+
* @param kind - How a page uses it.
|
|
36
|
+
* @param contentType - The `Content-Type`, or null.
|
|
37
|
+
* @returns Whether it is.
|
|
38
|
+
*/
|
|
39
|
+
declare function seoCompressible(kind: SeoResourceKindValue, contentType: string | null): boolean;
|
|
40
|
+
/** One URL to check. */
|
|
41
|
+
interface SeoResourceCheckTarget {
|
|
42
|
+
url: string;
|
|
43
|
+
kind: SeoResourceKindValue;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* What one check found. No response header is kept beyond the content type, the size and, for a file
|
|
47
|
+
* a page loads, its caching and compression headers (the rules read no other), and a failure is only
|
|
48
|
+
* ever one of the {@link SeoResourceCheckErrorValue} codes, never a transport's message.
|
|
49
|
+
*/
|
|
50
|
+
interface SeoResourceReading {
|
|
51
|
+
/** The URL checked, as it was handed out. */
|
|
52
|
+
url: string;
|
|
53
|
+
kind: SeoResourceKindValue;
|
|
54
|
+
/** The request whose answer is kept: a HEAD, or the GET that followed it. */
|
|
55
|
+
method: SeoResourceCheckMethodValue;
|
|
56
|
+
/** The first hop's status (a redirecting URL is a redirect), 0 when nothing answered. */
|
|
57
|
+
status: number;
|
|
58
|
+
/** The last hop's status, 0 when nothing answered. */
|
|
59
|
+
finalStatus: number;
|
|
60
|
+
finalUrl: string;
|
|
61
|
+
chain: SeoPageRedirectHop[];
|
|
62
|
+
contentType: string | null;
|
|
63
|
+
/** The body's size in bytes as served, or null when the server never said. */
|
|
64
|
+
sizeBytes: number | null;
|
|
65
|
+
/**
|
|
66
|
+
* An image's pixel size, from its file header; null when it was not read (not an image, a light
|
|
67
|
+
* one, an SVG, a format not known); absent from an app from before it was read.
|
|
68
|
+
*/
|
|
69
|
+
width?: number | null;
|
|
70
|
+
height?: number | null;
|
|
71
|
+
/**
|
|
72
|
+
* The caching and compression headers of a file a page loads; null for an outbound link or when
|
|
73
|
+
* nothing answered; absent from an app from before they were kept (an older app's `headers`, read
|
|
74
|
+
* from a HEAD alone, are never taken for them).
|
|
75
|
+
*/
|
|
76
|
+
delivery?: SeoResourceHeaders | null;
|
|
77
|
+
/**
|
|
78
|
+
* A linked document's title from its metadata (a PDF's): `''` when it holds none, null when it was
|
|
79
|
+
* not read; absent from an app from before it was read.
|
|
80
|
+
*/
|
|
81
|
+
title?: string | null;
|
|
82
|
+
responseMs: number;
|
|
83
|
+
error: SeoResourceCheckErrorValue | null;
|
|
84
|
+
}
|
|
85
|
+
/** What a check reaches outside itself. */
|
|
86
|
+
interface SeoResourceCheckDeps {
|
|
87
|
+
fetch: SeoCrawlFetcher;
|
|
88
|
+
now: () => Date;
|
|
89
|
+
sleep: (ms: number) => Promise<void>;
|
|
90
|
+
/** The per-host pace every crawl of a host shares (the server's); without one a list paces itself. */
|
|
91
|
+
hostGate?: SeoHostGate;
|
|
92
|
+
}
|
|
93
|
+
/** How a list of checks is read. */
|
|
94
|
+
interface SeoReadResourcesOptions {
|
|
95
|
+
/** The pause between two requests to the same host, in ms (a shared CDN host's is shorter). */
|
|
96
|
+
delayMs: number;
|
|
97
|
+
/**
|
|
98
|
+
* The site's own host. When given, `delayMs` (which may hold the site's own `Crawl-delay`) paces
|
|
99
|
+
* that host alone; every other host gets the crawler's own pause, so a site's robots.txt never
|
|
100
|
+
* slows the shared pace of a host it does not own (an image CDN, another site).
|
|
101
|
+
*/
|
|
102
|
+
siteHost?: string;
|
|
103
|
+
/** Asked before each check; true stops the list there (a deadline, a pause, a stop). */
|
|
104
|
+
stop?: () => boolean;
|
|
105
|
+
/** Told the URL about to be checked. */
|
|
106
|
+
onCheck?: (url: string) => void;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* How many hosts a list of checks reads at once. Each host is still read one check at a time, at its
|
|
110
|
+
* own pace: a page's files spread over a few hosts (the site, its image and script CDNs, the tags it
|
|
111
|
+
* loads), and checking them one after another made every host wait for the others' answers.
|
|
112
|
+
*/
|
|
113
|
+
declare const SEO_RESOURCE_HOSTS_AT_ONCE = 4;
|
|
114
|
+
/**
|
|
115
|
+
* READS A LIST OF CHECKS POLITELY: each host one check at a time, never two requests to one host
|
|
116
|
+
* closer than `delayMs` (a check that needs a second request waits again), up to
|
|
117
|
+
* {@link SEO_RESOURCE_HOSTS_AT_ONCE} hosts at once, so a host is never slowed by another's pause or
|
|
118
|
+
* answer. A shared CDN host (`SEO_SHARED_CDN_HOSTS`), built for every site's traffic, is asked at its
|
|
119
|
+
* own shorter pause; a site's own hosts keep `delayMs`. On the server every request takes its turn in
|
|
120
|
+
* the per-host pace every crawl of that host shares.
|
|
121
|
+
*
|
|
122
|
+
* @param targets - The URLs to check.
|
|
123
|
+
* @param userAgent - The crawler's User-Agent.
|
|
124
|
+
* @param deps - The guarded transport, the clock and the pause.
|
|
125
|
+
* @param options - The pause between two requests to one host and when to stop.
|
|
126
|
+
* @returns The readings in the list's order, of the checks made before the list stopped.
|
|
127
|
+
*/
|
|
128
|
+
declare function readResources(targets: readonly SeoResourceCheckTarget[], userAgent: string, deps: SeoResourceCheckDeps, options: SeoReadResourcesOptions): Promise<SeoResourceReading[]>;
|
|
129
|
+
|
|
130
|
+
export { SEO_COMPRESS_MIN_BYTES, SEO_IMAGE_SIZE_MIN_BYTES, SEO_RESOURCE_HOSTS_AT_ONCE, readResources, seoCompressible };
|
|
131
|
+
export type { SeoReadResourcesOptions, SeoResourceCheckDeps, SeoResourceCheckTarget, SeoResourceReading };
|