[object Object]

← back to Ventura Corridor

iter 79: ad-signal sponsored markers — /api/magazine joins business_enrichment.ad_signals JSONB and exposes paid_ads_count per feature; /magazine.html cards get a copper-tone '$ N× ads' badge top-right of the photo-frame for businesses with detected paid-ad pixels (Google Ads/Meta/TikTok/etc), tooltip explains 'proven ad buyer · candidate sponsored placement'; new chips bar adds toggle '$ paid-ad runners only' that filters the issue to businesses already buying ads (the natural sponsored-feature targets); 128 corridor businesses have ad-signal data so far

7037e0a632a21b9d58ab4f921707674f2ea423cc · 2026-05-06 16:36:07 -0700 · SteveStudio2

Files touched

Diff

commit 7037e0a632a21b9d58ab4f921707674f2ea423cc
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Wed May 6 16:36:07 2026 -0700

    iter 79: ad-signal sponsored markers — /api/magazine joins business_enrichment.ad_signals JSONB and exposes paid_ads_count per feature; /magazine.html cards get a copper-tone '$ N× ads' badge top-right of the photo-frame for businesses with detected paid-ad pixels (Google Ads/Meta/TikTok/etc), tooltip explains 'proven ad buyer · candidate sponsored placement'; new chips bar adds toggle '$ paid-ad runners only' that filters the issue to businesses already buying ads (the natural sponsored-feature targets); 128 corridor businesses have ad-signal data so far
---
 public/magazine.html | 21 +++++++++++++++++++--
 src/server/index.ts  |  5 ++++-
 2 files changed, 23 insertions(+), 3 deletions(-)

diff --git a/public/magazine.html b/public/magazine.html
index d90d729..1b171bb 100644
--- a/public/magazine.html
+++ b/public/magazine.html
@@ -219,6 +219,11 @@
   <span style="color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;align-self:center;margin-right:8px">vertical →</span>
   <!-- chips injected by JS -->
 </div>
+<div class="chips" style="padding-top:0;border-bottom:1px solid var(--rule)">
+  <span style="color:var(--ink-mute);font-size:9px;letter-spacing:.22em;text-transform:uppercase;align-self:center;margin-right:8px">candidates →</span>
+  <button id="ads-only-btn" onclick="toggleAdsOnly()">$ paid-ad runners only</button>
+  <span style="color:var(--ink-mute);font-size:9px;letter-spacing:.18em;align-self:center;margin-left:auto">businesses w/ detected ad pixels = sponsored-feature buyers</span>
+</div>
 
 <main class="spread" id="spread">
   <div class="loading">Loading the issue…</div>
@@ -228,6 +233,13 @@
 function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }
 let currentStatus = 'all';
 let currentCat    = 'all';
+let adsOnly       = false;
+function toggleAdsOnly() {
+  adsOnly = !adsOnly;
+  const btn = document.getElementById('ads-only-btn');
+  btn.classList.toggle('active', adsOnly);
+  load();
+}
 
 const CATS = [
   { id: 'all',          label: 'All' },
@@ -262,7 +274,8 @@ async function load() {
     if (currentCat !== 'all')    params.set('cat',    currentCat);
     const url = params.toString() ? '/api/magazine?' + params : '/api/magazine';
     const data = await fetch(url).then(r => r.json());
-    const rows = data.rows || [];
+    let rows = data.rows || [];
+    if (adsOnly) rows = rows.filter(r => Number(r.paid_ads_count || 0) > 0);
     const counts = data.by_status || [];
     const total = counts.reduce((a, x) => a + Number(x.n), 0);
     const draftN = (counts.find(x => x.status === 'draft') || {}).n || 0;
@@ -276,11 +289,15 @@ async function load() {
     }
     main.innerHTML = rows.map(r => {
       const cat = (r.category_tag || 'shop').toUpperCase();
-      const photoTxt = `${escHtml(r.name)}\n${escHtml((r.address||'').split(',')[0])}`;
+      const adsCount = Number(r.paid_ads_count || 0);
+      const adsBadge = adsCount > 0
+        ? `<span style="position:absolute;top:12px;right:12px;background:var(--accent);color:var(--paper);padding:4px 10px;font-size:8px;letter-spacing:.3em;text-transform:uppercase;font-family:'Inter',sans-serif;font-weight:500;z-index:2" title="${adsCount} paid-ad pixel${adsCount===1?'':'s'} detected — proven ad buyer, candidate sponsored placement">$ ${adsCount}× ads</span>`
+        : '';
       return `
       <article class="feature s-${r.status}" data-id="${r.id}">
         <div class="photo-frame">
           <span class="cat-tag">${escHtml(cat)}</span>
+          ${adsBadge}
           <div class="placeholder-text">${escHtml(r.name)}<br><span style="font-size:10px;letter-spacing:.18em;text-transform:uppercase">photo TBD</span></div>
         </div>
         <h2><a href="/magazine/${r.id}" style="color:inherit;text-decoration:none">${escHtml(r.headline || '(untitled)')}</a></h2>
diff --git a/src/server/index.ts b/src/server/index.ts
index 4c28f4b..97360a0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1779,9 +1779,12 @@ app.get('/api/magazine', async (req, res) => {
               mf.category_tag, mf.photo_url, mf.status, mf.model, mf.generated_at,
               mf.reviewed_at, mf.published_at, mf.views, mf.notes,
               b.name, b.address, b.city, b.zip,
-              b.raw->>'primary_naics_description' AS naics
+              b.raw->>'primary_naics_description' AS naics,
+              be.ad_signals,
+              COALESCE((be.ad_signals->>'paid_ads_count')::int, 0) AS paid_ads_count
        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.length ? 'WHERE ' + where.join(' AND ') : ''}
        ORDER BY mf.generated_at DESC
        LIMIT $${params.length}`,

← 34f78f5 iter 77+78: dual-model alternating generation + /magazine/:i  ·  back to Ventura Corridor  ·  iter 80: search box + table-of-contents view on /magazine.ht d7cd146 →