@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.
Files changed (144) hide show
  1. package/dist/extensions/aexol-mcp.d.ts.map +1 -1
  2. package/dist/extensions/aexol-mcp.js +44 -9
  3. package/dist/extensions/browser/browser-service.d.ts +4 -1
  4. package/dist/extensions/browser/browser-service.d.ts.map +1 -1
  5. package/dist/extensions/browser/browser-service.js +34 -7
  6. package/dist/extensions/browser/config.d.ts +3 -0
  7. package/dist/extensions/browser/config.d.ts.map +1 -1
  8. package/dist/extensions/browser/config.js +1 -0
  9. package/dist/extensions/browser/index.d.ts.map +1 -1
  10. package/dist/extensions/browser/index.js +3 -0
  11. package/dist/extensions/browser/record/record-tool.d.ts.map +1 -1
  12. package/dist/extensions/browser/record/record-tool.js +27 -4
  13. package/dist/extensions/browser/scenario/markdown-parser.d.ts +44 -0
  14. package/dist/extensions/browser/scenario/markdown-parser.d.ts.map +1 -0
  15. package/dist/extensions/browser/scenario/markdown-parser.js +159 -0
  16. package/dist/extensions/browser/scenario/scenario-tool.d.ts +4 -0
  17. package/dist/extensions/browser/scenario/scenario-tool.d.ts.map +1 -0
  18. package/dist/extensions/browser/scenario/scenario-tool.js +381 -0
  19. package/dist/extensions/browser/tools/core.js +1 -1
  20. package/dist/extensions/browser/tools/screenshot.d.ts.map +1 -1
  21. package/dist/extensions/browser/tools/screenshot.js +2 -1
  22. package/dist/extensions/seo/analyzers/agent-ux.d.ts +5 -0
  23. package/dist/extensions/seo/analyzers/agent-ux.d.ts.map +1 -0
  24. package/dist/extensions/seo/analyzers/agent-ux.js +95 -0
  25. package/dist/extensions/seo/analyzers/content-optimize.d.ts +3 -0
  26. package/dist/extensions/seo/analyzers/content-optimize.d.ts.map +1 -0
  27. package/dist/extensions/seo/analyzers/content-optimize.js +190 -0
  28. package/dist/extensions/seo/analyzers/content.d.ts +16 -0
  29. package/dist/extensions/seo/analyzers/content.d.ts.map +1 -0
  30. package/dist/extensions/seo/analyzers/content.js +174 -0
  31. package/dist/extensions/seo/analyzers/crawler.d.ts +4 -0
  32. package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -0
  33. package/dist/extensions/seo/analyzers/crawler.js +288 -0
  34. package/dist/extensions/seo/analyzers/drift.d.ts +13 -0
  35. package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -0
  36. package/dist/extensions/seo/analyzers/drift.js +204 -0
  37. package/dist/extensions/seo/analyzers/images.d.ts +10 -0
  38. package/dist/extensions/seo/analyzers/images.d.ts.map +1 -0
  39. package/dist/extensions/seo/analyzers/images.js +49 -0
  40. package/dist/extensions/seo/analyzers/keywords.d.ts +9 -0
  41. package/dist/extensions/seo/analyzers/keywords.d.ts.map +1 -0
  42. package/dist/extensions/seo/analyzers/keywords.js +183 -0
  43. package/dist/extensions/seo/analyzers/parasite.d.ts +18 -0
  44. package/dist/extensions/seo/analyzers/parasite.d.ts.map +1 -0
  45. package/dist/extensions/seo/analyzers/parasite.js +142 -0
  46. package/dist/extensions/seo/analyzers/performance.d.ts +6 -0
  47. package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -0
  48. package/dist/extensions/seo/analyzers/performance.js +257 -0
  49. package/dist/extensions/seo/analyzers/preload.d.ts +12 -0
  50. package/dist/extensions/seo/analyzers/preload.d.ts.map +1 -0
  51. package/dist/extensions/seo/analyzers/preload.js +108 -0
  52. package/dist/extensions/seo/analyzers/readability.d.ts +4 -0
  53. package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -0
  54. package/dist/extensions/seo/analyzers/readability.js +121 -0
  55. package/dist/extensions/seo/analyzers/schema.d.ts +21 -0
  56. package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -0
  57. package/dist/extensions/seo/analyzers/schema.js +324 -0
  58. package/dist/extensions/seo/analyzers/sitemap.d.ts +20 -0
  59. package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -0
  60. package/dist/extensions/seo/analyzers/sitemap.js +121 -0
  61. package/dist/extensions/seo/analyzers/technical.d.ts +11 -0
  62. package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -0
  63. package/dist/extensions/seo/analyzers/technical.js +268 -0
  64. package/dist/extensions/seo/config.d.ts +24 -0
  65. package/dist/extensions/seo/config.d.ts.map +1 -0
  66. package/dist/extensions/seo/config.js +48 -0
  67. package/dist/extensions/seo/index.d.ts +16 -0
  68. package/dist/extensions/seo/index.d.ts.map +1 -0
  69. package/dist/extensions/seo/index.js +126 -0
  70. package/dist/extensions/seo/tools/agent-ux.d.ts +7 -0
  71. package/dist/extensions/seo/tools/agent-ux.d.ts.map +1 -0
  72. package/dist/extensions/seo/tools/agent-ux.js +63 -0
  73. package/dist/extensions/seo/tools/batch.d.ts +4 -0
  74. package/dist/extensions/seo/tools/batch.d.ts.map +1 -0
  75. package/dist/extensions/seo/tools/batch.js +193 -0
  76. package/dist/extensions/seo/tools/content.d.ts +7 -0
  77. package/dist/extensions/seo/tools/content.d.ts.map +1 -0
  78. package/dist/extensions/seo/tools/content.js +55 -0
  79. package/dist/extensions/seo/tools/crawl.d.ts +4 -0
  80. package/dist/extensions/seo/tools/crawl.d.ts.map +1 -0
  81. package/dist/extensions/seo/tools/crawl.js +166 -0
  82. package/dist/extensions/seo/tools/drift.d.ts +7 -0
  83. package/dist/extensions/seo/tools/drift.d.ts.map +1 -0
  84. package/dist/extensions/seo/tools/drift.js +104 -0
  85. package/dist/extensions/seo/tools/fetch.d.ts +7 -0
  86. package/dist/extensions/seo/tools/fetch.d.ts.map +1 -0
  87. package/dist/extensions/seo/tools/fetch.js +123 -0
  88. package/dist/extensions/seo/tools/full-audit.d.ts +11 -0
  89. package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -0
  90. package/dist/extensions/seo/tools/full-audit.js +191 -0
  91. package/dist/extensions/seo/tools/helpers.d.ts +13 -0
  92. package/dist/extensions/seo/tools/helpers.d.ts.map +1 -0
  93. package/dist/extensions/seo/tools/helpers.js +24 -0
  94. package/dist/extensions/seo/tools/images.d.ts +7 -0
  95. package/dist/extensions/seo/tools/images.d.ts.map +1 -0
  96. package/dist/extensions/seo/tools/images.js +46 -0
  97. package/dist/extensions/seo/tools/keywords.d.ts +4 -0
  98. package/dist/extensions/seo/tools/keywords.d.ts.map +1 -0
  99. package/dist/extensions/seo/tools/keywords.js +234 -0
  100. package/dist/extensions/seo/tools/parasite.d.ts +7 -0
  101. package/dist/extensions/seo/tools/parasite.d.ts.map +1 -0
  102. package/dist/extensions/seo/tools/parasite.js +53 -0
  103. package/dist/extensions/seo/tools/parse.d.ts +7 -0
  104. package/dist/extensions/seo/tools/parse.d.ts.map +1 -0
  105. package/dist/extensions/seo/tools/parse.js +66 -0
  106. package/dist/extensions/seo/tools/performance.d.ts +4 -0
  107. package/dist/extensions/seo/tools/performance.d.ts.map +1 -0
  108. package/dist/extensions/seo/tools/performance.js +201 -0
  109. package/dist/extensions/seo/tools/preload.d.ts +7 -0
  110. package/dist/extensions/seo/tools/preload.d.ts.map +1 -0
  111. package/dist/extensions/seo/tools/preload.js +52 -0
  112. package/dist/extensions/seo/tools/report.d.ts +4 -0
  113. package/dist/extensions/seo/tools/report.d.ts.map +1 -0
  114. package/dist/extensions/seo/tools/report.js +167 -0
  115. package/dist/extensions/seo/tools/schema.d.ts +7 -0
  116. package/dist/extensions/seo/tools/schema.d.ts.map +1 -0
  117. package/dist/extensions/seo/tools/schema.js +105 -0
  118. package/dist/extensions/seo/tools/sitemap.d.ts +7 -0
  119. package/dist/extensions/seo/tools/sitemap.d.ts.map +1 -0
  120. package/dist/extensions/seo/tools/sitemap.js +85 -0
  121. package/dist/extensions/seo/tools/technical.d.ts +7 -0
  122. package/dist/extensions/seo/tools/technical.d.ts.map +1 -0
  123. package/dist/extensions/seo/tools/technical.js +62 -0
  124. package/dist/extensions/seo/types.d.ts +445 -0
  125. package/dist/extensions/seo/types.d.ts.map +1 -0
  126. package/dist/extensions/seo/types.js +7 -0
  127. package/dist/extensions/seo/utils/fetcher.d.ts +28 -0
  128. package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -0
  129. package/dist/extensions/seo/utils/fetcher.js +118 -0
  130. package/dist/extensions/seo/utils/parser.d.ts +8 -0
  131. package/dist/extensions/seo/utils/parser.d.ts.map +1 -0
  132. package/dist/extensions/seo/utils/parser.js +265 -0
  133. package/dist/extensions/seo/utils/renderer.d.ts +39 -0
  134. package/dist/extensions/seo/utils/renderer.d.ts.map +1 -0
  135. package/dist/extensions/seo/utils/renderer.js +190 -0
  136. package/dist/extensions/seo/utils/url-safety.d.ts +31 -0
  137. package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -0
  138. package/dist/extensions/seo/utils/url-safety.js +202 -0
  139. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  140. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +11 -0
  141. package/dist/server/handlers/paths-pick-directory.d.ts +0 -27
  142. package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
  143. package/dist/server/handlers/paths-pick-directory.js +86 -92
  144. 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,4 @@
1
+ import type { ReadabilityResult } from "../types.js";
2
+ export declare function analyzeReadability(text: string, url?: string): ReadabilityResult;
3
+ export declare function interpretReadability(result: ReadabilityResult): string;
4
+ //# sourceMappingURL=readability.d.ts.map
@@ -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"}