@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,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
- }