← back to Allnewsdaily
server.js
586 lines
const express = require('express');
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const { GUIDES } = require('./content/guides');
const { rebuild, getWire, meta, loadStaticWire } = require('./lib/aggregate');
// On prod (no Ollama) set WIRE_STATIC=1: serve the pre-built data/wire.json, refreshed by an
// external pusher (scripts/build-wire.js on a machine that HAS Ollama). Never fetches/rewrites here.
const WIRE_STATIC = process.env.WIRE_STATIC === '1';
// On prod (datacenter IP) YouTube serves a consent/bot wall, so the live checker finds 0 live.
// "Static-live" mode DISABLES the local checker and serves data/live-status.json pushed from a
// residential-IP machine (Mac2) via scripts/push-live-status.sh. Enabled by env LIVE_STATIC=1 OR
// the presence of data/live-static.flag (dropped on prod by the pusher) — the flag form needs NO
// prod env change and NO restart (avoids a --update-env that could drop PORT), and self-activates.
const LIVE_STATIC_FLAG = path.join(__dirname, 'data', 'live-static.flag');
function liveStatic() { return process.env.LIVE_STATIC === '1' || fs.existsSync(LIVE_STATIC_FLAG); }
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';
// Live AdSense display-unit slots (pub-5278231299883833). Slot IDs are PUBLIC (they render in the
// page's ad code), so they live here as defaults; env AD_SLOT_TOP/MID/BOTTOM still override.
// ⚠️ VERIFIED-LIVE — DO NOT REMOVE. These are REAL units created in the AdSense portal on
// 2026-09-09 and confirmed three ways against the live account: (1) the three unit names appear in
// the AdSense units list, (2) each slot's /generate-ad-code page loads its matching unit, (3) the
// data-ad-slot in the generated code matches. A prior sweep (commit 345ef9e) removed these on a
// FALSE "fabricated" assumption; re-instated after live re-verification. They are NOT hallucinated.
const AD_SLOT_TOP = process.env.AD_SLOT_TOP || '1009806200'; // "AND Top Leaderboard"
const AD_SLOT_MID = process.env.AD_SLOT_MID || '5567646667'; // "AND Mid Banner"
const AD_SLOT_BOTTOM = process.env.AD_SLOT_BOTTOM || '5129014617'; // "AND Bottom Banner"
const OUTLETS_PATH = path.join(__dirname, 'data', 'outlets.json');
const LIVE_STATUS_PATH = path.join(__dirname, 'data', 'live-status.json');
function loadOutlets() {
try {
return JSON.parse(fs.readFileSync(OUTLETS_PATH, 'utf8'));
} catch (e) {
console.error('[outlets] load failed', e.message);
return [];
}
}
function loadLiveStatus() {
try {
if (!fs.existsSync(LIVE_STATUS_PATH)) return {};
return JSON.parse(fs.readFileSync(LIVE_STATUS_PATH, 'utf8'));
} catch (e) {
return {};
}
}
function mergeOutlets() {
const outlets = loadOutlets();
const status = loadLiveStatus();
return outlets.map(o => {
const s = status[o.id];
return {
...o,
isLive: s ? !!s.isLive : false,
liveVideoId: s && s.videoId ? s.videoId : null,
lastCheck: s ? s.checkedAt : null
};
});
}
app.use(express.json());
app.use('/static', express.static(path.join(__dirname, 'public')));
// ---- Drudge-style front page (server-rendered so crawlers/AdSense see content) ----
function renderFront() {
const wire = getWire();
const cfg = meta();
const fmt = (iso) => {
try { return new Date(iso).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: 'America/New_York' }) + ' ET'; }
catch (_) { return ''; }
};
const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/New_York' });
const storyLink = (it) =>
`<a class="story" href="${esc(it.link)}" target="_blank" rel="noopener nofollow">${esc(cap20(it.topic))}<span class="src"> — ${esc(it.outlet)}</span></a>`;
const leadCard = (it) =>
`<a class="lead" href="${esc(it.link)}" target="_blank" rel="noopener nofollow"><img class="lead-img" src="${esc(it.image)}" loading="lazy" referrerpolicy="no-referrer" alt="" onerror="this.remove()"><span class="lead-topic">${esc(cap20(it.topic))}<span class="src"> — ${esc(it.outlet)}</span></span></a>`;
const columnsHtml = (wire.columns || []).map((c) => {
const items = c.items || [];
const li = items.findIndex((it) => it.image);
const lead = li >= 0 ? leadCard(items[li]) : '';
const rest = items.filter((_, i) => i !== li).map(storyLink).join('\n');
return `
<section class="col">
<h2 class="colhead">${esc(c.title)}</h2>
${lead}
${rest}
</section>`;
}).join('\n');
const splashHtml = wire.splash ? `
<a class="splash" href="${esc(wire.splash.link)}" target="_blank" rel="noopener nofollow">
${wire.splash.image ? `<img class="splash-img" src="${esc(wire.splash.image)}" loading="lazy" referrerpolicy="no-referrer" alt="" onerror="this.remove()">` : ''}
<span class="splash-topic">${esc(cap20(wire.splash.topic))}<span class="src"> — ${esc(wire.splash.outlet)}</span></span>
</a>` : `<div class="splash placeholder">Assembling today's wire…</div>`;
const columnistsHtml = (cfg.columnists || []).map((p) =>
`<a class="railitem" href="${esc(p.url)}" target="_blank" rel="noopener nofollow">${esc(p.name)}<span class="src"> · ${esc(p.outlet)}</span></a>`).join('\n');
const magsHtml = (cfg.magazines || []).map((m) =>
`<a class="railitem" href="${esc(m.url)}" target="_blank" rel="noopener nofollow">${esc(m.name)}</a>`).join('\n');
const updated = wire.updatedAt
? `Updated ${fmt(wire.updatedAt)} · ${wire.sourcesOk}/${wire.sources} sources`
: 'Loading wire…';
return `<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>ALL NEWS DAILY — the world's newsrooms, one page</title>
<meta name="description" content="All News Daily — a continuously-updated front page of the world's news, rewritten into plain one-line topics that link straight to the original reporting. Plus columnists, magazines, and a global outlet directory.">
<link rel="canonical" href="https://allnewsdaily.com/">
<meta property="og:type" content="website"><meta property="og:site_name" content="All News Daily">
<meta property="og:title" content="ALL NEWS DAILY">
<meta property="og:description" content="The world's newsrooms on one page — rewritten one-line topics linking to the source.">
<link rel="icon" href="/static/favicon.svg">
<style>${FRONT_CSS}</style>${ADSENSE}</head>
<body>
<header class="masthead">
<div class="tagline-l">EST. 2026 · GLOBAL WIRE</div>
<h1 class="wordmark"><a href="/">ALL NEWS DAILY</a></h1>
<div class="tagline-r">${esc(today)}</div>
</header>
<div class="updated">${esc(updated)} · <a href="/directory">outlet directory</a> · <a href="/guides">guides</a> · <a href="/videos">videos</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>
<div id="wire-splash">${splashHtml}</div>
${bannerAd('mid')}
<hr class="rule thin">
<div class="grid" id="wire-cols">
${columnsHtml}
</div>
<hr class="rule thin">
<section class="railstrip">
<div class="rcol"><h2 class="colhead">COLUMNISTS</h2>${columnistsHtml}</div>
<div class="rcol"><h2 class="colhead">MAGAZINES</h2>${magsHtml}</div>
<div class="rcol"><h2 class="colhead">DIRECTORY</h2><a class="railitem" href="/directory">All 98 outlets — live TV & broadcast →</a></div>
</section>
${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 {'&':'&','<':'<','>':'>','"':'"'}[c];});}
function cap20(s){var w=String(s==null?'':s).trim().split(/\\s+/);return w.length<=20?w.join(' '):w.slice(0,20).join(' ')+'…';}
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(cap20(it.topic))+'<span class="src"> — '+esc(it.outlet)+'</span></a>';}
function leadCard(it){return '<a class="lead" href="'+esc(it.link)+'" target="_blank" rel="noopener nofollow"><img class="lead-img" src="'+esc(it.image)+'" loading="lazy" referrerpolicy="no-referrer" alt="" onerror="this.remove()"><span class="lead-topic">'+esc(cap20(it.topic))+'<span class="src"> — '+esc(it.outlet)+'</span></span></a>';}
function paint(w){
if(w.splash && elSplash){ var s=w.splash; elSplash.innerHTML='<a class="splash" href="'+esc(s.link)+'" target="_blank" rel="noopener nofollow">'+(s.image?'<img class="splash-img" src="'+esc(s.image)+'" loading="lazy" referrerpolicy="no-referrer" alt="" onerror="this.remove()">':'')+'<span class="splash-topic">'+esc(cap20(s.topic))+'<span class="src"> — '+esc(s.outlet)+'</span></span></a>'; }
if(Array.isArray(w.columns) && elCols){
elCols.innerHTML = w.columns.map(function(c){
var items=c.items||[]; var li=-1; for(var i=0;i<items.length;i++){ if(items[i].image){li=i;break;} }
var lead= li>=0? leadCard(items[li]) : '';
var rest= items.filter(function(_,i){return i!==li;}).map(story).join('');
return '<section class="col"><h2 class="colhead">'+esc(c.title)+'</h2>'+lead+rest+'</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>
<script>
// AdSense fallback: while a slot's <ins> is unfilled (e.g. site in "Getting ready"), show the
// house banner; the instant a real ad fills, show the ad. Polls a few seconds to give AdSense time.
(function(){
function resolveAds(){
var zones=document.querySelectorAll('[data-adzone]');
for(var i=0;i<zones.length;i++){
var z=zones[i], ins=z.querySelector('ins.adsbygoogle'),
real=z.querySelector('.ad-real'), fb=z.querySelector('.ad-fallback');
if(!ins||!fb) continue;
var st=ins.getAttribute('data-ad-status');
if(st==='filled'){ fb.hidden=true; if(real) real.hidden=false; }
else if(st==='unfilled' || ins.offsetHeight<40){ if(real) real.hidden=true; fb.hidden=false; }
}
}
var n=0, iv=setInterval(function(){ resolveAds(); if(++n>=8) clearInterval(iv); }, 1500);
window.addEventListener('load', resolveAds);
})();
</script>
<footer class="foot">
<nav><a href="/directory">Directory</a> · <a href="/guides">Guides</a> · <a href="/videos">Videos</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. © ${new Date().getFullYear()} All News Daily.</p>
</footer>
</body></html>`;
}
app.get('/', (req, res) => {
res.type('html').send(renderFront());
});
// ---- Video Briefings (embedded All News Daily YouTube Shorts) --------------
const VIDEOS_PATH = path.join(__dirname, 'data', 'videos.json');
function loadVideos() {
try {
const arr = JSON.parse(fs.readFileSync(VIDEOS_PATH, 'utf8'));
return Array.isArray(arr) ? arr : [];
} catch (_) {
return [];
}
}
// A valid YouTube videoId is 11 chars of [A-Za-z0-9_-]. Never embed anything else.
const VALID_VIDEO_ID = /^[A-Za-z0-9_-]{11}$/;
function renderVideos() {
const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/New_York' });
const fmtDate = (iso) => {
try { return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'America/New_York' }); }
catch (_) { return ''; }
};
const videos = loadVideos().filter((v) => v && VALID_VIDEO_ID.test(v.videoId || ''));
const cardsHtml = videos.map((v) => {
const id = v.videoId; // regex-validated above — safe to interpolate into the embed URL
const title = esc(v.title || 'All News Daily briefing');
const when = v.publishedAt ? `<div class="vmeta">${esc(fmtDate(v.publishedAt))}</div>` : '';
return `<figure class="vcard">
<div class="vframe">
<iframe src="https://www.youtube.com/embed/${id}" title="${title}"
loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>
<figcaption class="vcap">${title}${when}</figcaption>
</figure>`;
}).join('\n');
const gridHtml = videos.length
? `<div class="vgrid">${cardsHtml}</div>`
: `<div class="vempty">Briefings publishing soon — check back shortly.</div>`;
return `<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Video Briefings — All News Daily</title>
<meta name="description" content="Video Briefings from All News Daily — short daily video rundowns of the world's top headlines, straight from the All News Daily YouTube channel.">
<link rel="canonical" href="https://allnewsdaily.com/videos">
<meta property="og:type" content="website"><meta property="og:site_name" content="All News Daily">
<meta property="og:title" content="Video Briefings — All News Daily">
<meta property="og:description" content="Short daily video rundowns of the world's top headlines from All News Daily.">
<link rel="icon" href="/static/favicon.svg">
<style>${FRONT_CSS}${VIDEOS_CSS}</style>${ADSENSE}</head>
<body>
<header class="masthead">
<div class="tagline-l">EST. 2026 · GLOBAL WIRE</div>
<h1 class="wordmark"><a href="/">ALL NEWS DAILY</a></h1>
<div class="tagline-r">${esc(today)}</div>
</header>
<div class="updated"><a href="/">front page</a> · <a href="/directory">outlet directory</a> · <a href="/guides">guides</a> · <a href="/videos">videos</a></div>
<hr class="rule">
${bannerAd('top')}
<main>
<h2 class="vhead">Video Briefings</h2>
<p class="vlede">Short daily video rundowns of the day's top headlines, from the All News Daily YouTube channel.</p>
<hr class="rule thin">
${gridHtml}
${bannerAd('bottom')}
</main>
<footer class="foot">
<nav><a href="/">Front page</a> · <a href="/directory">Directory</a> · <a href="/guides">Guides</a> · <a href="/videos">Videos</a> · <a href="/about">About</a> · <a href="/contact">Contact</a> · <a href="/privacy">Privacy</a></nav>
<p>Video Briefings are produced by All News Daily and published on our YouTube channel. © ${new Date().getFullYear()} All News Daily.</p>
</footer>
</body></html>`;
}
app.get('/videos', (req, res) => {
res.type('html').send(renderVideos());
});
// Original outlet directory (live TV/broadcast grid) preserved here.
app.get('/directory', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/ads.txt', (_q, res) => res.type('text/plain').send('google.com, pub-5278231299883833, DIRECT, f08c47fec0942fa0\n'));
app.get('/robots.txt', (_q, res) => res.type('text/plain').send('User-agent: *\nAllow: /\nSitemap: https://allnewsdaily.com/sitemap.xml\n'));
app.get('/sitemap.xml', (_q, res) => {
const B = 'https://allnewsdaily.com';
const urls = ['/', '/directory', '/guides', '/videos', '/about', '/contact', '/privacy']
.concat(GUIDES.map((g) => `/guides/${g.slug}`))
.map((u) => ` <url><loc>${B}${u}</loc></url>`).join('\n');
res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`);
});
// ---- Original editorial content (news-literacy guides) --------------------
// Turns the site from a bare outbound-link directory into a real publisher.
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
// Drudge-style: cap a headline to at most 20 words (adds an ellipsis when trimmed).
const cap20 = (s) => { const w = String(s == null ? '' : s).trim().split(/\s+/); return w.length <= 20 ? w.join(' ') : w.slice(0, 20).join(' ') + '…'; };
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;
// House banner — a real self-promo (links to the directory), shown as a fallback whenever the
// AdSense unit is unfilled (e.g. while the site is in AdSense "Getting ready"). It is NOT labeled
// "Advertisement" because it isn't a paid ad; the ad-resolve script (below the zones) swaps it out
// the instant a real ad fills the <ins>.
const house = `<a class="ad-house ad-fallback" href="/directory"${slot ? ' hidden' : ''}>`
+ `<span class="ad-house-brand">ALL NEWS DAILY</span>`
+ `<span class="ad-house-sub">The world's newsrooms on one page — explore the live directory →</span></a>`;
if (slot) {
return `<div class="adzone adzone-${pos}" data-adzone>`
+ `<div class="ad-real"><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></div>`
+ house
+ `<script>(adsbygoogle=window.adsbygoogle||[]).push({});</script></div>`;
}
return `<div class="adzone adzone-${pos} adzone-house">${house}</div>`;
}
const FRONT_CSS = `
:root{--ink:#111;--link:#0000cc;--vis:#551a8b;--red:#c00;--rule:#000;--bg:#f8f7f2;--src:#6a6a6a}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.28 Georgia,'Times New Roman',Times,serif}
a{color:var(--link);text-decoration:none}a:hover{text-decoration:underline}a:visited{color:var(--vis)}
.masthead{display:grid;grid-template-columns:1fr auto 1fr;align-items:end;gap:12px;padding:14px 16px 6px;text-align:center}
.wordmark{margin:0;font-size:clamp(30px,6vw,58px);letter-spacing:1px;font-weight:700;text-transform:uppercase;font-family:'Times New Roman',Times,serif}
.wordmark a{color:var(--ink)}.wordmark a:hover{text-decoration:none}
.tagline-l{text-align:left}.tagline-r{text-align:right}
.tagline-l,.tagline-r{font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#333;padding-bottom:8px}
.updated{text-align:center;font-size:12px;color:#444;padding:2px 16px 8px;text-transform:uppercase;letter-spacing:.4px}
.rule{border:0;border-top:3px double var(--rule);margin:6px 16px}
.rule.thin{border-top:1px solid #000;margin:10px 16px}
main{max-width:1180px;margin:0 auto;padding:0 12px 60px}
.splash{display:block;text-align:center;font-weight:700;text-transform:uppercase;color:var(--red);
font-size:clamp(16px,2.5vw,23px);line-height:1.18;padding:12px 12px 6px;letter-spacing:.3px}
.splash:hover{text-decoration:underline}.splash.placeholder{color:#888;font-style:italic;text-transform:none}
.splash .src{color:#7a2a2a;font-weight:400;font-size:.62em}
.splash-img{display:block;margin:0 auto 8px;width:100%;max-width:min(560px,100%);height:auto;border:1px solid #cbc9c0}
.splash-topic{display:block}
/* column lead photo card (Drudge-style) */
.lead{display:block;text-decoration:none;padding:2px 0 8px;margin-bottom:7px;border-bottom:2px solid #000}
.lead-img{display:block;width:100%;aspect-ratio:16/9;object-fit:cover;margin-bottom:5px;border:1px solid #cbc9c0;background:#e9e7df}
.lead-topic{display:block;color:var(--red);font-weight:700;font-size:13px;line-height:1.2;text-transform:uppercase;letter-spacing:.2px}
.lead:hover .lead-topic{text-decoration:underline}
.lead .src{color:var(--src);font-weight:400;font-style:italic;text-transform:none;font-size:11px}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:0 26px}
.col{padding:6px 0 10px;border-left:1px solid #ddd;padding-left:14px}
.col:first-child{border-left:0;padding-left:0}
.colhead{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#000;border-bottom:2px solid #000;
margin:6px 0 8px;padding-bottom:3px;font-family:Arial,Helvetica,sans-serif}
.story{display:block;padding:4px 0;border-bottom:1px dotted #cfcfcf;font-size:12px;line-height:1.22}
.story .src{color:var(--src);font-style:italic;font-size:11px}
.railstrip{display:grid;grid-template-columns:repeat(3,1fr);gap:0 26px;background:#f0efe8;padding:12px 14px;margin-top:4px}.railstrip .rcol{border-left:1px solid #ddd;padding-left:14px}.railstrip .rcol:first-child{border-left:0;padding-left:0}
.railitem{display:block;padding:3px 0;border-bottom:1px dotted #d5d5cf;font-size:12px}
.railitem .src{color:var(--src);font-style:italic;font-size:11px}
.foot{max-width:1180px;margin:0 auto;padding:20px 16px 40px;border-top:3px double #000;font-size:12px;color:#444;text-align:center}
.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}.railstrip{grid-template-columns:1fr}.railstrip .rcol{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;text-decoration:none;color:inherit;cursor:pointer;transition:background .15s ease,border-color .15s ease}
.ad-house:hover{border-color:#c9b79a;background:repeating-linear-gradient(45deg,#f4f2e9,#f4f2e9 12px,#eceadf 12px,#eceadf 24px)}
.ad-house .ad-house-sub{text-decoration:none}
.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}
`;
// /videos page — reuses FRONT_CSS (masthead/wordmark/ad zones/footer) + this grid of 9:16 embeds.
const VIDEOS_CSS = `
.vhead{text-align:center;font-family:'Times New Roman',Times,serif;text-transform:uppercase;letter-spacing:1px;font-size:clamp(22px,3.4vw,32px);margin:14px 0 4px}
.vlede{text-align:center;color:#444;font-size:14px;margin:0 auto 6px;max-width:640px}
.vgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:22px 20px;padding:14px 0 8px}
.vcard{margin:0}
.vframe{position:relative;aspect-ratio:9/16;background:#000;border:1px solid #cbc9bf;overflow:hidden}
.vframe iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}
.vcap{font-size:13px;line-height:1.32;padding:7px 2px 0;color:#111}
.vmeta{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--src);margin-top:3px}
.vempty{text-align:center;color:#888;font-style:italic;padding:48px 16px;border:1px dashed #cbc9bf;background:#efeee7;margin:14px 0}
`;
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}
.top{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #21262d;padding-bottom:14px;margin-bottom:26px}
.brand{font-weight:700;color:#e6edf3;text-decoration:none;font-size:18px}
h1{font-size:30px;line-height:1.25;margin:8px 0 6px}h2{font-size:20px;margin:30px 0 8px}
.dek{color:#9da7b3;font-size:18px;margin:0 0 6px}.meta{color:#6e7681;font-size:13px;margin-bottom:24px}
.guide-cta{margin-top:30px;padding:16px 18px;background:#161b22;border:1px solid #21262d;border-radius:10px}
.related{margin-top:44px;border-top:1px solid #21262d;padding-top:20px}.related a{display:block;margin:8px 0}
footer{color:#6e7681;font-size:13px;border-top:1px solid #21262d;margin-top:40px;padding-top:16px}`;
const guideFooterNav = `<div style="margin-top:8px"><a href="/" style="color:#6e7681;margin-right:12px">Directory</a><a href="/guides" style="color:#6e7681;margin-right:12px">Guides</a><a href="/about" style="color:#6e7681;margin-right:12px">About</a><a href="/contact" style="color:#6e7681;margin-right:12px">Contact</a><a href="/privacy" style="color:#6e7681">Privacy</a></div>`;
const guidePage = ({ title, desc, canonical, jsonld, inner }) => `<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)} · All News Daily</title>
<meta name="description" content="${esc(desc)}">
<link rel="canonical" href="https://allnewsdaily.com${canonical}">
<meta property="og:type" content="article"><meta property="og:title" content="${esc(title)}">
<meta property="og:description" content="${esc(desc)}"><meta property="og:site_name" content="All News Daily">
${jsonld ? `<script type="application/ld+json">${JSON.stringify(jsonld)}</script>` : ''}
<style>${GUIDE_CSS}</style>${ADSENSE}</head><body><div class="wrap">
<div class="top"><a class="brand" href="/">All News Daily</a><span style="display:flex;gap:16px"><a href="/guides">Guides</a><a href="/about">About</a><a href="/contact">Contact</a><a href="/privacy">Privacy</a></span></div>
${inner}
<footer>${guideFooterNav}<div style="margin-top:8px">© ${new Date().getFullYear()} All News Daily — an independent global newsroom directory & news-literacy resource. Educational content only.</div></footer>
</div></body></html>`;
app.get('/guides', (_req, res) => {
const items = GUIDES.map((g) => `<article style="margin:22px 0"><h2 style="margin:0 0 4px"><a href="/guides/${g.slug}">${esc(g.title)}</a></h2><p class="dek">${esc(g.dek)}</p></article>`).join('');
res.type('html').send(guidePage({
title: 'News-Literacy Guides', desc: 'Practical guides to evaluating news sources, understanding media bias, and spotting misinformation — from All News Daily.',
canonical: '/guides',
inner: `<h1>News-Literacy Guides</h1><p class="dek">How to actually use a global newsroom directory: evaluate sources, read past bias, and verify before you share.</p>${items}`,
}));
});
app.get('/guides/:slug', (req, res, next) => {
const g = GUIDES.find((x) => x.slug === req.params.slug);
if (!g) return next();
const related = GUIDES.filter((x) => x.slug !== g.slug).map((x) => `<a href="/guides/${x.slug}">${esc(x.title)} →</a>`).join('');
const jsonld = { '@context': 'https://schema.org', '@type': 'Article', headline: g.title, description: g.dek, datePublished: g.updated, dateModified: g.updated, author: { '@type': 'Organization', name: 'All News Daily' }, publisher: { '@type': 'Organization', name: 'All News Daily' } };
res.type('html').send(guidePage({
title: g.title, desc: g.dek, canonical: `/guides/${g.slug}`, jsonld,
inner: `<h1>${esc(g.title)}</h1><p class="dek">${esc(g.dek)}</p><div class="meta">Updated ${esc(g.updated)} · All News Daily</div>${g.body}<div class="related"><strong>Related guides</strong>${related}</div>`,
}));
});
// ---- Trust pages (About / Contact / Privacy) — AdSense review requirements ---
app.get('/about', (_req, res) => res.type('html').send(guidePage({
title: 'About All News Daily', desc: 'All News Daily is an independent global newsroom directory and news-literacy resource — who we are, what we do, and why.',
canonical: '/about',
inner: `<h1>About All News Daily</h1>
<p class="dek">An independent index of the world's newsrooms, built to help people read the news better.</p>
<h2>What this is</h2>
<p>All News Daily is a continuously-maintained directory of news outlets from every region and language, paired with original, practical guides on how to read the news critically. We don't republish other outlets' articles or claim their reporting as our own — we point you to the primary sources and teach you how to weigh them.</p>
<h2>Why it exists</h2>
<p>No single outlet gives you the whole picture. The durable facts of any story are the ones that show up across independent newsrooms with different owners and different biases. Our directory makes that triangulation fast, and our <a href="/guides">news-literacy guides</a> give you the method.</p>
<h2>How we work</h2>
<p>Outlets are listed on editorial merit and reach, not payment — inclusion in the directory is free and cannot be bought. Live-status indicators are checked automatically. Our guides are written in-house and dated; we correct errors openly and note the update.</p>
<h2>Editorial standards</h2>
<p>Our news-literacy guides follow a consistent method: explain a concept in plain language, give the reader a practical action they can take, and show how to verify claims across independent sources rather than trusting any single outlet. We don't run sponsored posts disguised as editorial, and we don't accept payment to include or rank an outlet.</p>
<h2>How we're funded</h2>
<p>All News Daily is supported by third-party advertising (see our <a href="/privacy">Privacy Policy</a> for how ad cookies work and how to opt out). Advertising keeps the directory and guides free and never influences which outlets we list or how we rank them.</p>
<h2>Who runs it</h2>
<p>All News Daily is an independently operated editorial project — not owned by, funded by, or affiliated with any of the outlets it indexes, which is what lets it stay neutral about them. Editorial questions, corrections, and outlet submissions are welcome at <a href="/contact">our contact page</a>.</p>` })));
app.get('/contact', (_req, res) => res.type('html').send(guidePage({
title: 'Contact All News Daily', desc: 'Get in touch with All News Daily — corrections, outlet submissions, and editorial questions.',
canonical: '/contact',
inner: `<h1>Contact</h1>
<p class="dek">We read every message — corrections, outlet suggestions, and editorial questions all welcome.</p>
<h2>Email</h2>
<p>The fastest way to reach us is by email: <a href="mailto:info@allnewsdaily.com">info@allnewsdaily.com</a>. We aim to reply within a few business days.</p>
<h2>Suggest or correct an outlet</h2>
<p>Spotted a newsroom we're missing, a broken link, or an outlet that has changed hands? Email us the outlet name and URL and we'll review it for the <a href="/">directory</a>.</p>
<h2>Corrections</h2>
<p>If something in one of our <a href="/guides">guides</a> is wrong or out of date, tell us — we publish corrections openly and date the update.</p>` })));
app.get('/privacy', (_req, res) => res.type('html').send(guidePage({
title: 'Privacy Policy', desc: 'How All News Daily handles data, cookies, and third-party advertising (including Google AdSense).',
canonical: '/privacy',
inner: `<h1>Privacy Policy</h1>
<p class="dek">Last updated August 5, 2026.</p>
<p>All News Daily respects your privacy. This page explains what data is and isn't collected when you use this site.</p>
<h2>What we collect</h2>
<p>We do not require accounts and do not ask you for personal information to browse the directory or read our guides. Standard, non-identifying server logs (such as page requests and approximate region) may be recorded to keep the site running and secure.</p>
<h2>Cookies and advertising</h2>
<p>We use third-party advertising, including <strong>Google AdSense</strong>, to support the site. Third-party vendors, including Google, use cookies to serve ads based on your prior visits to this and other websites. Google's use of advertising cookies enables it and its partners to serve ads to you based on your visits to our site and/or other sites on the internet.</p>
<p>You may opt out of personalized advertising by visiting <a href="https://www.google.com/settings/ads" rel="nofollow noopener" target="_blank">Google Ads Settings</a>. You can also opt out of a third-party vendor's use of cookies for personalized advertising by visiting <a href="https://www.aboutads.info/choices/" rel="nofollow noopener" target="_blank">aboutads.info</a>.</p>
<h2>Third-party links</h2>
<p>Our directory links out to independent news outlets. We are not responsible for the content or privacy practices of those external sites; their policies govern your use of them.</p>
<h2>Changes</h2>
<p>We may update this policy; the "last updated" date above reflects the current version. Questions? <a href="/contact">Contact us</a>.</p>` })));
app.get('/api/outlets', (req, res) => {
res.json({ outlets: mergeOutlets(), updatedAt: new Date().toISOString() });
});
app.get('/api/wire', (req, res) => {
const w = getWire();
res.json({ updatedAt: w.updatedAt, sources: w.sources, sourcesOk: w.sourcesOk, splash: w.splash, columns: w.columns });
});
app.get('/api/health', (req, res) => {
const outlets = loadOutlets();
const status = loadLiveStatus();
const liveCount = Object.values(status).filter(s => s && s.isLive).length;
res.json({
ok: true,
outlets: outlets.length,
liveOutlets: liveCount,
lastStatusFile: fs.existsSync(LIVE_STATUS_PATH)
? fs.statSync(LIVE_STATUS_PATH).mtime
: null
});
});
app.get('/api/regions', (req, res) => {
const outlets = loadOutlets();
const regions = [...new Set(outlets.map(o => o.region))].sort();
const countries = [...new Set(outlets.map(o => o.country))].sort();
const langs = [...new Set(outlets.map(o => o.lang))].sort();
const categories = [...new Set(outlets.map(o => o.category))].sort();
res.json({ regions, countries, langs, categories });
});
function runChecker() {
const proc = spawn('node', [path.join(__dirname, 'scripts', 'check-live.js')], {
stdio: ['ignore', 'pipe', 'pipe']
});
proc.stdout.on('data', d => process.stdout.write('[checker] ' + d));
proc.stderr.on('data', d => process.stderr.write('[checker] ' + d));
proc.on('exit', code => console.log('[checker] exit ' + code));
}
const POLL_INTERVAL_MS = parseInt(process.env.POLL_INTERVAL_MS || '90000', 10);
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}`);
console.log(`outlets loaded: ${loadOutlets().length}`);
if (liveStatic()) {
console.log('[live] static mode at boot — serving pushed live-status.json (local checker disabled)');
} else {
runChecker();
}
// Re-check each tick so the flag can enable static mode WITHOUT a restart; skip checking when static.
setInterval(() => { if (!liveStatic()) runChecker(); }, POLL_INTERVAL_MS);
if (WIRE_STATIC) {
// Prod: serve the pre-built snapshot; re-read the file as the pusher refreshes it.
const ok = loadStaticWire();
console.log(`[wire] static mode — loaded ${ok ? (getWire().columns || []).reduce((n, c) => n + c.items.length, 0) + ' stories' : 'NO snapshot yet'}`);
setInterval(() => { loadStaticWire(); }, 60000);
} else {
// Dev / builder host (has Ollama): fetch feeds + rewrite headlines, refresh on interval.
rebuild().then((w) => console.log(`[wire] first build: ${w.sourcesOk}/${w.sources} sources, ${(w.columns || []).reduce((n, c) => n + c.items.length, 0)} stories`))
.catch((e) => console.error('[wire] first build error', e.message));
setInterval(() => { rebuild().catch(() => {}); }, WIRE_REFRESH_MS);
}
});