import { sql } from "@/db"; import Link from "next/link"; import { fmtDateTime } from "@/lib/utils"; import { EnqueueButton } from "@/components/admin/enqueue-button"; export default async function Links() { const rows = await sql`SELECT DISTINCT ON (lc.tool_id, lc.field) t.id, t.slug, t.name, lc.field, lc.url, lc.ok, lc.status_code AS "statusCode", lc.final_url AS "finalUrl", lc.redirected, lc.error, lc.checked_at AS "checkedAt" FROM link_checks lc JOIN tools t ON t.id=lc.tool_id ORDER BY lc.tool_id, lc.field, lc.checked_at DESC`; const broken = rows.filter((r) => !r.ok); const moved = rows.filter((r) => r.ok && r.redirected && safeHost(r.url) !== safeHost(r.finalUrl)); const [s] = await sql`SELECT count(*) FILTER (WHERE link_state='ok')::int AS ok, count(*) FILTER (WHERE link_state='redirected')::int AS redirected, count(*) FILTER (WHERE link_state='broken')::int AS broken, count(*) FILTER (WHERE link_state='unchecked')::int AS unchecked, min(checked_at) AS oldest, max(checked_at) AS newest FROM tools WHERE visibility='public' AND editorial_status IN ('published','reviewed')`; return (

Link integrity

{s.ok} ok · {s.redirected} redirected · {s.broken} broken · {s.unchecked} unchecked · oldest check {fmtDateTime(s.oldest)} · newest {fmtDateTime(s.newest)}

Broken or unverifiable ({broken.length})

Status 401/403/405/429 means the site blocks automated requests; it is counted as unverifiable, not broken. A 404/5xx/timeout marks the record as broken and blocks publishing until fixed.

{broken.map((r) => )}{!broken.length && }
RecordFieldURLStatusErrorChecked
{r.name}{r.field}{r.url}{r.statusCode ?? "—"}{r.error}{fmtDateTime(r.checkedAt)}
No broken links.

Moved to another host ({moved.length})

{moved.map((r) => )}{!moved.length && }
RecordFieldFromTo
{r.name}{r.field}{r.url}{r.finalUrl}
None.
); } function safeHost(u: string) { try { return new URL(u).hostname.replace(/^www\./, ""); } catch { return u; } }