[object Object]

← 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

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&apos;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 &amp; 5pm PT) once we go live.{" "}
+            <a href="/newsletter" style={{ color: "#2563eb" }}>Subscribe now</a> and you&apos;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&apos;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 →