@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.
Files changed (124) hide show
  1. package/dist/types/check-runner.d.ts +16 -0
  2. package/dist/types/check-runner.d.ts.map +1 -0
  3. package/dist/types/checks/accessibility/aria-landmarks-check.d.ts +3 -0
  4. package/dist/types/checks/accessibility/aria-landmarks-check.d.ts.map +1 -0
  5. package/dist/types/checks/accessibility/form-label-check.d.ts +3 -0
  6. package/dist/types/checks/accessibility/form-label-check.d.ts.map +1 -0
  7. package/dist/types/checks/accessibility/index.d.ts +6 -0
  8. package/dist/types/checks/accessibility/index.d.ts.map +1 -0
  9. package/dist/types/checks/accessibility/lang-attribute-check.d.ts +3 -0
  10. package/dist/types/checks/accessibility/lang-attribute-check.d.ts.map +1 -0
  11. package/dist/types/checks/accessibility/link-text-check.d.ts +3 -0
  12. package/dist/types/checks/accessibility/link-text-check.d.ts.map +1 -0
  13. package/dist/types/checks/accessibility/tabindex-check.d.ts +3 -0
  14. package/dist/types/checks/accessibility/tabindex-check.d.ts.map +1 -0
  15. package/dist/types/checks/geo/agents-md-check.d.ts.map +1 -0
  16. package/dist/types/checks/geo/content-quality-check.d.ts +4 -0
  17. package/dist/types/checks/geo/content-quality-check.d.ts.map +1 -0
  18. package/dist/types/checks/geo/index.d.ts +3 -0
  19. package/dist/types/checks/geo/index.d.ts.map +1 -0
  20. package/dist/types/checks/geo/llms-txt-check.d.ts +3 -0
  21. package/dist/types/checks/geo/llms-txt-check.d.ts.map +1 -0
  22. package/dist/types/checks/performance/html-size-check.d.ts +3 -0
  23. package/dist/types/checks/performance/html-size-check.d.ts.map +1 -0
  24. package/dist/types/checks/performance/image-optimization-check.d.ts +4 -0
  25. package/dist/types/checks/performance/image-optimization-check.d.ts.map +1 -0
  26. package/dist/types/checks/performance/index.d.ts +7 -0
  27. package/dist/types/checks/performance/index.d.ts.map +1 -0
  28. package/dist/types/checks/performance/inline-size-check.d.ts +3 -0
  29. package/dist/types/checks/performance/inline-size-check.d.ts.map +1 -0
  30. package/dist/types/checks/performance/lazy-loading-check.d.ts +3 -0
  31. package/dist/types/checks/performance/lazy-loading-check.d.ts.map +1 -0
  32. package/dist/types/checks/performance/render-blocking-check.d.ts +3 -0
  33. package/dist/types/checks/performance/render-blocking-check.d.ts.map +1 -0
  34. package/dist/types/checks/performance/total-requests-check.d.ts +3 -0
  35. package/dist/types/checks/performance/total-requests-check.d.ts.map +1 -0
  36. package/dist/types/checks/seo/broken-internal-links-check.d.ts +3 -0
  37. package/dist/types/checks/seo/broken-internal-links-check.d.ts.map +1 -0
  38. package/dist/types/checks/seo/canonical-check.d.ts +5 -0
  39. package/dist/types/checks/seo/canonical-check.d.ts.map +1 -0
  40. package/dist/types/checks/seo/description-check.d.ts +4 -0
  41. package/dist/types/checks/seo/description-check.d.ts.map +1 -0
  42. package/dist/types/checks/seo/heading-hierarchy-check.d.ts +5 -0
  43. package/dist/types/checks/seo/heading-hierarchy-check.d.ts.map +1 -0
  44. package/dist/types/checks/seo/image-alt-check.d.ts +3 -0
  45. package/dist/types/checks/seo/image-alt-check.d.ts.map +1 -0
  46. package/dist/types/checks/seo/image-alt-quality-check.d.ts +3 -0
  47. package/dist/types/checks/seo/image-alt-quality-check.d.ts.map +1 -0
  48. package/dist/types/checks/seo/index.d.ts +15 -0
  49. package/dist/types/checks/seo/index.d.ts.map +1 -0
  50. package/dist/types/checks/seo/json-ld-check.d.ts +3 -0
  51. package/dist/types/checks/seo/json-ld-check.d.ts.map +1 -0
  52. package/dist/types/checks/seo/meta-duplicates-check.d.ts +3 -0
  53. package/dist/types/checks/seo/meta-duplicates-check.d.ts.map +1 -0
  54. package/dist/types/checks/seo/noindex-check.d.ts +3 -0
  55. package/dist/types/checks/seo/noindex-check.d.ts.map +1 -0
  56. package/dist/types/checks/seo/open-graph-check.d.ts +5 -0
  57. package/dist/types/checks/seo/open-graph-check.d.ts.map +1 -0
  58. package/dist/types/checks/seo/sitemap-robots-check.d.ts +4 -0
  59. package/dist/types/checks/seo/sitemap-robots-check.d.ts.map +1 -0
  60. package/dist/types/checks/seo/title-check.d.ts +5 -0
  61. package/dist/types/checks/seo/title-check.d.ts.map +1 -0
  62. package/dist/types/checks/seo/twitter-card-check.d.ts +3 -0
  63. package/dist/types/checks/seo/twitter-card-check.d.ts.map +1 -0
  64. package/dist/types/checks/seo/viewport-check.d.ts +3 -0
  65. package/dist/types/checks/seo/viewport-check.d.ts.map +1 -0
  66. package/dist/types/checks-integration.d.ts +4 -0
  67. package/dist/types/checks-integration.d.ts.map +1 -0
  68. package/dist/types/config.d.ts +3 -0
  69. package/dist/types/config.d.ts.map +1 -0
  70. package/dist/types/html-analyzer.d.ts +11 -0
  71. package/dist/types/html-analyzer.d.ts.map +1 -0
  72. package/dist/types/i18n/poor-texts.d.ts +14 -0
  73. package/dist/types/i18n/poor-texts.d.ts.map +1 -0
  74. package/dist/types/index.d.ts +4 -0
  75. package/dist/types/index.d.ts.map +1 -0
  76. package/dist/types/register.d.ts +4 -0
  77. package/dist/types/register.d.ts.map +1 -0
  78. package/dist/types/report.d.ts +5 -0
  79. package/dist/types/report.d.ts.map +1 -0
  80. package/dist/types/tsconfig.tsbuildinfo +1 -0
  81. package/dist/types/types.d.ts +220 -0
  82. package/dist/types/types.d.ts.map +1 -0
  83. package/package.json +49 -0
  84. package/src/check-runner.ts +92 -0
  85. package/src/checks/accessibility/aria-landmarks-check.ts +23 -0
  86. package/src/checks/accessibility/form-label-check.ts +29 -0
  87. package/src/checks/accessibility/index.ts +5 -0
  88. package/src/checks/accessibility/lang-attribute-check.ts +22 -0
  89. package/src/checks/accessibility/link-text-check.ts +30 -0
  90. package/src/checks/accessibility/tabindex-check.ts +28 -0
  91. package/src/checks/geo/content-quality-check.ts +48 -0
  92. package/src/checks/geo/index.ts +2 -0
  93. package/src/checks/geo/llms-txt-check.ts +37 -0
  94. package/src/checks/performance/html-size-check.ts +26 -0
  95. package/src/checks/performance/image-optimization-check.ts +82 -0
  96. package/src/checks/performance/index.ts +6 -0
  97. package/src/checks/performance/inline-size-check.ts +29 -0
  98. package/src/checks/performance/lazy-loading-check.ts +29 -0
  99. package/src/checks/performance/render-blocking-check.ts +27 -0
  100. package/src/checks/performance/total-requests-check.ts +27 -0
  101. package/src/checks/seo/broken-internal-links-check.ts +49 -0
  102. package/src/checks/seo/canonical-check.ts +77 -0
  103. package/src/checks/seo/description-check.ts +55 -0
  104. package/src/checks/seo/heading-hierarchy-check.ts +76 -0
  105. package/src/checks/seo/image-alt-check.ts +28 -0
  106. package/src/checks/seo/image-alt-quality-check.ts +31 -0
  107. package/src/checks/seo/index.ts +14 -0
  108. package/src/checks/seo/json-ld-check.ts +26 -0
  109. package/src/checks/seo/meta-duplicates-check.ts +44 -0
  110. package/src/checks/seo/noindex-check.ts +22 -0
  111. package/src/checks/seo/open-graph-check.ts +55 -0
  112. package/src/checks/seo/sitemap-robots-check.ts +55 -0
  113. package/src/checks/seo/title-check.ts +63 -0
  114. package/src/checks/seo/twitter-card-check.ts +22 -0
  115. package/src/checks/seo/viewport-check.ts +22 -0
  116. package/src/checks-integration.ts +126 -0
  117. package/src/config.ts +27 -0
  118. package/src/html-analyzer.ts +325 -0
  119. package/src/i18n/poor-texts.ts +66 -0
  120. package/src/index.ts +22 -0
  121. package/src/register.ts +110 -0
  122. package/src/report.ts +78 -0
  123. package/src/tsconfig.json +6 -0
  124. 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,2 @@
1
+ export { createContentTooShortCheck, createInsufficientHeadingsCheck } from './content-quality-check'
2
+ export { createLlmsTxtCheck } from './llms-txt-check'
@@ -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
+ }