From c81fd7a130effb0bac2fa79c6a0f8ec0ab38d14e Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Wed, 29 Jul 2026 10:46:15 -0400 Subject: [PATCH] Rank positions: report organic rank (rank_group), not absolute SERP slot (#429) A user's #1 organic result showed as position 3-4 because rank_absolute counts SERP features (local pack, PAA, AI overviews) as positions. Rank tracker, keyword-research SERP list, and onboarding market snapshot now prefer rank_group; MCP get-serp-results keeps rank_absolute since it returns all SERP item types. --- src/server/features/keywords/services/research/serp.ts | 2 +- src/server/features/onboarding/onboardingMarketTools.ts | 2 +- src/server/lib/dataforseo/serp.test.ts | 2 +- src/server/lib/dataforseo/serp.ts | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/server/features/keywords/services/research/serp.ts b/src/server/features/keywords/services/research/serp.ts index 7fde940..8eb404f 100644 --- a/src/server/features/keywords/services/research/serp.ts +++ b/src/server/features/keywords/services/research/serp.ts @@ -41,7 +41,7 @@ function mapOrganicSerpItems(items: SerpLiveItem[]): SerpResultItem[] { return items .filter((item) => item.type === "organic") .map((item) => ({ - rank: item.rank_absolute ?? item.rank_group ?? 0, + rank: item.rank_group ?? item.rank_absolute ?? 0, title: item.title ?? "", url: item.url ?? "", domain: item.domain ?? "", diff --git a/src/server/features/onboarding/onboardingMarketTools.ts b/src/server/features/onboarding/onboardingMarketTools.ts index 4a12c42..142458a 100644 --- a/src/server/features/onboarding/onboardingMarketTools.ts +++ b/src/server/features/onboarding/onboardingMarketTools.ts @@ -87,7 +87,7 @@ export function marketTools(ctx: ToolContext): ToolSet { .filter((item) => item.type === "organic") .slice(0, 10) .map((item) => ({ - rank: item.rank_absolute ?? item.rank_group ?? null, + rank: item.rank_group ?? item.rank_absolute ?? null, domain: item.domain ?? null, title: item.title ?? null, url: item.url ?? null, diff --git a/src/server/lib/dataforseo/serp.test.ts b/src/server/lib/dataforseo/serp.test.ts index 75130f3..f0073ce 100644 --- a/src/server/lib/dataforseo/serp.test.ts +++ b/src/server/lib/dataforseo/serp.test.ts @@ -168,7 +168,7 @@ describe("rank check task queue", () => { result: { keywordId: "kw-1", keyword: "alpha", - position: 4, + position: 3, url: "https://www.example.com/page", serpFeatures: ["organic"], }, diff --git a/src/server/lib/dataforseo/serp.ts b/src/server/lib/dataforseo/serp.ts index 9ed6908..6d9d787 100644 --- a/src/server/lib/dataforseo/serp.ts +++ b/src/server/lib/dataforseo/serp.ts @@ -129,8 +129,11 @@ function buildRankCheckResult( return { keywordId: input.keywordId, keyword: input.keyword, + // rank_group = position among organic results only (what users count as + // "my ranking"). rank_absolute would also count SERP features (local + // pack, PAA, AI overviews) and reads as worse than what users see. position: organicMatch - ? (organicMatch.rank_absolute ?? organicMatch.rank_group ?? null) + ? (organicMatch.rank_group ?? organicMatch.rank_absolute ?? null) : null, url: organicMatch?.url ?? null, serpFeatures: [...new Set(items.map((item) => item.type).filter(Boolean))],