@aexol/spectral 0.9.42 → 0.9.44
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/dist/extensions/aexol-mcp.d.ts.map +1 -1
- package/dist/extensions/aexol-mcp.js +44 -9
- package/dist/extensions/browser/browser-service.d.ts +4 -1
- package/dist/extensions/browser/browser-service.d.ts.map +1 -1
- package/dist/extensions/browser/browser-service.js +34 -7
- package/dist/extensions/browser/config.d.ts +3 -0
- package/dist/extensions/browser/config.d.ts.map +1 -1
- package/dist/extensions/browser/config.js +1 -0
- package/dist/extensions/browser/index.d.ts.map +1 -1
- package/dist/extensions/browser/index.js +3 -0
- package/dist/extensions/browser/record/record-tool.d.ts.map +1 -1
- package/dist/extensions/browser/record/record-tool.js +27 -4
- package/dist/extensions/browser/scenario/markdown-parser.d.ts +44 -0
- package/dist/extensions/browser/scenario/markdown-parser.d.ts.map +1 -0
- package/dist/extensions/browser/scenario/markdown-parser.js +159 -0
- package/dist/extensions/browser/scenario/scenario-tool.d.ts +4 -0
- package/dist/extensions/browser/scenario/scenario-tool.d.ts.map +1 -0
- package/dist/extensions/browser/scenario/scenario-tool.js +381 -0
- package/dist/extensions/browser/tools/core.js +1 -1
- package/dist/extensions/browser/tools/screenshot.d.ts.map +1 -1
- package/dist/extensions/browser/tools/screenshot.js +2 -1
- package/dist/extensions/seo/analyzers/agent-ux.d.ts +5 -0
- package/dist/extensions/seo/analyzers/agent-ux.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/agent-ux.js +95 -0
- package/dist/extensions/seo/analyzers/content-optimize.d.ts +3 -0
- package/dist/extensions/seo/analyzers/content-optimize.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/content-optimize.js +190 -0
- package/dist/extensions/seo/analyzers/content.d.ts +16 -0
- package/dist/extensions/seo/analyzers/content.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/content.js +174 -0
- package/dist/extensions/seo/analyzers/crawler.d.ts +4 -0
- package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/crawler.js +288 -0
- package/dist/extensions/seo/analyzers/drift.d.ts +13 -0
- package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/drift.js +204 -0
- package/dist/extensions/seo/analyzers/images.d.ts +10 -0
- package/dist/extensions/seo/analyzers/images.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/images.js +49 -0
- package/dist/extensions/seo/analyzers/keywords.d.ts +9 -0
- package/dist/extensions/seo/analyzers/keywords.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/keywords.js +183 -0
- package/dist/extensions/seo/analyzers/parasite.d.ts +18 -0
- package/dist/extensions/seo/analyzers/parasite.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/parasite.js +142 -0
- package/dist/extensions/seo/analyzers/performance.d.ts +6 -0
- package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/performance.js +257 -0
- package/dist/extensions/seo/analyzers/preload.d.ts +12 -0
- package/dist/extensions/seo/analyzers/preload.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/preload.js +108 -0
- package/dist/extensions/seo/analyzers/readability.d.ts +4 -0
- package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/readability.js +121 -0
- package/dist/extensions/seo/analyzers/schema.d.ts +21 -0
- package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/schema.js +324 -0
- package/dist/extensions/seo/analyzers/sitemap.d.ts +20 -0
- package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/sitemap.js +121 -0
- package/dist/extensions/seo/analyzers/technical.d.ts +11 -0
- package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/technical.js +268 -0
- package/dist/extensions/seo/config.d.ts +24 -0
- package/dist/extensions/seo/config.d.ts.map +1 -0
- package/dist/extensions/seo/config.js +48 -0
- package/dist/extensions/seo/index.d.ts +16 -0
- package/dist/extensions/seo/index.d.ts.map +1 -0
- package/dist/extensions/seo/index.js +126 -0
- package/dist/extensions/seo/tools/agent-ux.d.ts +7 -0
- package/dist/extensions/seo/tools/agent-ux.d.ts.map +1 -0
- package/dist/extensions/seo/tools/agent-ux.js +63 -0
- package/dist/extensions/seo/tools/batch.d.ts +4 -0
- package/dist/extensions/seo/tools/batch.d.ts.map +1 -0
- package/dist/extensions/seo/tools/batch.js +193 -0
- package/dist/extensions/seo/tools/content.d.ts +7 -0
- package/dist/extensions/seo/tools/content.d.ts.map +1 -0
- package/dist/extensions/seo/tools/content.js +55 -0
- package/dist/extensions/seo/tools/crawl.d.ts +4 -0
- package/dist/extensions/seo/tools/crawl.d.ts.map +1 -0
- package/dist/extensions/seo/tools/crawl.js +166 -0
- package/dist/extensions/seo/tools/drift.d.ts +7 -0
- package/dist/extensions/seo/tools/drift.d.ts.map +1 -0
- package/dist/extensions/seo/tools/drift.js +104 -0
- package/dist/extensions/seo/tools/fetch.d.ts +7 -0
- package/dist/extensions/seo/tools/fetch.d.ts.map +1 -0
- package/dist/extensions/seo/tools/fetch.js +123 -0
- package/dist/extensions/seo/tools/full-audit.d.ts +11 -0
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -0
- package/dist/extensions/seo/tools/full-audit.js +191 -0
- package/dist/extensions/seo/tools/helpers.d.ts +13 -0
- package/dist/extensions/seo/tools/helpers.d.ts.map +1 -0
- package/dist/extensions/seo/tools/helpers.js +24 -0
- package/dist/extensions/seo/tools/images.d.ts +7 -0
- package/dist/extensions/seo/tools/images.d.ts.map +1 -0
- package/dist/extensions/seo/tools/images.js +46 -0
- package/dist/extensions/seo/tools/keywords.d.ts +4 -0
- package/dist/extensions/seo/tools/keywords.d.ts.map +1 -0
- package/dist/extensions/seo/tools/keywords.js +234 -0
- package/dist/extensions/seo/tools/parasite.d.ts +7 -0
- package/dist/extensions/seo/tools/parasite.d.ts.map +1 -0
- package/dist/extensions/seo/tools/parasite.js +53 -0
- package/dist/extensions/seo/tools/parse.d.ts +7 -0
- package/dist/extensions/seo/tools/parse.d.ts.map +1 -0
- package/dist/extensions/seo/tools/parse.js +66 -0
- package/dist/extensions/seo/tools/performance.d.ts +4 -0
- package/dist/extensions/seo/tools/performance.d.ts.map +1 -0
- package/dist/extensions/seo/tools/performance.js +201 -0
- package/dist/extensions/seo/tools/preload.d.ts +7 -0
- package/dist/extensions/seo/tools/preload.d.ts.map +1 -0
- package/dist/extensions/seo/tools/preload.js +52 -0
- package/dist/extensions/seo/tools/report.d.ts +4 -0
- package/dist/extensions/seo/tools/report.d.ts.map +1 -0
- package/dist/extensions/seo/tools/report.js +167 -0
- package/dist/extensions/seo/tools/schema.d.ts +7 -0
- package/dist/extensions/seo/tools/schema.d.ts.map +1 -0
- package/dist/extensions/seo/tools/schema.js +105 -0
- package/dist/extensions/seo/tools/sitemap.d.ts +7 -0
- package/dist/extensions/seo/tools/sitemap.d.ts.map +1 -0
- package/dist/extensions/seo/tools/sitemap.js +85 -0
- package/dist/extensions/seo/tools/technical.d.ts +7 -0
- package/dist/extensions/seo/tools/technical.d.ts.map +1 -0
- package/dist/extensions/seo/tools/technical.js +62 -0
- package/dist/extensions/seo/types.d.ts +445 -0
- package/dist/extensions/seo/types.d.ts.map +1 -0
- package/dist/extensions/seo/types.js +7 -0
- package/dist/extensions/seo/utils/fetcher.d.ts +28 -0
- package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -0
- package/dist/extensions/seo/utils/fetcher.js +118 -0
- package/dist/extensions/seo/utils/parser.d.ts +8 -0
- package/dist/extensions/seo/utils/parser.d.ts.map +1 -0
- package/dist/extensions/seo/utils/parser.js +265 -0
- package/dist/extensions/seo/utils/renderer.d.ts +39 -0
- package/dist/extensions/seo/utils/renderer.d.ts.map +1 -0
- package/dist/extensions/seo/utils/renderer.js +190 -0
- package/dist/extensions/seo/utils/url-safety.d.ts +31 -0
- package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -0
- package/dist/extensions/seo/utils/url-safety.js +202 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +11 -0
- package/dist/server/handlers/paths-pick-directory.d.ts +0 -27
- package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
- package/dist/server/handlers/paths-pick-directory.js +86 -92
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const LCP_GOOD = 2500;
|
|
2
|
+
const LCP_POOR = 4000;
|
|
3
|
+
const CLS_GOOD = 0.1;
|
|
4
|
+
const CLS_POOR = 0.25;
|
|
5
|
+
const TBT_GOOD = 200;
|
|
6
|
+
const TBT_POOR = 600;
|
|
7
|
+
export function calculatePerformanceFromFetch(fetchResult, url) {
|
|
8
|
+
const content = fetchResult.content;
|
|
9
|
+
const headers = fetchResult.headers;
|
|
10
|
+
// Estimate metrics from available data
|
|
11
|
+
const htmlSize = fetchResult.contentLengthBytes;
|
|
12
|
+
const ttfbEstimate = estimateTTFB(headers);
|
|
13
|
+
// Count resources to estimate complexity
|
|
14
|
+
const cssCount = (content.match(/<link[^>]*stylesheet[^>]*>/gi) || []).length;
|
|
15
|
+
const jsCount = (content.match(/<script[^>]*src=[^>]*>/gi) || []).length;
|
|
16
|
+
const imgCount = (content.match(/<img[^>]*>/gi) || []).length;
|
|
17
|
+
const totalResources = cssCount + jsCount + imgCount;
|
|
18
|
+
// Rough LCP estimate based on resource count and HTML size
|
|
19
|
+
let lcpEstimate;
|
|
20
|
+
let lcpRating;
|
|
21
|
+
if (totalResources <= 5 && htmlSize < 50000) {
|
|
22
|
+
lcpEstimate = 800 + Math.round(Math.random() * 500);
|
|
23
|
+
}
|
|
24
|
+
else if (totalResources <= 15 && htmlSize < 200000) {
|
|
25
|
+
lcpEstimate = 1500 + Math.round(Math.random() * 800);
|
|
26
|
+
}
|
|
27
|
+
else if (totalResources <= 30 && htmlSize < 500000) {
|
|
28
|
+
lcpEstimate = 2500 + Math.round(Math.random() * 1000);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
lcpEstimate = 3500 + Math.round(Math.random() * 2000);
|
|
32
|
+
}
|
|
33
|
+
if (lcpEstimate <= LCP_GOOD)
|
|
34
|
+
lcpRating = "good";
|
|
35
|
+
else if (lcpEstimate <= LCP_POOR)
|
|
36
|
+
lcpRating = "needs-improvement";
|
|
37
|
+
else
|
|
38
|
+
lcpRating = "poor";
|
|
39
|
+
// CLS estimate based on missing image dimensions
|
|
40
|
+
const imagesWithoutDimensions = (content.match(/<img(?!.*width=)(?!.*height=)[^>]*>/gi) || []).length;
|
|
41
|
+
let clsEstimate;
|
|
42
|
+
let clsRating;
|
|
43
|
+
if (imagesWithoutDimensions === 0) {
|
|
44
|
+
clsEstimate = 0.02 + Math.random() * 0.05;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
clsEstimate = 0.1 + imagesWithoutDimensions * 0.05;
|
|
48
|
+
}
|
|
49
|
+
if (clsEstimate <= CLS_GOOD)
|
|
50
|
+
clsRating = "good";
|
|
51
|
+
else if (clsEstimate <= CLS_POOR)
|
|
52
|
+
clsRating = "needs-improvement";
|
|
53
|
+
else
|
|
54
|
+
clsRating = "poor";
|
|
55
|
+
// TBT estimate
|
|
56
|
+
let tbtEstimate;
|
|
57
|
+
let tbtRating;
|
|
58
|
+
if (jsCount <= 2 && htmlSize < 100000) {
|
|
59
|
+
tbtEstimate = 50 + Math.round(Math.random() * 100);
|
|
60
|
+
}
|
|
61
|
+
else if (jsCount <= 5 && htmlSize < 300000) {
|
|
62
|
+
tbtEstimate = 200 + Math.round(Math.random() * 200);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
tbtEstimate = 500 + Math.round(Math.random() * 500);
|
|
66
|
+
}
|
|
67
|
+
if (tbtEstimate <= TBT_GOOD)
|
|
68
|
+
tbtRating = "good";
|
|
69
|
+
else if (tbtEstimate <= TBT_POOR)
|
|
70
|
+
tbtRating = "needs-improvement";
|
|
71
|
+
else
|
|
72
|
+
tbtRating = "poor";
|
|
73
|
+
// FCP estimate
|
|
74
|
+
const fcp = Math.round(ttfbEstimate + 200 + Math.random() * 300);
|
|
75
|
+
const score = calculateCWVScore(lcpRating, clsRating, tbtRating);
|
|
76
|
+
return {
|
|
77
|
+
url,
|
|
78
|
+
lcp: { value: lcpEstimate, rating: lcpRating },
|
|
79
|
+
cls: { value: Number(clsEstimate.toFixed(3)), rating: clsRating },
|
|
80
|
+
tbt: { value: tbtEstimate, rating: tbtRating },
|
|
81
|
+
fcp,
|
|
82
|
+
ttfb: ttfbEstimate,
|
|
83
|
+
score,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function estimateTTFB(headers) {
|
|
87
|
+
const cfCache = headers["cf-cache-status"] || headers["x-cache"] || "";
|
|
88
|
+
if (cfCache.toLowerCase() === "hit")
|
|
89
|
+
return 30 + Math.round(Math.random() * 20);
|
|
90
|
+
const server = headers["server"] || "";
|
|
91
|
+
if (server.includes("cloudflare") || server.includes("fastly")) {
|
|
92
|
+
return 50 + Math.round(Math.random() * 50);
|
|
93
|
+
}
|
|
94
|
+
return 100 + Math.round(Math.random() * 200);
|
|
95
|
+
}
|
|
96
|
+
function calculateCWVScore(lcp, cls, tbt) {
|
|
97
|
+
let score = 100;
|
|
98
|
+
if (lcp === "needs-improvement")
|
|
99
|
+
score -= 20;
|
|
100
|
+
else if (lcp === "poor")
|
|
101
|
+
score -= 40;
|
|
102
|
+
if (cls === "needs-improvement")
|
|
103
|
+
score -= 15;
|
|
104
|
+
else if (cls === "poor")
|
|
105
|
+
score -= 35;
|
|
106
|
+
if (tbt === "needs-improvement")
|
|
107
|
+
score -= 15;
|
|
108
|
+
else if (tbt === "poor")
|
|
109
|
+
score -= 25;
|
|
110
|
+
return Math.max(0, score);
|
|
111
|
+
}
|
|
112
|
+
export function analyzePageWeight(fetchResult, url) {
|
|
113
|
+
const content = fetchResult.content;
|
|
114
|
+
const htmlKb = Math.round(fetchResult.contentLengthBytes / 1024);
|
|
115
|
+
// Estimate CSS size
|
|
116
|
+
const cssMatches = content.match(/<style[^>]*>([\s\S]*?)<\/style>/gi) || [];
|
|
117
|
+
let cssKb = 0;
|
|
118
|
+
for (const m of cssMatches) {
|
|
119
|
+
cssKb += Math.round(m.length / 1024);
|
|
120
|
+
}
|
|
121
|
+
// Count JS scripts (size estimation is rough)
|
|
122
|
+
const jsTags = (content.match(/<script[^>]*>/gi) || []).length;
|
|
123
|
+
const jsKb = jsTags * 5; // rough average
|
|
124
|
+
// Count images and estimate sizes
|
|
125
|
+
const imgTags = (content.match(/<img[^>]*>/gi) || []).length;
|
|
126
|
+
const imageKb = imgTags * 40; // rough average
|
|
127
|
+
// Fonts
|
|
128
|
+
const fontMatches = content.match(/url\([^)]*\.(woff2?|ttf|otf)[^)]*\)/gi) || [];
|
|
129
|
+
const fontKb = fontMatches.length * 50;
|
|
130
|
+
const totalKb = htmlKb + cssKb + jsKb + imageKb + fontKb;
|
|
131
|
+
const otherKb = 0;
|
|
132
|
+
const resourceCount = cssMatches.length + jsTags + imgTags + fontMatches.length;
|
|
133
|
+
const topHeavyResources = [];
|
|
134
|
+
if (htmlKb > 100)
|
|
135
|
+
topHeavyResources.push({ url, sizeKb: htmlKb, type: "html" });
|
|
136
|
+
if (cssKb > 50)
|
|
137
|
+
topHeavyResources.push({ url: `${url}#css`, sizeKb: cssKb, type: "css" });
|
|
138
|
+
if (jsKb > 100)
|
|
139
|
+
topHeavyResources.push({ url: `${url}#js`, sizeKb: jsKb, type: "js" });
|
|
140
|
+
return {
|
|
141
|
+
url: fetchResult.finalUrl || url,
|
|
142
|
+
totalKb,
|
|
143
|
+
htmlKb,
|
|
144
|
+
cssKb,
|
|
145
|
+
jsKb,
|
|
146
|
+
imageKb,
|
|
147
|
+
fontKb,
|
|
148
|
+
otherKb,
|
|
149
|
+
resourceCount,
|
|
150
|
+
topHeavyResources,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function analyzeRenderBlocking(fetchResult, url) {
|
|
154
|
+
const content = fetchResult.content;
|
|
155
|
+
const recommendations = [];
|
|
156
|
+
// Find blocking CSS (non-async, non-defer stylesheets in head)
|
|
157
|
+
const blockingCss = [];
|
|
158
|
+
const headMatch = content.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
159
|
+
const head = headMatch ? headMatch[1] : content;
|
|
160
|
+
const cssPattern = /<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
|
|
161
|
+
let cssMatch;
|
|
162
|
+
while ((cssMatch = cssPattern.exec(head)) !== null) {
|
|
163
|
+
const tag = cssMatch[0];
|
|
164
|
+
const href = cssMatch[1];
|
|
165
|
+
if (!tag.includes("media=") || tag.includes('media="all"') || tag.includes("media='all'")) {
|
|
166
|
+
blockingCss.push({ href, sizeEstimate: 15 });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Find blocking JS (non-async, non-defer in head)
|
|
170
|
+
const blockingJs = [];
|
|
171
|
+
const jsPattern = /<script[^>]*src=["']([^"']+)["'][^>]*>/gi;
|
|
172
|
+
let jsMatch;
|
|
173
|
+
while ((jsMatch = jsPattern.exec(head)) !== null) {
|
|
174
|
+
const tag = jsMatch[0];
|
|
175
|
+
const src = jsMatch[1];
|
|
176
|
+
if (!tag.includes("async") && !tag.includes("defer") && !tag.includes("module")) {
|
|
177
|
+
blockingJs.push({ src, sizeEstimate: 30 });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (blockingCss.length > 2) {
|
|
181
|
+
recommendations.push(`Reduce blocking CSS: ${blockingCss.length} stylesheets in <head>. Inline critical CSS and defer the rest.`);
|
|
182
|
+
}
|
|
183
|
+
if (blockingJs.length > 1) {
|
|
184
|
+
recommendations.push(`Reduce blocking JS: ${blockingJs.length} scripts in <head>. Add 'async' or 'defer' attributes.`);
|
|
185
|
+
}
|
|
186
|
+
if (blockingCss.length === 0 && blockingJs.length === 0) {
|
|
187
|
+
recommendations.push("No render-blocking resources detected — good optimization.");
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
url: fetchResult.finalUrl || url,
|
|
191
|
+
blockingCss,
|
|
192
|
+
blockingJs,
|
|
193
|
+
totalBlockingUrls: blockingCss.length + blockingJs.length,
|
|
194
|
+
recommendations,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export function analyzeFonts(fetchResult, url) {
|
|
198
|
+
const content = fetchResult.content;
|
|
199
|
+
const fonts = [];
|
|
200
|
+
const recommendations = [];
|
|
201
|
+
// Find @font-face declarations
|
|
202
|
+
const fontFacePattern = /@font-face\s*\{[^}]*\}/gi;
|
|
203
|
+
let fontMatch;
|
|
204
|
+
while ((fontMatch = fontFacePattern.exec(content)) !== null) {
|
|
205
|
+
const block = fontMatch[0];
|
|
206
|
+
const familyMatch = block.match(/font-family\s*:\s*["']?([^"';]+)["']?/i);
|
|
207
|
+
const srcMatch = block.match(/src\s*:\s*url\(["']?([^"')]+)["']?\)/i);
|
|
208
|
+
const displayMatch = block.match(/font-display\s*:\s*(\w+)/i);
|
|
209
|
+
const formatMatch = block.match(/format\(["']([^"']+)["']\)/i);
|
|
210
|
+
const family = familyMatch ? familyMatch[1].trim() : "unknown";
|
|
211
|
+
const source = srcMatch ? srcMatch[1].trim() : "inline";
|
|
212
|
+
const hasDisplay = !!displayMatch;
|
|
213
|
+
const format = formatMatch ? formatMatch[1] : source.match(/\.(\w+)$/)?.[1] ?? "unknown";
|
|
214
|
+
const isSubset = block.includes("unicode-range");
|
|
215
|
+
fonts.push({ family, source, hasDisplay, isSubset, format });
|
|
216
|
+
}
|
|
217
|
+
// Find Google Fonts links
|
|
218
|
+
const googleFontPattern = /<link[^>]*fonts\.googleapis\.com[^>]*>/gi;
|
|
219
|
+
let gfMatch;
|
|
220
|
+
while ((gfMatch = googleFontPattern.exec(content)) !== null) {
|
|
221
|
+
const tag = gfMatch[0];
|
|
222
|
+
const familyMatch = tag.match(/family=([^&"']+)/);
|
|
223
|
+
const displayMatch = tag.match(/display=(\w+)/);
|
|
224
|
+
fonts.push({
|
|
225
|
+
family: familyMatch ? decodeURIComponent(familyMatch[1]).replace(/\+/g, " ") : "Google Font",
|
|
226
|
+
source: "Google Fonts",
|
|
227
|
+
hasDisplay: !!displayMatch,
|
|
228
|
+
isSubset: tag.includes("text="),
|
|
229
|
+
format: "woff2",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const hasFontDisplay = fonts.every((f) => f.hasDisplay);
|
|
233
|
+
if (!hasFontDisplay) {
|
|
234
|
+
recommendations.push("Add 'font-display: swap' to @font-face declarations to prevent invisible text during load.");
|
|
235
|
+
}
|
|
236
|
+
if (fonts.some((f) => f.format === "ttf" || f.format === "otf")) {
|
|
237
|
+
recommendations.push("Use WOFF2 format instead of TTF/OTF for better compression (30%+ smaller).");
|
|
238
|
+
}
|
|
239
|
+
if (fonts.length > 0 && !fonts.some((f) => f.isSubset)) {
|
|
240
|
+
recommendations.push("Consider subsetting fonts with unicode-range to reduce download size for large character sets.");
|
|
241
|
+
}
|
|
242
|
+
if (fonts.length === 0) {
|
|
243
|
+
recommendations.push("No custom fonts detected — using system fonts is excellent for performance.");
|
|
244
|
+
}
|
|
245
|
+
let score = 100;
|
|
246
|
+
if (!hasFontDisplay)
|
|
247
|
+
score -= 40;
|
|
248
|
+
if (fonts.some((f) => f.format === "ttf" || f.format === "otf"))
|
|
249
|
+
score -= 20;
|
|
250
|
+
return {
|
|
251
|
+
url: fetchResult.finalUrl || url,
|
|
252
|
+
fontsFound: fonts,
|
|
253
|
+
hasFontDisplay,
|
|
254
|
+
recommendations,
|
|
255
|
+
score,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preload / Speculation Rules / bfcache analyzer.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Python `preload_check.py` in claude-seo. Audits static HTML
|
|
5
|
+
* and response headers for the four preload mechanisms Google rewards in
|
|
6
|
+
* 2025-2026: speculation rules, preload hints, deprecated prerender, and
|
|
7
|
+
* bfcache signals.
|
|
8
|
+
*/
|
|
9
|
+
import type { PreloadAuditResult } from "../types.js";
|
|
10
|
+
import type { FetchResult } from "../types.js";
|
|
11
|
+
export declare function auditPreload(fetchResult: FetchResult): PreloadAuditResult;
|
|
12
|
+
//# sourceMappingURL=preload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/preload.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAsB/C,wBAAgB,YAAY,CAAC,WAAW,EAAE,WAAW,GAAG,kBAAkB,CAsGzE"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const SPECULATION_BLOCK_RE = /<script\b[^>]*\btype\s*=\s*["']speculationrules["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
2
|
+
const PRELOAD_LINK_RE = /<link\b[^>]*\brel\s*=\s*["']preload["'][^>]*>/gi;
|
|
3
|
+
const PRERENDER_LINK_RE = /<link\b[^>]*\brel\s*=\s*["']prerender["'][^>]*>/gi;
|
|
4
|
+
const FETCHPRIORITY_HIGH_RE = /\bfetchpriority\s*=\s*["']high["']/gi;
|
|
5
|
+
const LCP_IMG_HINT_RE = /<(?:img|video|source)\b[^>]*\bfetchpriority\s*=\s*["']high["']/gi;
|
|
6
|
+
const UNLOAD_LISTENER_RE = /(?:addEventListener\s*\(\s*["']?unload["']?|\.onunload\s*=|on\s*\(\s*["']?unload["'])/i;
|
|
7
|
+
const BEFOREUNLOAD_LISTENER_RE = /(?:addEventListener\s*\(\s*["']?beforeunload["']?|\.onbeforeunload\s*=|on\s*\(\s*["']?beforeunload["'])/i;
|
|
8
|
+
function extractSpeculationActions(body) {
|
|
9
|
+
const actions = new Set();
|
|
10
|
+
try {
|
|
11
|
+
const payload = JSON.parse(body.trim());
|
|
12
|
+
if (Array.isArray(payload.prefetch))
|
|
13
|
+
actions.add("prefetch");
|
|
14
|
+
if (Array.isArray(payload.prerender))
|
|
15
|
+
actions.add("prerender");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// invalid JSON
|
|
19
|
+
}
|
|
20
|
+
return [...actions].sort();
|
|
21
|
+
}
|
|
22
|
+
export function auditPreload(fetchResult) {
|
|
23
|
+
const html = fetchResult.content;
|
|
24
|
+
const headers = fetchResult.headers;
|
|
25
|
+
// Speculation rules.
|
|
26
|
+
const speculationBlocks = [];
|
|
27
|
+
let match;
|
|
28
|
+
while ((match = SPECULATION_BLOCK_RE.exec(html)) !== null) {
|
|
29
|
+
speculationBlocks.push(match[1]);
|
|
30
|
+
}
|
|
31
|
+
const actions = new Set();
|
|
32
|
+
for (const block of speculationBlocks) {
|
|
33
|
+
for (const a of extractSpeculationActions(block)) {
|
|
34
|
+
actions.add(a);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const speculationHeader = Object.keys(headers).some((k) => k.toLowerCase() === "speculation-rules");
|
|
38
|
+
// Preload hints.
|
|
39
|
+
const preloadMatches = html.match(PRELOAD_LINK_RE) ?? [];
|
|
40
|
+
const preloadHints = preloadMatches.length;
|
|
41
|
+
// Deprecated prerender links.
|
|
42
|
+
const prerenderMatches = html.match(PRERENDER_LINK_RE) ?? [];
|
|
43
|
+
const prerenderLinks = prerenderMatches.length;
|
|
44
|
+
// bfcache signals.
|
|
45
|
+
const ccValue = Object.entries(headers).find(([k]) => k.toLowerCase() === "cache-control")?.[1] ?? "";
|
|
46
|
+
const cacheControlNoStore = ccValue.toLowerCase().includes("no-store");
|
|
47
|
+
const hasUnload = UNLOAD_LISTENER_RE.test(html);
|
|
48
|
+
const hasBeforeunload = BEFOREUNLOAD_LISTENER_RE.test(html);
|
|
49
|
+
// LCP resource hints.
|
|
50
|
+
const fetchpriorityMatches = html.match(FETCHPRIORITY_HIGH_RE) ?? [];
|
|
51
|
+
const lcpImgHint = LCP_IMG_HINT_RE.test(html);
|
|
52
|
+
// Score.
|
|
53
|
+
let score = 0;
|
|
54
|
+
const recommendations = [];
|
|
55
|
+
if (speculationBlocks.length > 0 || speculationHeader) {
|
|
56
|
+
score += 25;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
recommendations.push('Add <script type="speculationrules"> for prefetch+prerender on top user-paths. Saves entire next-navigation paint cost.');
|
|
60
|
+
}
|
|
61
|
+
if (lcpImgHint) {
|
|
62
|
+
score += 25;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
recommendations.push('Mark the LCP hero image with fetchpriority="high" so the browser preloads it ahead of other resources.');
|
|
66
|
+
}
|
|
67
|
+
if (!cacheControlNoStore && !hasUnload) {
|
|
68
|
+
score += 25;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (cacheControlNoStore) {
|
|
72
|
+
recommendations.push('Remove "Cache-Control: no-store" to enable bfcache restoration on back/forward navigation.');
|
|
73
|
+
}
|
|
74
|
+
if (hasUnload) {
|
|
75
|
+
recommendations.push('Remove "unload" event listeners (use "pagehide" instead) — they block bfcache.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (preloadHints > 0) {
|
|
79
|
+
score += 25;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
recommendations.push('Add <link rel="preload"> for critical resources (fonts, CSS, hero image) to speed up LCP.');
|
|
83
|
+
}
|
|
84
|
+
if (prerenderLinks > 0) {
|
|
85
|
+
recommendations.push('Replace deprecated <link rel="prerender"> with Speculation Rules API for modern prerendering.');
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
url: fetchResult.finalUrl,
|
|
89
|
+
speculationRules: {
|
|
90
|
+
inlineBlocks: speculationBlocks.length,
|
|
91
|
+
headerPresent: speculationHeader,
|
|
92
|
+
actions: [...actions],
|
|
93
|
+
},
|
|
94
|
+
preloadHints,
|
|
95
|
+
prerenderLinks,
|
|
96
|
+
bfcacheSignals: {
|
|
97
|
+
cacheControlNoStore,
|
|
98
|
+
unloadListener: hasUnload,
|
|
99
|
+
beforeunloadListener: hasBeforeunload,
|
|
100
|
+
},
|
|
101
|
+
lcpResourceHints: {
|
|
102
|
+
preloadLcpCandidate: lcpImgHint,
|
|
103
|
+
fetchpriorityHigh: fetchpriorityMatches.length,
|
|
104
|
+
},
|
|
105
|
+
score,
|
|
106
|
+
recommendations,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readability.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/readability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAuCrD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAyEhF;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAwBtE"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
function countSentences(text) {
|
|
2
|
+
const matches = text.match(/[.!?]+/g);
|
|
3
|
+
return matches ? matches.length : 1;
|
|
4
|
+
}
|
|
5
|
+
function countSyllables(word) {
|
|
6
|
+
const lower = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
7
|
+
if (lower.length <= 3)
|
|
8
|
+
return 1;
|
|
9
|
+
let count = 0;
|
|
10
|
+
const vowels = "aeiouy";
|
|
11
|
+
let prevWasVowel = false;
|
|
12
|
+
for (let i = 0; i < lower.length; i++) {
|
|
13
|
+
const isVowel = vowels.includes(lower[i]);
|
|
14
|
+
if (isVowel && !prevWasVowel) {
|
|
15
|
+
count++;
|
|
16
|
+
}
|
|
17
|
+
prevWasVowel = isVowel;
|
|
18
|
+
}
|
|
19
|
+
// Silent e at end
|
|
20
|
+
if (lower.endsWith("e") && count > 1)
|
|
21
|
+
count--;
|
|
22
|
+
// le at end adds a syllable
|
|
23
|
+
if (lower.endsWith("le") && lower.length > 3)
|
|
24
|
+
count++;
|
|
25
|
+
return Math.max(1, count);
|
|
26
|
+
}
|
|
27
|
+
function isComplexWord(word) {
|
|
28
|
+
return countSyllables(word) >= 3;
|
|
29
|
+
}
|
|
30
|
+
function countWords(text) {
|
|
31
|
+
return text.split(/\s+/).filter((w) => w.length > 0);
|
|
32
|
+
}
|
|
33
|
+
export function analyzeReadability(text, url) {
|
|
34
|
+
const words = countWords(text);
|
|
35
|
+
const wordCount = words.length;
|
|
36
|
+
const sentenceCount = countSentences(text);
|
|
37
|
+
if (wordCount === 0 || sentenceCount === 0) {
|
|
38
|
+
return {
|
|
39
|
+
url: url ?? "",
|
|
40
|
+
fleschKincaid: 0,
|
|
41
|
+
fleschReadingEase: 0,
|
|
42
|
+
smog: 0,
|
|
43
|
+
ari: 0,
|
|
44
|
+
colemanLiau: 0,
|
|
45
|
+
gradeAverage: 0,
|
|
46
|
+
sentenceCount: 0,
|
|
47
|
+
syllableCount: 0,
|
|
48
|
+
complexWordCount: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let totalSyllables = 0;
|
|
52
|
+
let complexWordCount = 0;
|
|
53
|
+
let totalLetters = 0;
|
|
54
|
+
for (const word of words) {
|
|
55
|
+
const clean = word.replace(/[^a-zA-Z]/g, "");
|
|
56
|
+
totalLetters += clean.length;
|
|
57
|
+
const syllables = countSyllables(clean);
|
|
58
|
+
totalSyllables += syllables;
|
|
59
|
+
if (isComplexWord(clean))
|
|
60
|
+
complexWordCount++;
|
|
61
|
+
}
|
|
62
|
+
// Flesch Reading Ease
|
|
63
|
+
const fleschReadingEase = Number((206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)).toFixed(1));
|
|
64
|
+
// Flesch-Kincaid Grade Level
|
|
65
|
+
const fleschKincaid = Number((0.39 * (wordCount / sentenceCount) + 11.8 * (totalSyllables / wordCount) - 15.59).toFixed(1));
|
|
66
|
+
// SMOG Index
|
|
67
|
+
const smog = complexWordCount > 0
|
|
68
|
+
? Number((1.043 * Math.sqrt(complexWordCount * (30 / sentenceCount)) + 3.1291).toFixed(1))
|
|
69
|
+
: 0;
|
|
70
|
+
// Automated Readability Index (ARI)
|
|
71
|
+
const ari = Number((4.71 * (totalLetters / wordCount) + 0.5 * (wordCount / sentenceCount) - 21.43).toFixed(1));
|
|
72
|
+
// Coleman-Liau Index
|
|
73
|
+
const L = (totalLetters / wordCount) * 100;
|
|
74
|
+
const S = (sentenceCount / wordCount) * 100;
|
|
75
|
+
const colemanLiau = Number((0.0588 * L - 0.296 * S - 15.8).toFixed(1));
|
|
76
|
+
const gradeAverage = Number(((fleschKincaid + smog + ari + colemanLiau) / 4).toFixed(1));
|
|
77
|
+
return {
|
|
78
|
+
url: url ?? "",
|
|
79
|
+
fleschKincaid: Number.isFinite(fleschKincaid) ? fleschKincaid : 0,
|
|
80
|
+
fleschReadingEase: Number.isFinite(fleschReadingEase) ? fleschReadingEase : 0,
|
|
81
|
+
smog: Number.isFinite(smog) ? smog : 0,
|
|
82
|
+
ari: Number.isFinite(ari) ? ari : 0,
|
|
83
|
+
colemanLiau: Number.isFinite(colemanLiau) ? colemanLiau : 0,
|
|
84
|
+
gradeAverage: Number.isFinite(gradeAverage) ? gradeAverage : 0,
|
|
85
|
+
sentenceCount,
|
|
86
|
+
syllableCount: totalSyllables,
|
|
87
|
+
complexWordCount,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function interpretReadability(result) {
|
|
91
|
+
const { fleschReadingEase, gradeAverage } = result;
|
|
92
|
+
let easeLabel;
|
|
93
|
+
if (fleschReadingEase >= 90)
|
|
94
|
+
easeLabel = "Very Easy (5th grade)";
|
|
95
|
+
else if (fleschReadingEase >= 80)
|
|
96
|
+
easeLabel = "Easy (6th grade)";
|
|
97
|
+
else if (fleschReadingEase >= 70)
|
|
98
|
+
easeLabel = "Fairly Easy (7th grade)";
|
|
99
|
+
else if (fleschReadingEase >= 60)
|
|
100
|
+
easeLabel = "Standard (8th-9th grade)";
|
|
101
|
+
else if (fleschReadingEase >= 50)
|
|
102
|
+
easeLabel = "Fairly Difficult (10th-12th grade)";
|
|
103
|
+
else if (fleschReadingEase >= 30)
|
|
104
|
+
easeLabel = "Difficult (College)";
|
|
105
|
+
else
|
|
106
|
+
easeLabel = "Very Difficult (College Graduate)";
|
|
107
|
+
let seoAdvice;
|
|
108
|
+
if (fleschReadingEase >= 60 && fleschReadingEase <= 70) {
|
|
109
|
+
seoAdvice = "Good readability for general web audiences. Aim for 60-70 Flesch Reading Ease.";
|
|
110
|
+
}
|
|
111
|
+
else if (fleschReadingEase < 50) {
|
|
112
|
+
seoAdvice = "Content may be too complex. Consider simplifying sentences and vocabulary for broader reach.";
|
|
113
|
+
}
|
|
114
|
+
else if (fleschReadingEase > 80) {
|
|
115
|
+
seoAdvice = "Content is very easy to read. Ensure it still conveys authority and depth for your topic.";
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
seoAdvice = "Readability is within an acceptable range.";
|
|
119
|
+
}
|
|
120
|
+
return `${easeLabel}. Average grade level: ${gradeAverage}. ${seoAdvice}`;
|
|
121
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema.org analyzer — detection, validation, and generation.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Python `schema_generate.py` and `seo-schema/SKILL.md` in
|
|
5
|
+
* claude-seo. Generates JSON-LD for high-leverage types and validates existing
|
|
6
|
+
* markup against Google's rich result requirements.
|
|
7
|
+
*/
|
|
8
|
+
import type { SchemaAuditResult } from "../types.js";
|
|
9
|
+
/** Audit schema markup on a page. */
|
|
10
|
+
export declare function auditSchema(html: string, url?: string): SchemaAuditResult;
|
|
11
|
+
export interface SchemaGenerateParams {
|
|
12
|
+
type: "Organization" | "LocalBusiness" | "Article" | "BlogPosting" | "Product" | "BreadcrumbList" | "WebSite" | "Person" | "ProfilePage" | "DiscussionForumPosting" | "OrderAction" | "Reservation";
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** Generate JSON-LD for the specified schema type. */
|
|
16
|
+
export declare function generateSchema(params: SchemaGenerateParams): {
|
|
17
|
+
jsonLd: unknown;
|
|
18
|
+
type: string;
|
|
19
|
+
required: string[];
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAe,MAAM,aAAa,CAAC;AA2FlE,qCAAqC;AACrC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA2DzE;AAID,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,GAAG,eAAe,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,GAC5E,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,GAAG,wBAAwB,GAClF,aAAa,GAAG,aAAa,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAsMlH"}
|