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 (