@automatelab/ai-seo-mcp 0.3.0 → 0.3.4
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/LICENSE +21 -21
- package/README.md +14 -1
- package/dist/cli/audit-cli.d.ts +3 -0
- package/dist/cli/audit-cli.d.ts.map +1 -0
- package/dist/cli/audit-cli.js +136 -0
- package/dist/cli/audit-cli.js.map +1 -0
- package/dist/index.js +246 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/cache.d.ts +12 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/cache.js +51 -0
- package/dist/lib/cache.js.map +1 -0
- package/dist/lib/config.js +1 -1
- package/dist/lib/fetch.d.ts +5 -0
- package/dist/lib/fetch.d.ts.map +1 -1
- package/dist/lib/fetch.js +42 -1
- package/dist/lib/fetch.js.map +1 -1
- package/dist/lib/headless.d.ts +13 -0
- package/dist/lib/headless.d.ts.map +1 -0
- package/dist/lib/headless.js +73 -0
- package/dist/lib/headless.js.map +1 -0
- package/dist/lib/scorecard-html.js +94 -94
- package/dist/tools/audit-page.d.ts +11 -0
- package/dist/tools/audit-page.d.ts.map +1 -1
- package/dist/tools/audit-page.js +49 -2
- package/dist/tools/audit-page.js.map +1 -1
- package/dist/tools/audit-schema.d.ts +2 -1
- package/dist/tools/audit-schema.d.ts.map +1 -1
- package/dist/tools/audit-schema.js +2 -1
- package/dist/tools/audit-schema.js.map +1 -1
- package/dist/tools/audit-site.d.ts +41 -0
- package/dist/tools/audit-site.d.ts.map +1 -0
- package/dist/tools/audit-site.js +113 -0
- package/dist/tools/audit-site.js.map +1 -0
- package/dist/tools/audit-sitemap.d.ts +66 -0
- package/dist/tools/audit-sitemap.d.ts.map +1 -0
- package/dist/tools/audit-sitemap.js +264 -0
- package/dist/tools/audit-sitemap.js.map +1 -0
- package/dist/tools/check-technical.d.ts +2 -1
- package/dist/tools/check-technical.d.ts.map +1 -1
- package/dist/tools/check-technical.js +2 -1
- package/dist/tools/check-technical.js.map +1 -1
- package/dist/tools/diff-pages.js +2 -2
- package/dist/tools/diff-pages.js.map +1 -1
- package/dist/tools/extract-entities.d.ts +8 -1
- package/dist/tools/extract-entities.d.ts.map +1 -1
- package/dist/tools/extract-entities.js +110 -3
- package/dist/tools/extract-entities.js.map +1 -1
- package/dist/tools/rewrite-for-aeo.js +28 -28
- package/dist/tools/rewrite-for-geo.js +28 -28
- package/dist/tools/save-audit-report.d.ts +22 -0
- package/dist/tools/save-audit-report.d.ts.map +1 -0
- package/dist/tools/save-audit-report.js +133 -0
- package/dist/tools/save-audit-report.js.map +1 -0
- package/dist/tools/test-citation.d.ts +51 -0
- package/dist/tools/test-citation.d.ts.map +1 -0
- package/dist/tools/test-citation.js +159 -0
- package/dist/tools/test-citation.js.map +1 -0
- package/package.json +7 -1
- package/src/lib/crawlers.json +6 -6
- package/src/lib/schema-types.json +56 -56
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/lib/cache.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,oFAAoF;AACpF,yEAAyE;AAIzE,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,MAAM;IACjD,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,IAAI,EAAE,CAAC;IACjE,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;CAC1D,CAAC;AAOX,oFAAoF;AACpF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;AAI5C,SAAS,OAAO,CAAC,GAAW,EAAE,IAAgB;IAC5C,OAAO,GAAG,IAAI,KAAK,GAAG,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW,EAAE,OAAmB,QAAQ;IAC/D,IAAI,kBAAkB,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,uBAAuB;IACvB,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAClB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtB,OAAO,KAAK,CAAC,MAAM,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW,EAAE,MAAmB,EAAE,OAAmB,QAAQ;IACpF,IAAI,kBAAkB,CAAC,QAAQ;QAAE,OAAO;IACxC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/B,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;QACb,MAAM;QACN,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,kBAAkB,CAAC,MAAM;KAClD,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,GAAG,kBAAkB,CAAC,WAAW,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;QACzC,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM;QAChC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC"}
|
package/dist/lib/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// All values are read once at server startup.
|
|
3
3
|
export const POLITE_FETCH = {
|
|
4
4
|
USER_AGENT: process.env["USER_AGENT"] ??
|
|
5
|
-
"automatelab-ai-seo-mcp/0.
|
|
5
|
+
"automatelab-ai-seo-mcp/0.3.4 (+https://github.com/AutomateLab-tech/ai-seo-mcp)",
|
|
6
6
|
TIMEOUT_MS: Number(process.env["FETCH_TIMEOUT_MS"] ?? 15_000),
|
|
7
7
|
MAX_BYTES: Number(process.env["MAX_BYTES"] ?? 5 * 1024 * 1024), // 5MB
|
|
8
8
|
INTER_REQUEST_DELAY_MS: Number(process.env["INTER_REQUEST_DELAY_MS"] ?? 1_500),
|
package/dist/lib/fetch.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RenderMode } from "./cache.js";
|
|
1
2
|
import type { ToolError } from "../types.js";
|
|
2
3
|
export type HostDelayMap = Map<string, number>;
|
|
3
4
|
export interface FetchOptions {
|
|
@@ -9,6 +10,10 @@ export interface FetchOptions {
|
|
|
9
10
|
robotsCache?: Map<string, string>;
|
|
10
11
|
/** Allow redirects up to this many hops. Default 5 (handled by fetch internally). */
|
|
11
12
|
maxRedirects?: number;
|
|
13
|
+
/** Render mode for cache keying. Default "static". */
|
|
14
|
+
renderMode?: RenderMode;
|
|
15
|
+
/** Skip the in-memory fetch cache for this call. Robots/delays still apply. */
|
|
16
|
+
noCache?: boolean;
|
|
12
17
|
}
|
|
13
18
|
export interface FetchResult {
|
|
14
19
|
body: string;
|
package/dist/lib/fetch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/lib/fetch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,kEAAkE;IAClE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/lib/fetch.ts"],"names":[],"mappings":"AAMA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,kEAAkE;IAClE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,qBAAa,cAAe,SAAQ,KAAK;aACX,SAAS,EAAE,SAAS;gBAApB,SAAS,EAAE,SAAS;CAOjD;AAeD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,YAAiB,GACtB,OAAO,CAAC,WAAW,CAAC,CAgLtB;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,YAAiB,GACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6BxB"}
|
package/dist/lib/fetch.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { fetch as undiciFetch } from "undici";
|
|
4
4
|
import { POLITE_FETCH } from "./config.js";
|
|
5
5
|
import { checkRobotsAllowed, fetchRobotsTxt } from "./robots.js";
|
|
6
|
+
import { cacheGet, cacheSet } from "./cache.js";
|
|
7
|
+
import { renderHeadless, HeadlessUnavailableError } from "./headless.js";
|
|
6
8
|
export class ToolFetchError extends Error {
|
|
7
9
|
toolError;
|
|
8
10
|
constructor(toolError) {
|
|
@@ -42,6 +44,15 @@ export async function politeFetch(url, opts = {}) {
|
|
|
42
44
|
}
|
|
43
45
|
const hostname = parsedUrl.hostname;
|
|
44
46
|
const respectRobots = opts.respectRobots !== undefined ? opts.respectRobots : POLITE_FETCH.RESPECT_ROBOTS;
|
|
47
|
+
const renderMode = opts.renderMode ?? "static";
|
|
48
|
+
// Cache lookup. Cached entries bypass robots check, rate limiting, and the network call.
|
|
49
|
+
if (!opts.noCache) {
|
|
50
|
+
const hit = cacheGet(url, renderMode);
|
|
51
|
+
if (hit) {
|
|
52
|
+
console.error(`[fetch] cache hit ${url}`);
|
|
53
|
+
return hit;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
// Check inter-request delay
|
|
46
57
|
if (opts.hostDelays) {
|
|
47
58
|
const lastRequest = opts.hostDelays.get(hostname);
|
|
@@ -84,6 +95,32 @@ export async function politeFetch(url, opts = {}) {
|
|
|
84
95
|
opts.hostDelays.set(hostname, Date.now());
|
|
85
96
|
}
|
|
86
97
|
const startTime = Date.now();
|
|
98
|
+
// Headless rendering branch: hand off to Playwright, skip undici entirely.
|
|
99
|
+
if (renderMode === "headless") {
|
|
100
|
+
console.error(`[fetch] GET (headless) ${url}`);
|
|
101
|
+
try {
|
|
102
|
+
const rendered = await renderHeadless(url);
|
|
103
|
+
const duration = Date.now() - startTime;
|
|
104
|
+
console.error(`[fetch] ${rendered.statusCode} (headless) ${url} (${duration}ms)`);
|
|
105
|
+
const headlessResult = {
|
|
106
|
+
body: rendered.body,
|
|
107
|
+
finalUrl: rendered.finalUrl,
|
|
108
|
+
statusCode: rendered.statusCode,
|
|
109
|
+
headers: rendered.headers,
|
|
110
|
+
redirected: rendered.redirected,
|
|
111
|
+
};
|
|
112
|
+
if (!opts.noCache)
|
|
113
|
+
cacheSet(url, headlessResult, renderMode);
|
|
114
|
+
return headlessResult;
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err instanceof HeadlessUnavailableError) {
|
|
118
|
+
throw new ToolFetchError({ type: "fetch_error", url, message: err.message });
|
|
119
|
+
}
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
throw new ToolFetchError({ type: "fetch_error", url, message: `headless render failed: ${msg}` });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
87
124
|
console.error(`[fetch] GET ${url}`);
|
|
88
125
|
try {
|
|
89
126
|
const response = await undiciFetch(url, {
|
|
@@ -127,13 +164,17 @@ export async function politeFetch(url, opts = {}) {
|
|
|
127
164
|
response.headers.forEach((value, key) => {
|
|
128
165
|
headers[key] = value;
|
|
129
166
|
});
|
|
130
|
-
|
|
167
|
+
const fetchResult = {
|
|
131
168
|
body,
|
|
132
169
|
finalUrl: response.url || url,
|
|
133
170
|
statusCode: response.status,
|
|
134
171
|
headers,
|
|
135
172
|
redirected: response.redirected,
|
|
136
173
|
};
|
|
174
|
+
if (!opts.noCache) {
|
|
175
|
+
cacheSet(url, fetchResult, renderMode);
|
|
176
|
+
}
|
|
177
|
+
return fetchResult;
|
|
137
178
|
}
|
|
138
179
|
catch (err) {
|
|
139
180
|
if (err instanceof ToolFetchError)
|
package/dist/lib/fetch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/lib/fetch.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,uFAAuF;AAEvF,OAAO,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/lib/fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface HeadlessResult {
|
|
2
|
+
body: string;
|
|
3
|
+
finalUrl: string;
|
|
4
|
+
statusCode: number;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
redirected: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class HeadlessUnavailableError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare function renderHeadless(url: string): Promise<HeadlessResult>;
|
|
12
|
+
export declare function closeHeadlessBrowser(): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=headless.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headless.d.ts","sourceRoot":"","sources":["../../src/lib/headless.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,EAAE,MAAM;CAI5B;AAuBD,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA8CzE;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAS1D"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Headless browser rendering via playwright-core. Optional dep, lazy-imported.
|
|
2
|
+
// Adds 3-10s per request and a ~50MB install; opt in via render: "headless".
|
|
3
|
+
import { POLITE_FETCH } from "./config.js";
|
|
4
|
+
export class HeadlessUnavailableError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "HeadlessUnavailableError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
let browserPromise = null;
|
|
11
|
+
async function getBrowser() {
|
|
12
|
+
if (browserPromise)
|
|
13
|
+
return browserPromise;
|
|
14
|
+
browserPromise = (async () => {
|
|
15
|
+
let pw;
|
|
16
|
+
try {
|
|
17
|
+
// Indirect specifier prevents TypeScript from requiring playwright-core types at compile time;
|
|
18
|
+
// the dep is an optional peer dependency installed only when users opt into headless mode.
|
|
19
|
+
const specifier = "playwright-core";
|
|
20
|
+
pw = (await import(specifier));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new HeadlessUnavailableError("playwright-core is not installed. Install it with `npm install playwright-core` then run `npx playwright install chromium` once.");
|
|
24
|
+
}
|
|
25
|
+
return pw.chromium.launch({ headless: true });
|
|
26
|
+
})();
|
|
27
|
+
return browserPromise;
|
|
28
|
+
}
|
|
29
|
+
export async function renderHeadless(url) {
|
|
30
|
+
const browser = (await getBrowser());
|
|
31
|
+
const context = await browser.newContext({ userAgent: POLITE_FETCH.USER_AGENT });
|
|
32
|
+
const page = await context.newPage();
|
|
33
|
+
try {
|
|
34
|
+
const response = await page.goto(url, {
|
|
35
|
+
waitUntil: "networkidle",
|
|
36
|
+
timeout: POLITE_FETCH.TIMEOUT_MS,
|
|
37
|
+
});
|
|
38
|
+
if (!response) {
|
|
39
|
+
throw new Error(`No response from ${url}`);
|
|
40
|
+
}
|
|
41
|
+
const status = response.status();
|
|
42
|
+
if (status >= 400) {
|
|
43
|
+
throw new Error(`HTTP ${status}`);
|
|
44
|
+
}
|
|
45
|
+
const body = await page.content();
|
|
46
|
+
const finalUrl = response.url();
|
|
47
|
+
const headers = response.headers();
|
|
48
|
+
return {
|
|
49
|
+
body,
|
|
50
|
+
finalUrl,
|
|
51
|
+
statusCode: status,
|
|
52
|
+
headers,
|
|
53
|
+
redirected: finalUrl !== url,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await page.close();
|
|
58
|
+
await context.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function closeHeadlessBrowser() {
|
|
62
|
+
if (!browserPromise)
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
const browser = (await browserPromise);
|
|
66
|
+
await browser.close();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// ignore
|
|
70
|
+
}
|
|
71
|
+
browserPromise = null;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=headless.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headless.js","sourceRoot":"","sources":["../../src/lib/headless.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,6EAA6E;AAE7E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAU3C,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,IAAI,cAAc,GAA4B,IAAI,CAAC;AAEnD,KAAK,UAAU,UAAU;IACvB,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAC1C,cAAc,GAAG,CAAC,KAAK,IAAI,EAAE;QAC3B,IAAI,EAAgE,CAAC;QACrE,IAAI,CAAC;YACH,+FAA+F;YAC/F,2FAA2F;YAC3F,MAAM,SAAS,GAAG,iBAAiB,CAAC;YACpC,EAAE,GAAG,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,CAAyB,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,wBAAwB,CAChC,kIAAkI,CACnI,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC,CAAC,EAAE,CAAC;IACL,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,MAAM,OAAO,GAAG,CAAC,MAAM,UAAU,EAAE,CAgBlC,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,YAAY,CAAC,UAAU,EAAE,CAAC,CAAC;IACjF,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;YACpC,SAAS,EAAE,aAAa;YACxB,OAAO,EAAE,YAAY,CAAC,UAAU;SACjC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,oBAAoB,GAAG,EAAE,CAAC,CAAC;QAC7C,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;QACjC,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;QACnC,OAAO;YACL,IAAI;YACJ,QAAQ;YACR,UAAU,EAAE,MAAM;YAClB,OAAO;YACP,UAAU,EAAE,QAAQ,KAAK,GAAG;SAC7B,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,IAAI,CAAC,cAAc;QAAE,OAAO;IAC5B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,CAAC,MAAM,cAAc,CAAmC,CAAC;QACzE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;IACD,cAAc,GAAG,IAAI,CAAC;AACxB,CAAC"}
|
|
@@ -54,13 +54,13 @@ export function renderScorecardHtml(input) {
|
|
|
54
54
|
.map(([key, val]) => {
|
|
55
55
|
const label = DIMENSION_LABELS[key] ?? key;
|
|
56
56
|
const color = barColor(val);
|
|
57
|
-
return `
|
|
58
|
-
<div class="dim-row">
|
|
59
|
-
<span class="dim-label">${escHtml(label)}</span>
|
|
60
|
-
<div class="bar-track">
|
|
61
|
-
<div class="bar-fill" style="width:${val}%;background:${color};"></div>
|
|
62
|
-
</div>
|
|
63
|
-
<span class="dim-score">${val}</span>
|
|
57
|
+
return `
|
|
58
|
+
<div class="dim-row">
|
|
59
|
+
<span class="dim-label">${escHtml(label)}</span>
|
|
60
|
+
<div class="bar-track">
|
|
61
|
+
<div class="bar-fill" style="width:${val}%;background:${color};"></div>
|
|
62
|
+
</div>
|
|
63
|
+
<span class="dim-score">${val}</span>
|
|
64
64
|
</div>`;
|
|
65
65
|
})
|
|
66
66
|
.join("");
|
|
@@ -69,99 +69,99 @@ export function renderScorecardHtml(input) {
|
|
|
69
69
|
const impactBadge = b.estimated_impact
|
|
70
70
|
? `<span class="badge badge-${b.estimated_impact}">${escHtml(b.estimated_impact)}</span>`
|
|
71
71
|
: "";
|
|
72
|
-
return `
|
|
73
|
-
<div class="blocker">
|
|
74
|
-
<div class="blocker-header">
|
|
75
|
-
<span class="blocker-num">${i + 1}</span>
|
|
76
|
-
<span class="blocker-cat">${escHtml(b.category)}</span>
|
|
77
|
-
${impactBadge}
|
|
78
|
-
</div>
|
|
79
|
-
<p class="blocker-msg">${escHtml(b.message)}</p>
|
|
80
|
-
<p class="blocker-fix"><strong>Fix:</strong> ${escHtml(b.fix)}</p>
|
|
72
|
+
return `
|
|
73
|
+
<div class="blocker">
|
|
74
|
+
<div class="blocker-header">
|
|
75
|
+
<span class="blocker-num">${i + 1}</span>
|
|
76
|
+
<span class="blocker-cat">${escHtml(b.category)}</span>
|
|
77
|
+
${impactBadge}
|
|
78
|
+
</div>
|
|
79
|
+
<p class="blocker-msg">${escHtml(b.message)}</p>
|
|
80
|
+
<p class="blocker-fix"><strong>Fix:</strong> ${escHtml(b.fix)}</p>
|
|
81
81
|
</div>`;
|
|
82
82
|
})
|
|
83
83
|
.join("");
|
|
84
84
|
const noBlockers = citation_verdict.top_3_blockers.length === 0
|
|
85
85
|
? `<p class="no-blockers">No critical blockers found.</p>`
|
|
86
86
|
: "";
|
|
87
|
-
return `<!DOCTYPE html>
|
|
88
|
-
<html lang="en">
|
|
89
|
-
<head>
|
|
90
|
-
<meta charset="UTF-8">
|
|
91
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
92
|
-
<title>AI-SEO Scorecard - ${escHtml(url)}</title>
|
|
93
|
-
<style>
|
|
94
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
95
|
-
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;padding:2rem 1rem}
|
|
96
|
-
.card{max-width:700px;margin:0 auto;background:#1e293b;border-radius:12px;overflow:hidden;box-shadow:0 4px 32px rgba(0,0,0,.4)}
|
|
97
|
-
.header{padding:2rem;border-bottom:1px solid #334155}
|
|
98
|
-
.header-url{font-size:.85rem;color:#94a3b8;word-break:break-all;margin-bottom:.5rem}
|
|
99
|
-
.header-ts{font-size:.78rem;color:#64748b}
|
|
100
|
-
.grade-row{display:flex;align-items:center;gap:1.5rem;margin-top:1.25rem}
|
|
101
|
-
.grade-badge{font-size:3rem;font-weight:800;line-height:1;color:${gradeColor(grade)}}
|
|
102
|
-
.score-num{font-size:1.75rem;font-weight:700;color:#f1f5f9}
|
|
103
|
-
.score-label{font-size:.8rem;color:#94a3b8;margin-top:.2rem}
|
|
104
|
-
.verdict-pill{display:inline-block;padding:.35rem .9rem;border-radius:999px;font-size:.85rem;font-weight:600;background:${verdictColor(citation_verdict.will_ai_cite)}22;color:${verdictColor(citation_verdict.will_ai_cite)};border:1px solid ${verdictColor(citation_verdict.will_ai_cite)}44;margin-top:.5rem}
|
|
105
|
-
.summary{font-size:.9rem;color:#cbd5e1;margin-top:.75rem;line-height:1.5}
|
|
106
|
-
.section{padding:1.5rem 2rem}
|
|
107
|
-
.section + .section{border-top:1px solid #334155}
|
|
108
|
-
.section-title{font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#64748b;margin-bottom:1rem}
|
|
109
|
-
.dim-row{display:grid;grid-template-columns:110px 1fr 36px;align-items:center;gap:.75rem;margin-bottom:.6rem}
|
|
110
|
-
.dim-label{font-size:.82rem;color:#94a3b8;text-align:right}
|
|
111
|
-
.bar-track{height:8px;background:#0f172a;border-radius:4px;overflow:hidden}
|
|
112
|
-
.bar-fill{height:100%;border-radius:4px;transition:width .3s}
|
|
113
|
-
.dim-score{font-size:.82rem;font-weight:600;color:#e2e8f0;text-align:right}
|
|
114
|
-
.blocker{background:#0f172a;border-radius:8px;padding:1rem;margin-bottom:.75rem;border-left:3px solid #ef4444}
|
|
115
|
-
.blocker:last-child{margin-bottom:0}
|
|
116
|
-
.blocker-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}
|
|
117
|
-
.blocker-num{width:22px;height:22px;border-radius:50%;background:#ef4444;color:#fff;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
118
|
-
.blocker-cat{font-size:.8rem;font-weight:600;color:#cbd5e1;text-transform:capitalize}
|
|
119
|
-
.badge{padding:.15rem .5rem;border-radius:4px;font-size:.7rem;font-weight:600;text-transform:uppercase}
|
|
120
|
-
.badge-high{background:#ef444422;color:#ef4444}
|
|
121
|
-
.badge-medium{background:#f59e0b22;color:#f59e0b}
|
|
122
|
-
.badge-low{background:#22c55e22;color:#22c55e}
|
|
123
|
-
.blocker-msg{font-size:.82rem;color:#94a3b8;margin-bottom:.35rem;line-height:1.4}
|
|
124
|
-
.blocker-fix{font-size:.82rem;color:#64748b;line-height:1.4}
|
|
125
|
-
.blocker-fix strong{color:#94a3b8}
|
|
126
|
-
.no-blockers{font-size:.9rem;color:#22c55e}
|
|
127
|
-
.footer{padding:1rem 2rem;text-align:center;font-size:.75rem;color:#475569;border-top:1px solid #334155}
|
|
128
|
-
.footer a{color:#6366f1;text-decoration:none}
|
|
129
|
-
.footer a:hover{text-decoration:underline}
|
|
130
|
-
</style>
|
|
131
|
-
</head>
|
|
132
|
-
<body>
|
|
133
|
-
<div class="card">
|
|
134
|
-
<div class="header">
|
|
135
|
-
<div class="header-url">${escHtml(url)}</div>
|
|
136
|
-
<div class="header-ts">Audited ${escHtml(formattedDate)}</div>
|
|
137
|
-
<div class="grade-row">
|
|
138
|
-
<div>
|
|
139
|
-
<div class="grade-badge">${escHtml(grade)}</div>
|
|
140
|
-
</div>
|
|
141
|
-
<div>
|
|
142
|
-
<div class="score-num">${score}<span style="font-size:1rem;color:#64748b">/100</span></div>
|
|
143
|
-
<div class="score-label">AI Citation Score</div>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
<div class="verdict-pill">${escHtml(verdictLabel)}</div>
|
|
147
|
-
<p class="summary">${escHtml(citation_verdict.one_line_summary)}</p>
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
<div class="section">
|
|
151
|
-
<div class="section-title">Dimension Scores</div>
|
|
152
|
-
${dimensionRows}
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<div class="section">
|
|
156
|
-
<div class="section-title">Top Blockers</div>
|
|
157
|
-
${blockerItems}${noBlockers}
|
|
158
|
-
</div>
|
|
159
|
-
|
|
160
|
-
<div class="footer">
|
|
161
|
-
Generated by <a href="https://github.com/AutomateLab-tech/ai-seo-mcp" target="_blank" rel="noopener">AI Citation Toolkit</a> - github.com/AutomateLab-tech/ai-seo-mcp
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
</body>
|
|
87
|
+
return `<!DOCTYPE html>
|
|
88
|
+
<html lang="en">
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="UTF-8">
|
|
91
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
92
|
+
<title>AI-SEO Scorecard - ${escHtml(url)}</title>
|
|
93
|
+
<style>
|
|
94
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
95
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;padding:2rem 1rem}
|
|
96
|
+
.card{max-width:700px;margin:0 auto;background:#1e293b;border-radius:12px;overflow:hidden;box-shadow:0 4px 32px rgba(0,0,0,.4)}
|
|
97
|
+
.header{padding:2rem;border-bottom:1px solid #334155}
|
|
98
|
+
.header-url{font-size:.85rem;color:#94a3b8;word-break:break-all;margin-bottom:.5rem}
|
|
99
|
+
.header-ts{font-size:.78rem;color:#64748b}
|
|
100
|
+
.grade-row{display:flex;align-items:center;gap:1.5rem;margin-top:1.25rem}
|
|
101
|
+
.grade-badge{font-size:3rem;font-weight:800;line-height:1;color:${gradeColor(grade)}}
|
|
102
|
+
.score-num{font-size:1.75rem;font-weight:700;color:#f1f5f9}
|
|
103
|
+
.score-label{font-size:.8rem;color:#94a3b8;margin-top:.2rem}
|
|
104
|
+
.verdict-pill{display:inline-block;padding:.35rem .9rem;border-radius:999px;font-size:.85rem;font-weight:600;background:${verdictColor(citation_verdict.will_ai_cite)}22;color:${verdictColor(citation_verdict.will_ai_cite)};border:1px solid ${verdictColor(citation_verdict.will_ai_cite)}44;margin-top:.5rem}
|
|
105
|
+
.summary{font-size:.9rem;color:#cbd5e1;margin-top:.75rem;line-height:1.5}
|
|
106
|
+
.section{padding:1.5rem 2rem}
|
|
107
|
+
.section + .section{border-top:1px solid #334155}
|
|
108
|
+
.section-title{font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#64748b;margin-bottom:1rem}
|
|
109
|
+
.dim-row{display:grid;grid-template-columns:110px 1fr 36px;align-items:center;gap:.75rem;margin-bottom:.6rem}
|
|
110
|
+
.dim-label{font-size:.82rem;color:#94a3b8;text-align:right}
|
|
111
|
+
.bar-track{height:8px;background:#0f172a;border-radius:4px;overflow:hidden}
|
|
112
|
+
.bar-fill{height:100%;border-radius:4px;transition:width .3s}
|
|
113
|
+
.dim-score{font-size:.82rem;font-weight:600;color:#e2e8f0;text-align:right}
|
|
114
|
+
.blocker{background:#0f172a;border-radius:8px;padding:1rem;margin-bottom:.75rem;border-left:3px solid #ef4444}
|
|
115
|
+
.blocker:last-child{margin-bottom:0}
|
|
116
|
+
.blocker-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}
|
|
117
|
+
.blocker-num{width:22px;height:22px;border-radius:50%;background:#ef4444;color:#fff;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
118
|
+
.blocker-cat{font-size:.8rem;font-weight:600;color:#cbd5e1;text-transform:capitalize}
|
|
119
|
+
.badge{padding:.15rem .5rem;border-radius:4px;font-size:.7rem;font-weight:600;text-transform:uppercase}
|
|
120
|
+
.badge-high{background:#ef444422;color:#ef4444}
|
|
121
|
+
.badge-medium{background:#f59e0b22;color:#f59e0b}
|
|
122
|
+
.badge-low{background:#22c55e22;color:#22c55e}
|
|
123
|
+
.blocker-msg{font-size:.82rem;color:#94a3b8;margin-bottom:.35rem;line-height:1.4}
|
|
124
|
+
.blocker-fix{font-size:.82rem;color:#64748b;line-height:1.4}
|
|
125
|
+
.blocker-fix strong{color:#94a3b8}
|
|
126
|
+
.no-blockers{font-size:.9rem;color:#22c55e}
|
|
127
|
+
.footer{padding:1rem 2rem;text-align:center;font-size:.75rem;color:#475569;border-top:1px solid #334155}
|
|
128
|
+
.footer a{color:#6366f1;text-decoration:none}
|
|
129
|
+
.footer a:hover{text-decoration:underline}
|
|
130
|
+
</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
<div class="card">
|
|
134
|
+
<div class="header">
|
|
135
|
+
<div class="header-url">${escHtml(url)}</div>
|
|
136
|
+
<div class="header-ts">Audited ${escHtml(formattedDate)}</div>
|
|
137
|
+
<div class="grade-row">
|
|
138
|
+
<div>
|
|
139
|
+
<div class="grade-badge">${escHtml(grade)}</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div>
|
|
142
|
+
<div class="score-num">${score}<span style="font-size:1rem;color:#64748b">/100</span></div>
|
|
143
|
+
<div class="score-label">AI Citation Score</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="verdict-pill">${escHtml(verdictLabel)}</div>
|
|
147
|
+
<p class="summary">${escHtml(citation_verdict.one_line_summary)}</p>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="section">
|
|
151
|
+
<div class="section-title">Dimension Scores</div>
|
|
152
|
+
${dimensionRows}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div class="section">
|
|
156
|
+
<div class="section-title">Top Blockers</div>
|
|
157
|
+
${blockerItems}${noBlockers}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="footer">
|
|
161
|
+
Generated by <a href="https://github.com/AutomateLab-tech/ai-seo-mcp" target="_blank" rel="noopener">AI Citation Toolkit</a> - github.com/AutomateLab-tech/ai-seo-mcp
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</body>
|
|
165
165
|
</html>`;
|
|
166
166
|
}
|
|
167
167
|
//# sourceMappingURL=scorecard-html.js.map
|
|
@@ -5,16 +5,19 @@ export declare const auditPageInputSchema: z.ZodObject<{
|
|
|
5
5
|
include_raw_html: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
6
6
|
respect_robots: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
7
7
|
generate_report: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
8
|
+
render: z.ZodDefault<z.ZodOptional<z.ZodEnum<["static", "headless"]>>>;
|
|
8
9
|
}, "strip", z.ZodTypeAny, {
|
|
9
10
|
url: string;
|
|
10
11
|
respect_robots: boolean;
|
|
11
12
|
include_raw_html: boolean;
|
|
12
13
|
generate_report: boolean;
|
|
14
|
+
render: "static" | "headless";
|
|
13
15
|
}, {
|
|
14
16
|
url: string;
|
|
15
17
|
respect_robots?: boolean | undefined;
|
|
16
18
|
include_raw_html?: boolean | undefined;
|
|
17
19
|
generate_report?: boolean | undefined;
|
|
20
|
+
render?: "static" | "headless" | undefined;
|
|
18
21
|
}>;
|
|
19
22
|
export type AuditPageInput = z.infer<typeof auditPageInputSchema>;
|
|
20
23
|
export interface CitationVerdict {
|
|
@@ -27,6 +30,7 @@ export interface CitationVerdict {
|
|
|
27
30
|
}>;
|
|
28
31
|
one_line_summary: string;
|
|
29
32
|
}
|
|
33
|
+
export type ContentQuality = "static_html" | "ssr_likely" | "spa_empty";
|
|
30
34
|
export interface AuditPageResult extends AuditResult {
|
|
31
35
|
citation_verdict: CitationVerdict;
|
|
32
36
|
dimension_scores: {
|
|
@@ -39,6 +43,13 @@ export interface AuditPageResult extends AuditResult {
|
|
|
39
43
|
entity_density: number;
|
|
40
44
|
sitemap: number;
|
|
41
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Heuristic classification of the fetched HTML's content readiness.
|
|
48
|
+
* - `static_html`: body text >= 500 chars; audit is reliable.
|
|
49
|
+
* - `ssr_likely`: body text < 500 chars but few scripts; small/edge page or stub.
|
|
50
|
+
* - `spa_empty`: body text < 500 chars AND >5 script tags; results are degraded — re-run with render: 'headless' once available.
|
|
51
|
+
*/
|
|
52
|
+
content_quality: ContentQuality;
|
|
42
53
|
raw_html?: string;
|
|
43
54
|
report_html?: string;
|
|
44
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-page.d.ts","sourceRoot":"","sources":["../../src/tools/audit-page.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"audit-page.d.ts","sourceRoot":"","sources":["../../src/tools/audit-page.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;EAyB/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IACjD,cAAc,EAAE,KAAK,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,EAAE,MAAM,CAAC;QACZ,gBAAgB,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;KAC9C,CAAC,CAAC;IACH,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,YAAY,GAAG,WAAW,CAAC;AAExE,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD,gBAAgB,EAAE,eAAe,CAAC;IAClC,gBAAgB,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF;;;;;OAKG;IACH,eAAe,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAmBD,wBAAsB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAyS/E"}
|
package/dist/tools/audit-page.js
CHANGED
|
@@ -30,15 +30,39 @@ export const auditPageInputSchema = z.object({
|
|
|
30
30
|
.optional()
|
|
31
31
|
.default(false)
|
|
32
32
|
.describe("If true, return a standalone HTML scorecard in the `report_html` field. The HTML is self-contained (no external dependencies) and can be saved as a .html file or pasted to Gist/CodePen. Default false to keep audits cheap."),
|
|
33
|
+
render: z
|
|
34
|
+
.enum(["static", "headless"])
|
|
35
|
+
.optional()
|
|
36
|
+
.default("static")
|
|
37
|
+
.describe("Rendering mode. `static` (default) fetches raw HTML via HTTP — fast (<1s) but misses JS-rendered content typical of SPAs (React/Vue/Angular landing pages). `headless` spins up Playwright Chromium, waits for networkidle, and audits the rendered DOM — adds 3-10s per audit and requires `playwright-core` installed plus a one-time `npx playwright install chromium`. Use `headless` when the static audit shows `content_quality: \"spa_empty\"` or you know the target is JS-rendered."),
|
|
33
38
|
});
|
|
39
|
+
const SPA_BODY_TEXT_MIN = 500;
|
|
40
|
+
const SPA_SCRIPT_TAG_MIN = 5;
|
|
41
|
+
function detectContentQuality(bodyText, scriptCount) {
|
|
42
|
+
const textLen = bodyText.trim().length;
|
|
43
|
+
if (textLen >= SPA_BODY_TEXT_MIN)
|
|
44
|
+
return "static_html";
|
|
45
|
+
if (scriptCount > SPA_SCRIPT_TAG_MIN)
|
|
46
|
+
return "spa_empty";
|
|
47
|
+
return "ssr_likely";
|
|
48
|
+
}
|
|
49
|
+
function countScriptTags(html) {
|
|
50
|
+
let n = 0;
|
|
51
|
+
const re = /<script\b/gi;
|
|
52
|
+
while (re.exec(html) !== null)
|
|
53
|
+
n++;
|
|
54
|
+
return n;
|
|
55
|
+
}
|
|
34
56
|
export async function auditPage(input) {
|
|
35
57
|
const hostDelays = new Map();
|
|
36
58
|
const robotsCache = new Map();
|
|
59
|
+
const renderMode = input.render ?? "static";
|
|
37
60
|
// Fetch URL once
|
|
38
61
|
const result = await politeFetch(input.url, {
|
|
39
62
|
respectRobots: input.respect_robots,
|
|
40
63
|
hostDelays,
|
|
41
64
|
robotsCache,
|
|
65
|
+
renderMode,
|
|
42
66
|
});
|
|
43
67
|
const ct = result.headers["content-type"];
|
|
44
68
|
const ctStr = Array.isArray(ct) ? ct[0] : (ct ?? "");
|
|
@@ -54,7 +78,7 @@ export async function auditPage(input) {
|
|
|
54
78
|
// --- Schema dimension ---
|
|
55
79
|
let schemaScore = 50;
|
|
56
80
|
try {
|
|
57
|
-
const schemaResult = await auditSchema({ url: input.url, respect_robots: input.respect_robots }, hostDelays, robotsCache);
|
|
81
|
+
const schemaResult = await auditSchema({ url: input.url, respect_robots: input.respect_robots }, hostDelays, robotsCache, renderMode);
|
|
58
82
|
schemaScore = schemaResult.ai_citation_readiness_score;
|
|
59
83
|
allFindings.push(...schemaResult.findings);
|
|
60
84
|
}
|
|
@@ -64,7 +88,7 @@ export async function auditPage(input) {
|
|
|
64
88
|
// --- Technical dimension ---
|
|
65
89
|
let technicalScore = 50;
|
|
66
90
|
try {
|
|
67
|
-
const techResult = await checkTechnical({ url: input.url, respect_robots: input.respect_robots }, hostDelays, robotsCache);
|
|
91
|
+
const techResult = await checkTechnical({ url: input.url, respect_robots: input.respect_robots }, hostDelays, robotsCache, renderMode);
|
|
68
92
|
// Derive technical score from findings
|
|
69
93
|
const techFindings = techResult.findings;
|
|
70
94
|
allFindings.push(...techFindings);
|
|
@@ -211,6 +235,28 @@ export async function auditPage(input) {
|
|
|
211
235
|
entity_density: entityDensityScore,
|
|
212
236
|
sitemap: sitemapScore,
|
|
213
237
|
};
|
|
238
|
+
const scriptCount = countScriptTags(result.body);
|
|
239
|
+
const contentQuality = detectContentQuality(body.bodyText, scriptCount);
|
|
240
|
+
if (contentQuality === "spa_empty" && renderMode === "static") {
|
|
241
|
+
allFindings.push({
|
|
242
|
+
severity: "critical",
|
|
243
|
+
category: "technical",
|
|
244
|
+
where: "<body>",
|
|
245
|
+
message: "Page appears to be a JS-rendered SPA (low body text, many script tags); audit results are likely incomplete.",
|
|
246
|
+
fix: "Re-run audit_page with render: 'headless' to capture the rendered DOM, or serve SSR/prerendered HTML so AI crawlers without JS see real content.",
|
|
247
|
+
estimated_impact: "high",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
else if (contentQuality === "spa_empty" && renderMode === "headless") {
|
|
251
|
+
allFindings.push({
|
|
252
|
+
severity: "critical",
|
|
253
|
+
category: "technical",
|
|
254
|
+
where: "<body>",
|
|
255
|
+
message: "Even with headless rendering the page body has little extractable text — the SPA may render content lazily or behind interaction.",
|
|
256
|
+
fix: "Serve SSR/prerendered HTML; AI crawlers (and audit_page even with render=headless) cannot reliably trigger lazy/interaction-gated content.",
|
|
257
|
+
estimated_impact: "high",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
214
260
|
const score = computeWeightedScore(dimensionScores);
|
|
215
261
|
const grade = deriveGrade(score);
|
|
216
262
|
// Deduplicate findings (same message from multiple sub-tools)
|
|
@@ -253,6 +299,7 @@ export async function auditPage(input) {
|
|
|
253
299
|
score,
|
|
254
300
|
grade,
|
|
255
301
|
dimension_scores: dimensionScores,
|
|
302
|
+
content_quality: contentQuality,
|
|
256
303
|
};
|
|
257
304
|
if (input.include_raw_html) {
|
|
258
305
|
output.raw_html = result.body;
|