@nuasite/checks 0.16.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/dist/types/check-runner.d.ts +16 -0
- package/dist/types/check-runner.d.ts.map +1 -0
- package/dist/types/checks/accessibility/aria-landmarks-check.d.ts +3 -0
- package/dist/types/checks/accessibility/aria-landmarks-check.d.ts.map +1 -0
- package/dist/types/checks/accessibility/form-label-check.d.ts +3 -0
- package/dist/types/checks/accessibility/form-label-check.d.ts.map +1 -0
- package/dist/types/checks/accessibility/index.d.ts +6 -0
- package/dist/types/checks/accessibility/index.d.ts.map +1 -0
- package/dist/types/checks/accessibility/lang-attribute-check.d.ts +3 -0
- package/dist/types/checks/accessibility/lang-attribute-check.d.ts.map +1 -0
- package/dist/types/checks/accessibility/link-text-check.d.ts +3 -0
- package/dist/types/checks/accessibility/link-text-check.d.ts.map +1 -0
- package/dist/types/checks/accessibility/tabindex-check.d.ts +3 -0
- package/dist/types/checks/accessibility/tabindex-check.d.ts.map +1 -0
- package/dist/types/checks/geo/agents-md-check.d.ts.map +1 -0
- package/dist/types/checks/geo/content-quality-check.d.ts +4 -0
- package/dist/types/checks/geo/content-quality-check.d.ts.map +1 -0
- package/dist/types/checks/geo/index.d.ts +3 -0
- package/dist/types/checks/geo/index.d.ts.map +1 -0
- package/dist/types/checks/geo/llms-txt-check.d.ts +3 -0
- package/dist/types/checks/geo/llms-txt-check.d.ts.map +1 -0
- package/dist/types/checks/performance/html-size-check.d.ts +3 -0
- package/dist/types/checks/performance/html-size-check.d.ts.map +1 -0
- package/dist/types/checks/performance/image-optimization-check.d.ts +4 -0
- package/dist/types/checks/performance/image-optimization-check.d.ts.map +1 -0
- package/dist/types/checks/performance/index.d.ts +7 -0
- package/dist/types/checks/performance/index.d.ts.map +1 -0
- package/dist/types/checks/performance/inline-size-check.d.ts +3 -0
- package/dist/types/checks/performance/inline-size-check.d.ts.map +1 -0
- package/dist/types/checks/performance/lazy-loading-check.d.ts +3 -0
- package/dist/types/checks/performance/lazy-loading-check.d.ts.map +1 -0
- package/dist/types/checks/performance/render-blocking-check.d.ts +3 -0
- package/dist/types/checks/performance/render-blocking-check.d.ts.map +1 -0
- package/dist/types/checks/performance/total-requests-check.d.ts +3 -0
- package/dist/types/checks/performance/total-requests-check.d.ts.map +1 -0
- package/dist/types/checks/seo/broken-internal-links-check.d.ts +3 -0
- package/dist/types/checks/seo/broken-internal-links-check.d.ts.map +1 -0
- package/dist/types/checks/seo/canonical-check.d.ts +5 -0
- package/dist/types/checks/seo/canonical-check.d.ts.map +1 -0
- package/dist/types/checks/seo/description-check.d.ts +4 -0
- package/dist/types/checks/seo/description-check.d.ts.map +1 -0
- package/dist/types/checks/seo/heading-hierarchy-check.d.ts +5 -0
- package/dist/types/checks/seo/heading-hierarchy-check.d.ts.map +1 -0
- package/dist/types/checks/seo/image-alt-check.d.ts +3 -0
- package/dist/types/checks/seo/image-alt-check.d.ts.map +1 -0
- package/dist/types/checks/seo/image-alt-quality-check.d.ts +3 -0
- package/dist/types/checks/seo/image-alt-quality-check.d.ts.map +1 -0
- package/dist/types/checks/seo/index.d.ts +15 -0
- package/dist/types/checks/seo/index.d.ts.map +1 -0
- package/dist/types/checks/seo/json-ld-check.d.ts +3 -0
- package/dist/types/checks/seo/json-ld-check.d.ts.map +1 -0
- package/dist/types/checks/seo/meta-duplicates-check.d.ts +3 -0
- package/dist/types/checks/seo/meta-duplicates-check.d.ts.map +1 -0
- package/dist/types/checks/seo/noindex-check.d.ts +3 -0
- package/dist/types/checks/seo/noindex-check.d.ts.map +1 -0
- package/dist/types/checks/seo/open-graph-check.d.ts +5 -0
- package/dist/types/checks/seo/open-graph-check.d.ts.map +1 -0
- package/dist/types/checks/seo/sitemap-robots-check.d.ts +4 -0
- package/dist/types/checks/seo/sitemap-robots-check.d.ts.map +1 -0
- package/dist/types/checks/seo/title-check.d.ts +5 -0
- package/dist/types/checks/seo/title-check.d.ts.map +1 -0
- package/dist/types/checks/seo/twitter-card-check.d.ts +3 -0
- package/dist/types/checks/seo/twitter-card-check.d.ts.map +1 -0
- package/dist/types/checks/seo/viewport-check.d.ts +3 -0
- package/dist/types/checks/seo/viewport-check.d.ts.map +1 -0
- package/dist/types/checks-integration.d.ts +4 -0
- package/dist/types/checks-integration.d.ts.map +1 -0
- package/dist/types/config.d.ts +3 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/html-analyzer.d.ts +11 -0
- package/dist/types/html-analyzer.d.ts.map +1 -0
- package/dist/types/i18n/poor-texts.d.ts +14 -0
- package/dist/types/i18n/poor-texts.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/register.d.ts +4 -0
- package/dist/types/register.d.ts.map +1 -0
- package/dist/types/report.d.ts +5 -0
- package/dist/types/report.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dist/types/types.d.ts +220 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +49 -0
- package/src/check-runner.ts +92 -0
- package/src/checks/accessibility/aria-landmarks-check.ts +23 -0
- package/src/checks/accessibility/form-label-check.ts +29 -0
- package/src/checks/accessibility/index.ts +5 -0
- package/src/checks/accessibility/lang-attribute-check.ts +22 -0
- package/src/checks/accessibility/link-text-check.ts +30 -0
- package/src/checks/accessibility/tabindex-check.ts +28 -0
- package/src/checks/geo/content-quality-check.ts +48 -0
- package/src/checks/geo/index.ts +2 -0
- package/src/checks/geo/llms-txt-check.ts +37 -0
- package/src/checks/performance/html-size-check.ts +26 -0
- package/src/checks/performance/image-optimization-check.ts +82 -0
- package/src/checks/performance/index.ts +6 -0
- package/src/checks/performance/inline-size-check.ts +29 -0
- package/src/checks/performance/lazy-loading-check.ts +29 -0
- package/src/checks/performance/render-blocking-check.ts +27 -0
- package/src/checks/performance/total-requests-check.ts +27 -0
- package/src/checks/seo/broken-internal-links-check.ts +49 -0
- package/src/checks/seo/canonical-check.ts +77 -0
- package/src/checks/seo/description-check.ts +55 -0
- package/src/checks/seo/heading-hierarchy-check.ts +76 -0
- package/src/checks/seo/image-alt-check.ts +28 -0
- package/src/checks/seo/image-alt-quality-check.ts +31 -0
- package/src/checks/seo/index.ts +14 -0
- package/src/checks/seo/json-ld-check.ts +26 -0
- package/src/checks/seo/meta-duplicates-check.ts +44 -0
- package/src/checks/seo/noindex-check.ts +22 -0
- package/src/checks/seo/open-graph-check.ts +55 -0
- package/src/checks/seo/sitemap-robots-check.ts +55 -0
- package/src/checks/seo/title-check.ts +63 -0
- package/src/checks/seo/twitter-card-check.ts +22 -0
- package/src/checks/seo/viewport-check.ts +22 -0
- package/src/checks-integration.ts +126 -0
- package/src/config.ts +27 -0
- package/src/html-analyzer.ts +325 -0
- package/src/i18n/poor-texts.ts +66 -0
- package/src/index.ts +22 -0
- package/src/register.ts +110 -0
- package/src/report.ts +78 -0
- package/src/tsconfig.json +6 -0
- package/src/types.ts +244 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { HTMLElement as ParsedHTMLElement } from 'node-html-parser';
|
|
2
|
+
export type CheckSeverity = 'error' | 'warning' | 'info';
|
|
3
|
+
export type CheckDomain = 'seo' | 'geo' | 'performance' | 'accessibility' | 'ai';
|
|
4
|
+
export type CheckMode = 'auto' | 'full' | 'essential';
|
|
5
|
+
/** Lightweight result returned by a check's run() method. The runner enriches it into a full CheckResult. */
|
|
6
|
+
export interface CheckIssue {
|
|
7
|
+
message: string;
|
|
8
|
+
suggestion?: string;
|
|
9
|
+
pagePath?: string;
|
|
10
|
+
filePath?: string;
|
|
11
|
+
line?: number;
|
|
12
|
+
actual?: string;
|
|
13
|
+
expected?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Issue returned by a site-level check — pagePath is required since there's no single page context. */
|
|
16
|
+
export type SiteCheckIssue = CheckIssue & {
|
|
17
|
+
pagePath: string;
|
|
18
|
+
};
|
|
19
|
+
export interface CheckResult {
|
|
20
|
+
checkId: string;
|
|
21
|
+
ruleName: string;
|
|
22
|
+
domain: CheckDomain;
|
|
23
|
+
severity: CheckSeverity;
|
|
24
|
+
message: string;
|
|
25
|
+
suggestion?: string;
|
|
26
|
+
pagePath: string;
|
|
27
|
+
filePath?: string;
|
|
28
|
+
line?: number;
|
|
29
|
+
actual?: string;
|
|
30
|
+
expected?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Per-page check — runs once for each HTML page */
|
|
33
|
+
export interface Check {
|
|
34
|
+
kind: 'page';
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
domain: CheckDomain;
|
|
38
|
+
defaultSeverity: CheckSeverity;
|
|
39
|
+
description: string;
|
|
40
|
+
/** Whether this check runs in essential mode (true) or only in full mode (false) */
|
|
41
|
+
essential: boolean;
|
|
42
|
+
run(context: PageCheckContext): CheckIssue[] | Promise<CheckIssue[]>;
|
|
43
|
+
}
|
|
44
|
+
/** Site-level check — runs once after all pages are processed */
|
|
45
|
+
export interface SiteCheck {
|
|
46
|
+
kind: 'site';
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
domain: CheckDomain;
|
|
50
|
+
defaultSeverity: CheckSeverity;
|
|
51
|
+
description: string;
|
|
52
|
+
essential: boolean;
|
|
53
|
+
run(context: SiteCheckContext): SiteCheckIssue[] | Promise<SiteCheckIssue[]>;
|
|
54
|
+
}
|
|
55
|
+
export interface PageCheckContext {
|
|
56
|
+
pagePath: string;
|
|
57
|
+
filePath: string;
|
|
58
|
+
distDir: string;
|
|
59
|
+
html: string;
|
|
60
|
+
root: ParsedHTMLElement;
|
|
61
|
+
pageData: ExtractedPageData;
|
|
62
|
+
}
|
|
63
|
+
export interface SiteCheckContext {
|
|
64
|
+
distDir: string;
|
|
65
|
+
projectRoot: string;
|
|
66
|
+
pages: Map<string, ExtractedPageData>;
|
|
67
|
+
siteUrl?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ExtractedPageData {
|
|
70
|
+
title?: {
|
|
71
|
+
content: string;
|
|
72
|
+
line: number;
|
|
73
|
+
};
|
|
74
|
+
metaDescription?: {
|
|
75
|
+
content: string;
|
|
76
|
+
line: number;
|
|
77
|
+
};
|
|
78
|
+
metaTags: MetaTagData[];
|
|
79
|
+
openGraph: Record<string, {
|
|
80
|
+
content: string;
|
|
81
|
+
line: number;
|
|
82
|
+
}>;
|
|
83
|
+
twitterCard: Record<string, {
|
|
84
|
+
content: string;
|
|
85
|
+
line: number;
|
|
86
|
+
}>;
|
|
87
|
+
canonical?: {
|
|
88
|
+
href: string;
|
|
89
|
+
line: number;
|
|
90
|
+
};
|
|
91
|
+
jsonLd: JsonLdData[];
|
|
92
|
+
headings: HeadingData[];
|
|
93
|
+
images: ImageData[];
|
|
94
|
+
links: LinkData[];
|
|
95
|
+
scripts: ScriptData[];
|
|
96
|
+
stylesheets: StylesheetData[];
|
|
97
|
+
forms: ExtractedFormData[];
|
|
98
|
+
htmlLang?: string;
|
|
99
|
+
htmlSize: number;
|
|
100
|
+
bodyTextLength: number;
|
|
101
|
+
hasViewport: boolean;
|
|
102
|
+
hasNoindex: boolean;
|
|
103
|
+
inlineScriptBytes: number;
|
|
104
|
+
inlineStyleBytes: number;
|
|
105
|
+
}
|
|
106
|
+
export interface MetaTagData {
|
|
107
|
+
name?: string;
|
|
108
|
+
property?: string;
|
|
109
|
+
content: string;
|
|
110
|
+
line: number;
|
|
111
|
+
}
|
|
112
|
+
export interface JsonLdData {
|
|
113
|
+
type: string;
|
|
114
|
+
raw: string;
|
|
115
|
+
valid: boolean;
|
|
116
|
+
error?: string;
|
|
117
|
+
line: number;
|
|
118
|
+
}
|
|
119
|
+
export interface HeadingData {
|
|
120
|
+
level: number;
|
|
121
|
+
text: string;
|
|
122
|
+
line: number;
|
|
123
|
+
}
|
|
124
|
+
export interface ImageData {
|
|
125
|
+
src: string;
|
|
126
|
+
alt?: string;
|
|
127
|
+
loading?: string;
|
|
128
|
+
line: number;
|
|
129
|
+
}
|
|
130
|
+
export interface LinkData {
|
|
131
|
+
href: string;
|
|
132
|
+
text: string;
|
|
133
|
+
rel?: string;
|
|
134
|
+
line: number;
|
|
135
|
+
}
|
|
136
|
+
export interface ScriptData {
|
|
137
|
+
src?: string;
|
|
138
|
+
type?: string;
|
|
139
|
+
isAsync: boolean;
|
|
140
|
+
isDefer: boolean;
|
|
141
|
+
isInline: boolean;
|
|
142
|
+
size: number;
|
|
143
|
+
line: number;
|
|
144
|
+
}
|
|
145
|
+
export interface StylesheetData {
|
|
146
|
+
href: string;
|
|
147
|
+
media?: string;
|
|
148
|
+
line: number;
|
|
149
|
+
}
|
|
150
|
+
export interface ExtractedFormData {
|
|
151
|
+
inputs: FormInputData[];
|
|
152
|
+
line: number;
|
|
153
|
+
}
|
|
154
|
+
export interface FormInputData {
|
|
155
|
+
type: string;
|
|
156
|
+
name?: string;
|
|
157
|
+
id?: string;
|
|
158
|
+
hasLabel: boolean;
|
|
159
|
+
line: number;
|
|
160
|
+
}
|
|
161
|
+
export interface ChecksOptions {
|
|
162
|
+
mode?: CheckMode;
|
|
163
|
+
seo?: boolean | SeoChecksConfig;
|
|
164
|
+
geo?: boolean | GeoChecksConfig;
|
|
165
|
+
performance?: boolean | PerformanceChecksConfig;
|
|
166
|
+
accessibility?: boolean | AccessibilityChecksConfig;
|
|
167
|
+
ai?: AiChecksConfig | false;
|
|
168
|
+
failOnError?: boolean;
|
|
169
|
+
failOnWarning?: boolean;
|
|
170
|
+
overrides?: Record<string, CheckSeverity | false>;
|
|
171
|
+
customChecks?: (Check | SiteCheck)[];
|
|
172
|
+
/** Write a JSON report to the dist dir. `true` writes `checks-report.json`, a string sets a custom filename. */
|
|
173
|
+
reportJson?: boolean | string;
|
|
174
|
+
}
|
|
175
|
+
export interface SeoChecksConfig {
|
|
176
|
+
titleMaxLength?: number;
|
|
177
|
+
descriptionMaxLength?: number;
|
|
178
|
+
descriptionMinLength?: number;
|
|
179
|
+
}
|
|
180
|
+
export interface GeoChecksConfig {
|
|
181
|
+
minContentLength?: number;
|
|
182
|
+
minHeadings?: number;
|
|
183
|
+
}
|
|
184
|
+
export interface PerformanceChecksConfig {
|
|
185
|
+
maxHtmlSize?: number;
|
|
186
|
+
maxImageSize?: number;
|
|
187
|
+
allowedImageFormats?: string[];
|
|
188
|
+
maxInlineSize?: number;
|
|
189
|
+
maxExternalRequests?: number;
|
|
190
|
+
}
|
|
191
|
+
export type AccessibilityChecksConfig = {};
|
|
192
|
+
export interface AiChecksConfig {
|
|
193
|
+
apiKey?: string;
|
|
194
|
+
checks?: string[];
|
|
195
|
+
maxPages?: number;
|
|
196
|
+
cache?: boolean;
|
|
197
|
+
}
|
|
198
|
+
export interface ResolvedChecksOptions {
|
|
199
|
+
mode: CheckMode;
|
|
200
|
+
seo: SeoChecksConfig | false;
|
|
201
|
+
geo: GeoChecksConfig | false;
|
|
202
|
+
performance: PerformanceChecksConfig | false;
|
|
203
|
+
accessibility: AccessibilityChecksConfig | false;
|
|
204
|
+
ai: AiChecksConfig | false;
|
|
205
|
+
failOnError: boolean;
|
|
206
|
+
failOnWarning: boolean;
|
|
207
|
+
overrides: Record<string, CheckSeverity | false>;
|
|
208
|
+
customChecks: (Check | SiteCheck)[];
|
|
209
|
+
reportJson: string | false;
|
|
210
|
+
}
|
|
211
|
+
export interface CheckReport {
|
|
212
|
+
totalPages: number;
|
|
213
|
+
totalChecks: number;
|
|
214
|
+
results: CheckResult[];
|
|
215
|
+
errors: CheckResult[];
|
|
216
|
+
warnings: CheckResult[];
|
|
217
|
+
infos: CheckResult[];
|
|
218
|
+
passed: boolean;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nuasite/checks",
|
|
3
|
+
"description": "SEO, GEO, performance, accessibility, and AI validation for Astro sites.",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist/**",
|
|
6
|
+
"src/**",
|
|
7
|
+
"package.json"
|
|
8
|
+
],
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/nuasite/nuasite.git",
|
|
12
|
+
"directory": "packages/checks"
|
|
13
|
+
},
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"version": "0.16.0",
|
|
16
|
+
"module": "src/index.ts",
|
|
17
|
+
"types": "src/index.ts",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./src/index.ts",
|
|
22
|
+
"import": "./src/index.ts",
|
|
23
|
+
"default": "./src/index.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"astro": "^6.0.2",
|
|
28
|
+
"node-html-parser": "^7.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bun": "1.3.10"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"typescript": "^5"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepack": "bun run ../../scripts/workspace-deps/resolve-deps.ts"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"devtools",
|
|
41
|
+
"nuasite",
|
|
42
|
+
"seo",
|
|
43
|
+
"geo",
|
|
44
|
+
"performance",
|
|
45
|
+
"accessibility",
|
|
46
|
+
"validation",
|
|
47
|
+
"withastro"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Check, CheckReport, CheckResult, CheckSeverity, PageCheckContext, ResolvedChecksOptions, SiteCheck, SiteCheckContext } from './types'
|
|
2
|
+
|
|
3
|
+
export class CheckRunner {
|
|
4
|
+
private pageChecks: Check[] = []
|
|
5
|
+
private siteChecks: SiteCheck[] = []
|
|
6
|
+
private overrides: Record<string, CheckSeverity | false>
|
|
7
|
+
private isEssential: boolean
|
|
8
|
+
|
|
9
|
+
constructor(options: ResolvedChecksOptions, isCI: boolean) {
|
|
10
|
+
this.overrides = options.overrides
|
|
11
|
+
this.isEssential = options.mode === 'essential' || (options.mode === 'auto' && !isCI)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
registerCheck(check: Check): void {
|
|
15
|
+
this.pageChecks.push(check)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
registerSiteCheck(check: SiteCheck): void {
|
|
19
|
+
this.siteChecks.push(check)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async runPageChecks(context: PageCheckContext): Promise<CheckResult[]> {
|
|
23
|
+
const results: CheckResult[] = []
|
|
24
|
+
for (const check of this.pageChecks) {
|
|
25
|
+
if (this.isSkipped(check)) continue
|
|
26
|
+
const issues = await check.run(context)
|
|
27
|
+
const enriched: CheckResult[] = issues.map(issue => ({
|
|
28
|
+
checkId: check.id,
|
|
29
|
+
ruleName: check.name,
|
|
30
|
+
domain: check.domain,
|
|
31
|
+
severity: check.defaultSeverity,
|
|
32
|
+
pagePath: context.pagePath,
|
|
33
|
+
filePath: context.filePath,
|
|
34
|
+
...issue,
|
|
35
|
+
}))
|
|
36
|
+
results.push(...this.applyOverrides(check, enriched))
|
|
37
|
+
}
|
|
38
|
+
return results
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async runSiteChecks(context: SiteCheckContext): Promise<CheckResult[]> {
|
|
42
|
+
const results: CheckResult[] = []
|
|
43
|
+
for (const check of this.siteChecks) {
|
|
44
|
+
if (this.isSkipped(check)) continue
|
|
45
|
+
const issues = await check.run(context)
|
|
46
|
+
const enriched: CheckResult[] = issues.map(issue => ({
|
|
47
|
+
checkId: check.id,
|
|
48
|
+
ruleName: check.name,
|
|
49
|
+
domain: check.domain,
|
|
50
|
+
severity: check.defaultSeverity,
|
|
51
|
+
...issue,
|
|
52
|
+
}))
|
|
53
|
+
results.push(...this.applyOverrides(check, enriched))
|
|
54
|
+
}
|
|
55
|
+
return results
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
generateReport(results: CheckResult[], totalPages: number): CheckReport {
|
|
59
|
+
const errors: CheckResult[] = []
|
|
60
|
+
const warnings: CheckResult[] = []
|
|
61
|
+
const infos: CheckResult[] = []
|
|
62
|
+
for (const r of results) {
|
|
63
|
+
if (r.severity === 'error') errors.push(r)
|
|
64
|
+
else if (r.severity === 'warning') warnings.push(r)
|
|
65
|
+
else infos.push(r)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
totalPages,
|
|
70
|
+
totalChecks: this.pageChecks.length + this.siteChecks.length,
|
|
71
|
+
results,
|
|
72
|
+
errors,
|
|
73
|
+
warnings,
|
|
74
|
+
infos,
|
|
75
|
+
passed: errors.length === 0,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private isSkipped(check: Check | SiteCheck): boolean {
|
|
80
|
+
if (this.overrides[check.id] === false) return true
|
|
81
|
+
if (this.isEssential && !check.essential) return true
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private applyOverrides(check: Check | SiteCheck, results: CheckResult[]): CheckResult[] {
|
|
86
|
+
const override = this.overrides[check.id]
|
|
87
|
+
if (override) {
|
|
88
|
+
return results.map(r => ({ ...r, severity: override }))
|
|
89
|
+
}
|
|
90
|
+
return results
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Check, CheckIssue, PageCheckContext } from '../../types'
|
|
2
|
+
|
|
3
|
+
export function createAriaLandmarksCheck(): Check {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'page',
|
|
6
|
+
id: 'accessibility/aria-landmarks',
|
|
7
|
+
name: 'ARIA Landmarks',
|
|
8
|
+
domain: 'accessibility',
|
|
9
|
+
defaultSeverity: 'info',
|
|
10
|
+
description: 'Page should have a <main> landmark element',
|
|
11
|
+
essential: false,
|
|
12
|
+
run(ctx: PageCheckContext): CheckIssue[] {
|
|
13
|
+
const hasMain = ctx.root.querySelector('main') || ctx.root.querySelector('[role="main"]')
|
|
14
|
+
if (!hasMain) {
|
|
15
|
+
return [{
|
|
16
|
+
message: 'Page is missing a <main> landmark element',
|
|
17
|
+
suggestion: 'Wrap the primary content in a <main> element or add role="main" to the content container',
|
|
18
|
+
}]
|
|
19
|
+
}
|
|
20
|
+
return []
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Check, CheckIssue, PageCheckContext } from '../../types'
|
|
2
|
+
|
|
3
|
+
export function createFormLabelCheck(): Check {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'page',
|
|
6
|
+
id: 'accessibility/form-label',
|
|
7
|
+
name: 'Form Labels',
|
|
8
|
+
domain: 'accessibility',
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
description: 'Form inputs must have associated labels',
|
|
11
|
+
essential: true,
|
|
12
|
+
run(ctx: PageCheckContext): CheckIssue[] {
|
|
13
|
+
const results: CheckIssue[] = []
|
|
14
|
+
for (const form of ctx.pageData.forms) {
|
|
15
|
+
for (const input of form.inputs) {
|
|
16
|
+
if (!input.hasLabel) {
|
|
17
|
+
const identifier = input.name ?? input.id ?? input.type
|
|
18
|
+
results.push({
|
|
19
|
+
message: `Input "${identifier}" is missing an associated label`,
|
|
20
|
+
suggestion: 'Add a <label> element with a matching "for" attribute, or wrap the input in a <label>',
|
|
21
|
+
line: input.line,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return results
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createAriaLandmarksCheck } from './aria-landmarks-check'
|
|
2
|
+
export { createFormLabelCheck } from './form-label-check'
|
|
3
|
+
export { createLangAttributeCheck } from './lang-attribute-check'
|
|
4
|
+
export { createLinkTextCheck } from './link-text-check'
|
|
5
|
+
export { createTabindexCheck } from './tabindex-check'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Check, CheckIssue, PageCheckContext } from '../../types'
|
|
2
|
+
|
|
3
|
+
export function createLangAttributeCheck(): Check {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'page',
|
|
6
|
+
id: 'accessibility/lang-attribute',
|
|
7
|
+
name: 'Lang Attribute',
|
|
8
|
+
domain: 'accessibility',
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
description: 'HTML element should have a lang attribute',
|
|
11
|
+
essential: true,
|
|
12
|
+
run(ctx: PageCheckContext): CheckIssue[] {
|
|
13
|
+
if (!ctx.pageData.htmlLang) {
|
|
14
|
+
return [{
|
|
15
|
+
message: 'Page is missing a lang attribute on the <html> element',
|
|
16
|
+
suggestion: 'Add lang="en" (or appropriate language code) to the <html> tag',
|
|
17
|
+
}]
|
|
18
|
+
}
|
|
19
|
+
return []
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { buildPoorTextSet, poorLinkTexts } from '../../i18n/poor-texts'
|
|
2
|
+
import type { Check, CheckIssue, PageCheckContext } from '../../types'
|
|
3
|
+
|
|
4
|
+
export function createLinkTextCheck(): Check {
|
|
5
|
+
return {
|
|
6
|
+
kind: 'page',
|
|
7
|
+
id: 'accessibility/link-text',
|
|
8
|
+
name: 'Descriptive Link Text',
|
|
9
|
+
domain: 'accessibility',
|
|
10
|
+
defaultSeverity: 'info',
|
|
11
|
+
description: 'Links should have descriptive text instead of generic phrases',
|
|
12
|
+
essential: false,
|
|
13
|
+
run(ctx: PageCheckContext): CheckIssue[] {
|
|
14
|
+
const poorTexts = buildPoorTextSet(poorLinkTexts, ctx.pageData.htmlLang)
|
|
15
|
+
const results: CheckIssue[] = []
|
|
16
|
+
for (const link of ctx.pageData.links) {
|
|
17
|
+
const trimmed = link.text.trim()
|
|
18
|
+
if (poorTexts.has(trimmed.toLowerCase())) {
|
|
19
|
+
results.push({
|
|
20
|
+
message: `Link with text "${trimmed}" is not descriptive`,
|
|
21
|
+
suggestion: 'Use descriptive text that explains where the link goes, e.g. "View pricing details" instead of "click here"',
|
|
22
|
+
line: link.line,
|
|
23
|
+
actual: trimmed,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return results
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Check, CheckIssue, PageCheckContext } from '../../types'
|
|
2
|
+
|
|
3
|
+
export function createTabindexCheck(): Check {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'page',
|
|
6
|
+
id: 'accessibility/tabindex',
|
|
7
|
+
name: 'Positive Tabindex',
|
|
8
|
+
domain: 'accessibility',
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
description: 'Elements should not have a positive tabindex value',
|
|
11
|
+
essential: false,
|
|
12
|
+
run(ctx: PageCheckContext): CheckIssue[] {
|
|
13
|
+
const results: CheckIssue[] = []
|
|
14
|
+
const elements = ctx.root.querySelectorAll('[tabindex]')
|
|
15
|
+
for (const el of elements) {
|
|
16
|
+
const tabindex = parseInt(el.getAttribute('tabindex') ?? '0', 10)
|
|
17
|
+
if (tabindex > 0) {
|
|
18
|
+
const tag = el.tagName?.toLowerCase() ?? 'unknown'
|
|
19
|
+
results.push({
|
|
20
|
+
message: `<${tag}> element has tabindex="${tabindex}" which disrupts natural tab order`,
|
|
21
|
+
suggestion: 'Use tabindex="0" to make an element focusable in natural order, or tabindex="-1" for programmatic focus only',
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return results
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Check, CheckIssue, PageCheckContext } from '../../types'
|
|
2
|
+
|
|
3
|
+
export function createContentTooShortCheck(minLength: number): Check {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'page',
|
|
6
|
+
id: 'geo/content-too-short',
|
|
7
|
+
name: 'Content Length',
|
|
8
|
+
domain: 'geo',
|
|
9
|
+
defaultSeverity: 'info',
|
|
10
|
+
description: `Page content should be at least ${minLength} characters`,
|
|
11
|
+
essential: false,
|
|
12
|
+
run(ctx: PageCheckContext): CheckIssue[] {
|
|
13
|
+
const length = ctx.pageData.bodyTextLength
|
|
14
|
+
if (length < minLength) {
|
|
15
|
+
return [{
|
|
16
|
+
message: `Page content is ${length} characters (min: ${minLength})`,
|
|
17
|
+
suggestion: `Add more meaningful content to reach at least ${minLength} characters`,
|
|
18
|
+
actual: `${length} characters`,
|
|
19
|
+
expected: `>= ${minLength} characters`,
|
|
20
|
+
}]
|
|
21
|
+
}
|
|
22
|
+
return []
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createInsufficientHeadingsCheck(minHeadings: number): Check {
|
|
28
|
+
return {
|
|
29
|
+
kind: 'page',
|
|
30
|
+
id: 'geo/insufficient-headings',
|
|
31
|
+
name: 'Heading Count',
|
|
32
|
+
domain: 'geo',
|
|
33
|
+
defaultSeverity: 'info',
|
|
34
|
+
description: `Page should have at least ${minHeadings} headings for structure`,
|
|
35
|
+
essential: false,
|
|
36
|
+
run(ctx: PageCheckContext): CheckIssue[] {
|
|
37
|
+
if (ctx.pageData.headings.length < minHeadings) {
|
|
38
|
+
return [{
|
|
39
|
+
message: `Page has ${ctx.pageData.headings.length} heading(s) (min: ${minHeadings})`,
|
|
40
|
+
suggestion: `Add more headings to improve content structure and LLM comprehension`,
|
|
41
|
+
actual: `${ctx.pageData.headings.length} headings`,
|
|
42
|
+
expected: `>= ${minHeadings} headings`,
|
|
43
|
+
}]
|
|
44
|
+
}
|
|
45
|
+
return []
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { SiteCheck, SiteCheckContext, SiteCheckIssue } from '../../types'
|
|
4
|
+
|
|
5
|
+
export function createLlmsTxtCheck(): SiteCheck {
|
|
6
|
+
return {
|
|
7
|
+
kind: 'site',
|
|
8
|
+
id: 'geo/llms-txt',
|
|
9
|
+
name: 'llms.txt',
|
|
10
|
+
domain: 'geo',
|
|
11
|
+
defaultSeverity: 'warning',
|
|
12
|
+
description: 'Site should have a valid llms.txt file',
|
|
13
|
+
essential: true,
|
|
14
|
+
async run(ctx: SiteCheckContext): Promise<SiteCheckIssue[]> {
|
|
15
|
+
const llmsPath = path.join(ctx.distDir, 'llms.txt')
|
|
16
|
+
let content: string
|
|
17
|
+
try {
|
|
18
|
+
content = await fs.readFile(llmsPath, 'utf-8')
|
|
19
|
+
} catch {
|
|
20
|
+
return [{
|
|
21
|
+
message: 'Site is missing a llms.txt file',
|
|
22
|
+
suggestion: 'Add a llms.txt file to make your site discoverable by LLMs',
|
|
23
|
+
pagePath: '/llms.txt',
|
|
24
|
+
}]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (content.trim().length === 0) {
|
|
28
|
+
return [{
|
|
29
|
+
message: 'llms.txt file is empty',
|
|
30
|
+
suggestion: 'Add content to llms.txt describing your site for LLM consumption',
|
|
31
|
+
pagePath: '/llms.txt',
|
|
32
|
+
}]
|
|
33
|
+
}
|
|
34
|
+
return []
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Check, CheckIssue, PageCheckContext } from '../../types'
|
|
2
|
+
|
|
3
|
+
export function createHtmlSizeCheck(maxSize: number): Check {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'page',
|
|
6
|
+
id: 'performance/html-size',
|
|
7
|
+
name: 'HTML Size',
|
|
8
|
+
domain: 'performance',
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
description: `HTML document should be under ${(maxSize / 1024).toFixed(0)} KB`,
|
|
11
|
+
essential: false,
|
|
12
|
+
run(ctx: PageCheckContext): CheckIssue[] {
|
|
13
|
+
if (ctx.pageData.htmlSize > maxSize) {
|
|
14
|
+
const actualKB = (ctx.pageData.htmlSize / 1024).toFixed(1)
|
|
15
|
+
const maxKB = (maxSize / 1024).toFixed(1)
|
|
16
|
+
return [{
|
|
17
|
+
message: `HTML document is ${actualKB} KB (max: ${maxKB} KB)`,
|
|
18
|
+
suggestion: 'Reduce HTML size by removing unnecessary markup, inlining less CSS, or splitting into multiple pages',
|
|
19
|
+
actual: `${actualKB} KB`,
|
|
20
|
+
expected: `<= ${maxKB} KB`,
|
|
21
|
+
}]
|
|
22
|
+
}
|
|
23
|
+
return []
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
}
|