Free backlink checker at /backlink-checker (#444)

This commit is contained in:
Ben Senescu 2026-08-01 13:50:50 -04:00 committed by GitHub
parent 145324138e
commit b8253fb083
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1033 additions and 36 deletions

View File

@ -10,6 +10,7 @@ data, or sensitive paths.
## Open ## Open
- [ ] `2026-08-01T16:28:36Z``claude` — web's pinned wrangler 4.71.0 fails `kv namespace create` with a bare "Authentication error [code: 10000]" even though the OAuth token has workers_kv write scope; wrangler@4.118.0 succeeds with identical auth. Fix: bump wrangler in web/package.json.
- [ ] `2026-07-20T20:08:28Z``claude` — In a fresh git worktree, `oxlint --type-aware` crashes with `Cannot find module '@oxlint/binding-darwin-arm64'` — the platform-specific optional dep is missing from the worktree's node_modules while tsc/prettier work fine, and plain `pnpm install` reports up-to-date without restoring it; `pnpm install --force` (~22s) fixes it. Worth making the worktree-setup hook (or a documented step) run the forced install so lint doesn't die on fresh worktrees. - [ ] `2026-07-20T20:08:28Z``claude` — In a fresh git worktree, `oxlint --type-aware` crashes with `Cannot find module '@oxlint/binding-darwin-arm64'` — the platform-specific optional dep is missing from the worktree's node_modules while tsc/prettier work fine, and plain `pnpm install` reports up-to-date without restoring it; `pnpm install --force` (~22s) fixes it. Worth making the worktree-setup hook (or a documented step) run the forced install so lint doesn't die on fresh worktrees.
- [ ] `2026-07-19T04:06:52Z``codex``pnpm --dir web build` fails with `vite: command not found` when `web/node_modules` is absent, despite the root toolchain being installed. Document or enforce the package-local install required before validating the `web/` subpackage. - [ ] `2026-07-19T04:06:52Z``codex``pnpm --dir web build` fails with `vite: command not found` when `web/node_modules` is absent, despite the root toolchain being installed. Document or enforce the package-local install required before validating the `web/` subpackage.
- [ ] `2026-07-19T02:55:56Z``claude` — Adding a docs folder under `web/content/docs` whose `meta.json` lists an `[Overview](...)` link renders a duplicated, double-highlighted sidebar entry, because the folder-index strip in `web/src/lib/source.ts` (`transformPageTree.folder`) is a per-folder-name allowlist. Derive it from the meta convention (or strip the index for all folders) so new sections don't need a hidden source.ts edit. - [ ] `2026-07-19T02:55:56Z``claude` — Adding a docs folder under `web/content/docs` whose `meta.json` lists an `[Overview](...)` link renders a duplicated, double-highlighted sidebar entry, because the folder-index strip in `web/src/lib/source.ts` (`transformPageTree.folder`) is a per-folder-name allowlist. Derive it from the meta convention (or strip the index for all folders) so new sections don't need a hidden source.ts edit.

8
web/.env.example Normal file
View File

@ -0,0 +1,8 @@
# Build-time vars (vite build inlines these).
VITE_TURNSTILE_SITE_KEY=
# Runtime secrets — set with `wrangler secret put <NAME>`, not here:
# DATAFORSEO_API_KEY base64("login:password") for api.dataforseo.com
# TURNSTILE_SECRET_KEY pairs with VITE_TURNSTILE_SITE_KEY; deploy the build
# containing the site key before setting this secret
# LOOPS_API_KEY newsletter subscribe endpoint

1
web/.gitignore vendored
View File

@ -7,3 +7,4 @@ dist
.source .source
source.generated.ts source.generated.ts
*.local *.local
.dev.vars*

View File

@ -15,7 +15,7 @@
"format:write": "prettier --write .", "format:write": "prettier --write .",
"deploy": "npm run build && wrangler deploy", "deploy": "npm run build && wrangler deploy",
"deploy:prod": "npm run build && wrangler deploy", "deploy:prod": "npm run build && wrangler deploy",
"deploy:preview": "npm run build && wrangler deploy --env preview" "deploy:preview": "CLOUDFLARE_ENV=preview npm run build && wrangler deploy"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-router": "^1.168.10", "@tanstack/react-router": "^1.168.10",

BIN
web/public/social-card.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

View File

@ -25,6 +25,7 @@ const STATIC_PATHS = [
"/docs", "/docs",
"/features", "/features",
"/features/mcp", "/features/mcp",
"/backlink-checker",
"/open-source-seo", "/open-source-seo",
"/google-search-console-mcp", "/google-search-console-mcp",
...Object.values(FEATURE_PAGE_SLUGS).map((slug) => `/features/${slug}`), ...Object.values(FEATURE_PAGE_SLUGS).map((slug) => `/features/${slug}`),

View File

@ -0,0 +1,396 @@
import { type FormEvent, useEffect, useRef, useState } from "react";
// Public site key. Cloudflare's always-passing test key is the dev-only
// fallback; a prod build without VITE_TURNSTILE_SITE_KEY renders no widget.
const TURNSTILE_SITE_KEY =
(import.meta.env.VITE_TURNSTILE_SITE_KEY as string | undefined) ||
(import.meta.env.DEV ? "1x00000000000000000000AA" : "");
type TurnstileApi = {
render(
container: HTMLElement,
options: {
sitekey: string;
appearance?: string;
callback: (token: string) => void;
"expired-callback"?: () => void;
},
): string;
reset(widgetId?: string): void;
};
type BacklinkRow = {
domainFrom: string | null;
urlFrom: string | null;
urlTo: string | null;
pageTitle: string | null;
anchor: string | null;
dofollow: boolean | null;
domainRank: number | null;
};
type CheckResult = {
target: string;
summary: {
rank: number | null;
backlinks: number | null;
referringDomains: number | null;
brokenBacklinks: number | null;
};
topBacklinks: BacklinkRow[];
};
function formatCount(value: number | null): string {
return typeof value === "number" ? value.toLocaleString("en-US") : "—";
}
/** Small info icon that reveals an explanation on hover or focus. */
function InfoTip({
tip,
align = "center",
}: {
tip: string;
align?: "center" | "right";
}) {
return (
<span className="group relative inline-flex align-middle">
<button
type="button"
aria-label={tip}
className="ml-1 inline-flex h-3.5 w-3.5 items-center justify-center rounded-full text-neutral-400 hover:text-neutral-700 focus:text-neutral-700 focus:outline-none"
>
<svg
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
className="h-3.5 w-3.5"
>
<circle cx="8" cy="8" r="6.5" stroke="currentColor" />
<path
d="M8 7.25v3.25M8 5.25v.1"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
/>
</svg>
</button>
<span
role="tooltip"
aria-hidden="true"
className={[
"pointer-events-none absolute top-full z-10 mt-1.5 w-56 rounded-lg bg-neutral-950 px-3 py-2 text-left text-xs font-normal normal-case leading-5 text-white opacity-0 shadow-lg transition-opacity group-focus-within:opacity-100 group-hover:opacity-100",
align === "right" ? "right-0" : "left-1/2 -translate-x-1/2",
].join(" ")}
>
{tip}
</span>
</span>
);
}
export function BacklinkCheckerTool() {
const [target, setTarget] = useState("");
const [status, setStatus] = useState<"idle" | "loading" | "done" | "error">(
"idle",
);
const [errorMessage, setErrorMessage] = useState("");
const [result, setResult] = useState<CheckResult | null>(null);
const widgetContainerRef = useRef<HTMLDivElement>(null);
const widgetIdRef = useRef<string | null>(null);
const tokenRef = useRef("");
useEffect(() => {
if (!TURNSTILE_SITE_KEY) return;
const w = window as unknown as {
turnstile?: TurnstileApi;
onloadTurnstileCallback?: () => void;
};
const renderWidget = () => {
if (!w.turnstile || !widgetContainerRef.current) return;
if (widgetIdRef.current !== null) return;
widgetIdRef.current = w.turnstile.render(widgetContainerRef.current, {
sitekey: TURNSTILE_SITE_KEY,
// Invisible unless Turnstile decides the visitor needs a challenge.
appearance: "interaction-only",
callback: (token) => {
tokenRef.current = token;
},
"expired-callback": () => {
tokenRef.current = "";
},
});
};
if (w.turnstile) {
renderWidget();
return;
}
w.onloadTurnstileCallback = renderWidget;
if (!document.querySelector("script[data-turnstile]")) {
const script = document.createElement("script");
script.src =
"https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback";
script.async = true;
script.dataset.turnstile = "true";
document.head.appendChild(script);
}
}, []);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setStatus("loading");
setErrorMessage("");
try {
const res = await fetch("/api/backlink-check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
target,
turnstileToken: tokenRef.current || undefined,
}),
});
const data = (await res.json().catch(() => ({}))) as
| CheckResult
| { error?: string };
if (!res.ok) {
throw new Error(
(data as { error?: string }).error || "Something went wrong",
);
}
setResult(data as CheckResult);
setStatus("done");
} catch (err) {
setStatus("error");
setErrorMessage(
err instanceof Error ? err.message : "Something went wrong",
);
} finally {
// Tokens are single-use; get a fresh one for the next check.
tokenRef.current = "";
const w = window as unknown as { turnstile?: TurnstileApi };
if (w.turnstile && widgetIdRef.current !== null) {
w.turnstile.reset(widgetIdRef.current);
}
}
};
return (
<div>
<form
onSubmit={handleSubmit}
className="rounded-xl border border-[var(--color-border-subtle)] bg-white p-4 md:p-5"
>
<div className="flex flex-col gap-2 sm:flex-row">
<label htmlFor="backlink-target" className="sr-only">
Domain to check
</label>
<input
id="backlink-target"
name="target"
type="text"
inputMode="url"
autoComplete="off"
spellCheck={false}
required
value={target}
onChange={(e) => setTarget(e.target.value)}
placeholder="example.com"
disabled={status === "loading"}
className="h-11 min-w-0 flex-1 rounded-lg border border-[var(--color-border-subtle)] bg-white px-3.5 text-base text-neutral-900 placeholder:text-neutral-500 transition focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900"
/>
<button
type="submit"
disabled={status === "loading"}
className="h-11 shrink-0 rounded-lg bg-neutral-950 px-6 text-sm font-medium text-white transition-colors hover:bg-neutral-800 disabled:opacity-50"
>
{status === "loading" ? "Checking…" : "Check backlinks"}
</button>
</div>
<div ref={widgetContainerRef} className="empty:hidden" />
<p className="mt-2.5 text-xs text-[var(--color-brand-muted)]">
Free &middot; No signup &middot; Instant results
</p>
{status === "error" && (
<p className="mt-2 text-sm text-red-600">{errorMessage}</p>
)}
</form>
{status === "done" && result ? <CheckResults result={result} /> : null}
</div>
);
}
function CheckResults({ result }: { result: CheckResult }) {
const { summary, topBacklinks } = result;
const total = summary.backlinks;
const hasMore = typeof total === "number" && total > topBacklinks.length;
const metrics = [
{
label: "Domain rank",
value: formatCount(summary.rank),
tip: "DataForSEO's 0-100 strength score for a domain's link profile. Similar idea to Ahrefs DR or Moz DA, but each tool uses its own index and formula, so numbers differ between tools.",
},
{
label: "Backlinks",
value: formatCount(summary.backlinks),
tip: "Total individual links pointing at this domain, counting multiple links from the same website.",
},
{
label: "Referring domains",
value: formatCount(summary.referringDomains),
tip: "Unique websites that link to this domain at least once.",
},
{
label: "Broken backlinks",
value: formatCount(summary.brokenBacklinks),
tip: "Links pointing at pages on this domain that no longer load, such as deleted pages returning 404.",
},
];
return (
<div className="mt-6">
<h2 className="text-lg font-semibold tracking-tight text-neutral-950">
Backlink profile for{" "}
<span className="text-[var(--color-brand-accent)]">
{result.target}
</span>
</h2>
<dl className="mt-4 grid grid-cols-2 overflow-hidden rounded-lg border border-[var(--color-border-subtle)] bg-white md:grid-cols-4">
{metrics.map((metric, index) => (
<div
key={metric.label}
className={[
"p-4 md:p-5",
index % 2 === 1 && "border-l border-[var(--color-border-subtle)]",
index > 1 && "border-t border-[var(--color-border-subtle)]",
index > 0 && "md:border-l md:border-t-0",
]
.filter(Boolean)
.join(" ")}
>
<dt className="text-xs text-[var(--color-brand-muted)]">
{metric.label}
<InfoTip
tip={metric.tip}
align={index > 1 ? "right" : "center"}
/>
</dt>
<dd className="mt-1 text-xl font-semibold tabular-nums text-neutral-950">
{metric.value}
</dd>
</div>
))}
</dl>
{topBacklinks.length > 0 ? (
<div className="mt-4 overflow-x-auto rounded-lg border border-[var(--color-border-subtle)] bg-white">
<table className="w-full min-w-[720px] text-left text-sm">
<thead>
<tr className="border-b border-[var(--color-border-subtle)] text-xs text-[var(--color-brand-muted)]">
<th className="px-4 py-3 font-medium">
Rank
<InfoTip tip="Strength (0-100) of the linking website's own link profile. Links from higher-rank domains generally carry more weight." />
</th>
<th className="px-4 py-3 font-medium">Referring page</th>
<th className="px-4 py-3 font-medium">
Anchor and target
<InfoTip tip="The clickable text of the link, and the page on this domain the link points to." />
</th>
<th className="px-4 py-3 font-medium">
Type
<InfoTip
tip="Follow links can pass ranking value to the target. Nofollow links ask search engines not to count them."
align="right"
/>
</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--color-border-subtle)]">
{topBacklinks.map((row) => (
<tr key={row.urlFrom ?? row.domainFrom ?? ""}>
<td className="px-4 py-3 align-top tabular-nums text-neutral-950">
{formatCount(row.domainRank)}
</td>
<td className="max-w-[300px] px-4 py-3 align-top">
<p className="truncate font-medium text-neutral-950">
{row.pageTitle ?? row.domainFrom ?? "—"}
</p>
{row.urlFrom ? (
<a
href={row.urlFrom}
target="_blank"
rel="nofollow noopener noreferrer"
className="block truncate text-xs text-[var(--color-brand-muted)] hover:text-neutral-900 hover:underline"
>
{row.urlFrom}
</a>
) : null}
</td>
<td className="max-w-[260px] px-4 py-3 align-top">
<p className="truncate text-neutral-700">
{row.anchor ?? "—"}
</p>
{row.urlTo ? (
<p className="truncate text-xs text-[var(--color-brand-muted)]">
{row.urlTo}
</p>
) : null}
</td>
<td className="px-4 py-3 align-top">
<span
className={
row.dofollow
? "rounded-full border border-[var(--color-border-subtle)] px-2 py-0.5 text-xs font-medium text-neutral-900"
: "rounded-full px-2 py-0.5 text-xs text-neutral-500"
}
>
{row.dofollow ? "Follow" : "Nofollow"}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="mt-4 rounded-lg border border-[var(--color-border-subtle)] bg-white p-5 text-sm text-neutral-700">
No live backlinks found for this domain in the index yet.
</p>
)}
<div className="mt-4 rounded-xl border border-[var(--color-border-subtle)] bg-white p-5 md:p-6">
<p className="text-sm leading-6 text-neutral-700">
{hasMore ? (
<>
Showing the top {topBacklinks.length} backlinks, one per referring
domain, strongest domains first.{" "}
<span className="font-medium text-neutral-950">
{formatCount(total)} total backlinks
</span>{" "}
are in the index for this domain.
</>
) : (
<>
Explore the full picture: referring domains, anchors, new and lost
links, and spam signals.
</>
)}
</p>
<div className="mt-3">
<a
href="https://app.openseo.so/sign-up"
className="inline-flex h-10 items-center justify-center rounded-lg bg-neutral-950 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
>
Explore the full profile free
<span aria-hidden="true" className="ml-2">
&rarr;
</span>
</a>
</div>
</div>
</div>
);
}

View File

@ -304,7 +304,7 @@ const FEATURE_CARDS = [
blurb: "Estimate organic traffic and ranking keywords.", blurb: "Estimate organic traffic and ranking keywords.",
}, },
{ {
page: featurePages.backlinkChecker, page: featurePages.backlinks,
blurb: "Inspect backlinks, referring domains, and link quality.", blurb: "Inspect backlinks, referring domains, and link quality.",
}, },
{ {

View File

@ -1,7 +1,7 @@
export const FEATURE_PAGE_SLUGS = { export const FEATURE_PAGE_SLUGS = {
keywordResearch: "keyword-research", keywordResearch: "keyword-research",
siteAudit: "site-audit", siteAudit: "site-audit",
backlinkChecker: "backlink-checker", backlinks: "backlinks",
domainOverview: "domain-overview", domainOverview: "domain-overview",
rankTracking: "rank-tracking", rankTracking: "rank-tracking",
savedKeywords: "saved-keywords", savedKeywords: "saved-keywords",

View File

@ -208,7 +208,7 @@ export const featurePages = {
], ],
related: [ related: [
{ label: "Domain Overview", href: "/features/domain-overview" }, { label: "Domain Overview", href: "/features/domain-overview" },
{ label: "Backlink Checker", href: "/features/backlink-checker" }, { label: "Backlinks", href: "/features/backlinks" },
{ label: "Keyword Research", href: "/features/keyword-research" }, { label: "Keyword Research", href: "/features/keyword-research" },
], ],
faqs: [ faqs: [
@ -229,18 +229,18 @@ export const featurePages = {
}, },
], ],
}, },
backlinkChecker: { backlinks: {
slug: FEATURE_PAGE_SLUGS.backlinkChecker, slug: FEATURE_PAGE_SLUGS.backlinks,
eyebrow: "Backlinks", eyebrow: "Backlinks",
navDescription: "Check links and referring domains.", navDescription: "Check links and referring domains.",
title: "Backlink checker for understanding a domain's link profile", title: "Backlink analysis for understanding a domain's link profile",
description: description:
"Analyze backlinks, referring domains, and linked pages without separating link research from the rest of your SEO workspace.", "Analyze backlinks, referring domains, and linked pages without separating link research from the rest of your SEO workspace.",
primaryKeyword: "backlink checker", primaryKeyword: "backlink analysis",
secondaryKeywords: [ secondaryKeywords: [
"free backlink checker",
"backlink analysis tool", "backlink analysis tool",
"google backlink checker", "referring domains",
"link profile",
], ],
imageAlt: "OpenSEO backlinks report", imageAlt: "OpenSEO backlinks report",
imageSrc: imageSrc:
@ -280,18 +280,18 @@ export const featurePages = {
"MCP support lets an AI agent pull backlink context during SEO research.", "MCP support lets an AI agent pull backlink context during SEO research.",
], ],
related: [ related: [
{ label: "Free Backlink Checker", href: "/backlink-checker" },
{ {
label: "Link Prospecting", label: "Link Prospecting",
href: "/docs/skills/link-prospecting", href: "/docs/skills/link-prospecting",
}, },
{ label: "Domain Overview", href: "/features/domain-overview" }, { label: "Domain Overview", href: "/features/domain-overview" },
{ label: "OpenSEO MCP", href: "/features/mcp" },
], ],
faqs: [ faqs: [
{ {
question: "What is a backlink checker used for?", question: "What is backlink analysis used for?",
answer: answer:
"A backlink checker helps you understand which sites link to a domain or page, which links have stronger rank, spam, broken, lost, or nofollow signals, and where competitors are earning authority.", "Backlink analysis helps you understand which sites link to a domain or page, which links have stronger rank, spam, broken, lost, or nofollow signals, and where competitors are earning authority.",
}, },
{ {
question: "Can I check competitor backlinks in OpenSEO?", question: "Can I check competitor backlinks in OpenSEO?",
@ -361,7 +361,7 @@ export const featurePages = {
href: "/docs/skills/competitor-analysis", href: "/docs/skills/competitor-analysis",
}, },
{ label: "Keyword Research", href: "/features/keyword-research" }, { label: "Keyword Research", href: "/features/keyword-research" },
{ label: "Backlink Checker", href: "/features/backlink-checker" }, { label: "Backlinks", href: "/features/backlinks" },
], ],
faqs: [ faqs: [
{ {
@ -695,7 +695,7 @@ export const featureGroups = [
description: "Understand competitors, backlinks, and technical health.", description: "Understand competitors, backlinks, and technical health.",
pages: [ pages: [
featurePages.domainOverview, featurePages.domainOverview,
featurePages.backlinkChecker, featurePages.backlinks,
featurePages.siteAudit, featurePages.siteAudit,
], ],
}, },

View File

@ -1,5 +1,5 @@
const DEFAULT_SITE_URL = "https://openseo.so"; const DEFAULT_SITE_URL = "https://openseo.so";
const DEFAULT_SOCIAL_IMAGE_PATH = "/social-card.png"; const DEFAULT_SOCIAL_IMAGE_PATH = "/social-card.jpg";
const DEFAULT_SOCIAL_IMAGE_ALT = "OpenSEO product preview"; const DEFAULT_SOCIAL_IMAGE_ALT = "OpenSEO product preview";
export const SITE_URL = ( export const SITE_URL = (
@ -53,7 +53,7 @@ export function buildPageSeo({
{ property: "og:url", content: canonicalUrl }, { property: "og:url", content: canonicalUrl },
{ property: "og:image", content: socialImageUrl }, { property: "og:image", content: socialImageUrl },
{ property: "og:image:alt", content: imageAlt }, { property: "og:image:alt", content: imageAlt },
{ property: "og:image:type", content: "image/png" }, { property: "og:image:type", content: "image/jpeg" },
{ property: "og:image:width", content: "1200" }, { property: "og:image:width", content: "1200" },
{ property: "og:image:height", content: "630" }, { property: "og:image:height", content: "630" },
{ name: "twitter:card", content: "summary_large_image" }, { name: "twitter:card", content: "summary_large_image" },

View File

@ -22,9 +22,11 @@ import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as BlogsSplatRouteImport } from './routes/blogs/$' import { Route as BlogsSplatRouteImport } from './routes/blogs/$'
import { Route as ApiSubscribeRouteImport } from './routes/api/subscribe' import { Route as ApiSubscribeRouteImport } from './routes/api/subscribe'
import { Route as ApiEventRouteImport } from './routes/api/event' import { Route as ApiEventRouteImport } from './routes/api/event'
import { Route as ApiBacklinkCheckRouteImport } from './routes/api/backlink-check'
import { Route as MarketingPricingRouteImport } from './routes/_marketing/pricing' import { Route as MarketingPricingRouteImport } from './routes/_marketing/pricing'
import { Route as MarketingOpenSourceSeoRouteImport } from './routes/_marketing/open-source-seo' import { Route as MarketingOpenSourceSeoRouteImport } from './routes/_marketing/open-source-seo'
import { Route as MarketingGoogleSearchConsoleMcpRouteImport } from './routes/_marketing/google-search-console-mcp' import { Route as MarketingGoogleSearchConsoleMcpRouteImport } from './routes/_marketing/google-search-console-mcp'
import { Route as MarketingBacklinkCheckerRouteImport } from './routes/_marketing/backlink-checker'
import { Route as MarketingFeaturesIndexRouteImport } from './routes/_marketing/features/index' import { Route as MarketingFeaturesIndexRouteImport } from './routes/_marketing/features/index'
import { Route as MarketingFeaturesSiteAuditRouteImport } from './routes/_marketing/features/site-audit' import { Route as MarketingFeaturesSiteAuditRouteImport } from './routes/_marketing/features/site-audit'
import { Route as MarketingFeaturesSavedKeywordsRouteImport } from './routes/_marketing/features/saved-keywords' import { Route as MarketingFeaturesSavedKeywordsRouteImport } from './routes/_marketing/features/saved-keywords'
@ -32,6 +34,7 @@ import { Route as MarketingFeaturesRankTrackingRouteImport } from './routes/_mar
import { Route as MarketingFeaturesMcpRouteImport } from './routes/_marketing/features/mcp' import { Route as MarketingFeaturesMcpRouteImport } from './routes/_marketing/features/mcp'
import { Route as MarketingFeaturesKeywordResearchRouteImport } from './routes/_marketing/features/keyword-research' import { Route as MarketingFeaturesKeywordResearchRouteImport } from './routes/_marketing/features/keyword-research'
import { Route as MarketingFeaturesDomainOverviewRouteImport } from './routes/_marketing/features/domain-overview' import { Route as MarketingFeaturesDomainOverviewRouteImport } from './routes/_marketing/features/domain-overview'
import { Route as MarketingFeaturesBacklinksRouteImport } from './routes/_marketing/features/backlinks'
import { Route as MarketingFeaturesBacklinkCheckerRouteImport } from './routes/_marketing/features/backlink-checker' import { Route as MarketingFeaturesBacklinkCheckerRouteImport } from './routes/_marketing/features/backlink-checker'
import { Route as MarketingFeaturesAiSearchPromptsRouteImport } from './routes/_marketing/features/ai-search-prompts' import { Route as MarketingFeaturesAiSearchPromptsRouteImport } from './routes/_marketing/features/ai-search-prompts'
import { Route as MarketingFeaturesAiBrandVisibilityRouteImport } from './routes/_marketing/features/ai-brand-visibility' import { Route as MarketingFeaturesAiBrandVisibilityRouteImport } from './routes/_marketing/features/ai-brand-visibility'
@ -105,6 +108,11 @@ const ApiEventRoute = ApiEventRouteImport.update({
path: '/api/event', path: '/api/event',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ApiBacklinkCheckRoute = ApiBacklinkCheckRouteImport.update({
id: '/api/backlink-check',
path: '/api/backlink-check',
getParentRoute: () => rootRouteImport,
} as any)
const MarketingPricingRoute = MarketingPricingRouteImport.update({ const MarketingPricingRoute = MarketingPricingRouteImport.update({
id: '/pricing', id: '/pricing',
path: '/pricing', path: '/pricing',
@ -121,6 +129,12 @@ const MarketingGoogleSearchConsoleMcpRoute =
path: '/google-search-console-mcp', path: '/google-search-console-mcp',
getParentRoute: () => MarketingRoute, getParentRoute: () => MarketingRoute,
} as any) } as any)
const MarketingBacklinkCheckerRoute =
MarketingBacklinkCheckerRouteImport.update({
id: '/backlink-checker',
path: '/backlink-checker',
getParentRoute: () => MarketingRoute,
} as any)
const MarketingFeaturesIndexRoute = MarketingFeaturesIndexRouteImport.update({ const MarketingFeaturesIndexRoute = MarketingFeaturesIndexRouteImport.update({
id: '/features/', id: '/features/',
path: '/features/', path: '/features/',
@ -161,6 +175,12 @@ const MarketingFeaturesDomainOverviewRoute =
path: '/features/domain-overview', path: '/features/domain-overview',
getParentRoute: () => MarketingRoute, getParentRoute: () => MarketingRoute,
} as any) } as any)
const MarketingFeaturesBacklinksRoute =
MarketingFeaturesBacklinksRouteImport.update({
id: '/features/backlinks',
path: '/features/backlinks',
getParentRoute: () => MarketingRoute,
} as any)
const MarketingFeaturesBacklinkCheckerRoute = const MarketingFeaturesBacklinkCheckerRoute =
MarketingFeaturesBacklinkCheckerRouteImport.update({ MarketingFeaturesBacklinkCheckerRouteImport.update({
id: '/features/backlink-checker', id: '/features/backlink-checker',
@ -214,9 +234,11 @@ export interface FileRoutesByFullPath {
'/': typeof MarketingIndexRoute '/': typeof MarketingIndexRoute
'/privacy': typeof PrivacyRoute '/privacy': typeof PrivacyRoute
'/terms-and-conditions': typeof TermsAndConditionsRoute '/terms-and-conditions': typeof TermsAndConditionsRoute
'/backlink-checker': typeof MarketingBacklinkCheckerRoute
'/google-search-console-mcp': typeof MarketingGoogleSearchConsoleMcpRoute '/google-search-console-mcp': typeof MarketingGoogleSearchConsoleMcpRoute
'/open-source-seo': typeof MarketingOpenSourceSeoRoute '/open-source-seo': typeof MarketingOpenSourceSeoRoute
'/pricing': typeof MarketingPricingRoute '/pricing': typeof MarketingPricingRoute
'/api/backlink-check': typeof ApiBacklinkCheckRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
'/blogs/$': typeof BlogsSplatRoute '/blogs/$': typeof BlogsSplatRoute
@ -229,6 +251,7 @@ export interface FileRoutesByFullPath {
'/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute '/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute
'/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute '/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute
'/features/backlink-checker': typeof MarketingFeaturesBacklinkCheckerRoute '/features/backlink-checker': typeof MarketingFeaturesBacklinkCheckerRoute
'/features/backlinks': typeof MarketingFeaturesBacklinksRoute
'/features/domain-overview': typeof MarketingFeaturesDomainOverviewRoute '/features/domain-overview': typeof MarketingFeaturesDomainOverviewRoute
'/features/keyword-research': typeof MarketingFeaturesKeywordResearchRoute '/features/keyword-research': typeof MarketingFeaturesKeywordResearchRoute
'/features/mcp': typeof MarketingFeaturesMcpRoute '/features/mcp': typeof MarketingFeaturesMcpRoute
@ -245,9 +268,11 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/privacy': typeof PrivacyRoute '/privacy': typeof PrivacyRoute
'/terms-and-conditions': typeof TermsAndConditionsRoute '/terms-and-conditions': typeof TermsAndConditionsRoute
'/backlink-checker': typeof MarketingBacklinkCheckerRoute
'/google-search-console-mcp': typeof MarketingGoogleSearchConsoleMcpRoute '/google-search-console-mcp': typeof MarketingGoogleSearchConsoleMcpRoute
'/open-source-seo': typeof MarketingOpenSourceSeoRoute '/open-source-seo': typeof MarketingOpenSourceSeoRoute
'/pricing': typeof MarketingPricingRoute '/pricing': typeof MarketingPricingRoute
'/api/backlink-check': typeof ApiBacklinkCheckRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
'/blogs/$': typeof BlogsSplatRoute '/blogs/$': typeof BlogsSplatRoute
@ -261,6 +286,7 @@ export interface FileRoutesByTo {
'/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute '/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute
'/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute '/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute
'/features/backlink-checker': typeof MarketingFeaturesBacklinkCheckerRoute '/features/backlink-checker': typeof MarketingFeaturesBacklinkCheckerRoute
'/features/backlinks': typeof MarketingFeaturesBacklinksRoute
'/features/domain-overview': typeof MarketingFeaturesDomainOverviewRoute '/features/domain-overview': typeof MarketingFeaturesDomainOverviewRoute
'/features/keyword-research': typeof MarketingFeaturesKeywordResearchRoute '/features/keyword-research': typeof MarketingFeaturesKeywordResearchRoute
'/features/mcp': typeof MarketingFeaturesMcpRoute '/features/mcp': typeof MarketingFeaturesMcpRoute
@ -279,9 +305,11 @@ export interface FileRoutesById {
'/_marketing': typeof MarketingRouteWithChildren '/_marketing': typeof MarketingRouteWithChildren
'/privacy': typeof PrivacyRoute '/privacy': typeof PrivacyRoute
'/terms-and-conditions': typeof TermsAndConditionsRoute '/terms-and-conditions': typeof TermsAndConditionsRoute
'/_marketing/backlink-checker': typeof MarketingBacklinkCheckerRoute
'/_marketing/google-search-console-mcp': typeof MarketingGoogleSearchConsoleMcpRoute '/_marketing/google-search-console-mcp': typeof MarketingGoogleSearchConsoleMcpRoute
'/_marketing/open-source-seo': typeof MarketingOpenSourceSeoRoute '/_marketing/open-source-seo': typeof MarketingOpenSourceSeoRoute
'/_marketing/pricing': typeof MarketingPricingRoute '/_marketing/pricing': typeof MarketingPricingRoute
'/api/backlink-check': typeof ApiBacklinkCheckRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
'/blogs/$': typeof BlogsSplatRoute '/blogs/$': typeof BlogsSplatRoute
@ -295,6 +323,7 @@ export interface FileRoutesById {
'/_marketing/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute '/_marketing/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute
'/_marketing/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute '/_marketing/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute
'/_marketing/features/backlink-checker': typeof MarketingFeaturesBacklinkCheckerRoute '/_marketing/features/backlink-checker': typeof MarketingFeaturesBacklinkCheckerRoute
'/_marketing/features/backlinks': typeof MarketingFeaturesBacklinksRoute
'/_marketing/features/domain-overview': typeof MarketingFeaturesDomainOverviewRoute '/_marketing/features/domain-overview': typeof MarketingFeaturesDomainOverviewRoute
'/_marketing/features/keyword-research': typeof MarketingFeaturesKeywordResearchRoute '/_marketing/features/keyword-research': typeof MarketingFeaturesKeywordResearchRoute
'/_marketing/features/mcp': typeof MarketingFeaturesMcpRoute '/_marketing/features/mcp': typeof MarketingFeaturesMcpRoute
@ -314,9 +343,11 @@ export interface FileRouteTypes {
| '/' | '/'
| '/privacy' | '/privacy'
| '/terms-and-conditions' | '/terms-and-conditions'
| '/backlink-checker'
| '/google-search-console-mcp' | '/google-search-console-mcp'
| '/open-source-seo' | '/open-source-seo'
| '/pricing' | '/pricing'
| '/api/backlink-check'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
| '/blogs/$' | '/blogs/$'
@ -329,6 +360,7 @@ export interface FileRouteTypes {
| '/features/ai-brand-visibility' | '/features/ai-brand-visibility'
| '/features/ai-search-prompts' | '/features/ai-search-prompts'
| '/features/backlink-checker' | '/features/backlink-checker'
| '/features/backlinks'
| '/features/domain-overview' | '/features/domain-overview'
| '/features/keyword-research' | '/features/keyword-research'
| '/features/mcp' | '/features/mcp'
@ -345,9 +377,11 @@ export interface FileRouteTypes {
to: to:
| '/privacy' | '/privacy'
| '/terms-and-conditions' | '/terms-and-conditions'
| '/backlink-checker'
| '/google-search-console-mcp' | '/google-search-console-mcp'
| '/open-source-seo' | '/open-source-seo'
| '/pricing' | '/pricing'
| '/api/backlink-check'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
| '/blogs/$' | '/blogs/$'
@ -361,6 +395,7 @@ export interface FileRouteTypes {
| '/features/ai-brand-visibility' | '/features/ai-brand-visibility'
| '/features/ai-search-prompts' | '/features/ai-search-prompts'
| '/features/backlink-checker' | '/features/backlink-checker'
| '/features/backlinks'
| '/features/domain-overview' | '/features/domain-overview'
| '/features/keyword-research' | '/features/keyword-research'
| '/features/mcp' | '/features/mcp'
@ -378,9 +413,11 @@ export interface FileRouteTypes {
| '/_marketing' | '/_marketing'
| '/privacy' | '/privacy'
| '/terms-and-conditions' | '/terms-and-conditions'
| '/_marketing/backlink-checker'
| '/_marketing/google-search-console-mcp' | '/_marketing/google-search-console-mcp'
| '/_marketing/open-source-seo' | '/_marketing/open-source-seo'
| '/_marketing/pricing' | '/_marketing/pricing'
| '/api/backlink-check'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
| '/blogs/$' | '/blogs/$'
@ -394,6 +431,7 @@ export interface FileRouteTypes {
| '/_marketing/features/ai-brand-visibility' | '/_marketing/features/ai-brand-visibility'
| '/_marketing/features/ai-search-prompts' | '/_marketing/features/ai-search-prompts'
| '/_marketing/features/backlink-checker' | '/_marketing/features/backlink-checker'
| '/_marketing/features/backlinks'
| '/_marketing/features/domain-overview' | '/_marketing/features/domain-overview'
| '/_marketing/features/keyword-research' | '/_marketing/features/keyword-research'
| '/_marketing/features/mcp' | '/_marketing/features/mcp'
@ -412,6 +450,7 @@ export interface RootRouteChildren {
MarketingRoute: typeof MarketingRouteWithChildren MarketingRoute: typeof MarketingRouteWithChildren
PrivacyRoute: typeof PrivacyRoute PrivacyRoute: typeof PrivacyRoute
TermsAndConditionsRoute: typeof TermsAndConditionsRoute TermsAndConditionsRoute: typeof TermsAndConditionsRoute
ApiBacklinkCheckRoute: typeof ApiBacklinkCheckRoute
ApiEventRoute: typeof ApiEventRoute ApiEventRoute: typeof ApiEventRoute
ApiSubscribeRoute: typeof ApiSubscribeRoute ApiSubscribeRoute: typeof ApiSubscribeRoute
BlogsSplatRoute: typeof BlogsSplatRoute BlogsSplatRoute: typeof BlogsSplatRoute
@ -516,6 +555,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiEventRouteImport preLoaderRoute: typeof ApiEventRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/api/backlink-check': {
id: '/api/backlink-check'
path: '/api/backlink-check'
fullPath: '/api/backlink-check'
preLoaderRoute: typeof ApiBacklinkCheckRouteImport
parentRoute: typeof rootRouteImport
}
'/_marketing/pricing': { '/_marketing/pricing': {
id: '/_marketing/pricing' id: '/_marketing/pricing'
path: '/pricing' path: '/pricing'
@ -537,6 +583,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MarketingGoogleSearchConsoleMcpRouteImport preLoaderRoute: typeof MarketingGoogleSearchConsoleMcpRouteImport
parentRoute: typeof MarketingRoute parentRoute: typeof MarketingRoute
} }
'/_marketing/backlink-checker': {
id: '/_marketing/backlink-checker'
path: '/backlink-checker'
fullPath: '/backlink-checker'
preLoaderRoute: typeof MarketingBacklinkCheckerRouteImport
parentRoute: typeof MarketingRoute
}
'/_marketing/features/': { '/_marketing/features/': {
id: '/_marketing/features/' id: '/_marketing/features/'
path: '/features' path: '/features'
@ -586,6 +639,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MarketingFeaturesDomainOverviewRouteImport preLoaderRoute: typeof MarketingFeaturesDomainOverviewRouteImport
parentRoute: typeof MarketingRoute parentRoute: typeof MarketingRoute
} }
'/_marketing/features/backlinks': {
id: '/_marketing/features/backlinks'
path: '/features/backlinks'
fullPath: '/features/backlinks'
preLoaderRoute: typeof MarketingFeaturesBacklinksRouteImport
parentRoute: typeof MarketingRoute
}
'/_marketing/features/backlink-checker': { '/_marketing/features/backlink-checker': {
id: '/_marketing/features/backlink-checker' id: '/_marketing/features/backlink-checker'
path: '/features/backlink-checker' path: '/features/backlink-checker'
@ -646,6 +706,7 @@ declare module '@tanstack/react-router' {
} }
interface MarketingRouteChildren { interface MarketingRouteChildren {
MarketingBacklinkCheckerRoute: typeof MarketingBacklinkCheckerRoute
MarketingGoogleSearchConsoleMcpRoute: typeof MarketingGoogleSearchConsoleMcpRoute MarketingGoogleSearchConsoleMcpRoute: typeof MarketingGoogleSearchConsoleMcpRoute
MarketingOpenSourceSeoRoute: typeof MarketingOpenSourceSeoRoute MarketingOpenSourceSeoRoute: typeof MarketingOpenSourceSeoRoute
MarketingPricingRoute: typeof MarketingPricingRoute MarketingPricingRoute: typeof MarketingPricingRoute
@ -653,6 +714,7 @@ interface MarketingRouteChildren {
MarketingFeaturesAiBrandVisibilityRoute: typeof MarketingFeaturesAiBrandVisibilityRoute MarketingFeaturesAiBrandVisibilityRoute: typeof MarketingFeaturesAiBrandVisibilityRoute
MarketingFeaturesAiSearchPromptsRoute: typeof MarketingFeaturesAiSearchPromptsRoute MarketingFeaturesAiSearchPromptsRoute: typeof MarketingFeaturesAiSearchPromptsRoute
MarketingFeaturesBacklinkCheckerRoute: typeof MarketingFeaturesBacklinkCheckerRoute MarketingFeaturesBacklinkCheckerRoute: typeof MarketingFeaturesBacklinkCheckerRoute
MarketingFeaturesBacklinksRoute: typeof MarketingFeaturesBacklinksRoute
MarketingFeaturesDomainOverviewRoute: typeof MarketingFeaturesDomainOverviewRoute MarketingFeaturesDomainOverviewRoute: typeof MarketingFeaturesDomainOverviewRoute
MarketingFeaturesKeywordResearchRoute: typeof MarketingFeaturesKeywordResearchRoute MarketingFeaturesKeywordResearchRoute: typeof MarketingFeaturesKeywordResearchRoute
MarketingFeaturesMcpRoute: typeof MarketingFeaturesMcpRoute MarketingFeaturesMcpRoute: typeof MarketingFeaturesMcpRoute
@ -668,6 +730,7 @@ interface MarketingRouteChildren {
} }
const MarketingRouteChildren: MarketingRouteChildren = { const MarketingRouteChildren: MarketingRouteChildren = {
MarketingBacklinkCheckerRoute: MarketingBacklinkCheckerRoute,
MarketingGoogleSearchConsoleMcpRoute: MarketingGoogleSearchConsoleMcpRoute, MarketingGoogleSearchConsoleMcpRoute: MarketingGoogleSearchConsoleMcpRoute,
MarketingOpenSourceSeoRoute: MarketingOpenSourceSeoRoute, MarketingOpenSourceSeoRoute: MarketingOpenSourceSeoRoute,
MarketingPricingRoute: MarketingPricingRoute, MarketingPricingRoute: MarketingPricingRoute,
@ -676,6 +739,7 @@ const MarketingRouteChildren: MarketingRouteChildren = {
MarketingFeaturesAiBrandVisibilityRoute, MarketingFeaturesAiBrandVisibilityRoute,
MarketingFeaturesAiSearchPromptsRoute: MarketingFeaturesAiSearchPromptsRoute, MarketingFeaturesAiSearchPromptsRoute: MarketingFeaturesAiSearchPromptsRoute,
MarketingFeaturesBacklinkCheckerRoute: MarketingFeaturesBacklinkCheckerRoute, MarketingFeaturesBacklinkCheckerRoute: MarketingFeaturesBacklinkCheckerRoute,
MarketingFeaturesBacklinksRoute: MarketingFeaturesBacklinksRoute,
MarketingFeaturesDomainOverviewRoute: MarketingFeaturesDomainOverviewRoute, MarketingFeaturesDomainOverviewRoute: MarketingFeaturesDomainOverviewRoute,
MarketingFeaturesKeywordResearchRoute: MarketingFeaturesKeywordResearchRoute, MarketingFeaturesKeywordResearchRoute: MarketingFeaturesKeywordResearchRoute,
MarketingFeaturesMcpRoute: MarketingFeaturesMcpRoute, MarketingFeaturesMcpRoute: MarketingFeaturesMcpRoute,
@ -703,6 +767,7 @@ const rootRouteChildren: RootRouteChildren = {
MarketingRoute: MarketingRouteWithChildren, MarketingRoute: MarketingRouteWithChildren,
PrivacyRoute: PrivacyRoute, PrivacyRoute: PrivacyRoute,
TermsAndConditionsRoute: TermsAndConditionsRoute, TermsAndConditionsRoute: TermsAndConditionsRoute,
ApiBacklinkCheckRoute: ApiBacklinkCheckRoute,
ApiEventRoute: ApiEventRoute, ApiEventRoute: ApiEventRoute,
ApiSubscribeRoute: ApiSubscribeRoute, ApiSubscribeRoute: ApiSubscribeRoute,
BlogsSplatRoute: BlogsSplatRoute, BlogsSplatRoute: BlogsSplatRoute,

View File

@ -0,0 +1,151 @@
import { createFileRoute } from "@tanstack/react-router";
import { BacklinkCheckerTool } from "@/components/backlink-checker-tool";
import { buildPageSeo } from "@/lib/seo";
export const Route = createFileRoute("/_marketing/backlink-checker")({
head: () =>
buildPageSeo({
title: "Free Backlink Checker: Check Backlinks to Any Website",
description:
"Check backlinks for any domain: referring domains, top backlinks, anchor text, and follow status. Instant results, no signup required.",
path: "/backlink-checker",
titleSuffix: "OpenSEO",
imageAlt: "OpenSEO free backlink checker",
}),
component: BacklinkCheckerPage,
});
const FAQS = [
{
question: "Where does the backlink data come from?",
answer:
"Results come from DataForSEO's link index, the same data source that powers backlink research inside OpenSEO. The index is refreshed continuously, so counts can differ slightly from other tools that crawl the web on their own schedule.",
},
{
question: "How many backlinks can I see for free?",
answer:
"The free checker shows a domain's summary metrics and its top 15 backlinks, one per referring domain, ranked by domain strength. Sign up for OpenSEO to page through the full list, see referring domains and anchors, filter out spam, and export the data.",
},
{
question: "Can I check a competitor's backlinks?",
answer:
"Yes. Enter any domain — yours, a competitor's, or a site you're evaluating for outreach. Backlink data is public-web data, so no site ownership or verification is needed.",
},
{
question: "What is domain rank?",
answer:
"Domain rank is a 0-100 score of a domain's link-profile strength, similar to domain authority metrics in other tools. Higher means the domain has more and stronger links pointing at it.",
},
];
function BacklinkCheckerPage() {
return (
<article className="mx-auto max-w-5xl">
<header className="max-w-3xl">
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
Free tool
</p>
<h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
Free Backlink Checker
</h1>
<p className="mt-5 text-lg leading-8 text-[var(--color-brand-muted)]">
Check the backlinks of any website. Enter a domain and get its domain
rank, referring domains, and top backlinks with anchor text and follow
status.
</p>
</header>
<div className="mt-8">
<BacklinkCheckerTool />
</div>
<section className="mt-12">
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
What you get
</h2>
<ol className="mt-5 grid gap-4 md:grid-cols-3">
{[
{
title: "Link profile summary",
description:
"Domain rank, total backlinks, referring domains, and broken backlinks for the domain you check.",
},
{
title: "Top backlinks",
description:
"The strongest links pointing at the domain, one per referring domain, with anchor text and follow status.",
},
{
title: "Competitor visibility",
description:
"Works on any domain, so you can see who links to competitors and where their authority comes from.",
},
].map((item, index) => (
<li
key={item.title}
className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-5"
>
<span className="font-mono text-sm tabular-nums text-[var(--color-brand-accent)]">
{String(index + 1).padStart(2, "0")}
</span>
<h3 className="mt-4 text-base font-semibold text-neutral-950">
{item.title}
</h3>
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
{item.description}
</p>
</li>
))}
</ol>
</section>
<section className="mt-12">
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
FAQ
</h2>
<div className="mt-5 divide-y divide-[var(--color-border-subtle)] rounded-lg border border-[var(--color-border-subtle)] bg-white">
{FAQS.map((faq) => (
<div key={faq.question} className="p-5">
<h3 className="text-sm font-semibold text-neutral-900">
{faq.question}
</h3>
<p className="mt-1.5 text-sm leading-6 text-[var(--color-brand-muted)]">
{faq.answer}
</p>
</div>
))}
</div>
</section>
<section className="mt-12 rounded-xl border border-[var(--color-border-subtle)] bg-white p-6 md:p-8">
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
Go deeper than a spot check
</h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
OpenSEO puts full backlink analysis next to keyword research, rank
tracking, and site audits open source, starting free.
</p>
<div className="mt-4 flex flex-wrap items-center gap-4">
<a
href="https://app.openseo.so/sign-up"
className="inline-flex h-10 items-center justify-center rounded-lg bg-neutral-950 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
>
Try OpenSEO
<span aria-hidden="true" className="ml-2">
&rarr;
</span>
</a>
<a
href="/features/backlinks"
className="text-sm font-medium text-neutral-950 underline decoration-[var(--color-brand-accent)] underline-offset-4"
>
Learn about the Backlinks feature
<span aria-hidden="true" className="ml-1">
&rarr;
</span>
</a>
</div>
</section>
</article>
);
}

View File

@ -1,18 +1,10 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { FeaturePageTemplate } from "@/components/feature-page";
import { featurePages } from "@/lib/feature-pages";
import { buildPageSeo } from "@/lib/seo";
const page = featurePages.backlinkChecker;
// The old feature page ranks for "backlink checker"; that intent is now served
// by the free tool at /backlink-checker. The feature content moved to
// /features/backlinks.
export const Route = createFileRoute("/_marketing/features/backlink-checker")({ export const Route = createFileRoute("/_marketing/features/backlink-checker")({
head: () => beforeLoad: () => {
buildPageSeo({ throw redirect({ to: "/backlink-checker", statusCode: 301 });
title: "Backlink Checker", },
description: page.description,
path: "/features/backlink-checker",
titleSuffix: "OpenSEO",
imageAlt: page.imageAlt,
}),
component: () => <FeaturePageTemplate page={page} />,
}); });

View File

@ -0,0 +1,18 @@
import { createFileRoute } from "@tanstack/react-router";
import { FeaturePageTemplate } from "@/components/feature-page";
import { featurePages } from "@/lib/feature-pages";
import { buildPageSeo } from "@/lib/seo";
const page = featurePages.backlinks;
export const Route = createFileRoute("/_marketing/features/backlinks")({
head: () =>
buildPageSeo({
title: "Backlink Analysis Tool",
description: page.description,
path: "/features/backlinks",
titleSuffix: "OpenSEO",
imageAlt: page.imageAlt,
}),
component: () => <FeaturePageTemplate page={page} />,
});

View File

@ -75,8 +75,8 @@ function GoogleSearchConsoleMcpPage() {
</a> </a>
</div> </div>
<p className="mt-3 text-xs text-neutral-500"> <p className="mt-3 text-xs text-neutral-500">
$10/month, 30-day money-back guarantee. Search Console tools never $10/month, 30-day money-back guarantee. Search Console tools never use
use credits. credits.
</p> </p>
</header> </header>

View File

@ -0,0 +1,330 @@
import { createFileRoute } from "@tanstack/react-router";
import { env } from "cloudflare:workers";
import { z } from "zod";
const DATAFORSEO_BASE = "https://api.dataforseo.com";
const TOP_BACKLINKS_LIMIT = 15;
const CACHE_TTL_SECONDS = 86_400;
// Hard ceiling on paid DataForSEO lookups per day (~$0.04 each). Cached
// checks don't count. Bumping this is a deliberate spend decision.
const DAILY_CHECK_BUDGET = 500;
const requestSchema = z.object({
target: z.string().trim().min(1, "Enter a domain").max(300),
turnstileToken: z.string().max(4096).optional(),
});
// DataForSEO task envelope: HTTP 200 with per-task status codes; 20000 = ok.
const taskEnvelopeSchema = z.object({
tasks: z
.array(
z
.object({
status_code: z.number().optional(),
status_message: z.string().optional(),
result: z
.array(z.record(z.string(), z.unknown()))
.nullable()
.optional(),
})
.passthrough(),
)
.optional(),
});
const summaryResultSchema = z
.object({
rank: z.number().nullable().optional(),
backlinks: z.number().nullable().optional(),
referring_domains: z.number().nullable().optional(),
broken_backlinks: z.number().nullable().optional(),
})
.passthrough();
const backlinksResultSchema = z
.object({
items: z
.array(
z
.object({
type: z.string().nullable().optional(),
domain_from: z.string().nullable().optional(),
url_from: z.string().nullable().optional(),
url_to: z.string().nullable().optional(),
anchor: z.string().nullable().optional(),
dofollow: z.boolean().nullable().optional(),
domain_from_rank: z.number().nullable().optional(),
page_from_title: z.string().nullable().optional(),
})
.passthrough(),
)
.nullable()
.optional(),
})
.passthrough();
type RateLimiter = {
limit(options: { key: string }): Promise<{ success: boolean }>;
};
type KvStore = {
get(key: string): Promise<string | null>;
put(
key: string,
value: string,
options?: { expirationTtl?: number },
): Promise<void>;
};
function normalizeDomain(input: string): string | null {
let hostname: string;
try {
hostname = new URL(input.includes("://") ? input : `https://${input}`)
.hostname;
} catch {
return null;
}
const domain = hostname.replace(/^www\./, "").toLowerCase();
const isValid =
/^(?=.{1,253}$)[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(
domain,
);
return isValid ? domain : null;
}
function jsonResponse(data: unknown, status = 200, headers?: HeadersInit) {
return new Response(JSON.stringify(data), {
status,
headers: { "Content-Type": "application/json", ...headers },
});
}
async function verifyTurnstile(
secret: string,
token: string,
ip: string | null,
): Promise<boolean> {
const body = new URLSearchParams({ secret, response: token });
if (ip) body.set("remoteip", ip);
const response = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{ method: "POST", body },
);
if (!response.ok) return false;
const data = (await response.json()) as { success?: boolean };
return data.success === true;
}
async function fetchDataforseoResult(
path: string,
payload: Record<string, unknown>,
apiKey: string,
) {
const response = await fetch(`${DATAFORSEO_BASE}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${apiKey}`,
},
body: JSON.stringify([payload]),
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
throw new Error(`DataForSEO HTTP ${response.status} on ${path}`);
}
const task = taskEnvelopeSchema.parse(await response.json()).tasks?.[0];
if (!task || task.status_code !== 20000) {
throw new Error(
`DataForSEO task ${task?.status_code ?? "missing"} on ${path}: ${task?.status_message ?? "no task"}`,
);
}
return task.result?.[0] ?? null;
}
export const Route = createFileRoute("/api/backlink-check")({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json().catch(() => null);
const parsed = requestSchema.safeParse(body);
if (!parsed.success) {
return jsonResponse(
{ error: parsed.error.issues[0]?.message ?? "Invalid request" },
400,
);
}
const domain = normalizeDomain(parsed.data.target);
if (!domain) {
return jsonResponse(
{ error: "Enter a valid domain, like example.com" },
400,
);
}
const apiKey = (env as any).DATAFORSEO_API_KEY as string | undefined;
if (!apiKey) {
console.error("Missing DATAFORSEO_API_KEY");
return jsonResponse(
{ error: "Service temporarily unavailable" },
503,
);
}
const ip = request.headers.get("cf-connecting-ip");
// Bot check. Enforced only when the secret is configured, so local
// dev and fresh deploys keep working without a Turnstile widget.
const turnstileSecret = (env as any).TURNSTILE_SECRET_KEY as
| string
| undefined;
if (turnstileSecret) {
const token = parsed.data.turnstileToken;
const verified = token
? await verifyTurnstile(turnstileSecret, token, ip)
: false;
if (!verified) {
console.error(
token
? "Turnstile siteverify rejected the token — VITE_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY may be from different widgets"
: "TURNSTILE_SECRET_KEY is set but the request sent no token — VITE_TURNSTILE_SITE_KEY may be missing from the deployed build",
);
return jsonResponse(
{
error:
"Human verification failed. Refresh the page and try again.",
},
403,
);
}
}
const limiter = (env as any).BACKLINK_CHECK_RATE_LIMIT as
| RateLimiter
| undefined;
if (limiter) {
const { success } = await limiter.limit({ key: ip ?? "unknown" });
if (!success) {
return jsonResponse(
{ error: "Too many checks. Try again in a minute." },
429,
);
}
}
// Per-colo cache so repeat checks of the same domain don't re-bill.
const cache = (caches as unknown as { default: Cache }).default;
const cacheKey = new Request(
`https://openseo.so/api/backlink-check/${domain}`,
);
const cached = await cache.match(cacheKey);
if (cached) return cached;
// Global daily budget. Best-effort: KV reads are edge-cached and the
// increment is non-atomic, so the ceiling is approximate — the hard
// spend bound is the DataForSEO account balance. Counts attempts, not
// successes, so charged-but-failed calls still consume budget, and a
// KV error can never fail a request the user already paid latency for.
const kv = (env as any).BACKLINK_CHECK_KV as KvStore | undefined;
if (kv) {
const budgetKey = `daily-checks:${new Date().toISOString().slice(0, 10)}`;
try {
const stored = Number(await kv.get(budgetKey));
const used = Number.isFinite(stored) ? stored : 0;
if (used >= DAILY_CHECK_BUDGET) {
return jsonResponse(
{
error:
"The free checker has reached today's limit. Try again tomorrow, or sign up for OpenSEO for full backlink research.",
},
429,
);
}
await kv.put(budgetKey, String(used + 1), {
expirationTtl: 2 * 86_400,
});
} catch (err) {
console.error("Backlink check budget counter error:", err);
}
}
// Mirrors the app's backlinks defaults (src/server/lib/dataforseo/backlinks.ts).
const commonPayload = {
target: domain,
include_subdomains: true,
include_indirect_links: true,
exclude_internal_backlinks: true,
backlinks_status_type: "live",
rank_scale: "one_hundred",
};
try {
const [summaryRaw, backlinksRaw] = await Promise.all([
fetchDataforseoResult(
"/v3/backlinks/summary/live",
commonPayload,
apiKey,
),
fetchDataforseoResult(
"/v3/backlinks/backlinks/live",
{
...commonPayload,
limit: TOP_BACKLINKS_LIMIT,
mode: "one_per_domain",
// Strongest linking domains first, like Ahrefs' free checker.
order_by: ["domain_from_rank,desc"],
},
apiKey,
),
]);
const summary = summaryResultSchema.parse(summaryRaw ?? {});
const backlinks = backlinksResultSchema.parse(backlinksRaw ?? {});
const topBacklinks = (backlinks.items ?? [])
.filter((item) => item.type === "backlink" && item.url_from)
.map((item) => ({
domainFrom: item.domain_from ?? null,
urlFrom: item.url_from ?? null,
urlTo: item.url_to ?? null,
pageTitle: item.page_from_title?.trim()
? item.page_from_title
: null,
anchor: item.anchor?.trim() ? item.anchor : null,
dofollow: item.dofollow ?? null,
domainRank: item.domain_from_rank ?? null,
}));
const response = jsonResponse(
{
target: domain,
summary: {
rank: summary.rank ?? null,
backlinks: summary.backlinks ?? null,
referringDomains: summary.referring_domains ?? null,
brokenBacklinks: summary.broken_backlinks ?? null,
},
topBacklinks,
},
200,
{ "Cache-Control": `public, max-age=${CACHE_TTL_SECONDS}` },
);
await cache.put(cacheKey, response.clone());
return response;
} catch (err) {
console.error("Backlink check error:", err);
// Negative cache: money was already spent, so stop an immediate
// retry loop on a reliably-failing domain. 502s are only stored
// when explicitly marked cacheable.
const response = jsonResponse(
{ error: "Backlink check failed. Please try again." },
502,
{ "Cache-Control": "public, max-age=120" },
);
await cache.put(cacheKey, response.clone()).catch(() => {});
return response;
}
},
},
},
});

View File

@ -16,7 +16,12 @@ body {
background: var(--color-fd-background); background: var(--color-fd-background);
color: var(--color-fd-foreground); color: var(--color-fd-foreground);
font-family: font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Inter,
"Segoe UI", sans-serif; ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
overflow-x: clip; overflow-x: clip;
} }

View File

@ -27,11 +27,40 @@
"custom_domain": true, "custom_domain": true,
}, },
], ],
"kv_namespaces": [
{
"binding": "BACKLINK_CHECK_KV",
"id": "b3a051a75a7b478e93b46cdaf6da3572",
},
],
"ratelimits": [
{
"name": "BACKLINK_CHECK_RATE_LIMIT",
"namespace_id": "1001",
"simple": { "limit": 5, "period": 60 },
},
],
"env": { "env": {
// Bindings are not inherited by named envs; keep preview's copies in sync.
// Shared KV id is deliberate: one global daily budget across both workers.
"preview": { "preview": {
"name": "open-seo-landing-preview", "name": "open-seo-landing-preview",
"workers_dev": true, "workers_dev": true,
"preview_urls": true, "preview_urls": true,
"routes": [],
"kv_namespaces": [
{
"binding": "BACKLINK_CHECK_KV",
"id": "b3a051a75a7b478e93b46cdaf6da3572",
},
],
"ratelimits": [
{
"name": "BACKLINK_CHECK_RATE_LIMIT",
"namespace_id": "1001",
"simple": { "limit": 5, "period": 60 },
},
],
}, },
}, },
} }