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