[object Object]

← back to Ventura Corridor

iter 145: /issue/archive — back-issues index w/ cover-image previews + Archive link in toolbar

3f9a76261259e2d7406ac3f8eec6c7b9fdaa7ac6 · 2026-05-06 19:11:24 -0700 · SteveStudio2

Files touched

Diff

commit 3f9a76261259e2d7406ac3f8eec6c7b9fdaa7ac6
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 6 19:11:24 2026 -0700

    iter 145: /issue/archive — back-issues index w/ cover-image previews + Archive link in toolbar
---
 src/server/index.ts | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 84 insertions(+)

diff --git a/src/server/index.ts b/src/server/index.ts
index ef0f3a4..be0cfa6 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -2288,6 +2288,89 @@ app.get(['/issue/feed.xml', '/issue/:cat/feed.xml'], async (req, res) => {
   }
 });
 
+// /issue/archive — index of every month that has a published feature
+app.get('/issue/archive', async (_req, res) => {
+  const r = await query(`
+    SELECT to_char(mf.published_at AT TIME ZONE 'America/Los_Angeles', 'YYYY-MM') AS month,
+           count(*) AS n,
+           min(mf.published_at) AS first_pub,
+           max(mf.published_at) AS last_pub,
+           i.cover_feature_id, i.cover_image_path,
+           cmf.headline AS cover_headline, b.name AS cover_biz
+    FROM magazine_features mf
+    LEFT JOIN magazine_issues i ON i.issue_month = to_char(mf.published_at AT TIME ZONE 'America/Los_Angeles', 'YYYY-MM')
+    LEFT JOIN magazine_features cmf ON cmf.id = i.cover_feature_id
+    LEFT JOIN businesses b ON b.id = cmf.business_id
+    WHERE mf.status = 'published' AND mf.published_at IS NOT NULL
+    GROUP BY 1, i.cover_feature_id, i.cover_image_path, cmf.headline, b.name
+    ORDER BY 1 DESC
+  `);
+  const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));
+  const monthsHtml = r.rows.length === 0
+    ? `<div style="text-align:center;padding:80px 24px;color:var(--ink-mute);font-style:italic;font-family:'Cormorant Garamond',serif;font-size:22px">No issues published yet.</div>`
+    : r.rows.map((m: any) => {
+        const [yy, mm] = String(m.month).split('-').map(Number);
+        const mLabel = new Date(yy, mm - 1, 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
+        const dateRange = `${new Date(m.first_pub).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${new Date(m.last_pub).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`;
+        const coverImg = m.cover_image_path ? `/covers/${esc(m.cover_image_path)}` : null;
+        return `<a href="/issue/${esc(m.month)}" class="month-card${coverImg ? ' has-img' : ''}">
+          ${coverImg ? `<div class="cover-img" style="background-image:url('${coverImg}')"></div><div class="img-overlay"></div>` : ''}
+          <div class="month-card-inner">
+            <div class="kicker">${esc(m.month)}</div>
+            <div class="month">${esc(mLabel)}</div>
+            ${m.cover_headline ? `<div class="cover-line">★ ${esc(m.cover_headline)}<small>${esc(m.cover_biz || '')}</small></div>` : ''}
+            <div class="stats"><b>${m.n}</b> feature${Number(m.n) === 1 ? '' : 's'} · ${esc(dateRange)}</div>
+          </div>
+        </a>`;
+      }).join('');
+
+  res.setHeader('Content-Type', 'text/html; charset=utf-8');
+  res.send(`<!doctype html><html lang="en"><head><meta charset="utf-8">
+<title>Archive · The Corridor</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<style>
+@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,400;1,500&family=Inter:wght@300;400;500&display=swap');
+:root{--paper:#faf6ee;--ink:#1a1815;--ink-mute:#6e6356;--metal:#8a6d3b;--metal-glow:#b89968;--accent:#6a3a1a;--rule:#d8cdb8}
+*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font-family:'Inter',sans-serif;font-weight:300;line-height:1.5}
+header{text-align:center;padding:64px 24px 36px;border-bottom:4px double var(--ink)}
+header .kicker{font-size:10px;letter-spacing:.45em;text-transform:uppercase;color:var(--metal);font-weight:500}
+header h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:84px;line-height:.95;margin:14px 0 0;letter-spacing:-0.02em}
+header h1 em{font-style:normal;color:var(--metal)}
+header .deck{font-family:'Cormorant Garamond',serif;font-style:italic;color:var(--ink-mute);font-size:18px;margin-top:12px}
+.toolbar{position:fixed;top:14px;right:14px;display:flex;gap:6px}.toolbar a{font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);background:var(--paper);padding:5px 10px;border:1px solid var(--rule);text-decoration:none}.toolbar a:hover{color:var(--metal);border-color:var(--metal)}
+main{max-width:1100px;margin:0 auto;padding:48px 24px 80px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:20px}
+.month-card{position:relative;display:block;background:rgba(184,153,104,0.04);border:1px solid var(--rule);text-decoration:none;color:inherit;padding:0;overflow:hidden;min-height:220px;transition:transform 0.15s ease,border-color 0.15s ease}
+.month-card:hover{border-color:var(--metal);transform:translateY(-2px)}
+.month-card-inner{padding:24px 22px}
+.month-card.has-img{color:var(--paper)}
+.month-card.has-img .month-card-inner{position:relative;z-index:2}
+.month-card.has-img .kicker{color:var(--metal-glow)}
+.month-card.has-img .cover-line{color:rgba(250,246,238,0.95)}
+.month-card.has-img .stats{color:rgba(250,246,238,0.7)}
+.month-card .cover-img{position:absolute;inset:0;background-size:cover;background-position:center;filter:saturate(0.85)}
+.month-card .img-overlay{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0.2) 0%,rgba(0,0,0,0.85) 100%);z-index:1}
+.kicker{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--metal);font-weight:500;margin-bottom:6px}
+.month{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:32px;line-height:1.05;margin-bottom:12px}
+.cover-line{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:16px;line-height:1.4;color:var(--ink);margin:14px 0 6px}
+.cover-line small{display:block;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute);margin-top:4px;font-style:normal}
+.stats{font-size:11px;color:var(--ink-mute);margin-top:14px;padding-top:14px;border-top:1px dotted var(--rule)}
+.stats b{color:var(--metal);font-weight:500}
+</style></head>
+<body>
+<div class="toolbar">
+  <a href="/issue">Current issue</a>
+  <a href="/about.html">About</a>
+  <a href="/find.html">Find</a>
+</div>
+<header>
+  <div class="kicker">The Corridor · Archive</div>
+  <h1>Back <em>issues</em>.</h1>
+  <div class="deck">Every month, every feature.</div>
+</header>
+<main>${monthsHtml}</main>
+</body></html>`);
+});
+
 // /issue — the curated public-facing issue (status='published' only)
 // Mirrors /magazine.html aesthetic but stripped of admin controls + prints clean.
 app.get(['/issue', '/issue/:cat'], async (req, res) => {
@@ -2410,6 +2493,7 @@ article .biz small{display:block;font-family:monospace;font-size:9px;color:var(-
   <a href="/magazine.html">⚙ Admin</a>
   <a href="/issue.pdf${cat ? '?cat=' + encodeURIComponent(cat) : ''}" download>⬇ PDF</a>
   <a href="/issue.epub${cat ? '?cat=' + encodeURIComponent(cat) : ''}" download>📖 EPUB</a>
+  <a href="/issue/archive">🗂 Archive</a>
   <a href="javascript:window.print()">🖨 Print</a>
 </div>
 <header>

← 4c4257c iter 144: cover-image upload — multer endpoint + /covers sta  ·  back to Ventura Corridor  ·  iter 146: auto-cover job — promotes top-tapped feature to co a4769a4 →