← back to Ventura Corridor
iter 111+112: /api/magazine/:id/embed.html iframe-friendly mini-card + ad-signal vertical heatmap on /scoreboard — embed renders 480px-max card w/ category-gradient hero + Cormorant 22px italic headline + subhead + biz/address footer + 'read more →' brand bar; X-Frame-Options:ALLOWALL so it can be iframed from Steve's other sites/blogs/IG; /api/magazine/ad-heatmap aggregates magazine_features × business_enrichment.ad_signals into per-vertical sponsor counts (google_ads, meta_pixel, tiktok_pixel, linkedin_insight, pinterest_tag) + sponsor_rate_pct; /scoreboard.html grows '$ Ad-signal heatmap by vertical' table below the 5 leaderboards (sponsor_rate>=30% glows metal-glow, individual platform counts highlighted at thresholds)
97d5a13a5ed3847aa2c3e7d77d0d5494dff02830 · 2026-05-06 17:51:12 -0700 · SteveStudio2
Files touched
M public/scoreboard.htmlM src/server/index.ts
Diff
commit 97d5a13a5ed3847aa2c3e7d77d0d5494dff02830
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 17:51:12 2026 -0700
iter 111+112: /api/magazine/:id/embed.html iframe-friendly mini-card + ad-signal vertical heatmap on /scoreboard — embed renders 480px-max card w/ category-gradient hero + Cormorant 22px italic headline + subhead + biz/address footer + 'read more →' brand bar; X-Frame-Options:ALLOWALL so it can be iframed from Steve's other sites/blogs/IG; /api/magazine/ad-heatmap aggregates magazine_features × business_enrichment.ad_signals into per-vertical sponsor counts (google_ads, meta_pixel, tiktok_pixel, linkedin_insight, pinterest_tag) + sponsor_rate_pct; /scoreboard.html grows '$ Ad-signal heatmap by vertical' table below the 5 leaderboards (sponsor_rate>=30% glows metal-glow, individual platform counts highlighted at thresholds)
---
public/scoreboard.html | 34 ++++++++++++++++++++
src/server/index.ts | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 121 insertions(+)
diff --git a/public/scoreboard.html b/public/scoreboard.html
index 27c3e75..2592b64 100644
--- a/public/scoreboard.html
+++ b/public/scoreboard.html
@@ -47,6 +47,26 @@ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:2
</nav>
<main id="boards"><div class="empty">Loading…</div></main>
+<section style="max-width:1100px;margin:0 auto 60px;padding:0 24px">
+ <div class="board" style="background:var(--noir-rise);border:1px solid var(--rule);padding:20px 22px">
+ <h2 style="font-family:var(--serif);font-style:italic;font-weight:500;font-size:22px;color:var(--metal);margin:0 0 4px">$ Ad-signal heatmap by vertical</h2>
+ <div style="font-size:9px;letter-spacing:.26em;color:var(--ink-mute);text-transform:uppercase;margin-bottom:14px">which categories already buy ads · monetization targets</div>
+ <table style="width:100%;border-collapse:collapse;font-size:12px">
+ <thead><tr style="border-bottom:1px solid var(--rule)">
+ <th style="text-align:left;padding:6px 0;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Vertical</th>
+ <th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Features</th>
+ <th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Sponsors</th>
+ <th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">% sponsor</th>
+ <th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Google</th>
+ <th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">Meta</th>
+ <th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">TikTok</th>
+ <th style="text-align:right;padding:6px 8px;color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;font-weight:400">LinkedIn</th>
+ </tr></thead>
+ <tbody id="heatmap-rows"></tbody>
+ </table>
+ </div>
+</section>
+
<script>
function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
async function load() {
@@ -81,8 +101,22 @@ async function load() {
</section>
`).join('');
}
+async function loadHeatmap() {
+ const d = await fetch('/api/magazine/ad-heatmap').then(r => r.json()).catch(() => ({rows:[]}));
+ const cell = (n, hi) => `<td style="text-align:right;padding:6px 8px;font-family:var(--mono);${Number(n) >= hi ? 'color:var(--metal-glow);font-weight:500' : 'color:var(--ink-mute)'}">${n || '·'}</td>`;
+ document.getElementById('heatmap-rows').innerHTML = (d.rows || []).map(r => `
+ <tr style="border-bottom:1px dotted var(--rule)">
+ <td style="padding:8px 0;font-family:var(--serif);font-size:14px;color:var(--metal);text-transform:capitalize">${escHtml((r.vertical || '').replace(/-/g,' '))}</td>
+ <td style="text-align:right;padding:8px;font-family:var(--mono);color:var(--ink)">${r.features}</td>
+ <td style="text-align:right;padding:8px;font-family:var(--mono);color:var(--metal-glow);font-weight:500">${r.sponsor_candidates}</td>
+ <td style="text-align:right;padding:8px;font-family:var(--mono);color:${Number(r.sponsor_rate_pct) >= 30 ? 'var(--metal-glow)' : 'var(--ink-mute)'}">${r.sponsor_rate_pct ?? '·'}${r.sponsor_rate_pct ? '%' : ''}</td>
+ ${cell(r.google_ads, 5)}${cell(r.meta_pixel, 5)}${cell(r.tiktok_pixel, 3)}${cell(r.linkedin_insight, 3)}
+ </tr>`).join('') || '<tr><td colspan="8" style="padding:20px;text-align:center;color:var(--ink-mute)">No data yet.</td></tr>';
+}
load();
+loadHeatmap();
setInterval(load, 60_000);
+setInterval(loadHeatmap, 60_000);
</script>
</body>
</html>
diff --git a/src/server/index.ts b/src/server/index.ts
index a19b023..608e765 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1849,6 +1849,32 @@ app.get('/api/magazine.csv', async (_req, res) => {
}
});
+app.get('/api/magazine/ad-heatmap', async (_req, res) => {
+ try {
+ const r = await query(`
+ SELECT
+ mf.category_tag AS vertical,
+ count(*) AS features,
+ count(*) FILTER (WHERE COALESCE((be.ad_signals->>'paid_ads_count')::int, 0) > 0) AS sponsor_candidates,
+ count(*) FILTER (WHERE (be.ad_signals->>'google_ads')::boolean) AS google_ads,
+ count(*) FILTER (WHERE (be.ad_signals->>'meta_pixel')::boolean) AS meta_pixel,
+ count(*) FILTER (WHERE (be.ad_signals->>'tiktok_pixel')::boolean) AS tiktok_pixel,
+ count(*) FILTER (WHERE (be.ad_signals->>'linkedin_insight')::boolean) AS linkedin_insight,
+ count(*) FILTER (WHERE (be.ad_signals->>'pinterest_tag')::boolean) AS pinterest_tag,
+ ROUND(100.0 * count(*) FILTER (WHERE COALESCE((be.ad_signals->>'paid_ads_count')::int, 0) > 0)
+ / NULLIF(count(*), 0), 1) AS sponsor_rate_pct
+ FROM magazine_features mf
+ LEFT JOIN business_enrichment be ON be.business_id = mf.business_id
+ WHERE mf.category_tag IS NOT NULL
+ GROUP BY mf.category_tag
+ ORDER BY sponsor_candidates DESC NULLS LAST, features DESC
+ `);
+ res.json({ count: r.rowCount, rows: r.rows });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
app.get('/api/magazine/scoreboard', async (_req, res) => {
try {
const [topViewed, hotVerticals, latest, longest, sponsors] = await Promise.all([
@@ -2491,6 +2517,67 @@ ${b.notes ? `<div style="margin-top:24px;padding:14px 18px;background:rgba(184,1
}
});
+// /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);
+ 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, mf.editorial,
+ b.name AS biz_name, b.address AS biz_address, b.city
+ FROM magazine_features mf JOIN businesses b ON b.id = mf.business_id
+ WHERE mf.id = $1`,
+ [id]
+ );
+ if (r.rowCount === 0) return res.status(404).send('not found');
+ const f = r.rows[0];
+ const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));
+ const cat = f.category_tag || 'shop';
+ const grad: Record<string,string> = {
+ restaurant: '#8a3a1a,#c47b5a',
+ professional: '#2d3a4a,#5a6c7d',
+ medical: '#2a5a4a,#5a8a7a',
+ beauty: '#6a3a5a,#a76a8a',
+ salon: '#5a2a4a,#8a5a7a',
+ fitness: '#2a3a1a,#5a7a3a',
+ shop: '#4a2a1a,#8a5a3a',
+ 'real-estate':'#1a3a4a,#3a6a8a',
+ hospitality: '#4a3a1a,#8a6a3a',
+ automotive: '#2a2a2a,#5a5a5a',
+ };
+ const [c1, c2] = (grad[cat] || grad.shop).split(',');
+ const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.setHeader('X-Frame-Options', 'ALLOWALL');
+ res.send(`<!doctype html><html><head><meta charset="utf-8">
+<title>${esc(f.headline)} — embed</title>
+<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&display=swap');
+*{margin:0;padding:0;box-sizing:border-box}
+html,body{margin:0;background:transparent;font-family:'Inter',sans-serif;font-weight:300;line-height:1.5;color:#1a1815}
+.card{display:block;background:#faf6ee;border:1px solid #d8cdb8;text-decoration:none;color:#1a1815;max-width:480px;overflow:hidden}
+.card .photo{aspect-ratio:16/9;background:linear-gradient(135deg,${c1} 0%,${c2} 100%);position:relative;display:flex;align-items:flex-end;justify-content:flex-start;padding:14px}
+.card .photo .cat{background:rgba(0,0,0,0.4);color:white;padding:3px 10px;font-size:9px;letter-spacing:.3em;text-transform:uppercase;font-weight:600}
+.card .body{padding:16px 18px}
+.card h2{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:22px;line-height:1.1;margin:0 0 6px;color:#1a1815}
+.card .subhead{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;color:#6e6356;margin-bottom:10px;line-height:1.4}
+.card .biz{font-size:11px;color:#888;letter-spacing:.04em}
+.card .biz strong{color:#1a1815;font-weight:500}
+.card .footer{padding:8px 18px;border-top:1px solid #d8cdb8;background:rgba(184,153,104,0.04);font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:#8a6d3b;font-weight:500}
+.card .footer em{font-style:italic;color:#6a3a1a}
+.card:hover{border-color:#8a6d3b}
+</style></head><body>
+<a class="card" href="${baseUrl}/magazine/${id}" target="_blank" rel="noopener">
+ <div class="photo"><span class="cat">${esc(cat)}</span></div>
+ <div class="body">
+ <h2>${esc(f.headline || f.biz_name)}</h2>
+ <div class="subhead">${esc(f.subhead || '')}</div>
+ <div class="biz"><strong>${esc(f.biz_name)}</strong>${f.biz_address ? ' · ' + esc(f.biz_address) : ''}${f.city ? ' · ' + esc(f.city) : ''}</div>
+ </div>
+ <div class="footer">The <em>Corridor</em> · read more →</div>
+</a>
+</body></html>`);
+});
+
// 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);
← 715edc1 iter 109+110: building-mates on /magazine/:id reader + score
·
back to Ventura Corridor
·
iter 113+114: pitch_event_log audit panel on /magazine/:id + 0bc6fd6 →