[object Object]

← back to Allnewsdaily

Front page: 2-min live-refresh loop + countdown counter + big banner ads

0337f69f413adf03441c2aa2c8967df9f287fb61 · 2026-09-09 12:45:56 -0700 · Steve

- Client polls /api/wire every 120s and re-renders the splash + wire columns
  in place (no full reload, so AdSense units don't re-request every cycle).
  Server-rendered content stays the initial paint (SEO/AdSense-safe).
- Live "counter at top": LIVE WIRE ticker bar with a MM:SS countdown to the
  next refresh, a cycle counter, and a live "updated · N/M sources" readout.
- Three big banner ad zones (top leaderboard, mid, bottom): render a real
  AdSense responsive display unit when AD_SLOT_TOP/MID/BOTTOM is set, else a
  labeled house placeholder so the zones are visibly present.
- Server wire rebuild cadence now follows REFRESH_MS (default 120s) to match.

Verified on a static-wire test instance: countdown ticks 1s cadence, all 3
banner zones render (776x132 / 752x152 / 752x132), 3 cols / 60 stories paint,
/api/wire returns 60 stories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PV1DXsyryWT3rLttpPXd4u

Files touched

Diff

commit 0337f69f413adf03441c2aa2c8967df9f287fb61
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 9 12:45:56 2026 -0700

    Front page: 2-min live-refresh loop + countdown counter + big banner ads
    
    - Client polls /api/wire every 120s and re-renders the splash + wire columns
      in place (no full reload, so AdSense units don't re-request every cycle).
      Server-rendered content stays the initial paint (SEO/AdSense-safe).
    - Live "counter at top": LIVE WIRE ticker bar with a MM:SS countdown to the
      next refresh, a cycle counter, and a live "updated · N/M sources" readout.
    - Three big banner ad zones (top leaderboard, mid, bottom): render a real
      AdSense responsive display unit when AD_SLOT_TOP/MID/BOTTOM is set, else a
      labeled house placeholder so the zones are visibly present.
    - Server wire rebuild cadence now follows REFRESH_MS (default 120s) to match.
    
    Verified on a static-wire test instance: countdown ticks 1s cadence, all 3
    banner zones render (776x132 / 752x152 / 752x132), 3 cols / 60 stories paint,
    /api/wire returns 60 stories.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01PV1DXsyryWT3rLttpPXd4u
---
 server.js | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 91 insertions(+), 3 deletions(-)

diff --git a/server.js b/server.js
index b3efbb7..9ba2869 100644
--- a/server.js
+++ b/server.js
@@ -11,6 +11,15 @@ const WIRE_STATIC = process.env.WIRE_STATIC === '1';
 const app = express();
 const PORT = process.env.PORT || 9788;
 
+// Client countdown + server wire cadence (Steve: poll the wire on an infinite loop, refresh every 2 min).
+const REFRESH_MS = parseInt(process.env.REFRESH_MS || '120000', 10); // 2 minutes
+// Optional real AdSense display-unit slot IDs per banner position. When a slot is unset a labeled
+// house placeholder renders instead, so the big banner zones are visible without a live slot wired.
+const AD_CLIENT = process.env.AD_CLIENT || 'ca-pub-5278231299883833';
+const AD_SLOT_TOP = process.env.AD_SLOT_TOP || '';
+const AD_SLOT_MID = process.env.AD_SLOT_MID || '';
+const AD_SLOT_BOTTOM = process.env.AD_SLOT_BOTTOM || '';
+
 const OUTLETS_PATH = path.join(__dirname, 'data', 'outlets.json');
 const LIVE_STATUS_PATH = path.join(__dirname, 'data', 'live-status.json');
 
@@ -99,13 +108,23 @@ function renderFront() {
   <div class="tagline-r">${esc(today)}</div>
 </header>
 <div class="updated">${esc(updated)} · <a href="/directory">outlet directory</a> · <a href="/guides">guides</a></div>
+<div class="ticker" id="ticker" aria-live="polite">
+  <span class="tk-live"><span class="tk-blink"></span> LIVE WIRE</span>
+  <span class="tk-seg">Next refresh in <b id="tk-count">2:00</b></span>
+  <span class="tk-seg">Cycle <b id="tk-cycle">#1</b></span>
+  <span class="tk-seg tk-upd" id="tk-upd">${esc(updated)}</span>
+</div>
 <hr class="rule">
+${bannerAd('top')}
 
 <main>
-  ${splashHtml}
+  <div id="wire-splash">${splashHtml}</div>
+  ${bannerAd('mid')}
   <hr class="rule thin">
   <div class="grid">
-    ${columnsHtml}
+    <div id="wire-cols" style="display:contents">
+      ${columnsHtml}
+    </div>
     <aside class="col rail">
       <h2 class="colhead">COLUMNISTS</h2>
       ${columnistsHtml}
@@ -115,8 +134,47 @@ function renderFront() {
       <a class="railitem" href="/directory">All 98 outlets — live TV &amp; broadcast →</a>
     </aside>
   </div>
+  ${bannerAd('bottom')}
 </main>
 
+<script>
+(function(){
+  var REFRESH_MS = ${REFRESH_MS};
+  var cycle = 1, remain = REFRESH_MS;
+  var elCount = document.getElementById('tk-count');
+  var elCycle = document.getElementById('tk-cycle');
+  var elUpd   = document.getElementById('tk-upd');
+  var elSplash= document.getElementById('wire-splash');
+  var elCols  = document.getElementById('wire-cols');
+  function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
+  function fmtTime(ms){var s=Math.max(0,Math.round(ms/1000));var m=Math.floor(s/60);var r=s%60;return m+':'+(r<10?'0':'')+r;}
+  function fmtET(iso){try{return new Date(iso).toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit',timeZone:'America/New_York'})+' ET';}catch(e){return '';}}
+  function story(it){return '<a class="story" href="'+esc(it.link)+'" target="_blank" rel="noopener nofollow">'+esc(it.topic)+'<span class="src"> — '+esc(it.outlet)+'</span></a>';}
+  function paint(w){
+    if(w.splash && elSplash){ elSplash.innerHTML='<a class="splash" href="'+esc(w.splash.link)+'" target="_blank" rel="noopener nofollow">'+esc(w.splash.topic)+'<span class="src"> — '+esc(w.splash.outlet)+'</span></a>'; }
+    if(Array.isArray(w.columns) && elCols){
+      elCols.innerHTML = w.columns.map(function(c){
+        return '<section class="col"><h2 class="colhead">'+esc(c.title)+'</h2>'+(c.items||[]).map(story).join('')+'</section>';
+      }).join('');
+    }
+    if(w.updatedAt && elUpd){ elUpd.textContent = 'Updated '+fmtET(w.updatedAt)+' · '+(w.sourcesOk||0)+'/'+(w.sources||0)+' sources'; }
+  }
+  function refresh(){
+    fetch('/api/wire',{cache:'no-store'})
+      .then(function(r){return r.json();})
+      .then(function(w){ paint(w); cycle++; if(elCycle) elCycle.textContent='#'+cycle; })
+      .catch(function(){})
+      .then(function(){ remain = REFRESH_MS; });
+  }
+  setInterval(function(){
+    remain -= 1000;
+    if(elCount) elCount.textContent = fmtTime(remain);
+    if(remain <= 0){ remain = REFRESH_MS; refresh(); }
+  }, 1000);
+  if(elCount) elCount.textContent = fmtTime(remain);
+})();
+</script>
+
 <footer class="foot">
   <nav><a href="/directory">Directory</a> · <a href="/guides">Guides</a> · <a href="/about">About</a> · <a href="/contact">Contact</a> · <a href="/privacy">Privacy</a></nav>
   <p>Topic lines are original one-sentence summaries written by All News Daily; every link goes to the source outlet's own reporting. All News Daily does not republish articles. &copy; ${new Date().getFullYear()} All News Daily.</p>
@@ -149,6 +207,21 @@ app.get('/sitemap.xml', (_q, res) => {
 // Turns the site from a bare outbound-link directory into a real publisher.
 const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
 const ADSENSE = '<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-5278231299883833" crossorigin="anonymous"></script>';
+
+// Big banner ad zone. Renders a live AdSense responsive display unit when the position's slot ID
+// is configured (env AD_SLOT_TOP/MID/BOTTOM); otherwise a clearly-labeled house placeholder so the
+// zone is visibly present without a wired slot. Wiring real slots needs an AdSense-portal step (gated).
+function bannerAd(pos) {
+  const slot = pos === 'top' ? AD_SLOT_TOP : pos === 'mid' ? AD_SLOT_MID : AD_SLOT_BOTTOM;
+  if (slot) {
+    return `<div class="adzone adzone-${pos}"><div class="ad-label">Advertisement</div>`
+      + `<ins class="adsbygoogle" style="display:block" data-ad-client="${esc(AD_CLIENT)}" data-ad-slot="${esc(slot)}" data-ad-format="auto" data-full-width-responsive="true"></ins>`
+      + `<script>(adsbygoogle=window.adsbygoogle||[]).push({});</script></div>`;
+  }
+  return `<div class="adzone adzone-${pos} adzone-house"><div class="ad-label">Advertisement</div>`
+    + `<div class="ad-house"><span class="ad-house-brand">ALL NEWS DAILY</span>`
+    + `<span class="ad-house-sub">Premium banner placement — 970×90 · 728×90 · responsive</span></div></div>`;
+}
 const FRONT_CSS = `
 :root{--ink:#111;--link:#0000cc;--vis:#551a8b;--red:#c00;--rule:#000;--bg:#f8f7f2;--src:#6a6a6a}
 *{box-sizing:border-box}
@@ -181,6 +254,21 @@ main{max-width:1180px;margin:0 auto;padding:0 12px 60px}
 .foot nav{margin-bottom:8px}.foot nav a{color:var(--link)}
 @media(max-width:960px){.grid{grid-template-columns:repeat(2,1fr)}.col:nth-child(3){border-left:0;padding-left:0}}
 @media(max-width:560px){.grid{grid-template-columns:1fr}.col{border-left:0;padding-left:0}.masthead{grid-template-columns:1fr}.tagline-l,.tagline-r{text-align:center}}
+/* Live counter / ticker bar */
+.ticker{display:flex;flex-wrap:wrap;align-items:center;gap:8px 18px;justify-content:center;max-width:1180px;margin:0 auto;padding:6px 16px 4px;font-family:Arial,Helvetica,sans-serif;font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:#333}
+.tk-live{display:inline-flex;align-items:center;gap:7px;font-weight:800;color:var(--red)}
+.tk-blink{width:9px;height:9px;border-radius:50%;background:var(--red);box-shadow:0 0 0 3px rgba(204,0,0,.22);animation:tkpulse 1.3s ease-in-out infinite}
+@keyframes tkpulse{0%,100%{opacity:1}50%{opacity:.3}}
+.tk-seg b{color:#000;font-weight:800;font-variant-numeric:tabular-nums}
+.tk-upd{color:#666;letter-spacing:.4px}
+/* Big banner ad zones */
+.adzone{max-width:1180px;margin:14px auto;padding:9px 12px 12px;border:1px dashed #cbc9bf;background:#efeee7}
+.adzone-top{margin-top:8px}
+.ad-label{font:11px/1 Arial,Helvetica,sans-serif;text-transform:uppercase;letter-spacing:1.6px;color:#9a978c;text-align:center;margin-bottom:8px}
+.ad-house{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;min-height:90px;background:repeating-linear-gradient(45deg,#f7f6f0,#f7f6f0 12px,#f1efe6 12px,#f1efe6 24px);border:1px solid #dddacd}
+.adzone-mid .ad-house{min-height:110px}
+.ad-house-brand{font:800 22px/1 'Times New Roman',Times,serif;letter-spacing:1.5px;color:#111;text-transform:uppercase}
+.ad-house-sub{font:12px/1 Arial,Helvetica,sans-serif;color:#8a8779}
 `;
 const GUIDE_CSS = `body{margin:0;background:#0d1117;color:#e6edf3;font:17px/1.7 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
 a{color:#58a6ff}.wrap{max-width:760px;margin:0 auto;padding:28px 22px 80px}
@@ -316,7 +404,7 @@ function runChecker() {
 
 const POLL_INTERVAL_MS = parseInt(process.env.POLL_INTERVAL_MS || '90000', 10);
 
-const WIRE_REFRESH_MS = parseInt(process.env.WIRE_REFRESH_MS || '600000', 10); // 10 min
+const WIRE_REFRESH_MS = parseInt(process.env.WIRE_REFRESH_MS || String(REFRESH_MS), 10); // default 2 min (matches client)
 
 app.listen(PORT, () => {
   console.log(`allnewsdaily listening on http://0.0.0.0:${PORT}`);

← 996668c feed balance: cap 3 items per outlet per column so no single  ·  back to Allnewsdaily  ·  Wire live AdSense display units into the 3 banner zones ef95156 →