From d773105e8db2b56f8b0ffa75fa4522132f7cdcf3 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:21:42 -0400 Subject: [PATCH] fix: use backlinks history for default trends (#69) * fix: use backlinks history for default trends * simplify backlinks: remove filters, always use history endpoint Remove the filter UI (status, subdomains, indirect links, exclude internal) and hardcode defaults across the stack. Replace the conditional timeseries_summary + timeseries_new_lost_summary fallback with a single backlinks/history/live call for trend data. This reduces the overview from 5 parallel API calls to 3 and removes all conditional branching. * improve charts * fix: refresh backlinks cost docs --- README.md | 6 +- scripts/backlinks-cost-profile.ts | 24 ++--- .../backlinks/BacklinksPageCharts.tsx | 35 +++++-- .../backlinks/BacklinksPageContent.tsx | 2 +- .../backlinks/BacklinksSearchCard.tsx | 94 +----------------- .../features/backlinks/backlinksPageTypes.ts | 5 - .../backlinks/useBacklinksPageData.ts | 35 +------ .../_project/p/$projectId/backlinks.tsx | 14 +-- .../services/BacklinksService.billing.test.ts | 65 +++--------- .../backlinks/services/BacklinksService.ts | 8 -- .../services/backlinksOverviewSchema.ts | 4 - .../services/backlinksServiceData.ts | 99 +++++++++---------- src/server/lib/dataforseoBacklinks.test.ts | 8 -- src/server/lib/dataforseoBacklinks.ts | 67 ++++--------- src/server/lib/dataforseoBacklinksSupport.ts | 8 +- src/server/lib/dataforseoClient.test.ts | 7 +- src/server/lib/dataforseoClient.ts | 12 +-- src/serverFunctions/backlinks.ts | 12 --- src/serverFunctions/backlinksAccess.ts | 4 - src/types/schemas/backlinks.ts | 13 --- src/types/schemas/search-params.test.ts | 12 +-- 21 files changed, 135 insertions(+), 399 deletions(-) diff --git a/README.md b/README.md index b8c91a6..2ae3f6c 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ That means you can try OpenSEO for free with the starter credit, then decide if/ ### 4) Backlinks search -- Backlinks search costs about `$0.08` for a domain or `$0.04` for a page. +- Backlinks search costs about `$0.06` for a domain or `$0.04` for a page. - Opening extra tabs like `Referring Domains` or `Top Pages` adds about `+$0.02` each. - Exact cost can vary slightly based on returned rows and DataForSEO pricing. @@ -276,7 +276,7 @@ That means you can try OpenSEO for free with the starter credit, then decide if/ - 100 keyword research requests at the default 150 results: `$3.50` - 100 keyword research requests at 500 results each: `$7.00` - 100 domain overviews (200 ranked keywords each): `$4.01` -- 100 backlinks domain searches at current defaults before opening extra tabs: about `$8.38` +- 100 backlinks domain searches at current defaults before opening extra tabs: about `$6.34` - 100 backlinks page searches at current defaults before opening extra tabs: about `$4.30` -- 100 fully explored backlinks domain searches: about `$12.98` +- 100 fully explored backlinks domain searches: about `$10.94` - 100 fully explored backlinks page searches: about `$8.61` diff --git a/scripts/backlinks-cost-profile.ts b/scripts/backlinks-cost-profile.ts index fa94ecd..2fcfb8c 100644 --- a/scripts/backlinks-cost-profile.ts +++ b/scripts/backlinks-cost-profile.ts @@ -2,7 +2,10 @@ import { existsSync, readFileSync } from "node:fs"; import process from "node:process"; import { createBacklinksService } from "@/server/features/backlinks/services/BacklinksService"; import type { BillingCustomerContext } from "@/server/billing/subscription"; -import type { BacklinksLookupInput } from "@/types/schemas/backlinks"; +import type { + BacklinksLookupInput, + BacklinksTargetScope, +} from "@/types/schemas/backlinks"; loadLocalEnv(); @@ -84,6 +87,7 @@ async function main() { function buildInput(cliArgs: Record): BacklinksLookupInput { const target = cliArgs.target; + const scope = parseScope(cliArgs.scope); if (!target) { printUsageAndExit("Missing target."); } @@ -93,10 +97,7 @@ function buildInput(cliArgs: Record): BacklinksLookupInput { return { target, - includeSubdomains: parseBoolean(cliArgs.subdomains, true), - includeIndirectLinks: parseBoolean(cliArgs.indirect, true), - excludeInternalBacklinks: parseBoolean(cliArgs.excludeInternal, true), - status: parseStatus(cliArgs.status), + scope, }; } @@ -147,13 +148,12 @@ function parseBoolean(value: string | undefined, fallback: boolean) { return value === "true"; } -function parseStatus( +function parseScope( value: string | undefined, -): BacklinksLookupInput["status"] { - if (value === "live" || value === "lost" || value === "all") { - return value; - } - return "live"; +): BacklinksTargetScope | undefined { + if (!value) return undefined; + if (value === "domain" || value === "page") return value; + printUsageAndExit(`Invalid scope: ${value}. Expected domain or page.`); } function parsePositiveInteger(value: string | undefined, fallback: number) { @@ -185,7 +185,7 @@ function loadLocalEnv() { function printUsageAndExit(message: string): never { console.error(message); console.error( - "Usage: pnpm billing:backlinks --target=example.com --confirmLive=true [--status=live|lost|all] [--subdomains=true|false] [--indirect=true|false] [--excludeInternal=true|false] [--repeat=1] [--includeTabs=true|false] [--allowCi=true]", + "Usage: pnpm billing:backlinks --target=example.com --confirmLive=true [--scope=domain|page] [--repeat=1] [--includeTabs=true|false] [--allowCi=true]", ); process.exit(1); } diff --git a/src/client/features/backlinks/BacklinksPageCharts.tsx b/src/client/features/backlinks/BacklinksPageCharts.tsx index 388fbe7..ebb3bca 100644 --- a/src/client/features/backlinks/BacklinksPageCharts.tsx +++ b/src/client/features/backlinks/BacklinksPageCharts.tsx @@ -49,13 +49,20 @@ export function BacklinksTrendChart({ tickFormatter={formatChartTick} minTickGap={24} /> - + + - + + - ) : null} @@ -174,6 +182,13 @@ function EmptyChartState() { ); } +function formatAxisValue(value: unknown) { + if (typeof value !== "number") return ""; + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; + if (value >= 1_000) return `${(value / 1_000).toFixed(0)}K`; + return String(value); +} + function formatChartTick(value: unknown) { return typeof value === "string" ? formatMonthLabel(value) : ""; } diff --git a/src/client/features/backlinks/BacklinksPageContent.tsx b/src/client/features/backlinks/BacklinksPageContent.tsx index 9bf1db2..7bdef34 100644 --- a/src/client/features/backlinks/BacklinksPageContent.tsx +++ b/src/client/features/backlinks/BacklinksPageContent.tsx @@ -129,7 +129,7 @@ function BacklinksContent({ useEffect(() => { setFilterText(""); - }, [searchState.target, searchState.status, searchState.tab]); + }, [searchState.target, searchState.tab]); const mergedData = useMemo( () => mergeTabData(data, referringDomains, topPages), diff --git a/src/client/features/backlinks/BacklinksSearchCard.tsx b/src/client/features/backlinks/BacklinksSearchCard.tsx index 8cc5a9e..977e562 100644 --- a/src/client/features/backlinks/BacklinksSearchCard.tsx +++ b/src/client/features/backlinks/BacklinksSearchCard.tsx @@ -9,10 +9,7 @@ import { import type { BacklinksSearchState } from "./backlinksPageTypes"; import { resolveBacklinksSearchScope } from "./backlinksSearchScope"; -type SearchDraft = Pick< - BacklinksSearchState, - "target" | "scope" | "subdomains" | "indirect" | "excludeInternal" | "status" ->; +type SearchDraft = Pick; function getBacklinksValidationErrors( value: SearchDraft, @@ -44,7 +41,6 @@ export function BacklinksSearchCard({ isFetching: boolean; onSubmit: (values: SearchDraft) => void; }) { - const [showAdvanced, setShowAdvanced] = useState(false); const [userSelectedScope, setUserSelectedScope] = useState(false); const form = useForm({ defaultValues: initialValues, @@ -93,7 +89,7 @@ export function BacklinksSearchCard({ return (