search-auditor 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/CHANGELOG.md +16 -0
- package/README.md +87 -38
- package/dist/cli.js +17 -9
- package/dist/collect.d.ts +5 -1
- package/dist/collect.js +186 -11
- package/dist/i18n/en.d.ts +462 -7
- package/dist/i18n/en.js +480 -25
- package/dist/i18n/index.d.ts +924 -14
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/ru.js +484 -24
- package/dist/index.d.ts +3 -145
- package/dist/index.js +14 -53
- package/dist/rules/can/can-1.d.ts +2 -0
- package/dist/rules/can/can-1.js +14 -0
- package/dist/rules/can/can-2.d.ts +2 -0
- package/dist/rules/can/can-2.js +20 -0
- package/dist/rules/can/can-3.d.ts +2 -0
- package/dist/rules/can/can-3.js +32 -0
- package/dist/rules/can/can-4.d.ts +2 -0
- package/dist/rules/can/can-4.js +18 -0
- package/dist/rules/can/can-5.d.ts +2 -0
- package/dist/rules/can/can-5.js +17 -0
- package/dist/rules/can/can-6.d.ts +2 -0
- package/dist/rules/can/can-6.js +22 -0
- package/dist/rules/can/can-7.d.ts +2 -0
- package/dist/rules/can/can-7.js +32 -0
- package/dist/rules/can/can-8.d.ts +2 -0
- package/dist/rules/can/can-8.js +23 -0
- package/dist/rules/can/can-9.d.ts +2 -0
- package/dist/rules/can/can-9.js +24 -0
- package/dist/rules/can/index.d.ts +7 -0
- package/dist/rules/can/index.js +10 -0
- package/dist/rules/can/shared.d.ts +11 -0
- package/dist/rules/can/shared.js +26 -0
- package/dist/rules/data/data-1.d.ts +2 -0
- package/dist/rules/data/data-1.js +16 -0
- package/dist/rules/data/data-2.d.ts +2 -0
- package/dist/rules/data/data-2.js +25 -0
- package/dist/rules/data/data-3.d.ts +2 -0
- package/dist/rules/data/data-3.js +29 -0
- package/dist/rules/data/data-4.d.ts +2 -0
- package/dist/rules/data/data-4.js +17 -0
- package/dist/rules/data/data-5.d.ts +2 -0
- package/dist/rules/data/data-5.js +28 -0
- package/dist/rules/data/data-6.d.ts +2 -0
- package/dist/rules/data/data-6.js +16 -0
- package/dist/rules/data/index.d.ts +8 -0
- package/dist/rules/data/index.js +7 -0
- package/dist/rules/data/shared.d.ts +22 -0
- package/dist/rules/data/shared.js +59 -0
- package/dist/rules/idx/idx-1.d.ts +2 -0
- package/dist/rules/idx/idx-1.js +15 -0
- package/dist/rules/idx/idx-2.d.ts +2 -0
- package/dist/rules/idx/idx-2.js +22 -0
- package/dist/rules/idx/idx-3.d.ts +2 -0
- package/dist/rules/idx/idx-3.js +35 -0
- package/dist/rules/idx/idx-4.d.ts +2 -0
- package/dist/rules/idx/idx-4.js +40 -0
- package/dist/rules/idx/idx-5.d.ts +2 -0
- package/dist/rules/idx/idx-5.js +24 -0
- package/dist/rules/idx/idx-6.d.ts +2 -0
- package/dist/rules/idx/idx-6.js +25 -0
- package/dist/rules/idx/idx-7.d.ts +2 -0
- package/dist/rules/idx/idx-7.js +29 -0
- package/dist/rules/idx/idx-8.d.ts +2 -0
- package/dist/rules/idx/idx-8.js +25 -0
- package/dist/rules/idx/idx-9.d.ts +2 -0
- package/dist/rules/idx/idx-9.js +22 -0
- package/dist/rules/idx/index.d.ts +7 -0
- package/dist/rules/idx/index.js +10 -0
- package/dist/rules/idx/shared.d.ts +18 -0
- package/dist/rules/idx/shared.js +18 -0
- package/dist/rules/img/img-1.d.ts +2 -0
- package/dist/rules/img/img-1.js +18 -0
- package/dist/rules/img/img-2.d.ts +2 -0
- package/dist/rules/img/img-2.js +20 -0
- package/dist/rules/img/img-3.d.ts +2 -0
- package/dist/rules/img/img-3.js +24 -0
- package/dist/rules/img/img-4.d.ts +2 -0
- package/dist/rules/img/img-4.js +19 -0
- package/dist/rules/img/img-5.d.ts +2 -0
- package/dist/rules/img/img-5.js +18 -0
- package/dist/rules/img/img-6.d.ts +2 -0
- package/dist/rules/img/img-6.js +20 -0
- package/dist/rules/img/img-7.d.ts +2 -0
- package/dist/rules/img/img-7.js +22 -0
- package/dist/rules/img/index.d.ts +7 -0
- package/dist/rules/img/index.js +8 -0
- package/dist/rules/img/shared.d.ts +26 -0
- package/dist/rules/img/shared.js +38 -0
- package/dist/rules/index.d.ts +1 -1
- package/dist/rules/index.js +23 -6
- package/dist/rules/lang/index.d.ts +8 -0
- package/dist/rules/lang/index.js +9 -0
- package/dist/rules/lang/lang-1.d.ts +2 -0
- package/dist/rules/lang/lang-1.js +17 -0
- package/dist/rules/lang/lang-2.d.ts +2 -0
- package/dist/rules/lang/lang-2.js +30 -0
- package/dist/rules/lang/lang-3.d.ts +2 -0
- package/dist/rules/lang/lang-3.js +22 -0
- package/dist/rules/lang/lang-4.d.ts +2 -0
- package/dist/rules/lang/lang-4.js +23 -0
- package/dist/rules/lang/lang-5.d.ts +2 -0
- package/dist/rules/lang/lang-5.js +33 -0
- package/dist/rules/lang/lang-6.d.ts +2 -0
- package/dist/rules/lang/lang-6.js +25 -0
- package/dist/rules/lang/lang-7.d.ts +2 -0
- package/dist/rules/lang/lang-7.js +25 -0
- package/dist/rules/lang/lang-8.d.ts +2 -0
- package/dist/rules/lang/lang-8.js +19 -0
- package/dist/rules/lang/shared.d.ts +7 -0
- package/dist/rules/lang/shared.js +5 -0
- package/dist/rules/lnk/index.d.ts +8 -0
- package/dist/rules/lnk/index.js +8 -0
- package/dist/rules/lnk/lnk-1.d.ts +2 -0
- package/dist/rules/lnk/lnk-1.js +22 -0
- package/dist/rules/lnk/lnk-2.d.ts +2 -0
- package/dist/rules/lnk/lnk-2.js +18 -0
- package/dist/rules/lnk/lnk-3.d.ts +2 -0
- package/dist/rules/lnk/lnk-3.js +32 -0
- package/dist/rules/lnk/lnk-4.d.ts +2 -0
- package/dist/rules/lnk/lnk-4.js +18 -0
- package/dist/rules/lnk/lnk-5.d.ts +2 -0
- package/dist/rules/lnk/lnk-5.js +22 -0
- package/dist/rules/lnk/lnk-6.d.ts +2 -0
- package/dist/rules/lnk/lnk-6.js +24 -0
- package/dist/rules/lnk/lnk-7.d.ts +2 -0
- package/dist/rules/lnk/lnk-7.js +26 -0
- package/dist/rules/lnk/shared.d.ts +20 -0
- package/dist/rules/lnk/shared.js +29 -0
- package/dist/rules/map/index.d.ts +7 -0
- package/dist/rules/map/index.js +9 -0
- package/dist/rules/map/map-1.d.ts +2 -0
- package/dist/rules/map/map-1.js +15 -0
- package/dist/rules/map/map-2.d.ts +2 -0
- package/dist/rules/map/map-2.js +26 -0
- package/dist/rules/map/map-3.d.ts +2 -0
- package/dist/rules/map/map-3.js +24 -0
- package/dist/rules/map/map-4.d.ts +2 -0
- package/dist/rules/map/map-4.js +34 -0
- package/dist/rules/map/map-5.d.ts +2 -0
- package/dist/rules/map/map-5.js +28 -0
- package/dist/rules/map/map-6.d.ts +2 -0
- package/dist/rules/map/map-6.js +22 -0
- package/dist/rules/map/map-7.d.ts +2 -0
- package/dist/rules/map/map-7.js +18 -0
- package/dist/rules/map/map-8.d.ts +2 -0
- package/dist/rules/map/map-8.js +31 -0
- package/dist/rules/map/shared.d.ts +14 -0
- package/dist/rules/map/shared.js +12 -0
- package/dist/rules/meta/index.d.ts +8 -0
- package/dist/rules/meta/index.js +10 -0
- package/dist/rules/meta/meta-1.d.ts +2 -0
- package/dist/rules/meta/meta-1.js +20 -0
- package/dist/rules/meta/meta-2.d.ts +2 -0
- package/dist/rules/meta/meta-2.js +19 -0
- package/dist/rules/meta/meta-3.d.ts +2 -0
- package/dist/rules/meta/meta-3.js +26 -0
- package/dist/rules/meta/meta-4.d.ts +2 -0
- package/dist/rules/meta/meta-4.js +22 -0
- package/dist/rules/meta/meta-5.d.ts +2 -0
- package/dist/rules/meta/meta-5.js +17 -0
- package/dist/rules/meta/meta-6.d.ts +2 -0
- package/dist/rules/meta/meta-6.js +18 -0
- package/dist/rules/meta/meta-7.d.ts +2 -0
- package/dist/rules/meta/meta-7.js +18 -0
- package/dist/rules/meta/meta-8.d.ts +2 -0
- package/dist/rules/meta/meta-8.js +17 -0
- package/dist/rules/meta/meta-9.d.ts +2 -0
- package/dist/rules/meta/meta-9.js +16 -0
- package/dist/rules/meta/shared.d.ts +15 -0
- package/dist/rules/meta/shared.js +9 -0
- package/dist/rules/sec/index.d.ts +8 -0
- package/dist/rules/sec/index.js +8 -0
- package/dist/rules/sec/sec-1.d.ts +2 -0
- package/dist/rules/sec/sec-1.js +21 -0
- package/dist/rules/sec/sec-2.d.ts +2 -0
- package/dist/rules/sec/sec-2.js +17 -0
- package/dist/rules/sec/sec-3.d.ts +2 -0
- package/dist/rules/sec/sec-3.js +18 -0
- package/dist/rules/sec/sec-4.d.ts +2 -0
- package/dist/rules/sec/sec-4.js +23 -0
- package/dist/rules/sec/sec-5.d.ts +2 -0
- package/dist/rules/sec/sec-5.js +23 -0
- package/dist/rules/sec/sec-6.d.ts +2 -0
- package/dist/rules/sec/sec-6.js +32 -0
- package/dist/rules/sec/sec-7.d.ts +2 -0
- package/dist/rules/sec/sec-7.js +26 -0
- package/dist/rules/sec/shared.d.ts +10 -0
- package/dist/rules/sec/shared.js +8 -0
- package/dist/rules/str/index.d.ts +8 -0
- package/dist/rules/str/index.js +11 -0
- package/dist/rules/str/shared.d.ts +14 -0
- package/dist/rules/str/shared.js +16 -0
- package/dist/rules/str/str-1.d.ts +2 -0
- package/dist/rules/str/str-1.js +17 -0
- package/dist/rules/str/str-10.d.ts +2 -0
- package/dist/rules/str/str-10.js +15 -0
- package/dist/rules/str/str-2.d.ts +2 -0
- package/dist/rules/str/str-2.js +19 -0
- package/dist/rules/str/str-3.d.ts +2 -0
- package/dist/rules/str/str-3.js +14 -0
- package/dist/rules/str/str-4.d.ts +2 -0
- package/dist/rules/str/str-4.js +14 -0
- package/dist/rules/str/str-5.d.ts +2 -0
- package/dist/rules/str/str-5.js +19 -0
- package/dist/rules/str/str-6.d.ts +2 -0
- package/dist/rules/str/str-6.js +22 -0
- package/dist/rules/str/str-7.d.ts +2 -0
- package/dist/rules/str/str-7.js +18 -0
- package/dist/rules/str/str-8.d.ts +2 -0
- package/dist/rules/str/str-8.js +24 -0
- package/dist/rules/str/str-9.d.ts +2 -0
- package/dist/rules/str/str-9.js +14 -0
- package/dist/rules/util.d.ts +1 -1
- package/dist/trace.d.ts +20 -0
- package/dist/trace.js +77 -0
- package/dist/types.d.ts +86 -6
- package/package.json +4 -9
- package/dist/rules/links.d.ts +0 -2
- package/dist/rules/links.js +0 -95
- package/dist/rules/page.d.ts +0 -2
- package/dist/rules/page.js +0 -145
- package/dist/rules/site.d.ts +0 -2
- package/dist/rules/site.js +0 -177
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { type CrawlOptions, type Lang, type Report } from "
|
|
2
|
-
import { collect } from "./collect.ts";
|
|
3
|
-
import { type SeoMessages } from "./i18n/index.ts";
|
|
1
|
+
import { type CrawlOptions, type Lang, type Report } from "auditor-engine";
|
|
4
2
|
import type { SearchConfig } from "./types.ts";
|
|
5
3
|
export type { Probe, SearchConfig, SeoExtras, SeoSite } from "./types.ts";
|
|
6
4
|
export type { SeoMessages, SeoRuleId } from "./i18n/index.ts";
|
|
@@ -8,7 +6,7 @@ export type { SeoRule } from "./rules/index.ts";
|
|
|
8
6
|
export { SEO_LANGS, seoMessages } from "./i18n/index.ts";
|
|
9
7
|
export { SEO_GROUPS, seoRules } from "./rules/index.ts";
|
|
10
8
|
export { collect, seoOf, type CollectOptions } from "./collect.ts";
|
|
11
|
-
export { toMarkdown, type Report } from "
|
|
9
|
+
export { toMarkdown, type Report } from "auditor-engine";
|
|
12
10
|
export declare const TOOL: {
|
|
13
11
|
name: string;
|
|
14
12
|
url: string;
|
|
@@ -16,143 +14,6 @@ export declare const TOOL: {
|
|
|
16
14
|
export declare const defaultConfig: SearchConfig;
|
|
17
15
|
/** SEO group ids and names in checklist order. */
|
|
18
16
|
export declare const seoGroups: (lang: Lang) => Record<string, string>;
|
|
19
|
-
/**
|
|
20
|
-
* The SEO checks as a plugin for another runner (ai-search-auditor --seo): everything it needs to run them on its own
|
|
21
|
-
* crawl and render them in its report.
|
|
22
|
-
*/
|
|
23
|
-
export declare const seoPlugin: {
|
|
24
|
-
name: string;
|
|
25
|
-
rules: import("./rules/util.ts").SeoRule[];
|
|
26
|
-
groups: (lang: Lang) => Record<string, string>;
|
|
27
|
-
texts: (lang: Lang) => {
|
|
28
|
-
"IDX-1": {
|
|
29
|
-
title: string;
|
|
30
|
-
fix: string;
|
|
31
|
-
impact: string;
|
|
32
|
-
};
|
|
33
|
-
"IDX-2": {
|
|
34
|
-
title: string;
|
|
35
|
-
fix: string;
|
|
36
|
-
impact: string;
|
|
37
|
-
};
|
|
38
|
-
"CAN-1": {
|
|
39
|
-
title: string;
|
|
40
|
-
fix: string;
|
|
41
|
-
impact: string;
|
|
42
|
-
};
|
|
43
|
-
"CAN-2": {
|
|
44
|
-
title: string;
|
|
45
|
-
fix: string;
|
|
46
|
-
impact: string;
|
|
47
|
-
};
|
|
48
|
-
"CAN-3": {
|
|
49
|
-
title: string;
|
|
50
|
-
fix: string;
|
|
51
|
-
impact: string;
|
|
52
|
-
};
|
|
53
|
-
"MAP-1": {
|
|
54
|
-
title: string;
|
|
55
|
-
fix: string;
|
|
56
|
-
impact: string;
|
|
57
|
-
};
|
|
58
|
-
"MAP-2": {
|
|
59
|
-
title: string;
|
|
60
|
-
fix: string;
|
|
61
|
-
impact: string;
|
|
62
|
-
};
|
|
63
|
-
"MAP-3": {
|
|
64
|
-
title: string;
|
|
65
|
-
fix: string;
|
|
66
|
-
impact: string;
|
|
67
|
-
};
|
|
68
|
-
"META-1": {
|
|
69
|
-
title: string;
|
|
70
|
-
fix: string;
|
|
71
|
-
impact: string;
|
|
72
|
-
};
|
|
73
|
-
"META-2": {
|
|
74
|
-
title: string;
|
|
75
|
-
fix: string;
|
|
76
|
-
impact: string;
|
|
77
|
-
};
|
|
78
|
-
"META-3": {
|
|
79
|
-
title: string;
|
|
80
|
-
fix: string;
|
|
81
|
-
impact: string;
|
|
82
|
-
};
|
|
83
|
-
"STR-1": {
|
|
84
|
-
title: string;
|
|
85
|
-
fix: string;
|
|
86
|
-
impact: string;
|
|
87
|
-
};
|
|
88
|
-
"STR-2": {
|
|
89
|
-
title: string;
|
|
90
|
-
fix: string;
|
|
91
|
-
impact: string;
|
|
92
|
-
};
|
|
93
|
-
"STR-3": {
|
|
94
|
-
title: string;
|
|
95
|
-
fix: string;
|
|
96
|
-
impact: string;
|
|
97
|
-
};
|
|
98
|
-
"STR-4": {
|
|
99
|
-
title: string;
|
|
100
|
-
fix: string;
|
|
101
|
-
impact: string;
|
|
102
|
-
};
|
|
103
|
-
"LNK-1": {
|
|
104
|
-
title: string;
|
|
105
|
-
fix: string;
|
|
106
|
-
impact: string;
|
|
107
|
-
};
|
|
108
|
-
"LNK-2": {
|
|
109
|
-
title: string;
|
|
110
|
-
fix: string;
|
|
111
|
-
impact: string;
|
|
112
|
-
};
|
|
113
|
-
"IMG-1": {
|
|
114
|
-
title: string;
|
|
115
|
-
fix: string;
|
|
116
|
-
impact: string;
|
|
117
|
-
};
|
|
118
|
-
"DATA-1": {
|
|
119
|
-
title: string;
|
|
120
|
-
fix: string;
|
|
121
|
-
impact: string;
|
|
122
|
-
};
|
|
123
|
-
"LANG-1": {
|
|
124
|
-
title: string;
|
|
125
|
-
fix: string;
|
|
126
|
-
impact: string;
|
|
127
|
-
};
|
|
128
|
-
"LANG-2": {
|
|
129
|
-
title: string;
|
|
130
|
-
fix: string;
|
|
131
|
-
impact: string;
|
|
132
|
-
};
|
|
133
|
-
"LANG-3": {
|
|
134
|
-
title: string;
|
|
135
|
-
fix: string;
|
|
136
|
-
impact: string;
|
|
137
|
-
};
|
|
138
|
-
"SEC-1": {
|
|
139
|
-
title: string;
|
|
140
|
-
fix: string;
|
|
141
|
-
impact: string;
|
|
142
|
-
};
|
|
143
|
-
"SEC-2": {
|
|
144
|
-
title: string;
|
|
145
|
-
fix: string;
|
|
146
|
-
impact: string;
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
|
-
messages: (lang: Lang) => SeoMessages["m"];
|
|
150
|
-
section: (lang: Lang) => string;
|
|
151
|
-
collect: typeof collect;
|
|
152
|
-
defaults: {
|
|
153
|
-
maxLinks: number;
|
|
154
|
-
};
|
|
155
|
-
};
|
|
156
17
|
export interface AuditSearchOptions {
|
|
157
18
|
base?: string;
|
|
158
19
|
config?: Partial<SearchConfig>;
|
|
@@ -160,8 +21,5 @@ export interface AuditSearchOptions {
|
|
|
160
21
|
urls?: string[];
|
|
161
22
|
log?: CrawlOptions["log"];
|
|
162
23
|
}
|
|
163
|
-
/**
|
|
164
|
-
* One crawl, two rule sets: classic search (this package) and AI search (ai-search-auditor). The report has an overall
|
|
165
|
-
* score and one per part, and renders with ai-search-auditor's toMarkdown.
|
|
166
|
-
*/
|
|
24
|
+
/** Crawls the site, collects what the classic search checks need besides the pages and runs them. */
|
|
167
25
|
export declare function auditSearch(target: string, opts?: AuditSearchOptions): Promise<Report>;
|
package/dist/index.js
CHANGED
|
@@ -1,76 +1,37 @@
|
|
|
1
|
-
import { crawl, defaultConfig as
|
|
1
|
+
import { crawl, defaultConfig as engineDefaults, runRules, scoreOf } from "auditor-engine";
|
|
2
2
|
import { collect } from "./collect.js";
|
|
3
3
|
import { seoMessages } from "./i18n/index.js";
|
|
4
4
|
import { SEO_GROUPS, seoRules } from "./rules/index.js";
|
|
5
5
|
export { SEO_LANGS, seoMessages } from "./i18n/index.js";
|
|
6
6
|
export { SEO_GROUPS, seoRules } from "./rules/index.js";
|
|
7
7
|
export { collect, seoOf } from "./collect.js";
|
|
8
|
-
export { toMarkdown } from "
|
|
8
|
+
export { toMarkdown } from "auditor-engine";
|
|
9
9
|
export const TOOL = { name: "search-auditor", url: "https://github.com/a-bibikov/search-auditor" };
|
|
10
|
-
export const defaultConfig = { ...
|
|
10
|
+
export const defaultConfig = { ...engineDefaults, maxLinks: 100 };
|
|
11
11
|
/** SEO group ids and names in checklist order. */
|
|
12
12
|
export const seoGroups = (lang) => {
|
|
13
13
|
const g = seoMessages(lang).groups;
|
|
14
14
|
return Object.fromEntries(SEO_GROUPS.map((id) => [id, g[id]]));
|
|
15
15
|
};
|
|
16
|
-
/**
|
|
17
|
-
* The SEO checks as a plugin for another runner (ai-search-auditor --seo): everything it needs to run them on its own
|
|
18
|
-
* crawl and render them in its report.
|
|
19
|
-
*/
|
|
20
|
-
export const seoPlugin = {
|
|
21
|
-
name: TOOL.name,
|
|
22
|
-
rules: seoRules,
|
|
23
|
-
groups: seoGroups,
|
|
24
|
-
texts: (lang) => seoMessages(lang).rules,
|
|
25
|
-
messages: (lang) => seoMessages(lang).m,
|
|
26
|
-
section: (lang) => seoMessages(lang).report.classic,
|
|
27
|
-
collect,
|
|
28
|
-
defaults: { maxLinks: defaultConfig.maxLinks },
|
|
29
|
-
};
|
|
30
|
-
/** Same grade scale as ai-search-auditor: an A needs at least two thirds of the checks to have run. */
|
|
31
|
-
function gradeOf(score, coverage) {
|
|
32
|
-
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
|
|
33
|
-
return grade === "A" && coverage < 2 / 3 ? { grade: "B", capped: true } : { grade, capped: false };
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* One crawl, two rule sets: classic search (this package) and AI search (ai-search-auditor). The report has an overall
|
|
37
|
-
* score and one per part, and renders with ai-search-auditor's toMarkdown.
|
|
38
|
-
*/
|
|
16
|
+
/** Crawls the site, collects what the classic search checks need besides the pages and runs them. */
|
|
39
17
|
export async function auditSearch(target, opts = {}) {
|
|
40
18
|
const config = { ...defaultConfig, ...opts.config };
|
|
41
|
-
const
|
|
42
|
-
const at = aiMessages(config.lang);
|
|
19
|
+
const t = seoMessages(config.lang);
|
|
43
20
|
const site = await crawl(target, opts.base, { maxPages: config.maxPages, urls: opts.urls, log: opts.log });
|
|
44
21
|
await collect(site, { maxLinks: config.maxLinks, log: opts.log });
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const parts = [{ name: st.report.classic, run: seo }, ...(ai ? [{ name: st.report.ai, run: ai }] : [])];
|
|
49
|
-
const sections = parts.map(({ name, run }) => {
|
|
50
|
-
const s = scoreOf(run.findings, { pages, checked: run.checked, total: run.total });
|
|
51
|
-
return { name, score: s.score, grade: s.grade, checked: s.checked, total: s.total };
|
|
52
|
-
});
|
|
53
|
-
const findings = parts.flatMap((p) => p.run.findings);
|
|
54
|
-
const checked = parts.reduce((n, p) => n + p.run.checked, 0);
|
|
55
|
-
const total = parts.reduce((n, p) => n + p.run.total, 0);
|
|
56
|
-
// The overall score is the mean of the parts, so two parts at 80 make 80, not the sum of both parts' penalties.
|
|
57
|
-
const overall = scoreOf(findings, { pages, checked, total });
|
|
58
|
-
const score = Math.round(sections.reduce((n, s) => n + s.score, 0) / sections.length);
|
|
59
|
-
const grade = gradeOf(score, total ? checked / total : 1);
|
|
22
|
+
// Findings about sitemap entries or linked pages beyond the sample do not count as a share of the audited pages.
|
|
23
|
+
const audited = site.pages.map((p) => p.fetched.publicUrl);
|
|
24
|
+
const run = runRules(site, seoRules, config, t.m, t.rules);
|
|
60
25
|
return {
|
|
61
26
|
site: site.base,
|
|
62
27
|
lang: config.lang,
|
|
63
28
|
checkedAt: new Date().toISOString(),
|
|
64
|
-
pages:
|
|
65
|
-
...
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
rules: parts.flatMap((p) => p.run.rules),
|
|
71
|
-
groups: { ...seoGroups(config.lang), ...(ai ? at.report.groups : {}) },
|
|
72
|
-
...(ai ? { sections } : {}),
|
|
73
|
-
chrome: { title: st.report.title, clean: st.report.clean, footer: st.report.footer },
|
|
29
|
+
pages: audited,
|
|
30
|
+
...scoreOf(run.findings, { pages: site.pages.length, audited, checked: run.checked, total: run.total }),
|
|
31
|
+
findings: run.findings,
|
|
32
|
+
rules: run.rules,
|
|
33
|
+
groups: seoGroups(config.lang),
|
|
34
|
+
chrome: { title: t.report.title, clean: t.report.clean, footer: t.report.footer },
|
|
74
35
|
tool: TOOL,
|
|
75
36
|
};
|
|
76
37
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** CAN-1: Every page has a canonical. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
export const can1 = {
|
|
4
|
+
id: "CAN-1",
|
|
5
|
+
group: "CAN",
|
|
6
|
+
level: "warn",
|
|
7
|
+
run(site, _c, t) {
|
|
8
|
+
const { out, add } = finder(this);
|
|
9
|
+
for (const p of site.pages)
|
|
10
|
+
if (!p.canonical)
|
|
11
|
+
add(p.fetched.publicUrl, t.noCanonical);
|
|
12
|
+
return out;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** CAN-2: The canonical is an absolute address on this site. */
|
|
2
|
+
import { finder, isSameSite } from "auditor-engine";
|
|
3
|
+
export const can2 = {
|
|
4
|
+
id: "CAN-2",
|
|
5
|
+
group: "CAN",
|
|
6
|
+
level: "warn",
|
|
7
|
+
run(site, _c, t) {
|
|
8
|
+
const { out, add } = finder(this);
|
|
9
|
+
const o = { target: site.target, base: site.base };
|
|
10
|
+
for (const p of site.pages) {
|
|
11
|
+
if (!p.canonical)
|
|
12
|
+
continue;
|
|
13
|
+
if (!/^https?:\/\//i.test(p.canonical))
|
|
14
|
+
add(p.fetched.publicUrl, t.relativeCanonical(p.canonical));
|
|
15
|
+
else if (!isSameSite(p.canonical, o))
|
|
16
|
+
add(p.fetched.publicUrl, t.foreignCanonical(p.canonical));
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** CAN-3: The canonical leads to a page that works. */
|
|
2
|
+
import { finder, isSameSite } from "auditor-engine";
|
|
3
|
+
import { seoOf } from "../../collect.js";
|
|
4
|
+
import { absolute, norm, rel, statusOf } from "../util.js";
|
|
5
|
+
export const can3 = {
|
|
6
|
+
id: "CAN-3",
|
|
7
|
+
group: "CAN",
|
|
8
|
+
level: "error",
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
const { canonicals } = seoOf(site);
|
|
12
|
+
const o = { target: site.target, base: site.base };
|
|
13
|
+
// A canonical pointing at a sampled page is known from the crawl; others were requested in collect().
|
|
14
|
+
const sampled = new Map(site.pages.map((p) => [norm(p.fetched.publicUrl), p.fetched]));
|
|
15
|
+
const failed = new Map(site.failed.map((f) => [norm(f.publicUrl), f]));
|
|
16
|
+
for (const p of site.pages) {
|
|
17
|
+
const href = p.canonical ? absolute(p.canonical, p.fetched.publicUrl) : "";
|
|
18
|
+
if (!href || !isSameSite(href, o))
|
|
19
|
+
continue;
|
|
20
|
+
const key = norm(href);
|
|
21
|
+
const known = sampled.get(key) ?? failed.get(key);
|
|
22
|
+
const probe = known ? { status: known.status, error: known.error, redirected: Boolean(known.redirected), finalUrl: known.finalUrl ?? known.publicUrl } : canonicals.get(key);
|
|
23
|
+
if (!probe)
|
|
24
|
+
continue;
|
|
25
|
+
if (probe.status !== 200)
|
|
26
|
+
add(p.fetched.publicUrl, t.canonicalStatus(rel(href, site.base), statusOf(probe)));
|
|
27
|
+
else if (probe.redirected && norm(probe.finalUrl) !== key)
|
|
28
|
+
add(p.fetched.publicUrl, t.canonicalRedirects(rel(href, site.base), rel(probe.finalUrl, site.base)), "warn");
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** CAN-4: There is one canonical per page. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { canonicalTags } from "./shared.js";
|
|
4
|
+
export const can4 = {
|
|
5
|
+
id: "CAN-4",
|
|
6
|
+
group: "CAN",
|
|
7
|
+
level: "warn",
|
|
8
|
+
run(site, _c, t) {
|
|
9
|
+
const { out, add } = finder(this);
|
|
10
|
+
// Two canonicals that disagree make Google ignore both; the same one twice is untidy but harmless.
|
|
11
|
+
for (const p of site.pages) {
|
|
12
|
+
const hrefs = [...new Set(canonicalTags(p.fetched.body).map((c) => c.href))];
|
|
13
|
+
if (hrefs.length > 1)
|
|
14
|
+
add(p.fetched.publicUrl, t.manyCanonicals(hrefs.length, hrefs.join(", ")));
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** CAN-5: The canonical sits in <head>. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { canonicalTags } from "./shared.js";
|
|
4
|
+
export const can5 = {
|
|
5
|
+
id: "CAN-5",
|
|
6
|
+
group: "CAN",
|
|
7
|
+
level: "warn",
|
|
8
|
+
run(site, _c, t) {
|
|
9
|
+
const { out, add } = finder(this);
|
|
10
|
+
for (const p of site.pages) {
|
|
11
|
+
const inBody = canonicalTags(p.fetched.body).find((c) => !c.inHead);
|
|
12
|
+
if (inBody)
|
|
13
|
+
add(p.fetched.publicUrl, t.canonicalInBody(inBody.href));
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** CAN-6: The canonical in the HTML and in the HTTP header agree. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { absolute, rel } from "../util.js";
|
|
4
|
+
import { headerCanonical } from "./shared.js";
|
|
5
|
+
export const can6 = {
|
|
6
|
+
id: "CAN-6",
|
|
7
|
+
group: "CAN",
|
|
8
|
+
level: "warn",
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
for (const p of site.pages) {
|
|
12
|
+
const header = headerCanonical(p);
|
|
13
|
+
if (!header || !p.canonical)
|
|
14
|
+
continue;
|
|
15
|
+
const fromHeader = absolute(header, p.fetched.publicUrl);
|
|
16
|
+
const fromHtml = absolute(p.canonical, p.fetched.publicUrl);
|
|
17
|
+
if (fromHeader !== fromHtml)
|
|
18
|
+
add(p.fetched.publicUrl, t.canonicalConflict(rel(fromHtml, site.base), rel(fromHeader, site.base)));
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** CAN-7: A canonical leads straight to the main page, not through another canonical. */
|
|
2
|
+
import { finder, isSameSite } from "auditor-engine";
|
|
3
|
+
import { seoOf } from "../../collect.js";
|
|
4
|
+
import { absolute, norm, rel } from "../util.js";
|
|
5
|
+
export const can7 = {
|
|
6
|
+
id: "CAN-7",
|
|
7
|
+
group: "CAN",
|
|
8
|
+
level: "warn",
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
const o = { target: site.target, base: site.base };
|
|
12
|
+
// The canonical each target declares: from the crawl for sampled pages, from collect() for the others.
|
|
13
|
+
const declared = new Map();
|
|
14
|
+
// A target that redirects is CAN-3's finding: what the page after the redirect declares says nothing new.
|
|
15
|
+
for (const p of site.pages)
|
|
16
|
+
if (!p.fetched.redirected)
|
|
17
|
+
declared.set(norm(p.fetched.publicUrl), p.canonical ? absolute(p.canonical, p.fetched.publicUrl) : "");
|
|
18
|
+
for (const [url, probe] of seoOf(site).canonicals)
|
|
19
|
+
if (probe.canonical !== undefined && !probe.redirected)
|
|
20
|
+
declared.set(url, probe.canonical);
|
|
21
|
+
for (const p of site.pages) {
|
|
22
|
+
const self = norm(p.fetched.publicUrl);
|
|
23
|
+
const target = p.canonical ? absolute(p.canonical, p.fetched.publicUrl) : "";
|
|
24
|
+
if (!target || norm(target) === self || !isSameSite(target, o))
|
|
25
|
+
continue;
|
|
26
|
+
const next = declared.get(norm(target));
|
|
27
|
+
if (next && norm(next) !== norm(target) && norm(next) !== self)
|
|
28
|
+
add(p.fetched.publicUrl, t.canonicalChain(rel(target, site.base), rel(next, site.base)));
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** CAN-8: An address with UTM parameters names the clean address as canonical. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { seoOf, UTM } from "../../collect.js";
|
|
4
|
+
import { norm, rel } from "../util.js";
|
|
5
|
+
export const can8 = {
|
|
6
|
+
id: "CAN-8",
|
|
7
|
+
group: "CAN",
|
|
8
|
+
level: "warn",
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
const utm = seoOf(site).utm;
|
|
12
|
+
// A page without any canonical is CAN-1's finding; this one is about a canonical that copies the parameters.
|
|
13
|
+
const main = site.pages.find((p) => p.fetched.publicUrl === utm?.page);
|
|
14
|
+
if (!utm || utm.status !== 200 || !main?.canonical)
|
|
15
|
+
return out;
|
|
16
|
+
const tagged = UTM.split("&")[0];
|
|
17
|
+
if (!utm.canonical)
|
|
18
|
+
add(utm.page, t.utmNoCanonical(rel(utm.url, site.base)));
|
|
19
|
+
else if (utm.canonical.includes(tagged) || norm(utm.canonical) === norm(utm.url))
|
|
20
|
+
add(utm.page, t.utmCanonical(rel(utm.url, site.base)));
|
|
21
|
+
return out;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** CAN-9: A language version does not name another language as canonical. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { absolute, norm, rel } from "../util.js";
|
|
4
|
+
import { langOf } from "./shared.js";
|
|
5
|
+
export const can9 = {
|
|
6
|
+
id: "CAN-9",
|
|
7
|
+
group: "CAN",
|
|
8
|
+
level: "warn",
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
const byUrl = new Map(site.pages.map((p) => [norm(p.fetched.publicUrl), p]));
|
|
12
|
+
for (const p of site.pages) {
|
|
13
|
+
const href = p.canonical ? absolute(p.canonical, p.fetched.publicUrl) : "";
|
|
14
|
+
const target = href ? byUrl.get(norm(href)) : undefined;
|
|
15
|
+
if (!target || target === p)
|
|
16
|
+
continue;
|
|
17
|
+
const [from, to] = [langOf(p), langOf(target)];
|
|
18
|
+
// Translations are separate pages joined by hreflang; a canonical across languages drops this one from search.
|
|
19
|
+
if (from && to && from !== to)
|
|
20
|
+
add(p.fetched.publicUrl, t.canonicalOtherLanguage(rel(href, site.base), to, from));
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical (CAN): does every page name its main address, so search engines rank that one and not a copy.
|
|
3
|
+
* A canonical on every page, one per page and in <head>, an absolute address on this site that answers 200,
|
|
4
|
+
* the same in the HTML and the HTTP header, without chains, ignoring UTM parameters, and never across languages.
|
|
5
|
+
*/
|
|
6
|
+
import type { SeoRule } from "../util.ts";
|
|
7
|
+
export declare const canRules: SeoRule[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { can1 } from "./can-1.js";
|
|
2
|
+
import { can2 } from "./can-2.js";
|
|
3
|
+
import { can3 } from "./can-3.js";
|
|
4
|
+
import { can4 } from "./can-4.js";
|
|
5
|
+
import { can5 } from "./can-5.js";
|
|
6
|
+
import { can6 } from "./can-6.js";
|
|
7
|
+
import { can7 } from "./can-7.js";
|
|
8
|
+
import { can8 } from "./can-8.js";
|
|
9
|
+
import { can9 } from "./can-9.js";
|
|
10
|
+
export const canRules = [can1, can2, can3, can4, can5, can6, can7, can8, can9];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Constants and helpers shared by the CAN checks. */
|
|
2
|
+
import type { Page } from "auditor-engine";
|
|
3
|
+
/** Every <link rel="canonical"> in the raw HTML, with its href and whether it sits in <head>. */
|
|
4
|
+
export declare function canonicalTags(html: string): {
|
|
5
|
+
href: string;
|
|
6
|
+
inHead: boolean;
|
|
7
|
+
}[];
|
|
8
|
+
/** The canonical from the HTTP Link header (`Link: <url>; rel="canonical"`), or "". */
|
|
9
|
+
export declare function headerCanonical(page: Page): string;
|
|
10
|
+
/** The language of a page without the region: «ru» for ru-RU. Empty when the page does not say. */
|
|
11
|
+
export declare const langOf: (p: Page) => string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Every <link rel="canonical"> in the raw HTML, with its href and whether it sits in <head>. */
|
|
2
|
+
export function canonicalTags(html) {
|
|
3
|
+
// Where <head> ends: the closing tag, or the first <body> when the closing tag is left out.
|
|
4
|
+
const end = html.search(/<\/head\s*>|<body[\s>]/i);
|
|
5
|
+
const tags = [];
|
|
6
|
+
for (const m of html.matchAll(/<link\b[^>]*>/gi)) {
|
|
7
|
+
const tag = m[0];
|
|
8
|
+
if (!/\brel\s*=\s*["']?[^"'>]*\bcanonical\b/i.test(tag))
|
|
9
|
+
continue;
|
|
10
|
+
const href = tag.match(/\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i);
|
|
11
|
+
tags.push({ href: (href?.[1] ?? href?.[2] ?? href?.[3] ?? "").trim(), inHead: end < 0 || m.index < end });
|
|
12
|
+
}
|
|
13
|
+
return tags;
|
|
14
|
+
}
|
|
15
|
+
/** The canonical from the HTTP Link header (`Link: <url>; rel="canonical"`), or "". */
|
|
16
|
+
export function headerCanonical(page) {
|
|
17
|
+
const link = page.fetched.headers.get("link") ?? "";
|
|
18
|
+
for (const part of link.split(/,(?=\s*<)/)) {
|
|
19
|
+
const m = part.match(/<([^>]+)>\s*;(.*)/);
|
|
20
|
+
if (m && /\brel\s*=\s*"?canonical"?/i.test(m[2]))
|
|
21
|
+
return m[1].trim();
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
/** The language of a page without the region: «ru» for ru-RU. Empty when the page does not say. */
|
|
26
|
+
export const langOf = (p) => p.htmlLang.trim().toLowerCase().split(/[-_]/)[0] ?? "";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** DATA-1: Structured data has no syntax errors. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { short } from "../util.js";
|
|
4
|
+
export const data1 = {
|
|
5
|
+
id: "DATA-1",
|
|
6
|
+
group: "DATA",
|
|
7
|
+
level: "error",
|
|
8
|
+
applies: (site, t) => (site.pages.some((p) => p.jsonLd.length || p.jsonLdErrors.length) ? null : t.skipNoJsonLd),
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
for (const p of site.pages)
|
|
12
|
+
for (const e of p.jsonLdErrors)
|
|
13
|
+
add(p.fetched.publicUrl, t.jsonLdError(short(e, 100)));
|
|
14
|
+
return out;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** DATA-2: Every block has @context and every entity a @type. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { blocks, isReference, topNodes, typesOf } from "./shared.js";
|
|
4
|
+
export const data2 = {
|
|
5
|
+
id: "DATA-2",
|
|
6
|
+
group: "DATA",
|
|
7
|
+
level: "warn",
|
|
8
|
+
applies: (site, t) => (site.pages.some((p) => p.jsonLd.length) ? null : t.skipNoJsonLd),
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
for (const p of site.pages) {
|
|
12
|
+
for (const block of blocks(p.fetched.body)) {
|
|
13
|
+
const tops = Array.isArray(block) ? block : [block];
|
|
14
|
+
// @context may sit on the block or on each item of an array.
|
|
15
|
+
const context = tops.every((b) => b && typeof b === "object" && /schema\.org/i.test(JSON.stringify(b["@context"] ?? "")));
|
|
16
|
+
if (!context)
|
|
17
|
+
add(p.fetched.publicUrl, t.noContext);
|
|
18
|
+
const untyped = topNodes(block).filter((n) => !isReference(n) && !typesOf(n).length);
|
|
19
|
+
if (untyped.length)
|
|
20
|
+
add(p.fetched.publicUrl, t.noType(untyped.length));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
},
|
|
25
|
+
};
|