geo-ai-search-optimization 2.0.0 → 2.2.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/action.yml +130 -0
- package/package.json +15 -3
- package/src/auto-fix.js +349 -0
- package/src/batch-full-page-audit.js +151 -0
- package/src/citability.js +311 -0
- package/src/citation-check.js +1 -1
- package/src/cli-site-ops-commands.js +391 -2
- package/src/compare.js +175 -0
- package/src/config.js +105 -0
- package/src/crawlers.js +286 -0
- package/src/diagnose.js +221 -0
- package/src/eeat.js +251 -0
- package/src/freshness.js +281 -0
- package/src/full-audit.js +269 -0
- package/src/full-page-audit.js +273 -0
- package/src/heading-structure.js +287 -0
- package/src/index.d.ts +492 -0
- package/src/index.js +24 -0
- package/src/internal-links.js +298 -0
- package/src/page-audit.js +1 -1
- package/src/page-snapshot.js +198 -0
- package/src/pdf-report.js +205 -0
- package/src/platform-ready.js +238 -0
- package/src/plugins.js +126 -0
- package/src/readability.js +252 -0
- package/src/security.js +249 -0
- package/src/sitemap.js +323 -0
- package/src/social-meta.js +293 -0
- package/src/topics.js +275 -0
- package/src/url-onboarding.js +1 -1
- package/src/validate-llms.js +307 -0
- package/src/validate-schema.js +306 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeScanOutput } from "./scan.js";
|
|
4
|
+
import { auditProject } from "./audit.js";
|
|
5
|
+
import { analyzeCrawlers } from "./crawlers.js";
|
|
6
|
+
import { validateLlmsTxt } from "./validate-llms.js";
|
|
7
|
+
import { fullPageAudit } from "./full-page-audit.js";
|
|
8
|
+
|
|
9
|
+
async function runSafe(label, fn) {
|
|
10
|
+
try {
|
|
11
|
+
return { ok: true, data: await fn() };
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return { ok: false, error: err.message, label };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function findSpecialFile(root, filename) {
|
|
18
|
+
const candidates = [
|
|
19
|
+
path.join(root, filename),
|
|
20
|
+
path.join(root, "public", filename),
|
|
21
|
+
path.join(root, "static", filename),
|
|
22
|
+
path.join(root, "dist", filename),
|
|
23
|
+
path.join(root, "out", filename)
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(candidate);
|
|
29
|
+
return candidate;
|
|
30
|
+
} catch {
|
|
31
|
+
// continue
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function fullAudit(input, options = {}) {
|
|
39
|
+
const root = path.resolve(input);
|
|
40
|
+
|
|
41
|
+
// Phase 1: Run base project audit
|
|
42
|
+
const baseAudit = await auditProject(root, options);
|
|
43
|
+
|
|
44
|
+
// Phase 2: Run infrastructure analyses in parallel
|
|
45
|
+
const robotsPath = await findSpecialFile(root, "robots.txt");
|
|
46
|
+
const llmsPath = await findSpecialFile(root, "llms.txt");
|
|
47
|
+
|
|
48
|
+
const [crawlersResult, llmsResult] = await Promise.all([
|
|
49
|
+
robotsPath
|
|
50
|
+
? runSafe("crawlers", () => analyzeCrawlers(robotsPath))
|
|
51
|
+
: { ok: true, data: { score: 0, crawlers: [], summary: "No robots.txt found in project.", recommendation: "" } },
|
|
52
|
+
llmsPath
|
|
53
|
+
? runSafe("validate-llms", () => validateLlmsTxt(llmsPath))
|
|
54
|
+
: { ok: true, data: { score: 0, found: false, summary: "No llms.txt found in project." } }
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// Phase 3: Sample page audits (if URLs or HTML files provided)
|
|
58
|
+
const sampleUrls = options.sampleUrls || [];
|
|
59
|
+
const samplePages = [];
|
|
60
|
+
|
|
61
|
+
if (sampleUrls.length > 0) {
|
|
62
|
+
const concurrency = options.concurrency || 2;
|
|
63
|
+
const chunks = [];
|
|
64
|
+
for (let i = 0; i < sampleUrls.length; i += concurrency) {
|
|
65
|
+
chunks.push(sampleUrls.slice(i, i + concurrency));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const chunk of chunks) {
|
|
69
|
+
const results = await Promise.all(
|
|
70
|
+
chunk.map((url) => runSafe(`page:${url}`, () => fullPageAudit(url, options)))
|
|
71
|
+
);
|
|
72
|
+
for (const result of results) {
|
|
73
|
+
if (result.ok) {
|
|
74
|
+
samplePages.push(result.data);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Phase 4: Compute enhanced score
|
|
81
|
+
const infraScores = {
|
|
82
|
+
base: baseAudit.score,
|
|
83
|
+
crawlers: crawlersResult.ok ? crawlersResult.data.score : 0,
|
|
84
|
+
llmsValidation: llmsResult.ok ? llmsResult.data.score : 0
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Composite: base audit (60%) + crawler health (20%) + llms.txt quality (20%)
|
|
88
|
+
const infraComposite = Math.round(
|
|
89
|
+
infraScores.base * 0.6 +
|
|
90
|
+
infraScores.crawlers * 0.2 +
|
|
91
|
+
infraScores.llmsValidation * 0.2
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Page-level composite (average of sampled pages)
|
|
95
|
+
const pageComposite = samplePages.length > 0
|
|
96
|
+
? Math.round(samplePages.reduce((sum, p) => sum + p.compositeScore, 0) / samplePages.length)
|
|
97
|
+
: null;
|
|
98
|
+
|
|
99
|
+
// Overall composite
|
|
100
|
+
const overallComposite = pageComposite !== null
|
|
101
|
+
? Math.round(infraComposite * 0.5 + pageComposite * 0.5)
|
|
102
|
+
: infraComposite;
|
|
103
|
+
|
|
104
|
+
const overallLabel = overallComposite >= 80 ? "Strong"
|
|
105
|
+
: overallComposite >= 60 ? "Moderate"
|
|
106
|
+
: overallComposite >= 40 ? "Weak"
|
|
107
|
+
: "Very weak";
|
|
108
|
+
|
|
109
|
+
// Merge all recommendations
|
|
110
|
+
const allRecs = [];
|
|
111
|
+
if (baseAudit.actionPlan) {
|
|
112
|
+
allRecs.push(...baseAudit.actionPlan.map((t) => ({
|
|
113
|
+
priority: t.priority,
|
|
114
|
+
source: "project-audit",
|
|
115
|
+
action: t.action,
|
|
116
|
+
owner: t.owner,
|
|
117
|
+
area: t.area
|
|
118
|
+
})));
|
|
119
|
+
}
|
|
120
|
+
if (crawlersResult.ok && crawlersResult.data.blocked?.length > 0) {
|
|
121
|
+
allRecs.push({
|
|
122
|
+
priority: "P1",
|
|
123
|
+
source: "crawlers",
|
|
124
|
+
action: `Unblock ${crawlersResult.data.blocked.length} AI crawler(s): ${crawlersResult.data.blocked.join(", ")}`,
|
|
125
|
+
owner: "Engineering",
|
|
126
|
+
area: "AI Crawler Access"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (llmsResult.ok && llmsResult.data.issues?.length > 0) {
|
|
130
|
+
allRecs.push({
|
|
131
|
+
priority: "P1",
|
|
132
|
+
source: "llms-validation",
|
|
133
|
+
action: `Fix ${llmsResult.data.issues.length} llms.txt issue(s): ${llmsResult.data.issues.map((i) => i.message).join("; ")}`,
|
|
134
|
+
owner: "Engineering / SEO",
|
|
135
|
+
area: "llms.txt"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Page-level top issues
|
|
140
|
+
for (const page of samplePages) {
|
|
141
|
+
for (const rec of (page.recommendations || []).slice(0, 3)) {
|
|
142
|
+
allRecs.push({
|
|
143
|
+
priority: "P2",
|
|
144
|
+
source: `page:${page.input}`,
|
|
145
|
+
action: rec.rec,
|
|
146
|
+
owner: "Content / SEO",
|
|
147
|
+
area: rec.source
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const errors = [];
|
|
153
|
+
if (!crawlersResult.ok) errors.push({ component: "crawlers", error: crawlersResult.error });
|
|
154
|
+
if (!llmsResult.ok) errors.push({ component: "validate-llms", error: llmsResult.error });
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
kind: "geo-full-audit",
|
|
158
|
+
root,
|
|
159
|
+
overallScore: overallComposite,
|
|
160
|
+
overallLabel,
|
|
161
|
+
infraScore: infraComposite,
|
|
162
|
+
pageScore: pageComposite,
|
|
163
|
+
scores: {
|
|
164
|
+
base: infraScores.base,
|
|
165
|
+
crawlers: infraScores.crawlers,
|
|
166
|
+
llmsValidation: infraScores.llmsValidation,
|
|
167
|
+
pageAverage: pageComposite
|
|
168
|
+
},
|
|
169
|
+
baseAudit,
|
|
170
|
+
crawlers: crawlersResult.ok ? crawlersResult.data : null,
|
|
171
|
+
llmsValidation: llmsResult.ok ? llmsResult.data : null,
|
|
172
|
+
samplePages,
|
|
173
|
+
actionPlan: allRecs.slice(0, 20),
|
|
174
|
+
errors,
|
|
175
|
+
summary: `Full GEO Audit: ${overallComposite}/100 (${overallLabel}). Infrastructure: ${infraComposite}/100. ${samplePages.length > 0 ? `Page quality: ${pageComposite}/100 (${samplePages.length} pages sampled).` : "No page samples."}`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function renderFullAuditMarkdown(report) {
|
|
180
|
+
const lines = [
|
|
181
|
+
"# GEO Full Audit Report",
|
|
182
|
+
"",
|
|
183
|
+
`- Project: \`${report.root}\``,
|
|
184
|
+
`- **Overall GEO Score: \`${report.overallScore}/100\` (${report.overallLabel})**`,
|
|
185
|
+
`- Infrastructure Score: \`${report.infraScore}/100\``,
|
|
186
|
+
report.pageScore !== null ? `- Page Quality Score: \`${report.pageScore}/100\` (${report.samplePages.length} pages)` : "- Page Quality: Not sampled",
|
|
187
|
+
`- Summary: ${report.summary}`,
|
|
188
|
+
"",
|
|
189
|
+
"## Score Breakdown",
|
|
190
|
+
"",
|
|
191
|
+
"| Component | Score | Weight |",
|
|
192
|
+
"|-----------|-------|--------|",
|
|
193
|
+
`| 🏗️ Base Project Audit | ${report.scores.base}/100 | 60% of infra |`,
|
|
194
|
+
`| 🤖 AI Crawler Access | ${report.scores.crawlers}/100 | 20% of infra |`,
|
|
195
|
+
`| 📄 llms.txt Validation | ${report.scores.llmsValidation}/100 | 20% of infra |`
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
if (report.pageScore !== null) {
|
|
199
|
+
lines.push(`| 📊 Page Quality Average | ${report.scores.pageAverage}/100 | 50% of overall |`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Crawler summary
|
|
203
|
+
if (report.crawlers) {
|
|
204
|
+
lines.push("", "## AI Crawler Access", "");
|
|
205
|
+
const blocked = report.crawlers.crawlers?.filter((c) => c.status === "blocked") || [];
|
|
206
|
+
const allowed = report.crawlers.crawlers?.filter((c) => c.status === "allowed") || [];
|
|
207
|
+
lines.push(`- Allowed: \`${allowed.length}\` | Blocked: \`${blocked.length}\` | Score: \`${report.crawlers.score}/100\``);
|
|
208
|
+
if (blocked.length > 0) {
|
|
209
|
+
lines.push(`- ⚠️ Blocked crawlers: ${blocked.map((c) => `**${c.crawler}** (${c.engine})`).join(", ")}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// llms.txt summary
|
|
214
|
+
if (report.llmsValidation) {
|
|
215
|
+
lines.push("", "## llms.txt Status", "");
|
|
216
|
+
if (report.llmsValidation.found) {
|
|
217
|
+
lines.push(`- Score: \`${report.llmsValidation.score}/100\` (${report.llmsValidation.scoreLabel})`);
|
|
218
|
+
if (report.llmsValidation.issues?.length > 0) {
|
|
219
|
+
lines.push(`- Issues: ${report.llmsValidation.issues.map((i) => i.message).join("; ")}`);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
lines.push("- ❌ No llms.txt found. Run `geo-ai-search-optimization init-llms` to create one.");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Base audit areas
|
|
227
|
+
if (report.baseAudit?.areas) {
|
|
228
|
+
lines.push("", "## Issue Areas (Project Level)", "");
|
|
229
|
+
for (const area of report.baseAudit.areas) {
|
|
230
|
+
const icon = area.severity === "高" ? "🔴" : area.severity === "中" ? "🟡" : "🟢";
|
|
231
|
+
lines.push(`- ${icon} **${area.title}** — ${area.status}`);
|
|
232
|
+
if (area.issues.length > 0) {
|
|
233
|
+
lines.push(` ${area.summary}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Sample page results
|
|
239
|
+
if (report.samplePages.length > 0) {
|
|
240
|
+
lines.push("", "## Sampled Page Results", "", "| Page | Composite | Base | Citability | E-E-A-T | Readability |", "|------|-----------|------|------------|---------|-------------|");
|
|
241
|
+
for (const page of report.samplePages) {
|
|
242
|
+
const d = page.dimensions;
|
|
243
|
+
const shortInput = page.input.length > 40 ? `...${page.input.slice(-37)}` : page.input;
|
|
244
|
+
lines.push(`| ${shortInput} | **${page.compositeScore}** | ${d.base?.score ?? "—"} | ${d.citability?.score ?? "—"} | ${d.eeat?.score ?? "—"} | ${d.readability?.score ?? "—"} |`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Unified action plan
|
|
249
|
+
if (report.actionPlan.length > 0) {
|
|
250
|
+
lines.push("", "## Unified Action Plan", "");
|
|
251
|
+
for (const task of report.actionPlan) {
|
|
252
|
+
lines.push(`- **${task.priority}** [${task.source}] ${task.action}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (report.errors.length > 0) {
|
|
257
|
+
lines.push("", "## Errors", "");
|
|
258
|
+
for (const err of report.errors) {
|
|
259
|
+
lines.push(`- ⚠️ ${err.component}: ${err.error}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
lines.push("");
|
|
264
|
+
return lines.join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function writeFullAuditOutput(outputPath, content) {
|
|
268
|
+
return writeScanOutput(outputPath, content);
|
|
269
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { writeScanOutput } from "./scan.js";
|
|
2
|
+
import { auditPage } from "./page-audit.js";
|
|
3
|
+
import { analyzeCitability } from "./citability.js";
|
|
4
|
+
import { analyzeEeat } from "./eeat.js";
|
|
5
|
+
import { analyzeReadability } from "./readability.js";
|
|
6
|
+
import { analyzeHeadingStructure } from "./heading-structure.js";
|
|
7
|
+
import { analyzeInternalLinks } from "./internal-links.js";
|
|
8
|
+
import { analyzeSocialMeta } from "./social-meta.js";
|
|
9
|
+
import { analyzePlatformReadiness } from "./platform-ready.js";
|
|
10
|
+
import { validateSchema } from "./validate-schema.js";
|
|
11
|
+
import { analyzeFreshness } from "./freshness.js";
|
|
12
|
+
import { analyzeSecurity } from "./security.js";
|
|
13
|
+
import { analyzeTopics } from "./topics.js";
|
|
14
|
+
|
|
15
|
+
const DIMENSION_WEIGHTS = {
|
|
16
|
+
base: 0.15,
|
|
17
|
+
citability: 0.12,
|
|
18
|
+
eeat: 0.12,
|
|
19
|
+
readability: 0.08,
|
|
20
|
+
headingStructure: 0.06,
|
|
21
|
+
socialMeta: 0.05,
|
|
22
|
+
internalLinks: 0.05,
|
|
23
|
+
platformReady: 0.08,
|
|
24
|
+
schema: 0.07,
|
|
25
|
+
freshness: 0.08,
|
|
26
|
+
security: 0.07,
|
|
27
|
+
topics: 0.07
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function runSafe(label, fn) {
|
|
31
|
+
try {
|
|
32
|
+
return { ok: true, data: await fn() };
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return { ok: false, error: err.message, label };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function fullPageAudit(input, options = {}) {
|
|
39
|
+
// Run all analyses in parallel (12 dimensions)
|
|
40
|
+
const [
|
|
41
|
+
baseResult,
|
|
42
|
+
citabilityResult,
|
|
43
|
+
eeatResult,
|
|
44
|
+
readabilityResult,
|
|
45
|
+
headingResult,
|
|
46
|
+
linksResult,
|
|
47
|
+
socialResult,
|
|
48
|
+
platformResult,
|
|
49
|
+
schemaResult,
|
|
50
|
+
freshnessResult,
|
|
51
|
+
securityResult,
|
|
52
|
+
topicsResult
|
|
53
|
+
] = await Promise.all([
|
|
54
|
+
runSafe("base", () => auditPage(input, options)),
|
|
55
|
+
runSafe("citability", () => analyzeCitability(input)),
|
|
56
|
+
runSafe("eeat", () => analyzeEeat(input)),
|
|
57
|
+
runSafe("readability", () => analyzeReadability(input)),
|
|
58
|
+
runSafe("heading-structure", () => analyzeHeadingStructure(input)),
|
|
59
|
+
runSafe("internal-links", () => analyzeInternalLinks(input, { baseUrl: options.baseUrl })),
|
|
60
|
+
runSafe("social-meta", () => analyzeSocialMeta(input)),
|
|
61
|
+
runSafe("platform-ready", () => analyzePlatformReadiness(input)),
|
|
62
|
+
runSafe("validate-schema", () => validateSchema(input)),
|
|
63
|
+
runSafe("freshness", () => analyzeFreshness(input)),
|
|
64
|
+
runSafe("security", () => analyzeSecurity(input)),
|
|
65
|
+
runSafe("topics", () => analyzeTopics(input))
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const dimensions = {};
|
|
69
|
+
const errors = [];
|
|
70
|
+
|
|
71
|
+
function addDimension(key, result, scoreField) {
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
dimensions[key] = {
|
|
74
|
+
score: result.data[scoreField || "score"],
|
|
75
|
+
label: result.data.scoreLabel || result.data[scoreField + "Label"] || "",
|
|
76
|
+
summary: result.data.summary || ""
|
|
77
|
+
};
|
|
78
|
+
} else {
|
|
79
|
+
errors.push({ dimension: key, error: result.error });
|
|
80
|
+
dimensions[key] = { score: 0, label: "Error", summary: result.error };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Base audit score
|
|
85
|
+
if (baseResult.ok) {
|
|
86
|
+
dimensions.base = {
|
|
87
|
+
score: baseResult.data.score.score,
|
|
88
|
+
label: baseResult.data.scoreLabel,
|
|
89
|
+
summary: baseResult.data.summary
|
|
90
|
+
};
|
|
91
|
+
} else {
|
|
92
|
+
errors.push({ dimension: "base", error: baseResult.error });
|
|
93
|
+
dimensions.base = { score: 0, label: "Error", summary: baseResult.error };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
addDimension("citability", citabilityResult);
|
|
97
|
+
addDimension("eeat", eeatResult);
|
|
98
|
+
addDimension("readability", readabilityResult);
|
|
99
|
+
addDimension("headingStructure", headingResult);
|
|
100
|
+
addDimension("internalLinks", linksResult);
|
|
101
|
+
addDimension("socialMeta", socialResult);
|
|
102
|
+
addDimension("platformReady", platformResult, "overallScore");
|
|
103
|
+
addDimension("schema", schemaResult);
|
|
104
|
+
addDimension("freshness", freshnessResult);
|
|
105
|
+
addDimension("security", securityResult);
|
|
106
|
+
addDimension("topics", topicsResult);
|
|
107
|
+
|
|
108
|
+
// Compute composite score
|
|
109
|
+
let compositeScore = 0;
|
|
110
|
+
for (const [key, weight] of Object.entries(DIMENSION_WEIGHTS)) {
|
|
111
|
+
const dimScore = dimensions[key]?.score || 0;
|
|
112
|
+
compositeScore += dimScore * weight;
|
|
113
|
+
}
|
|
114
|
+
compositeScore = Math.round(compositeScore);
|
|
115
|
+
|
|
116
|
+
const compositeLabel = compositeScore >= 80 ? "Strong"
|
|
117
|
+
: compositeScore >= 60 ? "Moderate"
|
|
118
|
+
: compositeScore >= 40 ? "Weak"
|
|
119
|
+
: "Very weak";
|
|
120
|
+
|
|
121
|
+
// Build top issues across all dimensions
|
|
122
|
+
const topIssues = [];
|
|
123
|
+
if (baseResult.ok && baseResult.data.problemAreas) {
|
|
124
|
+
for (const area of baseResult.data.problemAreas) {
|
|
125
|
+
topIssues.push(...area.issues.map((i) => ({ source: "base", severity: area.severity, issue: i })));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (eeatResult.ok && eeatResult.data.recommendations) {
|
|
129
|
+
for (const rec of eeatResult.data.recommendations.slice(0, 3)) {
|
|
130
|
+
topIssues.push({ source: "eeat", severity: "中", issue: rec });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (citabilityResult.ok && citabilityResult.data.recommendations) {
|
|
134
|
+
for (const rec of citabilityResult.data.recommendations.slice(0, 3)) {
|
|
135
|
+
topIssues.push({ source: "citability", severity: "中", issue: rec });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (socialResult.ok && socialResult.data.recommendations) {
|
|
139
|
+
for (const rec of socialResult.data.recommendations.slice(0, 2)) {
|
|
140
|
+
topIssues.push({ source: "social-meta", severity: "中", issue: rec });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build consolidated recommendations
|
|
145
|
+
const allRecs = [];
|
|
146
|
+
if (baseResult.ok) allRecs.push(...baseResult.data.recommendedBlocks.map((r) => ({ source: "base", rec: r })));
|
|
147
|
+
if (citabilityResult.ok) allRecs.push(...citabilityResult.data.recommendations.map((r) => ({ source: "citability", rec: r })));
|
|
148
|
+
if (eeatResult.ok) allRecs.push(...eeatResult.data.recommendations.map((r) => ({ source: "eeat", rec: r })));
|
|
149
|
+
if (readabilityResult.ok) allRecs.push(...readabilityResult.data.recommendations.map((r) => ({ source: "readability", rec: r })));
|
|
150
|
+
if (headingResult.ok) allRecs.push(...headingResult.data.recommendations.map((r) => ({ source: "heading", rec: r })));
|
|
151
|
+
if (socialResult.ok) allRecs.push(...socialResult.data.recommendations.map((r) => ({ source: "social", rec: r })));
|
|
152
|
+
if (freshnessResult.ok) allRecs.push(...freshnessResult.data.recommendations.map((r) => ({ source: "freshness", rec: r })));
|
|
153
|
+
if (securityResult.ok) allRecs.push(...securityResult.data.recommendations.map((r) => ({ source: "security", rec: r })));
|
|
154
|
+
if (topicsResult.ok) allRecs.push(...topicsResult.data.recommendations.map((r) => ({ source: "topics", rec: r })));
|
|
155
|
+
|
|
156
|
+
// Prioritize: dedupe and cap
|
|
157
|
+
const seenRecs = new Set();
|
|
158
|
+
const prioritizedRecs = allRecs.filter((r) => {
|
|
159
|
+
const key = r.rec.slice(0, 50);
|
|
160
|
+
if (seenRecs.has(key)) return false;
|
|
161
|
+
seenRecs.add(key);
|
|
162
|
+
return true;
|
|
163
|
+
}).slice(0, 15);
|
|
164
|
+
|
|
165
|
+
// Build platform readiness summary
|
|
166
|
+
const platformSummary = platformResult.ok
|
|
167
|
+
? platformResult.data.platforms.map((p) => ({ platform: p.platform, score: p.score, readiness: p.readiness }))
|
|
168
|
+
: [];
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
kind: "geo-full-page-audit",
|
|
172
|
+
input,
|
|
173
|
+
source: baseResult.ok ? baseResult.data.reference : input,
|
|
174
|
+
compositeScore,
|
|
175
|
+
compositeLabel,
|
|
176
|
+
dimensions,
|
|
177
|
+
platformSummary,
|
|
178
|
+
topIssues: topIssues.slice(0, 12),
|
|
179
|
+
recommendations: prioritizedRecs,
|
|
180
|
+
errors,
|
|
181
|
+
// Embed full sub-reports for JSON consumers
|
|
182
|
+
details: {
|
|
183
|
+
base: baseResult.ok ? baseResult.data : null,
|
|
184
|
+
citability: citabilityResult.ok ? citabilityResult.data : null,
|
|
185
|
+
eeat: eeatResult.ok ? eeatResult.data : null,
|
|
186
|
+
readability: readabilityResult.ok ? readabilityResult.data : null,
|
|
187
|
+
headingStructure: headingResult.ok ? headingResult.data : null,
|
|
188
|
+
internalLinks: linksResult.ok ? linksResult.data : null,
|
|
189
|
+
socialMeta: socialResult.ok ? socialResult.data : null,
|
|
190
|
+
platformReady: platformResult.ok ? platformResult.data : null,
|
|
191
|
+
schema: schemaResult.ok ? schemaResult.data : null,
|
|
192
|
+
freshness: freshnessResult.ok ? freshnessResult.data : null,
|
|
193
|
+
security: securityResult.ok ? securityResult.data : null,
|
|
194
|
+
topics: topicsResult.ok ? topicsResult.data : null
|
|
195
|
+
},
|
|
196
|
+
summary: `Composite GEO Score: ${compositeScore}/100 (${compositeLabel}). ${Object.entries(dimensions).filter(([, d]) => d.score < 40).length} dimension(s) need attention.`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function renderFullPageAuditMarkdown(report) {
|
|
201
|
+
const lines = [
|
|
202
|
+
"# GEO Full Page Audit",
|
|
203
|
+
"",
|
|
204
|
+
`- Input: \`${report.input}\``,
|
|
205
|
+
`- Source: \`${report.source}\``,
|
|
206
|
+
`- **Composite GEO Score: \`${report.compositeScore}/100\` (${report.compositeLabel})**`,
|
|
207
|
+
`- Summary: ${report.summary}`,
|
|
208
|
+
"",
|
|
209
|
+
"## Dimension Scores",
|
|
210
|
+
"",
|
|
211
|
+
"| Dimension | Score | Weight | Label |",
|
|
212
|
+
"|-----------|-------|--------|-------|"
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const dimLabels = {
|
|
216
|
+
base: "Base Audit",
|
|
217
|
+
citability: "Citability",
|
|
218
|
+
eeat: "E-E-A-T",
|
|
219
|
+
readability: "Readability",
|
|
220
|
+
headingStructure: "Heading Structure",
|
|
221
|
+
internalLinks: "Internal Links",
|
|
222
|
+
socialMeta: "Social Meta",
|
|
223
|
+
platformReady: "Platform Readiness",
|
|
224
|
+
schema: "Schema Validation",
|
|
225
|
+
freshness: "Content Freshness",
|
|
226
|
+
security: "Security",
|
|
227
|
+
topics: "Topic Coverage"
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
for (const [key, weight] of Object.entries(DIMENSION_WEIGHTS)) {
|
|
231
|
+
const dim = report.dimensions[key] || { score: 0, label: "—" };
|
|
232
|
+
const icon = dim.score >= 70 ? "🟢" : dim.score >= 40 ? "🟡" : "🔴";
|
|
233
|
+
lines.push(`| ${icon} ${dimLabels[key] || key} | ${dim.score}/100 | ${Math.round(weight * 100)}% | ${dim.label} |`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (report.platformSummary.length > 0) {
|
|
237
|
+
lines.push("", "## Platform Readiness", "");
|
|
238
|
+
for (const p of report.platformSummary) {
|
|
239
|
+
const icon = p.readiness === "Ready" ? "✅" : p.readiness === "Partial" ? "⚠️" : "❌";
|
|
240
|
+
lines.push(`- ${icon} **${p.platform}**: ${p.score}/100 (${p.readiness})`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (report.topIssues.length > 0) {
|
|
245
|
+
lines.push("", "## Top Issues", "");
|
|
246
|
+
for (const issue of report.topIssues) {
|
|
247
|
+
const severity = issue.severity === "高" ? "🔴" : "🟡";
|
|
248
|
+
lines.push(`- ${severity} [${issue.source}] ${issue.issue}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (report.recommendations.length > 0) {
|
|
253
|
+
lines.push("", "## Prioritized Recommendations", "");
|
|
254
|
+
for (let i = 0; i < report.recommendations.length; i++) {
|
|
255
|
+
const rec = report.recommendations[i];
|
|
256
|
+
lines.push(`${i + 1}. [${rec.source}] ${rec.rec}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (report.errors.length > 0) {
|
|
261
|
+
lines.push("", "## Errors", "");
|
|
262
|
+
for (const err of report.errors) {
|
|
263
|
+
lines.push(`- ⚠️ ${err.dimension}: ${err.error}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push("");
|
|
268
|
+
return lines.join("\n");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function writeFullPageAuditOutput(outputPath, content) {
|
|
272
|
+
return writeScanOutput(outputPath, content);
|
|
273
|
+
}
|