pagesight 0.3.0 → 0.4.0
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/package.json +1 -1
- package/src/tools/crux.ts +18 -16
- package/src/tools/metatags.ts +114 -14
- package/src/tools/pagespeed.ts +12 -3
package/package.json
CHANGED
package/src/tools/crux.ts
CHANGED
|
@@ -193,14 +193,15 @@ export function registerCruxTool(server: McpServer): void {
|
|
|
193
193
|
} catch (err) {
|
|
194
194
|
const msg = err instanceof Error ? err.message : String(err);
|
|
195
195
|
if (msg.includes("404")) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
const target = url ?? origin ?? "";
|
|
197
|
+
const lines = [`No CrUX data for ${target}.`, ""];
|
|
198
|
+
lines.push("CrUX requires sufficient Chrome user traffic (roughly 1,000+ monthly visits).");
|
|
199
|
+
if (url) {
|
|
200
|
+
const originUrl = new URL(url).origin;
|
|
201
|
+
lines.push(`Try origin-level data instead: origin "${originUrl}"`);
|
|
202
|
+
}
|
|
203
|
+
lines.push("For lab metrics without traffic requirements, use the pagespeed tool.");
|
|
204
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
204
205
|
}
|
|
205
206
|
if (msg.includes("SERVICE_DISABLED") || msg.includes("API_KEY_SERVICE_BLOCKED")) {
|
|
206
207
|
return {
|
|
@@ -268,14 +269,15 @@ export function registerCruxTool(server: McpServer): void {
|
|
|
268
269
|
} catch (err) {
|
|
269
270
|
const msg = err instanceof Error ? err.message : String(err);
|
|
270
271
|
if (msg.includes("404")) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
272
|
+
const target = url ?? origin ?? "";
|
|
273
|
+
const lines = [`No CrUX history data for ${target}.`, ""];
|
|
274
|
+
lines.push("CrUX requires sufficient Chrome user traffic (roughly 1,000+ monthly visits).");
|
|
275
|
+
if (url) {
|
|
276
|
+
const originUrl = new URL(url).origin;
|
|
277
|
+
lines.push(`Try origin-level data instead: origin "${originUrl}"`);
|
|
278
|
+
}
|
|
279
|
+
lines.push("For lab metrics without traffic requirements, use the pagespeed tool.");
|
|
280
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
279
281
|
}
|
|
280
282
|
if (msg.includes("SERVICE_DISABLED")) {
|
|
281
283
|
return {
|
package/src/tools/metatags.ts
CHANGED
|
@@ -134,6 +134,77 @@ function formatJsonLd(data: unknown, indent = 0): string[] {
|
|
|
134
134
|
return lines;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
interface RedirectHop {
|
|
138
|
+
url: string;
|
|
139
|
+
status: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface ImageCheck {
|
|
143
|
+
url: string;
|
|
144
|
+
tag: string;
|
|
145
|
+
status: number | null;
|
|
146
|
+
contentType: string | null;
|
|
147
|
+
contentLength: number | null;
|
|
148
|
+
error: string | null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function followRedirects(
|
|
152
|
+
url: string,
|
|
153
|
+
ua: string,
|
|
154
|
+
maxHops = 10,
|
|
155
|
+
): Promise<{ chain: RedirectHop[]; response: Response }> {
|
|
156
|
+
const chain: RedirectHop[] = [];
|
|
157
|
+
let current = url;
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < maxHops; i++) {
|
|
160
|
+
const res = await fetch(current, {
|
|
161
|
+
headers: { "User-Agent": ua, Accept: "text/html" },
|
|
162
|
+
redirect: "manual",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
chain.push({ url: current, status: res.status });
|
|
166
|
+
|
|
167
|
+
if (res.status >= 300 && res.status < 400) {
|
|
168
|
+
const location = res.headers.get("location");
|
|
169
|
+
if (!location) break;
|
|
170
|
+
current = new URL(location, current).href;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { chain, response: res };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// If we exhausted hops, do a final follow-redirect fetch
|
|
178
|
+
const res = await fetch(current, {
|
|
179
|
+
headers: { "User-Agent": ua, Accept: "text/html" },
|
|
180
|
+
redirect: "follow",
|
|
181
|
+
});
|
|
182
|
+
return { chain, response: res };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function checkImage(imageUrl: string, tag: string): Promise<ImageCheck> {
|
|
186
|
+
try {
|
|
187
|
+
const res = await fetch(imageUrl, { method: "HEAD", redirect: "follow" });
|
|
188
|
+
return {
|
|
189
|
+
url: imageUrl,
|
|
190
|
+
tag,
|
|
191
|
+
status: res.status,
|
|
192
|
+
contentType: res.headers.get("content-type"),
|
|
193
|
+
contentLength: res.headers.has("content-length") ? Number(res.headers.get("content-length")) : null,
|
|
194
|
+
error: null,
|
|
195
|
+
};
|
|
196
|
+
} catch (err) {
|
|
197
|
+
return {
|
|
198
|
+
url: imageUrl,
|
|
199
|
+
tag,
|
|
200
|
+
status: null,
|
|
201
|
+
contentType: null,
|
|
202
|
+
contentLength: null,
|
|
203
|
+
error: err instanceof Error ? err.message : String(err),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
137
208
|
function formatMetatags(url: string, parsed: ParsedHead): string {
|
|
138
209
|
const lines: string[] = [`=== Meta Tags: ${url} ===`, ""];
|
|
139
210
|
|
|
@@ -231,13 +302,7 @@ export function registerMetatagsTool(server: McpServer): void {
|
|
|
231
302
|
async ({ url, user_agent }) => {
|
|
232
303
|
try {
|
|
233
304
|
const ua = user_agent ?? "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)";
|
|
234
|
-
const res = await
|
|
235
|
-
headers: {
|
|
236
|
-
"User-Agent": ua,
|
|
237
|
-
Accept: "text/html",
|
|
238
|
-
},
|
|
239
|
-
redirect: "follow",
|
|
240
|
-
});
|
|
305
|
+
const { chain, response: res } = await followRedirects(url, ua);
|
|
241
306
|
|
|
242
307
|
if (!res.ok) {
|
|
243
308
|
return {
|
|
@@ -252,15 +317,50 @@ export function registerMetatagsTool(server: McpServer): void {
|
|
|
252
317
|
|
|
253
318
|
const html = await res.text();
|
|
254
319
|
const parsed = parseHead(html);
|
|
255
|
-
const
|
|
320
|
+
const output: string[] = [];
|
|
321
|
+
|
|
322
|
+
// Redirect chain (only if there were redirects)
|
|
323
|
+
if (chain.length > 1) {
|
|
324
|
+
output.push("--- Redirect Chain ---", "");
|
|
325
|
+
for (let i = 0; i < chain.length; i++) {
|
|
326
|
+
const hop = chain[i];
|
|
327
|
+
const prefix = i === chain.length - 1 ? "" : `${hop.status} → `;
|
|
328
|
+
output.push(`${i + 1}. ${prefix}${hop.url}`);
|
|
329
|
+
}
|
|
330
|
+
output.push("");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Main meta tag report
|
|
334
|
+
const finalUrl = chain.length > 1 ? chain[chain.length - 1].url : url;
|
|
335
|
+
output.push(formatMetatags(finalUrl, parsed));
|
|
336
|
+
|
|
337
|
+
// OG/Twitter image validation
|
|
338
|
+
const ogImage = getMeta(parsed.meta, "og:image");
|
|
339
|
+
const twitterImage = getMeta(parsed.meta, "twitter:image");
|
|
340
|
+
const imagesToCheck: Array<{ url: string; tag: string }> = [];
|
|
341
|
+
if (ogImage) imagesToCheck.push({ url: ogImage, tag: "og:image" });
|
|
342
|
+
if (twitterImage && twitterImage !== ogImage) imagesToCheck.push({ url: twitterImage, tag: "twitter:image" });
|
|
343
|
+
|
|
344
|
+
if (imagesToCheck.length > 0) {
|
|
345
|
+
const checks = await Promise.all(imagesToCheck.map((img) => checkImage(img.url, img.tag)));
|
|
346
|
+
output.push("", "--- Image Validation ---", "");
|
|
347
|
+
for (const check of checks) {
|
|
348
|
+
if (check.error) {
|
|
349
|
+
output.push(`${check.tag}: FAILED — ${check.error}`);
|
|
350
|
+
output.push(` URL: ${check.url}`);
|
|
351
|
+
} else if (check.status && check.status >= 400) {
|
|
352
|
+
output.push(`${check.tag}: BROKEN — HTTP ${check.status}`);
|
|
353
|
+
output.push(` URL: ${check.url}`);
|
|
354
|
+
} else {
|
|
355
|
+
const size = check.contentLength ? ` (${Math.round(check.contentLength / 1024)} KB)` : "";
|
|
356
|
+
const type = check.contentType ? ` ${check.contentType}` : "";
|
|
357
|
+
output.push(`${check.tag}: OK —${type}${size}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
256
361
|
|
|
257
362
|
return {
|
|
258
|
-
content: [
|
|
259
|
-
{
|
|
260
|
-
type: "text",
|
|
261
|
-
text: `${finalUrl}${formatMetatags(res.url, parsed)}`,
|
|
262
|
-
},
|
|
263
|
-
],
|
|
363
|
+
content: [{ type: "text", text: output.join("\n") }],
|
|
264
364
|
};
|
|
265
365
|
} catch (err) {
|
|
266
366
|
const msg = err instanceof Error ? err.message : String(err);
|
package/src/tools/pagespeed.ts
CHANGED
|
@@ -52,8 +52,12 @@ function formatOpportunities(audits: Record<string, PsiAudit>): string[] {
|
|
|
52
52
|
const opportunities: PsiAudit[] = [];
|
|
53
53
|
|
|
54
54
|
for (const audit of Object.values(audits)) {
|
|
55
|
-
if (audit.score
|
|
56
|
-
|
|
55
|
+
if (audit.score === null || audit.score >= 1) continue;
|
|
56
|
+
const mode = audit.scoreDisplayMode;
|
|
57
|
+
const hasItems = (audit.details?.items?.length ?? 0) > 0;
|
|
58
|
+
const hasNumeric = audit.numericValue && audit.numericValue > 0;
|
|
59
|
+
if (mode === "metricSavings" || ((mode === "numeric" || mode === "binary") && hasNumeric)) {
|
|
60
|
+
if (hasNumeric || hasItems) {
|
|
57
61
|
opportunities.push(audit);
|
|
58
62
|
}
|
|
59
63
|
}
|
|
@@ -90,7 +94,12 @@ function formatDiagnostics(audits: Record<string, PsiAudit>): string[] {
|
|
|
90
94
|
const failing: Array<{ title: string; displayValue: string }> = [];
|
|
91
95
|
|
|
92
96
|
for (const audit of Object.values(audits)) {
|
|
93
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
audit.score !== null &&
|
|
99
|
+
audit.score < 0.5 &&
|
|
100
|
+
(audit.scoreDisplayMode === "numeric" || audit.scoreDisplayMode === "metricSavings") &&
|
|
101
|
+
audit.displayValue
|
|
102
|
+
) {
|
|
94
103
|
failing.push({ title: audit.title, displayValue: audit.displayValue });
|
|
95
104
|
}
|
|
96
105
|
}
|