@rankcli/agent-runtime 0.0.15 → 0.0.17

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 (203) hide show
  1. package/README.md +5 -4
  2. package/dist/index.d.mts +44 -393
  3. package/dist/index.d.ts +44 -393
  4. package/dist/index.js +22098 -26131
  5. package/dist/index.mjs +11834 -15857
  6. package/package.json +10 -4
  7. package/scripts/build-deno.ts +0 -134
  8. package/src/analyzers/core-web-vitals-analyzer.test.ts +0 -236
  9. package/src/analyzers/core-web-vitals-analyzer.ts +0 -557
  10. package/src/analyzers/geo-analyzer.test.ts +0 -310
  11. package/src/analyzers/geo-analyzer.ts +0 -814
  12. package/src/analyzers/image-optimization-analyzer.test.ts +0 -145
  13. package/src/analyzers/image-optimization-analyzer.ts +0 -348
  14. package/src/analyzers/index.ts +0 -233
  15. package/src/analyzers/internal-linking-analyzer.test.ts +0 -141
  16. package/src/analyzers/internal-linking-analyzer.ts +0 -419
  17. package/src/analyzers/mobile-seo-analyzer.test.ts +0 -140
  18. package/src/analyzers/mobile-seo-analyzer.ts +0 -455
  19. package/src/analyzers/security-headers-analyzer.test.ts +0 -115
  20. package/src/analyzers/security-headers-analyzer.ts +0 -318
  21. package/src/analyzers/structured-data-analyzer.test.ts +0 -210
  22. package/src/analyzers/structured-data-analyzer.ts +0 -590
  23. package/src/audit/ai/analyzer.ts +0 -347
  24. package/src/audit/ai/index.ts +0 -29
  25. package/src/audit/ai/prompts/content-analysis.ts +0 -271
  26. package/src/audit/ai/types.ts +0 -179
  27. package/src/audit/checks/additional-checks.ts +0 -439
  28. package/src/audit/checks/agent-experience.ts +0 -108
  29. package/src/audit/checks/ai-citation-worthiness.ts +0 -399
  30. package/src/audit/checks/ai-content-structure.ts +0 -325
  31. package/src/audit/checks/ai-readiness.ts +0 -406
  32. package/src/audit/checks/anchor-text.ts +0 -179
  33. package/src/audit/checks/answer-conciseness.ts +0 -322
  34. package/src/audit/checks/asset-minification.ts +0 -270
  35. package/src/audit/checks/bing-optimization.ts +0 -206
  36. package/src/audit/checks/brand-mention-optimization.ts +0 -349
  37. package/src/audit/checks/caching-headers.ts +0 -305
  38. package/src/audit/checks/canonical-advanced.ts +0 -150
  39. package/src/audit/checks/canonical-domain.ts +0 -196
  40. package/src/audit/checks/citation-quality.ts +0 -358
  41. package/src/audit/checks/client-rendering.ts +0 -542
  42. package/src/audit/checks/color-contrast.ts +0 -342
  43. package/src/audit/checks/content-freshness.ts +0 -170
  44. package/src/audit/checks/content-science.ts +0 -589
  45. package/src/audit/checks/conversion-elements.ts +0 -526
  46. package/src/audit/checks/crawlability.ts +0 -220
  47. package/src/audit/checks/directory-listing.ts +0 -172
  48. package/src/audit/checks/dom-analysis.ts +0 -191
  49. package/src/audit/checks/dom-size.ts +0 -246
  50. package/src/audit/checks/duplicate-content.ts +0 -194
  51. package/src/audit/checks/eeat-signals.ts +0 -990
  52. package/src/audit/checks/entity-seo.ts +0 -396
  53. package/src/audit/checks/featured-snippet.ts +0 -473
  54. package/src/audit/checks/freshness-signals.ts +0 -443
  55. package/src/audit/checks/funnel-intent.ts +0 -463
  56. package/src/audit/checks/hreflang.ts +0 -174
  57. package/src/audit/checks/html-compliance.ts +0 -302
  58. package/src/audit/checks/image-dimensions.ts +0 -167
  59. package/src/audit/checks/images.ts +0 -160
  60. package/src/audit/checks/indexnow.ts +0 -275
  61. package/src/audit/checks/interactive-tools.ts +0 -475
  62. package/src/audit/checks/internal-link-graph.ts +0 -436
  63. package/src/audit/checks/keyword-analysis.ts +0 -239
  64. package/src/audit/checks/keyword-cannibalization.ts +0 -385
  65. package/src/audit/checks/keyword-placement.ts +0 -471
  66. package/src/audit/checks/links.ts +0 -273
  67. package/src/audit/checks/llms-txt.ts +0 -224
  68. package/src/audit/checks/local-seo.ts +0 -296
  69. package/src/audit/checks/mobile.ts +0 -167
  70. package/src/audit/checks/modern-images.ts +0 -226
  71. package/src/audit/checks/navboost-signals.ts +0 -395
  72. package/src/audit/checks/on-page.ts +0 -209
  73. package/src/audit/checks/page-resources.ts +0 -285
  74. package/src/audit/checks/pagination.ts +0 -180
  75. package/src/audit/checks/performance.ts +0 -153
  76. package/src/audit/checks/platform-presence.ts +0 -580
  77. package/src/audit/checks/rag-chunk-readiness.test.ts +0 -159
  78. package/src/audit/checks/rag-chunk-readiness.ts +0 -163
  79. package/src/audit/checks/redirect-analysis.ts +0 -153
  80. package/src/audit/checks/redirect-chain.ts +0 -389
  81. package/src/audit/checks/resource-hints.ts +0 -420
  82. package/src/audit/checks/responsive-css.ts +0 -247
  83. package/src/audit/checks/responsive-images.ts +0 -396
  84. package/src/audit/checks/review-ecosystem.ts +0 -415
  85. package/src/audit/checks/robots-validation.ts +0 -373
  86. package/src/audit/checks/security-headers.ts +0 -188
  87. package/src/audit/checks/security.ts +0 -144
  88. package/src/audit/checks/serp-preview.ts +0 -251
  89. package/src/audit/checks/site-maturity.ts +0 -453
  90. package/src/audit/checks/social-meta.test.ts +0 -275
  91. package/src/audit/checks/social-meta.ts +0 -134
  92. package/src/audit/checks/soft-404.ts +0 -151
  93. package/src/audit/checks/structured-data.ts +0 -238
  94. package/src/audit/checks/tech-detection.ts +0 -496
  95. package/src/audit/checks/topical-clusters.ts +0 -435
  96. package/src/audit/checks/tracker-bloat.ts +0 -462
  97. package/src/audit/checks/tracking-verification.test.ts +0 -371
  98. package/src/audit/checks/tracking-verification.ts +0 -636
  99. package/src/audit/checks/url-safety.ts +0 -682
  100. package/src/audit/deno-entry.ts +0 -66
  101. package/src/audit/discovery/index.ts +0 -15
  102. package/src/audit/discovery/link-crawler.ts +0 -233
  103. package/src/audit/discovery/repo-routes.ts +0 -347
  104. package/src/audit/engine.ts +0 -633
  105. package/src/audit/fixes/index.ts +0 -209
  106. package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
  107. package/src/audit/fixes/social-meta-fixes.ts +0 -463
  108. package/src/audit/index.ts +0 -74
  109. package/src/audit/runner.test.ts +0 -299
  110. package/src/audit/runner.ts +0 -130
  111. package/src/audit/types.ts +0 -1987
  112. package/src/content/featured-snippet.ts +0 -367
  113. package/src/content/generator.test.ts +0 -534
  114. package/src/content/generator.ts +0 -501
  115. package/src/content/headline.ts +0 -317
  116. package/src/content/index.ts +0 -62
  117. package/src/content/intent.ts +0 -258
  118. package/src/content/keyword-density.ts +0 -349
  119. package/src/content/readability.ts +0 -262
  120. package/src/executor.ts +0 -336
  121. package/src/fixer/framework-fixes.test.ts +0 -489
  122. package/src/fixer/framework-fixes.ts +0 -5287
  123. package/src/fixer/index.ts +0 -6
  124. package/src/fixer/schemas.ts +0 -971
  125. package/src/fixer.ts +0 -642
  126. package/src/frameworks/detector.test.ts +0 -248
  127. package/src/frameworks/detector.ts +0 -899
  128. package/src/frameworks/index.ts +0 -68
  129. package/src/frameworks/recipes/angular.yaml +0 -171
  130. package/src/frameworks/recipes/astro.yaml +0 -206
  131. package/src/frameworks/recipes/django.yaml +0 -180
  132. package/src/frameworks/recipes/laravel.yaml +0 -137
  133. package/src/frameworks/recipes/nextjs.yaml +0 -268
  134. package/src/frameworks/recipes/nuxt.yaml +0 -175
  135. package/src/frameworks/recipes/rails.yaml +0 -188
  136. package/src/frameworks/recipes/react.yaml +0 -202
  137. package/src/frameworks/recipes/sveltekit.yaml +0 -154
  138. package/src/frameworks/recipes/vue.yaml +0 -137
  139. package/src/frameworks/recipes/wordpress.yaml +0 -209
  140. package/src/frameworks/suggestion-engine.ts +0 -357
  141. package/src/geo/geo-content.test.ts +0 -305
  142. package/src/geo/geo-content.ts +0 -266
  143. package/src/geo/geo-history.test.ts +0 -473
  144. package/src/geo/geo-history.ts +0 -433
  145. package/src/geo/geo-tracker.test.ts +0 -359
  146. package/src/geo/geo-tracker.ts +0 -411
  147. package/src/geo/index.ts +0 -11
  148. package/src/geo/llm-citation-checker.ts +0 -188
  149. package/src/git/commit-helper.test.ts +0 -261
  150. package/src/git/commit-helper.ts +0 -329
  151. package/src/git/index.ts +0 -12
  152. package/src/git/pr-helper.test.ts +0 -284
  153. package/src/git/pr-helper.ts +0 -307
  154. package/src/index.ts +0 -76
  155. package/src/keywords/ai-keyword-engine.ts +0 -1062
  156. package/src/keywords/ai-summarizer.ts +0 -387
  157. package/src/keywords/ci-mode.ts +0 -555
  158. package/src/keywords/engine.ts +0 -359
  159. package/src/keywords/index.ts +0 -151
  160. package/src/keywords/llm-judge.ts +0 -357
  161. package/src/keywords/nlp-analysis.ts +0 -706
  162. package/src/keywords/prioritizer.ts +0 -295
  163. package/src/keywords/site-crawler.ts +0 -342
  164. package/src/keywords/sources/autocomplete.ts +0 -139
  165. package/src/keywords/sources/competitive-search.ts +0 -450
  166. package/src/keywords/sources/competitor-analysis.ts +0 -374
  167. package/src/keywords/sources/dataforseo.ts +0 -206
  168. package/src/keywords/sources/free-sources.ts +0 -294
  169. package/src/keywords/sources/gsc.ts +0 -123
  170. package/src/keywords/topic-grouping.ts +0 -327
  171. package/src/keywords/types.ts +0 -144
  172. package/src/keywords/wizard.ts +0 -457
  173. package/src/loader.ts +0 -40
  174. package/src/ranking/index.ts +0 -5
  175. package/src/ranking/serp-client.ts +0 -348
  176. package/src/ranking/tracker.ts +0 -380
  177. package/src/ranking/types.ts +0 -123
  178. package/src/reports/index.ts +0 -7
  179. package/src/reports/report-generator.test.ts +0 -293
  180. package/src/reports/report-generator.ts +0 -713
  181. package/src/scheduler/alerts.test.ts +0 -458
  182. package/src/scheduler/alerts.ts +0 -328
  183. package/src/scheduler/index.ts +0 -8
  184. package/src/scheduler/scheduled-audit.test.ts +0 -377
  185. package/src/scheduler/scheduled-audit.ts +0 -149
  186. package/src/test/integration-test.ts +0 -325
  187. package/src/tools/analyzer.ts +0 -373
  188. package/src/tools/crawl.ts +0 -293
  189. package/src/tools/files.ts +0 -301
  190. package/src/tools/h1-fixer.ts +0 -249
  191. package/src/tools/index.ts +0 -67
  192. package/src/tracking/github-action.ts +0 -326
  193. package/src/tracking/google-analytics.ts +0 -265
  194. package/src/tracking/index.ts +0 -45
  195. package/src/tracking/report-generator.ts +0 -386
  196. package/src/tracking/search-console.ts +0 -335
  197. package/src/types.ts +0 -148
  198. package/src/utils/http.ts +0 -302
  199. package/src/wasm-adapter.ts +0 -297
  200. package/src/wasm-entry.ts +0 -14
  201. package/tsconfig.json +0 -17
  202. package/tsup.wasm.config.ts +0 -26
  203. package/vitest.config.ts +0 -15
