← back to Ventura Corridor
iter 125+126: /sponsor.html public landing + sponsor-inquiry persistence + /today.html inquiries card — /sponsor.html lists all 200 most-recent published features as sponsorship slots, w/ category-filter chips + 'paid-ad runners only' toggle, sponsor-candidate (paid_ads_count > 0) cards highlighted w/ accent border + ad badge; each card has 'Read draft' + '⭐ Sponsor →' CTAs; CREATE TABLE sponsor_inquiries (id, feature_id, biz_name, contact, tier, notes, received_at, status, email_message_id); POST /api/sponsor/inquiry now persists rows alongside emailing Steve via George; GET /api/sponsor/inquiries lists last 50; /today.html grows '⭐ Sponsor inquiries · last 7' card (accent-tinted bg) auto-refreshing 60s, showing tier · biz · contact · email · ts · view→ link to /magazine/:id; auto-hides when no inquiries
26b38e18a569f90912356d1f99155af64119f55c · 2026-05-06 18:31:15 -0700 · SteveStudio2
Files touched
A public/sponsor.htmlM public/today.htmlM src/server/index.ts
Diff
commit 26b38e18a569f90912356d1f99155af64119f55c
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 18:31:15 2026 -0700
iter 125+126: /sponsor.html public landing + sponsor-inquiry persistence + /today.html inquiries card — /sponsor.html lists all 200 most-recent published features as sponsorship slots, w/ category-filter chips + 'paid-ad runners only' toggle, sponsor-candidate (paid_ads_count > 0) cards highlighted w/ accent border + ad badge; each card has 'Read draft' + '⭐ Sponsor →' CTAs; CREATE TABLE sponsor_inquiries (id, feature_id, biz_name, contact, tier, notes, received_at, status, email_message_id); POST /api/sponsor/inquiry now persists rows alongside emailing Steve via George; GET /api/sponsor/inquiries lists last 50; /today.html grows '⭐ Sponsor inquiries · last 7' card (accent-tinted bg) auto-refreshing 60s, showing tier · biz · contact · email · ts · view→ link to /magazine/:id; auto-hides when no inquiries
---
public/sponsor.html | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++
public/today.html | 30 ++++++++++++
src/server/index.ts | 18 ++++++++
3 files changed, 179 insertions(+)
diff --git a/public/sponsor.html b/public/sponsor.html
new file mode 100644
index 0000000..c4cf6ec
--- /dev/null
+++ b/public/sponsor.html
@@ -0,0 +1,131 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Sponsor a feature · 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;0,500;0,600;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}
+html,body{margin:0;background:var(--paper);color:var(--ink);font-family:'Inter',sans-serif;font-weight:300;line-height:1.55}
+header{text-align:center;padding:60px 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-weight:300;font-style:italic;font-size:18px;color:var(--ink-mute);margin-top:14px;max-width:560px;margin-left:auto;margin-right:auto;line-height:1.5}
+
+.summary{padding:30px 24px;border-bottom:1px solid var(--rule);background:rgba(184,153,104,0.04);text-align:center}
+.summary span{display:inline-block;margin:0 24px;font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)}
+.summary span b{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:32px;color:var(--accent);font-weight:500;display:block;margin-bottom:4px;letter-spacing:-0.02em}
+
+.controls{padding:18px 24px;display:flex;gap:8px;flex-wrap:wrap;justify-content:center;border-bottom:1px solid var(--rule)}
+.controls button{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);background:transparent;border:1px solid var(--rule);padding:6px 14px;cursor:pointer;font-family:'Inter',sans-serif}
+.controls button.active{color:var(--accent);border-color:var(--accent)}
+.controls button:hover{border-color:var(--metal)}
+
+main{max-width:1100px;margin:0 auto;padding:32px 24px 80px;display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:24px}
+.slot{background:rgba(184,153,104,0.04);border:1px solid var(--rule);padding:20px 22px;display:flex;flex-direction:column;gap:8px}
+.slot.featured{border-left:3px solid var(--accent);background:rgba(184,153,104,0.08)}
+.slot .cat{font-size:8px;letter-spacing:.32em;text-transform:uppercase;color:var(--metal);font-weight:600;display:flex;justify-content:space-between;align-items:baseline}
+.slot .cat .ads{color:var(--accent);font-weight:600}
+.slot h3{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:24px;line-height:1.1;margin:4px 0 0;color:var(--ink)}
+.slot h3 a{color:inherit;text-decoration:none}
+.slot h3 a:hover{color:var(--metal)}
+.slot .biz{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;color:var(--ink-mute)}
+.slot .deck{font-family:'Cormorant Garamond',serif;font-weight:300;font-style:italic;font-size:14px;line-height:1.45;color:var(--ink);margin:6px 0 8px}
+.slot .actions{margin-top:auto;padding-top:14px;border-top:1px dotted var(--rule);display:flex;gap:8px;flex-wrap:wrap;font-size:9px;letter-spacing:.22em;text-transform:uppercase}
+.slot .actions a{color:var(--metal);text-decoration:none;border:1px solid var(--rule);padding:6px 10px}
+.slot .actions a.cta{color:var(--paper);background:var(--accent);border-color:var(--accent);font-weight:500}
+.slot .actions a:hover{border-color:var(--metal)}
+
+.toolbar{position:fixed;top:14px;right:14px;display:flex;gap:6px;z-index:100}
+.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)}
+</style>
+</head>
+<body>
+<div class="toolbar">
+ <a href="/issue">Read the issue</a>
+ <a href="/rate-card.html">Rate card</a>
+ <a href="javascript:window.print()">🖨</a>
+</div>
+<header>
+ <div class="kicker">The Corridor · sponsorship</div>
+ <h1>Sponsor a <em>feature</em></h1>
+ <div class="deck">Every business on Ventura Boulevard already has a feature drafted. Pick yours, send a photo and a sentence — we typeset and ship.</div>
+</header>
+
+<div class="summary">
+ <span><b id="s-feat">—</b>features in this issue</span>
+ <span><b id="s-cand">—</b>sponsor candidates</span>
+ <span><b id="s-rate">$240</b>spread / issue</span>
+ <span><b>$1,800</b>cover / issue</span>
+</div>
+
+<div class="controls">
+ <button class="active" data-cat="all">All verticals</button>
+ <button data-cat="restaurant">🍴 Restaurants</button>
+ <button data-cat="professional">⚖️ Professional</button>
+ <button data-cat="medical">🩺 Medical</button>
+ <button data-cat="beauty">✨ Beauty</button>
+ <button data-cat="salon">💇 Salon</button>
+ <button data-cat="real-estate">🏠 Real estate</button>
+ <button data-cat="hospitality">🏨 Hospitality</button>
+ <button data-cat="fitness">💪 Fitness</button>
+ <button data-cat="shop">🛍 Shops</button>
+ <button data-cat="ads-only" style="border-color:var(--accent);color:var(--accent)">$ paid-ad runners only</button>
+</div>
+
+<main id="slots"></main>
+
+<script>
+let cat = 'all', adsOnly = false, all = [];
+function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
+async function load() {
+ const sb = await fetch('/api/magazine/scoreboard').then(r => r.json());
+ const stats = await fetch('/api/magazine/stats').then(r => r.json());
+ document.getElementById('s-feat').textContent = (stats.total || 0).toLocaleString();
+ document.getElementById('s-cand').textContent = (sb.sponsor_candidates || []).length;
+ const data = await fetch('/api/magazine?status=published&limit=200').then(r => r.json());
+ all = (data.rows || []).filter(r => r.editorial && r.editorial.length > 200);
+ render();
+}
+function render() {
+ let rows = all;
+ if (cat === 'ads-only') rows = rows.filter(r => Number(r.paid_ads_count || 0) > 0);
+ else if (cat !== 'all') rows = rows.filter(r => r.category_tag === cat);
+ rows.sort((a, b) => Number(b.paid_ads_count || 0) - Number(a.paid_ads_count || 0));
+ const main = document.getElementById('slots');
+ if (!rows.length) {
+ main.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:60px;color:var(--ink-mute);font-style:italic;font-family:Cormorant Garamond,serif;font-size:22px">No matches in this filter.</div>';
+ return;
+ }
+ main.innerHTML = rows.slice(0, 60).map(r => {
+ const ads = Number(r.paid_ads_count || 0);
+ return `
+ <div class="slot ${ads > 0 ? 'featured' : ''}">
+ <div class="cat">
+ <span>${escHtml(r.category_tag || 'feature')}</span>
+ ${ads > 0 ? `<span class="ads">$ ${ads}× ads</span>` : ''}
+ </div>
+ <h3><a href="/magazine/${r.id}">${escHtml(r.headline || r.name)}</a></h3>
+ <div class="biz">${escHtml(r.name)} · ${escHtml(r.city || '')}</div>
+ <div class="deck">${escHtml((r.subhead || (r.editorial || '').slice(0, 140) || '').slice(0, 140))}…</div>
+ <div class="actions">
+ <a href="/magazine/${r.id}">Read draft</a>
+ <a class="cta" href="/sponsor/${r.id}">⭐ Sponsor →</a>
+ </div>
+ </div>`;
+ }).join('');
+}
+document.querySelectorAll('.controls button').forEach(b => b.addEventListener('click', () => {
+ document.querySelectorAll('.controls button').forEach(x => x.classList.remove('active'));
+ b.classList.add('active');
+ cat = b.dataset.cat;
+ render();
+}));
+load();
+</script>
+</body>
+</html>
diff --git a/public/today.html b/public/today.html
index 8e916b4..2c777f0 100644
--- a/public/today.html
+++ b/public/today.html
@@ -154,6 +154,14 @@
<div id="fotd-body" style="margin-top:14px"></div>
</section>
+<section id="inq-section" style="display:none;padding:20px 32px;border-bottom:1px solid var(--rule);background:linear-gradient(135deg,rgba(106,58,26,0.08),transparent 60%)">
+ <div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:10px">
+ <h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:22px;color:var(--accent,#a8763a);margin:0">⭐ Sponsor inquiries · last 7</h3>
+ <span id="inq-stats" style="font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute)">—</span>
+ </div>
+ <div id="inq-list" style="margin-top:12px;display:flex;flex-direction:column;gap:6px"></div>
+</section>
+
<section id="fotw-section" style="display:none;padding:18px 32px;border-bottom:1px solid var(--rule);background:radial-gradient(ellipse at left, rgba(106,58,26,0.06), transparent 70%)">
<div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:10px">
<h3 style="font-family:var(--serif);font-style:italic;font-weight:400;font-size:18px;color:var(--accent,#a8763a);margin:0">📰 Feature of the week</h3>
@@ -653,6 +661,28 @@ async function loadFOTW() {
} catch {}
}
loadFOTW();
+
+async function loadInquiries() {
+ try {
+ const d = await fetch('/api/sponsor/inquiries').then(r => r.json());
+ const rows = (d.rows || []).slice(0, 7);
+ if (!rows.length) return;
+ document.getElementById('inq-section').style.display = 'block';
+ document.getElementById('inq-stats').innerHTML = `<b style="color:var(--accent,#a8763a);font-family:var(--mono)">${d.count}</b> total · ${rows.filter(r => r.status === 'new').length} new`;
+ document.getElementById('inq-list').innerHTML = rows.map(r => {
+ const ts = new Date(r.received_at).toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
+ const tierColor = ({ cover: 'var(--accent,#a8763a)', spread: 'var(--metal-glow)', ask: 'var(--ink-mute)' })[r.tier] || 'var(--ink-mute)';
+ return `<a href="/magazine/${r.feature_id}" target="_blank" style="display:grid;grid-template-columns:auto 1fr auto auto;gap:14px;align-items:center;padding:7px 10px;background:var(--noir);border:1px solid var(--rule);text-decoration:none;color:var(--ink);font-size:12px">
+ <span style="color:${tierColor};font-family:var(--mono);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:500">${escHtml(r.tier || 'ask')}</span>
+ <span><b style="font-family:var(--serif);font-size:14px;color:var(--metal-glow)">${escHtml(r.biz_name || '?')}</b> <span style="color:var(--ink-mute);font-family:var(--mono);font-size:10px;margin-left:6px">${escHtml(r.contact_name || '')} · ${escHtml(r.contact_email || '')}</span></span>
+ <span style="color:var(--ink-mute);font-family:var(--mono);font-size:9px">${ts}</span>
+ <span style="color:var(--metal);font-size:10px;letter-spacing:.18em;text-transform:uppercase">view →</span>
+ </a>`;
+ }).join('');
+ } catch {}
+}
+loadInquiries();
+setInterval(loadInquiries, 60_000);
</script>
</body>
</html>
diff --git a/src/server/index.ts b/src/server/index.ts
index 8ae9fc5..a9cf237 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -86,6 +86,7 @@ const ADMIN_PATHS = [
/^\/scoreboard(\.html)?\/?$/i,
/^\/rate-card(\.html)?\/?$/i,
/^\/sponsor(\.html)?\/?$/i,
+ /^\/api\/sponsor\/inquiries\/?$/i,
/^\/sponsor\/\d+\/?$/i,
/^\/api\/sponsor(\/.*)?$/i,
/^\/crawl-derby(\.html)?\/?$/i,
@@ -2825,12 +2826,29 @@ ${b.notes ? `<div style="margin-top:24px;padding:14px 18px;background:rgba(184,1
})
});
const out = await resp.json().catch(() => ({}));
+ await query(
+ `INSERT INTO sponsor_inquiries (feature_id, biz_name, contact_name, contact_email, contact_phone, tier, notes, email_message_id)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
+ [b.feature_id, b.biz_name, b.contact_name, b.contact_email, b.contact_phone, b.tier, b.notes, out.messageId || null]
+ );
res.json({ ok: resp.ok, messageId: out.messageId });
} catch (e: any) {
res.status(500).json({ error: e.message });
}
});
+app.get('/api/sponsor/inquiries', async (_req, res) => {
+ try {
+ const r = await query(
+ `SELECT id, feature_id, biz_name, contact_name, contact_email, tier, notes, received_at, status
+ FROM sponsor_inquiries ORDER BY received_at DESC LIMIT 50`
+ );
+ res.json({ count: r.rowCount, rows: r.rows });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
// /api/magazine/:id/embed.html — iframe-friendly mini-card for blog/IG sidebars/Steve's other sites
app.get('/api/magazine/:id/embed.html', async (req, res) => {
const id = parseInt(req.params.id, 10);
← ead33e9 iter 123+124: AIFF→M4A audio conversion + scoreboard top-tap
·
back to Ventura Corridor
·
iter 127+128: /magazine/:id/print.html print-only view + spo 5af0acf →