geo-ai-search-optimization 1.3.14 → 1.4.1
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/README.md +50 -0
- package/package.json +1 -1
- package/src/cli-site-ops-commands.js +36 -0
- package/src/index.js +2 -0
- package/src/page-audit.js +464 -0
- package/src/rewrite-pack.js +198 -0
package/README.md
CHANGED
|
@@ -63,6 +63,43 @@ geo-ai-search-optimization agent-orchestrator ./reports/agent-playbook-pack.json
|
|
|
63
63
|
- 做完之后下一条是什么
|
|
64
64
|
- 可直接复制给 agent 的 orchestrator prompt
|
|
65
65
|
|
|
66
|
+
## Page Audit 命令
|
|
67
|
+
|
|
68
|
+
如果你不想一次看整站,而是想先判断“这个单页为什么不容易被生成式搜索理解与引用”,可以直接用 `page-audit`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
geo-ai-search-optimization page-audit https://example.com/blog/geo-guide
|
|
72
|
+
geo-ai-search-optimization page-audit ./content/posts/geo-guide.mdx
|
|
73
|
+
geo-ai-search-optimization page-audit ./content/posts/geo-guide.mdx --json --out ./reports/page-audit.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
它会输出:
|
|
77
|
+
|
|
78
|
+
- 单页 GEO 分数
|
|
79
|
+
- 页面级问题区域
|
|
80
|
+
- 推荐新增模块
|
|
81
|
+
- rewrite brief
|
|
82
|
+
- 可直接交给 agent 的 page-level prompt
|
|
83
|
+
|
|
84
|
+
## Rewrite Pack 命令
|
|
85
|
+
|
|
86
|
+
如果你已经知道某个页面有问题,想直接拿到更接近“改写方案”的产物,可以用 `rewrite-pack`:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
geo-ai-search-optimization rewrite-pack https://example.com/blog/geo-guide
|
|
90
|
+
geo-ai-search-optimization rewrite-pack ./content/posts/geo-guide.mdx
|
|
91
|
+
geo-ai-search-optimization rewrite-pack ./content/posts/geo-guide.mdx --json --out ./reports/rewrite-pack.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
它会输出:
|
|
95
|
+
|
|
96
|
+
- metadata drafts
|
|
97
|
+
- 建议章节结构
|
|
98
|
+
- FAQ questions
|
|
99
|
+
- schema recommendations
|
|
100
|
+
- execution checklist
|
|
101
|
+
- 可直接交给 agent 的 rewrite prompt
|
|
102
|
+
|
|
66
103
|
## Agent Resume 命令
|
|
67
104
|
|
|
68
105
|
如果 GEO 工作已经做过一轮或多轮,你不想让下一个 agent 从头重新判断,而是想让它从最近一个可靠恢复点继续,可以直接用 `agent-resume`:
|
|
@@ -938,6 +975,19 @@ geo-ai-search-optimization help
|
|
|
938
975
|
- `agent-handoff / apply-plan / completion-report / handoff-bundle / share-pack / export-pack / html-pack / publish-pack / exec-summary / fix-plan / owner-board / meeting-pack / pm-brief / roadmap / report` 已统一进入 registry
|
|
939
976
|
- CLI 更接近所有命令族都使用显式 registry metadata 的结构
|
|
940
977
|
|
|
978
|
+
## New in 1.4.0
|
|
979
|
+
|
|
980
|
+
- 新增 `page-audit`
|
|
981
|
+
- 支持对单个 URL 或本地单页文件做 GEO 页面级审计
|
|
982
|
+
- 输出页面分数、问题区域、推荐新增模块、rewrite brief 与 agent prompt
|
|
983
|
+
- 产品能力从“站点级诊断”扩展到“页面级修复入口”
|
|
984
|
+
|
|
985
|
+
## New in 1.4.1
|
|
986
|
+
|
|
987
|
+
- 新增 `rewrite-pack`
|
|
988
|
+
- 基于 `page-audit` 继续产出 metadata drafts、章节结构、FAQ questions、schema recommendations 与 execution checklist
|
|
989
|
+
- 让产品从“页面级诊断”继续推进到“页面级改写方案”
|
|
990
|
+
|
|
941
991
|
## New in 1.2.20
|
|
942
992
|
|
|
943
993
|
- 新增 `agent-continue`
|
package/package.json
CHANGED
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
writeInteractiveOnboardingOutput
|
|
14
14
|
} from "./interactive-onboarding.js";
|
|
15
15
|
import { createLlmsTxt } from "./llms-txt.js";
|
|
16
|
+
import { auditPage, renderPageAuditMarkdown, writePageAuditOutput } from "./page-audit.js";
|
|
16
17
|
import { createQuickStartPlan, renderQuickStartMarkdown, writeQuickStartOutput } from "./quick-start.js";
|
|
18
|
+
import { createRewritePack, renderRewritePackMarkdown, writeRewritePackOutput } from "./rewrite-pack.js";
|
|
17
19
|
import { renderScanMarkdown, scanProject, writeScanOutput } from "./scan.js";
|
|
18
20
|
import { createSchemaTemplate } from "./schema.js";
|
|
19
21
|
import { analyzeWebsiteUrl, renderWebsiteOnboardingMarkdown, writeWebsiteOnboardingOutput } from "./url-onboarding.js";
|
|
@@ -23,6 +25,8 @@ export const SITE_OPS_HELP_LINES = [
|
|
|
23
25
|
" geo-ai-search-optimization quick-start [--json] [--out <file>]",
|
|
24
26
|
" geo-ai-search-optimization onboard [--url <website-url>] [--goal <goal>] [--existing-assets <list>] [--json] [--out <file>]",
|
|
25
27
|
" geo-ai-search-optimization onboard-url <website-url> [--json] [--out <file>]",
|
|
28
|
+
" geo-ai-search-optimization page-audit <url-or-file> [--json] [--out <file>]",
|
|
29
|
+
" geo-ai-search-optimization rewrite-pack <url-or-file> [--json] [--out <file>]",
|
|
26
30
|
" geo-ai-search-optimization init-llms [target-dir] [--site-name <name>] [--site-url <url>] [--overwrite] [--json]",
|
|
27
31
|
" geo-ai-search-optimization init-schema <type> [target-dir] [--site-url <url>] [--overwrite] [--json]",
|
|
28
32
|
" geo-ai-search-optimization audit <project-path> [--json] [--out <file>] [--max-file-size <bytes>] [--max-examples <count>]",
|
|
@@ -75,6 +79,36 @@ const handleOnboardUrl = createStructuredOutputCommandHandler({
|
|
|
75
79
|
getOutputJson: (args) => hasFlag(args, "--json")
|
|
76
80
|
});
|
|
77
81
|
|
|
82
|
+
const handlePageAudit = createStructuredOutputCommandHandler({
|
|
83
|
+
commandLabel: "单页审计",
|
|
84
|
+
execute: async (args) => {
|
|
85
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
86
|
+
if (!input) {
|
|
87
|
+
throw new Error("page-audit requires a URL or file path");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return auditPage(input, {});
|
|
91
|
+
},
|
|
92
|
+
renderMarkdown: (report) => `${renderPageAuditMarkdown(report)}\n`,
|
|
93
|
+
writeOutput: writePageAuditOutput,
|
|
94
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const handleRewritePack = createStructuredOutputCommandHandler({
|
|
98
|
+
commandLabel: "rewrite pack",
|
|
99
|
+
execute: async (args) => {
|
|
100
|
+
const input = args.find((value) => !value.startsWith("-"));
|
|
101
|
+
if (!input) {
|
|
102
|
+
throw new Error("rewrite-pack requires a URL or file path");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return createRewritePack(input, {});
|
|
106
|
+
},
|
|
107
|
+
renderMarkdown: (report) => `${renderRewritePackMarkdown(report)}\n`,
|
|
108
|
+
writeOutput: writeRewritePackOutput,
|
|
109
|
+
getOutputJson: (args) => hasFlag(args, "--json")
|
|
110
|
+
});
|
|
111
|
+
|
|
78
112
|
const handleInitLlms = createActionCommandHandler({
|
|
79
113
|
execute: async (args) =>
|
|
80
114
|
createLlmsTxt({
|
|
@@ -153,6 +187,8 @@ export const SITE_OPS_COMMAND_HANDLERS = {
|
|
|
153
187
|
"quick-start": handleQuickStart,
|
|
154
188
|
onboard: handleInteractiveOnboard,
|
|
155
189
|
"onboard-url": handleOnboardUrl,
|
|
190
|
+
"page-audit": handlePageAudit,
|
|
191
|
+
"rewrite-pack": handleRewritePack,
|
|
156
192
|
"init-llms": handleInitLlms,
|
|
157
193
|
"init-schema": handleInitSchema,
|
|
158
194
|
audit: handleAudit,
|
package/src/index.js
CHANGED
|
@@ -34,6 +34,7 @@ export { installSkill } from "./install-skill.js";
|
|
|
34
34
|
export { runCli } from "./cli.js";
|
|
35
35
|
export { runDoctor, renderDoctorMarkdown } from "./doctor.js";
|
|
36
36
|
export { createLlmsTxt } from "./llms-txt.js";
|
|
37
|
+
export { auditPage, renderPageAuditMarkdown, writePageAuditOutput } from "./page-audit.js";
|
|
37
38
|
export { createExecSummary, renderExecSummaryMarkdown, writeExecSummaryOutput } from "./exec-summary.js";
|
|
38
39
|
export { createExportPack, renderExportPackMarkdown, writeExportPack } from "./export-pack.js";
|
|
39
40
|
export { createMeetingPack, renderMeetingPackMarkdown, writeMeetingPackOutput } from "./meeting-pack.js";
|
|
@@ -42,6 +43,7 @@ export { createPmBrief, renderPmBriefMarkdown, writePmBriefOutput } from "./pm-b
|
|
|
42
43
|
export { renderPublishPackMarkdown, writePublishPack } from "./publish-pack.js";
|
|
43
44
|
export { createQuickStartPlan, renderQuickStartMarkdown, writeQuickStartOutput } from "./quick-start.js";
|
|
44
45
|
export { generateReport, writeReportOutput } from "./report.js";
|
|
46
|
+
export { createRewritePack, renderRewritePackMarkdown, writeRewritePackOutput } from "./rewrite-pack.js";
|
|
45
47
|
export { createRoadmap, renderRoadmapMarkdown, writeRoadmapOutput } from "./roadmap.js";
|
|
46
48
|
export { createSchemaTemplate } from "./schema.js";
|
|
47
49
|
export { scanProject, renderScanMarkdown, writeScanOutput } from "./scan.js";
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { SIGNAL_PATTERNS, writeScanOutput } from "./scan.js";
|
|
4
|
+
|
|
5
|
+
function isUrlInput(input) {
|
|
6
|
+
return /^https?:\/\//i.test(input);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeUrl(inputUrl) {
|
|
10
|
+
const normalizedInput = /^https?:\/\//i.test(inputUrl) ? inputUrl : `https://${inputUrl}`;
|
|
11
|
+
return new URL(normalizedInput);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function fetchText(url) {
|
|
15
|
+
const response = await fetch(url, {
|
|
16
|
+
redirect: "follow",
|
|
17
|
+
headers: {
|
|
18
|
+
"user-agent": "geo-ai-search-optimization/1.4.0"
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const text = await response.text();
|
|
23
|
+
return {
|
|
24
|
+
ok: response.ok,
|
|
25
|
+
status: response.status,
|
|
26
|
+
url: response.url,
|
|
27
|
+
text
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readLocalFile(inputPath) {
|
|
32
|
+
const resolvedPath = path.resolve(inputPath);
|
|
33
|
+
const text = await fs.readFile(resolvedPath, "utf8");
|
|
34
|
+
return {
|
|
35
|
+
path: resolvedPath,
|
|
36
|
+
text
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractTitle(text) {
|
|
41
|
+
const htmlMatch = text.match(/<title[^>]*>(.*?)<\/title>/i);
|
|
42
|
+
if (htmlMatch) {
|
|
43
|
+
return htmlMatch[1].trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const markdownMatch = text.match(/^#\s+(.+)$/m);
|
|
47
|
+
return markdownMatch ? markdownMatch[1].trim() : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractMetaDescription(text) {
|
|
51
|
+
const htmlMatch = text.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i);
|
|
52
|
+
if (htmlMatch) {
|
|
53
|
+
return htmlMatch[1].trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const markdownMatch = text.match(/^description:\s*["']?(.+?)["']?$/im);
|
|
57
|
+
return markdownMatch ? markdownMatch[1].trim() : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractCanonical(text) {
|
|
61
|
+
const htmlMatch = text.match(/<link[^>]+rel=["']canonical["'][^>]+href=["']([^"']+)["']/i);
|
|
62
|
+
if (htmlMatch) {
|
|
63
|
+
return htmlMatch[1].trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const frontmatterMatch = text.match(/^canonical:\s*["']?(.+?)["']?$/im);
|
|
67
|
+
return frontmatterMatch ? frontmatterMatch[1].trim() : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractHeadings(text) {
|
|
71
|
+
const headings = [];
|
|
72
|
+
const markdownMatches = text.matchAll(/^(#{1,6})\s+(.+)$/gm);
|
|
73
|
+
for (const match of markdownMatches) {
|
|
74
|
+
headings.push({
|
|
75
|
+
level: match[1].length,
|
|
76
|
+
text: match[2].trim()
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const htmlMatches = text.matchAll(/<h([1-6])[^>]*>(.*?)<\/h\1>/gim);
|
|
81
|
+
for (const match of htmlMatches) {
|
|
82
|
+
headings.push({
|
|
83
|
+
level: Number.parseInt(match[1], 10),
|
|
84
|
+
text: match[2].replace(/<[^>]+>/g, "").trim()
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return headings;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractFirstParagraph(text) {
|
|
92
|
+
const stripped = text
|
|
93
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
94
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
95
|
+
.replace(/<[^>]+>/g, " ")
|
|
96
|
+
.replace(/\r/g, "");
|
|
97
|
+
|
|
98
|
+
const paragraphs = stripped
|
|
99
|
+
.split(/\n\s*\n/)
|
|
100
|
+
.map((entry) => entry.replace(/\s+/g, " ").trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
|
|
103
|
+
return paragraphs[0] || null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildSignalSummary(reference, text) {
|
|
107
|
+
return Object.fromEntries(
|
|
108
|
+
Object.keys(SIGNAL_PATTERNS).map((signal) => {
|
|
109
|
+
const matched = SIGNAL_PATTERNS[signal].test(text);
|
|
110
|
+
return [
|
|
111
|
+
signal,
|
|
112
|
+
{
|
|
113
|
+
count: matched ? 1 : 0,
|
|
114
|
+
examples: matched ? [reference] : []
|
|
115
|
+
}
|
|
116
|
+
];
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const PAGE_SCORE_RULES = [
|
|
122
|
+
{
|
|
123
|
+
key: "title",
|
|
124
|
+
label: "页面有清晰 title / 主标题",
|
|
125
|
+
maxPoints: 12,
|
|
126
|
+
passed: (page) => Boolean(page.metadata.title)
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: "metaDescription",
|
|
130
|
+
label: "页面有摘要描述",
|
|
131
|
+
maxPoints: 8,
|
|
132
|
+
passed: (page) => Boolean(page.metadata.metaDescription)
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
key: "canonical",
|
|
136
|
+
label: "页面有 canonical",
|
|
137
|
+
maxPoints: 8,
|
|
138
|
+
passed: (page) => Boolean(page.metadata.canonical)
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: "jsonLd",
|
|
142
|
+
label: "页面有结构化数据",
|
|
143
|
+
maxPoints: 12,
|
|
144
|
+
passed: (page) => page.signals.json_ld.count > 0
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: "author",
|
|
148
|
+
label: "页面有作者 / 时间 / 审校信号",
|
|
149
|
+
maxPoints: 10,
|
|
150
|
+
passed: (page) => page.signals.author_markers.count > 0
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
key: "citations",
|
|
154
|
+
label: "页面有来源链接",
|
|
155
|
+
maxPoints: 10,
|
|
156
|
+
passed: (page) => page.signals.citations.count > 0
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: "qaHeadings",
|
|
160
|
+
label: "页面结构适合被提取答案",
|
|
161
|
+
maxPoints: 12,
|
|
162
|
+
passed: (page) => page.signals.qa_headings.count > 0 || page.headingStats.questionHeadingCount > 0
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
key: "comparison",
|
|
166
|
+
label: "页面覆盖比较 / 决策信息",
|
|
167
|
+
maxPoints: 8,
|
|
168
|
+
passed: (page) => page.signals.comparison_intent.count > 0
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
key: "evidence",
|
|
172
|
+
label: "页面有第一方证据或方法论",
|
|
173
|
+
maxPoints: 10,
|
|
174
|
+
passed: (page) => page.signals.original_research.count > 0
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: "directAnswer",
|
|
178
|
+
label: "页面开头可快速提取直接答案",
|
|
179
|
+
maxPoints: 10,
|
|
180
|
+
passed: (page) => Boolean(page.directAnswerCandidate && page.directAnswerCandidate.length >= 50)
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
function evaluatePageScore(page) {
|
|
185
|
+
const checks = PAGE_SCORE_RULES.map((rule) => {
|
|
186
|
+
const passed = rule.passed(page);
|
|
187
|
+
return {
|
|
188
|
+
key: rule.key,
|
|
189
|
+
label: rule.label,
|
|
190
|
+
passed,
|
|
191
|
+
maxPoints: rule.maxPoints,
|
|
192
|
+
pointsAwarded: passed ? rule.maxPoints : 0
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const score = checks.reduce((sum, check) => sum + check.pointsAwarded, 0);
|
|
197
|
+
return {
|
|
198
|
+
score,
|
|
199
|
+
maxScore: 100,
|
|
200
|
+
checks
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getScoreLabel(score) {
|
|
205
|
+
if (score >= 80) {
|
|
206
|
+
return "稳固";
|
|
207
|
+
}
|
|
208
|
+
if (score >= 60) {
|
|
209
|
+
return "可优化";
|
|
210
|
+
}
|
|
211
|
+
if (score >= 40) {
|
|
212
|
+
return "偏弱";
|
|
213
|
+
}
|
|
214
|
+
return "需要重做";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildProblemAreas(page) {
|
|
218
|
+
const areas = [];
|
|
219
|
+
|
|
220
|
+
if (!page.metadata.title || !page.metadata.metaDescription || !page.metadata.canonical) {
|
|
221
|
+
areas.push({
|
|
222
|
+
area: "页面元信息",
|
|
223
|
+
severity: !page.metadata.title ? "高" : "中",
|
|
224
|
+
issues: [
|
|
225
|
+
!page.metadata.title ? "缺少 title / 主标题" : null,
|
|
226
|
+
!page.metadata.metaDescription ? "缺少摘要描述" : null,
|
|
227
|
+
!page.metadata.canonical ? "缺少 canonical" : null
|
|
228
|
+
].filter(Boolean),
|
|
229
|
+
whyItMatters: "模型和搜索系统需要先理解这页是什么、应该归到哪个 canonical 实体上。"
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (page.signals.json_ld.count === 0 || page.signals.author_markers.count === 0 || page.signals.citations.count === 0) {
|
|
234
|
+
areas.push({
|
|
235
|
+
area: "可信度与引用",
|
|
236
|
+
severity: page.signals.citations.count === 0 ? "高" : "中",
|
|
237
|
+
issues: [
|
|
238
|
+
page.signals.json_ld.count === 0 ? "缺少结构化数据" : null,
|
|
239
|
+
page.signals.author_markers.count === 0 ? "缺少作者 / 更新时间 / 审校信号" : null,
|
|
240
|
+
page.signals.citations.count === 0 ? "缺少来源链接" : null
|
|
241
|
+
].filter(Boolean),
|
|
242
|
+
whyItMatters: "如果页面没有可信来源、作者和结构化实体,AI 搜索更难放心引用。"
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (page.headingStats.questionHeadingCount === 0 && page.signals.qa_headings.count === 0) {
|
|
247
|
+
areas.push({
|
|
248
|
+
area: "答案可提取性",
|
|
249
|
+
severity: "高",
|
|
250
|
+
issues: ["缺少问答式或决策式标题", "页面结构不够 answer-first"],
|
|
251
|
+
whyItMatters: "模型需要在很短上下文里抓到直接答案、适用对象和限制条件。"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (page.signals.original_research.count === 0 && page.signals.comparison_intent.count === 0) {
|
|
256
|
+
areas.push({
|
|
257
|
+
area: "差异化内容",
|
|
258
|
+
severity: "中",
|
|
259
|
+
issues: ["缺少方法论 / 第一方证据", "缺少比较与决策内容"],
|
|
260
|
+
whyItMatters: "如果页面没有独特证据和决策信息,就很难在生成式结果里占据引用位置。"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return areas;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildRecommendedBlocks(page) {
|
|
268
|
+
const blocks = [];
|
|
269
|
+
if (!page.directAnswerCandidate || page.directAnswerCandidate.length < 50) {
|
|
270
|
+
blocks.push("在首屏补一个直接答案摘要块,先回答“这是什么 / 适合谁 / 主要限制是什么”。");
|
|
271
|
+
}
|
|
272
|
+
if (page.headingStats.questionHeadingCount === 0) {
|
|
273
|
+
blocks.push("补 2 到 4 个问答式 H2,例如“适合谁”“什么时候不适合”“和替代方案差别是什么”。");
|
|
274
|
+
}
|
|
275
|
+
if (page.signals.citations.count === 0) {
|
|
276
|
+
blocks.push("增加来源链接或证据区块,给关键事实、数据和判断补出处。");
|
|
277
|
+
}
|
|
278
|
+
if (page.signals.original_research.count === 0) {
|
|
279
|
+
blocks.push("补第一方证据区块,例如方法论、流程、案例、截图、基准或内部数据。");
|
|
280
|
+
}
|
|
281
|
+
if (page.signals.comparison_intent.count === 0) {
|
|
282
|
+
blocks.push("增加 comparison / alternatives / trade-offs 模块,帮助模型提取决策信息。");
|
|
283
|
+
}
|
|
284
|
+
if (page.signals.json_ld.count === 0) {
|
|
285
|
+
blocks.push("补与页面可见内容一致的 JSON-LD。");
|
|
286
|
+
}
|
|
287
|
+
return blocks;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildRewriteBrief(page, problems, recommendedBlocks) {
|
|
291
|
+
return {
|
|
292
|
+
objective: "把当前页面改成更容易被生成式搜索引擎理解、提取、引用的 answer-first 页面。",
|
|
293
|
+
audience: "负责该页面的 PM、内容负责人、SEO 或 agent。",
|
|
294
|
+
keep: [
|
|
295
|
+
page.metadata.title ? `保留核心主题:${page.metadata.title}` : "保留当前页面的核心主题,不要改成另一个意图。",
|
|
296
|
+
"保留事实准确性,不用为了 SEO 或 GEO 添加页面没有证据支撑的说法。"
|
|
297
|
+
],
|
|
298
|
+
change: problems.flatMap((problem) => problem.issues).slice(0, 6),
|
|
299
|
+
addSections: recommendedBlocks,
|
|
300
|
+
openingPattern: "用 2 到 4 句先给直接答案,再说明适用对象、关键限制与下一步。",
|
|
301
|
+
evidencePattern: "给结论补来源链接、案例、方法论、数据或截图说明。",
|
|
302
|
+
schemaHint:
|
|
303
|
+
page.signals.json_ld.count > 0
|
|
304
|
+
? "保留现有 schema,但确认与页面可见内容一致。"
|
|
305
|
+
: "优先考虑 Article / FAQPage / Product / Organization 中最贴近页面真实内容的 schema。"
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildAgentPrompt(page, rewriteBrief) {
|
|
310
|
+
return [
|
|
311
|
+
"请先把这页当成单页 GEO 修复任务处理。",
|
|
312
|
+
`当前页面:${page.reference}`,
|
|
313
|
+
`当前分数:${page.score.score}/${page.score.maxScore}(${getScoreLabel(page.score.score)})`,
|
|
314
|
+
`优先改动:${rewriteBrief.change.slice(0, 4).join(";") || "补直接答案、证据与结构化信息"}`,
|
|
315
|
+
"先重写页面 opening,再补问答式标题、来源链接、证据区块和最合适的 schema。",
|
|
316
|
+
"如果是代码仓库,请优先找模板、metadata helper、schema 生成点,而不是只改单一页面文案。",
|
|
317
|
+
"完成后重新跑 page-audit,确认分数和问题区域是否改善。"
|
|
318
|
+
].join(" ");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildSummary(page, problems) {
|
|
322
|
+
if (page.score.score >= 80) {
|
|
323
|
+
return "这页已经具备较好的 GEO 基础,下一步重点是增强差异化证据与可引用素材。";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (problems.length === 0) {
|
|
327
|
+
return "这页没有明显结构性缺口,可以继续加强引用资产和第一方证据。";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return `这页当前最需要优先处理的是${problems[0].area},否则模型很难稳定理解并引用页面内容。`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function auditPage(input, options = {}) {
|
|
334
|
+
const sourceType = isUrlInput(input) ? "url" : "file";
|
|
335
|
+
let source;
|
|
336
|
+
|
|
337
|
+
if (sourceType === "url") {
|
|
338
|
+
const url = normalizeUrl(input);
|
|
339
|
+
const result = await fetchText(url);
|
|
340
|
+
if (!result.ok) {
|
|
341
|
+
throw new Error(`抓取失败:${url.toString()}(状态码 ${result.status})`);
|
|
342
|
+
}
|
|
343
|
+
source = {
|
|
344
|
+
reference: result.url,
|
|
345
|
+
content: result.text,
|
|
346
|
+
fetchStatus: result.status
|
|
347
|
+
};
|
|
348
|
+
} else {
|
|
349
|
+
const result = await readLocalFile(input);
|
|
350
|
+
source = {
|
|
351
|
+
reference: result.path,
|
|
352
|
+
content: result.text,
|
|
353
|
+
fetchStatus: null
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const headings = extractHeadings(source.content);
|
|
358
|
+
const page = {
|
|
359
|
+
kind: "geo-page-audit",
|
|
360
|
+
input,
|
|
361
|
+
sourceType,
|
|
362
|
+
reference: source.reference,
|
|
363
|
+
fetchStatus: source.fetchStatus,
|
|
364
|
+
metadata: {
|
|
365
|
+
title: extractTitle(source.content),
|
|
366
|
+
metaDescription: extractMetaDescription(source.content),
|
|
367
|
+
canonical: extractCanonical(source.content)
|
|
368
|
+
},
|
|
369
|
+
directAnswerCandidate: extractFirstParagraph(source.content),
|
|
370
|
+
headingStats: {
|
|
371
|
+
totalHeadingCount: headings.length,
|
|
372
|
+
questionHeadingCount: headings.filter((heading) => heading.text.includes("?") || heading.text.includes("?")).length
|
|
373
|
+
},
|
|
374
|
+
headings: headings.slice(0, options.maxHeadings || 12),
|
|
375
|
+
signals: buildSignalSummary(source.reference, source.content)
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
page.score = evaluatePageScore(page);
|
|
379
|
+
page.scoreLabel = getScoreLabel(page.score.score);
|
|
380
|
+
page.problemAreas = buildProblemAreas(page);
|
|
381
|
+
page.recommendedBlocks = buildRecommendedBlocks(page);
|
|
382
|
+
page.rewriteBrief = buildRewriteBrief(page, page.problemAreas, page.recommendedBlocks);
|
|
383
|
+
page.summary = buildSummary(page, page.problemAreas);
|
|
384
|
+
page.agentPrompt = buildAgentPrompt(page, page.rewriteBrief);
|
|
385
|
+
|
|
386
|
+
return page;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function renderPageAuditMarkdown(report) {
|
|
390
|
+
const lines = [
|
|
391
|
+
"# GEO 单页审计",
|
|
392
|
+
"",
|
|
393
|
+
`- 输入:\`${report.input}\``,
|
|
394
|
+
`- 来源类型:\`${report.sourceType}\``,
|
|
395
|
+
`- 分析对象:\`${report.reference}\``,
|
|
396
|
+
`- GEO 单页分数:\`${report.score.score}/${report.score.maxScore}\`(${report.scoreLabel})`,
|
|
397
|
+
`- 总结:${report.summary}`,
|
|
398
|
+
""
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
if (report.fetchStatus !== null) {
|
|
402
|
+
lines.push(`- 抓取状态:\`${report.fetchStatus}\``);
|
|
403
|
+
lines.push("");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
lines.push("## 页面快照", "");
|
|
407
|
+
lines.push(`- title:\`${report.metadata.title ?? "缺失"}\``);
|
|
408
|
+
lines.push(`- meta description:\`${report.metadata.metaDescription ?? "缺失"}\``);
|
|
409
|
+
lines.push(`- canonical:\`${report.metadata.canonical ?? "缺失"}\``);
|
|
410
|
+
lines.push(`- 首段直接答案候选:${report.directAnswerCandidate ? report.directAnswerCandidate.slice(0, 180) : "缺失"}`);
|
|
411
|
+
|
|
412
|
+
lines.push("", "## 问题区域", "");
|
|
413
|
+
if (report.problemAreas.length === 0) {
|
|
414
|
+
lines.push("- 当前没有明显的页面级高风险区域。");
|
|
415
|
+
} else {
|
|
416
|
+
for (const area of report.problemAreas) {
|
|
417
|
+
lines.push(`- ${area.area}|${area.severity}|${area.issues.join("、")}`);
|
|
418
|
+
lines.push(` ${area.whyItMatters}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
lines.push("", "## 推荐新增模块", "");
|
|
423
|
+
for (const item of report.recommendedBlocks) {
|
|
424
|
+
lines.push(`- ${item}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
lines.push("", "## Rewrite Brief", "");
|
|
428
|
+
lines.push(`- 目标:${report.rewriteBrief.objective}`);
|
|
429
|
+
lines.push(`- Opening pattern:${report.rewriteBrief.openingPattern}`);
|
|
430
|
+
lines.push(`- Evidence pattern:${report.rewriteBrief.evidencePattern}`);
|
|
431
|
+
lines.push(`- Schema hint:${report.rewriteBrief.schemaHint}`);
|
|
432
|
+
|
|
433
|
+
lines.push("", "## 建议保留", "");
|
|
434
|
+
for (const item of report.rewriteBrief.keep) {
|
|
435
|
+
lines.push(`- ${item}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
lines.push("", "## 建议调整", "");
|
|
439
|
+
for (const item of report.rewriteBrief.change) {
|
|
440
|
+
lines.push(`- ${item}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
lines.push("", "## 建议新增段落", "");
|
|
444
|
+
for (const item of report.rewriteBrief.addSections) {
|
|
445
|
+
lines.push(`- ${item}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
lines.push("", "## 页面标题快照", "");
|
|
449
|
+
for (const heading of report.headings) {
|
|
450
|
+
lines.push(`- H${heading.level}:${heading.text}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
lines.push("", "## 评分明细", "");
|
|
454
|
+
for (const check of report.score.checks) {
|
|
455
|
+
lines.push(`- ${check.label}:\`${check.passed}\`(${check.pointsAwarded}/${check.maxPoints})`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
lines.push("", "## Agent Prompt", "", report.agentPrompt, "");
|
|
459
|
+
return lines.join("\n");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function writePageAuditOutput(outputPath, content) {
|
|
463
|
+
return writeScanOutput(outputPath, content);
|
|
464
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { auditPage, writePageAuditOutput } from "./page-audit.js";
|
|
2
|
+
|
|
3
|
+
function buildOpeningDraft(report) {
|
|
4
|
+
const title = report.metadata.title || "这个页面";
|
|
5
|
+
return [
|
|
6
|
+
`${title} 是一页需要先用直接答案说明核心价值的内容。`,
|
|
7
|
+
"这页最适合先讲清楚它是什么、适合谁、什么时候不适合,以及用户下一步应该看什么。",
|
|
8
|
+
"如果没有这些信息,生成式搜索系统很难稳定提取出可引用的答案。"
|
|
9
|
+
].join(" ");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildSuggestedHeadings(report) {
|
|
13
|
+
const headings = [
|
|
14
|
+
"这是什么",
|
|
15
|
+
"适合谁",
|
|
16
|
+
"什么时候不适合",
|
|
17
|
+
"和其他方案有什么差别",
|
|
18
|
+
"关键事实与证据",
|
|
19
|
+
"下一步该怎么做"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
if (report.signals.comparison_intent.count > 0) {
|
|
23
|
+
headings.splice(3, 1, "和替代方案相比有什么差别");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return headings.slice(0, 6);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildFaqQuestions(report) {
|
|
30
|
+
return [
|
|
31
|
+
`${report.metadata.title || "这页的主题"} 适合哪些人?`,
|
|
32
|
+
"什么时候不应该选择这个方案?",
|
|
33
|
+
"有哪些限制条件或前提?",
|
|
34
|
+
"有哪些证据可以支持这页的主张?"
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildSchemaRecommendations(report) {
|
|
39
|
+
const recommendations = [];
|
|
40
|
+
|
|
41
|
+
if (report.signals.qa_headings.count > 0 || report.headingStats.questionHeadingCount > 0) {
|
|
42
|
+
recommendations.push("FAQPage");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (report.signals.original_research.count > 0) {
|
|
46
|
+
recommendations.push("Article");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (report.signals.comparison_intent.count > 0) {
|
|
50
|
+
recommendations.push("Article");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (recommendations.length === 0) {
|
|
54
|
+
recommendations.push("Article");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Array.from(new Set(recommendations));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildMetadataDrafts(report) {
|
|
61
|
+
const baseTitle = report.metadata.title || "页面主题";
|
|
62
|
+
return {
|
|
63
|
+
title: `${baseTitle}|适合谁、限制与关键信息`,
|
|
64
|
+
metaDescription: `${baseTitle} 的直接答案、适用对象、限制条件、关键证据与下一步建议。`,
|
|
65
|
+
canonical: report.metadata.canonical || "请设置到该页面的唯一正式 URL"
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildContentSections(report) {
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
name: "Opening summary",
|
|
73
|
+
goal: "用 2 到 4 句先给直接答案。",
|
|
74
|
+
draft: buildOpeningDraft(report)
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "Decision block",
|
|
78
|
+
goal: "明确适合谁、什么时候不适合、主要 trade-off。",
|
|
79
|
+
bullets: [
|
|
80
|
+
"适合谁",
|
|
81
|
+
"主要限制",
|
|
82
|
+
"决策条件",
|
|
83
|
+
"如果不适合,应该去看哪一页"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "Evidence block",
|
|
88
|
+
goal: "把结论和主张接到来源、案例、方法论或数据。",
|
|
89
|
+
bullets: [
|
|
90
|
+
"来源链接",
|
|
91
|
+
"方法论说明",
|
|
92
|
+
"案例或截图",
|
|
93
|
+
"更新时间"
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildExecutionChecklist(report) {
|
|
100
|
+
return [
|
|
101
|
+
"先重写 opening summary,让第一屏可以直接回答用户问题。",
|
|
102
|
+
"补上问答式 H2 和 decision block。",
|
|
103
|
+
"补来源链接、作者/更新时间和 evidence block。",
|
|
104
|
+
"补最贴近页面真实内容的 schema。",
|
|
105
|
+
"完成后重新跑 page-audit,对比分数和问题区域。"
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function createRewritePack(input, options = {}) {
|
|
110
|
+
const pageAudit = await auditPage(input, options);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
kind: "geo-rewrite-pack",
|
|
114
|
+
input,
|
|
115
|
+
sourceType: pageAudit.sourceType,
|
|
116
|
+
reference: pageAudit.reference,
|
|
117
|
+
summary: `这份 rewrite pack 基于单页审计生成,优先帮助你把页面改成更适合生成式搜索理解与引用的 answer-first 结构。`,
|
|
118
|
+
currentScore: {
|
|
119
|
+
score: pageAudit.score.score,
|
|
120
|
+
maxScore: pageAudit.score.maxScore,
|
|
121
|
+
label: pageAudit.scoreLabel
|
|
122
|
+
},
|
|
123
|
+
pageAudit,
|
|
124
|
+
metadataDrafts: buildMetadataDrafts(pageAudit),
|
|
125
|
+
suggestedHeadings: buildSuggestedHeadings(pageAudit),
|
|
126
|
+
faqQuestions: buildFaqQuestions(pageAudit),
|
|
127
|
+
schemaRecommendations: buildSchemaRecommendations(pageAudit),
|
|
128
|
+
contentSections: buildContentSections(pageAudit),
|
|
129
|
+
executionChecklist: buildExecutionChecklist(pageAudit),
|
|
130
|
+
agentPrompt: [
|
|
131
|
+
"请根据这份 rewrite pack 改写页面。",
|
|
132
|
+
`页面:${pageAudit.reference}`,
|
|
133
|
+
`当前分数:${pageAudit.score.score}/${pageAudit.score.maxScore}`,
|
|
134
|
+
`优先处理:${pageAudit.problemAreas.map((item) => item.area).join("、") || "opening summary 与 evidence block"}`,
|
|
135
|
+
"先改 metadata drafts 和 opening summary,再补 headings、FAQ、evidence block、schema。",
|
|
136
|
+
"改完后重新跑 page-audit,确认页面级 GEO 分数有提升。"
|
|
137
|
+
].join(" ")
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function renderRewritePackMarkdown(pack) {
|
|
142
|
+
const lines = [
|
|
143
|
+
"# GEO Rewrite Pack",
|
|
144
|
+
"",
|
|
145
|
+
`- 输入:\`${pack.input}\``,
|
|
146
|
+
`- 分析对象:\`${pack.reference}\``,
|
|
147
|
+
`- 当前分数:\`${pack.currentScore.score}/${pack.currentScore.maxScore}\`(${pack.currentScore.label})`,
|
|
148
|
+
`- 总结:${pack.summary}`,
|
|
149
|
+
"",
|
|
150
|
+
"## Metadata Drafts",
|
|
151
|
+
"",
|
|
152
|
+
`- title:${pack.metadataDrafts.title}`,
|
|
153
|
+
`- meta description:${pack.metadataDrafts.metaDescription}`,
|
|
154
|
+
`- canonical:${pack.metadataDrafts.canonical}`,
|
|
155
|
+
"",
|
|
156
|
+
"## Suggested Headings",
|
|
157
|
+
""
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
for (const heading of pack.suggestedHeadings) {
|
|
161
|
+
lines.push(`- ${heading}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
lines.push("", "## FAQ Questions", "");
|
|
165
|
+
for (const question of pack.faqQuestions) {
|
|
166
|
+
lines.push(`- ${question}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
lines.push("", "## Schema Recommendations", "");
|
|
170
|
+
for (const schema of pack.schemaRecommendations) {
|
|
171
|
+
lines.push(`- ${schema}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
lines.push("", "## Content Sections", "");
|
|
175
|
+
for (const section of pack.contentSections) {
|
|
176
|
+
lines.push(`- ${section.name}|${section.goal}`);
|
|
177
|
+
if (section.draft) {
|
|
178
|
+
lines.push(` Draft:${section.draft}`);
|
|
179
|
+
}
|
|
180
|
+
if (section.bullets) {
|
|
181
|
+
for (const bullet of section.bullets) {
|
|
182
|
+
lines.push(` - ${bullet}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
lines.push("", "## Execution Checklist", "");
|
|
188
|
+
for (const item of pack.executionChecklist) {
|
|
189
|
+
lines.push(`- ${item}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lines.push("", "## Agent Prompt", "", pack.agentPrompt, "");
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function writeRewritePackOutput(outputPath, content) {
|
|
197
|
+
return writePageAuditOutput(outputPath, content);
|
|
198
|
+
}
|