Three changes per Daniel (2026-09-15): 1. VERIFIED PUBLISHERS ONLY — lib/sources.js gains VERIFIED_HOSTS (established PH newsrooms + state wire + international) and publisherVerified(url); lib/crawl.js now drops any story whose article URL host is not on the allowlist BEFORE the relevance gate (skippedUnverified counter surfaces in the crawl result + cron log). Tabloids/blogs/radio (bomboradyo, unboxph, bnc.bilyonaryo, dzrh, thechronicle, …) can no longer reach the desk. Quiet-day fallback wires expanded to Inquirer/Rappler/GMA/ABS-CBN/Interaksyon/Manila Bulletin (all verified). 2. IN PICTURES — a new prominent full-width band on the front page: 7 real public-domain photos of Sen. BAM (official Senate/Facebook pages via Wikimedia Commons), each credited to its source with a link out to the Commons file page. lib/photos.js holds the curated, URL-verified set. 3. BOARD AS ITS OWN AREA — the 'Thank You, Senator BAM' community board is now a distinct gold-edged full-width band (front page + its own view), clearly separate from the news desks. The nav 'Thank You Board' button is gold (not a category tab) and jumps to the band. Tests: publisher-gate unit checks (22/22), ui-check.mjs updated to verify the In Pictures band (>=5 credited photos) + front-page board band posting; e2e + board suites still green.
189 lines
8.6 KiB
JavaScript
189 lines
8.6 KiB
JavaScript
// BidaAngMamamayan source registry — the crawler's supply line.
|
|
//
|
|
// This is a TRIBUTE site for Sen. BAM Aquino, so every feed is scoped to him:
|
|
// 1. BAM_QUERIES — keyword news searches (Bing News RSS, the same aggregator
|
|
// forthena learned to love 2026-08-31: it hands us the publisher's own URL
|
|
// inside the apiclick wrapper, which the reader pane needs).
|
|
// 2. PH_NATIVE — general PH news wires, kept only as an ASSOCIATED pool
|
|
// fallback: when a tick finds zero named-BAM stories, a couple of his
|
|
// senate colleagues' / committee's coverage still belongs on the site.
|
|
//
|
|
// Freshness: qft=interval="8" = last month. Bing's 7-day window only ever held
|
|
// 12 unique stories for "Bam Aquino" (measured 2026-09-15) — a senator's
|
|
// mention volume is low, so the crawl window is a month and the relevance
|
|
// gate (lib/crawl.js) does the actual filtering. The 48h crawl maxAge from
|
|
// forthena does NOT apply here: a named-BAM story from last Thursday is still
|
|
// front-page material this weekend.
|
|
//
|
|
// Bing quirks (all verified live on the agent host):
|
|
// * `OR` in queries is nil — single phrases only.
|
|
// * setlang=en-PH&cc=PH biases toward PH coverage.
|
|
// * A quoted exact phrase ("Bam Aquino") returns FEWER results than the
|
|
// unquoted phrase — Bing drops results whose snippet doesn't contain the
|
|
// exact string. Keep queries unquoted; the gate handles relevance.
|
|
|
|
// ── BAM queries ──────────────────────────────────────────────────────────
|
|
// Each phrase was measured through the live Bing news RSS on 2026-09-15.
|
|
// "Bam Aquino" alone is the workhorse (12 items/week, the rest overlap it).
|
|
export const BAM_QUERIES = [
|
|
{ query: "Bam Aquino", name: "BAM Wire", category: "bam" },
|
|
{ query: "Sen. Bam Aquino", name: "Senate Desk", category: "politics" },
|
|
{ query: "Bam Aquino education", name: "Education Desk", category: "education" },
|
|
{ query: "Bam Aquino college students scholarship", name: "Campus Desk", category: "education" },
|
|
{ query: "Bam Aquino power electricity", name: "Energy Desk", category: "energy" },
|
|
{ query: "Bam Aquino gaming esports", name: "Gamer Desk", category: "gaming" },
|
|
{ query: "Bam Aquino bill Senate", name: "Legislative Desk", category: "politics" },
|
|
];
|
|
|
|
// ── Verified publishers only ─────────────────────────────────────────────
|
|
// Daniel's rule (2026-09-15): the site features ESTABLISHED news outlets and
|
|
// state media — no tabloids, blogs, regional radio, or nameless aggregators.
|
|
// The gate is HOST-BASED (not the Bing <News:Source> label, which is
|
|
// unreliable): every story's unwrapped article URL must resolve to one of
|
|
// these suffixes, or it is dropped before the relevance gate even runs.
|
|
//
|
|
// Each entry is a hostname or a hostname-suffix (matched with endsWith ".").
|
|
// PNA = state wire; Inquirer / Rappler / GMA / ABS-CBN / Manila Bulletin /
|
|
// Philstar / BusinessWorld / Manila Times / SunStar / Interaksyon / Daily
|
|
// Tribune / NewsLine / Cebu Daily = established PH newsrooms; the international
|
|
// set covers his global stories (Pope Francis, etc.).
|
|
export const VERIFIED_HOSTS = [
|
|
// state wire
|
|
"pna.gov.ph",
|
|
// Philippine Daily Inquirer group
|
|
"inquirer.net", "newsinfo.inquirer.net", "manilastandard.inquirer.net", "cebudailynews.inquirer.net",
|
|
// Rappler
|
|
"rappler.com",
|
|
// GMA
|
|
"gmanetwork.com", "news.gmanetwork.com", "interaksyon.com",
|
|
// ABS-CBN
|
|
"abs-cbn.com", "news.abs-cbn.com", "tvnews.abs-cbn.com",
|
|
// Manila Bulletin
|
|
"mb.com.ph", "manilabulletin.com",
|
|
// Philstar (ABS-CBN)
|
|
"philstar.com",
|
|
// BusinessWorld
|
|
"bworldonline.com", "businessworld.com.ph",
|
|
// BusinessMirror (Manila Times)
|
|
"businessmirror.com.ph",
|
|
// Manila Times
|
|
"manilatimes.net",
|
|
// SunStar (established regional wire)
|
|
"sunstar.com.ph",
|
|
// Daily Tribune
|
|
"dailytribune.com.ph",
|
|
// NewsLine
|
|
"newsline.ph",
|
|
// international (his global coverage)
|
|
"bbc.com", "bbc.co.uk", "reuters.com", "apnews.com", "aljazeera.com", "cna.com.sg",
|
|
];
|
|
|
|
// A story's publisher is verified when its article URL's host equals a
|
|
// VERIFIED_HOSTS entry or sits under it (subdomains included: e.g.
|
|
// "news.rappler.com" is under "rappler.com"). Bing apiclick wrappers are
|
|
// unwrapped to the publisher URL BEFORE this check, so the host is real.
|
|
export function publisherVerified(url) {
|
|
let host;
|
|
try { host = new URL(String(url || "")).hostname.toLowerCase(); } catch { return false; }
|
|
host = host.replace(/^www\./, "");
|
|
if (!host) return false;
|
|
for (const v of VERIFIED_HOSTS) {
|
|
const vh = v.replace(/^www\./, "");
|
|
if (host === vh || host.endsWith("." + vh)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ── Associated pool (fallback only) ──────────────────────────────────────
|
|
// General PH wires from VERIFIED publishers. The crawler runs these ONLY when
|
|
// the BAM queries produced zero named stories this tick (quiet days). Their
|
|
// items pass the same verified-publisher + relevance gates, so only genuine
|
|
// senate / Aquino-family copy ever prints from here.
|
|
export const PH_NATIVE = [
|
|
{ key: "pna", name: "Philippine News Agency", category: "ph", feeds: ["https://www.pna.gov.ph/feed/"] },
|
|
{ key: "inqnews", name: "Inquirer News", category: "ph", feeds: ["https://newsinfo.inquirer.net/feed"] },
|
|
{ key: "rappler", name: "Rappler", category: "ph", feeds: ["https://www.rappler.com/rss"] },
|
|
{ key: "gma", name: "GMA News", category: "ph", feeds: ["https://www.gmanetwork.com/rss"] },
|
|
{ key: "interaksyon", name: "Interaksyon (GMA)", category: "ph", feeds: ["https://www.interaksyon.com/feed"] },
|
|
{ key: "abs", name: "ABS-CBN News", category: "ph", feeds: ["https://news.abs-cbn.com/rss"] },
|
|
{ key: "mb", name: "Manila Bulletin", category: "ph", feeds: ["https://mb.com.ph/rss"] },
|
|
];
|
|
|
|
// ── Bing News RSS ────────────────────────────────────────────────────────
|
|
const BING = "https://www.bing.com/news/search?q=";
|
|
const BING_PARAMS = "&format=RSS&setlang=en-PH&cc=PH&qft=interval%3d%228%22";
|
|
|
|
export function queryFeed(q) {
|
|
return `${BING}${encodeURIComponent(String(q).slice(0, 120))}${BING_PARAMS}`;
|
|
}
|
|
|
|
// Bing wraps item links in apiclick.aspx?...&url=<publisher url>. Returns the
|
|
// publisher's own URL when the wrapper carries one, else the original.
|
|
export function unwrapFeedLink(link) {
|
|
let l = String(link || "").trim();
|
|
if (!l) return l;
|
|
l = l.replace(/&/g, "&").replace(/&/g, "&");
|
|
if (/bing\.com\/news\/apiclick/i.test(l)) {
|
|
const m = /[?&]url=([^&]+)/.exec(l);
|
|
if (m) {
|
|
try {
|
|
const real = decodeURIComponent(m[1]);
|
|
if (/^https?:\/\//i.test(real)) return real;
|
|
} catch { /* leave the wrapper */ }
|
|
}
|
|
}
|
|
if (/^https?:\/\/(www\.)?bing\.com\//i.test(l)) {
|
|
const m = /[?&](?:url|u)=([^&]+)/.exec(l);
|
|
if (m) { try { return decodeURIComponent(m[1]); } catch {} }
|
|
}
|
|
return l;
|
|
}
|
|
|
|
// Human publisher label: <News:Source> first, then the URL's host, then the
|
|
// "Headline - Publisher" tail.
|
|
const HOST_NAMES = {
|
|
"pna.gov.ph": "PNA",
|
|
"inquirer.net": "Inquirer",
|
|
"newsinfo.inquirer.net": "Inquirer News",
|
|
"gmanetwork.com": "GMA News",
|
|
"abs-cbn.com": "ABS-CBN",
|
|
"mb.com.ph": "Manila Bulletin",
|
|
"rappler.com": "Rappler",
|
|
"bworldonline.com": "BusinessWorld",
|
|
"philstar.com": "Philstar",
|
|
"sunstar.com.ph": "SunStar",
|
|
"cnnphilippines.com": "CNN Philippines",
|
|
"businessmirror.com": "BusinessMirror",
|
|
"manilatimes.net": "Manila Times",
|
|
"dailytribune.com.ph": "Daily Tribune",
|
|
"newsline.ph": "NewsLine",
|
|
"bombardyo.com": "Bombo Radyo",
|
|
};
|
|
|
|
export function publisherFromUrl(url) {
|
|
try {
|
|
const host = new URL(url).hostname.replace(/^www\./, "").toLowerCase();
|
|
const reg = Object.keys(HOST_NAMES).find((h) => host === h || host.endsWith("." + h));
|
|
if (reg) return HOST_NAMES[reg];
|
|
const parts = host.split(".");
|
|
return parts.length > 2 ? parts[parts.length - 3] : parts[0];
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function publisherFrom(title, fallback) {
|
|
const t = String(title || "");
|
|
const i = t.lastIndexOf(" - ");
|
|
if (i > 24) return t.slice(i + 3).trim().slice(0, 40);
|
|
return fallback || "Wire";
|
|
}
|
|
|
|
// Aggregator titles sometimes still carry the " - Publisher" tail.
|
|
export function stripPublisherSuffix(title) {
|
|
const t = String(title || "");
|
|
const i = t.lastIndexOf(" - ");
|
|
if (i > 24) return t.slice(0, i).trim();
|
|
return t.trim();
|
|
}
|