@@ -194,7 +231,7 @@ export function BacklinksResultsCard({
) : null}
{activeTab === "backlinks" ? (
) : null}
@@ -203,7 +240,7 @@ export function BacklinksResultsCard({
) : null}
{activeTab === "domains" && !isTabLoading && !tabErrorMessage ? (
) : null}
@@ -235,6 +272,21 @@ function collectRatableDomains(filteredData: {
];
}
+function countVisibleFilters(
+ values: Record
,
+ showAhrefsDrFilter: boolean,
+) {
+ return Object.entries(values).filter(([key, value]) => {
+ if (
+ !showAhrefsDrFilter &&
+ (key === "minAhrefsDr" || key === "maxAhrefsDr")
+ ) {
+ return false;
+ }
+ return value.trim() !== "";
+ }).length;
+}
+
function OverviewGrid({
data,
summaryStats,
diff --git a/src/client/features/backlinks/backlinksFilterTypes.ts b/src/client/features/backlinks/backlinksFilterTypes.ts
index cca057f..b621ecf 100644
--- a/src/client/features/backlinks/backlinksFilterTypes.ts
+++ b/src/client/features/backlinks/backlinksFilterTypes.ts
@@ -3,6 +3,8 @@ export type BacklinksTabFilterValues = {
exclude: string;
minDomainRank: string;
maxDomainRank: string;
+ minAhrefsDr: string;
+ maxAhrefsDr: string;
minLinkAuthority: string;
maxLinkAuthority: string;
minSpamScore: string;
@@ -19,6 +21,8 @@ export type ReferringDomainsFilterValues = {
maxBacklinks: string;
minRank: string;
maxRank: string;
+ minAhrefsDr: string;
+ maxAhrefsDr: string;
minSpamScore: string;
maxSpamScore: string;
};
@@ -39,6 +43,8 @@ export const EMPTY_BACKLINKS_FILTERS: BacklinksTabFilterValues = {
exclude: "",
minDomainRank: "",
maxDomainRank: "",
+ minAhrefsDr: "",
+ maxAhrefsDr: "",
minLinkAuthority: "",
maxLinkAuthority: "",
minSpamScore: "",
@@ -55,6 +61,8 @@ export const EMPTY_REFERRING_DOMAINS_FILTERS: ReferringDomainsFilterValues = {
maxBacklinks: "",
minRank: "",
maxRank: "",
+ minAhrefsDr: "",
+ maxAhrefsDr: "",
minSpamScore: "",
maxSpamScore: "",
};
diff --git a/src/client/features/backlinks/backlinksFiltering.test.ts b/src/client/features/backlinks/backlinksFiltering.test.ts
index dcded4a..351c389 100644
--- a/src/client/features/backlinks/backlinksFiltering.test.ts
+++ b/src/client/features/backlinks/backlinksFiltering.test.ts
@@ -1,10 +1,39 @@
import { describe, expect, it } from "vitest";
import type { BacklinksOverviewData } from "./backlinksPageTypes";
-import { EMPTY_REFERRING_DOMAINS_FILTERS } from "./backlinksFilterTypes";
-import { filterReferringDomainRows } from "./backlinksFiltering";
+import {
+ EMPTY_BACKLINKS_FILTERS,
+ EMPTY_REFERRING_DOMAINS_FILTERS,
+} from "./backlinksFilterTypes";
+import {
+ filterBacklinkRows,
+ filterReferringDomainRows,
+} from "./backlinksFiltering";
+type BacklinkRow = BacklinksOverviewData["backlinks"][number];
type ReferringDomainRow = BacklinksOverviewData["referringDomains"][number];
+function makeBacklinkRow(overrides: Partial = {}): BacklinkRow {
+ return {
+ urlFrom: "https://example.com/post",
+ urlTo: "https://target.example/page",
+ domainFrom: "example.com",
+ anchor: "Example",
+ itemType: "anchor",
+ rank: 10,
+ domainFromRank: 20,
+ pageFromRank: 10,
+ spamScore: 2,
+ relAttributes: [],
+ firstSeen: null,
+ lastSeen: null,
+ linksCount: 1,
+ isDofollow: true,
+ isLost: false,
+ isBroken: false,
+ ...overrides,
+ };
+}
+
function makeReferringDomainRow(
overrides: Partial = {},
): ReferringDomainRow {
@@ -21,6 +50,57 @@ function makeReferringDomainRow(
};
}
+describe("filterBacklinkRows", () => {
+ it("ignores Ahrefs DR range until ratings are loaded for the backlinks table", () => {
+ const rows = [
+ makeBacklinkRow({ domainFrom: "low.example" }),
+ makeBacklinkRow({ domainFrom: "high.example" }),
+ ];
+
+ expect(
+ filterBacklinkRows(rows, {
+ ...EMPTY_BACKLINKS_FILTERS,
+ minAhrefsDr: "50",
+ }),
+ ).toEqual(rows);
+ });
+
+ it("filters by loaded Ahrefs DR range for the backlinks table", () => {
+ const rows = [
+ makeBacklinkRow({ domainFrom: "low.example" }),
+ makeBacklinkRow({ domainFrom: "www.high.example" }),
+ makeBacklinkRow({ domainFrom: "unknown.example" }),
+ ];
+ const ratings = {
+ "low.example": 12,
+ "high.example": 64,
+ "unknown.example": null,
+ };
+
+ expect(
+ filterBacklinkRows(
+ rows,
+ {
+ ...EMPTY_BACKLINKS_FILTERS,
+ minAhrefsDr: "50",
+ },
+ ratings,
+ ),
+ ).toEqual([rows[1], rows[2]]);
+
+ expect(
+ filterBacklinkRows(
+ rows,
+ {
+ ...EMPTY_BACKLINKS_FILTERS,
+ maxAhrefsDr: "50",
+ },
+ ratings,
+ ),
+ ).toEqual([rows[0], rows[2]]);
+ });
+});
+
describe("filterReferringDomainRows", () => {
it("filters by spam score range", () => {
const rows = [
@@ -43,4 +123,53 @@ describe("filterReferringDomainRows", () => {
}),
).toEqual([rows[1], rows[2]]);
});
+
+ it("ignores Ahrefs DR range until ratings are loaded for referring domains", () => {
+ const rows = [
+ makeReferringDomainRow({ domain: "low.example" }),
+ makeReferringDomainRow({ domain: "high.example" }),
+ ];
+
+ expect(
+ filterReferringDomainRows(rows, {
+ ...EMPTY_REFERRING_DOMAINS_FILTERS,
+ minAhrefsDr: "50",
+ }),
+ ).toEqual(rows);
+ });
+
+ it("filters by loaded Ahrefs DR range for referring domains", () => {
+ const rows = [
+ makeReferringDomainRow({ domain: "low.example" }),
+ makeReferringDomainRow({ domain: "high.example" }),
+ makeReferringDomainRow({ domain: "unknown.example" }),
+ ];
+ const ratings = {
+ "low.example": 12,
+ "high.example": 64,
+ "unknown.example": null,
+ };
+
+ expect(
+ filterReferringDomainRows(
+ rows,
+ {
+ ...EMPTY_REFERRING_DOMAINS_FILTERS,
+ minAhrefsDr: "50",
+ },
+ ratings,
+ ),
+ ).toEqual([rows[1], rows[2]]);
+
+ expect(
+ filterReferringDomainRows(
+ rows,
+ {
+ ...EMPTY_REFERRING_DOMAINS_FILTERS,
+ maxAhrefsDr: "50",
+ },
+ ratings,
+ ),
+ ).toEqual([rows[0], rows[2]]);
+ });
});
diff --git a/src/client/features/backlinks/backlinksFiltering.ts b/src/client/features/backlinks/backlinksFiltering.ts
index cdf2737..f128035 100644
--- a/src/client/features/backlinks/backlinksFiltering.ts
+++ b/src/client/features/backlinks/backlinksFiltering.ts
@@ -5,6 +5,7 @@ import type {
ReferringDomainsFilterValues,
TopPagesFilterValues,
} from "./backlinksFilterTypes";
+import type { DomainRatings } from "./useAhrefsDomainRatings";
function passesNumericFilter(
value: number | null | undefined,
@@ -40,6 +41,7 @@ function passesTextFilter(
export function filterBacklinkRows(
rows: BacklinksOverviewData["backlinks"],
filters: BacklinksTabFilterValues,
+ domainRatings?: DomainRatings | null,
): BacklinksOverviewData["backlinks"] {
const includeTerms = parseTerms(filters.include);
const excludeTerms = parseTerms(filters.exclude);
@@ -58,6 +60,17 @@ export function filterBacklinkRows(
)
)
return false;
+ if (
+ domainRatings &&
+ !passesNumericFilter(
+ row.domainFrom
+ ? domainRatings[row.domainFrom.replace(/^www\./, "")]
+ : null,
+ filters.minAhrefsDr,
+ filters.maxAhrefsDr,
+ )
+ )
+ return false;
if (
!passesNumericFilter(
row.rank,
@@ -90,6 +103,7 @@ export function filterBacklinkRows(
export function filterReferringDomainRows(
rows: BacklinksOverviewData["referringDomains"],
filters: ReferringDomainsFilterValues,
+ domainRatings?: DomainRatings | null,
): BacklinksOverviewData["referringDomains"] {
const includeTerms = parseTerms(filters.include);
const excludeTerms = parseTerms(filters.exclude);
@@ -107,6 +121,15 @@ export function filterReferringDomainRows(
return false;
if (!passesNumericFilter(row.rank, filters.minRank, filters.maxRank))
return false;
+ if (
+ domainRatings &&
+ !passesNumericFilter(
+ row.domain ? domainRatings[row.domain] : null,
+ filters.minAhrefsDr,
+ filters.maxAhrefsDr,
+ )
+ )
+ return false;
if (
!passesNumericFilter(
row.spamScore,
diff --git a/src/client/features/rank-tracking/KeywordSuggestionStep.tsx b/src/client/features/rank-tracking/KeywordSuggestionStep.tsx
index 051ba0d..0f22d27 100644
--- a/src/client/features/rank-tracking/KeywordSuggestionStep.tsx
+++ b/src/client/features/rank-tracking/KeywordSuggestionStep.tsx
@@ -5,11 +5,8 @@ import {
type RowSelectionState,
type SortingState,
} from "@tanstack/react-table";
-import { Loader2, AlertCircle, FileDown, Sheet, X } from "lucide-react";
+import { Loader2, AlertCircle, X } from "lucide-react";
import { toast } from "sonner";
-import { buildCsv, downloadCsv } from "@/client/lib/csv";
-import { exportTableToSheets } from "@/client/lib/exportToSheets";
-import { captureClientEvent } from "@/client/lib/posthog";
import { getDomainKeywordSuggestions } from "@/serverFunctions/domain";
import { addTrackingKeywords } from "@/serverFunctions/rank-tracking";
import { getStandardErrorMessage } from "@/client/lib/error-messages";
@@ -18,11 +15,6 @@ import {
makeSelectionColumn,
useAppTable,
} from "@/client/components/table/AppDataTable";
-import {
- TableBulkActionBar,
- TableBulkActionButton,
- TableBulkExportMenu,
-} from "@/client/components/table/TableBulkActionBar";
import { SortableHeader } from "./RankTrackingColumns";
import {
applyShiftRangeSelection,
@@ -37,12 +29,6 @@ type SuggestedKeyword = {
};
const PRE_SELECT_COUNT = 20;
-const SUGGESTED_KEYWORD_EXPORT_HEADERS = [
- "Keyword",
- "Position",
- "Volume",
- "Traffic",
-];
const baseColumns: ColumnDef[] = [
{
@@ -240,32 +226,6 @@ export function KeywordSuggestionStep({
addMutation.mutate(selectedKeywords);
}
};
- const selectedSuggestionRows = table
- .getSelectedRowModel()
- .rows.map((row) => row.original);
- const selectedSuggestionExportRows = selectedSuggestionRows.map((row) => [
- row.keyword,
- row.position ?? "",
- row.searchVolume ?? "",
- row.traffic ?? "",
- ]);
- const handleExportSelectionToSheets = () => {
- void exportTableToSheets({
- headers: SUGGESTED_KEYWORD_EXPORT_HEADERS,
- rows: selectedSuggestionExportRows,
- feature: "rank_tracking",
- });
- };
- const handleExportSelectionCsv = () => {
- downloadCsv(
- `rank-tracking-suggestions-${domain}.csv`,
- buildCsv(SUGGESTED_KEYWORD_EXPORT_HEADERS, selectedSuggestionExportRows),
- );
- captureClientEvent("rank_tracking:suggestions_export_csv", {
- result_count: selectedSuggestionRows.length,
- scope: "selection",
- });
- };
const sectionHeader = (title: string) => (
@@ -301,18 +261,12 @@ export function KeywordSuggestionStep({
- You can try again or add keywords manually later.
+ You can skip this step and add keywords manually later.
-
>
@@ -330,7 +284,7 @@ export function KeywordSuggestionStep({
add keywords manually.
- Continue
+ Skip
>
@@ -365,44 +319,25 @@ export function KeywordSuggestionStep({
/>
-
- Skip
-
-
table.resetRowSelection()}
- placement="inline"
- actions={
-
-
- ) : undefined
- }
- onClick={handleAdd}
- disabled={addMutation.isPending}
- >
- Add Keyword{selectedCount !== 1 ? "s" : ""}
-
-
,
- onClick: handleExportSelectionToSheets,
- },
- {
- label: "Export CSV",
- icon:
,
- onClick: handleExportSelectionCsv,
- },
- ]}
- />
-
- }
- />
+
+ {selectedCount} of {data.length} selected
+
+
+
+ Skip
+
+
+ {addMutation.isPending && (
+
+ )}
+ Save Keyword{selectedCount !== 1 ? "s" : ""}
+
+
);
diff --git a/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx b/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx
index 870f021..16516c9 100644
--- a/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx
+++ b/src/client/features/rank-tracking/RankTrackingDomainDetail.tsx
@@ -256,7 +256,6 @@ function RankTrackingDomainDetailInner({
{/* Portfolio overview */}
{(rows?.length ?? 0) > 0 && (