@rivi-software/agentic-seo-check 0.1.0 → 0.2.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 +1 -1
- package/README.md +124 -18
- package/dist/brand.d.ts +11 -0
- package/dist/brand.js +22 -0
- package/dist/brand.js.map +1 -0
- package/dist/cli.js +174 -86
- package/dist/cli.js.map +1 -1
- package/dist/core/aeo/audit.d.ts +44 -0
- package/dist/core/aeo/audit.js +310 -0
- package/dist/core/aeo/audit.js.map +1 -0
- package/dist/core/aeo/bots.d.ts +30 -0
- package/dist/core/aeo/bots.js +299 -0
- package/dist/core/aeo/bots.js.map +1 -0
- package/dist/core/aeo/llms-txt.d.ts +8 -0
- package/dist/core/aeo/llms-txt.js +88 -0
- package/dist/core/aeo/llms-txt.js.map +1 -0
- package/dist/core/aeo/page.d.ts +53 -0
- package/dist/core/aeo/page.js +312 -0
- package/dist/core/aeo/page.js.map +1 -0
- package/dist/core/aeo/probe.d.ts +10 -0
- package/dist/core/aeo/probe.js +53 -0
- package/dist/core/aeo/probe.js.map +1 -0
- package/dist/core/aeo/site-rules.d.ts +13 -0
- package/dist/core/aeo/site-rules.js +178 -0
- package/dist/core/aeo/site-rules.js.map +1 -0
- package/dist/core/audit.d.ts +37 -16
- package/dist/core/audit.js +389 -249
- package/dist/core/audit.js.map +1 -1
- package/dist/core/concurrency.d.ts +4 -4
- package/dist/core/concurrency.js +33 -23
- package/dist/core/concurrency.js.map +1 -1
- package/dist/core/crawl.d.ts +9 -5
- package/dist/core/crawl.js +271 -123
- package/dist/core/crawl.js.map +1 -1
- package/dist/core/fetch.d.ts +5 -5
- package/dist/core/fetch.js +151 -100
- package/dist/core/fetch.js.map +1 -1
- package/dist/core/html/answer.d.ts +4 -0
- package/dist/core/html/answer.js +198 -0
- package/dist/core/html/answer.js.map +1 -0
- package/dist/core/html/dom.d.ts +17 -0
- package/dist/core/html/dom.js +83 -0
- package/dist/core/html/dom.js.map +1 -0
- package/dist/core/html/extract.d.ts +2 -2
- package/dist/core/html/extract.js +102 -111
- package/dist/core/html/extract.js.map +1 -1
- package/dist/core/html/structured-data.d.ts +16 -6
- package/dist/core/html/structured-data.js +180 -145
- package/dist/core/html/structured-data.js.map +1 -1
- package/dist/core/html/text.d.ts +14 -14
- package/dist/core/html/text.js +110 -107
- package/dist/core/html/text.js.map +1 -1
- package/dist/core/lighthouse.d.ts +26 -25
- package/dist/core/lighthouse.js +181 -105
- package/dist/core/lighthouse.js.map +1 -1
- package/dist/core/performance-rules.d.ts +2 -2
- package/dist/core/performance-rules.js +105 -33
- package/dist/core/performance-rules.js.map +1 -1
- package/dist/core/render-rules.d.ts +5 -4
- package/dist/core/render-rules.js +129 -62
- package/dist/core/render-rules.js.map +1 -1
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +29 -23
- package/dist/core/render.js.map +1 -1
- package/dist/core/report/markdown.d.ts +4 -2
- package/dist/core/report/markdown.js +210 -129
- package/dist/core/report/markdown.js.map +1 -1
- package/dist/core/robots.d.ts +12 -5
- package/dist/core/robots.js +43 -17
- package/dist/core/robots.js.map +1 -1
- package/dist/core/rules/aeo.d.ts +11 -0
- package/dist/core/rules/aeo.js +423 -0
- package/dist/core/rules/aeo.js.map +1 -0
- package/dist/core/rules/canonical.d.ts +1 -1
- package/dist/core/rules/canonical.js +93 -57
- package/dist/core/rules/canonical.js.map +1 -1
- package/dist/core/rules/content.js +40 -26
- package/dist/core/rules/content.js.map +1 -1
- package/dist/core/rules/document.js +64 -41
- package/dist/core/rules/document.js.map +1 -1
- package/dist/core/rules/headings.js +58 -38
- package/dist/core/rules/headings.js.map +1 -1
- package/dist/core/rules/hreflang.js +70 -38
- package/dist/core/rules/hreflang.js.map +1 -1
- package/dist/core/rules/images.js +44 -26
- package/dist/core/rules/images.js.map +1 -1
- package/dist/core/rules/index.d.ts +10 -6
- package/dist/core/rules/index.js +43 -36
- package/dist/core/rules/index.js.map +1 -1
- package/dist/core/rules/indexing.d.ts +2 -2
- package/dist/core/rules/indexing.js +48 -26
- package/dist/core/rules/indexing.js.map +1 -1
- package/dist/core/rules/keyword.js +84 -47
- package/dist/core/rules/keyword.js.map +1 -1
- package/dist/core/rules/links.js +72 -37
- package/dist/core/rules/links.js.map +1 -1
- package/dist/core/rules/meta.js +55 -34
- package/dist/core/rules/meta.js.map +1 -1
- package/dist/core/rules/response.js +75 -41
- package/dist/core/rules/response.js.map +1 -1
- package/dist/core/rules/rule.d.ts +6 -6
- package/dist/core/rules/rule.js +36 -36
- package/dist/core/rules/rule.js.map +1 -1
- package/dist/core/rules/social.js +86 -45
- package/dist/core/rules/social.js.map +1 -1
- package/dist/core/rules/structured-data.d.ts +6 -4
- package/dist/core/rules/structured-data.js +314 -102
- package/dist/core/rules/structured-data.js.map +1 -1
- package/dist/core/rules/title.js +48 -33
- package/dist/core/rules/title.js.map +1 -1
- package/dist/core/score.d.ts +5 -3
- package/dist/core/score.js +11 -9
- package/dist/core/score.js.map +1 -1
- package/dist/core/site-rules.d.ts +3 -3
- package/dist/core/site-rules.js +409 -198
- package/dist/core/site-rules.js.map +1 -1
- package/dist/core/sitemap.d.ts +5 -5
- package/dist/core/sitemap.js +109 -64
- package/dist/core/sitemap.js.map +1 -1
- package/dist/core/static.d.ts +2 -2
- package/dist/core/static.js +210 -112
- package/dist/core/static.js.map +1 -1
- package/dist/core/summary.d.ts +2 -2
- package/dist/core/summary.js +11 -6
- package/dist/core/summary.js.map +1 -1
- package/dist/core/types.d.ts +189 -14
- package/dist/core/types.js +15 -8
- package/dist/core/types.js.map +1 -1
- package/dist/core/url.d.ts +11 -11
- package/dist/core/url.js +122 -70
- package/dist/core/url.js.map +1 -1
- package/dist/index.d.ts +15 -11
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +7 -4
- package/dist/mcp-bin.js.map +1 -1
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +437 -121
- package/dist/mcp.js.map +1 -1
- package/dist/version.d.ts +0 -1
- package/dist/version.js +4 -2
- package/dist/version.js.map +1 -1
- package/package.json +32 -10
- package/skills/seo-audit/SKILL.md +8 -2
|
@@ -1,105 +1,141 @@
|
|
|
1
|
-
import { hostOf, normalizeUrl, samePath } from
|
|
2
|
-
import { cap, defineRule, GOOGLE_DOCS } from
|
|
1
|
+
import { hostOf, normalizeUrl, samePath } from "../url.js";
|
|
2
|
+
import { cap, defineRule, GOOGLE_DOCS } from "./rule.js";
|
|
3
3
|
const SELECTOR = 'head > link[rel="canonical"]';
|
|
4
|
-
const LOCAL_HOSTS = new Set([
|
|
5
|
-
|
|
4
|
+
const LOCAL_HOSTS = new Set([
|
|
5
|
+
"localhost",
|
|
6
|
+
"127.0.0.1",
|
|
7
|
+
"0.0.0.0",
|
|
8
|
+
"::1",
|
|
9
|
+
"[::1]",
|
|
10
|
+
]);
|
|
11
|
+
export const isLocalHost = (url) => {
|
|
6
12
|
const host = hostOf(url);
|
|
7
|
-
if (!host)
|
|
13
|
+
if (!host) {
|
|
8
14
|
return false;
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
}
|
|
16
|
+
return (LOCAL_HOSTS.has(host) ||
|
|
17
|
+
host.endsWith(".local") ||
|
|
18
|
+
host.endsWith(".localhost") ||
|
|
19
|
+
host.endsWith(".test"));
|
|
20
|
+
};
|
|
21
|
+
const stripTrailingSlash = (url) => url.endsWith("/") && new URL(url).pathname !== "/" ? url.slice(0, -1) : url;
|
|
11
22
|
export const canonicalRules = [
|
|
12
23
|
defineRule({
|
|
13
|
-
id:
|
|
14
|
-
category:
|
|
15
|
-
severity:
|
|
24
|
+
id: "canonical.missing",
|
|
25
|
+
category: "canonical",
|
|
26
|
+
severity: "warning",
|
|
16
27
|
description: 'No <link rel="canonical"> is present.',
|
|
17
|
-
recommendation:
|
|
28
|
+
recommendation: "Add a self-referencing canonical with the absolute preferred URL (scheme, host, trailing slash) so duplicate URLs (query strings, www/non-www) consolidate to it.",
|
|
18
29
|
docs: GOOGLE_DOCS.canonical,
|
|
19
|
-
}, ({ page }, make) =>
|
|
30
|
+
}, ({ page }, make) => page.canonicals.length === 0
|
|
31
|
+
? make("Page has no canonical link.", { selector: SELECTOR })
|
|
32
|
+
: null),
|
|
20
33
|
defineRule({
|
|
21
|
-
id:
|
|
22
|
-
category:
|
|
23
|
-
severity:
|
|
24
|
-
description:
|
|
25
|
-
recommendation:
|
|
34
|
+
id: "canonical.multiple",
|
|
35
|
+
category: "canonical",
|
|
36
|
+
severity: "error",
|
|
37
|
+
description: "More than one canonical link is present. When they disagree, search engines ignore all of them.",
|
|
38
|
+
recommendation: "Emit exactly one canonical link per page. Check for a framework default plus a hand-written one.",
|
|
26
39
|
docs: GOOGLE_DOCS.canonical,
|
|
27
40
|
}, ({ page }, make) => {
|
|
28
|
-
if (page.canonicals.length <= 1)
|
|
41
|
+
if (page.canonicals.length <= 1) {
|
|
29
42
|
return null;
|
|
43
|
+
}
|
|
30
44
|
const distinct = new Set(page.canonicals.map((c) => c.trim()));
|
|
31
|
-
return make(`Page has ${page.canonicals.length} canonical links${distinct.size > 1 ?
|
|
45
|
+
return make(`Page has ${page.canonicals.length} canonical links${distinct.size > 1 ? " with different targets" : " (identical)"}.`, {
|
|
32
46
|
selector: SELECTOR,
|
|
33
|
-
severity: distinct.size > 1 ?
|
|
47
|
+
severity: distinct.size > 1 ? "error" : "warning",
|
|
34
48
|
evidence: { canonicals: cap(page.canonicals) },
|
|
35
49
|
});
|
|
36
50
|
}),
|
|
37
51
|
defineRule({
|
|
38
|
-
id:
|
|
39
|
-
category:
|
|
40
|
-
severity:
|
|
41
|
-
description:
|
|
42
|
-
recommendation:
|
|
52
|
+
id: "canonical.invalid",
|
|
53
|
+
category: "canonical",
|
|
54
|
+
severity: "error",
|
|
55
|
+
description: "The canonical href is empty or cannot be resolved to an http(s) URL.",
|
|
56
|
+
recommendation: "Set the canonical href to a valid absolute URL such as https://www.example.com/path/.",
|
|
43
57
|
docs: GOOGLE_DOCS.canonical,
|
|
44
58
|
}, ({ page }, make) => {
|
|
45
|
-
const first = page.canonicals
|
|
46
|
-
if (first === undefined)
|
|
59
|
+
const [first] = page.canonicals;
|
|
60
|
+
if (first === undefined) {
|
|
47
61
|
return null;
|
|
48
|
-
|
|
62
|
+
}
|
|
63
|
+
return page.canonical
|
|
64
|
+
? null
|
|
65
|
+
: make(`Canonical href "${first}" is not a valid URL.`, {
|
|
66
|
+
selector: SELECTOR,
|
|
67
|
+
evidence: { href: first },
|
|
68
|
+
});
|
|
49
69
|
}),
|
|
50
70
|
defineRule({
|
|
51
|
-
id:
|
|
52
|
-
category:
|
|
53
|
-
severity:
|
|
54
|
-
description:
|
|
55
|
-
recommendation:
|
|
71
|
+
id: "canonical.relative",
|
|
72
|
+
category: "canonical",
|
|
73
|
+
severity: "warning",
|
|
74
|
+
description: "The canonical href is a relative URL.",
|
|
75
|
+
recommendation: "Use an absolute URL in the canonical link. Relative canonicals are error-prone and some crawlers resolve them unexpectedly.",
|
|
56
76
|
docs: GOOGLE_DOCS.canonical,
|
|
57
77
|
}, ({ page }, make) => {
|
|
58
|
-
const first = page.canonicals
|
|
59
|
-
if (!first || !page.canonical)
|
|
78
|
+
const [first] = page.canonicals;
|
|
79
|
+
if (!first || !page.canonical) {
|
|
60
80
|
return null;
|
|
61
|
-
|
|
81
|
+
}
|
|
82
|
+
return /^https?:\/\//iu.test(first)
|
|
83
|
+
? null
|
|
84
|
+
: make(`Canonical href "${first}" is relative.`, {
|
|
85
|
+
selector: SELECTOR,
|
|
86
|
+
evidence: { href: first, resolved: page.canonical },
|
|
87
|
+
});
|
|
62
88
|
}),
|
|
63
89
|
defineRule({
|
|
64
|
-
id:
|
|
65
|
-
category:
|
|
66
|
-
severity:
|
|
67
|
-
description:
|
|
68
|
-
recommendation:
|
|
90
|
+
id: "canonical.protocol-mismatch",
|
|
91
|
+
category: "canonical",
|
|
92
|
+
severity: "warning",
|
|
93
|
+
description: "The page is served over https but the canonical points to http.",
|
|
94
|
+
recommendation: "Point the canonical at the https URL.",
|
|
69
95
|
docs: GOOGLE_DOCS.canonical,
|
|
70
96
|
}, ({ page, source }, make) => {
|
|
71
|
-
if (source !==
|
|
97
|
+
if (source !== "url" || !page.canonical) {
|
|
72
98
|
return null;
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
}
|
|
100
|
+
return page.url.startsWith("https://") &&
|
|
101
|
+
page.canonical.startsWith("http://")
|
|
102
|
+
? make("Canonical uses http:// while the page is served over https://.", {
|
|
103
|
+
selector: SELECTOR,
|
|
104
|
+
evidence: { canonical: page.canonical, page: page.url },
|
|
105
|
+
})
|
|
75
106
|
: null;
|
|
76
107
|
}),
|
|
77
108
|
defineRule({
|
|
78
|
-
id:
|
|
79
|
-
category:
|
|
80
|
-
severity:
|
|
81
|
-
description:
|
|
82
|
-
recommendation:
|
|
109
|
+
id: "canonical.other-page",
|
|
110
|
+
category: "canonical",
|
|
111
|
+
severity: "info",
|
|
112
|
+
description: "The canonical points to a different URL, so search engines will treat this page as a duplicate of that URL.",
|
|
113
|
+
recommendation: "If this page should rank on its own, make the canonical self-referencing. If it is intentionally a duplicate (print view, filtered list, tracking URL), this is correct.",
|
|
83
114
|
docs: GOOGLE_DOCS.canonical,
|
|
84
115
|
}, ({ page, source }, make) => {
|
|
85
|
-
if (!page.canonical)
|
|
116
|
+
if (!page.canonical) {
|
|
86
117
|
return null;
|
|
118
|
+
}
|
|
87
119
|
const pageUrl = normalizeUrl(page.url);
|
|
88
120
|
const canon = normalizeUrl(page.canonical);
|
|
89
|
-
if (!pageUrl || !canon)
|
|
121
|
+
if (!pageUrl || !canon) {
|
|
90
122
|
return null;
|
|
123
|
+
}
|
|
91
124
|
// For files and local dev servers the host is synthetic; compare paths only.
|
|
92
|
-
const pathOnly = source !==
|
|
93
|
-
const same = pathOnly
|
|
125
|
+
const pathOnly = source !== "url" || isLocalHost(page.url);
|
|
126
|
+
const same = pathOnly
|
|
127
|
+
? samePath(pageUrl, canon)
|
|
128
|
+
: stripTrailingSlash(pageUrl) === stripTrailingSlash(canon);
|
|
94
129
|
return same
|
|
95
130
|
? null
|
|
96
131
|
: make(`Canonical points to ${page.canonical}, not to this page.`, {
|
|
97
132
|
selector: SELECTOR,
|
|
98
|
-
evidence: {
|
|
133
|
+
evidence: {
|
|
134
|
+
canonical: page.canonical,
|
|
135
|
+
page: page.url,
|
|
136
|
+
comparedBy: pathOnly ? "path" : "url",
|
|
137
|
+
},
|
|
99
138
|
});
|
|
100
139
|
}),
|
|
101
140
|
];
|
|
102
|
-
function stripTrailingSlash(url) {
|
|
103
|
-
return url.endsWith('/') && new URL(url).pathname !== '/' ? url.slice(0, -1) : url;
|
|
104
|
-
}
|
|
105
141
|
//# sourceMappingURL=canonical.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canonical.js","sourceRoot":"","sources":["../../../src/core/rules/canonical.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEzD,MAAM,QAAQ,GAAG,8BAA8B,CAAC;AAEhD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"canonical.js","sourceRoot":"","sources":["../../../src/core/rules/canonical.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,41 +1,55 @@
|
|
|
1
|
-
import { fleschReadingEase, hasPlaceholderText } from
|
|
2
|
-
import { defineRule, GOOGLE_DOCS } from
|
|
1
|
+
import { fleschReadingEase, hasPlaceholderText } from "../html/text.js";
|
|
2
|
+
import { defineRule, GOOGLE_DOCS } from "./rule.js";
|
|
3
3
|
export const contentRules = [
|
|
4
4
|
defineRule({
|
|
5
|
-
id:
|
|
6
|
-
category:
|
|
7
|
-
severity:
|
|
8
|
-
description:
|
|
9
|
-
recommendation:
|
|
5
|
+
id: "content.empty",
|
|
6
|
+
category: "content",
|
|
7
|
+
severity: "warning",
|
|
8
|
+
description: "The HTML contains no visible text.",
|
|
9
|
+
recommendation: "If the page is rendered client-side, use server-side rendering or static generation so the HTML carries the content. Re-run with render enabled to compare the rendered DOM.",
|
|
10
10
|
docs: GOOGLE_DOCS.javascript,
|
|
11
|
-
}, ({ page }, make) =>
|
|
11
|
+
}, ({ page }, make) => page.wordCount === 0
|
|
12
|
+
? make("Page body has no visible text in the HTML response.")
|
|
13
|
+
: null),
|
|
12
14
|
defineRule({
|
|
13
|
-
id:
|
|
14
|
-
category:
|
|
15
|
-
severity:
|
|
16
|
-
description:
|
|
17
|
-
recommendation:
|
|
15
|
+
id: "content.thin",
|
|
16
|
+
category: "content",
|
|
17
|
+
severity: "warning",
|
|
18
|
+
description: "The page has very little text (default: fewer than 300 words).",
|
|
19
|
+
recommendation: "Add substantive, useful content, or set noindex if the page is a utility page (login, cart, thank-you). Thin pages rarely rank and can drag down site quality signals.",
|
|
18
20
|
}, ({ page, thresholds }, make) => page.wordCount > 0 && page.wordCount < thresholds.thinContentWords
|
|
19
|
-
? make(`Page has only ${page.wordCount} words of visible text (threshold ${thresholds.thinContentWords}).`, {
|
|
21
|
+
? make(`Page has only ${page.wordCount} words of visible text (threshold ${thresholds.thinContentWords}).`, {
|
|
22
|
+
evidence: {
|
|
23
|
+
wordCount: page.wordCount,
|
|
24
|
+
threshold: thresholds.thinContentWords,
|
|
25
|
+
},
|
|
26
|
+
})
|
|
20
27
|
: null),
|
|
21
28
|
defineRule({
|
|
22
|
-
id:
|
|
23
|
-
category:
|
|
24
|
-
severity:
|
|
29
|
+
id: "content.placeholder-text",
|
|
30
|
+
category: "content",
|
|
31
|
+
severity: "error",
|
|
25
32
|
description: 'Placeholder text ("lorem ipsum") is present in the page.',
|
|
26
|
-
recommendation:
|
|
27
|
-
}, ({ page }, make) =>
|
|
33
|
+
recommendation: "Replace the placeholder copy with real content before publishing.",
|
|
34
|
+
}, ({ page }, make) => hasPlaceholderText(page.text)
|
|
35
|
+
? make('Page contains "lorem ipsum" placeholder text.')
|
|
36
|
+
: null),
|
|
28
37
|
defineRule({
|
|
29
|
-
id:
|
|
30
|
-
category:
|
|
31
|
-
severity:
|
|
32
|
-
description:
|
|
33
|
-
recommendation:
|
|
38
|
+
id: "content.readability-low",
|
|
39
|
+
category: "content",
|
|
40
|
+
severity: "info",
|
|
41
|
+
description: "Flesch Reading Ease below 30 (very difficult). Only evaluated for English or unspecified language, 100+ words.",
|
|
42
|
+
recommendation: "Shorten sentences and prefer common words. Aim for a Flesch score of 50-70 for general audiences.",
|
|
34
43
|
}, ({ page }, make) => {
|
|
35
|
-
if (page.lang && !page.lang.toLowerCase().startsWith(
|
|
44
|
+
if (page.lang && !page.lang.toLowerCase().startsWith("en")) {
|
|
36
45
|
return null;
|
|
46
|
+
}
|
|
37
47
|
const score = fleschReadingEase(page.text);
|
|
38
|
-
return score !== null && score < 30
|
|
48
|
+
return score !== null && score < 30
|
|
49
|
+
? make(`Flesch Reading Ease is ${score} (very difficult).`, {
|
|
50
|
+
evidence: { fleschReadingEase: score, wordCount: page.wordCount },
|
|
51
|
+
})
|
|
52
|
+
: null;
|
|
39
53
|
}),
|
|
40
54
|
];
|
|
41
55
|
//# sourceMappingURL=content.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../../src/core/rules/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU,CACR;QACE,EAAE,EAAE,eAAe;QACnB,QAAQ,EAAE,SAAS;QACnB,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,oCAAoC;QACjD,cAAc,
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../../src/core/rules/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU,CACR;QACE,EAAE,EAAE,eAAe;QACnB,QAAQ,EAAE,SAAS;QACnB,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,oCAAoC;QACjD,cAAc,EACZ,8KAA8K;QAChL,IAAI,EAAE,WAAW,CAAC,UAAU;KAC7B,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CACjB,IAAI,CAAC,SAAS,KAAK,CAAC;QAClB,CAAC,CAAC,IAAI,CAAC,qDAAqD,CAAC;QAC7D,CAAC,CAAC,IAAI,CACX;IACD,UAAU,CACR;QACE,EAAE,EAAE,cAAc;QAClB,QAAQ,EAAE,SAAS;QACnB,QAAQ,EAAE,SAAS;QACnB,WAAW,EACT,gEAAgE;QAClE,cAAc,EACZ,wKAAwK;KAC3K,EACD,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,CAC7B,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,gBAAgB;QAChE,CAAC,CAAC,IAAI,CACF,iBAAiB,IAAI,CAAC,SAAS,qCAAqC,UAAU,CAAC,gBAAgB,IAAI,EACnG;YACE,QAAQ,EAAE;gBACR,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,SAAS,EAAE,UAAU,CAAC,gBAAgB;aACvC;SACF,CACF;QACH,CAAC,CAAC,IAAI,CACX;IACD,UAAU,CACR;QACE,EAAE,EAAE,0BAA0B;QAC9B,QAAQ,EAAE,SAAS;QACnB,QAAQ,EAAE,OAAO;QACjB,WAAW,EAAE,0DAA0D;QACvE,cAAc,EACZ,mEAAmE;KACtE,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CACjB,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC3B,CAAC,CAAC,IAAI,CAAC,+CAA+C,CAAC;QACvD,CAAC,CAAC,IAAI,CACX;IACD,UAAU,CACR;QACE,EAAE,EAAE,yBAAyB;QAC7B,QAAQ,EAAE,SAAS;QACnB,QAAQ,EAAE,MAAM;QAChB,WAAW,EACT,gHAAgH;QAClH,cAAc,EACZ,mGAAmG;KACtG,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE;QACjB,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE3C,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,GAAG,EAAE;YACjC,CAAC,CAAC,IAAI,CAAC,0BAA0B,KAAK,oBAAoB,EAAE;gBACxD,QAAQ,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;aAClE,CAAC;YACJ,CAAC,CAAC,IAAI,CAAC;IACX,CAAC,CACF;CACF,CAAC"}
|
|
@@ -1,60 +1,83 @@
|
|
|
1
|
-
import { defineRule, GOOGLE_DOCS } from
|
|
2
|
-
const BCP47 = /^[a-zA-Z]{2,3}(
|
|
1
|
+
import { defineRule, GOOGLE_DOCS } from "./rule.js";
|
|
2
|
+
const BCP47 = /^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})*$/u;
|
|
3
3
|
export const documentRules = [
|
|
4
4
|
defineRule({
|
|
5
|
-
id:
|
|
6
|
-
category:
|
|
7
|
-
severity:
|
|
8
|
-
description:
|
|
5
|
+
id: "html.lang-missing",
|
|
6
|
+
category: "document",
|
|
7
|
+
severity: "warning",
|
|
8
|
+
description: "The <html> element has no lang attribute.",
|
|
9
9
|
recommendation: 'Set <html lang="xx"> (for example "en", "nl", "en-GB") so search engines and screen readers know the content language.',
|
|
10
|
-
}, ({ page }, make) =>
|
|
10
|
+
}, ({ page }, make) => page.lang
|
|
11
|
+
? null
|
|
12
|
+
: make("<html> has no lang attribute.", { selector: "html" })),
|
|
11
13
|
defineRule({
|
|
12
|
-
id:
|
|
13
|
-
category:
|
|
14
|
-
severity:
|
|
15
|
-
description:
|
|
14
|
+
id: "html.lang-invalid",
|
|
15
|
+
category: "document",
|
|
16
|
+
severity: "warning",
|
|
17
|
+
description: "The <html lang> value is not a valid BCP 47 language tag.",
|
|
16
18
|
recommendation: 'Use a BCP 47 tag such as "en", "en-US" or "pt-BR".',
|
|
17
|
-
}, ({ page }, make) =>
|
|
19
|
+
}, ({ page }, make) => page.lang && !BCP47.test(page.lang)
|
|
20
|
+
? make(`<html lang="${page.lang}"> is not a valid language tag.`, {
|
|
21
|
+
selector: "html",
|
|
22
|
+
evidence: { lang: page.lang },
|
|
23
|
+
})
|
|
24
|
+
: null),
|
|
18
25
|
defineRule({
|
|
19
|
-
id:
|
|
20
|
-
category:
|
|
21
|
-
severity:
|
|
26
|
+
id: "viewport.missing",
|
|
27
|
+
category: "document",
|
|
28
|
+
severity: "warning",
|
|
22
29
|
description: 'No <meta name="viewport"> is present, so the page is not mobile-friendly.',
|
|
23
30
|
recommendation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1"> to <head>. Google indexes the mobile version of pages first.',
|
|
24
31
|
docs: GOOGLE_DOCS.mobile,
|
|
25
|
-
}, ({ page }, make) =>
|
|
32
|
+
}, ({ page }, make) => page.viewport
|
|
33
|
+
? null
|
|
34
|
+
: make("Page has no viewport meta tag.", {
|
|
35
|
+
selector: 'head > meta[name="viewport"]',
|
|
36
|
+
})),
|
|
26
37
|
defineRule({
|
|
27
|
-
id:
|
|
28
|
-
category:
|
|
29
|
-
severity:
|
|
30
|
-
description:
|
|
31
|
-
recommendation:
|
|
38
|
+
id: "viewport.not-scalable",
|
|
39
|
+
category: "document",
|
|
40
|
+
severity: "info",
|
|
41
|
+
description: "The viewport disables user zoom (user-scalable=no or maximum-scale=1).",
|
|
42
|
+
recommendation: "Remove user-scalable=no and maximum-scale limits; blocking zoom is an accessibility problem flagged by Lighthouse.",
|
|
32
43
|
}, ({ page }, make) => {
|
|
33
|
-
const v = (page.viewport ??
|
|
34
|
-
return /user-scalable=(no|0)
|
|
35
|
-
?
|
|
44
|
+
const v = (page.viewport ?? "").toLowerCase().replaceAll(/\s/gu, "");
|
|
45
|
+
return /user-scalable=(?:no|0)/u.test(v) ||
|
|
46
|
+
/maximum-scale=1(?:\.0*)?(?:,|$)/u.test(v)
|
|
47
|
+
? make("Viewport prevents users from zooming.", {
|
|
48
|
+
selector: 'head > meta[name="viewport"]',
|
|
49
|
+
evidence: { viewport: page.viewport },
|
|
50
|
+
})
|
|
36
51
|
: null;
|
|
37
52
|
}),
|
|
38
53
|
defineRule({
|
|
39
|
-
id:
|
|
40
|
-
category:
|
|
41
|
-
severity:
|
|
42
|
-
description:
|
|
54
|
+
id: "charset.missing",
|
|
55
|
+
category: "document",
|
|
56
|
+
severity: "info",
|
|
57
|
+
description: "No character encoding is declared.",
|
|
43
58
|
recommendation: 'Add <meta charset="utf-8"> as the first element in <head>.',
|
|
44
|
-
}, ({ page }, make) =>
|
|
59
|
+
}, ({ page }, make) => page.charset
|
|
60
|
+
? null
|
|
61
|
+
: make("No <meta charset> declaration found.", {
|
|
62
|
+
selector: "head > meta[charset]",
|
|
63
|
+
})),
|
|
45
64
|
defineRule({
|
|
46
|
-
id:
|
|
47
|
-
category:
|
|
48
|
-
severity:
|
|
49
|
-
description:
|
|
50
|
-
recommendation:
|
|
51
|
-
}, ({ page }, make) =>
|
|
65
|
+
id: "doctype.missing",
|
|
66
|
+
category: "document",
|
|
67
|
+
severity: "info",
|
|
68
|
+
description: "The document does not start with <!DOCTYPE html>.",
|
|
69
|
+
recommendation: "Start the document with <!DOCTYPE html> to avoid quirks-mode rendering.",
|
|
70
|
+
}, ({ page }, make) => page.hasDoctype ? null : make("Document has no HTML5 doctype.")),
|
|
52
71
|
defineRule({
|
|
53
|
-
id:
|
|
54
|
-
category:
|
|
55
|
-
severity:
|
|
56
|
-
description:
|
|
57
|
-
recommendation:
|
|
58
|
-
}, ({ page }, make) => page.htmlBytes > 1_500_000
|
|
72
|
+
id: "document.html-too-large",
|
|
73
|
+
category: "document",
|
|
74
|
+
severity: "info",
|
|
75
|
+
description: "The HTML document is larger than 1.5 MB.",
|
|
76
|
+
recommendation: "Reduce inline scripts/styles/data (large JSON state, inline SVG sprites). Very large documents slow parsing and may be truncated by crawlers.",
|
|
77
|
+
}, ({ page }, make) => page.htmlBytes > 1_500_000
|
|
78
|
+
? make(`HTML is ${(page.htmlBytes / 1_000_000).toFixed(1)} MB.`, {
|
|
79
|
+
evidence: { bytes: page.htmlBytes },
|
|
80
|
+
})
|
|
81
|
+
: null),
|
|
59
82
|
];
|
|
60
83
|
//# sourceMappingURL=document.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document.js","sourceRoot":"","sources":["../../../src/core/rules/document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,KAAK,GAAG,
|
|
1
|
+
{"version":3,"file":"document.js","sourceRoot":"","sources":["../../../src/core/rules/document.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,61 +1,81 @@
|
|
|
1
|
-
import { cap, defineRule } from
|
|
1
|
+
import { cap, defineRule } from "./rule.js";
|
|
2
2
|
export const headingRules = [
|
|
3
3
|
defineRule({
|
|
4
|
-
id:
|
|
5
|
-
category:
|
|
6
|
-
severity:
|
|
7
|
-
description:
|
|
8
|
-
recommendation:
|
|
9
|
-
}, ({ page }, make) =>
|
|
4
|
+
id: "h1.missing",
|
|
5
|
+
category: "headings",
|
|
6
|
+
severity: "warning",
|
|
7
|
+
description: "The page has no <h1>.",
|
|
8
|
+
recommendation: "Add one <h1> that states the main topic of the page. It is usually the visible page heading.",
|
|
9
|
+
}, ({ page }, make) => page.h1s.length === 0
|
|
10
|
+
? make("Page has no <h1> heading.", { selector: "h1" })
|
|
11
|
+
: null),
|
|
10
12
|
defineRule({
|
|
11
|
-
id:
|
|
12
|
-
category:
|
|
13
|
-
severity:
|
|
14
|
-
description:
|
|
15
|
-
recommendation:
|
|
16
|
-
}, ({ page }, make) => page.h1s.length > 1
|
|
13
|
+
id: "h1.multiple",
|
|
14
|
+
category: "headings",
|
|
15
|
+
severity: "info",
|
|
16
|
+
description: "The page has more than one <h1>.",
|
|
17
|
+
recommendation: "Prefer a single <h1> for the main topic and use <h2>-<h6> for sections. Multiple H1s are valid HTML but make the main topic less clear.",
|
|
18
|
+
}, ({ page }, make) => page.h1s.length > 1
|
|
19
|
+
? make(`Page has ${page.h1s.length} <h1> headings.`, {
|
|
20
|
+
selector: "h1",
|
|
21
|
+
evidence: { h1s: cap(page.h1s) },
|
|
22
|
+
})
|
|
23
|
+
: null),
|
|
17
24
|
defineRule({
|
|
18
|
-
id:
|
|
19
|
-
category:
|
|
20
|
-
severity:
|
|
21
|
-
description:
|
|
22
|
-
recommendation:
|
|
23
|
-
}, ({ page }, make) =>
|
|
25
|
+
id: "h1.empty",
|
|
26
|
+
category: "headings",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
description: "An <h1> exists but contains no text.",
|
|
29
|
+
recommendation: "Put the page heading text inside the <h1>. If it wraps only an image, give the image descriptive alt text.",
|
|
30
|
+
}, ({ page }, make) => page.h1s.length > 0 && page.h1s.every((h) => !h)
|
|
31
|
+
? make("All <h1> elements are empty.", { selector: "h1" })
|
|
32
|
+
: null),
|
|
24
33
|
defineRule({
|
|
25
|
-
id:
|
|
26
|
-
category:
|
|
27
|
-
severity:
|
|
28
|
-
description:
|
|
29
|
-
recommendation:
|
|
34
|
+
id: "h1.too-long",
|
|
35
|
+
category: "headings",
|
|
36
|
+
severity: "info",
|
|
37
|
+
description: "The <h1> is longer than the configured maximum (default 70 characters).",
|
|
38
|
+
recommendation: "Keep the H1 concise; move detail into the intro paragraph.",
|
|
30
39
|
}, ({ page, thresholds }, make) => {
|
|
31
40
|
const long = page.h1s.filter((h) => h.length > thresholds.h1MaxChars);
|
|
32
|
-
return long.length
|
|
41
|
+
return long.length
|
|
42
|
+
? make(`${long.length} <h1> heading(s) exceed ${thresholds.h1MaxChars} characters.`, { selector: "h1", evidence: { h1s: cap(long) } })
|
|
43
|
+
: null;
|
|
33
44
|
}),
|
|
34
45
|
defineRule({
|
|
35
|
-
id:
|
|
36
|
-
category:
|
|
37
|
-
severity:
|
|
38
|
-
description:
|
|
39
|
-
recommendation:
|
|
46
|
+
id: "headings.empty",
|
|
47
|
+
category: "headings",
|
|
48
|
+
severity: "warning",
|
|
49
|
+
description: "Heading elements (h2-h6) with no text content.",
|
|
50
|
+
recommendation: "Remove empty heading tags or fill them with text. Empty headings confuse outline parsers and screen readers.",
|
|
40
51
|
}, ({ page }, make) => {
|
|
41
52
|
const empty = page.headings.filter((h) => h.level > 1 && !h.text);
|
|
42
|
-
return empty.length
|
|
53
|
+
return empty.length
|
|
54
|
+
? make(`${empty.length} empty heading element(s) found.`, {
|
|
55
|
+
evidence: { levels: cap(empty.map((h) => `h${h.level}`)) },
|
|
56
|
+
})
|
|
57
|
+
: null;
|
|
43
58
|
}),
|
|
44
59
|
defineRule({
|
|
45
|
-
id:
|
|
46
|
-
category:
|
|
47
|
-
severity:
|
|
48
|
-
description:
|
|
49
|
-
recommendation:
|
|
60
|
+
id: "headings.skipped-level",
|
|
61
|
+
category: "headings",
|
|
62
|
+
severity: "info",
|
|
63
|
+
description: "Heading levels are skipped (for example an <h2> followed directly by an <h4>).",
|
|
64
|
+
recommendation: "Use heading levels in order (h1 > h2 > h3) so the document outline reflects the content structure.",
|
|
50
65
|
}, ({ page }, make) => {
|
|
51
66
|
const skips = [];
|
|
52
67
|
let prev = 0;
|
|
53
68
|
for (const h of page.headings) {
|
|
54
|
-
if (prev > 0 && h.level > prev + 1)
|
|
69
|
+
if (prev > 0 && h.level > prev + 1) {
|
|
55
70
|
skips.push(`h${prev} → h${h.level} ("${h.text.slice(0, 40)}")`);
|
|
71
|
+
}
|
|
56
72
|
prev = h.level;
|
|
57
73
|
}
|
|
58
|
-
return skips.length
|
|
74
|
+
return skips.length
|
|
75
|
+
? make(`${skips.length} heading level skip(s) found.`, {
|
|
76
|
+
evidence: { skips: cap(skips) },
|
|
77
|
+
})
|
|
78
|
+
: null;
|
|
59
79
|
}),
|
|
60
80
|
];
|
|
61
81
|
//# sourceMappingURL=headings.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headings.js","sourceRoot":"","sources":["../../../src/core/rules/headings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU,CACR;QACE,EAAE,EAAE,YAAY;QAChB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,uBAAuB;QACpC,cAAc,
|
|
1
|
+
{"version":3,"file":"headings.js","sourceRoot":"","sources":["../../../src/core/rules/headings.ts"],"names":[],"mappings":"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"}
|