@rankcli/agent-runtime 0.0.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 +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import type { ToolResult } from '../types.js';
|
|
4
|
+
import type { GeneratedFix } from '../fixer.js';
|
|
5
|
+
|
|
6
|
+
interface H1ScanResult {
|
|
7
|
+
hasH1: boolean;
|
|
8
|
+
h1Location?: {
|
|
9
|
+
file: string;
|
|
10
|
+
line: number;
|
|
11
|
+
content: string;
|
|
12
|
+
};
|
|
13
|
+
candidates: HeadlineCandidate[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface HeadlineCandidate {
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
tag: string;
|
|
20
|
+
className: string;
|
|
21
|
+
content: string;
|
|
22
|
+
score: number; // Higher = more likely to be the main headline
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Tailwind text size classes ranked by size
|
|
26
|
+
const TEXT_SIZE_SCORES: Record<string, number> = {
|
|
27
|
+
'text-9xl': 100,
|
|
28
|
+
'text-8xl': 90,
|
|
29
|
+
'text-7xl': 80,
|
|
30
|
+
'text-6xl': 70,
|
|
31
|
+
'text-5xl': 60,
|
|
32
|
+
'text-4xl': 50,
|
|
33
|
+
'text-3xl': 40,
|
|
34
|
+
'text-2xl': 30,
|
|
35
|
+
'text-xl': 20,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Component names that likely contain the main headline
|
|
39
|
+
const PRIORITY_COMPONENTS = ['hero', 'landing', 'home', 'index', 'main'];
|
|
40
|
+
|
|
41
|
+
export async function scanForH1(params: { cwd: string }): Promise<ToolResult> {
|
|
42
|
+
const { cwd } = params;
|
|
43
|
+
const result: H1ScanResult = {
|
|
44
|
+
hasH1: false,
|
|
45
|
+
candidates: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const srcDir = join(cwd, 'src');
|
|
49
|
+
if (!existsSync(srcDir)) {
|
|
50
|
+
return { success: true, data: result };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const files = findReactFiles(srcDir);
|
|
54
|
+
|
|
55
|
+
// Store all H1 locations to find the best one
|
|
56
|
+
const h1Locations: Array<{ file: string; line: number; content: string; priority: number }> = [];
|
|
57
|
+
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const content = readFileSync(file, 'utf-8');
|
|
60
|
+
const lines = content.split('\n');
|
|
61
|
+
const relativePath = file.replace(cwd + '/', '');
|
|
62
|
+
|
|
63
|
+
// Check for existing H1
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const line = lines[i];
|
|
66
|
+
|
|
67
|
+
// Look for <h1 or <H1 in JSX
|
|
68
|
+
if (/<h1[\s>]/i.test(line)) {
|
|
69
|
+
// Calculate priority based on file location
|
|
70
|
+
let priority = 0;
|
|
71
|
+
const lowerPath = relativePath.toLowerCase();
|
|
72
|
+
if (lowerPath.includes('hero')) priority += 100;
|
|
73
|
+
if (lowerPath.includes('landing')) priority += 80;
|
|
74
|
+
if (lowerPath.includes('home')) priority += 70;
|
|
75
|
+
if (lowerPath.includes('index')) priority += 60;
|
|
76
|
+
if (lowerPath.includes('main')) priority += 50;
|
|
77
|
+
if (lowerPath.includes('components/landing')) priority += 40;
|
|
78
|
+
|
|
79
|
+
h1Locations.push({
|
|
80
|
+
file: relativePath,
|
|
81
|
+
line: i + 1,
|
|
82
|
+
content: line.trim(),
|
|
83
|
+
priority,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Look for headline candidates (large text that's not an H1)
|
|
88
|
+
const candidate = findHeadlineCandidate(line, i + 1, relativePath);
|
|
89
|
+
if (candidate) {
|
|
90
|
+
result.candidates.push(candidate);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// If we found H1s, use the highest priority one
|
|
96
|
+
if (h1Locations.length > 0) {
|
|
97
|
+
h1Locations.sort((a, b) => b.priority - a.priority);
|
|
98
|
+
const best = h1Locations[0];
|
|
99
|
+
result.hasH1 = true;
|
|
100
|
+
result.h1Location = {
|
|
101
|
+
file: best.file,
|
|
102
|
+
line: best.line,
|
|
103
|
+
content: best.content,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Sort candidates by score (highest first)
|
|
108
|
+
result.candidates.sort((a, b) => b.score - a.score);
|
|
109
|
+
|
|
110
|
+
// Boost score for components in priority list
|
|
111
|
+
for (const candidate of result.candidates) {
|
|
112
|
+
const fileName = candidate.file.toLowerCase();
|
|
113
|
+
for (const priority of PRIORITY_COMPONENTS) {
|
|
114
|
+
if (fileName.includes(priority)) {
|
|
115
|
+
candidate.score += 25;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Re-sort after boosting
|
|
121
|
+
result.candidates.sort((a, b) => b.score - a.score);
|
|
122
|
+
|
|
123
|
+
return { success: true, data: result };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findReactFiles(dir: string): string[] {
|
|
127
|
+
const files: string[] = [];
|
|
128
|
+
|
|
129
|
+
function scan(currentDir: string) {
|
|
130
|
+
const entries = readdirSync(currentDir);
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (entry.startsWith('.') || entry === 'node_modules') continue;
|
|
133
|
+
|
|
134
|
+
const fullPath = join(currentDir, entry);
|
|
135
|
+
const stat = statSync(fullPath);
|
|
136
|
+
|
|
137
|
+
if (stat.isDirectory()) {
|
|
138
|
+
// Skip ui components directory - these are usually shadcn/ui
|
|
139
|
+
if (entry !== 'ui' || !currentDir.includes('components')) {
|
|
140
|
+
scan(fullPath);
|
|
141
|
+
}
|
|
142
|
+
} else if (['.tsx', '.jsx'].includes(extname(entry))) {
|
|
143
|
+
files.push(fullPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
scan(dir);
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findHeadlineCandidate(line: string, lineNum: number, file: string): HeadlineCandidate | null {
|
|
153
|
+
// Match patterns like <div className="text-5xl or <p className="... text-4xl
|
|
154
|
+
const tagMatch = line.match(/<(div|p|span|section)\s+[^>]*className=["']([^"']*text-[3-9]xl[^"']*)["']/i);
|
|
155
|
+
|
|
156
|
+
if (!tagMatch) return null;
|
|
157
|
+
|
|
158
|
+
const [, tag, className] = tagMatch;
|
|
159
|
+
|
|
160
|
+
// Calculate score based on text size
|
|
161
|
+
let score = 0;
|
|
162
|
+
for (const [sizeClass, points] of Object.entries(TEXT_SIZE_SCORES)) {
|
|
163
|
+
if (className.includes(sizeClass)) {
|
|
164
|
+
score = points;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Boost for bold
|
|
170
|
+
if (className.includes('font-bold') || className.includes('font-extrabold')) {
|
|
171
|
+
score += 10;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Extract text content if on same line
|
|
175
|
+
const textMatch = line.match(/>([^<]+)</);
|
|
176
|
+
const content = textMatch ? textMatch[1].trim() : '';
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
file,
|
|
180
|
+
line: lineNum,
|
|
181
|
+
tag,
|
|
182
|
+
className,
|
|
183
|
+
content,
|
|
184
|
+
score,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function generateH1Fix(params: { cwd: string }): Promise<GeneratedFix | null> {
|
|
189
|
+
const scanResult = await scanForH1(params);
|
|
190
|
+
|
|
191
|
+
if (!scanResult.success || !scanResult.data) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const data = scanResult.data as H1ScanResult;
|
|
196
|
+
|
|
197
|
+
// If H1 already exists, no fix needed
|
|
198
|
+
if (data.hasH1) {
|
|
199
|
+
return {
|
|
200
|
+
issue: {
|
|
201
|
+
code: 'MISSING_H1',
|
|
202
|
+
message: 'No H1 heading found',
|
|
203
|
+
severity: 'critical',
|
|
204
|
+
},
|
|
205
|
+
file: data.h1Location!.file,
|
|
206
|
+
before: null,
|
|
207
|
+
after: data.h1Location!.content,
|
|
208
|
+
explanation: `H1 already exists in component at line ${data.h1Location!.line}. The audit may not see it because it's rendered by JavaScript. Google's crawler does execute JS.`,
|
|
209
|
+
skipped: true,
|
|
210
|
+
skipReason: `H1 found in ${data.h1Location!.file}:${data.h1Location!.line} (JS-rendered)`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// No H1 and no candidates
|
|
215
|
+
if (data.candidates.length === 0) {
|
|
216
|
+
return {
|
|
217
|
+
issue: {
|
|
218
|
+
code: 'MISSING_H1',
|
|
219
|
+
message: 'No H1 heading found',
|
|
220
|
+
severity: 'critical',
|
|
221
|
+
},
|
|
222
|
+
file: 'Unknown',
|
|
223
|
+
before: null,
|
|
224
|
+
after: '<h1>Your Main Headline</h1>',
|
|
225
|
+
explanation: 'No suitable headline element found to convert. Add an H1 manually in your main page component.',
|
|
226
|
+
skipped: true,
|
|
227
|
+
skipReason: 'No headline-like elements found to convert',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Get the best candidate
|
|
232
|
+
const best = data.candidates[0];
|
|
233
|
+
|
|
234
|
+
// Generate the fix
|
|
235
|
+
const before = `<${best.tag}`;
|
|
236
|
+
const after = `<h1`;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
issue: {
|
|
240
|
+
code: 'MISSING_H1',
|
|
241
|
+
message: 'No H1 heading found',
|
|
242
|
+
severity: 'critical',
|
|
243
|
+
},
|
|
244
|
+
file: best.file,
|
|
245
|
+
before,
|
|
246
|
+
after,
|
|
247
|
+
explanation: `Convert the main headline from <${best.tag}> to <h1>. Found at line ${best.line} with classes: ${best.className}`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
crawlUrl,
|
|
3
|
+
extractMeta,
|
|
4
|
+
analyzeHeadings,
|
|
5
|
+
extractImages,
|
|
6
|
+
extractLinks,
|
|
7
|
+
extractSchema,
|
|
8
|
+
checkRobots,
|
|
9
|
+
checkSitemap,
|
|
10
|
+
} from './crawl.js';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
readFile,
|
|
14
|
+
writeFile,
|
|
15
|
+
listFiles,
|
|
16
|
+
detectFramework,
|
|
17
|
+
findHtmlEntry,
|
|
18
|
+
findPageFiles,
|
|
19
|
+
} from './files.js';
|
|
20
|
+
|
|
21
|
+
import { analyzeUrl } from './analyzer.js';
|
|
22
|
+
import { scanForH1, generateH1Fix } from './h1-fixer.js';
|
|
23
|
+
|
|
24
|
+
import type { ToolFunction } from '../types.js';
|
|
25
|
+
|
|
26
|
+
export const tools: Record<string, ToolFunction> = {
|
|
27
|
+
// Crawling tools
|
|
28
|
+
crawl_url: (params) => crawlUrl(params as { url: string }),
|
|
29
|
+
extract_meta: (params) => extractMeta(params as { html: string; url: string }),
|
|
30
|
+
analyze_headings: (params) => analyzeHeadings(params as { html: string }),
|
|
31
|
+
extract_images: (params) => extractImages(params as { html: string }),
|
|
32
|
+
extract_links: (params) => extractLinks(params as { html: string; baseUrl: string }),
|
|
33
|
+
extract_schema: (params) => extractSchema(params as { html: string }),
|
|
34
|
+
check_robots: (params) => checkRobots(params as { url: string }),
|
|
35
|
+
check_sitemap: (params) => checkSitemap(params as { url: string }),
|
|
36
|
+
|
|
37
|
+
// File tools
|
|
38
|
+
read_file: (params) => readFile(params as { path: string; cwd?: string }),
|
|
39
|
+
write_file: (params) => writeFile(params as { path: string; content: string; cwd?: string }),
|
|
40
|
+
list_files: (params) => listFiles(params as { path: string; cwd?: string; pattern?: string; recursive?: boolean }),
|
|
41
|
+
detect_framework: (params) => detectFramework(params as { cwd?: string }),
|
|
42
|
+
find_html_entry: (params) => findHtmlEntry(params as { cwd?: string }),
|
|
43
|
+
find_page_files: (params) => findPageFiles(params as { cwd?: string; framework?: string }),
|
|
44
|
+
|
|
45
|
+
// High-level analysis
|
|
46
|
+
analyze_url: (params) => analyzeUrl(params as { url: string }),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
crawlUrl,
|
|
51
|
+
extractMeta,
|
|
52
|
+
analyzeHeadings,
|
|
53
|
+
extractImages,
|
|
54
|
+
extractLinks,
|
|
55
|
+
extractSchema,
|
|
56
|
+
checkRobots,
|
|
57
|
+
checkSitemap,
|
|
58
|
+
readFile,
|
|
59
|
+
writeFile,
|
|
60
|
+
listFiles,
|
|
61
|
+
detectFramework,
|
|
62
|
+
findHtmlEntry,
|
|
63
|
+
findPageFiles,
|
|
64
|
+
analyzeUrl,
|
|
65
|
+
scanForH1,
|
|
66
|
+
generateH1Fix,
|
|
67
|
+
};
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// GitHub Action Workflow Generator
|
|
2
|
+
// Creates scheduled SEO monitoring workflows
|
|
3
|
+
|
|
4
|
+
export interface WorkflowConfig {
|
|
5
|
+
schedule: 'daily' | 'weekly' | 'monthly' | 'manual';
|
|
6
|
+
siteUrl: string;
|
|
7
|
+
features: {
|
|
8
|
+
audit: boolean;
|
|
9
|
+
tracking: boolean;
|
|
10
|
+
autoFix: boolean;
|
|
11
|
+
createIssues: boolean;
|
|
12
|
+
createPRs: boolean;
|
|
13
|
+
};
|
|
14
|
+
apiKey?: string; // SEO Autopilot API key for paid features
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate GitHub Action workflow YAML
|
|
19
|
+
*/
|
|
20
|
+
export function generateWorkflow(config: WorkflowConfig): string {
|
|
21
|
+
const cronSchedule = getCronSchedule(config.schedule);
|
|
22
|
+
|
|
23
|
+
return `# SEO Autopilot - Automated SEO Monitoring
|
|
24
|
+
# This workflow runs scheduled SEO audits and tracking
|
|
25
|
+
|
|
26
|
+
name: SEO Monitoring
|
|
27
|
+
|
|
28
|
+
on:
|
|
29
|
+
# Scheduled runs
|
|
30
|
+
schedule:
|
|
31
|
+
- cron: '${cronSchedule}'
|
|
32
|
+
|
|
33
|
+
# Manual trigger
|
|
34
|
+
workflow_dispatch:
|
|
35
|
+
inputs:
|
|
36
|
+
mode:
|
|
37
|
+
description: 'Run mode'
|
|
38
|
+
required: true
|
|
39
|
+
default: 'full'
|
|
40
|
+
type: choice
|
|
41
|
+
options:
|
|
42
|
+
- full
|
|
43
|
+
- audit-only
|
|
44
|
+
- track-only
|
|
45
|
+
|
|
46
|
+
# Run on main branch pushes (optional)
|
|
47
|
+
# push:
|
|
48
|
+
# branches: [main]
|
|
49
|
+
|
|
50
|
+
env:
|
|
51
|
+
SITE_URL: '${config.siteUrl}'
|
|
52
|
+
|
|
53
|
+
jobs:
|
|
54
|
+
seo-check:
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
permissions:
|
|
57
|
+
contents: write
|
|
58
|
+
issues: write
|
|
59
|
+
pull-requests: write
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- name: Checkout repository
|
|
63
|
+
uses: actions/checkout@v4
|
|
64
|
+
|
|
65
|
+
- name: Setup Node.js
|
|
66
|
+
uses: actions/setup-node@v4
|
|
67
|
+
with:
|
|
68
|
+
node-version: '20'
|
|
69
|
+
|
|
70
|
+
- name: Install SEO Autopilot
|
|
71
|
+
run: npm install -g @seo-autopilot/cli
|
|
72
|
+
|
|
73
|
+
${config.features.tracking ? `
|
|
74
|
+
- name: Pull GSC Data
|
|
75
|
+
if: \${{ inputs.mode != 'audit-only' }}
|
|
76
|
+
env:
|
|
77
|
+
GSC_SERVICE_ACCOUNT_EMAIL: \${{ secrets.GSC_SERVICE_ACCOUNT_EMAIL }}
|
|
78
|
+
GSC_PRIVATE_KEY: \${{ secrets.GSC_PRIVATE_KEY }}
|
|
79
|
+
run: |
|
|
80
|
+
seo track --site "$SITE_URL" --output tracking-report.json
|
|
81
|
+
continue-on-error: true
|
|
82
|
+
` : ''}
|
|
83
|
+
|
|
84
|
+
${config.features.audit ? `
|
|
85
|
+
- name: Run SEO Audit
|
|
86
|
+
if: \${{ inputs.mode != 'track-only' }}
|
|
87
|
+
run: |
|
|
88
|
+
seo audit --url "$SITE_URL" --output json > audit-report.json
|
|
89
|
+
` : ''}
|
|
90
|
+
|
|
91
|
+
- name: Generate Report
|
|
92
|
+
id: report
|
|
93
|
+
run: |
|
|
94
|
+
seo report --combine \\
|
|
95
|
+
${config.features.tracking ? '--tracking tracking-report.json \\' : ''}
|
|
96
|
+
${config.features.audit ? '--audit audit-report.json \\' : ''}
|
|
97
|
+
--format markdown > seo-report.md
|
|
98
|
+
|
|
99
|
+
# Set output for issue creation
|
|
100
|
+
echo "report<<EOF" >> $GITHUB_OUTPUT
|
|
101
|
+
cat seo-report.md >> $GITHUB_OUTPUT
|
|
102
|
+
echo "EOF" >> $GITHUB_OUTPUT
|
|
103
|
+
|
|
104
|
+
${config.features.createIssues ? `
|
|
105
|
+
- name: Create/Update Issue
|
|
106
|
+
uses: actions/github-script@v7
|
|
107
|
+
with:
|
|
108
|
+
script: |
|
|
109
|
+
const title = 'SEO Report - ' + new Date().toISOString().split('T')[0];
|
|
110
|
+
const body = \`\${{ steps.report.outputs.report }}\`;
|
|
111
|
+
|
|
112
|
+
// Find existing issue
|
|
113
|
+
const issues = await github.rest.issues.listForRepo({
|
|
114
|
+
owner: context.repo.owner,
|
|
115
|
+
repo: context.repo.repo,
|
|
116
|
+
labels: 'seo-report',
|
|
117
|
+
state: 'open'
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (issues.data.length > 0) {
|
|
121
|
+
// Update existing issue
|
|
122
|
+
await github.rest.issues.update({
|
|
123
|
+
owner: context.repo.owner,
|
|
124
|
+
repo: context.repo.repo,
|
|
125
|
+
issue_number: issues.data[0].number,
|
|
126
|
+
body: body
|
|
127
|
+
});
|
|
128
|
+
console.log('Updated issue #' + issues.data[0].number);
|
|
129
|
+
} else {
|
|
130
|
+
// Create new issue
|
|
131
|
+
await github.rest.issues.create({
|
|
132
|
+
owner: context.repo.owner,
|
|
133
|
+
repo: context.repo.repo,
|
|
134
|
+
title: title,
|
|
135
|
+
body: body,
|
|
136
|
+
labels: ['seo-report', 'automated']
|
|
137
|
+
});
|
|
138
|
+
console.log('Created new SEO report issue');
|
|
139
|
+
}
|
|
140
|
+
` : ''}
|
|
141
|
+
|
|
142
|
+
${config.features.autoFix && config.features.createPRs ? `
|
|
143
|
+
- name: Auto-fix Issues
|
|
144
|
+
id: autofix
|
|
145
|
+
run: |
|
|
146
|
+
# Run auto-fixer for safe, non-breaking changes
|
|
147
|
+
seo fix --auto --safe-only --output fixes.json
|
|
148
|
+
|
|
149
|
+
# Check if any fixes were made
|
|
150
|
+
if [ -s fixes.json ]; then
|
|
151
|
+
echo "fixes_made=true" >> $GITHUB_OUTPUT
|
|
152
|
+
else
|
|
153
|
+
echo "fixes_made=false" >> $GITHUB_OUTPUT
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
- name: Create PR with Fixes
|
|
157
|
+
if: steps.autofix.outputs.fixes_made == 'true'
|
|
158
|
+
uses: peter-evans/create-pull-request@v6
|
|
159
|
+
with:
|
|
160
|
+
token: \${{ secrets.GITHUB_TOKEN }}
|
|
161
|
+
commit-message: 'fix(seo): automated SEO improvements'
|
|
162
|
+
title: '🔧 SEO Auto-Fix: Automated Improvements'
|
|
163
|
+
body: |
|
|
164
|
+
## Automated SEO Fixes
|
|
165
|
+
|
|
166
|
+
This PR was automatically generated by SEO Autopilot.
|
|
167
|
+
|
|
168
|
+
### Changes Made
|
|
169
|
+
\`\`\`
|
|
170
|
+
$(cat fixes.json | jq -r '.fixes[] | "- " + .description')
|
|
171
|
+
\`\`\`
|
|
172
|
+
|
|
173
|
+
### Review Checklist
|
|
174
|
+
- [ ] Changes look correct
|
|
175
|
+
- [ ] No unintended modifications
|
|
176
|
+
- [ ] Tests pass
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
🤖 Generated by [SEO Autopilot](https://github.com/seo-autopilot)
|
|
180
|
+
branch: seo-autofix-\${{ github.run_number }}
|
|
181
|
+
labels: seo, automated
|
|
182
|
+
` : ''}
|
|
183
|
+
|
|
184
|
+
- name: Save Report Artifact
|
|
185
|
+
uses: actions/upload-artifact@v4
|
|
186
|
+
with:
|
|
187
|
+
name: seo-report-\${{ github.run_number }}
|
|
188
|
+
path: |
|
|
189
|
+
seo-report.md
|
|
190
|
+
${config.features.audit ? 'audit-report.json' : ''}
|
|
191
|
+
${config.features.tracking ? 'tracking-report.json' : ''}
|
|
192
|
+
retention-days: 90
|
|
193
|
+
|
|
194
|
+
${config.apiKey ? `
|
|
195
|
+
- name: Send to SEO Autopilot Dashboard
|
|
196
|
+
env:
|
|
197
|
+
SEO_AUTOPILOT_API_KEY: \${{ secrets.SEO_AUTOPILOT_API_KEY }}
|
|
198
|
+
run: |
|
|
199
|
+
seo sync --api-key "$SEO_AUTOPILOT_API_KEY" \\
|
|
200
|
+
${config.features.audit ? '--audit audit-report.json \\' : ''}
|
|
201
|
+
${config.features.tracking ? '--tracking tracking-report.json' : ''}
|
|
202
|
+
` : ''}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getCronSchedule(schedule: WorkflowConfig['schedule']): string {
|
|
207
|
+
switch (schedule) {
|
|
208
|
+
case 'daily':
|
|
209
|
+
return '0 9 * * *'; // 9 AM UTC daily
|
|
210
|
+
case 'weekly':
|
|
211
|
+
return '0 9 * * 1'; // 9 AM UTC every Monday
|
|
212
|
+
case 'monthly':
|
|
213
|
+
return '0 9 1 * *'; // 9 AM UTC first of month
|
|
214
|
+
case 'manual':
|
|
215
|
+
return '0 0 31 2 *'; // Never (Feb 31 doesn't exist)
|
|
216
|
+
default:
|
|
217
|
+
return '0 9 * * 1';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Generate the secrets documentation
|
|
223
|
+
*/
|
|
224
|
+
export function generateSecretsDoc(config: WorkflowConfig): string {
|
|
225
|
+
let secrets = `# Required GitHub Secrets
|
|
226
|
+
|
|
227
|
+
Go to your repository Settings > Secrets and variables > Actions
|
|
228
|
+
|
|
229
|
+
`;
|
|
230
|
+
|
|
231
|
+
if (config.features.tracking) {
|
|
232
|
+
secrets += `
|
|
233
|
+
## Google Search Console (Required for tracking)
|
|
234
|
+
|
|
235
|
+
1. \`GSC_SERVICE_ACCOUNT_EMAIL\`
|
|
236
|
+
- Your Google Cloud service account email
|
|
237
|
+
- Example: seo-bot@my-project.iam.gserviceaccount.com
|
|
238
|
+
|
|
239
|
+
2. \`GSC_PRIVATE_KEY\`
|
|
240
|
+
- The private key from your service account JSON
|
|
241
|
+
- Include the full key with -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----
|
|
242
|
+
|
|
243
|
+
### Setup Instructions:
|
|
244
|
+
1. Go to https://console.cloud.google.com
|
|
245
|
+
2. Create a project and enable "Search Console API"
|
|
246
|
+
3. Create a service account and download JSON key
|
|
247
|
+
4. In Search Console, add the service account email as a user
|
|
248
|
+
5. Copy email and private_key to GitHub Secrets
|
|
249
|
+
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (config.apiKey) {
|
|
254
|
+
secrets += `
|
|
255
|
+
## SEO Autopilot (Required for dashboard sync)
|
|
256
|
+
|
|
257
|
+
1. \`SEO_AUTOPILOT_API_KEY\`
|
|
258
|
+
- Get your API key from https://seo-autopilot.dev/dashboard
|
|
259
|
+
- This enables cloud dashboard, historical tracking, and alerts
|
|
260
|
+
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return secrets;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generate complete setup package
|
|
269
|
+
*/
|
|
270
|
+
export function generateGitHubActionSetup(config: WorkflowConfig): {
|
|
271
|
+
workflowYaml: string;
|
|
272
|
+
secretsDoc: string;
|
|
273
|
+
readmeBadge: string;
|
|
274
|
+
} {
|
|
275
|
+
return {
|
|
276
|
+
workflowYaml: generateWorkflow(config),
|
|
277
|
+
secretsDoc: generateSecretsDoc(config),
|
|
278
|
+
readmeBadge: `[](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/seo.yml)`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Write GitHub Action files to project
|
|
284
|
+
*/
|
|
285
|
+
export function writeGitHubActionFiles(
|
|
286
|
+
projectPath: string,
|
|
287
|
+
config: WorkflowConfig
|
|
288
|
+
): { files: string[]; instructions: string } {
|
|
289
|
+
const fs = require('fs');
|
|
290
|
+
const path = require('path');
|
|
291
|
+
|
|
292
|
+
const files: string[] = [];
|
|
293
|
+
|
|
294
|
+
// Create .github/workflows directory
|
|
295
|
+
const workflowDir = path.join(projectPath, '.github', 'workflows');
|
|
296
|
+
if (!fs.existsSync(workflowDir)) {
|
|
297
|
+
fs.mkdirSync(workflowDir, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Write workflow file
|
|
301
|
+
const workflowPath = path.join(workflowDir, 'seo.yml');
|
|
302
|
+
fs.writeFileSync(workflowPath, generateWorkflow(config));
|
|
303
|
+
files.push(workflowPath);
|
|
304
|
+
|
|
305
|
+
// Write secrets documentation
|
|
306
|
+
const docsDir = path.join(projectPath, '.github');
|
|
307
|
+
const secretsDocPath = path.join(docsDir, 'SEO_SETUP.md');
|
|
308
|
+
fs.writeFileSync(secretsDocPath, generateSecretsDoc(config));
|
|
309
|
+
files.push(secretsDocPath);
|
|
310
|
+
|
|
311
|
+
const instructions = `
|
|
312
|
+
GitHub Action created! Next steps:
|
|
313
|
+
|
|
314
|
+
1. Add required secrets to your repository:
|
|
315
|
+
Settings > Secrets and variables > Actions
|
|
316
|
+
See .github/SEO_SETUP.md for details
|
|
317
|
+
|
|
318
|
+
2. The workflow will run ${config.schedule}
|
|
319
|
+
Or trigger manually: Actions > SEO Monitoring > Run workflow
|
|
320
|
+
|
|
321
|
+
3. Add status badge to your README:
|
|
322
|
+
${generateGitHubActionSetup(config).readmeBadge.replace('YOUR_ORG/YOUR_REPO', '<your-org>/<your-repo>')}
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
return { files, instructions };
|
|
326
|
+
}
|