* docs: move project docs into docs directory * docs: rebrand landing site as OpenSEO * fix: update remaining OpenSEO backlinks links * Change landing page copy + format * website: add www custom domain route * Tweak README * save
236 lines
7.2 KiB
TypeScript
236 lines
7.2 KiB
TypeScript
import {
|
|
Link2,
|
|
ShieldAlert,
|
|
Sparkles,
|
|
TrendingUp,
|
|
Wrench,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import type { BacklinksAccessStatusData } from "./backlinksPageTypes";
|
|
import { formatRelativeTimestamp } from "./backlinksPageUtils";
|
|
|
|
export function BacklinksAccessLoadingState() {
|
|
return (
|
|
<div className="card bg-base-100 border border-base-300">
|
|
<div className="card-body gap-4">
|
|
<div className="skeleton h-6 w-48" />
|
|
<div className="skeleton h-4 w-full" />
|
|
<div className="skeleton h-4 w-4/5" />
|
|
<div className="skeleton h-10 w-48" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function BacklinksSetupGate({
|
|
status,
|
|
isTesting,
|
|
testError,
|
|
onTest,
|
|
}: {
|
|
status: BacklinksAccessStatusData | undefined;
|
|
isTesting: boolean;
|
|
testError: string | null;
|
|
onTest: () => void;
|
|
}) {
|
|
return (
|
|
<section>
|
|
<div className="rounded-2xl border border-base-300 bg-base-100 p-6 md:p-7 space-y-5">
|
|
<div className="flex items-start gap-3">
|
|
<div className="rounded-xl bg-warning/15 p-2.5 text-warning shrink-0">
|
|
<Wrench className="size-5" />
|
|
</div>
|
|
<div className="max-w-3xl space-y-1.5">
|
|
<h2 className="text-xl font-semibold">Enable Backlinks</h2>
|
|
<p className="text-sm text-base-content/68">
|
|
Backlinks is not enabled for your DataForSEO account yet. Turn it
|
|
on in DataForSEO, then test access here.
|
|
</p>
|
|
<p className="text-xs text-base-content/50">
|
|
DataForSEO offers a free 14-day trial for Backlinks. Then, it's
|
|
$100/month. We're gauging interest in building out a lower-cost
|
|
alternative, <InlineMailingListLink /> if you're interested.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={onTest}
|
|
disabled={isTesting}
|
|
>
|
|
{isTesting ? "Confirming..." : "Confirm Backlinks Access"}
|
|
</button>
|
|
<a
|
|
className="btn btn-outline"
|
|
href="https://app.dataforseo.com/api-access-subscriptions"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Open DataForSEO Backlinks
|
|
</a>
|
|
</div>
|
|
|
|
<BacklinksSetupFeedback status={status} testError={testError} />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function BacklinksEmptyState() {
|
|
return (
|
|
<section className="rounded-2xl border border-dashed border-base-300 bg-base-100/70 p-8 text-center space-y-3">
|
|
<div className="mx-auto flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
|
<Link2 className="size-6" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<p className="text-lg font-medium">Start with a domain or page URL</p>
|
|
<p className="mx-auto max-w-2xl text-sm text-base-content/65">
|
|
Keep backlink research simple: see who links to you, check what pages
|
|
attract links, and spot recent wins or losses without getting buried
|
|
in enterprise SEO dashboards.
|
|
</p>
|
|
</div>
|
|
<div className="grid gap-3 pt-2 text-left md:grid-cols-3">
|
|
<BeginnerCard
|
|
icon={TrendingUp}
|
|
title="Check link health"
|
|
body="Use the overview to see backlink totals, referring domains, and broken links worth fixing."
|
|
/>
|
|
<BeginnerCard
|
|
icon={Sparkles}
|
|
title="Review real links"
|
|
body="Start with the backlinks tab to see the pages linking to you and the strongest sources first."
|
|
/>
|
|
<BeginnerCard
|
|
icon={ShieldAlert}
|
|
title="Find easy actions"
|
|
body="Look for broken targets, recently lost links, and your most-linked pages to decide what to do next."
|
|
/>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function BacklinksLoadingState() {
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
{Array.from({ length: 8 }).map((_, index) => (
|
|
<div key={index} className="card bg-base-100 border border-base-300">
|
|
<div className="card-body gap-3 p-4">
|
|
<div className="skeleton h-3 w-24" />
|
|
<div className="skeleton h-8 w-28" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
|
{Array.from({ length: 2 }).map((_, index) => (
|
|
<div key={index} className="card bg-base-100 border border-base-300">
|
|
<div className="card-body gap-3">
|
|
<div className="skeleton h-4 w-32" />
|
|
<div className="skeleton h-64 w-full" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="card bg-base-100 border border-base-300">
|
|
<div className="card-body gap-3">
|
|
<div className="skeleton h-8 w-60" />
|
|
<div className="skeleton h-80 w-full" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function BacklinksErrorState({
|
|
errorMessage,
|
|
onRetry,
|
|
}: {
|
|
errorMessage: string | null;
|
|
onRetry: () => void;
|
|
}) {
|
|
return (
|
|
<section className="rounded-2xl border border-error/30 bg-error/5 p-6 space-y-3">
|
|
<div className="flex items-start gap-3">
|
|
<div className="rounded-xl bg-error/10 p-2.5 text-error shrink-0">
|
|
<ShieldAlert className="size-5" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<h2 className="text-lg font-semibold">Could not load backlinks</h2>
|
|
<p className="text-sm text-base-content/70">
|
|
{errorMessage ?? "Please try again in a moment."}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button className="btn btn-outline btn-sm" onClick={onRetry}>
|
|
Retry
|
|
</button>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function BacklinksSetupFeedback({
|
|
status,
|
|
testError,
|
|
}: {
|
|
status: BacklinksAccessStatusData | undefined;
|
|
testError: string | null;
|
|
}) {
|
|
return (
|
|
<div className="space-y-3">
|
|
{status?.lastCheckedAt ? (
|
|
<div className="text-sm text-base-content/60">
|
|
Last checked {formatRelativeTimestamp(status.lastCheckedAt)}.
|
|
</div>
|
|
) : null}
|
|
{status?.lastErrorMessage ? (
|
|
<div className="alert alert-warning">
|
|
<ShieldAlert className="size-4 shrink-0" />
|
|
<span>{status.lastErrorMessage}</span>
|
|
</div>
|
|
) : null}
|
|
{testError ? (
|
|
<div className="alert alert-error">
|
|
<ShieldAlert className="size-4 shrink-0" />
|
|
<span>{testError}</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BeginnerCard({
|
|
icon: Icon,
|
|
title,
|
|
body,
|
|
}: {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
body: string;
|
|
}) {
|
|
return (
|
|
<div className="rounded-xl border border-base-300 bg-base-100 p-4 space-y-2">
|
|
<Icon className="size-4 text-primary" />
|
|
<p className="font-medium">{title}</p>
|
|
<p className="text-sm text-base-content/65">{body}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InlineMailingListLink() {
|
|
return (
|
|
<a
|
|
className="underline underline-offset-2 hover:text-base-content/70"
|
|
href="https://openseo.so"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
join the OpenSEO mailing list
|
|
</a>
|
|
);
|
|
}
|