// 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 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=. 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: 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(); }