← back to Ventura Corridor
iter 101: /sponsor/:id owner-claim form — public-facing per-feature sponsorship inquiry page (Cormorant 'Make this a spread' header, feature-summary card w/ headline+biz+ad-pixel badge, 3-tier picker Spread/Cover/Ask radio cards, name/email/phone/notes form, sleek noir submit button); POST /api/sponsor/inquiry composes Georgia-serif HTML email + sends via George Gmail to steve@designerwallcoverings.com (account=info), subject '🪶 Corridor sponsor inquiry · <biz> · <tier>'; /magazine/:id reader toolbar gets ⭐ Sponsor link in copper accent; ADMIN_PATHS extended for /sponsor/:id and /api/sponsor/*
18a9a29d2ca2d26b4f432eaecc571a47b20e3991 · 2026-05-06 17:30:57 -0700 · SteveStudio2
Files touched
Diff
commit 18a9a29d2ca2d26b4f432eaecc571a47b20e3991
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 17:30:57 2026 -0700
iter 101: /sponsor/:id owner-claim form — public-facing per-feature sponsorship inquiry page (Cormorant 'Make this a spread' header, feature-summary card w/ headline+biz+ad-pixel badge, 3-tier picker Spread/Cover/Ask radio cards, name/email/phone/notes form, sleek noir submit button); POST /api/sponsor/inquiry composes Georgia-serif HTML email + sends via George Gmail to steve@designerwallcoverings.com (account=info), subject '🪶 Corridor sponsor inquiry · <biz> · <tier>'; /magazine/:id reader toolbar gets ⭐ Sponsor link in copper accent; ADMIN_PATHS extended for /sponsor/:id and /api/sponsor/*
---
src/server/index.ts | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 164 insertions(+)
diff --git a/src/server/index.ts b/src/server/index.ts
index bc2fcd2..bea37cc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -80,6 +80,8 @@ const ADMIN_PATHS = [
/^\/scoreboard(\.html)?\/?$/i,
/^\/rate-card(\.html)?\/?$/i,
/^\/sponsor(\.html)?\/?$/i,
+ /^\/sponsor\/\d+\/?$/i,
+ /^\/api\/sponsor(\/.*)?$/i,
/^\/crawl-derby(\.html)?\/?$/i,
/^\/api\/crawl(\/.*)?$/i,
/^\/postcards(\.html)?\/?$/i,
@@ -2102,6 +2104,167 @@ app.post('/api/magazine/review-drafts', express.json(), async (req, res) => {
}
});
+// /sponsor/:id — owner-claim form prefilled with the business + feature. Submits
+// via POST /api/sponsor/inquiry which emails Steve via George Gmail.
+app.get('/sponsor/:id', async (req, res) => {
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id)) return res.status(400).send('bad id');
+ const r = await query(
+ `SELECT mf.id, mf.headline, mf.subhead, mf.category_tag,
+ b.name AS biz_name, b.address AS biz_address, b.city,
+ COALESCE((be.ad_signals->>'paid_ads_count')::int, 0) AS ads
+ FROM magazine_features mf
+ JOIN businesses b ON b.id = mf.business_id
+ LEFT JOIN business_enrichment be ON be.business_id = mf.business_id
+ WHERE mf.id = $1`, [id]
+ );
+ if (r.rowCount === 0) return res.status(404).send('feature not found');
+ const f = r.rows[0];
+ const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.send(`<!doctype html><html><head><meta charset="utf-8">
+<title>Sponsor — ${esc(f.biz_name)} · 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,400;0,500;1,400;1,500&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@200;300&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:48px 24px 28px;border-bottom:4px double var(--ink)}
+header .kicker{font-size:9px;letter-spacing:.4em;text-transform:uppercase;color:var(--metal);font-weight:500}
+header h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:54px;margin:8px 0 0;color:var(--ink);letter-spacing:-0.02em}
+header h1 em{font-style:normal;color:var(--metal)}
+main{max-width:680px;margin:0 auto;padding:36px 24px 80px}
+.feature-summary{padding:18px 22px;background:rgba(184,153,104,0.06);border:1px solid var(--rule);margin-bottom:36px}
+.feature-summary .h{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:24px;margin:0 0 4px;color:var(--ink)}
+.feature-summary .biz{font-size:13px;color:var(--ink-mute);font-family:'JetBrains Mono',monospace}
+.feature-summary .ads{display:inline-block;background:var(--accent);color:var(--paper);padding:3px 10px;font-size:9px;letter-spacing:.3em;text-transform:uppercase;font-weight:500;margin-top:8px}
+form label{display:block;font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);margin-top:18px;margin-bottom:6px;font-weight:500}
+form input, form textarea, form select{width:100%;background:transparent;border:1px solid var(--rule);color:var(--ink);padding:10px 12px;font-family:'Inter',sans-serif;font-size:14px;border-radius:0}
+form input:focus, form textarea:focus, form select:focus{outline:none;border-color:var(--metal)}
+form textarea{min-height:90px;resize:vertical}
+form .tier-pick{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:6px}
+form .tier-pick label{cursor:pointer;padding:14px;border:1px solid var(--rule);text-align:center;display:block;text-transform:none;letter-spacing:0;color:var(--ink);font-size:13px;font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:400;margin:0}
+form .tier-pick label.sel{border-color:var(--metal);background:rgba(184,153,104,0.08);color:var(--accent);font-weight:500}
+form .tier-pick label .price{display:block;font-family:'Inter',sans-serif;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute);margin-top:4px;font-weight:400}
+form .tier-pick input[type=radio]{display:none}
+form .submit{margin-top:32px;background:var(--ink);color:var(--paper);border:none;padding:14px 28px;font-size:11px;letter-spacing:.32em;text-transform:uppercase;font-weight:500;cursor:pointer;font-family:'Inter',sans-serif}
+form .submit:hover{background:var(--accent)}
+form .submit:disabled{opacity:0.4;cursor:not-allowed}
+.success{padding:24px;background:#dfe9d9;border:1px solid #6a9b73;color:#2a4a2a;font-family:'Cormorant Garamond',serif;font-style:italic;font-size:18px;margin-top:24px}
+.note{font-size:12px;color:var(--ink-mute);margin-top:6px;line-height:1.45}
+.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="/magazine/${id}">← Read feature</a>
+ <a href="/rate-card.html">Rates</a>
+</div>
+<header>
+ <div class="kicker">The Corridor · sponsorship inquiry</div>
+ <h1>Make this a <em>spread</em></h1>
+</header>
+<main>
+ <div class="feature-summary">
+ <div class="h">${esc(f.headline || f.biz_name)}</div>
+ <div class="biz">${esc(f.biz_name)} · ${esc(f.biz_address || '')} · ${esc(f.city || '')} · ${esc(f.category_tag || '')}</div>
+ ${f.ads > 0 ? `<div class="ads">$ ${f.ads}× paid-ad pixels detected</div>` : ''}
+ </div>
+ <form id="form">
+ <input type="hidden" name="feature_id" value="${id}">
+ <input type="hidden" name="biz_name" value="${esc(f.biz_name)}">
+
+ <label>Tier</label>
+ <div class="tier-pick">
+ <label data-tier="spread" class="sel"><input type="radio" name="tier" value="spread" checked>Spread<span class="price">$240/issue</span></label>
+ <label data-tier="cover"><input type="radio" name="tier" value="cover">Cover<span class="price">$1,800/issue</span></label>
+ <label data-tier="ask"><input type="radio" name="tier" value="ask">Just curious<span class="price">no commitment</span></label>
+ </div>
+
+ <label>Your name</label>
+ <input type="text" name="contact_name" placeholder="Maria Rivera" required>
+
+ <label>Email</label>
+ <input type="email" name="contact_email" placeholder="maria@${esc((f.biz_name || '').toLowerCase().replace(/[^a-z]/g, ''))}example.com" required>
+
+ <label>Phone (optional)</label>
+ <input type="tel" name="contact_phone" placeholder="(818) 555-0100">
+
+ <label>What do you want to highlight?</label>
+ <textarea name="notes" placeholder="A new chef, a renovation, a community partnership — anything you'd like the editor to know."></textarea>
+ <div class="note">Don't worry about polished copy. Steve writes the editorial; you give the spark.</div>
+
+ <button type="submit" class="submit">Send inquiry</button>
+ </form>
+ <div id="result"></div>
+</main>
+<script>
+document.querySelectorAll('.tier-pick label').forEach(l => l.addEventListener('click', () => {
+ document.querySelectorAll('.tier-pick label').forEach(x => x.classList.remove('sel'));
+ l.classList.add('sel');
+}));
+document.getElementById('form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const btn = e.target.querySelector('button.submit');
+ btn.disabled = true; btn.textContent = 'Sending…';
+ const data = Object.fromEntries(new FormData(e.target));
+ try {
+ const r = await fetch('/api/sponsor/inquiry', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+ if (!r.ok) throw new Error(await r.text());
+ document.getElementById('form').style.display = 'none';
+ document.getElementById('result').innerHTML = '<div class="success">Sent. Steve will reply within one business day to ' + (data.contact_email || 'you') + '.</div>';
+ } catch (err) {
+ btn.disabled = false; btn.textContent = 'Send inquiry';
+ alert('Failed: ' + err.message);
+ }
+});
+</script>
+</body></html>`);
+});
+
+// Sponsor inquiry intake — emails Steve via George Gmail
+app.post('/api/sponsor/inquiry', express.json(), async (req, res) => {
+ try {
+ const b = req.body || {};
+ if (!b.contact_email || !b.contact_name) return res.status(400).json({ error: 'name + email required' });
+ const escEm = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
+ const html = `<div style="font-family:Georgia,serif;max-width:560px;color:#1a1a1a;padding:24px;background:#faf6ee">
+<h2 style="font-family:Georgia,serif;font-style:italic;font-size:28px;color:#6a3a1a;margin:0 0 8px">New sponsor inquiry</h2>
+<div style="font-size:11px;letter-spacing:.3em;text-transform:uppercase;color:#8a6d3b;margin-bottom:20px">The Corridor · ${new Date().toLocaleString('en-US',{dateStyle:'long',timeStyle:'short'})}</div>
+<table style="border-collapse:collapse;font-size:14px">
+<tr><td style="padding:6px 14px 6px 0;color:#888;width:140px">Tier</td><td><b style="text-transform:uppercase;letter-spacing:.18em;color:#6a3a1a">${escEm(b.tier || 'ask')}</b></td></tr>
+<tr><td style="padding:6px 14px 6px 0;color:#888">Business</td><td><b>${escEm(b.biz_name || 'unknown')}</b></td></tr>
+<tr><td style="padding:6px 14px 6px 0;color:#888">Feature ID</td><td><a href="http://127.0.0.1:9780/magazine/${escEm(b.feature_id)}">/magazine/${escEm(b.feature_id)}</a></td></tr>
+<tr><td style="padding:6px 14px 6px 0;color:#888">Contact</td><td>${escEm(b.contact_name)}</td></tr>
+<tr><td style="padding:6px 14px 6px 0;color:#888">Email</td><td><a href="mailto:${escEm(b.contact_email)}">${escEm(b.contact_email)}</a></td></tr>
+${b.contact_phone ? `<tr><td style="padding:6px 14px 6px 0;color:#888">Phone</td><td><a href="tel:${escEm(b.contact_phone)}">${escEm(b.contact_phone)}</a></td></tr>` : ''}
+</table>
+${b.notes ? `<div style="margin-top:24px;padding:14px 18px;background:rgba(184,153,104,0.1);border-left:3px solid #8a6d3b;font-style:italic;font-size:14px;line-height:1.5">${escEm(b.notes)}</div>` : ''}
+<div style="margin-top:24px;font-size:11px;color:#888">Reply directly to ${escEm(b.contact_email)} to start the back-and-forth.</div>
+</div>`;
+ const georgeAuth = 'Basic ' + Buffer.from('admin:DWSecure2024!').toString('base64');
+ const resp = await fetch('http://localhost:9850/api/send', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Authorization: georgeAuth },
+ body: JSON.stringify({
+ to: 'steve@designerwallcoverings.com',
+ subject: `🪶 Corridor sponsor inquiry · ${b.biz_name || 'unknown'} · ${b.tier || 'ask'}`,
+ body: html,
+ isHtml: true,
+ account: 'info'
+ })
+ });
+ const out = await resp.json().catch(() => ({}));
+ res.json({ ok: resp.ok, messageId: out.messageId });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
// 9:16 share-card — clean Instagram-story format, screenshot or screencap-ready
app.get('/share/:id', async (req, res) => {
const id = parseInt(req.params.id, 10);
@@ -2252,6 +2415,7 @@ footer{margin-top:48px;padding:24px 0;text-align:center;font-size:10px;letter-sp
<div class="breadcrumb"><a href="/magazine.html">← The Corridor</a> · ${esc(f.category_tag || 'feature')}</div>
<div>
<a href="/share/${id}" target="_blank">📱 IG share-card</a>
+ <a href="/sponsor/${id}" style="color:var(--accent,#6a3a1a);border-color:var(--accent,#6a3a1a)">⭐ Sponsor</a>
<a href="javascript:window.print()">🖨 Print</a>
<a href="/pitches.html?id=${f.business_id}" target="_blank">Pipeline ↗</a>
</div>
← b3c77b4 iter 97+98+99+100: PDF export + reader-search + IG share-car
·
back to Ventura Corridor
·
iter 102+103: nightly auto-publish + /words page — src/jobs/ 3bb691b →