From 2b7a3eae05eb4fdfe95191dfdeab04b9a074d707 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:30:49 -0400 Subject: [PATCH] Make domain overview URL paths clickable in tables (#120) --- src/client/components/SafeExternalLink.tsx | 34 ++++++++ .../features/backlinks/BacklinksPageLinks.tsx | 25 +----- .../domain/components/DomainKeywordsTable.tsx | 78 ++++++++++++------- .../domain/components/DomainPagesTable.tsx | 37 ++++++--- .../domain/components/DomainResultsCard.tsx | 2 + src/client/features/domain/utils.ts | 11 +++ 6 files changed, 127 insertions(+), 60 deletions(-) create mode 100644 src/client/components/SafeExternalLink.tsx diff --git a/src/client/components/SafeExternalLink.tsx b/src/client/components/SafeExternalLink.tsx new file mode 100644 index 0000000..9e3c9a3 --- /dev/null +++ b/src/client/components/SafeExternalLink.tsx @@ -0,0 +1,34 @@ +import { ExternalLink } from "lucide-react"; + +export function SafeExternalLink({ + url, + label, + className, +}: { + url: string; + label: string; + className: string; +}) { + const safeUrl = getSafeExternalUrl(url); + if (!safeUrl) { + return {label}; + } + + return ( + + {label} + + + ); +} + +function getSafeExternalUrl(value: string) { + try { + const parsed = new URL(value); + return parsed.protocol === "http:" || parsed.protocol === "https:" + ? parsed.toString() + : null; + } catch { + return null; + } +} diff --git a/src/client/features/backlinks/BacklinksPageLinks.tsx b/src/client/features/backlinks/BacklinksPageLinks.tsx index dc8ad59..e31f6a0 100644 --- a/src/client/features/backlinks/BacklinksPageLinks.tsx +++ b/src/client/features/backlinks/BacklinksPageLinks.tsx @@ -1,4 +1,4 @@ -import { ExternalLink } from "lucide-react"; +import { SafeExternalLink } from "@/client/components/SafeExternalLink"; import { extractUrlPath, truncateMiddle } from "./backlinksPageUtils"; export function BacklinksExternalLink({ @@ -10,17 +10,7 @@ export function BacklinksExternalLink({ label: string; className: string; }) { - const safeUrl = getSafeExternalUrl(url); - if (!safeUrl) { - return {label}; - } - - return ( - - {label} - - - ); + return ; } export function BacklinksSourceLink({ @@ -40,14 +30,3 @@ export function BacklinksSourceLink({ /> ); } - -function getSafeExternalUrl(value: string) { - try { - const parsed = new URL(value); - return parsed.protocol === "http:" || parsed.protocol === "https:" - ? parsed.toString() - : null; - } catch { - return null; - } -} diff --git a/src/client/features/domain/components/DomainKeywordsTable.tsx b/src/client/features/domain/components/DomainKeywordsTable.tsx index b2478c3..600ecf6 100644 --- a/src/client/features/domain/components/DomainKeywordsTable.tsx +++ b/src/client/features/domain/components/DomainKeywordsTable.tsx @@ -1,6 +1,11 @@ +import { SafeExternalLink } from "@/client/components/SafeExternalLink"; import { DifficultyBadge } from "@/client/features/domain/components/DifficultyBadge"; import { SortableHeader } from "@/client/features/domain/components/SortableHeader"; -import { formatFloat, formatNumber } from "@/client/features/domain/utils"; +import { + formatFloat, + formatNumber, + resolveDomainPageHref, +} from "@/client/features/domain/utils"; import type { DomainSortMode, KeywordRow, @@ -8,6 +13,7 @@ import type { } from "@/client/features/domain/types"; type Props = { + domain: string; rows: KeywordRow[]; selectedKeywords: Set; visibleKeywords: string[]; @@ -19,6 +25,7 @@ type Props = { }; export function DomainKeywordsTable({ + domain, rows, selectedKeywords, visibleKeywords, @@ -105,33 +112,48 @@ export function DomainKeywordsTable({ ) : ( - rows.slice(0, 100).map((row) => ( - - - onToggleKeyword(row.keyword)} - aria-label={`Select ${row.keyword}`} - /> - - {row.keyword} - {row.position ?? "-"} - {formatNumber(row.searchVolume)} - {formatFloat(row.traffic)} - {row.cpc == null ? "-" : `$${row.cpc.toFixed(2)}`} - - {row.relativeUrl ?? row.url ?? "-"} - - - - - - )) + rows.slice(0, 100).map((row) => { + const href = resolveDomainPageHref( + row.relativeUrl ?? row.url, + domain, + ); + + return ( + + + onToggleKeyword(row.keyword)} + aria-label={`Select ${row.keyword}`} + /> + + {row.keyword} + {row.position ?? "-"} + {formatNumber(row.searchVolume)} + {formatFloat(row.traffic)} + {row.cpc == null ? "-" : `$${row.cpc.toFixed(2)}`} + + {href ? ( + + ) : ( + "-" + )} + + + + + + ); + }) )} diff --git a/src/client/features/domain/components/DomainPagesTable.tsx b/src/client/features/domain/components/DomainPagesTable.tsx index 4ee4707..bf169f4 100644 --- a/src/client/features/domain/components/DomainPagesTable.tsx +++ b/src/client/features/domain/components/DomainPagesTable.tsx @@ -1,7 +1,9 @@ +import { SafeExternalLink } from "@/client/components/SafeExternalLink"; import { SortableHeader } from "@/client/features/domain/components/SortableHeader"; import { formatFloat, formatNumber, + resolveDomainPageHref, toPageSortMode, } from "@/client/features/domain/utils"; import type { @@ -11,6 +13,7 @@ import type { } from "@/client/features/domain/types"; type Props = { + domain: string; rows: PageRow[]; sortMode: DomainSortMode; currentSortOrder: SortOrder; @@ -18,6 +21,7 @@ type Props = { }; export function DomainPagesTable({ + domain, rows, sortMode, currentSortOrder, @@ -55,15 +59,30 @@ export function DomainPagesTable({ ) : ( - rows.slice(0, 100).map((row) => ( - - - {row.relativePath ?? row.page} - - {formatFloat(row.organicTraffic)} - {formatNumber(row.keywords)} - - )) + rows.slice(0, 100).map((row) => { + const href = resolveDomainPageHref( + row.relativePath ?? row.page, + domain, + ); + + return ( + + + {href ? ( + + ) : ( + (row.relativePath ?? row.page) + )} + + {formatFloat(row.organicTraffic)} + {formatNumber(row.keywords)} + + ); + }) )} diff --git a/src/client/features/domain/components/DomainResultsCard.tsx b/src/client/features/domain/components/DomainResultsCard.tsx index 0fb7436..3fb7567 100644 --- a/src/client/features/domain/components/DomainResultsCard.tsx +++ b/src/client/features/domain/components/DomainResultsCard.tsx @@ -205,6 +205,7 @@ export function DomainResultsCard({
{isKeywordsTab ? ( ) : (