@@ -1,302 +0,0 @@
1
- /**
2
- * HTML Compliance Checks
3
- *
4
- * Validates basic HTML requirements that all major SEO tools check:
5
- * - Doctype declaration (HTML5)
6
- * - Character encoding (UTF-8)
7
- * - Favicon presence
8
- * - Language attribute
9
- * - Viewport meta tag
10
- */
11
-
12
- import * as cheerio from 'cheerio';
13
- import { httpGet } from '../../utils/http.js';
14
- import type { AuditIssue } from '../types.js';
15
-
16
- export interface HtmlComplianceData {
17
- doctype: {
18
- present: boolean;
19
- isHtml5: boolean;
20
- value?: string;
21
- };
22
- charset: {
23
- present: boolean;
24
- isUtf8: boolean;
25
- value?: string;
26
- source: 'meta' | 'header' | 'none';
27
- };
28
- favicon: {
29
- present: boolean;
30
- locations: string[];
31
- hasAppleTouchIcon: boolean;
32
- formats: string[];
33
- };
34
- language: {
35
- present: boolean;
36
- value?: string;
37
- };
38
- viewport: {
39
- present: boolean;
40
- value?: string;
41
- isResponsive: boolean;
42
- };
43
- }
44
-
45
- export async function analyzeHtmlCompliance(
46
- html: string,
47
- url: string,
48
- headers?: Record<string, string>
49
- ): Promise<{ issues: AuditIssue[]; data: HtmlComplianceData }> {
50
- const issues: AuditIssue[] = [];
51
- const $ = cheerio.load(html);
52
- const parsedUrl = new URL(url);
53
-
54
- // === DOCTYPE CHECK ===
55
- const doctypeMatch = html.match(/<!DOCTYPE\s+([^>]+)>/i);
56
- const hasDoctype = doctypeMatch !== null;
57
- const doctypeValue = doctypeMatch ? doctypeMatch[0] : undefined;
58
- const isHtml5 = hasDoctype && /<!DOCTYPE\s+html\s*>/i.test(html);
59
-
60
- if (!hasDoctype) {
61
- issues.push({
62
- code: 'HTML_NO_DOCTYPE',
63
- severity: 'warning',
64
- category: 'on-page',
65
- title: 'Missing DOCTYPE declaration',
66
- description: 'Page is missing a DOCTYPE declaration. This puts browsers in quirks mode and may cause rendering issues.',
67
- impact: 'Browsers may render the page inconsistently. Search engines expect valid HTML.',
68
- howToFix: 'Add <!DOCTYPE html> as the very first line of your HTML document.',
69
- affectedUrls: [url],
70
- });
71
- } else if (!isHtml5) {
72
- issues.push({
73
- code: 'HTML_OLD_DOCTYPE',
74
- severity: 'notice',
75
- category: 'on-page',
76
- title: 'Using older DOCTYPE',
77
- description: `Page uses "${doctypeValue}" instead of HTML5 DOCTYPE. HTML5 is the current standard.`,
78
- impact: 'Older doctypes may limit modern HTML features.',
79
- howToFix: 'Change your DOCTYPE to the HTML5 declaration: <!DOCTYPE html>',
80
- affectedUrls: [url],
81
- });
82
- }
83
-
84
- // === CHARSET CHECK ===
85
- let charsetPresent = false;
86
- let charsetValue: string | undefined;
87
- let charsetSource: 'meta' | 'header' | 'none' = 'none';
88
-
89
- // Check meta charset
90
- const metaCharset = $('meta[charset]').attr('charset');
91
- const metaContentType = $('meta[http-equiv="Content-Type"]').attr('content');
92
-
93
- if (metaCharset) {
94
- charsetPresent = true;
95
- charsetValue = metaCharset;
96
- charsetSource = 'meta';
97
- } else if (metaContentType) {
98
- const charsetMatch = metaContentType.match(/charset=([^\s;]+)/i);
99
- if (charsetMatch) {
100
- charsetPresent = true;
101
- charsetValue = charsetMatch[1];
102
- charsetSource = 'meta';
103
- }
104
- }
105
-
106
- // Check Content-Type header
107
- if (!charsetPresent && headers) {
108
- const contentType = headers['content-type'] || headers['Content-Type'];
109
- if (contentType) {
110
- const charsetMatch = contentType.match(/charset=([^\s;]+)/i);
111
- if (charsetMatch) {
112
- charsetPresent = true;
113
- charsetValue = charsetMatch[1];
114
- charsetSource = 'header';
115
- }
116
- }
117
- }
118
-
119
- const isUtf8 = charsetValue?.toLowerCase().replace('-', '') === 'utf8';
120
-
121
- if (!charsetPresent) {
122
- issues.push({
123
- code: 'HTML_NO_CHARSET',
124
- severity: 'warning',
125
- category: 'on-page',
126
- title: 'Missing character encoding declaration',
127
- description: 'Page does not specify character encoding. This can cause text display issues.',
128
- impact: 'Characters may display incorrectly. Search engines may misinterpret content.',
129
- howToFix: 'Add <meta charset="UTF-8"> in the <head> section, before any other elements.',
130
- affectedUrls: [url],
131
- });
132
- } else if (!isUtf8) {
133
- issues.push({
134
- code: 'HTML_NOT_UTF8',
135
- severity: 'notice',
136
- category: 'on-page',
137
- title: 'Not using UTF-8 encoding',
138
- description: `Page uses "${charsetValue}" encoding. UTF-8 is recommended for universal character support.`,
139
- impact: 'Some characters may not display correctly. UTF-8 is the web standard.',
140
- howToFix: 'Change your charset to UTF-8: <meta charset="UTF-8">',
141
- affectedUrls: [url],
142
- });
143
- }
144
-
145
- // === FAVICON CHECK ===
146
- const faviconLocations: string[] = [];
147
- const faviconFormats: string[] = [];
148
- let hasAppleTouchIcon = false;
149
-
150
- // Check link tags for favicon
151
- $('link[rel*="icon"]').each((_, el) => {
152
- const href = $(el).attr('href');
153
- const rel = $(el).attr('rel') || '';
154
- const type = $(el).attr('type') || '';
155
-
156
- if (href) {
157
- faviconLocations.push(href);
158
-
159
- // Detect format
160
- if (href.endsWith('.ico')) faviconFormats.push('ico');
161
- else if (href.endsWith('.png')) faviconFormats.push('png');
162
- else if (href.endsWith('.svg')) faviconFormats.push('svg');
163
- else if (type) faviconFormats.push(type);
164
- }
165
-
166
- if (rel.includes('apple-touch-icon')) {
167
- hasAppleTouchIcon = true;
168
- }
169
- });
170
-
171
- // Check for Apple Touch Icon separately
172
- if ($('link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"]').length > 0) {
173
- hasAppleTouchIcon = true;
174
- }
175
-
176
- const hasFavicon = faviconLocations.length > 0;
177
-
178
- if (!hasFavicon) {
179
- // Check if /favicon.ico exists
180
- try {
181
- const faviconUrl = new URL('/favicon.ico', parsedUrl.origin).href;
182
- const response = await httpGet<string>(faviconUrl, {
183
- timeout: 5000,
184
- validateStatus: () => true,
185
- });
186
-
187
- if (response.status === 200) {
188
- faviconLocations.push('/favicon.ico');
189
- faviconFormats.push('ico');
190
- }
191
- } catch {
192
- // Favicon check failed
193
- }
194
- }
195
-
196
- if (faviconLocations.length === 0) {
197
- issues.push({
198
- code: 'HTML_NO_FAVICON',
199
- severity: 'notice',
200
- category: 'on-page',
201
- title: 'No favicon found',
202
- description: 'Page does not have a favicon defined. Favicons help with brand recognition and browser tab identification.',
203
- impact: 'Missing favicon affects brand visibility in browser tabs, bookmarks, and search results.',
204
- howToFix: 'Add a favicon link in <head>: <link rel="icon" href="/favicon.ico"> and consider adding multiple sizes.',
205
- affectedUrls: [url],
206
- details: {
207
- recommendation: 'Include multiple formats: ICO for legacy browsers, PNG for modern browsers, and SVG for scalability.',
208
- },
209
- });
210
- }
211
-
212
- if (!hasAppleTouchIcon && faviconLocations.length > 0) {
213
- issues.push({
214
- code: 'HTML_NO_APPLE_TOUCH_ICON',
215
- severity: 'notice',
216
- category: 'mobile',
217
- title: 'No Apple Touch Icon',
218
- description: 'Page does not define an Apple Touch Icon. This icon is used when users add your site to their iOS home screen.',
219
- impact: 'iOS users who bookmark your site won\'t see a proper app icon.',
220
- howToFix: 'Add: <link rel="apple-touch-icon" href="/apple-touch-icon.png"> with a 180x180 PNG image.',
221
- affectedUrls: [url],
222
- });
223
- }
224
-
225
- // === LANGUAGE CHECK ===
226
- const htmlLang = $('html').attr('lang');
227
- const langPresent = Boolean(htmlLang);
228
-
229
- if (!langPresent) {
230
- issues.push({
231
- code: 'HTML_NO_LANG',
232
- severity: 'warning',
233
- category: 'on-page',
234
- title: 'Missing language attribute',
235
- description: 'The <html> element does not have a lang attribute. This helps search engines and screen readers.',
236
- impact: 'Search engines may not properly identify the page language. Accessibility is reduced.',
237
- howToFix: 'Add lang attribute to <html>: <html lang="en"> (use appropriate language code).',
238
- affectedUrls: [url],
239
- });
240
- }
241
-
242
- // === VIEWPORT CHECK ===
243
- const viewportMeta = $('meta[name="viewport"]').attr('content');
244
- const hasViewport = Boolean(viewportMeta);
245
- const isResponsive = viewportMeta?.includes('width=device-width') || false;
246
-
247
- if (!hasViewport) {
248
- issues.push({
249
- code: 'HTML_NO_VIEWPORT',
250
- severity: 'warning',
251
- category: 'mobile',
252
- title: 'Missing viewport meta tag',
253
- description: 'Page does not have a viewport meta tag. This is required for proper mobile rendering.',
254
- impact: 'Page will not render correctly on mobile devices. Fails mobile-friendliness test.',
255
- howToFix: 'Add: <meta name="viewport" content="width=device-width, initial-scale=1">',
256
- affectedUrls: [url],
257
- });
258
- } else if (!isResponsive) {
259
- issues.push({
260
- code: 'HTML_FIXED_VIEWPORT',
261
- severity: 'notice',
262
- category: 'mobile',
263
- title: 'Viewport not responsive',
264
- description: `Viewport is set to "${viewportMeta}" but doesn't include width=device-width for responsive design.`,
265
- impact: 'Page may not scale properly on different screen sizes.',
266
- howToFix: 'Update viewport to: <meta name="viewport" content="width=device-width, initial-scale=1">',
267
- affectedUrls: [url],
268
- });
269
- }
270
-
271
- return {
272
- issues,
273
- data: {
274
- doctype: {
275
- present: hasDoctype,
276
- isHtml5,
277
- value: doctypeValue,
278
- },
279
- charset: {
280
- present: charsetPresent,
281
- isUtf8,
282
- value: charsetValue,
283
- source: charsetSource,
284
- },
285
- favicon: {
286
- present: faviconLocations.length > 0,
287
- locations: faviconLocations,
288
- hasAppleTouchIcon,
289
- formats: [...new Set(faviconFormats)],
290
- },
291
- language: {
292
- present: langPresent,
293
- value: htmlLang,
294
- },
295
- viewport: {
296
- present: hasViewport,
297
- value: viewportMeta,
298
- isResponsive,
299
- },
300
- },
301
- };
302
- }
@@ -1,167 +0,0 @@
1
- /**
2
- * Image Dimensions Check
3
- *
4
- * Verifies that images have explicit width and height attributes.
5
- * This prevents Cumulative Layout Shift (CLS) - a Core Web Vital.
6
- *
7
- * When images don't have dimensions, the browser doesn't know how much
8
- * space to reserve, causing content to jump when images load.
9
- */
10
-
11
- import * as cheerio from 'cheerio';
12
- import type { AuditIssue } from '../types.js';
13
-
14
- export interface ImageDimensionsData {
15
- totalImages: number;
16
- withDimensions: number;
17
- withoutDimensions: number;
18
- missingDimensions: ImageInfo[];
19
- aspectRatioSet: number;
20
- }
21
-
22
- interface ImageInfo {
23
- src: string;
24
- hasWidth: boolean;
25
- hasHeight: boolean;
26
- hasAspectRatio: boolean;
27
- context: string;
28
- }
29
-
30
- export function analyzeImageDimensions(
31
- html: string,
32
- url: string
33
- ): { issues: AuditIssue[]; data: ImageDimensionsData } {
34
- const issues: AuditIssue[] = [];
35
- const $ = cheerio.load(html);
36
-
37
- const images = $('img');
38
- const totalImages = images.length;
39
-
40
- let withDimensions = 0;
41
- let withoutDimensions = 0;
42
- let aspectRatioSet = 0;
43
- const missingDimensions: ImageInfo[] = [];
44
-
45
- images.each((_, img) => {
46
- const $img = $(img);
47
- const src = $img.attr('src') || $img.attr('data-src') || '';
48
-
49
- // Check for width attribute (HTML attribute or style)
50
- const widthAttr = $img.attr('width');
51
- const heightAttr = $img.attr('height');
52
- const style = $img.attr('style') || '';
53
-
54
- const hasWidthAttr = Boolean(widthAttr);
55
- const hasHeightAttr = Boolean(heightAttr);
56
- const hasStyleWidth = /width\s*:/i.test(style);
57
- const hasStyleHeight = /height\s*:/i.test(style);
58
- const hasAspectRatio = /aspect-ratio\s*:/i.test(style);
59
-
60
- // Check CSS classes that might set dimensions
61
- const className = $img.attr('class') || '';
62
-
63
- // Consider dimensions set if either HTML attributes or inline styles are present
64
- const hasWidth = hasWidthAttr || hasStyleWidth;
65
- const hasHeight = hasHeightAttr || hasStyleHeight;
66
- const hasBothDimensions = (hasWidth && hasHeight) || hasAspectRatio;
67
-
68
- if (hasBothDimensions) {
69
- withDimensions++;
70
- if (hasAspectRatio) {
71
- aspectRatioSet++;
72
- }
73
- } else {
74
- withoutDimensions++;
75
-
76
- // Only report visible images (not lazy-loaded placeholder images)
77
- if (src && !src.includes('data:image/') && !src.includes('placeholder')) {
78
- // Get context (parent element for identification)
79
- const parent = $img.parent();
80
- const parentTag = parent.length > 0 ? (parent[0] as any).name || '' : '';
81
- const parentClass = parent.attr('class')?.split(' ')[0] || '';
82
- const context = parentClass ? `${parentTag}.${parentClass}` : parentTag;
83
-
84
- missingDimensions.push({
85
- src: truncateSrc(src),
86
- hasWidth,
87
- hasHeight,
88
- hasAspectRatio,
89
- context,
90
- });
91
- }
92
- }
93
- });
94
-
95
- // Generate issues
96
- if (withoutDimensions > 0) {
97
- // Calculate percentage
98
- const percentage = Math.round((withoutDimensions / totalImages) * 100);
99
-
100
- if (percentage > 50) {
101
- issues.push({
102
- code: 'IMAGES_MANY_MISSING_DIMENSIONS',
103
- severity: 'warning',
104
- category: 'performance',
105
- title: 'Most images missing explicit dimensions',
106
- description: `${withoutDimensions} of ${totalImages} images (${percentage}%) don't have width and height attributes. This causes layout shifts.`,
107
- impact: 'High Cumulative Layout Shift (CLS). Content jumps around as images load, frustrating users.',
108
- howToFix: 'Add width and height attributes to all <img> tags. Use CSS aspect-ratio for responsive images. Example: <img src="..." width="800" height="600">',
109
- affectedUrls: [url],
110
- details: {
111
- totalImages,
112
- withDimensions,
113
- withoutDimensions,
114
- examples: missingDimensions.slice(0, 5),
115
- },
116
- });
117
- } else if (withoutDimensions > 0) {
118
- issues.push({
119
- code: 'IMAGES_MISSING_DIMENSIONS',
120
- severity: 'notice',
121
- category: 'performance',
122
- title: 'Some images missing explicit dimensions',
123
- description: `${withoutDimensions} of ${totalImages} image(s) don't have width and height attributes.`,
124
- impact: 'May cause layout shifts when images load, affecting CLS score.',
125
- howToFix: 'Add width and height attributes: <img src="..." width="800" height="600">. For responsive images, also use CSS: img { max-width: 100%; height: auto; }',
126
- affectedUrls: [url],
127
- details: {
128
- images: missingDimensions.slice(0, 10),
129
- },
130
- });
131
- }
132
- }
133
-
134
- // Check for aspect-ratio usage (modern approach)
135
- if (withDimensions > 0 && aspectRatioSet === 0) {
136
- issues.push({
137
- code: 'IMAGES_NO_ASPECT_RATIO',
138
- severity: 'notice',
139
- category: 'performance',
140
- title: 'Consider using CSS aspect-ratio for images',
141
- description: 'Images have width/height but no CSS aspect-ratio. The aspect-ratio property provides better responsive behavior.',
142
- impact: 'Minor - existing dimensions prevent CLS, but aspect-ratio offers better control.',
143
- howToFix: 'Add CSS aspect-ratio for responsive images: img { aspect-ratio: 16/9; width: 100%; height: auto; }',
144
- affectedUrls: [url],
145
- });
146
- }
147
-
148
- return {
149
- issues,
150
- data: {
151
- totalImages,
152
- withDimensions,
153
- withoutDimensions,
154
- missingDimensions: missingDimensions.slice(0, 20),
155
- aspectRatioSet,
156
- },
157
- };
158
- }
159
-
160
- function truncateSrc(src: string): string {
161
- // Remove query strings and truncate long URLs
162
- const cleanSrc = src.split('?')[0];
163
- if (cleanSrc.length > 60) {
164
- return cleanSrc.substring(0, 30) + '...' + cleanSrc.substring(cleanSrc.length - 25);
165
- }
166
- return cleanSrc;
167
- }
@@ -1,160 +0,0 @@
1
- import * as cheerio from 'cheerio';
2
- import { httpHead } from '../../utils/http.js';
3
- import type { AuditIssue } from '../types.js';
4
- import { ISSUE_DEFINITIONS } from '../types.js';
5
-
6
- export interface ImageData {
7
- total: number;
8
- missingAlt: { src: string }[];
9
- emptyAlt: { src: string }[];
10
- missingDimensions: { src: string }[];
11
- largImages: { src: string; size?: number }[];
12
- brokenImages: { src: string }[];
13
- lazyLoaded: number;
14
- withDimensions: number;
15
- }
16
-
17
- export async function analyzeImages(
18
- html: string,
19
- baseUrl: string,
20
- checkBroken: boolean = false
21
- ): Promise<{ issues: AuditIssue[]; data: ImageData }> {
22
- const issues: AuditIssue[] = [];
23
- const $ = cheerio.load(html);
24
-
25
- const missingAlt: ImageData['missingAlt'] = [];
26
- const emptyAlt: ImageData['emptyAlt'] = [];
27
- const missingDimensions: ImageData['missingDimensions'] = [];
28
- const largeImages: ImageData['largImages'] = [];
29
- const brokenImages: ImageData['brokenImages'] = [];
30
- let lazyLoaded = 0;
31
- let withDimensions = 0;
32
- let total = 0;
33
-
34
- const images: { src: string; alt: string | null; width: string | undefined; height: string | undefined; loading: string | undefined }[] = [];
35
-
36
- $('img').each((_, el) => {
37
- const src = $(el).attr('src') || $(el).attr('data-src') || '';
38
- const alt = $(el).attr('alt');
39
- const width = $(el).attr('width');
40
- const height = $(el).attr('height');
41
- const loading = $(el).attr('loading');
42
-
43
- if (!src || src.startsWith('data:')) return; // Skip data URIs
44
-
45
- total++;
46
- images.push({ src, alt: alt === undefined ? null : alt, width, height, loading });
47
-
48
- // Check alt text
49
- if (alt === undefined || alt === null) {
50
- missingAlt.push({ src });
51
- } else if (alt === '') {
52
- emptyAlt.push({ src });
53
- }
54
-
55
- // Check dimensions
56
- if (!width || !height) {
57
- missingDimensions.push({ src });
58
- } else {
59
- withDimensions++;
60
- }
61
-
62
- // Check lazy loading
63
- if (loading === 'lazy') {
64
- lazyLoaded++;
65
- }
66
- });
67
-
68
- // Check for broken images and large images (limited)
69
- if (checkBroken) {
70
- const imagesToCheck = images.slice(0, 10);
71
- for (const img of imagesToCheck) {
72
- try {
73
- const fullUrl = new URL(img.src, baseUrl).href;
74
- const response = await httpHead(fullUrl, {
75
- timeout: 5000,
76
- validateStatus: () => true,
77
- });
78
-
79
- if (response.status >= 400) {
80
- brokenImages.push({ src: img.src });
81
- } else {
82
- // Check file size
83
- const contentLength = parseInt(response.headers['content-length'] || '0', 10);
84
- if (contentLength > 200 * 1024) { // > 200KB
85
- largeImages.push({ src: img.src, size: contentLength });
86
- }
87
- }
88
- } catch {
89
- // Don't mark as broken for network errors
90
- }
91
- }
92
- }
93
-
94
- // Generate issues
95
- if (missingAlt.length > 0) {
96
- issues.push({
97
- ...ISSUE_DEFINITIONS.IMG_ALT_MISSING,
98
- affectedUrls: [baseUrl],
99
- details: {
100
- count: missingAlt.length,
101
- images: missingAlt.slice(0, 5).map(i => i.src),
102
- },
103
- });
104
- }
105
-
106
- if (emptyAlt.length > 0) {
107
- issues.push({
108
- ...ISSUE_DEFINITIONS.IMG_ALT_EMPTY,
109
- affectedUrls: [baseUrl],
110
- details: {
111
- count: emptyAlt.length,
112
- images: emptyAlt.slice(0, 5).map(i => i.src),
113
- },
114
- });
115
- }
116
-
117
- if (missingDimensions.length > 0) {
118
- issues.push({
119
- ...ISSUE_DEFINITIONS.IMG_NO_DIMENSIONS,
120
- affectedUrls: [baseUrl],
121
- details: {
122
- count: missingDimensions.length,
123
- images: missingDimensions.slice(0, 5).map(i => i.src),
124
- },
125
- });
126
- }
127
-
128
- for (const img of largeImages) {
129
- issues.push({
130
- ...ISSUE_DEFINITIONS.IMG_TOO_LARGE,
131
- affectedUrls: [baseUrl],
132
- details: {
133
- src: img.src,
134
- size: img.size ? `${(img.size / 1024).toFixed(0)}KB` : 'Unknown',
135
- },
136
- });
137
- }
138
-
139
- for (const img of brokenImages) {
140
- issues.push({
141
- ...ISSUE_DEFINITIONS.IMG_BROKEN,
142
- affectedUrls: [baseUrl],
143
- details: { src: img.src },
144
- });
145
- }
146
-
147
- return {
148
- issues,
149
- data: {
150
- total,
151
- missingAlt,
152
- emptyAlt,
153
- missingDimensions,
154
- largImages: largeImages,
155
- brokenImages,
156
- lazyLoaded,
157
- withDimensions,
158
- }
159
- };
160
- }