From c1190550b0c2e0f252436793a9d47cf9bbc5da43 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:10:58 -0400 Subject: [PATCH] fix: handle empty keyword research results (#81) Treat successful DataForSEO keyword responses with null items as empty results so auto mode can fall back cleanly. Simplify the no-results state by removing dead-end actions and top-aligning the empty card. --- .../page/KeywordResearchEmptyState.tsx | 60 +------------------ src/server/lib/dataforseoSchemas.test.ts | 40 +++++++++++++ src/server/lib/dataforseoSchemas.ts | 2 +- 3 files changed, 43 insertions(+), 59 deletions(-) create mode 100644 src/server/lib/dataforseoSchemas.test.ts diff --git a/src/client/features/keywords/page/KeywordResearchEmptyState.tsx b/src/client/features/keywords/page/KeywordResearchEmptyState.tsx index e65cd60..8756f0e 100644 --- a/src/client/features/keywords/page/KeywordResearchEmptyState.tsx +++ b/src/client/features/keywords/page/KeywordResearchEmptyState.tsx @@ -1,5 +1,4 @@ import { Clock, Globe, History, Search, X } from "lucide-react"; -import { reverse } from "remeda"; import { LOCATIONS } from "@/client/features/keywords/utils"; import type { KeywordResearchControllerState } from "./types"; @@ -18,17 +17,10 @@ export function KeywordResearchEmptyState({ controller }: Props) { } function NoResultsState({ controller }: Props) { - const { - controlsForm, - lastResultSource, - lastSearchKeyword, - lastSearchLocationCode, - lastUsedFallback, - onSearch, - } = controller; + const { lastSearchKeyword, lastSearchLocationCode } = controller; return ( -
+
@@ -47,54 +39,6 @@ function NoResultsState({ controller }: Props) { .

- -
-

- Source checked:{" "} - {lastResultSource} - {lastUsedFallback ? ( - (with fallback chain: related - suggestions - ideas) - ) : null} -

-

Try a broader phrase, swap word order, or change location.

-
- -
- - -
); diff --git a/src/server/lib/dataforseoSchemas.test.ts b/src/server/lib/dataforseoSchemas.test.ts new file mode 100644 index 0000000..f4b334f --- /dev/null +++ b/src/server/lib/dataforseoSchemas.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { + parseTaskItems, + relatedKeywordItemSchema, + successfulDataforseoTaskSchema, +} from "@/server/lib/dataforseoSchemas"; + +describe("dataforseoSchemas", () => { + it("accepts null items for empty successful tasks", () => { + const task = { + id: "04042314-1577-0387-0000-33dc4b485cfd", + status_code: 20000, + status_message: "Ok.", + path: ["v3", "dataforseo_labs", "google", "related_keywords", "live"], + cost: 0.02, + result_count: 1, + result: [ + { + se_type: "google", + seed_keyword: "canva ai video alternative", + location_code: 2840, + language_code: "en", + total_count: null, + items_count: 0, + items: null, + }, + ], + }; + + const parsedTask = successfulDataforseoTaskSchema.parse(task); + + expect( + parseTaskItems( + "google-related-keywords-live", + parsedTask, + relatedKeywordItemSchema, + ), + ).toEqual([]); + }); +}); diff --git a/src/server/lib/dataforseoSchemas.ts b/src/server/lib/dataforseoSchemas.ts index 0023201..a459200 100644 --- a/src/server/lib/dataforseoSchemas.ts +++ b/src/server/lib/dataforseoSchemas.ts @@ -12,7 +12,7 @@ const dataforseoTaskSchema = z .array( z .object({ - items: z.array(z.unknown()).optional(), + items: z.array(z.unknown()).nullable().optional(), }) .passthrough(), )