@rankcli/agent-runtime 0.0.14 → 0.0.16

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 (199) hide show
  1. package/dist/index.d.mts +77 -393
  2. package/dist/index.d.ts +77 -393
  3. package/dist/index.js +417 -6443
  4. package/dist/index.mjs +296 -6313
  5. package/package.json +6 -1
  6. package/scripts/build-deno.ts +0 -134
  7. package/src/analyzers/core-web-vitals-analyzer.test.ts +0 -236
  8. package/src/analyzers/core-web-vitals-analyzer.ts +0 -557
  9. package/src/analyzers/geo-analyzer.test.ts +0 -310
  10. package/src/analyzers/geo-analyzer.ts +0 -814
  11. package/src/analyzers/image-optimization-analyzer.test.ts +0 -145
  12. package/src/analyzers/image-optimization-analyzer.ts +0 -348
  13. package/src/analyzers/index.ts +0 -233
  14. package/src/analyzers/internal-linking-analyzer.test.ts +0 -141
  15. package/src/analyzers/internal-linking-analyzer.ts +0 -419
  16. package/src/analyzers/mobile-seo-analyzer.test.ts +0 -140
  17. package/src/analyzers/mobile-seo-analyzer.ts +0 -455
  18. package/src/analyzers/security-headers-analyzer.test.ts +0 -115
  19. package/src/analyzers/security-headers-analyzer.ts +0 -318
  20. package/src/analyzers/structured-data-analyzer.test.ts +0 -210
  21. package/src/analyzers/structured-data-analyzer.ts +0 -590
  22. package/src/audit/ai/analyzer.ts +0 -347
  23. package/src/audit/ai/index.ts +0 -29
  24. package/src/audit/ai/prompts/content-analysis.ts +0 -271
  25. package/src/audit/ai/types.ts +0 -179
  26. package/src/audit/checks/additional-checks.ts +0 -439
  27. package/src/audit/checks/ai-citation-worthiness.ts +0 -399
  28. package/src/audit/checks/ai-content-structure.ts +0 -325
  29. package/src/audit/checks/ai-readiness.ts +0 -339
  30. package/src/audit/checks/anchor-text.ts +0 -179
  31. package/src/audit/checks/answer-conciseness.ts +0 -322
  32. package/src/audit/checks/asset-minification.ts +0 -270
  33. package/src/audit/checks/bing-optimization.ts +0 -206
  34. package/src/audit/checks/brand-mention-optimization.ts +0 -349
  35. package/src/audit/checks/caching-headers.ts +0 -305
  36. package/src/audit/checks/canonical-advanced.ts +0 -150
  37. package/src/audit/checks/canonical-domain.ts +0 -196
  38. package/src/audit/checks/citation-quality.ts +0 -358
  39. package/src/audit/checks/client-rendering.ts +0 -542
  40. package/src/audit/checks/color-contrast.ts +0 -342
  41. package/src/audit/checks/content-freshness.ts +0 -170
  42. package/src/audit/checks/content-science.ts +0 -589
  43. package/src/audit/checks/conversion-elements.ts +0 -526
  44. package/src/audit/checks/crawlability.ts +0 -220
  45. package/src/audit/checks/directory-listing.ts +0 -172
  46. package/src/audit/checks/dom-analysis.ts +0 -191
  47. package/src/audit/checks/dom-size.ts +0 -246
  48. package/src/audit/checks/duplicate-content.ts +0 -194
  49. package/src/audit/checks/eeat-signals.ts +0 -990
  50. package/src/audit/checks/entity-seo.ts +0 -396
  51. package/src/audit/checks/featured-snippet.ts +0 -473
  52. package/src/audit/checks/freshness-signals.ts +0 -443
  53. package/src/audit/checks/funnel-intent.ts +0 -463
  54. package/src/audit/checks/hreflang.ts +0 -174
  55. package/src/audit/checks/html-compliance.ts +0 -302
  56. package/src/audit/checks/image-dimensions.ts +0 -167
  57. package/src/audit/checks/images.ts +0 -160
  58. package/src/audit/checks/indexnow.ts +0 -275
  59. package/src/audit/checks/interactive-tools.ts +0 -475
  60. package/src/audit/checks/internal-link-graph.ts +0 -436
  61. package/src/audit/checks/keyword-analysis.ts +0 -239
  62. package/src/audit/checks/keyword-cannibalization.ts +0 -385
  63. package/src/audit/checks/keyword-placement.ts +0 -471
  64. package/src/audit/checks/links.ts +0 -273
  65. package/src/audit/checks/llms-txt.ts +0 -224
  66. package/src/audit/checks/local-seo.ts +0 -296
  67. package/src/audit/checks/mobile.ts +0 -167
  68. package/src/audit/checks/modern-images.ts +0 -226
  69. package/src/audit/checks/navboost-signals.ts +0 -395
  70. package/src/audit/checks/on-page.ts +0 -209
  71. package/src/audit/checks/page-resources.ts +0 -285
  72. package/src/audit/checks/pagination.ts +0 -180
  73. package/src/audit/checks/performance.ts +0 -153
  74. package/src/audit/checks/platform-presence.ts +0 -580
  75. package/src/audit/checks/redirect-analysis.ts +0 -153
  76. package/src/audit/checks/redirect-chain.ts +0 -389
  77. package/src/audit/checks/resource-hints.ts +0 -420
  78. package/src/audit/checks/responsive-css.ts +0 -247
  79. package/src/audit/checks/responsive-images.ts +0 -396
  80. package/src/audit/checks/review-ecosystem.ts +0 -415
  81. package/src/audit/checks/robots-validation.ts +0 -373
  82. package/src/audit/checks/security-headers.ts +0 -188
  83. package/src/audit/checks/security.ts +0 -144
  84. package/src/audit/checks/serp-preview.ts +0 -251
  85. package/src/audit/checks/site-maturity.ts +0 -453
  86. package/src/audit/checks/social-meta.test.ts +0 -275
  87. package/src/audit/checks/social-meta.ts +0 -134
  88. package/src/audit/checks/soft-404.ts +0 -151
  89. package/src/audit/checks/structured-data.ts +0 -238
  90. package/src/audit/checks/tech-detection.ts +0 -496
  91. package/src/audit/checks/topical-clusters.ts +0 -435
  92. package/src/audit/checks/tracker-bloat.ts +0 -462
  93. package/src/audit/checks/tracking-verification.test.ts +0 -371
  94. package/src/audit/checks/tracking-verification.ts +0 -636
  95. package/src/audit/checks/url-safety.ts +0 -682
  96. package/src/audit/deno-entry.ts +0 -66
  97. package/src/audit/discovery/index.ts +0 -15
  98. package/src/audit/discovery/link-crawler.ts +0 -233
  99. package/src/audit/discovery/repo-routes.ts +0 -347
  100. package/src/audit/engine.ts +0 -627
  101. package/src/audit/fixes/index.ts +0 -209
  102. package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
  103. package/src/audit/fixes/social-meta-fixes.ts +0 -463
  104. package/src/audit/index.ts +0 -74
  105. package/src/audit/runner.test.ts +0 -299
  106. package/src/audit/runner.ts +0 -130
  107. package/src/audit/types.ts +0 -1963
  108. package/src/content/featured-snippet.ts +0 -367
  109. package/src/content/generator.test.ts +0 -534
  110. package/src/content/generator.ts +0 -501
  111. package/src/content/headline.ts +0 -317
  112. package/src/content/index.ts +0 -62
  113. package/src/content/intent.ts +0 -258
  114. package/src/content/keyword-density.ts +0 -349
  115. package/src/content/readability.ts +0 -262
  116. package/src/executor.ts +0 -336
  117. package/src/fixer/framework-fixes.test.ts +0 -489
  118. package/src/fixer/framework-fixes.ts +0 -5287
  119. package/src/fixer/index.ts +0 -6
  120. package/src/fixer/schemas.ts +0 -971
  121. package/src/fixer.ts +0 -642
  122. package/src/frameworks/detector.test.ts +0 -248
  123. package/src/frameworks/detector.ts +0 -899
  124. package/src/frameworks/index.ts +0 -68
  125. package/src/frameworks/recipes/angular.yaml +0 -171
  126. package/src/frameworks/recipes/astro.yaml +0 -206
  127. package/src/frameworks/recipes/django.yaml +0 -180
  128. package/src/frameworks/recipes/laravel.yaml +0 -137
  129. package/src/frameworks/recipes/nextjs.yaml +0 -268
  130. package/src/frameworks/recipes/nuxt.yaml +0 -175
  131. package/src/frameworks/recipes/rails.yaml +0 -188
  132. package/src/frameworks/recipes/react.yaml +0 -202
  133. package/src/frameworks/recipes/sveltekit.yaml +0 -154
  134. package/src/frameworks/recipes/vue.yaml +0 -137
  135. package/src/frameworks/recipes/wordpress.yaml +0 -209
  136. package/src/frameworks/suggestion-engine.ts +0 -357
  137. package/src/geo/geo-content.test.ts +0 -305
  138. package/src/geo/geo-content.ts +0 -266
  139. package/src/geo/geo-history.test.ts +0 -473
  140. package/src/geo/geo-history.ts +0 -433
  141. package/src/geo/geo-tracker.test.ts +0 -359
  142. package/src/geo/geo-tracker.ts +0 -411
  143. package/src/geo/index.ts +0 -11
  144. package/src/geo/llm-citation-checker.ts +0 -188
  145. package/src/git/commit-helper.test.ts +0 -261
  146. package/src/git/commit-helper.ts +0 -329
  147. package/src/git/index.ts +0 -12
  148. package/src/git/pr-helper.test.ts +0 -284
  149. package/src/git/pr-helper.ts +0 -307
  150. package/src/index.ts +0 -76
  151. package/src/keywords/ai-keyword-engine.ts +0 -1062
  152. package/src/keywords/ai-summarizer.ts +0 -387
  153. package/src/keywords/ci-mode.ts +0 -555
  154. package/src/keywords/engine.ts +0 -359
  155. package/src/keywords/index.ts +0 -151
  156. package/src/keywords/llm-judge.ts +0 -357
  157. package/src/keywords/nlp-analysis.ts +0 -706
  158. package/src/keywords/prioritizer.ts +0 -295
  159. package/src/keywords/site-crawler.ts +0 -342
  160. package/src/keywords/sources/autocomplete.ts +0 -139
  161. package/src/keywords/sources/competitive-search.ts +0 -450
  162. package/src/keywords/sources/competitor-analysis.ts +0 -374
  163. package/src/keywords/sources/dataforseo.ts +0 -206
  164. package/src/keywords/sources/free-sources.ts +0 -294
  165. package/src/keywords/sources/gsc.ts +0 -123
  166. package/src/keywords/topic-grouping.ts +0 -327
  167. package/src/keywords/types.ts +0 -144
  168. package/src/keywords/wizard.ts +0 -457
  169. package/src/loader.ts +0 -40
  170. package/src/ranking/index.ts +0 -5
  171. package/src/ranking/serp-client.ts +0 -348
  172. package/src/ranking/tracker.ts +0 -380
  173. package/src/ranking/types.ts +0 -123
  174. package/src/reports/index.ts +0 -7
  175. package/src/reports/report-generator.test.ts +0 -293
  176. package/src/reports/report-generator.ts +0 -713
  177. package/src/scheduler/alerts.test.ts +0 -458
  178. package/src/scheduler/alerts.ts +0 -328
  179. package/src/scheduler/index.ts +0 -8
  180. package/src/scheduler/scheduled-audit.test.ts +0 -377
  181. package/src/scheduler/scheduled-audit.ts +0 -149
  182. package/src/test/integration-test.ts +0 -325
  183. package/src/tools/analyzer.ts +0 -373
  184. package/src/tools/crawl.ts +0 -293
  185. package/src/tools/files.ts +0 -301
  186. package/src/tools/h1-fixer.ts +0 -249
  187. package/src/tools/index.ts +0 -67
  188. package/src/tracking/github-action.ts +0 -326
  189. package/src/tracking/google-analytics.ts +0 -265
  190. package/src/tracking/index.ts +0 -45
  191. package/src/tracking/report-generator.ts +0 -386
  192. package/src/tracking/search-console.ts +0 -335
  193. package/src/types.ts +0 -148
  194. package/src/utils/http.ts +0 -302
  195. package/src/wasm-adapter.ts +0 -297
  196. package/src/wasm-entry.ts +0 -14
  197. package/tsconfig.json +0 -17
  198. package/tsup.wasm.config.ts +0 -26
  199. 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
- }