← 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
M public/magazine.htmlM src/server/index.ts
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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 →