← back to Govarbitrage
Public SEO deal archive: /deals index + /deals/[date] teaser pages (ROI band + confidence shown, max-bid/net-profit withheld behind subscribe CTA), middleware allowlist, nav links
5e22c50a62a54ae339ab240e5c4470bc43d9ba82 · 2026-07-13 01:38:44 -0700 · Steve Abrams
Files touched
A src/app/deals/[date]/page.tsxA src/app/deals/page.tsxM src/app/newsletter/page.tsxM src/app/page.tsxA src/lib/site.tsM src/middleware.ts
Diff
commit 5e22c50a62a54ae339ab240e5c4470bc43d9ba82
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 13 01:38:44 2026 -0700
Public SEO deal archive: /deals index + /deals/[date] teaser pages (ROI band + confidence shown, max-bid/net-profit withheld behind subscribe CTA), middleware allowlist, nav links
---
src/app/deals/[date]/page.tsx | 192 ++++++++++++++++++++++++++++++++++++++++++
src/app/deals/page.tsx | 114 +++++++++++++++++++++++++
src/app/newsletter/page.tsx | 5 ++
src/app/page.tsx | 3 +
src/lib/site.ts | 6 ++
src/middleware.ts | 3 +
6 files changed, 323 insertions(+)
diff --git a/src/app/deals/[date]/page.tsx b/src/app/deals/[date]/page.tsx
new file mode 100644
index 0000000..6772d3f
--- /dev/null
+++ b/src/app/deals/[date]/page.tsx
@@ -0,0 +1,192 @@
+import type { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { getDigestEditionsForDate, type DigestEdition, type SnapshotDeal } from "@/lib/digest-snapshot";
+import { publicBaseUrl } from "@/lib/site";
+
+export const dynamic = "force-dynamic";
+
+const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
+
+const fmtDate = (iso: string) =>
+ new Date(`${iso}T12:00:00Z`).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ timeZone: "UTC",
+ });
+
+const confidenceLabel: Record<string, { text: string; color: string }> = {
+ HIGH: { text: "High confidence", color: "#16a34a" },
+ MEDIUM: { text: "Medium confidence", color: "#d97706" },
+ LOW: { text: "Low confidence", color: "#64748b" },
+};
+
+// The public teaser shows a conservative ROI BAND, not a point estimate —
+// roiConservative is already the haircut low band, so band = [~80% of it, it].
+function roiBand(d: SnapshotDeal): string {
+ const hi = Math.round(d.roiConservative * 100);
+ const lo = Math.max(0, Math.round(hi * 0.8));
+ return `${lo}–${hi}%${d.roiCapped ? " (capped)" : ""}`;
+}
+
+type Params = { date: string };
+
+export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
+ const { date } = await params;
+ if (!DATE_RE.test(date)) return {};
+ const editions = await getDigestEditionsForDate(date);
+ if (editions.length === 0) return {};
+
+ const base = publicBaseUrl();
+ const dealCount = editions.reduce((n, e) => n + e.dealCount, 0);
+ const top = editions.flatMap((e) => e.deals)[0];
+ const title = `Top-10 Gov-Surplus Deals — ${fmtDate(date)}`;
+ const description = top
+ ? `${dealCount} honesty-gated government-surplus deal${dealCount === 1 ? "" : "s"} on ${fmtDate(date)}, led by "${top.title}". Conservative, confidence-labeled numbers only.`
+ : `The ${fmtDate(date)} digest ran with no deals clearing the hot-deal gate — we publish nothing rather than pad the list.`;
+
+ return {
+ title: `${title} | GovArbitrage`,
+ description,
+ alternates: { canonical: `${base}/deals/${date}` },
+ openGraph: {
+ title,
+ description,
+ url: `${base}/deals/${date}`,
+ type: "article",
+ },
+ };
+}
+
+function editionJsonLd(edition: DigestEdition, base: string) {
+ return {
+ "@context": "https://schema.org",
+ "@type": "ItemList",
+ name: `Top-10 Gov-Surplus Deals — ${fmtDate(edition.date)} (${edition.slot === "AM" ? "Morning" : "Evening"} edition)`,
+ url: `${base}/deals/${edition.date}`,
+ numberOfItems: edition.deals.length,
+ itemListOrder: "https://schema.org/ItemListOrderAscending",
+ itemListElement: edition.deals.map((d) => ({
+ "@type": "ListItem",
+ position: d.rank,
+ name: d.title,
+ })),
+ };
+}
+
+function TeaserRow({ deal }: { deal: SnapshotDeal }) {
+ const where = [deal.locationCity, deal.locationState].filter(Boolean).join(", ");
+ const conf = confidenceLabel[deal.confidence] ?? confidenceLabel.LOW;
+ return (
+ <li style={{ border: "1px solid #e2e8f0", borderRadius: 10, padding: "14px 18px", marginBottom: 12 }}>
+ <div style={{ display: "flex", gap: 12, alignItems: "baseline" }}>
+ <span style={{ fontWeight: 700, color: "#94a3b8", fontSize: 15, flexShrink: 0 }}>#{deal.rank}</span>
+ <div style={{ minWidth: 0 }}>
+ <div style={{ fontWeight: 700, fontSize: 16 }}>{deal.title}</div>
+ <div style={{ color: "#64748b", fontSize: 13, marginTop: 2 }}>
+ {deal.source.replace(/_/g, " ")}
+ {where ? <> · {where}</> : null}
+ {" · "}
+ <span style={{ color: conf.color, fontWeight: 600 }}>{conf.text}</span>
+ </div>
+ <div style={{ fontSize: 14, marginTop: 8, color: "#334155" }}>
+ Conservative ROI band: <b>{roiBand(deal)}</b>
+ </div>
+ <div style={{ fontSize: 14, marginTop: 6, color: "#334155", display: "flex", flexWrap: "wrap", gap: "4px 14px", alignItems: "center" }}>
+ <span>
+ Rec. max bid:{" "}
+ <span aria-hidden style={{ filter: "blur(5px)", userSelect: "none" }}>$8,450</span>
+ </span>
+ <span>
+ Est. net profit:{" "}
+ <span aria-hidden style={{ filter: "blur(5px)", userSelect: "none" }}>$3,120</span>
+ </span>
+ <a href="/newsletter" style={{ color: "#2563eb", fontSize: 13, fontWeight: 600 }}>
+ Subscribe free to see the full math →
+ </a>
+ </div>
+ <div style={{ color: "#94a3b8", fontSize: 12, marginTop: 6 }}>{deal.disclaimer}</div>
+ </div>
+ </div>
+ </li>
+ );
+}
+
+export default async function DealsByDatePage({ params }: { params: Promise<Params> }) {
+ const { date } = await params;
+ if (!DATE_RE.test(date)) notFound();
+
+ const editions = await getDigestEditionsForDate(date);
+ if (editions.length === 0) notFound();
+
+ const base = publicBaseUrl();
+
+ return (
+ <main style={{ maxWidth: 720, margin: "56px auto", fontFamily: "system-ui", padding: "0 20px", color: "#0f172a" }}>
+ {editions.map((e) => (
+ <script
+ key={`ld-${e.slot}`}
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(editionJsonLd(e, base)) }}
+ />
+ ))}
+
+ <p style={{ fontSize: 13, marginBottom: 8 }}>
+ <a href="/deals" style={{ color: "#64748b", textDecoration: "none" }}>← Deal archive</a>
+ </p>
+ <header style={{ marginBottom: 24 }}>
+ <h1 style={{ fontSize: 30, margin: 0 }}>Top-10 Gov-Surplus Deals — {fmtDate(date)}</h1>
+ <p style={{ color: "#475569", fontSize: 15, lineHeight: 1.6 }}>
+ Frozen as published. Every figure is honesty-gated — confidence-discounted,
+ capped, and disclaimed. Exact recommended max bids and net-profit math go
+ to <a href="/newsletter" style={{ color: "#2563eb" }}>free subscribers</a>.
+ </p>
+ </header>
+
+ {editions.map((e) => (
+ <section key={e.slot} style={{ marginBottom: 30 }}>
+ <h2 style={{ fontSize: 20, margin: "0 0 4px" }}>
+ {e.slot === "AM" ? "Morning" : "Evening"} edition
+ <span style={{ color: "#94a3b8", fontWeight: 400, fontSize: 14 }}>
+ {" "}· {e.dealCount} deal{e.dealCount === 1 ? "" : "s"}
+ </span>
+ </h2>
+ {e.deals.length === 0 ? (
+ <p style={{ color: "#64748b", fontSize: 14, border: "1px dashed #cbd5e1", borderRadius: 10, padding: "18px" }}>
+ No deals cleared the hot-deal gate this run. We'd rather publish
+ nothing than pad the list with marginal lots.
+ </p>
+ ) : (
+ <ul style={{ listStyle: "none", padding: 0, margin: "12px 0 0" }}>
+ {e.deals.map((d) => (
+ <TeaserRow key={d.rank} deal={d} />
+ ))}
+ </ul>
+ )}
+ </section>
+ ))}
+
+ <div style={{ textAlign: "center", margin: "34px 0" }}>
+ <a
+ href="/newsletter"
+ style={{
+ display: "inline-block",
+ background: "#0f172a",
+ color: "#fff",
+ padding: "12px 26px",
+ borderRadius: 8,
+ fontSize: 15,
+ textDecoration: "none",
+ }}
+ >
+ Get the next Top-10 by email — subscribe free →
+ </a>
+ </div>
+
+ <p style={{ textAlign: "center", color: "#94a3b8", fontSize: 12, lineHeight: 1.6 }}>
+ Estimates are heuristic — always verify comps before bidding. Archive pages
+ are frozen at publish time; live auction state will have moved on.
+ </p>
+ </main>
+ );
+}
diff --git a/src/app/deals/page.tsx b/src/app/deals/page.tsx
new file mode 100644
index 0000000..48c3723
--- /dev/null
+++ b/src/app/deals/page.tsx
@@ -0,0 +1,114 @@
+import type { Metadata } from "next";
+import { listDigestEditions } from "@/lib/digest-snapshot";
+import { publicBaseUrl } from "@/lib/site";
+
+export const dynamic = "force-dynamic";
+
+const base = publicBaseUrl();
+
+export const metadata: Metadata = {
+ title: "Deal Archive — Top-10 Gov-Surplus Deals Digest | GovArbitrage",
+ description:
+ "Every edition of the twice-daily Top-10 government-surplus deals digest — honesty-gated, confidence-labeled, conservative numbers only.",
+ alternates: { canonical: `${base}/deals` },
+ openGraph: {
+ title: "GovArbitrage Deal Archive — Top-10 Gov-Surplus Deals",
+ description:
+ "Browse past editions of the twice-daily Top-10 government-surplus arbitrage digest. Every figure is confidence-discounted and capped, never inflated.",
+ url: `${base}/deals`,
+ type: "website",
+ },
+};
+
+const fmtDate = (iso: string) =>
+ new Date(`${iso}T12:00:00Z`).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ timeZone: "UTC",
+ });
+
+export default async function DealsArchivePage() {
+ const editions = await listDigestEditions(60);
+
+ return (
+ <main style={{ maxWidth: 720, margin: "56px auto", fontFamily: "system-ui", padding: "0 20px", color: "#0f172a" }}>
+ <header style={{ textAlign: "center", marginBottom: 28 }}>
+ <h1 style={{ fontSize: 32, margin: 0 }}>The Deal Archive</h1>
+ <p style={{ color: "#475569", fontSize: 16, lineHeight: 1.6 }}>
+ Every edition of our twice-daily Top-10 government-surplus deals digest,
+ frozen exactly as it went out. Every figure passes the Honesty Gate:
+ confidence-discounted, capped, and disclaimed — we publish the
+ conservative low band, never the hype number.
+ </p>
+ <a
+ href="/newsletter"
+ style={{
+ display: "inline-block",
+ marginTop: 10,
+ background: "#0f172a",
+ color: "#fff",
+ padding: "10px 22px",
+ borderRadius: 8,
+ fontSize: 15,
+ textDecoration: "none",
+ }}
+ >
+ Subscribe free — get the next edition by email →
+ </a>
+ </header>
+
+ {editions.length === 0 ? (
+ <div
+ style={{
+ textAlign: "center",
+ border: "1px dashed #cbd5e1",
+ borderRadius: 10,
+ padding: "40px 24px",
+ color: "#64748b",
+ fontSize: 15,
+ }}
+ >
+ <p style={{ margin: 0, fontWeight: 600, color: "#334155" }}>First edition coming at launch.</p>
+ <p style={{ margin: "8px 0 0" }}>
+ The digest publishes twice daily (6am & 5pm PT) once we go live.{" "}
+ <a href="/newsletter" style={{ color: "#2563eb" }}>Subscribe now</a> and you'll get edition #1.
+ </p>
+ </div>
+ ) : (
+ <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
+ {editions.map((e) => (
+ <li
+ key={`${e.date}-${e.slot}`}
+ style={{ border: "1px solid #e2e8f0", borderRadius: 10, padding: "14px 18px", marginBottom: 12 }}
+ >
+ <a href={`/deals/${e.date}`} style={{ textDecoration: "none", color: "inherit", display: "block" }}>
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 12 }}>
+ <span style={{ fontWeight: 700, fontSize: 16 }}>
+ {fmtDate(e.date)} <span style={{ color: "#64748b", fontWeight: 500 }}>· {e.slot === "AM" ? "Morning" : "Evening"} edition</span>
+ </span>
+ <span style={{ color: "#64748b", fontSize: 13, whiteSpace: "nowrap" }}>
+ {e.dealCount} deal{e.dealCount === 1 ? "" : "s"}
+ </span>
+ </div>
+ <div style={{ color: "#475569", fontSize: 14, marginTop: 4 }}>
+ {e.deals.length > 0 ? (
+ <>Top pick: {e.deals[0].title}</>
+ ) : (
+ <>No deals cleared the hot-deal gate this run — we'd rather send nothing than pad the list.</>
+ )}
+ </div>
+ </a>
+ </li>
+ ))}
+ </ul>
+ )}
+
+ <p style={{ textAlign: "center", color: "#94a3b8", fontSize: 12, marginTop: 30, lineHeight: 1.6 }}>
+ Archive editions show deal teasers. Exact recommended max bids and the
+ full cost math go to <a href="/newsletter" style={{ color: "#64748b" }}>free subscribers</a>.
+ Estimates are heuristic — always verify comps before bidding.
+ </p>
+ </main>
+ );
+}
diff --git a/src/app/newsletter/page.tsx b/src/app/newsletter/page.tsx
index 24b36e8..c7b07c3 100644
--- a/src/app/newsletter/page.tsx
+++ b/src/app/newsletter/page.tsx
@@ -36,6 +36,11 @@ export default function NewsletterPage() {
<SubscribeForm />
+ <p style={{ textAlign: "center", fontSize: 14, marginTop: 20 }}>
+ Want a preview first?{" "}
+ <a href="/deals" style={{ color: "#2563eb" }}>Browse the deal archive →</a>
+ </p>
+
<p style={{ textAlign: "center", color: "#94a3b8", fontSize: 12, marginTop: 26, lineHeight: 1.6 }}>
Double opt-in: we only email you after you click the confirmation link.
Every digest carries a single-click unsubscribe link — no login, no questions,
diff --git a/src/app/page.tsx b/src/app/page.tsx
index f2319bc..105a894 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -30,6 +30,9 @@ export default async function DashboardPage() {
<a href="/newsletter" className="text-primary underline-offset-4 hover:underline">
Newsletter →
</a>
+ <a href="/deals" className="text-primary underline-offset-4 hover:underline">
+ Deal archive →
+ </a>
{user && (
<span className="flex items-center gap-2 text-muted-foreground">
{user.email} · {user.role}
diff --git a/src/lib/site.ts b/src/lib/site.ts
new file mode 100644
index 0000000..e9ab118
--- /dev/null
+++ b/src/lib/site.ts
@@ -0,0 +1,6 @@
+// Public origin for canonical URLs, sitemap, robots, and OG tags.
+// NEWSLETTER_BASE_URL is already the env of record for public links
+// (confirm/unsubscribe emails use it) — reuse it rather than adding a twin.
+export function publicBaseUrl(): string {
+ return (process.env.NEWSLETTER_BASE_URL || "http://localhost:3737").replace(/\/+$/, "");
+}
diff --git a/src/middleware.ts b/src/middleware.ts
index 57d34b0..e2bca9a 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -14,6 +14,9 @@ const PUBLIC = [
/^\/api\/billing\/webhook$/, // Stripe calls this server-to-server, no session
/^\/newsletter(?:\/|$)/, // public digest subscribe landing
/^\/api\/newsletter\/(?:subscribe|confirm|unsubscribe)$/, // anonymous subscribers + email links
+ /^\/deals(?:\/|$)/, // public SEO digest archive (frozen snapshots, teaser numbers only)
+ /^\/sitemap\.xml$/, // crawlers (robots.txt is already excluded by the matcher)
+ /^\/robots\.txt$/,
];
function isPublic(pathname: string): boolean {
← 343863b DigestSnapshot model: freeze each honesty-gated Top-10 at se
·
back to Govarbitrage
·
SEO plumbing: sitemap.xml (static pages + every archive date 519132e →