SPB Git
3.3 KB · 19 lines tsx
1import { sql } from "@/db";2import Link from "next/link";3import { fmtDateTime } from "@/lib/utils";4import { EnqueueButton } from "@/components/admin/enqueue-button";5export default async function Links() {6  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`;7  const broken = rows.filter((r) => !r.ok); const moved = rows.filter((r) => r.ok && r.redirected && safeHost(r.url) !== safeHost(r.finalUrl));8  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')`;9  return (10    <div className="space-y-8">11      <div className="flex flex-wrap items-end justify-between gap-3"><div><h1 className="display h1">Link integrity</h1><p className="mt-1 text-sm text-fg-2">{s.ok} ok · {s.redirected} redirected · {s.broken} broken · {s.unchecked} unchecked · oldest check {fmtDateTime(s.oldest)} · newest {fmtDateTime(s.newest)}</p></div><EnqueueButton kind="link_sweep" label="Run full sweep now" /></div>12      <section><h2 className="font-semibold">Broken or unverifiable ({broken.length})</h2><p className="text-xs text-fg-3">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.</p>13        <div className="mt-2 overflow-x-auto rounded-2xl border border-line bg-bg-2"><table className="table"><thead><tr><th>Record</th><th>Field</th><th>URL</th><th>Status</th><th>Error</th><th>Checked</th></tr></thead><tbody>{broken.map((r) => <tr key={r.id + r.field}><td><Link href={`/admin/tools/${r.id}`} className="hover:text-accent">{r.name}</Link></td><td>{r.field}</td><td className="max-w-xs truncate font-mono text-xs"><a href={r.url} target="_blank" rel="noopener noreferrer">{r.url}</a></td><td>{r.statusCode ?? "—"}</td><td className="text-xs text-bad">{r.error}</td><td className="text-fg-3">{fmtDateTime(r.checkedAt)}</td></tr>)}{!broken.length && <tr><td colSpan={6} className="text-fg-3">No broken links.</td></tr>}</tbody></table></div></section>14      <section><h2 className="font-semibold">Moved to another host ({moved.length})</h2><div className="mt-2 overflow-x-auto rounded-2xl border border-line bg-bg-2"><table className="table"><thead><tr><th>Record</th><th>Field</th><th>From</th><th>To</th></tr></thead><tbody>{moved.map((r) => <tr key={r.id + r.field}><td><Link href={`/admin/tools/${r.id}`} className="hover:text-accent">{r.name}</Link></td><td>{r.field}</td><td className="font-mono text-xs">{r.url}</td><td className="font-mono text-xs">{r.finalUrl}</td></tr>)}{!moved.length && <tr><td colSpan={4} className="text-fg-3">None.</td></tr>}</tbody></table></div></section>15    </div>16  );17}18function safeHost(u: string) { try { return new URL(u).hostname.replace(/^www\./, ""); } catch { return u; } }19
Files · main
Menu

Theme