@rankcli/agent-runtime 0.0.8 → 0.0.11
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.
- package/README.md +90 -196
- package/dist/analyzer-GMURJADU.mjs +7 -0
- package/dist/chunk-2JADKV3Z.mjs +244 -0
- package/dist/chunk-3ZSCLNTW.mjs +557 -0
- package/dist/chunk-4E4MQOSP.mjs +374 -0
- package/dist/chunk-6BWS3CLP.mjs +16 -0
- package/dist/chunk-AK2IC22C.mjs +206 -0
- package/dist/chunk-K6VSXDD6.mjs +293 -0
- package/dist/chunk-M27NQCWW.mjs +303 -0
- package/dist/{chunk-YNZYHEYM.mjs → chunk-PJLNXOLN.mjs} +0 -14
- package/dist/chunk-VSQD74I7.mjs +474 -0
- package/dist/core-web-vitals-analyzer-TE6LQJMS.mjs +7 -0
- package/dist/geo-analyzer-D47LTMMA.mjs +25 -0
- package/dist/image-optimization-analyzer-XP4OQGRP.mjs +9 -0
- package/dist/index.d.mts +1523 -17
- package/dist/index.d.ts +1523 -17
- package/dist/index.js +9582 -2664
- package/dist/index.mjs +4812 -380
- package/dist/internal-linking-analyzer-MRMBV7NM.mjs +9 -0
- package/dist/mobile-seo-analyzer-67HNQ7IO.mjs +7 -0
- package/dist/security-headers-analyzer-3ZUQARS5.mjs +9 -0
- package/dist/structured-data-analyzer-2I4NQAUP.mjs +9 -0
- package/package.json +2 -2
- package/src/analyzers/core-web-vitals-analyzer.test.ts +236 -0
- package/src/analyzers/core-web-vitals-analyzer.ts +557 -0
- package/src/analyzers/geo-analyzer.test.ts +310 -0
- package/src/analyzers/geo-analyzer.ts +814 -0
- package/src/analyzers/image-optimization-analyzer.test.ts +145 -0
- package/src/analyzers/image-optimization-analyzer.ts +348 -0
- package/src/analyzers/index.ts +233 -0
- package/src/analyzers/internal-linking-analyzer.test.ts +141 -0
- package/src/analyzers/internal-linking-analyzer.ts +419 -0
- package/src/analyzers/mobile-seo-analyzer.test.ts +140 -0
- package/src/analyzers/mobile-seo-analyzer.ts +455 -0
- package/src/analyzers/security-headers-analyzer.test.ts +115 -0
- package/src/analyzers/security-headers-analyzer.ts +318 -0
- package/src/analyzers/structured-data-analyzer.test.ts +210 -0
- package/src/analyzers/structured-data-analyzer.ts +590 -0
- package/src/audit/engine.ts +3 -3
- package/src/audit/types.ts +3 -2
- package/src/fixer/framework-fixes.test.ts +489 -0
- package/src/fixer/framework-fixes.ts +3418 -0
- package/src/fixer/index.ts +1 -0
- package/src/fixer/schemas.ts +971 -0
- package/src/frameworks/detector.ts +642 -114
- package/src/frameworks/suggestion-engine.ts +38 -1
- package/src/index.ts +6 -0
- package/src/types.ts +15 -1
- package/dist/analyzer-2CSWIQGD.mjs +0 -6
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// src/analyzers/core-web-vitals-analyzer.ts
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
function analyzeLCP(html, $) {
|
|
4
|
+
const issues = [];
|
|
5
|
+
const recommendations = [];
|
|
6
|
+
let score = 100;
|
|
7
|
+
const heroImages = $("img").filter((_, el) => {
|
|
8
|
+
const parent = $(el).parent();
|
|
9
|
+
const isHero = parent.is('header, .hero, [class*="hero"], [class*="banner"], section:first-of-type');
|
|
10
|
+
const isLarge = parseInt($(el).attr("width") || "0") > 400 || ($(el).attr("class")?.includes("full") ?? false);
|
|
11
|
+
return isHero || isLarge;
|
|
12
|
+
});
|
|
13
|
+
heroImages.each((_, img) => {
|
|
14
|
+
const $img = $(img);
|
|
15
|
+
const src = $img.attr("src") || "";
|
|
16
|
+
if ($img.attr("loading") === "lazy") {
|
|
17
|
+
issues.push('Hero/LCP image has loading="lazy" - delays rendering');
|
|
18
|
+
score -= 20;
|
|
19
|
+
}
|
|
20
|
+
const preloads = $('link[rel="preload"][as="image"]');
|
|
21
|
+
const isPreloaded = preloads.toArray().some(
|
|
22
|
+
(p) => $(p).attr("href")?.includes(src.split("/").pop() || "")
|
|
23
|
+
);
|
|
24
|
+
if (!isPreloaded && src) {
|
|
25
|
+
issues.push("Hero image not preloaded");
|
|
26
|
+
recommendations.push(`Add: <link rel="preload" as="image" href="${src}">`);
|
|
27
|
+
score -= 15;
|
|
28
|
+
}
|
|
29
|
+
if (!$img.attr("width") || !$img.attr("height")) {
|
|
30
|
+
issues.push("Hero image missing width/height attributes");
|
|
31
|
+
score -= 10;
|
|
32
|
+
}
|
|
33
|
+
const format = src.split(".").pop()?.toLowerCase();
|
|
34
|
+
if (format && !["webp", "avif"].includes(format)) {
|
|
35
|
+
recommendations.push("Consider using WebP or AVIF format for hero image");
|
|
36
|
+
score -= 5;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const blockingCSS = $('link[rel="stylesheet"]').filter((_, el) => {
|
|
40
|
+
const media = $(el).attr("media");
|
|
41
|
+
return !media || media === "all" || media === "screen";
|
|
42
|
+
});
|
|
43
|
+
if (blockingCSS.length > 3) {
|
|
44
|
+
issues.push(`${blockingCSS.length} render-blocking stylesheets`);
|
|
45
|
+
recommendations.push("Inline critical CSS and defer non-critical stylesheets");
|
|
46
|
+
score -= 15;
|
|
47
|
+
}
|
|
48
|
+
const blockingScripts = $('head script:not([async]):not([defer]):not([type="module"])').filter((_, el) => {
|
|
49
|
+
return $(el).attr("src") !== void 0;
|
|
50
|
+
});
|
|
51
|
+
if (blockingScripts.length > 0) {
|
|
52
|
+
issues.push(`${blockingScripts.length} render-blocking scripts in <head>`);
|
|
53
|
+
recommendations.push("Add async or defer attribute to scripts");
|
|
54
|
+
score -= 20;
|
|
55
|
+
}
|
|
56
|
+
const fontLinks = $('link[rel="preload"][as="font"], link[href*="fonts.googleapis.com"], link[href*="fonts.gstatic.com"]');
|
|
57
|
+
const fontPreconnects = $('link[rel="preconnect"][href*="fonts"]');
|
|
58
|
+
if (fontLinks.length > 0 && fontPreconnects.length === 0) {
|
|
59
|
+
issues.push("Web fonts without preconnect");
|
|
60
|
+
recommendations.push('Add <link rel="preconnect" href="https://fonts.googleapis.com">');
|
|
61
|
+
score -= 10;
|
|
62
|
+
}
|
|
63
|
+
const inlineStyles = $("style").text();
|
|
64
|
+
if (inlineStyles.length > 5e4) {
|
|
65
|
+
issues.push("Very large inline CSS (>50KB)");
|
|
66
|
+
score -= 10;
|
|
67
|
+
}
|
|
68
|
+
let estimate = "unknown";
|
|
69
|
+
if (score >= 80) estimate = "good";
|
|
70
|
+
else if (score >= 50) estimate = "needs-improvement";
|
|
71
|
+
else estimate = "poor";
|
|
72
|
+
return { estimate, issues, recommendations };
|
|
73
|
+
}
|
|
74
|
+
function analyzeFID_INP(html, $) {
|
|
75
|
+
const issues = [];
|
|
76
|
+
const recommendations = [];
|
|
77
|
+
let score = 100;
|
|
78
|
+
let totalJSIndicators = 0;
|
|
79
|
+
$("script[src]").each((_, el) => {
|
|
80
|
+
const src = $(el).attr("src") || "";
|
|
81
|
+
if (src.includes("bundle") || src.includes("vendor") || src.includes("chunk")) {
|
|
82
|
+
totalJSIndicators++;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (totalJSIndicators > 5) {
|
|
86
|
+
issues.push("Many JavaScript bundle files detected");
|
|
87
|
+
recommendations.push("Consider code splitting and lazy loading");
|
|
88
|
+
score -= 15;
|
|
89
|
+
}
|
|
90
|
+
const hasHeavyFramework = html.includes("angular") || html.includes("__NUXT__") || html.includes("react") && !html.includes("preact");
|
|
91
|
+
const inlineScriptContent = $("script:not([src])").text();
|
|
92
|
+
if (inlineScriptContent.length > 1e4) {
|
|
93
|
+
issues.push("Large inline JavaScript (>10KB)");
|
|
94
|
+
recommendations.push("Move large scripts to external files with async/defer");
|
|
95
|
+
score -= 15;
|
|
96
|
+
}
|
|
97
|
+
const inlineHandlers = $("[onclick], [onchange], [onsubmit], [onload], [onerror]");
|
|
98
|
+
if (inlineHandlers.length > 10) {
|
|
99
|
+
issues.push(`${inlineHandlers.length} inline event handlers`);
|
|
100
|
+
recommendations.push("Use addEventListener instead of inline handlers");
|
|
101
|
+
score -= 10;
|
|
102
|
+
}
|
|
103
|
+
const thirdPartyScripts = $("script[src]").filter((_, el) => {
|
|
104
|
+
const src = $(el).attr("src") || "";
|
|
105
|
+
return src.includes("//") && !src.includes("localhost");
|
|
106
|
+
});
|
|
107
|
+
const heavyThirdParties = ["analytics", "facebook", "twitter", "linkedin", "hotjar", "intercom", "drift", "hubspot"];
|
|
108
|
+
let heavyScriptCount = 0;
|
|
109
|
+
thirdPartyScripts.each((_, el) => {
|
|
110
|
+
const src = $(el).attr("src") || "";
|
|
111
|
+
if (heavyThirdParties.some((tp) => src.toLowerCase().includes(tp))) {
|
|
112
|
+
heavyScriptCount++;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
if (heavyScriptCount > 3) {
|
|
116
|
+
issues.push(`${heavyScriptCount} heavy third-party scripts`);
|
|
117
|
+
recommendations.push("Lazy load third-party scripts after page interaction");
|
|
118
|
+
score -= 20;
|
|
119
|
+
}
|
|
120
|
+
if (html.includes("XMLHttpRequest") && !html.includes("async")) {
|
|
121
|
+
issues.push("Potential synchronous XHR detected");
|
|
122
|
+
score -= 10;
|
|
123
|
+
}
|
|
124
|
+
let estimate = "unknown";
|
|
125
|
+
if (score >= 80) estimate = "good";
|
|
126
|
+
else if (score >= 50) estimate = "needs-improvement";
|
|
127
|
+
else estimate = "poor";
|
|
128
|
+
return {
|
|
129
|
+
fidEstimate: estimate,
|
|
130
|
+
inpEstimate: estimate,
|
|
131
|
+
// Similar factors affect both
|
|
132
|
+
issues,
|
|
133
|
+
recommendations
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function analyzeCLS(html, $) {
|
|
137
|
+
const issues = [];
|
|
138
|
+
const recommendations = [];
|
|
139
|
+
let score = 100;
|
|
140
|
+
const imagesWithoutDimensions = $("img").filter((_, el) => {
|
|
141
|
+
const width = $(el).attr("width");
|
|
142
|
+
const height = $(el).attr("height");
|
|
143
|
+
const style = $(el).attr("style") || "";
|
|
144
|
+
const hasStyleDimensions = style.includes("width") && style.includes("height");
|
|
145
|
+
return !width || !height || !hasStyleDimensions && width === "auto";
|
|
146
|
+
});
|
|
147
|
+
if (imagesWithoutDimensions.length > 0) {
|
|
148
|
+
issues.push(`${imagesWithoutDimensions.length} images without width/height`);
|
|
149
|
+
recommendations.push("Add explicit width and height attributes to all images");
|
|
150
|
+
score -= Math.min(imagesWithoutDimensions.length * 5, 25);
|
|
151
|
+
}
|
|
152
|
+
const mediaWithoutDimensions = $("video, iframe").filter((_, el) => {
|
|
153
|
+
const width = $(el).attr("width");
|
|
154
|
+
const height = $(el).attr("height");
|
|
155
|
+
return !width || !height;
|
|
156
|
+
});
|
|
157
|
+
if (mediaWithoutDimensions.length > 0) {
|
|
158
|
+
issues.push(`${mediaWithoutDimensions.length} videos/iframes without dimensions`);
|
|
159
|
+
recommendations.push("Add explicit width and height to all video and iframe elements");
|
|
160
|
+
score -= Math.min(mediaWithoutDimensions.length * 10, 20);
|
|
161
|
+
}
|
|
162
|
+
const adContainers = $('[class*="ad-"], [class*="ads-"], [id*="ad-"], [data-ad], ins.adsbygoogle');
|
|
163
|
+
if (adContainers.length > 0) {
|
|
164
|
+
const hasReservedSpace = adContainers.filter((_, el) => {
|
|
165
|
+
const style = $(el).attr("style") || "";
|
|
166
|
+
return style.includes("min-height") || style.includes("height");
|
|
167
|
+
});
|
|
168
|
+
if (hasReservedSpace.length < adContainers.length) {
|
|
169
|
+
issues.push("Ad containers without reserved space");
|
|
170
|
+
recommendations.push("Reserve space for ads with min-height CSS");
|
|
171
|
+
score -= 20;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const dynamicContainers = $('[data-loading], [class*="skeleton"], [class*="placeholder"]');
|
|
175
|
+
if (dynamicContainers.length > 0) {
|
|
176
|
+
score += 5;
|
|
177
|
+
}
|
|
178
|
+
const webFonts = $('link[href*="fonts"], style').filter((_, el) => {
|
|
179
|
+
const content = $(el).attr("href") || $(el).text();
|
|
180
|
+
return content.includes("font-face") || content.includes("fonts.googleapis");
|
|
181
|
+
});
|
|
182
|
+
if (webFonts.length > 0) {
|
|
183
|
+
const hasFontDisplay = html.includes("font-display");
|
|
184
|
+
if (!hasFontDisplay) {
|
|
185
|
+
issues.push("Web fonts without font-display property");
|
|
186
|
+
recommendations.push("Add font-display: swap or optional to prevent FOUT");
|
|
187
|
+
score -= 10;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const stickyElements = $('[class*="sticky"], [class*="fixed"], [style*="position: fixed"], [style*="position: sticky"]');
|
|
191
|
+
const aspectRatioContainers = $('[style*="aspect-ratio"], [class*="aspect-"]');
|
|
192
|
+
if (aspectRatioContainers.length > 0) {
|
|
193
|
+
score += 5;
|
|
194
|
+
}
|
|
195
|
+
let estimate = "unknown";
|
|
196
|
+
if (score >= 80) estimate = "good";
|
|
197
|
+
else if (score >= 50) estimate = "needs-improvement";
|
|
198
|
+
else estimate = "poor";
|
|
199
|
+
return { estimate, issues, recommendations };
|
|
200
|
+
}
|
|
201
|
+
function analyzeTTFB($, headers) {
|
|
202
|
+
const issues = [];
|
|
203
|
+
const recommendations = [];
|
|
204
|
+
let score = 100;
|
|
205
|
+
if (headers) {
|
|
206
|
+
const cacheControl = headers["cache-control"];
|
|
207
|
+
if (!cacheControl) {
|
|
208
|
+
issues.push("No Cache-Control header");
|
|
209
|
+
recommendations.push("Add Cache-Control header for static assets");
|
|
210
|
+
score -= 10;
|
|
211
|
+
}
|
|
212
|
+
const hasCompression = headers["content-encoding"]?.includes("gzip") || headers["content-encoding"]?.includes("br");
|
|
213
|
+
if (!hasCompression) {
|
|
214
|
+
issues.push("Response not compressed (no gzip/brotli)");
|
|
215
|
+
recommendations.push("Enable Brotli or Gzip compression");
|
|
216
|
+
score -= 15;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const hasDynamicIndicators = $("[data-server-rendered], [data-user-id], [data-session]").length > 0;
|
|
220
|
+
if (hasDynamicIndicators) {
|
|
221
|
+
recommendations.push("Consider edge caching or CDN for dynamic pages");
|
|
222
|
+
}
|
|
223
|
+
const preconnects = $('link[rel="preconnect"], link[rel="dns-prefetch"]');
|
|
224
|
+
if (preconnects.length === 0) {
|
|
225
|
+
issues.push("No preconnect hints for external origins");
|
|
226
|
+
recommendations.push("Add preconnect for critical external origins (fonts, CDN, API)");
|
|
227
|
+
score -= 10;
|
|
228
|
+
}
|
|
229
|
+
const resourceHints = $('link[rel="preload"], link[rel="prefetch"], link[rel="modulepreload"]');
|
|
230
|
+
if (resourceHints.length === 0) {
|
|
231
|
+
recommendations.push("Consider adding preload hints for critical resources");
|
|
232
|
+
score -= 5;
|
|
233
|
+
}
|
|
234
|
+
let estimate = "unknown";
|
|
235
|
+
if (score >= 80) estimate = "good";
|
|
236
|
+
else if (score >= 50) estimate = "needs-improvement";
|
|
237
|
+
else estimate = "poor";
|
|
238
|
+
return { estimate, issues, recommendations };
|
|
239
|
+
}
|
|
240
|
+
function generateCWVIssues(lcp, fidInp, cls, ttfb, url) {
|
|
241
|
+
const issues = [];
|
|
242
|
+
if (lcp.estimate === "poor") {
|
|
243
|
+
issues.push({
|
|
244
|
+
code: "CWV_LCP_POOR",
|
|
245
|
+
severity: "critical",
|
|
246
|
+
category: "performance",
|
|
247
|
+
title: "Largest Contentful Paint likely poor",
|
|
248
|
+
description: `LCP issues detected: ${lcp.issues.join("; ")}`,
|
|
249
|
+
impact: "Poor LCP hurts rankings and user experience. Google uses LCP as a ranking factor.",
|
|
250
|
+
howToFix: lcp.recommendations.join("\n"),
|
|
251
|
+
affectedUrls: [url]
|
|
252
|
+
});
|
|
253
|
+
} else if (lcp.estimate === "needs-improvement" && lcp.issues.length > 0) {
|
|
254
|
+
issues.push({
|
|
255
|
+
code: "CWV_LCP_NEEDS_IMPROVEMENT",
|
|
256
|
+
severity: "warning",
|
|
257
|
+
category: "performance",
|
|
258
|
+
title: "Largest Contentful Paint needs improvement",
|
|
259
|
+
description: `LCP issues: ${lcp.issues.join("; ")}`,
|
|
260
|
+
impact: "LCP affects Core Web Vitals score and rankings",
|
|
261
|
+
howToFix: lcp.recommendations.join("\n"),
|
|
262
|
+
affectedUrls: [url]
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (fidInp.fidEstimate === "poor") {
|
|
266
|
+
issues.push({
|
|
267
|
+
code: "CWV_INP_POOR",
|
|
268
|
+
severity: "critical",
|
|
269
|
+
category: "performance",
|
|
270
|
+
title: "Interaction to Next Paint likely poor",
|
|
271
|
+
description: `INP/FID issues detected: ${fidInp.issues.join("; ")}`,
|
|
272
|
+
impact: "Poor interactivity hurts user experience and rankings",
|
|
273
|
+
howToFix: fidInp.recommendations.join("\n"),
|
|
274
|
+
affectedUrls: [url]
|
|
275
|
+
});
|
|
276
|
+
} else if (fidInp.fidEstimate === "needs-improvement" && fidInp.issues.length > 0) {
|
|
277
|
+
issues.push({
|
|
278
|
+
code: "CWV_INP_NEEDS_IMPROVEMENT",
|
|
279
|
+
severity: "warning",
|
|
280
|
+
category: "performance",
|
|
281
|
+
title: "Interaction responsiveness needs improvement",
|
|
282
|
+
description: `INP issues: ${fidInp.issues.join("; ")}`,
|
|
283
|
+
impact: "Affects user experience and Core Web Vitals",
|
|
284
|
+
howToFix: fidInp.recommendations.join("\n"),
|
|
285
|
+
affectedUrls: [url]
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (cls.estimate === "poor") {
|
|
289
|
+
issues.push({
|
|
290
|
+
code: "CWV_CLS_POOR",
|
|
291
|
+
severity: "critical",
|
|
292
|
+
category: "performance",
|
|
293
|
+
title: "Cumulative Layout Shift likely poor",
|
|
294
|
+
description: `CLS issues detected: ${cls.issues.join("; ")}`,
|
|
295
|
+
impact: "Visual instability frustrates users and hurts rankings",
|
|
296
|
+
howToFix: cls.recommendations.join("\n"),
|
|
297
|
+
affectedUrls: [url]
|
|
298
|
+
});
|
|
299
|
+
} else if (cls.estimate === "needs-improvement" && cls.issues.length > 0) {
|
|
300
|
+
issues.push({
|
|
301
|
+
code: "CWV_CLS_NEEDS_IMPROVEMENT",
|
|
302
|
+
severity: "warning",
|
|
303
|
+
category: "performance",
|
|
304
|
+
title: "Layout stability needs improvement",
|
|
305
|
+
description: `CLS issues: ${cls.issues.join("; ")}`,
|
|
306
|
+
impact: "Layout shifts degrade user experience",
|
|
307
|
+
howToFix: cls.recommendations.join("\n"),
|
|
308
|
+
affectedUrls: [url]
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (ttfb.issues.length > 0) {
|
|
312
|
+
issues.push({
|
|
313
|
+
code: "CWV_TTFB_ISSUES",
|
|
314
|
+
severity: "info",
|
|
315
|
+
category: "performance",
|
|
316
|
+
title: "Server response optimizations available",
|
|
317
|
+
description: `TTFB issues: ${ttfb.issues.join("; ")}`,
|
|
318
|
+
impact: "Slow server response delays all other metrics",
|
|
319
|
+
howToFix: ttfb.recommendations.join("\n"),
|
|
320
|
+
affectedUrls: [url]
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return issues;
|
|
324
|
+
}
|
|
325
|
+
function analyzeCoreWebVitals(html, url, headers) {
|
|
326
|
+
const $ = cheerio.load(html);
|
|
327
|
+
const lcp = analyzeLCP(html, $);
|
|
328
|
+
const fidInp = analyzeFID_INP(html, $);
|
|
329
|
+
const cls = analyzeCLS(html, $);
|
|
330
|
+
const ttfb = analyzeTTFB($, headers);
|
|
331
|
+
const issues = generateCWVIssues(lcp, fidInp, cls, ttfb, url);
|
|
332
|
+
const scores = {
|
|
333
|
+
lcp: lcp.estimate === "good" ? 100 : lcp.estimate === "needs-improvement" ? 60 : 30,
|
|
334
|
+
fid: fidInp.fidEstimate === "good" ? 100 : fidInp.fidEstimate === "needs-improvement" ? 60 : 30,
|
|
335
|
+
cls: cls.estimate === "good" ? 100 : cls.estimate === "needs-improvement" ? 60 : 30,
|
|
336
|
+
ttfb: ttfb.estimate === "good" ? 100 : ttfb.estimate === "needs-improvement" ? 60 : 30
|
|
337
|
+
};
|
|
338
|
+
const overallScore = Math.round(
|
|
339
|
+
scores.lcp * 0.3 + scores.fid * 0.2 + scores.cls * 0.3 + scores.ttfb * 0.2
|
|
340
|
+
);
|
|
341
|
+
return {
|
|
342
|
+
lcp: {
|
|
343
|
+
estimate: lcp.estimate,
|
|
344
|
+
issues: lcp.issues,
|
|
345
|
+
recommendations: lcp.recommendations
|
|
346
|
+
},
|
|
347
|
+
fid: {
|
|
348
|
+
estimate: fidInp.fidEstimate,
|
|
349
|
+
issues: fidInp.issues,
|
|
350
|
+
recommendations: fidInp.recommendations
|
|
351
|
+
},
|
|
352
|
+
cls: {
|
|
353
|
+
estimate: cls.estimate,
|
|
354
|
+
issues: cls.issues,
|
|
355
|
+
recommendations: cls.recommendations
|
|
356
|
+
},
|
|
357
|
+
inp: {
|
|
358
|
+
estimate: fidInp.inpEstimate,
|
|
359
|
+
issues: fidInp.issues,
|
|
360
|
+
recommendations: fidInp.recommendations
|
|
361
|
+
},
|
|
362
|
+
ttfb: {
|
|
363
|
+
estimate: ttfb.estimate,
|
|
364
|
+
issues: ttfb.issues,
|
|
365
|
+
recommendations: ttfb.recommendations
|
|
366
|
+
},
|
|
367
|
+
overallScore,
|
|
368
|
+
issues
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export {
|
|
373
|
+
analyzeCoreWebVitals
|
|
374
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
__require,
|
|
15
|
+
__export
|
|
16
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/analyzers/security-headers-analyzer.ts
|
|
2
|
+
function analyzeSecurityHeaders(headers, url) {
|
|
3
|
+
const issues = [];
|
|
4
|
+
let score = 100;
|
|
5
|
+
const normalizedHeaders = {};
|
|
6
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
7
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
8
|
+
}
|
|
9
|
+
const isHTTPS = url.startsWith("https://");
|
|
10
|
+
if (!isHTTPS) {
|
|
11
|
+
score -= 30;
|
|
12
|
+
issues.push({
|
|
13
|
+
code: "SEC_NO_HTTPS",
|
|
14
|
+
severity: "critical",
|
|
15
|
+
category: "technical",
|
|
16
|
+
title: "Site not using HTTPS",
|
|
17
|
+
description: "HTTPS is required for security and is a confirmed Google ranking factor.",
|
|
18
|
+
impact: "Major negative ranking impact and browser security warnings",
|
|
19
|
+
howToFix: "Install SSL certificate and redirect all HTTP traffic to HTTPS",
|
|
20
|
+
affectedUrls: [url]
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const hsts = normalizedHeaders["strict-transport-security"];
|
|
24
|
+
let hasHSTS = false;
|
|
25
|
+
let hstsMaxAge;
|
|
26
|
+
let includesSubdomains = false;
|
|
27
|
+
let preload = false;
|
|
28
|
+
if (hsts) {
|
|
29
|
+
hasHSTS = true;
|
|
30
|
+
const maxAgeMatch = hsts.match(/max-age=(\d+)/);
|
|
31
|
+
if (maxAgeMatch) {
|
|
32
|
+
hstsMaxAge = parseInt(maxAgeMatch[1]);
|
|
33
|
+
if (hstsMaxAge < 31536e3) {
|
|
34
|
+
score -= 5;
|
|
35
|
+
issues.push({
|
|
36
|
+
code: "SEC_HSTS_SHORT",
|
|
37
|
+
severity: "info",
|
|
38
|
+
category: "technical",
|
|
39
|
+
title: "HSTS max-age is less than 1 year",
|
|
40
|
+
description: `Current max-age is ${hstsMaxAge} seconds. Recommended: 31536000 (1 year) or more.`,
|
|
41
|
+
impact: "Reduced protection window",
|
|
42
|
+
howToFix: "Set Strict-Transport-Security: max-age=31536000; includeSubDomains; preload",
|
|
43
|
+
affectedUrls: [url]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
includesSubdomains = hsts.toLowerCase().includes("includesubdomains");
|
|
48
|
+
preload = hsts.toLowerCase().includes("preload");
|
|
49
|
+
} else if (isHTTPS) {
|
|
50
|
+
score -= 15;
|
|
51
|
+
issues.push({
|
|
52
|
+
code: "SEC_NO_HSTS",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
category: "technical",
|
|
55
|
+
title: "Missing HSTS header",
|
|
56
|
+
description: "HTTP Strict Transport Security (HSTS) header is not set.",
|
|
57
|
+
impact: "Users may access site over insecure HTTP",
|
|
58
|
+
howToFix: "Add header: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload",
|
|
59
|
+
affectedUrls: [url]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const csp = normalizedHeaders["content-security-policy"];
|
|
63
|
+
const cspIssues = [];
|
|
64
|
+
if (csp) {
|
|
65
|
+
if (csp.includes("'unsafe-inline'")) {
|
|
66
|
+
cspIssues.push("Contains 'unsafe-inline' - reduces XSS protection");
|
|
67
|
+
score -= 5;
|
|
68
|
+
}
|
|
69
|
+
if (csp.includes("'unsafe-eval'")) {
|
|
70
|
+
cspIssues.push("Contains 'unsafe-eval' - allows eval()");
|
|
71
|
+
score -= 5;
|
|
72
|
+
}
|
|
73
|
+
if (csp.includes("*") && !csp.includes("*.")) {
|
|
74
|
+
cspIssues.push("Contains wildcard (*) - too permissive");
|
|
75
|
+
score -= 3;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
score -= 10;
|
|
79
|
+
issues.push({
|
|
80
|
+
code: "SEC_NO_CSP",
|
|
81
|
+
severity: "warning",
|
|
82
|
+
category: "technical",
|
|
83
|
+
title: "Missing Content-Security-Policy header",
|
|
84
|
+
description: "CSP helps prevent XSS attacks and is a security best practice.",
|
|
85
|
+
impact: "Vulnerable to cross-site scripting attacks",
|
|
86
|
+
howToFix: "Add Content-Security-Policy header with appropriate directives. Start with: default-src 'self'",
|
|
87
|
+
affectedUrls: [url]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const xFrameOptions = normalizedHeaders["x-frame-options"];
|
|
91
|
+
if (!xFrameOptions) {
|
|
92
|
+
score -= 5;
|
|
93
|
+
issues.push({
|
|
94
|
+
code: "SEC_NO_XFRAME",
|
|
95
|
+
severity: "info",
|
|
96
|
+
category: "technical",
|
|
97
|
+
title: "Missing X-Frame-Options header",
|
|
98
|
+
description: "X-Frame-Options prevents clickjacking attacks.",
|
|
99
|
+
impact: "Site can be embedded in malicious iframes",
|
|
100
|
+
howToFix: "Add header: X-Frame-Options: SAMEORIGIN (or DENY)",
|
|
101
|
+
affectedUrls: [url]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const xContentTypeOptions = normalizedHeaders["x-content-type-options"];
|
|
105
|
+
if (!xContentTypeOptions) {
|
|
106
|
+
score -= 5;
|
|
107
|
+
issues.push({
|
|
108
|
+
code: "SEC_NO_XCONTENT_TYPE",
|
|
109
|
+
severity: "info",
|
|
110
|
+
category: "technical",
|
|
111
|
+
title: "Missing X-Content-Type-Options header",
|
|
112
|
+
description: "Prevents browsers from MIME-sniffing responses.",
|
|
113
|
+
impact: "Potential for MIME confusion attacks",
|
|
114
|
+
howToFix: "Add header: X-Content-Type-Options: nosniff",
|
|
115
|
+
affectedUrls: [url]
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const referrerPolicy = normalizedHeaders["referrer-policy"];
|
|
119
|
+
if (!referrerPolicy) {
|
|
120
|
+
score -= 3;
|
|
121
|
+
issues.push({
|
|
122
|
+
code: "SEC_NO_REFERRER_POLICY",
|
|
123
|
+
severity: "info",
|
|
124
|
+
category: "technical",
|
|
125
|
+
title: "Missing Referrer-Policy header",
|
|
126
|
+
description: "Controls how much referrer information is sent with requests.",
|
|
127
|
+
impact: "May leak sensitive URL information",
|
|
128
|
+
howToFix: "Add header: Referrer-Policy: strict-origin-when-cross-origin",
|
|
129
|
+
affectedUrls: [url]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const permissionsPolicy = normalizedHeaders["permissions-policy"] || normalizedHeaders["feature-policy"];
|
|
133
|
+
if (!permissionsPolicy) {
|
|
134
|
+
score -= 3;
|
|
135
|
+
issues.push({
|
|
136
|
+
code: "SEC_NO_PERMISSIONS_POLICY",
|
|
137
|
+
severity: "info",
|
|
138
|
+
category: "technical",
|
|
139
|
+
title: "Missing Permissions-Policy header",
|
|
140
|
+
description: "Controls which browser features can be used.",
|
|
141
|
+
impact: "All browser features enabled by default",
|
|
142
|
+
howToFix: "Add header: Permissions-Policy: geolocation=(), microphone=(), camera=()",
|
|
143
|
+
affectedUrls: [url]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
let grade;
|
|
147
|
+
if (score >= 95) grade = "A+";
|
|
148
|
+
else if (score >= 85) grade = "A";
|
|
149
|
+
else if (score >= 70) grade = "B";
|
|
150
|
+
else if (score >= 55) grade = "C";
|
|
151
|
+
else if (score >= 40) grade = "D";
|
|
152
|
+
else grade = "F";
|
|
153
|
+
return {
|
|
154
|
+
score: Math.max(0, score),
|
|
155
|
+
https: {
|
|
156
|
+
enabled: isHTTPS,
|
|
157
|
+
hasHSTS,
|
|
158
|
+
hstsMaxAge,
|
|
159
|
+
includesSubdomains,
|
|
160
|
+
preload
|
|
161
|
+
},
|
|
162
|
+
contentSecurity: {
|
|
163
|
+
hasCSP: !!csp,
|
|
164
|
+
policy: csp,
|
|
165
|
+
issues: cspIssues
|
|
166
|
+
},
|
|
167
|
+
frameOptions: {
|
|
168
|
+
hasXFrameOptions: !!xFrameOptions,
|
|
169
|
+
value: xFrameOptions
|
|
170
|
+
},
|
|
171
|
+
contentTypeOptions: {
|
|
172
|
+
hasXContentTypeOptions: !!xContentTypeOptions
|
|
173
|
+
},
|
|
174
|
+
xssProtection: {
|
|
175
|
+
hasXXSSProtection: !!normalizedHeaders["x-xss-protection"],
|
|
176
|
+
value: normalizedHeaders["x-xss-protection"]
|
|
177
|
+
},
|
|
178
|
+
referrerPolicy: {
|
|
179
|
+
hasReferrerPolicy: !!referrerPolicy,
|
|
180
|
+
value: referrerPolicy
|
|
181
|
+
},
|
|
182
|
+
permissionsPolicy: {
|
|
183
|
+
hasPermissionsPolicy: !!permissionsPolicy,
|
|
184
|
+
policy: permissionsPolicy
|
|
185
|
+
},
|
|
186
|
+
issues,
|
|
187
|
+
grade
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function generateSecurityHeaders(siteUrl) {
|
|
191
|
+
const domain = new URL(siteUrl).hostname;
|
|
192
|
+
return {
|
|
193
|
+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
|
194
|
+
"Content-Security-Policy": `default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.${domain}`,
|
|
195
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
196
|
+
"X-Content-Type-Options": "nosniff",
|
|
197
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
198
|
+
"Permissions-Policy": "geolocation=(), microphone=(), camera=(), payment=()",
|
|
199
|
+
"X-XSS-Protection": "1; mode=block"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
analyzeSecurityHeaders,
|
|
205
|
+
generateSecurityHeaders
|
|
206
|
+
};
|