@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,420 +0,0 @@
1
- // Resource Hints Optimization Checks
2
- // Analyzes dns-prefetch, preconnect, prefetch, preload, modulepreload, and early hints
3
- // Based on advanced performance SEO research
4
-
5
- import * as cheerio from 'cheerio';
6
- import { httpGet } from '../../utils/http.js';
7
- import type { AuditIssue } from '../types.js';
8
-
9
- export interface ResourceHint {
10
- type: 'dns-prefetch' | 'preconnect' | 'prefetch' | 'preload' | 'modulepreload';
11
- href: string;
12
- as?: string;
13
- crossorigin?: string;
14
- }
15
-
16
- export interface ResourceHintsData {
17
- dnsPrefetch: string[];
18
- preconnect: string[];
19
- prefetch: string[];
20
- preload: ResourceHint[];
21
- modulepreload: string[];
22
- thirdPartyDomains: string[];
23
- missingPreconnects: string[];
24
- earlyHints: boolean;
25
- http2Push: boolean;
26
- }
27
-
28
- /**
29
- * Extract all resource hints from HTML
30
- */
31
- export function extractResourceHints(html: string): ResourceHint[] {
32
- const $ = cheerio.load(html);
33
- const hints: ResourceHint[] = [];
34
-
35
- // dns-prefetch
36
- $('link[rel="dns-prefetch"]').each((_, el) => {
37
- const href = $(el).attr('href');
38
- if (href) {
39
- hints.push({ type: 'dns-prefetch', href });
40
- }
41
- });
42
-
43
- // preconnect
44
- $('link[rel="preconnect"]').each((_, el) => {
45
- const href = $(el).attr('href');
46
- if (href) {
47
- hints.push({
48
- type: 'preconnect',
49
- href,
50
- crossorigin: $(el).attr('crossorigin'),
51
- });
52
- }
53
- });
54
-
55
- // prefetch
56
- $('link[rel="prefetch"]').each((_, el) => {
57
- const href = $(el).attr('href');
58
- if (href) {
59
- hints.push({
60
- type: 'prefetch',
61
- href,
62
- as: $(el).attr('as'),
63
- });
64
- }
65
- });
66
-
67
- // preload
68
- $('link[rel="preload"]').each((_, el) => {
69
- const href = $(el).attr('href');
70
- if (href) {
71
- hints.push({
72
- type: 'preload',
73
- href,
74
- as: $(el).attr('as'),
75
- crossorigin: $(el).attr('crossorigin'),
76
- });
77
- }
78
- });
79
-
80
- // modulepreload
81
- $('link[rel="modulepreload"]').each((_, el) => {
82
- const href = $(el).attr('href');
83
- if (href) {
84
- hints.push({ type: 'modulepreload', href });
85
- }
86
- });
87
-
88
- return hints;
89
- }
90
-
91
- /**
92
- * Detect third-party domains used on the page
93
- */
94
- export function detectThirdPartyDomains(html: string, pageUrl: string): string[] {
95
- const $ = cheerio.load(html);
96
- const baseDomain = new URL(pageUrl).hostname;
97
- const domains = new Set<string>();
98
-
99
- // Check scripts
100
- $('script[src]').each((_, el) => {
101
- try {
102
- const src = $(el).attr('src')!;
103
- const url = new URL(src, pageUrl);
104
- if (url.hostname !== baseDomain) {
105
- domains.add(url.origin);
106
- }
107
- } catch {
108
- // Invalid URL
109
- }
110
- });
111
-
112
- // Check stylesheets
113
- $('link[rel="stylesheet"][href]').each((_, el) => {
114
- try {
115
- const href = $(el).attr('href')!;
116
- const url = new URL(href, pageUrl);
117
- if (url.hostname !== baseDomain) {
118
- domains.add(url.origin);
119
- }
120
- } catch {
121
- // Invalid URL
122
- }
123
- });
124
-
125
- // Check images
126
- $('img[src]').each((_, el) => {
127
- try {
128
- const src = $(el).attr('src')!;
129
- const url = new URL(src, pageUrl);
130
- if (url.hostname !== baseDomain) {
131
- domains.add(url.origin);
132
- }
133
- } catch {
134
- // Invalid URL
135
- }
136
- });
137
-
138
- // Check fonts (from @font-face in style tags - simplified)
139
- const styleContent = $('style').text();
140
- const fontUrlMatches = styleContent.match(/url\(['"]?(https?:\/\/[^'")\s]+)/g) || [];
141
- for (const match of fontUrlMatches) {
142
- try {
143
- const url = match.replace(/url\(['"]?/, '');
144
- const parsed = new URL(url);
145
- if (parsed.hostname !== baseDomain) {
146
- domains.add(parsed.origin);
147
- }
148
- } catch {
149
- // Invalid URL
150
- }
151
- }
152
-
153
- return [...domains];
154
- }
155
-
156
- /**
157
- * Common third-party services that should have preconnect
158
- */
159
- const CRITICAL_THIRD_PARTIES: Record<string, string[]> = {
160
- 'fonts.googleapis.com': ['https://fonts.googleapis.com', 'https://fonts.gstatic.com'],
161
- 'fonts.gstatic.com': ['https://fonts.googleapis.com', 'https://fonts.gstatic.com'],
162
- 'www.google-analytics.com': ['https://www.google-analytics.com'],
163
- 'www.googletagmanager.com': ['https://www.googletagmanager.com'],
164
- 'cdn.jsdelivr.net': ['https://cdn.jsdelivr.net'],
165
- 'unpkg.com': ['https://unpkg.com'],
166
- 'cdnjs.cloudflare.com': ['https://cdnjs.cloudflare.com'],
167
- 'ajax.googleapis.com': ['https://ajax.googleapis.com'],
168
- 'connect.facebook.net': ['https://connect.facebook.net'],
169
- 'platform.twitter.com': ['https://platform.twitter.com'],
170
- 'www.youtube.com': ['https://www.youtube.com', 'https://i.ytimg.com'],
171
- 'player.vimeo.com': ['https://player.vimeo.com'],
172
- 'js.stripe.com': ['https://js.stripe.com'],
173
- };
174
-
175
- /**
176
- * Find missing preconnects for critical third-party resources
177
- */
178
- export function findMissingPreconnects(
179
- thirdPartyDomains: string[],
180
- existingHints: ResourceHint[]
181
- ): string[] {
182
- const preconnected = new Set(
183
- existingHints
184
- .filter((h) => h.type === 'preconnect' || h.type === 'dns-prefetch')
185
- .map((h) => {
186
- try {
187
- return new URL(h.href).origin;
188
- } catch {
189
- return h.href;
190
- }
191
- })
192
- );
193
-
194
- const missing: string[] = [];
195
-
196
- for (const domain of thirdPartyDomains) {
197
- try {
198
- const hostname = new URL(domain).hostname;
199
- const recommended = CRITICAL_THIRD_PARTIES[hostname] || [domain];
200
-
201
- for (const rec of recommended) {
202
- if (!preconnected.has(rec)) {
203
- missing.push(rec);
204
- }
205
- }
206
- } catch {
207
- // Invalid URL
208
- }
209
- }
210
-
211
- return [...new Set(missing)];
212
- }
213
-
214
- /**
215
- * Check for HTTP/2 Server Push and 103 Early Hints
216
- */
217
- export async function checkAdvancedHints(
218
- url: string
219
- ): Promise<{ earlyHints: boolean; http2Push: boolean; linkHeaders: string[] }> {
220
- try {
221
- const response = await httpGet<string>(url, {
222
-
223
- timeout: 10000,
224
- validateStatus: () => true,
225
- // Note: fetch doesn't expose HTTP/2 details natively, this is simplified
226
- });
227
-
228
- const linkHeader = response.headers['link'] || '';
229
- const linkHeaders = Array.isArray(linkHeader) ? linkHeader : [linkHeader];
230
-
231
- // Check for preload hints in Link header
232
- const hasPreloadLinks = linkHeaders.some(
233
- (h) => h.includes('rel=preload') || h.includes("rel='preload'") || h.includes('rel="preload"')
234
- );
235
-
236
- return {
237
- earlyHints: false, // Would need special handling to detect 103
238
- http2Push: hasPreloadLinks,
239
- linkHeaders: linkHeaders.filter(Boolean),
240
- };
241
- } catch {
242
- return { earlyHints: false, http2Push: false, linkHeaders: [] };
243
- }
244
- }
245
-
246
- /**
247
- * Analyze preload usage for critical resources
248
- */
249
- export function analyzePreloads(
250
- html: string,
251
- preloads: ResourceHint[]
252
- ): { missing: string[]; unused: string[] } {
253
- const $ = cheerio.load(html);
254
- const criticalResources: string[] = [];
255
-
256
- // LCP image candidates (large hero images)
257
- $('img').each((i, el) => {
258
- if (i === 0) {
259
- // First image is often LCP
260
- const src = $(el).attr('src');
261
- if (src) criticalResources.push(src);
262
- }
263
- });
264
-
265
- // Critical fonts
266
- $('link[rel="stylesheet"][href*="font"]').each((_, el) => {
267
- const href = $(el).attr('href');
268
- if (href) criticalResources.push(href);
269
- });
270
-
271
- // Check for missing preloads
272
- const preloadedUrls = new Set(preloads.map((p) => p.href));
273
- const missing = criticalResources.filter((r) => !preloadedUrls.has(r));
274
-
275
- // Check for unused preloads (preloaded but not used)
276
- // This would need runtime analysis, so we'll do a basic check
277
- const pageContent = $.html();
278
- const unused = preloads
279
- .map((p) => p.href)
280
- .filter((href) => !pageContent.includes(href.split('/').pop() || ''));
281
-
282
- return { missing: missing.slice(0, 3), unused };
283
- }
284
-
285
- /**
286
- * Main function: Analyze resource hints optimization
287
- */
288
- export function analyzeResourceHints(
289
- html: string,
290
- url: string
291
- ): { issues: AuditIssue[]; data: ResourceHintsData } {
292
- const issues: AuditIssue[] = [];
293
-
294
- const hints = extractResourceHints(html);
295
- const thirdPartyDomains = detectThirdPartyDomains(html, url);
296
- const missingPreconnects = findMissingPreconnects(thirdPartyDomains, hints);
297
-
298
- const dnsPrefetch = hints.filter((h) => h.type === 'dns-prefetch').map((h) => h.href);
299
- const preconnect = hints.filter((h) => h.type === 'preconnect').map((h) => h.href);
300
- const prefetch = hints.filter((h) => h.type === 'prefetch').map((h) => h.href);
301
- const preload = hints.filter((h) => h.type === 'preload');
302
- const modulepreload = hints.filter((h) => h.type === 'modulepreload').map((h) => h.href);
303
-
304
- // Check for missing preconnects
305
- if (missingPreconnects.length > 0) {
306
- issues.push({
307
- code: 'MISSING_PRECONNECT',
308
- severity: 'warning',
309
- category: 'performance',
310
- title: 'Missing preconnect hints for third-party origins',
311
- description: `${missingPreconnects.length} third-party origins used without preconnect hints.`,
312
- impact: 'Connection setup to third-party origins adds latency to resource loading.',
313
- howToFix: `Add preconnect hints: ${missingPreconnects.slice(0, 3).map((d) => `<link rel="preconnect" href="${d}">`).join(' ')}`,
314
- affectedUrls: [url],
315
- details: { missingPreconnects: missingPreconnects.slice(0, 5) },
316
- });
317
- }
318
-
319
- // Check if Google Fonts are used without proper hints
320
- const usesGoogleFonts = thirdPartyDomains.some(
321
- (d) => d.includes('fonts.googleapis.com') || d.includes('fonts.gstatic.com')
322
- );
323
- const hasGoogleFontsPreconnect =
324
- preconnect.some((h) => h.includes('fonts.googleapis.com')) &&
325
- preconnect.some((h) => h.includes('fonts.gstatic.com'));
326
-
327
- if (usesGoogleFonts && !hasGoogleFontsPreconnect) {
328
- issues.push({
329
- code: 'GOOGLE_FONTS_NO_PRECONNECT',
330
- severity: 'warning',
331
- category: 'performance',
332
- title: 'Google Fonts without preconnect',
333
- description: 'Google Fonts are used but preconnect hints are missing.',
334
- impact: 'Adds significant latency to font loading, affecting LCP.',
335
- howToFix:
336
- 'Add: <link rel="preconnect" href="https://fonts.googleapis.com"> and <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
337
- affectedUrls: [url],
338
- });
339
- }
340
-
341
- // Check for LCP image preload
342
- const $ = cheerio.load(html);
343
- const hasHeroImage = $('img').first().length > 0;
344
- const hasImagePreload = preload.some((p) => p.as === 'image');
345
-
346
- if (hasHeroImage && !hasImagePreload) {
347
- issues.push({
348
- code: 'HERO_IMAGE_NOT_PRELOADED',
349
- severity: 'notice',
350
- category: 'performance',
351
- title: 'Hero image not preloaded',
352
- description: 'Page has images but none are preloaded for faster LCP.',
353
- impact: 'Preloading LCP images can improve Largest Contentful Paint.',
354
- howToFix: 'Add <link rel="preload" href="hero-image.jpg" as="image"> for your LCP image.',
355
- affectedUrls: [url],
356
- });
357
- }
358
-
359
- // Check for critical CSS preload
360
- const hasRenderBlockingCSS =
361
- $('link[rel="stylesheet"]').filter((_, el) => !$(el).attr('media') || $(el).attr('media') === 'all').length > 0;
362
- const hasCSSPreload = preload.some((p) => p.as === 'style');
363
-
364
- if (hasRenderBlockingCSS && !hasCSSPreload && preload.length === 0) {
365
- issues.push({
366
- code: 'NO_CRITICAL_RESOURCE_PRELOAD',
367
- severity: 'notice',
368
- category: 'performance',
369
- title: 'No critical resources preloaded',
370
- description: 'Page has render-blocking resources but no preload hints.',
371
- impact: 'Preloading critical resources can improve initial render time.',
372
- howToFix: 'Consider preloading critical CSS, fonts, or hero images.',
373
- affectedUrls: [url],
374
- });
375
- }
376
-
377
- // Check for excessive preloads (can hurt performance)
378
- if (preload.length > 10) {
379
- issues.push({
380
- code: 'EXCESSIVE_PRELOADS',
381
- severity: 'warning',
382
- category: 'performance',
383
- title: 'Too many preload hints',
384
- description: `Page has ${preload.length} preload hints, which can degrade performance.`,
385
- impact: 'Preloading too many resources competes for bandwidth and can delay critical resources.',
386
- howToFix: 'Limit preloads to truly critical resources (2-5 maximum).',
387
- affectedUrls: [url],
388
- });
389
- }
390
-
391
- // Check for modulepreload for ES modules
392
- const hasESModules = $('script[type="module"]').length > 0;
393
- if (hasESModules && modulepreload.length === 0) {
394
- issues.push({
395
- code: 'ES_MODULES_NO_MODULEPRELOAD',
396
- severity: 'notice',
397
- category: 'performance',
398
- title: 'ES modules without modulepreload',
399
- description: 'Page uses ES modules but no modulepreload hints are present.',
400
- impact: 'Modulepreload can parallelize module loading for faster execution.',
401
- howToFix: 'Add <link rel="modulepreload" href="critical-module.js"> for critical modules.',
402
- affectedUrls: [url],
403
- });
404
- }
405
-
406
- return {
407
- issues,
408
- data: {
409
- dnsPrefetch,
410
- preconnect,
411
- prefetch,
412
- preload,
413
- modulepreload,
414
- thirdPartyDomains,
415
- missingPreconnects,
416
- earlyHints: false,
417
- http2Push: false,
418
- },
419
- };
420
- }
@@ -1,247 +0,0 @@
1
- /**
2
- * Responsive CSS Check
3
- *
4
- * Checks if the site uses responsive design through CSS media queries.
5
- * Responsive design is essential for:
6
- * - Mobile-first indexing (Google uses mobile version for ranking)
7
- * - User experience across devices
8
- * - Core Web Vitals on mobile
9
- *
10
- * Detection methods:
11
- * - @media queries in inline styles
12
- * - @media queries in linked stylesheets
13
- * - Viewport meta tag (required for responsiveness)
14
- */
15
-
16
- import { httpGet } from '../../utils/http.js';
17
- import * as cheerio from 'cheerio';
18
- import type { AuditIssue } from '../types.js';
19
-
20
- export interface ResponsiveCssData {
21
- hasViewport: boolean;
22
- viewportContent?: string;
23
- hasMediaQueries: boolean;
24
- mediaQueryCount: number;
25
- mediaQuerySources: {
26
- inline: number;
27
- external: string[];
28
- };
29
- breakpoints: string[];
30
- hasMobileBreakpoint: boolean;
31
- hasTabletBreakpoint: boolean;
32
- hasDesktopBreakpoint: boolean;
33
- }
34
-
35
- /**
36
- * Extract media queries from CSS content
37
- */
38
- function extractMediaQueries(css: string): string[] {
39
- const mediaQueries: string[] = [];
40
- // Match @media queries
41
- const regex = /@media\s*([^{]+)/g;
42
- let match;
43
- while ((match = regex.exec(css)) !== null) {
44
- mediaQueries.push(match[1].trim());
45
- }
46
- return mediaQueries;
47
- }
48
-
49
- /**
50
- * Classify breakpoints
51
- */
52
- function classifyBreakpoints(mediaQueries: string[]): {
53
- hasMobile: boolean;
54
- hasTablet: boolean;
55
- hasDesktop: boolean;
56
- breakpoints: string[];
57
- } {
58
- const breakpoints = new Set<string>();
59
- let hasMobile = false;
60
- let hasTablet = false;
61
- let hasDesktop = false;
62
-
63
- for (const query of mediaQueries) {
64
- // Extract width values from media queries
65
- const widthMatches = query.match(/(?:min|max)-width\s*:\s*(\d+)(?:px|em|rem)?/gi);
66
- if (widthMatches) {
67
- for (const match of widthMatches) {
68
- const valueMatch = match.match(/(\d+)/);
69
- if (valueMatch) {
70
- const value = parseInt(valueMatch[1], 10);
71
- breakpoints.add(`${value}px`);
72
-
73
- // Classify breakpoint
74
- if (value <= 480) {
75
- hasMobile = true;
76
- } else if (value <= 768) {
77
- hasMobile = true; // Small tablet / large phone
78
- } else if (value <= 1024) {
79
- hasTablet = true;
80
- } else {
81
- hasDesktop = true;
82
- }
83
- }
84
- }
85
- }
86
-
87
- // Check for common responsive patterns
88
- if (/screen\s+and/i.test(query)) {
89
- // Has screen media type - indicates responsive design
90
- }
91
-
92
- // Check for print media (not responsive but valid)
93
- if (/print/i.test(query)) {
94
- continue; // Don't count print as responsive
95
- }
96
- }
97
-
98
- return {
99
- hasMobile,
100
- hasTablet,
101
- hasDesktop,
102
- breakpoints: Array.from(breakpoints).sort((a, b) => parseInt(a) - parseInt(b)),
103
- };
104
- }
105
-
106
- /**
107
- * Analyze responsive CSS
108
- */
109
- export async function analyzeResponsiveCss(
110
- html: string,
111
- url: string
112
- ): Promise<{ issues: AuditIssue[]; data: ResponsiveCssData }> {
113
- const issues: AuditIssue[] = [];
114
- const $ = cheerio.load(html);
115
- const baseUrl = new URL(url);
116
-
117
- // Check viewport meta tag
118
- const viewportMeta = $('meta[name="viewport"]').attr('content');
119
- const hasViewport = !!viewportMeta;
120
-
121
- // Collect all media queries
122
- const allMediaQueries: string[] = [];
123
- const externalSources: string[] = [];
124
- let inlineCount = 0;
125
-
126
- // Check inline styles
127
- $('style').each((_, el) => {
128
- const css = $(el).text();
129
- const queries = extractMediaQueries(css);
130
- if (queries.length > 0) {
131
- allMediaQueries.push(...queries);
132
- inlineCount += queries.length;
133
- }
134
- });
135
-
136
- // Check linked stylesheets (first-party only, limit to 3)
137
- const cssLinks = $('link[rel="stylesheet"][href]')
138
- .toArray()
139
- .slice(0, 3)
140
- .map((el) => $(el).attr('href'))
141
- .filter((href): href is string => {
142
- if (!href || href.startsWith('data:')) return false;
143
- try {
144
- const cssHostname = new URL(href, url).hostname;
145
- return cssHostname === baseUrl.hostname;
146
- } catch {
147
- return false;
148
- }
149
- });
150
-
151
- // Fetch CSS files in parallel with short timeout
152
- const cssPromises = cssLinks.map(async (cssUrl) => {
153
- try {
154
- const fullUrl = new URL(cssUrl, url).href;
155
- const response = await httpGet<string>(fullUrl, {
156
- timeout: 3000, // Reduced timeout
157
- validateStatus: () => true,
158
- });
159
-
160
- if (response.status === 200 && typeof response.data === 'string') {
161
- const queries = extractMediaQueries(response.data);
162
- if (queries.length > 0) {
163
- allMediaQueries.push(...queries);
164
- externalSources.push(cssUrl);
165
- }
166
- }
167
- } catch {
168
- // Skip failed requests
169
- }
170
- });
171
-
172
- // Wait for all CSS checks with overall timeout
173
- await Promise.race([
174
- Promise.all(cssPromises),
175
- new Promise<void>((resolve) => setTimeout(resolve, 6000)), // 6s overall timeout
176
- ]);
177
-
178
- const hasMediaQueries = allMediaQueries.length > 0;
179
- const { hasMobile, hasTablet, hasDesktop, breakpoints } = classifyBreakpoints(allMediaQueries);
180
-
181
- // Generate issues
182
- if (!hasViewport) {
183
- issues.push({
184
- code: 'RESPONSIVE_NO_VIEWPORT',
185
- severity: 'error',
186
- category: 'mobile',
187
- title: 'Missing viewport meta tag',
188
- description: 'No viewport meta tag found. This is required for responsive design to work properly.',
189
- impact:
190
- 'Without a viewport meta tag, mobile browsers will render the page at desktop width and scale down, making it unusable.',
191
- howToFix: 'Add <meta name="viewport" content="width=device-width, initial-scale=1"> to the <head> section.',
192
- affectedUrls: [url],
193
- });
194
- }
195
-
196
- if (!hasMediaQueries) {
197
- issues.push({
198
- code: 'RESPONSIVE_NO_MEDIA_QUERIES',
199
- severity: 'warning',
200
- category: 'mobile',
201
- title: 'No CSS media queries detected',
202
- description: 'No @media queries found in inline styles or first-party stylesheets.',
203
- impact:
204
- 'Without media queries, the site likely uses fixed layouts that do not adapt to different screen sizes. This hurts mobile usability and SEO.',
205
- howToFix:
206
- 'Add CSS media queries to adapt layout for different screen sizes. Consider using a mobile-first approach with min-width breakpoints.',
207
- affectedUrls: [url],
208
- details: {
209
- suggestion: 'Common breakpoints: 480px (mobile), 768px (tablet), 1024px (desktop)',
210
- },
211
- });
212
- } else if (!hasMobile && !hasTablet) {
213
- issues.push({
214
- code: 'RESPONSIVE_NO_MOBILE_BREAKPOINT',
215
- severity: 'warning',
216
- category: 'mobile',
217
- title: 'No mobile breakpoints detected',
218
- description: `Found ${allMediaQueries.length} media queries but none target mobile screen sizes (≤768px).`,
219
- impact:
220
- 'Site may not be optimized for mobile devices, which affects majority of web traffic and Google mobile-first indexing.',
221
- howToFix:
222
- 'Add media queries for mobile devices. Example: @media (max-width: 768px) { ... } or use min-width for mobile-first.',
223
- affectedUrls: [url],
224
- details: {
225
- detectedBreakpoints: breakpoints,
226
- },
227
- });
228
- }
229
-
230
- return {
231
- issues,
232
- data: {
233
- hasViewport,
234
- viewportContent: viewportMeta,
235
- hasMediaQueries,
236
- mediaQueryCount: allMediaQueries.length,
237
- mediaQuerySources: {
238
- inline: inlineCount,
239
- external: externalSources,
240
- },
241
- breakpoints,
242
- hasMobileBreakpoint: hasMobile,
243
- hasTabletBreakpoint: hasTablet,
244
- hasDesktopBreakpoint: hasDesktop,
245
- },
246
- };
247
- }