diff --git a/src/client/features/content/BriefTargets.tsx b/src/client/features/content/BriefTargets.tsx index 329c388c..2eadb5b5 100644 --- a/src/client/features/content/BriefTargets.tsx +++ b/src/client/features/content/BriefTargets.tsx @@ -5,14 +5,24 @@ export function BriefTargets({ analyzedCount, paaCount, analysesPending, + analysesFailed, }: { wordCounts: number[]; h2Counts: number[]; analyzedCount: number; paaCount: number; analysesPending: boolean; + /** How many per-page analyses failed. Separate from `analysesPending`, which + * goes false when requests SETTLE — successfully or not. Without this a page + * where every analysis failed reported "No data", which reads as "the top + * pages have nothing to measure" rather than "we could not measure them". */ + analysesFailed?: number; }) { - const pendingLabel = analysesPending ? "Analyzing top pages…" : "No data"; + const pendingLabel = analysesPending + ? "Analyzing top pages…" + : (analysesFailed ?? 0) > 0 + ? "Could not analyze the top pages" + : "No data"; return (
diff --git a/src/client/features/content/ContentOptimizerPage.tsx b/src/client/features/content/ContentOptimizerPage.tsx index b58c3cec..3248fb45 100644 --- a/src/client/features/content/ContentOptimizerPage.tsx +++ b/src/client/features/content/ContentOptimizerPage.tsx @@ -379,14 +379,27 @@ export function ContentOptimizerPage({ })), }); const analysisByUrl = new Map(); + // A FAILED per-URL analysis also has `data === undefined`, so without tracking + // it separately the row cells below could not tell "still fetching" from + // "this one is never coming" -- and showed loading dots forever. + const failedUrls = new Set(); competitorUrls.forEach((url, index) => { - const data = analysisQueries[index]?.data; + const query_ = analysisQueries[index]; + const data = query_?.data; if (data !== undefined) analysisByUrl.set(url, data); + if (query_?.isError) failedUrls.add(url); }); const loadedAnalyses = [...analysisByUrl.values()].filter( (analysis): analysis is NonNullable => analysis != null, ); const analysesPending = analysisQueries.some((query_) => query_.isLoading); + // Counted so the headline cards can say the analyses failed rather than + // "No data". Once every request settles, `analysesPending` is false whether + // they succeeded or not, which is how a page of failures came to be reported + // as an absence of data. + const analysesFailed = analysisQueries.filter( + (query_) => query_.isError, + ).length; const wordCounts = loadedAnalyses .map((analysis) => analysis.wordCount) @@ -610,6 +623,7 @@ export function ContentOptimizerPage({ analyzedCount={loadedAnalyses.length} paaCount={brief.paaQuestions.length} analysesPending={analysesPending} + analysesFailed={analysesFailed} /> {brief.terms.length > 0 ? ( @@ -696,6 +710,9 @@ export function ContentOptimizerPage({ const analysis = competitor.url ? analysisByUrl.get(competitor.url) : undefined; + const analysisFailed = competitor.url + ? failedUrls.has(competitor.url) + : false; return ( @@ -715,7 +732,15 @@ export function ContentOptimizerPage({
- {competitorsAuthorized && analysis === undefined ? ( + {analysisFailed ? ( + + failed + + ) : competitorsAuthorized && + analysis === undefined ? ( ) : analysis?.wordCount != null ? ( analysis.wordCount.toLocaleString() @@ -724,7 +749,15 @@ export function ContentOptimizerPage({ )} - {competitorsAuthorized && analysis === undefined ? ( + {analysisFailed ? ( + + failed + + ) : competitorsAuthorized && + analysis === undefined ? ( ) : ( (analysis?.h2.length ?? "—")