@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,1753 @@
|
|
|
1
|
+
import { seoResourceCheckLimits, seoCrawlerLimits, onSiteOrBelow, bareHostOfKey, hostOfKey } from './chunk-6QPF6O7D.js';
|
|
2
|
+
|
|
3
|
+
// src/urls.ts
|
|
4
|
+
var TRACKING_PARAMS = /* @__PURE__ */ new Set([
|
|
5
|
+
"fbclid",
|
|
6
|
+
"gclid",
|
|
7
|
+
"gbraid",
|
|
8
|
+
"wbraid",
|
|
9
|
+
"dclid",
|
|
10
|
+
"msclkid",
|
|
11
|
+
"yclid",
|
|
12
|
+
"twclid",
|
|
13
|
+
"ttclid",
|
|
14
|
+
"li_fat_id",
|
|
15
|
+
"mc_cid",
|
|
16
|
+
"mc_eid",
|
|
17
|
+
"_ga",
|
|
18
|
+
"_gl",
|
|
19
|
+
"_hsenc",
|
|
20
|
+
"_hsmi",
|
|
21
|
+
"__hstc",
|
|
22
|
+
"__hssc",
|
|
23
|
+
"__hsfp",
|
|
24
|
+
"igshid",
|
|
25
|
+
"srsltid"
|
|
26
|
+
]);
|
|
27
|
+
function isTracking(name) {
|
|
28
|
+
const lower = name.toLowerCase();
|
|
29
|
+
return lower.startsWith("utm_") || TRACKING_PARAMS.has(lower);
|
|
30
|
+
}
|
|
31
|
+
function canonicalPart(part, encode) {
|
|
32
|
+
let decoded;
|
|
33
|
+
try {
|
|
34
|
+
decoded = decodeURIComponent(part);
|
|
35
|
+
} catch {
|
|
36
|
+
return part;
|
|
37
|
+
}
|
|
38
|
+
return encode(decoded.normalize("NFC"));
|
|
39
|
+
}
|
|
40
|
+
function encodePath(path) {
|
|
41
|
+
return path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
42
|
+
}
|
|
43
|
+
function normalizePageUrl(url, host) {
|
|
44
|
+
const trimmed = url.trim();
|
|
45
|
+
if (trimmed === "") return null;
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
49
|
+
parsed = new URL(trimmed);
|
|
50
|
+
} else if (host !== void 0 && host !== null && host.trim() !== "") {
|
|
51
|
+
const path2 = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
52
|
+
parsed = new URL(`https://${host.trim()}${path2}`);
|
|
53
|
+
} else {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
60
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
|
|
61
|
+
if (hostname === "") return null;
|
|
62
|
+
const port = parsed.port === "" ? "" : `:${parsed.port}`;
|
|
63
|
+
const rawPath = canonicalPart(parsed.pathname, encodePath).replace(/\/{2,}/g, "/");
|
|
64
|
+
const path = rawPath.length > 1 ? rawPath.replace(/\/+$/, "") : "/";
|
|
65
|
+
const params = [...parsed.searchParams.entries()].filter(([name]) => !isTracking(name)).map(([name, value]) => [name.normalize("NFC"), value.normalize("NFC")]).sort(
|
|
66
|
+
([leftName, leftValue], [rightName, rightValue]) => leftName === rightName ? leftValue.localeCompare(rightValue) : leftName.localeCompare(rightName)
|
|
67
|
+
);
|
|
68
|
+
const query = params.length === 0 ? "" : `?${params.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("&")}`;
|
|
69
|
+
return `${hostname}${port}${path === "/" ? "/" : path}${query}`;
|
|
70
|
+
}
|
|
71
|
+
function canonicalMismatch(google, declared) {
|
|
72
|
+
if (google === null || declared === null) return false;
|
|
73
|
+
const left = normalizePageUrl(google) ?? google;
|
|
74
|
+
const right = normalizePageUrl(declared) ?? declared;
|
|
75
|
+
return left !== right;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/robots.ts
|
|
79
|
+
var SEO_ROBOTS_MAX_BYTES = 500 * 1024;
|
|
80
|
+
function parseRobots(text) {
|
|
81
|
+
const groups = [];
|
|
82
|
+
const sitemaps = [];
|
|
83
|
+
if (text === null) return { groups, sitemaps };
|
|
84
|
+
let current = null;
|
|
85
|
+
let lastWasAgent = false;
|
|
86
|
+
for (const rawLine of text.slice(0, SEO_ROBOTS_MAX_BYTES).split(/\r?\n/)) {
|
|
87
|
+
const hash = rawLine.indexOf("#");
|
|
88
|
+
const line = (hash === -1 ? rawLine : rawLine.slice(0, hash)).trim();
|
|
89
|
+
if (line === "") continue;
|
|
90
|
+
const colon = line.indexOf(":");
|
|
91
|
+
if (colon === -1) continue;
|
|
92
|
+
const field = line.slice(0, colon).trim().toLowerCase();
|
|
93
|
+
const value = line.slice(colon + 1).trim();
|
|
94
|
+
if (field === "sitemap") {
|
|
95
|
+
if (value !== "") sitemaps.push(value);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (field === "user-agent") {
|
|
99
|
+
if (current === null || !lastWasAgent) {
|
|
100
|
+
current = { agents: [], rules: [], crawlDelaySeconds: null };
|
|
101
|
+
groups.push(current);
|
|
102
|
+
}
|
|
103
|
+
current.agents.push(value);
|
|
104
|
+
lastWasAgent = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
lastWasAgent = false;
|
|
108
|
+
if (current === null) continue;
|
|
109
|
+
if (field === "allow" || field === "disallow") {
|
|
110
|
+
if (value === "" && field === "disallow") continue;
|
|
111
|
+
current.rules.push({ allow: field === "allow", path: value });
|
|
112
|
+
} else if (field === "crawl-delay") {
|
|
113
|
+
const seconds = Number.parseFloat(value);
|
|
114
|
+
if (Number.isFinite(seconds) && seconds >= 0) current.crawlDelaySeconds = seconds;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { groups, sitemaps };
|
|
118
|
+
}
|
|
119
|
+
function productToken(value) {
|
|
120
|
+
return (/^[a-z_-]+/i.exec(value.trim())?.[0] ?? "").toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
function compileRobots(robots, agent) {
|
|
123
|
+
const token = productToken(agent);
|
|
124
|
+
const names = (name) => {
|
|
125
|
+
const named2 = productToken(name);
|
|
126
|
+
return named2 !== "" && named2 === token;
|
|
127
|
+
};
|
|
128
|
+
const named = robots.groups.filter((group) => group.agents.some(names));
|
|
129
|
+
const chosen = named.length > 0 ? named : robots.groups.filter((group) => group.agents.includes("*"));
|
|
130
|
+
const delays = chosen.map((group) => group.crawlDelaySeconds).filter((delay) => delay !== null);
|
|
131
|
+
return {
|
|
132
|
+
rules: chosen.flatMap((group) => {
|
|
133
|
+
const userAgent = group.agents.find(names) ?? "*";
|
|
134
|
+
return group.rules.map((rule2) => {
|
|
135
|
+
const anchored = rule2.path.endsWith("$");
|
|
136
|
+
return {
|
|
137
|
+
allow: rule2.allow,
|
|
138
|
+
length: rule2.path.length,
|
|
139
|
+
parts: (anchored ? rule2.path.slice(0, -1) : rule2.path).split("*"),
|
|
140
|
+
anchored,
|
|
141
|
+
line: `${rule2.allow ? "Allow" : "Disallow"}: ${rule2.path}`,
|
|
142
|
+
userAgent
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}),
|
|
146
|
+
crawlDelayMs: delays.length > 0 ? Math.round(Math.max(...delays) * 1e3) : null,
|
|
147
|
+
sitemaps: robots.sitemaps
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function ruleMatches(rule2, target) {
|
|
151
|
+
const { parts, anchored } = rule2;
|
|
152
|
+
const first = parts[0] ?? "";
|
|
153
|
+
if (parts.length === 1) return anchored ? target === first : target.startsWith(first);
|
|
154
|
+
if (!target.startsWith(first)) return false;
|
|
155
|
+
let position = first.length;
|
|
156
|
+
for (let index = 1; index < parts.length - 1; index += 1) {
|
|
157
|
+
const part = parts[index] ?? "";
|
|
158
|
+
if (part === "") continue;
|
|
159
|
+
const found = target.indexOf(part, position);
|
|
160
|
+
if (found === -1) return false;
|
|
161
|
+
position = found + part.length;
|
|
162
|
+
}
|
|
163
|
+
const last = parts[parts.length - 1] ?? "";
|
|
164
|
+
if (anchored) return target.length - last.length >= position && target.endsWith(last);
|
|
165
|
+
return last === "" || target.includes(last, position);
|
|
166
|
+
}
|
|
167
|
+
function compileRobotsPattern(pattern) {
|
|
168
|
+
const anchored = pattern.endsWith("$");
|
|
169
|
+
return { parts: (anchored ? pattern.slice(0, -1) : pattern).split("*"), anchored };
|
|
170
|
+
}
|
|
171
|
+
function robotsPatternMatches(pattern, target) {
|
|
172
|
+
return ruleMatches(pattern, target);
|
|
173
|
+
}
|
|
174
|
+
function robotsDecision(robots, url) {
|
|
175
|
+
let target;
|
|
176
|
+
try {
|
|
177
|
+
const parsed = new URL(url);
|
|
178
|
+
target = `${parsed.pathname}${parsed.search}`;
|
|
179
|
+
} catch {
|
|
180
|
+
return { allowed: false, rule: null };
|
|
181
|
+
}
|
|
182
|
+
if (target === "/robots.txt") return { allowed: true, rule: null };
|
|
183
|
+
let best = null;
|
|
184
|
+
for (const rule2 of robots.rules) {
|
|
185
|
+
if (!ruleMatches(rule2, target)) continue;
|
|
186
|
+
if (best === null || rule2.length > best.length || rule2.length === best.length && rule2.allow) {
|
|
187
|
+
best = rule2;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
allowed: best === null || best.allow,
|
|
192
|
+
rule: best === null ? null : { line: best.line, userAgent: best.userAgent }
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function robotsAllows(robots, url) {
|
|
196
|
+
return robotsDecision(robots, url).allowed;
|
|
197
|
+
}
|
|
198
|
+
function directivesNoindex(...directives) {
|
|
199
|
+
return directives.some(
|
|
200
|
+
(value) => value !== null && value !== void 0 && /\b(?:noindex|none)\b/i.test(value)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
function directivesNofollow(...directives) {
|
|
204
|
+
return directives.some(
|
|
205
|
+
(value) => value !== null && value !== void 0 && /\b(?:nofollow|none)\b/i.test(value)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
var ROBOTS_UNREACHABLE = /* @__PURE__ */ new Set([
|
|
209
|
+
"server_error",
|
|
210
|
+
"timeout",
|
|
211
|
+
"redirect_loop"
|
|
212
|
+
]);
|
|
213
|
+
function isRobotsUnreachable(outcome) {
|
|
214
|
+
return ROBOTS_UNREACHABLE.has(outcome);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/reading-limits.ts
|
|
218
|
+
var SEO_READING_LIMITS = {
|
|
219
|
+
/** Characters of a page's main text. */
|
|
220
|
+
mainTextChars: 2e6,
|
|
221
|
+
/** Links of one page. */
|
|
222
|
+
links: 1e4,
|
|
223
|
+
/** Characters of a URL, an anchor or a header value. */
|
|
224
|
+
textChars: 4e3,
|
|
225
|
+
/** Entries of a list the audit only counts or samples (mixed content, unsized images, canonicals). */
|
|
226
|
+
listItems: 500,
|
|
227
|
+
/** JSON-LD blocks, and their characters together. */
|
|
228
|
+
jsonLdBlocks: 200,
|
|
229
|
+
jsonLdChars: 1e6,
|
|
230
|
+
/** hreflang alternates. */
|
|
231
|
+
hreflang: 1e3,
|
|
232
|
+
/** Schema types. */
|
|
233
|
+
schemaTypes: 200,
|
|
234
|
+
/** Hops of a redirect chain. */
|
|
235
|
+
chainHops: 20,
|
|
236
|
+
/** Readings in one batch. */
|
|
237
|
+
batchPages: 50,
|
|
238
|
+
/** Sitemap URLs in a plan reading, the characters of each, and sitemap files. */
|
|
239
|
+
sitemapUrls: seoCrawlerLimits.maxSitemapUrls,
|
|
240
|
+
sitemapUrlChars: 2048,
|
|
241
|
+
sitemapFiles: seoCrawlerLimits.maxSitemapFiles,
|
|
242
|
+
/** Characters of a robots.txt. */
|
|
243
|
+
robotsChars: SEO_ROBOTS_MAX_BYTES,
|
|
244
|
+
/** Console lines of a render, the characters of each, and the failed resources it names. */
|
|
245
|
+
renderConsoleLines: 20,
|
|
246
|
+
renderConsoleChars: 300,
|
|
247
|
+
renderFailures: 50,
|
|
248
|
+
/** Resource check readings in one upload: one hand-out's worth. */
|
|
249
|
+
resourceReadings: seoResourceCheckLimits.checksPerStep
|
|
250
|
+
};
|
|
251
|
+
var SEO_MARKUP_LIMITS = {
|
|
252
|
+
/** Entries of a list of tags of one kind (titles, canonicals, viewports, ...). */
|
|
253
|
+
values: 10,
|
|
254
|
+
/** Characters of one value kept. */
|
|
255
|
+
valueChars: 500,
|
|
256
|
+
/** Directives of one robots group. */
|
|
257
|
+
robotsTokens: 50,
|
|
258
|
+
/** Examples of a counted kind, and the characters of each. */
|
|
259
|
+
examples: 5,
|
|
260
|
+
exampleChars: 300,
|
|
261
|
+
/** Images described one by one. */
|
|
262
|
+
images: 40,
|
|
263
|
+
/** Element ids kept for bookmarks, and the characters of each. */
|
|
264
|
+
ids: 300,
|
|
265
|
+
idChars: 100,
|
|
266
|
+
/** Links to another page's fragment. */
|
|
267
|
+
externalBookmarks: 20,
|
|
268
|
+
/** Forms described one by one. */
|
|
269
|
+
forms: 10,
|
|
270
|
+
/** `rel="next"` and `rel="prev"` targets of each. */
|
|
271
|
+
pagination: 5,
|
|
272
|
+
/** Analytics ids. */
|
|
273
|
+
analyticsIds: 10,
|
|
274
|
+
/** Subresource URLs handed to the resource checks, and the characters of each. */
|
|
275
|
+
resources: 300,
|
|
276
|
+
resourceChars: 2e3,
|
|
277
|
+
/** Microdata and RDFa items kept, and their JSON's characters together (the JSON-LD budget's size). */
|
|
278
|
+
structuredItems: 30,
|
|
279
|
+
structuredChars: 16e3,
|
|
280
|
+
/** Prices and ratings the structured data states. */
|
|
281
|
+
markedValues: 20,
|
|
282
|
+
/** Elements the microdata and RDFa reads visit, and how deep an item may nest in another. */
|
|
283
|
+
structuredElements: 1e5,
|
|
284
|
+
structuredDepth: 32,
|
|
285
|
+
/** Characters of one microdata or RDFa text value, and of every value one read takes together. */
|
|
286
|
+
structuredValueChars: 1e3,
|
|
287
|
+
structuredReadChars: 2e5,
|
|
288
|
+
/** Characters of visible text searched for the marked-up prices and ratings. */
|
|
289
|
+
shownTextChars: 1e6,
|
|
290
|
+
/** JSON-LD blocks read for prices and ratings, the characters of one block, and of all together. */
|
|
291
|
+
jsonBlocks: 200,
|
|
292
|
+
jsonBlockChars: 1e6,
|
|
293
|
+
jsonChars: 1e6,
|
|
294
|
+
/** How deep a JSON-LD block may nest, the values one page's walks visit, and the `@id`s indexed. */
|
|
295
|
+
jsonDepth: 64,
|
|
296
|
+
jsonNodes: 1e5,
|
|
297
|
+
jsonIds: 1e3
|
|
298
|
+
};
|
|
299
|
+
var SEO_CUSTOM_CHECK_LIMITS = {
|
|
300
|
+
/** Patterns per project. */
|
|
301
|
+
checks: 10,
|
|
302
|
+
/** Characters of a pattern id, a pattern, a name and an attribute. */
|
|
303
|
+
idChars: 40,
|
|
304
|
+
patternChars: 300,
|
|
305
|
+
nameChars: 80,
|
|
306
|
+
attributeChars: 60,
|
|
307
|
+
/** Characters of a page's HTML searched. */
|
|
308
|
+
htmlChars: 2e6,
|
|
309
|
+
/**
|
|
310
|
+
* The work one page's patterns may do together, shared out evenly between them: a regular
|
|
311
|
+
* expression spends its size for every character it reads, a selector one unit per step through the
|
|
312
|
+
* page's elements. A pattern that spends its share yields no result on that page.
|
|
313
|
+
*/
|
|
314
|
+
pageWork: 3e7,
|
|
315
|
+
/** The largest regular expression a pattern may compile to (RE2 program instructions). */
|
|
316
|
+
regexProgram: 500,
|
|
317
|
+
/** Matches counted per pattern and page. */
|
|
318
|
+
matches: 1e3,
|
|
319
|
+
/** Values kept per pattern and page, and the characters of each. */
|
|
320
|
+
values: 10,
|
|
321
|
+
valueChars: 300
|
|
322
|
+
};
|
|
323
|
+
var SEO_CRAWL_OPTION_LIMITS = {
|
|
324
|
+
/** Characters of a member's own User-Agent. */
|
|
325
|
+
userAgentChars: 300,
|
|
326
|
+
/** The deepest link depth a member may set. */
|
|
327
|
+
maxDepth: 100,
|
|
328
|
+
/** The slowest and the fastest pace of a crawl on a member's computer, in requests per second. */
|
|
329
|
+
minRequestsPerSecond: 0.2,
|
|
330
|
+
maxRequestsPerSecond: 5,
|
|
331
|
+
/** Snippets per project, the characters of each, and of a snippet's or a prompt's name. */
|
|
332
|
+
scripts: 5,
|
|
333
|
+
scriptChars: 4e3,
|
|
334
|
+
nameChars: 80,
|
|
335
|
+
/** The characters of a snippet's value kept per page. */
|
|
336
|
+
scriptValueChars: 300,
|
|
337
|
+
/** AI prompts per project, and the characters of each. */
|
|
338
|
+
prompts: 3,
|
|
339
|
+
promptChars: 1e3
|
|
340
|
+
};
|
|
341
|
+
var SEO_CRAWL_USER_AGENT_PRESETS = [
|
|
342
|
+
"own",
|
|
343
|
+
"googlebot_smartphone",
|
|
344
|
+
"googlebot_desktop",
|
|
345
|
+
"bingbot",
|
|
346
|
+
"custom"
|
|
347
|
+
];
|
|
348
|
+
var SEO_CRAWL_ROBOTS_GROUPS = [
|
|
349
|
+
"own",
|
|
350
|
+
"googlebot",
|
|
351
|
+
"bingbot"
|
|
352
|
+
];
|
|
353
|
+
var SEO_CUSTOM_CHECK_MODES = [
|
|
354
|
+
"contains",
|
|
355
|
+
"absent",
|
|
356
|
+
"extract"
|
|
357
|
+
];
|
|
358
|
+
var SEO_CUSTOM_CHECK_MATCHES = [
|
|
359
|
+
"text",
|
|
360
|
+
"regex",
|
|
361
|
+
"css"
|
|
362
|
+
];
|
|
363
|
+
var SEO_GRAMMAR_LIMITS = {
|
|
364
|
+
/** Characters of text read. */
|
|
365
|
+
textChars: 2e4,
|
|
366
|
+
/** Problems kept. */
|
|
367
|
+
issues: 20,
|
|
368
|
+
/** Characters of a problem's kind, text and suggestion. */
|
|
369
|
+
textKept: 120
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/bytes.ts
|
|
373
|
+
async function readStreamCapped(stream, maxBytes) {
|
|
374
|
+
if (stream === null) return { body: new Uint8Array(0), truncated: false, sizeBytes: 0 };
|
|
375
|
+
const reader = stream.getReader();
|
|
376
|
+
const chunks = [];
|
|
377
|
+
let size = 0;
|
|
378
|
+
let truncated = false;
|
|
379
|
+
for (; ; ) {
|
|
380
|
+
const { done, value } = await reader.read();
|
|
381
|
+
if (done) break;
|
|
382
|
+
const room = maxBytes - size;
|
|
383
|
+
if (value.byteLength > room) {
|
|
384
|
+
if (room > 0) chunks.push(value.subarray(0, room));
|
|
385
|
+
size += value.byteLength;
|
|
386
|
+
truncated = true;
|
|
387
|
+
await reader.cancel().catch(() => void 0);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
chunks.push(value);
|
|
391
|
+
size += value.byteLength;
|
|
392
|
+
}
|
|
393
|
+
const body = new Uint8Array(Math.min(size, maxBytes));
|
|
394
|
+
let offset = 0;
|
|
395
|
+
for (const chunk of chunks) {
|
|
396
|
+
body.set(chunk, offset);
|
|
397
|
+
offset += chunk.byteLength;
|
|
398
|
+
}
|
|
399
|
+
return { body, truncated, sizeBytes: size };
|
|
400
|
+
}
|
|
401
|
+
async function drainStreamCapped(stream, maxBytes, sink) {
|
|
402
|
+
if (stream === null) return { truncated: false, sizeBytes: 0 };
|
|
403
|
+
const reader = stream.getReader();
|
|
404
|
+
let size = 0;
|
|
405
|
+
for (; ; ) {
|
|
406
|
+
let next;
|
|
407
|
+
try {
|
|
408
|
+
next = await reader.read();
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (size === 0) throw error;
|
|
411
|
+
return { truncated: true, sizeBytes: size };
|
|
412
|
+
}
|
|
413
|
+
const { done, value } = next;
|
|
414
|
+
if (done) return { truncated: false, sizeBytes: size };
|
|
415
|
+
const room = maxBytes - size;
|
|
416
|
+
size += value.byteLength;
|
|
417
|
+
const over = value.byteLength > room;
|
|
418
|
+
const more = await sink(over ? value.subarray(0, Math.max(0, room)) : value);
|
|
419
|
+
if (over || !more) {
|
|
420
|
+
await reader.cancel().catch(() => void 0);
|
|
421
|
+
return { truncated: over, sizeBytes: size };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function bytesStream(bytes) {
|
|
426
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
427
|
+
copy.set(bytes);
|
|
428
|
+
return new Blob([copy]).stream();
|
|
429
|
+
}
|
|
430
|
+
async function gunzipCapped(bytes, maxBytes) {
|
|
431
|
+
return readStreamCapped(
|
|
432
|
+
bytesStream(bytes).pipeThrough(new DecompressionStream("gzip")),
|
|
433
|
+
maxBytes
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/a11y-rules.generated.ts
|
|
438
|
+
var SEO_A11Y_RULES = {
|
|
439
|
+
a11y_accesskeys: { axe: "accesskeys", impact: "serious", seo: false },
|
|
440
|
+
a11y_area_alt: { axe: "area-alt", impact: "critical", seo: false },
|
|
441
|
+
a11y_aria_allowed_attr: { axe: "aria-allowed-attr", impact: "critical", seo: false },
|
|
442
|
+
a11y_aria_allowed_role: { axe: "aria-allowed-role", impact: "minor", seo: false },
|
|
443
|
+
a11y_aria_braille_equivalent: { axe: "aria-braille-equivalent", impact: "serious", seo: false },
|
|
444
|
+
a11y_aria_command_name: { axe: "aria-command-name", impact: "serious", seo: true },
|
|
445
|
+
a11y_aria_conditional_attr: { axe: "aria-conditional-attr", impact: "serious", seo: false },
|
|
446
|
+
a11y_aria_deprecated_role: { axe: "aria-deprecated-role", impact: "minor", seo: false },
|
|
447
|
+
a11y_aria_dialog_name: { axe: "aria-dialog-name", impact: "serious", seo: true },
|
|
448
|
+
a11y_aria_hidden_body: { axe: "aria-hidden-body", impact: "critical", seo: false },
|
|
449
|
+
a11y_aria_hidden_focus: { axe: "aria-hidden-focus", impact: "serious", seo: false },
|
|
450
|
+
a11y_aria_input_field_name: { axe: "aria-input-field-name", impact: "serious", seo: true },
|
|
451
|
+
a11y_aria_meter_name: { axe: "aria-meter-name", impact: "serious", seo: true },
|
|
452
|
+
a11y_aria_progressbar_name: { axe: "aria-progressbar-name", impact: "serious", seo: true },
|
|
453
|
+
a11y_aria_prohibited_attr: { axe: "aria-prohibited-attr", impact: "serious", seo: false },
|
|
454
|
+
a11y_aria_required_attr: { axe: "aria-required-attr", impact: "critical", seo: false },
|
|
455
|
+
a11y_aria_required_children: { axe: "aria-required-children", impact: "critical", seo: false },
|
|
456
|
+
a11y_aria_required_parent: { axe: "aria-required-parent", impact: "critical", seo: false },
|
|
457
|
+
a11y_aria_roles: { axe: "aria-roles", impact: "critical", seo: false },
|
|
458
|
+
a11y_aria_tab_name: { axe: "aria-tab-name", impact: "serious", seo: true },
|
|
459
|
+
a11y_aria_text: { axe: "aria-text", impact: "serious", seo: false },
|
|
460
|
+
a11y_aria_toggle_field_name: { axe: "aria-toggle-field-name", impact: "serious", seo: true },
|
|
461
|
+
a11y_aria_tooltip_name: { axe: "aria-tooltip-name", impact: "serious", seo: true },
|
|
462
|
+
a11y_aria_treeitem_name: { axe: "aria-treeitem-name", impact: "serious", seo: true },
|
|
463
|
+
a11y_aria_valid_attr: { axe: "aria-valid-attr", impact: "critical", seo: false },
|
|
464
|
+
a11y_aria_valid_attr_value: { axe: "aria-valid-attr-value", impact: "critical", seo: false },
|
|
465
|
+
a11y_autocomplete_valid: { axe: "autocomplete-valid", impact: "serious", seo: false },
|
|
466
|
+
a11y_avoid_inline_spacing: { axe: "avoid-inline-spacing", impact: "serious", seo: false },
|
|
467
|
+
a11y_blink: { axe: "blink", impact: "serious", seo: false },
|
|
468
|
+
a11y_button_name: { axe: "button-name", impact: "critical", seo: true },
|
|
469
|
+
a11y_bypass: { axe: "bypass", impact: "serious", seo: false },
|
|
470
|
+
a11y_color_contrast: { axe: "color-contrast", impact: "serious", seo: false },
|
|
471
|
+
a11y_definition_list: { axe: "definition-list", impact: "serious", seo: false },
|
|
472
|
+
a11y_dlitem: { axe: "dlitem", impact: "serious", seo: false },
|
|
473
|
+
a11y_duplicate_id_aria: { axe: "duplicate-id-aria", impact: "critical", seo: true },
|
|
474
|
+
a11y_empty_heading: { axe: "empty-heading", impact: "minor", seo: true },
|
|
475
|
+
a11y_empty_table_header: { axe: "empty-table-header", impact: "minor", seo: false },
|
|
476
|
+
a11y_form_field_multiple_labels: {
|
|
477
|
+
axe: "form-field-multiple-labels",
|
|
478
|
+
impact: "moderate",
|
|
479
|
+
seo: false
|
|
480
|
+
},
|
|
481
|
+
a11y_frame_focusable_content: { axe: "frame-focusable-content", impact: "serious", seo: false },
|
|
482
|
+
a11y_frame_title: { axe: "frame-title", impact: "serious", seo: true },
|
|
483
|
+
a11y_frame_title_unique: { axe: "frame-title-unique", impact: "serious", seo: false },
|
|
484
|
+
a11y_html_xml_lang_mismatch: { axe: "html-xml-lang-mismatch", impact: "moderate", seo: false },
|
|
485
|
+
a11y_image_redundant_alt: { axe: "image-redundant-alt", impact: "minor", seo: false },
|
|
486
|
+
a11y_input_button_name: { axe: "input-button-name", impact: "critical", seo: true },
|
|
487
|
+
a11y_input_image_alt: { axe: "input-image-alt", impact: "critical", seo: true },
|
|
488
|
+
a11y_label: { axe: "label", impact: "critical", seo: true },
|
|
489
|
+
a11y_label_title_only: { axe: "label-title-only", impact: "serious", seo: false },
|
|
490
|
+
a11y_landmark_banner_is_top_level: {
|
|
491
|
+
axe: "landmark-banner-is-top-level",
|
|
492
|
+
impact: "moderate",
|
|
493
|
+
seo: false
|
|
494
|
+
},
|
|
495
|
+
a11y_landmark_contentinfo_is_top_level: {
|
|
496
|
+
axe: "landmark-contentinfo-is-top-level",
|
|
497
|
+
impact: "moderate",
|
|
498
|
+
seo: false
|
|
499
|
+
},
|
|
500
|
+
a11y_landmark_main_is_top_level: {
|
|
501
|
+
axe: "landmark-main-is-top-level",
|
|
502
|
+
impact: "moderate",
|
|
503
|
+
seo: false
|
|
504
|
+
},
|
|
505
|
+
a11y_landmark_no_duplicate_banner: {
|
|
506
|
+
axe: "landmark-no-duplicate-banner",
|
|
507
|
+
impact: "moderate",
|
|
508
|
+
seo: false
|
|
509
|
+
},
|
|
510
|
+
a11y_landmark_no_duplicate_contentinfo: {
|
|
511
|
+
axe: "landmark-no-duplicate-contentinfo",
|
|
512
|
+
impact: "moderate",
|
|
513
|
+
seo: false
|
|
514
|
+
},
|
|
515
|
+
a11y_landmark_no_duplicate_main: {
|
|
516
|
+
axe: "landmark-no-duplicate-main",
|
|
517
|
+
impact: "moderate",
|
|
518
|
+
seo: false
|
|
519
|
+
},
|
|
520
|
+
a11y_landmark_one_main: { axe: "landmark-one-main", impact: "moderate", seo: false },
|
|
521
|
+
a11y_landmark_unique: { axe: "landmark-unique", impact: "moderate", seo: false },
|
|
522
|
+
a11y_link_in_text_block: { axe: "link-in-text-block", impact: "serious", seo: false },
|
|
523
|
+
a11y_list: { axe: "list", impact: "serious", seo: false },
|
|
524
|
+
a11y_listitem: { axe: "listitem", impact: "serious", seo: false },
|
|
525
|
+
a11y_marquee: { axe: "marquee", impact: "serious", seo: false },
|
|
526
|
+
a11y_meta_viewport_large: { axe: "meta-viewport-large", impact: "minor", seo: false },
|
|
527
|
+
a11y_nested_interactive: { axe: "nested-interactive", impact: "serious", seo: false },
|
|
528
|
+
a11y_no_autoplay_audio: { axe: "no-autoplay-audio", impact: "moderate", seo: false },
|
|
529
|
+
a11y_object_alt: { axe: "object-alt", impact: "serious", seo: false },
|
|
530
|
+
a11y_presentation_role_conflict: {
|
|
531
|
+
axe: "presentation-role-conflict",
|
|
532
|
+
impact: "minor",
|
|
533
|
+
seo: false
|
|
534
|
+
},
|
|
535
|
+
a11y_region: { axe: "region", impact: "moderate", seo: false },
|
|
536
|
+
a11y_role_img_alt: { axe: "role-img-alt", impact: "serious", seo: true },
|
|
537
|
+
a11y_scope_attr_valid: { axe: "scope-attr-valid", impact: "moderate", seo: false },
|
|
538
|
+
a11y_scrollable_region_focusable: {
|
|
539
|
+
axe: "scrollable-region-focusable",
|
|
540
|
+
impact: "serious",
|
|
541
|
+
seo: false
|
|
542
|
+
},
|
|
543
|
+
a11y_select_name: { axe: "select-name", impact: "critical", seo: true },
|
|
544
|
+
a11y_server_side_image_map: { axe: "server-side-image-map", impact: "minor", seo: false },
|
|
545
|
+
a11y_skip_link: { axe: "skip-link", impact: "moderate", seo: false },
|
|
546
|
+
a11y_summary_name: { axe: "summary-name", impact: "serious", seo: true },
|
|
547
|
+
a11y_svg_img_alt: { axe: "svg-img-alt", impact: "serious", seo: true },
|
|
548
|
+
a11y_tabindex: { axe: "tabindex", impact: "serious", seo: false },
|
|
549
|
+
a11y_table_duplicate_name: { axe: "table-duplicate-name", impact: "minor", seo: false },
|
|
550
|
+
a11y_td_headers_attr: { axe: "td-headers-attr", impact: "serious", seo: false },
|
|
551
|
+
a11y_th_has_data_cells: { axe: "th-has-data-cells", impact: "serious", seo: false },
|
|
552
|
+
a11y_valid_lang: { axe: "valid-lang", impact: "serious", seo: true },
|
|
553
|
+
a11y_video_caption: { axe: "video-caption", impact: "critical", seo: false }
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/catalog.ts
|
|
557
|
+
function seoAuditByCategory(value) {
|
|
558
|
+
return {
|
|
559
|
+
indexing: value("indexing"),
|
|
560
|
+
redirects: value("redirects"),
|
|
561
|
+
links: value("links"),
|
|
562
|
+
content: value("content"),
|
|
563
|
+
metadata: value("metadata"),
|
|
564
|
+
structured_data: value("structured_data"),
|
|
565
|
+
international: value("international"),
|
|
566
|
+
performance: value("performance"),
|
|
567
|
+
mobile: value("mobile"),
|
|
568
|
+
accessibility: value("accessibility"),
|
|
569
|
+
security: value("security"),
|
|
570
|
+
javascript: value("javascript"),
|
|
571
|
+
resources: value("resources"),
|
|
572
|
+
social: value("social"),
|
|
573
|
+
google: value("google")
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
var SEO_AUDIT_SEVERITIES = ["critical", "warning", "notice"];
|
|
577
|
+
function rule(severity, category, options = {}) {
|
|
578
|
+
return {
|
|
579
|
+
severity,
|
|
580
|
+
category,
|
|
581
|
+
scope: options.site === true ? "site" : "page",
|
|
582
|
+
sure: options.sure === true,
|
|
583
|
+
insight: options.insight === true || options.fact === true,
|
|
584
|
+
crawlFact: options.fact === true
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function insight(category, options = {}) {
|
|
588
|
+
return { ...rule("notice", category, options), insight: true };
|
|
589
|
+
}
|
|
590
|
+
function accessibilityRules(rules) {
|
|
591
|
+
const entries = Object.entries(rules).map(([id, entry]) => [
|
|
592
|
+
id,
|
|
593
|
+
rule(
|
|
594
|
+
entry.impact === "critical" || entry.impact === "serious" ? "warning" : "notice",
|
|
595
|
+
"accessibility"
|
|
596
|
+
)
|
|
597
|
+
]);
|
|
598
|
+
return Object.fromEntries(entries);
|
|
599
|
+
}
|
|
600
|
+
var SEO_AUDIT_RULES = {
|
|
601
|
+
// Indexing: what answered, what search engines may crawl and are told to index, the crawl files.
|
|
602
|
+
unreachable: rule("critical", "indexing"),
|
|
603
|
+
status_client_error: rule("critical", "indexing"),
|
|
604
|
+
missing_page_with_visitors: rule("warning", "indexing"),
|
|
605
|
+
status_server_error: rule("critical", "indexing"),
|
|
606
|
+
crawl_blocked: rule("notice", "indexing"),
|
|
607
|
+
crawl_rate_limited: rule("notice", "indexing", { site: true }),
|
|
608
|
+
bad_content_type: rule("notice", "indexing"),
|
|
609
|
+
soft_404_page: rule("warning", "indexing"),
|
|
610
|
+
soft_404_catch_all: rule("warning", "indexing", { site: true }),
|
|
611
|
+
url_too_long: rule("notice", "indexing", { fact: true }),
|
|
612
|
+
robots_unreachable: rule("critical", "indexing", { site: true }),
|
|
613
|
+
robots_syntax_error: rule("warning", "indexing", { site: true }),
|
|
614
|
+
robots_blocked_url: rule("notice", "indexing", { fact: true, site: true }),
|
|
615
|
+
sitemap_not_in_robots: rule("notice", "indexing", { site: true }),
|
|
616
|
+
url_multiple_slashes: rule("notice", "indexing"),
|
|
617
|
+
url_uppercase: rule("notice", "indexing", { fact: true }),
|
|
618
|
+
url_whitespace: rule("notice", "indexing"),
|
|
619
|
+
url_non_ascii: rule("notice", "indexing", { fact: true }),
|
|
620
|
+
url_underscores: rule("notice", "indexing", { fact: true }),
|
|
621
|
+
url_repetitive_path: rule("notice", "indexing"),
|
|
622
|
+
url_default_slug: insight("indexing"),
|
|
623
|
+
url_session_or_tracking: rule("warning", "indexing"),
|
|
624
|
+
url_parameters: rule("notice", "indexing", { fact: true }),
|
|
625
|
+
url_internal_search: rule("notice", "indexing"),
|
|
626
|
+
url_ajax_fragment: rule("notice", "indexing"),
|
|
627
|
+
url_technical_duplicate: rule("warning", "indexing"),
|
|
628
|
+
html_structure_broken: rule("notice", "indexing"),
|
|
629
|
+
html_over_2mb: rule("critical", "indexing", { sure: true }),
|
|
630
|
+
html_over_2mb_minor: rule("warning", "indexing"),
|
|
631
|
+
noindex: rule("notice", "indexing", { fact: true }),
|
|
632
|
+
nofollow: rule("notice", "indexing", { fact: true }),
|
|
633
|
+
canonical_missing: rule("notice", "indexing"),
|
|
634
|
+
canonical_elsewhere: rule("notice", "indexing", { fact: true }),
|
|
635
|
+
canonical_multiple: rule("warning", "indexing"),
|
|
636
|
+
canonical_conflict: rule("warning", "indexing"),
|
|
637
|
+
canonical_target_error: rule("critical", "indexing", { sure: true }),
|
|
638
|
+
canonical_target_redirect: rule("warning", "indexing"),
|
|
639
|
+
canonical_target_noindex: rule("warning", "indexing"),
|
|
640
|
+
canonical_unlinked: rule("warning", "indexing"),
|
|
641
|
+
sitemap_error_url: rule("warning", "indexing"),
|
|
642
|
+
sitemap_redirect_url: rule("warning", "indexing"),
|
|
643
|
+
sitemap_noindex_url: rule("warning", "indexing"),
|
|
644
|
+
checkout_page_indexable: rule("notice", "indexing"),
|
|
645
|
+
sitemap_non_canonical_url: rule("warning", "indexing"),
|
|
646
|
+
sitemap_missing: rule("notice", "indexing", { site: true }),
|
|
647
|
+
sitemap_empty: rule("notice", "indexing", { site: true }),
|
|
648
|
+
sitemap_lastmod_uniform: rule("notice", "indexing", { site: true }),
|
|
649
|
+
search_engine_blocked: rule("critical", "indexing", { site: true }),
|
|
650
|
+
head_invalid_element: rule("critical", "indexing", { sure: true }),
|
|
651
|
+
directive_outside_head: rule("critical", "indexing", { sure: true }),
|
|
652
|
+
directives_mismatch: rule("warning", "indexing"),
|
|
653
|
+
directives_duplicated: rule("notice", "indexing", { fact: true }),
|
|
654
|
+
search_features_restricted: rule("notice", "indexing"),
|
|
655
|
+
unavailable_after_passed: rule("warning", "indexing"),
|
|
656
|
+
canonical_chain: rule("warning", "indexing"),
|
|
657
|
+
canonical_target_disallowed: rule("warning", "indexing"),
|
|
658
|
+
canonical_to_home: rule("warning", "indexing", { site: true }),
|
|
659
|
+
canonical_external: rule("notice", "indexing", { fact: true }),
|
|
660
|
+
canonical_protocol_mismatch: rule("warning", "indexing", { sure: true }),
|
|
661
|
+
canonical_invalid: rule("warning", "indexing"),
|
|
662
|
+
canonical_invalid_attributes: rule("warning", "indexing"),
|
|
663
|
+
canonical_noindex_conflict: rule("notice", "indexing", { fact: true }),
|
|
664
|
+
sitemap_disallowed_url: rule("warning", "indexing", { site: true }),
|
|
665
|
+
sitemap_unreachable: rule("warning", "indexing", { site: true }),
|
|
666
|
+
sitemap_invalid_xml: rule("warning", "indexing", { site: true }),
|
|
667
|
+
sitemap_too_big: rule("critical", "indexing", { site: true }),
|
|
668
|
+
sitemap_out_of_scope: rule("warning", "indexing", { site: true }),
|
|
669
|
+
sitemap_extension_invalid: rule("warning", "indexing", { site: true }),
|
|
670
|
+
sitemap_lastmod_invalid: rule("notice", "indexing", { site: true }),
|
|
671
|
+
sitemap_deprecated_tags: rule("notice", "indexing", { fact: true, site: true }),
|
|
672
|
+
sitemap_url_in_multiple: rule("notice", "indexing", { fact: true, site: true }),
|
|
673
|
+
page_not_in_sitemap: rule("notice", "indexing"),
|
|
674
|
+
paginated_canonical_to_first: rule("warning", "indexing"),
|
|
675
|
+
pagination_not_linked: rule("notice", "indexing"),
|
|
676
|
+
pagination_invalid: rule("notice", "indexing", { fact: true }),
|
|
677
|
+
infinite_scroll_no_pages: rule("notice", "indexing"),
|
|
678
|
+
amp_status_error: rule("warning", "indexing"),
|
|
679
|
+
amp_canonical_invalid: rule("warning", "indexing"),
|
|
680
|
+
amp_missing_return_link: rule("notice", "indexing"),
|
|
681
|
+
amp_invalid: rule("warning", "indexing"),
|
|
682
|
+
ai_search_crawlers_blocked: rule("warning", "indexing", { site: true }),
|
|
683
|
+
ai_bots_inconsistent: rule("notice", "indexing", { fact: true, site: true }),
|
|
684
|
+
content_signal_missing: rule("notice", "indexing", { fact: true, site: true }),
|
|
685
|
+
llms_txt_invalid: rule("notice", "indexing", { fact: true, site: true }),
|
|
686
|
+
markdown_alternate_missing: rule("notice", "indexing", { fact: true, site: true }),
|
|
687
|
+
well_known_invalid: rule("notice", "indexing", { site: true }),
|
|
688
|
+
// Redirects.
|
|
689
|
+
redirect: rule("notice", "redirects", { fact: true }),
|
|
690
|
+
redirect_chain: rule("warning", "redirects"),
|
|
691
|
+
redirect_loop: rule("critical", "redirects", { sure: true }),
|
|
692
|
+
redirect_unlinked: rule("notice", "redirects", { fact: true }),
|
|
693
|
+
redirect_temporary: rule("notice", "redirects"),
|
|
694
|
+
redirect_too_many: rule("critical", "redirects"),
|
|
695
|
+
redirect_broken: rule("critical", "redirects", { sure: true }),
|
|
696
|
+
redirect_https_to_http: rule("warning", "redirects", { sure: true }),
|
|
697
|
+
redirect_slash_or_case: rule("notice", "redirects"),
|
|
698
|
+
redirect_meta_refresh: rule("notice", "redirects"),
|
|
699
|
+
redirect_http_refresh: rule("notice", "redirects"),
|
|
700
|
+
redirect_javascript: rule("notice", "redirects"),
|
|
701
|
+
host_variants_not_redirecting: rule("warning", "redirects", { site: true, sure: true }),
|
|
702
|
+
host_variants_canonicalised: rule("notice", "redirects", { site: true }),
|
|
703
|
+
// Links.
|
|
704
|
+
base_tag_invalid: rule("warning", "links"),
|
|
705
|
+
broken_internal_links: rule("critical", "links", { sure: true }),
|
|
706
|
+
links_to_redirect: rule("notice", "links"),
|
|
707
|
+
orphan: rule("warning", "links"),
|
|
708
|
+
no_internal_links_out: rule("notice", "links"),
|
|
709
|
+
links_localhost: rule("critical", "links", { sure: true }),
|
|
710
|
+
links_localhost_in_text: rule("warning", "links"),
|
|
711
|
+
links_malformed: rule("warning", "links"),
|
|
712
|
+
links_space_in_query: rule("notice", "links", { fact: true }),
|
|
713
|
+
links_uncrawlable: rule("warning", "links"),
|
|
714
|
+
links_to_http: rule("warning", "links"),
|
|
715
|
+
links_broken_bookmark: rule("notice", "links"),
|
|
716
|
+
isolated_page: rule("warning", "links"),
|
|
717
|
+
inlinks_nofollow_only: rule("warning", "links"),
|
|
718
|
+
inlinks_follow_nofollow_mix: rule("notice", "links"),
|
|
719
|
+
outlinks_internal_nofollow: rule("notice", "links"),
|
|
720
|
+
inlinks_single: rule("notice", "links"),
|
|
721
|
+
deep_page: rule("notice", "links"),
|
|
722
|
+
anchor_missing: rule("notice", "links"),
|
|
723
|
+
anchor_non_descriptive: rule("notice", "links"),
|
|
724
|
+
outlinks_high: rule("notice", "links", { fact: true }),
|
|
725
|
+
external_broken: rule("warning", "links"),
|
|
726
|
+
document_broken: rule("warning", "links"),
|
|
727
|
+
external_redirect: rule("notice", "links", { fact: true }),
|
|
728
|
+
// Content: the words on the page and its headings.
|
|
729
|
+
h1_missing: rule("warning", "content"),
|
|
730
|
+
h1_multiple: rule("notice", "content"),
|
|
731
|
+
h1_duplicate: rule("notice", "content"),
|
|
732
|
+
h1_length: rule("notice", "content", { fact: true }),
|
|
733
|
+
h2_facts: rule("notice", "content", { fact: true }),
|
|
734
|
+
heading_skip: rule("notice", "content"),
|
|
735
|
+
thin_content: rule("notice", "content"),
|
|
736
|
+
lorem_ipsum: rule("warning", "content", { sure: true }),
|
|
737
|
+
html_empty: rule("critical", "content", { sure: true }),
|
|
738
|
+
spelling_errors: insight("content"),
|
|
739
|
+
spelling_in_title: rule("notice", "content"),
|
|
740
|
+
low_relevance: rule("notice", "content", { fact: true }),
|
|
741
|
+
custom_search: insight("content"),
|
|
742
|
+
linked_documents: rule("notice", "content", { fact: true }),
|
|
743
|
+
document_title_missing: rule("notice", "content"),
|
|
744
|
+
custom_script: insight("content"),
|
|
745
|
+
ai_prompt: insight("content"),
|
|
746
|
+
grammar_errors: insight("content"),
|
|
747
|
+
readability_difficult: rule("notice", "content", { fact: true }),
|
|
748
|
+
content_date_missing: rule("notice", "content", { fact: true }),
|
|
749
|
+
eeat_signals: insight("content"),
|
|
750
|
+
duplicate_content: rule("warning", "content"),
|
|
751
|
+
duplicate_body: rule("warning", "content"),
|
|
752
|
+
images_missing_alt: rule("notice", "content"),
|
|
753
|
+
image_alt_quality: insight("content"),
|
|
754
|
+
// Metadata: titles and meta descriptions.
|
|
755
|
+
metadata_outside_head: rule("warning", "metadata"),
|
|
756
|
+
title_missing: rule("critical", "metadata", { sure: true }),
|
|
757
|
+
title_multiple: rule("warning", "metadata"),
|
|
758
|
+
title_too_long: rule("notice", "metadata"),
|
|
759
|
+
title_too_short: rule("notice", "metadata"),
|
|
760
|
+
title_placeholder: rule("warning", "metadata", { sure: true }),
|
|
761
|
+
title_same_as_description: rule("notice", "metadata"),
|
|
762
|
+
title_serp_rewritten: rule("notice", "metadata"),
|
|
763
|
+
meta_description_missing: rule("notice", "metadata"),
|
|
764
|
+
meta_description_multiple: rule("warning", "metadata"),
|
|
765
|
+
meta_description_too_long: rule("notice", "metadata"),
|
|
766
|
+
meta_description_too_short: rule("notice", "metadata"),
|
|
767
|
+
meta_description_placeholder: rule("warning", "metadata", { sure: true }),
|
|
768
|
+
duplicate_title: rule("warning", "metadata"),
|
|
769
|
+
duplicate_meta_description: rule("notice", "metadata"),
|
|
770
|
+
metadata_templated: rule("warning", "metadata"),
|
|
771
|
+
// Structured data.
|
|
772
|
+
structured_data_missing: rule("notice", "structured_data", { fact: true }),
|
|
773
|
+
schema_invalid_json: rule("warning", "structured_data", { sure: true }),
|
|
774
|
+
schema_missing_context: rule("warning", "structured_data"),
|
|
775
|
+
schema_missing_type: rule("notice", "structured_data"),
|
|
776
|
+
schema_missing_required: rule("warning", "structured_data"),
|
|
777
|
+
schema_placeholder: rule("warning", "structured_data"),
|
|
778
|
+
schema_relative_url: rule("notice", "structured_data"),
|
|
779
|
+
schema_retired_type: rule("notice", "structured_data", { fact: true }),
|
|
780
|
+
schema_vocabulary_invalid: rule("warning", "structured_data"),
|
|
781
|
+
schema_missing_recommended: rule("notice", "structured_data", { insight: true }),
|
|
782
|
+
schema_type_expected: insight("structured_data"),
|
|
783
|
+
product_schema_missing: rule("warning", "structured_data"),
|
|
784
|
+
schema_content_mismatch: rule("warning", "structured_data"),
|
|
785
|
+
structured_data_truncated: rule("notice", "structured_data", { fact: true }),
|
|
786
|
+
rich_result_invalid_google: rule("warning", "structured_data"),
|
|
787
|
+
// International: languages and regions.
|
|
788
|
+
translation_missing: rule("warning", "international"),
|
|
789
|
+
lang_missing: rule("notice", "international"),
|
|
790
|
+
hreflang_invalid_code: rule("warning", "international"),
|
|
791
|
+
hreflang_relative_url: rule("warning", "international"),
|
|
792
|
+
hreflang_no_self: rule("warning", "international"),
|
|
793
|
+
hreflang_no_return: rule("warning", "international"),
|
|
794
|
+
hreflang_conflict: rule("warning", "international"),
|
|
795
|
+
hreflang_to_non_canonical: rule("warning", "international"),
|
|
796
|
+
hreflang_on_canonicalized: rule("warning", "international"),
|
|
797
|
+
hreflang_missing_x_default: rule("notice", "international"),
|
|
798
|
+
lang_body_mismatch: rule("warning", "international"),
|
|
799
|
+
hreflang_target_error: rule("warning", "international"),
|
|
800
|
+
hreflang_target_not_indexable: rule("warning", "international"),
|
|
801
|
+
hreflang_inconsistent_codes: rule("warning", "international"),
|
|
802
|
+
hreflang_lang_mismatch: rule("warning", "international"),
|
|
803
|
+
hreflang_multiple_methods: rule("notice", "international", { fact: true }),
|
|
804
|
+
hreflang_sitemap_invalid: rule("warning", "international", { site: true }),
|
|
805
|
+
hreflang_group_incomplete: rule("notice", "international", { fact: true }),
|
|
806
|
+
hreflang_page_unreferenced: rule("notice", "international", { fact: true }),
|
|
807
|
+
hreflang_missing: rule("notice", "international", { site: true }),
|
|
808
|
+
lang_invalid: rule("warning", "international"),
|
|
809
|
+
// Performance: response, weight, images and Core Web Vitals, then the lab tests (Lighthouse, run by
|
|
810
|
+
// PageSpeed Insights on a sample of pages).
|
|
811
|
+
response_slow: rule("warning", "performance"),
|
|
812
|
+
response_moderate: rule("notice", "performance", { fact: true }),
|
|
813
|
+
page_moderate: rule("notice", "performance"),
|
|
814
|
+
images_lazy_hero: rule("warning", "performance"),
|
|
815
|
+
images_missing_dimensions: rule("notice", "performance"),
|
|
816
|
+
compression_missing: rule("warning", "performance"),
|
|
817
|
+
cache_policy_weak: rule("notice", "performance"),
|
|
818
|
+
compression_missing_third_party: rule("notice", "performance", { fact: true }),
|
|
819
|
+
cache_policy_weak_third_party: rule("notice", "performance", { fact: true }),
|
|
820
|
+
image_incorrectly_sized: rule("notice", "performance"),
|
|
821
|
+
image_offscreen_eager: rule("notice", "performance"),
|
|
822
|
+
image_lcp_priority: insight("performance"),
|
|
823
|
+
image_too_large: rule("notice", "performance"),
|
|
824
|
+
image_legacy_format: insight("performance"),
|
|
825
|
+
image_animated_gif: insight("performance"),
|
|
826
|
+
image_srcset_missing: insight("performance"),
|
|
827
|
+
network_payload_large: rule("notice", "performance"),
|
|
828
|
+
document_large: rule("notice", "performance"),
|
|
829
|
+
dom_size_excessive: rule("notice", "performance"),
|
|
830
|
+
cwv_lcp_poor: rule("warning", "performance"),
|
|
831
|
+
cwv_inp_poor: rule("warning", "performance"),
|
|
832
|
+
cwv_cls_poor: rule("warning", "performance"),
|
|
833
|
+
cwv_regressed: rule("warning", "performance"),
|
|
834
|
+
lh_render_blocking: rule("warning", "performance"),
|
|
835
|
+
lh_document_latency: rule("warning", "performance"),
|
|
836
|
+
lh_lcp_discovery: rule("warning", "performance"),
|
|
837
|
+
lh_network_dependency_tree: rule("warning", "performance"),
|
|
838
|
+
lh_layout_shift_culprits: rule("warning", "performance"),
|
|
839
|
+
lh_image_delivery: rule("warning", "performance"),
|
|
840
|
+
lh_forced_reflow: rule("notice", "performance"),
|
|
841
|
+
lh_legacy_javascript: rule("notice", "performance"),
|
|
842
|
+
lh_duplicated_javascript: rule("notice", "performance"),
|
|
843
|
+
lh_unused_css: rule("notice", "performance"),
|
|
844
|
+
lh_unused_javascript: rule("notice", "performance"),
|
|
845
|
+
lh_unminified: rule("notice", "performance"),
|
|
846
|
+
lh_js_execution: rule("notice", "performance"),
|
|
847
|
+
lh_main_thread: rule("notice", "performance"),
|
|
848
|
+
lh_font_display: rule("notice", "performance"),
|
|
849
|
+
lh_performance_score: rule("notice", "performance", { fact: true }),
|
|
850
|
+
// Mobile.
|
|
851
|
+
mobile_desktop_parity: rule("critical", "mobile"),
|
|
852
|
+
unsupported_plugins: rule("notice", "mobile"),
|
|
853
|
+
viewport_missing: rule("critical", "mobile", { sure: true }),
|
|
854
|
+
viewport_invalid: rule("notice", "mobile"),
|
|
855
|
+
viewport_fixed_width: rule("warning", "mobile"),
|
|
856
|
+
mobile_alternate_link: rule("notice", "mobile", { fact: true }),
|
|
857
|
+
content_not_sized: rule("warning", "mobile"),
|
|
858
|
+
content_not_sized_severe: rule("critical", "mobile"),
|
|
859
|
+
font_too_small: rule("notice", "mobile"),
|
|
860
|
+
tap_targets_small: rule("notice", "mobile"),
|
|
861
|
+
intrusive_interstitial: rule("warning", "mobile"),
|
|
862
|
+
// Accessibility: axe-core's rules, generated.
|
|
863
|
+
...accessibilityRules(SEO_A11Y_RULES),
|
|
864
|
+
// Security.
|
|
865
|
+
mixed_content_active: rule("critical", "security", { sure: true }),
|
|
866
|
+
mixed_content_passive: rule("notice", "security"),
|
|
867
|
+
security_headers_missing: rule("notice", "security", { site: true }),
|
|
868
|
+
form_insecure: rule("warning", "security", { sure: true }),
|
|
869
|
+
internal_http_page: rule("warning", "security", { sure: true }),
|
|
870
|
+
protocol_relative_resources: rule("notice", "security", { fact: true }),
|
|
871
|
+
tls_certificate: rule("warning", "security", { site: true }),
|
|
872
|
+
// JavaScript: the page as a browser rendered it, against its raw HTML.
|
|
873
|
+
render_failed: rule("notice", "javascript", { fact: true }),
|
|
874
|
+
render_timeout: rule("notice", "javascript", { fact: true }),
|
|
875
|
+
needs_javascript: rule("notice", "javascript"),
|
|
876
|
+
js_links_only: rule("notice", "javascript"),
|
|
877
|
+
js_title_changed: rule("notice", "javascript"),
|
|
878
|
+
js_h1_changed: rule("notice", "javascript"),
|
|
879
|
+
js_meta_description_changed: rule("notice", "javascript"),
|
|
880
|
+
js_canonical_changed: rule("warning", "javascript"),
|
|
881
|
+
js_canonical_raw_elsewhere: rule("critical", "javascript"),
|
|
882
|
+
js_noindex_removed: rule("warning", "javascript"),
|
|
883
|
+
js_noindex_added: rule("warning", "javascript"),
|
|
884
|
+
js_nofollow_removed: rule("notice", "javascript"),
|
|
885
|
+
js_structured_data_only: rule("notice", "javascript"),
|
|
886
|
+
js_hreflang_changed: rule("notice", "javascript"),
|
|
887
|
+
js_errors: rule("notice", "javascript"),
|
|
888
|
+
js_back_button_hijack: rule("critical", "javascript"),
|
|
889
|
+
// Resources: the files the pages load.
|
|
890
|
+
resources_failed: rule("warning", "resources"),
|
|
891
|
+
resources_blocked: rule("warning", "resources"),
|
|
892
|
+
resources_blocked_minor: rule("notice", "resources", { fact: true }),
|
|
893
|
+
resource_over_2mb: rule("warning", "resources"),
|
|
894
|
+
image_broken: rule("warning", "resources", { sure: true }),
|
|
895
|
+
resource_redirect: rule("notice", "resources"),
|
|
896
|
+
// Social.
|
|
897
|
+
open_graph_missing: rule("notice", "social"),
|
|
898
|
+
social_tags_incomplete: rule("notice", "social"),
|
|
899
|
+
og_url_not_canonical: rule("notice", "social"),
|
|
900
|
+
og_image_broken: rule("notice", "social"),
|
|
901
|
+
og_image_too_large: rule("notice", "social"),
|
|
902
|
+
// Google data: what Search Console, URL Inspection and Analytics say about the pages.
|
|
903
|
+
error_page_with_traffic: rule("critical", "google", { sure: true }),
|
|
904
|
+
redirect_with_traffic: rule("notice", "google"),
|
|
905
|
+
disallowed_with_traffic: rule("warning", "google"),
|
|
906
|
+
noindex_with_traffic: rule("warning", "google"),
|
|
907
|
+
canonicalised_with_traffic: rule("notice", "google"),
|
|
908
|
+
indexable_not_indexed: rule("warning", "google", { fact: true }),
|
|
909
|
+
google_canonical_differs: rule("warning", "google"),
|
|
910
|
+
indexed_with_issues: rule("notice", "google"),
|
|
911
|
+
no_search_data: rule("notice", "google", { fact: true }),
|
|
912
|
+
analytics_tags: insight("google"),
|
|
913
|
+
orphan_with_traffic: rule("warning", "google")
|
|
914
|
+
};
|
|
915
|
+
var SEO_AUDIT_RULE_IDS = Object.keys(SEO_AUDIT_RULES);
|
|
916
|
+
function isSeoAuditRuleId(value) {
|
|
917
|
+
return Object.hasOwn(SEO_AUDIT_RULES, value);
|
|
918
|
+
}
|
|
919
|
+
var SEO_AUDIT_TRAFFIC_JOINS = {
|
|
920
|
+
status_client_error: "error_page_with_traffic",
|
|
921
|
+
status_server_error: "error_page_with_traffic",
|
|
922
|
+
noindex: "noindex_with_traffic",
|
|
923
|
+
canonical_elsewhere: "canonicalised_with_traffic",
|
|
924
|
+
orphan: "orphan_with_traffic",
|
|
925
|
+
redirect: "redirect_with_traffic"
|
|
926
|
+
};
|
|
927
|
+
var SCHEMA_ERRORS = [
|
|
928
|
+
"schema_invalid_json",
|
|
929
|
+
"schema_missing_context",
|
|
930
|
+
"schema_missing_type",
|
|
931
|
+
"schema_missing_required",
|
|
932
|
+
"schema_placeholder",
|
|
933
|
+
"schema_relative_url",
|
|
934
|
+
"schema_vocabulary_invalid",
|
|
935
|
+
"schema_content_mismatch"
|
|
936
|
+
];
|
|
937
|
+
var SUPERSEDING = [
|
|
938
|
+
["title_missing", ["html_empty"]],
|
|
939
|
+
["viewport_missing", ["html_empty"]],
|
|
940
|
+
["meta_description_missing", ["html_empty"]],
|
|
941
|
+
["lang_missing", ["html_empty"]],
|
|
942
|
+
["open_graph_missing", ["html_empty"]],
|
|
943
|
+
["no_internal_links_out", ["html_empty"]],
|
|
944
|
+
["thin_content", ["html_empty"]],
|
|
945
|
+
["h1_missing", ["html_empty"]],
|
|
946
|
+
["canonical_elsewhere", ["canonical_target_error", "hreflang_on_canonicalized"]],
|
|
947
|
+
["canonical_external", ["canonical_elsewhere"]],
|
|
948
|
+
["sitemap_non_canonical_url", ["canonical_target_error"]],
|
|
949
|
+
[
|
|
950
|
+
"redirect",
|
|
951
|
+
[
|
|
952
|
+
"redirect_chain",
|
|
953
|
+
"redirect_loop",
|
|
954
|
+
"redirect_too_many",
|
|
955
|
+
"redirect_broken",
|
|
956
|
+
"redirect_temporary",
|
|
957
|
+
"redirect_https_to_http",
|
|
958
|
+
"redirect_slash_or_case"
|
|
959
|
+
]
|
|
960
|
+
],
|
|
961
|
+
["redirect_javascript", ["redirect_broken"]],
|
|
962
|
+
["rich_result_invalid_google", SCHEMA_ERRORS],
|
|
963
|
+
["lang_body_mismatch", ["translation_missing"]],
|
|
964
|
+
["duplicate_title", ["title_placeholder"]],
|
|
965
|
+
["duplicate_meta_description", ["meta_description_placeholder"]],
|
|
966
|
+
["structured_data_missing", ["product_schema_missing", "html_empty"]],
|
|
967
|
+
["schema_type_expected", ["product_schema_missing"]]
|
|
968
|
+
];
|
|
969
|
+
function supersededBy() {
|
|
970
|
+
const direct = new Map(
|
|
971
|
+
SUPERSEDING.map(([ruleId, by]) => [ruleId, [...by]])
|
|
972
|
+
);
|
|
973
|
+
for (const [ruleId, join] of Object.entries(SEO_AUDIT_TRAFFIC_JOINS)) {
|
|
974
|
+
if (!isSeoAuditRuleId(ruleId) || join === void 0) continue;
|
|
975
|
+
direct.set(ruleId, [...direct.get(ruleId) ?? [], join]);
|
|
976
|
+
}
|
|
977
|
+
const closed = /* @__PURE__ */ new Map();
|
|
978
|
+
for (const ruleId of direct.keys()) {
|
|
979
|
+
const all = /* @__PURE__ */ new Set();
|
|
980
|
+
const queue = [...direct.get(ruleId) ?? []];
|
|
981
|
+
for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
|
|
982
|
+
if (next === ruleId || all.has(next)) continue;
|
|
983
|
+
all.add(next);
|
|
984
|
+
queue.push(...direct.get(next) ?? []);
|
|
985
|
+
}
|
|
986
|
+
closed.set(ruleId, [...all]);
|
|
987
|
+
}
|
|
988
|
+
return closed;
|
|
989
|
+
}
|
|
990
|
+
var SEO_AUDIT_SUPERSEDED_BY = supersededBy();
|
|
991
|
+
function seoAuditRule(ruleId) {
|
|
992
|
+
return isSeoAuditRuleId(ruleId) ? SEO_AUDIT_RULES[ruleId] : null;
|
|
993
|
+
}
|
|
994
|
+
SEO_AUDIT_RULE_IDS.filter(
|
|
995
|
+
(id) => SEO_AUDIT_RULES[id].sure
|
|
996
|
+
);
|
|
997
|
+
SEO_AUDIT_RULE_IDS.filter(
|
|
998
|
+
(id) => SEO_AUDIT_RULES[id].insight
|
|
999
|
+
);
|
|
1000
|
+
SEO_AUDIT_RULE_IDS.filter(
|
|
1001
|
+
(id) => SEO_AUDIT_RULES[id].crawlFact
|
|
1002
|
+
);
|
|
1003
|
+
function isSeoAuditCrawlFact(ruleId) {
|
|
1004
|
+
return isSeoAuditRuleId(ruleId) && SEO_AUDIT_RULES[ruleId].crawlFact;
|
|
1005
|
+
}
|
|
1006
|
+
var HINT_TOPICS = {
|
|
1007
|
+
canonical: [
|
|
1008
|
+
"canonical_missing",
|
|
1009
|
+
"canonical_multiple",
|
|
1010
|
+
"canonical_to_home",
|
|
1011
|
+
"canonical_target_error",
|
|
1012
|
+
"canonical_target_redirect",
|
|
1013
|
+
"canonical_target_noindex",
|
|
1014
|
+
"canonical_protocol_mismatch",
|
|
1015
|
+
"canonical_chain"
|
|
1016
|
+
],
|
|
1017
|
+
sitemap: [
|
|
1018
|
+
"sitemap_missing",
|
|
1019
|
+
"sitemap_empty",
|
|
1020
|
+
"sitemap_non_canonical_url",
|
|
1021
|
+
"sitemap_noindex_url",
|
|
1022
|
+
"sitemap_redirect_url",
|
|
1023
|
+
"sitemap_error_url",
|
|
1024
|
+
"page_not_in_sitemap"
|
|
1025
|
+
],
|
|
1026
|
+
titles: [
|
|
1027
|
+
"title_missing",
|
|
1028
|
+
"title_multiple",
|
|
1029
|
+
"title_too_long",
|
|
1030
|
+
"title_too_short",
|
|
1031
|
+
"duplicate_title",
|
|
1032
|
+
"meta_description_missing",
|
|
1033
|
+
"meta_description_multiple",
|
|
1034
|
+
"meta_description_too_long",
|
|
1035
|
+
"meta_description_too_short",
|
|
1036
|
+
"duplicate_meta_description",
|
|
1037
|
+
"title_placeholder",
|
|
1038
|
+
"meta_description_placeholder",
|
|
1039
|
+
"metadata_templated"
|
|
1040
|
+
],
|
|
1041
|
+
redirects: ["redirect_chain", "redirect_temporary", "redirect_loop", "redirect_too_many"],
|
|
1042
|
+
noindex: ["noindex", "noindex_with_traffic"],
|
|
1043
|
+
structured_data: [
|
|
1044
|
+
"structured_data_missing",
|
|
1045
|
+
"product_schema_missing",
|
|
1046
|
+
"schema_invalid_json",
|
|
1047
|
+
"schema_missing_context",
|
|
1048
|
+
"schema_missing_type",
|
|
1049
|
+
"schema_missing_required",
|
|
1050
|
+
"schema_placeholder",
|
|
1051
|
+
"schema_vocabulary_invalid",
|
|
1052
|
+
"schema_content_mismatch",
|
|
1053
|
+
"schema_relative_url",
|
|
1054
|
+
"rich_result_invalid_google"
|
|
1055
|
+
],
|
|
1056
|
+
images: ["images_missing_alt"],
|
|
1057
|
+
rendering: ["needs_javascript"],
|
|
1058
|
+
pagination: ["paginated_canonical_to_first"]
|
|
1059
|
+
};
|
|
1060
|
+
new Map(
|
|
1061
|
+
Object.entries(HINT_TOPICS).flatMap(
|
|
1062
|
+
([topic, ids]) => ids.map((id) => [id, topic])
|
|
1063
|
+
)
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
// src/thresholds.ts
|
|
1067
|
+
var SEO_AUDIT_THRESHOLD_SPECS = {
|
|
1068
|
+
/** Title width on Google's results page past which it is cut. */
|
|
1069
|
+
titleMaxPx: { value: 580, min: 300, max: 1200, unit: "px", rules: ["title_too_long"] },
|
|
1070
|
+
/** Title width under which it is short. */
|
|
1071
|
+
titleMinPx: { value: 200, min: 0, max: 600, unit: "px", rules: ["title_too_short"] },
|
|
1072
|
+
/** Description width on Google's results page past which it is cut. */
|
|
1073
|
+
descriptionMaxPx: {
|
|
1074
|
+
value: 990,
|
|
1075
|
+
min: 400,
|
|
1076
|
+
max: 2e3,
|
|
1077
|
+
unit: "px",
|
|
1078
|
+
rules: ["meta_description_too_long"]
|
|
1079
|
+
},
|
|
1080
|
+
/** Description width under which it is short. */
|
|
1081
|
+
descriptionMinPx: {
|
|
1082
|
+
value: 400,
|
|
1083
|
+
min: 0,
|
|
1084
|
+
max: 1200,
|
|
1085
|
+
unit: "px",
|
|
1086
|
+
rules: ["meta_description_too_short"]
|
|
1087
|
+
},
|
|
1088
|
+
/** Main-text words under which a page has little text. */
|
|
1089
|
+
thinWords: { value: 300, min: 0, max: 5e3, unit: "words", rules: ["thin_content"] },
|
|
1090
|
+
/** How much two main texts overlap, in percent, from which they are near-duplicates. */
|
|
1091
|
+
nearDuplicatePercent: {
|
|
1092
|
+
value: 90,
|
|
1093
|
+
min: 80,
|
|
1094
|
+
max: 99,
|
|
1095
|
+
unit: "percent",
|
|
1096
|
+
rules: ["duplicate_content"]
|
|
1097
|
+
},
|
|
1098
|
+
/** Server answer time past which a page is very slow (measured from our crawler). */
|
|
1099
|
+
slowMs: { value: 3e3, min: 500, max: 3e4, unit: "ms", rules: ["response_slow"] },
|
|
1100
|
+
/** Server answer time past which a page is slow. */
|
|
1101
|
+
moderateMs: { value: 1500, min: 200, max: 3e4, unit: "ms", rules: ["response_moderate"] },
|
|
1102
|
+
/** HTML size past which a page is heavy (Googlebot reads 2 MB of a file). */
|
|
1103
|
+
pageKb: { value: 1024, min: 100, max: 2048, unit: "kb", rules: ["page_moderate"] },
|
|
1104
|
+
/** Redirect hops from which a chain is filed. */
|
|
1105
|
+
redirectHops: { value: 2, min: 2, max: 10, unit: "hops", rules: ["redirect_chain"] },
|
|
1106
|
+
/** URL length past which a URL is very long. */
|
|
1107
|
+
urlMaxChars: { value: 200, min: 50, max: 2e3, unit: "chars", rules: ["url_too_long"] },
|
|
1108
|
+
/** Clicks from the home page past which a page is deep. */
|
|
1109
|
+
deepClicks: { value: 4, min: 2, max: 20, unit: "clicks", rules: ["deep_page"] },
|
|
1110
|
+
/** The weight an image may have. */
|
|
1111
|
+
imageKb: { value: 100, min: 10, max: 5e3, unit: "kb", rules: ["image_too_large"] }
|
|
1112
|
+
};
|
|
1113
|
+
var SEO_AUDIT_THRESHOLD_KEYS = Object.keys(
|
|
1114
|
+
SEO_AUDIT_THRESHOLD_SPECS
|
|
1115
|
+
);
|
|
1116
|
+
var SEO_AUDIT_THRESHOLD_DEFAULTS = Object.fromEntries(
|
|
1117
|
+
SEO_AUDIT_THRESHOLD_KEYS.map((key) => [key, SEO_AUDIT_THRESHOLD_SPECS[key].value])
|
|
1118
|
+
);
|
|
1119
|
+
var ORDERED_PAIRS = [
|
|
1120
|
+
["titleMinPx", "titleMaxPx"],
|
|
1121
|
+
["descriptionMinPx", "descriptionMaxPx"],
|
|
1122
|
+
["moderateMs", "slowMs"]
|
|
1123
|
+
];
|
|
1124
|
+
function isSeoAuditThresholdKey(key) {
|
|
1125
|
+
return Object.hasOwn(SEO_AUDIT_THRESHOLD_SPECS, key);
|
|
1126
|
+
}
|
|
1127
|
+
function customThresholdsOf(value) {
|
|
1128
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
1129
|
+
const kept = {};
|
|
1130
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
1131
|
+
if (!isSeoAuditThresholdKey(key) || typeof raw !== "number" || !Number.isFinite(raw)) continue;
|
|
1132
|
+
const spec = SEO_AUDIT_THRESHOLD_SPECS[key];
|
|
1133
|
+
if (raw < spec.min || raw > spec.max || raw === spec.value) continue;
|
|
1134
|
+
kept[key] = raw;
|
|
1135
|
+
}
|
|
1136
|
+
for (const [low, high] of ORDERED_PAIRS) {
|
|
1137
|
+
const values = auditThresholdsOf(kept);
|
|
1138
|
+
if (values[low] >= values[high]) {
|
|
1139
|
+
delete kept[low];
|
|
1140
|
+
delete kept[high];
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return kept;
|
|
1144
|
+
}
|
|
1145
|
+
function thresholdOrderProblem(custom) {
|
|
1146
|
+
const values = auditThresholdsOf(custom);
|
|
1147
|
+
return ORDERED_PAIRS.find(([low, high]) => values[low] >= values[high]) ?? null;
|
|
1148
|
+
}
|
|
1149
|
+
function auditThresholdsOf(custom) {
|
|
1150
|
+
return { ...SEO_AUDIT_THRESHOLD_DEFAULTS, ...custom };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/rules/page-state.ts
|
|
1154
|
+
var SEO_BOT_AGENT_REFUSED = "status_403_bot";
|
|
1155
|
+
var CRAWL_BLOCK_ERRORS = /* @__PURE__ */ new Set([
|
|
1156
|
+
"bot_wall",
|
|
1157
|
+
"blocked",
|
|
1158
|
+
"status_403",
|
|
1159
|
+
SEO_BOT_AGENT_REFUSED,
|
|
1160
|
+
"status_429"
|
|
1161
|
+
]);
|
|
1162
|
+
var AUDIT_EXAMPLES = 5;
|
|
1163
|
+
function thresholdsOf(context) {
|
|
1164
|
+
return auditThresholdsOf(context.site.thresholds);
|
|
1165
|
+
}
|
|
1166
|
+
var PAGE_REDIRECT_RULES = /* @__PURE__ */ new Set([
|
|
1167
|
+
"redirect_meta_refresh",
|
|
1168
|
+
"redirect_http_refresh",
|
|
1169
|
+
"redirect_javascript"
|
|
1170
|
+
]);
|
|
1171
|
+
function finding(pageKey, ruleId, params = {}) {
|
|
1172
|
+
return { pageKey, ruleId, severity: SEO_AUDIT_RULES[ruleId].severity, params };
|
|
1173
|
+
}
|
|
1174
|
+
function crawlBlocked(page) {
|
|
1175
|
+
return page.lastError !== null && CRAWL_BLOCK_ERRORS.has(page.lastError);
|
|
1176
|
+
}
|
|
1177
|
+
function failureUnconfirmed(page) {
|
|
1178
|
+
return page.lastError !== null && !crawlBlocked(page) && (page.failCount ?? 2) < 2;
|
|
1179
|
+
}
|
|
1180
|
+
function htmlSnapshot(page) {
|
|
1181
|
+
const snapshot = page.snapshot;
|
|
1182
|
+
if (snapshot === null || page.lastError !== null) return null;
|
|
1183
|
+
if (page.status !== null && (page.status < 200 || page.status >= 300)) return null;
|
|
1184
|
+
if (snapshot.status < 200 || snapshot.status >= 300) return null;
|
|
1185
|
+
const type = (snapshot.contentType ?? "").toLowerCase();
|
|
1186
|
+
return type === "" || type.includes("html") ? snapshot : null;
|
|
1187
|
+
}
|
|
1188
|
+
function noindexOf(snapshot) {
|
|
1189
|
+
return directivesNoindex(snapshot.metaRobots, snapshot.xRobotsTag);
|
|
1190
|
+
}
|
|
1191
|
+
function nofollowOf(snapshot) {
|
|
1192
|
+
return directivesNofollow(snapshot.metaRobots, snapshot.xRobotsTag);
|
|
1193
|
+
}
|
|
1194
|
+
function canonicalKey(page, context) {
|
|
1195
|
+
const raw = page.facts?.canonicals[0] ?? page.snapshot?.canonical ?? page.facts?.headers.linkCanonical ?? "";
|
|
1196
|
+
if (raw === "") return null;
|
|
1197
|
+
const key = normalizePageUrl(raw);
|
|
1198
|
+
return key === null ? null : context.resolve(key);
|
|
1199
|
+
}
|
|
1200
|
+
function canonicalElsewhere(page, context) {
|
|
1201
|
+
const target = canonicalKey(page, context);
|
|
1202
|
+
return target !== null && target !== context.resolve(page.pageKey);
|
|
1203
|
+
}
|
|
1204
|
+
var TEMPORARY_REDIRECTS = /* @__PURE__ */ new Set([302, 303, 307]);
|
|
1205
|
+
var confirmedCopyQueries = /* @__PURE__ */ new WeakMap();
|
|
1206
|
+
function temporaryRedirectTarget(page) {
|
|
1207
|
+
const status = page.status ?? page.snapshot?.status ?? null;
|
|
1208
|
+
if (status === null || !TEMPORARY_REDIRECTS.has(status)) return null;
|
|
1209
|
+
const finalUrl = page.snapshot?.finalUrl ?? "";
|
|
1210
|
+
const targetKey = finalUrl === "" ? null : normalizePageUrl(finalUrl);
|
|
1211
|
+
return targetKey === null || targetKey === page.pageKey ? null : targetKey;
|
|
1212
|
+
}
|
|
1213
|
+
function namesBack(pageKey, targetKey, context) {
|
|
1214
|
+
const target = context.byKey.get(targetKey);
|
|
1215
|
+
const raw = target?.facts?.canonicals[0] ?? target?.snapshot?.canonical ?? target?.facts?.headers.linkCanonical ?? "";
|
|
1216
|
+
return raw !== "" && normalizePageUrl(raw) === pageKey;
|
|
1217
|
+
}
|
|
1218
|
+
function appendedQuery(pageKey, targetKey) {
|
|
1219
|
+
if (pageKey.includes("?") || !targetKey.startsWith(`${pageKey}?`)) return null;
|
|
1220
|
+
return targetKey.slice(pageKey.length);
|
|
1221
|
+
}
|
|
1222
|
+
function copyQueries(context) {
|
|
1223
|
+
const known = confirmedCopyQueries.get(context);
|
|
1224
|
+
if (known !== void 0) return known;
|
|
1225
|
+
const found = /* @__PURE__ */ new Set();
|
|
1226
|
+
for (const page of context.pages) {
|
|
1227
|
+
const targetKey = temporaryRedirectTarget(page);
|
|
1228
|
+
if (targetKey === null || !namesBack(page.pageKey, targetKey, context)) continue;
|
|
1229
|
+
const query = appendedQuery(page.pageKey, targetKey);
|
|
1230
|
+
if (query !== null) found.add(query);
|
|
1231
|
+
}
|
|
1232
|
+
confirmedCopyQueries.set(context, found);
|
|
1233
|
+
return found;
|
|
1234
|
+
}
|
|
1235
|
+
function copyRedirect(page, context) {
|
|
1236
|
+
const targetKey = temporaryRedirectTarget(page);
|
|
1237
|
+
if (targetKey === null) return false;
|
|
1238
|
+
if ((context.byKey.get(targetKey)?.snapshot ?? null) !== null) {
|
|
1239
|
+
return namesBack(page.pageKey, targetKey, context);
|
|
1240
|
+
}
|
|
1241
|
+
const query = appendedQuery(page.pageKey, targetKey);
|
|
1242
|
+
return query !== null && copyQueries(context).has(query);
|
|
1243
|
+
}
|
|
1244
|
+
function redirectCopy(page, context) {
|
|
1245
|
+
const raw = page.facts?.canonicals[0] ?? page.snapshot?.canonical ?? page.facts?.headers.linkCanonical ?? "";
|
|
1246
|
+
const sourceKey = raw === "" ? null : normalizePageUrl(raw);
|
|
1247
|
+
if (sourceKey === null || sourceKey === page.pageKey) return false;
|
|
1248
|
+
const source = context.byKey.get(sourceKey);
|
|
1249
|
+
return source !== void 0 && temporaryRedirectTarget(source) === page.pageKey && copyRedirect(source, context);
|
|
1250
|
+
}
|
|
1251
|
+
function indexableSnapshot(page, context) {
|
|
1252
|
+
const snapshot = htmlSnapshot(page);
|
|
1253
|
+
if (snapshot === null || snapshot.mode === "rendered") return null;
|
|
1254
|
+
if (noindexOf(snapshot) || canonicalElsewhere(page, context)) return null;
|
|
1255
|
+
return snapshot;
|
|
1256
|
+
}
|
|
1257
|
+
function isHome(page, context) {
|
|
1258
|
+
const home = context.site.homeKey;
|
|
1259
|
+
if (home === null) return page.pageKey.indexOf("/") === page.pageKey.length - 1;
|
|
1260
|
+
return context.resolve(page.pageKey) === context.resolve(home);
|
|
1261
|
+
}
|
|
1262
|
+
var CHECKOUT_SEGMENT = /^(?:cart|basket|checkouts?|check-out|paypal-checkout|order-confirmation|order-received|order-complete|panier|commande|warenkorb|carrito|winkelwagen|afrekenen|carrello|carrinho|koszyk|varukorg)$/i;
|
|
1263
|
+
function checkoutPage(pageKey) {
|
|
1264
|
+
const at = pageKey.indexOf("/");
|
|
1265
|
+
const path = (at < 0 ? "" : pageKey.slice(at)).split(/[?#]/)[0] ?? "";
|
|
1266
|
+
return path.split("/").some((segment) => CHECKOUT_SEGMENT.test(segment.replace(/\.(?:html?|php|aspx)$/i, "")));
|
|
1267
|
+
}
|
|
1268
|
+
function h1MissingOf(page, snapshot) {
|
|
1269
|
+
const count = page.facts?.page?.h1Count ?? snapshot.h1Count ?? 0;
|
|
1270
|
+
const empty = Math.min(count, page.facts?.page?.h1Empty ?? 0);
|
|
1271
|
+
if ((snapshot.h1 ?? "").trim() !== "") return { missing: false, empty: 0 };
|
|
1272
|
+
return { missing: count === 0 || empty >= count, empty };
|
|
1273
|
+
}
|
|
1274
|
+
function brokenStatus(status) {
|
|
1275
|
+
return status !== null && (status === 0 || status >= 400);
|
|
1276
|
+
}
|
|
1277
|
+
var PLATFORM_PATHS = /^\/(?:cdn-cgi\/(?!image\/)|_hcms\/|hs\/)/i;
|
|
1278
|
+
function platformManaged(url) {
|
|
1279
|
+
try {
|
|
1280
|
+
return PLATFORM_PATHS.test(new URL(url).pathname);
|
|
1281
|
+
} catch {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function onOwnHost(url, siteHost) {
|
|
1286
|
+
return onSiteOrBelow(url, siteHost) && !platformManaged(url);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/rules/rendering.ts
|
|
1290
|
+
var FETCH_FAILURE = /^(?:Uncaught (?:\(in promise\) )?)?TypeError: (?:Failed to fetch|NetworkError when attempting to fetch resource\.?|Load failed)\s*$/i;
|
|
1291
|
+
var CART_OR_PIXEL = /(?:^|[/_.-])(?:carts?|checkouts?|basket|bag|wpm|pixels?|analytics|tracking|gtag|gtm)(?=[/_.?-]|$)/i;
|
|
1292
|
+
function contentScript(url) {
|
|
1293
|
+
let path;
|
|
1294
|
+
try {
|
|
1295
|
+
path = new URL(url).pathname;
|
|
1296
|
+
} catch {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
return /\.m?js$/i.test(path) && !CART_OR_PIXEL.test(path);
|
|
1300
|
+
}
|
|
1301
|
+
var RENDERING_THRESHOLDS = {
|
|
1302
|
+
/** Rendered main-text words from which a page's content is judged. */
|
|
1303
|
+
jsContentMinWords: 50,
|
|
1304
|
+
/** The share of the rendered words at or under which the raw HTML lacks the main content. */
|
|
1305
|
+
jsContentRawShare: 0.25
|
|
1306
|
+
};
|
|
1307
|
+
function kindOf(change) {
|
|
1308
|
+
if (change.raw === "") return "added";
|
|
1309
|
+
if (change.rendered === "") return "removed";
|
|
1310
|
+
return "changed";
|
|
1311
|
+
}
|
|
1312
|
+
function changeParams(change) {
|
|
1313
|
+
return { change: kindOf(change), raw: change.raw, rendered: change.rendered };
|
|
1314
|
+
}
|
|
1315
|
+
function rawCanonicalElsewhere(pageKey, change) {
|
|
1316
|
+
if (change.raw === "" || change.rendered === "") return false;
|
|
1317
|
+
const host = hostOfKey(pageKey);
|
|
1318
|
+
const raw = normalizePageUrl(change.raw.trim(), host);
|
|
1319
|
+
return raw !== null && raw !== pageKey && normalizePageUrl(change.rendered.trim(), host) === pageKey;
|
|
1320
|
+
}
|
|
1321
|
+
function addedSelfCanonical(pageKey, change) {
|
|
1322
|
+
if (kindOf(change) !== "added") return false;
|
|
1323
|
+
return normalizePageUrl(change.rendered.trim(), hostOfKey(pageKey)) === pageKey;
|
|
1324
|
+
}
|
|
1325
|
+
function renderingFindings(page) {
|
|
1326
|
+
const render = page.facts?.render;
|
|
1327
|
+
if (render == null || htmlSnapshot(page) === null) return [];
|
|
1328
|
+
const key = page.pageKey;
|
|
1329
|
+
if (render.outcome === "failed") {
|
|
1330
|
+
if (render.error === "navigated") return [];
|
|
1331
|
+
return [finding(key, "render_failed", { error: render.error ?? "" })];
|
|
1332
|
+
}
|
|
1333
|
+
const found = [];
|
|
1334
|
+
if (render.outcome === "timeout") {
|
|
1335
|
+
found.push(
|
|
1336
|
+
finding(key, "render_timeout", {
|
|
1337
|
+
seconds: Math.round(seoCrawlerLimits.renderTimeoutMs / 1e3)
|
|
1338
|
+
})
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const t = RENDERING_THRESHOLDS;
|
|
1342
|
+
const words = render.renderedWords ?? 0;
|
|
1343
|
+
const built = words >= t.jsContentMinWords && render.rawWords <= words * t.jsContentRawShare;
|
|
1344
|
+
if (built) {
|
|
1345
|
+
found.push(
|
|
1346
|
+
finding(key, "needs_javascript", { rawWords: render.rawWords, renderedWords: words })
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
if (render.jsOnlyLinks.count > 0 && !built) {
|
|
1350
|
+
found.push(
|
|
1351
|
+
finding(key, "js_links_only", {
|
|
1352
|
+
count: render.jsOnlyLinks.count,
|
|
1353
|
+
pages: render.jsOnlyLinks.examples.slice(0, AUDIT_EXAMPLES)
|
|
1354
|
+
})
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
const changed = render.changed;
|
|
1358
|
+
const own = (change) => change !== void 0 && !(built && kindOf(change) === "added");
|
|
1359
|
+
if (own(changed.title)) found.push(finding(key, "js_title_changed", changeParams(changed.title)));
|
|
1360
|
+
if (own(changed.metaDescription))
|
|
1361
|
+
found.push(finding(key, "js_meta_description_changed", changeParams(changed.metaDescription)));
|
|
1362
|
+
if (own(changed.h1)) found.push(finding(key, "js_h1_changed", changeParams(changed.h1)));
|
|
1363
|
+
if (changed.canonical !== void 0 && !addedSelfCanonical(key, changed.canonical))
|
|
1364
|
+
found.push(
|
|
1365
|
+
finding(
|
|
1366
|
+
key,
|
|
1367
|
+
rawCanonicalElsewhere(key, changed.canonical) ? "js_canonical_raw_elsewhere" : "js_canonical_changed",
|
|
1368
|
+
changeParams(changed.canonical)
|
|
1369
|
+
)
|
|
1370
|
+
);
|
|
1371
|
+
if (changed.robots !== void 0) {
|
|
1372
|
+
const rawNoindex = directivesNoindex(changed.robots.raw);
|
|
1373
|
+
const renderedNoindex = directivesNoindex(changed.robots.rendered);
|
|
1374
|
+
if (rawNoindex && !renderedNoindex)
|
|
1375
|
+
found.push(finding(key, "js_noindex_removed", { raw: changed.robots.raw }));
|
|
1376
|
+
else if (!rawNoindex && renderedNoindex)
|
|
1377
|
+
found.push(finding(key, "js_noindex_added", { rendered: changed.robots.rendered }));
|
|
1378
|
+
if (!rawNoindex && directivesNofollow(changed.robots.raw) && !directivesNofollow(changed.robots.rendered))
|
|
1379
|
+
found.push(finding(key, "js_nofollow_removed", { raw: changed.robots.raw }));
|
|
1380
|
+
}
|
|
1381
|
+
const added = changed.schemaTypesAdded ?? [];
|
|
1382
|
+
if (added.length > 0) found.push(finding(key, "js_structured_data_only", { types: added }));
|
|
1383
|
+
if (changed.hreflang !== void 0) {
|
|
1384
|
+
found.push(
|
|
1385
|
+
finding(key, "js_hreflang_changed", {
|
|
1386
|
+
rawCount: changed.hreflang.rawCount,
|
|
1387
|
+
renderedCount: changed.hreflang.renderedCount
|
|
1388
|
+
})
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
const errors = render.consoleErrors;
|
|
1392
|
+
const onlyBlockedFetches = render.blockedResources.count > 0 && errors.examples.length === errors.count && errors.examples.every((message) => FETCH_FAILURE.test(message));
|
|
1393
|
+
if (errors.count > 0 && !onlyBlockedFetches) {
|
|
1394
|
+
found.push(
|
|
1395
|
+
finding(key, "js_errors", {
|
|
1396
|
+
count: render.consoleErrors.count,
|
|
1397
|
+
messages: render.consoleErrors.examples.slice(0, AUDIT_EXAMPLES)
|
|
1398
|
+
})
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
const blocked = render.blockedResources;
|
|
1402
|
+
if (blocked.count > 0) {
|
|
1403
|
+
const contentByScript = words >= t.jsContentMinWords && render.rawWords <= words * t.jsContentRawShare;
|
|
1404
|
+
const needed2 = (blocked.stylesheets ?? 0) > 0 || (blocked.scripts ?? 0) > 0 && contentByScript && blocked.examples.some(contentScript);
|
|
1405
|
+
found.push(
|
|
1406
|
+
finding(key, needed2 ? "resources_blocked" : "resources_blocked_minor", {
|
|
1407
|
+
count: blocked.count,
|
|
1408
|
+
urls: blocked.examples.slice(0, AUDIT_EXAMPLES)
|
|
1409
|
+
})
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
const failed = render.failedResources;
|
|
1413
|
+
const siteHost = bareHostOfKey(key);
|
|
1414
|
+
const needed = failed.examples.filter(
|
|
1415
|
+
(failure) => (render.outcome !== "timeout" || failure.status !== null) && (failure.type !== "script" || built || onOwnHost(failure.url, siteHost))
|
|
1416
|
+
);
|
|
1417
|
+
const count = render.outcome !== "timeout" && needed.length === failed.examples.length ? failed.count : needed.length;
|
|
1418
|
+
if (count > 0) {
|
|
1419
|
+
found.push(
|
|
1420
|
+
finding(key, "resources_failed", {
|
|
1421
|
+
count,
|
|
1422
|
+
urls: needed.map((failure) => failure.url).slice(0, AUDIT_EXAMPLES)
|
|
1423
|
+
})
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
return found;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/rules/head.ts
|
|
1430
|
+
var GOOGLEBOT_FILE_BYTES = 2 * 1024 * 1024;
|
|
1431
|
+
var DIRECTIVES = ["canonical", "robots", "hreflang"];
|
|
1432
|
+
var CHANGE_OF = {
|
|
1433
|
+
canonical: "canonical",
|
|
1434
|
+
robots: "robots",
|
|
1435
|
+
hreflang: "hreflang",
|
|
1436
|
+
title: "title",
|
|
1437
|
+
description: "metaDescription"
|
|
1438
|
+
};
|
|
1439
|
+
function outsideHead(markup) {
|
|
1440
|
+
const kinds = [];
|
|
1441
|
+
if (markup.canonicals.some((entry) => !entry.inHead)) kinds.push("canonical");
|
|
1442
|
+
if (markup.robots.some((entry) => !entry.inHead)) kinds.push("robots");
|
|
1443
|
+
if (markup.hreflangOutsideHead > 0) kinds.push("hreflang");
|
|
1444
|
+
if (markup.titles.some((entry) => !entry.inHead)) kinds.push("title");
|
|
1445
|
+
if (markup.descriptions.some((entry) => !entry.inHead)) kinds.push("description");
|
|
1446
|
+
return kinds;
|
|
1447
|
+
}
|
|
1448
|
+
function rawHeadOf(page, markup) {
|
|
1449
|
+
const rendered = page.snapshot?.mode === "browser" || page.fetchMode === "browser";
|
|
1450
|
+
if (!rendered) return markup.head;
|
|
1451
|
+
return page.facts?.render?.rawHead ?? null;
|
|
1452
|
+
}
|
|
1453
|
+
function headBreakOf(page, markup, raw) {
|
|
1454
|
+
const rendered = page.snapshot?.mode === "browser" || page.fetchMode === "browser";
|
|
1455
|
+
const rawLost = page.facts?.render?.rawOutsideHead;
|
|
1456
|
+
if (rendered && markup.head.breaker != null && rawLost !== void 0) {
|
|
1457
|
+
return { breaker: raw?.breaker ?? null, lost: rawLost };
|
|
1458
|
+
}
|
|
1459
|
+
const changed = page.facts?.render?.changed ?? {};
|
|
1460
|
+
const moved = (kind) => rendered && rawLost !== void 0 && !rawLost.includes(kind) && !(CHANGE_OF[kind] in changed);
|
|
1461
|
+
return {
|
|
1462
|
+
breaker: markup.head.breaker ?? raw?.breaker ?? null,
|
|
1463
|
+
lost: outsideHead(markup).filter((kind) => !moved(kind))
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function structureProblems(head) {
|
|
1467
|
+
const kinds = [];
|
|
1468
|
+
if (head.htmlTags > 1) kinds.push("multiple_html");
|
|
1469
|
+
if (head.headTags > 1) kinds.push("multiple_head");
|
|
1470
|
+
if (head.bodyTags > 1) kinds.push("multiple_body");
|
|
1471
|
+
if (head.contentBeforeHtml) kinds.push("content_before_html");
|
|
1472
|
+
if (head.headNotFirst) kinds.push("head_not_first");
|
|
1473
|
+
return kinds;
|
|
1474
|
+
}
|
|
1475
|
+
function baseProblem(markup, pageUrl) {
|
|
1476
|
+
const withHref = markup.bases.filter((base2) => base2.href !== null);
|
|
1477
|
+
if (withHref.length === 0) return null;
|
|
1478
|
+
if (new Set(withHref.map((base2) => base2.href)).size > 1) return "multiple";
|
|
1479
|
+
const base = withHref[0];
|
|
1480
|
+
if (base === void 0 || base.href === null) return null;
|
|
1481
|
+
if (base.href.trim() === "") return null;
|
|
1482
|
+
try {
|
|
1483
|
+
const url = new URL(base.href, pageUrl);
|
|
1484
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "malformed";
|
|
1485
|
+
} catch {
|
|
1486
|
+
return "malformed";
|
|
1487
|
+
}
|
|
1488
|
+
return base.inHead ? null : "outside_head";
|
|
1489
|
+
}
|
|
1490
|
+
function headFindings(page) {
|
|
1491
|
+
const snapshot = htmlSnapshot(page);
|
|
1492
|
+
if (snapshot === null || snapshot.mode === "rendered") return [];
|
|
1493
|
+
const key = page.pageKey;
|
|
1494
|
+
const found = [];
|
|
1495
|
+
const size = page.facts?.sizeBytes ?? snapshot.sizeBytes ?? 0;
|
|
1496
|
+
if (size > GOOGLEBOT_FILE_BYTES) {
|
|
1497
|
+
const cut = page.facts?.htmlCut;
|
|
1498
|
+
const minor = cut !== void 0 && !cut.head && !cut.mainText;
|
|
1499
|
+
found.push(
|
|
1500
|
+
finding(key, minor ? "html_over_2mb_minor" : "html_over_2mb", {
|
|
1501
|
+
bytes: size,
|
|
1502
|
+
max: GOOGLEBOT_FILE_BYTES
|
|
1503
|
+
})
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
const markup = page.facts?.markup ?? null;
|
|
1507
|
+
if (markup === null) return found;
|
|
1508
|
+
const raw = rawHeadOf(page, markup);
|
|
1509
|
+
const { breaker, lost } = headBreakOf(page, markup, raw);
|
|
1510
|
+
if (breaker?.inNoscript !== true && lost.length > 0) {
|
|
1511
|
+
if (breaker !== null) {
|
|
1512
|
+
found.push(
|
|
1513
|
+
finding(key, "head_invalid_element", {
|
|
1514
|
+
tag: breaker.tag,
|
|
1515
|
+
line: breaker.line,
|
|
1516
|
+
lost
|
|
1517
|
+
})
|
|
1518
|
+
);
|
|
1519
|
+
} else {
|
|
1520
|
+
const directives = lost.filter((kind) => DIRECTIVES.includes(kind));
|
|
1521
|
+
const metadata = lost.filter((kind) => !DIRECTIVES.includes(kind));
|
|
1522
|
+
if (directives.length > 0) {
|
|
1523
|
+
found.push(finding(key, "directive_outside_head", { elements: directives }));
|
|
1524
|
+
}
|
|
1525
|
+
if (metadata.length > 0) {
|
|
1526
|
+
found.push(finding(key, "metadata_outside_head", { elements: metadata }));
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const structure = raw === null ? [] : structureProblems(raw);
|
|
1531
|
+
if (structure.length > 0) {
|
|
1532
|
+
found.push(finding(key, "html_structure_broken", { kinds: structure, kind: structure[0] }));
|
|
1533
|
+
}
|
|
1534
|
+
const base = baseProblem(markup, snapshot.finalUrl ?? `https://${key}`);
|
|
1535
|
+
if (base !== null) found.push(finding(key, "base_tag_invalid", { kind: base }));
|
|
1536
|
+
const plugins = markup.plugins.applet + (markup.plugins.legacy ?? 0);
|
|
1537
|
+
if (plugins > 0) found.push(finding(key, "unsupported_plugins", { applets: plugins }));
|
|
1538
|
+
return found;
|
|
1539
|
+
}
|
|
1540
|
+
function builtByScript(page) {
|
|
1541
|
+
const render = page.facts?.render ?? null;
|
|
1542
|
+
if (render === null) return false;
|
|
1543
|
+
const words = render.renderedWords ?? 0;
|
|
1544
|
+
const t = RENDERING_THRESHOLDS;
|
|
1545
|
+
return words >= t.jsContentMinWords && render.rawWords <= words * t.jsContentRawShare;
|
|
1546
|
+
}
|
|
1547
|
+
var oversizedResourceFindings = (page, resources) => {
|
|
1548
|
+
const siteHost = bareHostOfKey(page.pageKey);
|
|
1549
|
+
const anyScript = builtByScript(page);
|
|
1550
|
+
const over = resources.filter((resource) => {
|
|
1551
|
+
if (resource.kind !== "script" && resource.kind !== "stylesheet") return false;
|
|
1552
|
+
if (resource.kind === "script" && !anyScript && !onOwnHost(resource.url, siteHost))
|
|
1553
|
+
return false;
|
|
1554
|
+
const bytes = resource.headers?.decodedBytes ?? resource.sizeBytes;
|
|
1555
|
+
return bytes !== null && bytes > GOOGLEBOT_FILE_BYTES;
|
|
1556
|
+
});
|
|
1557
|
+
if (over.length === 0) return [];
|
|
1558
|
+
return [
|
|
1559
|
+
finding(page.pageKey, "resource_over_2mb", {
|
|
1560
|
+
count: over.length,
|
|
1561
|
+
urls: over.map((resource) => resource.url).slice(0, AUDIT_EXAMPLES),
|
|
1562
|
+
max: GOOGLEBOT_FILE_BYTES
|
|
1563
|
+
})
|
|
1564
|
+
];
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
// src/fetch.ts
|
|
1568
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
1569
|
+
function toFetchUrl(url) {
|
|
1570
|
+
let parsed;
|
|
1571
|
+
try {
|
|
1572
|
+
parsed = new URL(url);
|
|
1573
|
+
} catch {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
1577
|
+
parsed.protocol = "https:";
|
|
1578
|
+
parsed.hash = "";
|
|
1579
|
+
return parsed.href;
|
|
1580
|
+
}
|
|
1581
|
+
function failureKind(error) {
|
|
1582
|
+
if (error instanceof Error) {
|
|
1583
|
+
if (error.name === "BlockedUrlError") return "blocked";
|
|
1584
|
+
if (error.name === "TimeoutError" || error.name === "AbortError") return "timeout";
|
|
1585
|
+
}
|
|
1586
|
+
return "network";
|
|
1587
|
+
}
|
|
1588
|
+
var SEO_OPTIONAL_AUDIT_HEADERS = [
|
|
1589
|
+
"contentType",
|
|
1590
|
+
"contentEncoding",
|
|
1591
|
+
"cacheControl",
|
|
1592
|
+
"contentLanguage",
|
|
1593
|
+
"refresh"
|
|
1594
|
+
];
|
|
1595
|
+
function optionalAuditHeaders(headers, maxChars) {
|
|
1596
|
+
const kept = {};
|
|
1597
|
+
for (const name of SEO_OPTIONAL_AUDIT_HEADERS) {
|
|
1598
|
+
const value = headers[name];
|
|
1599
|
+
if (value !== void 0) kept[name] = value === null ? null : value.slice(0, maxChars);
|
|
1600
|
+
}
|
|
1601
|
+
return kept;
|
|
1602
|
+
}
|
|
1603
|
+
var NO_AUDIT_HEADERS = {
|
|
1604
|
+
strictTransportSecurity: null,
|
|
1605
|
+
contentSecurityPolicy: null,
|
|
1606
|
+
xContentTypeOptions: null,
|
|
1607
|
+
xFrameOptions: null,
|
|
1608
|
+
referrerPolicy: null,
|
|
1609
|
+
link: null,
|
|
1610
|
+
contentType: null,
|
|
1611
|
+
contentEncoding: null,
|
|
1612
|
+
cacheControl: null,
|
|
1613
|
+
contentLanguage: null,
|
|
1614
|
+
refresh: null
|
|
1615
|
+
};
|
|
1616
|
+
function readAuditHeaders(headers) {
|
|
1617
|
+
return {
|
|
1618
|
+
strictTransportSecurity: headers.get("strict-transport-security"),
|
|
1619
|
+
contentSecurityPolicy: headers.get("content-security-policy"),
|
|
1620
|
+
xContentTypeOptions: headers.get("x-content-type-options"),
|
|
1621
|
+
xFrameOptions: headers.get("x-frame-options"),
|
|
1622
|
+
referrerPolicy: headers.get("referrer-policy"),
|
|
1623
|
+
link: headers.get("link"),
|
|
1624
|
+
contentType: headers.get("content-type"),
|
|
1625
|
+
contentEncoding: headers.get("content-encoding"),
|
|
1626
|
+
cacheControl: headers.get("cache-control"),
|
|
1627
|
+
contentLanguage: headers.get("content-language"),
|
|
1628
|
+
refresh: headers.get("refresh")
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
async function fetchResource(url, options) {
|
|
1632
|
+
const started = options.now().getTime();
|
|
1633
|
+
const chain = [];
|
|
1634
|
+
let current = toFetchUrl(url);
|
|
1635
|
+
let spelled = null;
|
|
1636
|
+
const failed = (error) => ({
|
|
1637
|
+
requestedUrl: url,
|
|
1638
|
+
finalUrl: current ?? url,
|
|
1639
|
+
status: chain[0]?.status ?? 0,
|
|
1640
|
+
finalStatus: 0,
|
|
1641
|
+
chain,
|
|
1642
|
+
contentType: null,
|
|
1643
|
+
xRobotsTag: null,
|
|
1644
|
+
auditHeaders: NO_AUDIT_HEADERS,
|
|
1645
|
+
body: null,
|
|
1646
|
+
truncated: false,
|
|
1647
|
+
sizeBytes: 0,
|
|
1648
|
+
responseMs: options.now().getTime() - started,
|
|
1649
|
+
error
|
|
1650
|
+
});
|
|
1651
|
+
if (current === null) return failed("blocked");
|
|
1652
|
+
for (let hop = 0; hop <= options.maxRedirects; hop += 1) {
|
|
1653
|
+
const hopStarted = options.now().getTime();
|
|
1654
|
+
const left = options.timeoutMs - (hopStarted - started);
|
|
1655
|
+
if (left <= 0) return failed("timeout");
|
|
1656
|
+
let response;
|
|
1657
|
+
try {
|
|
1658
|
+
response = await options.fetcher(
|
|
1659
|
+
current,
|
|
1660
|
+
{
|
|
1661
|
+
method: "GET",
|
|
1662
|
+
headers: {
|
|
1663
|
+
"User-Agent": options.userAgent,
|
|
1664
|
+
Accept: options.accept,
|
|
1665
|
+
"Accept-Language": "*"
|
|
1666
|
+
}
|
|
1667
|
+
},
|
|
1668
|
+
left
|
|
1669
|
+
);
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
return failed(failureKind(error));
|
|
1672
|
+
}
|
|
1673
|
+
chain.push(
|
|
1674
|
+
spelled === null || spelled === current ? { url: current, status: response.status } : { url: current, status: response.status, location: spelled }
|
|
1675
|
+
);
|
|
1676
|
+
const location = response.headers.get("location");
|
|
1677
|
+
if (REDIRECT_STATUSES.has(response.status) && location !== null) {
|
|
1678
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1679
|
+
let next;
|
|
1680
|
+
try {
|
|
1681
|
+
spelled = new URL(location, current).href;
|
|
1682
|
+
next = toFetchUrl(spelled);
|
|
1683
|
+
} catch {
|
|
1684
|
+
next = null;
|
|
1685
|
+
}
|
|
1686
|
+
if (next === null) return failed("blocked");
|
|
1687
|
+
current = next;
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
let read;
|
|
1691
|
+
try {
|
|
1692
|
+
const { sink } = options;
|
|
1693
|
+
if (sink === void 0) read = await readStreamCapped(response.body, options.maxBytes);
|
|
1694
|
+
else if (response.status >= 200 && response.status < 300) {
|
|
1695
|
+
const contentType = response.headers.get("content-type");
|
|
1696
|
+
const drained = await drainStreamCapped(
|
|
1697
|
+
response.body,
|
|
1698
|
+
options.maxBytes,
|
|
1699
|
+
(bytes) => sink(bytes, contentType)
|
|
1700
|
+
);
|
|
1701
|
+
read = { body: null, ...drained };
|
|
1702
|
+
} else {
|
|
1703
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1704
|
+
read = { body: null, truncated: false, sizeBytes: 0 };
|
|
1705
|
+
}
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
return failed(failureKind(error));
|
|
1708
|
+
}
|
|
1709
|
+
return {
|
|
1710
|
+
requestedUrl: url,
|
|
1711
|
+
finalUrl: current,
|
|
1712
|
+
status: chain[0]?.status ?? response.status,
|
|
1713
|
+
finalStatus: response.status,
|
|
1714
|
+
chain,
|
|
1715
|
+
contentType: response.headers.get("content-type"),
|
|
1716
|
+
xRobotsTag: response.headers.get("x-robots-tag"),
|
|
1717
|
+
auditHeaders: readAuditHeaders(response.headers),
|
|
1718
|
+
body: read.body,
|
|
1719
|
+
truncated: read.truncated,
|
|
1720
|
+
sizeBytes: read.sizeBytes,
|
|
1721
|
+
responseMs: options.now().getTime() - hopStarted,
|
|
1722
|
+
error: null,
|
|
1723
|
+
retryAfter: response.headers.get("retry-after")
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
return failed("too_many_redirects");
|
|
1727
|
+
}
|
|
1728
|
+
async function decodeServedBody(body, contentType, maxBytes) {
|
|
1729
|
+
let bytes = body.byteLength > maxBytes ? body.subarray(0, maxBytes) : body;
|
|
1730
|
+
if (bytes.byteLength > 2 && bytes[0] === 31 && bytes[1] === 139) {
|
|
1731
|
+
try {
|
|
1732
|
+
bytes = (await gunzipCapped(bytes, maxBytes)).body;
|
|
1733
|
+
} catch {
|
|
1734
|
+
bytes = body.subarray(0, Math.min(body.byteLength, maxBytes));
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const charset = /charset=([^;\s]+)/i.exec(contentType ?? "")?.[1]?.replace(/["']/g, "");
|
|
1738
|
+
let text;
|
|
1739
|
+
try {
|
|
1740
|
+
text = new TextDecoder(charset ?? "utf-8").decode(bytes);
|
|
1741
|
+
} catch {
|
|
1742
|
+
text = new TextDecoder("utf-8").decode(bytes);
|
|
1743
|
+
}
|
|
1744
|
+
return text;
|
|
1745
|
+
}
|
|
1746
|
+
function withoutNul(text) {
|
|
1747
|
+
return text.replaceAll("\0", "");
|
|
1748
|
+
}
|
|
1749
|
+
async function decodeBody(body, contentType, maxBytes) {
|
|
1750
|
+
return withoutNul(await decodeServedBody(body, contentType, maxBytes));
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
export { AUDIT_EXAMPLES, GOOGLEBOT_FILE_BYTES, PAGE_REDIRECT_RULES, RENDERING_THRESHOLDS, SEO_AUDIT_RULES, SEO_AUDIT_SEVERITIES, SEO_AUDIT_SUPERSEDED_BY, SEO_AUDIT_THRESHOLD_KEYS, SEO_AUDIT_THRESHOLD_SPECS, SEO_AUDIT_TRAFFIC_JOINS, SEO_BOT_AGENT_REFUSED, SEO_CRAWL_OPTION_LIMITS, SEO_CRAWL_ROBOTS_GROUPS, SEO_CRAWL_USER_AGENT_PRESETS, SEO_CUSTOM_CHECK_LIMITS, SEO_CUSTOM_CHECK_MATCHES, SEO_CUSTOM_CHECK_MODES, SEO_GRAMMAR_LIMITS, SEO_MARKUP_LIMITS, SEO_READING_LIMITS, SEO_ROBOTS_MAX_BYTES, brokenStatus, canonicalElsewhere, canonicalKey, canonicalMismatch, checkoutPage, compileRobots, compileRobotsPattern, copyRedirect, crawlBlocked, customThresholdsOf, decodeBody, decodeServedBody, directivesNofollow, directivesNoindex, failureUnconfirmed, fetchResource, finding, gunzipCapped, h1MissingOf, headFindings, htmlSnapshot, indexableSnapshot, isHome, isRobotsUnreachable, isSeoAuditCrawlFact, isSeoAuditRuleId, nofollowOf, noindexOf, normalizePageUrl, onOwnHost, optionalAuditHeaders, outsideHead, oversizedResourceFindings, parseRobots, readStreamCapped, redirectCopy, renderingFindings, robotsAllows, robotsDecision, robotsPatternMatches, seoAuditByCategory, seoAuditRule, thresholdOrderProblem, thresholdsOf, toFetchUrl, withoutNul };
|