geo-ai-search-optimization 1.4.2 → 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/examples/github-action.yml +27 -0
- package/package.json +17 -3
- package/src/audit-diff.js +184 -0
- package/src/auto-fix.js +349 -0
- package/src/batch-full-page-audit.js +151 -0
- package/src/batch-page-audit.js +140 -0
- package/src/benchmark.js +126 -0
- package/src/ci.js +81 -0
- package/src/citability.js +311 -0
- package/src/citation-check.js +157 -0
- package/src/citation-monitor.js +86 -0
- package/src/cli-site-ops-commands.js +638 -4
- package/src/compare.js +175 -0
- package/src/config.js +105 -0
- package/src/content-gap.js +170 -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 +35 -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/pm-brief.js +8 -0
- package/src/pre-commit.js +62 -0
- package/src/readability.js +252 -0
- package/src/report.js +37 -12
- package/src/security.js +249 -0
- package/src/sitemap.js +323 -0
- package/src/snapshot.js +51 -0
- package/src/social-meta.js +293 -0
- package/src/topics.js +275 -0
- package/src/trend.js +102 -0
- package/src/url-onboarding.js +1 -1
- package/src/validate-llms.js +307 -0
- package/src/validate-schema.js +306 -0
- package/src/watch.js +49 -0
package/action.yml
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
name: "GEO AI Search Optimization"
|
|
2
|
+
description: "Run GEO audit in CI/CD. Check your site's AI search readiness score and fail on regression."
|
|
3
|
+
author: "geo-ai-search-optimization"
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
project-path:
|
|
7
|
+
description: "Path to the project to audit"
|
|
8
|
+
required: true
|
|
9
|
+
default: "."
|
|
10
|
+
min-score:
|
|
11
|
+
description: "Minimum GEO score to pass (0-100)"
|
|
12
|
+
required: false
|
|
13
|
+
default: "40"
|
|
14
|
+
baseline:
|
|
15
|
+
description: "Path to baseline audit JSON for regression detection"
|
|
16
|
+
required: false
|
|
17
|
+
fail-on-regression:
|
|
18
|
+
description: "Fail if score drops compared to baseline"
|
|
19
|
+
required: false
|
|
20
|
+
default: "false"
|
|
21
|
+
output-format:
|
|
22
|
+
description: "Output format: json or markdown"
|
|
23
|
+
required: false
|
|
24
|
+
default: "markdown"
|
|
25
|
+
save-snapshot:
|
|
26
|
+
description: "Save audit snapshot for trend tracking"
|
|
27
|
+
required: false
|
|
28
|
+
default: "false"
|
|
29
|
+
|
|
30
|
+
outputs:
|
|
31
|
+
score:
|
|
32
|
+
description: "GEO audit score (0-100)"
|
|
33
|
+
value: ${{ steps.audit.outputs.score }}
|
|
34
|
+
score-label:
|
|
35
|
+
description: "Human-readable score label"
|
|
36
|
+
value: ${{ steps.audit.outputs.score-label }}
|
|
37
|
+
passed:
|
|
38
|
+
description: "Whether the audit passed (true/false)"
|
|
39
|
+
value: ${{ steps.audit.outputs.passed }}
|
|
40
|
+
report-path:
|
|
41
|
+
description: "Path to the generated report file"
|
|
42
|
+
value: ${{ steps.audit.outputs.report-path }}
|
|
43
|
+
|
|
44
|
+
runs:
|
|
45
|
+
using: "composite"
|
|
46
|
+
steps:
|
|
47
|
+
- name: Setup Node.js
|
|
48
|
+
uses: actions/setup-node@v4
|
|
49
|
+
with:
|
|
50
|
+
node-version: "18"
|
|
51
|
+
|
|
52
|
+
- name: Install geo-ai-search-optimization
|
|
53
|
+
shell: bash
|
|
54
|
+
run: npm install -g geo-ai-search-optimization
|
|
55
|
+
|
|
56
|
+
- name: Run GEO Audit
|
|
57
|
+
id: audit
|
|
58
|
+
shell: bash
|
|
59
|
+
run: |
|
|
60
|
+
PROJ_PATH="${{ inputs.project-path }}"
|
|
61
|
+
BASELINE="${{ inputs.baseline }}"
|
|
62
|
+
MIN_SCORE="${{ inputs.min-score }}"
|
|
63
|
+
|
|
64
|
+
ARGS=("$PROJ_PATH" "--json")
|
|
65
|
+
|
|
66
|
+
if [ -n "$BASELINE" ]; then
|
|
67
|
+
ARGS+=("--baseline" "$BASELINE")
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if [ "${{ inputs.fail-on-regression }}" = "true" ]; then
|
|
71
|
+
ARGS+=("--fail-on-regression")
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
ARGS+=("--min-score" "$MIN_SCORE")
|
|
75
|
+
|
|
76
|
+
RESULT=$(geo-ai-search-optimization ci "${ARGS[@]}" 2>geo-audit-stderr.log) || true
|
|
77
|
+
echo "$RESULT" > geo-audit-result.json
|
|
78
|
+
|
|
79
|
+
SCORE=$(echo "$RESULT" | node -e "
|
|
80
|
+
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
|
|
81
|
+
console.log(data.audit?.score ?? data.score ?? 0);
|
|
82
|
+
" 2>/dev/null || echo "0")
|
|
83
|
+
|
|
84
|
+
LABEL=$(echo "$RESULT" | node -e "
|
|
85
|
+
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
|
|
86
|
+
console.log(data.audit?.scoreLabel ?? data.scoreLabel ?? 'unknown');
|
|
87
|
+
" 2>/dev/null || echo "unknown")
|
|
88
|
+
|
|
89
|
+
PASSED=$(echo "$RESULT" | node -e "
|
|
90
|
+
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
|
|
91
|
+
console.log(data.passed ?? (data.exitCode === 0));
|
|
92
|
+
" 2>/dev/null || echo "false")
|
|
93
|
+
|
|
94
|
+
echo "score=$SCORE" >> $GITHUB_OUTPUT
|
|
95
|
+
echo "score-label=$LABEL" >> $GITHUB_OUTPUT
|
|
96
|
+
echo "passed=$PASSED" >> $GITHUB_OUTPUT
|
|
97
|
+
echo "report-path=geo-audit-result.json" >> $GITHUB_OUTPUT
|
|
98
|
+
|
|
99
|
+
- name: Save Snapshot
|
|
100
|
+
if: inputs.save-snapshot == 'true'
|
|
101
|
+
shell: bash
|
|
102
|
+
run: |
|
|
103
|
+
geo-ai-search-optimization audit "${{ inputs.project-path }}" --save --json > /dev/null 2>&1 || true
|
|
104
|
+
|
|
105
|
+
- name: Generate Markdown Report
|
|
106
|
+
if: inputs.output-format == 'markdown'
|
|
107
|
+
shell: bash
|
|
108
|
+
run: |
|
|
109
|
+
geo-ai-search-optimization audit "${{ inputs.project-path }}" --out geo-audit-report.md || true
|
|
110
|
+
|
|
111
|
+
- name: Post Summary
|
|
112
|
+
if: always()
|
|
113
|
+
shell: bash
|
|
114
|
+
run: |
|
|
115
|
+
echo "### GEO Audit Results" >> $GITHUB_STEP_SUMMARY
|
|
116
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
117
|
+
echo "- **Score:** ${{ steps.audit.outputs.score }}/100 (${{ steps.audit.outputs.score-label }})" >> $GITHUB_STEP_SUMMARY
|
|
118
|
+
echo "- **Passed:** ${{ steps.audit.outputs.passed }}" >> $GITHUB_STEP_SUMMARY
|
|
119
|
+
echo "- **Min Score:** ${{ inputs.min-score }}" >> $GITHUB_STEP_SUMMARY
|
|
120
|
+
|
|
121
|
+
- name: Check Result
|
|
122
|
+
if: steps.audit.outputs.passed == 'false'
|
|
123
|
+
shell: bash
|
|
124
|
+
run: |
|
|
125
|
+
echo "::error::GEO audit failed. Score: ${{ steps.audit.outputs.score }}/100 (minimum: ${{ inputs.min-score }})"
|
|
126
|
+
exit 1
|
|
127
|
+
|
|
128
|
+
branding:
|
|
129
|
+
icon: "search"
|
|
130
|
+
color: "blue"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Example GitHub Action workflow for GEO CI checks
|
|
2
|
+
# Add this to .github/workflows/geo-ci.yml in your project
|
|
3
|
+
|
|
4
|
+
name: GEO CI
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [main]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
geo-check:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 20
|
|
19
|
+
- run: npm install -g geo-ai-search-optimization
|
|
20
|
+
- name: Run GEO audit
|
|
21
|
+
run: geo-ai-search-optimization ci . --min-score 40 --json
|
|
22
|
+
- name: Run GEO audit with baseline
|
|
23
|
+
if: always()
|
|
24
|
+
run: |
|
|
25
|
+
# Optional: compare against a saved baseline
|
|
26
|
+
# geo-ai-search-optimization ci . --min-score 40 --baseline baseline.json --fail-on-regression
|
|
27
|
+
echo "Baseline comparison skipped (no baseline file)"
|
package/package.json
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geo-ai-search-optimization",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Install and run a Generative Engine Optimization (GEO)-first, SEO-supported Codex skill for website optimization.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"geo-ai-search-optimization": "bin/geo-ai-search-optimization.js"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
|
-
".":
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
}
|
|
11
14
|
},
|
|
15
|
+
"types": "./src/index.d.ts",
|
|
12
16
|
"files": [
|
|
13
17
|
"bin",
|
|
14
18
|
"src",
|
|
15
19
|
"resources",
|
|
20
|
+
"examples",
|
|
21
|
+
"action.yml",
|
|
16
22
|
"README.md",
|
|
17
23
|
"LICENSE"
|
|
18
24
|
],
|
|
@@ -20,6 +26,7 @@
|
|
|
20
26
|
"node": ">=18"
|
|
21
27
|
},
|
|
22
28
|
"scripts": {
|
|
29
|
+
"test": "node --test 'test/**/*.test.js'",
|
|
23
30
|
"postinstall": "node ./src/postinstall.js",
|
|
24
31
|
"check": "node ./src/cli.js --help",
|
|
25
32
|
"scan:self": "node ./src/cli.js scan ./resources/geo-ai-search-optimization"
|
|
@@ -32,7 +39,14 @@
|
|
|
32
39
|
"codex-skill",
|
|
33
40
|
"chatgpt",
|
|
34
41
|
"perplexity",
|
|
35
|
-
"gemini"
|
|
42
|
+
"gemini",
|
|
43
|
+
"llms-txt",
|
|
44
|
+
"schema-validation",
|
|
45
|
+
"eeat",
|
|
46
|
+
"citability",
|
|
47
|
+
"ai-crawler",
|
|
48
|
+
"aeo",
|
|
49
|
+
"ai-optimization"
|
|
36
50
|
],
|
|
37
51
|
"license": "MIT"
|
|
38
52
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeScanOutput } from "./scan.js";
|
|
4
|
+
|
|
5
|
+
async function loadAuditJson(filePath) {
|
|
6
|
+
const resolved = path.resolve(filePath);
|
|
7
|
+
const raw = await fs.readFile(resolved, "utf8");
|
|
8
|
+
const data = JSON.parse(raw);
|
|
9
|
+
if (!data.kind || !data.score || !data.checks) {
|
|
10
|
+
throw new Error(`${resolved} 不是有效的审计 JSON(缺少 kind / score / checks)`);
|
|
11
|
+
}
|
|
12
|
+
return data;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function diffChecks(baselineChecks, currentChecks) {
|
|
16
|
+
const baselineMap = new Map(baselineChecks.map((c) => [c.key, c]));
|
|
17
|
+
return currentChecks.map((current) => {
|
|
18
|
+
const baseline = baselineMap.get(current.key);
|
|
19
|
+
if (!baseline) {
|
|
20
|
+
return { ...current, baselinePassed: null, delta: current.pointsAwarded };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
key: current.key,
|
|
24
|
+
label: current.label,
|
|
25
|
+
baselinePassed: baseline.passed,
|
|
26
|
+
currentPassed: current.passed,
|
|
27
|
+
baselinePoints: baseline.pointsAwarded,
|
|
28
|
+
currentPoints: current.pointsAwarded,
|
|
29
|
+
maxPoints: current.maxPoints,
|
|
30
|
+
delta: current.pointsAwarded - baseline.pointsAwarded
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function classifyChanges(checkDiffs) {
|
|
36
|
+
const improved = [];
|
|
37
|
+
const regressed = [];
|
|
38
|
+
const unchanged = [];
|
|
39
|
+
|
|
40
|
+
for (const diff of checkDiffs) {
|
|
41
|
+
if (diff.delta > 0) {
|
|
42
|
+
improved.push(diff);
|
|
43
|
+
} else if (diff.delta < 0) {
|
|
44
|
+
regressed.push(diff);
|
|
45
|
+
} else {
|
|
46
|
+
unchanged.push(diff);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { improved, regressed, unchanged };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function diffAreas(baselineAreas, currentAreas) {
|
|
54
|
+
if (!baselineAreas || !currentAreas) return [];
|
|
55
|
+
|
|
56
|
+
const baselineMap = new Map(baselineAreas.map((a) => [a.key, a]));
|
|
57
|
+
return currentAreas.map((current) => {
|
|
58
|
+
const baseline = baselineMap.get(current.key);
|
|
59
|
+
if (!baseline) {
|
|
60
|
+
return { key: current.key, title: current.title, baselineSeverity: null, currentSeverity: current.severity, newIssues: current.issues, resolvedIssues: [] };
|
|
61
|
+
}
|
|
62
|
+
const baselineIssueSet = new Set(baseline.issues);
|
|
63
|
+
const currentIssueSet = new Set(current.issues);
|
|
64
|
+
return {
|
|
65
|
+
key: current.key,
|
|
66
|
+
title: current.title,
|
|
67
|
+
baselineSeverity: baseline.severity,
|
|
68
|
+
currentSeverity: current.severity,
|
|
69
|
+
newIssues: current.issues.filter((i) => !baselineIssueSet.has(i)),
|
|
70
|
+
resolvedIssues: baseline.issues.filter((i) => !currentIssueSet.has(i))
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createAuditDiff(baseline, current) {
|
|
76
|
+
const scoreDelta = current.score - baseline.score;
|
|
77
|
+
const checkDiffs = diffChecks(baseline.checks, current.checks);
|
|
78
|
+
const { improved, regressed, unchanged } = classifyChanges(checkDiffs);
|
|
79
|
+
const areaDiffs = diffAreas(baseline.areas, current.areas);
|
|
80
|
+
|
|
81
|
+
const hasRegression = regressed.length > 0;
|
|
82
|
+
const allNewIssues = areaDiffs.flatMap((a) => a.newIssues);
|
|
83
|
+
const allResolvedIssues = areaDiffs.flatMap((a) => a.resolvedIssues);
|
|
84
|
+
|
|
85
|
+
let verdict;
|
|
86
|
+
if (scoreDelta > 0 && !hasRegression) {
|
|
87
|
+
verdict = "改善";
|
|
88
|
+
} else if (scoreDelta === 0 && !hasRegression) {
|
|
89
|
+
verdict = "持平";
|
|
90
|
+
} else if (scoreDelta > 0 && hasRegression) {
|
|
91
|
+
verdict = "部分改善(存在回归)";
|
|
92
|
+
} else if (scoreDelta < 0) {
|
|
93
|
+
verdict = "退步";
|
|
94
|
+
} else {
|
|
95
|
+
verdict = "持平(存在回归)";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
kind: "geo-audit-diff",
|
|
100
|
+
baseline: {
|
|
101
|
+
score: baseline.score,
|
|
102
|
+
maxScore: baseline.maxScore,
|
|
103
|
+
scoreLabel: baseline.scoreLabel,
|
|
104
|
+
kind: baseline.kind
|
|
105
|
+
},
|
|
106
|
+
current: {
|
|
107
|
+
score: current.score,
|
|
108
|
+
maxScore: current.maxScore,
|
|
109
|
+
scoreLabel: current.scoreLabel,
|
|
110
|
+
kind: current.kind
|
|
111
|
+
},
|
|
112
|
+
scoreDelta,
|
|
113
|
+
verdict,
|
|
114
|
+
checkDiffs,
|
|
115
|
+
improved,
|
|
116
|
+
regressed,
|
|
117
|
+
unchanged,
|
|
118
|
+
areaDiffs,
|
|
119
|
+
newIssues: allNewIssues,
|
|
120
|
+
resolvedIssues: allResolvedIssues,
|
|
121
|
+
hasRegression
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function createAuditDiffFromFiles(baselinePath, currentPath) {
|
|
126
|
+
const baseline = await loadAuditJson(baselinePath);
|
|
127
|
+
const current = await loadAuditJson(currentPath);
|
|
128
|
+
return createAuditDiff(baseline, current);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function renderAuditDiffMarkdown(diff) {
|
|
132
|
+
const lines = [
|
|
133
|
+
"# GEO 审计对比",
|
|
134
|
+
"",
|
|
135
|
+
`- 判定:**${diff.verdict}**`,
|
|
136
|
+
`- 基线分数:\`${diff.baseline.score}/${diff.baseline.maxScore}\`(${diff.baseline.scoreLabel})`,
|
|
137
|
+
`- 当前分数:\`${diff.current.score}/${diff.current.maxScore}\`(${diff.current.scoreLabel})`,
|
|
138
|
+
`- 分数变化:\`${diff.scoreDelta >= 0 ? "+" : ""}${diff.scoreDelta}\``,
|
|
139
|
+
""
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
lines.push("## 评分明细对比", "", "| 检查项 | 基线 | 当前 | 变化 |", "|--------|------|------|------|");
|
|
143
|
+
for (const check of diff.checkDiffs) {
|
|
144
|
+
const baseStr = check.baselinePassed === null ? "N/A" : `${check.baselinePassed ? "✓" : "✗"} (${check.baselinePoints})`;
|
|
145
|
+
const curStr = `${check.currentPassed ? "✓" : "✗"} (${check.currentPoints})`;
|
|
146
|
+
const deltaStr = check.delta > 0 ? `+${check.delta}` : check.delta === 0 ? "—" : String(check.delta);
|
|
147
|
+
lines.push(`| ${check.label} | ${baseStr} | ${curStr} | ${deltaStr} |`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (diff.improved.length > 0) {
|
|
151
|
+
lines.push("", "## 改善项", "");
|
|
152
|
+
for (const item of diff.improved) {
|
|
153
|
+
lines.push(`- ${item.label}:+${item.delta} 分`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (diff.regressed.length > 0) {
|
|
158
|
+
lines.push("", "## ⚠ 回归项", "");
|
|
159
|
+
for (const item of diff.regressed) {
|
|
160
|
+
lines.push(`- ${item.label}:${item.delta} 分`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (diff.resolvedIssues.length > 0) {
|
|
165
|
+
lines.push("", "## 已解决问题", "");
|
|
166
|
+
for (const issue of diff.resolvedIssues) {
|
|
167
|
+
lines.push(`- ${issue}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (diff.newIssues.length > 0) {
|
|
172
|
+
lines.push("", "## 新增问题", "");
|
|
173
|
+
for (const issue of diff.newIssues) {
|
|
174
|
+
lines.push(`- ${issue}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lines.push("");
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function writeAuditDiffOutput(outputPath, content) {
|
|
183
|
+
return writeScanOutput(outputPath, content);
|
|
184
|
+
}
|