@rankcli/agent-runtime 0.0.15 → 0.0.16
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/dist/index.d.mts +43 -393
- package/dist/index.d.ts +43 -393
- package/dist/index.js +306 -6526
- package/dist/index.mjs +202 -6412
- package/package.json +6 -1
- package/scripts/build-deno.ts +0 -134
- package/src/analyzers/core-web-vitals-analyzer.test.ts +0 -236
- package/src/analyzers/core-web-vitals-analyzer.ts +0 -557
- package/src/analyzers/geo-analyzer.test.ts +0 -310
- package/src/analyzers/geo-analyzer.ts +0 -814
- package/src/analyzers/image-optimization-analyzer.test.ts +0 -145
- package/src/analyzers/image-optimization-analyzer.ts +0 -348
- package/src/analyzers/index.ts +0 -233
- package/src/analyzers/internal-linking-analyzer.test.ts +0 -141
- package/src/analyzers/internal-linking-analyzer.ts +0 -419
- package/src/analyzers/mobile-seo-analyzer.test.ts +0 -140
- package/src/analyzers/mobile-seo-analyzer.ts +0 -455
- package/src/analyzers/security-headers-analyzer.test.ts +0 -115
- package/src/analyzers/security-headers-analyzer.ts +0 -318
- package/src/analyzers/structured-data-analyzer.test.ts +0 -210
- package/src/analyzers/structured-data-analyzer.ts +0 -590
- package/src/audit/ai/analyzer.ts +0 -347
- package/src/audit/ai/index.ts +0 -29
- package/src/audit/ai/prompts/content-analysis.ts +0 -271
- package/src/audit/ai/types.ts +0 -179
- package/src/audit/checks/additional-checks.ts +0 -439
- package/src/audit/checks/agent-experience.ts +0 -108
- package/src/audit/checks/ai-citation-worthiness.ts +0 -399
- package/src/audit/checks/ai-content-structure.ts +0 -325
- package/src/audit/checks/ai-readiness.ts +0 -406
- package/src/audit/checks/anchor-text.ts +0 -179
- package/src/audit/checks/answer-conciseness.ts +0 -322
- package/src/audit/checks/asset-minification.ts +0 -270
- package/src/audit/checks/bing-optimization.ts +0 -206
- package/src/audit/checks/brand-mention-optimization.ts +0 -349
- package/src/audit/checks/caching-headers.ts +0 -305
- package/src/audit/checks/canonical-advanced.ts +0 -150
- package/src/audit/checks/canonical-domain.ts +0 -196
- package/src/audit/checks/citation-quality.ts +0 -358
- package/src/audit/checks/client-rendering.ts +0 -542
- package/src/audit/checks/color-contrast.ts +0 -342
- package/src/audit/checks/content-freshness.ts +0 -170
- package/src/audit/checks/content-science.ts +0 -589
- package/src/audit/checks/conversion-elements.ts +0 -526
- package/src/audit/checks/crawlability.ts +0 -220
- package/src/audit/checks/directory-listing.ts +0 -172
- package/src/audit/checks/dom-analysis.ts +0 -191
- package/src/audit/checks/dom-size.ts +0 -246
- package/src/audit/checks/duplicate-content.ts +0 -194
- package/src/audit/checks/eeat-signals.ts +0 -990
- package/src/audit/checks/entity-seo.ts +0 -396
- package/src/audit/checks/featured-snippet.ts +0 -473
- package/src/audit/checks/freshness-signals.ts +0 -443
- package/src/audit/checks/funnel-intent.ts +0 -463
- package/src/audit/checks/hreflang.ts +0 -174
- package/src/audit/checks/html-compliance.ts +0 -302
- package/src/audit/checks/image-dimensions.ts +0 -167
- package/src/audit/checks/images.ts +0 -160
- package/src/audit/checks/indexnow.ts +0 -275
- package/src/audit/checks/interactive-tools.ts +0 -475
- package/src/audit/checks/internal-link-graph.ts +0 -436
- package/src/audit/checks/keyword-analysis.ts +0 -239
- package/src/audit/checks/keyword-cannibalization.ts +0 -385
- package/src/audit/checks/keyword-placement.ts +0 -471
- package/src/audit/checks/links.ts +0 -273
- package/src/audit/checks/llms-txt.ts +0 -224
- package/src/audit/checks/local-seo.ts +0 -296
- package/src/audit/checks/mobile.ts +0 -167
- package/src/audit/checks/modern-images.ts +0 -226
- package/src/audit/checks/navboost-signals.ts +0 -395
- package/src/audit/checks/on-page.ts +0 -209
- package/src/audit/checks/page-resources.ts +0 -285
- package/src/audit/checks/pagination.ts +0 -180
- package/src/audit/checks/performance.ts +0 -153
- package/src/audit/checks/platform-presence.ts +0 -580
- package/src/audit/checks/rag-chunk-readiness.test.ts +0 -159
- package/src/audit/checks/rag-chunk-readiness.ts +0 -163
- package/src/audit/checks/redirect-analysis.ts +0 -153
- package/src/audit/checks/redirect-chain.ts +0 -389
- package/src/audit/checks/resource-hints.ts +0 -420
- package/src/audit/checks/responsive-css.ts +0 -247
- package/src/audit/checks/responsive-images.ts +0 -396
- package/src/audit/checks/review-ecosystem.ts +0 -415
- package/src/audit/checks/robots-validation.ts +0 -373
- package/src/audit/checks/security-headers.ts +0 -188
- package/src/audit/checks/security.ts +0 -144
- package/src/audit/checks/serp-preview.ts +0 -251
- package/src/audit/checks/site-maturity.ts +0 -453
- package/src/audit/checks/social-meta.test.ts +0 -275
- package/src/audit/checks/social-meta.ts +0 -134
- package/src/audit/checks/soft-404.ts +0 -151
- package/src/audit/checks/structured-data.ts +0 -238
- package/src/audit/checks/tech-detection.ts +0 -496
- package/src/audit/checks/topical-clusters.ts +0 -435
- package/src/audit/checks/tracker-bloat.ts +0 -462
- package/src/audit/checks/tracking-verification.test.ts +0 -371
- package/src/audit/checks/tracking-verification.ts +0 -636
- package/src/audit/checks/url-safety.ts +0 -682
- package/src/audit/deno-entry.ts +0 -66
- package/src/audit/discovery/index.ts +0 -15
- package/src/audit/discovery/link-crawler.ts +0 -233
- package/src/audit/discovery/repo-routes.ts +0 -347
- package/src/audit/engine.ts +0 -633
- package/src/audit/fixes/index.ts +0 -209
- package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
- package/src/audit/fixes/social-meta-fixes.ts +0 -463
- package/src/audit/index.ts +0 -74
- package/src/audit/runner.test.ts +0 -299
- package/src/audit/runner.ts +0 -130
- package/src/audit/types.ts +0 -1987
- package/src/content/featured-snippet.ts +0 -367
- package/src/content/generator.test.ts +0 -534
- package/src/content/generator.ts +0 -501
- package/src/content/headline.ts +0 -317
- package/src/content/index.ts +0 -62
- package/src/content/intent.ts +0 -258
- package/src/content/keyword-density.ts +0 -349
- package/src/content/readability.ts +0 -262
- package/src/executor.ts +0 -336
- package/src/fixer/framework-fixes.test.ts +0 -489
- package/src/fixer/framework-fixes.ts +0 -5287
- package/src/fixer/index.ts +0 -6
- package/src/fixer/schemas.ts +0 -971
- package/src/fixer.ts +0 -642
- package/src/frameworks/detector.test.ts +0 -248
- package/src/frameworks/detector.ts +0 -899
- package/src/frameworks/index.ts +0 -68
- package/src/frameworks/recipes/angular.yaml +0 -171
- package/src/frameworks/recipes/astro.yaml +0 -206
- package/src/frameworks/recipes/django.yaml +0 -180
- package/src/frameworks/recipes/laravel.yaml +0 -137
- package/src/frameworks/recipes/nextjs.yaml +0 -268
- package/src/frameworks/recipes/nuxt.yaml +0 -175
- package/src/frameworks/recipes/rails.yaml +0 -188
- package/src/frameworks/recipes/react.yaml +0 -202
- package/src/frameworks/recipes/sveltekit.yaml +0 -154
- package/src/frameworks/recipes/vue.yaml +0 -137
- package/src/frameworks/recipes/wordpress.yaml +0 -209
- package/src/frameworks/suggestion-engine.ts +0 -357
- package/src/geo/geo-content.test.ts +0 -305
- package/src/geo/geo-content.ts +0 -266
- package/src/geo/geo-history.test.ts +0 -473
- package/src/geo/geo-history.ts +0 -433
- package/src/geo/geo-tracker.test.ts +0 -359
- package/src/geo/geo-tracker.ts +0 -411
- package/src/geo/index.ts +0 -11
- package/src/geo/llm-citation-checker.ts +0 -188
- package/src/git/commit-helper.test.ts +0 -261
- package/src/git/commit-helper.ts +0 -329
- package/src/git/index.ts +0 -12
- package/src/git/pr-helper.test.ts +0 -284
- package/src/git/pr-helper.ts +0 -307
- package/src/index.ts +0 -76
- package/src/keywords/ai-keyword-engine.ts +0 -1062
- package/src/keywords/ai-summarizer.ts +0 -387
- package/src/keywords/ci-mode.ts +0 -555
- package/src/keywords/engine.ts +0 -359
- package/src/keywords/index.ts +0 -151
- package/src/keywords/llm-judge.ts +0 -357
- package/src/keywords/nlp-analysis.ts +0 -706
- package/src/keywords/prioritizer.ts +0 -295
- package/src/keywords/site-crawler.ts +0 -342
- package/src/keywords/sources/autocomplete.ts +0 -139
- package/src/keywords/sources/competitive-search.ts +0 -450
- package/src/keywords/sources/competitor-analysis.ts +0 -374
- package/src/keywords/sources/dataforseo.ts +0 -206
- package/src/keywords/sources/free-sources.ts +0 -294
- package/src/keywords/sources/gsc.ts +0 -123
- package/src/keywords/topic-grouping.ts +0 -327
- package/src/keywords/types.ts +0 -144
- package/src/keywords/wizard.ts +0 -457
- package/src/loader.ts +0 -40
- package/src/ranking/index.ts +0 -5
- package/src/ranking/serp-client.ts +0 -348
- package/src/ranking/tracker.ts +0 -380
- package/src/ranking/types.ts +0 -123
- package/src/reports/index.ts +0 -7
- package/src/reports/report-generator.test.ts +0 -293
- package/src/reports/report-generator.ts +0 -713
- package/src/scheduler/alerts.test.ts +0 -458
- package/src/scheduler/alerts.ts +0 -328
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/scheduled-audit.test.ts +0 -377
- package/src/scheduler/scheduled-audit.ts +0 -149
- package/src/test/integration-test.ts +0 -325
- package/src/tools/analyzer.ts +0 -373
- package/src/tools/crawl.ts +0 -293
- package/src/tools/files.ts +0 -301
- package/src/tools/h1-fixer.ts +0 -249
- package/src/tools/index.ts +0 -67
- package/src/tracking/github-action.ts +0 -326
- package/src/tracking/google-analytics.ts +0 -265
- package/src/tracking/index.ts +0 -45
- package/src/tracking/report-generator.ts +0 -386
- package/src/tracking/search-console.ts +0 -335
- package/src/types.ts +0 -148
- package/src/utils/http.ts +0 -302
- package/src/wasm-adapter.ts +0 -297
- package/src/wasm-entry.ts +0 -14
- package/tsconfig.json +0 -17
- package/tsup.wasm.config.ts +0 -26
- package/vitest.config.ts +0 -15
package/src/ranking/tracker.ts
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
// Rank Tracker - Manages keyword tracking and history
|
|
2
|
-
|
|
3
|
-
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
4
|
-
import type {
|
|
5
|
-
TrackedKeyword,
|
|
6
|
-
RankingResult,
|
|
7
|
-
RankHistory,
|
|
8
|
-
KeywordTrend,
|
|
9
|
-
RankCheckResult,
|
|
10
|
-
} from './types.js';
|
|
11
|
-
import { SerpClient, type SerpClientConfig } from './serp-client.js';
|
|
12
|
-
|
|
13
|
-
export interface TrackerConfig {
|
|
14
|
-
supabase: SupabaseClient;
|
|
15
|
-
serpConfig: SerpClientConfig;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class RankTracker {
|
|
19
|
-
private supabase: SupabaseClient;
|
|
20
|
-
private serpClient: SerpClient;
|
|
21
|
-
|
|
22
|
-
constructor(config: TrackerConfig) {
|
|
23
|
-
this.supabase = config.supabase;
|
|
24
|
-
this.serpClient = new SerpClient(config.serpConfig);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Add keywords to track for a project
|
|
29
|
-
*/
|
|
30
|
-
async addKeywords(
|
|
31
|
-
projectId: string,
|
|
32
|
-
keywords: string[],
|
|
33
|
-
options?: {
|
|
34
|
-
searchEngine?: 'google' | 'bing';
|
|
35
|
-
country?: string;
|
|
36
|
-
language?: string;
|
|
37
|
-
trackUrl?: string;
|
|
38
|
-
}
|
|
39
|
-
): Promise<TrackedKeyword[]> {
|
|
40
|
-
const keywordRecords = keywords.map(keyword => ({
|
|
41
|
-
project_id: projectId,
|
|
42
|
-
keyword: keyword.toLowerCase().trim(),
|
|
43
|
-
search_engine: options?.searchEngine || 'google',
|
|
44
|
-
country: options?.country || 'US',
|
|
45
|
-
language: options?.language || 'en',
|
|
46
|
-
track_url: options?.trackUrl,
|
|
47
|
-
is_active: true,
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
const { data, error } = await this.supabase
|
|
51
|
-
.from('keywords')
|
|
52
|
-
.upsert(keywordRecords, {
|
|
53
|
-
onConflict: 'project_id,keyword',
|
|
54
|
-
ignoreDuplicates: false,
|
|
55
|
-
})
|
|
56
|
-
.select();
|
|
57
|
-
|
|
58
|
-
if (error) {
|
|
59
|
-
throw new Error(`Failed to add keywords: ${error.message}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return (data || []).map(this.mapKeyword);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Remove keywords from tracking
|
|
67
|
-
*/
|
|
68
|
-
async removeKeywords(projectId: string, keywords: string[]): Promise<void> {
|
|
69
|
-
const normalizedKeywords = keywords.map(k => k.toLowerCase().trim());
|
|
70
|
-
|
|
71
|
-
const { error } = await this.supabase
|
|
72
|
-
.from('keywords')
|
|
73
|
-
.update({ is_active: false })
|
|
74
|
-
.eq('project_id', projectId)
|
|
75
|
-
.in('keyword', normalizedKeywords);
|
|
76
|
-
|
|
77
|
-
if (error) {
|
|
78
|
-
throw new Error(`Failed to remove keywords: ${error.message}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get all tracked keywords for a project
|
|
84
|
-
*/
|
|
85
|
-
async getKeywords(projectId: string, includeInactive = false): Promise<TrackedKeyword[]> {
|
|
86
|
-
let query = this.supabase
|
|
87
|
-
.from('keywords')
|
|
88
|
-
.select('*')
|
|
89
|
-
.eq('project_id', projectId);
|
|
90
|
-
|
|
91
|
-
if (!includeInactive) {
|
|
92
|
-
query = query.eq('is_active', true);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const { data, error } = await query;
|
|
96
|
-
|
|
97
|
-
if (error) {
|
|
98
|
-
throw new Error(`Failed to get keywords: ${error.message}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return (data || []).map(this.mapKeyword);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Check rankings for all active keywords in a project
|
|
106
|
-
*/
|
|
107
|
-
async checkRankings(projectId: string, domain: string): Promise<RankingResult[]> {
|
|
108
|
-
// Get active keywords
|
|
109
|
-
const keywords = await this.getKeywords(projectId);
|
|
110
|
-
|
|
111
|
-
if (keywords.length === 0) {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Group by search engine and country for efficiency
|
|
116
|
-
const groups = this.groupKeywords(keywords);
|
|
117
|
-
const results: RankingResult[] = [];
|
|
118
|
-
|
|
119
|
-
for (const group of groups) {
|
|
120
|
-
const checkResults = await this.serpClient.checkRank({
|
|
121
|
-
keywords: group.keywords.map(k => k.keyword),
|
|
122
|
-
domain,
|
|
123
|
-
searchEngine: group.searchEngine,
|
|
124
|
-
country: group.country,
|
|
125
|
-
language: group.language,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Process results
|
|
129
|
-
for (const result of checkResults) {
|
|
130
|
-
const keyword = group.keywords.find(k => k.keyword === result.keyword);
|
|
131
|
-
if (!keyword) continue;
|
|
132
|
-
|
|
133
|
-
// Save to database
|
|
134
|
-
await this.saveRanking(keyword.id, result);
|
|
135
|
-
|
|
136
|
-
results.push({
|
|
137
|
-
keywordId: keyword.id,
|
|
138
|
-
keyword: result.keyword,
|
|
139
|
-
position: result.position,
|
|
140
|
-
url: result.url,
|
|
141
|
-
serpFeatures: result.serpFeatures,
|
|
142
|
-
competitorUrls: result.topResults,
|
|
143
|
-
checkedAt: result.checkedAt,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Update project last check time
|
|
149
|
-
await this.supabase
|
|
150
|
-
.from('projects')
|
|
151
|
-
.update({ last_rank_check_at: new Date().toISOString() })
|
|
152
|
-
.eq('id', projectId);
|
|
153
|
-
|
|
154
|
-
return results;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Save a ranking result to the database
|
|
159
|
-
*/
|
|
160
|
-
private async saveRanking(keywordId: string, result: RankCheckResult): Promise<void> {
|
|
161
|
-
// Get current position to set as previous
|
|
162
|
-
const { data: currentKeyword } = await this.supabase
|
|
163
|
-
.from('keywords')
|
|
164
|
-
.select('current_position, best_position')
|
|
165
|
-
.eq('id', keywordId)
|
|
166
|
-
.single();
|
|
167
|
-
|
|
168
|
-
const previousPosition = currentKeyword?.current_position;
|
|
169
|
-
const bestPosition = currentKeyword?.best_position;
|
|
170
|
-
const newBestPosition = result.position !== null &&
|
|
171
|
-
(bestPosition === null || result.position < bestPosition)
|
|
172
|
-
? result.position
|
|
173
|
-
: bestPosition;
|
|
174
|
-
|
|
175
|
-
// Update keyword with new position
|
|
176
|
-
const { error: updateError } = await this.supabase
|
|
177
|
-
.from('keywords')
|
|
178
|
-
.update({
|
|
179
|
-
previous_position: previousPosition,
|
|
180
|
-
current_position: result.position,
|
|
181
|
-
best_position: newBestPosition,
|
|
182
|
-
last_checked: result.checkedAt.toISOString(),
|
|
183
|
-
})
|
|
184
|
-
.eq('id', keywordId);
|
|
185
|
-
|
|
186
|
-
if (updateError) {
|
|
187
|
-
console.error(`Failed to save ranking for ${result.keyword}:`, updateError);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Get ranking history for a keyword
|
|
193
|
-
* Note: Full history requires keyword_ranking_history table with keyword_ranking_id
|
|
194
|
-
* For now, returns current state as single history entry
|
|
195
|
-
*/
|
|
196
|
-
async getHistory(keywordId: string, _days = 30): Promise<RankHistory[]> {
|
|
197
|
-
const { data, error } = await this.supabase
|
|
198
|
-
.from('keywords')
|
|
199
|
-
.select('current_position, target_url, last_checked')
|
|
200
|
-
.eq('id', keywordId)
|
|
201
|
-
.single();
|
|
202
|
-
|
|
203
|
-
if (error || !data) {
|
|
204
|
-
return [];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Return current position as single history entry
|
|
208
|
-
return [{
|
|
209
|
-
position: data.current_position,
|
|
210
|
-
url: data.target_url,
|
|
211
|
-
serpFeatures: [],
|
|
212
|
-
recordedAt: data.last_checked ? new Date(data.last_checked) : new Date(),
|
|
213
|
-
}];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Get keyword trends for a project
|
|
218
|
-
*/
|
|
219
|
-
async getTrends(projectId: string): Promise<KeywordTrend[]> {
|
|
220
|
-
const { data, error } = await this.supabase
|
|
221
|
-
.from('keyword_rank_trends')
|
|
222
|
-
.select('*')
|
|
223
|
-
.eq('project_id', projectId);
|
|
224
|
-
|
|
225
|
-
if (error) {
|
|
226
|
-
// View might not exist, fall back to manual calculation
|
|
227
|
-
return this.calculateTrends(projectId);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
231
|
-
return (data || []).map((row: any) => ({
|
|
232
|
-
keywordId: row.keyword_id,
|
|
233
|
-
keyword: row.keyword,
|
|
234
|
-
currentPosition: row.current_position,
|
|
235
|
-
bestPosition: row.best_position,
|
|
236
|
-
positionChange: row.position_change || 0,
|
|
237
|
-
avgPosition: row.avg_position || 0,
|
|
238
|
-
dataPoints: row.data_points || 0,
|
|
239
|
-
}));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Manual trend calculation fallback
|
|
244
|
-
*/
|
|
245
|
-
private async calculateTrends(projectId: string): Promise<KeywordTrend[]> {
|
|
246
|
-
const keywords = await this.getKeywords(projectId);
|
|
247
|
-
const trends: KeywordTrend[] = [];
|
|
248
|
-
|
|
249
|
-
for (const keyword of keywords) {
|
|
250
|
-
const history = await this.getHistory(keyword.id, 30);
|
|
251
|
-
|
|
252
|
-
if (history.length === 0) {
|
|
253
|
-
trends.push({
|
|
254
|
-
keywordId: keyword.id,
|
|
255
|
-
keyword: keyword.keyword,
|
|
256
|
-
currentPosition: keyword.currentPosition,
|
|
257
|
-
bestPosition: keyword.bestPosition,
|
|
258
|
-
positionChange: 0,
|
|
259
|
-
avgPosition: keyword.currentPosition || 0,
|
|
260
|
-
dataPoints: 0,
|
|
261
|
-
});
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const positions = history
|
|
266
|
-
.filter(h => h.position !== null)
|
|
267
|
-
.map(h => h.position!);
|
|
268
|
-
|
|
269
|
-
const avgPosition = positions.length > 0
|
|
270
|
-
? positions.reduce((a, b) => a + b, 0) / positions.length
|
|
271
|
-
: 0;
|
|
272
|
-
|
|
273
|
-
const positionChange = keyword.previousPosition && keyword.currentPosition
|
|
274
|
-
? keyword.previousPosition - keyword.currentPosition
|
|
275
|
-
: 0;
|
|
276
|
-
|
|
277
|
-
trends.push({
|
|
278
|
-
keywordId: keyword.id,
|
|
279
|
-
keyword: keyword.keyword,
|
|
280
|
-
currentPosition: keyword.currentPosition,
|
|
281
|
-
bestPosition: keyword.bestPosition,
|
|
282
|
-
positionChange,
|
|
283
|
-
avgPosition,
|
|
284
|
-
dataPoints: history.length,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return trends;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Export ranking data as CSV
|
|
293
|
-
*/
|
|
294
|
-
async exportCSV(projectId: string, days = 30): Promise<string> {
|
|
295
|
-
const keywords = await this.getKeywords(projectId);
|
|
296
|
-
const rows: string[] = ['Keyword,Current Position,Best Position,Last Checked,Trend'];
|
|
297
|
-
|
|
298
|
-
for (const keyword of keywords) {
|
|
299
|
-
const history = await this.getHistory(keyword.id, days);
|
|
300
|
-
const trend = this.calculatePositionTrend(history);
|
|
301
|
-
|
|
302
|
-
rows.push([
|
|
303
|
-
`"${keyword.keyword}"`,
|
|
304
|
-
keyword.currentPosition?.toString() || 'N/A',
|
|
305
|
-
keyword.bestPosition?.toString() || 'N/A',
|
|
306
|
-
keyword.lastChecked?.toISOString() || 'Never',
|
|
307
|
-
trend,
|
|
308
|
-
].join(','));
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return rows.join('\n');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Calculate position trend from history
|
|
316
|
-
*/
|
|
317
|
-
private calculatePositionTrend(history: RankHistory[]): string {
|
|
318
|
-
if (history.length < 2) return '→';
|
|
319
|
-
|
|
320
|
-
const recent = history[0]?.position;
|
|
321
|
-
const older = history[history.length - 1]?.position;
|
|
322
|
-
|
|
323
|
-
if (recent === null || older === null) return '→';
|
|
324
|
-
if (recent < older) return '↑';
|
|
325
|
-
if (recent > older) return '↓';
|
|
326
|
-
return '→';
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Group keywords by search engine and country
|
|
331
|
-
*/
|
|
332
|
-
private groupKeywords(keywords: TrackedKeyword[]): Array<{
|
|
333
|
-
searchEngine: 'google' | 'bing';
|
|
334
|
-
country: string;
|
|
335
|
-
language: string;
|
|
336
|
-
keywords: TrackedKeyword[];
|
|
337
|
-
}> {
|
|
338
|
-
const groups = new Map<string, TrackedKeyword[]>();
|
|
339
|
-
|
|
340
|
-
for (const keyword of keywords) {
|
|
341
|
-
const key = `${keyword.searchEngine}:${keyword.country}:${keyword.language}`;
|
|
342
|
-
if (!groups.has(key)) {
|
|
343
|
-
groups.set(key, []);
|
|
344
|
-
}
|
|
345
|
-
groups.get(key)!.push(keyword);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return Array.from(groups.entries()).map(([key, keywords]) => {
|
|
349
|
-
const [searchEngine, country, language] = key.split(':');
|
|
350
|
-
return {
|
|
351
|
-
searchEngine: searchEngine as 'google' | 'bing',
|
|
352
|
-
country,
|
|
353
|
-
language,
|
|
354
|
-
keywords,
|
|
355
|
-
};
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Map database row to TrackedKeyword
|
|
361
|
-
*/
|
|
362
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
363
|
-
private mapKeyword(row: any): TrackedKeyword {
|
|
364
|
-
return {
|
|
365
|
-
id: row.id as string,
|
|
366
|
-
projectId: row.project_id as string,
|
|
367
|
-
keyword: row.keyword as string,
|
|
368
|
-
searchEngine: (row.search_engine as 'google' | 'bing') || 'google',
|
|
369
|
-
country: (row.country as string) || 'US',
|
|
370
|
-
language: (row.language as string) || 'en',
|
|
371
|
-
currentPosition: row.current_position as number | null,
|
|
372
|
-
previousPosition: row.previous_position as number | null,
|
|
373
|
-
bestPosition: row.best_position as number | null,
|
|
374
|
-
trackUrl: row.track_url as string | undefined,
|
|
375
|
-
isActive: row.is_active as boolean,
|
|
376
|
-
lastChecked: row.last_checked ? new Date(row.last_checked as string) : null,
|
|
377
|
-
createdAt: new Date(row.created_at as string),
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
}
|
package/src/ranking/types.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// Rank Tracking Types
|
|
2
|
-
|
|
3
|
-
export interface TrackedKeyword {
|
|
4
|
-
id: string;
|
|
5
|
-
projectId: string;
|
|
6
|
-
keyword: string;
|
|
7
|
-
searchEngine: 'google' | 'bing' | 'duckduckgo';
|
|
8
|
-
country: string;
|
|
9
|
-
language: string;
|
|
10
|
-
currentPosition: number | null;
|
|
11
|
-
previousPosition: number | null;
|
|
12
|
-
bestPosition: number | null;
|
|
13
|
-
trackUrl?: string; // Specific URL to track
|
|
14
|
-
isActive: boolean;
|
|
15
|
-
lastChecked: Date | null;
|
|
16
|
-
createdAt: Date;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface RankingResult {
|
|
20
|
-
keywordId: string;
|
|
21
|
-
keyword: string;
|
|
22
|
-
position: number | null; // null = not in top 100
|
|
23
|
-
url: string | null;
|
|
24
|
-
serpFeatures: SerpFeature[];
|
|
25
|
-
competitorUrls: CompetitorUrl[];
|
|
26
|
-
checkedAt: Date;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface SerpFeature {
|
|
30
|
-
type: 'featured_snippet' | 'people_also_ask' | 'local_pack' | 'knowledge_panel' |
|
|
31
|
-
'image_pack' | 'video_carousel' | 'top_stories' | 'shopping_results' |
|
|
32
|
-
'site_links' | 'faq_rich_result';
|
|
33
|
-
position?: number; // Position where feature appears
|
|
34
|
-
hasOwnSite?: boolean; // Whether your site is in this feature
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface CompetitorUrl {
|
|
38
|
-
position: number;
|
|
39
|
-
url: string;
|
|
40
|
-
domain: string;
|
|
41
|
-
title?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface RankHistory {
|
|
45
|
-
position: number | null;
|
|
46
|
-
url: string | null;
|
|
47
|
-
serpFeatures: SerpFeature[];
|
|
48
|
-
recordedAt: Date;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface KeywordTrend {
|
|
52
|
-
keywordId: string;
|
|
53
|
-
keyword: string;
|
|
54
|
-
currentPosition: number | null;
|
|
55
|
-
bestPosition: number | null;
|
|
56
|
-
positionChange: number; // Positive = improved, negative = dropped
|
|
57
|
-
avgPosition: number;
|
|
58
|
-
dataPoints: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface SerpApiConfig {
|
|
62
|
-
provider: 'valueserp' | 'serpapi' | 'scraper';
|
|
63
|
-
apiKey?: string;
|
|
64
|
-
rateLimit?: number; // Queries per minute
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface RankCheckOptions {
|
|
68
|
-
keywords: string[];
|
|
69
|
-
domain: string;
|
|
70
|
-
searchEngine?: 'google' | 'bing';
|
|
71
|
-
country?: string;
|
|
72
|
-
language?: string;
|
|
73
|
-
device?: 'desktop' | 'mobile';
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface RankCheckResult {
|
|
77
|
-
keyword: string;
|
|
78
|
-
position: number | null;
|
|
79
|
-
url: string | null;
|
|
80
|
-
serpFeatures: SerpFeature[];
|
|
81
|
-
topResults: CompetitorUrl[];
|
|
82
|
-
checkedAt: Date;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Tier-based limits
|
|
86
|
-
export interface RankingTierLimits {
|
|
87
|
-
maxKeywords: number;
|
|
88
|
-
checksPerDay: number;
|
|
89
|
-
serpFeatures: boolean;
|
|
90
|
-
competitorTracking: boolean;
|
|
91
|
-
historyDays: number;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export const TIER_LIMITS: Record<string, RankingTierLimits> = {
|
|
95
|
-
free: {
|
|
96
|
-
maxKeywords: 10,
|
|
97
|
-
checksPerDay: 1,
|
|
98
|
-
serpFeatures: false,
|
|
99
|
-
competitorTracking: false,
|
|
100
|
-
historyDays: 7,
|
|
101
|
-
},
|
|
102
|
-
solo: {
|
|
103
|
-
maxKeywords: 100,
|
|
104
|
-
checksPerDay: 1,
|
|
105
|
-
serpFeatures: true,
|
|
106
|
-
competitorTracking: false,
|
|
107
|
-
historyDays: 30,
|
|
108
|
-
},
|
|
109
|
-
pro: {
|
|
110
|
-
maxKeywords: 500,
|
|
111
|
-
checksPerDay: 2,
|
|
112
|
-
serpFeatures: true,
|
|
113
|
-
competitorTracking: true,
|
|
114
|
-
historyDays: 90,
|
|
115
|
-
},
|
|
116
|
-
agency: {
|
|
117
|
-
maxKeywords: 2000,
|
|
118
|
-
checksPerDay: 4,
|
|
119
|
-
serpFeatures: true,
|
|
120
|
-
competitorTracking: true,
|
|
121
|
-
historyDays: 365,
|
|
122
|
-
},
|
|
123
|
-
};
|