[object Object]

← 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

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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 →