Add per-tab CSV export for backlinks results (#95)
* Add CSV export per backlinks tab * save
This commit is contained in:
parent
d2fc85342a
commit
868d83fb1c
@ -155,6 +155,7 @@ export function BacklinksBody({
|
|||||||
onSetActiveTab={onSetActiveTab}
|
onSetActiveTab={onSetActiveTab}
|
||||||
onSetHideSpam={onSetHideSpam}
|
onSetHideSpam={onSetHideSpam}
|
||||||
onSetSpamThreshold={onSetSpamThreshold}
|
onSetSpamThreshold={onSetSpamThreshold}
|
||||||
|
exportTarget={mergedData.displayTarget || searchState.target}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { HeaderHelpLabel } from "@/client/features/keywords/components";
|
import { HeaderHelpLabel } from "@/client/features/keywords/components";
|
||||||
import { Search } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
BacklinksNewLostChart,
|
BacklinksNewLostChart,
|
||||||
BacklinksTrendChart,
|
BacklinksTrendChart,
|
||||||
} from "./BacklinksPageCharts";
|
} from "./BacklinksPageCharts";
|
||||||
|
import { ResultsHeader } from "./BacklinksResultsHeader";
|
||||||
import { BacklinksTable } from "./BacklinksTable";
|
import { BacklinksTable } from "./BacklinksTable";
|
||||||
import { ReferringDomainsTable } from "./ReferringDomainsTable";
|
import { ReferringDomainsTable } from "./ReferringDomainsTable";
|
||||||
import { TopPagesTable } from "./TopPagesTable";
|
import { TopPagesTable } from "./TopPagesTable";
|
||||||
@ -12,10 +11,7 @@ import type {
|
|||||||
BacklinksOverviewData,
|
BacklinksOverviewData,
|
||||||
BacklinksSearchState,
|
BacklinksSearchState,
|
||||||
} from "./backlinksPageTypes";
|
} from "./backlinksPageTypes";
|
||||||
import {
|
import { formatRelativeTimestamp } from "./backlinksPageUtils";
|
||||||
TAB_DESCRIPTIONS,
|
|
||||||
formatRelativeTimestamp,
|
|
||||||
} from "./backlinksPageUtils";
|
|
||||||
|
|
||||||
export function BacklinksOverviewPanels({
|
export function BacklinksOverviewPanels({
|
||||||
data,
|
data,
|
||||||
@ -58,6 +54,7 @@ export function BacklinksResultsCard({
|
|||||||
onSetActiveTab,
|
onSetActiveTab,
|
||||||
onSetHideSpam,
|
onSetHideSpam,
|
||||||
onSetSpamThreshold,
|
onSetSpamThreshold,
|
||||||
|
exportTarget,
|
||||||
}: {
|
}: {
|
||||||
activeTab: BacklinksSearchState["tab"];
|
activeTab: BacklinksSearchState["tab"];
|
||||||
filteredData: {
|
filteredData: {
|
||||||
@ -74,6 +71,7 @@ export function BacklinksResultsCard({
|
|||||||
onSetActiveTab: (tab: BacklinksSearchState["tab"]) => void;
|
onSetActiveTab: (tab: BacklinksSearchState["tab"]) => void;
|
||||||
onSetHideSpam: (hideSpam: boolean) => void;
|
onSetHideSpam: (hideSpam: boolean) => void;
|
||||||
onSetSpamThreshold: (threshold: number) => void;
|
onSetSpamThreshold: (threshold: number) => void;
|
||||||
|
exportTarget: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="card bg-base-100 border border-base-300">
|
<div className="card bg-base-100 border border-base-300">
|
||||||
@ -87,6 +85,8 @@ export function BacklinksResultsCard({
|
|||||||
onSetActiveTab={onSetActiveTab}
|
onSetActiveTab={onSetActiveTab}
|
||||||
onSetHideSpam={onSetHideSpam}
|
onSetHideSpam={onSetHideSpam}
|
||||||
onSetSpamThreshold={onSetSpamThreshold}
|
onSetSpamThreshold={onSetSpamThreshold}
|
||||||
|
filteredData={filteredData}
|
||||||
|
exportTarget={exportTarget}
|
||||||
/>
|
/>
|
||||||
{tabErrorMessage ? (
|
{tabErrorMessage ? (
|
||||||
<div className="alert alert-error">
|
<div className="alert alert-error">
|
||||||
@ -132,115 +132,6 @@ function OverviewGrid({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResultsHeader({
|
|
||||||
activeTab,
|
|
||||||
filterText,
|
|
||||||
hideSpam,
|
|
||||||
spamThreshold,
|
|
||||||
onFilterTextChange,
|
|
||||||
onSetActiveTab,
|
|
||||||
onSetHideSpam,
|
|
||||||
onSetSpamThreshold,
|
|
||||||
}: {
|
|
||||||
activeTab: BacklinksSearchState["tab"];
|
|
||||||
filterText: string;
|
|
||||||
hideSpam: boolean;
|
|
||||||
spamThreshold: number;
|
|
||||||
onFilterTextChange: (value: string) => void;
|
|
||||||
onSetActiveTab: (tab: BacklinksSearchState["tab"]) => void;
|
|
||||||
onSetHideSpam: (hideSpam: boolean) => void;
|
|
||||||
onSetSpamThreshold: (threshold: number) => void;
|
|
||||||
}) {
|
|
||||||
const [draftSpamThreshold, setDraftSpamThreshold] = useState(
|
|
||||||
String(spamThreshold),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDraftSpamThreshold(String(spamThreshold));
|
|
||||||
}, [spamThreshold]);
|
|
||||||
|
|
||||||
function commitSpamThreshold() {
|
|
||||||
onSetSpamThreshold(
|
|
||||||
draftSpamThreshold.trim() === ""
|
|
||||||
? spamThreshold
|
|
||||||
: Number(draftSpamThreshold),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div role="tablist" className="tabs tabs-box w-fit">
|
|
||||||
<TabButton
|
|
||||||
activeTab={activeTab}
|
|
||||||
tab="backlinks"
|
|
||||||
onClick={onSetActiveTab}
|
|
||||||
>
|
|
||||||
Backlinks
|
|
||||||
</TabButton>
|
|
||||||
<TabButton
|
|
||||||
activeTab={activeTab}
|
|
||||||
tab="domains"
|
|
||||||
onClick={onSetActiveTab}
|
|
||||||
>
|
|
||||||
Referring Domains
|
|
||||||
</TabButton>
|
|
||||||
<TabButton activeTab={activeTab} tab="pages" onClick={onSetActiveTab}>
|
|
||||||
Top Pages
|
|
||||||
</TabButton>
|
|
||||||
</div>
|
|
||||||
<p className="max-w-xl text-sm text-base-content/60">
|
|
||||||
{TAB_DESCRIPTIONS[activeTab]}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-end gap-2">
|
|
||||||
<label className="input input-bordered input-sm flex w-full max-w-xs items-center gap-2">
|
|
||||||
<Search className="size-4 text-base-content/60" />
|
|
||||||
<input
|
|
||||||
placeholder="Filter current tab"
|
|
||||||
value={filterText}
|
|
||||||
onChange={(event) => onFilterTextChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{activeTab === "backlinks" ? (
|
|
||||||
<div className="flex items-center gap-3 text-sm">
|
|
||||||
<label className="flex cursor-pointer items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="checkbox checkbox-xs"
|
|
||||||
checked={hideSpam}
|
|
||||||
onChange={(event) => onSetHideSpam(event.target.checked)}
|
|
||||||
/>
|
|
||||||
<span className="text-base-content/70">Hide spam</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 text-base-content/70">
|
|
||||||
<span>Max spam</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={draftSpamThreshold}
|
|
||||||
disabled={!hideSpam}
|
|
||||||
className="input input-bordered input-xs w-20"
|
|
||||||
onBlur={commitSpamThreshold}
|
|
||||||
onChange={(event) => setDraftSpamThreshold(event.target.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
commitSpamThreshold();
|
|
||||||
event.currentTarget.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SummaryStatsGrid({
|
function SummaryStatsGrid({
|
||||||
data,
|
data,
|
||||||
summaryStats,
|
summaryStats,
|
||||||
@ -312,28 +203,6 @@ function TrendCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabButton({
|
|
||||||
activeTab,
|
|
||||||
children,
|
|
||||||
onClick,
|
|
||||||
tab,
|
|
||||||
}: {
|
|
||||||
activeTab: BacklinksSearchState["tab"];
|
|
||||||
children: string;
|
|
||||||
onClick: (tab: BacklinksSearchState["tab"]) => void;
|
|
||||||
tab: BacklinksSearchState["tab"];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
className={`tab ${activeTab === tab ? "tab-active" : ""}`}
|
|
||||||
onClick={() => onClick(tab)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabLoadingState({ label }: { label: string }) {
|
function TabLoadingState({ label }: { label: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 py-2">
|
<div className="space-y-3 py-2">
|
||||||
|
|||||||
162
src/client/features/backlinks/BacklinksResultsHeader.tsx
Normal file
162
src/client/features/backlinks/BacklinksResultsHeader.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Download, Search } from "lucide-react";
|
||||||
|
import type {
|
||||||
|
BacklinksOverviewData,
|
||||||
|
BacklinksSearchState,
|
||||||
|
} from "./backlinksPageTypes";
|
||||||
|
import { TAB_DESCRIPTIONS } from "./backlinksPageUtils";
|
||||||
|
import { exportBacklinksTabCsv } from "./export";
|
||||||
|
|
||||||
|
type BacklinksResultsData = {
|
||||||
|
backlinks: BacklinksOverviewData["backlinks"];
|
||||||
|
referringDomains: BacklinksOverviewData["referringDomains"];
|
||||||
|
topPages: BacklinksOverviewData["topPages"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ResultsHeader({
|
||||||
|
activeTab,
|
||||||
|
filterText,
|
||||||
|
hideSpam,
|
||||||
|
spamThreshold,
|
||||||
|
onFilterTextChange,
|
||||||
|
onSetActiveTab,
|
||||||
|
onSetHideSpam,
|
||||||
|
onSetSpamThreshold,
|
||||||
|
filteredData,
|
||||||
|
exportTarget,
|
||||||
|
}: {
|
||||||
|
activeTab: BacklinksSearchState["tab"];
|
||||||
|
filterText: string;
|
||||||
|
hideSpam: boolean;
|
||||||
|
spamThreshold: number;
|
||||||
|
onFilterTextChange: (value: string) => void;
|
||||||
|
onSetActiveTab: (tab: BacklinksSearchState["tab"]) => void;
|
||||||
|
onSetHideSpam: (hideSpam: boolean) => void;
|
||||||
|
onSetSpamThreshold: (threshold: number) => void;
|
||||||
|
filteredData: BacklinksResultsData;
|
||||||
|
exportTarget: string;
|
||||||
|
}) {
|
||||||
|
const [draftSpamThreshold, setDraftSpamThreshold] = useState(
|
||||||
|
String(spamThreshold),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraftSpamThreshold(String(spamThreshold));
|
||||||
|
}, [spamThreshold]);
|
||||||
|
|
||||||
|
function commitSpamThreshold() {
|
||||||
|
onSetSpamThreshold(
|
||||||
|
draftSpamThreshold.trim() === ""
|
||||||
|
? spamThreshold
|
||||||
|
: Number(draftSpamThreshold),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div role="tablist" className="tabs tabs-box w-fit">
|
||||||
|
<TabButton
|
||||||
|
activeTab={activeTab}
|
||||||
|
tab="backlinks"
|
||||||
|
onClick={onSetActiveTab}
|
||||||
|
>
|
||||||
|
Backlinks
|
||||||
|
</TabButton>
|
||||||
|
<TabButton
|
||||||
|
activeTab={activeTab}
|
||||||
|
tab="domains"
|
||||||
|
onClick={onSetActiveTab}
|
||||||
|
>
|
||||||
|
Referring Domains
|
||||||
|
</TabButton>
|
||||||
|
<TabButton activeTab={activeTab} tab="pages" onClick={onSetActiveTab}>
|
||||||
|
Top Pages
|
||||||
|
</TabButton>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-xl text-sm text-base-content/60">
|
||||||
|
{TAB_DESCRIPTIONS[activeTab]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-ghost w-full max-w-xs justify-start lg:justify-center"
|
||||||
|
onClick={() =>
|
||||||
|
exportBacklinksTabCsv({
|
||||||
|
tab: activeTab,
|
||||||
|
target: exportTarget,
|
||||||
|
rows: filteredData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Download className="size-4" />
|
||||||
|
Export CSV
|
||||||
|
</button>
|
||||||
|
<label className="input input-bordered input-sm flex w-full max-w-xs items-center gap-2">
|
||||||
|
<Search className="size-4 text-base-content/60" />
|
||||||
|
<input
|
||||||
|
placeholder="Filter current tab"
|
||||||
|
value={filterText}
|
||||||
|
onChange={(event) => onFilterTextChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{activeTab === "backlinks" ? (
|
||||||
|
<div className="flex items-center gap-3 text-sm">
|
||||||
|
<label className="flex cursor-pointer items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox checkbox-xs"
|
||||||
|
checked={hideSpam}
|
||||||
|
onChange={(event) => onSetHideSpam(event.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="text-base-content/70">Hide spam</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-base-content/70">
|
||||||
|
<span>Max spam</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={draftSpamThreshold}
|
||||||
|
disabled={!hideSpam}
|
||||||
|
className="input input-bordered input-xs w-20"
|
||||||
|
onBlur={commitSpamThreshold}
|
||||||
|
onChange={(event) => setDraftSpamThreshold(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
commitSpamThreshold();
|
||||||
|
event.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabButton({
|
||||||
|
activeTab,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
tab,
|
||||||
|
}: {
|
||||||
|
activeTab: BacklinksSearchState["tab"];
|
||||||
|
children: string;
|
||||||
|
onClick: (tab: BacklinksSearchState["tab"]) => void;
|
||||||
|
tab: BacklinksSearchState["tab"];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
role="tab"
|
||||||
|
className={`tab ${activeTab === tab ? "tab-active" : ""}`}
|
||||||
|
onClick={() => onClick(tab)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
src/client/features/backlinks/export.test.ts
Normal file
93
src/client/features/backlinks/export.test.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildBacklinksTabCsvFile } from "./export";
|
||||||
|
|
||||||
|
describe("buildBacklinksTabCsvFile", () => {
|
||||||
|
it("builds backlinks csv with backlink-specific columns", () => {
|
||||||
|
const file = buildBacklinksTabCsvFile({
|
||||||
|
tab: "backlinks",
|
||||||
|
target: "https://Example.com/path?q=1",
|
||||||
|
rows: {
|
||||||
|
backlinks: [
|
||||||
|
{
|
||||||
|
domainFrom: "example.org",
|
||||||
|
urlFrom: "https://example.org/post",
|
||||||
|
urlTo: "https://example.com/path",
|
||||||
|
anchor: "Example",
|
||||||
|
itemType: "organic",
|
||||||
|
isDofollow: true,
|
||||||
|
relAttributes: ["noopener", "noreferrer"],
|
||||||
|
rank: 123,
|
||||||
|
domainFromRank: 45,
|
||||||
|
pageFromRank: 12,
|
||||||
|
spamScore: 10,
|
||||||
|
firstSeen: "2025-01-01",
|
||||||
|
lastSeen: "2025-01-15",
|
||||||
|
isLost: false,
|
||||||
|
isBroken: false,
|
||||||
|
linksCount: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
referringDomains: [],
|
||||||
|
topPages: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(file.filename).toBe("backlinks-backlinks-example.com-path-q-1.csv");
|
||||||
|
expect(file.content).toContain('"Domain","Source URL","Target URL"');
|
||||||
|
expect(file.content).toContain('"example.org"');
|
||||||
|
expect(file.content).toContain('"noopener, noreferrer"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds referring domains csv", () => {
|
||||||
|
const file = buildBacklinksTabCsvFile({
|
||||||
|
tab: "domains",
|
||||||
|
target: "Example.com",
|
||||||
|
rows: {
|
||||||
|
backlinks: [],
|
||||||
|
referringDomains: [
|
||||||
|
{
|
||||||
|
domain: "source.com",
|
||||||
|
backlinks: 12,
|
||||||
|
referringPages: 7,
|
||||||
|
rank: 101,
|
||||||
|
spamScore: 4,
|
||||||
|
firstSeen: "2024-05-10",
|
||||||
|
brokenBacklinks: 1,
|
||||||
|
brokenPages: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
topPages: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(file.filename).toBe("backlinks-referring-domains-example.com.csv");
|
||||||
|
expect(file.content).toContain('"Domain","Backlinks","Referring Pages"');
|
||||||
|
expect(file.content).toContain('"source.com"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds top pages csv", () => {
|
||||||
|
const file = buildBacklinksTabCsvFile({
|
||||||
|
tab: "pages",
|
||||||
|
target: "docs.example.com",
|
||||||
|
rows: {
|
||||||
|
backlinks: [],
|
||||||
|
referringDomains: [],
|
||||||
|
topPages: [
|
||||||
|
{
|
||||||
|
page: "https://docs.example.com/start",
|
||||||
|
backlinks: 22,
|
||||||
|
referringDomains: 9,
|
||||||
|
rank: 88,
|
||||||
|
brokenBacklinks: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(file.filename).toBe("backlinks-top-pages-docs.example.com.csv");
|
||||||
|
expect(file.content).toContain(
|
||||||
|
'"Page","Backlinks","Referring Domains","Rank","Broken Backlinks"',
|
||||||
|
);
|
||||||
|
expect(file.content).toContain('"https://docs.example.com/start"');
|
||||||
|
});
|
||||||
|
});
|
||||||
132
src/client/features/backlinks/export.ts
Normal file
132
src/client/features/backlinks/export.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { buildCsv, downloadCsv } from "@/client/lib/csv";
|
||||||
|
import type {
|
||||||
|
BacklinksOverviewData,
|
||||||
|
BacklinksSearchState,
|
||||||
|
} from "./backlinksPageTypes";
|
||||||
|
|
||||||
|
type BacklinksFilteredData = {
|
||||||
|
backlinks: BacklinksOverviewData["backlinks"];
|
||||||
|
referringDomains: BacklinksOverviewData["referringDomains"];
|
||||||
|
topPages: BacklinksOverviewData["topPages"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildBacklinksTabCsvFile(args: {
|
||||||
|
tab: BacklinksSearchState["tab"];
|
||||||
|
target: string;
|
||||||
|
rows: BacklinksFilteredData;
|
||||||
|
}) {
|
||||||
|
const { tab, target, rows } = args;
|
||||||
|
|
||||||
|
if (tab === "backlinks") {
|
||||||
|
const headers = [
|
||||||
|
"Domain",
|
||||||
|
"Source URL",
|
||||||
|
"Target URL",
|
||||||
|
"Anchor",
|
||||||
|
"Type",
|
||||||
|
"Dofollow",
|
||||||
|
"Rel Attributes",
|
||||||
|
"Domain Rank",
|
||||||
|
"Source Page Rank",
|
||||||
|
"Target Rank",
|
||||||
|
"Spam Score",
|
||||||
|
"First Seen",
|
||||||
|
"Last Seen",
|
||||||
|
"Lost",
|
||||||
|
"Broken",
|
||||||
|
"Links Count",
|
||||||
|
];
|
||||||
|
const lines = rows.backlinks.map((row) => [
|
||||||
|
row.domainFrom,
|
||||||
|
row.urlFrom,
|
||||||
|
row.urlTo,
|
||||||
|
row.anchor,
|
||||||
|
row.itemType,
|
||||||
|
row.isDofollow,
|
||||||
|
row.relAttributes.join(", "),
|
||||||
|
row.domainFromRank,
|
||||||
|
row.pageFromRank,
|
||||||
|
row.rank,
|
||||||
|
row.spamScore,
|
||||||
|
row.firstSeen,
|
||||||
|
row.lastSeen,
|
||||||
|
row.isLost,
|
||||||
|
row.isBroken,
|
||||||
|
row.linksCount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: buildFilename("backlinks", target),
|
||||||
|
content: buildCsv(headers, lines),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab === "domains") {
|
||||||
|
const headers = [
|
||||||
|
"Domain",
|
||||||
|
"Backlinks",
|
||||||
|
"Referring Pages",
|
||||||
|
"Rank",
|
||||||
|
"Spam Score",
|
||||||
|
"First Seen",
|
||||||
|
"Broken Backlinks",
|
||||||
|
"Broken Pages",
|
||||||
|
];
|
||||||
|
const lines = rows.referringDomains.map((row) => [
|
||||||
|
row.domain,
|
||||||
|
row.backlinks,
|
||||||
|
row.referringPages,
|
||||||
|
row.rank,
|
||||||
|
row.spamScore,
|
||||||
|
row.firstSeen,
|
||||||
|
row.brokenBacklinks,
|
||||||
|
row.brokenPages,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: buildFilename("referring-domains", target),
|
||||||
|
content: buildCsv(headers, lines),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
"Page",
|
||||||
|
"Backlinks",
|
||||||
|
"Referring Domains",
|
||||||
|
"Rank",
|
||||||
|
"Broken Backlinks",
|
||||||
|
];
|
||||||
|
const lines = rows.topPages.map((row) => [
|
||||||
|
row.page,
|
||||||
|
row.backlinks,
|
||||||
|
row.referringDomains,
|
||||||
|
row.rank,
|
||||||
|
row.brokenBacklinks,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: buildFilename("top-pages", target),
|
||||||
|
content: buildCsv(headers, lines),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportBacklinksTabCsv(args: {
|
||||||
|
tab: BacklinksSearchState["tab"];
|
||||||
|
target: string;
|
||||||
|
rows: BacklinksFilteredData;
|
||||||
|
}) {
|
||||||
|
const file = buildBacklinksTabCsvFile(args);
|
||||||
|
downloadCsv(file.filename, file.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilename(tabPrefix: string, target: string) {
|
||||||
|
const normalizedTarget = target
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/https?:\/\//g, "")
|
||||||
|
.replace(/[^a-z0-9.-]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "")
|
||||||
|
.slice(0, 80);
|
||||||
|
|
||||||
|
return `backlinks-${tabPrefix}${normalizedTarget ? `-${normalizedTarget}` : ""}.csv`;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user